Skip to content

Commit b3e90bf

Browse files
committed
feat(dashboard): add usage dashboard with mode column, multi-model aggregation, i18n, and CI fixes
1 parent a9e3a25 commit b3e90bf

31 files changed

Lines changed: 97 additions & 40 deletions

packages/types/src/usage-stats.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,9 +127,23 @@ export interface SessionSummary {
127127
taskId: string
128128
title: string // First line of user input (truncated); falls back to taskId
129129
timestamp: number // Last activity (epoch ms)
130-
model: string
130+
model: string // First-seen model (kept for backward compatibility)
131131
provider: string
132-
mode: string
132+
mode: string // First-seen mode (kept for backward compatibility)
133+
/**
134+
* All unique models used in the session, in first-seen order.
135+
* A session may switch models (e.g. orchestrator delegating to a
136+
* different provider), so this array captures the full set while
137+
* `model` retains the earliest value for backward compatibility.
138+
*/
139+
models: string[]
140+
/**
141+
* All unique modes used in the session, in first-seen order.
142+
* A session may span multiple modes (e.g. orchestrator-crow
143+
* delegating to code, debug, ask), so this array captures the full
144+
* set while `mode` retains the earliest value for backward compat.
145+
*/
146+
modes: string[]
133147
totalTokens: number
134148
totalCost: number
135149
callCount: number
@@ -148,6 +162,7 @@ export interface SessionDetail extends SessionSummary {
148162
*/
149163
export interface APICallRecord {
150164
index: number
165+
mode: string
151166
timestamp: number
152167
inputTokens: number
153168
outputTokens: number

src/api/providers/__tests__/anthropic-vertex.spec.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,7 @@ describe("VertexHandler", () => {
217217
type: "usage",
218218
inputTokens: 10,
219219
outputTokens: 0,
220+
totalCost: 0.00003,
220221
})
221222
expect(chunks[1]).toEqual({
222223
type: "text",
@@ -431,6 +432,7 @@ describe("VertexHandler", () => {
431432
outputTokens: 0,
432433
cacheWriteTokens: 3,
433434
cacheReadTokens: 2,
435+
totalCost: 0.00004185,
434436
})
435437
expect(usageChunks[1]).toEqual({
436438
type: "usage",

src/api/providers/__tests__/kenari.spec.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ describe("KenariHandler", () => {
141141
inputTokens: 12,
142142
outputTokens: 7,
143143
cacheReadTokens: 4,
144+
totalCost: 0,
144145
})
145146
})
146147

@@ -210,6 +211,7 @@ describe("KenariHandler", () => {
210211
inputTokens: 3,
211212
outputTokens: 2,
212213
cacheReadTokens: undefined,
214+
totalCost: 0,
213215
},
214216
])
215217
})
@@ -318,6 +320,7 @@ describe("KenariHandler", () => {
318320
inputTokens: 0,
319321
outputTokens: 0,
320322
cacheReadTokens: undefined,
323+
totalCost: 0,
321324
})
322325
})
323326

src/api/providers/__tests__/openai-usage-tracking.spec.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,8 +138,9 @@ describe("OpenAiHandler with usage tracking fix", () => {
138138
type: "usage",
139139
inputTokens: 10,
140140
outputTokens: 5,
141+
totalCost: 0,
141142
})
142-
143+
143144
// Check the usage chunk is the last one reported from the API
144145
const lastChunk = chunks[chunks.length - 1]
145146
expect(lastChunk.type).toBe("usage")
@@ -198,9 +199,10 @@ describe("OpenAiHandler with usage tracking fix", () => {
198199
type: "usage",
199200
inputTokens: 10,
200201
outputTokens: 5,
202+
totalCost: 0,
201203
})
202204
})
203-
205+
204206
it("should handle case where no usage is provided", async () => {
205207
// Override the mock for this specific test
206208
mockCreate.mockImplementationOnce(async (options) => {

src/core/webview/usageStatsMessageHandler.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -566,6 +566,12 @@ async function buildSessionSummaries(
566566
model: first.model,
567567
provider: first.provider,
568568
mode: first.mode,
569+
// Preserve first-seen order across the session's events so that
570+
// multi-model/multi-mode sessions (e.g. orchestrator delegations)
571+
// are fully represented. `model`/`mode` above keep the earliest
572+
// value for backward compatibility.
573+
models: [...new Set(sorted.map((e) => e.model))],
574+
modes: [...new Set(sorted.map((e) => e.mode))],
569575
totalTokens,
570576
totalCost,
571577
callCount: sorted.length,
@@ -645,9 +651,14 @@ export async function handleGetDashboardSessions(
645651
let summaries = await buildSessionSummaries(events, globalStoragePath)
646652

647653
// Apply optional model/provider filters (post-grouping).
654+
// The model filter checks `models` (the full set used in the session)
655+
// so that sessions which switched models are still matched; it falls
656+
// back to the legacy `model` field when `models` is absent.
648657
const filters = message.dashboardSessionFilters
649658
if (filters?.model) {
650-
summaries = summaries.filter((s) => s.model === filters.model)
659+
summaries = summaries.filter(
660+
(s) => s.models?.includes(filters.model!) ?? s.model === filters.model,
661+
)
651662
}
652663
if (filters?.provider) {
653664
summaries = summaries.filter((s) => s.provider === filters.provider)
@@ -688,6 +699,7 @@ export async function handleGetDashboardSessions(
688699
function mapEventToApiCall(event: UsageEventV1, index: number): APICallRecord {
689700
return {
690701
index,
702+
mode: event.mode,
691703
timestamp: new Date(event.occurredAt).getTime(),
692704
inputTokens: event.usage.inputTokens?.value ?? 0,
693705
outputTokens: event.usage.outputTokens?.value ?? 0,
@@ -746,6 +758,10 @@ async function buildSessionDetail(
746758
model: first.model,
747759
provider: first.provider,
748760
mode: first.mode,
761+
// Mirror buildSessionSummaries: capture every unique model/mode in
762+
// first-seen order so the detail view can show the full set.
763+
models: [...new Set(sorted.map((e) => e.model))],
764+
modes: [...new Set(sorted.map((e) => e.mode))],
749765
totalTokens,
750766
totalCost,
751767
callCount: sorted.length,
@@ -844,6 +860,8 @@ export async function handleGetDashboardSessionDetail(
844860
model: "",
845861
provider: "",
846862
mode: "",
863+
models: [],
864+
modes: [],
847865
totalTokens: 0,
848866
totalCost: 0,
849867
callCount: 0,

src/package.nls.ca.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/package.nls.de.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/package.nls.es.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/package.nls.fr.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/package.nls.hi.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)