@genee/omp-opsx-addon 0.3.0 → 0.5.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/README.md +82 -0
- package/index.ts +248 -154
- package/lib/agent-defs.ts +4 -4
- package/lib/edit-variant-pin.ts +4 -4
- package/lib/logger.ts +33 -0
- package/lib/model-roles.ts +270 -0
- package/lib/pipe-core.ts +784 -0
- package/lib/pipe-push.ts +467 -0
- package/lib/reachability-cache.ts +2 -1
- package/lib/reachability-probe.ts +3 -2
- package/lib/status-probe.ts +4 -3
- package/lib/unified-config.ts +42 -1
- package/lib/usage-poller.ts +208 -7
- package/lib/usage-redis-multiproc.worker.ts +5 -1
- package/lib/usage-render.ts +151 -97
- package/lib/usage-resolver.ts +10 -4
- package/package.json +1 -1
package/lib/usage-render.ts
CHANGED
|
@@ -1,20 +1,27 @@
|
|
|
1
|
-
import { visibleWidth, truncateToWidth } from
|
|
2
|
-
import { resolveUsedFraction, getProviderDefinition, type UsageLimit, type UsageReport } from
|
|
3
|
-
import { summarizeReport, quotaPolicyFor } from
|
|
1
|
+
import { visibleWidth, truncateToWidth } from '@oh-my-pi/pi-tui';
|
|
2
|
+
import { resolveUsedFraction, getProviderDefinition, type UsageLimit, type UsageReport } from '@oh-my-pi/pi-ai';
|
|
3
|
+
import { summarizeReport, quotaPolicyFor } from './usage-resolver.js';
|
|
4
4
|
|
|
5
5
|
// Hardcoded ANSI retained for `ansiPainter` — the RPC/print fallback path and
|
|
6
6
|
// the legacy `render.test.ts` baseline. The interactive component-factory path
|
|
7
7
|
// swaps them for the host theme via `themePainter(theme)`.
|
|
8
|
-
const R =
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
const R = '\x1b[0m',
|
|
9
|
+
G = '\x1b[32m',
|
|
10
|
+
Y = '\x1b[33m',
|
|
11
|
+
X = '\x1b[31m',
|
|
12
|
+
D = '\x1b[2m';
|
|
13
|
+
const BLUE = '\x1b[34m',
|
|
14
|
+
PURPLE = '\x1b[35m',
|
|
15
|
+
CYAN = '\x1b[36m',
|
|
16
|
+
WHITE = '\x1b[37m';
|
|
17
|
+
const BOLD = '\x1b[1m';
|
|
11
18
|
const c = (s: string, t: string): string => `${s}${t}${R}`;
|
|
12
19
|
|
|
13
20
|
// The subset of the host `ThemeColor` union the widget actually paints with.
|
|
14
21
|
// Keeping it narrow lets widget tests inject a mock theme that only implements
|
|
15
22
|
// `fg` instead of the full `Theme` surface (the host passes a real `Theme`,
|
|
16
23
|
// which is structurally assignable thanks to `PainterColor ⊆ ThemeColor`).
|
|
17
|
-
export type PainterColor =
|
|
24
|
+
export type PainterColor = 'success' | 'warning' | 'error' | 'dim' | 'accent' | 'text';
|
|
18
25
|
|
|
19
26
|
/** Minimal theme surface the rendering painter consumes (`host Theme.fg`). */
|
|
20
27
|
export interface PainterTheme {
|
|
@@ -46,29 +53,43 @@ export interface Painter {
|
|
|
46
53
|
}
|
|
47
54
|
|
|
48
55
|
const pctValue = (p: number): string => `${Math.round(p * 100)}%`;
|
|
49
|
-
const pctColor = (p: number): PainterColor => (p >= 0.85 ?
|
|
56
|
+
const pctColor = (p: number): PainterColor => (p >= 0.85 ? 'error' : p >= 0.6 ? 'warning' : 'success');
|
|
50
57
|
|
|
51
58
|
/** Pre-refactor hardcoded ANSI — the RPC/print fallback and the test baseline. */
|
|
52
59
|
export const ansiPainter: Painter = {
|
|
53
60
|
pct: (p) => {
|
|
54
61
|
const col = pctColor(p);
|
|
55
|
-
return c(col ===
|
|
62
|
+
return c(col === 'error' ? X : col === 'warning' ? Y : G, pctValue(p));
|
|
56
63
|
},
|
|
57
64
|
exhausted: (text) => c(X, text),
|
|
58
65
|
sep: ` ${D}│${R} `,
|
|
59
66
|
header: (label, brandAnsi) => c(brandAnsi ? BOLD + brandAnsi : BOLD, label),
|
|
60
67
|
balance: (text) => c(WHITE, text),
|
|
61
|
-
fg: (color, text) =>
|
|
68
|
+
fg: (color, text) =>
|
|
69
|
+
c(
|
|
70
|
+
color === 'error'
|
|
71
|
+
? X
|
|
72
|
+
: color === 'warning'
|
|
73
|
+
? Y
|
|
74
|
+
: color === 'success'
|
|
75
|
+
? G
|
|
76
|
+
: color === 'dim'
|
|
77
|
+
? D
|
|
78
|
+
: color === 'accent'
|
|
79
|
+
? CYAN
|
|
80
|
+
: WHITE,
|
|
81
|
+
text,
|
|
82
|
+
),
|
|
62
83
|
};
|
|
63
84
|
|
|
64
85
|
/** Status colors via the active host theme; brand headers keep raw ANSI. */
|
|
65
86
|
export function themePainter(theme: PainterTheme): Painter {
|
|
66
87
|
return {
|
|
67
88
|
pct: (p) => theme.fg(pctColor(p), pctValue(p)),
|
|
68
|
-
exhausted: (text) => theme.fg(
|
|
69
|
-
sep: ` ${theme.fg(
|
|
70
|
-
header: (label, brandAnsi) => (brandAnsi ? c(brandAnsi, label) : theme.fg(
|
|
71
|
-
balance: (text) => theme.fg(
|
|
89
|
+
exhausted: (text) => theme.fg('error', text),
|
|
90
|
+
sep: ` ${theme.fg('dim', '│')} `,
|
|
91
|
+
header: (label, brandAnsi) => (brandAnsi ? c(brandAnsi, label) : theme.fg('accent', label)),
|
|
92
|
+
balance: (text) => theme.fg('text', text),
|
|
72
93
|
fg: (color, text) => theme.fg(color, text),
|
|
73
94
|
};
|
|
74
95
|
}
|
|
@@ -77,34 +98,43 @@ export function themePainter(theme: PainterTheme): Painter {
|
|
|
77
98
|
// verbose for a compact status widget (e.g. "Z.AI (GLM Coding Plan · Sign in)").
|
|
78
99
|
// Unknown providers fall back to the omp registry display name, then the raw id.
|
|
79
100
|
const PROVIDER: Record<string, { label: string; color: string }> = {
|
|
80
|
-
|
|
81
|
-
zai:
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
101
|
+
'zhipu-coding-plan': { label: '智谱', color: PURPLE },
|
|
102
|
+
zai: { label: '智谱', color: PURPLE },
|
|
103
|
+
'kimi-code': { label: 'Kimi', color: CYAN },
|
|
104
|
+
'minimax-code-cn': { label: 'MiniMax', color: X },
|
|
105
|
+
'ark-coding-plan': { label: '火山方舟', color: CYAN },
|
|
106
|
+
deepseek: { label: 'DeepSeek', color: BLUE },
|
|
107
|
+
cursor: { label: 'Cursor', color: CYAN },
|
|
108
|
+
'opencode-go': { label: 'OpenCode', color: G },
|
|
109
|
+
moonshot: { label: 'Moonshot', color: G },
|
|
88
110
|
};
|
|
89
111
|
|
|
90
112
|
const providerLabel = (id: string): { label: string; color: string } => {
|
|
91
113
|
const known = PROVIDER[id];
|
|
92
114
|
if (known) return known;
|
|
93
115
|
const reg = getProviderDefinition(id);
|
|
94
|
-
if (reg?.name) return { label: reg.name, color:
|
|
95
|
-
return { label: id, color:
|
|
116
|
+
if (reg?.name) return { label: reg.name, color: '' };
|
|
117
|
+
return { label: id, color: '' };
|
|
96
118
|
};
|
|
97
119
|
|
|
98
|
-
const WL: Record<string, string> = {
|
|
120
|
+
const WL: Record<string, string> = {
|
|
121
|
+
'5h': '5h',
|
|
122
|
+
'rolling-5h': '5h',
|
|
123
|
+
'1d': '日',
|
|
124
|
+
'1w': '周',
|
|
125
|
+
weekly: '周',
|
|
126
|
+
'1mo': '月',
|
|
127
|
+
monthly: '月',
|
|
128
|
+
};
|
|
99
129
|
|
|
100
130
|
// ── helpers ──────────────────────────────────────────────────────────
|
|
101
131
|
|
|
102
|
-
const pad2 = (n: number): string => String(n).padStart(2,
|
|
132
|
+
const pad2 = (n: number): string => String(n).padStart(2, '0');
|
|
103
133
|
|
|
104
134
|
/** Fixed-width reset countdown, e.g. "00d03h12m05s"; "即将重置" once elapsed. */
|
|
105
135
|
const resetIn = (resetsAt: number): string => {
|
|
106
136
|
const diffMs = resetsAt - Date.now();
|
|
107
|
-
if (diffMs <= 0) return
|
|
137
|
+
if (diffMs <= 0) return '即将重置';
|
|
108
138
|
const totalSec = Math.floor(diffMs / 1_000);
|
|
109
139
|
const d = Math.floor(totalSec / 86_400);
|
|
110
140
|
const h = Math.floor((totalSec % 86_400) / 3_600);
|
|
@@ -114,9 +144,9 @@ const resetIn = (resetsAt: number): string => {
|
|
|
114
144
|
};
|
|
115
145
|
|
|
116
146
|
function isQuotaFull(l: UsageLimit): boolean {
|
|
117
|
-
if (l.status ===
|
|
147
|
+
if (l.status === 'exhausted') return true;
|
|
118
148
|
const pct = resolveUsedFraction(l);
|
|
119
|
-
return typeof pct ===
|
|
149
|
+
return typeof pct === 'number' && pct >= 1;
|
|
120
150
|
}
|
|
121
151
|
|
|
122
152
|
function resetAtOf(l: UsageLimit, report?: UsageReport): number | undefined {
|
|
@@ -127,7 +157,7 @@ function resetAtOf(l: UsageLimit, report?: UsageReport): number | undefined {
|
|
|
127
157
|
/** Exhausted / 100% text (uncolored); the painter wraps it in the error color. */
|
|
128
158
|
const exhaustedText = (l: UsageLimit, report?: UsageReport): string => {
|
|
129
159
|
const at = resetAtOf(l, report);
|
|
130
|
-
return at !== undefined ? resetIn(at) :
|
|
160
|
+
return at !== undefined ? resetIn(at) : '耗尽';
|
|
131
161
|
};
|
|
132
162
|
|
|
133
163
|
/** Recovery countdown when the provider is unusable.
|
|
@@ -137,7 +167,7 @@ const exhaustedText = (l: UsageLimit, report?: UsageReport): string => {
|
|
|
137
167
|
* Falls back to any sibling resetsAt / `耗尽` when no exhausted window has one.
|
|
138
168
|
*/
|
|
139
169
|
const providerExhaustedText = (report: UsageReport): string => {
|
|
140
|
-
const preferLatest = quotaPolicyFor(report.provider) !==
|
|
170
|
+
const preferLatest = quotaPolicyFor(report.provider) !== 'bucket';
|
|
141
171
|
const pick = (a: number, b: number): number => (preferLatest ? Math.max(a, b) : Math.min(a, b));
|
|
142
172
|
|
|
143
173
|
let chosen: number | undefined;
|
|
@@ -146,24 +176,33 @@ const providerExhaustedText = (report: UsageReport): string => {
|
|
|
146
176
|
for (const l of report.limits) {
|
|
147
177
|
if (!isQuotaFull(l)) continue;
|
|
148
178
|
const at = l.window?.resetsAt;
|
|
149
|
-
if (typeof at !==
|
|
179
|
+
if (typeof at !== 'number') continue;
|
|
150
180
|
chosen = chosen === undefined ? at : pick(chosen, at);
|
|
151
181
|
}
|
|
152
182
|
if (chosen === undefined) {
|
|
153
183
|
for (const l of report.limits) {
|
|
154
184
|
const at = l.window?.resetsAt;
|
|
155
|
-
if (typeof at !==
|
|
185
|
+
if (typeof at !== 'number') continue;
|
|
156
186
|
chosen = chosen === undefined ? at : pick(chosen, at);
|
|
157
187
|
}
|
|
158
188
|
}
|
|
159
|
-
return chosen !== undefined ? resetIn(chosen) :
|
|
189
|
+
return chosen !== undefined ? resetIn(chosen) : '耗尽';
|
|
160
190
|
};
|
|
161
191
|
|
|
162
192
|
const cursorWinLabel = (l: UsageLimit): string | undefined => {
|
|
163
|
-
const hay = `${l.id ??
|
|
164
|
-
if (hay.includes(
|
|
165
|
-
|
|
166
|
-
if (
|
|
193
|
+
const hay = `${l.id ?? ''} ${l.label ?? ''} ${l.window?.id ?? ''}`.toLowerCase();
|
|
194
|
+
if (hay.includes('ondemand') || hay.includes('on-demand') || hay.includes('spendlimit') || hay.includes('按需'))
|
|
195
|
+
return '按需';
|
|
196
|
+
if (
|
|
197
|
+
hay.includes('builtin') ||
|
|
198
|
+
hay.includes('cursor models') ||
|
|
199
|
+
hay.includes('planusage') ||
|
|
200
|
+
hay.includes('autopercent') ||
|
|
201
|
+
hay.includes('内置')
|
|
202
|
+
)
|
|
203
|
+
return '内置';
|
|
204
|
+
if (hay.includes('other') || hay.includes('named') || hay.includes('apipercent') || hay.includes('其他'))
|
|
205
|
+
return '其他';
|
|
167
206
|
return undefined;
|
|
168
207
|
};
|
|
169
208
|
|
|
@@ -171,36 +210,36 @@ const winLabel = (l: UsageLimit): string => {
|
|
|
171
210
|
const cursor = cursorWinLabel(l);
|
|
172
211
|
if (cursor) return cursor;
|
|
173
212
|
const w = l.window;
|
|
174
|
-
if (!w) return l.label ||
|
|
213
|
+
if (!w) return l.label || '?';
|
|
175
214
|
if (WL[w.id]) return WL[w.id];
|
|
176
|
-
if (w.durationMs === 18_000_000) return
|
|
177
|
-
if (w.durationMs === 86_400_000) return
|
|
178
|
-
if (w.durationMs === 604_800_000) return
|
|
179
|
-
if (w.durationMs === 2_592_000_000) return
|
|
180
|
-
if (w.id ===
|
|
215
|
+
if (w.durationMs === 18_000_000) return '5h';
|
|
216
|
+
if (w.durationMs === 86_400_000) return '日';
|
|
217
|
+
if (w.durationMs === 604_800_000) return '周';
|
|
218
|
+
if (w.durationMs === 2_592_000_000) return '月';
|
|
219
|
+
if (w.id === 'default') return l.label === 'Total quota' ? '总' : l.label;
|
|
181
220
|
return w.label || w.id;
|
|
182
221
|
};
|
|
183
222
|
|
|
184
223
|
const windowSortKey = (l: UsageLimit): number => {
|
|
185
224
|
const cursor = cursorWinLabel(l);
|
|
186
|
-
if (cursor ===
|
|
187
|
-
if (cursor ===
|
|
188
|
-
if (cursor ===
|
|
225
|
+
if (cursor === '内置') return 4;
|
|
226
|
+
if (cursor === '其他') return 5;
|
|
227
|
+
if (cursor === '按需') return 7;
|
|
189
228
|
const w = l.window;
|
|
190
229
|
if (!w) return 50;
|
|
191
|
-
if (w.id ===
|
|
192
|
-
if (w.id ===
|
|
193
|
-
if (w.id ===
|
|
194
|
-
if (w.id ===
|
|
230
|
+
if (w.id === '5h') return 0;
|
|
231
|
+
if (w.id === '1d') return 1;
|
|
232
|
+
if (w.id === '1w') return 2;
|
|
233
|
+
if (w.id === '1mo') return 3;
|
|
195
234
|
// Cursor dashboard order: Cursor Models(内置)→ Other Models(其他)→ On-demand(按需).
|
|
196
|
-
if (w.id ===
|
|
197
|
-
if (w.id ===
|
|
198
|
-
if (w.id ===
|
|
235
|
+
if (w.id === 'builtin' || w.id === 'auto') return 4;
|
|
236
|
+
if (w.id === 'other' || w.id === 'api') return 5;
|
|
237
|
+
if (w.id === 'ondemand') return 7;
|
|
199
238
|
if (w.durationMs === 18_000_000) return 0;
|
|
200
239
|
if (w.durationMs === 86_400_000) return 1;
|
|
201
240
|
if (w.durationMs === 604_800_000) return 2;
|
|
202
241
|
if (w.durationMs === 2_592_000_000) return 3;
|
|
203
|
-
if (w.id ===
|
|
242
|
+
if (w.id === 'default' || l.label === 'Total quota') return 100;
|
|
204
243
|
return 50;
|
|
205
244
|
};
|
|
206
245
|
|
|
@@ -208,7 +247,7 @@ const windowSortKey = (l: UsageLimit): number => {
|
|
|
208
247
|
const padRight = (s: string, width: number): string => {
|
|
209
248
|
const v = visibleWidth(s);
|
|
210
249
|
if (v >= width) return s;
|
|
211
|
-
return s +
|
|
250
|
+
return s + ' '.repeat(width - v);
|
|
212
251
|
};
|
|
213
252
|
|
|
214
253
|
// ── column builder ───────────────────────────────────────────────────
|
|
@@ -243,13 +282,13 @@ export const buildColumn = (r: UsageReport, painter: Painter = ansiPainter, load
|
|
|
243
282
|
let val: string;
|
|
244
283
|
if (health.exhausted || isQuotaFull(l)) {
|
|
245
284
|
val = painter.exhausted(exhaustedText(l, r));
|
|
246
|
-
} else if (typeof l.amount.remaining ===
|
|
247
|
-
const sym = (l.amount.unit as string) ===
|
|
248
|
-
const text = r.provider ===
|
|
285
|
+
} else if (typeof l.amount.remaining === 'number') {
|
|
286
|
+
const sym = (l.amount.unit as string) === 'CNY' ? '¥' : '$';
|
|
287
|
+
const text = r.provider === 'deepseek' ? `余额 ${sym}${l.amount.remaining}` : `${sym}${l.amount.remaining}`;
|
|
249
288
|
val = painter.balance(text);
|
|
250
289
|
} else {
|
|
251
290
|
const pct = resolveUsedFraction(l);
|
|
252
|
-
val = typeof pct ===
|
|
291
|
+
val = typeof pct === 'number' ? painter.pct(pct) : '?';
|
|
253
292
|
}
|
|
254
293
|
return [header, val];
|
|
255
294
|
}
|
|
@@ -266,26 +305,26 @@ export const buildColumn = (r: UsageReport, painter: Painter = ansiPainter, load
|
|
|
266
305
|
// are noise and are hidden.
|
|
267
306
|
const sorted = [...r.limits].sort((a, b) => windowSortKey(a) - windowSortKey(b));
|
|
268
307
|
const parts: string[] = [];
|
|
269
|
-
const isCursor = r.provider ===
|
|
308
|
+
const isCursor = r.provider === 'cursor';
|
|
270
309
|
for (const l of sorted) {
|
|
271
310
|
if (isCursor && !cursorWinLabel(l)) continue;
|
|
272
311
|
const wl = winLabel(l);
|
|
273
312
|
let val: string;
|
|
274
313
|
if (isQuotaFull(l)) {
|
|
275
314
|
const pct = resolveUsedFraction(l);
|
|
276
|
-
val = typeof pct ===
|
|
315
|
+
val = typeof pct === 'number' ? painter.pct(Math.min(pct, 1)) : painter.exhausted('耗尽');
|
|
277
316
|
} else {
|
|
278
317
|
const pct = resolveUsedFraction(l);
|
|
279
|
-
val = typeof pct ===
|
|
318
|
+
val = typeof pct === 'number' ? painter.pct(pct) : '?';
|
|
280
319
|
}
|
|
281
320
|
parts.push(`${wl} ${val}`);
|
|
282
321
|
}
|
|
283
322
|
if (parts.length === 0) {
|
|
284
323
|
// e.g. Cursor only returned unclassified monthly leftovers — keep the
|
|
285
324
|
// column visible rather than dropping a logged-in provider.
|
|
286
|
-
return isCursor ? [header, painter.balance(
|
|
325
|
+
return isCursor ? [header, painter.balance('—')] : undefined;
|
|
287
326
|
}
|
|
288
|
-
return [header, parts.join(
|
|
327
|
+
return [header, parts.join(' · ')];
|
|
289
328
|
};
|
|
290
329
|
|
|
291
330
|
/**
|
|
@@ -296,8 +335,8 @@ export const buildColumn = (r: UsageReport, painter: Painter = ansiPainter, load
|
|
|
296
335
|
*/
|
|
297
336
|
export const buildEmptyReportColumn = (provider: string, painter: Painter = ansiPainter, loading = false): string[] => {
|
|
298
337
|
const { label, color } = providerLabel(provider);
|
|
299
|
-
return [painter.header(loading ? `${label}⟳` : label, color), painter.balance(loading ?
|
|
300
|
-
}
|
|
338
|
+
return [painter.header(loading ? `${label}⟳` : label, color), painter.balance(loading ? '⟳' : '—')];
|
|
339
|
+
};
|
|
301
340
|
|
|
302
341
|
/**
|
|
303
342
|
* Render a logged-in provider that has no report yet as a header + a single
|
|
@@ -307,20 +346,24 @@ export const buildEmptyReportColumn = (provider: string, painter: Painter = ansi
|
|
|
307
346
|
* windows is not mistaken for a logged-in-but-unfetched provider. `exhausted`
|
|
308
347
|
* reflects the process-local 429/403 signal (see `lib/provider-status.ts`).
|
|
309
348
|
*/
|
|
310
|
-
export const buildPlaceholderColumn = (
|
|
349
|
+
export const buildPlaceholderColumn = (
|
|
350
|
+
provider: string,
|
|
351
|
+
painter: Painter = ansiPainter,
|
|
352
|
+
loading = false,
|
|
353
|
+
exhausted = false,
|
|
354
|
+
): string[] => {
|
|
311
355
|
const { label, color } = providerLabel(provider);
|
|
312
|
-
const body = loading ?
|
|
356
|
+
const body = loading ? '⟳' : exhausted ? '耗尽' : '可用';
|
|
313
357
|
return [
|
|
314
358
|
painter.header(loading ? `${label}⟳` : label, color),
|
|
315
359
|
exhausted ? painter.exhausted(body) : painter.balance(body),
|
|
316
360
|
];
|
|
317
|
-
}
|
|
361
|
+
};
|
|
318
362
|
|
|
319
363
|
// ── flex-wrap table layout ───────────────────────────────────────────
|
|
320
364
|
|
|
321
365
|
const SEP_VISUAL = 3;
|
|
322
366
|
|
|
323
|
-
|
|
324
367
|
export function renderUsageReports(
|
|
325
368
|
reports: UsageReport[],
|
|
326
369
|
maxWidth = 120,
|
|
@@ -342,11 +385,15 @@ export function renderUsageReports(
|
|
|
342
385
|
// Build each provider column (header + usage / placeholder). Waveforms are
|
|
343
386
|
// appended AFTER the uniform block width is known so every provider's
|
|
344
387
|
// mini chart is the same width.
|
|
345
|
-
interface ProviderColumn {
|
|
388
|
+
interface ProviderColumn {
|
|
389
|
+
provider: string;
|
|
390
|
+
col: string[];
|
|
391
|
+
}
|
|
346
392
|
const cols: ProviderColumn[] = (reports ?? []).map((r) => {
|
|
347
|
-
const col =
|
|
348
|
-
|
|
349
|
-
|
|
393
|
+
const col =
|
|
394
|
+
r.limits.length === 0
|
|
395
|
+
? buildEmptyReportColumn(r.provider, painter, loading.has(r.provider))
|
|
396
|
+
: buildColumn(r, painter, loading.has(r.provider));
|
|
350
397
|
if (!col) return { provider: r.provider, col: [] };
|
|
351
398
|
// Second-level estimate override replaces the usage line (balance style).
|
|
352
399
|
const est = usageEstimates?.get(r.provider);
|
|
@@ -354,7 +401,10 @@ export function renderUsageReports(
|
|
|
354
401
|
return { provider: r.provider, col };
|
|
355
402
|
});
|
|
356
403
|
for (const id of placeholders) {
|
|
357
|
-
cols.push({
|
|
404
|
+
cols.push({
|
|
405
|
+
provider: id,
|
|
406
|
+
col: buildPlaceholderColumn(id, painter, loading.has(id), exhaustedProviders?.has(id)),
|
|
407
|
+
});
|
|
358
408
|
}
|
|
359
409
|
const valid = cols.filter((c) => c.col.length > 0);
|
|
360
410
|
if (valid.length === 0) return [];
|
|
@@ -386,7 +436,7 @@ export function renderUsageReports(
|
|
|
386
436
|
const out: string[] = [];
|
|
387
437
|
for (let line = 0; line < n; line++) {
|
|
388
438
|
const cells = valid.map((c) => {
|
|
389
|
-
const t = line < c.col.length ? c.col[line] :
|
|
439
|
+
const t = line < c.col.length ? c.col[line] : '';
|
|
390
440
|
return pad ? padRight(t, blockWidth) : t;
|
|
391
441
|
});
|
|
392
442
|
out.push(cells.join(painter.sep));
|
|
@@ -406,24 +456,24 @@ export function braillePixelHeight(consumption: number): number {
|
|
|
406
456
|
|
|
407
457
|
/** Single-column dot bitmask for `rows` pixels lit from the bottom (0..4). */
|
|
408
458
|
const LEFT_DOTS = [0x00, 0x40, 0x44, 0x46, 0x47];
|
|
409
|
-
const RIGHT_DOTS = [0x00, 0x80,
|
|
459
|
+
const RIGHT_DOTS = [0x00, 0x80, 0xa0, 0xb0, 0xb8];
|
|
410
460
|
|
|
411
|
-
export function sideDots(rows: number, side:
|
|
461
|
+
export function sideDots(rows: number, side: 'left' | 'right'): number {
|
|
412
462
|
const r = Math.max(0, Math.min(4, rows));
|
|
413
|
-
return side ===
|
|
463
|
+
return side === 'left' ? LEFT_DOTS[r] : RIGHT_DOTS[r];
|
|
414
464
|
}
|
|
415
465
|
|
|
416
466
|
/** Compose a Braille cell (U+2800 | left | right) from two column heights. */
|
|
417
467
|
export function composeChar(leftRows: number, rightRows: number): string {
|
|
418
|
-
const code = 0x2800 | sideDots(leftRows,
|
|
468
|
+
const code = 0x2800 | sideDots(leftRows, 'left') | sideDots(rightRows, 'right');
|
|
419
469
|
return String.fromCharCode(code);
|
|
420
470
|
}
|
|
421
471
|
|
|
422
472
|
/** Named-color band for a stock consumption level (`1 - remaining`): remaining ≥ 50% → success, ≥ 20% → warning, else error. */
|
|
423
473
|
export function consumptionColor(c: number): PainterColor {
|
|
424
|
-
if (c <= 0.5) return
|
|
425
|
-
if (c <= 0.8) return
|
|
426
|
-
return
|
|
474
|
+
if (c <= 0.5) return 'success';
|
|
475
|
+
if (c <= 0.8) return 'warning';
|
|
476
|
+
return 'error';
|
|
427
477
|
}
|
|
428
478
|
|
|
429
479
|
/** One provider's consumption track feeding its embedded mini chart. */
|
|
@@ -482,15 +532,16 @@ export function renderConsumptionLines(
|
|
|
482
532
|
// through the line. Skipped while the window is still filling or too
|
|
483
533
|
// short (≥8 samples) — the live right edge keeps its exact per-second
|
|
484
534
|
// shape during warm-up.
|
|
485
|
-
const plot =
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
535
|
+
const plot =
|
|
536
|
+
m >= totalPx && m >= 8
|
|
537
|
+
? (() => {
|
|
538
|
+
const s = new Array<number>(m);
|
|
539
|
+
s[0] = (win[0] + win[1]) / 2;
|
|
540
|
+
for (let i = 1; i < m - 1; i++) s[i] = (win[i - 1] + 2 * win[i] + win[i + 1]) / 4;
|
|
541
|
+
s[m - 1] = (win[m - 2] + win[m - 1]) / 2;
|
|
542
|
+
return s;
|
|
543
|
+
})()
|
|
544
|
+
: [...win];
|
|
494
545
|
// Dynamic scale over the SMOOTHED window keeps "largest increment = full
|
|
495
546
|
// bar" after averaging trims the raw peak.
|
|
496
547
|
const max = Math.max(...plot);
|
|
@@ -522,7 +573,7 @@ export function renderConsumptionLines(
|
|
|
522
573
|
// half-column keeps its OWN interpolated height; only COLOR is blended
|
|
523
574
|
// (terminals can't half-tint a char, so the char takes the mean intensity).
|
|
524
575
|
const rows = 2;
|
|
525
|
-
const lines = new Array<string>(rows).fill(
|
|
576
|
+
const lines = new Array<string>(rows).fill('');
|
|
526
577
|
for (let c = 0; c < cols; c++) {
|
|
527
578
|
const l = intensityAt(2 * c);
|
|
528
579
|
const r = intensityAt(2 * c + 1);
|
|
@@ -531,9 +582,12 @@ export function renderConsumptionLines(
|
|
|
531
582
|
for (let li = 0; li < rows; li++) lines[li] += blank;
|
|
532
583
|
continue;
|
|
533
584
|
}
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
585
|
+
// Minimum visible dot: any non-zero consumption keeps ≥1px no matter
|
|
586
|
+
// how small its ratio to the window max — a low-rate second must
|
|
587
|
+
// never vanish under the dynamic scale.
|
|
588
|
+
const hL = l > 0 ? Math.max(1, Math.round(8 * l)) : 0;
|
|
589
|
+
const hR = r > 0 ? Math.max(1, Math.round(8 * r)) : 0;
|
|
590
|
+
const color = c === accentChar ? 'accent' : consumptionColor((l + r) / 2);
|
|
537
591
|
// Row 0 = top (px 4..8), row 1 = bottom (px 0..4) of each column.
|
|
538
592
|
lines[0] += painter.fg(color, composeChar(Math.max(0, hL - 4), Math.max(0, hR - 4)));
|
|
539
593
|
lines[1] += painter.fg(color, composeChar(Math.min(4, hL), Math.min(4, hR)));
|
package/lib/usage-resolver.ts
CHANGED
|
@@ -35,11 +35,17 @@ export interface DirectFetcher {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
|
-
* Provider id aliases
|
|
39
|
-
*
|
|
40
|
-
*
|
|
38
|
+
* Provider id aliases — a PERMANENT legacy migration mapping (not a temporary
|
|
39
|
+
* patch): the old `omp-ark-coding-plan` npm package wrote `arkcodingplan`
|
|
40
|
+
* (lowercase form of the mixed-case `ArkCodingPlan`), while the runtime
|
|
41
|
+
* canonical id is `ark-coding-plan`. Kept indefinitely so any historical
|
|
42
|
+
* on-disk data — and anything the old package writes again — normalizes onto
|
|
43
|
+
* the canonical id. `canonicalizeProvider` lowercases first, so all three
|
|
44
|
+
* spellings land on `arkcodingplan` before the alias applies.
|
|
41
45
|
*/
|
|
42
|
-
const PROVIDER_ALIAS: Record<string, string> = {
|
|
46
|
+
const PROVIDER_ALIAS: Record<string, string> = {
|
|
47
|
+
arkcodingplan: 'ark-coding-plan',
|
|
48
|
+
};
|
|
43
49
|
|
|
44
50
|
/**
|
|
45
51
|
* Normalize a provider id to its canonical form: lowercase first, then alias
|
package/package.json
CHANGED