@astrosheep/pi-quota 0.5.6 → 0.5.7

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.5.6",
3
+ "version": "0.5.7",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
package/src/bar/card.ts CHANGED
@@ -1,8 +1,10 @@
1
- import { Text } from '@earendil-works/pi-tui';
1
+ import { Text, visibleWidth } from '@earendil-works/pi-tui';
2
2
  import type { Component } from '@earendil-works/pi-tui';
3
- import type { Paint } from './bar.ts';
3
+ import { duration, formatPercent, horizontalBar, remainingTone } from './bar.ts';
4
+ import type { Paint, Tone } from './bar.ts';
5
+ import { money, remainingMoney } from './balance.ts';
4
6
  import { renderUsage } from './quota.ts';
5
- import type { QuotaState } from '../query/types.ts';
7
+ import type { QuotaErrorCode, QuotaState } from '../query/types.ts';
6
8
 
7
9
  export const USAGE_ENTRY = 'quota-bar:usage:v1';
8
10
 
@@ -22,12 +24,120 @@ export function captureAllUsage(states: readonly QuotaState[], now = Date.now())
22
24
  )) };
23
25
  }
24
26
 
25
- export function usageLines(card: UsageCard, paint?: Paint, barWidth = 20): string[] {
27
+ const errorText: Record<QuotaErrorCode, string> = {
28
+ auth: 'Sign in with /login',
29
+ 'unsupported-auth': 'Unsupported credentials or endpoint',
30
+ 'account-access': 'Account quota access denied',
31
+ network: 'Network error', timeout: 'Request timed out',
32
+ 'rate-limit': 'Rate limited', http: 'Service unavailable', schema: 'Unrecognized quota response',
33
+ };
34
+
35
+ interface UsageRow {
36
+ group: number;
37
+ kind: 'quota' | 'balance' | 'error';
38
+ provider: string;
39
+ metric: string;
40
+ value: string;
41
+ secondary: string;
42
+ valueTone: Tone;
43
+ metricTone: Tone;
44
+ secondaryTone: Tone;
45
+ }
46
+
47
+ function padEndVisible(text: string, width: number): string {
48
+ return text + ' '.repeat(Math.max(0, width - visibleWidth(text)));
49
+ }
50
+
51
+ function padStartVisible(text: string, width: number): string {
52
+ return ' '.repeat(Math.max(0, width - visibleWidth(text))) + text;
53
+ }
54
+
55
+ function gridRows(states: readonly Exclude<QuotaState, { kind: 'loading' }>[], now: number, barWidth: number): UsageRow[] {
56
+ const rows: UsageRow[] = [];
57
+ states.forEach((state, group) => {
58
+ if (state.kind === 'ready') {
59
+ const windows = state.snapshot.windows;
60
+ if (windows.length > 0) {
61
+ windows.forEach((window, index) => {
62
+ const expired = window.resetAt !== null && window.resetAt <= now;
63
+ const remaining = expired ? null : window.remainingPercent;
64
+ rows.push({
65
+ group, kind: 'quota',
66
+ provider: index === 0 ? state.label : '',
67
+ metric: `${window.scope ? `${window.scope}/` : ''}${window.label}`,
68
+ value: `${horizontalBar(remaining, barWidth)} ${formatPercent(remaining)}`,
69
+ secondary: `↺ ${window.resetAt === null ? '?' : duration(window.resetAt - now)}`,
70
+ valueTone: remainingTone(remaining), metricTone: 'dim', secondaryTone: 'dim',
71
+ });
72
+ });
73
+ return;
74
+ }
75
+ if (state.snapshot.balance) {
76
+ const balance = state.snapshot.balance;
77
+ rows.push({
78
+ group, kind: 'balance', provider: state.label, metric: '',
79
+ value: remainingMoney(balance),
80
+ secondary: state.snapshot.spend?.today !== undefined
81
+ ? `today ${money(state.snapshot.spend.today, state.snapshot.spend.currency)}` : '',
82
+ valueTone: !balance.unlimited && balance.remaining <= 0 ? 'error' : 'text',
83
+ metricTone: 'dim', secondaryTone: 'dim',
84
+ });
85
+ return;
86
+ }
87
+ if (state.snapshot.allowance) {
88
+ const allowance = state.snapshot.allowance;
89
+ const remaining = Math.max(0, Math.min(100, allowance.remaining / allowance.limit * 100));
90
+ rows.push({
91
+ group, kind: 'balance', provider: state.label, metric: '',
92
+ value: `${horizontalBar(remaining, barWidth)} ${formatPercent(remaining)}`,
93
+ secondary: `${money(allowance.used, allowance.currency)} used`,
94
+ valueTone: remainingTone(remaining), metricTone: 'dim', secondaryTone: 'dim',
95
+ });
96
+ return;
97
+ }
98
+ }
99
+ if (state.kind === 'error') {
100
+ rows.push({ group, kind: 'error', provider: state.label, metric: '', value: errorText[state.code], secondary: '',
101
+ valueTone: 'warning', metricTone: 'dim', secondaryTone: 'dim' });
102
+ }
103
+ });
104
+ return rows;
105
+ }
106
+
107
+ export function aggregateUsageLines(
108
+ states: readonly Exclude<QuotaState, { kind: 'loading' }>[],
109
+ paint: Paint = (_tone, text) => text,
110
+ now = Date.now(),
111
+ barWidth = 12,
112
+ ): string[] {
113
+ const rows = gridRows(states, now, barWidth);
114
+ if (rows.length === 0) return ['No quota data available. Run /usage --all for details.'];
115
+ const quotaRows = rows.filter(row => row.kind === 'quota');
116
+ const otherRows = rows.filter(row => row.kind !== 'quota');
117
+ const providerWidth = Math.max(...quotaRows.map(row => visibleWidth(row.provider)), ...otherRows.map(row => visibleWidth(row.provider)));
118
+ const metricWidth = Math.max(0, ...quotaRows.map(row => visibleWidth(row.metric)));
119
+ const quotaValueWidth = Math.max(0, ...quotaRows.map(row => visibleWidth(row.value)));
120
+ const otherValueWidth = Math.max(0, ...otherRows.map(row => visibleWidth(row.value)));
121
+ const result: string[] = [];
122
+ let previousGroup: number | undefined;
123
+ for (const row of rows) {
124
+ if (previousGroup !== undefined && row.group !== previousGroup && (row.kind === 'quota' || rows.some(other => other.group === previousGroup && other.kind === 'quota'))) result.push('');
125
+ previousGroup = row.group;
126
+ const provider = padEndVisible(row.provider, providerWidth);
127
+ const metric = row.kind === 'quota' ? padEndVisible(row.metric, metricWidth) : '';
128
+ const value = padStartVisible(row.value, row.kind === 'quota' ? quotaValueWidth : otherValueWidth);
129
+ const line = paint('text', provider)
130
+ + (metric ? paint(row.metricTone, ` ${metric}`) : '')
131
+ + paint(row.valueTone, ` ${value}`)
132
+ + (row.secondary ? paint(row.secondaryTone, ` ${row.secondary}`) : '');
133
+ result.push(line.replace(/\s+$/u, ''));
134
+ }
135
+ return result;
136
+ }
137
+
138
+ export function usageLines(card: UsageCard, paint?: Paint, barWidth = 12): string[] {
26
139
  if (card.version === 1) return renderUsage(card.state, paint, card.capturedAt, barWidth);
27
- if (card.states.length === 0) return ['No quota data available. Run /usage --all for details.'];
28
- return card.states.flatMap((state, index) => [
29
- ...(index ? ['', ''] : []), ...renderUsage(state, paint, card.capturedAt, barWidth),
30
- ]);
140
+ return aggregateUsageLines(card.states, paint, card.capturedAt, barWidth);
31
141
  }
32
142
 
33
143
  export function usageCardComponent(card: UsageCard | undefined, paint?: Paint): Component {
@@ -38,7 +148,7 @@ export function usageCardComponent(card: UsageCard | undefined, paint?: Paint):
38
148
  return new Text('Usage snapshot unavailable.', 0, 0).render(width);
39
149
  }
40
150
  // Rebuild on every render for theme/width changes, but freeze time at capture.
41
- const lines = usageLines(card, paint, width < 55 ? 10 : 18);
151
+ const lines = usageLines(card, paint, width < 55 ? 6 : 12);
42
152
  // Wrapping, not clipping: long quota names remain readable in narrow terminals.
43
153
  return new Text(lines.join('\n'), 0, 0).render(width);
44
154
  },
package/src/extension.ts CHANGED
@@ -180,11 +180,20 @@ export function createQuotaExtension(options: QuotaExtensionOptions = {}) {
180
180
  const getAuth = resolveAuth(ctx);
181
181
  const providers = supportedProviders(ctx);
182
182
  const states = await Promise.all(providers.map(provider => queryProvider(provider, getAuth)));
183
+ const ordered = states
184
+ .map((state, index) => ({ state, index }))
185
+ .sort((a, b) => {
186
+ const rank = (state: QuotaState) => state.kind === 'error' ? 3
187
+ : state.kind === 'ready' && state.snapshot.windows.length > 0 ? 1
188
+ : 2;
189
+ return rank(a.state) - rank(b.state) || a.index - b.index;
190
+ })
191
+ .map(item => item.state);
183
192
  // Never append via a stale Pi runtime, or label a replacement model's data
184
193
  // as the result of the original command. A newer command owns its result.
185
194
  if (!current || generation !== commandGeneration) return;
186
195
  const visibleStates = args.trim() === '--all'
187
- ? states : states.filter(state => state.kind === 'ready');
196
+ ? ordered : ordered.filter(state => state.kind === 'ready');
188
197
  const card = captureAllUsage(visibleStates);
189
198
  pi.appendEntry<UsageCard>(USAGE_ENTRY, card);
190
199
  // Custom entry renderers are TUI-only; RPC clients still get readable output.