@latentminds/pi-quotas 0.2.6 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,357 @@
1
+ import type { Theme } from "@mariozechner/pi-coding-agent";
2
+ import { DynamicBorder } from "@mariozechner/pi-coding-agent";
3
+ import type { Component } from "@mariozechner/pi-tui";
4
+ import { Loader, matchesKey, truncateToWidth } from "@mariozechner/pi-tui";
5
+ import pkg from "../../../package.json" with { type: "json" };
6
+ import type {
7
+ AggregateResult,
8
+ } from "../../lib/session-tokens.js";
9
+ import {
10
+ formatCost,
11
+ formatNumber,
12
+ formatTokenSummary,
13
+ } from "../../lib/session-tokens.js";
14
+
15
+ type TokensState =
16
+ | { type: "loading" }
17
+ | { type: "loaded"; result: AggregateResult };
18
+
19
+ type TabId = "overview" | "models" | "sessions";
20
+
21
+ const TABS: Array<{ id: TabId; label: string }> = [
22
+ { id: "overview", label: "Overview" },
23
+ { id: "models", label: "Models" },
24
+ { id: "sessions", label: "Sessions" },
25
+ ];
26
+
27
+ function renderProgressBar(
28
+ value: number,
29
+ max: number,
30
+ width: number,
31
+ theme: Theme,
32
+ ): string {
33
+ if (max <= 0 || value <= 0) return theme.fg("dim", "░".repeat(width));
34
+ const ratio = Math.min(1, value / max);
35
+ const filled = Math.round(ratio * width);
36
+ const parts: string[] = [];
37
+ for (let i = 0; i < width; i++) {
38
+ parts.push(i < filled ? theme.fg("accent", "█") : theme.fg("dim", "░"));
39
+ }
40
+ return parts.join("");
41
+ }
42
+
43
+ export class TokensComponent implements Component {
44
+ private state: TokensState = { type: "loading" };
45
+ private loader: Loader | null = null;
46
+ private activeTab: TabId = "overview";
47
+ private scrollOffset = 0;
48
+
49
+ constructor(
50
+ private theme: Theme,
51
+ private tui: any,
52
+ private onClose: () => void,
53
+ private onRefetch: () => void,
54
+ private cwd?: string,
55
+ ) {
56
+ this.startLoader();
57
+ }
58
+
59
+ private startLoader(): void {
60
+ this.loader = new Loader(
61
+ this.tui,
62
+ (s: string) => this.theme.fg("accent", s),
63
+ (s: string) => this.theme.fg("muted", s),
64
+ "Scanning sessions...",
65
+ );
66
+ }
67
+
68
+ destroy(): void {
69
+ this.loader?.stop();
70
+ this.loader = null;
71
+ }
72
+
73
+ setState(state: TokensState): void {
74
+ if (state.type === "loading") {
75
+ this.loader?.stop();
76
+ this.startLoader();
77
+ this.scrollOffset = 0;
78
+ } else if (this.state.type === "loading") {
79
+ this.loader?.stop();
80
+ this.loader = null;
81
+ }
82
+ this.state = state;
83
+ }
84
+
85
+ handleInput(data: string): boolean {
86
+ if (matchesKey(data, "escape") || data === "q") {
87
+ this.onClose();
88
+ return true;
89
+ }
90
+ if (data === "r") {
91
+ this.onRefetch();
92
+ return true;
93
+ }
94
+ if (data === "\t" || data === "right") {
95
+ const idx = TABS.findIndex((t) => t.id === this.activeTab);
96
+ this.activeTab = TABS[(idx + 1) % TABS.length].id;
97
+ this.scrollOffset = 0;
98
+ this.tui.requestRender();
99
+ return true;
100
+ }
101
+ if (data === "left") {
102
+ const idx = TABS.findIndex((t) => t.id === this.activeTab);
103
+ this.activeTab = TABS[(idx - 1 + TABS.length) % TABS.length].id;
104
+ this.scrollOffset = 0;
105
+ this.tui.requestRender();
106
+ return true;
107
+ }
108
+ if (data === "1") {
109
+ this.activeTab = "overview";
110
+ this.scrollOffset = 0;
111
+ this.tui.requestRender();
112
+ return true;
113
+ }
114
+ if (data === "2") {
115
+ this.activeTab = "models";
116
+ this.scrollOffset = 0;
117
+ this.tui.requestRender();
118
+ return true;
119
+ }
120
+ if (data === "3") {
121
+ this.activeTab = "sessions";
122
+ this.scrollOffset = 0;
123
+ this.tui.requestRender();
124
+ return true;
125
+ }
126
+ if (matchesKey(data, "down") || data === "j") {
127
+ this.scrollOffset++;
128
+ this.tui.requestRender();
129
+ return true;
130
+ }
131
+ if (matchesKey(data, "up") || data === "k") {
132
+ this.scrollOffset = Math.max(0, this.scrollOffset - 1);
133
+ this.tui.requestRender();
134
+ return true;
135
+ }
136
+ return false;
137
+ }
138
+
139
+ render(width: number): string[] {
140
+ const lines: string[] = [];
141
+ const border = new DynamicBorder((s: string) => this.theme.fg("border", s));
142
+ lines.push(...border.render(width));
143
+ lines.push(
144
+ truncateToWidth(
145
+ ` ${this.theme.fg("accent", this.theme.bold("Token Usage"))}`,
146
+ width,
147
+ ),
148
+ );
149
+
150
+ // Tab bar
151
+ const tabParts = TABS.map((tab) => {
152
+ if (tab.id === this.activeTab) {
153
+ return this.theme.fg("accent", this.theme.bold(`[${tab.label}]`));
154
+ }
155
+ return this.theme.fg("dim", ` ${tab.label} `);
156
+ });
157
+ lines.push(truncateToWidth(` ${tabParts.join(" ")}`, width));
158
+ lines.push("");
159
+
160
+ if (this.state.type === "loading") {
161
+ lines.push(
162
+ ...(this.loader
163
+ ? this.loader.render(width)
164
+ : [this.theme.fg("muted", " Scanning sessions...")]),
165
+ );
166
+ } else {
167
+ const contentLines = this.renderTab(width);
168
+ // Apply scroll
169
+ const maxVisible = Math.max(1, 20); // approximate visible lines
170
+ const visible = contentLines.slice(
171
+ this.scrollOffset,
172
+ this.scrollOffset + maxVisible,
173
+ );
174
+ lines.push(...visible);
175
+ }
176
+
177
+ lines.push("");
178
+ lines.push(
179
+ this.theme.fg(
180
+ "dim",
181
+ ` pi-quotas v${pkg.version} · 1/2/3 switch tab r refresh q/Esc close`,
182
+ ),
183
+ );
184
+ lines.push(...border.render(width));
185
+ return lines;
186
+ }
187
+
188
+ private renderTab(width: number): string[] {
189
+ if (this.state.type !== "loaded") return [];
190
+ const { result } = this.state;
191
+
192
+ switch (this.activeTab) {
193
+ case "overview":
194
+ return this.renderOverview(result, width);
195
+ case "models":
196
+ return this.renderModels(result, width);
197
+ case "sessions":
198
+ return this.renderSessions(result, width);
199
+ }
200
+ }
201
+
202
+ private renderOverview(result: AggregateResult, width: number): string[] {
203
+ const lines: string[] = [];
204
+ const { totals } = result;
205
+
206
+ lines.push(
207
+ truncateToWidth(
208
+ ` ${this.theme.fg("accent", this.theme.bold("Summary"))}`,
209
+ width,
210
+ ),
211
+ );
212
+ lines.push("");
213
+
214
+ const statRows = [
215
+ ["Sessions", String(result.sessionCount)],
216
+ ["Messages", String(result.messageCount)],
217
+ ["Input tokens", formatNumber(totals.input)],
218
+ ["Output tokens", formatNumber(totals.output)],
219
+ ["Cache read", formatNumber(totals.cacheRead)],
220
+ ["Cache write", formatNumber(totals.cacheWrite)],
221
+ ["Total tokens", formatNumber(totals.totalTokens)],
222
+ ["Total cost", formatCost(totals.costTotal)],
223
+ ];
224
+
225
+ const labelWidth = Math.max(...statRows.map(([l]) => l.length));
226
+ for (const [label, value] of statRows) {
227
+ lines.push(
228
+ truncateToWidth(
229
+ ` ${this.theme.fg("dim", label.padEnd(labelWidth))} ${this.theme.fg("accent", value)}`,
230
+ width,
231
+ ),
232
+ );
233
+ }
234
+
235
+ // Provider breakdown
236
+ if (result.byProvider.length > 0) {
237
+ lines.push("");
238
+ lines.push(
239
+ truncateToWidth(
240
+ ` ${this.theme.fg("accent", this.theme.bold("By Provider"))}`,
241
+ width,
242
+ ),
243
+ );
244
+ lines.push("");
245
+ const maxCost = Math.max(
246
+ ...result.byProvider.map((p) => p.tokens.costTotal),
247
+ );
248
+ for (const provider of result.byProvider) {
249
+ const barWidth = Math.min(20, Math.max(8, width - 40));
250
+ const bar = renderProgressBar(
251
+ provider.tokens.costTotal,
252
+ maxCost,
253
+ barWidth,
254
+ this.theme,
255
+ );
256
+ const cost = formatCost(provider.tokens.costTotal);
257
+ const msgs = `${provider.messageCount} msgs`;
258
+ lines.push(
259
+ truncateToWidth(
260
+ ` ${this.theme.fg("accent", provider.provider.padEnd(20))} ${bar} ${this.theme.fg("accent", cost)} ${this.theme.fg("dim", msgs)}`,
261
+ width,
262
+ ),
263
+ );
264
+ }
265
+ }
266
+
267
+ return lines;
268
+ }
269
+
270
+ private renderModels(result: AggregateResult, width: number): string[] {
271
+ const lines: string[] = [];
272
+
273
+ if (result.byModel.length === 0) {
274
+ lines.push(this.theme.fg("dim", " No assistant messages found"));
275
+ return lines;
276
+ }
277
+
278
+ const maxCost = Math.max(...result.byModel.map((m) => m.tokens.costTotal));
279
+
280
+ for (const model of result.byModel) {
281
+ const barWidth = Math.min(16, Math.max(8, width - 50));
282
+ const bar = renderProgressBar(
283
+ model.tokens.costTotal,
284
+ maxCost,
285
+ barWidth,
286
+ this.theme,
287
+ );
288
+ const cost = formatCost(model.tokens.costTotal);
289
+ const label =
290
+ model.provider === model.model
291
+ ? model.model
292
+ : `${model.provider}/${model.model}`;
293
+ const shortLabel = label.length > 30 ? label.slice(0, 27) + "..." : label;
294
+
295
+ lines.push(
296
+ truncateToWidth(` ${this.theme.fg("accent", shortLabel)}`, width),
297
+ );
298
+ lines.push(
299
+ truncateToWidth(
300
+ ` ${bar} ${this.theme.fg("accent", cost)} ${this.theme.fg("dim", formatTokenSummary(model.tokens))}`,
301
+ width,
302
+ ),
303
+ );
304
+ }
305
+
306
+ return lines;
307
+ }
308
+
309
+ private renderSessions(result: AggregateResult, width: number): string[] {
310
+ const lines: string[] = [];
311
+
312
+ if (result.bySession.length === 0) {
313
+ lines.push(this.theme.fg("dim", " No sessions found"));
314
+ return lines;
315
+ }
316
+
317
+ const maxCost = Math.max(
318
+ ...result.bySession.map((s) => s.tokens.costTotal),
319
+ );
320
+
321
+ for (const session of result.bySession) {
322
+ const dateStr = session.created.toISOString().slice(0, 10);
323
+ const name = session.name ?? session.sessionId.slice(0, 8);
324
+ const cost = formatCost(session.tokens.costTotal);
325
+ const msgs = `${session.messageCount} msgs`;
326
+
327
+ const barWidth = Math.min(12, Math.max(6, width - 50));
328
+ const bar = renderProgressBar(
329
+ session.tokens.costTotal,
330
+ maxCost,
331
+ barWidth,
332
+ this.theme,
333
+ );
334
+
335
+ lines.push(
336
+ truncateToWidth(
337
+ ` ${this.theme.fg("dim", dateStr)} ${this.theme.fg("accent", name)} ${bar} ${this.theme.fg("accent", cost)} ${this.theme.fg("dim", msgs)}`,
338
+ width,
339
+ ),
340
+ );
341
+ }
342
+
343
+ if (result.sessionCount > result.bySession.length) {
344
+ lines.push("");
345
+ lines.push(
346
+ this.theme.fg(
347
+ "dim",
348
+ ` Showing ${result.bySession.length} of ${result.sessionCount} sessions`,
349
+ ),
350
+ );
351
+ }
352
+
353
+ return lines;
354
+ }
355
+
356
+ invalidate(): void {}
357
+ }
@@ -1,4 +1,7 @@
1
- import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
1
+ import type {
2
+ ExtensionAPI,
3
+ ExtensionContext,
4
+ } from "@mariozechner/pi-coding-agent";
2
5
  import {
3
6
  QUOTAS_CONFIG_UPDATED_EVENT,
4
7
  QUOTAS_EXTENSIONS_REGISTER_EVENT,
@@ -6,7 +9,11 @@ import {
6
9
  type QuotasConfigUpdatedPayload,
7
10
  configLoader,
8
11
  } from "../../config.js";
9
- import { fetchProviderQuotas, isSupportedProvider } from "../../lib/quotas.js";
12
+ import {
13
+ fetchProviderQuotas,
14
+ isSupportedProvider,
15
+ PROVIDER_LABELS,
16
+ } from "../../lib/quotas.js";
10
17
  import {
11
18
  assessWindow,
12
19
  formatTimeRemaining,
@@ -24,9 +31,11 @@ function shouldNotify(key: string, severity: RiskSeverity): boolean {
24
31
  const current = alertState.get(key);
25
32
  if (!current) return true;
26
33
  const order: RiskSeverity[] = ["none", "warning", "high", "critical"];
27
- if (order.indexOf(severity) > order.indexOf(current.lastSeverity)) return true;
34
+ if (order.indexOf(severity) > order.indexOf(current.lastSeverity))
35
+ return true;
28
36
  if (severity === "high" || severity === "critical") return true;
29
- if (severity === "warning") return Date.now() - current.lastNotifiedAt >= COOLDOWN_MS;
37
+ if (severity === "warning")
38
+ return Date.now() - current.lastNotifiedAt >= COOLDOWN_MS;
30
39
  return false;
31
40
  }
32
41
 
@@ -50,7 +59,10 @@ export default async function (pi: ExtensionAPI) {
50
59
  if (onlyNew && now - lastFetchAt < MIN_FETCH_INTERVAL_MS) return;
51
60
  lastFetchAt = now;
52
61
 
53
- const result = await fetchProviderQuotas(ctx.modelRegistry.authStorage, provider);
62
+ const result = await fetchProviderQuotas(
63
+ ctx.modelRegistry.authStorage,
64
+ provider,
65
+ );
54
66
  if (!result.success) return;
55
67
 
56
68
  const risky = result.data.windows
@@ -59,19 +71,23 @@ export default async function (pi: ExtensionAPI) {
59
71
  if (risky.length === 0) return;
60
72
 
61
73
  const toNotify = onlyNew
62
- ? risky.filter((entry) => shouldNotify(`${provider}:${entry.window.label}`, entry.assessment.severity))
74
+ ? risky.filter((entry) =>
75
+ shouldNotify(
76
+ `${provider}:${entry.window.label}`,
77
+ entry.assessment.severity,
78
+ ),
79
+ )
63
80
  : risky;
64
81
  if (toNotify.length === 0) return;
65
82
 
66
83
  for (const entry of toNotify) {
67
- markNotified(`${provider}:${entry.window.label}`, entry.assessment.severity);
84
+ markNotified(
85
+ `${provider}:${entry.window.label}`,
86
+ entry.assessment.severity,
87
+ );
68
88
  }
69
89
 
70
- const providerName = provider === "openai-codex"
71
- ? "Codex"
72
- : provider === "github-copilot"
73
- ? "GitHub Copilot"
74
- : "Anthropic";
90
+ const providerName = PROVIDER_LABELS[provider];
75
91
 
76
92
  const lines = toNotify.map(({ window, assessment }) => {
77
93
  const projected = Math.round(assessment.projectedPercent);
@@ -79,7 +95,11 @@ export default async function (pi: ExtensionAPI) {
79
95
  return `- ${window.label}: ${used}% used, projected ${projected}% (${assessment.severity}), resets in ${formatTimeRemaining(window.resetsAt)}`;
80
96
  });
81
97
 
82
- const level = toNotify.some((entry) => entry.assessment.severity === "critical" || entry.assessment.severity === "high")
98
+ const level = toNotify.some(
99
+ (entry) =>
100
+ entry.assessment.severity === "critical" ||
101
+ entry.assessment.severity === "high",
102
+ )
83
103
  ? "error"
84
104
  : "warning";
85
105
  ctx.ui.notify(`${providerName} quota warning:\n${lines.join("\n")}`, level);
@@ -128,7 +148,9 @@ export default async function (pi: ExtensionAPI) {
128
148
 
129
149
  pi.events.on(QUOTAS_EXTENSIONS_REQUEST_EVENT, () => {
130
150
  if (configLoader.getConfig().quotaWarnings) {
131
- pi.events.emit(QUOTAS_EXTENSIONS_REGISTER_EVENT, { feature: "quotaWarnings" });
151
+ pi.events.emit(QUOTAS_EXTENSIONS_REGISTER_EVENT, {
152
+ feature: "quotaWarnings",
153
+ });
132
154
  }
133
155
  });
134
156
  }
@@ -0,0 +1,241 @@
1
+ import type {
2
+ ExtensionAPI,
3
+ ExtensionContext,
4
+ ThemeColor,
5
+ } from "@mariozechner/pi-coding-agent";
6
+ import {
7
+ QUOTAS_CONFIG_UPDATED_EVENT,
8
+ QUOTAS_EXTENSIONS_REGISTER_EVENT,
9
+ QUOTAS_EXTENSIONS_REQUEST_EVENT,
10
+ type QuotasConfigUpdatedPayload,
11
+ configLoader,
12
+ } from "../../config.js";
13
+ import { aggregateAllSessions, formatCost } from "../../lib/session-tokens.js";
14
+
15
+ const EXTENSION_ID = "pi-quotas-token-status";
16
+ const REFRESH_INTERVAL_MS = 60_000;
17
+
18
+ /** Go tier limits (approximate, from docs) */
19
+ const GO_LIMITS = {
20
+ rolling5h: 12, // $12 per 5 hours
21
+ weekly: 30, // $30 per week
22
+ monthly: 60, // $60 per month
23
+ } as const;
24
+
25
+ interface RollingWindowCosts {
26
+ rolling5h: number;
27
+ weekly: number;
28
+ monthly: number;
29
+ }
30
+
31
+ function computeWindowTimestamps(now: Date): {
32
+ since5h: Date;
33
+ sinceWeekly: Date;
34
+ sinceMonthly: Date;
35
+ } {
36
+ return {
37
+ since5h: new Date(now.getTime() - 5 * 60 * 60 * 1000),
38
+ sinceWeekly: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000),
39
+ sinceMonthly: new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000),
40
+ };
41
+ }
42
+
43
+ async function computeRollingCosts(cwd?: string): Promise<RollingWindowCosts> {
44
+ const now = new Date();
45
+ const { since5h, sinceWeekly, sinceMonthly } = computeWindowTimestamps(now);
46
+
47
+ // Run all three aggregations in parallel
48
+ const [result5h, resultWeekly, resultMonthly] = await Promise.all([
49
+ aggregateAllSessions({ cwd, since: since5h }),
50
+ aggregateAllSessions({ cwd, since: sinceWeekly }),
51
+ aggregateAllSessions({ cwd, since: sinceMonthly }),
52
+ ]);
53
+
54
+ return {
55
+ rolling5h: result5h.totals.costTotal,
56
+ weekly: resultWeekly.totals.costTotal,
57
+ monthly: resultMonthly.totals.costTotal,
58
+ };
59
+ }
60
+
61
+ function formatWindowForStatus(
62
+ theme: ExtensionContext["ui"]["theme"],
63
+ label: string,
64
+ cost: number,
65
+ limit: number,
66
+ ): string {
67
+ const percent = limit > 0 ? Math.round((cost / limit) * 100) : 0;
68
+ const costStr = formatCost(cost);
69
+
70
+ // Color by usage level
71
+ let color: ThemeColor;
72
+ if (percent >= 90) {
73
+ color = "error";
74
+ } else if (percent >= 70) {
75
+ color = "warning";
76
+ } else {
77
+ color = "dim";
78
+ }
79
+
80
+ return `${theme.fg(color, `${label}:`)}${theme.fg("accent", costStr)}`;
81
+ }
82
+
83
+ function formatTokenStatus(
84
+ theme: ExtensionContext["ui"]["theme"],
85
+ costs: RollingWindowCosts,
86
+ ): string {
87
+ const parts = [
88
+ formatWindowForStatus(theme, "5h", costs.rolling5h, GO_LIMITS.rolling5h),
89
+ formatWindowForStatus(theme, "wk", costs.weekly, GO_LIMITS.weekly),
90
+ formatWindowForStatus(theme, "mo", costs.monthly, GO_LIMITS.monthly),
91
+ ];
92
+ return parts.join(" · ");
93
+ }
94
+
95
+ function isGoProvider(provider: string | undefined): boolean {
96
+ if (!provider) return false;
97
+ return provider === "opencode-go" || provider.startsWith("opencode-go/");
98
+ }
99
+
100
+ function createTokenStatusRefresher() {
101
+ let refreshTimer: ReturnType<typeof setInterval> | undefined;
102
+ let activeContext: ExtensionContext | undefined;
103
+ let lastCosts: RollingWindowCosts | undefined;
104
+ let inFlight = false;
105
+ let queued = false;
106
+
107
+ async function update(ctx: ExtensionContext): Promise<void> {
108
+ if (!ctx.hasUI) return;
109
+ if (inFlight) {
110
+ queued = true;
111
+ return;
112
+ }
113
+ inFlight = true;
114
+ try {
115
+ const costs = await computeRollingCosts(ctx.cwd);
116
+ if (!ctx.hasUI) return;
117
+ lastCosts = costs;
118
+ const status = formatTokenStatus(ctx.ui.theme, costs);
119
+ ctx.ui.setStatus(EXTENSION_ID, status);
120
+ } catch {
121
+ ctx.ui.setStatus(
122
+ EXTENSION_ID,
123
+ ctx.ui.theme.fg("warning", "token tracking unavailable"),
124
+ );
125
+ } finally {
126
+ inFlight = false;
127
+ if (queued) {
128
+ queued = false;
129
+ void update(ctx);
130
+ }
131
+ }
132
+ }
133
+
134
+ return {
135
+ async refreshFor(ctx: ExtensionContext): Promise<void> {
136
+ activeContext = ctx;
137
+ if (!isGoProvider(ctx.model?.provider)) {
138
+ ctx.ui.setStatus(EXTENSION_ID, undefined);
139
+ return;
140
+ }
141
+ await update(ctx);
142
+ },
143
+ start(): void {
144
+ if (refreshTimer) clearInterval(refreshTimer);
145
+ refreshTimer = setInterval(() => {
146
+ if (activeContext) void update(activeContext);
147
+ }, REFRESH_INTERVAL_MS);
148
+ refreshTimer.unref?.();
149
+ },
150
+ stop(ctx?: ExtensionContext): void {
151
+ if (refreshTimer) clearInterval(refreshTimer);
152
+ refreshTimer = undefined;
153
+ activeContext = undefined;
154
+ lastCosts = undefined;
155
+ ctx?.ui.setStatus(EXTENSION_ID, undefined);
156
+ },
157
+ renderLast(ctx: ExtensionContext): boolean {
158
+ if (!lastCosts || !ctx.hasUI) return false;
159
+ ctx.ui.setStatus(
160
+ EXTENSION_ID,
161
+ formatTokenStatus(ctx.ui.theme, lastCosts),
162
+ );
163
+ return true;
164
+ },
165
+ };
166
+ }
167
+
168
+ export default async function (pi: ExtensionAPI) {
169
+ await configLoader.load();
170
+ const refresher = createTokenStatusRefresher();
171
+ let enabled = configLoader.getConfig().tokenStatus;
172
+ let currentContext: ExtensionContext | undefined;
173
+
174
+ function scheduleRefresh(ctx: ExtensionContext): void {
175
+ void refresher.refreshFor(ctx).catch(() => {
176
+ if (ctx.hasUI)
177
+ ctx.ui.setStatus(
178
+ EXTENSION_ID,
179
+ ctx.ui.theme.fg("warning", "token tracking unavailable"),
180
+ );
181
+ });
182
+ }
183
+
184
+ pi.events.on(QUOTAS_CONFIG_UPDATED_EVENT, (data: unknown) => {
185
+ const config = (data as QuotasConfigUpdatedPayload).config;
186
+ enabled = config.tokenStatus;
187
+ if (!enabled) {
188
+ refresher.stop(currentContext);
189
+ return;
190
+ }
191
+ if (currentContext) {
192
+ refresher.start();
193
+ scheduleRefresh(currentContext);
194
+ }
195
+ });
196
+
197
+ pi.on("session_start", (_event, ctx) => {
198
+ currentContext = ctx;
199
+ if (!enabled) return;
200
+ if (!isGoProvider(ctx.model?.provider)) {
201
+ ctx.ui.setStatus(EXTENSION_ID, undefined);
202
+ return;
203
+ }
204
+ refresher.start();
205
+ scheduleRefresh(ctx);
206
+ });
207
+
208
+ pi.on("turn_end", (_event, ctx) => {
209
+ currentContext = ctx;
210
+ if (!enabled) return;
211
+ if (!isGoProvider(ctx.model?.provider)) return;
212
+ scheduleRefresh(ctx);
213
+ });
214
+
215
+ pi.on("model_select", (_event, ctx) => {
216
+ currentContext = ctx;
217
+ if (!enabled) {
218
+ refresher.stop(ctx);
219
+ return;
220
+ }
221
+ if (!isGoProvider(ctx.model?.provider)) {
222
+ refresher.stop(ctx);
223
+ return;
224
+ }
225
+ refresher.start();
226
+ scheduleRefresh(ctx);
227
+ });
228
+
229
+ pi.on("session_shutdown", async (_event, ctx) => {
230
+ currentContext = undefined;
231
+ refresher.stop(ctx);
232
+ });
233
+
234
+ pi.events.on(QUOTAS_EXTENSIONS_REQUEST_EVENT, () => {
235
+ if (configLoader.getConfig().tokenStatus) {
236
+ pi.events.emit(QUOTAS_EXTENSIONS_REGISTER_EVENT, {
237
+ feature: "tokenStatus",
238
+ });
239
+ }
240
+ });
241
+ }