Skip to content

Commit 6bada16

Browse files
committed
C10 live quota status foundation
1 parent 516b795 commit 6bada16

11 files changed

Lines changed: 182 additions & 49 deletions

scripts/smoke-core.cjs

Lines changed: 61 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -106,16 +106,16 @@ const { createInitialStatus, applyRefreshResults } = require(path.join(OUT, 'cor
106106

107107
// === Status bar text: all no-data ===
108108

109-
test('formatStatusBarText: all disabled returns no local usage', () => {
109+
test('formatStatusBarText: all disabled returns local history label', () => {
110110
const status = createInitialStatus([]);
111111
const t = formatStatusBarText(status);
112-
assert.strictEqual(t, 'PromptFuel: no local usage');
112+
assert.strictEqual(t, 'PromptFuel: local history');
113113
});
114114

115-
test('formatStatusBarText: all no-data returns no local usage', () => {
115+
test('formatStatusBarText: all no-data returns local history label', () => {
116116
const status = createInitialStatus(['claude', 'codex']);
117117
const t = formatStatusBarText(status);
118-
assert.strictEqual(t, 'PromptFuel: no local usage');
118+
assert.strictEqual(t, 'PromptFuel: local history');
119119
});
120120

121121
// === Status bar text: loaded states ===
@@ -136,7 +136,7 @@ test('formatStatusBarText: loaded shows compact aggregate with local suffix', ()
136136
);
137137
const t = formatStatusBarText(status);
138138
assert.ok(t.includes('5.0K'), `expected "5.0K" in "${t}"`);
139-
assert.ok(t.includes('local'), `expected "local" suffix in "${t}"`);
139+
assert.ok(t.includes('local history'), `expected "local history" suffix in "${t}"`);
140140
assert.ok(!t.includes(' | '), `should not include per-provider pipe separator in "${t}"`);
141141
});
142142

@@ -160,7 +160,7 @@ test('formatStatusBarText: mixed providers shows single aggregate', () => {
160160
const t = formatStatusBarText(status);
161161
assert.ok(t.startsWith('PromptFuel:'), `expected "PromptFuel:" prefix in "${t}"`);
162162
assert.ok(t.includes('5.0K'), `expected aggregate "5.0K" in "${t}"`);
163-
assert.ok(t.includes('local'), `expected "local" suffix in "${t}"`);
163+
assert.ok(t.includes('local history'), `expected "local history" suffix in "${t}"`);
164164
assert.ok(!t.includes(' | '), `should not include per-provider pipe separator in "${t}"`);
165165
});
166166

@@ -176,7 +176,7 @@ test('formatStatusBarText: both loaded shows single aggregate total', () => {
176176
);
177177
const t = formatStatusBarText(status);
178178
assert.ok(t.includes('15.5K'), `expected aggregate "15.5K" in "${t}"`);
179-
assert.ok(t.includes('local'), `expected "local" suffix in "${t}"`);
179+
assert.ok(t.includes('local history'), `expected "local history" suffix in "${t}"`);
180180
assert.ok(!t.includes(' | '), `should not include per-provider pipe separator in "${t}"`);
181181
});
182182

@@ -189,7 +189,17 @@ test('formatStatusBarText: large token counts use M suffix', () => {
189189
);
190190
const t = formatStatusBarText(status);
191191
assert.ok(t.includes('2.5M'), `expected "2.5M" in "${t}"`);
192-
assert.ok(t.includes('local'), `expected "local" suffix in "${t}"`);
192+
assert.ok(t.includes('local history'), `expected "local history" suffix in "${t}"`);
193+
});
194+
195+
test('formatStatusBarText: billion token counts use B suffix', () => {
196+
const status = applyRefreshResults(
197+
createInitialStatus(['claude']),
198+
[{ providerId: 'claude', status: 'ok', totalTokens: 6700000000, totalAssistantMessages: 10, filesFound: 5 }],
199+
);
200+
const t = formatStatusBarText(status);
201+
assert.ok(t.includes('6.7B'), `expected "6.7B" in "${t}"`);
202+
assert.ok(t.includes('local history'), `expected "local history" suffix in "${t}"`);
193203
});
194204

195205
test('formatStatusBarText: small token counts show raw number', () => {
@@ -199,7 +209,7 @@ test('formatStatusBarText: small token counts show raw number', () => {
199209
);
200210
const t = formatStatusBarText(status);
201211
assert.ok(t.includes('500'), `expected "500" in "${t}"`);
202-
assert.ok(t.includes('local'), `expected "local" suffix in "${t}"`);
212+
assert.ok(t.includes('local history'), `expected "local history" suffix in "${t}"`);
203213
});
204214

205215
// === Disabled provider omitted ===
@@ -256,7 +266,7 @@ test('formatTooltip: includes Local history only disclaimer', () => {
256266
test('formatTooltip: includes Live quota not enabled disclaimer', () => {
257267
const status = createInitialStatus(['claude']);
258268
const tooltip = formatTooltip(status);
259-
assert.ok(tooltip.includes('Live quota not enabled'), `expected "Live quota not enabled" in tooltip`);
269+
assert.ok(tooltip.includes('Live quota not enabled yet'), `expected "Live quota not enabled yet" in tooltip`);
260270
});
261271

262272
test('formatTooltip: includes Snapshots not included disclaimer', () => {
@@ -510,6 +520,7 @@ test('applyRefreshResults: sets lastRefreshedMs on each refresh', () => {
510520
{ providerId: 'claude', status: 'ok', totalTokens: 100, totalAssistantMessages: 1, filesFound: 1 },
511521
]);
512522
assert.ok(updated.lastRefreshedMs >= before, 'lastRefreshedMs should be recent');
523+
assert.ok(updated.localHistoryLastRefreshedMs >= before, 'localHistoryLastRefreshedMs should be recent');
513524
});
514525

515526
test('applyRefreshResults: second refresh updates lastRefreshedMs', () => {
@@ -522,6 +533,7 @@ test('applyRefreshResults: second refresh updates lastRefreshedMs', () => {
522533
{ providerId: 'claude', status: 'ok', totalTokens: 200, totalAssistantMessages: 2, filesFound: 1 },
523534
]);
524535
assert.ok(updated.lastRefreshedMs >= firstRefresh, 'second refresh should update timestamp');
536+
assert.ok(updated.localHistoryLastRefreshedMs >= firstRefresh, 'second refresh should update local timestamp');
525537
});
526538

527539
test('applyRefreshResults: preserves enabledProviderIds', () => {
@@ -701,7 +713,7 @@ test('formatStatusBarText: fallback to local history when no live quota', () =>
701713
[{ providerId: 'claude', status: 'ok', totalTokens: 5000, totalAssistantMessages: 2, filesFound: 1 }],
702714
);
703715
const t = formatStatusBarText(status);
704-
assert.ok(t.includes('local'), `expected "local" suffix "${t}"`);
716+
assert.ok(t.includes('local history'), `expected "local history" suffix "${t}"`);
705717
});
706718

707719
test('formatStatusBarText: live quota unavailable falls back to local history', () => {
@@ -717,7 +729,7 @@ test('formatStatusBarText: live quota unavailable falls back to local history',
717729
},
718730
]);
719731
const t = formatStatusBarText(updated);
720-
assert.ok(t.includes('local'), `expected "local" suffix when live quota unavailable "${t}"`);
732+
assert.ok(t.includes('local history'), `expected "local history" suffix when live quota unavailable "${t}"`);
721733
});
722734

723735
test('formatStatusBarText: live quota error falls back to local history', () => {
@@ -734,7 +746,7 @@ test('formatStatusBarText: live quota error falls back to local history', () =>
734746
},
735747
]);
736748
const t = formatStatusBarText(updated);
737-
assert.ok(t.includes('local'), `expected "local" suffix when live quota error "${t}"`);
749+
assert.ok(t.includes('local history'), `expected "local history" suffix when live quota error "${t}"`);
738750
assert.ok(!t.includes('secret'), `should not leak secrets "${t}"`);
739751
assert.ok(!t.includes('.jsonl'), `should not leak file paths "${t}"`);
740752
});
@@ -806,6 +818,42 @@ test('formatTooltip: live quota shows Local history refreshed timestamp', () =>
806818
assert.ok(tooltip.includes('Local history refreshed:'), `expected "Local history refreshed:" in tooltip`);
807819
});
808820

821+
test('applyLiveQuotaResults: preserves local history timestamp separately', () => {
822+
const status = applyRefreshResults(
823+
createInitialStatus(['claude']),
824+
[{ providerId: 'claude', status: 'ok', totalTokens: 5000, totalAssistantMessages: 2, filesFound: 1 }],
825+
);
826+
const localHistoryRefreshed = status.localHistoryLastRefreshedMs;
827+
const updated = applyLiveQuotaResults(status, [syntheticLiveQuota]);
828+
assert.strictEqual(updated.localHistoryLastRefreshedMs, localHistoryRefreshed);
829+
assert.ok(typeof updated.liveQuotaLastRefreshedMs === 'number', 'expected live quota timestamp');
830+
});
831+
832+
test('applyLiveQuotaResults: normalizes window labels, reset countdown, and status', () => {
833+
const status = createInitialStatus(['claude']);
834+
const updated = applyLiveQuotaResults(status, [syntheticLiveQuota]);
835+
const window = updated.liveQuotaStates[0].windows[0];
836+
assert.strictEqual(window.label, '5h');
837+
assert.strictEqual(window.status, 'available');
838+
assert.ok(typeof window.resetInMs === 'number', 'expected resetInMs');
839+
});
840+
841+
test('applyLiveQuotaResults: stores sanitized unavailable message only', () => {
842+
const status = createInitialStatus(['claude']);
843+
const updated = applyLiveQuotaResults(status, [
844+
{
845+
providerId: 'claude',
846+
windows: [],
847+
freshness: 'error',
848+
error: 'raw /path/to/file.jsonl secret-token',
849+
},
850+
]);
851+
const liveState = updated.liveQuotaStates[0];
852+
assert.strictEqual(liveState.status, 'error');
853+
assert.strictEqual(liveState.sanitizedMessage, 'Live quota unavailable');
854+
assert.strictEqual(liveState.error, undefined);
855+
});
856+
809857
test('formatTooltip: stale freshness shown correctly', () => {
810858
const staleLiveQuota = {
811859
...syntheticLiveQuota,

scripts/smoke-providers.cjs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ async function main() {
7070
assert.strictEqual(status.providerStates[0].status, 'no-data');
7171
assert.strictEqual(status.providerStates[1].status, 'no-data');
7272
assert.strictEqual(status.lastRefreshedMs, undefined);
73+
assert.strictEqual(status.localHistoryLastRefreshedMs, undefined);
74+
assert.strictEqual(status.liveQuotaLastRefreshedMs, undefined);
7375
assert.deepStrictEqual(status.enabledProviderIds, ['claude', 'codex']);
7476
});
7577

@@ -89,6 +91,8 @@ async function main() {
8991
const updated = applyRefreshResults(status, results);
9092
assert.ok(typeof updated.lastRefreshedMs === 'number', 'expected lastRefreshedMs to be a number');
9193
assert.ok(updated.lastRefreshedMs > 0, 'expected lastRefreshedMs > 0');
94+
assert.strictEqual(updated.localHistoryLastRefreshedMs, updated.lastRefreshedMs);
95+
assert.strictEqual(updated.liveQuotaLastRefreshedMs, undefined);
9296
});
9397

9498
await test('applyRefreshResults: ok with tokens -> loaded status', async () => {
@@ -184,10 +188,10 @@ async function main() {
184188

185189
// ===== formatStatusBarText: compact aggregate status bar =====
186190

187-
await test('formatStatusBarText: all providers no-data shows no local usage', async () => {
191+
await test('formatStatusBarText: all providers no-data shows local history label', async () => {
188192
const status = createInitialStatus(['claude', 'codex']);
189193
const text = formatStatusBarText(status);
190-
assert.strictEqual(text, 'PromptFuel: no local usage');
194+
assert.strictEqual(text, 'PromptFuel: local history');
191195
});
192196

193197
await test('formatStatusBarText: one provider loaded shows aggregate with local suffix', async () => {
@@ -198,7 +202,7 @@ async function main() {
198202
]);
199203
const text = formatStatusBarText(updated);
200204
assert.ok(text.includes('12.4K'), `expected "12.4K" in "${text}"`);
201-
assert.ok(text.includes('local'), `expected "local" in "${text}"`);
205+
assert.ok(text.includes('local history'), `expected "local history" in "${text}"`);
202206
assert.ok(!text.includes(' | '), `should not include pipe separator in "${text}"`);
203207
});
204208

@@ -210,7 +214,7 @@ async function main() {
210214
]);
211215
const text = formatStatusBarText(updated);
212216
assert.ok(text.includes('15.5K'), `expected aggregate "15.5K" in "${text}"`);
213-
assert.ok(text.includes('local'), `expected "local" in "${text}"`);
217+
assert.ok(text.includes('local history'), `expected "local history" in "${text}"`);
214218
assert.ok(!text.includes(' | '), `should not include pipe separator in "${text}"`);
215219
assert.ok(!text.includes('⛽'), `should not include emoji in "${text}"`);
216220
});
@@ -232,7 +236,7 @@ async function main() {
232236
]);
233237
const text = formatStatusBarText(updated);
234238
assert.ok(text.includes('5.0K'), `expected "5.0K" in "${text}"`);
235-
assert.ok(text.includes('local'), `expected "local" in "${text}"`);
239+
assert.ok(text.includes('local history'), `expected "local history" in "${text}"`);
236240
assert.ok(!text.includes(' | '), `should not include pipe separator in "${text}"`);
237241
});
238242

src/core/formatLiveQuota.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ function fallbackStatusBarText(status: PromptFuelStatus): string {
143143
const allNoData = status.providerStates.length > 0 &&
144144
status.providerStates.every(s => s.status === 'no-data');
145145
if (allNoData) {
146-
return 'PromptFuel: no local usage';
146+
return 'PromptFuel: local history';
147147
}
148148

149149
let totalTokens = 0;
@@ -156,13 +156,16 @@ function fallbackStatusBarText(status: PromptFuelStatus): string {
156156
}
157157

158158
if (!anyLoaded) {
159-
return 'PromptFuel: no local usage';
159+
return 'PromptFuel: local history';
160160
}
161161

162-
return `PromptFuel: ${formatTokenCountCompact(totalTokens)} local`;
162+
return `PromptFuel: ${formatTokenCountCompact(totalTokens)} local history`;
163163
}
164164

165165
function formatTokenCountCompact(count: number): string {
166+
if (count >= 1_000_000_000) {
167+
return `${(count / 1_000_000_000).toFixed(1)}B`;
168+
}
166169
if (count >= 1_000_000) {
167170
return `${(count / 1_000_000).toFixed(1)}M`;
168171
}
@@ -240,8 +243,8 @@ export function formatLiveQuotaTooltip(status: PromptFuelStatus): string {
240243
}
241244
}
242245

243-
if (status.lastRefreshedMs) {
244-
lines.push(formatLocalHistoryRefreshedAt(status.lastRefreshedMs));
246+
if (status.localHistoryLastRefreshedMs) {
247+
lines.push(formatLocalHistoryRefreshedAt(status.localHistoryLastRefreshedMs));
245248
}
246249

247250
return lines.join(LINE_SEPARATOR);
@@ -264,7 +267,7 @@ function formatLiveQuotaProviderSection(liveState: LiveQuotaStatus): string {
264267
const freshness = getFreshnessLabel(liveState.freshness);
265268

266269
if (liveState.freshness === 'unavailable' || liveState.freshness === 'error') {
267-
sectionLines.push(`${label} live quota: ${getSanitizedErrorLabel()} [${freshness}]`);
270+
sectionLines.push(`${label} live quota: ${liveState.sanitizedMessage ?? getSanitizedErrorLabel()} [${freshness}]`);
268271
return sectionLines.join(LINE_SEPARATOR);
269272
}
270273

src/core/liveQuotaTypes.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,16 @@ export type LiveQuotaFreshness = 'live' | 'cached' | 'stale' | 'unavailable' | '
44

55
export type LiveQuotaSourceKind = 'authenticated' | 'localSession' | 'cache' | 'stale' | 'unknown';
66

7+
export type LiveQuotaAvailability = 'available' | 'stale' | 'unavailable' | 'error';
8+
79
export interface LiveQuotaWindow {
810
windowId: QuotaWindowId;
11+
label?: string;
912
usedPercentage?: number;
1013
remainingPercentage?: number;
1114
resetsAtEpochMs?: number;
15+
resetInMs?: number;
16+
status?: LiveQuotaAvailability;
1217
sourceKind?: LiveQuotaSourceKind;
1318
sourceLabel?: string;
1419
sourceUpdatedEpochMs?: number;
@@ -18,7 +23,33 @@ export interface LiveQuotaWindow {
1823
export interface LiveQuotaStatus {
1924
providerId: string;
2025
windows: LiveQuotaWindow[];
26+
status?: LiveQuotaAvailability;
2127
freshness: LiveQuotaFreshness;
2228
lastUpdatedEpochMs?: number;
29+
sanitizedMessage?: string;
2330
error?: string;
2431
}
32+
33+
export function availabilityFromFreshness(freshness: LiveQuotaFreshness): LiveQuotaAvailability {
34+
if (freshness === 'error') {
35+
return 'error';
36+
}
37+
if (freshness === 'unavailable') {
38+
return 'unavailable';
39+
}
40+
if (freshness === 'stale') {
41+
return 'stale';
42+
}
43+
return 'available';
44+
}
45+
46+
export function getResetInMs(resetEpochMs: number | undefined, nowMs: number = Date.now()): number | undefined {
47+
if (resetEpochMs === undefined || !Number.isFinite(resetEpochMs)) {
48+
return undefined;
49+
}
50+
return Math.max(0, resetEpochMs - nowMs);
51+
}
52+
53+
export function getGenericQuotaUnavailableMessage(): string {
54+
return 'Live quota unavailable';
55+
}

src/core/refreshScheduler.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,9 @@ export class RefreshScheduler {
115115

116116
try {
117117
this.statusState = applyRefreshResults(this.statusState, localResults);
118-
this.statusState = applyLiveQuotaResults(this.statusState, liveResults);
118+
if (cfg.liveQuotaEnabled) {
119+
this.statusState = applyLiveQuotaResults(this.statusState, liveResults);
120+
}
119121
this.updateBar();
120122
this.onRefreshed?.();
121123
} finally {

0 commit comments

Comments
 (0)