Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions packages/kilo-vscode/tests/unit/session-utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, number>([
["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<string, number>([
["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<string, number>([
["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<string, string>()
const result = buildCostBreakdown("s1", costs, labels, "Root", "ältere Sitzungen")
// 5 visible + 1 aggregated
expect(result[result.length - 1].label).toBe("1 ältere Sitzungen")
})
})
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
39 changes: 36 additions & 3 deletions packages/kilo-vscode/webview-ui/src/context/session-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,20 +131,53 @@ 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(
root: string,
costs: Map<string, number>,
labels: Map<string, string>,
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 })
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: This aggregation label cannot be localized correctly

This builds the final text by prefixing the count onto a translated suffix. That hardcodes English word order and skips singular/plural handling, so locales like German end up with incorrect strings such as 1 ältere Sitzungen, and languages that place the count elsewhere cannot translate this at all. Please move the full label into i18n (for example with *_one / *_other keys and {{count}}).

}

return items
}
8 changes: 7 additions & 1 deletion packages/kilo-vscode/webview-ui/src/context/session.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions packages/kilo-vscode/webview-ui/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
73 changes: 73 additions & 0 deletions packages/kilo-vscode/webview-ui/src/stories/chat.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<StoryProviders sessionID={SESSION_ID} status="idle" noPadding>
<SessionContext.Provider value={session as any}>
<div style={{ "max-height": "400px" }}>
<TaskHeader />
</div>
</SessionContext.Provider>
</StoryProviders>
)
},
}

/** 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 (
<StoryProviders sessionID={SESSION_ID} status="idle" noPadding>
<SessionContext.Provider value={session as any}>
<div style={{ "max-height": "400px" }}>
<TaskHeader />
</div>
</SessionContext.Provider>
</StoryProviders>
)
},
}

// ---------------------------------------------------------------------------
// Welcome screen with AccountSwitcher + KiloNotifications
// ---------------------------------------------------------------------------
Expand Down
Loading