@astrosheep/pi-quota 0.7.0 → 0.8.0
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/package.json +1 -1
- package/src/bar/card.ts +55 -10
- package/src/extension.ts +10 -4
package/package.json
CHANGED
package/src/bar/card.ts
CHANGED
|
@@ -10,7 +10,8 @@ export const USAGE_ENTRY = 'quota-bar:usage:v1';
|
|
|
10
10
|
|
|
11
11
|
export type UsageCard =
|
|
12
12
|
| { version: 1; capturedAt: number; state: Exclude<QuotaState, { kind: 'loading' }> }
|
|
13
|
-
|
|
13
|
+
// currentProvider is optional so cards saved before the marker existed still render.
|
|
14
|
+
| { version: 2; capturedAt: number; states: Exclude<QuotaState, { kind: 'loading' }>[]; currentProvider?: string };
|
|
14
15
|
|
|
15
16
|
export function captureUsage(state: QuotaState, now = Date.now()): UsageCard | undefined {
|
|
16
17
|
if (state.kind === 'loading') return undefined;
|
|
@@ -18,8 +19,8 @@ export function captureUsage(state: QuotaState, now = Date.now()): UsageCard | u
|
|
|
18
19
|
return { version: 1, capturedAt: now, state: structuredClone(state) };
|
|
19
20
|
}
|
|
20
21
|
|
|
21
|
-
export function captureAllUsage(states: readonly QuotaState[], now = Date.now()): UsageCard {
|
|
22
|
-
return { version: 2, capturedAt: now, states: structuredClone(states.filter(
|
|
22
|
+
export function captureAllUsage(states: readonly QuotaState[], now = Date.now(), currentProvider?: string): UsageCard {
|
|
23
|
+
return { version: 2, capturedAt: now, currentProvider, states: structuredClone(states.filter(
|
|
23
24
|
(state): state is Exclude<QuotaState, { kind: 'loading' }> => state.kind !== 'loading',
|
|
24
25
|
)) };
|
|
25
26
|
}
|
|
@@ -52,9 +53,13 @@ function padStartVisible(text: string, width: number): string {
|
|
|
52
53
|
return ' '.repeat(Math.max(0, width - visibleWidth(text))) + text;
|
|
53
54
|
}
|
|
54
55
|
|
|
55
|
-
function gridRows(states: readonly Exclude<QuotaState, { kind: 'loading' }>[], now: number, barWidth: number): UsageRow[] {
|
|
56
|
+
function gridRows(states: readonly Exclude<QuotaState, { kind: 'loading' }>[], now: number, barWidth: number, currentProvider?: string): UsageRow[] {
|
|
56
57
|
const rows: UsageRow[] = [];
|
|
57
58
|
states.forEach((state, group) => {
|
|
59
|
+
// The session's own provider is marked so it stays findable in long lists.
|
|
60
|
+
// (Only ready/error states carry provider and label; hidden rows never render.)
|
|
61
|
+
const mark = (entry: { provider: string; label: string }) =>
|
|
62
|
+
currentProvider !== undefined && entry.provider === currentProvider ? `★ ${entry.label}` : entry.label;
|
|
58
63
|
if (state.kind === 'ready') {
|
|
59
64
|
const windows = state.snapshot.windows;
|
|
60
65
|
if (windows.length > 0) {
|
|
@@ -63,7 +68,7 @@ function gridRows(states: readonly Exclude<QuotaState, { kind: 'loading' }>[], n
|
|
|
63
68
|
const remaining = expired ? null : window.remainingPercent;
|
|
64
69
|
rows.push({
|
|
65
70
|
group, kind: 'quota',
|
|
66
|
-
provider: index === 0 ? state
|
|
71
|
+
provider: index === 0 ? mark(state) : '',
|
|
67
72
|
metric: `${window.scope ? `${window.scope}/` : ''}${window.label}`,
|
|
68
73
|
value: `${horizontalBar(remaining, barWidth)} ${padStartVisible(formatPercent(remaining), 4)}`,
|
|
69
74
|
secondary: `↺ ${window.resetAt === null ? '?' : duration(window.resetAt - now)}`,
|
|
@@ -75,7 +80,7 @@ function gridRows(states: readonly Exclude<QuotaState, { kind: 'loading' }>[], n
|
|
|
75
80
|
if (state.snapshot.balance) {
|
|
76
81
|
const balance = state.snapshot.balance;
|
|
77
82
|
rows.push({
|
|
78
|
-
group, kind: 'balance', provider: state
|
|
83
|
+
group, kind: 'balance', provider: mark(state), metric: '',
|
|
79
84
|
value: remainingMoney(balance),
|
|
80
85
|
secondary: state.snapshot.spend?.today !== undefined
|
|
81
86
|
? `today ${money(state.snapshot.spend.today, state.snapshot.spend.currency)}` : '',
|
|
@@ -88,7 +93,7 @@ function gridRows(states: readonly Exclude<QuotaState, { kind: 'loading' }>[], n
|
|
|
88
93
|
const allowance = state.snapshot.allowance;
|
|
89
94
|
const remaining = Math.max(0, Math.min(100, allowance.remaining / allowance.limit * 100));
|
|
90
95
|
rows.push({
|
|
91
|
-
group, kind: 'balance', provider: state
|
|
96
|
+
group, kind: 'balance', provider: mark(state), metric: '',
|
|
92
97
|
value: `${horizontalBar(remaining, barWidth)} ${formatPercent(remaining)}`,
|
|
93
98
|
secondary: `${money(allowance.used, allowance.currency)} used`,
|
|
94
99
|
valueTone: remainingTone(remaining), metricTone: 'dim', secondaryTone: 'dim',
|
|
@@ -97,7 +102,7 @@ function gridRows(states: readonly Exclude<QuotaState, { kind: 'loading' }>[], n
|
|
|
97
102
|
}
|
|
98
103
|
}
|
|
99
104
|
if (state.kind === 'error') {
|
|
100
|
-
rows.push({ group, kind: 'error', provider: state
|
|
105
|
+
rows.push({ group, kind: 'error', provider: mark(state), metric: '', value: errorText[state.code], secondary: '',
|
|
101
106
|
valueTone: 'warning', metricTone: 'dim', secondaryTone: 'dim' });
|
|
102
107
|
}
|
|
103
108
|
});
|
|
@@ -109,8 +114,9 @@ export function aggregateUsageLines(
|
|
|
109
114
|
paint: Paint = (_tone, text) => text,
|
|
110
115
|
now = Date.now(),
|
|
111
116
|
barWidth = 12,
|
|
117
|
+
currentProvider?: string,
|
|
112
118
|
): string[] {
|
|
113
|
-
const rows = gridRows(states, now, barWidth);
|
|
119
|
+
const rows = gridRows(states, now, barWidth, currentProvider);
|
|
114
120
|
if (rows.length === 0) return ['No quota data available. Run /usage --all for details.'];
|
|
115
121
|
const quotaRows = rows.filter(row => row.kind === 'quota');
|
|
116
122
|
const otherRows = rows.filter(row => row.kind !== 'quota');
|
|
@@ -137,7 +143,46 @@ export function aggregateUsageLines(
|
|
|
137
143
|
|
|
138
144
|
export function usageLines(card: UsageCard, paint?: Paint, barWidth = 12): string[] {
|
|
139
145
|
if (card.version === 1) return renderUsage(card.state, paint, card.capturedAt, barWidth);
|
|
140
|
-
return aggregateUsageLines(card.states, paint, card.capturedAt, barWidth);
|
|
146
|
+
return aggregateUsageLines(card.states, paint, card.capturedAt, barWidth, card.currentProvider);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Narrow-client layout (RPC notify bubbles, phones): provider name on its own
|
|
150
|
+
// header line, windows indented below. No wide grid columns, so monospace code
|
|
151
|
+
// blocks never wrap. Paseo's fence blocks have no horizontal scroll: RN Text
|
|
152
|
+
// wraps long lines and a wrapped grid is worse than no grid.
|
|
153
|
+
export function aggregateCompactLines(
|
|
154
|
+
states: readonly Exclude<QuotaState, { kind: 'loading' }>[],
|
|
155
|
+
paint: Paint = (_tone, text) => text,
|
|
156
|
+
now = Date.now(),
|
|
157
|
+
barWidth = 6,
|
|
158
|
+
currentProvider?: string,
|
|
159
|
+
): string[] {
|
|
160
|
+
const rows = gridRows(states, now, barWidth, currentProvider);
|
|
161
|
+
if (rows.length === 0) return ['No quota data available. Run /usage --all for details.'];
|
|
162
|
+
const metricWidth = Math.max(0, ...rows.filter(row => row.kind === 'quota').map(row => visibleWidth(row.metric)));
|
|
163
|
+
const lines: string[] = [];
|
|
164
|
+
let previousGroup: number | undefined;
|
|
165
|
+
for (const row of rows) {
|
|
166
|
+
if (previousGroup !== undefined && row.group !== previousGroup) lines.push('');
|
|
167
|
+
previousGroup = row.group;
|
|
168
|
+
if (row.kind === 'quota') {
|
|
169
|
+
if (row.provider) lines.push(paint('text', row.provider));
|
|
170
|
+
const metric = padEndVisible(row.metric, metricWidth);
|
|
171
|
+
const line = ` ${paint(row.metricTone, metric)} ${paint(row.valueTone, row.value)}`
|
|
172
|
+
+ (row.secondary ? ` ${paint(row.secondaryTone, row.secondary)}` : '');
|
|
173
|
+
lines.push(line.replace(/\s+$/u, ''));
|
|
174
|
+
} else {
|
|
175
|
+
const line = paint('text', row.provider) + ` ${paint(row.valueTone, row.value)}`
|
|
176
|
+
+ (row.secondary ? ` ${paint(row.secondaryTone, row.secondary)}` : '');
|
|
177
|
+
lines.push(line.replace(/\s+$/u, ''));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return lines;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function compactUsageLines(card: UsageCard, paint?: Paint, barWidth = 6): string[] {
|
|
184
|
+
if (card.version === 1) return renderUsage(card.state, paint, card.capturedAt, barWidth);
|
|
185
|
+
return aggregateCompactLines(card.states, paint, card.capturedAt, barWidth, card.currentProvider);
|
|
141
186
|
}
|
|
142
187
|
|
|
143
188
|
export function usageCardComponent(card: UsageCard | undefined, paint?: Paint): Component {
|
package/src/extension.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-agent';
|
|
2
2
|
import { renderFooter } from './bar/quota.ts';
|
|
3
|
-
import { captureAllUsage, USAGE_ENTRY, usageCardComponent, usageLines } from './bar/card.ts';
|
|
3
|
+
import { captureAllUsage, USAGE_ENTRY, usageCardComponent, usageLines, compactUsageLines } from './bar/card.ts';
|
|
4
4
|
import type { UsageCard } from './bar/card.ts';
|
|
5
5
|
import { codexAdapter } from './query/codex.ts';
|
|
6
6
|
import { createDeepSeekAdapter } from './query/deepseek.ts';
|
|
@@ -214,13 +214,17 @@ export function createQuotaExtension(options: QuotaExtensionOptions = {}) {
|
|
|
214
214
|
const getAuth = resolveAuth(ctx);
|
|
215
215
|
const providers = supportedProviders(ctx);
|
|
216
216
|
const states = await Promise.all(providers.map(provider => queryProvider(provider, getAuth)));
|
|
217
|
+
// The session's own provider always leads, whatever its health looks like.
|
|
218
|
+
const currentProvider = ctx.model?.provider;
|
|
217
219
|
const ordered = states
|
|
218
220
|
.map((state, index) => ({ state, index }))
|
|
219
221
|
.sort((a, b) => {
|
|
222
|
+
const pinned = (item: { state: QuotaState }) =>
|
|
223
|
+
currentProvider !== undefined && 'provider' in item.state && item.state.provider === currentProvider ? 0 : 1;
|
|
220
224
|
const rank = (state: QuotaState) => state.kind === 'error' ? 3
|
|
221
225
|
: state.kind === 'ready' && state.snapshot.windows.length > 0 ? 1
|
|
222
226
|
: 2;
|
|
223
|
-
return rank(a.state) - rank(b.state) || a.index - b.index;
|
|
227
|
+
return pinned(a) - pinned(b) || rank(a.state) - rank(b.state) || a.index - b.index;
|
|
224
228
|
})
|
|
225
229
|
.map(item => item.state);
|
|
226
230
|
// Never append via a stale Pi runtime, or label a replacement model's data
|
|
@@ -228,12 +232,14 @@ export function createQuotaExtension(options: QuotaExtensionOptions = {}) {
|
|
|
228
232
|
if (!current || generation !== commandGeneration) return;
|
|
229
233
|
const visibleStates = args.trim() === '--all'
|
|
230
234
|
? ordered : ordered.filter(state => state.kind === 'ready');
|
|
231
|
-
const card = captureAllUsage(visibleStates);
|
|
235
|
+
const card = captureAllUsage(visibleStates, Date.now(), currentProvider);
|
|
232
236
|
hasUsageSnapshot = true;
|
|
233
237
|
pi.appendEntry<UsageCard>(USAGE_ENTRY, card);
|
|
234
238
|
// Custom entry renderers are TUI-only; RPC clients still get readable output.
|
|
239
|
+
// Fenced so clients that render markdown use a monospace font; compact
|
|
240
|
+
// layout because narrow clients (phones) wrap long grid lines anyway.
|
|
235
241
|
if (ctx.mode !== 'tui') {
|
|
236
|
-
ctx.ui.notify(
|
|
242
|
+
ctx.ui.notify(`\`\`\`\n${compactUsageLines(card, undefined).join('\n')}\n\`\`\``, 'info');
|
|
237
243
|
}
|
|
238
244
|
} finally {
|
|
239
245
|
if (spinner) { clearInterval(spinner); spinner = undefined; }
|