Skip to content

Commit c769caa

Browse files
cursoragentgaoyu06
andcommitted
Merge branch 'cursor/desktop-usage-breakdown-17c0'
Co-authored-by: Gao Yu <gaoyu06@users.noreply.github.com>
2 parents cb010ae + 2ca5fe5 commit c769caa

5 files changed

Lines changed: 464 additions & 26 deletions

File tree

src/lib/OverviewPanel.svelte

Lines changed: 129 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
<script lang="ts">
22
import { CalendarDays, Activity } from 'lucide-svelte';
33
import UsageHeatmap from '$lib/UsageHeatmap.svelte';
4-
import { getDailyUsage, fmtTokens } from '$lib/usageStats';
4+
import Segmented from '$lib/ui/Segmented.svelte';
5+
import {
6+
getDailyUsage,
7+
fmtTokens,
8+
sumDimension,
9+
collectAgentLabels,
10+
type UsageDimension
11+
} from '$lib/usageStats';
512
import { t } from '$lib/i18n';
613
714
const DETAIL_DAYS = 15;
@@ -11,10 +18,39 @@
1118
.filter(([, d]) => d.in + d.out > 0)
1219
.sort((a, b) => (a[0] < b[0] ? 1 : -1))
1320
.slice(0, DETAIL_DAYS);
21+
const dayValues = days.map(([, d]) => d);
22+
// days is newest-first; merge oldest-first so the latest ACP name wins.
23+
const agentLabels = collectAgentLabels([...dayValues].reverse());
1424
15-
const provName = (p: string) => (p === 'other' ? t('settings.overview.other') : p);
16-
const provRows = (d: (typeof days)[number][1]) =>
17-
Object.entries(d.prov ?? {}).sort((a, b) => b[1].in + b[1].out - (a[1].in + a[1].out));
25+
let dim = $state<UsageDimension>('prov');
26+
27+
// 稳定 agent key → 展示名;acp:<id> 用记录到的名称,缺失时退回 id。
28+
const AGENT_NAMES: Record<string, string> = {
29+
jucode: 'JuCode',
30+
claude: 'Claude',
31+
codex: 'Codex',
32+
acp: 'ACP'
33+
};
34+
35+
function keyName(key: string): string {
36+
if (key === 'other') return t('settings.overview.other');
37+
if (dim === 'agents') {
38+
if (AGENT_NAMES[key]) return AGENT_NAMES[key];
39+
if (key.startsWith('acp:')) return agentLabels[key] ?? key.slice(4);
40+
}
41+
return key;
42+
}
43+
44+
const emptyKey = $derived(
45+
dim === 'prov'
46+
? 'settings.overview.noProvDetail'
47+
: dim === 'models'
48+
? 'settings.overview.noModelDetail'
49+
: 'settings.overview.noAgentDetail'
50+
);
51+
52+
const windowRows = $derived(sumDimension(dayValues, dim));
53+
const windowMax = $derived(windowRows.length ? windowRows[0][1].in + windowRows[0][1].out : 0);
1854
</script>
1955

2056
<div class="group">
@@ -23,21 +59,50 @@
2359
<UsageHeatmap />
2460

2561
<div class="detail">
26-
<div class="glabel"><Activity size={13} /> {t('settings.overview.detail')}</div>
62+
<div class="dhead">
63+
<div class="glabel"><Activity size={13} /> {t('settings.overview.detail')}</div>
64+
<Segmented
65+
value={dim}
66+
options={[
67+
{ value: 'prov', label: t('settings.overview.dimProvider') },
68+
{ value: 'models', label: t('settings.overview.dimModel') },
69+
{ value: 'agents', label: t('settings.overview.dimAgent') }
70+
]}
71+
onChange={(v) => (dim = v as UsageDimension)}
72+
/>
73+
</div>
2774
{#if days.length === 0}
2875
<p class="hint">{t('settings.overview.noData')}</p>
2976
{:else}
77+
{#if windowRows.length > 0}
78+
<div class="win">
79+
<div class="wtitle">{t('settings.overview.windowTotal', { n: DETAIL_DAYS })}</div>
80+
{#each windowRows as [k, v] (k)}
81+
<div class="wrow">
82+
<span class="wname" title={k}>{keyName(k)}</span>
83+
<span class="wbar">
84+
<span
85+
class="wfill"
86+
style:width={`${windowMax ? Math.max(1, ((v.in + v.out) / windowMax) * 100) : 0}%`}
87+
></span>
88+
</span>
89+
<span class="wval mono">↑{fmtTokens(v.in)} ↓{fmtTokens(v.out)} · {fmtTokens(v.in + v.out)}</span>
90+
</div>
91+
{/each}
92+
</div>
93+
{/if}
3094
{#each days as [date, d] (date)}
95+
{@const rows = sumDimension([d], dim)}
3196
<div class="drow">
3297
<span class="dd mono">{date}</span>
3398
<span class="dt mono">↑{fmtTokens(d.in)} ↓{fmtTokens(d.out)} · {t('settings.overview.total')} {fmtTokens(d.in + d.out)}</span>
3499
<span class="dp">
35-
{#if d.prov && Object.keys(d.prov).length > 0}
36-
{#each provRows(d) as [p, v] (p)}
37-
<span class="chip">{provName(p)} <b class="mono">↑{fmtTokens(v.in)} ↓{fmtTokens(v.out)}</b></span>
100+
{#if rows.length > 0}
101+
{#each rows as [k, v] (k)}
102+
<span class="chip" title={k}>{keyName(k)} <b class="mono">↑{fmtTokens(v.in)} ↓{fmtTokens(v.out)}</b></span>
38103
{/each}
39104
{:else}
40-
<span class="chip dimmed">{t('settings.overview.noProvDetail')}</span>
105+
<span class="chip dimmed">{t(emptyKey)}</span>
41106
{/if}
42107
</span>
43108
</div>
@@ -69,6 +134,61 @@
69134
.detail {
70135
margin-top: 20px;
71136
}
137+
.dhead {
138+
display: flex;
139+
align-items: center;
140+
justify-content: space-between;
141+
gap: 12px;
142+
margin-bottom: 10px;
143+
}
144+
.dhead .glabel {
145+
margin-bottom: 0;
146+
}
147+
.win {
148+
margin-bottom: 14px;
149+
padding: 10px 12px;
150+
border: 1px solid var(--border);
151+
border-radius: 8px;
152+
background: var(--surface);
153+
}
154+
.wtitle {
155+
font-size: 11px;
156+
font-weight: 600;
157+
color: var(--dim2);
158+
margin-bottom: 8px;
159+
}
160+
.wrow {
161+
display: grid;
162+
grid-template-columns: minmax(60px, 160px) 1fr auto;
163+
gap: 10px;
164+
align-items: center;
165+
padding: 3px 0;
166+
}
167+
.wname {
168+
font-size: 12px;
169+
color: var(--text);
170+
overflow: hidden;
171+
text-overflow: ellipsis;
172+
white-space: nowrap;
173+
}
174+
.wbar {
175+
height: 6px;
176+
border-radius: 3px;
177+
background: var(--surface2);
178+
box-shadow: inset 0 0 0 1px var(--border);
179+
overflow: hidden;
180+
}
181+
.wfill {
182+
display: block;
183+
height: 100%;
184+
border-radius: 3px;
185+
background: var(--accent);
186+
}
187+
.wval {
188+
font-size: 11px;
189+
color: var(--dim);
190+
white-space: nowrap;
191+
}
72192
.drow {
73193
display: grid;
74194
grid-template-columns: 1fr auto;

src/lib/chat.svelte.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -776,7 +776,19 @@ export class ChatState {
776776
this.totalOut += out;
777777
// Estimate cost from tokens when the engine doesn't report it itself.
778778
if (!this.#engineCost) this.cost += costUsd(this.model, inn, out);
779-
recordUsage(inn, out, this.provider);
779+
recordUsage(inn, out, {
780+
provider: this.provider,
781+
model: this.model,
782+
// Stable agent keys: native backend ids as-is; acp sessions keyed
783+
// by registry id so renames don't split history.
784+
agent:
785+
this.backendId === 'acp'
786+
? this.acpAgentId
787+
? `acp:${this.acpAgentId}`
788+
: 'acp'
789+
: this.backendId,
790+
agentLabel: this.backendId === 'acp' ? this.acpAgentName : undefined
791+
});
780792
// Prefer the active assistant message (jucode reports usage per
781793
// message, mid-turn). When it's already reset — e.g. claude reports
782794
// one usage at the end of the turn, after the assistant finished —

src/lib/i18n/messages/settings.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,12 @@ const settings = {
282282
total: '共',
283283
other: '其他',
284284
noProvDetail: '无 Provider 明细',
285+
noModelDetail: '无模型明细',
286+
noAgentDetail: '无智能体明细',
287+
dimProvider: '渠道',
288+
dimModel: '模型',
289+
dimAgent: '智能体',
290+
windowTotal: '近 {n} 天合计',
285291
heatmap: {
286292
month: '{n}月',
287293
ariaLabel: '每日 Token 用量热力图',
@@ -580,6 +586,12 @@ const settings = {
580586
total: 'total',
581587
other: 'Other',
582588
noProvDetail: 'No provider breakdown',
589+
noModelDetail: 'No model breakdown',
590+
noAgentDetail: 'No agent breakdown',
591+
dimProvider: 'Provider',
592+
dimModel: 'Model',
593+
dimAgent: 'Agent',
594+
windowTotal: 'Last {n} days total',
583595
heatmap: {
584596
month: 'M{n}',
585597
ariaLabel: 'Daily token usage heatmap',

0 commit comments

Comments
 (0)