@bacnh85/pi-sub 0.1.2 → 0.1.4
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.
- package/README.md +3 -3
- package/index.ts +194 -42
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Pi extension that shows subscription usage for the currently selected supported model provider.
|
|
4
4
|
|
|
5
|
-
V1 supports OpenAI Codex models by reading Pi's auth state from `~/.pi/agent/auth.json` (or `$PI_CODING_AGENT_DIR/auth.json`) and displays a
|
|
5
|
+
V1 supports OpenAI Codex models by reading Pi's auth state from `~/.pi/agent/auth.json` (or `$PI_CODING_AGENT_DIR/auth.json`) and displays a subscription footer status after Pi's built-in status/token usage line only while the active Pi model provider is `openai-codex`.
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
@@ -26,7 +26,7 @@ pi -e ./extensions/pi-sub
|
|
|
26
26
|
|
|
27
27
|
## What it shows
|
|
28
28
|
|
|
29
|
-
The footer status includes:
|
|
29
|
+
The footer status appears after Pi's built-in status/token usage line and includes:
|
|
30
30
|
|
|
31
31
|
- active account email;
|
|
32
32
|
- subscription plan, such as `Plus`;
|
|
@@ -54,7 +54,7 @@ When Pi OpenAI Codex auth is available, `/sub` shows the active account usage:
|
|
|
54
54
|
|
|
55
55
|
```text
|
|
56
56
|
ACCOUNT PLAN 5H USAGE WEEKLY USAGE LAST ACTIVITY
|
|
57
|
-
*
|
|
57
|
+
* user@example.com Plus 85% (18:12) 80% (08:00 on 2 Jul) Now
|
|
58
58
|
```
|
|
59
59
|
|
|
60
60
|
## Refresh behavior
|
package/index.ts
CHANGED
|
@@ -10,7 +10,8 @@ const USAGE_ENDPOINT = "https://chatgpt.com/backend-api/wham/usage";
|
|
|
10
10
|
const REFRESH_INTERVAL_MS = 60_000;
|
|
11
11
|
const REFRESH_TTL_MS = 30_000;
|
|
12
12
|
const REFRESH_DEBOUNCE_MS = 2_000;
|
|
13
|
-
const
|
|
13
|
+
const CODEX_PROVIDER = "openai-codex";
|
|
14
|
+
const OPC_PROVIDER = "opencode-go";
|
|
14
15
|
|
|
15
16
|
type ModelLike = { provider?: string; id?: string } | undefined;
|
|
16
17
|
|
|
@@ -34,12 +35,15 @@ type PiAuthEntry = {
|
|
|
34
35
|
refresh?: string;
|
|
35
36
|
expires?: number;
|
|
36
37
|
accountId?: string;
|
|
38
|
+
key?: string;
|
|
37
39
|
};
|
|
38
40
|
|
|
39
41
|
interface UsageWindow {
|
|
40
42
|
used?: number;
|
|
41
43
|
limit?: number;
|
|
42
44
|
percent?: number;
|
|
45
|
+
remaining?: number;
|
|
46
|
+
remainingLabel?: string;
|
|
43
47
|
resetLabel?: string;
|
|
44
48
|
resetsAt?: string | number | Date;
|
|
45
49
|
label?: string;
|
|
@@ -52,6 +56,7 @@ interface SubscriptionAccountSnapshot {
|
|
|
52
56
|
plan?: string;
|
|
53
57
|
fiveHour?: UsageWindow;
|
|
54
58
|
weekly?: UsageWindow;
|
|
59
|
+
monthly?: UsageWindow;
|
|
55
60
|
lastActivity?: string;
|
|
56
61
|
}
|
|
57
62
|
|
|
@@ -62,6 +67,7 @@ interface SubscriptionUsageSnapshot {
|
|
|
62
67
|
activeAccount?: SubscriptionAccountSnapshot;
|
|
63
68
|
fetchedAt: number;
|
|
64
69
|
error?: string;
|
|
70
|
+
cost?: number;
|
|
65
71
|
}
|
|
66
72
|
|
|
67
73
|
interface SubscriptionProviderAdapter {
|
|
@@ -83,7 +89,11 @@ interface State {
|
|
|
83
89
|
|
|
84
90
|
function isCodexModel(model: ModelLike): boolean {
|
|
85
91
|
const provider = model?.provider?.toLowerCase() ?? "";
|
|
86
|
-
return provider ===
|
|
92
|
+
return provider === CODEX_PROVIDER || provider.includes(CODEX_PROVIDER);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function isOpenCodeGoModel(model: ModelLike): boolean {
|
|
96
|
+
return (model?.provider?.toLowerCase() ?? "") === OPC_PROVIDER;
|
|
87
97
|
}
|
|
88
98
|
|
|
89
99
|
function piAuthPath(): string {
|
|
@@ -140,6 +150,19 @@ function planLabel(plan: string | undefined): string | undefined {
|
|
|
140
150
|
return labels[normalized] ?? plan;
|
|
141
151
|
}
|
|
142
152
|
|
|
153
|
+
function formatRemainingTime(resetAtSec: number | undefined): string | undefined {
|
|
154
|
+
if (!resetAtSec) return undefined;
|
|
155
|
+
const nowSec = Date.now() / 1000;
|
|
156
|
+
const remainingSec = resetAtSec - nowSec;
|
|
157
|
+
if (remainingSec <= 0) return "0M";
|
|
158
|
+
const remainingMin = Math.ceil(remainingSec / 60);
|
|
159
|
+
if (remainingMin < 60) return `${remainingMin}M`;
|
|
160
|
+
const remainingH = Math.ceil(remainingMin / 60);
|
|
161
|
+
if (remainingH < 24) return `${remainingH}H`;
|
|
162
|
+
const remainingD = Math.ceil(remainingH / 24);
|
|
163
|
+
return `${remainingD}D`;
|
|
164
|
+
}
|
|
165
|
+
|
|
143
166
|
function formatReset(timestampSeconds: number | undefined): string | undefined {
|
|
144
167
|
if (!timestampSeconds) return undefined;
|
|
145
168
|
const date = new Date(timestampSeconds * 1000);
|
|
@@ -154,12 +177,16 @@ function formatReset(timestampSeconds: number | undefined): string | undefined {
|
|
|
154
177
|
function usageWindowFromApi(window: UsageApiWindow | undefined): UsageWindow | undefined {
|
|
155
178
|
if (!window || typeof window.used_percent !== "number") return undefined;
|
|
156
179
|
const percent = Math.round(window.used_percent);
|
|
180
|
+
const remaining = Math.max(0, 100 - percent);
|
|
157
181
|
const resetLabel = formatReset(window.reset_at);
|
|
182
|
+
const remainingLabel = formatRemainingTime(window.reset_at);
|
|
158
183
|
return {
|
|
159
184
|
percent,
|
|
185
|
+
remaining,
|
|
186
|
+
remainingLabel,
|
|
160
187
|
resetLabel,
|
|
161
188
|
resetsAt: window.reset_at,
|
|
162
|
-
label:
|
|
189
|
+
label: remainingLabel ? `${remaining}% (${remainingLabel})` : `${remaining}%`,
|
|
163
190
|
};
|
|
164
191
|
}
|
|
165
192
|
|
|
@@ -193,13 +220,39 @@ function parseUsageResponse(body: unknown): UsageApiSnapshot | undefined {
|
|
|
193
220
|
};
|
|
194
221
|
}
|
|
195
222
|
|
|
223
|
+
function parseOpcodeUsageResponse(body: unknown): UsageApiSnapshot | undefined {
|
|
224
|
+
if (!body || typeof body !== "object") return undefined;
|
|
225
|
+
const root = body as any;
|
|
226
|
+
const usage = root.usage ?? root;
|
|
227
|
+
const parseWindow = (window: any): UsageApiWindow | undefined => {
|
|
228
|
+
if (!window || typeof window !== "object" || typeof window.used_percent !== "number") return undefined;
|
|
229
|
+
return {
|
|
230
|
+
used_percent: window.used_percent,
|
|
231
|
+
limit_window_seconds: typeof window.limit_window_seconds === "number" ? window.limit_window_seconds : undefined,
|
|
232
|
+
reset_at: typeof window.reset_at === "number" ? window.reset_at : typeof window.reset_ts === "number" ? window.reset_ts : undefined,
|
|
233
|
+
};
|
|
234
|
+
};
|
|
235
|
+
return {
|
|
236
|
+
primary: parseWindow(usage.rolling ?? usage.primary_window),
|
|
237
|
+
secondary: parseWindow(usage.weekly ?? usage.secondary_window),
|
|
238
|
+
plan_type: typeof root.plan_type === "string" ? root.plan_type : typeof usage.plan_type === "string" ? usage.plan_type : undefined,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
196
242
|
async function readPiCodexAuth(): Promise<PiAuthEntry> {
|
|
197
243
|
const auth = await readJsonFile<PiAuthFile>(piAuthPath());
|
|
198
|
-
const entry = auth[
|
|
244
|
+
const entry = auth[CODEX_PROVIDER];
|
|
199
245
|
if (!entry?.access || !entry.accountId) throw new Error("Missing openai-codex OAuth entry in Pi auth");
|
|
200
246
|
return entry;
|
|
201
247
|
}
|
|
202
248
|
|
|
249
|
+
async function readOpenCodeGoAuth(): Promise<{ key: string }> {
|
|
250
|
+
const auth = await readJsonFile<PiAuthFile>(piAuthPath());
|
|
251
|
+
const entry = auth[OPC_PROVIDER];
|
|
252
|
+
if (!entry?.key || typeof entry.key !== "string") throw new Error("Missing opencode-go API key in Pi auth");
|
|
253
|
+
return { key: entry.key };
|
|
254
|
+
}
|
|
255
|
+
|
|
203
256
|
async function fetchUsageFromPiAuth(entry: PiAuthEntry, signal?: AbortSignal): Promise<UsageApiSnapshot | undefined> {
|
|
204
257
|
const timeoutSignal = AbortSignal.timeout(7_000);
|
|
205
258
|
const combinedSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
|
|
@@ -216,13 +269,14 @@ async function fetchUsageFromPiAuth(entry: PiAuthEntry, signal?: AbortSignal): P
|
|
|
216
269
|
return parseUsageResponse(await response.json());
|
|
217
270
|
}
|
|
218
271
|
|
|
219
|
-
function redactedError(error: unknown): string {
|
|
272
|
+
function redactedError(error: unknown, provider = "Codex"): string {
|
|
220
273
|
const message = error instanceof Error ? error.message : String(error || "Unknown error");
|
|
221
274
|
if (/ENOENT|no such file/i.test(message)) return "Pi auth not found";
|
|
222
275
|
if (/missing openai-codex/i.test(message)) return "openai-codex auth not found";
|
|
223
|
-
if (/
|
|
224
|
-
if (/
|
|
225
|
-
return
|
|
276
|
+
if (/missing opencode-go/i.test(message)) return "opencode-go auth not found";
|
|
277
|
+
if (/timed out|timeout|aborted/i.test(message)) return `${provider} usage refresh timed out`;
|
|
278
|
+
if (/401|403|auth|token|unauthorized|forbidden/i.test(message)) return `${provider} auth unavailable`;
|
|
279
|
+
return `${provider} usage unavailable`;
|
|
226
280
|
}
|
|
227
281
|
|
|
228
282
|
async function fetchCodexUsage(signal?: AbortSignal): Promise<SubscriptionUsageSnapshot> {
|
|
@@ -232,7 +286,7 @@ async function fetchCodexUsage(signal?: AbortSignal): Promise<SubscriptionUsageS
|
|
|
232
286
|
const usage = await fetchUsageFromPiAuth(entry, signal);
|
|
233
287
|
activeAccount = mergeUsageIntoAccount(activeAccount, usage);
|
|
234
288
|
return {
|
|
235
|
-
providerId:
|
|
289
|
+
providerId: CODEX_PROVIDER,
|
|
236
290
|
providerDisplayName: "Codex",
|
|
237
291
|
accounts: [activeAccount],
|
|
238
292
|
activeAccount,
|
|
@@ -240,7 +294,7 @@ async function fetchCodexUsage(signal?: AbortSignal): Promise<SubscriptionUsageS
|
|
|
240
294
|
};
|
|
241
295
|
} catch (error) {
|
|
242
296
|
return {
|
|
243
|
-
providerId:
|
|
297
|
+
providerId: CODEX_PROVIDER,
|
|
244
298
|
providerDisplayName: "Codex",
|
|
245
299
|
accounts: [],
|
|
246
300
|
fetchedAt: Date.now(),
|
|
@@ -249,19 +303,75 @@ async function fetchCodexUsage(signal?: AbortSignal): Promise<SubscriptionUsageS
|
|
|
249
303
|
}
|
|
250
304
|
}
|
|
251
305
|
|
|
306
|
+
async function fetchOpenCodeGoUsage(signal?: AbortSignal): Promise<SubscriptionUsageSnapshot> {
|
|
307
|
+
try {
|
|
308
|
+
const auth = await readOpenCodeGoAuth();
|
|
309
|
+
const timeoutSignal = AbortSignal.timeout(7_000);
|
|
310
|
+
const combinedSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
|
|
311
|
+
const response = await fetch("https://opencode.ai/zen/go/v1/usage", {
|
|
312
|
+
headers: {
|
|
313
|
+
Accept: "application/json",
|
|
314
|
+
Authorization: `Bearer ${auth.key}`,
|
|
315
|
+
"User-Agent": "pi-sub/0.1.4",
|
|
316
|
+
},
|
|
317
|
+
signal: combinedSignal,
|
|
318
|
+
});
|
|
319
|
+
if (!response.ok) throw new Error(`usage request failed with HTTP ${response.status}`);
|
|
320
|
+
const usage = parseOpcodeUsageResponse(await response.json());
|
|
321
|
+
const account: SubscriptionAccountSnapshot = {
|
|
322
|
+
isActive: true,
|
|
323
|
+
accountLabel: "OpenCode Go",
|
|
324
|
+
plan: planLabel(usage?.plan_type),
|
|
325
|
+
fiveHour: usageWindowFromApi(usage?.primary),
|
|
326
|
+
weekly: usageWindowFromApi(usage?.secondary),
|
|
327
|
+
lastActivity: "Now",
|
|
328
|
+
};
|
|
329
|
+
return {
|
|
330
|
+
providerId: OPC_PROVIDER,
|
|
331
|
+
providerDisplayName: "OpenCode Go",
|
|
332
|
+
accounts: [account],
|
|
333
|
+
activeAccount: account,
|
|
334
|
+
fetchedAt: Date.now(),
|
|
335
|
+
};
|
|
336
|
+
} catch (error) {
|
|
337
|
+
return {
|
|
338
|
+
providerId: OPC_PROVIDER,
|
|
339
|
+
providerDisplayName: "OpenCode Go",
|
|
340
|
+
accounts: [],
|
|
341
|
+
fetchedAt: Date.now(),
|
|
342
|
+
error: redactedError(error, "OpenCode Go"),
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
252
347
|
const codexAdapter: SubscriptionProviderAdapter = {
|
|
253
|
-
id:
|
|
348
|
+
id: CODEX_PROVIDER,
|
|
254
349
|
displayName: "Codex",
|
|
255
350
|
isModelSupported: isCodexModel,
|
|
256
351
|
fetchUsage: fetchCodexUsage,
|
|
257
352
|
};
|
|
258
353
|
|
|
259
|
-
const
|
|
354
|
+
const openCodeGoAdapter: SubscriptionProviderAdapter = {
|
|
355
|
+
id: OPC_PROVIDER,
|
|
356
|
+
displayName: "OpenCode Go",
|
|
357
|
+
isModelSupported: isOpenCodeGoModel,
|
|
358
|
+
fetchUsage: fetchOpenCodeGoUsage,
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
const adapters = [codexAdapter, openCodeGoAdapter];
|
|
260
362
|
|
|
261
363
|
function supportedAdapter(model: ModelLike): SubscriptionProviderAdapter | undefined {
|
|
262
364
|
return adapters.find((adapter) => adapter.isModelSupported(model));
|
|
263
365
|
}
|
|
264
366
|
|
|
367
|
+
function formatRemaining(window: UsageWindow | undefined): string {
|
|
368
|
+
if (!window) return "?";
|
|
369
|
+
if (window.remaining !== undefined && window.remainingLabel) return `${window.remaining}%/${window.remainingLabel}`;
|
|
370
|
+
if (window.remaining !== undefined) return `${window.remaining}%`;
|
|
371
|
+
if (window.percent !== undefined && window.remainingLabel) return `${Math.max(0, 100 - window.percent)}%/${window.remainingLabel}`;
|
|
372
|
+
return "?";
|
|
373
|
+
}
|
|
374
|
+
|
|
265
375
|
function formatWindow(window: UsageWindow | undefined, _compact = true): string {
|
|
266
376
|
if (!window) return "?";
|
|
267
377
|
if (window.percent !== undefined && window.resetLabel) return `${window.percent}% (${window.resetLabel})`;
|
|
@@ -271,37 +381,62 @@ function formatWindow(window: UsageWindow | undefined, _compact = true): string
|
|
|
271
381
|
return "?";
|
|
272
382
|
}
|
|
273
383
|
|
|
274
|
-
function
|
|
275
|
-
|
|
384
|
+
function minRemaining(account: SubscriptionAccountSnapshot | undefined): number {
|
|
385
|
+
const values: number[] = [];
|
|
386
|
+
if (account?.fiveHour?.remaining !== undefined) values.push(account.fiveHour.remaining);
|
|
387
|
+
if (account?.weekly?.remaining !== undefined) values.push(account.weekly.remaining);
|
|
388
|
+
if (account?.monthly?.remaining !== undefined) values.push(account.monthly.remaining);
|
|
389
|
+
if (values.length === 0) return 100;
|
|
390
|
+
return Math.min(...values);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function windowSegments(account: SubscriptionAccountSnapshot | undefined): string[] {
|
|
394
|
+
if (!account) return [];
|
|
395
|
+
const segments: string[] = [];
|
|
396
|
+
if (account.fiveHour) segments.push(`R:${formatRemaining(account.fiveHour)}`);
|
|
397
|
+
if (account.weekly) segments.push(`W:${formatRemaining(account.weekly)}`);
|
|
398
|
+
if (account.monthly) segments.push(`M:${formatRemaining(account.monthly)}`);
|
|
399
|
+
return segments;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function aggregateSessionCost(ctx: ExtensionContext): number {
|
|
403
|
+
let total = 0;
|
|
404
|
+
for (const entry of ctx.sessionManager.getBranch()) {
|
|
405
|
+
if (entry.type === "message" && entry.message.role === "assistant") {
|
|
406
|
+
total += (entry.message.usage as any)?.cost?.total ?? 0;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
return total;
|
|
276
410
|
}
|
|
277
411
|
|
|
278
412
|
function renderSubscriptionLine(ctx: ExtensionContext, state: State): void {
|
|
279
413
|
if (!state.adapter) {
|
|
280
|
-
ctx.ui.
|
|
414
|
+
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
281
415
|
return;
|
|
282
416
|
}
|
|
283
417
|
const theme = ctx.ui.theme;
|
|
284
418
|
const snapshot = state.snapshot;
|
|
285
|
-
const modelId = state.model?.id ?? "unknown-model";
|
|
286
419
|
let line: string;
|
|
287
420
|
let color: "dim" | "warning" | "error" = "dim";
|
|
288
421
|
if (!snapshot) {
|
|
289
|
-
line = `Sub ${state.adapter.displayName} loading
|
|
422
|
+
line = `Sub ${state.adapter.displayName} loading`;
|
|
290
423
|
} else if (snapshot.error) {
|
|
291
|
-
line = `Sub ${snapshot.error}
|
|
424
|
+
line = `Sub ${snapshot.error}`;
|
|
292
425
|
color = "warning";
|
|
293
426
|
} else {
|
|
294
427
|
const account = snapshot.activeAccount;
|
|
295
|
-
const
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
428
|
+
const segments = windowSegments(account);
|
|
429
|
+
const cost = snapshot.cost;
|
|
430
|
+
if (cost !== undefined && cost > 0) segments.push(`$${cost.toFixed(2)}`);
|
|
431
|
+
if (segments.length === 0) {
|
|
432
|
+
line = `Sub ${state.adapter.displayName}`;
|
|
433
|
+
} else {
|
|
434
|
+
line = segments.join(" ");
|
|
435
|
+
}
|
|
436
|
+
const remaining = minRemaining(account);
|
|
437
|
+
color = remaining <= 10 ? "error" : remaining <= 20 ? "warning" : "dim";
|
|
303
438
|
}
|
|
304
|
-
ctx.ui.
|
|
439
|
+
ctx.ui.setStatus(STATUS_KEY, theme.fg(color, line));
|
|
305
440
|
}
|
|
306
441
|
|
|
307
442
|
function startTimer(ctx: ExtensionContext, state: State): void {
|
|
@@ -340,6 +475,7 @@ async function refreshUsage(ctx: ExtensionContext, state: State, force: boolean)
|
|
|
340
475
|
if (state.inFlight) return state.inFlight;
|
|
341
476
|
renderSubscriptionLine(ctx, state);
|
|
342
477
|
state.inFlight = adapter.fetchUsage(ctx.signal).then((snapshot) => {
|
|
478
|
+
snapshot.cost = aggregateSessionCost(ctx);
|
|
343
479
|
state.snapshot = snapshot;
|
|
344
480
|
state.lastRefreshAt = Date.now();
|
|
345
481
|
renderSubscriptionLine(ctx, state);
|
|
@@ -368,24 +504,40 @@ function buildDetails(snapshot: SubscriptionUsageSnapshot | undefined, state: St
|
|
|
368
504
|
if (!snapshot) return "Subscription usage has not been loaded yet.";
|
|
369
505
|
if (snapshot.error) return `${snapshot.providerDisplayName}: ${snapshot.error}`;
|
|
370
506
|
if (snapshot.accounts.length === 0) return `${snapshot.providerDisplayName}: no accounts found.`;
|
|
507
|
+
|
|
508
|
+
const columns: { key: string; label: string; get: (a: SubscriptionAccountSnapshot) => string }[] = [
|
|
509
|
+
{ key: "account", label: "ACCOUNT", get: (a) => a.accountLabel ?? "unknown" },
|
|
510
|
+
{ key: "plan", label: "PLAN", get: (a) => a.plan ?? "?" },
|
|
511
|
+
];
|
|
512
|
+
|
|
513
|
+
const hasFiveHour = snapshot.accounts.some((a) => a.fiveHour);
|
|
514
|
+
const hasWeekly = snapshot.accounts.some((a) => a.weekly);
|
|
515
|
+
const hasMonthly = snapshot.accounts.some((a) => a.monthly);
|
|
516
|
+
|
|
517
|
+
if (hasFiveHour) columns.push({ key: "five", label: "ROLLING", get: (a) => formatRemaining(a.fiveHour) });
|
|
518
|
+
if (hasWeekly) columns.push({ key: "weekly", label: "WEEKLY", get: (a) => formatRemaining(a.weekly) });
|
|
519
|
+
if (hasMonthly) columns.push({ key: "monthly", label: "MONTHLY", get: (a) => formatRemaining(a.monthly) });
|
|
520
|
+
|
|
371
521
|
const rows = snapshot.accounts.map((account) => ({
|
|
372
522
|
active: account.isActive ? "*" : " ",
|
|
373
|
-
|
|
374
|
-
plan: account.plan ?? "?",
|
|
375
|
-
five: formatWindow(account.fiveHour, false),
|
|
376
|
-
weekly: formatWindow(account.weekly, false),
|
|
377
|
-
activity: account.lastActivity ?? "",
|
|
523
|
+
snapshot: account,
|
|
378
524
|
}));
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
const
|
|
525
|
+
|
|
526
|
+
const widths: Record<string, number> = {};
|
|
527
|
+
for (const col of columns) {
|
|
528
|
+
widths[col.key] = Math.max(col.label.length, ...snapshot.accounts.map((a) => col.get(a).length));
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
const headerCols = columns.map((c) => pad(c.label, widths[c.key]));
|
|
532
|
+
const header = ` ${headerCols.join(" ")} LAST ACTIVITY`;
|
|
386
533
|
const sep = "-".repeat(header.length);
|
|
387
|
-
const body = rows.map((row) =>
|
|
388
|
-
|
|
534
|
+
const body = rows.map((row) => {
|
|
535
|
+
const cols = columns.map((c) => pad(c.get(row.snapshot), widths[c.key]));
|
|
536
|
+
return `${row.active} ${cols.join(" ")} ${row.snapshot.lastActivity ?? ""}`;
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
const costLine = snapshot.cost !== undefined ? `\nSession cost: $${snapshot.cost.toFixed(2)}` : "";
|
|
540
|
+
return [`Provider: ${snapshot.providerDisplayName} · Model: ${state.model?.id ?? "unknown-model"} · Fetched: ${new Date(snapshot.fetchedAt).toLocaleTimeString()}${costLine}`, "", header, sep, ...body].join("\n");
|
|
389
541
|
}
|
|
390
542
|
|
|
391
543
|
export default function (pi: ExtensionAPI) {
|
|
@@ -413,7 +565,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
413
565
|
|
|
414
566
|
pi.on("session_shutdown", async (_event, ctx) => {
|
|
415
567
|
stopTimer(state);
|
|
416
|
-
ctx.ui.
|
|
568
|
+
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
417
569
|
});
|
|
418
570
|
|
|
419
571
|
pi.registerCommand("sub", {
|