@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 +1 -1
- package/src/bar/card.ts +119 -9
- package/src/extension.ts +10 -1
package/package.json
CHANGED
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
|
|
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
|
-
|
|
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
|
-
|
|
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 ?
|
|
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
|
-
?
|
|
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.
|