sigrank 0.16.0 → 0.17.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/tui.mjs CHANGED
@@ -12,215 +12,291 @@
12
12
  * - Dashboard + Compare load once, refresh on [R].
13
13
  */
14
14
 
15
- import { callTool, DEFAULT_API_BASE, pullActivePlatforms } from './tools.mjs'
16
- import { ALL_PLATFORMS } from './adapters.mjs'
17
- import { freshVerifierPillars } from './tokenpull.mjs'
18
- import { isSignedIn, isCodeChar } from './connect.mjs'
19
- import { loadIdentity, clearIdentity } from './keystore.mjs'
20
- import { execFile } from 'child_process'
21
- import { existsSync, readFileSync } from 'fs'
22
- import os from 'os'
23
- import path from 'path'
24
- import { createRequire } from 'module'
15
+ import { callTool, DEFAULT_API_BASE, pullActivePlatforms } from "./tools.mjs";
16
+ import { ALL_PLATFORMS } from "./adapters.mjs";
17
+ import { freshVerifierPillars } from "./tokenpull.mjs";
18
+ import { isSignedIn, isCodeChar } from "./connect.mjs";
19
+ import { loadIdentity, clearIdentity } from "./keystore.mjs";
20
+ import { execFile } from "child_process";
21
+ import { existsSync, readFileSync } from "fs";
22
+ import os from "os";
23
+ import path from "path";
24
+ import { createRequire } from "module";
25
25
 
26
26
  // Version read from package.json (single source of truth — matches cli.mjs;
27
27
  // never hardcode, that's what caused the version drift).
28
28
  const VERSION = (() => {
29
- try { return createRequire(import.meta.url)('./package.json').version } catch { return '?' }
30
- })()
29
+ try {
30
+ return createRequire(import.meta.url)("./package.json").version;
31
+ } catch {
32
+ return "?";
33
+ }
34
+ })();
31
35
 
32
36
  // ── ANSI ───────────────────────────────────────────────────────────────────
33
- const ESC = '\x1b['
37
+ const ESC = "\x1b[";
34
38
  const c = {
35
- reset: `${ESC}0m`,
36
- bold: `${ESC}1m`,
37
- dim: `${ESC}2m`,
38
- gold: `${ESC}33m`,
39
- boldGold: `${ESC}1;33m`,
40
- cyan: `${ESC}36m`,
41
- boldCyan: `${ESC}1;36m`,
42
- green: `${ESC}32m`,
43
- red: `${ESC}31m`,
44
- white: `${ESC}97m`,
39
+ reset: `${ESC}0m`,
40
+ bold: `${ESC}1m`,
41
+ dim: `${ESC}2m`,
42
+ gold: `${ESC}33m`,
43
+ boldGold: `${ESC}1;33m`,
44
+ cyan: `${ESC}36m`,
45
+ boldCyan: `${ESC}1;36m`,
46
+ green: `${ESC}32m`,
47
+ red: `${ESC}31m`,
48
+ white: `${ESC}97m`,
45
49
  boldWhite: `${ESC}1;97m`,
46
- magenta: `${ESC}35m`,
47
- blue: `${ESC}34m`,
48
- bgDim: `${ESC}48;5;236m`, // dark grey bg for active tab
49
- bgCyan: `${ESC}48;5;23m`, // dark teal bg for active tab
50
- }
51
- const paint = (col, s) => `${col}${s}${c.reset}`
52
- const bold = (s) => paint(c.bold, s)
53
- const dim = (s) => paint(c.dim, s)
54
- const gold = (s) => paint(c.boldGold, s)
55
- const cyan = (s) => paint(c.boldCyan, s)
56
- const hdr = (s) => paint(c.boldCyan, s) // table column-header label color (readability)
57
- const green = (s) => paint(c.green, s)
58
- const red = (s) => paint(c.red, s)
59
-
60
- const CLEAR = `${ESC}H${ESC}2J` // home + erase visible area
61
- const CLEAR_SB = `${ESC}3J` // clear scrollback/saved lines (prevents scroll-up past TUI)
62
- const ENTER_ALT = `${ESC}?1049h` // enter alternate screen buffer
63
- const EXIT_ALT = `${ESC}?1049l` // exit alternate screen buffer (restores original)
64
- const HIDE = `${ESC}?25l`
65
- const SHOW = `${ESC}?25h`
66
- const UP = (n) => `${ESC}${n}A`
67
- const ERLINE = `${ESC}2K`
68
- const GOTO = (r, col = 1) => `${ESC}${r};${col}H` // absolute cursor position
69
-
70
- const W = () => process.stdout.columns || 100
50
+ magenta: `${ESC}35m`,
51
+ blue: `${ESC}34m`,
52
+ bgDim: `${ESC}48;5;236m`, // dark grey bg for active tab
53
+ bgCyan: `${ESC}48;5;23m`, // dark teal bg for active tab
54
+ };
55
+ const paint = (col, s) => `${col}${s}${c.reset}`;
56
+ const bold = (s) => paint(c.bold, s);
57
+ const dim = (s) => paint(c.dim, s);
58
+ const gold = (s) => paint(c.boldGold, s);
59
+ const cyan = (s) => paint(c.boldCyan, s);
60
+ const hdr = (s) => paint(c.boldCyan, s); // table column-header label color (readability)
61
+ const green = (s) => paint(c.green, s);
62
+ const red = (s) => paint(c.red, s);
63
+
64
+ const CLEAR = `${ESC}H${ESC}2J`; // home + erase visible area
65
+ const CLEAR_SB = `${ESC}3J`; // clear scrollback/saved lines (prevents scroll-up past TUI)
66
+ const ENTER_ALT = `${ESC}?1049h`; // enter alternate screen buffer
67
+ const EXIT_ALT = `${ESC}?1049l`; // exit alternate screen buffer (restores original)
68
+ const HIDE = `${ESC}?25l`;
69
+ const SHOW = `${ESC}?25h`;
70
+ const UP = (n) => `${ESC}${n}A`;
71
+ const ERLINE = `${ESC}2K`;
72
+ const GOTO = (r, col = 1) => `${ESC}${r};${col}H`; // absolute cursor position
73
+
74
+ const W = () => process.stdout.columns || 100;
71
75
  // FIX 0b: safe default 24 (not 40) when rows is undefined (non-TTY / IDE terminals).
72
76
  // A 40-row assumption into a 24-row window scrolls the alt-screen → footer smears off.
73
- const H = () => process.stdout.rows || 24
77
+ const H = () => process.stdout.rows || 24;
74
78
 
75
79
  // ── Screen buffer: collect lines, then paint only what fits ─────────────
76
80
  // Render functions call write/writeln as before; the buffer captures them.
77
81
  // flushScreen() paints to the real terminal using absolute cursor positioning
78
82
  // so the TUI never scrolls — it's a locked frame like tokscale.
79
- let _screenBuf = null // null = direct mode (unbuffered); string[] = buffered
80
- const write = (s) => { if (_screenBuf) { const parts = s.split('\n'); if (parts.length === 1) { _screenBuf[_screenBuf.length-1] += s } else { _screenBuf[_screenBuf.length-1] += parts[0]; for (let i=1;i<parts.length;i++) _screenBuf.push(parts[i]) } } else { process.stdout.write(s) } }
81
- const writeln = (s = '') => { if (_screenBuf) { _screenBuf[_screenBuf.length-1] += s; _screenBuf.push('') } else { process.stdout.write(s + '\n') } }
83
+ let _screenBuf = null; // null = direct mode (unbuffered); string[] = buffered
84
+ const write = (s) => {
85
+ if (_screenBuf) {
86
+ const parts = s.split("\n");
87
+ if (parts.length === 1) {
88
+ _screenBuf[_screenBuf.length - 1] += s;
89
+ } else {
90
+ _screenBuf[_screenBuf.length - 1] += parts[0];
91
+ for (let i = 1; i < parts.length; i++) _screenBuf.push(parts[i]);
92
+ }
93
+ } else {
94
+ process.stdout.write(s);
95
+ }
96
+ };
97
+ const writeln = (s = "") => {
98
+ if (_screenBuf) {
99
+ _screenBuf[_screenBuf.length - 1] += s;
100
+ _screenBuf.push("");
101
+ } else {
102
+ process.stdout.write(s + "\n");
103
+ }
104
+ };
82
105
 
83
- let _footerBuf = null // footer lines pinned to bottom
84
- function startBuffer() { _screenBuf = ['']; _footerBuf = null }
85
- function setFooter(lines) { _footerBuf = lines }
106
+ let _footerBuf = null; // footer lines pinned to bottom
107
+ function startBuffer() {
108
+ _screenBuf = [""];
109
+ _footerBuf = null;
110
+ }
111
+ function setFooter(lines) {
112
+ _footerBuf = lines;
113
+ }
86
114
  function flushScreen() {
87
- if (!_screenBuf) return
88
- const lines = _screenBuf
89
- const footer = _footerBuf || []
90
- _screenBuf = null
91
- _footerBuf = null
92
- const h = H()
93
- const w = W()
115
+ if (!_screenBuf) return;
116
+ const lines = _screenBuf;
117
+ const footer = _footerBuf || [];
118
+ _screenBuf = null;
119
+ _footerBuf = null;
120
+ const h = H();
121
+ const w = W();
94
122
  // FIX 0b: footer is SACROSANCT. Clamp CONTENT to h - footer.length first, then
95
123
  // always append every footer line. The footer must NEVER be the thing dropped
96
124
  // (it was — footer painted last, clamp cut the tail = footer first → lost hints
97
125
  // + lost lower cascade rows, together, because they're the same tail of the frame).
98
- const contentRows = Math.max(0, h - footer.length)
99
- const content = lines.slice(0, contentRows)
100
- const frame = [...content, ...footer]
101
- const maxRows = Math.min(frame.length, h)
102
- let out = ''
126
+ const contentRows = Math.max(0, h - footer.length);
127
+ const content = lines.slice(0, contentRows);
128
+ const frame = [...content, ...footer];
129
+ const maxRows = Math.min(frame.length, h);
130
+ let out = "";
103
131
  for (let i = 0; i < maxRows; i++) {
104
- out += GOTO(i + 1) + ERLINE + ansiTrunc(frame[i], w)
132
+ out += GOTO(i + 1) + ERLINE + ansiTrunc(frame[i], w);
105
133
  }
106
134
  // Clear any rows below the frame left over from a taller previous frame.
107
135
  for (let i = maxRows; i < h; i++) {
108
- out += GOTO(i + 1) + ERLINE
136
+ out += GOTO(i + 1) + ERLINE;
109
137
  }
110
- process.stdout.write(out)
138
+ process.stdout.write(out);
111
139
  }
112
140
 
113
- function stripAnsi(s) { return s.replace(/\x1b\[[0-9;]*m/g, '') }
141
+ function stripAnsi(s) {
142
+ return s.replace(/\x1b\[[0-9;]*m/g, "");
143
+ }
114
144
  // ANSI-aware truncate: cut to `w` VISIBLE columns while keeping color escapes
115
145
  // intact (escapes don't count toward width, and we never slice mid-sequence).
116
146
  // Replaces raw `.slice(0, w)`, which corrupts color codes + miscounts width.
117
147
  function ansiTrunc(s, w) {
118
- if (stripAnsi(s).length <= w) return s
119
- let out = '', vis = 0, i = 0
148
+ if (stripAnsi(s).length <= w) return s;
149
+ let out = "",
150
+ vis = 0,
151
+ i = 0;
120
152
  while (i < s.length && vis < w) {
121
- if (s[i] === '\x1b') {
122
- const m = s.slice(i).match(/^\x1b\[[0-9;]*m/)
123
- if (m) { out += m[0]; i += m[0].length; continue }
153
+ if (s[i] === "\x1b") {
154
+ const m = s.slice(i).match(/^\x1b\[[0-9;]*m/);
155
+ if (m) {
156
+ out += m[0];
157
+ i += m[0].length;
158
+ continue;
159
+ }
124
160
  }
125
- out += s[i]; vis++; i++
161
+ out += s[i];
162
+ vis++;
163
+ i++;
126
164
  }
127
- return out + '\x1b[0m' // reset so a cut mid-color doesn't bleed into next line
165
+ return out + "\x1b[0m"; // reset so a cut mid-color doesn't bleed into next line
166
+ }
167
+ function padEnd(s, w) {
168
+ const v = stripAnsi(s).length;
169
+ return v >= w ? s : s + " ".repeat(w - v);
170
+ }
171
+ function padStart(s, w) {
172
+ const v = stripAnsi(s).length;
173
+ return v >= w ? s : " ".repeat(w - v) + s;
174
+ }
175
+ function trunc(s, w) {
176
+ return stripAnsi(s).length <= w ? s : s.slice(0, w - 1) + "…";
177
+ }
178
+ function hr(ch = "─") {
179
+ return dim(ch.repeat(Math.max(0, W() - 4)));
128
180
  }
129
- function padEnd(s, w) { const v = stripAnsi(s).length; return v >= w ? s : s + ' '.repeat(w - v) }
130
- function padStart(s,w) { const v = stripAnsi(s).length; return v >= w ? s : ' '.repeat(w - v) + s }
131
- function trunc(s, w) { return stripAnsi(s).length <= w ? s : s.slice(0, w - 1) + '…' }
132
- function hr(ch = '─') { return dim(ch.repeat(Math.max(0, W() - 4))) }
133
181
 
134
182
  // ── Number formatters ───────────────────────────────────────────────────────
135
- const fmtY = (y) => y == null ? '—' : y >= 10000 ? `${(y/1000).toFixed(1)}K` : y >= 1000 ? `${(y/1000).toFixed(2)}K` : y.toFixed(1)
136
- const fmtLev = (l) => l == null ? '—' : l >= 1000 ? `${(l/1000).toFixed(1)}K` : l.toFixed(0)
137
- const fmtSNR = (n) => n == null ? '—' : `${(n*100).toFixed(1)}%`
138
- const fmtTok = (n) => n == null ? '—' : n >= 1e9 ? `${(n/1e9).toFixed(1)}B` : n >= 1e6 ? `${(n/1e6).toFixed(1)}M` : n >= 1e3 ? `${(n/1e3).toFixed(1)}K` : String(n)
139
- const fmtMov = (n) => (n == null || n === 0) ? dim('—') : n > 0 ? green(`+${n}`) : red(`${n}`)
183
+ const fmtY = (y) =>
184
+ y == null
185
+ ? "—"
186
+ : y >= 10000
187
+ ? `${(y / 1000).toFixed(1)}K`
188
+ : y >= 1000
189
+ ? `${(y / 1000).toFixed(2)}K`
190
+ : y.toFixed(1);
191
+ const fmtLev = (l) =>
192
+ l == null ? "—" : l >= 1000 ? `${(l / 1000).toFixed(1)}K` : l.toFixed(0);
193
+ const fmtSNR = (n) => (n == null ? "—" : `${(n * 100).toFixed(1)}%`);
194
+ const fmtTok = (n) =>
195
+ n == null
196
+ ? "—"
197
+ : n >= 1e9
198
+ ? `${(n / 1e9).toFixed(1)}B`
199
+ : n >= 1e6
200
+ ? `${(n / 1e6).toFixed(1)}M`
201
+ : n >= 1e3
202
+ ? `${(n / 1e3).toFixed(1)}K`
203
+ : String(n);
204
+ const fmtMov = (n) =>
205
+ n == null || n === 0 ? dim("—") : n > 0 ? green(`+${n}`) : red(`${n}`);
140
206
 
141
207
  // ── Class tier colors ────────────────────────────────────────────────────────
142
208
  const CLS = {
143
- TRANSMITTER: (s) => paint(c.boldGold, s),
144
- 'ARCH+': (s) => paint(c.boldCyan, s),
145
- ARCH: (s) => paint(c.cyan, s),
146
- POWER: (s) => paint(c.boldWhite, s),
147
- BASE: (s) => paint(c.white, s),
148
- SEEKER: (s) => paint(c.magenta, s),
149
- REFINER: (s) => paint(c.blue, s),
150
- BEARER: (s) => paint(c.dim, s),
151
- IGNITER: (s) => paint(c.dim, s),
152
- }
153
- const colorCls = (cls) => (CLS[cls] ?? ((s) => s))(cls)
209
+ TRANSMITTER: (s) => paint(c.boldGold, s),
210
+ "ARCH+": (s) => paint(c.boldCyan, s),
211
+ ARCH: (s) => paint(c.cyan, s),
212
+ POWER: (s) => paint(c.boldWhite, s),
213
+ BASE: (s) => paint(c.white, s),
214
+ SEEKER: (s) => paint(c.magenta, s),
215
+ REFINER: (s) => paint(c.blue, s),
216
+ BEARER: (s) => paint(c.dim, s),
217
+ IGNITER: (s) => paint(c.dim, s),
218
+ };
219
+ const colorCls = (cls) => (CLS[cls] ?? ((s) => s))(cls);
154
220
 
155
221
  // ── Cascade math (inline, no dep) ───────────────────────────────────────────
156
222
  function cascadeFrom(p) {
157
- if (!p) return null
158
- const i = p.input ?? 0, o = p.output ?? 0, cr = p.cacheRead ?? 0
159
- if (i === 0 || o === 0) return null
160
- const leverage = cr / i
161
- const velocity = o / i
162
- const yld = leverage * velocity
163
- const dev10x = (i > 0 && o > 0 && (p.cacheCreate ?? 0) > 0 && cr > 0)
164
- ? Math.log10(cr / i) : null
165
- const snr = o / (i + o)
166
-
167
- let cls = 'IGNITER'
168
- if (yld >= 1000 || dev10x >= 3) cls = 'TRANSMITTER'
169
- else if (dev10x != null && dev10x >= 1.45) cls = 'ARCH+'
170
- else if (dev10x != null && dev10x >= 1.35) cls = 'ARCH'
171
- else if (dev10x != null && dev10x >= 1.2) cls = 'POWER'
172
- else if (dev10x != null && dev10x >= 1.0) cls = 'BASE'
173
- else if (dev10x != null && dev10x >= 0) cls = 'SEEKER'
174
- else if (dev10x != null && dev10x >= -0.3) cls = 'REFINER'
175
-
176
- return { yield: yld, snr, leverage, velocity, dev10x, class: cls }
223
+ if (!p) return null;
224
+ const i = p.input ?? 0,
225
+ o = p.output ?? 0,
226
+ cr = p.cacheRead ?? 0;
227
+ if (i === 0 || o === 0) return null;
228
+ const leverage = cr / i;
229
+ const velocity = o / i;
230
+ const yld = leverage * velocity;
231
+ const dev10x =
232
+ i > 0 && o > 0 && (p.cacheCreate ?? 0) > 0 && cr > 0
233
+ ? Math.log10(cr / i)
234
+ : null;
235
+ const snr = o / (i + o);
236
+
237
+ let cls = "IGNITER";
238
+ if (yld >= 1000 || dev10x >= 3) cls = "TRANSMITTER";
239
+ else if (dev10x != null && dev10x >= 1.45) cls = "ARCH+";
240
+ else if (dev10x != null && dev10x >= 1.35) cls = "ARCH";
241
+ else if (dev10x != null && dev10x >= 1.2) cls = "POWER";
242
+ else if (dev10x != null && dev10x >= 1.0) cls = "BASE";
243
+ else if (dev10x != null && dev10x >= 0) cls = "SEEKER";
244
+ else if (dev10x != null && dev10x >= -0.3) cls = "REFINER";
245
+
246
+ return { yield: yld, snr, leverage, velocity, dev10x, class: cls };
177
247
  }
178
248
 
179
249
  // ── Unicode bar chart (no dep) ───────────────────────────────────────────────
180
- const BLOCKS = ' ▏▎▍▌▋▊▉█'
250
+ const BLOCKS = " ▏▎▍▌▋▊▉█";
181
251
 
182
252
  // Linear bar — use when all values are the same order of magnitude.
183
253
  function barChart(values, labels, opts = {}) {
184
- const { width = 30, colorFn = (s) => s, maxVal } = opts
185
- const max = maxVal ?? Math.max(...values.filter(Number.isFinite), 1)
186
- const lines = []
254
+ const { width = 30, colorFn = (s) => s, maxVal } = opts;
255
+ const max = maxVal ?? Math.max(...values.filter(Number.isFinite), 1);
256
+ const lines = [];
187
257
  for (let i = 0; i < values.length; i++) {
188
- const v = values[i] ?? 0
189
- const pct = Math.min(v / max, 1)
190
- const full = Math.floor(pct * width)
191
- const frac = Math.floor((pct * width - full) * 8)
192
- const bar = colorFn('█'.repeat(full) + (frac > 0 ? BLOCKS[frac] : ''))
193
- const lbl = padEnd(dim(labels[i] ?? ''), 10)
194
- const val = padStart(fmtTok(v), 8)
195
- lines.push(` ${lbl} ${padEnd(bar, width)} ${val}`)
258
+ const v = values[i] ?? 0;
259
+ const pct = Math.min(v / max, 1);
260
+ const full = Math.floor(pct * width);
261
+ const frac = Math.floor((pct * width - full) * 8);
262
+ const bar = colorFn("█".repeat(full) + (frac > 0 ? BLOCKS[frac] : ""));
263
+ const lbl = padEnd(dim(labels[i] ?? ""), 10);
264
+ const val = padStart(fmtTok(v), 8);
265
+ lines.push(` ${lbl} ${padEnd(bar, width)} ${val}`);
196
266
  }
197
- return lines
267
+ return lines;
198
268
  }
199
269
 
200
270
  // Log-scale bar — use when values span multiple orders of magnitude (e.g. token pillars
201
271
  // where cacheRead >> input). Maps log10(v) to bar width so each 10x = same visual step.
202
272
  // minLog floor prevents zero/tiny values from going negative.
203
273
  function logBar(v, maxLog, width = 40, colorCode = c.cyan) {
204
- if (!v || v <= 0) return { bar: dim('·'.repeat(width)), pct: 0 }
205
- const log = Math.log10(v)
206
- const pct = Math.min(log / maxLog, 1)
207
- const full = Math.floor(pct * width)
208
- const frac = Math.floor((pct * width - full) * 8)
209
- const bar = paint(colorCode, '█'.repeat(full) + (frac > 0 ? BLOCKS[frac] : ''))
210
- return { bar, pct }
274
+ if (!v || v <= 0) return { bar: dim("·".repeat(width)), pct: 0 };
275
+ const log = Math.log10(v);
276
+ const pct = Math.min(log / maxLog, 1);
277
+ const full = Math.floor(pct * width);
278
+ const frac = Math.floor((pct * width - full) * 8);
279
+ const bar = paint(
280
+ colorCode,
281
+ "█".repeat(full) + (frac > 0 ? BLOCKS[frac] : ""),
282
+ );
283
+ return { bar, pct };
211
284
  }
212
285
 
213
286
  // ── Sparkline (no dep) ────────────────────────────────────────────────────────
214
- const SPARK = '▁▂▃▄▅▆▇█'
287
+ const SPARK = "▁▂▃▄▅▆▇█";
215
288
  function sparkline(values) {
216
- const valid = values.filter(Number.isFinite)
217
- if (valid.length === 0) return dim('no data')
218
- const min = Math.min(...valid), max = Math.max(...valid)
219
- return values.map(v => {
220
- if (!Number.isFinite(v)) return dim('·')
221
- const idx = max === min ? 7 : Math.round(((v - min) / (max - min)) * 7)
222
- return SPARK[idx]
223
- }).join('')
289
+ const valid = values.filter(Number.isFinite);
290
+ if (valid.length === 0) return dim("no data");
291
+ const min = Math.min(...valid),
292
+ max = Math.max(...valid);
293
+ return values
294
+ .map((v) => {
295
+ if (!Number.isFinite(v)) return dim("·");
296
+ const idx = max === min ? 7 : Math.round(((v - min) / (max - min)) * 7);
297
+ return SPARK[idx];
298
+ })
299
+ .join("");
224
300
  }
225
301
 
226
302
  // ── Data sources ─────────────────────────────────────────────────────────────
@@ -233,35 +309,51 @@ function sparkline(values) {
233
309
 
234
310
  async function loadDashboardData() {
235
311
  const boardPromise = Promise.race([
236
- fetch(`${DEFAULT_API_BASE}/api/v1/leaderboard?window=30d&metric=yield_`, { headers: { accept: 'application/json' } })
237
- .then(r => r.ok ? r.json() : null).catch(() => null),
238
- new Promise(r => setTimeout(() => r(null), 5000)),
239
- ])
312
+ fetch(`${DEFAULT_API_BASE}/api/v1/leaderboard?window=30d&metric=yield_`, {
313
+ headers: { accept: "application/json" },
314
+ })
315
+ .then((r) => (r.ok ? r.json() : null))
316
+ .catch(() => null),
317
+ new Promise((r) => setTimeout(() => r(null), 5000)),
318
+ ]);
240
319
 
241
320
  // FIX 0 + unify (0.14): progressive load via the SHARED loader (pullActivePlatforms) — paint the
242
321
  // primary platform (claude) FAST, then fill the rest in fillDashboardRest. This is the SAME loader
243
322
  // `me` and `watch` use, so the three views can't drift apart. (Verifier comparison still runs
244
323
  // FRESH + on-demand in the Compare tab — see loadCompareData — never on this Dashboard path.)
245
- const active = await pullActivePlatforms({ platforms: ['claude'] }).catch(() => [])
246
-
247
- const boardData = await boardPromise
248
- return { active, verifierMap: {}, tdPillars: null, boardData, _loading: true, _remaining: ALL_PLATFORMS.filter(p => p !== 'claude') }
324
+ const active = await pullActivePlatforms({ platforms: ["claude"] }).catch(
325
+ () => [],
326
+ );
327
+
328
+ const boardData = await boardPromise;
329
+ return {
330
+ active,
331
+ verifierMap: {},
332
+ tdPillars: null,
333
+ boardData,
334
+ _loading: true,
335
+ _remaining: ALL_PLATFORMS.filter((p) => p !== "claude"),
336
+ };
249
337
  }
250
338
 
251
339
  /** FIX 0: fill the remaining platforms after the primary render. Mutates dashData.active. */
252
340
  async function fillDashboardRest(dashData) {
253
- if (!dashData?._remaining) return false
341
+ if (!dashData?._remaining) return false;
254
342
  // Unify: fill the remaining platforms through the SAME shared loader.
255
- const rest = await pullActivePlatforms({ platforms: dashData._remaining }).catch(() => [])
343
+ const rest = await pullActivePlatforms({
344
+ platforms: dashData._remaining,
345
+ }).catch(() => []);
256
346
  for (const d of rest) {
257
- if (!dashData.active.find(a => a.platform === d.platform)) dashData.active.push(d)
347
+ if (!dashData.active.find((a) => a.platform === d.platform))
348
+ dashData.active.push(d);
258
349
  }
259
350
  // stable display order: claude → codex → rest (matches the shared loader)
260
- const rank = (p) => (p === 'claude' ? -2 : p === 'codex' ? -1 : ALL_PLATFORMS.indexOf(p))
261
- dashData.active.sort((a, b) => rank(a.platform) - rank(b.platform))
262
- dashData._loading = false
263
- dashData._remaining = null
264
- return true
351
+ const rank = (p) =>
352
+ p === "claude" ? -2 : p === "codex" ? -1 : ALL_PLATFORMS.indexOf(p);
353
+ dashData.active.sort((a, b) => rank(a.platform) - rank(b.platform));
354
+ dashData._loading = false;
355
+ dashData._remaining = null;
356
+ return true;
265
357
  }
266
358
 
267
359
  // #9 / C1 (2026-06-27): Compare pulls the three external verifier sources FRESH + on-demand
@@ -269,96 +361,126 @@ async function fillDashboardRest(dashData) {
269
361
  // NOT the old stale snapshot readers (static tokscale_report.json / 3-day-old db). This runs ONLY
270
362
  // when the Compare tab opens (it's a 5–60s scan), never on the Dashboard load path. Scoped to the
271
363
  // current platform. tokenpull (the canonical board/cascade source) stays the fast/fresh tpData.
272
- async function loadCompareData(platform = 'claude') {
273
- const tpData = await callTool('tokenpull', { platform }).catch(() => null)
364
+ async function loadCompareData(platform = "claude") {
365
+ const tpData = await callTool("tokenpull", { platform }).catch(() => null);
274
366
  // Fresh pull — { ccusage, tokscale, tokendash }, each null or window-keyed pillars.
275
- const fresh = await freshVerifierPillars(platform).catch(() => ({ ccusage: null, tokscale: null, tokendash: null }))
276
- return { tpData, cc: fresh.ccusage, ts: fresh.tokscale, td: fresh.tokendash, platform }
367
+ const fresh = await freshVerifierPillars(platform).catch(() => ({
368
+ ccusage: null,
369
+ tokscale: null,
370
+ tokendash: null,
371
+ }));
372
+ return {
373
+ tpData,
374
+ cc: fresh.ccusage,
375
+ ts: fresh.tokscale,
376
+ td: fresh.tokendash,
377
+ platform,
378
+ };
277
379
  }
278
380
 
279
381
  // #8 (2026-06-27): the Board tab shows ALL SUBMISSIONS ranked (raw submission rows), not operator
280
382
  // aggregates. Try the NEW GET /api/v1/submissions first; if it's not deployed yet (non-200 / 404),
281
383
  // FALL BACK to the existing /api/v1/leaderboard so nothing breaks pre-deploy. The return is tagged
282
384
  // with `_source` ('submissions' | 'leaderboard') so renderBoard picks the right column layout.
283
- async function loadBoardData(window = '30d') {
385
+ async function loadBoardData(window = "30d") {
284
386
  // Primary: submissions endpoint (ranked submission rows).
285
387
  try {
286
- const res = await fetch(`${DEFAULT_API_BASE}/api/v1/submissions?window=${window}&metric=yield_`, {
287
- headers: { accept: 'application/json' },
288
- })
388
+ const res = await fetch(
389
+ `${DEFAULT_API_BASE}/api/v1/submissions?window=${window}&metric=yield_`,
390
+ {
391
+ headers: { accept: "application/json" },
392
+ },
393
+ );
289
394
  if (res.ok) {
290
- const data = await res.json()
291
- return { ...data, _source: 'submissions' }
395
+ const data = await res.json();
396
+ return { ...data, _source: "submissions" };
292
397
  }
293
- } catch { /* fall through to leaderboard */ }
398
+ } catch {
399
+ /* fall through to leaderboard */
400
+ }
294
401
  // Fallback: existing leaderboard endpoint + current render (pre-deploy safety).
295
402
  try {
296
- const res = await fetch(`${DEFAULT_API_BASE}/api/v1/leaderboard?window=${window}&metric=yield_`, {
297
- headers: { accept: 'application/json' },
298
- })
299
- if (!res.ok) return null
300
- const data = await res.json()
301
- return { ...data, _source: 'leaderboard' }
302
- } catch { return null }
403
+ const res = await fetch(
404
+ `${DEFAULT_API_BASE}/api/v1/leaderboard?window=${window}&metric=yield_`,
405
+ {
406
+ headers: { accept: "application/json" },
407
+ },
408
+ );
409
+ if (!res.ok) return null;
410
+ const data = await res.json();
411
+ return { ...data, _source: "leaderboard" };
412
+ } catch {
413
+ return null;
414
+ }
303
415
  }
304
416
 
305
417
  // ── TAB BAR ──────────────────────────────────────────────────────────────────
306
418
  const TABS = [
307
- { key: '1', label: 'Dashboard', short: 'D' },
308
- { key: '2', label: 'Trends', short: 'T' }, // every metric across windows (sub-views: You/Platform/Field)
309
- { key: '3', label: 'Compare', short: 'C' },
310
- { key: '4', label: 'Board', short: 'B' },
311
- { key: '5', label: 'Watch', short: 'W' }, // in-TUI landing panel; [Enter] launches the watcher
312
- { key: '6', label: 'Connect', short: 'N' }, // sign in / switch device — the whole app is the TUI
313
- ]
419
+ { key: "1", label: "Dashboard", short: "D" },
420
+ { key: "2", label: "Trends", short: "T" }, // every metric across windows (sub-views: You/Platform/Field)
421
+ { key: "3", label: "Compare", short: "C" },
422
+ { key: "4", label: "Board", short: "B" },
423
+ { key: "5", label: "Watch", short: "W" }, // in-TUI landing panel; [Enter] launches the watcher
424
+ { key: "6", label: "Connect", short: "N" }, // sign in / switch device — the whole app is the TUI
425
+ ];
314
426
 
315
427
  // Three Degrees of Leverage — reference values pulled from signalaf.com/wiki (2026-06-25).
316
428
  // base = AI avg (modeled 7:2:1 baseline) · field = SigRank avg (wild field median) · top = best operator.
317
429
  // STATIC for now; goes live (field/top computed from the board) once there's real user volume.
318
430
  const TD = {
319
- base: { yield: 1.57, snr: 0.33, vel: 0.50, lev: 3.2, d10: 0.50 },
320
- field: { yield: 1.51, snr: 0.07, vel: 0.08, lev: 22.3, d10: 1.35 },
321
- top: { yield: 745.40, snr: 0.63, vel: 1.70, lev: 438.6, d10: 2.64 },
322
- }
431
+ base: { yield: 1.57, snr: 0.33, vel: 0.5, lev: 3.2, d10: 0.5 },
432
+ field: { yield: 1.51, snr: 0.07, vel: 0.08, lev: 22.3, d10: 1.35 },
433
+ top: { yield: 745.4, snr: 0.63, vel: 1.7, lev: 438.6, d10: 2.64 },
434
+ };
323
435
 
324
436
  function renderTabBar(activeIdx) {
325
- const w = W()
326
- const ts = new Date().toLocaleTimeString('en-US', { hour12: false })
437
+ const w = W();
438
+ const ts = new Date().toLocaleTimeString("en-US", { hour12: false });
327
439
 
328
440
  // logo
329
- const logo = ` ${gold('⊙ SigRank')}`
441
+ const logo = ` ${gold("⊙ SigRank")}`;
330
442
  // tabs
331
443
  const tabStr = TABS.map((t, i) => {
332
- const lbl = ` ${t.key}:${t.label} `
444
+ const lbl = ` ${t.key}:${t.label} `;
333
445
  return i === activeIdx
334
446
  ? `${c.bgCyan}${c.boldCyan}${lbl}${c.reset}`
335
- : `${c.bgDim}${c.dim}${lbl}${c.reset}`
336
- }).join('')
447
+ : `${c.bgDim}${c.dim}${lbl}${c.reset}`;
448
+ }).join("");
337
449
  // right side — version (single source of truth) · site · clock
338
- const right = dim(`${gold('v' + VERSION)}${c.reset}${c.dim} · signalaf.com · ${ts}`)
450
+ const right = dim(
451
+ `${gold("v" + VERSION)}${c.reset}${c.dim} · signalaf.com · ${ts}`,
452
+ );
339
453
 
340
- const logoVis = stripAnsi(logo).length
341
- const tabsVis = TABS.reduce((a, t) => a + t.label.length + 4, 0)
342
- const rightVis = stripAnsi(right).length
343
- const gap = Math.max(1, w - logoVis - tabsVis - rightVis - 2)
454
+ const logoVis = stripAnsi(logo).length;
455
+ const tabsVis = TABS.reduce((a, t) => a + t.label.length + 4, 0);
456
+ const rightVis = stripAnsi(right).length;
457
+ const gap = Math.max(1, w - logoVis - tabsVis - rightVis - 2);
344
458
 
345
- writeln(`${logo} ${tabStr}${' '.repeat(gap)}${right}`)
346
- writeln(` ${hr()}`)
459
+ writeln(`${logo} ${tabStr}${" ".repeat(gap)}${right}`);
460
+ writeln(` ${hr()}`);
347
461
  }
348
462
 
349
463
  // ── GRAPHICS helpers ─────────────────────────────────────────────────────────
350
464
 
351
465
  // Horizontal stacked bar: input | cacheW | cacheR | output
352
466
  function tokenBar(p, width = 40) {
353
- if (!p) return dim(' no data')
354
- const total = (p.input ?? 0) + (p.output ?? 0) + (p.cacheCreate ?? 0) + (p.cacheRead ?? 0)
355
- if (total === 0) return dim(' no data')
467
+ if (!p) return dim(" no data");
468
+ const total =
469
+ (p.input ?? 0) +
470
+ (p.output ?? 0) +
471
+ (p.cacheCreate ?? 0) +
472
+ (p.cacheRead ?? 0);
473
+ if (total === 0) return dim(" no data");
356
474
  const seg = (val, colorCode) => {
357
- const w = Math.round((val / total) * width)
358
- return w > 0 ? paint(colorCode, '█'.repeat(w)) : ''
359
- }
360
- const bar = seg(p.input, c.cyan) + seg(p.cacheCreate, c.blue) + seg(p.cacheRead, c.boldGold) + seg(p.output, c.green)
361
- return bar
475
+ const w = Math.round((val / total) * width);
476
+ return w > 0 ? paint(colorCode, "█".repeat(w)) : "";
477
+ };
478
+ const bar =
479
+ seg(p.input, c.cyan) +
480
+ seg(p.cacheCreate, c.blue) +
481
+ seg(p.cacheRead, c.boldGold) +
482
+ seg(p.output, c.green);
483
+ return bar;
362
484
  }
363
485
 
364
486
  // Generalized cascade-metric sparkline across windows (7d→30d→90d→all).
@@ -367,34 +489,43 @@ function tokenBar(p, width = 40) {
367
489
  // renderCompare's column layout). metricSpark/yieldSpark now return sparkline + RIGHT-
368
490
  // ALIGNED values in fixed COL_W columns — NO per-value `w:` prefix (the header row
369
491
  // carries the window labels). Null windows render a dim `—` (columns stay aligned).
370
- const TREND_COL_W = 10
371
- const TREND_WINS = ['7d', '30d', '90d', 'all']
492
+ const TREND_COL_W = 10;
493
+ const TREND_WINS = ["7d", "30d", "90d", "all"];
372
494
 
373
495
  function trendHeader() {
374
- return padEnd(dim(''), 12) + TREND_WINS.map(w => padStart(dim(w), TREND_COL_W)).join(' ')
496
+ return (
497
+ padEnd(dim(""), 12) +
498
+ TREND_WINS.map((w) => padStart(dim(w), TREND_COL_W)).join(" ")
499
+ );
375
500
  }
376
501
 
377
502
  function metricSpark(d, pick, fmt) {
378
- const vals = TREND_WINS.map(w => {
379
- const wd = d.windows?.find(x => x.window === w)
380
- if (!wd) return null
381
- const cas = cascadeFrom(wd.pillars)
382
- return cas ? pick(cas, wd.pillars) : null // pick gets (cascade, pillars)
383
- })
384
- const cols = vals.map(v => v != null ? padStart(fmt(v), TREND_COL_W) : padStart(dim('—'), TREND_COL_W))
385
- return sparkline(vals) + ` ${cols.join(' ')}`
503
+ const vals = TREND_WINS.map((w) => {
504
+ const wd = d.windows?.find((x) => x.window === w);
505
+ if (!wd) return null;
506
+ const cas = cascadeFrom(wd.pillars);
507
+ return cas ? pick(cas, wd.pillars) : null; // pick gets (cascade, pillars)
508
+ });
509
+ const cols = vals.map((v) =>
510
+ v != null ? padStart(fmt(v), TREND_COL_W) : padStart(dim("—"), TREND_COL_W),
511
+ );
512
+ return sparkline(vals) + ` ${cols.join(" ")}`;
386
513
  }
387
514
 
388
515
  // Yield sparkline across windows
389
516
  function yieldSpark(d) {
390
- const vals = TREND_WINS.map(w => {
391
- const wd = d.windows?.find(x => x.window === w)
392
- if (!wd) return null
393
- const cas = cascadeFrom(wd.pillars)
394
- return cas?.yield ?? null
395
- })
396
- const cols = vals.map(v => v != null ? padStart(fmtY(v), TREND_COL_W) : padStart(dim('—'), TREND_COL_W))
397
- return sparkline(vals) + ` ${cols.join(' ')}`
517
+ const vals = TREND_WINS.map((w) => {
518
+ const wd = d.windows?.find((x) => x.window === w);
519
+ if (!wd) return null;
520
+ const cas = cascadeFrom(wd.pillars);
521
+ return cas?.yield ?? null;
522
+ });
523
+ const cols = vals.map((v) =>
524
+ v != null
525
+ ? padStart(fmtY(v), TREND_COL_W)
526
+ : padStart(dim("—"), TREND_COL_W),
527
+ );
528
+ return sparkline(vals) + ` ${cols.join(" ")}`;
398
529
  }
399
530
 
400
531
  // ── TAB 1: DASHBOARD ─────────────────────────────────────────────────────────
@@ -406,200 +537,253 @@ function yieldSpark(d) {
406
537
  // scroll" text (A3) which implied scroll existed but didn't (alt-screen locked
407
538
  // frame = no terminal scroll). The scroll is IN-SECTION only, on the Dashboard tab.
408
539
 
409
- const DASH_WINS = ['7d', '30d', '90d', 'all']
540
+ const DASH_WINS = ["7d", "30d", "90d", "all"];
410
541
 
411
542
  // Count how many cascade rows the active platforms would produce (every window
412
543
  // with data across all active platforms). This is the "total wanted" count.
413
544
  function cascadeScrollableCount(active) {
414
- return active.reduce((n, d) =>
415
- n + DASH_WINS.filter(wk => {
416
- const wd = d.windows?.find(x => x.window === wk)
417
- return wd && (wd.pillars.input + wd.pillars.output) > 0
418
- }).length, 0)
545
+ return active.reduce(
546
+ (n, d) =>
547
+ n +
548
+ DASH_WINS.filter((wk) => {
549
+ const wd = d.windows?.find((x) => x.window === wk);
550
+ return wd && wd.pillars.input + wd.pillars.output > 0;
551
+ }).length,
552
+ 0,
553
+ );
419
554
  }
420
555
 
421
556
  // Compute the max cascade rows that fit in the current terminal height budget.
422
557
  // Mirrors the calculation in renderDashboard (budget - used - sectionsBelow).
423
558
  function maxCascadeRowsFor(active) {
424
- const budget = H() - 4 // 2 tab bar + 2 footer
425
- const platformCount = active.length
426
- const barLines = 4 + platformCount + (platformCount > 1 ? 1 : 0)
427
- const boardLines = 17
428
- const sectionsBelow = barLines + boardLines
559
+ const budget = H() - 4; // 2 tab bar + 2 footer
560
+ const platformCount = active.length;
561
+ const barLines = 4 + platformCount + (platformCount > 1 ? 1 : 0);
562
+ const boardLines = 17;
563
+ const sectionsBelow = barLines + boardLines;
429
564
  // used before cascade = header(2) + blank(1) + title(1) + blank(1) + col-header(1) + separator(1) = 7
430
- const usedBeforeCascade = 7
431
- return Math.max(8, budget - usedBeforeCascade - sectionsBelow)
565
+ const usedBeforeCascade = 7;
566
+ return Math.max(8, budget - usedBeforeCascade - sectionsBelow);
432
567
  }
433
568
 
434
- function renderDashboard(data, status = '', scrollOffset = 0) {
435
- const w = W()
436
- const budget = H() - 4 // 2 tab bar + 2 footer
437
- const WINS = ['7d', '30d', '90d', 'all']
438
- let used = 0
439
- const emit = (s = '') => { if (used < budget) { writeln(s); used++ } }
569
+ function renderDashboard(data, status = "", scrollOffset = 0) {
570
+ const w = W();
571
+ const budget = H() - 4; // 2 tab bar + 2 footer
572
+ const WINS = ["7d", "30d", "90d", "all"];
573
+ let used = 0;
574
+ const emit = (s = "") => {
575
+ if (used < budget) {
576
+ writeln(s);
577
+ used++;
578
+ }
579
+ };
440
580
 
441
581
  // A2 (2026-06-27): tolerate null/empty dashData so the FIRST paint can happen BEFORE the
442
582
  // initial load resolves (loadAll renders an empty frame + "reading your cascade…" immediately,
443
583
  // then re-renders with real data). Guard the destructure: no data → header + status + return.
444
584
  if (!data || !Array.isArray(data.active)) {
445
- emit()
446
- emit(` ${bold('Your Cascade')} ${dim('all platforms · all windows')}`)
447
- emit()
448
- emit(` ${dim(' reading token logs… (press [R] to refresh)')}`)
449
- if (status && used < budget) emit(` ${dim(status)}`)
450
- return
585
+ emit();
586
+ emit(` ${bold("Your Cascade")} ${dim("all platforms · all windows")}`);
587
+ emit();
588
+ emit(` ${dim(" reading token logs… (press [R] to refresh)")}`);
589
+ if (status && used < budget) emit(` ${dim(status)}`);
590
+ return;
451
591
  }
452
592
 
453
- const { active } = data
593
+ const { active } = data;
454
594
 
455
595
  // Responsive: the full 12-col table is ~124 wide. On terminals narrower than
456
596
  // that, drop the two derived columns (Vel, 10x) and tighten the inter-column
457
597
  // gap from 2 spaces to 1, so the core pillars + Υ/SNR/Lev/Class still fit.
458
- const narrow = w < 124
459
- const gap = narrow ? ' ' : ' '
598
+ const narrow = w < 124;
599
+ const gap = narrow ? " " : " ";
460
600
  const renderRow = (label, colorFn, winKey, p, est = false) => {
461
- const cas = cascadeFrom(p)
462
- if (!cas) return
463
- const clsFn = CLS[cas.class] ?? ((s) => s)
601
+ const cas = cascadeFrom(p);
602
+ if (!cas) return;
603
+ const clsFn = CLS[cas.class] ?? ((s) => s);
464
604
  const cols = [
465
- padEnd(colorFn(label), 12), padEnd(dim(winKey), 5),
466
- padStart(est ? dim('~')+fmtTok(p.input) : fmtTok(p.input), 8),
605
+ padEnd(colorFn(label), 12),
606
+ padEnd(dim(winKey), 5),
607
+ padStart(est ? dim("~") + fmtTok(p.input) : fmtTok(p.input), 8),
467
608
  padStart(fmtTok(p.output), 8),
468
- padStart((p.cacheCreate??0) > 0 ? (est ? dim('~')+fmtTok(p.cacheCreate) : fmtTok(p.cacheCreate)) : dim('—'), 8),
469
- padStart((p.cacheRead??0) > 0 ? fmtTok(p.cacheRead) : dim('—'), 9),
609
+ padStart(
610
+ (p.cacheCreate ?? 0) > 0
611
+ ? est
612
+ ? dim("~") + fmtTok(p.cacheCreate)
613
+ : fmtTok(p.cacheCreate)
614
+ : dim("—"),
615
+ 8,
616
+ ),
617
+ padStart((p.cacheRead ?? 0) > 0 ? fmtTok(p.cacheRead) : dim("—"), 9),
470
618
  padStart(cas.yield > 10000 ? gold(fmtY(cas.yield)) : fmtY(cas.yield), 9),
471
619
  padStart(fmtSNR(cas.snr), 7),
472
- padStart(fmtLev(cas.leverage)+'×', 7),
473
- ...(narrow ? [] : [
474
- padStart(cas.velocity?.toFixed(2) ?? '—', 6),
475
- padStart(cas.dev10x?.toFixed(2) ?? '—', 6),
476
- ]),
620
+ padStart(fmtLev(cas.leverage) + "×", 7),
621
+ ...(narrow
622
+ ? []
623
+ : [
624
+ padStart(cas.velocity?.toFixed(2) ?? "—", 6),
625
+ padStart(cas.dev10x?.toFixed(2) ?? "—", 6),
626
+ ]),
477
627
  padEnd(clsFn(cas.class), 13),
478
- ]
479
- emit(` ${cols.join(gap)}`)
480
- }
628
+ ];
629
+ emit(` ${cols.join(gap)}`);
630
+ };
481
631
 
482
632
  // ── Cascade table
483
- emit()
484
- emit(` ${bold('Your Cascade')} ${dim('all platforms · all windows')}`)
485
- emit()
633
+ emit();
634
+ emit(` ${bold("Your Cascade")} ${dim("all platforms · all windows")}`);
635
+ emit();
486
636
 
487
637
  // header (matches renderRow's responsive column set + gap)
488
638
  const CH = [
489
- padEnd(hdr('Platform'), 12), padEnd(hdr('Win'), 5),
490
- padStart(hdr('Input'), 8), padStart(hdr('Output'), 8),
491
- padStart(hdr('CacheW'), 8), padStart(hdr('CacheR'), 9),
492
- padStart(hdr('Υ Yield'), 9), padStart(hdr('SNR'), 7),
493
- padStart(hdr('Lev'), 7),
494
- ...(narrow ? [] : [padStart(hdr('Vel'), 6), padStart(hdr('10x'), 6)]),
495
- padEnd(hdr('Class'), 13),
496
- ]
497
- emit(` ${CH.join(gap)}`)
498
- emit(` ${dim('·'.repeat(Math.max(0, Math.min(w - 4, narrow ? 96 : 114))))}`)
639
+ padEnd(hdr("Platform"), 12),
640
+ padEnd(hdr("Win"), 5),
641
+ padStart(hdr("Input"), 8),
642
+ padStart(hdr("Output"), 8),
643
+ padStart(hdr("CacheW"), 8),
644
+ padStart(hdr("CacheR"), 9),
645
+ padStart(hdr("Υ Yield"), 9),
646
+ padStart(hdr("SNR"), 7),
647
+ padStart(hdr("Lev"), 7),
648
+ ...(narrow ? [] : [padStart(hdr("Vel"), 6), padStart(hdr("10x"), 6)]),
649
+ padEnd(hdr("Class"), 13),
650
+ ];
651
+ emit(` ${CH.join(gap)}`);
652
+ emit(` ${dim("·".repeat(Math.max(0, Math.min(w - 4, narrow ? 96 : 114))))}`);
499
653
 
500
654
  if (active.length === 0) {
501
- emit(` ${dim(' reading token logs… (14 platforms, ~5s · press [R] to refresh)')}`)
655
+ emit(
656
+ ` ${dim(" reading token logs… (14 platforms, ~5s · press [R] to refresh)")}`,
657
+ );
502
658
  }
503
659
 
504
660
  // Calculate how many cascade rows we can fit — reserve space for lower sections.
505
661
  // (Υ-trend section moved to the Trends tab; its old `sparkLines` reservation is gone,
506
662
  // which is what was starving codex's cascade rows after the Your-Read/Three-Degrees panels landed.)
507
- const platformCount = active.length
508
- const barLines = 4 + platformCount + (platformCount > 1 ? 1 : 0) // Token Composition: hr + header + platforms + combined + note
663
+ const platformCount = active.length;
664
+ const barLines = 4 + platformCount + (platformCount > 1 ? 1 : 0); // Token Composition: hr + header + platforms + combined + note
509
665
  // Your Read (hr+header+~3 wins+1 gap = 6) + Three Degrees (blank+hr+header+col-header+5 rows+note = 10) + status
510
- const boardLines = 17
511
- const sectionsBelow = barLines + boardLines
512
- const maxCascadeRows = Math.max(8, budget - used - sectionsBelow)
666
+ const boardLines = 17;
667
+ const sectionsBelow = barLines + boardLines;
668
+ const maxCascadeRows = Math.max(8, budget - used - sectionsBelow);
513
669
 
514
670
  // SCROLL-VIEW (2026-06-27): Build the full list of cascade rows first, then
515
671
  // slice by [scrollOffset, scrollOffset + maxCascadeRows]. This gives a real
516
672
  // in-section scroll viewport — up/down arrows scroll just the cascade rows
517
673
  // while the rest of the dashboard stays pinned. Replaces the A3 "+N more —
518
674
  // resize / scroll" text which implied scroll existed but didn't.
519
- let firstWin = {}
520
- const allCascadeRows = [] // flat list: { platform, winKey, pillars, estimated }
675
+ let firstWin = {};
676
+ const allCascadeRows = []; // flat list: { platform, winKey, pillars, estimated }
521
677
  for (const d of active) {
522
- firstWin[d.platform] = WINS.find(wk => {
523
- const wd = d.windows?.find(w => w.window === wk)
524
- return wd && (wd.pillars.input + wd.pillars.output) > 0
525
- })
678
+ firstWin[d.platform] = WINS.find((wk) => {
679
+ const wd = d.windows?.find((w) => w.window === wk);
680
+ return wd && wd.pillars.input + wd.pillars.output > 0;
681
+ });
526
682
  for (const wk of WINS) {
527
- const wd = d.windows?.find(x => x.window === wk)
528
- if (!wd || (wd.pillars.input + wd.pillars.output) === 0) continue
529
- allCascadeRows.push({ platform: d.platform, winKey: wk, pillars: wd.pillars, estimated: d.estimated })
683
+ const wd = d.windows?.find((x) => x.window === wk);
684
+ if (!wd || wd.pillars.input + wd.pillars.output === 0) continue;
685
+ allCascadeRows.push({
686
+ platform: d.platform,
687
+ winKey: wk,
688
+ pillars: wd.pillars,
689
+ estimated: d.estimated,
690
+ });
530
691
  }
531
692
  }
532
693
 
533
- const totalWanted = allCascadeRows.length
534
- const canScroll = totalWanted > maxCascadeRows
694
+ const totalWanted = allCascadeRows.length;
695
+ const canScroll = totalWanted > maxCascadeRows;
535
696
  // Clamp scroll offset to valid range [0, totalWanted - maxCascadeRows]
536
- const maxOffset = Math.max(0, totalWanted - maxCascadeRows)
537
- const effectiveOffset = Math.min(scrollOffset, maxOffset)
538
- const visibleRows = allCascadeRows.slice(effectiveOffset, effectiveOffset + maxCascadeRows)
697
+ const maxOffset = Math.max(0, totalWanted - maxCascadeRows);
698
+ const effectiveOffset = Math.min(scrollOffset, maxOffset);
699
+ const visibleRows = allCascadeRows.slice(
700
+ effectiveOffset,
701
+ effectiveOffset + maxCascadeRows,
702
+ );
539
703
 
540
704
  // ▲ indicator: rows above the current viewport
541
705
  if (canScroll && effectiveOffset > 0) {
542
- emit(` ${dim(` ▲ ${effectiveOffset} row${effectiveOffset > 1 ? 's' : ''} above`)}`)
706
+ emit(
707
+ ` ${dim(` ▲ ${effectiveOffset} row${effectiveOffset > 1 ? "s" : ""} above`)}`,
708
+ );
543
709
  }
544
710
 
545
711
  // Render the visible slice — group by platform (emit a blank line between platforms)
546
- let lastPlatform = null
712
+ let lastPlatform = null;
547
713
  for (const row of visibleRows) {
548
- if (lastPlatform !== null && row.platform !== lastPlatform) emit()
549
- renderRow(row.platform, (s) => row.winKey === firstWin[row.platform] ? cyan(s) : dim(s), row.winKey, row.pillars, row.estimated)
550
- lastPlatform = row.platform
714
+ if (lastPlatform !== null && row.platform !== lastPlatform) emit();
715
+ renderRow(
716
+ row.platform,
717
+ (s) => (row.winKey === firstWin[row.platform] ? cyan(s) : dim(s)),
718
+ row.winKey,
719
+ row.pillars,
720
+ row.estimated,
721
+ );
722
+ lastPlatform = row.platform;
551
723
  }
552
- if (visibleRows.length > 0) emit()
724
+ if (visibleRows.length > 0) emit();
553
725
 
554
726
  // ▼ indicator: rows below the current viewport
555
- const rowsBelow = totalWanted - effectiveOffset - visibleRows.length
727
+ const rowsBelow = totalWanted - effectiveOffset - visibleRows.length;
556
728
  if (canScroll && rowsBelow > 0) {
557
- emit(` ${dim(` ▼ ${rowsBelow} row${rowsBelow > 1 ? 's' : ''} below · ↑↓ scroll`)}${effectiveOffset > 0 ? dim(` · ${effectiveOffset}/${maxOffset}`) : ''}`)
729
+ emit(
730
+ ` ${dim(` ▼ ${rowsBelow} row${rowsBelow > 1 ? "s" : ""} below · ↑↓ scroll`)}${effectiveOffset > 0 ? dim(` · ${effectiveOffset}/${maxOffset}`) : ""}`,
731
+ );
558
732
  }
559
733
 
560
734
  // combined
561
735
  if (active.length > 1 && used < budget - sectionsBelow) {
562
- const lbl = active.map(d => d.platform).join('+')
563
- const hasEst = active.some(d => d.estimated)
564
- let fst = true
736
+ const lbl = active.map((d) => d.platform).join("+");
737
+ const hasEst = active.some((d) => d.estimated);
738
+ let fst = true;
565
739
  for (const wk of WINS) {
566
- const cp = { input: 0, output: 0, cacheCreate: 0, cacheRead: 0 }
567
- let any = false
740
+ const cp = { input: 0, output: 0, cacheCreate: 0, cacheRead: 0 };
741
+ let any = false;
568
742
  for (const d of active) {
569
- const wd = d.windows?.find(x => x.window === wk)
570
- if (!wd || (wd.pillars.input + wd.pillars.output) === 0) continue
571
- cp.input += wd.pillars.input ?? 0; cp.output += wd.pillars.output ?? 0
572
- cp.cacheCreate += wd.pillars.cacheCreate ?? 0; cp.cacheRead += wd.pillars.cacheRead ?? 0
573
- any = true
743
+ const wd = d.windows?.find((x) => x.window === wk);
744
+ if (!wd || wd.pillars.input + wd.pillars.output === 0) continue;
745
+ cp.input += wd.pillars.input ?? 0;
746
+ cp.output += wd.pillars.output ?? 0;
747
+ cp.cacheCreate += wd.pillars.cacheCreate ?? 0;
748
+ cp.cacheRead += wd.pillars.cacheRead ?? 0;
749
+ any = true;
574
750
  }
575
- if (!any) continue
576
- renderRow(lbl, (s) => fst ? bold(cyan(s)) : dim(s), wk, cp, hasEst)
577
- fst = false
751
+ if (!any) continue;
752
+ renderRow(lbl, (s) => (fst ? bold(cyan(s)) : dim(s)), wk, cp, hasEst);
753
+ fst = false;
578
754
  }
579
- emit()
755
+ emit();
580
756
  }
581
757
 
582
758
  // (Υ Trend moved to the dedicated Trends tab [2] — every metric, with sub-views.)
583
759
 
584
760
  // ── Token bar charts (skip if no room)
585
761
  if (used < budget - boardLines) {
586
- emit(` ${hr()}`)
587
- emit(` ${bold('Token Composition')} ${dim('█')}${paint(c.cyan,'I')}${dim(' in █')}${paint(c.blue,'W')}${dim(' cW █')}${paint(c.boldGold,'R')}${dim(' cR █')}${paint(c.green,'O')}${dim(' out')}`)
762
+ emit(` ${hr()}`);
763
+ emit(
764
+ ` ${bold("Token Composition")} ${dim("█")}${paint(c.cyan, "I")}${dim(" in █")}${paint(c.blue, "W")}${dim(" cW █")}${paint(c.boldGold, "R")}${dim(" cR █")}${paint(c.green, "O")}${dim(" out")}`,
765
+ );
588
766
  for (const d of active) {
589
- if (used >= budget - boardLines) break
590
- const all = d.windows?.find(w => w.window === 'all')
591
- if (!all) continue
592
- emit(` ${padEnd(cyan(d.platform), 10)} ${tokenBar(all.pillars, 50)} ${dim(fmtTok((all.pillars.input??0)+(all.pillars.output??0)+(all.pillars.cacheCreate??0)+(all.pillars.cacheRead??0)))}`)
767
+ if (used >= budget - boardLines) break;
768
+ const all = d.windows?.find((w) => w.window === "all");
769
+ if (!all) continue;
770
+ emit(
771
+ ` ${padEnd(cyan(d.platform), 10)} ${tokenBar(all.pillars, 50)} ${dim(fmtTok((all.pillars.input ?? 0) + (all.pillars.output ?? 0) + (all.pillars.cacheCreate ?? 0) + (all.pillars.cacheRead ?? 0)))}`,
772
+ );
593
773
  }
594
774
  if (active.length > 1) {
595
- const cp = { input: 0, output: 0, cacheCreate: 0, cacheRead: 0 }
775
+ const cp = { input: 0, output: 0, cacheCreate: 0, cacheRead: 0 };
596
776
  for (const d of active) {
597
- const all = d.windows?.find(w => w.window === 'all')
598
- if (!all) continue
599
- cp.input += all.pillars.input??0; cp.output += all.pillars.output??0
600
- cp.cacheCreate += all.pillars.cacheCreate??0; cp.cacheRead += all.pillars.cacheRead??0
777
+ const all = d.windows?.find((w) => w.window === "all");
778
+ if (!all) continue;
779
+ cp.input += all.pillars.input ?? 0;
780
+ cp.output += all.pillars.output ?? 0;
781
+ cp.cacheCreate += all.pillars.cacheCreate ?? 0;
782
+ cp.cacheRead += all.pillars.cacheRead ?? 0;
601
783
  }
602
- emit(` ${padEnd(bold(active.map(d => d.platform).join('+')), 10)} ${tokenBar(cp, 50)} ${dim(fmtTok(cp.input+cp.output+cp.cacheCreate+cp.cacheRead))}`)
784
+ emit(
785
+ ` ${padEnd(bold(active.map((d) => d.platform).join("+")), 10)} ${tokenBar(cp, 50)} ${dim(fmtTok(cp.input + cp.output + cp.cacheCreate + cp.cacheRead))}`,
786
+ );
603
787
  }
604
788
  }
605
789
 
@@ -609,57 +793,117 @@ function renderDashboard(data, status = '', scrollOffset = 0) {
609
793
  const you = (() => {
610
794
  // Best available cascade for the user: prefer claude's first-window, else any computed row.
611
795
  for (const d of active) {
612
- const wk = firstWin[d.platform]
613
- const wd = d.windows?.find((x) => x.window === wk)
614
- const cas = wd && cascadeFrom(wd.pillars)
615
- if (cas) return cas // cascadeFrom already returns null for non-compounding rows
796
+ const wk = firstWin[d.platform];
797
+ const wd = d.windows?.find((x) => x.window === wk);
798
+ const cas = wd && cascadeFrom(wd.pillars);
799
+ if (cas) return cas; // cascadeFrom already returns null for non-compounding rows
616
800
  }
617
- return null
618
- })()
801
+ return null;
802
+ })();
619
803
 
620
804
  if (used < budget - 8) {
621
805
  // ── Custom insights — comparison vs the field + a concrete tip for the weakest metric
622
- emit(` ${hr()}`)
623
- emit(` ${bold('Your Read')} ${dim('how your cascade sits vs the field')}`)
806
+ emit(` ${hr()}`);
807
+ emit(
808
+ ` ${bold("Your Read")} ${dim("how your cascade sits vs the field")}`,
809
+ );
624
810
  if (you) {
625
- const wins = [], gaps = []
811
+ const wins = [],
812
+ gaps = [];
626
813
  const chk = (label, val, fieldAvg, top, fmt, tip) => {
627
- if (val == null) return
628
- if (val >= top * 0.9) wins.push(`${label} ${fmt(val)} — ${gold('top tier')}`)
629
- else if (val >= fieldAvg) wins.push(`${label} ${fmt(val)} — above field avg`)
630
- else gaps.push({ label, txt: `${label} ${fmt(val)} — below field (${fmt(fieldAvg)}): ${tip}`, val, fieldAvg })
631
- }
632
- chk('Υ Yield', you.yield, TD.field.yield, TD.top.yield, fmtY, 'compound more — reuse cache, raise output')
633
- chk('SNR', you.snr, TD.field.snr, TD.top.snr, fmtSNR, 'tighten prompts — less input per unit output')
634
- chk('Leverage', you.leverage, TD.field.lev, TD.top.lev, (v) => fmtLev(v) + '×', 'lean on cache-read — amplify prior context')
635
- chk('Velocity', you.velocity, TD.field.vel, TD.top.vel, (v) => v.toFixed(2), 'more output per input token')
636
- for (const w of wins.slice(0, 3)) emit(` ${paint(c.green, '▲')} ${w}`)
814
+ if (val == null) return;
815
+ if (val >= top * 0.9)
816
+ wins.push(`${label} ${fmt(val)} — ${gold("top tier")}`);
817
+ else if (val >= fieldAvg)
818
+ wins.push(`${label} ${fmt(val)} — above field avg`);
819
+ else
820
+ gaps.push({
821
+ label,
822
+ txt: `${label} ${fmt(val)} — below field (${fmt(fieldAvg)}): ${tip}`,
823
+ val,
824
+ fieldAvg,
825
+ });
826
+ };
827
+ chk(
828
+ "Υ Yield",
829
+ you.yield,
830
+ TD.field.yield,
831
+ TD.top.yield,
832
+ fmtY,
833
+ "compound more — reuse cache, raise output",
834
+ );
835
+ chk(
836
+ "SNR",
837
+ you.snr,
838
+ TD.field.snr,
839
+ TD.top.snr,
840
+ fmtSNR,
841
+ "tighten prompts — less input per unit output",
842
+ );
843
+ chk(
844
+ "Leverage",
845
+ you.leverage,
846
+ TD.field.lev,
847
+ TD.top.lev,
848
+ (v) => fmtLev(v) + "×",
849
+ "lean on cache-read — amplify prior context",
850
+ );
851
+ chk(
852
+ "Velocity",
853
+ you.velocity,
854
+ TD.field.vel,
855
+ TD.top.vel,
856
+ (v) => v.toFixed(2),
857
+ "more output per input token",
858
+ );
859
+ for (const w of wins.slice(0, 3)) emit(` ${paint(c.green, "▲")} ${w}`);
637
860
  // weakest gap gets the prescriptive tip
638
- const weakest = gaps.sort((a, b) => (a.val / a.fieldAvg) - (b.val / b.fieldAvg))[0]
639
- if (weakest) emit(` ${paint(c.magenta, '▽')} ${dim(weakest.txt)}`)
640
- if (!wins.length && !weakest) emit(` ${dim(' building your read…')}`)
861
+ const weakest = gaps.sort(
862
+ (a, b) => a.val / a.fieldAvg - b.val / b.fieldAvg,
863
+ )[0];
864
+ if (weakest) emit(` ${paint(c.magenta, "▽")} ${dim(weakest.txt)}`);
865
+ if (!wins.length && !weakest) emit(` ${dim(" building your read…")}`);
641
866
  } else {
642
- emit(` ${dim(' reading your cascade… (press [R] to refresh)')}`)
867
+ emit(` ${dim(" reading your cascade… (press [R] to refresh)")}`);
643
868
  }
644
869
 
645
870
  // ── Three Degrees — AI avg · SigRank avg · Top (live values from signalaf.com)
646
- emit()
647
- emit(` ${hr()}`)
648
- emit(` ${bold('Three Degrees')} ${dim('AI avg · SigRank avg · top — signalaf.com/wiki')}`)
649
- const tdHead = [padEnd(hdr('Metric'), 10), padStart(hdr('AI avg'), 9), padStart(hdr('SigRank avg'), 12), padStart(hdr('Top'), 9)]
650
- emit(` ${tdHead.join(' ')}`)
651
- const white = (s) => paint(c.white, s)
871
+ emit();
872
+ emit(` ${hr()}`);
873
+ emit(
874
+ ` ${bold("Three Degrees")} ${dim("AI avg · SigRank avg · top — signalaf.com/wiki")}`,
875
+ );
876
+ const tdHead = [
877
+ padEnd(hdr("Metric"), 10),
878
+ padStart(hdr("AI avg"), 9),
879
+ padStart(hdr("SigRank avg"), 12),
880
+ padStart(hdr("Top"), 9),
881
+ ];
882
+ emit(` ${tdHead.join(" ")}`);
883
+ const white = (s) => paint(c.white, s);
652
884
  const tdRow = (label, b, f, t, fmt) =>
653
- emit(` ${padEnd(label, 10)} ${padStart(white(fmt(b)), 9)} ${padStart(fmt(f), 12)} ${padStart(gold(fmt(t)), 9)}`)
654
- tdRow('Υ Yield', TD.base.yield, TD.field.yield, TD.top.yield, fmtY)
655
- tdRow('SNR', TD.base.snr, TD.field.snr, TD.top.snr, fmtSNR)
656
- tdRow('Velocity', TD.base.vel, TD.field.vel, TD.top.vel, (v) => v.toFixed(2))
657
- tdRow('Leverage', TD.base.lev, TD.field.lev, TD.top.lev, (v) => fmtLev(v) + '×')
658
- tdRow('10xDEV', TD.base.d10, TD.field.d10, TD.top.d10, (v) => v.toFixed(2))
659
- emit(` ${dim('reference values until live user volume calibrates SigRank avg')}`)
885
+ emit(
886
+ ` ${padEnd(label, 10)} ${padStart(white(fmt(b)), 9)} ${padStart(fmt(f), 12)} ${padStart(gold(fmt(t)), 9)}`,
887
+ );
888
+ tdRow("Υ Yield", TD.base.yield, TD.field.yield, TD.top.yield, fmtY);
889
+ tdRow("SNR", TD.base.snr, TD.field.snr, TD.top.snr, fmtSNR);
890
+ tdRow("Velocity", TD.base.vel, TD.field.vel, TD.top.vel, (v) =>
891
+ v.toFixed(2),
892
+ );
893
+ tdRow(
894
+ "Leverage",
895
+ TD.base.lev,
896
+ TD.field.lev,
897
+ TD.top.lev,
898
+ (v) => fmtLev(v) + "×",
899
+ );
900
+ tdRow("10xDEV", TD.base.d10, TD.field.d10, TD.top.d10, (v) => v.toFixed(2));
901
+ emit(
902
+ ` ${dim("reference values until live user volume calibrates SigRank avg")}`,
903
+ );
660
904
  }
661
905
 
662
- if (status && used < budget) emit(` ${dim(status)}`)
906
+ if (status && used < budget) emit(` ${dim(status)}`);
663
907
  }
664
908
 
665
909
  // ── TAB 2: TRENDS ─────────────────────────────────────────────────────────────
@@ -668,176 +912,300 @@ function renderDashboard(data, status = '', scrollOffset = 0) {
668
912
  // Platform — per-platform (claude / codex / combined)
669
913
  // Field — board-wide trend (calculating until window-history materializes)
670
914
  // Switch sub-tabs with [T] (cycles You · Platform · Field). Data: the same `active` the Dashboard loads.
671
- const TREND_SUBTABS = ['You', 'Platform', 'Field']
915
+ const TREND_SUBTABS = ["You", "Platform", "Field"];
672
916
  // All leaderboard metrics. Most derive from the per-window cascade (pick takes the
673
917
  // cascade obj + its pillars); §IGNA (proprietary composite) and $/1M (needs cost data)
674
918
  // aren't computed client-side yet → rendered "calculating", same honest pattern as Rank.
675
- const AA_EFF_BASELINE = 4.0 // efficiency is measured vs the AA 4.0 baseline (per the wiki)
919
+ const AA_EFF_BASELINE = 4.0; // efficiency is measured vs the AA 4.0 baseline (per the wiki)
676
920
  const TREND_METRICS = [
677
- { label: '∑ Total', pick: (c, p) => (p.input ?? 0) + (p.output ?? 0) + (p.cacheCreate ?? 0) + (p.cacheRead ?? 0), fmt: fmtTok },
678
- { label: 'Υ Yield', pick: (c) => c.yield, fmt: fmtY },
679
- { label: 'SNR', pick: (c) => c.snr, fmt: fmtSNR },
680
- { label: 'Velocity', pick: (c) => c.velocity, fmt: (v) => v.toFixed(2) },
681
- { label: 'Leverage', pick: (c) => c.leverage, fmt: (v) => fmtLev(v) + '×' },
682
- { label: '10xDEV', pick: (c) => c.dev10x, fmt: (v) => v.toFixed(2) },
683
- { label: 'Eff', pick: (c) => c.yield != null ? c.yield / AA_EFF_BASELINE : null, fmt: (v) => v.toFixed(1) + '×' },
684
- { label: '§IGNA', pick: () => null, fmt: () => '—', stub: 'composite calibrating (needs user volume)' },
921
+ {
922
+ label: "∑ Total",
923
+ pick: (c, p) =>
924
+ (p.input ?? 0) +
925
+ (p.output ?? 0) +
926
+ (p.cacheCreate ?? 0) +
927
+ (p.cacheRead ?? 0),
928
+ fmt: fmtTok,
929
+ },
930
+ { label: "Υ Yield", pick: (c) => c.yield, fmt: fmtY },
931
+ { label: "SNR", pick: (c) => c.snr, fmt: fmtSNR },
932
+ { label: "Velocity", pick: (c) => c.velocity, fmt: (v) => v.toFixed(2) },
933
+ { label: "Leverage", pick: (c) => c.leverage, fmt: (v) => fmtLev(v) + "×" },
934
+ { label: "10xDEV", pick: (c) => c.dev10x, fmt: (v) => v.toFixed(2) },
935
+ {
936
+ label: "Eff",
937
+ pick: (c) => (c.yield != null ? c.yield / AA_EFF_BASELINE : null),
938
+ fmt: (v) => v.toFixed(1) + "×",
939
+ },
940
+ {
941
+ label: "§IGNA",
942
+ pick: () => null,
943
+ fmt: () => "—",
944
+ stub: "composite calibrating (needs user volume)",
945
+ },
685
946
  // FIX A4 (2026-06-27): operating ratio is INPUT-NORMALIZED `cache : 1 : output` (matches web canon —
686
947
  // ThreeDegreesChart / OperatingRatioBar, e.g. "3.5 : 1 : 0.50"), not raw token counts (which also
687
948
  // overflowed the Trends columns).
688
- { label: 'Op Ratio', pick: (c, p) => (p.input ?? 0) > 0 ? { cacheRead: p.cacheRead ?? 0, input: p.input ?? 0, output: p.output ?? 0 } : null, fmt: (v) => { const i = v.input || 1; return `${(v.cacheRead / i).toFixed(1)} : 1 : ${(v.output / i).toFixed(2)}` } },
689
- { label: '$/1M', pick: () => null, fmt: () => '—', stub: 'cost data wires post-ingest' },
690
- ]
949
+ {
950
+ label: "Op Ratio",
951
+ pick: (c, p) =>
952
+ (p.input ?? 0) > 0
953
+ ? {
954
+ cacheRead: p.cacheRead ?? 0,
955
+ input: p.input ?? 0,
956
+ output: p.output ?? 0,
957
+ }
958
+ : null,
959
+ fmt: (v) => {
960
+ const i = v.input || 1;
961
+ return `${(v.cacheRead / i).toFixed(1)} : 1 : ${(v.output / i).toFixed(2)}`;
962
+ },
963
+ },
964
+ {
965
+ label: "$/1M",
966
+ pick: () => null,
967
+ fmt: () => "—",
968
+ stub: "cost data wires post-ingest",
969
+ },
970
+ ];
691
971
  function renderTrends(data, subIdx = 0) {
692
- const { active } = data ?? {}
693
- const budget = H() - 4
694
- let used = 0
695
- const emit = (s = '') => { if (used < budget) { writeln(s); used++ } }
696
- const sub = TREND_SUBTABS[subIdx] ?? 'You'
972
+ const { active } = data ?? {};
973
+ const budget = H() - 4;
974
+ let used = 0;
975
+ const emit = (s = "") => {
976
+ if (used < budget) {
977
+ writeln(s);
978
+ used++;
979
+ }
980
+ };
981
+ const sub = TREND_SUBTABS[subIdx] ?? "You";
697
982
 
698
983
  // sub-tab switcher row
699
984
  const subBar = TREND_SUBTABS.map((t, i) =>
700
- i === subIdx ? `${c.bgCyan}${c.boldCyan} ${t} ${c.reset}` : `${dim(` ${t} `)}`).join(' ')
701
- emit()
702
- emit(` ${bold('Trends')} ${dim('every metric across windows (7d → all)')} ${subBar}`)
703
- emit(` ${dim('·'.repeat(Math.min(W() - 4, 70)))}`)
704
- emit()
705
-
706
- if (sub === 'You' || sub === 'Platform') {
707
- const rows = sub === 'You'
708
- ? ((active?.length) ? [{ platform: 'you', windows: active[0]?.windows }] : []) // primary cascade
709
- : (active ?? [])
710
- if (!rows.length) { emit(` ${dim(' reading your cascade… (press [R] to refresh)')}`); }
985
+ i === subIdx
986
+ ? `${c.bgCyan}${c.boldCyan} ${t} ${c.reset}`
987
+ : `${dim(` ${t} `)}`,
988
+ ).join(" ");
989
+ emit();
990
+ emit(
991
+ ` ${bold("Trends")} ${dim("every metric across windows (7d → all)")} ${subBar}`,
992
+ );
993
+ emit(` ${dim("·".repeat(Math.min(W() - 4, 70)))}`);
994
+ emit();
995
+
996
+ if (sub === "You" || sub === "Platform") {
997
+ const rows =
998
+ sub === "You"
999
+ ? active?.length
1000
+ ? [{ platform: "you", windows: active[0]?.windows }]
1001
+ : [] // primary cascade
1002
+ : (active ?? []);
1003
+ if (!rows.length) {
1004
+ emit(` ${dim(" reading your cascade… (press [R] to refresh)")}`);
1005
+ }
711
1006
  // A3 (2026-06-27): count metric/rank rows dropped by the height budget so we can show a
712
1007
  // "+N more — resize / scroll" note instead of silently truncating. Each rendered block wants
713
1008
  // TREND_METRICS.length metric rows + 1 rank row. (Fit logic below unchanged — observe only.)
714
- let trendsDropped = 0
1009
+ let trendsDropped = 0;
715
1010
  for (let ri = 0; ri < rows.length; ri++) {
716
- const d = rows[ri]
717
- if (used >= budget - 2) { trendsDropped += (rows.length - ri) * (TREND_METRICS.length + 1); break }
718
- emit(` ${bold(sub === 'You' ? 'Your trajectory' : cyan(d.platform))}`)
1011
+ const d = rows[ri];
1012
+ if (used >= budget - 2) {
1013
+ trendsDropped += (rows.length - ri) * (TREND_METRICS.length + 1);
1014
+ break;
1015
+ }
1016
+ emit(` ${bold(sub === "You" ? "Your trajectory" : cyan(d.platform))}`);
719
1017
  // FIX G1: window column headers (mirror renderCompare) — one header row, then
720
1018
  // values in fixed-width columns below. No per-value `w:` prefix.
721
- emit(` ${trendHeader()}`)
722
- let mi = 0
1019
+ emit(` ${trendHeader()}`);
1020
+ let mi = 0;
723
1021
  for (; mi < TREND_METRICS.length; mi++) {
724
- const m = TREND_METRICS[mi]
725
- if (used >= budget - 2) break
1022
+ const m = TREND_METRICS[mi];
1023
+ if (used >= budget - 2) break;
726
1024
  if (m.stub) {
727
- emit(` ${padEnd(dim(m.label), 10)} ${dim('▁▁▁▁ calculating… (' + m.stub + ')')}`)
1025
+ emit(
1026
+ ` ${padEnd(dim(m.label), 10)} ${dim("▁▁▁▁ calculating… (" + m.stub + ")")}`,
1027
+ );
728
1028
  } else {
729
- emit(` ${padEnd(cyan(m.label), 10)} ${metricSpark(d, m.pick, m.fmt)}`)
1029
+ emit(
1030
+ ` ${padEnd(cyan(m.label), 10)} ${metricSpark(d, m.pick, m.fmt)}`,
1031
+ );
730
1032
  }
731
1033
  }
732
- trendsDropped += (TREND_METRICS.length - mi) // metrics this block couldn't fit
1034
+ trendsDropped += TREND_METRICS.length - mi; // metrics this block couldn't fit
733
1035
  // rank trend — wired later; honest placeholder for now
734
- if (used < budget - 1) emit(` ${padEnd(dim('Rank'), 10)} ${dim('▁▁▁▁ calculating… (rank history wires post-ingest)')}`)
735
- else trendsDropped += 1
736
- emit()
1036
+ if (used < budget - 1)
1037
+ emit(
1038
+ ` ${padEnd(dim("Rank"), 10)} ${dim("▁▁▁▁ calculating… (rank history wires post-ingest)")}`,
1039
+ );
1040
+ else trendsDropped += 1;
1041
+ emit();
737
1042
  }
738
1043
  // A3: surface the dropped count instead of hiding it.
739
1044
  // SCROLL-VIEW (2026-06-27): changed "resize / scroll" → "resize to see more"
740
1045
  // because these tabs don't have a real scroll viewport (only Dashboard cascade does).
741
- if (trendsDropped > 0) emit(` ${dim(` +${trendsDropped} more — resize to see more`)}`)
742
- } else { // Field
743
- emit(` ${dim(' Field-wide trends — calculating…')}`)
744
- emit(` ${dim(' (needs per-window board history; materializes once live ingest lands)')}`)
1046
+ if (trendsDropped > 0)
1047
+ emit(` ${dim(` +${trendsDropped} more — resize to see more`)}`);
1048
+ } else {
1049
+ // Field
1050
+ emit(` ${dim(" Field-wide trends — calculating…")}`);
1051
+ emit(
1052
+ ` ${dim(" (needs per-window board history; materializes once live ingest lands)")}`,
1053
+ );
745
1054
  }
746
1055
  }
747
1056
 
748
1057
  // ── TAB 3: COMPARE ───────────────────────────────────────────────────────────
749
1058
  function renderCompare(data) {
750
- const { tpData, cc, ts, td, platform } = data
751
- const WINS = ['7d', '30d', '90d', 'all']
752
- const w = W()
753
- const budget = H() - 4
754
- let used = 0
755
- const emit = (s = '') => { if (used < budget) { writeln(s); used++ } }
1059
+ const { tpData, cc, ts, td, platform } = data;
1060
+ const WINS = ["7d", "30d", "90d", "all"];
1061
+ const w = W();
1062
+ const budget = H() - 4;
1063
+ let used = 0;
1064
+ const emit = (s = "") => {
1065
+ if (used < budget) {
1066
+ writeln(s);
1067
+ used++;
1068
+ }
1069
+ };
756
1070
 
757
- const tpPillars = {}
758
- for (const win of (tpData?.windows ?? [])) tpPillars[win.window] = win.pillars
1071
+ const tpPillars = {};
1072
+ for (const win of tpData?.windows ?? []) tpPillars[win.window] = win.pillars;
759
1073
 
760
1074
  const SOURCES = [
761
- { name: 'tokenpull', color: (s) => paint(c.boldCyan, s), pillars: tpPillars, note: 'JSONL canon' },
762
- { name: 'ccusage', color: (s) => paint(c.green, s), pillars: cc ?? {}, note: 'ccusage CLI' },
763
- { name: 'token-dash', color: (s) => paint(c.magenta, s), pillars: td ?? {}, note: 'SQLite' },
764
- { name: 'tokscale', color: (s) => paint(c.blue, s), pillars: ts ?? {}, note: 'report.json' },
765
- ].filter(s => Object.keys(s.pillars).length > 0)
766
-
767
- emit()
768
- emit(` ${bold('Source Comparison')} ${dim(`platform: ${platform}`)} ${dim('· tokenpull vs ccusage vs token-dash vs tokscale')}`)
1075
+ {
1076
+ name: "tokenpull",
1077
+ color: (s) => paint(c.boldCyan, s),
1078
+ pillars: tpPillars,
1079
+ note: "JSONL canon",
1080
+ },
1081
+ {
1082
+ name: "ccusage",
1083
+ color: (s) => paint(c.green, s),
1084
+ pillars: cc ?? {},
1085
+ note: "ccusage CLI",
1086
+ },
1087
+ {
1088
+ name: "token-dash",
1089
+ color: (s) => paint(c.magenta, s),
1090
+ pillars: td ?? {},
1091
+ note: "SQLite",
1092
+ },
1093
+ {
1094
+ name: "tokscale",
1095
+ color: (s) => paint(c.blue, s),
1096
+ pillars: ts ?? {},
1097
+ note: "report.json",
1098
+ },
1099
+ ].filter((s) => Object.keys(s.pillars).length > 0);
1100
+
1101
+ emit();
1102
+ emit(
1103
+ ` ${bold("Source Comparison")} ${dim(`platform: ${platform}`)} ${dim("· tokenpull vs ccusage vs token-dash vs tokscale")}`,
1104
+ );
769
1105
 
770
1106
  if (!tpData) {
771
- emit(` ${dim('tokenpull: no JSONL data found — check ~/.claude/projects/')}`)
1107
+ emit(
1108
+ ` ${dim("tokenpull: no JSONL data found — check ~/.claude/projects/")}`,
1109
+ );
772
1110
  } else {
773
- const all = tpPillars['all']
1111
+ const all = tpPillars["all"];
774
1112
  if (all) {
775
- emit(` ${dim('tokenpull all-time:')} In ${cyan(fmtTok(all.input))} Out ${green(fmtTok(all.output))} CW ${paint(c.blue, fmtTok(all.cacheCreate))} CR ${gold(fmtTok(all.cacheRead))}`)
1113
+ emit(
1114
+ ` ${dim("tokenpull all-time:")} In ${cyan(fmtTok(all.input))} Out ${green(fmtTok(all.output))} CW ${paint(c.blue, fmtTok(all.cacheCreate))} CR ${gold(fmtTok(all.cacheRead))}`,
1115
+ );
776
1116
  }
777
1117
  }
778
- emit()
779
-
780
- const COL_W = 11
781
- const hcols = [padEnd(dim('Source'), 12), padEnd(dim('Pillar'), 10), ...WINS.map(wn => padStart(dim(wn), COL_W))]
782
- emit(` ${hcols.join(' ')}`)
783
- emit(` ${dim('·'.repeat(Math.min(w-4, 12 + 12 + WINS.length*(COL_W+2))))}`)
1118
+ emit();
1119
+
1120
+ const COL_W = 11;
1121
+ const hcols = [
1122
+ padEnd(dim("Source"), 12),
1123
+ padEnd(dim("Pillar"), 10),
1124
+ ...WINS.map((wn) => padStart(dim(wn), COL_W)),
1125
+ ];
1126
+ emit(` ${hcols.join(" ")}`);
1127
+ emit(
1128
+ ` ${dim("·".repeat(Math.min(w - 4, 12 + 12 + WINS.length * (COL_W + 2))))}`,
1129
+ );
784
1130
 
785
1131
  const PILLARS = [
786
- { key: 'input', label: 'Input' },
787
- { key: 'output', label: 'Output' },
788
- { key: 'cacheWrite', label: 'CacheW', dbKey: 'cacheCreate' },
789
- { key: 'cacheRead', label: 'CacheR' },
790
- ]
1132
+ { key: "input", label: "Input" },
1133
+ { key: "output", label: "Output" },
1134
+ { key: "cacheWrite", label: "CacheW", dbKey: "cacheCreate" },
1135
+ { key: "cacheRead", label: "CacheR" },
1136
+ ];
791
1137
 
792
1138
  // Reserve space for cascade metrics below (~8 lines)
793
- const metricsLines = 6 + SOURCES.length
1139
+ const metricsLines = 6 + SOURCES.length;
794
1140
  for (const src of SOURCES) {
795
- let firstRow = true
1141
+ let firstRow = true;
796
1142
  for (const pil of PILLARS) {
797
- if (used >= budget - metricsLines) break
798
- const dbKey = pil.dbKey ?? pil.key
799
- const cells = WINS.map(win => {
800
- const p = src.pillars[win]
801
- const val = p?.[dbKey] ?? null
802
- if (val == null) return padStart(dim('—'), COL_W)
803
- const baseVal = tpPillars[win]?.[dbKey]
804
- if (src.name !== 'tokenpull' && baseVal != null && baseVal > 0) {
805
- const delta = (val - baseVal) / baseVal * 100
806
- const dStr = delta === 0 ? '' : delta > 0 ? green(` +${delta.toFixed(0)}%`) : red(` ${delta.toFixed(0)}%`)
807
- return padStart(`${fmtTok(val)}${dStr}`, COL_W)
1143
+ if (used >= budget - metricsLines) break;
1144
+ const dbKey = pil.dbKey ?? pil.key;
1145
+ const cells = WINS.map((win) => {
1146
+ const p = src.pillars[win];
1147
+ const val = p?.[dbKey] ?? null;
1148
+ if (val == null) return padStart(dim("—"), COL_W);
1149
+ const baseVal = tpPillars[win]?.[dbKey];
1150
+ if (src.name !== "tokenpull" && baseVal != null && baseVal > 0) {
1151
+ const delta = ((val - baseVal) / baseVal) * 100;
1152
+ const dStr =
1153
+ delta === 0
1154
+ ? ""
1155
+ : delta > 0
1156
+ ? green(` +${delta.toFixed(0)}%`)
1157
+ : red(` ${delta.toFixed(0)}%`);
1158
+ return padStart(`${fmtTok(val)}${dStr}`, COL_W);
808
1159
  }
809
- return padStart(fmtTok(val), COL_W)
810
- })
811
- const srcLabel = firstRow ? padEnd(src.color(src.name), 12) : padEnd(dim(''), 12)
812
- emit(` ${srcLabel} ${padEnd(dim(pil.label), 10)} ${cells.join(' ')}`)
813
- firstRow = false
1160
+ return padStart(fmtTok(val), COL_W);
1161
+ });
1162
+ const srcLabel = firstRow
1163
+ ? padEnd(src.color(src.name), 12)
1164
+ : padEnd(dim(""), 12);
1165
+ emit(
1166
+ ` ${srcLabel} ${padEnd(dim(pil.label), 10)} ${cells.join(" ")}`,
1167
+ );
1168
+ firstRow = false;
814
1169
  }
815
- emit()
1170
+ emit();
816
1171
  }
817
1172
 
818
1173
  if (used < budget - 4) {
819
- emit(` ${hr()}`)
820
- emit(` ${bold('Cascade Metrics')} ${dim('all-time · computed from each source')}`)
821
- const MCH = [padEnd(hdr('Source'),12), padStart(hdr('Υ Yield'),9), padStart(hdr('SNR'),7), padStart(hdr('Lev'),8), padStart(hdr('Vel'),6), padStart(hdr('10x'),6), padEnd(hdr('Class'),13)]
822
- emit(` ${MCH.join(' ')}`)
823
- emit(` ${dim('·'.repeat(Math.min(w-4, 72)))}`)
1174
+ emit(` ${hr()}`);
1175
+ emit(
1176
+ ` ${bold("Cascade Metrics")} ${dim("all-time · computed from each source")}`,
1177
+ );
1178
+ const MCH = [
1179
+ padEnd(hdr("Source"), 12),
1180
+ padStart(hdr("Υ Yield"), 9),
1181
+ padStart(hdr("SNR"), 7),
1182
+ padStart(hdr("Lev"), 8),
1183
+ padStart(hdr("Vel"), 6),
1184
+ padStart(hdr("10x"), 6),
1185
+ padEnd(hdr("Class"), 13),
1186
+ ];
1187
+ emit(` ${MCH.join(" ")}`);
1188
+ emit(` ${dim("·".repeat(Math.min(w - 4, 72)))}`);
824
1189
  for (const src of SOURCES) {
825
- if (used >= budget) break
826
- const p = src.pillars['all']
827
- if (!p) continue
828
- const cas = cascadeFrom(p)
829
- if (!cas) continue
830
- const clsFn = CLS[cas.class] ?? ((s) => s)
1190
+ if (used >= budget) break;
1191
+ const p = src.pillars["all"];
1192
+ if (!p) continue;
1193
+ const cas = cascadeFrom(p);
1194
+ if (!cas) continue;
1195
+ const clsFn = CLS[cas.class] ?? ((s) => s);
831
1196
  const cols = [
832
1197
  padEnd(src.color(src.name), 12),
833
- padStart(cas.yield > 10000 ? gold(fmtY(cas.yield)) : fmtY(cas.yield), 9),
1198
+ padStart(
1199
+ cas.yield > 10000 ? gold(fmtY(cas.yield)) : fmtY(cas.yield),
1200
+ 9,
1201
+ ),
834
1202
  padStart(fmtSNR(cas.snr), 7),
835
- padStart(fmtLev(cas.leverage)+'×', 8),
836
- padStart(cas.velocity?.toFixed(2) ?? '—', 6),
837
- padStart(cas.dev10x?.toFixed(2) ?? '—', 6),
1203
+ padStart(fmtLev(cas.leverage) + "×", 8),
1204
+ padStart(cas.velocity?.toFixed(2) ?? "—", 6),
1205
+ padStart(cas.dev10x?.toFixed(2) ?? "—", 6),
838
1206
  padEnd(clsFn(cas.class), 13),
839
- ]
840
- emit(` ${cols.join(' ')}`)
1207
+ ];
1208
+ emit(` ${cols.join(" ")}`);
841
1209
  }
842
1210
  }
843
1211
  }
@@ -847,77 +1215,124 @@ function renderCompare(data) {
847
1215
  // aggregates. Columns: rank · codename · platform · window · Υ Yield · op_ratio · class · tokens.
848
1216
  // Field names are read defensively (the endpoint is new) so a slightly different server shape still
849
1217
  // renders. [Y] "just me" filtering + the you-highlight carry over from the leaderboard view.
850
- function renderSubmissions(boardData, window = '30d', filterCodename = null, highlightCodename = null) {
851
- const rows = boardData?.submissions ?? boardData?.entries ?? boardData?.rows ??
852
- (Array.isArray(boardData) ? boardData : [])
853
- const w = W()
854
- const budget = H() - 4
855
- let used = 0
856
- const emit = (s = '') => { if (used < budget) { writeln(s); used++ } }
1218
+ function renderSubmissions(
1219
+ boardData,
1220
+ window = "30d",
1221
+ filterCodename = null,
1222
+ highlightCodename = null,
1223
+ ) {
1224
+ const rows =
1225
+ boardData?.submissions ??
1226
+ boardData?.entries ??
1227
+ boardData?.rows ??
1228
+ (Array.isArray(boardData) ? boardData : []);
1229
+ const w = W();
1230
+ const budget = H() - 4;
1231
+ let used = 0;
1232
+ const emit = (s = "") => {
1233
+ if (used < budget) {
1234
+ writeln(s);
1235
+ used++;
1236
+ }
1237
+ };
857
1238
 
858
1239
  // Defensive field readers (server shape is new / not yet pinned).
859
- const gName = (e) => e.display_name || e.codename || e.operator_codename || '—'
860
- const gPlat = (e) => e.platform ?? e.platform_primary ?? '—'
861
- const gWin = (e) => e.window ?? e.window_type ?? '—'
862
- const gYield = (e) => e.yield_ ?? e.yield ?? e.y ?? null
863
- const gOpRatio = (e) => e.op_ratio ?? e.operating_ratio ?? e.opRatio ?? null
864
- const gClass = (e) => e.class_tier ?? e.class ?? e.class_ ?? '—'
865
- const gTokens = (e) => e.tokens ?? e.total_tokens ?? e.token_total ?? null
866
-
867
- const modeLabel = filterCodename ? `${dim(' · just you')}` : highlightCodename ? `${dim(' · you highlighted')}` : ''
868
- emit()
869
- emit(` ${bold('Submissions')} ${dim(`window: ${window} · all submissions ranked by Υ Yield · signalaf.com`)}${modeLabel}`)
870
- emit()
1240
+ const gName = (e) =>
1241
+ e.display_name || e.codename || e.operator_codename || "—";
1242
+ const gPlat = (e) => e.platform ?? e.platform_primary ?? "—";
1243
+ const gWin = (e) => e.window ?? e.window_type ?? "—";
1244
+ const gYield = (e) => e.yield_ ?? e.yield ?? e.y ?? null;
1245
+ const gOpRatio = (e) => e.op_ratio ?? e.operating_ratio ?? e.opRatio ?? null;
1246
+ const gClass = (e) => e.class_tier ?? e.class ?? e.class_ ?? "—";
1247
+ const gTokens = (e) => e.tokens ?? e.total_tokens ?? e.token_total ?? null;
1248
+
1249
+ const modeLabel = filterCodename
1250
+ ? `${dim(" · just you")}`
1251
+ : highlightCodename
1252
+ ? `${dim(" · you highlighted")}`
1253
+ : "";
1254
+ emit();
1255
+ emit(
1256
+ ` ${bold("Submissions")} ${dim(`window: ${window} · all submissions ranked by Υ Yield · signalaf.com`)}${modeLabel}`,
1257
+ );
1258
+ emit();
871
1259
 
872
1260
  if (!Array.isArray(rows) || rows.length === 0) {
873
- emit(` ${dim(' no submissions for this window')}`)
874
- return
1261
+ emit(` ${dim(" no submissions for this window")}`);
1262
+ return;
875
1263
  }
876
1264
 
877
- let sorted = [...rows].sort((a, b) => (gYield(b) ?? 0) - (gYield(a) ?? 0))
1265
+ let sorted = [...rows].sort((a, b) => (gYield(b) ?? 0) - (gYield(a) ?? 0));
878
1266
  if (filterCodename) {
879
- sorted = sorted.filter(e => (gName(e) ?? '').toLowerCase() === filterCodename.toLowerCase())
1267
+ sorted = sorted.filter(
1268
+ (e) => (gName(e) ?? "").toLowerCase() === filterCodename.toLowerCase(),
1269
+ );
880
1270
  if (sorted.length === 0) {
881
- emit(` ${dim(` you have no submissions in the ${window} window yet`)}`)
882
- emit(` ${dim(' press [S] from a read tab to submit your cascade')}`)
883
- return
1271
+ emit(` ${dim(` you have no submissions in the ${window} window yet`)}`);
1272
+ emit(` ${dim(" press [S] from a read tab to submit your cascade")}`);
1273
+ return;
884
1274
  }
885
1275
  }
886
1276
 
887
1277
  // op_ratio may arrive as a preformatted string ("3.5 : 1 : 0.50") or a number — render either.
888
- const fmtOp = (v) => v == null ? '—' : (typeof v === 'string' ? v : (typeof v === 'number' ? v.toFixed(2) : String(v)))
1278
+ const fmtOp = (v) =>
1279
+ v == null
1280
+ ? "—"
1281
+ : typeof v === "string"
1282
+ ? v
1283
+ : typeof v === "number"
1284
+ ? v.toFixed(2)
1285
+ : String(v);
889
1286
 
890
1287
  const SH = [
891
- padStart(hdr('#'), 4), padEnd(hdr('Codename'), 20), padEnd(hdr('Platform'), 10),
892
- padEnd(hdr('Win'), 6), padStart(hdr('Υ Yield'), 9), padEnd(hdr('Op Ratio'), 18),
893
- padEnd(hdr('Class'), 12), padStart(hdr('Tokens'), 9),
894
- ]
895
- emit(` ${SH.join(' ')}`)
896
- emit(` ${dim('·'.repeat(Math.min(w - 4, 100)))}`)
897
-
898
- let shown = 0
1288
+ padStart(hdr("#"), 4),
1289
+ padEnd(hdr("Codename"), 20),
1290
+ padEnd(hdr("Platform"), 10),
1291
+ padEnd(hdr("Win"), 6),
1292
+ padStart(hdr("Υ Yield"), 9),
1293
+ padEnd(hdr("Op Ratio"), 18),
1294
+ padEnd(hdr("Class"), 12),
1295
+ padStart(hdr("Tokens"), 9),
1296
+ ];
1297
+ emit(` ${SH.join(" ")}`);
1298
+ emit(` ${dim("·".repeat(Math.min(w - 4, 100)))}`);
1299
+
1300
+ let shown = 0;
899
1301
  for (let idx = 0; idx < sorted.length; idx++) {
900
- if (used >= budget) break
901
- const e = sorted[idx]
902
- const isYou = highlightCodename && (gName(e) ?? '').toLowerCase() === highlightCodename.toLowerCase()
903
- const rk = idx === 0 ? gold(`#${idx+1}`) : idx < 3 ? cyan(`#${idx+1}`) : `#${idx+1}`
904
- const nmRaw = trunc(gName(e), 20)
1302
+ if (used >= budget) break;
1303
+ const e = sorted[idx];
1304
+ const isYou =
1305
+ highlightCodename &&
1306
+ (gName(e) ?? "").toLowerCase() === highlightCodename.toLowerCase();
1307
+ const rk =
1308
+ idx === 0
1309
+ ? gold(`#${idx + 1}`)
1310
+ : idx < 3
1311
+ ? cyan(`#${idx + 1}`)
1312
+ : `#${idx + 1}`;
1313
+ const nmRaw = trunc(gName(e), 20);
905
1314
  const nm = isYou
906
1315
  ? `${c.bgCyan}${c.boldCyan}${padEnd(` ${trunc(gName(e), 18)} `, 20)}${c.reset}`
907
- : padEnd(nmRaw, 20)
908
- const yv = gYield(e)
909
- const yld = padStart(yv != null ? (yv > 10000 ? gold(fmtY(yv)) : fmtY(yv)) : '—', 9)
910
- const cls = padEnd(colorCls(gClass(e)), 12)
911
- const tk = gTokens(e)
912
- const youMark = isYou ? ` ${c.bgCyan}${c.boldCyan}YOU${c.reset}` : ''
913
- emit(` ${padStart(rk, 4)} ${nm} ${padEnd(cyan(trunc(gPlat(e), 10)), 10)} ${padEnd(dim(trunc(gWin(e), 6)), 6)} ${yld} ${padEnd(dim(fmtOp(gOpRatio(e))), 18)} ${cls} ${padStart(tk != null ? fmtTok(tk) : '—', 9)}${youMark}`)
914
- shown++
1316
+ : padEnd(nmRaw, 20);
1317
+ const yv = gYield(e);
1318
+ const yld = padStart(
1319
+ yv != null ? (yv > 10000 ? gold(fmtY(yv)) : fmtY(yv)) : "—",
1320
+ 9,
1321
+ );
1322
+ const cls = padEnd(colorCls(gClass(e)), 12);
1323
+ const tk = gTokens(e);
1324
+ const youMark = isYou ? ` ${c.bgCyan}${c.boldCyan}YOU${c.reset}` : "";
1325
+ emit(
1326
+ ` ${padStart(rk, 4)} ${nm} ${padEnd(cyan(trunc(gPlat(e), 10)), 10)} ${padEnd(dim(trunc(gWin(e), 6)), 6)} ${yld} ${padEnd(dim(fmtOp(gOpRatio(e))), 18)} ${cls} ${padStart(tk != null ? fmtTok(tk) : "—", 9)}${youMark}`,
1327
+ );
1328
+ shown++;
915
1329
  }
916
1330
  // A3-style: if the height budget dropped rows, say so instead of silently truncating.
917
1331
  // SCROLL-VIEW (2026-06-27): changed "resize / scroll" → "resize to see more"
918
1332
  // (Board tab doesn't have a real scroll viewport — only Dashboard cascade does).
919
- const dropped = sorted.length - shown
920
- if (dropped > 0 && used < budget) emit(` ${dim(` +${dropped} more — resize to see more`)}`)
1333
+ const dropped = sorted.length - shown;
1334
+ if (dropped > 0 && used < budget)
1335
+ emit(` ${dim(` +${dropped} more — resize to see more`)}`);
921
1336
  }
922
1337
 
923
1338
  // ── TAB 3: BOARD ─────────────────────────────────────────────────────────────
@@ -925,91 +1340,152 @@ function renderSubmissions(boardData, window = '30d', filterCodename = null, hig
925
1340
  // ONLY the signed-in operator's rows. `highlightCodename` (non-null = global mode)
926
1341
  // highlights the signed-in operator's row in the global board. Both come from the
927
1342
  // signed-in identity's codename; the [Y] key toggles between the two modes.
928
- function renderBoard(boardData, window = '30d', filterCodename = null, highlightCodename = null) {
1343
+ function renderBoard(
1344
+ boardData,
1345
+ window = "30d",
1346
+ filterCodename = null,
1347
+ highlightCodename = null,
1348
+ ) {
929
1349
  // #8 (2026-06-27): when the data came from the NEW /api/v1/submissions endpoint, render ALL
930
1350
  // submissions ranked (raw rows). Otherwise fall through to the existing leaderboard render
931
1351
  // (pre-deploy fallback). loadBoardData tags the payload with `_source`.
932
- if (boardData?._source === 'submissions') {
933
- renderSubmissions(boardData, window, filterCodename, highlightCodename)
934
- return
1352
+ if (boardData?._source === "submissions") {
1353
+ renderSubmissions(boardData, window, filterCodename, highlightCodename);
1354
+ return;
935
1355
  }
936
- const entries = boardData?.entries ?? boardData?.operators ?? boardData ?? []
937
- const w = W()
938
- const budget = H() - 4
939
- let used = 0
940
- const emit = (s = '') => { if (used < budget) { writeln(s); used++ } }
941
-
942
- const modeLabel = filterCodename ? `${dim(' · just you')}` : highlightCodename ? `${dim(' · you highlighted')}` : ''
943
- emit()
944
- emit(` ${bold('Leaderboard')} ${dim(`window: ${window} · sorted by Υ Yield · signalaf.com/leaderboard`)}${modeLabel}`)
945
- emit()
1356
+ const entries = boardData?.entries ?? boardData?.operators ?? boardData ?? [];
1357
+ const w = W();
1358
+ const budget = H() - 4;
1359
+ let used = 0;
1360
+ const emit = (s = "") => {
1361
+ if (used < budget) {
1362
+ writeln(s);
1363
+ used++;
1364
+ }
1365
+ };
1366
+
1367
+ const modeLabel = filterCodename
1368
+ ? `${dim(" · just you")}`
1369
+ : highlightCodename
1370
+ ? `${dim(" · you highlighted")}`
1371
+ : "";
1372
+ emit();
1373
+ emit(
1374
+ ` ${bold("Leaderboard")} ${dim(`window: ${window} · sorted by Υ Yield · signalaf.com/leaderboard`)}${modeLabel}`,
1375
+ );
1376
+ emit();
946
1377
 
947
1378
  if (!Array.isArray(entries) || entries.length === 0) {
948
- emit(` ${dim(' board unavailable')}`)
949
- return
1379
+ emit(` ${dim(" board unavailable")}`);
1380
+ return;
950
1381
  }
951
1382
 
952
1383
  // FIX I2: "just me" mode — filter to the signed-in operator's rows only.
953
- let sorted = [...entries].sort((a, b) => (b.yield_ ?? 0) - (a.yield_ ?? 0))
1384
+ let sorted = [...entries].sort((a, b) => (b.yield_ ?? 0) - (a.yield_ ?? 0));
954
1385
  if (filterCodename) {
955
- sorted = sorted.filter(e => (e.codename ?? '').toLowerCase() === filterCodename.toLowerCase())
1386
+ sorted = sorted.filter(
1387
+ (e) => (e.codename ?? "").toLowerCase() === filterCodename.toLowerCase(),
1388
+ );
956
1389
  if (sorted.length === 0) {
957
- emit(` ${dim(` you have no ranked rows in the ${window} window yet`)}`)
958
- emit(` ${dim(' press [S] from a read tab to submit your cascade')}`)
959
- return
1390
+ emit(` ${dim(` you have no ranked rows in the ${window} window yet`)}`);
1391
+ emit(` ${dim(" press [S] from a read tab to submit your cascade")}`);
1392
+ return;
960
1393
  }
961
1394
  }
962
1395
 
963
1396
  // Per-column top-3 "shadowbox": medal bg-tint (gold/silver/bronze) on the 1st/2nd/3rd
964
1397
  // best DISTINCT value in each metric column, mirroring the website's podium boxes.
965
- const MEDAL_BG = { 1: 220, 2: 250, 3: 130 } // gold · silver · bronze (256-color)
1398
+ const MEDAL_BG = { 1: 220, 2: 250, 3: 130 }; // gold · silver · bronze (256-color)
966
1399
  const top3 = (pick) => {
967
- const distinct = [...new Set(sorted.map(pick).filter((v) => v != null && isFinite(v)))].sort((a, b) => b - a)
968
- const m = new Map(); distinct.slice(0, 3).forEach((v, i) => m.set(v, i + 1)); return m
969
- }
1400
+ const distinct = [
1401
+ ...new Set(sorted.map(pick).filter((v) => v != null && isFinite(v))),
1402
+ ].sort((a, b) => b - a);
1403
+ const m = new Map();
1404
+ distinct.slice(0, 3).forEach((v, i) => m.set(v, i + 1));
1405
+ return m;
1406
+ };
970
1407
  const pods = {
971
- yield: top3((e) => e.yield_), snr: top3((e) => e.snr ?? e.compression_ratio),
972
- lev: top3((e) => e.leverage), vel: top3((e) => e.velocity), d10: top3((e) => e.dev10x),
973
- }
1408
+ yield: top3((e) => e.yield_),
1409
+ snr: top3((e) => e.snr ?? e.compression_ratio),
1410
+ lev: top3((e) => e.leverage),
1411
+ vel: top3((e) => e.velocity),
1412
+ d10: top3((e) => e.dev10x),
1413
+ };
974
1414
  // Wrap a padded cell in a medal bg-tint (dark fg on the medal color) when its value places top-3.
975
1415
  const medal = (map, val, padded) => {
976
- const place = val == null ? null : map.get(val)
977
- if (!place) return padded
978
- return `${ESC}48;5;${MEDAL_BG[place]}m${ESC}38;5;232m${padded}${c.reset}`
979
- }
1416
+ const place = val == null ? null : map.get(val);
1417
+ if (!place) return padded;
1418
+ return `${ESC}48;5;${MEDAL_BG[place]}m${ESC}38;5;232m${padded}${c.reset}`;
1419
+ };
980
1420
 
981
1421
  const BH = [
982
- padStart(hdr('#'), 4), padEnd(hdr('Codename'), 22), padEnd(hdr('Class'), 13),
983
- padStart(hdr('Υ Yield'), 9), padStart(hdr('SNR'), 7), padStart(hdr('Lev'), 7),
984
- padStart(hdr('Vel'), 6), padStart(hdr('10x'), 6), padStart(hdr('Pct'), 5), padStart(hdr('7d↕'), 5),
985
- ]
986
- emit(` ${BH.join(' ')}`)
987
- emit(` ${dim('·'.repeat(Math.min(w-4, 98)))}`)
1422
+ padStart(hdr("#"), 4),
1423
+ padEnd(hdr("Codename"), 22),
1424
+ padEnd(hdr("Class"), 13),
1425
+ padStart(hdr("Υ Yield"), 9),
1426
+ padStart(hdr("SNR"), 7),
1427
+ padStart(hdr("Lev"), 7),
1428
+ padStart(hdr("Vel"), 6),
1429
+ padStart(hdr("10x"), 6),
1430
+ padStart(hdr("Pct"), 5),
1431
+ padStart(hdr("7d↕"), 5),
1432
+ ];
1433
+ emit(` ${BH.join(" ")}`);
1434
+ emit(` ${dim("·".repeat(Math.min(w - 4, 98)))}`);
988
1435
 
989
1436
  for (let idx = 0; idx < sorted.length; idx++) {
990
- if (used >= budget) break
991
- const e = sorted[idx]
1437
+ if (used >= budget) break;
1438
+ const e = sorted[idx];
992
1439
  // FIX I2: highlight the signed-in operator's row (global mode) with a cyan bg-tint.
993
- const isYou = highlightCodename && (e.codename ?? '').toLowerCase() === highlightCodename.toLowerCase()
994
- const rk = idx === 0 ? gold(`#${idx+1}`) : idx < 3 ? cyan(`#${idx+1}`) : `#${idx+1}`
995
- const nmRaw = trunc(e.display_name || e.codename || '—', 22)
1440
+ const isYou =
1441
+ highlightCodename &&
1442
+ (e.codename ?? "").toLowerCase() === highlightCodename.toLowerCase();
1443
+ const rk =
1444
+ idx === 0
1445
+ ? gold(`#${idx + 1}`)
1446
+ : idx < 3
1447
+ ? cyan(`#${idx + 1}`)
1448
+ : `#${idx + 1}`;
1449
+ const nmRaw = trunc(e.display_name || e.codename || "—", 22);
996
1450
  // FIX I2: pad the visible content INSIDE the ANSI codes so the cyan bg fills
997
1451
  // the full column width (padEnd after reset would leave uncolored trailing spaces).
998
- const nm = isYou
1452
+ const nm = isYou
999
1453
  ? `${c.bgCyan}${c.boldCyan}${padEnd(` ${nmRaw} `, 22)}${c.reset}`
1000
- : padEnd(nmRaw, 22)
1001
- const cls = padEnd(colorCls(e.class_tier ?? '—'), 13)
1002
- const snrVal = e.snr ?? e.compression_ratio
1454
+ : padEnd(nmRaw, 22);
1455
+ const cls = padEnd(colorCls(e.class_tier ?? "—"), 13);
1456
+ const snrVal = e.snr ?? e.compression_ratio;
1003
1457
  // value cells: pad first, then medal-wrap so the tint fills the whole column width
1004
- const yld = medal(pods.yield, e.yield_, padStart(e.yield_ != null ? fmtY(e.yield_) : '—', 9))
1005
- const snr = medal(pods.snr, snrVal, padStart(snrVal != null ? fmtSNR(snrVal) : '—', 7))
1006
- const lev = medal(pods.lev, e.leverage, padStart(e.leverage != null ? fmtLev(e.leverage)+'×' : '—', 7))
1007
- const vel = medal(pods.vel, e.velocity, padStart(e.velocity != null ? e.velocity.toFixed(2) : '—', 6))
1008
- const d10 = medal(pods.d10, e.dev10x, padStart(e.dev10x != null ? e.dev10x.toFixed(2) : '—', 6))
1009
- const pct = padStart(e.percentile != null ? `${e.percentile}%` : '—', 5)
1010
- const mv = padStart(fmtMov(e.movement_7d), 5)
1011
- const youMark = isYou ? ` ${c.bgCyan}${c.boldCyan}YOU${c.reset}` : ''
1012
- emit(` ${padStart(rk,4)} ${nm} ${cls} ${yld} ${snr} ${lev} ${vel} ${d10} ${pct} ${mv}${youMark}`)
1458
+ const yld = medal(
1459
+ pods.yield,
1460
+ e.yield_,
1461
+ padStart(e.yield_ != null ? fmtY(e.yield_) : "—", 9),
1462
+ );
1463
+ const snr = medal(
1464
+ pods.snr,
1465
+ snrVal,
1466
+ padStart(snrVal != null ? fmtSNR(snrVal) : "—", 7),
1467
+ );
1468
+ const lev = medal(
1469
+ pods.lev,
1470
+ e.leverage,
1471
+ padStart(e.leverage != null ? fmtLev(e.leverage) + "×" : "—", 7),
1472
+ );
1473
+ const vel = medal(
1474
+ pods.vel,
1475
+ e.velocity,
1476
+ padStart(e.velocity != null ? e.velocity.toFixed(2) : "—", 6),
1477
+ );
1478
+ const d10 = medal(
1479
+ pods.d10,
1480
+ e.dev10x,
1481
+ padStart(e.dev10x != null ? e.dev10x.toFixed(2) : "—", 6),
1482
+ );
1483
+ const pct = padStart(e.percentile != null ? `${e.percentile}%` : "—", 5);
1484
+ const mv = padStart(fmtMov(e.movement_7d), 5);
1485
+ const youMark = isYou ? ` ${c.bgCyan}${c.boldCyan}YOU${c.reset}` : "";
1486
+ emit(
1487
+ ` ${padStart(rk, 4)} ${nm} ${cls} ${yld} ${snr} ${lev} ${vel} ${d10} ${pct} ${mv}${youMark}`,
1488
+ );
1013
1489
  }
1014
1490
  }
1015
1491
 
@@ -1021,57 +1497,90 @@ function renderBoard(boardData, window = '30d', filterCodename = null, highlight
1021
1497
  // change). `platform`/`win` are OPTIONAL focus filters — null/'all' means "everything". [P]/[W]
1022
1498
  // cycle the focus (including back to "all"); the spawned watcher + this copy show everything by default.
1023
1499
  function renderWatchInfo(platform, win, refresh) {
1024
- const platLabel = (!platform || platform === 'all') ? 'all active platforms' : platform
1025
- const winLabel = (!win || win === 'all-windows') ? 'all windows' : win
1026
- const allDefault = platLabel === 'all active platforms' && winLabel === 'all windows'
1027
- writeln()
1028
- writeln(` ${bold('Live Watch')} ${dim('the agent that keeps your rank current')}`)
1029
- writeln()
1030
- writeln(` ${dim('What it does')}`)
1031
- writeln(` Re-reads your local token logs for ${cyan(platLabel)} across ${cyan(winLabel)} every`)
1032
- writeln(` ${gold(refresh + 's')} and recomputes each cascade (Υ Yield · SNR · Leverage · class) live, on`)
1033
- writeln(` this machine. ${dim('By default it watches everything — every platform you actually use.')}`)
1034
- writeln()
1035
- writeln(` ${dim('Why it matters')}`)
1036
- writeln(` Watch is how the board stays ${bold('current')}: each refresh submits your latest`)
1037
- writeln(` verified cascade so ${cyan('signalaf.com/leaderboard')} ${bold('auto-updates')} as you work —`)
1038
- writeln(` no manual re-submit. Tokens never leave your machine; only the metrics post.`)
1039
- writeln()
1040
- writeln(` ${dim('Settings')}`)
1041
- writeln(` ${dim('Platform')} ${cyan(platLabel)} ${dim('Window')} ${cyan(winLabel)} ${dim('Refresh')} ${gold(refresh + 's')}`)
1042
- writeln(` ${dim(allDefault ? 'watching everything (default)' : 'focused — press [P]/[W] to widen back to all')}`)
1043
- writeln()
1044
- writeln(` ${dim('[Enter]')} launch watcher (new window) ${dim('[+]/[-]')} refresh ±5s ${dim('[P]')} platform focus ${dim('[W]')} window focus`)
1045
- writeln()
1500
+ const platLabel =
1501
+ !platform || platform === "all" ? "all active platforms" : platform;
1502
+ const winLabel = !win || win === "all-windows" ? "all windows" : win;
1503
+ const allDefault =
1504
+ platLabel === "all active platforms" && winLabel === "all windows";
1505
+ writeln();
1506
+ writeln(
1507
+ ` ${bold("Live Watch")} ${dim("the agent that keeps your rank current")}`,
1508
+ );
1509
+ writeln();
1510
+ writeln(` ${dim("What it does")}`);
1511
+ writeln(
1512
+ ` Re-reads your local token logs for ${cyan(platLabel)} across ${cyan(winLabel)} every`,
1513
+ );
1514
+ writeln(
1515
+ ` ${gold(refresh + "s")} and recomputes each cascade (Υ Yield · SNR · Leverage · class) live, on`,
1516
+ );
1517
+ writeln(
1518
+ ` this machine. ${dim("By default it watches everything — every platform you actually use.")}`,
1519
+ );
1520
+ writeln();
1521
+ writeln(` ${dim("Why it matters")}`);
1522
+ writeln(
1523
+ ` Watch is how the board stays ${bold("current")}: each refresh submits your latest`,
1524
+ );
1525
+ writeln(
1526
+ ` verified cascade so ${cyan("signalaf.com/leaderboard")} ${bold("auto-updates")} as you work —`,
1527
+ );
1528
+ writeln(
1529
+ ` no manual re-submit. Tokens never leave your machine; only the metrics post.`,
1530
+ );
1531
+ writeln();
1532
+ writeln(` ${dim("Settings")}`);
1533
+ writeln(
1534
+ ` ${dim("Platform")} ${cyan(platLabel)} ${dim("Window")} ${cyan(winLabel)} ${dim("Refresh")} ${gold(refresh + "s")}`,
1535
+ );
1536
+ writeln(
1537
+ ` ${dim(allDefault ? "watching everything (default)" : "focused — press [P]/[W] to widen back to all")}`,
1538
+ );
1539
+ writeln();
1540
+ writeln(
1541
+ ` ${dim("[Enter]")} launch watcher (new window) ${dim("[+]/[-]")} refresh ±5s ${dim("[P]")} platform focus ${dim("[W]")} window focus`,
1542
+ );
1543
+ writeln();
1046
1544
  }
1047
1545
 
1048
1546
  // ── TAB 6: CONNECT ───────────────────────────────────────────────────────────
1049
1547
  // Sign in (paste a connect code) / show signed-in status. The whole app lives in
1050
1548
  // the TUI; this replaces every "go run the CLI to enroll" hint. Uses only helpers
1051
1549
  // already defined above (bold/dim/cyan/green/red/hr/writeln).
1052
- function renderConnect(id, codeBuf = '', msg = '') {
1053
- writeln()
1550
+ function renderConnect(id, codeBuf = "", msg = "") {
1551
+ writeln();
1054
1552
  if (isSignedIn(id)) {
1055
- writeln(` ${bold('Connect')} ${dim('your account')}`)
1056
- writeln(` ${hr()}`)
1057
- writeln(` ${green('✓')} Signed in as ${cyan(id.codename)}`)
1058
- writeln(` ${dim(`device ${id.device_id}`)}`)
1059
- writeln()
1060
- writeln(` ${dim('Press')} ${bold('[S]')} ${dim('from any read tab to submit your runs to the board.')}`)
1061
- writeln()
1062
- writeln(` ${dim('Need a new key? Click "New key" at signalaf.com → Settings, then paste it here.')}`)
1063
- writeln(` ${dim('Signed in on the wrong device, or want a fresh start?')} ${bold('[X]')} ${dim('signs out — next paste provisions a fresh device.')}`)
1553
+ writeln(` ${bold("Connect")} ${dim("your account")}`);
1554
+ writeln(` ${hr()}`);
1555
+ writeln(` ${green("✓")} Signed in as ${cyan(id.codename)}`);
1556
+ writeln(` ${dim(`device ${id.device_id}`)}`);
1557
+ writeln();
1558
+ writeln(
1559
+ ` ${dim("Press")} ${bold("[S]")} ${dim("from any read tab to submit your runs to the board.")}`,
1560
+ );
1561
+ writeln();
1562
+ writeln(
1563
+ ` ${dim('Need a new key? Click "New key" at signalaf.com → Settings, then paste it here.')}`,
1564
+ );
1565
+ writeln(
1566
+ ` ${dim("Signed in on the wrong device, or want a fresh start?")} ${bold("[X]")} ${dim("signs out — next paste provisions a fresh device.")}`,
1567
+ );
1064
1568
  } else {
1065
- writeln(` ${bold('Log in to submit to board')}`)
1066
- writeln(` ${hr()}`)
1067
- writeln(` ${dim('status:')} ${red('not signed in')}`)
1068
- writeln()
1069
- writeln(` ${dim('Paste your key, then')} ${bold('[Enter]')}${dim(':')}`)
1569
+ writeln(` ${bold("Log in to submit to board")}`);
1570
+ writeln(` ${hr()}`);
1571
+ writeln(` ${dim("status:")} ${red("not signed in")}`);
1572
+ writeln();
1573
+ writeln(` ${dim("Paste your key, then")} ${bold("[Enter]")}${dim(":")}`);
1574
+ }
1575
+ writeln(` ${cyan(">")} ${codeBuf}${dim("▏")}`);
1576
+ writeln();
1577
+ writeln(
1578
+ ` ${dim('Get a key at signalaf.com → Settings → "New key" (or "Generate connect code").')}`,
1579
+ );
1580
+ if (msg) {
1581
+ writeln();
1582
+ writeln(` ${msg}`);
1070
1583
  }
1071
- writeln(` ${cyan('>')} ${codeBuf}${dim('▏')}`)
1072
- writeln()
1073
- writeln(` ${dim('Get a key at signalaf.com → Settings → "New key" (or "Generate connect code").')}`)
1074
- if (msg) { writeln(); writeln(` ${msg}`) }
1075
1584
  }
1076
1585
 
1077
1586
  // ── TAB 4: WATCH (live grid) ──────────────────────────────────────────────────
@@ -1079,59 +1588,95 @@ function renderConnect(id, codeBuf = '', msg = '') {
1079
1588
  // watch grid), instead of a single platform/window. `platform`/`win` are OPTIONAL focus filters —
1080
1589
  // null/'all'/'all-windows' means "everything". Each row is a (platform, window) cascade cell:
1081
1590
  // Υ Yield + SNR/Lev/Vel/class. Iterates active platforms (input+output > 0 in any window).
1082
- async function renderWatch(platform = 'all', win = 'all-windows') {
1083
- const { tokenpullAny } = await import('./tokenpull.mjs')
1591
+ async function renderWatch(platform = "all", win = "all-windows") {
1592
+ const { tokenpullAny } = await import("./tokenpull.mjs");
1084
1593
  const ALL_PLATFORMS = [
1085
- 'claude','codex','amp','gemini','kimi','qwen','goose','kilo',
1086
- 'hermes','droid','codebuff','copilot','openclaw','pi',
1087
- ]
1088
- const platFilter = (platform && platform !== 'all') ? platform : null
1089
- const winFilter = (win && win !== 'all-windows') ? win : null
1090
- const WINS = winFilter ? [winFilter] : ['7d', '30d', '90d', 'all']
1091
- const w = W()
1092
- const budget = H() - 4
1093
- let used = 0
1094
- const emit = (s = '') => { if (used < budget) { writeln(s); used++ } }
1594
+ "claude",
1595
+ "codex",
1596
+ "amp",
1597
+ "gemini",
1598
+ "kimi",
1599
+ "qwen",
1600
+ "goose",
1601
+ "kilo",
1602
+ "hermes",
1603
+ "droid",
1604
+ "codebuff",
1605
+ "copilot",
1606
+ "openclaw",
1607
+ "pi",
1608
+ ];
1609
+ const platFilter = platform && platform !== "all" ? platform : null;
1610
+ const winFilter = win && win !== "all-windows" ? win : null;
1611
+ const WINS = winFilter ? [winFilter] : ["7d", "30d", "90d", "all"];
1612
+ const w = W();
1613
+ const budget = H() - 4;
1614
+ let used = 0;
1615
+ const emit = (s = "") => {
1616
+ if (used < budget) {
1617
+ writeln(s);
1618
+ used++;
1619
+ }
1620
+ };
1095
1621
 
1096
1622
  // Detect active platforms (any window with input+output > 0), respecting an optional [P] focus.
1097
- const candidates = platFilter ? [platFilter] : ALL_PLATFORMS
1098
- const settled = await Promise.allSettled(candidates.map(p => tokenpullAny(p)))
1099
- const active = []
1623
+ const candidates = platFilter ? [platFilter] : ALL_PLATFORMS;
1624
+ const settled = await Promise.allSettled(
1625
+ candidates.map((p) => tokenpullAny(p)),
1626
+ );
1627
+ const active = [];
1100
1628
  for (const r of settled) {
1101
- if (r.status !== 'fulfilled' || !r.value) continue
1102
- const d = r.value
1103
- if ((d.windows || []).some(ww => (ww.pillars.input + ww.pillars.output) > 0)) active.push(d)
1629
+ if (r.status !== "fulfilled" || !r.value) continue;
1630
+ const d = r.value;
1631
+ if (
1632
+ (d.windows || []).some((ww) => ww.pillars.input + ww.pillars.output > 0)
1633
+ )
1634
+ active.push(d);
1104
1635
  }
1105
1636
  // stable display order
1106
- active.sort((a, b) => ALL_PLATFORMS.indexOf(a.platform) - ALL_PLATFORMS.indexOf(b.platform))
1107
-
1108
- const scopeLabel = (platFilter || 'all active platforms') + ' · ' + (winFilter ? `${winFilter} window` : 'all windows')
1109
- emit()
1110
- emit(` ${bold('Live Watch')} ${dim(scopeLabel)}`)
1111
- emit(` ${dim('·'.repeat(Math.min(w - 4, 80)))}`)
1637
+ active.sort(
1638
+ (a, b) =>
1639
+ ALL_PLATFORMS.indexOf(a.platform) - ALL_PLATFORMS.indexOf(b.platform),
1640
+ );
1641
+
1642
+ const scopeLabel =
1643
+ (platFilter || "all active platforms") +
1644
+ " · " +
1645
+ (winFilter ? `${winFilter} window` : "all windows");
1646
+ emit();
1647
+ emit(` ${bold("Live Watch")} ${dim(scopeLabel)}`);
1648
+ emit(` ${dim("·".repeat(Math.min(w - 4, 80)))}`);
1112
1649
 
1113
1650
  if (active.length === 0) {
1114
- emit()
1115
- emit(` ${dim(' no active platforms detected — run some sessions, this picks them up automatically')}`)
1116
- return
1651
+ emit();
1652
+ emit(
1653
+ ` ${dim(" no active platforms detected — run some sessions, this picks them up automatically")}`,
1654
+ );
1655
+ return;
1117
1656
  }
1118
1657
 
1119
1658
  for (const d of active) {
1120
- if (used >= budget - 1) break
1121
- const byWin = {}
1122
- for (const ww of (d.windows || [])) byWin[ww.window] = ww
1123
- emit()
1124
- emit(` ${cyan(d.platform)}${d.estimated ? dim(' (est)') : ''}`)
1659
+ if (used >= budget - 1) break;
1660
+ const byWin = {};
1661
+ for (const ww of d.windows || []) byWin[ww.window] = ww;
1662
+ emit();
1663
+ emit(` ${cyan(d.platform)}${d.estimated ? dim(" (est)") : ""}`);
1125
1664
  for (const wk of WINS) {
1126
- if (used >= budget - 1) break
1127
- const ww = byWin[wk]
1128
- const cas = ww ? cascadeFrom(ww.pillars) : null
1129
- const winLabel = wk === 'all' ? 'all-time' : wk
1130
- if (!cas) { emit(` ${padEnd(dim(winLabel), 9)} ${dim('no data')}`); continue }
1131
- const clsFn = CLS[cas.class] ?? ((s) => s)
1132
- const yDisplay = cas.yield > 1000 ? gold(fmtY(cas.yield)) : cyan(fmtY(cas.yield))
1133
- const metrics = `${dim('SNR')} ${fmtSNR(cas.snr)} ${dim('Lev')} ${cas.leverage != null ? fmtLev(cas.leverage) + '×' : '—'} ${dim('Vel')} ${cas.velocity != null ? cas.velocity.toFixed(2) : '—'} ${clsFn(cas.class)}`
1134
- emit(` ${padEnd(dim(winLabel), 9)} ${bold('Υ')} ${yDisplay} ${metrics}`)
1665
+ if (used >= budget - 1) break;
1666
+ const ww = byWin[wk];
1667
+ const cas = ww ? cascadeFrom(ww.pillars) : null;
1668
+ const winLabel = wk === "all" ? "all-time" : wk;
1669
+ if (!cas) {
1670
+ emit(` ${padEnd(dim(winLabel), 9)} ${dim("no data")}`);
1671
+ continue;
1672
+ }
1673
+ const clsFn = CLS[cas.class] ?? ((s) => s);
1674
+ const yDisplay =
1675
+ cas.yield > 1000 ? gold(fmtY(cas.yield)) : cyan(fmtY(cas.yield));
1676
+ const metrics = `${dim("SNR")} ${fmtSNR(cas.snr)} ${dim("Lev")} ${cas.leverage != null ? fmtLev(cas.leverage) + "×" : "—"} ${dim("Vel")} ${cas.velocity != null ? cas.velocity.toFixed(2) : "—"} ${clsFn(cas.class)}`;
1677
+ emit(
1678
+ ` ${padEnd(dim(winLabel), 9)} ${bold("Υ")} ${yDisplay} ${metrics}`,
1679
+ );
1135
1680
  }
1136
1681
  }
1137
1682
  }
@@ -1141,38 +1686,53 @@ async function renderWatch(platform = 'all', win = 'all-windows') {
1141
1686
  // already-loaded dashData. [Enter] confirms → fires submit_verified per platform;
1142
1687
  // [Esc] cancels → back to the read tab. Mirrors the Connect focused-field pattern.
1143
1688
  function renderSubmitPreview(dashData, id) {
1144
- const budget = H() - 4
1145
- let used = 0
1146
- const emit = (s = '') => { if (used < budget) { writeln(s); used++ } }
1147
- const WINS = ['7d', '30d', '90d', 'all']
1148
- const COL_W = 14
1149
-
1150
- emit()
1151
- emit(` ${bold('Submit Preview')} ${dim('what will publish to the board')}`)
1152
- emit(` ${dim(`signed in as ${id?.codename ?? '—'} · device ${id?.device_id?.slice(0,8) ?? '—'}…`)}`)
1153
- emit()
1689
+ const budget = H() - 4;
1690
+ let used = 0;
1691
+ const emit = (s = "") => {
1692
+ if (used < budget) {
1693
+ writeln(s);
1694
+ used++;
1695
+ }
1696
+ };
1697
+ const WINS = ["7d", "30d", "90d", "all"];
1698
+ const COL_W = 14;
1699
+
1700
+ emit();
1701
+ emit(` ${bold("Submit Preview")} ${dim("what will publish to the board")}`);
1702
+ emit(
1703
+ ` ${dim(`signed in as ${id?.codename ?? "—"} · device ${id?.device_id?.slice(0, 8) ?? "—"}…`)}`,
1704
+ );
1705
+ emit();
1154
1706
  // header row: platform | 7d | 30d | 90d | all
1155
- const hcols = [padEnd(hdr('Platform'), 12), ...WINS.map(w => padStart(hdr(w), COL_W))]
1156
- emit(` ${hcols.join(' ')}`)
1157
- emit(` ${dim('·'.repeat(Math.min(W() - 4, 12 + WINS.length * (COL_W + 2))))}`)
1158
-
1159
- const active = dashData?.active ?? []
1707
+ const hcols = [
1708
+ padEnd(hdr("Platform"), 12),
1709
+ ...WINS.map((w) => padStart(hdr(w), COL_W)),
1710
+ ];
1711
+ emit(` ${hcols.join(" ")}`);
1712
+ emit(
1713
+ ` ${dim("·".repeat(Math.min(W() - 4, 12 + WINS.length * (COL_W + 2))))}`,
1714
+ );
1715
+
1716
+ const active = dashData?.active ?? [];
1160
1717
  for (const d of active) {
1161
- if (used >= budget - 3) break
1162
- const cells = WINS.map(w => {
1163
- const wd = d.windows?.find(x => x.window === w)
1164
- if (!wd || (wd.pillars.input + wd.pillars.output) === 0) return padStart(dim('—'), COL_W)
1165
- const cas = cascadeFrom(wd.pillars)
1166
- if (!cas) return padStart(dim('—'), COL_W)
1167
- return padStart(gold(fmtY(cas.yield)), COL_W)
1168
- })
1169
- emit(` ${padEnd(cyan(d.platform), 12)} ${cells.join(' ')}`)
1718
+ if (used >= budget - 3) break;
1719
+ const cells = WINS.map((w) => {
1720
+ const wd = d.windows?.find((x) => x.window === w);
1721
+ if (!wd || wd.pillars.input + wd.pillars.output === 0)
1722
+ return padStart(dim("—"), COL_W);
1723
+ const cas = cascadeFrom(wd.pillars);
1724
+ if (!cas) return padStart(dim("—"), COL_W);
1725
+ return padStart(gold(fmtY(cas.yield)), COL_W);
1726
+ });
1727
+ emit(` ${padEnd(cyan(d.platform), 12)} ${cells.join(" ")}`);
1170
1728
  }
1171
1729
  if (active.length === 0) {
1172
- emit(` ${dim(' no platforms with data — press [R] to refresh')}`)
1730
+ emit(` ${dim(" no platforms with data — press [R] to refresh")}`);
1173
1731
  }
1174
- emit()
1175
- emit(` ${dim('[Enter]')} confirm + submit all platforms ${dim('[Esc]')} cancel`)
1732
+ emit();
1733
+ emit(
1734
+ ` ${dim("[Enter]")} confirm + submit all platforms ${dim("[Esc]")} cancel`,
1735
+ );
1176
1736
  }
1177
1737
 
1178
1738
  // ── DEBUG: render a tab once (no TTY/alt-screen) + audit each line's visible
@@ -1180,29 +1740,37 @@ function renderSubmitPreview(dashData, id) {
1180
1740
  // width report (>w = would overflow/wrap) then the raw frame. For diagnosing
1181
1741
  // layout overflow without an interactive session.
1182
1742
  async function renderOnce(tabIdx = 0) {
1183
- const w = W()
1743
+ const w = W();
1184
1744
  const data = {
1185
1745
  0: await loadDashboardData().catch((e) => ({ error: e.message })),
1186
- 1: await loadCompareData('claude').catch(() => null),
1187
- 2: await loadBoardData('30d').catch(() => null),
1188
- }[tabIdx]
1189
- startBuffer()
1190
- if (tabIdx === 0) renderDashboard(data, 'debug')
1191
- else if (tabIdx === 1) renderCompare(data)
1192
- else if (tabIdx === 2) renderBoard(data, '30d')
1193
- else if (tabIdx === 4) await renderWatch('all', 'all-windows')
1194
- else if (tabIdx === 5) renderConnect(loadIdentity(), '', '')
1195
- const lines = _screenBuf || []
1196
- _screenBuf = null; _footerBuf = null
1197
- process.stdout.write(`\n=== WIDTH AUDIT (terminal w=${w}) — lines exceeding w wrap/overflow ===\n`)
1746
+ 1: await loadCompareData("claude").catch(() => null),
1747
+ 2: await loadBoardData("30d").catch(() => null),
1748
+ }[tabIdx];
1749
+ startBuffer();
1750
+ if (tabIdx === 0) renderDashboard(data, "debug");
1751
+ else if (tabIdx === 1) renderCompare(data);
1752
+ else if (tabIdx === 2) renderBoard(data, "30d");
1753
+ else if (tabIdx === 4) await renderWatch("all", "all-windows");
1754
+ else if (tabIdx === 5) renderConnect(loadIdentity(), "", "");
1755
+ const lines = _screenBuf || [];
1756
+ _screenBuf = null;
1757
+ _footerBuf = null;
1758
+ process.stdout.write(
1759
+ `\n=== WIDTH AUDIT (terminal w=${w}) — lines exceeding w wrap/overflow ===\n`,
1760
+ );
1198
1761
  lines.forEach((ln, i) => {
1199
- const vis = stripAnsi(ln).length
1200
- if (vis > w) process.stdout.write(` OVERFLOW line ${i}: visible=${vis} (>${w} by ${vis - w}) «${stripAnsi(ln).slice(0, 60)}…»\n`)
1201
- })
1202
- const maxVis = Math.max(0, ...lines.map((l) => stripAnsi(l).length))
1203
- process.stdout.write(` widest visible line = ${maxVis} (terminal w=${w}) → ${maxVis > w ? 'OVERFLOWS' : 'fits'}\n`)
1204
- process.stdout.write(`\n=== RAW FRAME (stripAnsi) ===\n`)
1205
- lines.forEach((ln) => process.stdout.write(stripAnsi(ln) + '\n'))
1762
+ const vis = stripAnsi(ln).length;
1763
+ if (vis > w)
1764
+ process.stdout.write(
1765
+ ` OVERFLOW line ${i}: visible=${vis} (>${w} by ${vis - w}) «${stripAnsi(ln).slice(0, 60)}…»\n`,
1766
+ );
1767
+ });
1768
+ const maxVis = Math.max(0, ...lines.map((l) => stripAnsi(l).length));
1769
+ process.stdout.write(
1770
+ ` widest visible line = ${maxVis} (terminal w=${w}) → ${maxVis > w ? "OVERFLOWS" : "fits"}\n`,
1771
+ );
1772
+ process.stdout.write(`\n=== RAW FRAME (stripAnsi) ===\n`);
1773
+ lines.forEach((ln) => process.stdout.write(stripAnsi(ln) + "\n"));
1206
1774
  }
1207
1775
 
1208
1776
  // ── OPENING SPLASH ───────────────────────────────────────────────────────────
@@ -1211,297 +1779,373 @@ async function renderOnce(tabIdx = 0) {
1211
1779
  // mode each launch (moses gold-shimmer / rainbow / chaos per-letter). Centered to
1212
1780
  // terminal width. Any key dismisses → the dashboard loads.
1213
1781
  const SPLASH_ART = [
1214
- '███████ ██ ██████ ██████ █████ ███ ██ ██ ██',
1215
- '██ ██ ██ ██ ██ ██ ██ ████ ██ ██ ██ ',
1216
- '███████ ██ ██ ███ ██████ ███████ ██ ██ ██ █████ ',
1217
- ' ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ',
1218
- '███████ ██ ██████ ██ ██ ██ ██ ██ ████ ██ ██',
1219
- ]
1220
- const SPLASH_RULE = '◈ ─────────────────────────────────────────── ◈'
1221
- const MOSES_PAL = [220, 178, 214, 208, 222, 230]
1222
- const RAINBOW_PAL = [196, 202, 226, 46, 51, 21, 201, 213]
1223
- const col256 = (n, s) => `${ESC}38;5;${n}m${s}${c.reset}`
1224
- const stripA = (s) => s.replace(/\x1b\[[0-9;]*m/g, '')
1225
- const ctr = (s) => { const pad = Math.max(0, Math.floor((W() - stripA(s).length) / 2)); return ' '.repeat(pad) + s }
1782
+ "███████ ██ ██████ ██████ █████ ███ ██ ██ ██",
1783
+ "██ ██ ██ ██ ██ ██ ██ ████ ██ ██ ██ ",
1784
+ "███████ ██ ██ ███ ██████ ███████ ██ ██ ██ █████ ",
1785
+ " ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ",
1786
+ "███████ ██ ██████ ██ ██ ██ ██ ██ ████ ██ ██",
1787
+ ];
1788
+ const SPLASH_RULE = "◈ ─────────────────────────────────────────── ◈";
1789
+ const MOSES_PAL = [220, 178, 214, 208, 222, 230];
1790
+ const RAINBOW_PAL = [196, 202, 226, 46, 51, 21, 201, 213];
1791
+ const col256 = (n, s) => `${ESC}38;5;${n}m${s}${c.reset}`;
1792
+ const stripA = (s) => s.replace(/\x1b\[[0-9;]*m/g, "");
1793
+ const ctr = (s) => {
1794
+ const pad = Math.max(0, Math.floor((W() - stripA(s).length) / 2));
1795
+ return " ".repeat(pad) + s;
1796
+ };
1226
1797
 
1227
1798
  function splashFrame(mode) {
1228
- const pick = (a) => a[Math.floor(Math.random() * a.length)]
1229
- const out = ['', '', ctr(dim(SPLASH_RULE)), '']
1799
+ const pick = (a) => a[Math.floor(Math.random() * a.length)];
1800
+ const out = ["", "", ctr(dim(SPLASH_RULE)), ""];
1230
1801
  for (const line of SPLASH_ART) {
1231
- if (mode === 'chaos') {
1232
- out.push(ctr([...line].map((ch) => ch === ' ' ? ' ' : col256(Math.floor(Math.random() * 256), ch)).join('')))
1802
+ if (mode === "chaos") {
1803
+ out.push(
1804
+ ctr(
1805
+ [...line]
1806
+ .map((ch) =>
1807
+ ch === " " ? " " : col256(Math.floor(Math.random() * 256), ch),
1808
+ )
1809
+ .join(""),
1810
+ ),
1811
+ );
1233
1812
  } else {
1234
- const n = mode === 'rainbow' ? pick(RAINBOW_PAL) : pick(MOSES_PAL)
1235
- out.push(ctr(col256(n, line)))
1813
+ const n = mode === "rainbow" ? pick(RAINBOW_PAL) : pick(MOSES_PAL);
1814
+ out.push(ctr(col256(n, line)));
1236
1815
  }
1237
1816
  }
1238
- out.push('', ctr(dim(SPLASH_RULE)), '')
1239
- out.push(ctr(bold('For all builders, burners and 10xers')))
1240
- out.push(ctr(dim('signalaf')), '')
1241
- out.push(ctr(`${dim('powered by')} ${gold('MO§ES™')}`))
1242
- out.push(ctr(dim('$ npx sigrank')), '')
1243
- out.push(ctr(`${gold('▸')} ${dim('press any key')} ${gold('◂')}`))
1244
- return out.join('\n')
1817
+ out.push("", ctr(dim(SPLASH_RULE)), "");
1818
+ out.push(ctr(bold("For all builders, burners and 10xers")));
1819
+ out.push(ctr(dim("signalaf")), "");
1820
+ out.push(ctr(`${dim("powered by")} ${gold("MO§ES™")}`));
1821
+ out.push(ctr(dim("$ npx sigrank")), "");
1822
+ out.push(ctr(`${gold("▸")} ${dim("press any key")} ${gold("◂")}`));
1823
+ return out.join("\n");
1245
1824
  }
1246
1825
 
1247
1826
  // Animated splash. Controls: [P] pause/resume the color animation · [Enter] (or
1248
1827
  // any other key) enter the TUI · Ctrl-C quit. Mode (moses/rainbow/chaos) varies
1249
1828
  // per launch. Frozen frame keeps the last colors so a pause reads as intentional.
1250
1829
  function showSplash() {
1251
- const modes = ['moses', 'rainbow', 'chaos']
1252
- const mode = modes[Math.floor((Date.now() / 1000) % modes.length)]
1830
+ const modes = ["moses", "rainbow", "chaos"];
1831
+ const mode = modes[Math.floor((Date.now() / 1000) % modes.length)];
1253
1832
  return new Promise((resolve) => {
1254
- let paused = false
1255
- const interactive = !!process.stdin.isTTY // can we read raw keypresses to dismiss?
1256
- const hint = () => interactive
1257
- ? `${dim(' [P]')} ${paused ? 'resume' : 'pause'} ${dim('[Enter]')} enter`
1258
- : dim(' loading…')
1833
+ let paused = false;
1834
+ const interactive = !!process.stdin.isTTY; // can we read raw keypresses to dismiss?
1835
+ const hint = () =>
1836
+ interactive
1837
+ ? `${dim(" [P]")} ${paused ? "resume" : "pause"} ${dim("[Enter]")} enter`
1838
+ : dim(" loading…");
1259
1839
  // FIXED-WINDOW FIX (2026-06-27): paint the splash via the buffered flushScreen
1260
1840
  // path (GOTO positioning), NOT raw write('\n'). Raw \n scrolls the alt-screen
1261
1841
  // buffer, creating scrollback above the TUI — that's why you could scroll up
1262
1842
  // to see what was above it. flushScreen uses absolute cursor positioning so
1263
1843
  // nothing ever scrolls the buffer. This matches how tokscale renders its frame.
1264
1844
  const paint = () => {
1265
- startBuffer()
1266
- const lines = splashFrame(mode).split('\n')
1267
- lines.forEach((ln) => writeln(ln))
1268
- writeln(ctr(hint()))
1269
- setFooter([])
1270
- flushScreen()
1271
- }
1272
- paint()
1273
- let timer = setInterval(paint, 110)
1845
+ startBuffer();
1846
+ const lines = splashFrame(mode).split("\n");
1847
+ lines.forEach((ln) => writeln(ln));
1848
+ writeln(ctr(hint()));
1849
+ setFooter([]);
1850
+ flushScreen();
1851
+ };
1852
+ paint();
1853
+ let timer = setInterval(paint, 110);
1274
1854
 
1275
1855
  // Non-interactive stdin (some IDE/integrated terminals): still SHOW the animated
1276
1856
  // wordmark, but auto-advance after a short run instead of waiting for a key (which
1277
1857
  // would never arrive → hang). ~2.2s of animation, then enter the TUI.
1278
1858
  if (!interactive) {
1279
- setTimeout(() => { clearInterval(timer); write(CLEAR); resolve() }, 2200)
1280
- return
1859
+ setTimeout(() => {
1860
+ clearInterval(timer);
1861
+ write(CLEAR);
1862
+ resolve();
1863
+ }, 2200);
1864
+ return;
1281
1865
  }
1282
1866
 
1283
- process.stdin.setRawMode?.(true)
1284
- process.stdin.resume()
1285
- process.stdin.setEncoding('utf8')
1867
+ process.stdin.setRawMode?.(true);
1868
+ process.stdin.resume();
1869
+ process.stdin.setEncoding("utf8");
1286
1870
  const onKey = (key) => {
1287
- const k = key.toLowerCase()
1288
- if (k === 'p') { // toggle pause — don't enter
1289
- paused = !paused
1290
- if (paused) { clearInterval(timer); timer = null; paint() } // freeze on current frame
1291
- else { timer = setInterval(paint, 110) } // resume animating
1292
- return
1871
+ const k = key.toLowerCase();
1872
+ if (k === "p") {
1873
+ // toggle pause — don't enter
1874
+ paused = !paused;
1875
+ if (paused) {
1876
+ clearInterval(timer);
1877
+ timer = null;
1878
+ paint();
1879
+ } // freeze on current frame
1880
+ else {
1881
+ timer = setInterval(paint, 110);
1882
+ } // resume animating
1883
+ return;
1293
1884
  }
1294
- if (timer) clearInterval(timer)
1295
- process.stdin.removeListener('data', onKey)
1296
- if (key === '\x03') { write(SHOW); write(EXIT_ALT); process.exit(0) } // Ctrl-C
1297
- resolve() // Enter / any other key → enter the TUI
1298
- }
1299
- process.stdin.on('data', onKey)
1300
- })
1885
+ if (timer) clearInterval(timer);
1886
+ process.stdin.removeListener("data", onKey);
1887
+ if (key === "\x03") {
1888
+ write(SHOW);
1889
+ write(EXIT_ALT);
1890
+ process.exit(0);
1891
+ } // Ctrl-C
1892
+ resolve(); // Enter / any other key → enter the TUI
1893
+ };
1894
+ process.stdin.on("data", onKey);
1895
+ });
1301
1896
  }
1302
1897
 
1303
1898
  // ── MAIN TUI LOOP ─────────────────────────────────────────────────────────────
1304
- export async function runTui({ platform: initPlatform = 'claude', window: win = '7d' } = {}) {
1899
+ export async function runTui({
1900
+ platform: initPlatform = "claude",
1901
+ window: win = "7d",
1902
+ } = {}) {
1305
1903
  // FIX J: platform is mutable — [P] cycles it on Compare + Watch tabs. Was a const
1306
1904
  // param (permanently locked to 'claude'); now a let so the user can switch + [S]
1307
1905
  // submits the selected platform (needed for Phase 4 multi-platform submit).
1308
- let platform = initPlatform
1906
+ let platform = initPlatform;
1309
1907
  // Debug render mode — non-interactive, dumps a tab + width audit, then exits.
1310
- const ri = process.argv.indexOf('--render')
1908
+ const ri = process.argv.indexOf("--render");
1311
1909
  if (ri !== -1) {
1312
- const tab = parseInt(process.argv[ri + 1] ?? '0', 10) || 0
1313
- await renderOnce(tab)
1314
- return
1910
+ const tab = parseInt(process.argv[ri + 1] ?? "0", 10) || 0;
1911
+ await renderOnce(tab);
1912
+ return;
1315
1913
  }
1316
1914
 
1317
- write(ENTER_ALT) // switch to alternate screen — original terminal state preserved on exit
1318
- write(CLEAR_SB) // FIXED-WINDOW (2026-06-27): clear scrollback so nothing is above the TUI
1319
- write(HIDE)
1320
- write(CLEAR)
1915
+ write(ENTER_ALT); // switch to alternate screen — original terminal state preserved on exit
1916
+ write(CLEAR_SB); // FIXED-WINDOW (2026-06-27): clear scrollback so nothing is above the TUI
1917
+ write(HIDE);
1918
+ write(CLEAR);
1321
1919
 
1322
1920
  // Opening splash — animated SIGRANK wordmark.
1323
1921
  // Gate on stdOUT.isTTY (can we draw the animation?), matching the launch guard —
1324
1922
  // NOT stdin.isTTY, which was skipping the splash in terminals whose input isn't a
1325
1923
  // raw TTY (some IDE/integrated terminals). showSplash() itself auto-advances when
1326
1924
  // stdin can't be read raw, so it never hangs. (Skippable with --no-splash.)
1327
- if (!process.argv.includes('--no-splash') && process.stdout.isTTY) {
1328
- await showSplash()
1329
- write(CLEAR)
1925
+ if (!process.argv.includes("--no-splash") && process.stdout.isTTY) {
1926
+ await showSplash();
1927
+ write(CLEAR);
1330
1928
  }
1331
1929
 
1332
1930
  // ── State
1333
- let activeTab = 0
1334
- let dashData = null
1335
- let compareData = null
1336
- let boardData = null
1337
- let boardWindow = '30d'
1931
+ let activeTab = 0;
1932
+ let dashData = null;
1933
+ let compareData = null;
1934
+ let boardData = null;
1935
+ let boardWindow = "30d";
1338
1936
  // A5 (2026-06-27): Watch defaults to "all" focus — all active platforms × all windows. [P]/[W]
1339
1937
  // optionally narrow the focus and cycle back to 'all'. ('all-windows' is the window sentinel so
1340
1938
  // it never collides with the real 'all' window bucket in the cli watcher's optional --window.)
1341
- let watchPlatform = 'all'
1342
- let watchWindow = 'all-windows'
1343
- let watchRefresh = 30 // [5] Watch poll interval (seconds) — [+]/[-] adjust
1344
- let trendSub = 0 // [2] Trends sub-view index (You/Platform/Field) — [T] cycles
1345
- let loading = true
1346
- let status = 'loading…'
1347
- let refreshTimer = null
1348
- let codeBuf = '' // [6] Connect tab in-place code field buffer
1349
- let connectMsg = '' // [6] Connect last-action message (signed in / invalid code)
1350
- let submitMsg = '' // [S] in-place submit result, shown in the read-tab footer
1351
- let submitPreview = false // FIX I1: [S] opens a preview grid before sending (see→confirm→send)
1352
- let boardYouOnly = false // FIX I2: [Y] toggles the Board to "just me" (hybrid model)
1353
- let cascadeScroll = 0 // SCROLL-VIEW (2026-06-27): cascade-section scroll offset (Dashboard only)
1939
+ let watchPlatform = "all";
1940
+ let watchWindow = "all-windows";
1941
+ let watchRefresh = 30; // [5] Watch poll interval (seconds) — [+]/[-] adjust
1942
+ let trendSub = 0; // [2] Trends sub-view index (You/Platform/Field) — [T] cycles
1943
+ let loading = true;
1944
+ let status = "loading…";
1945
+ let refreshTimer = null;
1946
+ let codeBuf = ""; // [6] Connect tab in-place code field buffer
1947
+ let connectMsg = ""; // [6] Connect last-action message (signed in / invalid code)
1948
+ let submitMsg = ""; // [S] in-place submit result, shown in the read-tab footer
1949
+ let submitPreview = false; // FIX I1: [S] opens a preview grid before sending (see→confirm→send)
1950
+ let boardYouOnly = false; // FIX I2: [Y] toggles the Board to "just me" (hybrid model)
1951
+ let cascadeScroll = 0; // SCROLL-VIEW (2026-06-27): cascade-section scroll offset (Dashboard only)
1354
1952
 
1355
1953
  // ── Redraw (buffered: renders into memory, then paints as a locked frame)
1356
1954
  const redraw = async () => {
1357
- startBuffer()
1358
- renderTabBar(activeTab)
1955
+ startBuffer();
1956
+ renderTabBar(activeTab);
1359
1957
 
1360
- const hint = ` ${dim('← → or 1-6')} switch tabs ${dim('[R]')} refresh ${dim('[Q]')} quit`
1361
- const submitHint = ` ${dim('[S]')} submit to board · ${dim('[C]')} sign in`
1958
+ const hint = ` ${dim("← → or 1-6")} switch tabs ${dim("[R]")} refresh ${dim("[Q]")} quit`;
1959
+ const submitHint = ` ${dim("[S]")} submit to board · ${dim("[C]")} sign in`;
1362
1960
  // Read-tab footer: hr + hint, plus the in-place submit result line when present.
1363
- const readFooter = (hintLine) => submitMsg
1364
- ? [` ${hr()}`, hintLine, ` ${submitMsg}`]
1365
- : [` ${hr()}`, hintLine]
1961
+ const readFooter = (hintLine) =>
1962
+ submitMsg
1963
+ ? [` ${hr()}`, hintLine, ` ${submitMsg}`]
1964
+ : [` ${hr()}`, hintLine];
1366
1965
 
1367
1966
  // FIX I1: submit preview is a focused overlay — renders INSTEAD of the tab
1368
1967
  // content, with its own footer. [Enter] sends, [Esc] cancels.
1369
1968
  if (submitPreview) {
1370
- renderSubmitPreview(dashData, loadIdentity())
1371
- setFooter([` ${hr()}`, ` ${dim('[Enter]')} submit all platforms ${dim('[Esc]')} cancel ${dim('[Q]')} quit`])
1372
- flushScreen()
1373
- return
1969
+ renderSubmitPreview(dashData, loadIdentity());
1970
+ setFooter([
1971
+ ` ${hr()}`,
1972
+ ` ${dim("[Enter]")} submit all platforms ${dim("[Esc]")} cancel ${dim("[Q]")} quit`,
1973
+ ]);
1974
+ flushScreen();
1975
+ return;
1374
1976
  }
1375
1977
 
1376
- if (activeTab === 0) { // Dashboard
1978
+ if (activeTab === 0) {
1979
+ // Dashboard
1377
1980
  if (!dashData) {
1378
- writeln(`\n ${dim('loading dashboard…')}`)
1981
+ writeln(`\n ${dim("loading dashboard…")}`);
1379
1982
  } else {
1380
- renderDashboard(dashData, status, cascadeScroll)
1983
+ renderDashboard(dashData, status, cascadeScroll);
1381
1984
  }
1382
1985
  // SCROLL-VIEW: show ↑↓ hint only when there are cascade rows to scroll
1383
- const scrollHint = dashData && Array.isArray(dashData.active)
1384
- ? cascadeScrollableCount(dashData.active) > maxCascadeRowsFor(dashData.active) ? ` ${dim('↑↓ scroll cascade')}` : ''
1385
- : ''
1386
- setFooter(readFooter(`${hint}${scrollHint}${submitHint}`))
1387
- } else if (activeTab === 1) { // Trends
1986
+ const scrollHint =
1987
+ dashData && Array.isArray(dashData.active)
1988
+ ? cascadeScrollableCount(dashData.active) >
1989
+ maxCascadeRowsFor(dashData.active)
1990
+ ? ` ${dim("↑↓ scroll cascade")}`
1991
+ : ""
1992
+ : "";
1993
+ setFooter(readFooter(`${hint}${scrollHint}${submitHint}`));
1994
+ } else if (activeTab === 1) {
1995
+ // Trends
1388
1996
  if (!dashData) {
1389
- writeln(`\n ${dim('loading trends…')}`)
1997
+ writeln(`\n ${dim("loading trends…")}`);
1390
1998
  } else {
1391
- renderTrends(dashData, trendSub)
1999
+ renderTrends(dashData, trendSub);
1392
2000
  }
1393
- setFooter(readFooter(`${hint} ${dim('[T]')} view${submitHint}`))
1394
- } else if (activeTab === 2) { // Compare
2001
+ setFooter(readFooter(`${hint} ${dim("[T]")} view${submitHint}`));
2002
+ } else if (activeTab === 2) {
2003
+ // Compare
1395
2004
  if (!compareData) {
1396
2005
  // #9 (2026-06-27): Compare runs a FRESH on-demand pull of the external verifier sources
1397
2006
  // (ccusage + tokscale + token-dashboard rescan) — it can take several seconds. Show a clear
1398
2007
  // spinner line while it runs, instead of a bare "loading…".
1399
- writeln(`\n ${bold('Source Comparison')} ${dim(`platform: ${platform}`)}`)
1400
- writeln(`\n ${gold('◇')} ${dim(`pulling fresh sources… (ccusage · tokscale · token-dash — this can take several seconds)`)}`)
2008
+ writeln(
2009
+ `\n ${bold("Source Comparison")} ${dim(`platform: ${platform}`)}`,
2010
+ );
2011
+ writeln(
2012
+ `\n ${gold("◇")} ${dim(`pulling fresh sources… (ccusage · tokscale · token-dash — this can take several seconds)`)}`,
2013
+ );
1401
2014
  } else {
1402
- renderCompare(compareData)
2015
+ renderCompare(compareData);
1403
2016
  }
1404
- setFooter(readFooter(`${hint} ${dim('[P]')} switch platform${submitHint}`))
1405
- } else if (activeTab === 3) { // Board
2017
+ setFooter(
2018
+ readFooter(`${hint} ${dim("[P]")} switch platform${submitHint}`),
2019
+ );
2020
+ } else if (activeTab === 3) {
2021
+ // Board
1406
2022
  if (!boardData) {
1407
- writeln(`\n ${dim(`loading board (${boardWindow})…`)}`)
2023
+ writeln(`\n ${dim(`loading board (${boardWindow})…`)}`);
1408
2024
  } else {
1409
2025
  // FIX I2: hybrid board model — global board + your row highlighted, [Y]
1410
2026
  // toggles "just me" (your placements only). Pass the signed-in codename so
1411
2027
  // renderBoard can highlight/filter.
1412
- const id = loadIdentity()
1413
- renderBoard(boardData, boardWindow, boardYouOnly ? id?.codename : null, id?.codename ?? null)
2028
+ const id = loadIdentity();
2029
+ renderBoard(
2030
+ boardData,
2031
+ boardWindow,
2032
+ boardYouOnly ? id?.codename : null,
2033
+ id?.codename ?? null,
2034
+ );
1414
2035
  }
1415
- const youHint = isSignedIn(loadIdentity()) ? ` ${dim('[Y]')} ${boardYouOnly ? 'all' : 'just me'}` : ''
1416
- setFooter(readFooter(`${hint} ${dim('[W]')} window${youHint}${submitHint}`))
1417
- } else if (activeTab === 4) { // Watch
1418
- renderWatchInfo(watchPlatform, watchWindow, watchRefresh)
1419
- setFooter(readFooter(`${hint}${submitHint}`))
1420
- } else if (activeTab === 5) { // Connect
1421
- const connectId = loadIdentity()
1422
- renderConnect(connectId, codeBuf, connectMsg)
1423
- const signOutHint = isSignedIn(connectId) ? ` ${dim('[X]')} sign out` : ''
1424
- setFooter([` ${hr()}`, ` ${dim('[Enter]')} sign in ${dim('[Esc]')} clear/back${signOutHint} ${dim('← →')} tabs ${dim('[Q]')} quit`])
2036
+ const youHint = isSignedIn(loadIdentity())
2037
+ ? ` ${dim("[Y]")} ${boardYouOnly ? "all" : "just me"}`
2038
+ : "";
2039
+ setFooter(
2040
+ readFooter(`${hint} ${dim("[W]")} window${youHint}${submitHint}`),
2041
+ );
2042
+ } else if (activeTab === 4) {
2043
+ // Watch
2044
+ renderWatchInfo(watchPlatform, watchWindow, watchRefresh);
2045
+ setFooter(readFooter(`${hint}${submitHint}`));
2046
+ } else if (activeTab === 5) {
2047
+ // Connect
2048
+ const connectId = loadIdentity();
2049
+ renderConnect(connectId, codeBuf, connectMsg);
2050
+ const signOutHint = isSignedIn(connectId)
2051
+ ? ` ${dim("[X]")} sign out`
2052
+ : "";
2053
+ setFooter([
2054
+ ` ${hr()}`,
2055
+ ` ${dim("[Enter]")} sign in ${dim("[Esc]")} clear/back${signOutHint} ${dim("← →")} tabs ${dim("[Q]")} quit`,
2056
+ ]);
1425
2057
  }
1426
- flushScreen()
1427
- }
2058
+ flushScreen();
2059
+ };
1428
2060
 
1429
2061
  // ── Initial data load
1430
2062
  const loadAll = async () => {
1431
2063
  // A2 (2026-06-27): paint the frame + a clear "reading your cascade…" status IMMEDIATELY,
1432
2064
  // BEFORE the await — so the very first thing on screen is the table header + a live status,
1433
2065
  // not a blank "loading dashboard…". renderDashboard tolerates this empty dashData (guarded).
1434
- status = 'reading your cascade…'
1435
- dashData = { active: [], _loading: true }
1436
- await redraw()
2066
+ status = "reading your cascade…";
2067
+ dashData = { active: [], _loading: true };
2068
+ await redraw();
1437
2069
  // FIX 0: progressive Dashboard load — claude first (fast), render it, THEN fill
1438
2070
  // the other 13 platforms in the background + redraw. The user sees their cascade
1439
2071
  // within ~1 read instead of a 7s blank "loading dashboard…".
1440
- dashData = await loadDashboardData().catch(e => { status = `dashboard error: ${e.message}`; return null })
1441
- status = `last refreshed ${new Date().toLocaleTimeString('en-US', { hour12: false })}`
1442
- await redraw()
2072
+ dashData = await loadDashboardData().catch((e) => {
2073
+ status = `dashboard error: ${e.message}`;
2074
+ return null;
2075
+ });
2076
+ status = `last refreshed ${new Date().toLocaleTimeString("en-US", { hour12: false })}`;
2077
+ await redraw();
1443
2078
  // Keep a "filling other platforms…" status visible while fillDashboardRest runs.
1444
2079
  // #9 (2026-06-27): Compare is NO LONGER eagerly loaded here — loadCompareData now runs the
1445
2080
  // FRESH verifier pull (ccusage + tokscale + token-dash rescan, 5–60s), which would re-introduce
1446
2081
  // a slow Dashboard load path. Compare loads lazily ON-DEMAND when the Compare tab is opened.
1447
2082
  // Board still preloads (fast, cached) in parallel with the platform fill.
1448
2083
  if (dashData?._remaining) {
1449
- status = 'claude ready · filling other platforms…'
1450
- await redraw()
2084
+ status = "claude ready · filling other platforms…";
2085
+ await redraw();
1451
2086
  }
1452
- ;[, boardData] = await Promise.all([
1453
- dashData?._remaining ? fillDashboardRest(dashData) : Promise.resolve(false),
2087
+ [, boardData] = await Promise.all([
2088
+ dashData?._remaining
2089
+ ? fillDashboardRest(dashData)
2090
+ : Promise.resolve(false),
1454
2091
  loadBoardData(boardWindow).catch(() => null),
1455
- ])
1456
- status = `last refreshed ${new Date().toLocaleTimeString('en-US', { hour12: false })}`
1457
- await redraw()
1458
- }
2092
+ ]);
2093
+ status = `last refreshed ${new Date().toLocaleTimeString("en-US", { hour12: false })}`;
2094
+ await redraw();
2095
+ };
1459
2096
 
1460
2097
  // Always restore terminal on unexpected exit
1461
- const cleanup = () => { write(SHOW); write(EXIT_ALT); process.exit(0) }
1462
- process.once('SIGINT', cleanup)
1463
- process.once('SIGTERM', cleanup)
2098
+ const cleanup = () => {
2099
+ write(SHOW);
2100
+ write(EXIT_ALT);
2101
+ process.exit(0);
2102
+ };
2103
+ process.once("SIGINT", cleanup);
2104
+ process.once("SIGTERM", cleanup);
1464
2105
 
1465
2106
  // FIX 0b: re-render on terminal resize so the frame refits the new dimensions.
1466
- process.stdout.on('resize', () => { redraw().catch(() => {}) })
2107
+ process.stdout.on("resize", () => {
2108
+ redraw().catch(() => {});
2109
+ });
1467
2110
 
1468
2111
  // Soft-landing: if not signed in, open on the Connect tab (a prompt, not a gate —
1469
2112
  // the user can still tab away to read Board/Dashboard while signed out).
1470
- if (!isSignedIn(loadIdentity())) activeTab = 5
2113
+ if (!isSignedIn(loadIdentity())) activeTab = 5;
1471
2114
 
1472
2115
  // Draw loading state immediately so user sees tab bar + border
1473
- await redraw()
1474
- await loadAll()
2116
+ await redraw();
2117
+ await loadAll();
1475
2118
 
1476
2119
  // auto-refresh board every 30s
1477
2120
  const startAutoRefresh = () => {
1478
- if (refreshTimer) clearInterval(refreshTimer)
2121
+ if (refreshTimer) clearInterval(refreshTimer);
1479
2122
  refreshTimer = setInterval(async () => {
1480
- if (activeTab === 3) { // Board tab (now index 3)
1481
- boardData = await loadBoardData(boardWindow).catch(() => null)
1482
- await redraw()
2123
+ if (activeTab === 3) {
2124
+ // Board tab (now index 3)
2125
+ boardData = await loadBoardData(boardWindow).catch(() => null);
2126
+ await redraw();
1483
2127
  }
1484
- }, 30000)
1485
- }
1486
- startAutoRefresh()
2128
+ }, 30000);
2129
+ };
2130
+ startAutoRefresh();
1487
2131
 
1488
2132
  // ── Keyboard
1489
- if (!process.stdin.isTTY) return
2133
+ if (!process.stdin.isTTY) return;
1490
2134
 
1491
- process.stdin.setRawMode(true)
1492
- process.stdin.resume()
1493
- process.stdin.setEncoding('utf8')
2135
+ process.stdin.setRawMode(true);
2136
+ process.stdin.resume();
2137
+ process.stdin.setEncoding("utf8");
1494
2138
 
1495
2139
  await new Promise((resolve) => {
1496
- process.stdin.on('data', async (key) => {
1497
- const k = key.toLowerCase()
1498
-
1499
- if (k === 'q' || k === '\x03') {
1500
- if (refreshTimer) clearInterval(refreshTimer)
1501
- write(SHOW)
1502
- write(EXIT_ALT)
1503
- resolve()
1504
- return
2140
+ process.stdin.on("data", async (key) => {
2141
+ const k = key.toLowerCase();
2142
+
2143
+ if (k === "q" || k === "\x03") {
2144
+ if (refreshTimer) clearInterval(refreshTimer);
2145
+ write(SHOW);
2146
+ write(EXIT_ALT);
2147
+ resolve();
2148
+ return;
1505
2149
  }
1506
2150
 
1507
2151
  // ── Connect tab is a FOCUSED code field (the one modal tab). Printable code
@@ -1509,164 +2153,251 @@ export async function runTui({ platform: initPlatform = 'claude', window: win =
1509
2153
  // Sits AFTER the quit block (so Q/Ctrl-C always escape) and BEFORE everything
1510
2154
  // else (so single-letter hotkeys don't fire while you're typing a code).
1511
2155
  if (activeTab === 5) {
1512
- if (key === '\r' || key === '\n') { // [Enter] → sign in
1513
- const code = codeBuf.trim()
1514
- if (!code) { connectMsg = dim('paste a connect code first'); await redraw(); return }
1515
- connectMsg = dim('signing in…'); await redraw()
2156
+ if (key === "\r" || key === "\n") {
2157
+ // [Enter] → sign in
2158
+ const code = codeBuf.trim();
2159
+ if (!code) {
2160
+ connectMsg = dim("paste a connect code first");
2161
+ await redraw();
2162
+ return;
2163
+ }
2164
+ connectMsg = dim("signing in…");
2165
+ await redraw();
1516
2166
  try {
1517
- const out = await callTool('enroll', { code })
1518
- if (out.status === 'enrolled') {
1519
- connectMsg = `${green('✓')} signed in as ${cyan(out.codename || '(operator)')}`
1520
- codeBuf = ''
2167
+ const out = await callTool("enroll", { code });
2168
+ if (out.status === "enrolled") {
2169
+ connectMsg = `${green("✓")} signed in as ${cyan(out.codename || "(operator)")}`;
2170
+ codeBuf = "";
1521
2171
  } else {
1522
2172
  const reasons = {
1523
- code_invalid: 'that code is invalid, expired, or already used — generate a fresh one.',
1524
- device_already_enrolled: 'this device is already signed in. Need a new key? Click "New key" at signalaf.com → Settings, then paste it here.',
1525
- bad_request: 'the code or device key was malformed.',
1526
- rate_limited: 'too many attempts — wait a few minutes and retry.',
1527
- persistence_unavailable: 'sign-in is temporarily unavailable — try again shortly.',
1528
- }
1529
- connectMsg = `${red('✗')} ${reasons[out.reason] || `sign-in failed (${out.reason || 'unknown'}).`}`
2173
+ code_invalid:
2174
+ "that code is invalid, expired, or already used — generate a fresh one.",
2175
+ device_already_enrolled:
2176
+ 'this device is already signed in. Need a new key? Click "New key" at signalaf.com → Settings, then paste it here.',
2177
+ bad_request: "the code or device key was malformed.",
2178
+ rate_limited:
2179
+ "too many attempts — wait a few minutes and retry.",
2180
+ persistence_unavailable:
2181
+ "sign-in is temporarily unavailable — try again shortly.",
2182
+ };
2183
+ connectMsg = `${red("✗")} ${reasons[out.reason] || `sign-in failed (${out.reason || "unknown"}).`}`;
1530
2184
  }
1531
2185
  } catch (e) {
1532
- connectMsg = `${red('✗')} ${e.message}`
2186
+ connectMsg = `${red("✗")} ${e.message}`;
1533
2187
  }
1534
- await redraw(); return
2188
+ await redraw();
2189
+ return;
1535
2190
  }
1536
- if (key === '\x1b') { // [Esc] → clear, or leave when empty
1537
- if (codeBuf) { codeBuf = ''; connectMsg = '' } else { activeTab = 0 }
1538
- await redraw(); return
2191
+ if (key === "\x1b") {
2192
+ // [Esc] → clear, or leave when empty
2193
+ if (codeBuf) {
2194
+ codeBuf = "";
2195
+ connectMsg = "";
2196
+ } else {
2197
+ activeTab = 0;
2198
+ }
2199
+ await redraw();
2200
+ return;
1539
2201
  }
1540
- if (key === '\x1b[C') { activeTab = Math.min(5, activeTab + 1); await redraw(); return } // → tab
1541
- if (key === '\x1b[D') { activeTab = Math.max(0, activeTab - 1); await redraw(); return } // ← tab
1542
- if (key === '\x7f' || key === '\b') { codeBuf = codeBuf.slice(0, -1); await redraw(); return } // backspace
2202
+ if (key === "\x1b[C") {
2203
+ activeTab = Math.min(5, activeTab + 1);
2204
+ await redraw();
2205
+ return;
2206
+ } // → tab
2207
+ if (key === "\x1b[D") {
2208
+ activeTab = Math.max(0, activeTab - 1);
2209
+ await redraw();
2210
+ return;
2211
+ } // ← tab
2212
+ if (key === "\x7f" || key === "\b") {
2213
+ codeBuf = codeBuf.slice(0, -1);
2214
+ await redraw();
2215
+ return;
2216
+ } // backspace
1543
2217
  // [X] sign out / reset — only when signed in AND the code field is empty (so an
1544
2218
  // 'x' typed mid-code still feeds the buffer). Drops the stale local credential so
1545
2219
  // a server-revoked device stops reading as "signed in" + the next enroll provisions
1546
2220
  // a fresh device_id (escapes the Frankenstein-identity / 409-re-enroll deadlock).
1547
- if (k === 'x' && !codeBuf && isSignedIn(loadIdentity())) {
1548
- clearIdentity()
1549
- codeBuf = ''
1550
- connectMsg = `${green('✓')} signed out — paste a new code to sign in`
1551
- await redraw(); return
2221
+ if (k === "x" && !codeBuf && isSignedIn(loadIdentity())) {
2222
+ clearIdentity();
2223
+ codeBuf = "";
2224
+ connectMsg = `${green("✓")} signed out — paste a new code to sign in`;
2225
+ await redraw();
2226
+ return;
1552
2227
  }
1553
2228
  // A typed code char (len 1) or a pasted code (one multi-char chunk). Exclude any
1554
2229
  // escape sequence (↑/↓/etc.) so stray control bytes never pollute the buffer.
1555
- if (!key.startsWith('\x1b') && (key.length > 1 || isCodeChar(key))) {
1556
- codeBuf += key.split('').filter(isCodeChar).join('')
1557
- await redraw(); return
2230
+ if (!key.startsWith("\x1b") && (key.length > 1 || isCodeChar(key))) {
2231
+ codeBuf += key.split("").filter(isCodeChar).join("");
2232
+ await redraw();
2233
+ return;
1558
2234
  }
1559
- return // swallow any other key while focused (never falls through to global hotkeys)
2235
+ return; // swallow any other key while focused (never falls through to global hotkeys)
1560
2236
  }
1561
2237
 
1562
2238
  // FIX I1: submit preview is a focused overlay (like Connect). [Enter] sends
1563
2239
  // all platforms, [Esc] cancels. Intercepts keys BEFORE the tab/ESC handlers.
1564
2240
  if (submitPreview) {
1565
- if (key === '\r' || key === '\n') { // [Enter] → confirm + submit all platforms
1566
- submitPreview = false
1567
- submitMsg = dim('submitting…')
1568
- await redraw()
1569
- const id = loadIdentity()
2241
+ if (key === "\r" || key === "\n") {
2242
+ // [Enter] → confirm + submit all platforms
2243
+ submitPreview = false;
2244
+ submitMsg = dim("submitting…");
2245
+ await redraw();
2246
+ const id = loadIdentity();
1570
2247
  if (!isSignedIn(id)) {
1571
- submitMsg = `${red('✗')} sign in to submit`
1572
- activeTab = 5
1573
- await redraw(); return
2248
+ submitMsg = `${red("✗")} sign in to submit`;
2249
+ activeTab = 5;
2250
+ await redraw();
2251
+ return;
1574
2252
  }
1575
2253
  // FIX I1: submit every active platform (data already loaded for the Dashboard).
1576
2254
  // Each platform is a separate submit_verified call → its own (platform, window) slot.
1577
- const platforms = (dashData?.active ?? []).map(d => d.platform)
1578
- let totalVerified = 0, totalReceived = 0, hadFail = false, didMulti = false
2255
+ const platforms = (dashData?.active ?? []).map((d) => d.platform);
2256
+ let totalVerified = 0,
2257
+ totalReceived = 0,
2258
+ hadFail = false,
2259
+ didMulti = false;
1579
2260
  for (const p of platforms) {
1580
2261
  try {
1581
- const out = await callTool('submit_verified', { platform: p })
1582
- const ws = out.windows || []
1583
- const received = ws.filter(w => w.status === 'received')
1584
- const verified = received.filter(w => w.verification_tier === 'verified')
1585
- totalReceived += received.length
1586
- totalVerified += verified.length
1587
- if (received.length > 0 && verified.length < received.length) hadFail = true
1588
- } catch { hadFail = true }
2262
+ const out = await callTool("submit_verified", { platform: p });
2263
+ const ws = out.windows || [];
2264
+ const received = ws.filter((w) => w.status === "received");
2265
+ const verified = received.filter(
2266
+ (w) => w.verification_tier === "verified",
2267
+ );
2268
+ totalReceived += received.length;
2269
+ totalVerified += verified.length;
2270
+ if (received.length > 0 && verified.length < received.length)
2271
+ hadFail = true;
2272
+ } catch {
2273
+ hadFail = true;
2274
+ }
1589
2275
  }
1590
2276
  // MULTI: the combined cross-platform cascade (claude+codex+… summed). Only
1591
2277
  // meaningful with 2+ active platforms; submit_verified('multi') aggregates them.
1592
2278
  if (platforms.length >= 2) {
1593
2279
  try {
1594
- const out = await callTool('submit_verified', { platform: 'multi' })
1595
- const ws = out.windows || []
1596
- const received = ws.filter(w => w.status === 'received')
1597
- const verified = received.filter(w => w.verification_tier === 'verified')
1598
- totalReceived += received.length
1599
- totalVerified += verified.length
1600
- if (received.length > 0 && verified.length < received.length) hadFail = true
1601
- if (received.length > 0) didMulti = true
1602
- } catch { hadFail = true }
2280
+ const out = await callTool("submit_verified", {
2281
+ platform: "multi",
2282
+ });
2283
+ const ws = out.windows || [];
2284
+ const received = ws.filter((w) => w.status === "received");
2285
+ const verified = received.filter(
2286
+ (w) => w.verification_tier === "verified",
2287
+ );
2288
+ totalReceived += received.length;
2289
+ totalVerified += verified.length;
2290
+ if (received.length > 0 && verified.length < received.length)
2291
+ hadFail = true;
2292
+ if (received.length > 0) didMulti = true;
2293
+ } catch {
2294
+ hadFail = true;
2295
+ }
1603
2296
  }
1604
2297
  if (totalReceived && !hadFail && totalVerified === totalReceived) {
1605
- submitMsg = `${green('✓')} submitted · ${totalVerified} window${totalVerified > 1 ? 's' : ''} across ${platforms.length} platform${platforms.length > 1 ? 's' : ''}${didMulti ? ' + combined' : ''} · verified`
2298
+ submitMsg = `${green("✓")} submitted · ${totalVerified} window${totalVerified > 1 ? "s" : ""} across ${platforms.length} platform${platforms.length > 1 ? "s" : ""}${didMulti ? " + combined" : ""} · verified`;
1606
2299
  } else if (hadFail && totalReceived) {
1607
- submitMsg = `${red('✗')} device not verified — sign in again ([C])`
2300
+ submitMsg = `${red("✗")} device not verified — sign in again ([C])`;
1608
2301
  } else if (totalReceived === 0) {
1609
- submitMsg = `${red('✗')} nothing to submit — no windows with data`
2302
+ submitMsg = `${red("✗")} nothing to submit — no windows with data`;
1610
2303
  } else {
1611
- submitMsg = `${red('✗')} submit failed`
2304
+ submitMsg = `${red("✗")} submit failed`;
1612
2305
  }
1613
- await redraw(); return
2306
+ await redraw();
2307
+ return;
1614
2308
  }
1615
- if (key === '\x1b') { // [Esc] → cancel
1616
- submitPreview = false
1617
- await redraw(); return
2309
+ if (key === "\x1b") {
2310
+ // [Esc] → cancel
2311
+ submitPreview = false;
2312
+ await redraw();
2313
+ return;
1618
2314
  }
1619
- return // swallow all other keys while the preview is open
2315
+ return; // swallow all other keys while the preview is open
1620
2316
  }
1621
2317
 
1622
2318
  // ESC → go back to Dashboard from any tab
1623
- if (key === '\x1b' && activeTab !== 0) {
1624
- activeTab = 0
1625
- await redraw()
1626
- return
2319
+ if (key === "\x1b" && activeTab !== 0) {
2320
+ activeTab = 0;
2321
+ await redraw();
2322
+ return;
1627
2323
  }
1628
2324
 
1629
2325
  // SCROLL-VIEW (2026-06-27): ↑/↓ (or j/k) scroll the cascade section on the
1630
2326
  // Dashboard tab ONLY. The rest of the dashboard stays pinned. Only active
1631
2327
  // when there are more cascade rows than fit in the viewport.
1632
2328
  if (activeTab === 0 && dashData && Array.isArray(dashData.active)) {
1633
- const total = cascadeScrollableCount(dashData.active)
1634
- const maxRows = maxCascadeRowsFor(dashData.active)
2329
+ const total = cascadeScrollableCount(dashData.active);
2330
+ const maxRows = maxCascadeRowsFor(dashData.active);
1635
2331
  if (total > maxRows) {
1636
- const maxOff = total - maxRows
1637
- if (key === '\x1b[A' || k === 'k') { // ↑ / k → scroll up
1638
- cascadeScroll = Math.max(0, cascadeScroll - 1)
1639
- await redraw(); return
2332
+ const maxOff = total - maxRows;
2333
+ if (key === "\x1b[A" || k === "k") {
2334
+ // ↑ / k → scroll up
2335
+ cascadeScroll = Math.max(0, cascadeScroll - 1);
2336
+ await redraw();
2337
+ return;
1640
2338
  }
1641
- if (key === '\x1b[B' || k === 'j') { // ↓ / j → scroll down
1642
- cascadeScroll = Math.min(maxOff, cascadeScroll + 1)
1643
- await redraw(); return
2339
+ if (key === "\x1b[B" || k === "j") {
2340
+ // ↓ / j → scroll down
2341
+ cascadeScroll = Math.min(maxOff, cascadeScroll + 1);
2342
+ await redraw();
2343
+ return;
1644
2344
  }
1645
- if (key === '\x1b[5~') { // PageUp → scroll up by viewport
1646
- cascadeScroll = Math.max(0, cascadeScroll - maxRows)
1647
- await redraw(); return
2345
+ if (key === "\x1b[5~") {
2346
+ // PageUp → scroll up by viewport
2347
+ cascadeScroll = Math.max(0, cascadeScroll - maxRows);
2348
+ await redraw();
2349
+ return;
1648
2350
  }
1649
- if (key === '\x1b[6~') { // PageDown → scroll down by viewport
1650
- cascadeScroll = Math.min(maxOff, cascadeScroll + maxRows)
1651
- await redraw(); return
2351
+ if (key === "\x1b[6~") {
2352
+ // PageDown → scroll down by viewport
2353
+ cascadeScroll = Math.min(maxOff, cascadeScroll + maxRows);
2354
+ await redraw();
2355
+ return;
1652
2356
  }
1653
2357
  }
1654
2358
  }
1655
2359
 
1656
2360
  // tab switching (6 tabs: 0=Dashboard 1=Trends 2=Compare 3=Board 4=Watch 5=Connect)
1657
- let switched = false
1658
- if (key === '\x1b[C') { activeTab = Math.min(5, activeTab + 1); switched = true }
1659
- if (key === '\x1b[D') { activeTab = Math.max(0, activeTab - 1); switched = true }
1660
- if (k === '1') { activeTab = 0; switched = true }
1661
- if (k === '2') { activeTab = 1; switched = true }
1662
- if (k === '3') { activeTab = 2; switched = true }
1663
- if (k === '4') { activeTab = 3; switched = true }
1664
- if (k === '5') { activeTab = 4; switched = true } // Watch = an in-TUI landing panel
1665
- if (k === '6') { activeTab = 5; switched = true } // Connect = sign in / switch device
1666
- if (k === 'c' && activeTab !== 5) { activeTab = 5; switched = true } // [C] → Connect from any read tab
2361
+ let switched = false;
2362
+ if (key === "\x1b[C") {
2363
+ activeTab = Math.min(5, activeTab + 1);
2364
+ switched = true;
2365
+ }
2366
+ if (key === "\x1b[D") {
2367
+ activeTab = Math.max(0, activeTab - 1);
2368
+ switched = true;
2369
+ }
2370
+ if (k === "1") {
2371
+ activeTab = 0;
2372
+ switched = true;
2373
+ }
2374
+ if (k === "2") {
2375
+ activeTab = 1;
2376
+ switched = true;
2377
+ }
2378
+ if (k === "3") {
2379
+ activeTab = 2;
2380
+ switched = true;
2381
+ }
2382
+ if (k === "4") {
2383
+ activeTab = 3;
2384
+ switched = true;
2385
+ }
2386
+ if (k === "5") {
2387
+ activeTab = 4;
2388
+ switched = true;
2389
+ } // Watch = an in-TUI landing panel
2390
+ if (k === "6") {
2391
+ activeTab = 5;
2392
+ switched = true;
2393
+ } // Connect = sign in / switch device
2394
+ if (k === "c" && activeTab !== 5) {
2395
+ activeTab = 5;
2396
+ switched = true;
2397
+ } // [C] → Connect from any read tab
1667
2398
 
1668
2399
  // SCROLL-VIEW: reset cascade scroll when leaving the Dashboard tab
1669
- if (switched && activeTab !== 0) cascadeScroll = 0
2400
+ if (switched && activeTab !== 0) cascadeScroll = 0;
1670
2401
 
1671
2402
  // #9 (2026-06-27): Compare loads its FRESH verifier pull ON-DEMAND when the tab opens
1672
2403
  // (NOT on the Dashboard load path — that pull is a 5–60s scan). Paint the "pulling fresh
@@ -1675,145 +2406,193 @@ export async function runTui({ platform: initPlatform = 'claude', window: win =
1675
2406
  // pull — key presses are NOT blocked. Guard: only render the result if the user is still
1676
2407
  // on the Compare tab (they may have switched away during the pull).
1677
2408
  if (switched && activeTab === 2 && !compareData) {
1678
- submitMsg = ''
1679
- await redraw() // show the spinner line before the (slow) fresh pull
1680
- const result = await loadCompareData(platform).catch(() => null)
1681
- if (activeTab === 2) { // still on Compare? render the result
1682
- compareData = result
1683
- await redraw()
2409
+ submitMsg = "";
2410
+ await redraw(); // show the spinner line before the (slow) fresh pull
2411
+ const result = await loadCompareData(platform).catch(() => null);
2412
+ if (activeTab === 2) {
2413
+ // still on Compare? render the result
2414
+ compareData = result;
2415
+ await redraw();
1684
2416
  }
1685
- return
2417
+ return;
1686
2418
  }
1687
2419
 
1688
2420
  // Trends tab: [T] cycles the sub-view (You · Platform · Field)
1689
- if (activeTab === 1 && k === 't') { trendSub = (trendSub + 1) % 3; await redraw(); return }
2421
+ if (activeTab === 1 && k === "t") {
2422
+ trendSub = (trendSub + 1) % 3;
2423
+ await redraw();
2424
+ return;
2425
+ }
1690
2426
 
1691
2427
  // Watch tab: [+]/[-] tune the refresh interval (5–600s), [Enter] launches the watcher
1692
- if (activeTab === 4 && (k === '+' || k === '=' )) { watchRefresh = Math.min(600, watchRefresh + 5); await redraw(); return }
1693
- if (activeTab === 4 && (k === '-' || k === '_')) { watchRefresh = Math.max(5, watchRefresh - 5); await redraw(); return }
2428
+ if (activeTab === 4 && (k === "+" || k === "=")) {
2429
+ watchRefresh = Math.min(600, watchRefresh + 5);
2430
+ await redraw();
2431
+ return;
2432
+ }
2433
+ if (activeTab === 4 && (k === "-" || k === "_")) {
2434
+ watchRefresh = Math.max(5, watchRefresh - 5);
2435
+ await redraw();
2436
+ return;
2437
+ }
1694
2438
 
1695
2439
  // FIX J: [P] cycles platform on Compare + Watch tabs (was a dead hinted key).
1696
2440
  // Compare reloads compareData for the new platform; Watch just re-renders.
1697
- const CYCLE_PLATFORMS = ['claude','codex','amp','gemini','kimi','qwen','goose','kilo','hermes','droid','codebuff','copilot','openclaw','pi']
1698
- if (k === 'p' && (activeTab === 2 || activeTab === 4)) {
2441
+ const CYCLE_PLATFORMS = [
2442
+ "claude",
2443
+ "codex",
2444
+ "amp",
2445
+ "gemini",
2446
+ "kimi",
2447
+ "qwen",
2448
+ "goose",
2449
+ "kilo",
2450
+ "hermes",
2451
+ "droid",
2452
+ "codebuff",
2453
+ "copilot",
2454
+ "openclaw",
2455
+ "pi",
2456
+ ];
2457
+ if (k === "p" && (activeTab === 2 || activeTab === 4)) {
1699
2458
  if (activeTab === 2) {
1700
2459
  // Compare: real platforms only (no 'all' — Compare is per-platform by design).
1701
- const idx = CYCLE_PLATFORMS.indexOf(platform)
1702
- platform = CYCLE_PLATFORMS[(idx + 1) % CYCLE_PLATFORMS.length]
2460
+ const idx = CYCLE_PLATFORMS.indexOf(platform);
2461
+ platform = CYCLE_PLATFORMS[(idx + 1) % CYCLE_PLATFORMS.length];
1703
2462
  // #9: clearing compareData makes redraw show the "pulling fresh sources…" spinner
1704
2463
  // for the new platform while the fresh on-demand verifier pull runs.
1705
- compareData = null
1706
- status = `loading ${platform}…`
1707
- await redraw()
2464
+ compareData = null;
2465
+ status = `loading ${platform}…`;
2466
+ await redraw();
1708
2467
  // ASYNC FIX: guard — only render if still on Compare (user may have switched away)
1709
- const result = await loadCompareData(platform).catch(() => null)
2468
+ const result = await loadCompareData(platform).catch(() => null);
1710
2469
  if (activeTab === 2) {
1711
- compareData = result
1712
- status = `last refreshed ${new Date().toLocaleTimeString('en-US', { hour12: false })}`
2470
+ compareData = result;
2471
+ status = `last refreshed ${new Date().toLocaleTimeString("en-US", { hour12: false })}`;
1713
2472
  }
1714
2473
  } else {
1715
2474
  // A5: Watch [P] cycles 'all' → claude → codex → … → 'all' (optional focus; default 'all').
1716
- const WATCH_PLAT_CYCLE = ['all', ...CYCLE_PLATFORMS]
1717
- const idx = WATCH_PLAT_CYCLE.indexOf(watchPlatform)
1718
- watchPlatform = WATCH_PLAT_CYCLE[(idx + 1) % WATCH_PLAT_CYCLE.length]
2475
+ const WATCH_PLAT_CYCLE = ["all", ...CYCLE_PLATFORMS];
2476
+ const idx = WATCH_PLAT_CYCLE.indexOf(watchPlatform);
2477
+ watchPlatform = WATCH_PLAT_CYCLE[(idx + 1) % WATCH_PLAT_CYCLE.length];
1719
2478
  }
1720
- await redraw(); return
2479
+ await redraw();
2480
+ return;
1721
2481
  }
1722
2482
 
1723
2483
  // FIX J / A5: [W] cycles window FOCUS on the Watch tab. 'all-windows' (default) → 7d → 30d →
1724
2484
  // 90d → all → back to 'all-windows'. Board tab's [W] (activeTab === 3) is handled below.
1725
- if (k === 'w' && activeTab === 4) {
1726
- const WATCH_WIN_CYCLE = ['all-windows', '7d', '30d', '90d', 'all']
1727
- const idx = WATCH_WIN_CYCLE.indexOf(watchWindow)
1728
- watchWindow = WATCH_WIN_CYCLE[(idx + 1) % WATCH_WIN_CYCLE.length]
1729
- await redraw(); return
2485
+ if (k === "w" && activeTab === 4) {
2486
+ const WATCH_WIN_CYCLE = ["all-windows", "7d", "30d", "90d", "all"];
2487
+ const idx = WATCH_WIN_CYCLE.indexOf(watchWindow);
2488
+ watchWindow = WATCH_WIN_CYCLE[(idx + 1) % WATCH_WIN_CYCLE.length];
2489
+ await redraw();
2490
+ return;
1730
2491
  }
1731
2492
 
1732
- if (activeTab === 4 && (key === '\r' || key === '\n')) {
2493
+ if (activeTab === 4 && (key === "\r" || key === "\n")) {
1733
2494
  // A5 (2026-06-27): launch the live watcher in its own window. By default (focus = 'all')
1734
2495
  // pass NO --platform/--window so the cli watcher auto-loads ALL active platforms × ALL
1735
2496
  // windows. Only add a filter flag when the user has set an explicit [P]/[W] focus.
1736
2497
  try {
1737
- let watchCmd = 'sigrank watch'
1738
- if (watchPlatform && watchPlatform !== 'all') watchCmd += ` --platform ${watchPlatform}`
1739
- if (watchWindow && watchWindow !== 'all-windows') watchCmd += ` --window ${watchWindow}`
1740
- watchCmd += ` --refresh ${watchRefresh}`
2498
+ let watchCmd = "sigrank watch";
2499
+ if (watchPlatform && watchPlatform !== "all")
2500
+ watchCmd += ` --platform ${watchPlatform}`;
2501
+ if (watchWindow && watchWindow !== "all-windows")
2502
+ watchCmd += ` --window ${watchWindow}`;
2503
+ watchCmd += ` --refresh ${watchRefresh}`;
1741
2504
  // ASYNC FIX (2026-06-27): execFile instead of execSync — no shell
1742
2505
  // interpolation. osascript args passed as array (defense-in-depth).
1743
- execFile('osascript', ['-e', `tell application "Terminal" to do script "${watchCmd}"`], { stdio: 'ignore' }, () => {})
1744
- const scope = (!watchPlatform || watchPlatform === 'all') && (!watchWindow || watchWindow === 'all-windows')
1745
- ? 'all platforms × all windows' : 'focused'
1746
- status = `watcher launched (${scope}, ${watchRefresh}s) in a new window`
1747
- } catch { status = 'could not open Terminal.app — run: sigrank watch' }
1748
- await redraw()
1749
- return
2506
+ execFile(
2507
+ "osascript",
2508
+ ["-e", `tell application "Terminal" to do script "${watchCmd}"`],
2509
+ { stdio: "ignore" },
2510
+ () => {},
2511
+ );
2512
+ const scope =
2513
+ (!watchPlatform || watchPlatform === "all") &&
2514
+ (!watchWindow || watchWindow === "all-windows")
2515
+ ? "all platforms × all windows"
2516
+ : "focused";
2517
+ status = `watcher launched (${scope}, ${watchRefresh}s) in a new window`;
2518
+ } catch {
2519
+ status = "could not open Terminal.app — run: sigrank watch";
2520
+ }
2521
+ await redraw();
2522
+ return;
1750
2523
  }
1751
2524
 
1752
- if (k === 'r') {
1753
- status = 'refreshing…'
1754
- cascadeScroll = 0 // SCROLL-VIEW: reset scroll on refresh
2525
+ if (k === "r") {
2526
+ status = "refreshing…";
2527
+ cascadeScroll = 0; // SCROLL-VIEW: reset scroll on refresh
1755
2528
  // #9: on the Compare tab, [R] re-runs the FRESH verifier pull for the current platform
1756
2529
  // (loadAll no longer touches Compare). Clearing compareData shows the spinner first.
1757
2530
  if (activeTab === 2) {
1758
- compareData = null
1759
- await redraw()
2531
+ compareData = null;
2532
+ await redraw();
1760
2533
  // ASYNC FIX: guard — only render if still on Compare
1761
- const result = await loadCompareData(platform).catch(() => null)
2534
+ const result = await loadCompareData(platform).catch(() => null);
1762
2535
  if (activeTab === 2) {
1763
- compareData = result
1764
- status = `last refreshed ${new Date().toLocaleTimeString('en-US', { hour12: false })}`
1765
- await redraw()
2536
+ compareData = result;
2537
+ status = `last refreshed ${new Date().toLocaleTimeString("en-US", { hour12: false })}`;
2538
+ await redraw();
1766
2539
  }
1767
- return
2540
+ return;
1768
2541
  }
1769
- await redraw()
1770
- await loadAll()
1771
- return
2542
+ await redraw();
2543
+ await loadAll();
2544
+ return;
1772
2545
  }
1773
2546
 
1774
- if (k === 's' && activeTab !== 5) {
2547
+ if (k === "s" && activeTab !== 5) {
1775
2548
  // FIX I1/E: [S] opens a submit PREVIEW (see→confirm→send) instead of firing
1776
2549
  // blind. The preview shows a platform × window grid of what will publish;
1777
2550
  // [Enter] sends all platforms, [Esc] cancels. (On the Connect tab, 's' is a
1778
2551
  // code char handled by the focused field above.)
1779
- const id = loadIdentity()
1780
- if (!isSignedIn(id)) { // not signed in → open Connect, never error
1781
- activeTab = 5
1782
- connectMsg = dim('sign in to submit to board')
1783
- await redraw()
1784
- return
2552
+ const id = loadIdentity();
2553
+ if (!isSignedIn(id)) {
2554
+ // not signed in → open Connect, never error
2555
+ activeTab = 5;
2556
+ connectMsg = dim("sign in to submit to board");
2557
+ await redraw();
2558
+ return;
1785
2559
  }
1786
- submitPreview = true
1787
- submitMsg = ''
1788
- await redraw()
1789
- return
2560
+ submitPreview = true;
2561
+ submitMsg = "";
2562
+ await redraw();
2563
+ return;
1790
2564
  }
1791
2565
 
1792
2566
  // FIX I2: [Y] toggles the Board between global+highlighted and "just me"
1793
2567
  // (hybrid model — owner decision 2026-06-26). Only when signed in.
1794
- if (k === 'y' && activeTab === 3 && isSignedIn(loadIdentity())) {
1795
- boardYouOnly = !boardYouOnly
1796
- await redraw(); return
2568
+ if (k === "y" && activeTab === 3 && isSignedIn(loadIdentity())) {
2569
+ boardYouOnly = !boardYouOnly;
2570
+ await redraw();
2571
+ return;
1797
2572
  }
1798
2573
 
1799
- if (k === 'w' && activeTab === 3) { // Board tab (now index 3)
1800
- const windows = ['7d', '30d', '90d', 'all']
1801
- const idx = windows.indexOf(boardWindow)
1802
- boardWindow = windows[(idx + 1) % windows.length]
1803
- boardData = await loadBoardData(boardWindow).catch(() => null)
2574
+ if (k === "w" && activeTab === 3) {
2575
+ // Board tab (now index 3)
2576
+ const windows = ["7d", "30d", "90d", "all"];
2577
+ const idx = windows.indexOf(boardWindow);
2578
+ boardWindow = windows[(idx + 1) % windows.length];
2579
+ boardData = await loadBoardData(boardWindow).catch(() => null);
1804
2580
  }
1805
2581
 
1806
- if (switched) submitMsg = '' // submit result is tied to the tab it was sent from
1807
- if (switched || k === 'w') await redraw()
1808
- })
1809
- })
2582
+ if (switched) submitMsg = ""; // submit result is tied to the tab it was sent from
2583
+ if (switched || k === "w") await redraw();
2584
+ });
2585
+ });
1810
2586
 
1811
- process.stdin.setRawMode(false)
1812
- process.stdin.pause()
1813
- writeln()
2587
+ process.stdin.setRawMode(false);
2588
+ process.stdin.pause();
2589
+ writeln();
1814
2590
  }
1815
2591
 
1816
2592
  // Direct-run entry (e.g. `node tui.mjs --render 0`). Normal launch is via cli.mjs.
1817
2593
  if (import.meta.url === `file://${process.argv[1]}`) {
1818
- runTui().catch((e) => { console.error(e); process.exit(1) })
2594
+ runTui().catch((e) => {
2595
+ console.error(e);
2596
+ process.exit(1);
2597
+ });
1819
2598
  }