@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.
@@ -1,20 +1,27 @@
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";
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 = "\x1b[0m", G = "\x1b[32m", Y = "\x1b[33m", X = "\x1b[31m", D = "\x1b[2m";
9
- const BLUE = "\x1b[34m", PURPLE = "\x1b[35m", CYAN = "\x1b[36m", WHITE = "\x1b[37m";
10
- const BOLD = "\x1b[1m";
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 = "success" | "warning" | "error" | "dim" | "accent" | "text";
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 ? "error" : p >= 0.6 ? "warning" : "success");
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 === "error" ? X : col === "warning" ? Y : G, pctValue(p));
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) => c(color === "error" ? X : color === "warning" ? Y : color === "success" ? G : color === "dim" ? D : color === "accent" ? CYAN : WHITE, 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("error", text),
69
- sep: ` ${theme.fg("dim", "│")} `,
70
- header: (label, brandAnsi) => (brandAnsi ? c(brandAnsi, label) : theme.fg("accent", label)),
71
- balance: (text) => theme.fg("text", text),
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
- "zhipu-coding-plan": { label: "智谱", color: PURPLE },
81
- zai: { label: "智谱", color: PURPLE },
82
- "kimi-code": { label: "Kimi", color: CYAN },
83
- "minimax-code-cn": { label: "MiniMax", color: X },
84
- deepseek: { label: "DeepSeek",color: BLUE },
85
- cursor: { label: "Cursor", color: CYAN },
86
- "opencode-go": { label: "OpenCode",color: G },
87
- moonshot: { label: "Moonshot",color: G },
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> = { "5h": "5h", "rolling-5h": "5h", "1d": "日", "1w": "周", weekly: "周", "1mo": "月", monthly: "月" };
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, "0");
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 === "exhausted") return true;
147
+ if (l.status === 'exhausted') return true;
118
148
  const pct = resolveUsedFraction(l);
119
- return typeof pct === "number" && pct >= 1;
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) !== "bucket";
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 !== "number") continue;
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 !== "number") continue;
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 ?? ""} ${l.label ?? ""} ${l.window?.id ?? ""}`.toLowerCase();
164
- if (hay.includes("ondemand") || hay.includes("on-demand") || hay.includes("spendlimit") || hay.includes("按需")) return "按需";
165
- if (hay.includes("builtin") || hay.includes("cursor models") || hay.includes("planusage") || hay.includes("autopercent") || hay.includes("内置")) return "内置";
166
- if (hay.includes("other") || hay.includes("named") || hay.includes("apipercent") || hay.includes("其他")) return "其他";
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 "5h";
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 === "default") return l.label === "Total quota" ? "总" : l.label;
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 === "内置") return 4;
187
- if (cursor === "其他") return 5;
188
- if (cursor === "按需") return 7;
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 === "5h") return 0;
192
- if (w.id === "1d") return 1;
193
- if (w.id === "1w") return 2;
194
- if (w.id === "1mo") return 3;
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 === "builtin" || w.id === "auto") return 4;
197
- if (w.id === "other" || w.id === "api") return 5;
198
- if (w.id === "ondemand") return 7;
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 === "default" || l.label === "Total quota") return 100;
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 + " ".repeat(width - v);
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 === "number") {
247
- const sym = (l.amount.unit as string) === "CNY" ? "¥" : "$";
248
- const text = r.provider === "deepseek" ? `余额 ${sym}${l.amount.remaining}` : `${sym}${l.amount.remaining}`;
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 === "number" ? painter.pct(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 === "cursor";
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 === "number" ? painter.pct(Math.min(pct, 1)) : painter.exhausted("耗尽");
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 === "number" ? painter.pct(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("—")] : undefined;
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 = (provider: string, painter: Painter = ansiPainter, loading = false, exhausted = false): string[] => {
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 ? "⟳" : exhausted ? "耗尽" : "可用";
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 { provider: string; col: string[] }
388
+ interface ProviderColumn {
389
+ provider: string;
390
+ col: string[];
391
+ }
346
392
  const cols: ProviderColumn[] = (reports ?? []).map((r) => {
347
- const col = r.limits.length === 0
348
- ? buildEmptyReportColumn(r.provider, painter, loading.has(r.provider))
349
- : buildColumn(r, painter, loading.has(r.provider));
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({ provider: id, col: buildPlaceholderColumn(id, painter, loading.has(id), exhaustedProviders?.has(id)) });
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, 0xA0, 0xB0, 0xB8];
459
+ const RIGHT_DOTS = [0x00, 0x80, 0xa0, 0xb0, 0xb8];
410
460
 
411
- export function sideDots(rows: number, side: "left" | "right"): number {
461
+ export function sideDots(rows: number, side: 'left' | 'right'): number {
412
462
  const r = Math.max(0, Math.min(4, rows));
413
- return side === "left" ? LEFT_DOTS[r] : RIGHT_DOTS[r];
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, "left") | sideDots(rightRows, "right");
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 "success";
425
- if (c <= 0.8) return "warning";
426
- return "error";
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 = m >= totalPx && m >= 8
486
- ? (() => {
487
- const s = new Array<number>(m);
488
- s[0] = (win[0] + win[1]) / 2;
489
- for (let i = 1; i < m - 1; i++) s[i] = (win[i - 1] + 2 * win[i] + win[i + 1]) / 4;
490
- s[m - 1] = (win[m - 2] + win[m - 1]) / 2;
491
- return s;
492
- })()
493
- : [...win];
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
- const hL = Math.round(8 * l);
535
- const hR = Math.round(8 * r);
536
- const color = c === accentChar ? "accent" : consumptionColor((l + r) / 2);
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)));
@@ -35,11 +35,17 @@ export interface DirectFetcher {
35
35
  }
36
36
 
37
37
  /**
38
- * Provider id aliases (currently empty). All fetchers and reports use the same
39
- * provider id as the auth/model registry (e.g. `zhipu-coding-plan`,
40
- * `minimax-code-cn`), so no canonicalization is needed.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genee/omp-opsx-addon",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "description": "Pi Extension: OpenSpec workflow orchestration - coder/reviewer/planner agents, session title & progress",
6
6
  "main": "./index.ts",