@genee/omp-opsx-addon 0.5.0 → 0.6.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.
Files changed (2) hide show
  1. package/lib/usage-render.ts +60 -42
  2. package/package.json +1 -1
@@ -48,13 +48,46 @@ export interface Painter {
48
48
  header(label: string, brandAnsi: string): string;
49
49
  /** Numeric balance text (e.g. "余额 ¥18.06"), text-colored. */
50
50
  balance(text: string): string;
51
- /** Foreground-color an arbitrary span with a named color (waveform cells). */
52
- fg(color: PainterColor, text: string): string;
51
+ /**
52
+ * Waveform layer paint — btop row gradient over the provider's brand hue.
53
+ * `brandAnsi` is the raw brand SGR ('' → accent fallback, like header).
54
+ * Both rows share the hue family with a fixed, theme-independent step:
55
+ * 'bottom' a curated 256-color bright shade of the hue (aixterm bright /
56
+ * bold when the code isn't a mapped 16-color SGR), 'top' the plain brand.
57
+ */
58
+ wave(brandAnsi: string, layer: 'top' | 'bottom', text: string): string;
53
59
  }
54
60
 
55
61
  const pctValue = (p: number): string => `${Math.round(p * 100)}%`;
56
62
  const pctColor = (p: number): PainterColor => (p >= 0.85 ? 'error' : p >= 0.6 ? 'warning' : 'success');
57
63
 
64
+ /**
65
+ * Curated 256-color bottom-row shade per 16-color brand hue (chart only —
66
+ * headers keep the raw brand SGR). The 256 palette's 16–255 range renders as
67
+ * FIXED rgb in every terminal, so the bottom/top step is guaranteed visible
68
+ * regardless of the active 16-color theme. Each shade is anchored on the
69
+ * de-facto standard dark-theme value of that hue (TokyoNight family) and is
70
+ * the SAME ink one purity/brightness step up — same hue direction, no hot
71
+ * magenta/red jumps that would break the "one color" read.
72
+ */
73
+ const WAVE_SHADES: Record<string, string> = {
74
+ [X]: '38;5;204', // red #ff5f87 (TN #f7768e, denser)
75
+ [G]: '38;5;114', // green #87d787 (TN #9ece6a, denser)
76
+ [BLUE]: '38;5;75', // blue #5fafff (TN #7aa2f7, denser)
77
+ [PURPLE]: '38;5;177', // purple #d787ff (TN #bb9af7, denser)
78
+ [CYAN]: '38;5;81', // cyan #5fd7ff (TN #7dcfff, denser)
79
+ };
80
+
81
+ /** Aixterm bright variant of a 16-color brand code (`\x1b[3Nm` → `\x1b[9Nm`); undefined when not derivable. */
82
+ const brightVariant = (code: string): string | undefined => {
83
+ const m = code.match(/^\x1b\[3([0-7])m$/);
84
+ return m ? `\x1b[9${m[1]}m` : undefined;
85
+ };
86
+
87
+ /** Bottom-row shade of a raw hue: curated 256 bright shade, aixterm bright, bold as the last step. */
88
+ const bottomShade = (hue: string): string =>
89
+ WAVE_SHADES[hue] ? `\x1b[${WAVE_SHADES[hue]}m` : brightVariant(hue) ?? BOLD + hue;
90
+
58
91
  /** Pre-refactor hardcoded ANSI — the RPC/print fallback and the test baseline. */
59
92
  export const ansiPainter: Painter = {
60
93
  pct: (p) => {
@@ -65,21 +98,7 @@ export const ansiPainter: Painter = {
65
98
  sep: ` ${D}│${R} `,
66
99
  header: (label, brandAnsi) => c(brandAnsi ? BOLD + brandAnsi : BOLD, label),
67
100
  balance: (text) => c(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
- ),
101
+ wave: (brandAnsi, layer, text) => c(layer === 'bottom' ? bottomShade(brandAnsi || CYAN) : brandAnsi || CYAN, text),
83
102
  };
84
103
 
85
104
  /** Status colors via the active host theme; brand headers keep raw ANSI. */
@@ -90,7 +109,12 @@ export function themePainter(theme: PainterTheme): Painter {
90
109
  sep: ` ${theme.fg('dim', '│')} `,
91
110
  header: (label, brandAnsi) => (brandAnsi ? c(brandAnsi, label) : theme.fg('accent', label)),
92
111
  balance: (text) => theme.fg('text', text),
93
- fg: (color, text) => theme.fg(color, text),
112
+ wave: (brandAnsi, layer, text) =>
113
+ brandAnsi
114
+ ? c(layer === 'bottom' ? bottomShade(brandAnsi) : brandAnsi, text)
115
+ : layer === 'bottom'
116
+ ? BOLD + theme.fg('accent', text)
117
+ : theme.fg('accent', text),
94
118
  };
95
119
  }
96
120
 
@@ -424,7 +448,7 @@ export function renderUsageReports(
424
448
  // placeholder strip, keeping the column height stable.
425
449
  if (consumptionTracks) {
426
450
  const track = consumptionTracks.get(c.provider);
427
- if (track) c.col.push(...renderConsumptionLines(track.samples, painter, waveCols));
451
+ if (track) c.col.push(...renderConsumptionLines(track.samples, painter, waveCols, providerLabel(c.provider).color));
428
452
  }
429
453
  if (pad) c.col = c.col.map((l) => padRight(l, blockWidth));
430
454
  }
@@ -469,13 +493,6 @@ export function composeChar(leftRows: number, rightRows: number): string {
469
493
  return String.fromCharCode(code);
470
494
  }
471
495
 
472
- /** Named-color band for a stock consumption level (`1 - remaining`): remaining ≥ 50% → success, ≥ 20% → warning, else error. */
473
- export function consumptionColor(c: number): PainterColor {
474
- if (c <= 0.5) return 'success';
475
- if (c <= 0.8) return 'warning';
476
- return 'error';
477
- }
478
-
479
496
  /** One provider's consumption track feeding its embedded mini chart. */
480
497
  export interface ConsumptionTrack {
481
498
  /** Per-interval consumption increments (raw values), newest last. */
@@ -498,12 +515,17 @@ export interface ConsumptionTrack {
498
515
  * window yields a blank placeholder strip, never `undefined`, so every
499
516
  * provider column keeps a stable chart area and the layout never jumps.
500
517
  *
501
- * Color is bound to RATE INTENSITY (image-scaling blend, not quota severity —
502
- * quota urgency already reads in the usage text, e.g. "5h 0%", balance). Each
503
- * Braille character takes the MEAN of its two half-column interpolated
504
- * intensities, mapped via consumptionColor (low→green, mid→yellow, high→red),
505
- * so consecutive chars blend smoothly and never hard-switch. The newest column
506
- * keeps accent.
518
+ * Color is a btop-style ROW gradient bound to the vertical position only —
519
+ * never to the data. The chart paints the provider's brand hue in two layers
520
+ * with a fixed step: the bottom line takes a curated 256-color bright shade
521
+ * of the hue (fixed rgb — visible under any 16-color theme; aixterm bright /
522
+ * bold when the code isn't mapped), the top line the plain brand — visibly
523
+ * stronger at the base, never washed out. Sliding samples reshape the
524
+ * envelope while every
525
+ * pixel's color stays put — the per-column intensity heat bands this
526
+ * replaces flipped green/yellow/red as the window moved, reading as flicker.
527
+ * Providers without a curated brand hue fall back to the theme accent.
528
+ * Quota urgency still reads in the usage text ("5h 0%", balance).
507
529
  *
508
530
  * Geometry unchanged: 1px per sample (half-char step), right-aligned; each
509
531
  * character merges its two half-column samples with max and renders both
@@ -514,6 +536,7 @@ export function renderConsumptionLines(
514
536
  samples: readonly number[],
515
537
  painter: Painter = ansiPainter,
516
538
  cols: number = WAVEFORM_COLS,
539
+ brandAnsi = '',
517
540
  ): string[] {
518
541
  const n = samples.length;
519
542
  const totalPx = cols * 2;
@@ -564,14 +587,10 @@ export function renderConsumptionLines(
564
587
  return plot[m - 1] / max;
565
588
  };
566
589
 
567
- // Each half-column keeps its OWN interpolated height (two samples share one
568
- // char, possibly at different heights — the pair-into-one-char geometry);
569
- // only COLOR is blended (terminals can't half-tint a char, so the char
570
- // takes the mean intensity of its two half-columns).
571
- const accentChar = Math.floor((totalPx - 1) / 2);
572
590
  // Two-line strip: full scale = 8px (2 Braille rows × 4 dot rows). Each
573
- // half-column keeps its OWN interpolated height; only COLOR is blended
574
- // (terminals can't half-tint a char, so the char takes the mean intensity).
591
+ // half-column keeps its OWN interpolated height; COLOR is ROW-bound only
592
+ // (bottom = 256 bright shade of the brand hue, top = plain brand) and
593
+ // never varies per column.
575
594
  const rows = 2;
576
595
  const lines = new Array<string>(rows).fill('');
577
596
  for (let c = 0; c < cols; c++) {
@@ -587,10 +606,9 @@ export function renderConsumptionLines(
587
606
  // never vanish under the dynamic scale.
588
607
  const hL = l > 0 ? Math.max(1, Math.round(8 * l)) : 0;
589
608
  const hR = r > 0 ? Math.max(1, Math.round(8 * r)) : 0;
590
- const color = c === accentChar ? 'accent' : consumptionColor((l + r) / 2);
591
609
  // Row 0 = top (px 4..8), row 1 = bottom (px 0..4) of each column.
592
- lines[0] += painter.fg(color, composeChar(Math.max(0, hL - 4), Math.max(0, hR - 4)));
593
- lines[1] += painter.fg(color, composeChar(Math.min(4, hL), Math.min(4, hR)));
610
+ lines[0] += painter.wave(brandAnsi, 'top', composeChar(Math.max(0, hL - 4), Math.max(0, hR - 4)));
611
+ lines[1] += painter.wave(brandAnsi, 'bottom', composeChar(Math.min(4, hL), Math.min(4, hR)));
594
612
  }
595
613
  return lines;
596
614
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genee/omp-opsx-addon",
3
- "version": "0.5.0",
3
+ "version": "0.6.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",