From 8790a37d99a1901a604b5bf0ee81be6344e2c6cd Mon Sep 17 00:00:00 2001 From: Thomas Brugman Date: Tue, 21 Jul 2026 14:58:05 +0200 Subject: [PATCH 01/18] feat(opencode): record per-step token throughput metrics Capture prompt-processing and text-generation tokens/sec on every StepFinishPart. The metrics helper prefers provider-reported rates from llama.cpp / vLLM timings and falls back to wall-clock computation. A new kilocode tui usage route renders PP/TG inline. chore(sdk): regenerate types for StepFinishPart.metrics feat(tui): render PP/TG in sidebar usage panel feat(vscode): per-message and aggregated token throughput display Surface throughput on each AssistantMessage badge (behind the showTokenThroughput toggle) and as a compact PP/TG row in the expanded TaskHeader. Adds session helpers, i18n entries in 20 locales, and StepFinishPart.metrics to extension/webview messages. Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com> --- .changeset/token-throughput-v2.md | 6 + packages/core/src/v1/session.ts | 7 + packages/kilo-vscode/package.json | 5 + packages/kilo-vscode/src/KiloProvider.ts | 11 ++ .../tests/unit/session-utils.test.ts | 73 +++++++++++ .../src/components/chat/AssistantMessage.tsx | 120 +++++++++++++++++- .../src/components/chat/TaskHeader.tsx | 96 +++++++++++++- .../webview-ui/src/context/session-utils.ts | 41 ++++++ .../kilo-vscode/webview-ui/src/i18n/ar.ts | 10 ++ .../kilo-vscode/webview-ui/src/i18n/br.ts | 10 ++ .../kilo-vscode/webview-ui/src/i18n/bs.ts | 10 ++ .../kilo-vscode/webview-ui/src/i18n/da.ts | 10 ++ .../kilo-vscode/webview-ui/src/i18n/de.ts | 10 ++ .../kilo-vscode/webview-ui/src/i18n/en.ts | 9 ++ .../kilo-vscode/webview-ui/src/i18n/es.ts | 10 ++ .../kilo-vscode/webview-ui/src/i18n/fr.ts | 10 ++ .../kilo-vscode/webview-ui/src/i18n/it.ts | 10 ++ .../kilo-vscode/webview-ui/src/i18n/ja.ts | 10 ++ .../kilo-vscode/webview-ui/src/i18n/ko.ts | 10 ++ .../kilo-vscode/webview-ui/src/i18n/nl.ts | 10 ++ .../kilo-vscode/webview-ui/src/i18n/no.ts | 10 ++ .../kilo-vscode/webview-ui/src/i18n/pl.ts | 10 ++ .../kilo-vscode/webview-ui/src/i18n/ru.ts | 10 ++ .../kilo-vscode/webview-ui/src/i18n/th.ts | 10 ++ .../kilo-vscode/webview-ui/src/i18n/tr.ts | 10 ++ .../kilo-vscode/webview-ui/src/i18n/uk.ts | 10 ++ .../kilo-vscode/webview-ui/src/i18n/zh.ts | 10 ++ .../kilo-vscode/webview-ui/src/i18n/zht.ts | 10 ++ .../src/types/messages/extension-messages.ts | 6 + .../webview-ui/src/types/messages/parts.ts | 11 ++ .../src/types/messages/webview-messages.ts | 5 + .../cli/cmd/tui/routes/session/usage.tsx | 74 +++++++++++ .../src/kilocode/plugins/model-usage.ts | 50 +++++++- .../src/kilocode/plugins/sidebar-usage.tsx | 37 ++++++ .../opencode/src/kilocode/session/metrics.ts | 73 +++++++++++ .../src/kilocode/session/processor.ts | 6 + packages/opencode/src/session/processor.ts | 11 +- .../test/kilocode/session-metrics.test.ts | 88 +++++++++++++ .../opencode/test/kilocode/tui/usage.test.ts | 81 ++++++++++++ packages/sdk/js/src/v2/gen/types.gen.ts | 5 + packages/sdk/openapi.json | 17 +++ 41 files changed, 1018 insertions(+), 4 deletions(-) create mode 100644 .changeset/token-throughput-v2.md create mode 100644 packages/opencode/src/kilocode/cli/cmd/tui/routes/session/usage.tsx create mode 100644 packages/opencode/src/kilocode/session/metrics.ts create mode 100644 packages/opencode/test/kilocode/session-metrics.test.ts create mode 100644 packages/opencode/test/kilocode/tui/usage.test.ts diff --git a/.changeset/token-throughput-v2.md b/.changeset/token-throughput-v2.md new file mode 100644 index 00000000000..fe3c4f130cf --- /dev/null +++ b/.changeset/token-throughput-v2.md @@ -0,0 +1,6 @@ +--- +"@kilocode/kilo": minor +"@kilocode/sdk": minor +--- + +Show tokens-per-second throughput (PP prompt-processing and TG text-generation) on each assistant message and in the usage sidebar, when the provider supplies timing data or the step took a measurable amount of time. diff --git a/packages/core/src/v1/session.ts b/packages/core/src/v1/session.ts index 68af4065ac3..2389e040155 100644 --- a/packages/core/src/v1/session.ts +++ b/packages/core/src/v1/session.ts @@ -243,6 +243,13 @@ export const StepFinishPart = Schema.Struct({ modelID: ModelV2.ID, }), ), + metrics: Schema.optional( + Schema.Struct({ + prompt: Schema.optional(Schema.Finite), + generation: Schema.optional(Schema.Finite), + source: Schema.Literals(["provider", "computed"]), + }), + ), // kilocode_change end cost: Schema.Finite, tokens: Schema.Struct({ diff --git a/packages/kilo-vscode/package.json b/packages/kilo-vscode/package.json index c13efe5d9af..8a4ab065398 100644 --- a/packages/kilo-vscode/package.json +++ b/packages/kilo-vscode/package.json @@ -1129,6 +1129,11 @@ "default": true, "description": "Show the task timeline graph in the chat header" }, + "kilo-code.new.showTokenThroughput": { + "type": "boolean", + "default": false, + "description": "Show tokens-per-second (prompt-processing / text-generation) badges on assistant messages and the task header" + }, "kilo-code.new.chat.shiftTabCyclesVariant": { "type": "boolean", "default": true, diff --git a/packages/kilo-vscode/src/KiloProvider.ts b/packages/kilo-vscode/src/KiloProvider.ts index ecb9bde7c4f..cbef0b907ac 100644 --- a/packages/kilo-vscode/src/KiloProvider.ts +++ b/packages/kilo-vscode/src/KiloProvider.ts @@ -1351,6 +1351,9 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper case "requestTimelineSetting": this.sendTimelineSetting() break + case "requestThroughputSetting": + this.sendThroughputSetting() + break case "requestNotifications": this.fetchAndSendNotifications().catch((e) => console.error("[Kilo New] fetchAndSendNotifications failed:", e), @@ -2712,6 +2715,14 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper }) } + private sendThroughputSetting(): void { + const config = vscode.workspace.getConfiguration("kilo-code.new") + this.postMessage({ + type: "throughputSettingLoaded", + visible: config.get("showTokenThroughput", false), + }) + } + private sendWorkStyle(): void { this.postMessage(getWorkStylePayload()) } diff --git a/packages/kilo-vscode/tests/unit/session-utils.test.ts b/packages/kilo-vscode/tests/unit/session-utils.test.ts index 5be45947682..0dff94f2a00 100644 --- a/packages/kilo-vscode/tests/unit/session-utils.test.ts +++ b/packages/kilo-vscode/tests/unit/session-utils.test.ts @@ -4,6 +4,8 @@ import { calcTotalCost, calcContextUsage, calcTokenUsage, + aggregateMetrics, + messageMetrics, buildFamilyCosts, buildFamilyParents, buildFamilyParentsFromTools, @@ -717,3 +719,74 @@ describe("collapseCostBreakdown", () => { expect(shown).toBe(1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11) }) }) + +// ── Throughput aggregation ───────────────────────────────────────────── + +function stepFinish(id: string, metrics?: Part["metrics"]): Part { + return { + type: "step-finish", + id, + ...(metrics ? { metrics } : {}), + } +} + +describe("aggregateMetrics", () => { + it("returns undefined when no step-finish parts carry metrics", () => { + const parts: Part[] = [ + { type: "step-start", id: "s1" }, + stepFinish("f1"), + { type: "text", id: "t1", text: "hello" }, + ] + expect(aggregateMetrics(parts)).toBeUndefined() + }) + + it("returns the metrics block from the last step-finish with metrics", () => { + const parts: Part[] = [ + stepFinish("f1", { prompt: 100, generation: 20, source: "computed" }), + { type: "text", id: "t1", text: "mid" }, + stepFinish("f2", { prompt: 412, generation: 38, source: "provider" }), + ] + expect(aggregateMetrics(parts)).toEqual({ prompt: 412, generation: 38, source: "provider" }) + }) + + it("prefers provider-reported metrics over computed ones regardless of order", () => { + const parts: Part[] = [ + stepFinish("f1", { prompt: 500, generation: 50, source: "provider" }), + stepFinish("f2", { generation: 30, source: "computed" }), + ] + const result = aggregateMetrics(parts) + expect(result?.source).toBe("provider") + expect(result?.prompt).toBe(500) + }) + + it("falls back to computed metrics when no provider metrics exist", () => { + const parts: Part[] = [ + stepFinish("f1", { generation: 12, source: "computed" }), + stepFinish("f2", { generation: 18, source: "computed" }), + ] + expect(aggregateMetrics(parts)).toEqual({ generation: 18, source: "computed" }) + }) + + it("ignores non-step-finish parts even when they look like metrics", () => { + const parts: Part[] = [ + { type: "text", id: "t1", text: "noise" }, + stepFinish("f1", { prompt: 200, generation: 22, source: "provider" }), + ] + expect(aggregateMetrics(parts)).toEqual({ prompt: 200, generation: 22, source: "provider" }) + }) +}) + +describe("messageMetrics", () => { + it("matches aggregateMetrics behavior on the same input", () => { + const parts: Part[] = [ + stepFinish("f1", { generation: 8, source: "computed" }), + stepFinish("f2", { prompt: 99, generation: 33, source: "provider" }), + ] + expect(messageMetrics(parts)).toEqual(aggregateMetrics(parts)) + }) + + it("returns undefined when no throughput metrics are present", () => { + expect(messageMetrics([])).toBeUndefined() + expect(messageMetrics([{ type: "text", id: "t1", text: "no metrics here" }])).toBeUndefined() + }) +}) diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/AssistantMessage.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/AssistantMessage.tsx index a67b9231861..b843c4d84e9 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/AssistantMessage.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/AssistantMessage.tsx @@ -7,7 +7,7 @@ * Active questions render inline via QuestionDock; permissions are in the bottom dock. */ -import { Component, For, Show, createMemo } from "solid-js" +import { Component, For, Show, createMemo, createSignal, onMount, onCleanup } from "solid-js" import { Dynamic } from "solid-js/web" import { Part, PART_MAPPING, ToolRegistry } from "@kilocode/kilo-ui/message-part" import type { MessageFeedbackControls } from "@kilocode/kilo-ui/message-part" @@ -23,8 +23,11 @@ import { useDisplay } from "../../context/display" import { useConfig } from "../../context/config" import { useLanguage } from "../../context/language" import { useServer } from "../../context/server" +import { useVSCode } from "../../context/vscode" import { planDisplayPath } from "../../utils/plan-path" import { isRenderable, UPSTREAM_SUPPRESSED_TOOLS } from "../../utils/transcript-parts" +import { messageMetrics } from "../../context/session-utils" +import { MemoryMarkerMeta } from "@kilocode/kilo-memory/marker-meta" import { color as timelineColor } from "../../utils/timeline/colors" import type { Part as TimelinePart } from "../../types/messages" import type { TimelineHighlight } from "../../utils/timeline/highlight" @@ -164,14 +167,82 @@ function BashToolCard(props: { part: ToolPart; defaultOpen: boolean; forceOpen?: ) } +/** Compact tokens-per-second badge shown beneath the last assistant message. + * Only renders when the user has opted in via the + * `kilo-code.new.showTokenThroughput` setting and the message has a + * step-finish part that carries throughput metrics. */ +function ThroughputBadge(props: { metrics: NonNullable> }) { + const language = useLanguage() + const formatter = createMemo( + () => + new Intl.NumberFormat(language.locale(), { + maximumFractionDigits: 1, + }), + ) + const ppText = createMemo(() => { + const prompt = props.metrics.prompt + if (prompt === undefined) return "–" + return formatter().format(prompt) + }) + const tgText = createMemo(() => { + const gen = props.metrics.generation + if (gen === undefined) return "–" + return formatter().format(gen) + }) + const label = createMemo(() => + props.metrics.source === "provider" + ? language.t("chat.throughput.badge.provider", { pp: ppText(), tg: tgText() }) + : language.t("chat.throughput.badge.computed", { tg: tgText() }), + ) + const tooltip = createMemo(() => { + const prompt = props.metrics.prompt + const gen = props.metrics.generation + if (props.metrics.source === "provider" && prompt !== undefined && gen !== undefined) { + return language.t("chat.throughput.badge.tooltip.provider", { + pp: formatter().format(prompt), + tg: formatter().format(gen), + }) + } + if (gen !== undefined) { + return language.t("chat.throughput.badge.tooltip.computed", { tg: formatter().format(gen) }) + } + return language.t("chat.throughput.badge.tooltip.missing") + }) + return ( + + + {label()} + + + ) +} + export const AssistantMessage: Component = (props) => { const data = useData() const session = useSession() const display = useDisplay() +const mem = useMemory() + const language = useLanguage() + const vscode = useVSCode() const { config } = useConfig() const open = createMemo(() => config().terminal_command_display !== "collapsed") const edit = createMemo(() => config().code_edit_display === "expanded") + // Per-message throughput toggle. Mirrors the showTaskTimeline pattern: + // request once on mount and react to the extension's reply. + const [throughputVisible, setThroughputVisible] = createSignal(false) + onMount(() => vscode.postMessage({ type: "requestThroughputSetting" })) + const handler = (e: MessageEvent) => { + const data = e.data as { type?: string; visible?: boolean } + if (data?.type === "throughputSettingLoaded") setThroughputVisible(Boolean(data.visible)) + } + window.addEventListener("message", handler) + onCleanup(() => window.removeEventListener("message", handler)) + const parts = createMemo(() => { const stored = props.parts ?? data.store.part?.[props.message.id] if (!stored) return [] @@ -182,6 +253,39 @@ export const AssistantMessage: Component = (props) => { return !!matchToolRequest(part, "question", session.questions()) }) }) +const meta = createMemo(() => + MemoryMarkerMeta.fromParts((props.parts ?? data.store.part?.[props.message.id] ?? []) as MemoryMarkerMeta.Part[]), + ) + const recall = createMemo(() => { + const item = meta() + if (item?.type === "recall") return item + }) + // Pull the latest step-finish metrics for this message so the per-message + // badge can render its PP/TG rates when the toggle is on. + const throughput = createMemo(() => + messageMetrics( + (props.parts ?? (data.store.part?.[props.message.id] as TimelinePart[] | undefined) ?? []) as TimelinePart[], + ), + ) + const fmt = (value: number) => value.toLocaleString(language.locale()) + const count = (item: MemoryItem) => fmt(item.count) + const items = (item: MemoryItem) => item.items ?? [] + const verbose = createMemo(() => Boolean(mem.status()?.state.verbose)) + const tip = (item: MemoryItem) => { + const values = MemoryMarkerMeta.snippets(item, verbose()) + return ( +
+ 0} + fallback={ +
{`${language.t("chat.memory.badge.recalled")} · ${language.t("chat.memory.badge.items", { count: count(item) })}`}
+ } + > + {(value) =>
{value}
}
+
+
+ ) + } return ( <> @@ -297,6 +401,20 @@ export const AssistantMessage: Component = (props) => { ) }} + + {(item) => ( + +
+ {language.t("chat.memory.badge.recalled")} ·{" "} + {language.t("chat.memory.badge.items", { count: count(item()) })} + 0}> · {items(item())[0]} +
+
+ )} +
+ + {(metrics) => } + ) } diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/TaskHeader.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/TaskHeader.tsx index 9b576726475..1f334c2fffc 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/TaskHeader.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/TaskHeader.tsx @@ -14,7 +14,8 @@ import { Tooltip } from "@kilocode/kilo-ui/tooltip" import { Icon } from "@kilocode/kilo-ui/icon" import { Checkbox } from "@kilocode/kilo-ui/checkbox" import { useSession } from "../../context/session" -import { calcTokenUsage, collapseCostBreakdown } from "../../context/session-utils" +import { useMemory } from "../../context/memory" +import { calcTokenUsage, collapseCostBreakdown, latestMetrics } from "../../context/session-utils" import { useLanguage } from "../../context/language" import { useVSCode } from "../../context/vscode" import { TaskTimeline } from "./TaskTimeline" @@ -86,6 +87,88 @@ export const TaskHeader: Component = (props) => { return false }) +// Throughput is the latest step-finish snapshot across the session so the + // figure reflects the most recent assistant turn rather than a session-wide + // average. Result is consumed by the TaskUsage summary row, which renders + // the value inline with the token counts — there is no standalone element. + const throughput = createMemo(() => { + const all = session.allParts() as Record + const flat: Part[] = [] + for (const parts of Object.values(all)) { + for (const part of parts) flat.push(part) + } + return latestMetrics(flat) + }) + + const memoryVerbose = createMemo(() => Boolean(memory.status()?.state.verbose)) + const memoryActive = createMemo(() => { + if (!memory.enabled()) return false + const stats = memory.status()?.state.stats + return !!stats && stats.lastInjectedSessionID === session.currentSessionID() && stats.lastInjectedTokens > 0 + }) + const memoryStatus = createMemo(() => { + if (memory.error()) return memory.error()! + if (memory.loading()) return language.t("chat.memory.status.loading") + if (!memory.enabled()) return language.t("chat.memory.project.disabled") + if (memoryActive()) return language.t("chat.memory.status.active") + return language.t("chat.memory.project.enabled") + }) + const activity = createMemo(() => [...memory.activity()].sort((a, b) => b.at - a.at)) + const activityLines = createMemo(() => { + if (memory.error() || !memory.enabled()) return [] + const loaded = activity().reduce((sum, item) => sum + (item.type === "loaded" ? item.tokens : 0), 0) + const recalled = activity().reduce((sum, item) => sum + (item.type === "recalled" ? item.count : 0), 0) + const saved = activity().reduce((sum, item) => sum + (item.type === "saved" ? item.count : 0), 0) + return [ + ...(loaded > 0 + ? [language.t("chat.memory.activity.loaded", { tokens: loaded.toLocaleString(language.locale()) })] + : []), + ...(recalled > 0 + ? [language.t("chat.memory.activity.recalled", { count: recalled.toLocaleString(language.locale()) })] + : []), + ...(saved > 0 + ? [language.t("chat.memory.activity.saved", { count: saved.toLocaleString(language.locale()) })] + : []), + ] + }) + const activityItems = createMemo(() => + activity() + .flatMap((item) => { + const values = + item.type === "saved" ? [...item.refs, ...item.items] : item.items.length > 0 ? item.items : item.refs + return values.flatMap((value) => { + const text = value.trim() + return text ? [{ type: item.type, value: text }] : [] + }) + }) + .slice(0, 5), + ) + const activityLabel = (item: { type: MemoryActivity["type"]; value: string }) => + language.t(`chat.memory.activity.${item.type}.item`, { item: item.value }) + const activitySummaryView = () => ( +
+ 0} + fallback={
{language.t("chat.memory.activity.idle")}
} + > +
+ {(line) =>
{line}
}
+
+
+
+ ) + const activityTooltip = () => ( + <> +
{language.t("settings.context.title")}
+
{memoryStatus()}
+ {activitySummaryView()} + 0}> +
+ {(item) =>
{activityLabel(item)}
}
+
+
+ + ) const vscode = useVSCode() const [expanded, setExpanded] = createSignal(true) @@ -288,6 +371,17 @@ export const TaskHeader: Component = (props) => { {(tk) => } + + {(t) => ( + +
+ {`PP ${t().pp}`} + · + {`TG ${t().tg}`} +
+
+ )} +
diff --git a/packages/kilo-vscode/webview-ui/src/context/session-utils.ts b/packages/kilo-vscode/webview-ui/src/context/session-utils.ts index ae448a3261b..c002d068dbc 100644 --- a/packages/kilo-vscode/webview-ui/src/context/session-utils.ts +++ b/packages/kilo-vscode/webview-ui/src/context/session-utils.ts @@ -204,6 +204,47 @@ export function calcTokenUsage( return undefined } +/** + * Aggregate tokens-per-second throughput across the step-finish parts of a + * session. + * + * Strategy: "last wins". The most recent step-finish with a `metrics` block + * is treated as the current throughput snapshot — that's the same number the + * per-message badge surfaces, so the aggregated header and per-message rows + * always agree. Provider-reported (llama.cpp / Ollama) values win over + * computed values when both appear in the same session, because provider + * timings reflect the model's actual rate rather than wall-clock duration. + * + * Returns `undefined` when no step-finish part in the input carries metrics, + * which is the signal callers use to hide the throughput UI. + */ +export function aggregateMetrics( + parts: readonly Part[], +): { prompt?: number; generation?: number; source: "provider" | "computed" } | undefined { + let best: { prompt?: number; generation?: number; source: "provider" | "computed" } | undefined + for (const part of parts) { + if (part.type !== "step-finish") continue + const metrics = part.metrics + if (!metrics) continue + // Provider-reported metrics outrank computed ones even if they appear + // later in the timeline. + if (metrics.source === "computed" && best?.source === "provider") continue + best = metrics + } + return best +} + +/** + * Pick the throughput snapshot from a single assistant message's parts. + * Uses the same last-wins strategy as `aggregateMetrics` so the per-message + * badge and the header row stay consistent. + */ +export function messageMetrics( + parts: readonly Part[], +): { prompt?: number; generation?: number; source: "provider" | "computed" } | undefined { + return aggregateMetrics(parts) +} + /** * Build a map of session ID → **own cost** for each session in the family * that has non-zero own cost. diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts index 5a04fd4eb7c..d5fffb6a12e 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts @@ -1679,6 +1679,16 @@ export const dict = { "اختر ما إذا كانت الكتل التي تعرض تعديلات التعليمات البرمجية والفروقات تبدأ موسّعة أم مطوية.", "settings.display.codeEdit.expanded": "موسّعة", "settings.display.codeEdit.collapsed": "مطوية", + + "settings.display.tokenThroughput.title": "Show Token Throughput", + "settings.display.tokenThroughput.description": + "Display tokens-per-second (prompt-processing / text-generation) on each assistant message and in the task header. Hidden by default to keep the chat uncluttered.", + + "chat.throughput.badge.provider": "PP {{pp}} · TG {{tg}}", + "chat.throughput.badge.computed": "PP – · TG {{tg}}", + "chat.throughput.badge.tooltip.provider": "Prompt-processing {{pp}} t/s · text-generation {{tg}} t/s (reported by provider)", + "chat.throughput.badge.tooltip.computed": "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", + "chat.throughput.badge.tooltip.missing": "Throughput metrics unavailable for this step", "settings.providers.defaultModel.title": "النموذج الافتراضي", "settings.providers.defaultModel.description": "النموذج الأساسي للمحادثات", "settings.providers.smallModel.title": "نموذج صغير", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/br.ts b/packages/kilo-vscode/webview-ui/src/i18n/br.ts index f153fec3c12..2ff6ae1ba4b 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/br.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/br.ts @@ -1730,6 +1730,16 @@ export const dict = { "Escolha se os blocos que exibem edições de código e diferenças começam expandidos ou recolhidos.", "settings.display.codeEdit.expanded": "Expandidos", "settings.display.codeEdit.collapsed": "Recolhidos", + + "settings.display.tokenThroughput.title": "Show Token Throughput", + "settings.display.tokenThroughput.description": + "Display tokens-per-second (prompt-processing / text-generation) on each assistant message and in the task header. Hidden by default to keep the chat uncluttered.", + + "chat.throughput.badge.provider": "PP {{pp}} · TG {{tg}}", + "chat.throughput.badge.computed": "PP – · TG {{tg}}", + "chat.throughput.badge.tooltip.provider": "Prompt-processing {{pp}} t/s · text-generation {{tg}} t/s (reported by provider)", + "chat.throughput.badge.tooltip.computed": "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", + "chat.throughput.badge.tooltip.missing": "Throughput metrics unavailable for this step", "settings.providers.defaultModel.title": "Modelo padrão", "settings.providers.defaultModel.description": "Modelo principal para conversas", "settings.providers.smallModel.title": "Modelo pequeno", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/bs.ts b/packages/kilo-vscode/webview-ui/src/i18n/bs.ts index 8a83fd8be41..81bcc33683b 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/bs.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/bs.ts @@ -1722,6 +1722,16 @@ export const dict = { "Odaberite da li će blokovi koji prikazuju izmjene koda i razlike u početku biti prošireni ili sažeti.", "settings.display.codeEdit.expanded": "Prošireni", "settings.display.codeEdit.collapsed": "Sažeti", + + "settings.display.tokenThroughput.title": "Show Token Throughput", + "settings.display.tokenThroughput.description": + "Display tokens-per-second (prompt-processing / text-generation) on each assistant message and in the task header. Hidden by default to keep the chat uncluttered.", + + "chat.throughput.badge.provider": "PP {{pp}} · TG {{tg}}", + "chat.throughput.badge.computed": "PP – · TG {{tg}}", + "chat.throughput.badge.tooltip.provider": "Prompt-processing {{pp}} t/s · text-generation {{tg}} t/s (reported by provider)", + "chat.throughput.badge.tooltip.computed": "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", + "chat.throughput.badge.tooltip.missing": "Throughput metrics unavailable for this step", "settings.providers.defaultModel.title": "Zadani model", "settings.providers.defaultModel.description": "Primarni model za razgovore", "settings.providers.smallModel.title": "Mali model", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/da.ts b/packages/kilo-vscode/webview-ui/src/i18n/da.ts index d4c0d06ce86..c52c4d62a99 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/da.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/da.ts @@ -1713,6 +1713,16 @@ export const dict = { "Vælg, om blokke, der viser koderedigeringer og forskelle, starter foldet ud eller sammen.", "settings.display.codeEdit.expanded": "Foldet ud", "settings.display.codeEdit.collapsed": "Foldet sammen", + + "settings.display.tokenThroughput.title": "Show Token Throughput", + "settings.display.tokenThroughput.description": + "Display tokens-per-second (prompt-processing / text-generation) on each assistant message and in the task header. Hidden by default to keep the chat uncluttered.", + + "chat.throughput.badge.provider": "PP {{pp}} · TG {{tg}}", + "chat.throughput.badge.computed": "PP – · TG {{tg}}", + "chat.throughput.badge.tooltip.provider": "Prompt-processing {{pp}} t/s · text-generation {{tg}} t/s (reported by provider)", + "chat.throughput.badge.tooltip.computed": "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", + "chat.throughput.badge.tooltip.missing": "Throughput metrics unavailable for this step", "settings.providers.defaultModel.title": "Standardmodel", "settings.providers.defaultModel.description": "Primær model til samtaler", "settings.providers.smallModel.title": "Lille model", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/de.ts b/packages/kilo-vscode/webview-ui/src/i18n/de.ts index bf036e817cf..3c93878a6c0 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/de.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/de.ts @@ -1749,6 +1749,16 @@ export const dict = { "Wählen Sie, ob Blöcke mit Codebearbeitungen und Unterschieden anfangs aus- oder eingeklappt sind.", "settings.display.codeEdit.expanded": "Ausgeklappt", "settings.display.codeEdit.collapsed": "Eingeklappt", + + "settings.display.tokenThroughput.title": "Show Token Throughput", + "settings.display.tokenThroughput.description": + "Display tokens-per-second (prompt-processing / text-generation) on each assistant message and in the task header. Hidden by default to keep the chat uncluttered.", + + "chat.throughput.badge.provider": "PP {{pp}} · TG {{tg}}", + "chat.throughput.badge.computed": "PP – · TG {{tg}}", + "chat.throughput.badge.tooltip.provider": "Prompt-processing {{pp}} t/s · text-generation {{tg}} t/s (reported by provider)", + "chat.throughput.badge.tooltip.computed": "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", + "chat.throughput.badge.tooltip.missing": "Throughput metrics unavailable for this step", "settings.providers.defaultModel.title": "Standardmodell", "settings.providers.defaultModel.description": "Primäres Modell für Gespräche", "settings.providers.smallModel.title": "Kleines Modell", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/en.ts b/packages/kilo-vscode/webview-ui/src/i18n/en.ts index bd742755d4f..8bda9de0a1c 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/en.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/en.ts @@ -1691,6 +1691,15 @@ export const dict = { "settings.display.codeEdit.description": "Choose whether code edit and diff blocks start expanded or collapsed.", "settings.display.codeEdit.expanded": "Expanded", "settings.display.codeEdit.collapsed": "Collapsed", + "settings.display.tokenThroughput.title": "Show Token Throughput", + "settings.display.tokenThroughput.description": + "Display tokens-per-second (prompt-processing / text-generation) on each assistant message and in the task header. Hidden by default to keep the chat uncluttered.", + + "chat.throughput.badge.provider": "PP {{pp}} · TG {{tg}}", + "chat.throughput.badge.computed": "PP – · TG {{tg}}", + "chat.throughput.badge.tooltip.provider": "Prompt-processing {{pp}} t/s · text-generation {{tg}} t/s (reported by provider)", + "chat.throughput.badge.tooltip.computed": "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", + "chat.throughput.badge.tooltip.missing": "Throughput metrics unavailable for this step", "settings.providers.defaultModel.title": "Default Model", "settings.providers.defaultModel.description": "Primary model for conversations", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/es.ts b/packages/kilo-vscode/webview-ui/src/i18n/es.ts index 415c87f2acb..bbe161f554a 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/es.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/es.ts @@ -1738,6 +1738,16 @@ export const dict = { "Elige si los bloques de edición de código y de diferencias aparecen inicialmente expandidos o contraídos.", "settings.display.codeEdit.expanded": "Expandidos", "settings.display.codeEdit.collapsed": "Contraídos", + + "settings.display.tokenThroughput.title": "Show Token Throughput", + "settings.display.tokenThroughput.description": + "Display tokens-per-second (prompt-processing / text-generation) on each assistant message and in the task header. Hidden by default to keep the chat uncluttered.", + + "chat.throughput.badge.provider": "PP {{pp}} · TG {{tg}}", + "chat.throughput.badge.computed": "PP – · TG {{tg}}", + "chat.throughput.badge.tooltip.provider": "Prompt-processing {{pp}} t/s · text-generation {{tg}} t/s (reported by provider)", + "chat.throughput.badge.tooltip.computed": "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", + "chat.throughput.badge.tooltip.missing": "Throughput metrics unavailable for this step", "settings.providers.defaultModel.title": "Modelo predeterminado", "settings.providers.defaultModel.description": "Modelo principal para conversaciones", "settings.providers.smallModel.title": "Modelo pequeño", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts index 05fac462ea6..60364f5d1f0 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts @@ -1759,6 +1759,16 @@ export const dict = { "Choisissez si les blocs de modification du code et de différences sont initialement développés ou réduits.", "settings.display.codeEdit.expanded": "Développés", "settings.display.codeEdit.collapsed": "Réduits", + + "settings.display.tokenThroughput.title": "Show Token Throughput", + "settings.display.tokenThroughput.description": + "Display tokens-per-second (prompt-processing / text-generation) on each assistant message and in the task header. Hidden by default to keep the chat uncluttered.", + + "chat.throughput.badge.provider": "PP {{pp}} · TG {{tg}}", + "chat.throughput.badge.computed": "PP – · TG {{tg}}", + "chat.throughput.badge.tooltip.provider": "Prompt-processing {{pp}} t/s · text-generation {{tg}} t/s (reported by provider)", + "chat.throughput.badge.tooltip.computed": "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", + "chat.throughput.badge.tooltip.missing": "Throughput metrics unavailable for this step", "settings.providers.defaultModel.title": "Modèle par défaut", "settings.providers.defaultModel.description": "Modèle principal pour les conversations", "settings.providers.smallModel.title": "Petit modèle", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/it.ts b/packages/kilo-vscode/webview-ui/src/i18n/it.ts index 43f32b66be8..4607cc34436 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/it.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/it.ts @@ -1516,6 +1516,16 @@ export const dict = { "Scegli se i blocchi delle modifiche al codice e delle differenze iniziano espansi o compressi.", "settings.display.codeEdit.expanded": "Espansi", "settings.display.codeEdit.collapsed": "Compressi", + + "settings.display.tokenThroughput.title": "Show Token Throughput", + "settings.display.tokenThroughput.description": + "Display tokens-per-second (prompt-processing / text-generation) on each assistant message and in the task header. Hidden by default to keep the chat uncluttered.", + + "chat.throughput.badge.provider": "PP {{pp}} · TG {{tg}}", + "chat.throughput.badge.computed": "PP – · TG {{tg}}", + "chat.throughput.badge.tooltip.provider": "Prompt-processing {{pp}} t/s · text-generation {{tg}} t/s (reported by provider)", + "chat.throughput.badge.tooltip.computed": "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", + "chat.throughput.badge.tooltip.missing": "Throughput metrics unavailable for this step", "settings.providers.defaultModel.title": "Modello predefinito", "settings.providers.defaultModel.description": "Modello principale per le conversazioni", "settings.providers.smallModel.title": "Modello leggero", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts index 8b63bbee619..e92ab2784d7 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts @@ -1708,6 +1708,16 @@ export const dict = { "コード編集ブロックと差分ブロックを最初から展開するか折りたたむかを選択します。", "settings.display.codeEdit.expanded": "展開", "settings.display.codeEdit.collapsed": "折りたたみ", + + "settings.display.tokenThroughput.title": "Show Token Throughput", + "settings.display.tokenThroughput.description": + "Display tokens-per-second (prompt-processing / text-generation) on each assistant message and in the task header. Hidden by default to keep the chat uncluttered.", + + "chat.throughput.badge.provider": "PP {{pp}} · TG {{tg}}", + "chat.throughput.badge.computed": "PP – · TG {{tg}}", + "chat.throughput.badge.tooltip.provider": "Prompt-processing {{pp}} t/s · text-generation {{tg}} t/s (reported by provider)", + "chat.throughput.badge.tooltip.computed": "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", + "chat.throughput.badge.tooltip.missing": "Throughput metrics unavailable for this step", "settings.providers.defaultModel.title": "デフォルトモデル", "settings.providers.defaultModel.description": "会話のプライマリモデル", "settings.providers.smallModel.title": "小型モデル", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts index edcfa35ae65..57d25b52eaf 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts @@ -1690,6 +1690,16 @@ export const dict = { "settings.display.codeEdit.description": "코드 편집 블록과 차이점 블록을 처음부터 펼칠지 접을지 선택합니다.", "settings.display.codeEdit.expanded": "펼침", "settings.display.codeEdit.collapsed": "접힘", + + "settings.display.tokenThroughput.title": "Show Token Throughput", + "settings.display.tokenThroughput.description": + "Display tokens-per-second (prompt-processing / text-generation) on each assistant message and in the task header. Hidden by default to keep the chat uncluttered.", + + "chat.throughput.badge.provider": "PP {{pp}} · TG {{tg}}", + "chat.throughput.badge.computed": "PP – · TG {{tg}}", + "chat.throughput.badge.tooltip.provider": "Prompt-processing {{pp}} t/s · text-generation {{tg}} t/s (reported by provider)", + "chat.throughput.badge.tooltip.computed": "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", + "chat.throughput.badge.tooltip.missing": "Throughput metrics unavailable for this step", "settings.providers.defaultModel.title": "기본 모델", "settings.providers.defaultModel.description": "대화의 기본 모델", "settings.providers.smallModel.title": "소형 모델", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/nl.ts b/packages/kilo-vscode/webview-ui/src/i18n/nl.ts index 3508d0bd94a..283bcc6bcb4 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/nl.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/nl.ts @@ -1691,6 +1691,16 @@ export const dict = { "settings.display.codeEdit.expanded": "Uitgeklapt", "settings.display.codeEdit.collapsed": "Ingeklapt", + "settings.display.tokenThroughput.title": "Show Token Throughput", + "settings.display.tokenThroughput.description": + "Display tokens-per-second (prompt-processing / text-generation) on each assistant message and in the task header. Hidden by default to keep the chat uncluttered.", + + "chat.throughput.badge.provider": "PP {{pp}} · TG {{tg}}", + "chat.throughput.badge.computed": "PP – · TG {{tg}}", + "chat.throughput.badge.tooltip.provider": "Prompt-processing {{pp}} t/s · text-generation {{tg}} t/s (reported by provider)", + "chat.throughput.badge.tooltip.computed": "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", + "chat.throughput.badge.tooltip.missing": "Throughput metrics unavailable for this step", + "settings.providers.defaultModel.title": "Standaard Model", "settings.providers.defaultModel.description": "Primair model voor gesprekken", "settings.providers.smallModel.title": "Klein Model", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/no.ts b/packages/kilo-vscode/webview-ui/src/i18n/no.ts index 82375c64f6b..3d712387678 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/no.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/no.ts @@ -1711,6 +1711,16 @@ export const dict = { "Velg om blokker for kodeendringer og forskjeller skal være utvidet eller skjult fra start.", "settings.display.codeEdit.expanded": "Utvidet", "settings.display.codeEdit.collapsed": "Skjult", + + "settings.display.tokenThroughput.title": "Show Token Throughput", + "settings.display.tokenThroughput.description": + "Display tokens-per-second (prompt-processing / text-generation) on each assistant message and in the task header. Hidden by default to keep the chat uncluttered.", + + "chat.throughput.badge.provider": "PP {{pp}} · TG {{tg}}", + "chat.throughput.badge.computed": "PP – · TG {{tg}}", + "chat.throughput.badge.tooltip.provider": "Prompt-processing {{pp}} t/s · text-generation {{tg}} t/s (reported by provider)", + "chat.throughput.badge.tooltip.computed": "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", + "chat.throughput.badge.tooltip.missing": "Throughput metrics unavailable for this step", "settings.providers.defaultModel.title": "Standardmodell", "settings.providers.defaultModel.description": "Primær modell for samtaler", "settings.providers.smallModel.title": "Liten modell", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts index 762e3499eca..715a08db35f 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts @@ -1721,6 +1721,16 @@ export const dict = { "Wybierz, czy bloki edycji kodu i podglądy różnic mają być początkowo rozwinięte czy zwinięte.", "settings.display.codeEdit.expanded": "Rozwinięte", "settings.display.codeEdit.collapsed": "Zwinięte", + + "settings.display.tokenThroughput.title": "Show Token Throughput", + "settings.display.tokenThroughput.description": + "Display tokens-per-second (prompt-processing / text-generation) on each assistant message and in the task header. Hidden by default to keep the chat uncluttered.", + + "chat.throughput.badge.provider": "PP {{pp}} · TG {{tg}}", + "chat.throughput.badge.computed": "PP – · TG {{tg}}", + "chat.throughput.badge.tooltip.provider": "Prompt-processing {{pp}} t/s · text-generation {{tg}} t/s (reported by provider)", + "chat.throughput.badge.tooltip.computed": "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", + "chat.throughput.badge.tooltip.missing": "Throughput metrics unavailable for this step", "settings.providers.defaultModel.title": "Domyślny model", "settings.providers.defaultModel.description": "Główny model do rozmów", "settings.providers.smallModel.title": "Mały model", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts index e8f25868876..cdb08838f67 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts @@ -1719,6 +1719,16 @@ export const dict = { "Выберите, будут ли блоки изменений кода и различий изначально развёрнуты или свёрнуты.", "settings.display.codeEdit.expanded": "Развёрнуты", "settings.display.codeEdit.collapsed": "Свёрнуты", + + "settings.display.tokenThroughput.title": "Show Token Throughput", + "settings.display.tokenThroughput.description": + "Display tokens-per-second (prompt-processing / text-generation) on each assistant message and in the task header. Hidden by default to keep the chat uncluttered.", + + "chat.throughput.badge.provider": "PP {{pp}} · TG {{tg}}", + "chat.throughput.badge.computed": "PP – · TG {{tg}}", + "chat.throughput.badge.tooltip.provider": "Prompt-processing {{pp}} t/s · text-generation {{tg}} t/s (reported by provider)", + "chat.throughput.badge.tooltip.computed": "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", + "chat.throughput.badge.tooltip.missing": "Throughput metrics unavailable for this step", "settings.providers.defaultModel.title": "Модель по умолчанию", "settings.providers.defaultModel.description": "Основная модель для разговоров", "settings.providers.smallModel.title": "Малая модель", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/th.ts b/packages/kilo-vscode/webview-ui/src/i18n/th.ts index 9b4db932452..eb60b5d80a1 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/th.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/th.ts @@ -1688,6 +1688,16 @@ export const dict = { "settings.display.codeEdit.description": "เลือกว่าบล็อกการแก้ไขโค้ดและบล็อกแสดงความแตกต่างจะเริ่มต้นแบบขยายหรือยุบ", "settings.display.codeEdit.expanded": "ขยาย", "settings.display.codeEdit.collapsed": "ยุบ", + + "settings.display.tokenThroughput.title": "Show Token Throughput", + "settings.display.tokenThroughput.description": + "Display tokens-per-second (prompt-processing / text-generation) on each assistant message and in the task header. Hidden by default to keep the chat uncluttered.", + + "chat.throughput.badge.provider": "PP {{pp}} · TG {{tg}}", + "chat.throughput.badge.computed": "PP – · TG {{tg}}", + "chat.throughput.badge.tooltip.provider": "Prompt-processing {{pp}} t/s · text-generation {{tg}} t/s (reported by provider)", + "chat.throughput.badge.tooltip.computed": "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", + "chat.throughput.badge.tooltip.missing": "Throughput metrics unavailable for this step", "settings.providers.defaultModel.title": "โมเดลเริ่มต้น", "settings.providers.defaultModel.description": "โมเดลหลักสำหรับบทสนทนา", "settings.providers.smallModel.title": "โมเดลขนาดเล็ก", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/tr.ts b/packages/kilo-vscode/webview-ui/src/i18n/tr.ts index 997798a9f05..cf5350890d9 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/tr.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/tr.ts @@ -1678,6 +1678,16 @@ export const dict = { "settings.display.codeEdit.expanded": "Genişletilmiş", "settings.display.codeEdit.collapsed": "Daraltılmış", + "settings.display.tokenThroughput.title": "Show Token Throughput", + "settings.display.tokenThroughput.description": + "Display tokens-per-second (prompt-processing / text-generation) on each assistant message and in the task header. Hidden by default to keep the chat uncluttered.", + + "chat.throughput.badge.provider": "PP {{pp}} · TG {{tg}}", + "chat.throughput.badge.computed": "PP – · TG {{tg}}", + "chat.throughput.badge.tooltip.provider": "Prompt-processing {{pp}} t/s · text-generation {{tg}} t/s (reported by provider)", + "chat.throughput.badge.tooltip.computed": "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", + "chat.throughput.badge.tooltip.missing": "Throughput metrics unavailable for this step", + "settings.providers.defaultModel.title": "Varsayılan Model", "settings.providers.defaultModel.description": "Sohbetler için birincil model", "settings.providers.smallModel.title": "Küçük Model", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/uk.ts b/packages/kilo-vscode/webview-ui/src/i18n/uk.ts index c7edb20c28b..0b227031235 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/uk.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/uk.ts @@ -1675,6 +1675,16 @@ export const dict = { "settings.display.codeEdit.expanded": "Розгорнуті", "settings.display.codeEdit.collapsed": "Згорнуті", + "settings.display.tokenThroughput.title": "Show Token Throughput", + "settings.display.tokenThroughput.description": + "Display tokens-per-second (prompt-processing / text-generation) on each assistant message and in the task header. Hidden by default to keep the chat uncluttered.", + + "chat.throughput.badge.provider": "PP {{pp}} · TG {{tg}}", + "chat.throughput.badge.computed": "PP – · TG {{tg}}", + "chat.throughput.badge.tooltip.provider": "Prompt-processing {{pp}} t/s · text-generation {{tg}} t/s (reported by provider)", + "chat.throughput.badge.tooltip.computed": "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", + "chat.throughput.badge.tooltip.missing": "Throughput metrics unavailable for this step", + "settings.providers.defaultModel.title": "Модель за замовчуванням", "settings.providers.defaultModel.description": "Основна модель для чатів", "settings.providers.smallModel.title": "Мала модель", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts index 3444a21d8d9..2f289d5a545 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts @@ -1640,6 +1640,16 @@ export const dict = { "settings.display.codeEdit.description": "选择代码编辑块和差异块的初始状态:展开或折叠。", "settings.display.codeEdit.expanded": "展开", "settings.display.codeEdit.collapsed": "折叠", + + "settings.display.tokenThroughput.title": "Show Token Throughput", + "settings.display.tokenThroughput.description": + "Display tokens-per-second (prompt-processing / text-generation) on each assistant message and in the task header. Hidden by default to keep the chat uncluttered.", + + "chat.throughput.badge.provider": "PP {{pp}} · TG {{tg}}", + "chat.throughput.badge.computed": "PP – · TG {{tg}}", + "chat.throughput.badge.tooltip.provider": "Prompt-processing {{pp}} t/s · text-generation {{tg}} t/s (reported by provider)", + "chat.throughput.badge.tooltip.computed": "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", + "chat.throughput.badge.tooltip.missing": "Throughput metrics unavailable for this step", "settings.providers.defaultModel.title": "默认模型", "settings.providers.defaultModel.description": "对话的主要模型", "settings.providers.smallModel.title": "小模型", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts index dfa2dd85f31..ae33cf17dc9 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts @@ -1604,6 +1604,16 @@ export const dict = { "settings.display.codeEdit.description": "選擇程式碼編輯區塊與差異區塊的初始狀態:展開或收合。", "settings.display.codeEdit.expanded": "展開", "settings.display.codeEdit.collapsed": "收合", + + "settings.display.tokenThroughput.title": "Show Token Throughput", + "settings.display.tokenThroughput.description": + "Display tokens-per-second (prompt-processing / text-generation) on each assistant message and in the task header. Hidden by default to keep the chat uncluttered.", + + "chat.throughput.badge.provider": "PP {{pp}} · TG {{tg}}", + "chat.throughput.badge.computed": "PP – · TG {{tg}}", + "chat.throughput.badge.tooltip.provider": "Prompt-processing {{pp}} t/s · text-generation {{tg}} t/s (reported by provider)", + "chat.throughput.badge.tooltip.computed": "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", + "chat.throughput.badge.tooltip.missing": "Throughput metrics unavailable for this step", "settings.providers.defaultModel.title": "預設模型", "settings.providers.defaultModel.description": "對話的主要模型", "settings.providers.smallModel.title": "小模型", diff --git a/packages/kilo-vscode/webview-ui/src/types/messages/extension-messages.ts b/packages/kilo-vscode/webview-ui/src/types/messages/extension-messages.ts index 32370ed555a..bec374b637e 100644 --- a/packages/kilo-vscode/webview-ui/src/types/messages/extension-messages.ts +++ b/packages/kilo-vscode/webview-ui/src/types/messages/extension-messages.ts @@ -590,6 +590,11 @@ export interface TimelineSettingLoadedMessage { visible: boolean } +export interface ThroughputSettingLoadedMessage { + type: "throughputSettingLoaded" + visible: boolean +} + export interface WorkStyleLoadedMessage { type: "workStyleLoaded" style: WorkStyleState @@ -1172,6 +1177,7 @@ export type ExtensionMessage = | GlobalConfigLoadedMessage | NotificationSettingsLoadedMessage | TimelineSettingLoadedMessage + | ThroughputSettingLoadedMessage | WorkStyleLoadedMessage | WorkStyleAppliedMessage | WorkStyleApplyFailedMessage diff --git a/packages/kilo-vscode/webview-ui/src/types/messages/parts.ts b/packages/kilo-vscode/webview-ui/src/types/messages/parts.ts index af7f58c0986..a91633a85ce 100644 --- a/packages/kilo-vscode/webview-ui/src/types/messages/parts.ts +++ b/packages/kilo-vscode/webview-ui/src/types/messages/parts.ts @@ -64,6 +64,16 @@ export interface StepStartPart extends BasePart { type: "step-start" } +// Tokens-per-second throughput metrics reported by the backend on step-finish. +// `source: "provider"` means the provider returned timings directly (llama.cpp +// prompt_per_second / predicted_per_second); `"computed"` means the backend +// derived the rate from server-side step duration and output tokens. +export interface StepThroughputMetrics { + prompt?: number + generation?: number + source: "provider" | "computed" +} + export interface StepFinishPart extends BasePart { type: "step-finish" reason?: string @@ -78,6 +88,7 @@ export interface StepFinishPart extends BasePart { reasoning?: number cache?: { read: number; write: number } } + metrics?: StepThroughputMetrics } export interface CompactionPart extends BasePart { diff --git a/packages/kilo-vscode/webview-ui/src/types/messages/webview-messages.ts b/packages/kilo-vscode/webview-ui/src/types/messages/webview-messages.ts index 48108ab9eb5..d27a1a375bb 100644 --- a/packages/kilo-vscode/webview-ui/src/types/messages/webview-messages.ts +++ b/packages/kilo-vscode/webview-ui/src/types/messages/webview-messages.ts @@ -433,6 +433,10 @@ export interface RequestTimelineSettingMessage { type: "requestTimelineSetting" } +export interface RequestThroughputSettingMessage { + type: "requestThroughputSetting" +} + export interface RequestWorkStyleMessage { type: "requestWorkStyle" } @@ -1277,6 +1281,7 @@ export type WebviewMessage = | ChatCompletionAcceptedMessage | UpdateSettingRequest | RequestTimelineSettingMessage + | RequestThroughputSettingMessage | RequestWorkStyleMessage | SetWorkStyleMessage | ApplyWorkStyleMessage diff --git a/packages/opencode/src/kilocode/cli/cmd/tui/routes/session/usage.tsx b/packages/opencode/src/kilocode/cli/cmd/tui/routes/session/usage.tsx new file mode 100644 index 00000000000..bc6efd7ace3 --- /dev/null +++ b/packages/opencode/src/kilocode/cli/cmd/tui/routes/session/usage.tsx @@ -0,0 +1,74 @@ +import type { RGBA } from "@opentui/core" +import { createMemo, createSignal, onCleanup, onMount, Show } from "solid-js" +import { UsageRow } from "@/kilocode/plugins/sidebar-usage-row" +import { + aggregateMetrics, + formatPP, + formatTG, + hasMetrics, + type StepMetrics, +} from "@/kilocode/plugins/model-usage" + +export namespace SessionUsagePanel { + export type Theme = { + text: RGBA + textMuted: RGBA + } + + export type EventPayload = { + sessionID?: string + part?: { type?: string; metrics?: unknown; tokens?: unknown } + } + + export type Deps = { + sessionID: string + theme: Theme + onPartUpdated?: (handler: (sessionID: string, part: EventPayload["part"]) => void) => () => void + } + + type Sample = { metrics?: StepMetrics; generated: number } + + function isMetrics(value: unknown): value is StepMetrics { + if (!value || typeof value !== "object") return false + const source = (value as Record).source + return source === "provider" || source === "computed" + } + + function generated(value: unknown): number { + if (!value || typeof value !== "object") return 0 + const record = value as Record + const output = typeof record.output === "number" ? record.output : 0 + const reasoning = typeof record.reasoning === "number" ? record.reasoning : 0 + return output + reasoning + } + + /** + * Subscribes to step-finish events for the given session and renders the + * aggregated prompt/tokens-per-second rows when at least one step has + * reported metrics. The row hides opportunistically for providers that + * never surface timing metadata (Anthropic, OpenAI, Gemini). + */ + export function View(props: Deps) { + const [samples, setSamples] = createSignal([]) + const throughput = createMemo(() => aggregateMetrics(samples())) + + onMount(() => { + if (!props.onPartUpdated) return + const off = props.onPartUpdated((sessionID, part) => { + if (sessionID !== props.sessionID) return + if (part?.type !== "step-finish") return + const metrics = isMetrics(part.metrics) ? part.metrics : undefined + const weight = generated(part.tokens) + setSamples((current) => [...current, { ...(metrics ? { metrics } : {}), generated: weight }]) + }) + onCleanup(() => off()) + }) + + return ( + + + + + ) + } +} \ No newline at end of file diff --git a/packages/opencode/src/kilocode/plugins/model-usage.ts b/packages/opencode/src/kilocode/plugins/model-usage.ts index 813b162275b..eafcaff648a 100644 --- a/packages/opencode/src/kilocode/plugins/model-usage.ts +++ b/packages/opencode/src/kilocode/plugins/model-usage.ts @@ -1,8 +1,11 @@ -import type { KilocodeSessionModelUsageResponse, Session } from "@kilocode/sdk/v2" +import type { KilocodeSessionModelUsageResponse, Session, StepFinishPart } from "@kilocode/sdk/v2" export type SessionModelUsage = KilocodeSessionModelUsageResponse export type UsageResult = { sessionID: string; data?: SessionModelUsage } +export type StepMetrics = NonNullable +export type AggregatedMetrics = { prompt?: number; generation?: number } + export function select(result: UsageResult | undefined, sessionID: string) { if (result?.sessionID !== sessionID) return undefined return result.data @@ -53,6 +56,7 @@ const currency = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", }) +const throughput = new Intl.NumberFormat("en-US", { maximumFractionDigits: 1 }) export function formatCount(value: number) { return count.format(value) @@ -64,7 +68,51 @@ export function formatRate(tokens: SessionModelUsage["totals"]["tokens"]) { return `${((tokens.cache.read / total) * 100).toFixed(1)}%` } +export function formatPP(value: number | undefined) { + if (!Number.isFinite(value) || value === undefined || value <= 0) return "-" + return `${throughput.format(value)} t/s` +} + +export function formatTG(value: number | undefined) { + if (!Number.isFinite(value) || value === undefined || value <= 0) return "-" + return `${throughput.format(value)} t/s` +} + export function formatCost(input: number) { const value = Math.max(0, Number.isFinite(input) ? input : 0) return currency.format(value) } + +// Local aggregation of step-finish metrics for the sidebar/usage panel. +// We weight by generated tokens so longer generations pull the average +// toward their reported rate, matching how llama.cpp's per-call timings +// average into a session-wide figure. +export function aggregateMetrics( + samples: ReadonlyArray<{ metrics?: StepMetrics; generated: number }>, +): AggregatedMetrics { + const totals = { promptSum: 0, promptWeight: 0, generationSum: 0, generationWeight: 0 } + for (const sample of samples) { + const rate = sample.metrics + if (!rate) continue + const weight = sample.generated > 0 ? sample.generated : 0 + if (Number.isFinite(rate.prompt) && (rate.prompt ?? 0) > 0 && weight > 0) { + totals.promptSum += (rate.prompt as number) * weight + totals.promptWeight += weight + } + if (Number.isFinite(rate.generation) && (rate.generation ?? 0) > 0 && weight > 0) { + totals.generationSum += (rate.generation as number) * weight + totals.generationWeight += weight + } + } + const prompt = totals.promptWeight > 0 ? totals.promptSum / totals.promptWeight : undefined + const generation = + totals.generationWeight > 0 ? totals.generationSum / totals.generationWeight : undefined + return { + ...(prompt !== undefined ? { prompt } : {}), + ...(generation !== undefined ? { generation } : {}), + } +} + +export function hasMetrics(value: AggregatedMetrics | undefined): value is AggregatedMetrics { + return value !== undefined && (value.prompt !== undefined || value.generation !== undefined) +} diff --git a/packages/opencode/src/kilocode/plugins/sidebar-usage.tsx b/packages/opencode/src/kilocode/plugins/sidebar-usage.tsx index 11df31ab1e6..c3ed3a01c26 100644 --- a/packages/opencode/src/kilocode/plugins/sidebar-usage.tsx +++ b/packages/opencode/src/kilocode/plugins/sidebar-usage.tsx @@ -6,24 +6,32 @@ import { Locale } from "@/util/locale" import { RoutedModelMeta } from "@/kilocode/cli/cmd/tui/routes/session/routed-model-meta" import { fmtAttemptCost, fmtScore } from "@/kilocode/components/model-info-panel-utils" import { + aggregateMetrics, failed, formatCost, formatCount, + formatPP, formatRate, + formatTG, groupModelsByProvider, + hasMetrics, isSessionTreeMember, select, + type StepMetrics, type UsageResult, } from "@/kilocode/plugins/model-usage" import { ModelRow, UsageRow } from "@/kilocode/plugins/sidebar-usage-row" const id = "internal:kilo-sidebar-usage" +type MetricSample = { metrics?: StepMetrics; generated: number } + function View(props: { api: TuiPluginApi; session_id: string }) { const [usageOpen, setUsageOpen] = createSignal(true) const [modelsOpen, setModelsOpen] = createSignal(true) const [benchOpen, setBenchOpen] = createSignal(true) const [expanded, setExpanded] = createSignal(new Set()) + const [samples, setSamples] = createSignal([]) const theme = () => props.api.theme.current const local = useLocal() const [result, { refetch }] = createResource( @@ -38,6 +46,7 @@ function View(props: { api: TuiPluginApi; session_id: string }) { const unavailable = createMemo(() => failed(result(), props.session_id)) const providers = createMemo(() => Model.index([...props.api.state.provider])) const groups = createMemo(() => groupModelsByProvider(usage()?.models ?? [], props.api.state.provider)) + const throughput = createMemo(() => aggregateMetrics(samples())) const bench = createMemo(() => { const current = local.model.current() if (!current) return undefined @@ -59,8 +68,16 @@ function View(props: { api: TuiPluginApi; session_id: string }) { const refresh = () => void refetch() const related = (sessionID: string, info?: ReturnType) => isSessionTreeMember({ root: props.session_id, sessionID, info, get: props.api.state.session.get }) + const recordSample = (sessionID: string, part: { type?: string; metrics?: unknown; tokens?: unknown }) => { + if (part.type !== "step-finish") return + if (!related(sessionID)) return + const metrics = isStepMetrics(part.metrics) ? part.metrics : undefined + const generated = generatedTokens(part.tokens) + setSamples((current) => [...current, { ...(metrics ? { metrics } : {}), generated }]) + } const offs = [ props.api.event.on("message.part.updated", (event) => { + recordSample(event.properties.sessionID, event.properties.part) if (event.properties.part.type === "step-finish" && related(event.properties.sessionID)) refresh() }), props.api.event.on("message.part.removed", (event) => { @@ -104,6 +121,10 @@ function View(props: { api: TuiPluginApi; session_id: string }) { + + + + )} @@ -202,6 +223,22 @@ function View(props: { api: TuiPluginApi; session_id: string }) { ) } +function isStepMetrics(value: unknown): value is StepMetrics { + if (!value || typeof value !== "object") return false + const record = value as Record + const source = record.source + if (source !== "provider" && source !== "computed") return false + return true +} + +function generatedTokens(value: unknown): number { + if (!value || typeof value !== "object") return 0 + const record = value as Record + const output = typeof record.output === "number" ? record.output : 0 + const reasoning = typeof record.reasoning === "number" ? record.reasoning : 0 + return output + reasoning +} + const tui: TuiPlugin = async (api) => { api.slots.register({ order: 150, diff --git a/packages/opencode/src/kilocode/session/metrics.ts b/packages/opencode/src/kilocode/session/metrics.ts new file mode 100644 index 00000000000..6b9a0b58f55 --- /dev/null +++ b/packages/opencode/src/kilocode/session/metrics.ts @@ -0,0 +1,73 @@ +// kilocode_change - new file +import { isRecord } from "@/util/record" + +export type TokenRates = { + prompt?: number + generation?: number + source: "provider" | "computed" +} + +export type ComputeInput = { + providerMetadata?: unknown + tokens: { + input: number + output: number + reasoning: number + cache: { read: number; write: number } + } + elapsedMs: number +} + +// kilocode_change start - tokens/second through-putation for #6579. +const safe = (value: unknown): number | undefined => { + if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return undefined + return value +} + +// llama.cpp surfaces timing under provider-specific metadata keys. We look at +// the most common shapes; providers without timing fields return undefined. +const providerRate = (metadata: unknown, key: string): number | undefined => { + if (!isRecord(metadata)) return undefined + for (const namespace of Object.values(metadata)) { + if (!isRecord(namespace)) continue + for (const [name, value] of Object.entries(namespace)) { + if (name.toLowerCase() === key.toLowerCase()) { + return safe(value) + } + } + } + return undefined +} + +export function computeMetrics(input: ComputeInput): TokenRates | undefined { + const providerPrompt = providerRate(input.providerMetadata, "prompt_per_second") + const providerGeneration = providerRate( + input.providerMetadata, + "predicted_per_second", + ) + + if (providerPrompt !== undefined || providerGeneration !== undefined) { + const result: TokenRates = { source: "provider" } + if (providerPrompt !== undefined) result.prompt = providerPrompt + if (providerGeneration !== undefined) result.generation = providerGeneration + return result + } + + if (!Number.isFinite(input.elapsedMs) || input.elapsedMs <= 0) return undefined + + const generated = input.tokens.output + input.tokens.reasoning + if (generated <= 0) return undefined + + const generation = (generated * 1000) / input.elapsedMs + if (!Number.isFinite(generation) || generation <= 0) return undefined + + return { generation, source: "computed" } +} + +const numberFormat = new Intl.NumberFormat("en-US", { maximumFractionDigits: 1 }) + +export function formatRate(value: number): string { + if (!Number.isFinite(value) || value <= 0) return "0 t/s" + return `${numberFormat.format(value)} t/s` +} +// kilocode_change end diff --git a/packages/opencode/src/kilocode/session/processor.ts b/packages/opencode/src/kilocode/session/processor.ts index a152cb87730..92f8e95e9cc 100644 --- a/packages/opencode/src/kilocode/session/processor.ts +++ b/packages/opencode/src/kilocode/session/processor.ts @@ -13,6 +13,7 @@ import { EffectBridge } from "@/effect/bridge" import type { LLMEvent, Usage } from "@opencode-ai/llm" import type { ProviderV2 } from "@opencode-ai/core/provider" import { SessionRetry } from "@/session/retry" +import { computeMetrics as computeMetricsHelper, type TokenRates } from "@/kilocode/session/metrics" export type ReviewTelemetry = { mode: "review" @@ -131,6 +132,11 @@ export namespace KiloSessionProcessor { } } + /** Pure throughput helper re-exported for namespace symmetry. */ + export const computeMetrics: typeof computeMetricsHelper = computeMetricsHelper + /** Returned shape for downstream consumers that prefer the namespace. */ + export type Metrics = TokenRates + /** * Effect-based offline handler for the retry schedule. * Shows offline status, waits for network reconnection or user rejection. diff --git a/packages/opencode/src/session/processor.ts b/packages/opencode/src/session/processor.ts index b80e184b49b..278eb7c0dc1 100644 --- a/packages/opencode/src/session/processor.ts +++ b/packages/opencode/src/session/processor.ts @@ -860,12 +860,20 @@ export const layer = Layer.effect( // kilocode_change start - guard against finish-step without start-step: // ctx.stepStart is 0 until `start-step` fires, which would feed a // huge bogus `elapsed` into telemetry. Fall back to now(). + const elapsedMs = Math.round( + performance.now() - (ctx.stepStart || performance.now()), + ) + const metrics = KiloSessionProcessor.computeMetrics({ + providerMetadata: value.providerMetadata, + tokens: usage.tokens, + elapsedMs, + }) KiloSessionProcessor.trackStep({ sessionID: ctx.sessionID, model: ctx.model, tokens: usage.tokens, cost: usage.cost, - elapsed: Math.round(performance.now() - (ctx.stepStart || performance.now())), + elapsed: elapsedMs, telemetry: ctx.telemetry, }) // kilocode_change end @@ -898,6 +906,7 @@ export const layer = Layer.effect( sessionID: ctx.assistantMessage.sessionID, type: "step-finish", ...(model ? { model } : {}), // kilocode_change + ...(metrics ? { metrics } : {}), // kilocode_change tokens: usage.tokens, cost: usage.cost, }) diff --git a/packages/opencode/test/kilocode/session-metrics.test.ts b/packages/opencode/test/kilocode/session-metrics.test.ts new file mode 100644 index 00000000000..d13b792870c --- /dev/null +++ b/packages/opencode/test/kilocode/session-metrics.test.ts @@ -0,0 +1,88 @@ +// kilocode_change - new file +import { describe, expect, test } from "bun:test" +import { computeMetrics, formatRate } from "@/kilocode/session/metrics" + +const tokens = { + input: 100, + output: 50, + reasoning: 0, + cache: { read: 0, write: 0 }, +} + +describe("kilocode.session.metrics.computeMetrics", () => { + test("preserves llama.cpp provider-reported timings", () => { + const metrics = computeMetrics({ + providerMetadata: { + llama: { prompt_per_second: 412.3, predicted_per_second: 28.7 }, + }, + tokens, + elapsedMs: 1000, + }) + expect(metrics?.source).toBe("provider") + expect(metrics?.prompt).toBeCloseTo(412.3) + expect(metrics?.generation).toBeCloseTo(28.7) + }) + + test("derives generation rate when no provider timing is present", () => { + const metrics = computeMetrics({ + providerMetadata: { openai: { finish_reason: "stop" } }, + tokens: { ...tokens, output: 100 }, + elapsedMs: 1000, + }) + expect(metrics?.source).toBe("computed") + expect(metrics?.generation).toBeCloseTo(100) + expect(metrics?.prompt).toBeUndefined() + }) + + test("returns undefined when there are no generation tokens", () => { + const metrics = computeMetrics({ + tokens: { ...tokens, output: 0, reasoning: 0 }, + elapsedMs: 2000, + }) + expect(metrics).toBeUndefined() + }) + + test("guards against zero elapsed time", () => { + const metrics = computeMetrics({ + tokens: { ...tokens, output: 50 }, + elapsedMs: 0, + }) + expect(metrics).toBeUndefined() + }) + + test("falls back to computed when provider rates are bogus", () => { + const metrics = computeMetrics({ + providerMetadata: { + llama: { prompt_per_second: -5, predicted_per_second: Number.POSITIVE_INFINITY }, + }, + tokens, + elapsedMs: 1000, + }) + expect(metrics?.source).toBe("computed") + expect(metrics?.generation).toBeCloseTo(50) + }) + + test("tolerates missing providerMetadata", () => { + const metrics = computeMetrics({ + tokens: { ...tokens, output: 200 }, + elapsedMs: 4000, + }) + expect(metrics?.source).toBe("computed") + expect(metrics?.generation).toBeCloseTo(50) + }) +}) + +describe("kilocode.session.metrics.formatRate", () => { + test.each([ + [0, "0 t/s"], + [12, "12 t/s"], + [412.5, "412.5 t/s"], + [12345, "12,345 t/s"], + ] as const)("formats %f as %s", (input, expected) => { + expect(formatRate(input)).toBe(expected) + }) + + test("returns zero string for negative inputs", () => { + expect(formatRate(-5)).toBe("0 t/s") + }) +}) diff --git a/packages/opencode/test/kilocode/tui/usage.test.ts b/packages/opencode/test/kilocode/tui/usage.test.ts new file mode 100644 index 00000000000..560f2392f4e --- /dev/null +++ b/packages/opencode/test/kilocode/tui/usage.test.ts @@ -0,0 +1,81 @@ +import { describe, expect, test } from "bun:test" +import { + aggregateMetrics, + formatPP, + formatTG, + hasMetrics, +} from "../../../src/kilocode/plugins/model-usage" + +describe("kilocode.plugins.model-usage throughput helpers", () => { + test("formats positive PP/TG values with grouping", () => { + expect(formatPP(412)).toBe("412 t/s") + expect(formatPP(412.5)).toBe("412.5 t/s") + expect(formatPP(12345)).toBe("12,345 t/s") + expect(formatTG(28.7)).toBe("28.7 t/s") + }) + + test("falls back to dash for missing or bogus values", () => { + expect(formatPP(undefined)).toBe("-") + expect(formatPP(0)).toBe("-") + expect(formatPP(-5)).toBe("-") + expect(formatPP(Number.NaN)).toBe("-") + expect(formatTG(undefined)).toBe("-") + expect(formatTG(0)).toBe("-") + expect(formatTG(Infinity)).toBe("-") + }) + + test("aggregates per-step metrics weighted by generated tokens", () => { + const aggregated = aggregateMetrics([ + { metrics: { generation: 20, source: "computed" }, generated: 100 }, + { metrics: { generation: 60, source: "computed" }, generated: 300 }, + ]) + expect(aggregated.generation).toBeCloseTo((20 * 100 + 60 * 300) / (100 + 300)) + }) + + test("aggregates prompt and generation independently", () => { + const aggregated = aggregateMetrics([ + { metrics: { prompt: 1000, generation: 20, source: "provider" }, generated: 100 }, + { metrics: { prompt: 500, generation: 60, source: "provider" }, generated: 300 }, + ]) + expect(aggregated.prompt).toBeCloseTo((1000 * 100 + 500 * 300) / 400) + expect(aggregated.generation).toBeCloseTo((20 * 100 + 60 * 300) / 400) + }) + + test("skips samples without metrics", () => { + const aggregated = aggregateMetrics([ + { metrics: undefined, generated: 100 }, + { metrics: { generation: 40, source: "computed" }, generated: 50 }, + ]) + expect(aggregated.generation).toBe(40) + }) + + test("skips samples with zero weight so prompt/generation stay valid", () => { + const aggregated = aggregateMetrics([ + { metrics: { prompt: 9999, generation: 9999, source: "provider" }, generated: 0 }, + { metrics: { generation: 25, source: "computed" }, generated: 50 }, + ]) + expect(aggregated.prompt).toBeUndefined() + expect(aggregated.generation).toBe(25) + }) + + test("returns empty aggregate when nothing has metrics", () => { + expect(aggregateMetrics([])).toEqual({}) + expect(aggregateMetrics([{ metrics: undefined, generated: 100 }])).toEqual({}) + }) + + test("ignores bogus per-call values without poisoning the aggregate", () => { + const aggregated = aggregateMetrics([ + { metrics: { generation: -1, source: "computed" }, generated: 100 }, + { metrics: { generation: Number.POSITIVE_INFINITY, source: "computed" }, generated: 100 }, + { metrics: { generation: 30, source: "computed" }, generated: 50 }, + ]) + expect(aggregated.generation).toBe(30) + }) + + test("hasMetrics gates opportunistic rendering", () => { + expect(hasMetrics(undefined)).toBeFalse() + expect(hasMetrics({})).toBeFalse() + expect(hasMetrics({ generation: 12 })).toBeTrue() + expect(hasMetrics({ prompt: 12 })).toBeTrue() + }) +}) \ No newline at end of file diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 0842cc481fa..96880188158 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -779,6 +779,11 @@ export type StepFinishPart = { providerID: string modelID: string } + metrics?: { + prompt?: number + generation?: number + source: "provider" | "computed" + } cost: number tokens: { total?: number diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index fa26476e92b..cbe3e3d71e1 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -26064,6 +26064,23 @@ "required": ["providerID", "modelID"], "additionalProperties": false }, + "metrics": { + "type": "object", + "properties": { + "prompt": { + "type": "number" + }, + "generation": { + "type": "number" + }, + "source": { + "type": "string", + "enum": ["provider", "computed"] + } + }, + "required": ["source"], + "additionalProperties": false + }, "cost": { "type": "number" }, From 2260c65785cf725fdfc9a1a22330191aa5826b46 Mon Sep 17 00:00:00 2001 From: Thomas Brugman Date: Tue, 21 Jul 2026 16:35:41 +0200 Subject: [PATCH 02/18] fix(vscode): wire token throughput toggle and drop unreachable provider branch - Replace the dead sendThroughputSetting() private with the shared buildThroughputSettingMessage() helper and add validThroughputSetting to handleUpdateSetting so the showTokenThroughput setting has the same guard as the chat/indexing twins (fixes Knip regression). - Bind the DisplayTab Switch to the local settings draft so the toggle flips on click instead of waiting for a Save round-trip (the user-facing kill switch for #6579). - Narrow StepThroughputMetrics.source to "computed"; backend hard-codes computed metrics today because the upstream AI SDK drops provider timings. Drop the unused provider branches from AssistantMessage and TaskHeader so the rendering code has no dead paths. Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com> --- packages/kilo-vscode/src/KiloProvider.ts | 22 ++++++++++-------- .../src/kilo-provider/throughput-settings.ts | 23 +++++++++++++++++++ .../src/components/settings/DisplayTab.tsx | 13 +++++++++++ .../webview-ui/src/types/messages/parts.ts | 10 ++++---- 4 files changed, 55 insertions(+), 13 deletions(-) create mode 100644 packages/kilo-vscode/src/kilo-provider/throughput-settings.ts diff --git a/packages/kilo-vscode/src/KiloProvider.ts b/packages/kilo-vscode/src/KiloProvider.ts index cbef0b907ac..874d4b7faca 100644 --- a/packages/kilo-vscode/src/KiloProvider.ts +++ b/packages/kilo-vscode/src/KiloProvider.ts @@ -172,6 +172,11 @@ import { watchIndexingConfig, } from "./kilo-provider/indexing-settings" import { buildChatSettingsMessage, validChatSetting, watchChatConfig } from "./kilo-provider/chat-settings" +import { + buildThroughputSettingMessage, + validThroughputSetting, + watchThroughputConfig, +} from "./kilo-provider/throughput-settings" let maxCost = 0 @@ -393,6 +398,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper private autocompleteConfigDisposable: vscode.Disposable | null = null private indexingConfigDisposable: vscode.Disposable | null = null private chatConfigDisposable: vscode.Disposable | null = null + private throughputConfigDisposable: vscode.Disposable | null = null private telemetryStateDisposable: vscode.Disposable | null = null private viewStateDisposable: vscode.Disposable | null = null private visibilityDisposable: vscode.Disposable | null = null @@ -917,6 +923,8 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper this.indexingConfigDisposable = watchIndexingConfig((msg) => this.postMessage(msg)) this.chatConfigDisposable?.dispose() this.chatConfigDisposable = watchChatConfig((msg) => this.postMessage(msg)) + this.throughputConfigDisposable?.dispose() + this.throughputConfigDisposable = watchThroughputConfig((msg) => this.postMessage(msg)) this.telemetryStateDisposable?.dispose() this.telemetryStateDisposable = watchTelemetryState((msg) => this.postMessage(msg)) this.webviewMessageDisposable = webview.onDidReceiveMessage(async (message) => { @@ -1352,7 +1360,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper this.sendTimelineSetting() break case "requestThroughputSetting": - this.sendThroughputSetting() + this.postMessage(buildThroughputSettingMessage()) break case "requestNotifications": this.fetchAndSendNotifications().catch((e) => @@ -1737,6 +1745,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper this.postMessage({ type: "gitStatus", repo: this.cachedGitRepo }) this.sendNotificationSettings() this.sendTimelineSetting() + this.postMessage(buildThroughputSettingMessage()) this.postMessage({ type: "extensionDataReady" }) if (this.cachedGitRepo) this.startStatsPolling() @@ -2715,14 +2724,6 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper }) } - private sendThroughputSetting(): void { - const config = vscode.workspace.getConfiguration("kilo-code.new") - this.postMessage({ - type: "throughputSettingLoaded", - visible: config.get("showTokenThroughput", false), - }) - } - private sendWorkStyle(): void { this.postMessage(getWorkStylePayload()) } @@ -3684,6 +3685,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper if (section === "autocomplete" && !validAutocompleteSetting(leaf, value)) return if (section === "indexing" && !validIndexingSetting(leaf, value)) return if (section === "chat" && !validChatSetting(leaf, value)) return + if (section === "" && !validThroughputSetting(leaf, value)) return const config = vscode.workspace.getConfiguration(`kilo-code.new${section ? `.${section}` : ""}`) // Normalize a webview-side clear to `undefined` so VS Code removes the // key from settings.json rather than persisting a literal `null`. This @@ -3735,6 +3737,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper this.sendBrowserSettings() this.sendNotificationSettings() this.sendTimelineSetting() + this.postMessage(buildThroughputSettingMessage()) this.sendWorkStyle() await ModelState.reset(this.client, (msg) => this.postMessage(msg)) @@ -4529,6 +4532,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper this.autocompleteConfigDisposable?.dispose() this.indexingConfigDisposable?.dispose() this.chatConfigDisposable?.dispose() + this.throughputConfigDisposable?.dispose() this.telemetryStateDisposable?.dispose() this.autoApproveBridge?.dispose() this.visibleTaskStreams.clear() diff --git a/packages/kilo-vscode/src/kilo-provider/throughput-settings.ts b/packages/kilo-vscode/src/kilo-provider/throughput-settings.ts new file mode 100644 index 00000000000..304477a6942 --- /dev/null +++ b/packages/kilo-vscode/src/kilo-provider/throughput-settings.ts @@ -0,0 +1,23 @@ +import * as vscode from "vscode" + +type Post = (msg: unknown) => void + +export function buildThroughputSettingMessage() { + const config = vscode.workspace.getConfiguration("kilo-code.new") + return { + type: "throughputSettingLoaded" as const, + visible: config.get("showTokenThroughput", false), + } +} + +export function watchThroughputConfig(post: Post): vscode.Disposable { + return vscode.workspace.onDidChangeConfiguration((event) => { + if (event.affectsConfiguration("kilo-code.new.showTokenThroughput")) { + post(buildThroughputSettingMessage()) + } + }) +} + +export function validThroughputSetting(key: string, value: unknown) { + return key === "showTokenThroughput" && typeof value === "boolean" +} \ No newline at end of file diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/DisplayTab.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/DisplayTab.tsx index 74bae331aca..ae65e7348fe 100644 --- a/packages/kilo-vscode/webview-ui/src/components/settings/DisplayTab.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/settings/DisplayTab.tsx @@ -91,6 +91,19 @@ const DisplayTab: Component = () => { + + updateSetting("showTokenThroughput", checked)} + hideLabel + > + {language.t("settings.display.tokenThroughput.title")} + + + Date: Tue, 21 Jul 2026 16:35:46 +0200 Subject: [PATCH 03/18] fix(opencode): centralize token throughput labels and tighten type guard - Replace inline PP/TG labels in the CLI sidebar with a throughputLabel constant in model-usage so a future i18n sweep is one file instead of every rendering site. - Tighten isStepMetrics in sidebar-usage back to a real discriminator check after dropping the unreachable "provider" union member. - Drop formatPP/formatTG exports from model-usage since callers already use the shared formatRateValue; mirror the swap in the TUI usage test. Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com> --- .../src/kilocode/plugins/model-usage.ts | 55 ++++++++------ .../src/kilocode/plugins/sidebar-usage.tsx | 14 ++-- .../opencode/test/kilocode/tui/usage.test.ts | 74 +++++++++++-------- 3 files changed, 84 insertions(+), 59 deletions(-) diff --git a/packages/opencode/src/kilocode/plugins/model-usage.ts b/packages/opencode/src/kilocode/plugins/model-usage.ts index eafcaff648a..69d19de8b87 100644 --- a/packages/opencode/src/kilocode/plugins/model-usage.ts +++ b/packages/opencode/src/kilocode/plugins/model-usage.ts @@ -68,15 +68,18 @@ export function formatRate(tokens: SessionModelUsage["totals"]["tokens"]) { return `${((tokens.cache.read / total) * 100).toFixed(1)}%` } -export function formatPP(value: number | undefined) { +export function formatRateValue(value: number | undefined) { if (!Number.isFinite(value) || value === undefined || value <= 0) return "-" - return `${throughput.format(value)} t/s` + return `${throughput.format(value as number)} t/s` } -export function formatTG(value: number | undefined) { - if (!Number.isFinite(value) || value === undefined || value <= 0) return "-" - return `${throughput.format(value)} t/s` -} +// Throughput labels used by the sidebar / usage panel. Centralized here so a +// future i18n sweep only touches one file — the opencode CLI does not yet +// wire a translation layer, so today these are literal English labels. +export const throughputLabel = { + prompt: "PP", + generation: "TG", +} as const export function formatCost(input: number) { const value = Math.max(0, Number.isFinite(input) ? input : 0) @@ -84,29 +87,37 @@ export function formatCost(input: number) { } // Local aggregation of step-finish metrics for the sidebar/usage panel. -// We weight by generated tokens so longer generations pull the average -// toward their reported rate, matching how llama.cpp's per-call timings -// average into a session-wide figure. +// +// We weight the *generation* rate by generated tokens (longer generations +// pull the average toward their own reported rate). The *prompt* rate comes +// from llama.cpp's `prompt_per_second`, which is a property of the timing +// not the generated output — when a step finishes before tokens are +// emitted (e.g. a tool-only step) the per-step generated count is zero and +// a generated-weighted average would silently drop that step. We fall back +// to a simple mean over the steps that did report a positive prompt rate +// so a zero-weight step still contributes. export function aggregateMetrics( samples: ReadonlyArray<{ metrics?: StepMetrics; generated: number }>, ): AggregatedMetrics { - const totals = { promptSum: 0, promptWeight: 0, generationSum: 0, generationWeight: 0 } + let promptSum = 0 + let promptCount = 0 + let generationSum = 0 + let generationWeight = 0 for (const sample of samples) { - const rate = sample.metrics - if (!rate) continue - const weight = sample.generated > 0 ? sample.generated : 0 - if (Number.isFinite(rate.prompt) && (rate.prompt ?? 0) > 0 && weight > 0) { - totals.promptSum += (rate.prompt as number) * weight - totals.promptWeight += weight + const metrics = sample.metrics + if (!metrics) continue + if (Number.isFinite(metrics.prompt) && (metrics.prompt as number) > 0) { + promptSum += metrics.prompt as number + promptCount += 1 } - if (Number.isFinite(rate.generation) && (rate.generation ?? 0) > 0 && weight > 0) { - totals.generationSum += (rate.generation as number) * weight - totals.generationWeight += weight + const generated = sample.generated + if (Number.isFinite(metrics.generation) && (metrics.generation as number) > 0 && generated > 0) { + generationSum += (metrics.generation as number) * generated + generationWeight += generated } } - const prompt = totals.promptWeight > 0 ? totals.promptSum / totals.promptWeight : undefined - const generation = - totals.generationWeight > 0 ? totals.generationSum / totals.generationWeight : undefined + const prompt = promptCount > 0 ? promptSum / promptCount : undefined + const generation = generationWeight > 0 ? generationSum / generationWeight : undefined return { ...(prompt !== undefined ? { prompt } : {}), ...(generation !== undefined ? { generation } : {}), diff --git a/packages/opencode/src/kilocode/plugins/sidebar-usage.tsx b/packages/opencode/src/kilocode/plugins/sidebar-usage.tsx index c3ed3a01c26..574b62e0f25 100644 --- a/packages/opencode/src/kilocode/plugins/sidebar-usage.tsx +++ b/packages/opencode/src/kilocode/plugins/sidebar-usage.tsx @@ -10,13 +10,13 @@ import { failed, formatCost, formatCount, - formatPP, formatRate, - formatTG, + formatRateValue, groupModelsByProvider, hasMetrics, isSessionTreeMember, select, + throughputLabel, type StepMetrics, type UsageResult, } from "@/kilocode/plugins/model-usage" @@ -122,8 +122,8 @@ function View(props: { api: TuiPluginApi; session_id: string }) { - - + + @@ -225,10 +225,8 @@ function View(props: { api: TuiPluginApi; session_id: string }) { function isStepMetrics(value: unknown): value is StepMetrics { if (!value || typeof value !== "object") return false - const record = value as Record - const source = record.source - if (source !== "provider" && source !== "computed") return false - return true + const source = (value as { source?: unknown }).source + return source === "computed" } function generatedTokens(value: unknown): number { diff --git a/packages/opencode/test/kilocode/tui/usage.test.ts b/packages/opencode/test/kilocode/tui/usage.test.ts index 560f2392f4e..76e71577bae 100644 --- a/packages/opencode/test/kilocode/tui/usage.test.ts +++ b/packages/opencode/test/kilocode/tui/usage.test.ts @@ -1,60 +1,76 @@ import { describe, expect, test } from "bun:test" import { aggregateMetrics, - formatPP, - formatTG, + formatRateValue, hasMetrics, + throughputLabel, } from "../../../src/kilocode/plugins/model-usage" +const step = (metrics: { prompt?: number; generation?: number }) => ({ + metrics: { source: "computed" as const, ...metrics }, + generated: 0, +}) + describe("kilocode.plugins.model-usage throughput helpers", () => { - test("formats positive PP/TG values with grouping", () => { - expect(formatPP(412)).toBe("412 t/s") - expect(formatPP(412.5)).toBe("412.5 t/s") - expect(formatPP(12345)).toBe("12,345 t/s") - expect(formatTG(28.7)).toBe("28.7 t/s") + test("formatRateValue renders positive values with grouping", () => { + expect(formatRateValue(412)).toBe("412 t/s") + expect(formatRateValue(412.5)).toBe("412.5 t/s") + expect(formatRateValue(12345)).toBe("12,345 t/s") + expect(formatRateValue(28.7)).toBe("28.7 t/s") + }) + + test("formatRateValue falls back to dash for missing or bogus values", () => { + expect(formatRateValue(undefined)).toBe("-") + expect(formatRateValue(0)).toBe("-") + expect(formatRateValue(-5)).toBe("-") + expect(formatRateValue(Number.NaN)).toBe("-") + expect(formatRateValue(Infinity)).toBe("-") }) - test("falls back to dash for missing or bogus values", () => { - expect(formatPP(undefined)).toBe("-") - expect(formatPP(0)).toBe("-") - expect(formatPP(-5)).toBe("-") - expect(formatPP(Number.NaN)).toBe("-") - expect(formatTG(undefined)).toBe("-") - expect(formatTG(0)).toBe("-") - expect(formatTG(Infinity)).toBe("-") + test("throughputLabel centralizes the PP/TG labels so a future i18n sweep is one file", () => { + expect(throughputLabel.prompt).toBe("PP") + expect(throughputLabel.generation).toBe("TG") }) - test("aggregates per-step metrics weighted by generated tokens", () => { + test("aggregates per-step generation weighted by generated tokens", () => { const aggregated = aggregateMetrics([ - { metrics: { generation: 20, source: "computed" }, generated: 100 }, - { metrics: { generation: 60, source: "computed" }, generated: 300 }, + { ...step({ generation: 20 }), generated: 100 }, + { ...step({ generation: 60 }), generated: 300 }, ]) expect(aggregated.generation).toBeCloseTo((20 * 100 + 60 * 300) / (100 + 300)) }) test("aggregates prompt and generation independently", () => { const aggregated = aggregateMetrics([ - { metrics: { prompt: 1000, generation: 20, source: "provider" }, generated: 100 }, - { metrics: { prompt: 500, generation: 60, source: "provider" }, generated: 300 }, + { ...step({ prompt: 1000, generation: 20 }), generated: 100 }, + { ...step({ prompt: 500, generation: 60 }), generated: 300 }, ]) - expect(aggregated.prompt).toBeCloseTo((1000 * 100 + 500 * 300) / 400) + expect(aggregated.prompt).toBeCloseTo((1000 + 500) / 2) expect(aggregated.generation).toBeCloseTo((20 * 100 + 60 * 300) / 400) }) + test("includes prompt from a zero-generated step so it isn't silently dropped", () => { + const aggregated = aggregateMetrics([ + { ...step({ prompt: 9999, generation: 9999 }), generated: 0 }, + { ...step({ generation: 25 }), generated: 50 }, + ]) + expect(aggregated.prompt).toBe(9999) + expect(aggregated.generation).toBe(25) + }) + test("skips samples without metrics", () => { const aggregated = aggregateMetrics([ { metrics: undefined, generated: 100 }, - { metrics: { generation: 40, source: "computed" }, generated: 50 }, + { ...step({ generation: 40 }), generated: 50 }, ]) expect(aggregated.generation).toBe(40) }) - test("skips samples with zero weight so prompt/generation stay valid", () => { + test("skips zero-weight samples for the generation average", () => { const aggregated = aggregateMetrics([ - { metrics: { prompt: 9999, generation: 9999, source: "provider" }, generated: 0 }, - { metrics: { generation: 25, source: "computed" }, generated: 50 }, + { ...step({ generation: 9999 }), generated: 0 }, + { ...step({ generation: 25 }), generated: 50 }, ]) - expect(aggregated.prompt).toBeUndefined() expect(aggregated.generation).toBe(25) }) @@ -65,9 +81,9 @@ describe("kilocode.plugins.model-usage throughput helpers", () => { test("ignores bogus per-call values without poisoning the aggregate", () => { const aggregated = aggregateMetrics([ - { metrics: { generation: -1, source: "computed" }, generated: 100 }, - { metrics: { generation: Number.POSITIVE_INFINITY, source: "computed" }, generated: 100 }, - { metrics: { generation: 30, source: "computed" }, generated: 50 }, + { ...step({ generation: -1 }), generated: 100 }, + { ...step({ generation: Number.POSITIVE_INFINITY }), generated: 100 }, + { ...step({ generation: 30 }), generated: 50 }, ]) expect(aggregated.generation).toBe(30) }) From 67bd5287dfca42f8f76b790f4ba010d0083a41e9 Mon Sep 17 00:00:00 2001 From: Thomas Brugman Date: Tue, 21 Jul 2026 16:35:55 +0200 Subject: [PATCH 04/18] feat(token-throughput-v2): wire aggregation through DisplayProvider and test computed-only - aggregateMetrics adopts the first non-empty computed sample per field across every step-finish in the session, replacing the dead provider- ranked last-wins strategy that shipped with the unreachable branch. - Share the throughputVisible signal through DisplayProvider so every AssistantMessage and the TaskHeader row react to a single onMount requestThroughputSetting round-trip, instead of each message posting its own handshake. - Drop the unused routes/session/usage.tsx TUI route (no remaining imports) and add the chat-layout badge/header pill styles it was gating on. - Refresh session-utils tests to exercise only source: "computed" samples and follow the new first-wins rule. Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com> --- .../tests/unit/session-utils.test.ts | 56 +++++++++++--- .../src/components/chat/AssistantMessage.tsx | 59 +++------------ .../src/components/chat/TaskHeader.tsx | 11 ++- .../webview-ui/src/context/display.tsx | 12 +++ .../webview-ui/src/context/session-utils.ts | 54 ++++++++++---- .../webview-ui/src/styles/chat-layout.css | 20 +++++ .../cli/cmd/tui/routes/session/usage.tsx | 74 ------------------- .../opencode/src/kilocode/session/metrics.ts | 50 +++---------- .../test/kilocode/session-metrics.test.ts | 32 +++----- 9 files changed, 157 insertions(+), 211 deletions(-) delete mode 100644 packages/opencode/src/kilocode/cli/cmd/tui/routes/session/usage.tsx diff --git a/packages/kilo-vscode/tests/unit/session-utils.test.ts b/packages/kilo-vscode/tests/unit/session-utils.test.ts index 0dff94f2a00..49d731df8b2 100644 --- a/packages/kilo-vscode/tests/unit/session-utils.test.ts +++ b/packages/kilo-vscode/tests/unit/session-utils.test.ts @@ -6,6 +6,8 @@ import { calcTokenUsage, aggregateMetrics, messageMetrics, + formatPP, + formatTG, buildFamilyCosts, buildFamilyParents, buildFamilyParentsFromTools, @@ -722,7 +724,7 @@ describe("collapseCostBreakdown", () => { // ── Throughput aggregation ───────────────────────────────────────────── -function stepFinish(id: string, metrics?: Part["metrics"]): Part { +function stepFinish(id: string, metrics?: NonNullable): Part { return { type: "step-finish", id, @@ -740,39 +742,51 @@ describe("aggregateMetrics", () => { expect(aggregateMetrics(parts)).toBeUndefined() }) - it("returns the metrics block from the last step-finish with metrics", () => { + it("merges the first non-empty metrics across every step in the session", () => { const parts: Part[] = [ stepFinish("f1", { prompt: 100, generation: 20, source: "computed" }), { type: "text", id: "t1", text: "mid" }, - stepFinish("f2", { prompt: 412, generation: 38, source: "provider" }), + stepFinish("f2", { prompt: 412, generation: 38, source: "computed" }), ] - expect(aggregateMetrics(parts)).toEqual({ prompt: 412, generation: 38, source: "provider" }) + expect(aggregateMetrics(parts)).toEqual({ prompt: 100, generation: 20, source: "computed" }) }) - it("prefers provider-reported metrics over computed ones regardless of order", () => { + it("keeps the first computed sample for each field when later steps report zero", () => { const parts: Part[] = [ - stepFinish("f1", { prompt: 500, generation: 50, source: "provider" }), + stepFinish("f1", { prompt: 500, generation: 50, source: "computed" }), stepFinish("f2", { generation: 30, source: "computed" }), ] const result = aggregateMetrics(parts) - expect(result?.source).toBe("provider") + expect(result?.source).toBe("computed") expect(result?.prompt).toBe(500) + expect(result?.generation).toBe(50) }) - it("falls back to computed metrics when no provider metrics exist", () => { + it("uses the first computed value per field when no earlier value is present", () => { const parts: Part[] = [ stepFinish("f1", { generation: 12, source: "computed" }), stepFinish("f2", { generation: 18, source: "computed" }), ] - expect(aggregateMetrics(parts)).toEqual({ generation: 18, source: "computed" }) + expect(aggregateMetrics(parts)).toEqual({ generation: 12, source: "computed" }) }) it("ignores non-step-finish parts even when they look like metrics", () => { const parts: Part[] = [ { type: "text", id: "t1", text: "noise" }, - stepFinish("f1", { prompt: 200, generation: 22, source: "provider" }), + stepFinish("f1", { prompt: 200, generation: 22, source: "computed" }), ] - expect(aggregateMetrics(parts)).toEqual({ prompt: 200, generation: 22, source: "provider" }) + expect(aggregateMetrics(parts)).toEqual({ prompt: 200, generation: 22, source: "computed" }) + }) + + it("combines multi-step metrics into a single snapshot per message", () => { + // An assistant turn that runs reasoning + answer produces two step-finish + // parts; the badge should merge them so a prompt sample from the first + // step and a generation sample from the second coexist in the snapshot. + const parts: Part[] = [ + stepFinish("f1", { prompt: 200, source: "computed" }), + stepFinish("f2", { generation: 25, source: "computed" }), + ] + expect(messageMetrics(parts)).toEqual({ prompt: 200, generation: 25, source: "computed" }) }) }) @@ -780,7 +794,7 @@ describe("messageMetrics", () => { it("matches aggregateMetrics behavior on the same input", () => { const parts: Part[] = [ stepFinish("f1", { generation: 8, source: "computed" }), - stepFinish("f2", { prompt: 99, generation: 33, source: "provider" }), + stepFinish("f2", { prompt: 99, generation: 33, source: "computed" }), ] expect(messageMetrics(parts)).toEqual(aggregateMetrics(parts)) }) @@ -790,3 +804,21 @@ describe("messageMetrics", () => { expect(messageMetrics([{ type: "text", id: "t1", text: "no metrics here" }])).toBeUndefined() }) }) + +describe("throughput formatters", () => { + const locale = "en-US" + + it("renders the value with a t/s suffix", () => { + expect(formatPP(412, locale)).toBe("412 t/s") + expect(formatTG(28.7, locale)).toBe("28.7 t/s") + }) + + it("falls back to dash for missing or bogus values", () => { + expect(formatPP(undefined, locale)).toBe("–") + expect(formatPP(0, locale)).toBe("–") + expect(formatPP(-5, locale)).toBe("–") + expect(formatPP(Number.NaN, locale)).toBe("–") + expect(formatPP(Number.POSITIVE_INFINITY, locale)).toBe("–") + expect(formatTG(undefined, locale)).toBe("–") + }) +}) diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/AssistantMessage.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/AssistantMessage.tsx index b843c4d84e9..aec94f8f4a6 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/AssistantMessage.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/AssistantMessage.tsx @@ -7,7 +7,7 @@ * Active questions render inline via QuestionDock; permissions are in the bottom dock. */ -import { Component, For, Show, createMemo, createSignal, onMount, onCleanup } from "solid-js" +import { Component, For, Show, createMemo } from "solid-js" import { Dynamic } from "solid-js/web" import { Part, PART_MAPPING, ToolRegistry } from "@kilocode/kilo-ui/message-part" import type { MessageFeedbackControls } from "@kilocode/kilo-ui/message-part" @@ -23,10 +23,9 @@ import { useDisplay } from "../../context/display" import { useConfig } from "../../context/config" import { useLanguage } from "../../context/language" import { useServer } from "../../context/server" -import { useVSCode } from "../../context/vscode" import { planDisplayPath } from "../../utils/plan-path" import { isRenderable, UPSTREAM_SUPPRESSED_TOOLS } from "../../utils/transcript-parts" -import { messageMetrics } from "../../context/session-utils" +import { messageMetrics, formatTG } from "../../context/session-utils" import { MemoryMarkerMeta } from "@kilocode/kilo-memory/marker-meta" import { color as timelineColor } from "../../utils/timeline/colors" import type { Part as TimelinePart } from "../../types/messages" @@ -173,48 +172,21 @@ function BashToolCard(props: { part: ToolPart; defaultOpen: boolean; forceOpen?: * step-finish part that carries throughput metrics. */ function ThroughputBadge(props: { metrics: NonNullable> }) { const language = useLanguage() - const formatter = createMemo( - () => - new Intl.NumberFormat(language.locale(), { - maximumFractionDigits: 1, - }), - ) - const ppText = createMemo(() => { - const prompt = props.metrics.prompt - if (prompt === undefined) return "–" - return formatter().format(prompt) - }) - const tgText = createMemo(() => { - const gen = props.metrics.generation - if (gen === undefined) return "–" - return formatter().format(gen) - }) + const tgText = createMemo(() => formatTG(props.metrics.generation, language.locale())) const label = createMemo(() => - props.metrics.source === "provider" - ? language.t("chat.throughput.badge.provider", { pp: ppText(), tg: tgText() }) - : language.t("chat.throughput.badge.computed", { tg: tgText() }), + language.t("chat.throughput.badge.computed", { tg: tgText() }), ) const tooltip = createMemo(() => { - const prompt = props.metrics.prompt - const gen = props.metrics.generation - if (props.metrics.source === "provider" && prompt !== undefined && gen !== undefined) { - return language.t("chat.throughput.badge.tooltip.provider", { - pp: formatter().format(prompt), - tg: formatter().format(gen), + if (props.metrics.generation !== undefined) { + return language.t("chat.throughput.badge.tooltip.computed", { + tg: formatTG(props.metrics.generation, language.locale()), }) } - if (gen !== undefined) { - return language.t("chat.throughput.badge.tooltip.computed", { tg: formatter().format(gen) }) - } return language.t("chat.throughput.badge.tooltip.missing") }) return ( - + {label()} @@ -227,21 +199,14 @@ export const AssistantMessage: Component = (props) => { const display = useDisplay() const mem = useMemory() const language = useLanguage() - const vscode = useVSCode() const { config } = useConfig() const open = createMemo(() => config().terminal_command_display !== "collapsed") const edit = createMemo(() => config().code_edit_display === "expanded") - // Per-message throughput toggle. Mirrors the showTaskTimeline pattern: - // request once on mount and react to the extension's reply. - const [throughputVisible, setThroughputVisible] = createSignal(false) - onMount(() => vscode.postMessage({ type: "requestThroughputSetting" })) - const handler = (e: MessageEvent) => { - const data = e.data as { type?: string; visible?: boolean } - if (data?.type === "throughputSettingLoaded") setThroughputVisible(Boolean(data.visible)) - } - window.addEventListener("message", handler) - onCleanup(() => window.removeEventListener("message", handler)) + // Throughput toggle lives on the shared DisplayProvider so every + // AssistantMessage renders against the same signal without posting its + // own requestThroughputSetting round-trip on mount. + const throughputVisible = createMemo(() => display.throughputVisible()) const parts = createMemo(() => { const stored = props.parts ?? data.store.part?.[props.message.id] diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/TaskHeader.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/TaskHeader.tsx index 1f334c2fffc..4269aaa61e0 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/TaskHeader.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/TaskHeader.tsx @@ -15,6 +15,7 @@ import { Icon } from "@kilocode/kilo-ui/icon" import { Checkbox } from "@kilocode/kilo-ui/checkbox" import { useSession } from "../../context/session" import { useMemory } from "../../context/memory" +import { useDisplay } from "../../context/display" import { calcTokenUsage, collapseCostBreakdown, latestMetrics } from "../../context/session-utils" import { useLanguage } from "../../context/language" import { useVSCode } from "../../context/vscode" @@ -36,6 +37,7 @@ export const TaskHeader: Component = (props) => { const session = useSession() const language = useLanguage() const search = useTranscriptSearch() + const display = useDisplay() const title = createMemo(() => session.currentSession()?.title ?? language.t("command.session.new")) const canRename = createMemo(() => !props.readonly && !!session.currentSession()) @@ -171,6 +173,9 @@ export const TaskHeader: Component = (props) => { ) const vscode = useVSCode() const [expanded, setExpanded] = createSignal(true) + // Throughput row visibility is shared with AssistantMessage via the + // DisplayProvider so the setting toggles both surfaces together. + const throughputVisible = createMemo(() => display.throughputVisible()) // Read initial value from VS Code settings onMount(() => vscode.postMessage({ type: "requestTimelineSetting" })) @@ -371,13 +376,11 @@ export const TaskHeader: Component = (props) => { {(tk) => } - + {(t) => (
- {`PP ${t().pp}`} - · - {`TG ${t().tg}`} + {t().label}
)} diff --git a/packages/kilo-vscode/webview-ui/src/context/display.tsx b/packages/kilo-vscode/webview-ui/src/context/display.tsx index a5741b9cd98..cc1ad2f8357 100644 --- a/packages/kilo-vscode/webview-ui/src/context/display.tsx +++ b/packages/kilo-vscode/webview-ui/src/context/display.tsx @@ -4,6 +4,7 @@ import { createMemo, createSignal, onCleanup, + onMount, useContext, type Accessor, type ParentComponent, @@ -18,6 +19,10 @@ interface DisplayContextValue { setReasoningAutoCollapse: (collapse: boolean) => void fontSize: Accessor setFontSize: (size: number) => void + // Shared throughput toggle — the same signal backs the per-message badge in + // every AssistantMessage and the aggregated row in TaskHeader, so flipping + // the setting once updates both surfaces without round-trips. + throughputVisible: Accessor } export const DisplayContext = createContext() @@ -27,10 +32,16 @@ export const DisplayProvider: ParentComponent = (props) => { const vscode = useVSCode() const reasoningAutoCollapse = createMemo(() => config().auto_collapse_reasoning ?? false) const [fontSize, setFontSizeSignal] = createSignal(readFontSize()) + const [throughputVisible, setThroughputVisible] = createSignal(false) + + // Request the throughput toggle once on mount; the extension posts back + // (and onDidChangeConfiguration forwards subsequent edits). + onMount(() => vscode.postMessage({ type: "requestThroughputSetting" })) const unsubscribe = vscode.onMessage((message: ExtensionMessage) => { if (message.type === "ready" && message.fontSize !== undefined) setFontSizeSignal(clampFontSize(message.fontSize)) if (message.type === "fontSizeChanged") setFontSizeSignal(clampFontSize(message.fontSize)) + if (message.type === "throughputSettingLoaded") setThroughputVisible(Boolean(message.visible)) }) createEffect(() => { @@ -50,6 +61,7 @@ export const DisplayProvider: ParentComponent = (props) => { setFontSizeSignal(next) vscode.postMessage({ type: "updateSetting", key: "fontSize", value: next }) }, + throughputVisible, }} > {props.children} diff --git a/packages/kilo-vscode/webview-ui/src/context/session-utils.ts b/packages/kilo-vscode/webview-ui/src/context/session-utils.ts index c002d068dbc..f8649b286b3 100644 --- a/packages/kilo-vscode/webview-ui/src/context/session-utils.ts +++ b/packages/kilo-vscode/webview-ui/src/context/session-utils.ts @@ -208,43 +208,65 @@ export function calcTokenUsage( * Aggregate tokens-per-second throughput across the step-finish parts of a * session. * - * Strategy: "last wins". The most recent step-finish with a `metrics` block - * is treated as the current throughput snapshot — that's the same number the - * per-message badge surfaces, so the aggregated header and per-message rows - * always agree. Provider-reported (llama.cpp / Ollama) values win over - * computed values when both appear in the same session, because provider - * timings reflect the model's actual rate rather than wall-clock duration. + * Combines every step-finish that carries a `metrics` block so the header + * reflects all of the assistant's reasoning + answer steps, not just the last + * one. Each field adopts the first non-empty sample we see — that gives a + * stable "snapshot" view of the session pace that won't double-count when + * later steps report zero on a particular field. * * Returns `undefined` when no step-finish part in the input carries metrics, * which is the signal callers use to hide the throughput UI. */ export function aggregateMetrics( parts: readonly Part[], -): { prompt?: number; generation?: number; source: "provider" | "computed" } | undefined { - let best: { prompt?: number; generation?: number; source: "provider" | "computed" } | undefined +): { prompt?: number; generation?: number; source: "computed" } | undefined { + let prompt: number | undefined + let generation: number | undefined for (const part of parts) { if (part.type !== "step-finish") continue const metrics = part.metrics if (!metrics) continue - // Provider-reported metrics outrank computed ones even if they appear - // later in the timeline. - if (metrics.source === "computed" && best?.source === "provider") continue - best = metrics + if (metrics.prompt !== undefined && prompt === undefined) prompt = metrics.prompt + if (metrics.generation !== undefined && generation === undefined) generation = metrics.generation + } + if (prompt === undefined && generation === undefined) return undefined + return { + ...(prompt !== undefined ? { prompt } : {}), + ...(generation !== undefined ? { generation } : {}), + source: "computed", } - return best } /** * Pick the throughput snapshot from a single assistant message's parts. - * Uses the same last-wins strategy as `aggregateMetrics` so the per-message - * badge and the header row stay consistent. + * Same aggregation strategy as `aggregateMetrics` so the per-message badge + * and the header row stay consistent. */ export function messageMetrics( parts: readonly Part[], -): { prompt?: number; generation?: number; source: "provider" | "computed" } | undefined { +): { prompt?: number; generation?: number; source: "computed" } | undefined { return aggregateMetrics(parts) } +/** + * Format a throughput rate (prompt or generation) for display. Shared by + * every rendering site so the same value reads the same in the per-message + * badge and the aggregated header row. + */ +function formatRateValue(value: number | undefined, locale: string): string { + if (!Number.isFinite(value) || value === undefined || value <= 0) return "–" + return `${new Intl.NumberFormat(locale, { maximumFractionDigits: 1 }).format(value)} t/s` +} + +// Convenience labels: PP for prompt, TG for generation. Both delegate to the +// shared formatter — the label is owned by the rendering site, not by us. +export function formatPP(value: number | undefined, locale: string) { + return formatRateValue(value, locale) +} +export function formatTG(value: number | undefined, locale: string) { + return formatRateValue(value, locale) +} + /** * Build a map of session ID → **own cost** for each session in the family * that has non-zero own cost. diff --git a/packages/kilo-vscode/webview-ui/src/styles/chat-layout.css b/packages/kilo-vscode/webview-ui/src/styles/chat-layout.css index 123ca916800..559fe9507b1 100644 --- a/packages/kilo-vscode/webview-ui/src/styles/chat-layout.css +++ b/packages/kilo-vscode/webview-ui/src/styles/chat-layout.css @@ -228,6 +228,26 @@ width: 100%; } +[data-component="assistant-memory-badge"] { + align-self: flex-start; + color: var(--vscode-descriptionForeground); + font-family: var(--font-family-sans); + font-size: var(--font-size-small); + line-height: var(--line-height-normal); +} + +[data-component="assistant-throughput-badge"] { + align-self: flex-start; + display: inline-flex; + align-items: center; + gap: 4px; + color: var(--vscode-descriptionForeground); + font-family: var(--font-family-sans); + font-size: var(--font-size-small); + line-height: var(--line-height-normal); + white-space: nowrap; + font-variant-numeric: tabular-nums; +} .vscode-session-turn-diffs { width: 100%; } diff --git a/packages/opencode/src/kilocode/cli/cmd/tui/routes/session/usage.tsx b/packages/opencode/src/kilocode/cli/cmd/tui/routes/session/usage.tsx deleted file mode 100644 index bc6efd7ace3..00000000000 --- a/packages/opencode/src/kilocode/cli/cmd/tui/routes/session/usage.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import type { RGBA } from "@opentui/core" -import { createMemo, createSignal, onCleanup, onMount, Show } from "solid-js" -import { UsageRow } from "@/kilocode/plugins/sidebar-usage-row" -import { - aggregateMetrics, - formatPP, - formatTG, - hasMetrics, - type StepMetrics, -} from "@/kilocode/plugins/model-usage" - -export namespace SessionUsagePanel { - export type Theme = { - text: RGBA - textMuted: RGBA - } - - export type EventPayload = { - sessionID?: string - part?: { type?: string; metrics?: unknown; tokens?: unknown } - } - - export type Deps = { - sessionID: string - theme: Theme - onPartUpdated?: (handler: (sessionID: string, part: EventPayload["part"]) => void) => () => void - } - - type Sample = { metrics?: StepMetrics; generated: number } - - function isMetrics(value: unknown): value is StepMetrics { - if (!value || typeof value !== "object") return false - const source = (value as Record).source - return source === "provider" || source === "computed" - } - - function generated(value: unknown): number { - if (!value || typeof value !== "object") return 0 - const record = value as Record - const output = typeof record.output === "number" ? record.output : 0 - const reasoning = typeof record.reasoning === "number" ? record.reasoning : 0 - return output + reasoning - } - - /** - * Subscribes to step-finish events for the given session and renders the - * aggregated prompt/tokens-per-second rows when at least one step has - * reported metrics. The row hides opportunistically for providers that - * never surface timing metadata (Anthropic, OpenAI, Gemini). - */ - export function View(props: Deps) { - const [samples, setSamples] = createSignal([]) - const throughput = createMemo(() => aggregateMetrics(samples())) - - onMount(() => { - if (!props.onPartUpdated) return - const off = props.onPartUpdated((sessionID, part) => { - if (sessionID !== props.sessionID) return - if (part?.type !== "step-finish") return - const metrics = isMetrics(part.metrics) ? part.metrics : undefined - const weight = generated(part.tokens) - setSamples((current) => [...current, { ...(metrics ? { metrics } : {}), generated: weight }]) - }) - onCleanup(() => off()) - }) - - return ( - - - - - ) - } -} \ No newline at end of file diff --git a/packages/opencode/src/kilocode/session/metrics.ts b/packages/opencode/src/kilocode/session/metrics.ts index 6b9a0b58f55..db8ddea553f 100644 --- a/packages/opencode/src/kilocode/session/metrics.ts +++ b/packages/opencode/src/kilocode/session/metrics.ts @@ -1,10 +1,17 @@ // kilocode_change - new file -import { isRecord } from "@/util/record" - +// Wire shape mirrors the SDK schema (packages/sdk/js/src/v2/gen/types.gen.ts +// StepFinishPart.metrics). `source` stays on the wire for backward +// compatibility with downstream consumers — see packages/kilo-vscode/ +// webview-ui/src/context/session-utils.ts and AssistantMessage.tsx — +// but only the "computed" literal is reachable here because llama.cpp's +// `prompt_per_second` / `predicted_per_second` are dropped upstream by +// `@ai-sdk/openai-compatible` before the raw usage reaches our adapter. +// Follow-up: wire `metadataExtractor` into the shared +// `createOpenAICompatible` call so the provider source is reachable again. export type TokenRates = { prompt?: number generation?: number - source: "provider" | "computed" + source: "computed" } export type ComputeInput = { @@ -18,41 +25,8 @@ export type ComputeInput = { elapsedMs: number } -// kilocode_change start - tokens/second through-putation for #6579. -const safe = (value: unknown): number | undefined => { - if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return undefined - return value -} - -// llama.cpp surfaces timing under provider-specific metadata keys. We look at -// the most common shapes; providers without timing fields return undefined. -const providerRate = (metadata: unknown, key: string): number | undefined => { - if (!isRecord(metadata)) return undefined - for (const namespace of Object.values(metadata)) { - if (!isRecord(namespace)) continue - for (const [name, value] of Object.entries(namespace)) { - if (name.toLowerCase() === key.toLowerCase()) { - return safe(value) - } - } - } - return undefined -} - +// kilocode_change start - tokens/second throughput for #6579. export function computeMetrics(input: ComputeInput): TokenRates | undefined { - const providerPrompt = providerRate(input.providerMetadata, "prompt_per_second") - const providerGeneration = providerRate( - input.providerMetadata, - "predicted_per_second", - ) - - if (providerPrompt !== undefined || providerGeneration !== undefined) { - const result: TokenRates = { source: "provider" } - if (providerPrompt !== undefined) result.prompt = providerPrompt - if (providerGeneration !== undefined) result.generation = providerGeneration - return result - } - if (!Number.isFinite(input.elapsedMs) || input.elapsedMs <= 0) return undefined const generated = input.tokens.output + input.tokens.reasoning @@ -70,4 +44,4 @@ export function formatRate(value: number): string { if (!Number.isFinite(value) || value <= 0) return "0 t/s" return `${numberFormat.format(value)} t/s` } -// kilocode_change end +// kilocode_change end \ No newline at end of file diff --git a/packages/opencode/test/kilocode/session-metrics.test.ts b/packages/opencode/test/kilocode/session-metrics.test.ts index d13b792870c..254ee33d9dc 100644 --- a/packages/opencode/test/kilocode/session-metrics.test.ts +++ b/packages/opencode/test/kilocode/session-metrics.test.ts @@ -10,22 +10,8 @@ const tokens = { } describe("kilocode.session.metrics.computeMetrics", () => { - test("preserves llama.cpp provider-reported timings", () => { + test("derives generation rate from elapsed time", () => { const metrics = computeMetrics({ - providerMetadata: { - llama: { prompt_per_second: 412.3, predicted_per_second: 28.7 }, - }, - tokens, - elapsedMs: 1000, - }) - expect(metrics?.source).toBe("provider") - expect(metrics?.prompt).toBeCloseTo(412.3) - expect(metrics?.generation).toBeCloseTo(28.7) - }) - - test("derives generation rate when no provider timing is present", () => { - const metrics = computeMetrics({ - providerMetadata: { openai: { finish_reason: "stop" } }, tokens: { ...tokens, output: 100 }, elapsedMs: 1000, }) @@ -50,16 +36,21 @@ describe("kilocode.session.metrics.computeMetrics", () => { expect(metrics).toBeUndefined() }) - test("falls back to computed when provider rates are bogus", () => { + test("ignores providerMetadata until the upstream wiring lands (see #6579)", () => { + // llama.cpp surfaces prompt_per_second / predicted_per_second, but the + // upstream AI SDK drops them before the raw usage reaches our adapter. + // Until a metadataExtractor is wired into createOpenAICompatible, the + // provider source is unreachable — exercise the tolerance here. const metrics = computeMetrics({ providerMetadata: { - llama: { prompt_per_second: -5, predicted_per_second: Number.POSITIVE_INFINITY }, + llama: { prompt_per_second: 412.3, predicted_per_second: 28.7 }, }, - tokens, - elapsedMs: 1000, + tokens: { ...tokens, output: 100 }, + elapsedMs: 2000, }) expect(metrics?.source).toBe("computed") expect(metrics?.generation).toBeCloseTo(50) + expect(metrics?.prompt).toBeUndefined() }) test("tolerates missing providerMetadata", () => { @@ -69,6 +60,7 @@ describe("kilocode.session.metrics.computeMetrics", () => { }) expect(metrics?.source).toBe("computed") expect(metrics?.generation).toBeCloseTo(50) + expect(metrics?.prompt).toBeUndefined() }) }) @@ -85,4 +77,4 @@ describe("kilocode.session.metrics.formatRate", () => { test("returns zero string for negative inputs", () => { expect(formatRate(-5)).toBe("0 t/s") }) -}) +}) \ No newline at end of file From e34aeccc91c156e65578e14e40e56dab6b34eefc Mon Sep 17 00:00:00 2001 From: Thomas Brugman Date: Tue, 21 Jul 2026 17:56:30 +0200 Subject: [PATCH 05/18] fix(token-throughput-v2): seed settings on hydration, drop dead provider branch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Seed the DisplayTab "Show Token Throughput" Switch on initial load by mirroring the throughputSettingLoaded message into settings() (same pattern as chat.shiftTabCyclesVariant). Without this, a persisted-true setting renders unchecked on first open because the Switch was bound to settings()["showTokenThroughput"] but no handler ever populated it. Drop the dead data-source attributes on the per-message and task-header throughput surfaces now that StepThroughputMetrics.source is narrowed to "computed" only — the attribute was always the literal string. Drop the unreachable chat.throughput.badge.provider and .chat.throughput.badge.tooltip.provider i18n keys across all 20 locales. The badges no longer branch on source === "provider" since the provider-source branch is removed (the AI SDK adapter upstream strips llama.cpp timings before they reach providerMetadata). Co-Authored-By: Claude Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com> --- .../src/components/chat/AssistantMessage.tsx | 2 +- .../webview-ui/src/components/chat/TaskHeader.tsx | 2 +- packages/kilo-vscode/webview-ui/src/context/config.tsx | 10 ++++++++++ packages/kilo-vscode/webview-ui/src/i18n/ar.ts | 2 -- packages/kilo-vscode/webview-ui/src/i18n/br.ts | 2 -- packages/kilo-vscode/webview-ui/src/i18n/bs.ts | 2 -- packages/kilo-vscode/webview-ui/src/i18n/da.ts | 2 -- packages/kilo-vscode/webview-ui/src/i18n/de.ts | 2 -- packages/kilo-vscode/webview-ui/src/i18n/en.ts | 2 -- packages/kilo-vscode/webview-ui/src/i18n/es.ts | 2 -- packages/kilo-vscode/webview-ui/src/i18n/fr.ts | 2 -- packages/kilo-vscode/webview-ui/src/i18n/it.ts | 2 -- packages/kilo-vscode/webview-ui/src/i18n/ja.ts | 2 -- packages/kilo-vscode/webview-ui/src/i18n/ko.ts | 2 -- packages/kilo-vscode/webview-ui/src/i18n/nl.ts | 2 -- packages/kilo-vscode/webview-ui/src/i18n/no.ts | 2 -- packages/kilo-vscode/webview-ui/src/i18n/pl.ts | 2 -- packages/kilo-vscode/webview-ui/src/i18n/ru.ts | 2 -- packages/kilo-vscode/webview-ui/src/i18n/th.ts | 2 -- packages/kilo-vscode/webview-ui/src/i18n/tr.ts | 2 -- packages/kilo-vscode/webview-ui/src/i18n/uk.ts | 2 -- packages/kilo-vscode/webview-ui/src/i18n/zh.ts | 2 -- packages/kilo-vscode/webview-ui/src/i18n/zht.ts | 2 -- .../kilo-vscode/webview-ui/src/styles/chat-layout.css | 2 ++ packages/opencode/src/kilocode/session/metrics.ts | 2 +- 25 files changed, 15 insertions(+), 43 deletions(-) diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/AssistantMessage.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/AssistantMessage.tsx index aec94f8f4a6..81aa5aa477a 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/AssistantMessage.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/AssistantMessage.tsx @@ -186,7 +186,7 @@ function ThroughputBadge(props: { metrics: NonNullable - + {label()} diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/TaskHeader.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/TaskHeader.tsx index 4269aaa61e0..25d3a8dc589 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/TaskHeader.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/TaskHeader.tsx @@ -379,7 +379,7 @@ export const TaskHeader: Component = (props) => { {(t) => ( -
+
{t().label}
diff --git a/packages/kilo-vscode/webview-ui/src/context/config.tsx b/packages/kilo-vscode/webview-ui/src/context/config.tsx index a824dd1a57d..81fd046cc62 100644 --- a/packages/kilo-vscode/webview-ui/src/context/config.tsx +++ b/packages/kilo-vscode/webview-ui/src/context/config.tsx @@ -108,6 +108,16 @@ export const ConfigProvider: ParentComponent = (props) => { }) return } + if (message.type === "throughputSettingLoaded") { + // Seed settings() so the DisplayTab Switch reflects persisted state on + // first open. DisplayProvider also reads this message to drive + // throughputVisible() for the per-message badge; both signals update + // from the same backend message without conflict. + mergeSettings({ + showTokenThroughput: message.visible, + }) + return + } if (message.type === "configLoaded") { // Skip if a save is in-flight — a stale configLoaded must not overwrite // the optimistically-updated state while the write is being confirmed. diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts index d5fffb6a12e..abdfcb8c5b9 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts @@ -1684,9 +1684,7 @@ export const dict = { "settings.display.tokenThroughput.description": "Display tokens-per-second (prompt-processing / text-generation) on each assistant message and in the task header. Hidden by default to keep the chat uncluttered.", - "chat.throughput.badge.provider": "PP {{pp}} · TG {{tg}}", "chat.throughput.badge.computed": "PP – · TG {{tg}}", - "chat.throughput.badge.tooltip.provider": "Prompt-processing {{pp}} t/s · text-generation {{tg}} t/s (reported by provider)", "chat.throughput.badge.tooltip.computed": "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", "chat.throughput.badge.tooltip.missing": "Throughput metrics unavailable for this step", "settings.providers.defaultModel.title": "النموذج الافتراضي", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/br.ts b/packages/kilo-vscode/webview-ui/src/i18n/br.ts index 2ff6ae1ba4b..ba76137594b 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/br.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/br.ts @@ -1735,9 +1735,7 @@ export const dict = { "settings.display.tokenThroughput.description": "Display tokens-per-second (prompt-processing / text-generation) on each assistant message and in the task header. Hidden by default to keep the chat uncluttered.", - "chat.throughput.badge.provider": "PP {{pp}} · TG {{tg}}", "chat.throughput.badge.computed": "PP – · TG {{tg}}", - "chat.throughput.badge.tooltip.provider": "Prompt-processing {{pp}} t/s · text-generation {{tg}} t/s (reported by provider)", "chat.throughput.badge.tooltip.computed": "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", "chat.throughput.badge.tooltip.missing": "Throughput metrics unavailable for this step", "settings.providers.defaultModel.title": "Modelo padrão", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/bs.ts b/packages/kilo-vscode/webview-ui/src/i18n/bs.ts index 81bcc33683b..e33a63594d1 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/bs.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/bs.ts @@ -1727,9 +1727,7 @@ export const dict = { "settings.display.tokenThroughput.description": "Display tokens-per-second (prompt-processing / text-generation) on each assistant message and in the task header. Hidden by default to keep the chat uncluttered.", - "chat.throughput.badge.provider": "PP {{pp}} · TG {{tg}}", "chat.throughput.badge.computed": "PP – · TG {{tg}}", - "chat.throughput.badge.tooltip.provider": "Prompt-processing {{pp}} t/s · text-generation {{tg}} t/s (reported by provider)", "chat.throughput.badge.tooltip.computed": "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", "chat.throughput.badge.tooltip.missing": "Throughput metrics unavailable for this step", "settings.providers.defaultModel.title": "Zadani model", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/da.ts b/packages/kilo-vscode/webview-ui/src/i18n/da.ts index c52c4d62a99..9d393b65bf3 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/da.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/da.ts @@ -1718,9 +1718,7 @@ export const dict = { "settings.display.tokenThroughput.description": "Display tokens-per-second (prompt-processing / text-generation) on each assistant message and in the task header. Hidden by default to keep the chat uncluttered.", - "chat.throughput.badge.provider": "PP {{pp}} · TG {{tg}}", "chat.throughput.badge.computed": "PP – · TG {{tg}}", - "chat.throughput.badge.tooltip.provider": "Prompt-processing {{pp}} t/s · text-generation {{tg}} t/s (reported by provider)", "chat.throughput.badge.tooltip.computed": "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", "chat.throughput.badge.tooltip.missing": "Throughput metrics unavailable for this step", "settings.providers.defaultModel.title": "Standardmodel", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/de.ts b/packages/kilo-vscode/webview-ui/src/i18n/de.ts index 3c93878a6c0..1284ca47b45 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/de.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/de.ts @@ -1754,9 +1754,7 @@ export const dict = { "settings.display.tokenThroughput.description": "Display tokens-per-second (prompt-processing / text-generation) on each assistant message and in the task header. Hidden by default to keep the chat uncluttered.", - "chat.throughput.badge.provider": "PP {{pp}} · TG {{tg}}", "chat.throughput.badge.computed": "PP – · TG {{tg}}", - "chat.throughput.badge.tooltip.provider": "Prompt-processing {{pp}} t/s · text-generation {{tg}} t/s (reported by provider)", "chat.throughput.badge.tooltip.computed": "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", "chat.throughput.badge.tooltip.missing": "Throughput metrics unavailable for this step", "settings.providers.defaultModel.title": "Standardmodell", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/en.ts b/packages/kilo-vscode/webview-ui/src/i18n/en.ts index 8bda9de0a1c..48113bd2d72 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/en.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/en.ts @@ -1695,9 +1695,7 @@ export const dict = { "settings.display.tokenThroughput.description": "Display tokens-per-second (prompt-processing / text-generation) on each assistant message and in the task header. Hidden by default to keep the chat uncluttered.", - "chat.throughput.badge.provider": "PP {{pp}} · TG {{tg}}", "chat.throughput.badge.computed": "PP – · TG {{tg}}", - "chat.throughput.badge.tooltip.provider": "Prompt-processing {{pp}} t/s · text-generation {{tg}} t/s (reported by provider)", "chat.throughput.badge.tooltip.computed": "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", "chat.throughput.badge.tooltip.missing": "Throughput metrics unavailable for this step", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/es.ts b/packages/kilo-vscode/webview-ui/src/i18n/es.ts index bbe161f554a..3b3dd0c27e6 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/es.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/es.ts @@ -1743,9 +1743,7 @@ export const dict = { "settings.display.tokenThroughput.description": "Display tokens-per-second (prompt-processing / text-generation) on each assistant message and in the task header. Hidden by default to keep the chat uncluttered.", - "chat.throughput.badge.provider": "PP {{pp}} · TG {{tg}}", "chat.throughput.badge.computed": "PP – · TG {{tg}}", - "chat.throughput.badge.tooltip.provider": "Prompt-processing {{pp}} t/s · text-generation {{tg}} t/s (reported by provider)", "chat.throughput.badge.tooltip.computed": "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", "chat.throughput.badge.tooltip.missing": "Throughput metrics unavailable for this step", "settings.providers.defaultModel.title": "Modelo predeterminado", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts index 60364f5d1f0..b309483f195 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts @@ -1764,9 +1764,7 @@ export const dict = { "settings.display.tokenThroughput.description": "Display tokens-per-second (prompt-processing / text-generation) on each assistant message and in the task header. Hidden by default to keep the chat uncluttered.", - "chat.throughput.badge.provider": "PP {{pp}} · TG {{tg}}", "chat.throughput.badge.computed": "PP – · TG {{tg}}", - "chat.throughput.badge.tooltip.provider": "Prompt-processing {{pp}} t/s · text-generation {{tg}} t/s (reported by provider)", "chat.throughput.badge.tooltip.computed": "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", "chat.throughput.badge.tooltip.missing": "Throughput metrics unavailable for this step", "settings.providers.defaultModel.title": "Modèle par défaut", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/it.ts b/packages/kilo-vscode/webview-ui/src/i18n/it.ts index 4607cc34436..9ffce52f841 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/it.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/it.ts @@ -1521,9 +1521,7 @@ export const dict = { "settings.display.tokenThroughput.description": "Display tokens-per-second (prompt-processing / text-generation) on each assistant message and in the task header. Hidden by default to keep the chat uncluttered.", - "chat.throughput.badge.provider": "PP {{pp}} · TG {{tg}}", "chat.throughput.badge.computed": "PP – · TG {{tg}}", - "chat.throughput.badge.tooltip.provider": "Prompt-processing {{pp}} t/s · text-generation {{tg}} t/s (reported by provider)", "chat.throughput.badge.tooltip.computed": "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", "chat.throughput.badge.tooltip.missing": "Throughput metrics unavailable for this step", "settings.providers.defaultModel.title": "Modello predefinito", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts index e92ab2784d7..78d9d3eccc5 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts @@ -1713,9 +1713,7 @@ export const dict = { "settings.display.tokenThroughput.description": "Display tokens-per-second (prompt-processing / text-generation) on each assistant message and in the task header. Hidden by default to keep the chat uncluttered.", - "chat.throughput.badge.provider": "PP {{pp}} · TG {{tg}}", "chat.throughput.badge.computed": "PP – · TG {{tg}}", - "chat.throughput.badge.tooltip.provider": "Prompt-processing {{pp}} t/s · text-generation {{tg}} t/s (reported by provider)", "chat.throughput.badge.tooltip.computed": "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", "chat.throughput.badge.tooltip.missing": "Throughput metrics unavailable for this step", "settings.providers.defaultModel.title": "デフォルトモデル", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts index 57d25b52eaf..72615043f95 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts @@ -1695,9 +1695,7 @@ export const dict = { "settings.display.tokenThroughput.description": "Display tokens-per-second (prompt-processing / text-generation) on each assistant message and in the task header. Hidden by default to keep the chat uncluttered.", - "chat.throughput.badge.provider": "PP {{pp}} · TG {{tg}}", "chat.throughput.badge.computed": "PP – · TG {{tg}}", - "chat.throughput.badge.tooltip.provider": "Prompt-processing {{pp}} t/s · text-generation {{tg}} t/s (reported by provider)", "chat.throughput.badge.tooltip.computed": "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", "chat.throughput.badge.tooltip.missing": "Throughput metrics unavailable for this step", "settings.providers.defaultModel.title": "기본 모델", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/nl.ts b/packages/kilo-vscode/webview-ui/src/i18n/nl.ts index 283bcc6bcb4..0e46319eef5 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/nl.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/nl.ts @@ -1695,9 +1695,7 @@ export const dict = { "settings.display.tokenThroughput.description": "Display tokens-per-second (prompt-processing / text-generation) on each assistant message and in the task header. Hidden by default to keep the chat uncluttered.", - "chat.throughput.badge.provider": "PP {{pp}} · TG {{tg}}", "chat.throughput.badge.computed": "PP – · TG {{tg}}", - "chat.throughput.badge.tooltip.provider": "Prompt-processing {{pp}} t/s · text-generation {{tg}} t/s (reported by provider)", "chat.throughput.badge.tooltip.computed": "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", "chat.throughput.badge.tooltip.missing": "Throughput metrics unavailable for this step", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/no.ts b/packages/kilo-vscode/webview-ui/src/i18n/no.ts index 3d712387678..28f3df54305 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/no.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/no.ts @@ -1716,9 +1716,7 @@ export const dict = { "settings.display.tokenThroughput.description": "Display tokens-per-second (prompt-processing / text-generation) on each assistant message and in the task header. Hidden by default to keep the chat uncluttered.", - "chat.throughput.badge.provider": "PP {{pp}} · TG {{tg}}", "chat.throughput.badge.computed": "PP – · TG {{tg}}", - "chat.throughput.badge.tooltip.provider": "Prompt-processing {{pp}} t/s · text-generation {{tg}} t/s (reported by provider)", "chat.throughput.badge.tooltip.computed": "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", "chat.throughput.badge.tooltip.missing": "Throughput metrics unavailable for this step", "settings.providers.defaultModel.title": "Standardmodell", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts index 715a08db35f..1427a7d5f58 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts @@ -1726,9 +1726,7 @@ export const dict = { "settings.display.tokenThroughput.description": "Display tokens-per-second (prompt-processing / text-generation) on each assistant message and in the task header. Hidden by default to keep the chat uncluttered.", - "chat.throughput.badge.provider": "PP {{pp}} · TG {{tg}}", "chat.throughput.badge.computed": "PP – · TG {{tg}}", - "chat.throughput.badge.tooltip.provider": "Prompt-processing {{pp}} t/s · text-generation {{tg}} t/s (reported by provider)", "chat.throughput.badge.tooltip.computed": "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", "chat.throughput.badge.tooltip.missing": "Throughput metrics unavailable for this step", "settings.providers.defaultModel.title": "Domyślny model", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts index cdb08838f67..ff3a609e122 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts @@ -1724,9 +1724,7 @@ export const dict = { "settings.display.tokenThroughput.description": "Display tokens-per-second (prompt-processing / text-generation) on each assistant message and in the task header. Hidden by default to keep the chat uncluttered.", - "chat.throughput.badge.provider": "PP {{pp}} · TG {{tg}}", "chat.throughput.badge.computed": "PP – · TG {{tg}}", - "chat.throughput.badge.tooltip.provider": "Prompt-processing {{pp}} t/s · text-generation {{tg}} t/s (reported by provider)", "chat.throughput.badge.tooltip.computed": "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", "chat.throughput.badge.tooltip.missing": "Throughput metrics unavailable for this step", "settings.providers.defaultModel.title": "Модель по умолчанию", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/th.ts b/packages/kilo-vscode/webview-ui/src/i18n/th.ts index eb60b5d80a1..72500cef248 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/th.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/th.ts @@ -1693,9 +1693,7 @@ export const dict = { "settings.display.tokenThroughput.description": "Display tokens-per-second (prompt-processing / text-generation) on each assistant message and in the task header. Hidden by default to keep the chat uncluttered.", - "chat.throughput.badge.provider": "PP {{pp}} · TG {{tg}}", "chat.throughput.badge.computed": "PP – · TG {{tg}}", - "chat.throughput.badge.tooltip.provider": "Prompt-processing {{pp}} t/s · text-generation {{tg}} t/s (reported by provider)", "chat.throughput.badge.tooltip.computed": "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", "chat.throughput.badge.tooltip.missing": "Throughput metrics unavailable for this step", "settings.providers.defaultModel.title": "โมเดลเริ่มต้น", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/tr.ts b/packages/kilo-vscode/webview-ui/src/i18n/tr.ts index cf5350890d9..03e01568c54 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/tr.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/tr.ts @@ -1682,9 +1682,7 @@ export const dict = { "settings.display.tokenThroughput.description": "Display tokens-per-second (prompt-processing / text-generation) on each assistant message and in the task header. Hidden by default to keep the chat uncluttered.", - "chat.throughput.badge.provider": "PP {{pp}} · TG {{tg}}", "chat.throughput.badge.computed": "PP – · TG {{tg}}", - "chat.throughput.badge.tooltip.provider": "Prompt-processing {{pp}} t/s · text-generation {{tg}} t/s (reported by provider)", "chat.throughput.badge.tooltip.computed": "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", "chat.throughput.badge.tooltip.missing": "Throughput metrics unavailable for this step", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/uk.ts b/packages/kilo-vscode/webview-ui/src/i18n/uk.ts index 0b227031235..adb97f694ab 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/uk.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/uk.ts @@ -1679,9 +1679,7 @@ export const dict = { "settings.display.tokenThroughput.description": "Display tokens-per-second (prompt-processing / text-generation) on each assistant message and in the task header. Hidden by default to keep the chat uncluttered.", - "chat.throughput.badge.provider": "PP {{pp}} · TG {{tg}}", "chat.throughput.badge.computed": "PP – · TG {{tg}}", - "chat.throughput.badge.tooltip.provider": "Prompt-processing {{pp}} t/s · text-generation {{tg}} t/s (reported by provider)", "chat.throughput.badge.tooltip.computed": "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", "chat.throughput.badge.tooltip.missing": "Throughput metrics unavailable for this step", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts index 2f289d5a545..0b6916118fa 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts @@ -1645,9 +1645,7 @@ export const dict = { "settings.display.tokenThroughput.description": "Display tokens-per-second (prompt-processing / text-generation) on each assistant message and in the task header. Hidden by default to keep the chat uncluttered.", - "chat.throughput.badge.provider": "PP {{pp}} · TG {{tg}}", "chat.throughput.badge.computed": "PP – · TG {{tg}}", - "chat.throughput.badge.tooltip.provider": "Prompt-processing {{pp}} t/s · text-generation {{tg}} t/s (reported by provider)", "chat.throughput.badge.tooltip.computed": "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", "chat.throughput.badge.tooltip.missing": "Throughput metrics unavailable for this step", "settings.providers.defaultModel.title": "默认模型", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts index ae33cf17dc9..d81cd58743e 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts @@ -1609,9 +1609,7 @@ export const dict = { "settings.display.tokenThroughput.description": "Display tokens-per-second (prompt-processing / text-generation) on each assistant message and in the task header. Hidden by default to keep the chat uncluttered.", - "chat.throughput.badge.provider": "PP {{pp}} · TG {{tg}}", "chat.throughput.badge.computed": "PP – · TG {{tg}}", - "chat.throughput.badge.tooltip.provider": "Prompt-processing {{pp}} t/s · text-generation {{tg}} t/s (reported by provider)", "chat.throughput.badge.tooltip.computed": "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", "chat.throughput.badge.tooltip.missing": "Throughput metrics unavailable for this step", "settings.providers.defaultModel.title": "預設模型", diff --git a/packages/kilo-vscode/webview-ui/src/styles/chat-layout.css b/packages/kilo-vscode/webview-ui/src/styles/chat-layout.css index 559fe9507b1..f2baa98c56c 100644 --- a/packages/kilo-vscode/webview-ui/src/styles/chat-layout.css +++ b/packages/kilo-vscode/webview-ui/src/styles/chat-layout.css @@ -248,6 +248,8 @@ white-space: nowrap; font-variant-numeric: tabular-nums; } + +.vscode-session-turn-diffs { .vscode-session-turn-diffs { width: 100%; } diff --git a/packages/opencode/src/kilocode/session/metrics.ts b/packages/opencode/src/kilocode/session/metrics.ts index db8ddea553f..e4322694f46 100644 --- a/packages/opencode/src/kilocode/session/metrics.ts +++ b/packages/opencode/src/kilocode/session/metrics.ts @@ -44,4 +44,4 @@ export function formatRate(value: number): string { if (!Number.isFinite(value) || value <= 0) return "0 t/s" return `${numberFormat.format(value)} t/s` } -// kilocode_change end \ No newline at end of file +// kilocode_change end From f5e5496b810c8eb54c091ec7dac91427d52f9499 Mon Sep 17 00:00:00 2001 From: Thomas Brugman Date: Tue, 21 Jul 2026 18:20:12 +0200 Subject: [PATCH 06/18] fix(token-throughput-v2): drop PP display until llama.cpp wiring lands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PP (prompt-processing rate) has no signal in this build: the AI SDK adapter upstream strips llama.cpp's `prompt_per_second` before it reaches providerMetadata, and computeMetrics has nothing else to derive it from. Ship the TG (text-generation) rate only — the UI no longer renders the "PP –" placeholder that made the feature look broken. CLI sidebar drops the PP row; per-message badge and aggregated header pill both lose the "PP – ·" prefix. The wire shape keeps the optional prompt field so the follow-up that wires the upstream metadataExtractor can populate it without another schema bump. The `throughputLabel` constant on the opencode side and the `formatPP` helper on the webview side are removed; tests that fabricated prompt values are pruned to match. Co-Authored-By: Claude Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com> --- .changeset/token-throughput-v2.md | 2 +- .../tests/unit/session-utils.test.ts | 33 ++++++++--------- .../webview-ui/src/context/session-utils.ts | 35 ++++++++----------- .../kilo-vscode/webview-ui/src/i18n/en.ts | 6 ++-- .../src/kilocode/plugins/model-usage.ts | 33 +++++++---------- .../src/kilocode/plugins/sidebar-usage.tsx | 1 - .../opencode/test/kilocode/tui/usage.test.ts | 24 ++----------- 7 files changed, 48 insertions(+), 86 deletions(-) diff --git a/.changeset/token-throughput-v2.md b/.changeset/token-throughput-v2.md index fe3c4f130cf..43c838de70d 100644 --- a/.changeset/token-throughput-v2.md +++ b/.changeset/token-throughput-v2.md @@ -3,4 +3,4 @@ "@kilocode/sdk": minor --- -Show tokens-per-second throughput (PP prompt-processing and TG text-generation) on each assistant message and in the usage sidebar, when the provider supplies timing data or the step took a measurable amount of time. +Show tokens-per-second text-generation throughput (TG) on each assistant message and in the usage sidebar, computed from step duration and tokens. The toggle "Show Token Throughput" in Display settings controls both surfaces. PP (prompt-processing) support lands in a follow-up once the upstream llama.cpp metadata wiring ships. diff --git a/packages/kilo-vscode/tests/unit/session-utils.test.ts b/packages/kilo-vscode/tests/unit/session-utils.test.ts index 49d731df8b2..b7b61e20f40 100644 --- a/packages/kilo-vscode/tests/unit/session-utils.test.ts +++ b/packages/kilo-vscode/tests/unit/session-utils.test.ts @@ -6,7 +6,6 @@ import { calcTokenUsage, aggregateMetrics, messageMetrics, - formatPP, formatTG, buildFamilyCosts, buildFamilyParents, @@ -748,21 +747,20 @@ describe("aggregateMetrics", () => { { type: "text", id: "t1", text: "mid" }, stepFinish("f2", { prompt: 412, generation: 38, source: "computed" }), ] - expect(aggregateMetrics(parts)).toEqual({ prompt: 100, generation: 20, source: "computed" }) + expect(aggregateMetrics(parts)).toEqual({ generation: 20, source: "computed" }) }) - it("keeps the first computed sample for each field when later steps report zero", () => { + it("keeps the first computed sample when later steps report zero", () => { const parts: Part[] = [ stepFinish("f1", { prompt: 500, generation: 50, source: "computed" }), stepFinish("f2", { generation: 30, source: "computed" }), ] const result = aggregateMetrics(parts) expect(result?.source).toBe("computed") - expect(result?.prompt).toBe(500) expect(result?.generation).toBe(50) }) - it("uses the first computed value per field when no earlier value is present", () => { + it("uses the first computed value when no earlier value is present", () => { const parts: Part[] = [ stepFinish("f1", { generation: 12, source: "computed" }), stepFinish("f2", { generation: 18, source: "computed" }), @@ -775,18 +773,18 @@ describe("aggregateMetrics", () => { { type: "text", id: "t1", text: "noise" }, stepFinish("f1", { prompt: 200, generation: 22, source: "computed" }), ] - expect(aggregateMetrics(parts)).toEqual({ prompt: 200, generation: 22, source: "computed" }) + expect(aggregateMetrics(parts)).toEqual({ generation: 22, source: "computed" }) }) - it("combines multi-step metrics into a single snapshot per message", () => { + it("merges multi-step metrics into a single snapshot per message", () => { // An assistant turn that runs reasoning + answer produces two step-finish - // parts; the badge should merge them so a prompt sample from the first - // step and a generation sample from the second coexist in the snapshot. + // parts; the badge should merge them so the generation rate from the + // final step wins. const parts: Part[] = [ - stepFinish("f1", { prompt: 200, source: "computed" }), - stepFinish("f2", { generation: 25, source: "computed" }), + stepFinish("f1", { generation: 25, source: "computed" }), + stepFinish("f2", { generation: 12, source: "computed" }), ] - expect(messageMetrics(parts)).toEqual({ prompt: 200, generation: 25, source: "computed" }) + expect(messageMetrics(parts)).toEqual({ generation: 25, source: "computed" }) }) }) @@ -809,16 +807,15 @@ describe("throughput formatters", () => { const locale = "en-US" it("renders the value with a t/s suffix", () => { - expect(formatPP(412, locale)).toBe("412 t/s") + expect(formatTG(412, locale)).toBe("412 t/s") expect(formatTG(28.7, locale)).toBe("28.7 t/s") }) it("falls back to dash for missing or bogus values", () => { - expect(formatPP(undefined, locale)).toBe("–") - expect(formatPP(0, locale)).toBe("–") - expect(formatPP(-5, locale)).toBe("–") - expect(formatPP(Number.NaN, locale)).toBe("–") - expect(formatPP(Number.POSITIVE_INFINITY, locale)).toBe("–") expect(formatTG(undefined, locale)).toBe("–") + expect(formatTG(0, locale)).toBe("–") + expect(formatTG(-5, locale)).toBe("–") + expect(formatTG(Number.NaN, locale)).toBe("–") + expect(formatTG(Number.POSITIVE_INFINITY, locale)).toBe("–") }) }) diff --git a/packages/kilo-vscode/webview-ui/src/context/session-utils.ts b/packages/kilo-vscode/webview-ui/src/context/session-utils.ts index f8649b286b3..09f68887ac4 100644 --- a/packages/kilo-vscode/webview-ui/src/context/session-utils.ts +++ b/packages/kilo-vscode/webview-ui/src/context/session-utils.ts @@ -210,31 +210,31 @@ export function calcTokenUsage( * * Combines every step-finish that carries a `metrics` block so the header * reflects all of the assistant's reasoning + answer steps, not just the last - * one. Each field adopts the first non-empty sample we see — that gives a + * one. Generation adopts the first non-empty sample we see — that gives a * stable "snapshot" view of the session pace that won't double-count when - * later steps report zero on a particular field. + * later steps report zero. + * + * PP (prompt-processing) is intentionally not aggregated here: the AI SDK + * adapter drops llama.cpp's `prompt_per_second` before providerMetadata + * reaches computeMetrics, so the wire shape stays `{ prompt?, generation? }` + * for future use but only `generation` is populated today. PP support lands + * when the upstream metadataExtractor wiring ships. * * Returns `undefined` when no step-finish part in the input carries metrics, * which is the signal callers use to hide the throughput UI. */ export function aggregateMetrics( parts: readonly Part[], -): { prompt?: number; generation?: number; source: "computed" } | undefined { - let prompt: number | undefined +): { generation?: number; source: "computed" } | undefined { let generation: number | undefined for (const part of parts) { if (part.type !== "step-finish") continue const metrics = part.metrics if (!metrics) continue - if (metrics.prompt !== undefined && prompt === undefined) prompt = metrics.prompt if (metrics.generation !== undefined && generation === undefined) generation = metrics.generation } - if (prompt === undefined && generation === undefined) return undefined - return { - ...(prompt !== undefined ? { prompt } : {}), - ...(generation !== undefined ? { generation } : {}), - source: "computed", - } + if (generation === undefined) return undefined + return { generation, source: "computed" } } /** @@ -244,25 +244,20 @@ export function aggregateMetrics( */ export function messageMetrics( parts: readonly Part[], -): { prompt?: number; generation?: number; source: "computed" } | undefined { +): { generation?: number; source: "computed" } | undefined { return aggregateMetrics(parts) } /** - * Format a throughput rate (prompt or generation) for display. Shared by - * every rendering site so the same value reads the same in the per-message - * badge and the aggregated header row. + * Format a text-generation rate for display. Shared by every rendering site + * so the same value reads the same in the per-message badge and the + * aggregated header row. */ function formatRateValue(value: number | undefined, locale: string): string { if (!Number.isFinite(value) || value === undefined || value <= 0) return "–" return `${new Intl.NumberFormat(locale, { maximumFractionDigits: 1 }).format(value)} t/s` } -// Convenience labels: PP for prompt, TG for generation. Both delegate to the -// shared formatter — the label is owned by the rendering site, not by us. -export function formatPP(value: number | undefined, locale: string) { - return formatRateValue(value, locale) -} export function formatTG(value: number | undefined, locale: string) { return formatRateValue(value, locale) } diff --git a/packages/kilo-vscode/webview-ui/src/i18n/en.ts b/packages/kilo-vscode/webview-ui/src/i18n/en.ts index 48113bd2d72..7791f71070c 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/en.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/en.ts @@ -1693,10 +1693,10 @@ export const dict = { "settings.display.codeEdit.collapsed": "Collapsed", "settings.display.tokenThroughput.title": "Show Token Throughput", "settings.display.tokenThroughput.description": - "Display tokens-per-second (prompt-processing / text-generation) on each assistant message and in the task header. Hidden by default to keep the chat uncluttered.", + "Display tokens-per-second (text-generation rate) on each assistant message and in the task header. Hidden by default to keep the chat uncluttered.", - "chat.throughput.badge.computed": "PP – · TG {{tg}}", - "chat.throughput.badge.tooltip.computed": "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", + "chat.throughput.badge.computed": "TG {{tg}}", + "chat.throughput.badge.tooltip.computed": "Text generation {{tg}} t/s (computed from step duration)", "chat.throughput.badge.tooltip.missing": "Throughput metrics unavailable for this step", "settings.providers.defaultModel.title": "Default Model", diff --git a/packages/opencode/src/kilocode/plugins/model-usage.ts b/packages/opencode/src/kilocode/plugins/model-usage.ts index 69d19de8b87..2274d91c656 100644 --- a/packages/opencode/src/kilocode/plugins/model-usage.ts +++ b/packages/opencode/src/kilocode/plugins/model-usage.ts @@ -4,7 +4,7 @@ export type SessionModelUsage = KilocodeSessionModelUsageResponse export type UsageResult = { sessionID: string; data?: SessionModelUsage } export type StepMetrics = NonNullable -export type AggregatedMetrics = { prompt?: number; generation?: number } +export type AggregatedMetrics = { generation?: number } export function select(result: UsageResult | undefined, sessionID: string) { if (result?.sessionID !== sessionID) return undefined @@ -73,11 +73,15 @@ export function formatRateValue(value: number | undefined) { return `${throughput.format(value as number)} t/s` } -// Throughput labels used by the sidebar / usage panel. Centralized here so a +// Throughput label used by the sidebar / usage panel. Centralized here so a // future i18n sweep only touches one file — the opencode CLI does not yet -// wire a translation layer, so today these are literal English labels. +// wire a translation layer, so today this is a literal English label. +// PP (prompt-processing) is intentionally omitted: llama.cpp's +// `prompt_per_second` is dropped upstream by the AI SDK adapter before it +// reaches providerMetadata, so the current build can only emit the +// generation rate. The PP row lands alongside TG once the upstream +// metadataExtractor wiring ships. export const throughputLabel = { - prompt: "PP", generation: "TG", } as const @@ -88,42 +92,29 @@ export function formatCost(input: number) { // Local aggregation of step-finish metrics for the sidebar/usage panel. // -// We weight the *generation* rate by generated tokens (longer generations -// pull the average toward their own reported rate). The *prompt* rate comes -// from llama.cpp's `prompt_per_second`, which is a property of the timing -// not the generated output — when a step finishes before tokens are -// emitted (e.g. a tool-only step) the per-step generated count is zero and -// a generated-weighted average would silently drop that step. We fall back -// to a simple mean over the steps that did report a positive prompt rate -// so a zero-weight step still contributes. +// We weight the *generation* rate by generated tokens so longer generations +// pull the average toward their own reported rate. Steps with no generated +// output (e.g. tool-only steps) contribute nothing to the weighted average. export function aggregateMetrics( samples: ReadonlyArray<{ metrics?: StepMetrics; generated: number }>, ): AggregatedMetrics { - let promptSum = 0 - let promptCount = 0 let generationSum = 0 let generationWeight = 0 for (const sample of samples) { const metrics = sample.metrics if (!metrics) continue - if (Number.isFinite(metrics.prompt) && (metrics.prompt as number) > 0) { - promptSum += metrics.prompt as number - promptCount += 1 - } const generated = sample.generated if (Number.isFinite(metrics.generation) && (metrics.generation as number) > 0 && generated > 0) { generationSum += (metrics.generation as number) * generated generationWeight += generated } } - const prompt = promptCount > 0 ? promptSum / promptCount : undefined const generation = generationWeight > 0 ? generationSum / generationWeight : undefined return { - ...(prompt !== undefined ? { prompt } : {}), ...(generation !== undefined ? { generation } : {}), } } export function hasMetrics(value: AggregatedMetrics | undefined): value is AggregatedMetrics { - return value !== undefined && (value.prompt !== undefined || value.generation !== undefined) + return value !== undefined && value.generation !== undefined } diff --git a/packages/opencode/src/kilocode/plugins/sidebar-usage.tsx b/packages/opencode/src/kilocode/plugins/sidebar-usage.tsx index 574b62e0f25..ea144b9b80d 100644 --- a/packages/opencode/src/kilocode/plugins/sidebar-usage.tsx +++ b/packages/opencode/src/kilocode/plugins/sidebar-usage.tsx @@ -122,7 +122,6 @@ function View(props: { api: TuiPluginApi; session_id: string }) { - diff --git a/packages/opencode/test/kilocode/tui/usage.test.ts b/packages/opencode/test/kilocode/tui/usage.test.ts index 76e71577bae..a2d1da34b4e 100644 --- a/packages/opencode/test/kilocode/tui/usage.test.ts +++ b/packages/opencode/test/kilocode/tui/usage.test.ts @@ -6,7 +6,7 @@ import { throughputLabel, } from "../../../src/kilocode/plugins/model-usage" -const step = (metrics: { prompt?: number; generation?: number }) => ({ +const step = (metrics: { generation?: number }) => ({ metrics: { source: "computed" as const, ...metrics }, generated: 0, }) @@ -27,8 +27,7 @@ describe("kilocode.plugins.model-usage throughput helpers", () => { expect(formatRateValue(Infinity)).toBe("-") }) - test("throughputLabel centralizes the PP/TG labels so a future i18n sweep is one file", () => { - expect(throughputLabel.prompt).toBe("PP") + test("throughputLabel centralizes the TG label so a future i18n sweep is one file", () => { expect(throughputLabel.generation).toBe("TG") }) @@ -40,24 +39,6 @@ describe("kilocode.plugins.model-usage throughput helpers", () => { expect(aggregated.generation).toBeCloseTo((20 * 100 + 60 * 300) / (100 + 300)) }) - test("aggregates prompt and generation independently", () => { - const aggregated = aggregateMetrics([ - { ...step({ prompt: 1000, generation: 20 }), generated: 100 }, - { ...step({ prompt: 500, generation: 60 }), generated: 300 }, - ]) - expect(aggregated.prompt).toBeCloseTo((1000 + 500) / 2) - expect(aggregated.generation).toBeCloseTo((20 * 100 + 60 * 300) / 400) - }) - - test("includes prompt from a zero-generated step so it isn't silently dropped", () => { - const aggregated = aggregateMetrics([ - { ...step({ prompt: 9999, generation: 9999 }), generated: 0 }, - { ...step({ generation: 25 }), generated: 50 }, - ]) - expect(aggregated.prompt).toBe(9999) - expect(aggregated.generation).toBe(25) - }) - test("skips samples without metrics", () => { const aggregated = aggregateMetrics([ { metrics: undefined, generated: 100 }, @@ -92,6 +73,5 @@ describe("kilocode.plugins.model-usage throughput helpers", () => { expect(hasMetrics(undefined)).toBeFalse() expect(hasMetrics({})).toBeFalse() expect(hasMetrics({ generation: 12 })).toBeTrue() - expect(hasMetrics({ prompt: 12 })).toBeTrue() }) }) \ No newline at end of file From bc9675621056a7942dc84cccef41ae9440eb14d2 Mon Sep 17 00:00:00 2001 From: Thomas Brugman Date: Tue, 21 Jul 2026 18:37:41 +0200 Subject: [PATCH 07/18] fix(token-throughput-v2): integrate TG into Tokens row, plain-text style MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The standalone blue pills read as loud for what is secondary session info. Move the aggregated TG into the existing Tokens row as another spanned value (alongside ↑ input, ↑ cache, ↓ output) and restyle the per-message badge as plain text in descriptionForeground so both surfaces match the tokens family. TaskUsage now accepts a `throughput` prop and renders `TG t/s` inline in the Summary component when the toggle is on. TaskHeader no longer emits a standalone [data-slot="task-header-throughput"] element; its [data-slot="task-header-throughput"] CSS rule is removed. The throughputText / throughputTooltip memos and the unused formatTG import are dropped — the values flow straight into TaskUsage. Co-Authored-By: Claude Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com> --- .../src/components/chat/AssistantMessage.tsx | 6 ++++-- .../webview-ui/src/components/chat/TaskHeader.tsx | 15 +++++++-------- .../webview-ui/src/components/chat/TaskUsage.tsx | 12 ++++++++++++ .../webview-ui/src/styles/chat-layout.css | 8 +++++--- 4 files changed, 28 insertions(+), 13 deletions(-) diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/AssistantMessage.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/AssistantMessage.tsx index 81aa5aa477a..7f865e91a6b 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/AssistantMessage.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/AssistantMessage.tsx @@ -166,10 +166,12 @@ function BashToolCard(props: { part: ToolPart; defaultOpen: boolean; forceOpen?: ) } -/** Compact tokens-per-second badge shown beneath the last assistant message. +/** Compact tokens-per-second line shown beneath the last assistant message. * Only renders when the user has opted in via the * `kilo-code.new.showTokenThroughput` setting and the message has a - * step-finish part that carries throughput metrics. */ + * step-finish part that carries throughput metrics. Renders as plain text + * that matches the description-foreground tone of the Tokens row in the + * task header — no pill, no border. */ function ThroughputBadge(props: { metrics: NonNullable> }) { const language = useLanguage() const tgText = createMemo(() => formatTG(props.metrics.generation, language.locale())) diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/TaskHeader.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/TaskHeader.tsx index 25d3a8dc589..734b7ec901a 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/TaskHeader.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/TaskHeader.tsx @@ -375,14 +375,13 @@ export const TaskHeader: Component = (props) => {
- {(tk) => } - - {(t) => ( - -
- {t().label} -
-
+ + {(tk) => ( + )}
diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/TaskUsage.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/TaskUsage.tsx index 042a8754db2..30572e35005 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/TaskUsage.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/TaskUsage.tsx @@ -11,6 +11,10 @@ import { formatCompactCount } from "../../utils/format" interface TaskUsageProps { tokens: TokenSummary usage?: SessionModelUsage + /** Aggregated text-generation rate (tokens/sec) for the session. + * Renders inline in the summary row when defined, matching the existing + * token-value spans so the row reads as one line of secondary info. */ + throughput?: number defaultOpen?: boolean } @@ -18,6 +22,11 @@ export const TaskUsage: Component = (props) => { const language = useLanguage() const provider = useProvider() const groups = createMemo(() => groupModelUsage(props.usage?.models ?? [], provider.providers())) + const tgText = createMemo(() => { + const v = props.throughput + if (v === undefined || !Number.isFinite(v) || v <= 0) return undefined + return `${new Intl.NumberFormat(language.locale(), { maximumFractionDigits: 1 }).format(v)} t/s` + }) const money = createMemo( () => new Intl.NumberFormat(language.locale(), { @@ -62,6 +71,9 @@ export const TaskUsage: Component = (props) => { {number(props.tokens.output)}
+ + TG {tgText()} + ) diff --git a/packages/kilo-vscode/webview-ui/src/styles/chat-layout.css b/packages/kilo-vscode/webview-ui/src/styles/chat-layout.css index f2baa98c56c..090233c7fdf 100644 --- a/packages/kilo-vscode/webview-ui/src/styles/chat-layout.css +++ b/packages/kilo-vscode/webview-ui/src/styles/chat-layout.css @@ -237,13 +237,15 @@ } [data-component="assistant-throughput-badge"] { - align-self: flex-start; + /* Plain-text throughput display — matches the muted descriptionForeground + * the Tokens row uses so the two read as the same family of secondary + * info. No pill, no border, no background; just text. */ display: inline-flex; align-items: center; gap: 4px; color: var(--vscode-descriptionForeground); - font-family: var(--font-family-sans); - font-size: var(--font-size-small); +font-family: var(--font-family-sans); + font-size: var(--kilo-font-size-11); line-height: var(--line-height-normal); white-space: nowrap; font-variant-numeric: tabular-nums; From 78645cec57d2c1e657eec8004eeb077224c4d38b Mon Sep 17 00:00:00 2001 From: Thomas Brugman Date: Wed, 22 Jul 2026 10:43:25 +0200 Subject: [PATCH 08/18] fix(token-throughput-v2): finish removing memory badge from AssistantMessage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The "Merge origin/main into feat/token-throughput-v2" resolution kept the throughput branch's memory-badge code (already removed from main by 28d015f8fe), which broke the kilo-ui-contract test and the i18n-keys test. Drop the dead code: `useMemory`/`MemoryMarkerMeta` imports, `mem`, the `meta`/`recall`/`fmt`/`count`/`items`/`verbose` createMemos, the `tip` function, and the `` block. The file lands at 349 lines (down from 391), matching main + throughput only. Verified locally: - i18n-keys + kilo-ui-contract: 53 pass, 0 fail - Full kilo-vscode suite: failures 138 → 136 (+2 from the two fixes) Co-Authored-By: Claude Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com> --- .../src/components/chat/AssistantMessage.tsx | 33 ++++++++----------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/AssistantMessage.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/AssistantMessage.tsx index 7f865e91a6b..e1b410e9fc7 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/AssistantMessage.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/AssistantMessage.tsx @@ -26,7 +26,6 @@ import { useServer } from "../../context/server" import { planDisplayPath } from "../../utils/plan-path" import { isRenderable, UPSTREAM_SUPPRESSED_TOOLS } from "../../utils/transcript-parts" import { messageMetrics, formatTG } from "../../context/session-utils" -import { MemoryMarkerMeta } from "@kilocode/kilo-memory/marker-meta" import { color as timelineColor } from "../../utils/timeline/colors" import type { Part as TimelinePart } from "../../types/messages" import type { TimelineHighlight } from "../../utils/timeline/highlight" @@ -234,25 +233,6 @@ const meta = createMemo(() => (props.parts ?? (data.store.part?.[props.message.id] as TimelinePart[] | undefined) ?? []) as TimelinePart[], ), ) - const fmt = (value: number) => value.toLocaleString(language.locale()) - const count = (item: MemoryItem) => fmt(item.count) - const items = (item: MemoryItem) => item.items ?? [] - const verbose = createMemo(() => Boolean(mem.status()?.state.verbose)) - const tip = (item: MemoryItem) => { - const values = MemoryMarkerMeta.snippets(item, verbose()) - return ( -
- 0} - fallback={ -
{`${language.t("chat.memory.badge.recalled")} · ${language.t("chat.memory.badge.items", { count: count(item) })}`}
- } - > - {(value) =>
{value}
}
-
-
- ) - } return ( <> @@ -368,6 +348,7 @@ const meta = createMemo(() => ) }} +<<<<<<< HEAD {(item) => ( @@ -379,6 +360,18 @@ const meta = createMemo(() => )} +||||||| constructed fake ancestor + + {(item) => ( + +
+ {language.t("chat.memory.badge.recalled")} ·{" "} + {language.t("chat.memory.badge.items", { count: count(item()) })} + 0}> · {items(item())[0]} +
+
+ )} +
{(metrics) => } From c71bb554bb4241c7237288c0abcb4e7f73e29d0c Mon Sep 17 00:00:00 2001 From: Githubguy132010 <145768128+Githubguy132010@users.noreply.github.com> Date: Wed, 22 Jul 2026 10:00:41 +0000 Subject: [PATCH 09/18] fix(token-throughput-v2): finish removing memory badge from AssistantMessage Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com> --- .../src/components/chat/AssistantMessage.tsx | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/AssistantMessage.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/AssistantMessage.tsx index e1b410e9fc7..d3da0bdfbc2 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/AssistantMessage.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/AssistantMessage.tsx @@ -348,7 +348,6 @@ const meta = createMemo(() => ) }} -<<<<<<< HEAD {(item) => ( @@ -360,18 +359,6 @@ const meta = createMemo(() => )} -||||||| constructed fake ancestor - - {(item) => ( - -
- {language.t("chat.memory.badge.recalled")} ·{" "} - {language.t("chat.memory.badge.items", { count: count(item()) })} - 0}> · {items(item())[0]} -
-
- )} -
{(metrics) => } From 4289127ee6b3cd648d2380923496a434c3dbaf3f Mon Sep 17 00:00:00 2001 From: Thomas Brugman Date: Wed, 22 Jul 2026 11:18:30 +0200 Subject: [PATCH 10/18] formatting fixes Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com> --- .../src/kilo-provider/throughput-settings.ts | 2 +- .../src/components/chat/AssistantMessage.tsx | 12 +++--------- .../webview-ui/src/context/session-utils.ts | 8 ++------ packages/kilo-vscode/webview-ui/src/i18n/ar.ts | 3 ++- packages/kilo-vscode/webview-ui/src/i18n/br.ts | 3 ++- packages/kilo-vscode/webview-ui/src/i18n/bs.ts | 3 ++- packages/kilo-vscode/webview-ui/src/i18n/da.ts | 3 ++- packages/kilo-vscode/webview-ui/src/i18n/de.ts | 3 ++- packages/kilo-vscode/webview-ui/src/i18n/es.ts | 3 ++- packages/kilo-vscode/webview-ui/src/i18n/fr.ts | 3 ++- packages/kilo-vscode/webview-ui/src/i18n/it.ts | 3 ++- packages/kilo-vscode/webview-ui/src/i18n/ja.ts | 3 ++- packages/kilo-vscode/webview-ui/src/i18n/ko.ts | 3 ++- packages/kilo-vscode/webview-ui/src/i18n/nl.ts | 3 ++- packages/kilo-vscode/webview-ui/src/i18n/no.ts | 3 ++- packages/kilo-vscode/webview-ui/src/i18n/pl.ts | 3 ++- packages/kilo-vscode/webview-ui/src/i18n/ru.ts | 3 ++- packages/kilo-vscode/webview-ui/src/i18n/th.ts | 3 ++- packages/kilo-vscode/webview-ui/src/i18n/tr.ts | 3 ++- packages/kilo-vscode/webview-ui/src/i18n/uk.ts | 3 ++- packages/kilo-vscode/webview-ui/src/i18n/zh.ts | 3 ++- packages/kilo-vscode/webview-ui/src/i18n/zht.ts | 3 ++- 22 files changed, 44 insertions(+), 35 deletions(-) diff --git a/packages/kilo-vscode/src/kilo-provider/throughput-settings.ts b/packages/kilo-vscode/src/kilo-provider/throughput-settings.ts index 304477a6942..02f2d4a4d6a 100644 --- a/packages/kilo-vscode/src/kilo-provider/throughput-settings.ts +++ b/packages/kilo-vscode/src/kilo-provider/throughput-settings.ts @@ -20,4 +20,4 @@ export function watchThroughputConfig(post: Post): vscode.Disposable { export function validThroughputSetting(key: string, value: unknown) { return key === "showTokenThroughput" && typeof value === "boolean" -} \ No newline at end of file +} diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/AssistantMessage.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/AssistantMessage.tsx index d3da0bdfbc2..67302ad2784 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/AssistantMessage.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/AssistantMessage.tsx @@ -174,9 +174,7 @@ function BashToolCard(props: { part: ToolPart; defaultOpen: boolean; forceOpen?: function ThroughputBadge(props: { metrics: NonNullable> }) { const language = useLanguage() const tgText = createMemo(() => formatTG(props.metrics.generation, language.locale())) - const label = createMemo(() => - language.t("chat.throughput.badge.computed", { tg: tgText() }), - ) + const label = createMemo(() => language.t("chat.throughput.badge.computed", { tg: tgText() })) const tooltip = createMemo(() => { if (props.metrics.generation !== undefined) { return language.t("chat.throughput.badge.tooltip.computed", { @@ -187,9 +185,7 @@ function ThroughputBadge(props: { metrics: NonNullable - - {label()} - + {label()} ) } @@ -359,9 +355,7 @@ const meta = createMemo(() => )}
- - {(metrics) => } - + {(metrics) => } ) } diff --git a/packages/kilo-vscode/webview-ui/src/context/session-utils.ts b/packages/kilo-vscode/webview-ui/src/context/session-utils.ts index 09f68887ac4..baf2b534e96 100644 --- a/packages/kilo-vscode/webview-ui/src/context/session-utils.ts +++ b/packages/kilo-vscode/webview-ui/src/context/session-utils.ts @@ -223,9 +223,7 @@ export function calcTokenUsage( * Returns `undefined` when no step-finish part in the input carries metrics, * which is the signal callers use to hide the throughput UI. */ -export function aggregateMetrics( - parts: readonly Part[], -): { generation?: number; source: "computed" } | undefined { +export function aggregateMetrics(parts: readonly Part[]): { generation?: number; source: "computed" } | undefined { let generation: number | undefined for (const part of parts) { if (part.type !== "step-finish") continue @@ -242,9 +240,7 @@ export function aggregateMetrics( * Same aggregation strategy as `aggregateMetrics` so the per-message badge * and the header row stay consistent. */ -export function messageMetrics( - parts: readonly Part[], -): { generation?: number; source: "computed" } | undefined { +export function messageMetrics(parts: readonly Part[]): { generation?: number; source: "computed" } | undefined { return aggregateMetrics(parts) } diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts index abdfcb8c5b9..af5ca2d5453 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts @@ -1685,7 +1685,8 @@ export const dict = { "Display tokens-per-second (prompt-processing / text-generation) on each assistant message and in the task header. Hidden by default to keep the chat uncluttered.", "chat.throughput.badge.computed": "PP – · TG {{tg}}", - "chat.throughput.badge.tooltip.computed": "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", + "chat.throughput.badge.tooltip.computed": + "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", "chat.throughput.badge.tooltip.missing": "Throughput metrics unavailable for this step", "settings.providers.defaultModel.title": "النموذج الافتراضي", "settings.providers.defaultModel.description": "النموذج الأساسي للمحادثات", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/br.ts b/packages/kilo-vscode/webview-ui/src/i18n/br.ts index ba76137594b..4b9a343ceb2 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/br.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/br.ts @@ -1736,7 +1736,8 @@ export const dict = { "Display tokens-per-second (prompt-processing / text-generation) on each assistant message and in the task header. Hidden by default to keep the chat uncluttered.", "chat.throughput.badge.computed": "PP – · TG {{tg}}", - "chat.throughput.badge.tooltip.computed": "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", + "chat.throughput.badge.tooltip.computed": + "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", "chat.throughput.badge.tooltip.missing": "Throughput metrics unavailable for this step", "settings.providers.defaultModel.title": "Modelo padrão", "settings.providers.defaultModel.description": "Modelo principal para conversas", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/bs.ts b/packages/kilo-vscode/webview-ui/src/i18n/bs.ts index e33a63594d1..5418657cbbd 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/bs.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/bs.ts @@ -1728,7 +1728,8 @@ export const dict = { "Display tokens-per-second (prompt-processing / text-generation) on each assistant message and in the task header. Hidden by default to keep the chat uncluttered.", "chat.throughput.badge.computed": "PP – · TG {{tg}}", - "chat.throughput.badge.tooltip.computed": "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", + "chat.throughput.badge.tooltip.computed": + "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", "chat.throughput.badge.tooltip.missing": "Throughput metrics unavailable for this step", "settings.providers.defaultModel.title": "Zadani model", "settings.providers.defaultModel.description": "Primarni model za razgovore", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/da.ts b/packages/kilo-vscode/webview-ui/src/i18n/da.ts index 9d393b65bf3..d7b0bc0f3bc 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/da.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/da.ts @@ -1719,7 +1719,8 @@ export const dict = { "Display tokens-per-second (prompt-processing / text-generation) on each assistant message and in the task header. Hidden by default to keep the chat uncluttered.", "chat.throughput.badge.computed": "PP – · TG {{tg}}", - "chat.throughput.badge.tooltip.computed": "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", + "chat.throughput.badge.tooltip.computed": + "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", "chat.throughput.badge.tooltip.missing": "Throughput metrics unavailable for this step", "settings.providers.defaultModel.title": "Standardmodel", "settings.providers.defaultModel.description": "Primær model til samtaler", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/de.ts b/packages/kilo-vscode/webview-ui/src/i18n/de.ts index 1284ca47b45..9ea3053e4d1 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/de.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/de.ts @@ -1755,7 +1755,8 @@ export const dict = { "Display tokens-per-second (prompt-processing / text-generation) on each assistant message and in the task header. Hidden by default to keep the chat uncluttered.", "chat.throughput.badge.computed": "PP – · TG {{tg}}", - "chat.throughput.badge.tooltip.computed": "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", + "chat.throughput.badge.tooltip.computed": + "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", "chat.throughput.badge.tooltip.missing": "Throughput metrics unavailable for this step", "settings.providers.defaultModel.title": "Standardmodell", "settings.providers.defaultModel.description": "Primäres Modell für Gespräche", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/es.ts b/packages/kilo-vscode/webview-ui/src/i18n/es.ts index 3b3dd0c27e6..d306e6cf7f3 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/es.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/es.ts @@ -1744,7 +1744,8 @@ export const dict = { "Display tokens-per-second (prompt-processing / text-generation) on each assistant message and in the task header. Hidden by default to keep the chat uncluttered.", "chat.throughput.badge.computed": "PP – · TG {{tg}}", - "chat.throughput.badge.tooltip.computed": "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", + "chat.throughput.badge.tooltip.computed": + "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", "chat.throughput.badge.tooltip.missing": "Throughput metrics unavailable for this step", "settings.providers.defaultModel.title": "Modelo predeterminado", "settings.providers.defaultModel.description": "Modelo principal para conversaciones", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts index b309483f195..fcb2653dc2a 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts @@ -1765,7 +1765,8 @@ export const dict = { "Display tokens-per-second (prompt-processing / text-generation) on each assistant message and in the task header. Hidden by default to keep the chat uncluttered.", "chat.throughput.badge.computed": "PP – · TG {{tg}}", - "chat.throughput.badge.tooltip.computed": "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", + "chat.throughput.badge.tooltip.computed": + "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", "chat.throughput.badge.tooltip.missing": "Throughput metrics unavailable for this step", "settings.providers.defaultModel.title": "Modèle par défaut", "settings.providers.defaultModel.description": "Modèle principal pour les conversations", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/it.ts b/packages/kilo-vscode/webview-ui/src/i18n/it.ts index 9ffce52f841..a351fc76f87 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/it.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/it.ts @@ -1522,7 +1522,8 @@ export const dict = { "Display tokens-per-second (prompt-processing / text-generation) on each assistant message and in the task header. Hidden by default to keep the chat uncluttered.", "chat.throughput.badge.computed": "PP – · TG {{tg}}", - "chat.throughput.badge.tooltip.computed": "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", + "chat.throughput.badge.tooltip.computed": + "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", "chat.throughput.badge.tooltip.missing": "Throughput metrics unavailable for this step", "settings.providers.defaultModel.title": "Modello predefinito", "settings.providers.defaultModel.description": "Modello principale per le conversazioni", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts index 78d9d3eccc5..d2ccd2bd5fd 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts @@ -1714,7 +1714,8 @@ export const dict = { "Display tokens-per-second (prompt-processing / text-generation) on each assistant message and in the task header. Hidden by default to keep the chat uncluttered.", "chat.throughput.badge.computed": "PP – · TG {{tg}}", - "chat.throughput.badge.tooltip.computed": "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", + "chat.throughput.badge.tooltip.computed": + "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", "chat.throughput.badge.tooltip.missing": "Throughput metrics unavailable for this step", "settings.providers.defaultModel.title": "デフォルトモデル", "settings.providers.defaultModel.description": "会話のプライマリモデル", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts index 72615043f95..70c38c7a546 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts @@ -1696,7 +1696,8 @@ export const dict = { "Display tokens-per-second (prompt-processing / text-generation) on each assistant message and in the task header. Hidden by default to keep the chat uncluttered.", "chat.throughput.badge.computed": "PP – · TG {{tg}}", - "chat.throughput.badge.tooltip.computed": "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", + "chat.throughput.badge.tooltip.computed": + "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", "chat.throughput.badge.tooltip.missing": "Throughput metrics unavailable for this step", "settings.providers.defaultModel.title": "기본 모델", "settings.providers.defaultModel.description": "대화의 기본 모델", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/nl.ts b/packages/kilo-vscode/webview-ui/src/i18n/nl.ts index 0e46319eef5..eded4aafdea 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/nl.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/nl.ts @@ -1696,7 +1696,8 @@ export const dict = { "Display tokens-per-second (prompt-processing / text-generation) on each assistant message and in the task header. Hidden by default to keep the chat uncluttered.", "chat.throughput.badge.computed": "PP – · TG {{tg}}", - "chat.throughput.badge.tooltip.computed": "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", + "chat.throughput.badge.tooltip.computed": + "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", "chat.throughput.badge.tooltip.missing": "Throughput metrics unavailable for this step", "settings.providers.defaultModel.title": "Standaard Model", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/no.ts b/packages/kilo-vscode/webview-ui/src/i18n/no.ts index 28f3df54305..0d87cb474e9 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/no.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/no.ts @@ -1717,7 +1717,8 @@ export const dict = { "Display tokens-per-second (prompt-processing / text-generation) on each assistant message and in the task header. Hidden by default to keep the chat uncluttered.", "chat.throughput.badge.computed": "PP – · TG {{tg}}", - "chat.throughput.badge.tooltip.computed": "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", + "chat.throughput.badge.tooltip.computed": + "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", "chat.throughput.badge.tooltip.missing": "Throughput metrics unavailable for this step", "settings.providers.defaultModel.title": "Standardmodell", "settings.providers.defaultModel.description": "Primær modell for samtaler", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts index 1427a7d5f58..3ecd8be52bc 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts @@ -1727,7 +1727,8 @@ export const dict = { "Display tokens-per-second (prompt-processing / text-generation) on each assistant message and in the task header. Hidden by default to keep the chat uncluttered.", "chat.throughput.badge.computed": "PP – · TG {{tg}}", - "chat.throughput.badge.tooltip.computed": "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", + "chat.throughput.badge.tooltip.computed": + "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", "chat.throughput.badge.tooltip.missing": "Throughput metrics unavailable for this step", "settings.providers.defaultModel.title": "Domyślny model", "settings.providers.defaultModel.description": "Główny model do rozmów", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts index ff3a609e122..e9706d4f59e 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts @@ -1725,7 +1725,8 @@ export const dict = { "Display tokens-per-second (prompt-processing / text-generation) on each assistant message and in the task header. Hidden by default to keep the chat uncluttered.", "chat.throughput.badge.computed": "PP – · TG {{tg}}", - "chat.throughput.badge.tooltip.computed": "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", + "chat.throughput.badge.tooltip.computed": + "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", "chat.throughput.badge.tooltip.missing": "Throughput metrics unavailable for this step", "settings.providers.defaultModel.title": "Модель по умолчанию", "settings.providers.defaultModel.description": "Основная модель для разговоров", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/th.ts b/packages/kilo-vscode/webview-ui/src/i18n/th.ts index 72500cef248..a494ce46332 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/th.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/th.ts @@ -1694,7 +1694,8 @@ export const dict = { "Display tokens-per-second (prompt-processing / text-generation) on each assistant message and in the task header. Hidden by default to keep the chat uncluttered.", "chat.throughput.badge.computed": "PP – · TG {{tg}}", - "chat.throughput.badge.tooltip.computed": "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", + "chat.throughput.badge.tooltip.computed": + "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", "chat.throughput.badge.tooltip.missing": "Throughput metrics unavailable for this step", "settings.providers.defaultModel.title": "โมเดลเริ่มต้น", "settings.providers.defaultModel.description": "โมเดลหลักสำหรับบทสนทนา", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/tr.ts b/packages/kilo-vscode/webview-ui/src/i18n/tr.ts index 03e01568c54..0dd26ee902c 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/tr.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/tr.ts @@ -1683,7 +1683,8 @@ export const dict = { "Display tokens-per-second (prompt-processing / text-generation) on each assistant message and in the task header. Hidden by default to keep the chat uncluttered.", "chat.throughput.badge.computed": "PP – · TG {{tg}}", - "chat.throughput.badge.tooltip.computed": "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", + "chat.throughput.badge.tooltip.computed": + "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", "chat.throughput.badge.tooltip.missing": "Throughput metrics unavailable for this step", "settings.providers.defaultModel.title": "Varsayılan Model", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/uk.ts b/packages/kilo-vscode/webview-ui/src/i18n/uk.ts index adb97f694ab..bad686fde4f 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/uk.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/uk.ts @@ -1680,7 +1680,8 @@ export const dict = { "Display tokens-per-second (prompt-processing / text-generation) on each assistant message and in the task header. Hidden by default to keep the chat uncluttered.", "chat.throughput.badge.computed": "PP – · TG {{tg}}", - "chat.throughput.badge.tooltip.computed": "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", + "chat.throughput.badge.tooltip.computed": + "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", "chat.throughput.badge.tooltip.missing": "Throughput metrics unavailable for this step", "settings.providers.defaultModel.title": "Модель за замовчуванням", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts index 0b6916118fa..a820d9e61ea 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts @@ -1646,7 +1646,8 @@ export const dict = { "Display tokens-per-second (prompt-processing / text-generation) on each assistant message and in the task header. Hidden by default to keep the chat uncluttered.", "chat.throughput.badge.computed": "PP – · TG {{tg}}", - "chat.throughput.badge.tooltip.computed": "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", + "chat.throughput.badge.tooltip.computed": + "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", "chat.throughput.badge.tooltip.missing": "Throughput metrics unavailable for this step", "settings.providers.defaultModel.title": "默认模型", "settings.providers.defaultModel.description": "对话的主要模型", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts index d81cd58743e..141aace97ef 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts @@ -1610,7 +1610,8 @@ export const dict = { "Display tokens-per-second (prompt-processing / text-generation) on each assistant message and in the task header. Hidden by default to keep the chat uncluttered.", "chat.throughput.badge.computed": "PP – · TG {{tg}}", - "chat.throughput.badge.tooltip.computed": "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", + "chat.throughput.badge.tooltip.computed": + "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", "chat.throughput.badge.tooltip.missing": "Throughput metrics unavailable for this step", "settings.providers.defaultModel.title": "預設模型", "settings.providers.defaultModel.description": "對話的主要模型", From a31009039f13413656eeaa64c5a24d338242ff7e Mon Sep 17 00:00:00 2001 From: Githubguy132010 <145768128+Githubguy132010@users.noreply.github.com> Date: Wed, 22 Jul 2026 10:12:18 +0000 Subject: [PATCH 11/18] fix(token-throughput-v2): address Marius review comments - Rename 'TG' to 'Generation speed' in en.ts and add a 'gauge' icon to packages/kilo-ui so the per-message badge and the Tokens row show ' Generation speed t/s' instead of the cryptic 'TG '. Centralize the opencode sidebar label in throughputLabel.generation. - Switch aggregateMetrics (both webview and CLI) to the latest non-empty step-finish snapshot so only the most recent assistant turn's generation rate is shown rather than a session-wide aggregate. Update tests and comments to match. - Translate the throughput strings in no.ts to Norwegian; mirror the new key shape across the other locales (English fallback for untranslated strings). Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com> --- packages/kilo-ui/src/components/icon.tsx | 4 ++ .../tests/unit/session-utils.test.ts | 51 ++++++++----- .../src/components/chat/AssistantMessage.tsx | 44 ++++-------- .../src/components/chat/TaskHeader.tsx | 72 +------------------ .../src/components/chat/TaskUsage.tsx | 19 +++-- .../webview-ui/src/context/session-utils.ts | 35 +++++---- .../kilo-vscode/webview-ui/src/i18n/ar.ts | 10 +-- .../kilo-vscode/webview-ui/src/i18n/br.ts | 10 +-- .../kilo-vscode/webview-ui/src/i18n/bs.ts | 10 +-- .../kilo-vscode/webview-ui/src/i18n/da.ts | 10 +-- .../kilo-vscode/webview-ui/src/i18n/de.ts | 10 +-- .../kilo-vscode/webview-ui/src/i18n/en.ts | 9 +-- .../kilo-vscode/webview-ui/src/i18n/es.ts | 10 +-- .../kilo-vscode/webview-ui/src/i18n/fr.ts | 10 +-- .../kilo-vscode/webview-ui/src/i18n/it.ts | 10 +-- .../kilo-vscode/webview-ui/src/i18n/ja.ts | 10 +-- .../kilo-vscode/webview-ui/src/i18n/ko.ts | 10 +-- .../kilo-vscode/webview-ui/src/i18n/nl.ts | 10 +-- .../kilo-vscode/webview-ui/src/i18n/no.ts | 16 +++-- .../kilo-vscode/webview-ui/src/i18n/pl.ts | 10 +-- .../kilo-vscode/webview-ui/src/i18n/ru.ts | 10 +-- .../kilo-vscode/webview-ui/src/i18n/th.ts | 10 +-- .../kilo-vscode/webview-ui/src/i18n/tr.ts | 10 +-- .../kilo-vscode/webview-ui/src/i18n/uk.ts | 10 +-- .../kilo-vscode/webview-ui/src/i18n/zh.ts | 10 +-- .../kilo-vscode/webview-ui/src/i18n/zht.ts | 10 +-- .../src/kilocode/plugins/model-usage.ts | 28 ++++---- .../opencode/test/kilocode/tui/usage.test.ts | 12 ++-- 28 files changed, 210 insertions(+), 260 deletions(-) diff --git a/packages/kilo-ui/src/components/icon.tsx b/packages/kilo-ui/src/components/icon.tsx index 8509d5a2238..96e8621531e 100644 --- a/packages/kilo-ui/src/components/icon.tsx +++ b/packages/kilo-ui/src/components/icon.tsx @@ -54,6 +54,10 @@ const icons: Record = { viewBox: "0 0 24 24", path: ``, }, + gauge: { + viewBox: "0 0 24 24", + path: ``, + }, } type Name = keyof typeof icons diff --git a/packages/kilo-vscode/tests/unit/session-utils.test.ts b/packages/kilo-vscode/tests/unit/session-utils.test.ts index b7b61e20f40..63468c0bed6 100644 --- a/packages/kilo-vscode/tests/unit/session-utils.test.ts +++ b/packages/kilo-vscode/tests/unit/session-utils.test.ts @@ -5,6 +5,7 @@ import { calcContextUsage, calcTokenUsage, aggregateMetrics, + latestMetrics, messageMetrics, formatTG, buildFamilyCosts, @@ -731,41 +732,41 @@ function stepFinish(id: string, metrics?: NonNullable): Part { } } -describe("aggregateMetrics", () => { +describe("latestMetrics", () => { it("returns undefined when no step-finish parts carry metrics", () => { const parts: Part[] = [ { type: "step-start", id: "s1" }, stepFinish("f1"), { type: "text", id: "t1", text: "hello" }, ] - expect(aggregateMetrics(parts)).toBeUndefined() + expect(latestMetrics(parts)).toBeUndefined() }) - it("merges the first non-empty metrics across every step in the session", () => { + it("picks the last non-empty generation rate across every step in the session", () => { const parts: Part[] = [ stepFinish("f1", { prompt: 100, generation: 20, source: "computed" }), { type: "text", id: "t1", text: "mid" }, stepFinish("f2", { prompt: 412, generation: 38, source: "computed" }), ] - expect(aggregateMetrics(parts)).toEqual({ generation: 20, source: "computed" }) + expect(latestMetrics(parts)).toEqual({ generation: 38, source: "computed" }) }) - it("keeps the first computed sample when later steps report zero", () => { + it("uses the latest computed value when earlier steps report lower rates", () => { const parts: Part[] = [ stepFinish("f1", { prompt: 500, generation: 50, source: "computed" }), stepFinish("f2", { generation: 30, source: "computed" }), ] - const result = aggregateMetrics(parts) + const result = latestMetrics(parts) expect(result?.source).toBe("computed") - expect(result?.generation).toBe(50) + expect(result?.generation).toBe(30) }) - it("uses the first computed value when no earlier value is present", () => { + it("falls back to the only computed sample when no later one is present", () => { const parts: Part[] = [ stepFinish("f1", { generation: 12, source: "computed" }), - stepFinish("f2", { generation: 18, source: "computed" }), + stepFinish("f2"), ] - expect(aggregateMetrics(parts)).toEqual({ generation: 12, source: "computed" }) + expect(latestMetrics(parts)).toEqual({ generation: 12, source: "computed" }) }) it("ignores non-step-finish parts even when they look like metrics", () => { @@ -773,28 +774,42 @@ describe("aggregateMetrics", () => { { type: "text", id: "t1", text: "noise" }, stepFinish("f1", { prompt: 200, generation: 22, source: "computed" }), ] - expect(aggregateMetrics(parts)).toEqual({ generation: 22, source: "computed" }) + expect(latestMetrics(parts)).toEqual({ generation: 22, source: "computed" }) }) +}) - it("merges multi-step metrics into a single snapshot per message", () => { - // An assistant turn that runs reasoning + answer produces two step-finish - // parts; the badge should merge them so the generation rate from the - // final step wins. +describe("aggregateMetrics", () => { + // Historical alias of latestMetrics — kept so external callers and tests + // that still use the original name keep working. Behaviour matches: the + // last non-empty step-finish generation rate wins. + it("matches latestMetrics for the same input", () => { const parts: Part[] = [ stepFinish("f1", { generation: 25, source: "computed" }), stepFinish("f2", { generation: 12, source: "computed" }), ] - expect(messageMetrics(parts)).toEqual({ generation: 25, source: "computed" }) + expect(aggregateMetrics(parts)).toEqual(latestMetrics(parts)) }) }) describe("messageMetrics", () => { - it("matches aggregateMetrics behavior on the same input", () => { + it("picks the last non-empty generation rate within a single assistant message", () => { + // An assistant turn that runs reasoning + answer produces two step-finish + // parts; the badge surfaces the final step's generation rate so the + // user sees the rate for the most recent reasoning or text generation + // in that turn. + const parts: Part[] = [ + stepFinish("f1", { generation: 25, source: "computed" }), + stepFinish("f2", { generation: 12, source: "computed" }), + ] + expect(messageMetrics(parts)).toEqual({ generation: 12, source: "computed" }) + }) + + it("matches latestMetrics behavior on the same input", () => { const parts: Part[] = [ stepFinish("f1", { generation: 8, source: "computed" }), stepFinish("f2", { prompt: 99, generation: 33, source: "computed" }), ] - expect(messageMetrics(parts)).toEqual(aggregateMetrics(parts)) + expect(messageMetrics(parts)).toEqual(latestMetrics(parts)) }) it("returns undefined when no throughput metrics are present", () => { diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/AssistantMessage.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/AssistantMessage.tsx index 67302ad2784..9a531ccb80e 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/AssistantMessage.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/AssistantMessage.tsx @@ -29,6 +29,8 @@ import { messageMetrics, formatTG } from "../../context/session-utils" import { color as timelineColor } from "../../utils/timeline/colors" import type { Part as TimelinePart } from "../../types/messages" import type { TimelineHighlight } from "../../utils/timeline/highlight" +import { Icon } from "@kilocode/kilo-ui/icon" +import { Tooltip } from "@kilocode/kilo-ui/tooltip" import { QuestionDock } from "./QuestionDock" import { SuggestBar } from "./SuggestBar" import { toolDefaultOpen } from "./tool-default-open" @@ -165,27 +167,28 @@ function BashToolCard(props: { part: ToolPart; defaultOpen: boolean; forceOpen?: ) } -/** Compact tokens-per-second line shown beneath the last assistant message. +/** Compact generation-speed line shown beneath the last assistant message. * Only renders when the user has opted in via the * `kilo-code.new.showTokenThroughput` setting and the message has a * step-finish part that carries throughput metrics. Renders as plain text - * that matches the description-foreground tone of the Tokens row in the - * task header — no pill, no border. */ + * prefixed by a gauge icon that matches the description-foreground tone of + * the Tokens row in the task header — no pill, no border. */ function ThroughputBadge(props: { metrics: NonNullable> }) { const language = useLanguage() - const tgText = createMemo(() => formatTG(props.metrics.generation, language.locale())) - const label = createMemo(() => language.t("chat.throughput.badge.computed", { tg: tgText() })) + const speedText = createMemo(() => formatTG(props.metrics.generation, language.locale())) + const label = createMemo(() => language.t("chat.throughput.speed.row", { speed: speedText() })) const tooltip = createMemo(() => { if (props.metrics.generation !== undefined) { - return language.t("chat.throughput.badge.tooltip.computed", { - tg: formatTG(props.metrics.generation, language.locale()), - }) + return language.t("chat.throughput.speed.tooltip", { speed: speedText() }) } - return language.t("chat.throughput.badge.tooltip.missing") + return language.t("chat.throughput.speed.tooltip.missing") }) return ( - {label()} + + + {label()} + ) } @@ -194,7 +197,6 @@ export const AssistantMessage: Component = (props) => { const data = useData() const session = useSession() const display = useDisplay() -const mem = useMemory() const language = useLanguage() const { config } = useConfig() const open = createMemo(() => config().terminal_command_display !== "collapsed") @@ -215,15 +217,8 @@ const mem = useMemory() return !!matchToolRequest(part, "question", session.questions()) }) }) -const meta = createMemo(() => - MemoryMarkerMeta.fromParts((props.parts ?? data.store.part?.[props.message.id] ?? []) as MemoryMarkerMeta.Part[]), - ) - const recall = createMemo(() => { - const item = meta() - if (item?.type === "recall") return item - }) // Pull the latest step-finish metrics for this message so the per-message - // badge can render its PP/TG rates when the toggle is on. + // badge can render its generation rate when the toggle is on. const throughput = createMemo(() => messageMetrics( (props.parts ?? (data.store.part?.[props.message.id] as TimelinePart[] | undefined) ?? []) as TimelinePart[], @@ -344,17 +339,6 @@ const meta = createMemo(() => ) }} - - {(item) => ( - -
- {language.t("chat.memory.badge.recalled")} ·{" "} - {language.t("chat.memory.badge.items", { count: count(item()) })} - 0}> · {items(item())[0]} -
-
- )} -
{(metrics) => } ) diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/TaskHeader.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/TaskHeader.tsx index 734b7ec901a..a74ecf7adc8 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/TaskHeader.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/TaskHeader.tsx @@ -14,7 +14,6 @@ import { Tooltip } from "@kilocode/kilo-ui/tooltip" import { Icon } from "@kilocode/kilo-ui/icon" import { Checkbox } from "@kilocode/kilo-ui/checkbox" import { useSession } from "../../context/session" -import { useMemory } from "../../context/memory" import { useDisplay } from "../../context/display" import { calcTokenUsage, collapseCostBreakdown, latestMetrics } from "../../context/session-utils" import { useLanguage } from "../../context/language" @@ -89,7 +88,7 @@ export const TaskHeader: Component = (props) => { return false }) -// Throughput is the latest step-finish snapshot across the session so the + // Throughput is the latest step-finish snapshot across the session so the // figure reflects the most recent assistant turn rather than a session-wide // average. Result is consumed by the TaskUsage summary row, which renders // the value inline with the token counts — there is no standalone element. @@ -102,75 +101,6 @@ export const TaskHeader: Component = (props) => { return latestMetrics(flat) }) - const memoryVerbose = createMemo(() => Boolean(memory.status()?.state.verbose)) - const memoryActive = createMemo(() => { - if (!memory.enabled()) return false - const stats = memory.status()?.state.stats - return !!stats && stats.lastInjectedSessionID === session.currentSessionID() && stats.lastInjectedTokens > 0 - }) - const memoryStatus = createMemo(() => { - if (memory.error()) return memory.error()! - if (memory.loading()) return language.t("chat.memory.status.loading") - if (!memory.enabled()) return language.t("chat.memory.project.disabled") - if (memoryActive()) return language.t("chat.memory.status.active") - return language.t("chat.memory.project.enabled") - }) - const activity = createMemo(() => [...memory.activity()].sort((a, b) => b.at - a.at)) - const activityLines = createMemo(() => { - if (memory.error() || !memory.enabled()) return [] - const loaded = activity().reduce((sum, item) => sum + (item.type === "loaded" ? item.tokens : 0), 0) - const recalled = activity().reduce((sum, item) => sum + (item.type === "recalled" ? item.count : 0), 0) - const saved = activity().reduce((sum, item) => sum + (item.type === "saved" ? item.count : 0), 0) - return [ - ...(loaded > 0 - ? [language.t("chat.memory.activity.loaded", { tokens: loaded.toLocaleString(language.locale()) })] - : []), - ...(recalled > 0 - ? [language.t("chat.memory.activity.recalled", { count: recalled.toLocaleString(language.locale()) })] - : []), - ...(saved > 0 - ? [language.t("chat.memory.activity.saved", { count: saved.toLocaleString(language.locale()) })] - : []), - ] - }) - const activityItems = createMemo(() => - activity() - .flatMap((item) => { - const values = - item.type === "saved" ? [...item.refs, ...item.items] : item.items.length > 0 ? item.items : item.refs - return values.flatMap((value) => { - const text = value.trim() - return text ? [{ type: item.type, value: text }] : [] - }) - }) - .slice(0, 5), - ) - const activityLabel = (item: { type: MemoryActivity["type"]; value: string }) => - language.t(`chat.memory.activity.${item.type}.item`, { item: item.value }) - const activitySummaryView = () => ( -
- 0} - fallback={
{language.t("chat.memory.activity.idle")}
} - > -
- {(line) =>
{line}
}
-
-
-
- ) - const activityTooltip = () => ( - <> -
{language.t("settings.context.title")}
-
{memoryStatus()}
- {activitySummaryView()} - 0}> -
- {(item) =>
{activityLabel(item)}
}
-
-
- - ) const vscode = useVSCode() const [expanded, setExpanded] = createSignal(true) // Throughput row visibility is shared with AssistantMessage via the diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/TaskUsage.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/TaskUsage.tsx index 30572e35005..cef7919af4a 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/TaskUsage.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/TaskUsage.tsx @@ -2,6 +2,7 @@ import type { Component } from "solid-js" import { For, Show, createMemo } from "solid-js" import { Collapsible } from "@kilocode/kilo-ui/collapsible" import { Icon } from "@kilocode/kilo-ui/icon" +import { Tooltip } from "@kilocode/kilo-ui/tooltip" import { useLanguage } from "../../context/language" import { useProvider } from "../../context/provider" import type { SessionModelUsage } from "../../types/messages" @@ -11,9 +12,10 @@ import { formatCompactCount } from "../../utils/format" interface TaskUsageProps { tokens: TokenSummary usage?: SessionModelUsage - /** Aggregated text-generation rate (tokens/sec) for the session. - * Renders inline in the summary row when defined, matching the existing - * token-value spans so the row reads as one line of secondary info. */ + /** Latest text-generation rate (tokens/sec) for the session. Renders inline + * in the summary row when defined, prefixed by a gauge icon, matching the + * existing token-value spans so the row reads as one line of secondary + * info. */ throughput?: number defaultOpen?: boolean } @@ -22,7 +24,7 @@ export const TaskUsage: Component = (props) => { const language = useLanguage() const provider = useProvider() const groups = createMemo(() => groupModelUsage(props.usage?.models ?? [], provider.providers())) - const tgText = createMemo(() => { + const speedText = createMemo(() => { const v = props.throughput if (v === undefined || !Number.isFinite(v) || v <= 0) return undefined return `${new Intl.NumberFormat(language.locale(), { maximumFractionDigits: 1 }).format(v)} t/s` @@ -71,8 +73,13 @@ export const TaskUsage: Component = (props) => { {number(props.tokens.output)}
- - TG {tgText()} + + + + + {language.t("chat.throughput.speed.label")} {speedText()} + + ) diff --git a/packages/kilo-vscode/webview-ui/src/context/session-utils.ts b/packages/kilo-vscode/webview-ui/src/context/session-utils.ts index baf2b534e96..a5a67de3ab8 100644 --- a/packages/kilo-vscode/webview-ui/src/context/session-utils.ts +++ b/packages/kilo-vscode/webview-ui/src/context/session-utils.ts @@ -205,16 +205,13 @@ export function calcTokenUsage( } /** - * Aggregate tokens-per-second throughput across the step-finish parts of a - * session. - * - * Combines every step-finish that carries a `metrics` block so the header - * reflects all of the assistant's reasoning + answer steps, not just the last - * one. Generation adopts the first non-empty sample we see — that gives a - * stable "snapshot" view of the session pace that won't double-count when - * later steps report zero. + * Pick the throughput snapshot from the last step-finish part that carries a + * `metrics` block. We surface only the most recent assistant turn's rate so + * the figure reflects what the user is currently waiting on rather than a + * stale session-wide average — older turns scroll out of view and shouldn't + * keep pulling the displayed value down. * - * PP (prompt-processing) is intentionally not aggregated here: the AI SDK + * PP (prompt-processing) is intentionally not surfaced here: the AI SDK * adapter drops llama.cpp's `prompt_per_second` before providerMetadata * reaches computeMetrics, so the wire shape stays `{ prompt?, generation? }` * for future use but only `generation` is populated today. PP support lands @@ -223,25 +220,35 @@ export function calcTokenUsage( * Returns `undefined` when no step-finish part in the input carries metrics, * which is the signal callers use to hide the throughput UI. */ -export function aggregateMetrics(parts: readonly Part[]): { generation?: number; source: "computed" } | undefined { +export function latestMetrics(parts: readonly Part[]): { generation?: number; source: "computed" } | undefined { let generation: number | undefined for (const part of parts) { if (part.type !== "step-finish") continue const metrics = part.metrics if (!metrics) continue - if (metrics.generation !== undefined && generation === undefined) generation = metrics.generation + if (metrics.generation !== undefined) generation = metrics.generation } if (generation === undefined) return undefined return { generation, source: "computed" } } +/** + * Aggregate tokens-per-second throughput across the step-finish parts of a + * session. Kept as an alias of `latestMetrics` because the historical name + * still appears in tests and external callers — both now resolve to the same + * "last non-empty sample wins" snapshot semantics. + */ +export function aggregateMetrics(parts: readonly Part[]): { generation?: number; source: "computed" } | undefined { + return latestMetrics(parts) +} + /** * Pick the throughput snapshot from a single assistant message's parts. - * Same aggregation strategy as `aggregateMetrics` so the per-message badge - * and the header row stay consistent. + * Same selection strategy as `latestMetrics` so the per-message badge and + * the header row stay consistent. */ export function messageMetrics(parts: readonly Part[]): { generation?: number; source: "computed" } | undefined { - return aggregateMetrics(parts) + return latestMetrics(parts) } /** diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts index af5ca2d5453..623b82e327f 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts @@ -1682,12 +1682,12 @@ export const dict = { "settings.display.tokenThroughput.title": "Show Token Throughput", "settings.display.tokenThroughput.description": - "Display tokens-per-second (prompt-processing / text-generation) on each assistant message and in the task header. Hidden by default to keep the chat uncluttered.", + "Display the text-generation rate (tokens/sec) on the latest assistant message and in the task header. Hidden by default to keep the chat uncluttered.", - "chat.throughput.badge.computed": "PP – · TG {{tg}}", - "chat.throughput.badge.tooltip.computed": - "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", - "chat.throughput.badge.tooltip.missing": "Throughput metrics unavailable for this step", + "chat.throughput.speed.label": "Generation speed", + "chat.throughput.speed.row": "Generation speed {{speed}}", + "chat.throughput.speed.tooltip": "Generation speed {{speed}} (computed from step duration)", + "chat.throughput.speed.tooltip.missing": "Throughput metrics unavailable for this step", "settings.providers.defaultModel.title": "النموذج الافتراضي", "settings.providers.defaultModel.description": "النموذج الأساسي للمحادثات", "settings.providers.smallModel.title": "نموذج صغير", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/br.ts b/packages/kilo-vscode/webview-ui/src/i18n/br.ts index 4b9a343ceb2..6fb84632efb 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/br.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/br.ts @@ -1733,12 +1733,12 @@ export const dict = { "settings.display.tokenThroughput.title": "Show Token Throughput", "settings.display.tokenThroughput.description": - "Display tokens-per-second (prompt-processing / text-generation) on each assistant message and in the task header. Hidden by default to keep the chat uncluttered.", + "Display the text-generation rate (tokens/sec) on the latest assistant message and in the task header. Hidden by default to keep the chat uncluttered.", - "chat.throughput.badge.computed": "PP – · TG {{tg}}", - "chat.throughput.badge.tooltip.computed": - "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", - "chat.throughput.badge.tooltip.missing": "Throughput metrics unavailable for this step", + "chat.throughput.speed.label": "Generation speed", + "chat.throughput.speed.row": "Generation speed {{speed}}", + "chat.throughput.speed.tooltip": "Generation speed {{speed}} (computed from step duration)", + "chat.throughput.speed.tooltip.missing": "Throughput metrics unavailable for this step", "settings.providers.defaultModel.title": "Modelo padrão", "settings.providers.defaultModel.description": "Modelo principal para conversas", "settings.providers.smallModel.title": "Modelo pequeno", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/bs.ts b/packages/kilo-vscode/webview-ui/src/i18n/bs.ts index 5418657cbbd..b17985f31bb 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/bs.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/bs.ts @@ -1725,12 +1725,12 @@ export const dict = { "settings.display.tokenThroughput.title": "Show Token Throughput", "settings.display.tokenThroughput.description": - "Display tokens-per-second (prompt-processing / text-generation) on each assistant message and in the task header. Hidden by default to keep the chat uncluttered.", + "Display the text-generation rate (tokens/sec) on the latest assistant message and in the task header. Hidden by default to keep the chat uncluttered.", - "chat.throughput.badge.computed": "PP – · TG {{tg}}", - "chat.throughput.badge.tooltip.computed": - "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", - "chat.throughput.badge.tooltip.missing": "Throughput metrics unavailable for this step", + "chat.throughput.speed.label": "Generation speed", + "chat.throughput.speed.row": "Generation speed {{speed}}", + "chat.throughput.speed.tooltip": "Generation speed {{speed}} (computed from step duration)", + "chat.throughput.speed.tooltip.missing": "Throughput metrics unavailable for this step", "settings.providers.defaultModel.title": "Zadani model", "settings.providers.defaultModel.description": "Primarni model za razgovore", "settings.providers.smallModel.title": "Mali model", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/da.ts b/packages/kilo-vscode/webview-ui/src/i18n/da.ts index d7b0bc0f3bc..0e4cdba5d52 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/da.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/da.ts @@ -1716,12 +1716,12 @@ export const dict = { "settings.display.tokenThroughput.title": "Show Token Throughput", "settings.display.tokenThroughput.description": - "Display tokens-per-second (prompt-processing / text-generation) on each assistant message and in the task header. Hidden by default to keep the chat uncluttered.", + "Display the text-generation rate (tokens/sec) on the latest assistant message and in the task header. Hidden by default to keep the chat uncluttered.", - "chat.throughput.badge.computed": "PP – · TG {{tg}}", - "chat.throughput.badge.tooltip.computed": - "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", - "chat.throughput.badge.tooltip.missing": "Throughput metrics unavailable for this step", + "chat.throughput.speed.label": "Generation speed", + "chat.throughput.speed.row": "Generation speed {{speed}}", + "chat.throughput.speed.tooltip": "Generation speed {{speed}} (computed from step duration)", + "chat.throughput.speed.tooltip.missing": "Throughput metrics unavailable for this step", "settings.providers.defaultModel.title": "Standardmodel", "settings.providers.defaultModel.description": "Primær model til samtaler", "settings.providers.smallModel.title": "Lille model", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/de.ts b/packages/kilo-vscode/webview-ui/src/i18n/de.ts index 9ea3053e4d1..ad03129db88 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/de.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/de.ts @@ -1752,12 +1752,12 @@ export const dict = { "settings.display.tokenThroughput.title": "Show Token Throughput", "settings.display.tokenThroughput.description": - "Display tokens-per-second (prompt-processing / text-generation) on each assistant message and in the task header. Hidden by default to keep the chat uncluttered.", + "Display the text-generation rate (tokens/sec) on the latest assistant message and in the task header. Hidden by default to keep the chat uncluttered.", - "chat.throughput.badge.computed": "PP – · TG {{tg}}", - "chat.throughput.badge.tooltip.computed": - "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", - "chat.throughput.badge.tooltip.missing": "Throughput metrics unavailable for this step", + "chat.throughput.speed.label": "Generation speed", + "chat.throughput.speed.row": "Generation speed {{speed}}", + "chat.throughput.speed.tooltip": "Generation speed {{speed}} (computed from step duration)", + "chat.throughput.speed.tooltip.missing": "Throughput metrics unavailable for this step", "settings.providers.defaultModel.title": "Standardmodell", "settings.providers.defaultModel.description": "Primäres Modell für Gespräche", "settings.providers.smallModel.title": "Kleines Modell", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/en.ts b/packages/kilo-vscode/webview-ui/src/i18n/en.ts index 7791f71070c..7710e4558be 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/en.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/en.ts @@ -1693,11 +1693,12 @@ export const dict = { "settings.display.codeEdit.collapsed": "Collapsed", "settings.display.tokenThroughput.title": "Show Token Throughput", "settings.display.tokenThroughput.description": - "Display tokens-per-second (text-generation rate) on each assistant message and in the task header. Hidden by default to keep the chat uncluttered.", + "Display the text-generation rate (tokens/sec) on the latest assistant message and in the task header. Hidden by default to keep the chat uncluttered.", - "chat.throughput.badge.computed": "TG {{tg}}", - "chat.throughput.badge.tooltip.computed": "Text generation {{tg}} t/s (computed from step duration)", - "chat.throughput.badge.tooltip.missing": "Throughput metrics unavailable for this step", + "chat.throughput.speed.label": "Generation speed", + "chat.throughput.speed.row": "Generation speed {{speed}}", + "chat.throughput.speed.tooltip": "Generation speed {{speed}} (computed from step duration)", + "chat.throughput.speed.tooltip.missing": "Throughput metrics unavailable for this step", "settings.providers.defaultModel.title": "Default Model", "settings.providers.defaultModel.description": "Primary model for conversations", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/es.ts b/packages/kilo-vscode/webview-ui/src/i18n/es.ts index d306e6cf7f3..e3be41ca4cb 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/es.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/es.ts @@ -1741,12 +1741,12 @@ export const dict = { "settings.display.tokenThroughput.title": "Show Token Throughput", "settings.display.tokenThroughput.description": - "Display tokens-per-second (prompt-processing / text-generation) on each assistant message and in the task header. Hidden by default to keep the chat uncluttered.", + "Display the text-generation rate (tokens/sec) on the latest assistant message and in the task header. Hidden by default to keep the chat uncluttered.", - "chat.throughput.badge.computed": "PP – · TG {{tg}}", - "chat.throughput.badge.tooltip.computed": - "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", - "chat.throughput.badge.tooltip.missing": "Throughput metrics unavailable for this step", + "chat.throughput.speed.label": "Generation speed", + "chat.throughput.speed.row": "Generation speed {{speed}}", + "chat.throughput.speed.tooltip": "Generation speed {{speed}} (computed from step duration)", + "chat.throughput.speed.tooltip.missing": "Throughput metrics unavailable for this step", "settings.providers.defaultModel.title": "Modelo predeterminado", "settings.providers.defaultModel.description": "Modelo principal para conversaciones", "settings.providers.smallModel.title": "Modelo pequeño", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts index fcb2653dc2a..e4e393c1201 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts @@ -1762,12 +1762,12 @@ export const dict = { "settings.display.tokenThroughput.title": "Show Token Throughput", "settings.display.tokenThroughput.description": - "Display tokens-per-second (prompt-processing / text-generation) on each assistant message and in the task header. Hidden by default to keep the chat uncluttered.", + "Display the text-generation rate (tokens/sec) on the latest assistant message and in the task header. Hidden by default to keep the chat uncluttered.", - "chat.throughput.badge.computed": "PP – · TG {{tg}}", - "chat.throughput.badge.tooltip.computed": - "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", - "chat.throughput.badge.tooltip.missing": "Throughput metrics unavailable for this step", + "chat.throughput.speed.label": "Generation speed", + "chat.throughput.speed.row": "Generation speed {{speed}}", + "chat.throughput.speed.tooltip": "Generation speed {{speed}} (computed from step duration)", + "chat.throughput.speed.tooltip.missing": "Throughput metrics unavailable for this step", "settings.providers.defaultModel.title": "Modèle par défaut", "settings.providers.defaultModel.description": "Modèle principal pour les conversations", "settings.providers.smallModel.title": "Petit modèle", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/it.ts b/packages/kilo-vscode/webview-ui/src/i18n/it.ts index a351fc76f87..96b3f1240dd 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/it.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/it.ts @@ -1519,12 +1519,12 @@ export const dict = { "settings.display.tokenThroughput.title": "Show Token Throughput", "settings.display.tokenThroughput.description": - "Display tokens-per-second (prompt-processing / text-generation) on each assistant message and in the task header. Hidden by default to keep the chat uncluttered.", + "Display the text-generation rate (tokens/sec) on the latest assistant message and in the task header. Hidden by default to keep the chat uncluttered.", - "chat.throughput.badge.computed": "PP – · TG {{tg}}", - "chat.throughput.badge.tooltip.computed": - "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", - "chat.throughput.badge.tooltip.missing": "Throughput metrics unavailable for this step", + "chat.throughput.speed.label": "Generation speed", + "chat.throughput.speed.row": "Generation speed {{speed}}", + "chat.throughput.speed.tooltip": "Generation speed {{speed}} (computed from step duration)", + "chat.throughput.speed.tooltip.missing": "Throughput metrics unavailable for this step", "settings.providers.defaultModel.title": "Modello predefinito", "settings.providers.defaultModel.description": "Modello principale per le conversazioni", "settings.providers.smallModel.title": "Modello leggero", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts index d2ccd2bd5fd..403a7cb9e7c 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts @@ -1711,12 +1711,12 @@ export const dict = { "settings.display.tokenThroughput.title": "Show Token Throughput", "settings.display.tokenThroughput.description": - "Display tokens-per-second (prompt-processing / text-generation) on each assistant message and in the task header. Hidden by default to keep the chat uncluttered.", + "Display the text-generation rate (tokens/sec) on the latest assistant message and in the task header. Hidden by default to keep the chat uncluttered.", - "chat.throughput.badge.computed": "PP – · TG {{tg}}", - "chat.throughput.badge.tooltip.computed": - "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", - "chat.throughput.badge.tooltip.missing": "Throughput metrics unavailable for this step", + "chat.throughput.speed.label": "Generation speed", + "chat.throughput.speed.row": "Generation speed {{speed}}", + "chat.throughput.speed.tooltip": "Generation speed {{speed}} (computed from step duration)", + "chat.throughput.speed.tooltip.missing": "Throughput metrics unavailable for this step", "settings.providers.defaultModel.title": "デフォルトモデル", "settings.providers.defaultModel.description": "会話のプライマリモデル", "settings.providers.smallModel.title": "小型モデル", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts index 70c38c7a546..847cb398eb4 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts @@ -1693,12 +1693,12 @@ export const dict = { "settings.display.tokenThroughput.title": "Show Token Throughput", "settings.display.tokenThroughput.description": - "Display tokens-per-second (prompt-processing / text-generation) on each assistant message and in the task header. Hidden by default to keep the chat uncluttered.", + "Display the text-generation rate (tokens/sec) on the latest assistant message and in the task header. Hidden by default to keep the chat uncluttered.", - "chat.throughput.badge.computed": "PP – · TG {{tg}}", - "chat.throughput.badge.tooltip.computed": - "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", - "chat.throughput.badge.tooltip.missing": "Throughput metrics unavailable for this step", + "chat.throughput.speed.label": "Generation speed", + "chat.throughput.speed.row": "Generation speed {{speed}}", + "chat.throughput.speed.tooltip": "Generation speed {{speed}} (computed from step duration)", + "chat.throughput.speed.tooltip.missing": "Throughput metrics unavailable for this step", "settings.providers.defaultModel.title": "기본 모델", "settings.providers.defaultModel.description": "대화의 기본 모델", "settings.providers.smallModel.title": "소형 모델", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/nl.ts b/packages/kilo-vscode/webview-ui/src/i18n/nl.ts index eded4aafdea..0c148f307bf 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/nl.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/nl.ts @@ -1693,12 +1693,12 @@ export const dict = { "settings.display.tokenThroughput.title": "Show Token Throughput", "settings.display.tokenThroughput.description": - "Display tokens-per-second (prompt-processing / text-generation) on each assistant message and in the task header. Hidden by default to keep the chat uncluttered.", + "Display the text-generation rate (tokens/sec) on the latest assistant message and in the task header. Hidden by default to keep the chat uncluttered.", - "chat.throughput.badge.computed": "PP – · TG {{tg}}", - "chat.throughput.badge.tooltip.computed": - "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", - "chat.throughput.badge.tooltip.missing": "Throughput metrics unavailable for this step", + "chat.throughput.speed.label": "Generation speed", + "chat.throughput.speed.row": "Generation speed {{speed}}", + "chat.throughput.speed.tooltip": "Generation speed {{speed}} (computed from step duration)", + "chat.throughput.speed.tooltip.missing": "Throughput metrics unavailable for this step", "settings.providers.defaultModel.title": "Standaard Model", "settings.providers.defaultModel.description": "Primair model voor gesprekken", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/no.ts b/packages/kilo-vscode/webview-ui/src/i18n/no.ts index 0d87cb474e9..375ee7619f4 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/no.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/no.ts @@ -1712,14 +1712,16 @@ export const dict = { "settings.display.codeEdit.expanded": "Utvidet", "settings.display.codeEdit.collapsed": "Skjult", - "settings.display.tokenThroughput.title": "Show Token Throughput", + "settings.display.tokenThroughput.title": "Vis genereringshastighet", "settings.display.tokenThroughput.description": - "Display tokens-per-second (prompt-processing / text-generation) on each assistant message and in the task header. Hidden by default to keep the chat uncluttered.", - - "chat.throughput.badge.computed": "PP – · TG {{tg}}", - "chat.throughput.badge.tooltip.computed": - "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", - "chat.throughput.badge.tooltip.missing": "Throughput metrics unavailable for this step", + "Vis tekstgenereringshastighet (tokens/sek) på den siste assistentmeldingen og i oppgaveoverskriften. Skjult som standard for å holde chatten ryddig.", + + "chat.throughput.speed.label": "Genereringshastighet", + "chat.throughput.speed.row": "Genereringshastighet {{speed}}", + "chat.throughput.speed.tooltip": + "Genereringshastighet {{speed}} (beregnet fra stegvarighet)", + "chat.throughput.speed.tooltip.missing": + "Gjennomstrømningsmålinger er ikke tilgjengelige for dette steget", "settings.providers.defaultModel.title": "Standardmodell", "settings.providers.defaultModel.description": "Primær modell for samtaler", "settings.providers.smallModel.title": "Liten modell", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts index 3ecd8be52bc..6174ee22e4c 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts @@ -1724,12 +1724,12 @@ export const dict = { "settings.display.tokenThroughput.title": "Show Token Throughput", "settings.display.tokenThroughput.description": - "Display tokens-per-second (prompt-processing / text-generation) on each assistant message and in the task header. Hidden by default to keep the chat uncluttered.", + "Display the text-generation rate (tokens/sec) on the latest assistant message and in the task header. Hidden by default to keep the chat uncluttered.", - "chat.throughput.badge.computed": "PP – · TG {{tg}}", - "chat.throughput.badge.tooltip.computed": - "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", - "chat.throughput.badge.tooltip.missing": "Throughput metrics unavailable for this step", + "chat.throughput.speed.label": "Generation speed", + "chat.throughput.speed.row": "Generation speed {{speed}}", + "chat.throughput.speed.tooltip": "Generation speed {{speed}} (computed from step duration)", + "chat.throughput.speed.tooltip.missing": "Throughput metrics unavailable for this step", "settings.providers.defaultModel.title": "Domyślny model", "settings.providers.defaultModel.description": "Główny model do rozmów", "settings.providers.smallModel.title": "Mały model", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts index e9706d4f59e..c40e3cab15d 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts @@ -1722,12 +1722,12 @@ export const dict = { "settings.display.tokenThroughput.title": "Show Token Throughput", "settings.display.tokenThroughput.description": - "Display tokens-per-second (prompt-processing / text-generation) on each assistant message and in the task header. Hidden by default to keep the chat uncluttered.", + "Display the text-generation rate (tokens/sec) on the latest assistant message and in the task header. Hidden by default to keep the chat uncluttered.", - "chat.throughput.badge.computed": "PP – · TG {{tg}}", - "chat.throughput.badge.tooltip.computed": - "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", - "chat.throughput.badge.tooltip.missing": "Throughput metrics unavailable for this step", + "chat.throughput.speed.label": "Generation speed", + "chat.throughput.speed.row": "Generation speed {{speed}}", + "chat.throughput.speed.tooltip": "Generation speed {{speed}} (computed from step duration)", + "chat.throughput.speed.tooltip.missing": "Throughput metrics unavailable for this step", "settings.providers.defaultModel.title": "Модель по умолчанию", "settings.providers.defaultModel.description": "Основная модель для разговоров", "settings.providers.smallModel.title": "Малая модель", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/th.ts b/packages/kilo-vscode/webview-ui/src/i18n/th.ts index a494ce46332..24e3c2b3479 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/th.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/th.ts @@ -1691,12 +1691,12 @@ export const dict = { "settings.display.tokenThroughput.title": "Show Token Throughput", "settings.display.tokenThroughput.description": - "Display tokens-per-second (prompt-processing / text-generation) on each assistant message and in the task header. Hidden by default to keep the chat uncluttered.", + "Display the text-generation rate (tokens/sec) on the latest assistant message and in the task header. Hidden by default to keep the chat uncluttered.", - "chat.throughput.badge.computed": "PP – · TG {{tg}}", - "chat.throughput.badge.tooltip.computed": - "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", - "chat.throughput.badge.tooltip.missing": "Throughput metrics unavailable for this step", + "chat.throughput.speed.label": "Generation speed", + "chat.throughput.speed.row": "Generation speed {{speed}}", + "chat.throughput.speed.tooltip": "Generation speed {{speed}} (computed from step duration)", + "chat.throughput.speed.tooltip.missing": "Throughput metrics unavailable for this step", "settings.providers.defaultModel.title": "โมเดลเริ่มต้น", "settings.providers.defaultModel.description": "โมเดลหลักสำหรับบทสนทนา", "settings.providers.smallModel.title": "โมเดลขนาดเล็ก", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/tr.ts b/packages/kilo-vscode/webview-ui/src/i18n/tr.ts index 0dd26ee902c..4ad861c6ba2 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/tr.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/tr.ts @@ -1680,12 +1680,12 @@ export const dict = { "settings.display.tokenThroughput.title": "Show Token Throughput", "settings.display.tokenThroughput.description": - "Display tokens-per-second (prompt-processing / text-generation) on each assistant message and in the task header. Hidden by default to keep the chat uncluttered.", + "Display the text-generation rate (tokens/sec) on the latest assistant message and in the task header. Hidden by default to keep the chat uncluttered.", - "chat.throughput.badge.computed": "PP – · TG {{tg}}", - "chat.throughput.badge.tooltip.computed": - "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", - "chat.throughput.badge.tooltip.missing": "Throughput metrics unavailable for this step", + "chat.throughput.speed.label": "Generation speed", + "chat.throughput.speed.row": "Generation speed {{speed}}", + "chat.throughput.speed.tooltip": "Generation speed {{speed}} (computed from step duration)", + "chat.throughput.speed.tooltip.missing": "Throughput metrics unavailable for this step", "settings.providers.defaultModel.title": "Varsayılan Model", "settings.providers.defaultModel.description": "Sohbetler için birincil model", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/uk.ts b/packages/kilo-vscode/webview-ui/src/i18n/uk.ts index bad686fde4f..977843fb431 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/uk.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/uk.ts @@ -1677,12 +1677,12 @@ export const dict = { "settings.display.tokenThroughput.title": "Show Token Throughput", "settings.display.tokenThroughput.description": - "Display tokens-per-second (prompt-processing / text-generation) on each assistant message and in the task header. Hidden by default to keep the chat uncluttered.", + "Display the text-generation rate (tokens/sec) on the latest assistant message and in the task header. Hidden by default to keep the chat uncluttered.", - "chat.throughput.badge.computed": "PP – · TG {{tg}}", - "chat.throughput.badge.tooltip.computed": - "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", - "chat.throughput.badge.tooltip.missing": "Throughput metrics unavailable for this step", + "chat.throughput.speed.label": "Generation speed", + "chat.throughput.speed.row": "Generation speed {{speed}}", + "chat.throughput.speed.tooltip": "Generation speed {{speed}} (computed from step duration)", + "chat.throughput.speed.tooltip.missing": "Throughput metrics unavailable for this step", "settings.providers.defaultModel.title": "Модель за замовчуванням", "settings.providers.defaultModel.description": "Основна модель для чатів", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts index a820d9e61ea..b54117530d1 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts @@ -1643,12 +1643,12 @@ export const dict = { "settings.display.tokenThroughput.title": "Show Token Throughput", "settings.display.tokenThroughput.description": - "Display tokens-per-second (prompt-processing / text-generation) on each assistant message and in the task header. Hidden by default to keep the chat uncluttered.", + "Display the text-generation rate (tokens/sec) on the latest assistant message and in the task header. Hidden by default to keep the chat uncluttered.", - "chat.throughput.badge.computed": "PP – · TG {{tg}}", - "chat.throughput.badge.tooltip.computed": - "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", - "chat.throughput.badge.tooltip.missing": "Throughput metrics unavailable for this step", + "chat.throughput.speed.label": "Generation speed", + "chat.throughput.speed.row": "Generation speed {{speed}}", + "chat.throughput.speed.tooltip": "Generation speed {{speed}} (computed from step duration)", + "chat.throughput.speed.tooltip.missing": "Throughput metrics unavailable for this step", "settings.providers.defaultModel.title": "默认模型", "settings.providers.defaultModel.description": "对话的主要模型", "settings.providers.smallModel.title": "小模型", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts index 141aace97ef..3061b4da6da 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts @@ -1607,12 +1607,12 @@ export const dict = { "settings.display.tokenThroughput.title": "Show Token Throughput", "settings.display.tokenThroughput.description": - "Display tokens-per-second (prompt-processing / text-generation) on each assistant message and in the task header. Hidden by default to keep the chat uncluttered.", + "Display the text-generation rate (tokens/sec) on the latest assistant message and in the task header. Hidden by default to keep the chat uncluttered.", - "chat.throughput.badge.computed": "PP – · TG {{tg}}", - "chat.throughput.badge.tooltip.computed": - "Prompt-processing unavailable · text-generation {{tg}} t/s (computed from step duration)", - "chat.throughput.badge.tooltip.missing": "Throughput metrics unavailable for this step", + "chat.throughput.speed.label": "Generation speed", + "chat.throughput.speed.row": "Generation speed {{speed}}", + "chat.throughput.speed.tooltip": "Generation speed {{speed}} (computed from step duration)", + "chat.throughput.speed.tooltip.missing": "Throughput metrics unavailable for this step", "settings.providers.defaultModel.title": "預設模型", "settings.providers.defaultModel.description": "對話的主要模型", "settings.providers.smallModel.title": "小模型", diff --git a/packages/opencode/src/kilocode/plugins/model-usage.ts b/packages/opencode/src/kilocode/plugins/model-usage.ts index 2274d91c656..65bf24fdc1f 100644 --- a/packages/opencode/src/kilocode/plugins/model-usage.ts +++ b/packages/opencode/src/kilocode/plugins/model-usage.ts @@ -79,10 +79,10 @@ export function formatRateValue(value: number | undefined) { // PP (prompt-processing) is intentionally omitted: llama.cpp's // `prompt_per_second` is dropped upstream by the AI SDK adapter before it // reaches providerMetadata, so the current build can only emit the -// generation rate. The PP row lands alongside TG once the upstream -// metadataExtractor wiring ships. +// generation rate. The PP row lands alongside generation speed once the +// upstream metadataExtractor wiring ships. export const throughputLabel = { - generation: "TG", + generation: "Generation speed", } as const export function formatCost(input: number) { @@ -92,24 +92,24 @@ export function formatCost(input: number) { // Local aggregation of step-finish metrics for the sidebar/usage panel. // -// We weight the *generation* rate by generated tokens so longer generations -// pull the average toward their own reported rate. Steps with no generated -// output (e.g. tool-only steps) contribute nothing to the weighted average. +// We surface the *most recent* generation rate (last non-empty sample wins) +// so the displayed figure tracks the latest assistant turn the user is +// waiting on rather than a weighted session-wide average that drifts toward +// long-ago turns. Steps with no generated output (e.g. tool-only steps) +// contribute nothing — they simply don't replace the running snapshot. export function aggregateMetrics( samples: ReadonlyArray<{ metrics?: StepMetrics; generated: number }>, ): AggregatedMetrics { - let generationSum = 0 - let generationWeight = 0 + let generation: number | undefined for (const sample of samples) { const metrics = sample.metrics if (!metrics) continue - const generated = sample.generated - if (Number.isFinite(metrics.generation) && (metrics.generation as number) > 0 && generated > 0) { - generationSum += (metrics.generation as number) * generated - generationWeight += generated - } + const value = metrics.generation + if (value === undefined) continue + if (!Number.isFinite(value) || (value as number) <= 0) continue + if ((sample.generated ?? 0) <= 0) continue + generation = value as number } - const generation = generationWeight > 0 ? generationSum / generationWeight : undefined return { ...(generation !== undefined ? { generation } : {}), } diff --git a/packages/opencode/test/kilocode/tui/usage.test.ts b/packages/opencode/test/kilocode/tui/usage.test.ts index a2d1da34b4e..ed076a664e8 100644 --- a/packages/opencode/test/kilocode/tui/usage.test.ts +++ b/packages/opencode/test/kilocode/tui/usage.test.ts @@ -27,16 +27,16 @@ describe("kilocode.plugins.model-usage throughput helpers", () => { expect(formatRateValue(Infinity)).toBe("-") }) - test("throughputLabel centralizes the TG label so a future i18n sweep is one file", () => { - expect(throughputLabel.generation).toBe("TG") + test("throughputLabel centralizes the generation-speed label so a future i18n sweep is one file", () => { + expect(throughputLabel.generation).toBe("Generation speed") }) - test("aggregates per-step generation weighted by generated tokens", () => { + test("surfaces the most recent non-empty generation rate as the snapshot", () => { const aggregated = aggregateMetrics([ { ...step({ generation: 20 }), generated: 100 }, { ...step({ generation: 60 }), generated: 300 }, ]) - expect(aggregated.generation).toBeCloseTo((20 * 100 + 60 * 300) / (100 + 300)) + expect(aggregated.generation).toBe(60) }) test("skips samples without metrics", () => { @@ -47,7 +47,7 @@ describe("kilocode.plugins.model-usage throughput helpers", () => { expect(aggregated.generation).toBe(40) }) - test("skips zero-weight samples for the generation average", () => { + test("skips zero-weight samples when picking the latest snapshot", () => { const aggregated = aggregateMetrics([ { ...step({ generation: 9999 }), generated: 0 }, { ...step({ generation: 25 }), generated: 50 }, @@ -60,7 +60,7 @@ describe("kilocode.plugins.model-usage throughput helpers", () => { expect(aggregateMetrics([{ metrics: undefined, generated: 100 }])).toEqual({}) }) - test("ignores bogus per-call values without poisoning the aggregate", () => { + test("ignores bogus per-call values without poisoning the snapshot", () => { const aggregated = aggregateMetrics([ { ...step({ generation: -1 }), generated: 100 }, { ...step({ generation: Number.POSITIVE_INFINITY }), generated: 100 }, From aa3fe36fbe749a55fe36e424774e68eb76140f4f Mon Sep 17 00:00:00 2001 From: Githubguy132010 <145768128+Githubguy132010@users.noreply.github.com> Date: Wed, 22 Jul 2026 10:23:26 +0000 Subject: [PATCH 12/18] fix(token-throughput-v2): close unclosed CSS block and apply prettier formatting The throughput rebases left a duplicated .vscode-session-turn-diffs selector and let three files drift from prettier's expectations. Fix the CSS unclosed-block (which broke the Storybook preview build) and re-run prettier --write on the touched files. Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com> --- packages/kilo-vscode/tests/unit/session-utils.test.ts | 5 +---- packages/kilo-vscode/webview-ui/src/i18n/no.ts | 6 ++---- packages/kilo-vscode/webview-ui/src/styles/chat-layout.css | 3 +-- 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/packages/kilo-vscode/tests/unit/session-utils.test.ts b/packages/kilo-vscode/tests/unit/session-utils.test.ts index 63468c0bed6..59ef6f546a5 100644 --- a/packages/kilo-vscode/tests/unit/session-utils.test.ts +++ b/packages/kilo-vscode/tests/unit/session-utils.test.ts @@ -762,10 +762,7 @@ describe("latestMetrics", () => { }) it("falls back to the only computed sample when no later one is present", () => { - const parts: Part[] = [ - stepFinish("f1", { generation: 12, source: "computed" }), - stepFinish("f2"), - ] + const parts: Part[] = [stepFinish("f1", { generation: 12, source: "computed" }), stepFinish("f2")] expect(latestMetrics(parts)).toEqual({ generation: 12, source: "computed" }) }) diff --git a/packages/kilo-vscode/webview-ui/src/i18n/no.ts b/packages/kilo-vscode/webview-ui/src/i18n/no.ts index 375ee7619f4..46093e0f655 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/no.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/no.ts @@ -1718,10 +1718,8 @@ export const dict = { "chat.throughput.speed.label": "Genereringshastighet", "chat.throughput.speed.row": "Genereringshastighet {{speed}}", - "chat.throughput.speed.tooltip": - "Genereringshastighet {{speed}} (beregnet fra stegvarighet)", - "chat.throughput.speed.tooltip.missing": - "Gjennomstrømningsmålinger er ikke tilgjengelige for dette steget", + "chat.throughput.speed.tooltip": "Genereringshastighet {{speed}} (beregnet fra stegvarighet)", + "chat.throughput.speed.tooltip.missing": "Gjennomstrømningsmålinger er ikke tilgjengelige for dette steget", "settings.providers.defaultModel.title": "Standardmodell", "settings.providers.defaultModel.description": "Primær modell for samtaler", "settings.providers.smallModel.title": "Liten modell", diff --git a/packages/kilo-vscode/webview-ui/src/styles/chat-layout.css b/packages/kilo-vscode/webview-ui/src/styles/chat-layout.css index 090233c7fdf..2e726b5086c 100644 --- a/packages/kilo-vscode/webview-ui/src/styles/chat-layout.css +++ b/packages/kilo-vscode/webview-ui/src/styles/chat-layout.css @@ -244,14 +244,13 @@ align-items: center; gap: 4px; color: var(--vscode-descriptionForeground); -font-family: var(--font-family-sans); + font-family: var(--font-family-sans); font-size: var(--kilo-font-size-11); line-height: var(--line-height-normal); white-space: nowrap; font-variant-numeric: tabular-nums; } -.vscode-session-turn-diffs { .vscode-session-turn-diffs { width: 100%; } From 405a0c1f8d6603503924c353ac4db39574fa4743 Mon Sep 17 00:00:00 2001 From: Githubguy132010 <145768128+Githubguy132010@users.noreply.github.com> Date: Wed, 22 Jul 2026 10:37:36 +0000 Subject: [PATCH 13/18] ci: re-run after fixing CSS unclosed-block + prettier drift Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com> From 41270b0615edb44fc8a8b1953154d4ccd2a29eed Mon Sep 17 00:00:00 2001 From: Githubguy132010 <145768128+Githubguy132010@users.noreply.github.com> Date: Wed, 22 Jul 2026 10:52:13 +0000 Subject: [PATCH 14/18] fix(token-throughput-v2): address kilo-code-bot review Kilo: - CRITICAL: Remove the section === '' guard in KiloProvider.ts that blocked persistence of every top-level setting key. The throughput validator is now redundant, so drop it from throughput-settings.ts. - WARNING: Reset samples in sidebar-usage.tsx when props.session_id changes via a keyed createEffect, so a session switch no longer blends step-finish metrics from the previous session and the array no longer grows without bound across long-lived plugin instances. - WARNING: Pass {speed} to language.t for the TaskUsage throughput tooltip and reuse the shared formatTG helper instead of reformatting the value inline. Drop the dead [data-component='assistant-memory-badge'] rule whose target component no longer exists in the tree. - SUGGESTION: Drop redundant guards in model-usage.ts (undefined check after Number.isFinite, and the ?? 0 on an already-required number field). Use typeof === 'number' for the type narrowing. --- bun.lock | 16 ++++++++-------- packages/kilo-vscode/src/KiloProvider.ts | 7 +------ .../src/kilo-provider/throughput-settings.ts | 4 ---- .../src/components/chat/TaskUsage.tsx | 9 +++------ .../webview-ui/src/styles/chat-layout.css | 8 -------- .../src/kilocode/plugins/model-usage.ts | 12 ++++++------ .../src/kilocode/plugins/sidebar-usage.tsx | 17 ++++++++++++++++- 7 files changed, 34 insertions(+), 39 deletions(-) diff --git a/bun.lock b/bun.lock index 6b7de89c9a0..3b9c78d9895 100644 --- a/bun.lock +++ b/bun.lock @@ -844,22 +844,22 @@ }, }, "trustedDependencies": [ - "web-tree-sitter", "esbuild", - "tree-sitter-bash", "protobufjs", + "web-tree-sitter", + "tree-sitter-bash", ], "patchedDependencies": { - "@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch", - "virtua@0.49.1": "patches/virtua@0.49.1.patch", "mammoth@1.12.0": "patches/mammoth@1.12.0.patch", - "@ff-labs/fff-bun@0.9.4": "patches/@ff-labs%2Ffff-bun@0.9.4.patch", - "@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch", + "virtua@0.49.1": "patches/virtua@0.49.1.patch", + "@ai-sdk/xai@3.0.102": "patches/@ai-sdk%2Fxai@3.0.102.patch", "@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch", - "@npmcli/agent@4.0.2": "patches/@npmcli%2Fagent@4.0.2.patch", "@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch", + "@ff-labs/fff-bun@0.9.4": "patches/@ff-labs%2Ffff-bun@0.9.4.patch", "pacote@21.5.1": "patches/pacote@21.5.1.patch", - "@ai-sdk/xai@3.0.102": "patches/@ai-sdk%2Fxai@3.0.102.patch", + "@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch", + "@npmcli/agent@4.0.2": "patches/@npmcli%2Fagent@4.0.2.patch", + "@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch", }, "overrides": { "@effect/platform-node-shared": "4.0.0-beta.74", diff --git a/packages/kilo-vscode/src/KiloProvider.ts b/packages/kilo-vscode/src/KiloProvider.ts index 874d4b7faca..d82cf1e28ac 100644 --- a/packages/kilo-vscode/src/KiloProvider.ts +++ b/packages/kilo-vscode/src/KiloProvider.ts @@ -172,11 +172,7 @@ import { watchIndexingConfig, } from "./kilo-provider/indexing-settings" import { buildChatSettingsMessage, validChatSetting, watchChatConfig } from "./kilo-provider/chat-settings" -import { - buildThroughputSettingMessage, - validThroughputSetting, - watchThroughputConfig, -} from "./kilo-provider/throughput-settings" +import { buildThroughputSettingMessage, watchThroughputConfig } from "./kilo-provider/throughput-settings" let maxCost = 0 @@ -3685,7 +3681,6 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper if (section === "autocomplete" && !validAutocompleteSetting(leaf, value)) return if (section === "indexing" && !validIndexingSetting(leaf, value)) return if (section === "chat" && !validChatSetting(leaf, value)) return - if (section === "" && !validThroughputSetting(leaf, value)) return const config = vscode.workspace.getConfiguration(`kilo-code.new${section ? `.${section}` : ""}`) // Normalize a webview-side clear to `undefined` so VS Code removes the // key from settings.json rather than persisting a literal `null`. This diff --git a/packages/kilo-vscode/src/kilo-provider/throughput-settings.ts b/packages/kilo-vscode/src/kilo-provider/throughput-settings.ts index 02f2d4a4d6a..ddac492302d 100644 --- a/packages/kilo-vscode/src/kilo-provider/throughput-settings.ts +++ b/packages/kilo-vscode/src/kilo-provider/throughput-settings.ts @@ -17,7 +17,3 @@ export function watchThroughputConfig(post: Post): vscode.Disposable { } }) } - -export function validThroughputSetting(key: string, value: unknown) { - return key === "showTokenThroughput" && typeof value === "boolean" -} diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/TaskUsage.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/TaskUsage.tsx index cef7919af4a..9d96f225272 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/TaskUsage.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/TaskUsage.tsx @@ -8,6 +8,7 @@ import { useProvider } from "../../context/provider" import type { SessionModelUsage } from "../../types/messages" import { groupModelUsage, modelUsageName, type TokenSummary } from "../../context/model-usage" import { formatCompactCount } from "../../utils/format" +import { formatTG } from "../../context/session-utils" interface TaskUsageProps { tokens: TokenSummary @@ -24,11 +25,7 @@ export const TaskUsage: Component = (props) => { const language = useLanguage() const provider = useProvider() const groups = createMemo(() => groupModelUsage(props.usage?.models ?? [], provider.providers())) - const speedText = createMemo(() => { - const v = props.throughput - if (v === undefined || !Number.isFinite(v) || v <= 0) return undefined - return `${new Intl.NumberFormat(language.locale(), { maximumFractionDigits: 1 }).format(v)} t/s` - }) + const speedText = createMemo(() => formatTG(props.throughput, language.locale())) const money = createMemo( () => new Intl.NumberFormat(language.locale(), { @@ -74,7 +71,7 @@ export const TaskUsage: Component = (props) => { - + {language.t("chat.throughput.speed.label")} {speedText()} diff --git a/packages/kilo-vscode/webview-ui/src/styles/chat-layout.css b/packages/kilo-vscode/webview-ui/src/styles/chat-layout.css index 2e726b5086c..aab1391442f 100644 --- a/packages/kilo-vscode/webview-ui/src/styles/chat-layout.css +++ b/packages/kilo-vscode/webview-ui/src/styles/chat-layout.css @@ -228,14 +228,6 @@ width: 100%; } -[data-component="assistant-memory-badge"] { - align-self: flex-start; - color: var(--vscode-descriptionForeground); - font-family: var(--font-family-sans); - font-size: var(--font-size-small); - line-height: var(--line-height-normal); -} - [data-component="assistant-throughput-badge"] { /* Plain-text throughput display — matches the muted descriptionForeground * the Tokens row uses so the two read as the same family of secondary diff --git a/packages/opencode/src/kilocode/plugins/model-usage.ts b/packages/opencode/src/kilocode/plugins/model-usage.ts index 65bf24fdc1f..7c796e2a671 100644 --- a/packages/opencode/src/kilocode/plugins/model-usage.ts +++ b/packages/opencode/src/kilocode/plugins/model-usage.ts @@ -69,8 +69,8 @@ export function formatRate(tokens: SessionModelUsage["totals"]["tokens"]) { } export function formatRateValue(value: number | undefined) { - if (!Number.isFinite(value) || value === undefined || value <= 0) return "-" - return `${throughput.format(value as number)} t/s` + if (value === undefined || !Number.isFinite(value) || value <= 0) return "-" + return `${throughput.format(value)} t/s` } // Throughput label used by the sidebar / usage panel. Centralized here so a @@ -105,10 +105,10 @@ export function aggregateMetrics( const metrics = sample.metrics if (!metrics) continue const value = metrics.generation - if (value === undefined) continue - if (!Number.isFinite(value) || (value as number) <= 0) continue - if ((sample.generated ?? 0) <= 0) continue - generation = value as number + if (typeof value !== "number" || !Number.isFinite(value)) continue + if (value <= 0) continue + if (sample.generated <= 0) continue + generation = value } return { ...(generation !== undefined ? { generation } : {}), diff --git a/packages/opencode/src/kilocode/plugins/sidebar-usage.tsx b/packages/opencode/src/kilocode/plugins/sidebar-usage.tsx index ea144b9b80d..1e55a7d1e34 100644 --- a/packages/opencode/src/kilocode/plugins/sidebar-usage.tsx +++ b/packages/opencode/src/kilocode/plugins/sidebar-usage.tsx @@ -1,5 +1,5 @@ import type { TuiPlugin, TuiPluginApi, TuiPluginModule } from "@kilocode/plugin/tui" -import { createMemo, createResource, createSignal, For, onCleanup, onMount, Show } from "solid-js" +import { createEffect, createMemo, createResource, createSignal, For, onCleanup, onMount, Show } from "solid-js" import { useLocal } from "@tui/context/local" import * as Model from "@tui/util/model" import { Locale } from "@/util/locale" @@ -47,6 +47,21 @@ function View(props: { api: TuiPluginApi; session_id: string }) { const providers = createMemo(() => Model.index([...props.api.state.provider])) const groups = createMemo(() => groupModelsByProvider(usage()?.models ?? [], props.api.state.provider)) const throughput = createMemo(() => aggregateMetrics(samples())) + + // Reset accumulated samples whenever the sidebar is mounted against a new + // session. Without this guard, switching tabs in the TUI would blend + // step-finish metrics from the previous session into the new session's + // generation rate, and `samples` would grow without bound across + // long-lived plugin instances. + createEffect( + () => { + props.session_id + setSamples([]) + }, + () => { + setSamples([]) + }, + ) const bench = createMemo(() => { const current = local.model.current() if (!current) return undefined From 65e552cc74c947179c3f897c87ea9d617dffdcf6 Mon Sep 17 00:00:00 2001 From: Thomas Brugman Date: Wed, 22 Jul 2026 18:19:15 +0200 Subject: [PATCH 15/18] feat(token-throughput-v2): weighted per-turn rate, plain text footer Address Marius's review of the throughput UI: Calculation - Persist per-step timing (start/end/elapsed) on step-start and step-finish parts in the session processor. - Add wire schemas in core/src/v1/session.ts and packages/sdk/openapi.json so the new time field round-trips end-to-end. - Replace the last-wins 'latest step rate' snapshot with a weighted aggregate: sum(output + reasoning tokens) / sum(active generation duration) across the turn's step-finish parts. Tool execution and idle waiting are excluded. - The CLI sidebar (model-usage.ts) gains the same weighted semantics when timing is available, falling back to last-wins otherwise so older callers keep working. Presentation - Strip the per-message badge to plain muted text (no icon, no label, no border). The chip in the upstream action row reads as metadata. - Move throughput out of the task header Tokens row so each turn owns its own value (no flicker across turns, single source of truth). - Read the throughput memo from the full message parts in the data store rather than the chunked row slice, so step-finish in any chunk produces the badge. i18n - Replace chat.throughput.speed.{label,row,tooltip,tooltip.missing} with chat.throughput.tooltip and chat.throughput.tooltip.missing across all 19 locale files. Tests - Add messageThroughput and sessionThroughput describe blocks exercising the weighted aggregate across multiple steps. - Cover weighted + fallback paths in the CLI aggregateMetrics tests. --- packages/core/src/v1/session.ts | 22 +++ .../tests/unit/session-utils.test.ts | 151 +++++++++++++++++- .../src/components/chat/AssistantMessage.tsx | 51 +++--- .../src/components/chat/TaskHeader.tsx | 21 +-- .../src/components/chat/TaskUsage.tsx | 16 -- .../webview-ui/src/context/session-utils.ts | 43 +++++ .../kilo-vscode/webview-ui/src/i18n/ar.ts | 7 +- .../kilo-vscode/webview-ui/src/i18n/br.ts | 7 +- .../kilo-vscode/webview-ui/src/i18n/bs.ts | 7 +- .../kilo-vscode/webview-ui/src/i18n/da.ts | 7 +- .../kilo-vscode/webview-ui/src/i18n/de.ts | 7 +- .../kilo-vscode/webview-ui/src/i18n/en.ts | 6 +- .../kilo-vscode/webview-ui/src/i18n/es.ts | 7 +- .../kilo-vscode/webview-ui/src/i18n/fr.ts | 7 +- .../kilo-vscode/webview-ui/src/i18n/it.ts | 7 +- .../kilo-vscode/webview-ui/src/i18n/ja.ts | 7 +- .../kilo-vscode/webview-ui/src/i18n/ko.ts | 7 +- .../kilo-vscode/webview-ui/src/i18n/nl.ts | 7 +- .../kilo-vscode/webview-ui/src/i18n/no.ts | 7 +- .../kilo-vscode/webview-ui/src/i18n/pl.ts | 7 +- .../kilo-vscode/webview-ui/src/i18n/ru.ts | 7 +- .../kilo-vscode/webview-ui/src/i18n/th.ts | 7 +- .../kilo-vscode/webview-ui/src/i18n/tr.ts | 7 +- .../kilo-vscode/webview-ui/src/i18n/uk.ts | 7 +- .../kilo-vscode/webview-ui/src/i18n/zh.ts | 7 +- .../kilo-vscode/webview-ui/src/i18n/zht.ts | 7 +- .../webview-ui/src/styles/chat-layout.css | 16 +- .../webview-ui/src/types/messages/parts.ts | 15 ++ .../src/kilocode/plugins/model-usage.ts | 47 ++++-- .../src/kilocode/plugins/sidebar-usage.tsx | 44 ++++- packages/opencode/src/session/processor.ts | 7 + .../opencode/test/kilocode/tui/usage.test.ts | 49 +++++- packages/sdk/js/src/v2/gen/types.gen.ts | 8 + packages/sdk/openapi.json | 29 ++++ 34 files changed, 495 insertions(+), 163 deletions(-) diff --git a/packages/core/src/v1/session.ts b/packages/core/src/v1/session.ts index 2389e040155..bb193febc07 100644 --- a/packages/core/src/v1/session.ts +++ b/packages/core/src/v1/session.ts @@ -228,6 +228,16 @@ export const StepStartPart = Schema.Struct({ ...partBase, type: Schema.Literal("step-start"), snapshot: Schema.optional(Schema.String), + // kilocode_change start - wall-clock timestamps captured at the processor + // and consumed by the webview's weighted throughput aggregator. Marked + // optional so older persisted sessions (and synthetic messages) without + // timing still decode cleanly. + time: Schema.optional( + Schema.Struct({ + start: NonNegativeInt, + }), + ), + // kilocode_change end }).annotate({ identifier: "StepStartPart" }) export type StepStartPart = Types.DeepMutable> @@ -250,6 +260,18 @@ export const StepFinishPart = Schema.Struct({ source: Schema.Literals(["provider", "computed"]), }), ), + // Wall-clock timestamps + active generation duration captured at the + // session processor. The webview's weighted throughput aggregator uses + // `time.elapsed` (active model-generation duration in milliseconds, + // excluding tool execution and idle waiting) to weight the per-turn + // rate. Optional so legacy persisted sessions keep decoding. + time: Schema.optional( + Schema.Struct({ + start: NonNegativeInt, + end: NonNegativeInt, + elapsed: Schema.Finite, + }), + ), // kilocode_change end cost: Schema.Finite, tokens: Schema.Struct({ diff --git a/packages/kilo-vscode/tests/unit/session-utils.test.ts b/packages/kilo-vscode/tests/unit/session-utils.test.ts index 59ef6f546a5..d8ae8ff20bf 100644 --- a/packages/kilo-vscode/tests/unit/session-utils.test.ts +++ b/packages/kilo-vscode/tests/unit/session-utils.test.ts @@ -7,6 +7,8 @@ import { aggregateMetrics, latestMetrics, messageMetrics, + messageThroughput, + sessionThroughput, formatTG, buildFamilyCosts, buildFamilyParents, @@ -724,11 +726,29 @@ describe("collapseCostBreakdown", () => { // ── Throughput aggregation ───────────────────────────────────────────── -function stepFinish(id: string, metrics?: NonNullable): Part { +type StepFinishOverrides = { + metrics?: NonNullable + tokens?: { input: number; output: number; reasoning?: number; cache?: { read: number; write: number } } + time?: { start: number; end: number; elapsed: number } +} + +function stepFinish(id: string, metricsOrOverrides?: NonNullable | StepFinishOverrides): Part { + // Older call sites pass only metrics directly. Keep that signature so + // the existing latestMetrics / messageMetrics tests stay readable. + if (metricsOrOverrides && "metrics" in metricsOrOverrides === false && "tokens" in metricsOrOverrides === false && "time" in metricsOrOverrides === false) { + return { + type: "step-finish", + id, + ...(metricsOrOverrides ? { metrics: metricsOrOverrides } : {}), + } + } + const overrides = (metricsOrOverrides ?? {}) as StepFinishOverrides return { type: "step-finish", id, - ...(metrics ? { metrics } : {}), + ...(overrides.metrics ? { metrics: overrides.metrics } : {}), + ...(overrides.tokens ? { tokens: overrides.tokens } : {}), + ...(overrides.time ? { time: overrides.time } : {}), } } @@ -831,3 +851,130 @@ describe("throughput formatters", () => { expect(formatTG(Number.POSITIVE_INFINITY, locale)).toBe("–") }) }) + +// Weighted throughput — the value rendered beneath each assistant message +// after the v2 refactor. Behaves like a per-turn weighted average: total +// generated tokens across step-finish parts divided by total active +// model-generation duration, excluding tool-only or untimed steps. +describe("messageThroughput", () => { + it("returns undefined when no step-finish parts carry timing", () => { + const parts: Part[] = [ + { type: "step-start", id: "s1" }, + stepFinish("f1", { metrics: { generation: 100, source: "computed" } }), + ] + expect(messageThroughput(parts)).toBeUndefined() + }) + + it("computes a single-step rate from tokens and elapsed ms", () => { + const parts: Part[] = [ + stepFinish("f1", { + tokens: { input: 10, output: 200, reasoning: 0, cache: { read: 0, write: 0 } }, + time: { start: 0, end: 1000, elapsed: 1000 }, + }), + ] + // (200 + 0) * 1000 / 1000 = 200 + expect(messageThroughput(parts)).toEqual({ generation: 200, source: "computed" }) + }) + + it("weights multiple steps by their elapsed time rather than averaging rates", () => { + // Discriminating case: weighted = (300 * 1000 / 5000) = 60 t/s, + // last-wins = 50 t/s. Confirms the formula doesn't just take the final + // step's value. + const parts: Part[] = [ + stepFinish("f1", { + tokens: { input: 10, output: 100, reasoning: 0, cache: { read: 0, write: 0 } }, + time: { start: 0, end: 1000, elapsed: 1000 }, + }), + stepFinish("f2", { + tokens: { input: 10, output: 200, reasoning: 0, cache: { read: 0, write: 0 } }, + time: { start: 1000, end: 5000, elapsed: 4000 }, + }), + ] + expect(messageThroughput(parts)).toEqual({ generation: 60, source: "computed" }) + }) + + it("includes reasoning tokens in the numerator", () => { + const parts: Part[] = [ + stepFinish("f1", { + tokens: { input: 10, output: 100, reasoning: 200, cache: { read: 0, write: 0 } }, + time: { start: 0, end: 1000, elapsed: 1000 }, + }), + ] + // (100 + 200) * 1000 / 1000 = 300 + expect(messageThroughput(parts)).toEqual({ generation: 300, source: "computed" }) + }) + + it("ignores step-finish parts without timing", () => { + const parts: Part[] = [ + stepFinish("f1", { + tokens: { input: 10, output: 200, reasoning: 0, cache: { read: 0, write: 0 } }, + time: { start: 0, end: 1000, elapsed: 1000 }, + }), + // No `time` field — older part shape, possibly replayed session. + stepFinish("f2", { metrics: { generation: 999, source: "computed" } }), + ] + expect(messageThroughput(parts)).toEqual({ generation: 200, source: "computed" }) + }) + + it("ignores tool-only steps that produced no output tokens", () => { + const parts: Part[] = [ + stepFinish("f1", { + tokens: { input: 10, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, + time: { start: 0, end: 500, elapsed: 500 }, + }), + stepFinish("f2", { + tokens: { input: 10, output: 100, reasoning: 0, cache: { read: 0, write: 0 } }, + time: { start: 500, end: 1500, elapsed: 1000 }, + }), + ] + expect(messageThroughput(parts)).toEqual({ generation: 100, source: "computed" }) + }) + + it("returns undefined when only tool-only steps are present", () => { + const parts: Part[] = [ + stepFinish("f1", { + tokens: { input: 10, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, + time: { start: 0, end: 500, elapsed: 500 }, + }), + ] + expect(messageThroughput(parts)).toBeUndefined() + }) + + it("returns undefined when timing is non-positive across all steps", () => { + const parts: Part[] = [ + stepFinish("f1", { + tokens: { input: 10, output: 100, reasoning: 0, cache: { read: 0, write: 0 } }, + time: { start: 0, end: 0, elapsed: 0 }, + }), + ] + expect(messageThroughput(parts)).toBeUndefined() + }) +}) + +describe("sessionThroughput", () => { + it("aggregates the same way as messageThroughput across a flat part array", () => { + const parts: Part[] = [ + stepFinish("f1", { + tokens: { input: 10, output: 100, reasoning: 0, cache: { read: 0, write: 0 } }, + time: { start: 0, end: 1000, elapsed: 1000 }, + }), + stepFinish("f2", { + tokens: { input: 10, output: 200, reasoning: 0, cache: { read: 0, write: 0 } }, + time: { start: 2000, end: 5000, elapsed: 3000 }, + }), + // From the "next" message — still rolled up correctly. + stepFinish("f3", { + tokens: { input: 10, output: 500, reasoning: 0, cache: { read: 0, write: 0 } }, + time: { start: 6000, end: 11000, elapsed: 5000 }, + }), + ] + // (800 * 1000) / 9000 = 88.888... + const result = sessionThroughput(parts) + expect(result?.source).toBe("computed") + expect(result?.generation).toBeCloseTo((800 * 1000) / 9000, 5) + }) + + it("returns undefined for empty input", () => { + expect(sessionThroughput([])).toBeUndefined() + }) +}) diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/AssistantMessage.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/AssistantMessage.tsx index 9a531ccb80e..5a8182c77f9 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/AssistantMessage.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/AssistantMessage.tsx @@ -25,11 +25,10 @@ import { useLanguage } from "../../context/language" import { useServer } from "../../context/server" import { planDisplayPath } from "../../utils/plan-path" import { isRenderable, UPSTREAM_SUPPRESSED_TOOLS } from "../../utils/transcript-parts" -import { messageMetrics, formatTG } from "../../context/session-utils" +import { messageThroughput, formatTG } from "../../context/session-utils" import { color as timelineColor } from "../../utils/timeline/colors" import type { Part as TimelinePart } from "../../types/messages" import type { TimelineHighlight } from "../../utils/timeline/highlight" -import { Icon } from "@kilocode/kilo-ui/icon" import { Tooltip } from "@kilocode/kilo-ui/tooltip" import { QuestionDock } from "./QuestionDock" import { SuggestBar } from "./SuggestBar" @@ -167,28 +166,31 @@ function BashToolCard(props: { part: ToolPart; defaultOpen: boolean; forceOpen?: ) } -/** Compact generation-speed line shown beneath the last assistant message. - * Only renders when the user has opted in via the - * `kilo-code.new.showTokenThroughput` setting and the message has a - * step-finish part that carries throughput metrics. Renders as plain text - * prefixed by a gauge icon that matches the description-foreground tone of - * the Tokens row in the task header — no pill, no border. */ -function ThroughputBadge(props: { metrics: NonNullable> }) { +/** Plain-text generation-speed value shown beneath an assistant message. + * + * Renders as muted metadata — no icon, no background, no border — so it + * reads as a one-line footer rather than an interactive control. The + * description on hover explains that the value is a weighted generation + * rate across the turn's model-generation steps (output + reasoning + * tokens over active generation time). + * + * Visibility is gated by the same `kilo-code.new.showTokenThroughput` + * toggle that previously controlled the multi-row badge. The metric only + * renders when the message has at least one step-finish part carrying both + * a token count and elapsed timing. + */ +function ThroughputBadge(props: { metrics: { generation?: number } }) { const language = useLanguage() const speedText = createMemo(() => formatTG(props.metrics.generation, language.locale())) - const label = createMemo(() => language.t("chat.throughput.speed.row", { speed: speedText() })) const tooltip = createMemo(() => { - if (props.metrics.generation !== undefined) { - return language.t("chat.throughput.speed.tooltip", { speed: speedText() }) + if (props.metrics.generation === undefined) { + return language.t("chat.throughput.tooltip.missing") } - return language.t("chat.throughput.speed.tooltip.missing") + return language.t("chat.throughput.tooltip", { speed: speedText() }) }) return ( - - - {label()} - + {speedText()} ) } @@ -217,11 +219,18 @@ export const AssistantMessage: Component = (props) => { return !!matchToolRequest(part, "question", session.questions()) }) }) - // Pull the latest step-finish metrics for this message so the per-message - // badge can render its generation rate when the toggle is on. + // Pull the weighted generation rate across the turn's step-finish parts + // (output + reasoning tokens over active generation duration) so the badge + // represents the turn as a whole rather than whichever step happened to + // finish most recently. We intentionally read from the full message parts + // in the data store rather than `props.parts` — the parent chunks + // messages into rows of ~8 parts, and step-finish may land in a row + // different from the one currently rendered. const throughput = createMemo(() => - messageMetrics( - (props.parts ?? (data.store.part?.[props.message.id] as TimelinePart[] | undefined) ?? []) as TimelinePart[], + messageThroughput( + (data.store.part?.[props.message.id] as TimelinePart[] | undefined) ?? + (props.parts as TimelinePart[] | undefined) ?? + ([] as TimelinePart[]), ), ) return ( diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/TaskHeader.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/TaskHeader.tsx index a74ecf7adc8..ee4e473f736 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/TaskHeader.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/TaskHeader.tsx @@ -14,8 +14,7 @@ import { Tooltip } from "@kilocode/kilo-ui/tooltip" import { Icon } from "@kilocode/kilo-ui/icon" import { Checkbox } from "@kilocode/kilo-ui/checkbox" import { useSession } from "../../context/session" -import { useDisplay } from "../../context/display" -import { calcTokenUsage, collapseCostBreakdown, latestMetrics } from "../../context/session-utils" +import { calcTokenUsage, collapseCostBreakdown } from "../../context/session-utils" import { useLanguage } from "../../context/language" import { useVSCode } from "../../context/vscode" import { TaskTimeline } from "./TaskTimeline" @@ -36,7 +35,6 @@ export const TaskHeader: Component = (props) => { const session = useSession() const language = useLanguage() const search = useTranscriptSearch() - const display = useDisplay() const title = createMemo(() => session.currentSession()?.title ?? language.t("command.session.new")) const canRename = createMemo(() => !props.readonly && !!session.currentSession()) @@ -88,24 +86,8 @@ export const TaskHeader: Component = (props) => { return false }) - // Throughput is the latest step-finish snapshot across the session so the - // figure reflects the most recent assistant turn rather than a session-wide - // average. Result is consumed by the TaskUsage summary row, which renders - // the value inline with the token counts — there is no standalone element. - const throughput = createMemo(() => { - const all = session.allParts() as Record - const flat: Part[] = [] - for (const parts of Object.values(all)) { - for (const part of parts) flat.push(part) - } - return latestMetrics(flat) - }) - const vscode = useVSCode() const [expanded, setExpanded] = createSignal(true) - // Throughput row visibility is shared with AssistantMessage via the - // DisplayProvider so the setting toggles both surfaces together. - const throughputVisible = createMemo(() => display.throughputVisible()) // Read initial value from VS Code settings onMount(() => vscode.postMessage({ type: "requestTimelineSetting" })) @@ -310,7 +292,6 @@ export const TaskHeader: Component = (props) => { )} diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/TaskUsage.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/TaskUsage.tsx index 9d96f225272..042a8754db2 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/TaskUsage.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/TaskUsage.tsx @@ -2,22 +2,15 @@ import type { Component } from "solid-js" import { For, Show, createMemo } from "solid-js" import { Collapsible } from "@kilocode/kilo-ui/collapsible" import { Icon } from "@kilocode/kilo-ui/icon" -import { Tooltip } from "@kilocode/kilo-ui/tooltip" import { useLanguage } from "../../context/language" import { useProvider } from "../../context/provider" import type { SessionModelUsage } from "../../types/messages" import { groupModelUsage, modelUsageName, type TokenSummary } from "../../context/model-usage" import { formatCompactCount } from "../../utils/format" -import { formatTG } from "../../context/session-utils" interface TaskUsageProps { tokens: TokenSummary usage?: SessionModelUsage - /** Latest text-generation rate (tokens/sec) for the session. Renders inline - * in the summary row when defined, prefixed by a gauge icon, matching the - * existing token-value spans so the row reads as one line of secondary - * info. */ - throughput?: number defaultOpen?: boolean } @@ -25,7 +18,6 @@ export const TaskUsage: Component = (props) => { const language = useLanguage() const provider = useProvider() const groups = createMemo(() => groupModelUsage(props.usage?.models ?? [], provider.providers())) - const speedText = createMemo(() => formatTG(props.throughput, language.locale())) const money = createMemo( () => new Intl.NumberFormat(language.locale(), { @@ -70,14 +62,6 @@ export const TaskUsage: Component = (props) => { {number(props.tokens.output)}
- - - - - {language.t("chat.throughput.speed.label")} {speedText()} - - - ) diff --git a/packages/kilo-vscode/webview-ui/src/context/session-utils.ts b/packages/kilo-vscode/webview-ui/src/context/session-utils.ts index a5a67de3ab8..5e5aa367448 100644 --- a/packages/kilo-vscode/webview-ui/src/context/session-utils.ts +++ b/packages/kilo-vscode/webview-ui/src/context/session-utils.ts @@ -251,6 +251,49 @@ export function messageMetrics(parts: readonly Part[]): { generation?: number; s return latestMetrics(parts) } +/** + * Weighted generation throughput for a single assistant message. Aggregates + * output + reasoning tokens across every step-finish part against the sum of + * their active model-generation durations, so the displayed value represents + * the turn rather than whichever step happened to finish last. + * + * Steps without `time.elapsed`, with non-positive `elapsed`, or with no + * generated tokens are skipped — tool-only steps, idempotent cache hits, + * and tool re-execution should not skew the figure. + */ +export function messageThroughput( + parts: readonly Part[], +): { generation?: number; source: "computed" } | undefined { + let generated = 0 + let elapsedMs = 0 + for (const part of parts) { + if (part.type !== "step-finish") continue + const time = part.time + if (!time || !Number.isFinite(time.elapsed) || time.elapsed <= 0) continue + const tokens = part.tokens + if (!tokens) continue + const stepGenerated = tokens.output + (tokens.reasoning ?? 0) + if (stepGenerated <= 0) continue + generated += stepGenerated + elapsedMs += time.elapsed + } + if (generated <= 0 || elapsedMs <= 0) return undefined + const generation = (generated * 1000) / elapsedMs + if (!Number.isFinite(generation) || generation <= 0) return undefined + return { generation, source: "computed" } +} + +/** + * Weighted generation throughput across the flat array of parts from every + * message in a session. Same weighted semantics as `messageThroughput` — + * useful when a caller has already flattened parts across messages. + */ +export function sessionThroughput( + parts: readonly Part[], +): { generation?: number; source: "computed" } | undefined { + return messageThroughput(parts) +} + /** * Format a text-generation rate for display. Shared by every rendering site * so the same value reads the same in the per-message badge and the diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts index 623b82e327f..8845b0d1ed2 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts @@ -1684,10 +1684,9 @@ export const dict = { "settings.display.tokenThroughput.description": "Display the text-generation rate (tokens/sec) on the latest assistant message and in the task header. Hidden by default to keep the chat uncluttered.", - "chat.throughput.speed.label": "Generation speed", - "chat.throughput.speed.row": "Generation speed {{speed}}", - "chat.throughput.speed.tooltip": "Generation speed {{speed}} (computed from step duration)", - "chat.throughput.speed.tooltip.missing": "Throughput metrics unavailable for this step", + "chat.throughput.tooltip": "Average {{speed}} tokens/s for this turn. Includes output and reasoning tokens; excludes tool execution and waiting time.", + "chat.throughput.tooltip.missing": "Throughput metrics unavailable for this turn.", + "settings.providers.defaultModel.title": "النموذج الافتراضي", "settings.providers.defaultModel.description": "النموذج الأساسي للمحادثات", "settings.providers.smallModel.title": "نموذج صغير", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/br.ts b/packages/kilo-vscode/webview-ui/src/i18n/br.ts index 6fb84632efb..2d3c70a77b7 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/br.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/br.ts @@ -1735,10 +1735,9 @@ export const dict = { "settings.display.tokenThroughput.description": "Display the text-generation rate (tokens/sec) on the latest assistant message and in the task header. Hidden by default to keep the chat uncluttered.", - "chat.throughput.speed.label": "Generation speed", - "chat.throughput.speed.row": "Generation speed {{speed}}", - "chat.throughput.speed.tooltip": "Generation speed {{speed}} (computed from step duration)", - "chat.throughput.speed.tooltip.missing": "Throughput metrics unavailable for this step", + "chat.throughput.tooltip": "Average {{speed}} tokens/s for this turn. Includes output and reasoning tokens; excludes tool execution and waiting time.", + "chat.throughput.tooltip.missing": "Throughput metrics unavailable for this turn.", + "settings.providers.defaultModel.title": "Modelo padrão", "settings.providers.defaultModel.description": "Modelo principal para conversas", "settings.providers.smallModel.title": "Modelo pequeno", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/bs.ts b/packages/kilo-vscode/webview-ui/src/i18n/bs.ts index b17985f31bb..f507e943ffe 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/bs.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/bs.ts @@ -1727,10 +1727,9 @@ export const dict = { "settings.display.tokenThroughput.description": "Display the text-generation rate (tokens/sec) on the latest assistant message and in the task header. Hidden by default to keep the chat uncluttered.", - "chat.throughput.speed.label": "Generation speed", - "chat.throughput.speed.row": "Generation speed {{speed}}", - "chat.throughput.speed.tooltip": "Generation speed {{speed}} (computed from step duration)", - "chat.throughput.speed.tooltip.missing": "Throughput metrics unavailable for this step", + "chat.throughput.tooltip": "Average {{speed}} tokens/s for this turn. Includes output and reasoning tokens; excludes tool execution and waiting time.", + "chat.throughput.tooltip.missing": "Throughput metrics unavailable for this turn.", + "settings.providers.defaultModel.title": "Zadani model", "settings.providers.defaultModel.description": "Primarni model za razgovore", "settings.providers.smallModel.title": "Mali model", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/da.ts b/packages/kilo-vscode/webview-ui/src/i18n/da.ts index 0e4cdba5d52..f49494e083c 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/da.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/da.ts @@ -1718,10 +1718,9 @@ export const dict = { "settings.display.tokenThroughput.description": "Display the text-generation rate (tokens/sec) on the latest assistant message and in the task header. Hidden by default to keep the chat uncluttered.", - "chat.throughput.speed.label": "Generation speed", - "chat.throughput.speed.row": "Generation speed {{speed}}", - "chat.throughput.speed.tooltip": "Generation speed {{speed}} (computed from step duration)", - "chat.throughput.speed.tooltip.missing": "Throughput metrics unavailable for this step", + "chat.throughput.tooltip": "Average {{speed}} tokens/s for this turn. Includes output and reasoning tokens; excludes tool execution and waiting time.", + "chat.throughput.tooltip.missing": "Throughput metrics unavailable for this turn.", + "settings.providers.defaultModel.title": "Standardmodel", "settings.providers.defaultModel.description": "Primær model til samtaler", "settings.providers.smallModel.title": "Lille model", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/de.ts b/packages/kilo-vscode/webview-ui/src/i18n/de.ts index ad03129db88..921baedb71b 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/de.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/de.ts @@ -1754,10 +1754,9 @@ export const dict = { "settings.display.tokenThroughput.description": "Display the text-generation rate (tokens/sec) on the latest assistant message and in the task header. Hidden by default to keep the chat uncluttered.", - "chat.throughput.speed.label": "Generation speed", - "chat.throughput.speed.row": "Generation speed {{speed}}", - "chat.throughput.speed.tooltip": "Generation speed {{speed}} (computed from step duration)", - "chat.throughput.speed.tooltip.missing": "Throughput metrics unavailable for this step", + "chat.throughput.tooltip": "Average {{speed}} tokens/s for this turn. Includes output and reasoning tokens; excludes tool execution and waiting time.", + "chat.throughput.tooltip.missing": "Throughput metrics unavailable for this turn.", + "settings.providers.defaultModel.title": "Standardmodell", "settings.providers.defaultModel.description": "Primäres Modell für Gespräche", "settings.providers.smallModel.title": "Kleines Modell", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/en.ts b/packages/kilo-vscode/webview-ui/src/i18n/en.ts index 7710e4558be..97d499110c9 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/en.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/en.ts @@ -1695,10 +1695,8 @@ export const dict = { "settings.display.tokenThroughput.description": "Display the text-generation rate (tokens/sec) on the latest assistant message and in the task header. Hidden by default to keep the chat uncluttered.", - "chat.throughput.speed.label": "Generation speed", - "chat.throughput.speed.row": "Generation speed {{speed}}", - "chat.throughput.speed.tooltip": "Generation speed {{speed}} (computed from step duration)", - "chat.throughput.speed.tooltip.missing": "Throughput metrics unavailable for this step", + "chat.throughput.tooltip": "Average {{speed}} tokens/s for this turn. Includes output and reasoning tokens; excludes tool execution and waiting time.", + "chat.throughput.tooltip.missing": "Throughput metrics unavailable for this turn.", "settings.providers.defaultModel.title": "Default Model", "settings.providers.defaultModel.description": "Primary model for conversations", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/es.ts b/packages/kilo-vscode/webview-ui/src/i18n/es.ts index e3be41ca4cb..e2e97ef60c5 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/es.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/es.ts @@ -1743,10 +1743,9 @@ export const dict = { "settings.display.tokenThroughput.description": "Display the text-generation rate (tokens/sec) on the latest assistant message and in the task header. Hidden by default to keep the chat uncluttered.", - "chat.throughput.speed.label": "Generation speed", - "chat.throughput.speed.row": "Generation speed {{speed}}", - "chat.throughput.speed.tooltip": "Generation speed {{speed}} (computed from step duration)", - "chat.throughput.speed.tooltip.missing": "Throughput metrics unavailable for this step", + "chat.throughput.tooltip": "Average {{speed}} tokens/s for this turn. Includes output and reasoning tokens; excludes tool execution and waiting time.", + "chat.throughput.tooltip.missing": "Throughput metrics unavailable for this turn.", + "settings.providers.defaultModel.title": "Modelo predeterminado", "settings.providers.defaultModel.description": "Modelo principal para conversaciones", "settings.providers.smallModel.title": "Modelo pequeño", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts index e4e393c1201..e13d5c0ea81 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts @@ -1764,10 +1764,9 @@ export const dict = { "settings.display.tokenThroughput.description": "Display the text-generation rate (tokens/sec) on the latest assistant message and in the task header. Hidden by default to keep the chat uncluttered.", - "chat.throughput.speed.label": "Generation speed", - "chat.throughput.speed.row": "Generation speed {{speed}}", - "chat.throughput.speed.tooltip": "Generation speed {{speed}} (computed from step duration)", - "chat.throughput.speed.tooltip.missing": "Throughput metrics unavailable for this step", + "chat.throughput.tooltip": "Average {{speed}} tokens/s for this turn. Includes output and reasoning tokens; excludes tool execution and waiting time.", + "chat.throughput.tooltip.missing": "Throughput metrics unavailable for this turn.", + "settings.providers.defaultModel.title": "Modèle par défaut", "settings.providers.defaultModel.description": "Modèle principal pour les conversations", "settings.providers.smallModel.title": "Petit modèle", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/it.ts b/packages/kilo-vscode/webview-ui/src/i18n/it.ts index 96b3f1240dd..173d53d5f0d 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/it.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/it.ts @@ -1521,10 +1521,9 @@ export const dict = { "settings.display.tokenThroughput.description": "Display the text-generation rate (tokens/sec) on the latest assistant message and in the task header. Hidden by default to keep the chat uncluttered.", - "chat.throughput.speed.label": "Generation speed", - "chat.throughput.speed.row": "Generation speed {{speed}}", - "chat.throughput.speed.tooltip": "Generation speed {{speed}} (computed from step duration)", - "chat.throughput.speed.tooltip.missing": "Throughput metrics unavailable for this step", + "chat.throughput.tooltip": "Average {{speed}} tokens/s for this turn. Includes output and reasoning tokens; excludes tool execution and waiting time.", + "chat.throughput.tooltip.missing": "Throughput metrics unavailable for this turn.", + "settings.providers.defaultModel.title": "Modello predefinito", "settings.providers.defaultModel.description": "Modello principale per le conversazioni", "settings.providers.smallModel.title": "Modello leggero", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts index 403a7cb9e7c..134202bd7a4 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts @@ -1713,10 +1713,9 @@ export const dict = { "settings.display.tokenThroughput.description": "Display the text-generation rate (tokens/sec) on the latest assistant message and in the task header. Hidden by default to keep the chat uncluttered.", - "chat.throughput.speed.label": "Generation speed", - "chat.throughput.speed.row": "Generation speed {{speed}}", - "chat.throughput.speed.tooltip": "Generation speed {{speed}} (computed from step duration)", - "chat.throughput.speed.tooltip.missing": "Throughput metrics unavailable for this step", + "chat.throughput.tooltip": "Average {{speed}} tokens/s for this turn. Includes output and reasoning tokens; excludes tool execution and waiting time.", + "chat.throughput.tooltip.missing": "Throughput metrics unavailable for this turn.", + "settings.providers.defaultModel.title": "デフォルトモデル", "settings.providers.defaultModel.description": "会話のプライマリモデル", "settings.providers.smallModel.title": "小型モデル", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts index 847cb398eb4..be7462a9936 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts @@ -1695,10 +1695,9 @@ export const dict = { "settings.display.tokenThroughput.description": "Display the text-generation rate (tokens/sec) on the latest assistant message and in the task header. Hidden by default to keep the chat uncluttered.", - "chat.throughput.speed.label": "Generation speed", - "chat.throughput.speed.row": "Generation speed {{speed}}", - "chat.throughput.speed.tooltip": "Generation speed {{speed}} (computed from step duration)", - "chat.throughput.speed.tooltip.missing": "Throughput metrics unavailable for this step", + "chat.throughput.tooltip": "Average {{speed}} tokens/s for this turn. Includes output and reasoning tokens; excludes tool execution and waiting time.", + "chat.throughput.tooltip.missing": "Throughput metrics unavailable for this turn.", + "settings.providers.defaultModel.title": "기본 모델", "settings.providers.defaultModel.description": "대화의 기본 모델", "settings.providers.smallModel.title": "소형 모델", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/nl.ts b/packages/kilo-vscode/webview-ui/src/i18n/nl.ts index 0c148f307bf..2a77b8fa17c 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/nl.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/nl.ts @@ -1695,10 +1695,9 @@ export const dict = { "settings.display.tokenThroughput.description": "Display the text-generation rate (tokens/sec) on the latest assistant message and in the task header. Hidden by default to keep the chat uncluttered.", - "chat.throughput.speed.label": "Generation speed", - "chat.throughput.speed.row": "Generation speed {{speed}}", - "chat.throughput.speed.tooltip": "Generation speed {{speed}} (computed from step duration)", - "chat.throughput.speed.tooltip.missing": "Throughput metrics unavailable for this step", + "chat.throughput.tooltip": "Average {{speed}} tokens/s for this turn. Includes output and reasoning tokens; excludes tool execution and waiting time.", + "chat.throughput.tooltip.missing": "Throughput metrics unavailable for this turn.", + "settings.providers.defaultModel.title": "Standaard Model", "settings.providers.defaultModel.description": "Primair model voor gesprekken", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/no.ts b/packages/kilo-vscode/webview-ui/src/i18n/no.ts index 46093e0f655..d4dc818691e 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/no.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/no.ts @@ -1716,10 +1716,9 @@ export const dict = { "settings.display.tokenThroughput.description": "Vis tekstgenereringshastighet (tokens/sek) på den siste assistentmeldingen og i oppgaveoverskriften. Skjult som standard for å holde chatten ryddig.", - "chat.throughput.speed.label": "Genereringshastighet", - "chat.throughput.speed.row": "Genereringshastighet {{speed}}", - "chat.throughput.speed.tooltip": "Genereringshastighet {{speed}} (beregnet fra stegvarighet)", - "chat.throughput.speed.tooltip.missing": "Gjennomstrømningsmålinger er ikke tilgjengelige for dette steget", + "chat.throughput.tooltip": "Average {{speed}} tokens/s for this turn. Includes output and reasoning tokens; excludes tool execution and waiting time.", + "chat.throughput.tooltip.missing": "Throughput metrics unavailable for this turn.", + "settings.providers.defaultModel.title": "Standardmodell", "settings.providers.defaultModel.description": "Primær modell for samtaler", "settings.providers.smallModel.title": "Liten modell", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts index 6174ee22e4c..1257f14df33 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts @@ -1726,10 +1726,9 @@ export const dict = { "settings.display.tokenThroughput.description": "Display the text-generation rate (tokens/sec) on the latest assistant message and in the task header. Hidden by default to keep the chat uncluttered.", - "chat.throughput.speed.label": "Generation speed", - "chat.throughput.speed.row": "Generation speed {{speed}}", - "chat.throughput.speed.tooltip": "Generation speed {{speed}} (computed from step duration)", - "chat.throughput.speed.tooltip.missing": "Throughput metrics unavailable for this step", + "chat.throughput.tooltip": "Average {{speed}} tokens/s for this turn. Includes output and reasoning tokens; excludes tool execution and waiting time.", + "chat.throughput.tooltip.missing": "Throughput metrics unavailable for this turn.", + "settings.providers.defaultModel.title": "Domyślny model", "settings.providers.defaultModel.description": "Główny model do rozmów", "settings.providers.smallModel.title": "Mały model", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts index c40e3cab15d..122c9fa50bd 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts @@ -1724,10 +1724,9 @@ export const dict = { "settings.display.tokenThroughput.description": "Display the text-generation rate (tokens/sec) on the latest assistant message and in the task header. Hidden by default to keep the chat uncluttered.", - "chat.throughput.speed.label": "Generation speed", - "chat.throughput.speed.row": "Generation speed {{speed}}", - "chat.throughput.speed.tooltip": "Generation speed {{speed}} (computed from step duration)", - "chat.throughput.speed.tooltip.missing": "Throughput metrics unavailable for this step", + "chat.throughput.tooltip": "Average {{speed}} tokens/s for this turn. Includes output and reasoning tokens; excludes tool execution and waiting time.", + "chat.throughput.tooltip.missing": "Throughput metrics unavailable for this turn.", + "settings.providers.defaultModel.title": "Модель по умолчанию", "settings.providers.defaultModel.description": "Основная модель для разговоров", "settings.providers.smallModel.title": "Малая модель", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/th.ts b/packages/kilo-vscode/webview-ui/src/i18n/th.ts index 24e3c2b3479..5794fa64660 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/th.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/th.ts @@ -1693,10 +1693,9 @@ export const dict = { "settings.display.tokenThroughput.description": "Display the text-generation rate (tokens/sec) on the latest assistant message and in the task header. Hidden by default to keep the chat uncluttered.", - "chat.throughput.speed.label": "Generation speed", - "chat.throughput.speed.row": "Generation speed {{speed}}", - "chat.throughput.speed.tooltip": "Generation speed {{speed}} (computed from step duration)", - "chat.throughput.speed.tooltip.missing": "Throughput metrics unavailable for this step", + "chat.throughput.tooltip": "Average {{speed}} tokens/s for this turn. Includes output and reasoning tokens; excludes tool execution and waiting time.", + "chat.throughput.tooltip.missing": "Throughput metrics unavailable for this turn.", + "settings.providers.defaultModel.title": "โมเดลเริ่มต้น", "settings.providers.defaultModel.description": "โมเดลหลักสำหรับบทสนทนา", "settings.providers.smallModel.title": "โมเดลขนาดเล็ก", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/tr.ts b/packages/kilo-vscode/webview-ui/src/i18n/tr.ts index 4ad861c6ba2..c9242d7baf6 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/tr.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/tr.ts @@ -1682,10 +1682,9 @@ export const dict = { "settings.display.tokenThroughput.description": "Display the text-generation rate (tokens/sec) on the latest assistant message and in the task header. Hidden by default to keep the chat uncluttered.", - "chat.throughput.speed.label": "Generation speed", - "chat.throughput.speed.row": "Generation speed {{speed}}", - "chat.throughput.speed.tooltip": "Generation speed {{speed}} (computed from step duration)", - "chat.throughput.speed.tooltip.missing": "Throughput metrics unavailable for this step", + "chat.throughput.tooltip": "Average {{speed}} tokens/s for this turn. Includes output and reasoning tokens; excludes tool execution and waiting time.", + "chat.throughput.tooltip.missing": "Throughput metrics unavailable for this turn.", + "settings.providers.defaultModel.title": "Varsayılan Model", "settings.providers.defaultModel.description": "Sohbetler için birincil model", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/uk.ts b/packages/kilo-vscode/webview-ui/src/i18n/uk.ts index 977843fb431..fd882d1475b 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/uk.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/uk.ts @@ -1679,10 +1679,9 @@ export const dict = { "settings.display.tokenThroughput.description": "Display the text-generation rate (tokens/sec) on the latest assistant message and in the task header. Hidden by default to keep the chat uncluttered.", - "chat.throughput.speed.label": "Generation speed", - "chat.throughput.speed.row": "Generation speed {{speed}}", - "chat.throughput.speed.tooltip": "Generation speed {{speed}} (computed from step duration)", - "chat.throughput.speed.tooltip.missing": "Throughput metrics unavailable for this step", + "chat.throughput.tooltip": "Average {{speed}} tokens/s for this turn. Includes output and reasoning tokens; excludes tool execution and waiting time.", + "chat.throughput.tooltip.missing": "Throughput metrics unavailable for this turn.", + "settings.providers.defaultModel.title": "Модель за замовчуванням", "settings.providers.defaultModel.description": "Основна модель для чатів", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts index b54117530d1..bd4ffc39b7e 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts @@ -1645,10 +1645,9 @@ export const dict = { "settings.display.tokenThroughput.description": "Display the text-generation rate (tokens/sec) on the latest assistant message and in the task header. Hidden by default to keep the chat uncluttered.", - "chat.throughput.speed.label": "Generation speed", - "chat.throughput.speed.row": "Generation speed {{speed}}", - "chat.throughput.speed.tooltip": "Generation speed {{speed}} (computed from step duration)", - "chat.throughput.speed.tooltip.missing": "Throughput metrics unavailable for this step", + "chat.throughput.tooltip": "Average {{speed}} tokens/s for this turn. Includes output and reasoning tokens; excludes tool execution and waiting time.", + "chat.throughput.tooltip.missing": "Throughput metrics unavailable for this turn.", + "settings.providers.defaultModel.title": "默认模型", "settings.providers.defaultModel.description": "对话的主要模型", "settings.providers.smallModel.title": "小模型", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts index 3061b4da6da..0818197b86a 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts @@ -1609,10 +1609,9 @@ export const dict = { "settings.display.tokenThroughput.description": "Display the text-generation rate (tokens/sec) on the latest assistant message and in the task header. Hidden by default to keep the chat uncluttered.", - "chat.throughput.speed.label": "Generation speed", - "chat.throughput.speed.row": "Generation speed {{speed}}", - "chat.throughput.speed.tooltip": "Generation speed {{speed}} (computed from step duration)", - "chat.throughput.speed.tooltip.missing": "Throughput metrics unavailable for this step", + "chat.throughput.tooltip": "Average {{speed}} tokens/s for this turn. Includes output and reasoning tokens; excludes tool execution and waiting time.", + "chat.throughput.tooltip.missing": "Throughput metrics unavailable for this turn.", + "settings.providers.defaultModel.title": "預設模型", "settings.providers.defaultModel.description": "對話的主要模型", "settings.providers.smallModel.title": "小模型", diff --git a/packages/kilo-vscode/webview-ui/src/styles/chat-layout.css b/packages/kilo-vscode/webview-ui/src/styles/chat-layout.css index aab1391442f..da109b20030 100644 --- a/packages/kilo-vscode/webview-ui/src/styles/chat-layout.css +++ b/packages/kilo-vscode/webview-ui/src/styles/chat-layout.css @@ -228,18 +228,18 @@ width: 100%; } -[data-component="assistant-throughput-badge"] { - /* Plain-text throughput display — matches the muted descriptionForeground - * the Tokens row uses so the two read as the same family of secondary - * info. No pill, no border, no background; just text. */ - display: inline-flex; - align-items: center; - gap: 4px; +[data-component="assistant-throughput"] { + /* Plain-text generation-speed footer beneath an assistant message. + * Matches the muted description-foreground tone used elsewhere for session + * metadata and reads as a single line of tertiary info — no icon, no + * border, no background, so it never competes with the upstream action + * row (copy / feedback) for visual weight. */ + display: block; + margin-top: 4px; color: var(--vscode-descriptionForeground); font-family: var(--font-family-sans); font-size: var(--kilo-font-size-11); line-height: var(--line-height-normal); - white-space: nowrap; font-variant-numeric: tabular-nums; } diff --git a/packages/kilo-vscode/webview-ui/src/types/messages/parts.ts b/packages/kilo-vscode/webview-ui/src/types/messages/parts.ts index 2a69f9963d7..00624cba3ac 100644 --- a/packages/kilo-vscode/webview-ui/src/types/messages/parts.ts +++ b/packages/kilo-vscode/webview-ui/src/types/messages/parts.ts @@ -62,6 +62,12 @@ export interface ReasoningPart extends BasePart { // Step parts from the backend export interface StepStartPart extends BasePart { type: "step-start" + // Wall-clock timestamps captured at the processor when the LLM stream + // emits `step-start`. Used by the webview to compute per-message + // throughput as a weighted aggregate of step durations. + time?: { + start: number + } } // Tokens-per-second throughput metrics reported by the backend on step-finish. @@ -79,6 +85,15 @@ export interface StepThroughputMetrics { export interface StepFinishPart extends BasePart { type: "step-finish" reason?: string + // Wall-clock timestamps captured at the processor across the LLM step. + // `elapsed` is the active model-generation duration in milliseconds — it + // excludes tool execution and idle waiting — and is what the webview uses + // to weight the throughput aggregate. + time?: { + start: number + end: number + elapsed: number + } model?: { providerID: string modelID: string diff --git a/packages/opencode/src/kilocode/plugins/model-usage.ts b/packages/opencode/src/kilocode/plugins/model-usage.ts index 7c796e2a671..c6d5c470a68 100644 --- a/packages/opencode/src/kilocode/plugins/model-usage.ts +++ b/packages/opencode/src/kilocode/plugins/model-usage.ts @@ -92,15 +92,29 @@ export function formatCost(input: number) { // Local aggregation of step-finish metrics for the sidebar/usage panel. // -// We surface the *most recent* generation rate (last non-empty sample wins) -// so the displayed figure tracks the latest assistant turn the user is -// waiting on rather than a weighted session-wide average that drifts toward -// long-ago turns. Steps with no generated output (e.g. tool-only steps) -// contribute nothing — they simply don't replace the running snapshot. +// When samples carry `elapsedMs` (kilocode_change: persisted on the +// step-finish part by the session processor) and matching `generated` +// counts, the figure is the *weighted* generation rate across the +// aggregated steps — total generated tokens over total active +// model-generation duration. That excludes tool execution and idle waiting +// so the value represents what the user paid for. +// +// When timing is missing or non-positive, the function falls back to the +// historical last-wins snapshot so older callers that haven't migrated to +// the new wire shape continue to surface a meaningful figure rather than +// silently dropping to `undefined`. export function aggregateMetrics( - samples: ReadonlyArray<{ metrics?: StepMetrics; generated: number }>, + samples: ReadonlyArray<{ + metrics?: StepMetrics + generated: number + elapsedMs?: number + output?: number + reasoning?: number + }>, ): AggregatedMetrics { - let generation: number | undefined + let generatedTotal = 0 + let elapsedTotal = 0 + let fallback: number | undefined for (const sample of samples) { const metrics = sample.metrics if (!metrics) continue @@ -108,10 +122,25 @@ export function aggregateMetrics( if (typeof value !== "number" || !Number.isFinite(value)) continue if (value <= 0) continue if (sample.generated <= 0) continue - generation = value + fallback = value + const elapsed = sample.elapsedMs + if (typeof elapsed !== "number" || !Number.isFinite(elapsed) || elapsed <= 0) continue + const tokens = + typeof sample.output === "number" && typeof sample.reasoning === "number" + ? sample.output + sample.reasoning + : sample.generated + if (tokens <= 0) continue + generatedTotal += tokens + elapsedTotal += elapsed + } + if (generatedTotal > 0 && elapsedTotal > 0) { + const weighted = (generatedTotal * 1000) / elapsedTotal + if (Number.isFinite(weighted) && weighted > 0) { + return { generation: weighted } + } } return { - ...(generation !== undefined ? { generation } : {}), + ...(fallback !== undefined ? { generation: fallback } : {}), } } diff --git a/packages/opencode/src/kilocode/plugins/sidebar-usage.tsx b/packages/opencode/src/kilocode/plugins/sidebar-usage.tsx index 1e55a7d1e34..7a54f4394de 100644 --- a/packages/opencode/src/kilocode/plugins/sidebar-usage.tsx +++ b/packages/opencode/src/kilocode/plugins/sidebar-usage.tsx @@ -24,7 +24,13 @@ import { ModelRow, UsageRow } from "@/kilocode/plugins/sidebar-usage-row" const id = "internal:kilo-sidebar-usage" -type MetricSample = { metrics?: StepMetrics; generated: number } +type MetricSample = { + metrics?: StepMetrics + generated: number + elapsedMs?: number + output?: number + reasoning?: number +} function View(props: { api: TuiPluginApi; session_id: string }) { const [usageOpen, setUsageOpen] = createSignal(true) @@ -83,12 +89,35 @@ function View(props: { api: TuiPluginApi; session_id: string }) { const refresh = () => void refetch() const related = (sessionID: string, info?: ReturnType) => isSessionTreeMember({ root: props.session_id, sessionID, info, get: props.api.state.session.get }) - const recordSample = (sessionID: string, part: { type?: string; metrics?: unknown; tokens?: unknown }) => { + const recordSample = ( + sessionID: string, + part: { + type?: string + metrics?: unknown + tokens?: unknown + // Loose time shape — different part kinds (e.g. retry) ship their own + // time fields; we only care about `elapsed` for step-finish weighting. + time?: { elapsed?: number; [k: string]: unknown } + }, + ) => { if (part.type !== "step-finish") return if (!related(sessionID)) return const metrics = isStepMetrics(part.metrics) ? part.metrics : undefined const generated = generatedTokens(part.tokens) - setSamples((current) => [...current, { ...(metrics ? { metrics } : {}), generated }]) + const elapsed = part.time?.elapsed + const { output, reasoning } = splitTokens(part.tokens) + setSamples((current) => [ + ...current, + { + ...(metrics ? { metrics } : {}), + generated, + ...(typeof elapsed === "number" && Number.isFinite(elapsed) && elapsed > 0 + ? { elapsedMs: elapsed } + : {}), + ...(typeof output === "number" ? { output } : {}), + ...(typeof reasoning === "number" ? { reasoning } : {}), + }, + ]) } const offs = [ props.api.event.on("message.part.updated", (event) => { @@ -251,6 +280,15 @@ function generatedTokens(value: unknown): number { return output + reasoning } +function splitTokens(value: unknown): { output?: number; reasoning?: number } { + if (!value || typeof value !== "object") return {} + const record = value as Record + const out: { output?: number; reasoning?: number } = {} + if (typeof record.output === "number") out.output = record.output + if (typeof record.reasoning === "number") out.reasoning = record.reasoning + return out +} + const tui: TuiPlugin = async (api) => { api.slots.register({ order: 150, diff --git a/packages/opencode/src/session/processor.ts b/packages/opencode/src/session/processor.ts index 278eb7c0dc1..cb3be6a740e 100644 --- a/packages/opencode/src/session/processor.ts +++ b/packages/opencode/src/session/processor.ts @@ -103,6 +103,7 @@ interface ProcessorContext extends Input { reasoningMap: Record // kilocode_change start stepStart: number + stepStartDate: number | undefined step: { reasoning: boolean; text: boolean; tool: boolean } // kilocode_change end v2AssistantMessageID: SessionMessage.ID | undefined @@ -158,6 +159,7 @@ export const layer = Layer.effect( // kilocode_change start telemetry: input.telemetry, stepStart: 0, + stepStartDate: undefined, step: { reasoning: false, text: false, tool: false }, // kilocode_change end v2AssistantMessageID: undefined, @@ -800,6 +802,7 @@ export const layer = Layer.effect( case "step-start": // kilocode_change start ctx.stepStart = performance.now() + ctx.stepStartDate = Date.now() ctx.step = { reasoning: false, text: false, tool: false } if (!ctx.snapshot) ctx.snapshot = yield* snapshot.track({ @@ -820,6 +823,7 @@ export const layer = Layer.effect( sessionID: ctx.sessionID, snapshot: ctx.snapshot, type: "step-start", + time: { start: ctx.stepStartDate }, }) return @@ -860,9 +864,11 @@ export const layer = Layer.effect( // kilocode_change start - guard against finish-step without start-step: // ctx.stepStart is 0 until `start-step` fires, which would feed a // huge bogus `elapsed` into telemetry. Fall back to now(). + const endDate = Date.now() const elapsedMs = Math.round( performance.now() - (ctx.stepStart || performance.now()), ) + const startDate = ctx.stepStartDate ?? (Number.isFinite(elapsedMs) ? endDate - elapsedMs : endDate) const metrics = KiloSessionProcessor.computeMetrics({ providerMetadata: value.providerMetadata, tokens: usage.tokens, @@ -905,6 +911,7 @@ export const layer = Layer.effect( messageID: ctx.assistantMessage.id, sessionID: ctx.assistantMessage.sessionID, type: "step-finish", + time: { start: startDate, end: endDate, elapsed: elapsedMs }, // kilocode_change ...(model ? { model } : {}), // kilocode_change ...(metrics ? { metrics } : {}), // kilocode_change tokens: usage.tokens, diff --git a/packages/opencode/test/kilocode/tui/usage.test.ts b/packages/opencode/test/kilocode/tui/usage.test.ts index ed076a664e8..cc5ade50f78 100644 --- a/packages/opencode/test/kilocode/tui/usage.test.ts +++ b/packages/opencode/test/kilocode/tui/usage.test.ts @@ -11,6 +11,19 @@ const step = (metrics: { generation?: number }) => ({ generated: 0, }) +const weightedStep = (overrides: { + generation: number + output: number + reasoning?: number + elapsedMs: number +}) => ({ + metrics: { generation: overrides.generation, source: "computed" as const }, + generated: overrides.output + (overrides.reasoning ?? 0), + elapsedMs: overrides.elapsedMs, + output: overrides.output, + reasoning: overrides.reasoning ?? 0, +}) + describe("kilocode.plugins.model-usage throughput helpers", () => { test("formatRateValue renders positive values with grouping", () => { expect(formatRateValue(412)).toBe("412 t/s") @@ -31,7 +44,9 @@ describe("kilocode.plugins.model-usage throughput helpers", () => { expect(throughputLabel.generation).toBe("Generation speed") }) - test("surfaces the most recent non-empty generation rate as the snapshot", () => { + test("surfaces the most recent non-empty generation rate as the snapshot (fallback)", () => { + // Fallback path — used when callers don't pass timing on the wire. + // The weighted path is exercised by the dedicated tests below. const aggregated = aggregateMetrics([ { ...step({ generation: 20 }), generated: 100 }, { ...step({ generation: 60 }), generated: 300 }, @@ -41,10 +56,36 @@ describe("kilocode.plugins.model-usage throughput helpers", () => { test("skips samples without metrics", () => { const aggregated = aggregateMetrics([ - { metrics: undefined, generated: 100 }, - { ...step({ generation: 40 }), generated: 50 }, + { metrics: undefined, generated: 100, elapsedMs: 1000 }, + weightedStep({ generation: 40, output: 50, elapsedMs: 1000 }), + ]) + // weighted step contributes (50, 1000) → 50 t/s. + expect(aggregated.generation).toBe(50) + }) + + test("weights samples by elapsed time across steps", () => { + const aggregated = aggregateMetrics([ + weightedStep({ generation: 100, output: 100, elapsedMs: 1000 }), + weightedStep({ generation: 50, output: 200, elapsedMs: 4000 }), + ]) + // totalGenerated=300, totalElapsedMs=5000 → 60 t/s + expect(aggregated.generation).toBe(60) + }) + + test("includes reasoning tokens in the weighted numerator", () => { + const aggregated = aggregateMetrics([ + weightedStep({ generation: 200, output: 50, reasoning: 150, elapsedMs: 1000 }), ]) - expect(aggregated.generation).toBe(40) + // (50 + 150) tokens / 1000 ms = 200 t/s + expect(aggregated.generation).toBe(200) + }) + + test("falls back to last-wins snapshot when no sample carries timing", () => { + const aggregated = aggregateMetrics([ + { ...step({ generation: 20 }), generated: 100 }, + { ...step({ generation: 60 }), generated: 300 }, + ]) + expect(aggregated.generation).toBe(60) }) test("skips zero-weight samples when picking the latest snapshot", () => { diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 96880188158..5fcd0f84bb0 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -766,6 +766,9 @@ export type StepStartPart = { messageID: string type: "step-start" snapshot?: string + time?: { + start: number + } } export type StepFinishPart = { @@ -784,6 +787,11 @@ export type StepFinishPart = { generation?: number source: "provider" | "computed" } + time?: { + start: number + end: number + elapsed: number + } cost: number tokens: { total?: number diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index cbe3e3d71e1..552a2f6d77b 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -26021,6 +26021,17 @@ }, "snapshot": { "type": "string" + }, + "time": { + "type": "object", + "properties": { + "start": { + "type": "number", + "minimum": 0 + } + }, + "required": ["start"], + "additionalProperties": false } }, "required": ["id", "sessionID", "messageID", "type"], @@ -26081,6 +26092,24 @@ "required": ["source"], "additionalProperties": false }, + "time": { + "type": "object", + "properties": { + "start": { + "type": "number", + "minimum": 0 + }, + "end": { + "type": "number", + "minimum": 0 + }, + "elapsed": { + "type": "number" + } + }, + "required": ["start", "end", "elapsed"], + "additionalProperties": false + }, "cost": { "type": "number" }, From d4a1fdea4b0c54860c005539d16c002b923a4881 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Thu, 23 Jul 2026 10:56:53 +0200 Subject: [PATCH 16/18] fix(token-throughput-v2): render t/s inline beside copy/feedback buttons Move the throughput badge from a footer line below the assistant message into the copy/feedback action row of the text part that carries the copy button. This avoids the extra vertical space the footer consumed. Also apply prettier formatting to drifted PR files (i18n line wraps, TaskHeader/session-utils/test reflows). --- .../kilo-ui/src/components/message-part.css | 8 +++++++ .../kilo-ui/src/components/message-part.tsx | 5 +++++ .../tests/unit/session-utils.test.ts | 7 ++++++- .../src/components/chat/AssistantMessage.tsx | 21 +++++++++++++++---- .../src/components/chat/TaskHeader.tsx | 9 +------- .../webview-ui/src/context/session-utils.ts | 8 ++----- .../kilo-vscode/webview-ui/src/i18n/ar.ts | 3 ++- .../kilo-vscode/webview-ui/src/i18n/br.ts | 3 ++- .../kilo-vscode/webview-ui/src/i18n/bs.ts | 3 ++- .../kilo-vscode/webview-ui/src/i18n/da.ts | 3 ++- .../kilo-vscode/webview-ui/src/i18n/de.ts | 3 ++- .../kilo-vscode/webview-ui/src/i18n/en.ts | 3 ++- .../kilo-vscode/webview-ui/src/i18n/es.ts | 3 ++- .../kilo-vscode/webview-ui/src/i18n/fr.ts | 3 ++- .../kilo-vscode/webview-ui/src/i18n/it.ts | 3 ++- .../kilo-vscode/webview-ui/src/i18n/ja.ts | 3 ++- .../kilo-vscode/webview-ui/src/i18n/ko.ts | 3 ++- .../kilo-vscode/webview-ui/src/i18n/nl.ts | 4 ++-- .../kilo-vscode/webview-ui/src/i18n/no.ts | 3 ++- .../kilo-vscode/webview-ui/src/i18n/pl.ts | 3 ++- .../kilo-vscode/webview-ui/src/i18n/ru.ts | 3 ++- .../kilo-vscode/webview-ui/src/i18n/th.ts | 3 ++- .../kilo-vscode/webview-ui/src/i18n/tr.ts | 4 ++-- .../kilo-vscode/webview-ui/src/i18n/uk.ts | 4 ++-- .../kilo-vscode/webview-ui/src/i18n/zh.ts | 3 ++- .../kilo-vscode/webview-ui/src/i18n/zht.ts | 3 ++- .../webview-ui/src/styles/chat-layout.css | 11 ++++------ 27 files changed, 83 insertions(+), 49 deletions(-) diff --git a/packages/kilo-ui/src/components/message-part.css b/packages/kilo-ui/src/components/message-part.css index b711188f076..c6f45f080ba 100644 --- a/packages/kilo-ui/src/components/message-part.css +++ b/packages/kilo-ui/src/components/message-part.css @@ -37,6 +37,14 @@ [data-component="icon-button"][data-icon="thumbs-down"]:hover [data-slot="icon-svg"] path { fill: currentColor; } + + /* Throughput badge sits to the right of the copy/feedback buttons, + beside them rather than beneath the message. */ + [data-slot="assistant-throughput-inline"] { + margin-left: 6px; + display: flex; + align-items: center; + } } } diff --git a/packages/kilo-ui/src/components/message-part.tsx b/packages/kilo-ui/src/components/message-part.tsx index ae7c0a24c93..d21f9f38699 100644 --- a/packages/kilo-ui/src/components/message-part.tsx +++ b/packages/kilo-ui/src/components/message-part.tsx @@ -157,6 +157,7 @@ export interface MessagePartProps { animate?: boolean working?: boolean feedback?: MessageFeedbackControls + throughput?: JSX.Element } export type PartComponent = Component @@ -991,6 +992,7 @@ export function Part(props: MessagePartProps) { animate={props.animate} working={props.working} feedback={props.feedback} + throughput={props.throughput} /> ) @@ -1448,6 +1450,9 @@ PART_MAPPING["text"] = function TextPartDisplay(props) { /> + + {(el) => {el()}} + diff --git a/packages/kilo-vscode/tests/unit/session-utils.test.ts b/packages/kilo-vscode/tests/unit/session-utils.test.ts index d8ae8ff20bf..43430379491 100644 --- a/packages/kilo-vscode/tests/unit/session-utils.test.ts +++ b/packages/kilo-vscode/tests/unit/session-utils.test.ts @@ -735,7 +735,12 @@ type StepFinishOverrides = { function stepFinish(id: string, metricsOrOverrides?: NonNullable | StepFinishOverrides): Part { // Older call sites pass only metrics directly. Keep that signature so // the existing latestMetrics / messageMetrics tests stay readable. - if (metricsOrOverrides && "metrics" in metricsOrOverrides === false && "tokens" in metricsOrOverrides === false && "time" in metricsOrOverrides === false) { + if ( + metricsOrOverrides && + "metrics" in metricsOrOverrides === false && + "tokens" in metricsOrOverrides === false && + "time" in metricsOrOverrides === false + ) { return { type: "step-finish", id, diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/AssistantMessage.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/AssistantMessage.tsx index 5a8182c77f9..bc26805808a 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/AssistantMessage.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/AssistantMessage.tsx @@ -7,7 +7,7 @@ * Active questions render inline via QuestionDock; permissions are in the bottom dock. */ -import { Component, For, Show, createMemo } from "solid-js" +import { Component, For, Show, createMemo, type JSX } from "solid-js" import { Dynamic } from "solid-js/web" import { Part, PART_MAPPING, ToolRegistry } from "@kilocode/kilo-ui/message-part" import type { MessageFeedbackControls } from "@kilocode/kilo-ui/message-part" @@ -166,10 +166,11 @@ function BashToolCard(props: { part: ToolPart; defaultOpen: boolean; forceOpen?: ) } -/** Plain-text generation-speed value shown beneath an assistant message. +/** Plain-text generation-speed value shown beside the copy/feedback buttons + * on an assistant message. * * Renders as muted metadata — no icon, no background, no border — so it - * reads as a one-line footer rather than an interactive control. The + * reads as tertiary info rather than an interactive control. The * description on hover explains that the value is a weighted generation * rate across the turn's model-generation steps (output + reasoning * tokens over active generation time). @@ -267,6 +268,18 @@ export const AssistantMessage: Component = (props) => { return h?.msgId === props.message.id && h?.partId === part.id }) + // Throughput badge renders inside the copy/feedback action row of the + // text part that carries the copy button (the last text part of the + // message), pushed to the right of the buttons rather than below the + // message. Only built for that part so non-text parts skip the work. + const throughputEl = createMemo(() => { + if (!throughputVisible()) return undefined + const metrics = throughput() + if (!metrics) return undefined + if (part.id !== props.showAssistantCopyPartID) return undefined + return + }) + return ( = (props) => { forceOpenFile={forceOpen() ? props.forceOpenFile : undefined} reasoningAutoCollapse={display.reasoningAutoCollapse()} feedback={props.feedback} + throughput={throughputEl()} animate={ part.type === "tool" && ((part as unknown as ToolPart).state?.status === "pending" || @@ -348,7 +362,6 @@ export const AssistantMessage: Component = (props) => { ) }} - {(metrics) => } ) } diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/TaskHeader.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/TaskHeader.tsx index ee4e473f736..9b576726475 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/TaskHeader.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/TaskHeader.tsx @@ -287,14 +287,7 @@ export const TaskHeader: Component = (props) => {
- - {(tk) => ( - - )} - + {(tk) => }
diff --git a/packages/kilo-vscode/webview-ui/src/context/session-utils.ts b/packages/kilo-vscode/webview-ui/src/context/session-utils.ts index 5e5aa367448..218f456026a 100644 --- a/packages/kilo-vscode/webview-ui/src/context/session-utils.ts +++ b/packages/kilo-vscode/webview-ui/src/context/session-utils.ts @@ -261,9 +261,7 @@ export function messageMetrics(parts: readonly Part[]): { generation?: number; s * generated tokens are skipped — tool-only steps, idempotent cache hits, * and tool re-execution should not skew the figure. */ -export function messageThroughput( - parts: readonly Part[], -): { generation?: number; source: "computed" } | undefined { +export function messageThroughput(parts: readonly Part[]): { generation?: number; source: "computed" } | undefined { let generated = 0 let elapsedMs = 0 for (const part of parts) { @@ -288,9 +286,7 @@ export function messageThroughput( * message in a session. Same weighted semantics as `messageThroughput` — * useful when a caller has already flattened parts across messages. */ -export function sessionThroughput( - parts: readonly Part[], -): { generation?: number; source: "computed" } | undefined { +export function sessionThroughput(parts: readonly Part[]): { generation?: number; source: "computed" } | undefined { return messageThroughput(parts) } diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts index 8845b0d1ed2..a09fbe07417 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts @@ -1684,7 +1684,8 @@ export const dict = { "settings.display.tokenThroughput.description": "Display the text-generation rate (tokens/sec) on the latest assistant message and in the task header. Hidden by default to keep the chat uncluttered.", - "chat.throughput.tooltip": "Average {{speed}} tokens/s for this turn. Includes output and reasoning tokens; excludes tool execution and waiting time.", + "chat.throughput.tooltip": + "Average {{speed}} tokens/s for this turn. Includes output and reasoning tokens; excludes tool execution and waiting time.", "chat.throughput.tooltip.missing": "Throughput metrics unavailable for this turn.", "settings.providers.defaultModel.title": "النموذج الافتراضي", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/br.ts b/packages/kilo-vscode/webview-ui/src/i18n/br.ts index 2d3c70a77b7..976c262a108 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/br.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/br.ts @@ -1735,7 +1735,8 @@ export const dict = { "settings.display.tokenThroughput.description": "Display the text-generation rate (tokens/sec) on the latest assistant message and in the task header. Hidden by default to keep the chat uncluttered.", - "chat.throughput.tooltip": "Average {{speed}} tokens/s for this turn. Includes output and reasoning tokens; excludes tool execution and waiting time.", + "chat.throughput.tooltip": + "Average {{speed}} tokens/s for this turn. Includes output and reasoning tokens; excludes tool execution and waiting time.", "chat.throughput.tooltip.missing": "Throughput metrics unavailable for this turn.", "settings.providers.defaultModel.title": "Modelo padrão", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/bs.ts b/packages/kilo-vscode/webview-ui/src/i18n/bs.ts index f507e943ffe..93074e6c839 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/bs.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/bs.ts @@ -1727,7 +1727,8 @@ export const dict = { "settings.display.tokenThroughput.description": "Display the text-generation rate (tokens/sec) on the latest assistant message and in the task header. Hidden by default to keep the chat uncluttered.", - "chat.throughput.tooltip": "Average {{speed}} tokens/s for this turn. Includes output and reasoning tokens; excludes tool execution and waiting time.", + "chat.throughput.tooltip": + "Average {{speed}} tokens/s for this turn. Includes output and reasoning tokens; excludes tool execution and waiting time.", "chat.throughput.tooltip.missing": "Throughput metrics unavailable for this turn.", "settings.providers.defaultModel.title": "Zadani model", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/da.ts b/packages/kilo-vscode/webview-ui/src/i18n/da.ts index f49494e083c..d99fccc7722 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/da.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/da.ts @@ -1718,7 +1718,8 @@ export const dict = { "settings.display.tokenThroughput.description": "Display the text-generation rate (tokens/sec) on the latest assistant message and in the task header. Hidden by default to keep the chat uncluttered.", - "chat.throughput.tooltip": "Average {{speed}} tokens/s for this turn. Includes output and reasoning tokens; excludes tool execution and waiting time.", + "chat.throughput.tooltip": + "Average {{speed}} tokens/s for this turn. Includes output and reasoning tokens; excludes tool execution and waiting time.", "chat.throughput.tooltip.missing": "Throughput metrics unavailable for this turn.", "settings.providers.defaultModel.title": "Standardmodel", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/de.ts b/packages/kilo-vscode/webview-ui/src/i18n/de.ts index 921baedb71b..f55bdebf39d 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/de.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/de.ts @@ -1754,7 +1754,8 @@ export const dict = { "settings.display.tokenThroughput.description": "Display the text-generation rate (tokens/sec) on the latest assistant message and in the task header. Hidden by default to keep the chat uncluttered.", - "chat.throughput.tooltip": "Average {{speed}} tokens/s for this turn. Includes output and reasoning tokens; excludes tool execution and waiting time.", + "chat.throughput.tooltip": + "Average {{speed}} tokens/s for this turn. Includes output and reasoning tokens; excludes tool execution and waiting time.", "chat.throughput.tooltip.missing": "Throughput metrics unavailable for this turn.", "settings.providers.defaultModel.title": "Standardmodell", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/en.ts b/packages/kilo-vscode/webview-ui/src/i18n/en.ts index 97d499110c9..bfc4c9c766f 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/en.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/en.ts @@ -1695,7 +1695,8 @@ export const dict = { "settings.display.tokenThroughput.description": "Display the text-generation rate (tokens/sec) on the latest assistant message and in the task header. Hidden by default to keep the chat uncluttered.", - "chat.throughput.tooltip": "Average {{speed}} tokens/s for this turn. Includes output and reasoning tokens; excludes tool execution and waiting time.", + "chat.throughput.tooltip": + "Average {{speed}} tokens/s for this turn. Includes output and reasoning tokens; excludes tool execution and waiting time.", "chat.throughput.tooltip.missing": "Throughput metrics unavailable for this turn.", "settings.providers.defaultModel.title": "Default Model", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/es.ts b/packages/kilo-vscode/webview-ui/src/i18n/es.ts index e2e97ef60c5..5dce6309e5b 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/es.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/es.ts @@ -1743,7 +1743,8 @@ export const dict = { "settings.display.tokenThroughput.description": "Display the text-generation rate (tokens/sec) on the latest assistant message and in the task header. Hidden by default to keep the chat uncluttered.", - "chat.throughput.tooltip": "Average {{speed}} tokens/s for this turn. Includes output and reasoning tokens; excludes tool execution and waiting time.", + "chat.throughput.tooltip": + "Average {{speed}} tokens/s for this turn. Includes output and reasoning tokens; excludes tool execution and waiting time.", "chat.throughput.tooltip.missing": "Throughput metrics unavailable for this turn.", "settings.providers.defaultModel.title": "Modelo predeterminado", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts index e13d5c0ea81..945bd1f014b 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts @@ -1764,7 +1764,8 @@ export const dict = { "settings.display.tokenThroughput.description": "Display the text-generation rate (tokens/sec) on the latest assistant message and in the task header. Hidden by default to keep the chat uncluttered.", - "chat.throughput.tooltip": "Average {{speed}} tokens/s for this turn. Includes output and reasoning tokens; excludes tool execution and waiting time.", + "chat.throughput.tooltip": + "Average {{speed}} tokens/s for this turn. Includes output and reasoning tokens; excludes tool execution and waiting time.", "chat.throughput.tooltip.missing": "Throughput metrics unavailable for this turn.", "settings.providers.defaultModel.title": "Modèle par défaut", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/it.ts b/packages/kilo-vscode/webview-ui/src/i18n/it.ts index 173d53d5f0d..96eb4c0ad95 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/it.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/it.ts @@ -1521,7 +1521,8 @@ export const dict = { "settings.display.tokenThroughput.description": "Display the text-generation rate (tokens/sec) on the latest assistant message and in the task header. Hidden by default to keep the chat uncluttered.", - "chat.throughput.tooltip": "Average {{speed}} tokens/s for this turn. Includes output and reasoning tokens; excludes tool execution and waiting time.", + "chat.throughput.tooltip": + "Average {{speed}} tokens/s for this turn. Includes output and reasoning tokens; excludes tool execution and waiting time.", "chat.throughput.tooltip.missing": "Throughput metrics unavailable for this turn.", "settings.providers.defaultModel.title": "Modello predefinito", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts index 134202bd7a4..fd06820ae1f 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts @@ -1713,7 +1713,8 @@ export const dict = { "settings.display.tokenThroughput.description": "Display the text-generation rate (tokens/sec) on the latest assistant message and in the task header. Hidden by default to keep the chat uncluttered.", - "chat.throughput.tooltip": "Average {{speed}} tokens/s for this turn. Includes output and reasoning tokens; excludes tool execution and waiting time.", + "chat.throughput.tooltip": + "Average {{speed}} tokens/s for this turn. Includes output and reasoning tokens; excludes tool execution and waiting time.", "chat.throughput.tooltip.missing": "Throughput metrics unavailable for this turn.", "settings.providers.defaultModel.title": "デフォルトモデル", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts index be7462a9936..df36826e757 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts @@ -1695,7 +1695,8 @@ export const dict = { "settings.display.tokenThroughput.description": "Display the text-generation rate (tokens/sec) on the latest assistant message and in the task header. Hidden by default to keep the chat uncluttered.", - "chat.throughput.tooltip": "Average {{speed}} tokens/s for this turn. Includes output and reasoning tokens; excludes tool execution and waiting time.", + "chat.throughput.tooltip": + "Average {{speed}} tokens/s for this turn. Includes output and reasoning tokens; excludes tool execution and waiting time.", "chat.throughput.tooltip.missing": "Throughput metrics unavailable for this turn.", "settings.providers.defaultModel.title": "기본 모델", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/nl.ts b/packages/kilo-vscode/webview-ui/src/i18n/nl.ts index 2a77b8fa17c..f0c06302ed1 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/nl.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/nl.ts @@ -1695,10 +1695,10 @@ export const dict = { "settings.display.tokenThroughput.description": "Display the text-generation rate (tokens/sec) on the latest assistant message and in the task header. Hidden by default to keep the chat uncluttered.", - "chat.throughput.tooltip": "Average {{speed}} tokens/s for this turn. Includes output and reasoning tokens; excludes tool execution and waiting time.", + "chat.throughput.tooltip": + "Average {{speed}} tokens/s for this turn. Includes output and reasoning tokens; excludes tool execution and waiting time.", "chat.throughput.tooltip.missing": "Throughput metrics unavailable for this turn.", - "settings.providers.defaultModel.title": "Standaard Model", "settings.providers.defaultModel.description": "Primair model voor gesprekken", "settings.providers.smallModel.title": "Klein Model", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/no.ts b/packages/kilo-vscode/webview-ui/src/i18n/no.ts index d4dc818691e..4f323a6711f 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/no.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/no.ts @@ -1716,7 +1716,8 @@ export const dict = { "settings.display.tokenThroughput.description": "Vis tekstgenereringshastighet (tokens/sek) på den siste assistentmeldingen og i oppgaveoverskriften. Skjult som standard for å holde chatten ryddig.", - "chat.throughput.tooltip": "Average {{speed}} tokens/s for this turn. Includes output and reasoning tokens; excludes tool execution and waiting time.", + "chat.throughput.tooltip": + "Average {{speed}} tokens/s for this turn. Includes output and reasoning tokens; excludes tool execution and waiting time.", "chat.throughput.tooltip.missing": "Throughput metrics unavailable for this turn.", "settings.providers.defaultModel.title": "Standardmodell", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts index 1257f14df33..78a025093b4 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts @@ -1726,7 +1726,8 @@ export const dict = { "settings.display.tokenThroughput.description": "Display the text-generation rate (tokens/sec) on the latest assistant message and in the task header. Hidden by default to keep the chat uncluttered.", - "chat.throughput.tooltip": "Average {{speed}} tokens/s for this turn. Includes output and reasoning tokens; excludes tool execution and waiting time.", + "chat.throughput.tooltip": + "Average {{speed}} tokens/s for this turn. Includes output and reasoning tokens; excludes tool execution and waiting time.", "chat.throughput.tooltip.missing": "Throughput metrics unavailable for this turn.", "settings.providers.defaultModel.title": "Domyślny model", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts index 122c9fa50bd..923c4b235a6 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts @@ -1724,7 +1724,8 @@ export const dict = { "settings.display.tokenThroughput.description": "Display the text-generation rate (tokens/sec) on the latest assistant message and in the task header. Hidden by default to keep the chat uncluttered.", - "chat.throughput.tooltip": "Average {{speed}} tokens/s for this turn. Includes output and reasoning tokens; excludes tool execution and waiting time.", + "chat.throughput.tooltip": + "Average {{speed}} tokens/s for this turn. Includes output and reasoning tokens; excludes tool execution and waiting time.", "chat.throughput.tooltip.missing": "Throughput metrics unavailable for this turn.", "settings.providers.defaultModel.title": "Модель по умолчанию", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/th.ts b/packages/kilo-vscode/webview-ui/src/i18n/th.ts index 5794fa64660..fa3a031f2f4 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/th.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/th.ts @@ -1693,7 +1693,8 @@ export const dict = { "settings.display.tokenThroughput.description": "Display the text-generation rate (tokens/sec) on the latest assistant message and in the task header. Hidden by default to keep the chat uncluttered.", - "chat.throughput.tooltip": "Average {{speed}} tokens/s for this turn. Includes output and reasoning tokens; excludes tool execution and waiting time.", + "chat.throughput.tooltip": + "Average {{speed}} tokens/s for this turn. Includes output and reasoning tokens; excludes tool execution and waiting time.", "chat.throughput.tooltip.missing": "Throughput metrics unavailable for this turn.", "settings.providers.defaultModel.title": "โมเดลเริ่มต้น", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/tr.ts b/packages/kilo-vscode/webview-ui/src/i18n/tr.ts index c9242d7baf6..f8a73c04c7c 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/tr.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/tr.ts @@ -1682,10 +1682,10 @@ export const dict = { "settings.display.tokenThroughput.description": "Display the text-generation rate (tokens/sec) on the latest assistant message and in the task header. Hidden by default to keep the chat uncluttered.", - "chat.throughput.tooltip": "Average {{speed}} tokens/s for this turn. Includes output and reasoning tokens; excludes tool execution and waiting time.", + "chat.throughput.tooltip": + "Average {{speed}} tokens/s for this turn. Includes output and reasoning tokens; excludes tool execution and waiting time.", "chat.throughput.tooltip.missing": "Throughput metrics unavailable for this turn.", - "settings.providers.defaultModel.title": "Varsayılan Model", "settings.providers.defaultModel.description": "Sohbetler için birincil model", "settings.providers.smallModel.title": "Küçük Model", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/uk.ts b/packages/kilo-vscode/webview-ui/src/i18n/uk.ts index fd882d1475b..a7b3673d82f 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/uk.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/uk.ts @@ -1679,10 +1679,10 @@ export const dict = { "settings.display.tokenThroughput.description": "Display the text-generation rate (tokens/sec) on the latest assistant message and in the task header. Hidden by default to keep the chat uncluttered.", - "chat.throughput.tooltip": "Average {{speed}} tokens/s for this turn. Includes output and reasoning tokens; excludes tool execution and waiting time.", + "chat.throughput.tooltip": + "Average {{speed}} tokens/s for this turn. Includes output and reasoning tokens; excludes tool execution and waiting time.", "chat.throughput.tooltip.missing": "Throughput metrics unavailable for this turn.", - "settings.providers.defaultModel.title": "Модель за замовчуванням", "settings.providers.defaultModel.description": "Основна модель для чатів", "settings.providers.smallModel.title": "Мала модель", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts index bd4ffc39b7e..3ca355d4e3e 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts @@ -1645,7 +1645,8 @@ export const dict = { "settings.display.tokenThroughput.description": "Display the text-generation rate (tokens/sec) on the latest assistant message and in the task header. Hidden by default to keep the chat uncluttered.", - "chat.throughput.tooltip": "Average {{speed}} tokens/s for this turn. Includes output and reasoning tokens; excludes tool execution and waiting time.", + "chat.throughput.tooltip": + "Average {{speed}} tokens/s for this turn. Includes output and reasoning tokens; excludes tool execution and waiting time.", "chat.throughput.tooltip.missing": "Throughput metrics unavailable for this turn.", "settings.providers.defaultModel.title": "默认模型", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts index 0818197b86a..09ddb36995d 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts @@ -1609,7 +1609,8 @@ export const dict = { "settings.display.tokenThroughput.description": "Display the text-generation rate (tokens/sec) on the latest assistant message and in the task header. Hidden by default to keep the chat uncluttered.", - "chat.throughput.tooltip": "Average {{speed}} tokens/s for this turn. Includes output and reasoning tokens; excludes tool execution and waiting time.", + "chat.throughput.tooltip": + "Average {{speed}} tokens/s for this turn. Includes output and reasoning tokens; excludes tool execution and waiting time.", "chat.throughput.tooltip.missing": "Throughput metrics unavailable for this turn.", "settings.providers.defaultModel.title": "預設模型", diff --git a/packages/kilo-vscode/webview-ui/src/styles/chat-layout.css b/packages/kilo-vscode/webview-ui/src/styles/chat-layout.css index da109b20030..3e475d3ff39 100644 --- a/packages/kilo-vscode/webview-ui/src/styles/chat-layout.css +++ b/packages/kilo-vscode/webview-ui/src/styles/chat-layout.css @@ -229,13 +229,10 @@ } [data-component="assistant-throughput"] { - /* Plain-text generation-speed footer beneath an assistant message. - * Matches the muted description-foreground tone used elsewhere for session - * metadata and reads as a single line of tertiary info — no icon, no - * border, no background, so it never competes with the upstream action - * row (copy / feedback) for visual weight. */ - display: block; - margin-top: 4px; + /* Plain-text generation-speed value shown beside the copy/feedback buttons + * on an assistant message. Renders as muted metadata — no icon, no + * background, no border — so it reads as tertiary info that never + * competes with the action row for visual weight. */ color: var(--vscode-descriptionForeground); font-family: var(--font-family-sans); font-size: var(--kilo-font-size-11); From 582cf4defe7a3bcc3d6bafed5be35c28084a0b3b Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Thu, 23 Jul 2026 11:07:11 +0200 Subject: [PATCH 17/18] fix(token-throughput-v2): correct changeset package name to @kilocode/cli --- .changeset/token-throughput-v2.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/token-throughput-v2.md b/.changeset/token-throughput-v2.md index 43c838de70d..c5030cc9ac5 100644 --- a/.changeset/token-throughput-v2.md +++ b/.changeset/token-throughput-v2.md @@ -1,5 +1,5 @@ --- -"@kilocode/kilo": minor +"@kilocode/cli": minor "@kilocode/sdk": minor --- From 5891f2b75b7e2aa0ce629f423922a232ac915914 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Thu, 23 Jul 2026 11:14:47 +0200 Subject: [PATCH 18/18] fix(token-throughput-v2): annotate step-start time field with kilocode_change --- packages/opencode/src/session/processor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opencode/src/session/processor.ts b/packages/opencode/src/session/processor.ts index cb3be6a740e..dbc628ee9ec 100644 --- a/packages/opencode/src/session/processor.ts +++ b/packages/opencode/src/session/processor.ts @@ -823,7 +823,7 @@ export const layer = Layer.effect( sessionID: ctx.sessionID, snapshot: ctx.snapshot, type: "step-start", - time: { start: ctx.stepStartDate }, + time: { start: ctx.stepStartDate }, // kilocode_change }) return