|
- {localizedRow.label}
- {localizedRow.detail && (
- {localizedRow.detail}
+ {row.children ? (
+
+ ) : (
+ {localizedRow.label}
)}
|
diff --git a/src/renderer/src/styles.css b/src/renderer/src/styles.css
index af0c423f..90388d14 100644
--- a/src/renderer/src/styles.css
+++ b/src/renderer/src/styles.css
@@ -14786,6 +14786,43 @@ details.settings-section > :not(summary) + :not(summary) {
text-align: left;
}
+.token-usage__expand {
+ display: flex;
+ align-items: center;
+ gap: var(--space-2);
+ width: 100%;
+ padding: var(--space-1) 0;
+ border: 0;
+ background: transparent;
+ color: inherit;
+ font: inherit;
+ text-align: left;
+ cursor: pointer;
+}
+
+.token-usage__expand:hover {
+ color: var(--accent);
+}
+
+.token-usage__expand:focus-visible {
+ outline: 2px solid var(--accent);
+ outline-offset: 2px;
+}
+
+.token-usage__expand svg {
+ flex-shrink: 0;
+}
+
+.token-usage__expand[aria-expanded='true'] svg {
+ transform: rotate(90deg);
+}
+
+.token-usage .token-usage__model-row th:first-child {
+ padding-left: var(--space-6);
+ color: var(--text-secondary);
+ font-weight: 400;
+}
+
.token-usage tbody th span,
.token-usage tbody th small {
display: block;
diff --git a/src/renderer/src/token-usage.test.ts b/src/renderer/src/token-usage.test.ts
index a92af1bb..470a45ed 100644
--- a/src/renderer/src/token-usage.test.ts
+++ b/src/renderer/src/token-usage.test.ts
@@ -67,10 +67,11 @@ describe('token usage aggregation', () => {
})
expect(groupTokenUsage(usage, 'project')).toEqual([
{
- key: 'project:project-1:runtime:model:model:gpt-5',
+ key: 'project:project-1',
label: '项目一',
- model: 'gpt-5',
- runtime: 'model',
+ model: '',
+ runtime: '',
+ children: groupTokenUsage(usage, 'model'),
inputTokens: 112,
outputTokens: 23,
cacheReadTokens: 40,
@@ -143,7 +144,7 @@ describe('token usage aggregation', () => {
expect(groupTokenUsage(usage, 'model')[0]?.label).toBe('')
})
- it('keeps project and conversation totals separated by model', () => {
+ it('aggregates project and conversation totals with model children', () => {
const usage = makeTokenUsage()
usage.records.push({
...usage.records[0]!,
@@ -154,8 +155,22 @@ describe('token usage aggregation', () => {
output: 2
})
- expect(groupTokenUsage(usage, 'project')).toHaveLength(2)
- expect(groupTokenUsage(usage, 'conversation')).toHaveLength(3)
+ const projects = groupTokenUsage(usage, 'project')
+ expect(projects).toHaveLength(1)
+ expect(projects[0]).toMatchObject({
+ inputTokens: 119,
+ outputTokens: 25,
+ cacheInputTokens: 169,
+ cacheHitRate: 80 / 169,
+ totalTokens: 144
+ })
+ expect(projects[0]?.children).toEqual(groupTokenUsage(usage, 'model'))
+ const conversations = groupTokenUsage(usage, 'conversation')
+ expect(conversations).toHaveLength(2)
+ expect(conversations[0]?.children).toHaveLength(2)
+ expect(conversations[0]?.totalTokens).toBe(129)
+ expect(conversations[1]?.children).toHaveLength(1)
+ expect(conversations[1]?.totalTokens).toBe(15)
})
it('groups identical model names by Runtime instead of provider', () => {
diff --git a/src/renderer/src/token-usage.ts b/src/renderer/src/token-usage.ts
index 706ea774..bfa3629d 100644
--- a/src/renderer/src/token-usage.ts
+++ b/src/renderer/src/token-usage.ts
@@ -17,6 +17,7 @@ export type TokenUsageGroupRow = TokenUsageTotals & {
label: string
model: string
runtime: string
+ children?: TokenUsageGroupRow[]
}
type TokenUsageRecord = TokenUsageSummary['records'][number]
@@ -80,10 +81,10 @@ function groupIdentity(
? `project:${record.projectId}`
: 'project:unassigned'
return {
- key: `${projectKey}:${runtimeModelKey}`,
+ key: projectKey,
label: record.projectName?.trim() || '',
- model,
- runtime
+ model: '',
+ runtime: ''
}
}
@@ -92,10 +93,10 @@ function groupIdentity(
? `conversation:${record.conversationId}`
: 'conversation:deleted'
return {
- key: `${conversationKey}:${runtimeModelKey}`,
+ key: conversationKey,
label: record.conversationTitle?.trim() || '',
- model,
- runtime
+ model: '',
+ runtime: ''
}
}
@@ -118,9 +119,15 @@ export function groupTokenUsage(
group: TokenUsageGroup
): TokenUsageGroupRow[] {
const rows = new Map()
+ const groupedRecords = new Map()
for (const record of tokenUsage.records) {
const identity = groupIdentity(record, group)
+ if (group !== 'model') {
+ const records = groupedRecords.get(identity.key) ?? []
+ records.push(record)
+ groupedRecords.set(identity.key, records)
+ }
const usage = usageNumbers(record, record.provider)
const existing = rows.get(identity.key)
@@ -151,5 +158,15 @@ export function groupTokenUsage(
})
}
- return [...rows.values()]
+ return [...rows.values()].map((row) =>
+ group === 'model'
+ ? row
+ : {
+ ...row,
+ children: groupTokenUsage(
+ { ...tokenUsage, records: groupedRecords.get(row.key)! },
+ 'model'
+ )
+ }
+ )
}
|