Skip to content

Commit 2ca5fe5

Browse files
cursoragentgaoyu06
andcommitted
Harden usage breakdown persist and overview UI
Co-authored-by: Gao Yu <gaoyu06@users.noreply.github.com>
1 parent ca89ac4 commit 2ca5fe5

3 files changed

Lines changed: 95 additions & 20 deletions

File tree

src/lib/OverviewPanel.svelte

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@
1919
.sort((a, b) => (a[0] < b[0] ? 1 : -1))
2020
.slice(0, DETAIL_DAYS);
2121
const dayValues = days.map(([, d]) => d);
22-
const agentLabels = collectAgentLabels(dayValues);
22+
// days is newest-first; merge oldest-first so the latest ACP name wins.
23+
const agentLabels = collectAgentLabels([...dayValues].reverse());
2324
2425
let dim = $state<UsageDimension>('prov');
2526

src/lib/usageStats.test.ts

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,33 @@ describe('recordUsage', () => {
6262
expect(m.getDailyUsage()).toEqual({});
6363
});
6464

65+
it('ignores invalid token counts without poisoning valid totals', async () => {
66+
const m = await fresh();
67+
m.recordUsage(Number.NaN, 4, { provider: 'openai', model: 'gpt-6', agent: 'codex' });
68+
m.recordUsage(Number.POSITIVE_INFINITY, -2, {
69+
provider: 'invalid',
70+
model: 'invalid',
71+
agent: 'invalid'
72+
});
73+
const day = m.getDailyUsage()[m.dayKey(new Date())];
74+
expect(day).toEqual({
75+
in: 0,
76+
out: 4,
77+
prov: { openai: { in: 0, out: 4 } },
78+
models: { 'gpt-6': { in: 0, out: 4 } },
79+
agents: { codex: { in: 0, out: 4 } }
80+
});
81+
});
82+
83+
it('records keys that match object prototype properties', async () => {
84+
const m = await fresh();
85+
m.recordUsage(10, 1, { provider: 'constructor', model: '__proto__', agent: 'toString' });
86+
const day = m.getDailyUsage()[m.dayKey(new Date())];
87+
expect(Object.entries(day.prov!)).toEqual([['constructor', { in: 10, out: 1 }]]);
88+
expect(Object.entries(day.models!)).toEqual([['__proto__', { in: 10, out: 1 }]]);
89+
expect(Object.entries(day.agents!)).toEqual([['toString', { in: 10, out: 1 }]]);
90+
});
91+
6592
it('stores agent display labels for stable keys', async () => {
6693
const m = await fresh();
6794
m.recordUsage(10, 1, { agent: 'acp:gemini-cli', agentLabel: 'Gemini CLI' });
@@ -110,12 +137,26 @@ describe('load compatibility', () => {
110137
'jucode-usage-daily': JSON.stringify({
111138
'not-a-day': { in: 99, out: 99 },
112139
'2026-1-2': { in: 99, out: 99 },
113-
'2026-01-02': { in: 'x', out: 5, models: { m1: { in: 'y', out: 1 } } }
140+
'2026-01-02': {
141+
in: '1e400',
142+
out: 5,
143+
models: {
144+
m1: { in: 'y', out: 1 },
145+
' ': { in: 20, out: 2 },
146+
invalid: { in: 'Infinity', out: -1 }
147+
},
148+
agentLabels: { ' acp:x ': ' New name ', '': 'junk', 'acp:y': ' ' }
149+
}
114150
})
115151
});
116152
const usage = m.getDailyUsage();
117153
expect(Object.keys(usage)).toEqual(['2026-01-02']);
118-
expect(usage['2026-01-02']).toEqual({ in: 0, out: 5, models: { m1: { in: 0, out: 1 } } });
154+
expect(usage['2026-01-02']).toEqual({
155+
in: 0,
156+
out: 5,
157+
models: { m1: { in: 0, out: 1 } },
158+
agentLabels: { 'acp:x': 'New name' }
159+
});
119160
});
120161

121162
it('starts empty when the stored JSON is corrupt', async () => {

src/lib/usageStats.ts

Lines changed: 50 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,22 @@ export interface UsageMeta {
3535
let cache: Record<string, DayUsage> | null = null;
3636
let saveTimer: ReturnType<typeof setTimeout> | null = null;
3737

38+
function tokenCount(v: unknown): number {
39+
const n = typeof v === 'number' || typeof v === 'string' ? Number(v) : 0;
40+
return Number.isFinite(n) && n >= 0 ? n : 0;
41+
}
42+
43+
function ownUsage(map: Record<string, DimUsage>, key: string): DimUsage {
44+
if (Object.prototype.hasOwnProperty.call(map, key)) return map[key];
45+
const usage = { in: 0, out: 0 };
46+
Object.defineProperty(map, key, { value: usage, enumerable: true, configurable: true, writable: true });
47+
return usage;
48+
}
49+
50+
function setOwnLabel(map: Record<string, string>, key: string, label: string) {
51+
Object.defineProperty(map, key, { value: label, enumerable: true, configurable: true, writable: true });
52+
}
53+
3854
export function dayKey(d: Date): string {
3955
const m = `${d.getMonth() + 1}`.padStart(2, '0');
4056
const day = `${d.getDate()}`.padStart(2, '0');
@@ -44,9 +60,17 @@ export function dayKey(d: Date): string {
4460
function readDim(v: unknown): Record<string, DimUsage> | undefined {
4561
if (!v || typeof v !== 'object') return undefined;
4662
const out: Record<string, DimUsage> = {};
47-
for (const [k, e] of Object.entries(v as Record<string, Partial<DimUsage> | null>))
48-
out[k] = { in: Number(e?.in) || 0, out: Number(e?.out) || 0 };
49-
return out;
63+
for (const [rawKey, e] of Object.entries(v as Record<string, Partial<DimUsage> | null>)) {
64+
const key = rawKey.trim();
65+
if (!key) continue;
66+
const inTokens = tokenCount(e?.in);
67+
const outTokens = tokenCount(e?.out);
68+
if (!inTokens && !outTokens) continue;
69+
const usage = ownUsage(out, key);
70+
usage.in += inTokens;
71+
usage.out += outTokens;
72+
}
73+
return Object.keys(out).length ? out : undefined;
5074
}
5175

5276
function load(): Record<string, DayUsage> {
@@ -59,17 +83,21 @@ function load(): Record<string, DayUsage> {
5983
for (const [k, v] of Object.entries(parsed)) {
6084
const d = v as Partial<DayUsage> | null;
6185
if (!/^\d{4}-\d{2}-\d{2}$/.test(k)) continue;
62-
const day: DayUsage = { in: Number(d?.in) || 0, out: Number(d?.out) || 0 };
86+
const day: DayUsage = { in: tokenCount(d?.in), out: tokenCount(d?.out) };
6387
const prov = readDim(d?.prov);
6488
if (prov) day.prov = prov;
6589
const models = readDim(d?.models);
6690
if (models) day.models = models;
6791
const agents = readDim(d?.agents);
6892
if (agents) day.agents = agents;
6993
if (d?.agentLabels && typeof d.agentLabels === 'object') {
70-
day.agentLabels = {};
71-
for (const [ak, al] of Object.entries(d.agentLabels))
72-
if (typeof al === 'string' && al) day.agentLabels[ak] = al;
94+
const labels: Record<string, string> = {};
95+
for (const [rawKey, rawLabel] of Object.entries(d.agentLabels)) {
96+
const key = rawKey.trim();
97+
const label = typeof rawLabel === 'string' ? rawLabel.trim() : '';
98+
if (key && label) setOwnLabel(labels, key, label);
99+
}
100+
if (Object.keys(labels).length) day.agentLabels = labels;
73101
}
74102
cache[k] = day;
75103
}
@@ -98,24 +126,26 @@ function persist() {
98126
/** 空白/缺失的 key 归入 'other' 桶。 */
99127
function bump(map: Record<string, DimUsage>, key: string | undefined, inT: number, outT: number): string {
100128
const k = key?.trim() || 'other';
101-
const e = (map[k] ??= { in: 0, out: 0 });
129+
const e = ownUsage(map, k);
102130
e.in += inT;
103131
e.out += outT;
104132
return k;
105133
}
106134

107135
export function recordUsage(inTokens: number, outTokens: number, meta?: UsageMeta) {
108-
if (!inTokens && !outTokens) return;
136+
const inT = tokenCount(inTokens);
137+
const outT = tokenCount(outTokens);
138+
if (!inT && !outT) return;
109139
const map = load();
110140
const k = dayKey(new Date());
111141
const d = (map[k] ??= { in: 0, out: 0 });
112-
d.in += inTokens;
113-
d.out += outTokens;
114-
bump((d.prov ??= {}), meta?.provider, inTokens, outTokens);
115-
bump((d.models ??= {}), meta?.model, inTokens, outTokens);
116-
const agentKey = bump((d.agents ??= {}), meta?.agent, inTokens, outTokens);
142+
d.in += inT;
143+
d.out += outT;
144+
bump((d.prov ??= {}), meta?.provider, inT, outT);
145+
bump((d.models ??= {}), meta?.model, inT, outT);
146+
const agentKey = bump((d.agents ??= {}), meta?.agent, inT, outT);
117147
const label = meta?.agentLabel?.trim();
118-
if (label && agentKey !== 'other') (d.agentLabels ??= {})[agentKey] = label;
148+
if (label && agentKey !== 'other') setOwnLabel((d.agentLabels ??= {}), agentKey, label);
119149
persist();
120150
}
121151

@@ -130,7 +160,7 @@ export function sumDimension(days: DayUsage[], dim: UsageDimension): [string, Di
130160
const m = d[dim];
131161
if (!m) continue;
132162
for (const [k, v] of Object.entries(m)) {
133-
const e = (acc[k] ??= { in: 0, out: 0 });
163+
const e = ownUsage(acc, k);
134164
e.in += v.in;
135165
e.out += v.out;
136166
}
@@ -141,7 +171,10 @@ export function sumDimension(days: DayUsage[], dim: UsageDimension): [string, Di
141171
/** 汇总各天记录到的 agent 展示名(后出现的覆盖先出现的)。 */
142172
export function collectAgentLabels(days: DayUsage[]): Record<string, string> {
143173
const out: Record<string, string> = {};
144-
for (const d of days) if (d.agentLabels) Object.assign(out, d.agentLabels);
174+
for (const d of days) {
175+
if (!d.agentLabels) continue;
176+
for (const [key, label] of Object.entries(d.agentLabels)) setOwnLabel(out, key, label);
177+
}
145178
return out;
146179
}
147180

0 commit comments

Comments
 (0)