@astrosheep/pi-quota 0.7.0 → 0.8.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrosheep/pi-quota",
3
- "version": "0.7.0",
3
+ "version": "0.8.1",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
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
- | { version: 2; capturedAt: number; states: Exclude<QuotaState, { kind: 'loading' }>[] };
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.label : '',
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.label, metric: '',
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.label, metric: '',
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.label, metric: '', value: errorText[state.code], secondary: '',
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,15 @@ 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
+ // No markdown fence: paseo routes notify to a plain-text Notification
240
+ // component (no markdown), so fence markers would show literally. The
241
+ // compact layout works as preserved-whitespace plain text on its own.
235
242
  if (ctx.mode !== 'tui') {
236
- ctx.ui.notify(usageLines(card, undefined).join('\n'), 'info');
243
+ ctx.ui.notify(compactUsageLines(card, undefined).join('\n'), 'info');
237
244
  }
238
245
  } finally {
239
246
  if (spinner) { clearInterval(spinner); spinner = undefined; }