diff --git a/packages/kilo-vscode/tests/unit/session-utils.test.ts b/packages/kilo-vscode/tests/unit/session-utils.test.ts index e1a97d914f4..12aa6f6a458 100644 --- a/packages/kilo-vscode/tests/unit/session-utils.test.ts +++ b/packages/kilo-vscode/tests/unit/session-utils.test.ts @@ -325,4 +325,78 @@ describe("buildCostBreakdown", () => { const result = buildCostBreakdown("s1", costs, new Map(), "This session") expect(result[1].label).toBe("abcdef12") }) + + it("shows children in reverse chronological order", () => { + const costs = new Map([ + ["s1", 0.1], + ["c1", 0.01], + ["c2", 0.02], + ["c3", 0.03], + ]) + const labels = new Map([ + ["c1", "first"], + ["c2", "second"], + ["c3", "third"], + ]) + const result = buildCostBreakdown("s1", costs, labels, "Root") + expect(result).toEqual([ + { label: "Root", cost: 0.1 }, + { label: "third", cost: 0.03 }, + { label: "second", cost: 0.02 }, + { label: "first", cost: 0.01 }, + ]) + }) + + it("aggregates older children when more than 5", () => { + const costs = new Map([ + ["s1", 0.5], + ["c1", 0.01], + ["c2", 0.02], + ["c3", 0.03], + ["c4", 0.04], + ["c5", 0.05], + ["c6", 0.06], + ["c7", 0.07], + ["c8", 0.08], + ]) + const labels = new Map([ + ["c1", "agent-1"], + ["c2", "agent-2"], + ["c3", "agent-3"], + ["c4", "agent-4"], + ["c5", "agent-5"], + ["c6", "agent-6"], + ["c7", "agent-7"], + ["c8", "agent-8"], + ]) + const result = buildCostBreakdown("s1", costs, labels, "Root", "older sessions") + // Root + 5 most recent + 1 aggregated line = 7 items + expect(result.length).toBe(7) + expect(result[0]).toEqual({ label: "Root", cost: 0.5 }) + // Most recent 5 (reversed: c8, c7, c6, c5, c4) + expect(result[1]).toEqual({ label: "agent-8", cost: 0.08 }) + expect(result[2]).toEqual({ label: "agent-7", cost: 0.07 }) + expect(result[3]).toEqual({ label: "agent-6", cost: 0.06 }) + expect(result[4]).toEqual({ label: "agent-5", cost: 0.05 }) + expect(result[5]).toEqual({ label: "agent-4", cost: 0.04 }) + // Aggregated: c3 + c2 + c1 = 0.03 + 0.02 + 0.01 = 0.06 + expect(result[6].label).toBe("3 older sessions") + expect(result[6].cost).toBeCloseTo(0.06) + }) + + it("uses custom olderLabel for aggregated line", () => { + const costs = new Map([ + ["s1", 0.1], + ["c1", 0.01], + ["c2", 0.02], + ["c3", 0.03], + ["c4", 0.04], + ["c5", 0.05], + ["c6", 0.06], + ]) + const labels = new Map() + const result = buildCostBreakdown("s1", costs, labels, "Root", "ältere Sitzungen") + // 5 visible + 1 aggregated + expect(result[result.length - 1].label).toBe("1 ältere Sitzungen") + }) }) diff --git a/packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/chat/task-header-cost-few-subagents-chromium-linux.png b/packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/chat/task-header-cost-few-subagents-chromium-linux.png new file mode 100644 index 00000000000..d91d768b927 --- /dev/null +++ b/packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/chat/task-header-cost-few-subagents-chromium-linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4bda1647b5b58c29d663cccf2bf5dee89fe47167d080d4b451bd4b4cadfef2a9 +size 3459 diff --git a/packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/chat/task-header-cost-many-subagents-chromium-linux.png b/packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/chat/task-header-cost-many-subagents-chromium-linux.png new file mode 100644 index 00000000000..be9d90f2ac3 --- /dev/null +++ b/packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/chat/task-header-cost-many-subagents-chromium-linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:144b98b4e0beeff01a0c5d9a3fe99d2fc338695805cad47e137cb6c98245d498 +size 4758 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 b6191cb1af1..a4f06909609 100644 --- a/packages/kilo-vscode/webview-ui/src/context/session-utils.ts +++ b/packages/kilo-vscode/webview-ui/src/context/session-utils.ts @@ -131,8 +131,16 @@ export function buildFamilyLabels( return labels } +/** Max individually-listed child sessions before older ones are aggregated. */ +const VISIBLE_CAP = 5 + /** * Combine costs and labels into the final breakdown array. + * + * Order: root session first, then child sessions in reverse chronological + * order (most recent on top). When there are more than VISIBLE_CAP children, + * the oldest entries are collapsed into a single aggregated line. + * * Pure function — no store dependency. */ export function buildCostBreakdown( @@ -140,11 +148,36 @@ export function buildCostBreakdown( costs: Map, labels: Map, rootLabel: string, + olderLabel = "older sessions", ): Array<{ label: string; cost: number }> { - const items: Array<{ label: string; cost: number }> = [] + const children: Array<{ label: string; cost: number }> = [] + let rootItem: { label: string; cost: number } | undefined + for (const [sid, cost] of costs) { - const label = sid === root ? rootLabel : (labels.get(sid) ?? sid.slice(0, 8)) - items.push({ label, cost }) + if (sid === root) { + rootItem = { label: rootLabel, cost } + } else { + const label = labels.get(sid) ?? sid.slice(0, 8) + children.push({ label, cost }) + } + } + + // Reverse so the most-recently-discovered (newest) children come first + children.reverse() + + const items: Array<{ label: string; cost: number }> = [] + if (rootItem) items.push(rootItem) + + if (children.length <= VISIBLE_CAP) { + items.push(...children) + } else { + // Show the most recent VISIBLE_CAP children individually + const recent = children.slice(0, VISIBLE_CAP) + const older = children.slice(VISIBLE_CAP) + const sum = older.reduce((s, e) => s + e.cost, 0) + items.push(...recent) + items.push({ label: `${older.length} ${olderLabel}`, cost: sum }) } + return items } diff --git a/packages/kilo-vscode/webview-ui/src/context/session.tsx b/packages/kilo-vscode/webview-ui/src/context/session.tsx index 9c1430532f4..44798ef9c05 100644 --- a/packages/kilo-vscode/webview-ui/src/context/session.tsx +++ b/packages/kilo-vscode/webview-ui/src/context/session.tsx @@ -1749,7 +1749,13 @@ export const SessionProvider: ParentComponent = (props) => { const id = currentSessionID() const costs = familyCosts() if (!id || costs.size === 0) return [] - return buildCostBreakdown(id, costs, familyLabels(), language.t("context.stats.thisSession")) + return buildCostBreakdown( + id, + costs, + familyLabels(), + language.t("context.stats.thisSession"), + language.t("context.stats.olderSessions"), + ) }) // Status text derived from last assistant message parts diff --git a/packages/kilo-vscode/webview-ui/src/i18n/en.ts b/packages/kilo-vscode/webview-ui/src/i18n/en.ts index 63ffdccf483..62bb3a2250d 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/en.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/en.ts @@ -954,6 +954,7 @@ export const dict = { "context.usage.sessionCost": "Session cost", "context.stats.thisSession": "This session", + "context.stats.olderSessions": "older sessions", "time.justNow": "just now", "time.minutesAgo": "{{count}} min ago", diff --git a/packages/kilo-vscode/webview-ui/src/stories/chat.stories.tsx b/packages/kilo-vscode/webview-ui/src/stories/chat.stories.tsx index 93c64ab4ade..c5e08fbbdb2 100644 --- a/packages/kilo-vscode/webview-ui/src/stories/chat.stories.tsx +++ b/packages/kilo-vscode/webview-ui/src/stories/chat.stories.tsx @@ -239,6 +239,79 @@ export const TaskHeaderWithTodosAllDone: Story = { }, } +// --------------------------------------------------------------------------- +// TaskHeader cost tooltip with many subagents +// --------------------------------------------------------------------------- + +/** Few subagents — each shown individually, reverse chronological order */ +export const TaskHeaderCostFewSubagents: Story = { + name: "TaskHeader — cost tooltip (few subagents)", + render: () => { + const session = { + ...mockSessionValue({ id: SESSION_ID, status: "idle" }), + messages: () => [{ id: "msg-001" }] as any[], + currentSession: () => ({ + id: SESSION_ID, + title: "Building a feature", + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }), + costBreakdown: () => [ + { label: "This session", cost: 0.12 }, + { label: "general", cost: 0.08 }, + { label: "explore", cost: 0.04 }, + { label: "docs", cost: 0.02 }, + ], + contextUsage: () => ({ tokens: 8192, percentage: 12 }), + } + return ( + + +
+ +
+
+
+ ) + }, +} + +/** Many subagents — older entries aggregated into a summary line */ +export const TaskHeaderCostManySubagents: Story = { + name: "TaskHeader — cost tooltip (many subagents, aggregated)", + render: () => { + const session = { + ...mockSessionValue({ id: SESSION_ID, status: "idle" }), + messages: () => [{ id: "msg-001" }] as any[], + currentSession: () => ({ + id: SESSION_ID, + title: "Complex refactoring across 12 modules", + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }), + costBreakdown: () => [ + { label: "This session", cost: 0.45 }, + { label: "general", cost: 0.12 }, + { label: "explore", cost: 0.09 }, + { label: "docs", cost: 0.07 }, + { label: "general", cost: 0.06 }, + { label: "explore", cost: 0.05 }, + { label: "5 older sessions", cost: 0.18 }, + ], + contextUsage: () => ({ tokens: 42000, percentage: 52 }), + } + return ( + + +
+ +
+
+
+ ) + }, +} + // --------------------------------------------------------------------------- // Welcome screen with AccountSwitcher + KiloNotifications // ---------------------------------------------------------------------------