sigrank 1.0.45 → 1.0.47

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.
@@ -19,7 +19,7 @@ import { freshVerifierPillars } from "../tokenpull.mjs";
19
19
  import { isSignedIn, isCodeChar } from "../connect.mjs";
20
20
  import { loadIdentity, clearIdentity } from "../keystore.mjs";
21
21
  import { execFile } from "child_process";
22
- import { existsSync, readFileSync } from "fs";
22
+ import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
23
23
  import os from "os";
24
24
  import path from "path";
25
25
  import { pathToFileURL } from "node:url";
@@ -54,7 +54,9 @@ export function dashboardEnabledPlatforms({
54
54
  // The `c` object is a live proxy to the current theme — changing the theme
55
55
  // via setTheme() updates all colors. Supports dark/light/high-contrast/monochrome.
56
56
  import * as _themeMod from "./tui-themes.mjs";
57
- import { platformColor } from "./tui-themes.mjs";
57
+ import { platformColor, archetypeColor } from "./tui-themes.mjs";
58
+ import { classifyArchetype } from "./narrate.mjs";
59
+ import { _computeReportBlock } from "../tools/_report.mjs";
58
60
  const ESC = "\x1b[";
59
61
  // `c` is a live proxy — reads currentTheme at access time so theme switches
60
62
  // take effect immediately. The helper functions below (paint/bold/dim/etc.)
@@ -154,15 +156,44 @@ function flushScreen() {
154
156
  function stripAnsi(s) {
155
157
  return s.replace(/\x1b\[[0-9;]*m/g, "");
156
158
  }
159
+ // Terminal cell width of one code point: 0 for combining marks / zero-width /
160
+ // variation selectors, 2 for East Asian wide + emoji, else 1. Counting UTF-16
161
+ // units (.length) miscounts these and pushes columns out of line.
162
+ function cpWidth(cp) {
163
+ if (cp === 0x200b || cp === 0x200c || cp === 0x200d || cp === 0xfeff) return 0;
164
+ if ((cp >= 0x0300 && cp <= 0x036f) || (cp >= 0xfe00 && cp <= 0xfe0f)) return 0;
165
+ if (
166
+ (cp >= 0x1100 && cp <= 0x115f) ||
167
+ (cp >= 0x2e80 && cp <= 0xa4cf) ||
168
+ (cp >= 0xac00 && cp <= 0xd7a3) ||
169
+ (cp >= 0xf900 && cp <= 0xfaff) ||
170
+ (cp >= 0xfe30 && cp <= 0xfe4f) ||
171
+ (cp >= 0xff00 && cp <= 0xff60) ||
172
+ (cp >= 0xffe0 && cp <= 0xffe6) ||
173
+ (cp >= 0x1f300 && cp <= 0x1faff) ||
174
+ (cp >= 0x20000 && cp <= 0x3fffd)
175
+ )
176
+ return 2;
177
+ return 1;
178
+ }
179
+ // Visible width in terminal cells (ANSI escapes stripped).
180
+ function visW(s) {
181
+ let w = 0;
182
+ for (const ch of stripAnsi(s)) w += cpWidth(ch.codePointAt(0));
183
+ return w;
184
+ }
157
185
  // ANSI-aware truncate: cut to `w` VISIBLE columns while keeping color escapes
158
186
  // intact (escapes don't count toward width, and we never slice mid-sequence).
159
187
  // Replaces raw `.slice(0, w)`, which corrupts color codes + miscounts width.
160
188
  function ansiTrunc(s, w) {
161
- if (stripAnsi(s).length <= w) return s;
189
+ if (visW(s) <= w) return s;
190
+ if (w <= 0) return "";
191
+ // Leave one cell for "…" so a cut line reads as cut, not as complete.
192
+ const room = w - 1;
162
193
  let out = "",
163
194
  vis = 0,
164
195
  i = 0;
165
- while (i < s.length && vis < w) {
196
+ while (i < s.length) {
166
197
  if (s[i] === "\x1b") {
167
198
  const m = s.slice(i).match(/^\x1b\[[0-9;]*m/);
168
199
  if (m) {
@@ -171,22 +202,63 @@ function ansiTrunc(s, w) {
171
202
  continue;
172
203
  }
173
204
  }
174
- out += s[i];
175
- vis++;
176
- i++;
205
+ const cp = s.codePointAt(i);
206
+ const ch = String.fromCodePoint(cp);
207
+ const cw = cpWidth(cp);
208
+ if (vis + cw > room) break;
209
+ out += ch;
210
+ vis += cw;
211
+ i += ch.length;
177
212
  }
178
- return out + c.reset; // reset so a cut mid-color doesn't bleed into next line
213
+ return out + c.reset + dim("…"); // reset so a cut mid-color doesn't bleed into next line
179
214
  }
180
215
  function padEnd(s, w) {
181
- const v = stripAnsi(s).length;
216
+ const v = visW(s);
182
217
  return v >= w ? s : s + " ".repeat(w - v);
183
218
  }
184
219
  function padStart(s, w) {
185
- const v = stripAnsi(s).length;
220
+ const v = visW(s);
186
221
  return v >= w ? s : " ".repeat(w - v) + s;
187
222
  }
188
223
  function trunc(s, w) {
189
- return stripAnsi(s).length <= w ? s : s.slice(0, w - 1) + "…";
224
+ return visW(s) <= w ? s : ansiTrunc(s, w);
225
+ }
226
+
227
+ // Footer keybar that degrades in steps instead of being cut off: full hints on one line → wrap to two lines → compact
228
+ // "[K]" keys only → drop the lowest-priority hints. items: [{ k, label, pri }]
229
+ // where k is the key text ("[R]", "← →"), label the words after it, and pri
230
+ // 1 (always keep) … 3 (first to go). Returns 1–2 lines, each prefixed " ".
231
+ function fitHints(items, w = W()) {
232
+ const room = Math.max(10, w - 2);
233
+ const fmt = (it, compact) =>
234
+ compact || !it.label ? dim(it.k) : `${dim(it.k)} ${it.label}`;
235
+ const join = (list, compact) => list.map((it) => fmt(it, compact)).join(" ");
236
+ const pack = (list, compact) => {
237
+ const lines = [];
238
+ let cur = [];
239
+ for (const it of list) {
240
+ const next = [...cur, it];
241
+ if (cur.length && visW(join(next, compact)) > room) {
242
+ lines.push(cur);
243
+ cur = [it];
244
+ } else cur = next;
245
+ }
246
+ if (cur.length) lines.push(cur);
247
+ return lines.map((l) => join(l, compact));
248
+ };
249
+ for (const compact of [false, true]) {
250
+ let list = items.slice();
251
+ for (;;) {
252
+ const lines = pack(list, compact);
253
+ if (lines.length <= (compact ? 1 : 2) && lines.every((l) => visW(l) <= room))
254
+ return lines.map((l) => ` ${l}`);
255
+ const worst = Math.max(...list.map((it) => it.pri ?? 2));
256
+ if (worst <= 1) break;
257
+ const idx = list.map((it) => it.pri ?? 2).lastIndexOf(worst);
258
+ list.splice(idx, 1);
259
+ }
260
+ }
261
+ return [` ${ansiTrunc(join(items.filter((it) => (it.pri ?? 2) <= 1), true), room)}`];
190
262
  }
191
263
  // Wrap a plain-text string into dim'd lines that fit within `maxW` columns.
192
264
  // Used for no-data/empty-state messages that can be long on narrow terminals.
@@ -209,15 +281,21 @@ function wrapDim(text, indent = " ", maxW) {
209
281
  if (line) lines.push(`${indent}${dim(line.trim())}`);
210
282
  return lines;
211
283
  }
284
+ // _hrW: column width override while a section is rendered into a column.
285
+ let _hrW = null;
212
286
  function hr(ch = "─") {
213
- return dim(ch.repeat(Math.max(0, W() - 4)));
287
+ return dim(ch.repeat(Math.max(0, _hrW ?? W() - 4)));
214
288
  }
215
289
 
216
290
  // ── Number formatters ───────────────────────────────────────────────────────
217
291
  const fmtY = (y) =>
218
292
  y == null
219
293
  ? "—"
220
- : y >= 10000
294
+ : y >= 1e9
295
+ ? `${(y / 1e9).toFixed(1)}B`
296
+ : y >= 1e6
297
+ ? `${(y / 1e6).toFixed(1)}M`
298
+ : y >= 10000
221
299
  ? `${(y / 1000).toFixed(1)}K`
222
300
  : y >= 1000
223
301
  ? `${(y / 1000).toFixed(2)}K`
@@ -331,7 +409,7 @@ function sparkline(values) {
331
409
  // (tools.mjs keeps its own `_ccusagePillars`/`_tokscalePillars`/`_tokenDashPillars` —
332
410
  // still used by the `tokenpull_compare` MCP tool.)
333
411
 
334
- async function loadDashboardData({ includeOmp = false } = {}) {
412
+ async function loadDashboardData({ includeOmp = false, onPrimary } = {}) {
335
413
  const boardPromise = Promise.race([
336
414
  fetch(`${DEFAULT_API_BASE}/api/v1/leaderboard?window=30d&metric=${LEADERBOARD_METRIC}`, {
337
415
  headers: { accept: "application/json" },
@@ -363,6 +441,16 @@ async function loadDashboardData({ includeOmp = false } = {}) {
363
441
  const active = await pullActivePlatforms({ platforms: ["claude"] }).catch(
364
442
  () => [],
365
443
  );
444
+ // PERF: hand the primary platform to the TUI the moment it lands, so Trends (and the cascade)
445
+ // can paint without waiting on the board/batched fetches (up to 5s each) below. The batched
446
+ // "all" override further down mutates these same objects in place.
447
+ if (active.length) {
448
+ try {
449
+ await onPrimary?.(active);
450
+ } catch {
451
+ /* paint hook is best-effort */
452
+ }
453
+ }
366
454
 
367
455
  const boardData = await boardPromise;
368
456
  const batchedData = await batchedPromise;
@@ -490,6 +578,87 @@ async function loadCompareData(platform = "claude") {
490
578
  };
491
579
  }
492
580
 
581
+ // PERF (2026-10-15): Compare cache. The fresh verifier pull above takes seconds to minutes, so
582
+ // the Compare tab shows the LAST SUCCESSFUL result for the platform immediately (with a dim
583
+ // "updated Ns ago · refreshing…" note) and refreshes in the background. Kept in memory for the
584
+ // session and mirrored to ~/.sigrank-mcp/compare-cache.json (dir 0700, file 0600 — same
585
+ // conventions as identity/keystore.mjs) so a fresh launch also paints instantly. Only the
586
+ // aggregated pillar numbers already shown on screen are stored. Never consulted off the TUI.
587
+ const COMPARE_CACHE_PATH = () =>
588
+ path.join(os.homedir(), ".sigrank-mcp", "compare-cache.json");
589
+ let _compareCache = null; // { [platform]: { at: epochMs, data } }
590
+
591
+ function _compareCacheAll() {
592
+ if (_compareCache) return _compareCache;
593
+ _compareCache = {};
594
+ try {
595
+ const raw = JSON.parse(readFileSync(COMPARE_CACHE_PATH(), "utf-8"));
596
+ if (raw && typeof raw === "object" && raw.v === 1 && raw.platforms) {
597
+ for (const [p, e] of Object.entries(raw.platforms)) {
598
+ if (e && Number.isFinite(e.at) && e.data && typeof e.data === "object")
599
+ _compareCache[p] = { at: e.at, data: { ...e.data, platform: p } };
600
+ }
601
+ }
602
+ } catch {
603
+ /* absent / corrupt cache → start empty */
604
+ }
605
+ return _compareCache;
606
+ }
607
+
608
+ function getCachedCompare(platform) {
609
+ return _compareCacheAll()[platform] ?? null;
610
+ }
611
+
612
+ // A result is worth caching only if at least one source produced data.
613
+ function _compareHasData(d) {
614
+ return !!(d && ((d.tpData?.windows?.length ?? 0) > 0 || d.cc || d.ts || d.td));
615
+ }
616
+
617
+ function putCachedCompare(platform, data) {
618
+ if (!_compareHasData(data)) return;
619
+ const all = _compareCacheAll();
620
+ // Keep only what renderCompare reads: per-window pillars from each source.
621
+ const slim = {
622
+ tpData: data.tpData
623
+ ? {
624
+ windows: (data.tpData.windows ?? []).map((w) => ({
625
+ window: w.window,
626
+ pillars: w.pillars,
627
+ })),
628
+ }
629
+ : null,
630
+ cc: data.cc ?? null,
631
+ ts: data.ts ?? null,
632
+ td: data.td ?? null,
633
+ platform,
634
+ };
635
+ all[platform] = { at: Date.now(), data: slim };
636
+ try {
637
+ const dir = path.dirname(COMPARE_CACHE_PATH());
638
+ mkdirSync(dir, { recursive: true });
639
+ try {
640
+ chmodSync(dir, 0o700);
641
+ } catch {
642
+ /* best-effort */
643
+ }
644
+ const tmp = `${COMPARE_CACHE_PATH()}.tmp-${process.pid}`;
645
+ writeFileSync(tmp, JSON.stringify({ v: 1, platforms: all }), { mode: 0o600 });
646
+ renameSync(tmp, COMPARE_CACHE_PATH());
647
+ } catch {
648
+ /* disk cache is best-effort — the in-memory cache still works */
649
+ }
650
+ }
651
+
652
+ function fmtAgo(ms) {
653
+ const s = Math.max(0, Math.round(ms / 1000));
654
+ if (s < 60) return `${s}s ago`;
655
+ const m = Math.round(s / 60);
656
+ if (m < 60) return `${m}m ago`;
657
+ const h = Math.round(m / 60);
658
+ if (h < 48) return `${h}h ago`;
659
+ return `${Math.round(h / 24)}d ago`;
660
+ }
661
+
493
662
  // #8 (2026-06-27): the Board tab shows ALL SUBMISSIONS ranked (raw submission rows), not operator
494
663
  // aggregates. Try the NEW GET /api/v1/submissions first; if it's not deployed yet (non-200 / 404),
495
664
  // FALL BACK to the existing /api/v1/leaderboard so nothing breaks pre-deploy. The return is tagged
@@ -536,18 +705,20 @@ const TABS = [
536
705
  { key: "6", label: "Connect", short: "N" }, // sign in / switch device — the whole app is the TUI
537
706
  ];
538
707
 
539
- // Four Degrees of Leverage — reference values matching signalaf.com/wiki/four-degrees.
708
+ // Four Degrees of Leverage — reference values from signalaf.com/wiki/four-degrees
709
+ // (table as published 2026-10-15).
540
710
  // base = AA baseline (7:2:1 modeled reference, static)
541
- // hcm = Human Center of Mass (median of all real operators)
542
- // power = Power users (median of top 100 real operators)
543
- // top = Top Evals (single top real operator)
544
- // STATIC fallback values match the web app's GoldColumn fallbacks (lib/marketing/top-operator-column.ts).
545
- // Goes live (hcm/power/top computed from the board) once there's real user volume.
711
+ // hcm = Operator Center of Mass (median of the screened operator cohort)
712
+ // power = Power users (median of the top 100 by yield)
713
+ // top = Top Evals (single leading operator to date)
714
+ // These are field references. The operator's OWN numbers must never be
715
+ // substituted in here — comparing someone against themselves made every
716
+ // metric read "top tier" (fixed 2026-10-15).
546
717
  const TD = {
547
- base: { yield: 1.75, snr: 0.33, vel: 0.50, lev: 3.5, d10: 0.54 },
548
- hcm: { yield: 1.57, snr: 0.33, vel: 0.50, lev: 3.2, d10: 0.50 },
549
- power: { yield: 1.51, snr: 0.07, vel: 0.08, lev: 22.3, d10: 1.35 },
550
- top: { yield: 488.65, snr: 0.58, vel: 1.36, lev: 360.2, d10: 2.56 },
718
+ base: { yield: 1.75, snr: 0.33, vel: 0.50, lev: 3.5, d10: 0.54 },
719
+ hcm: { yield: 1.90, snr: 0.09, vel: 0.10, lev: 20.3, d10: 1.31 },
720
+ power: { yield: 239.61, snr: 0.51, vel: 1.04, lev: 241.6, d10: 2.38 },
721
+ top: { yield: 941.23, snr: 0.63, vel: 1.70, lev: 553.2, d10: 2.74 },
551
722
  };
552
723
 
553
724
  function renderTabBar(activeIdx) {
@@ -557,28 +728,218 @@ function renderTabBar(activeIdx) {
557
728
  // logo
558
729
  const logo = ` ${gold("⊙ SigRank")}`;
559
730
  // tabs
560
- const tabStr = TABS.map((t, i) => {
561
- const lbl = ` ${t.key}:${t.label} `;
562
- return i === activeIdx
563
- ? `${c.bgCyan}${c.boldCyan}${lbl}${c.reset}`
564
- : `${c.bgDim}${c.dim}${lbl}${c.reset}`;
565
- }).join("");
566
731
  // right side — version (single source of truth) · site · clock
567
732
  const right = dim(
568
733
  `${gold("v" + VERSION)}${c.reset}${c.dim} · signalaf.com · ${ts}`,
569
734
  );
570
735
 
571
- const logoVis = stripAnsi(logo).length;
572
- const tabsVis = TABS.reduce((a, t) => a + t.label.length + 4, 0);
573
- const rightVis = stripAnsi(right).length;
574
- const gap = Math.max(1, w - logoVis - tabsVis - rightVis - 2);
575
-
576
- writeln(`${logo} ${tabStr}${" ".repeat(gap)}${right}`);
736
+ // Narrow terminals: shed the right-hand meta first (clock → site → version),
737
+ // then shorten INACTIVE tab labels (1:Dashboard → 1D → 1). The active tab
738
+ // always keeps its full name so you know where you are.
739
+ const tabLbl = (t, i, mode) =>
740
+ i === activeIdx || mode === 0
741
+ ? ` ${t.key}:${t.label} `
742
+ : mode === 1
743
+ ? ` ${t.key}${t.short} `
744
+ : ` ${t.key} `;
745
+ const tabsFor = (mode) =>
746
+ TABS.map((t, i) => {
747
+ const lbl = tabLbl(t, i, mode);
748
+ return i === activeIdx
749
+ ? `${c.bgCyan}${c.boldCyan}${lbl}${c.reset}`
750
+ : `${c.bgDim}${c.dim}${lbl}${c.reset}`;
751
+ }).join("");
752
+ const rights = [
753
+ right,
754
+ dim(`${gold("v" + VERSION)}${c.reset}${c.dim} · signalaf.com`),
755
+ gold("v" + VERSION),
756
+ "",
757
+ ];
758
+ const logoVis = visW(logo);
759
+ let line = null;
760
+ outer: for (const mode of [0, 1, 2]) {
761
+ for (const r of rights) {
762
+ const t = tabsFor(mode);
763
+ const used = logoVis + 2 + visW(t) + (r ? visW(r) + 1 : 0);
764
+ if (used <= w) {
765
+ const gap = r ? Math.max(1, w - used) : 0;
766
+ line = `${logo} ${t}${" ".repeat(gap)}${r}`;
767
+ break outer;
768
+ }
769
+ }
770
+ }
771
+ writeln(line ?? `${logo} ${tabsFor(2)}`);
577
772
  writeln(` ${hr()}`);
578
773
  }
579
774
 
580
775
  // ── GRAPHICS helpers ─────────────────────────────────────────────────────────
581
776
 
777
+ // Build archetype pixel glyphs (5×5, same art as the SignalAF promo graphics),
778
+ // drawn with half-blocks so 5 rows fit in 3 terminal lines.
779
+ const ARCH_GLYPH = {
780
+ "input-bound": ["10000", "10000", "10000", "10000", "11111"],
781
+ priming: ["00000", "00000", "00100", "01110", "11111"],
782
+ contextual: ["00000", "01110", "01010", "01110", "00000"],
783
+ "deep-reader": ["11111", "10001", "10101", "10001", "11111"],
784
+ archivist: ["11111", "11111", "00000", "11111", "11111"],
785
+ builder: ["00011", "00011", "01111", "01111", "11111"],
786
+ recursive: ["11111", "10000", "10111", "10101", "11101"],
787
+ amplifier: ["00100", "01110", "11111", "00100", "00100"],
788
+ convergent: ["10001", "01010", "00100", "00100", "00100"],
789
+ kinetic: ["00110", "01100", "11111", "00110", "01100"],
790
+ };
791
+ const ARCH_SHORT = {
792
+ "input-bound": "fresh input carries the workload",
793
+ priming: "reuse is beginning to form",
794
+ contextual: "retained context supports the work",
795
+ "deep-reader": "draws deep from accumulated context",
796
+ archivist: "a deep context library carries it",
797
+ builder: "creating material for future reuse",
798
+ recursive: "building on top of its own base",
799
+ amplifier: "reuse and construction at scale",
800
+ convergent: "elite on all three axes at once",
801
+ kinetic: "output breaks out past input",
802
+ };
803
+ function archGlyphLines(key) {
804
+ const g = ARCH_GLYPH[key];
805
+ if (!g) return ["", "", ""];
806
+ const half = (a, b = "00000") =>
807
+ [...a]
808
+ .map((x, i) =>
809
+ x === "1" && b[i] === "1" ? "██" : x === "1" ? "▀▀" : b[i] === "1" ? "▄▄" : " ",
810
+ )
811
+ .join("");
812
+ return [half(g[0], g[1]), half(g[2], g[3]), half(g[4])];
813
+ }
814
+ // cache : in : out, normalized to input = 1
815
+ function fmtOpRatio(p) {
816
+ if (!p?.input) return "—";
817
+ const lev = (p.cacheRead ?? 0) / p.input,
818
+ vel = (p.output ?? 0) / p.input;
819
+ const fl = lev >= 100 ? Math.round(lev).toLocaleString("en-US") : lev.toFixed(1);
820
+ return `${fl} : 1 : ${vel.toFixed(2)}`;
821
+ }
822
+ // Build archetype field medians (signalaf.com/field "Build Archetypes"; field
823
+ // snapshot — share of field, median leverage and velocity per archetype).
824
+ const FIELD_ARCH = [
825
+ { key: "input-bound", name: "INPUT-BOUND", n: 6.8, lev: 2.7, vel: 0.02 },
826
+ { key: "priming", name: "PRIMING", n: 9.4, lev: 7.7, vel: 0.042 },
827
+ { key: "contextual", name: "CONTEXTUAL", n: 11.7, lev: 12.7, vel: 0.066 },
828
+ { key: "deep-reader", name: "DEEP READER", n: 10.7, lev: 18.4, vel: 0.081 },
829
+ { key: "archivist", name: "ARCHIVIST", n: 11.7, lev: 29.8, vel: 0.124 },
830
+ { key: "builder", name: "BUILDER", n: 17.5, lev: 13.7, vel: 0.077 },
831
+ { key: "recursive", name: "RECURSIVE", n: 8.3, lev: 38.9, vel: 0.191 },
832
+ { key: "amplifier", name: "AMPLIFIER", n: 10.2, lev: 77.2, vel: 0.329 },
833
+ { key: "convergent", name: "CONVERGENT", n: 6.6, lev: 245.3, vel: 1.049 },
834
+ { key: "kinetic", name: "KINETIC", n: 7.1, lev: 361.5, vel: 1.52 },
835
+ ];
836
+ // Field map: the 10 archetype medians on log leverage × log velocity, plus
837
+ // ▲ YOU. Returns lines (no emit) so callers can budget them as one block.
838
+ function fieldMapLines(you, w, rows) {
839
+ const PH = Math.max(8, Math.min(14, rows - 5));
840
+ const PW = Math.max(40, Math.min(78, w - 14));
841
+ const X0 = Math.log10(1.5), X1 = Math.log10(900);
842
+ const Y0 = Math.log10(0.012), Y1 = Math.log10(4);
843
+ const gx = (lev) => Math.round(((Math.log10(Math.max(lev, 1.5)) - X0) / (X1 - X0)) * (PW - 1));
844
+ const gy = (vel) =>
845
+ PH - 1 - Math.round(((Math.log10(Math.min(Math.max(vel, 0.012), 4)) - Y0) / (Y1 - Y0)) * (PH - 1));
846
+ // cell grid: { ch, paint }
847
+ const grid = Array.from({ length: PH }, () => Array.from({ length: PW }, () => ({ ch: " ", p: null })));
848
+ const put = (x, y, ch, p) => {
849
+ if (y >= 0 && y < PH && x >= 0 && x < PW) grid[y][x] = { ch, p };
850
+ };
851
+ const free = (x, y, len) => {
852
+ if (y < 0 || y >= PH || x < 0 || x + len > PW) return false;
853
+ for (let i = 0; i < len; i++) if (grid[y][x + i].ch !== " " && grid[y][x + i].ch !== "┄") return false;
854
+ return true;
855
+ };
856
+ // output = input reference line
857
+ const yOne = gy(1);
858
+ for (let x = 0; x < PW; x++) put(x, yOne, "┄", dim);
859
+ const marks = FIELD_ARCH.map((a) => ({ x: gx(a.lev), y: gy(a.vel), a }));
860
+ const youMark = you ? { x: gx(you.lev), y: gy(you.vel) } : null;
861
+ for (const m of marks) put(m.x, m.y, "■", archetypeColor(m.a.key));
862
+ if (youMark) put(youMark.x, youMark.y, "▲", (s) => bold(paint(c.boldGold, s)));
863
+ const label = (x, y, txt, p) => {
864
+ for (const [lx, ly] of [[x + 2, y], [x - txt.length - 1, y], [x + 2, y - 1], [x + 2, y + 1], [x - txt.length - 1, y + 1]]) {
865
+ if (free(lx, ly, txt.length)) {
866
+ [...txt].forEach((ch, i) => put(lx + i, ly, ch, p));
867
+ return;
868
+ }
869
+ }
870
+ };
871
+ if (youMark) label(youMark.x, youMark.y, "YOU", (s) => bold(paint(c.boldGold, s)));
872
+ for (const m of marks) label(m.x, m.y, m.a.name, archetypeColor(m.a.key));
873
+ if (yOne >= 0 && yOne < PH && free(1, yOne, 14)) [..."output = input"].forEach((ch, i) => put(1 + i, yOne, ch, dim));
874
+ const yTick = { [gy(1)]: "1.0", [gy(0.1)]: "0.1", [gy(0.01)]: "0.01" };
875
+ const out = grid.map((row, y) => {
876
+ let line = "";
877
+ for (const cell of row) line += cell.p ? cell.p(cell.ch) : cell.ch;
878
+ return ` ${dim(padStart(yTick[y] ?? "", 5))} ${dim("│")}${line}`;
879
+ });
880
+ let axis = " ".repeat(PW).split("");
881
+ for (const [v, t] of [[2, "2×"], [10, "10×"], [100, "100×"], [500, "500×"]]) {
882
+ const x = gx(v);
883
+ [...t].forEach((ch, i) => { if (x + i < PW) axis[x + i] = ch; });
884
+ }
885
+ out.push(` ${" ".repeat(5)} ${dim("└" + "─".repeat(PW))}`);
886
+ out.push(` ${" ".repeat(6)}${dim(axis.join(""))}`);
887
+ out.push(
888
+ w >= 112
889
+ ? ` ${dim("velocity ↑ output per input · leverage → cache reads per input · ■ field median per archetype · signalaf.com/field")}`
890
+ : ` ${dim("↑ velocity · → leverage · ■ archetype median · signalaf.com/field")}`,
891
+ );
892
+ return out;
893
+ }
894
+
895
+ // Operator's combined all-time pillars across every active platform.
896
+ function combinedAllPillars(active) {
897
+ const p = { input: 0, output: 0, cacheCreate: 0, cacheRead: 0 };
898
+ for (const d of active ?? []) {
899
+ const all = d.windows?.find((x) => x.window === "all");
900
+ if (!all?.pillars) continue;
901
+ p.input += all.pillars.input ?? 0;
902
+ p.output += all.pillars.output ?? 0;
903
+ p.cacheCreate += all.pillars.cacheCreate ?? 0;
904
+ p.cacheRead += all.pillars.cacheRead ?? 0;
905
+ }
906
+ return p.input > 0 ? p : null;
907
+ }
908
+ // Identity block at the top of the Dashboard: archetype (shape), Υ, operating
909
+ // ratio, class (scale). Full 3-line version with the pixel glyph when there's
910
+ // room; one line on short/narrow terminals so the cascade table keeps its rows.
911
+ function heroLines(active, w, h) {
912
+ const p = combinedAllPillars(active);
913
+ if (!p) return [];
914
+ const cas = cascadeFrom(p);
915
+ if (!cas) return [];
916
+ const arch = classifyArchetype({ velocity: cas.velocity, leverage: cas.leverage, pillars: p });
917
+ const ac = archetypeColor(arch.key);
918
+ const clsFn = CLS[cas.class] ?? ((s) => s);
919
+ const ratio = fmtOpRatio(p);
920
+ if (h < 46 || w < 96) {
921
+ return [
922
+ ` ${ac("■")} ${ac(bold(arch.name))} ${dim("·")} ${dim("Υ")} ${bold(fmtY(cas.yield))} ${dim("·")} ${ratio} ${dim("·")} ${clsFn(cas.class)}`,
923
+ ];
924
+ }
925
+ const g = archGlyphLines(arch.key);
926
+ const col = (lbl, val, sub, width) => [padEnd(dim(lbl), width), padEnd(val, width), padEnd(sub, width)];
927
+ const cols = [
928
+ col("ARCHETYPE", ac(bold(arch.name)), dim(ARCH_SHORT[arch.key] ?? arch.familyLabel), 38),
929
+ col("Υ YIELD", bold(paint(c.boldGold, fmtY(cas.yield))), dim("all-time · all platforms"), 26),
930
+ col("OP RATIO", bold(ratio), dim("cache : in : out"), 22),
931
+ col("CLASS", clsFn(cas.class), dim("scale"), 12),
932
+ ];
933
+ // Cascade flow: what one unit of fresh input turns into.
934
+ const lev = (p.cacheRead ?? 0) / p.input, vel = (p.output ?? 0) / p.input;
935
+ const arrow = dim(" ──▶ ");
936
+ const flow =
937
+ ` ${dim("flow")} ${paint(c.cyan, `1 fresh input`)}${arrow}` +
938
+ `${paint(c.blue, `${fmtTok(Math.round((p.cacheCreate ?? 0) / p.input))} written`)}${dim(" · ")}` +
939
+ `${paint(c.boldGold, `${fmtLev(lev)} read back`)}${arrow}${paint(c.green, `${vel.toFixed(2)} output`)}`;
940
+ return [...[0, 1, 2].map((r) => ` ${ac(g[r])} ${cols.map((cc) => cc[r]).join("")}`), "", flow];
941
+ }
942
+
582
943
  // Horizontal stacked bar: input | cacheW | cacheR | output
583
944
  function tokenBar(p, width = 40) {
584
945
  if (!p) return dim(" no data");
@@ -702,7 +1063,17 @@ function renderDashboard(data, status = "", scrollOffset = 0) {
702
1063
  const budget = H() - 4; // 2 tab bar + 2 footer
703
1064
  const WINS = ["7d", "30d", "90d", "all"];
704
1065
  let used = 0;
1066
+ // Sections that didn't fit whole are listed on one "▾ … below" line instead
1067
+ // of being cut mid-table (height-budget policy).
1068
+ const hidden = [];
1069
+ // colBuf: when set, emit() collects lines for the two-column layout instead
1070
+ // of writing them (the lower report sections on wide terminals).
1071
+ let colBuf = null;
705
1072
  const emit = (s = "") => {
1073
+ if (colBuf) {
1074
+ colBuf.push(s);
1075
+ return;
1076
+ }
706
1077
  if (used < budget) {
707
1078
  writeln(s);
708
1079
  used++;
@@ -731,6 +1102,8 @@ function renderDashboard(data, status = "", scrollOffset = 0) {
731
1102
  const narrow = w < 124;
732
1103
  const veryNarrow = w < 100;
733
1104
  const ultraNarrow = w < 90;
1105
+ // Wide terminals (≥136) get a Build column: the archetype for that window.
1106
+ const wide = w >= 136;
734
1107
  const gap = narrow ? " " : " ";
735
1108
  const cwLabel = veryNarrow ? "CW" : "CacheW";
736
1109
  const crLabel = veryNarrow ? "CR" : "CacheR";
@@ -762,10 +1135,25 @@ function renderDashboard(data, status = "", scrollOffset = 0) {
762
1135
  padStart(cas.dev10x?.toFixed(2) ?? "—", 6),
763
1136
  ]),
764
1137
  ...(veryNarrow ? [] : [padEnd(clsFn(cas.class), 13)]),
1138
+ ...(wide
1139
+ ? [
1140
+ (() => {
1141
+ const a = classifyArchetype({ velocity: cas.velocity, leverage: cas.leverage, pillars: p });
1142
+ return padEnd(archetypeColor(a.key)(a.name), 12);
1143
+ })(),
1144
+ ]
1145
+ : []),
765
1146
  ];
766
1147
  emit(` ${cols.join(gap)}`);
767
1148
  };
768
1149
 
1150
+ // ── Identity: archetype · Υ · op ratio · class
1151
+ const hero = heroLines(active, w, H());
1152
+ if (hero.length) {
1153
+ emit();
1154
+ for (const ln of hero) emit(ln);
1155
+ }
1156
+
769
1157
  // ── Cascade table
770
1158
  emit();
771
1159
  emit(` ${bold("Your Cascade")} ${dim("all platforms · all windows")}`);
@@ -784,9 +1172,10 @@ function renderDashboard(data, status = "", scrollOffset = 0) {
784
1172
  ...(ultraNarrow ? [] : [padStart(hdr("Lev"), 7)]),
785
1173
  ...(narrow ? [] : [padStart(hdr("Vel"), 6), padStart(hdr("10x"), 6)]),
786
1174
  ...(veryNarrow ? [] : [padEnd(hdr("Class"), 13)]),
1175
+ ...(wide ? [padEnd(hdr("Build"), 12)] : []),
787
1176
  ];
788
1177
  emit(` ${CH.join(gap)}`);
789
- emit(` ${dim("·".repeat(Math.max(0, Math.min(w - 4, narrow ? (ultraNarrow ? 70 : veryNarrow ? 80 : 96) : 114))))}`);
1178
+ emit(` ${dim("·".repeat(Math.max(0, Math.min(w - 4, narrow ? (ultraNarrow ? 70 : veryNarrow ? 80 : 96) : wide ? 128 : 114))))}`);
790
1179
 
791
1180
  if (active.length === 0) {
792
1181
  // Count is derived from the registry — a hardcoded number drifts every time an
@@ -796,15 +1185,35 @@ function renderDashboard(data, status = "", scrollOffset = 0) {
796
1185
  );
797
1186
  }
798
1187
 
1188
+ // When 7d, 30d and 90d are identical but "all" is larger, the logs simply
1189
+ // have no activity between 7 and 90 days ago — say so, so it doesn't read as
1190
+ // a windowing bug (it was reported as one, 2026-10-15).
1191
+ const gapNote = (platform) => {
1192
+ const d = active.find((x) => x.platform === platform);
1193
+ const pick = (wk) => d?.windows?.find((x) => x.window === wk)?.pillars;
1194
+ const [a, b, c90, all] = ["7d", "30d", "90d", "all"].map(pick);
1195
+ const same = (x, y) =>
1196
+ x && y && ["input", "output", "cacheCreate", "cacheRead"].every((k) => (x[k] ?? 0) === (y[k] ?? 0));
1197
+ if (!a || !(a.input > 0) || !same(a, b) || !same(a, c90) || !all || same(a, all)) return null;
1198
+ return ` ${dim(`↳ 7d = 30d = 90d: no ${platform} activity logged between 7 and 90 days ago`)}`;
1199
+ };
1200
+
799
1201
  // Calculate how many cascade rows we can fit — reserve space for lower sections.
800
1202
  // (Υ-trend section moved to the Trends tab; its old `sparkLines` reservation is gone,
801
1203
  // which is what was starving codex's cascade rows after the Your-Read/Three-Degrees panels landed.)
802
1204
  const platformCount = active.length;
803
- const barLines = 4 + platformCount + (platformCount > 1 ? 1 : 0); // Token Composition: hr + header + platforms + combined + note
1205
+ // Token Composition: hr + header + one row per platform (local ∪ lifetime) + combined + submissions note
1206
+ const tcPlatCount = new Set([
1207
+ ...active.map((d) => d.platform),
1208
+ ...Object.keys(data?.batchedData?.per_platform ?? {}).filter((k) => k !== "multi"),
1209
+ ]).size;
1210
+ const barLines = 4 + tcPlatCount + (tcPlatCount > 1 ? 1 : 0);
804
1211
  // Your Read (hr+header+up to 4 wins+up to 5 gaps+summary = 13) + Four Degrees (blank+hr+header+col-header+5 rows+note = 10) + Lifetime Tokens (blank+hr+header+platforms+combined+note = ~7) + status
805
- const boardLines = 24;
1212
+ // +2: "Where you sit" line. Two-column layout (≥130 cols) needs fewer rows.
1213
+ const boardLines = w >= 130 ? 20 : 26;
806
1214
  const sectionsBelow = barLines + boardLines;
807
- const maxCascadeRows = Math.max(8, budget - used - sectionsBelow);
1215
+ const gapNotes = active.filter((d) => gapNote(d.platform)).length;
1216
+ const maxCascadeRows = Math.max(8, budget - used - sectionsBelow - gapNotes);
808
1217
 
809
1218
  // SCROLL-VIEW (2026-06-27): Build the full list of cascade rows first, then
810
1219
  // slice by [scrollOffset, scrollOffset + maxCascadeRows]. This gives a real
@@ -850,7 +1259,11 @@ function renderDashboard(data, status = "", scrollOffset = 0) {
850
1259
  // Render the visible slice — group by platform (emit a blank line between platforms)
851
1260
  let lastPlatform = null;
852
1261
  for (const row of visibleRows) {
853
- if (lastPlatform !== null && row.platform !== lastPlatform) emit();
1262
+ if (lastPlatform !== null && row.platform !== lastPlatform) {
1263
+ const note = gapNote(lastPlatform);
1264
+ if (note) emit(note);
1265
+ emit();
1266
+ }
854
1267
  renderRow(
855
1268
  row.platform,
856
1269
  (s) => {
@@ -863,6 +1276,10 @@ function renderDashboard(data, status = "", scrollOffset = 0) {
863
1276
  );
864
1277
  lastPlatform = row.platform;
865
1278
  }
1279
+ if (lastPlatform !== null) {
1280
+ const note = gapNote(lastPlatform);
1281
+ if (note) emit(note);
1282
+ }
866
1283
  if (visibleRows.length > 0) emit();
867
1284
 
868
1285
  // ▼ indicator: rows below the current viewport
@@ -899,34 +1316,72 @@ function renderDashboard(data, status = "", scrollOffset = 0) {
899
1316
 
900
1317
  // (Υ Trend moved to the dedicated Trends tab [2] — every metric, with sub-views.)
901
1318
 
902
- // ── Token bar charts (skip if no room)
903
- if (used < budget - boardLines) {
1319
+ // ── Token Composition — lifetime per platform (owner 2026-10-15: "combine token
1320
+ // composition with lifetime" + "split up i o cw"). Each row: stacked bar + the
1321
+ // four pillars as separate numbers (cache read swamps the bar, so the small
1322
+ // pillars need their own figures) + total. Lifetime = archived submissions
1323
+ // (never decreases) when signed in; local all-time otherwise / for local-only
1324
+ // platforms. Replaces the separate Lifetime Tokens section.
1325
+ const batchedTC = data?.batchedData;
1326
+ const tcList = [];
1327
+ const seenTC = new Set();
1328
+ for (const [plat, pw] of Object.entries(batchedTC?.per_platform ?? {})) {
1329
+ if (plat === "multi" || !pw?.all) continue;
1330
+ tcList.push({ plat, p: pw.all, d90: pw["90d"], src: "lifetime" });
1331
+ seenTC.add(plat);
1332
+ }
1333
+ const samePillars = (a, b) =>
1334
+ ["input", "output", "cacheCreate", "cacheRead"].every((k) => (a?.[k] ?? 0) === (b?.[k] ?? 0));
1335
+ for (const d of active) {
1336
+ if (seenTC.has(d.platform)) continue;
1337
+ const all = d.windows?.find((x) => x.window === "all");
1338
+ // A local-only platform whose totals exactly match a lifetime row was
1339
+ // already submitted under that row's name (e.g. antigravity → "other").
1340
+ if (all?.pillars && !tcList.some((r) => samePillars(r.p, all.pillars)))
1341
+ tcList.push({ plat: d.platform, p: all.pillars, src: "local" });
1342
+ }
1343
+ const tot = (p) => (p?.input ?? 0) + (p?.output ?? 0) + (p?.cacheCreate ?? 0) + (p?.cacheRead ?? 0);
1344
+ tcList.sort((a, b) => tot(b.p) - tot(a.p));
1345
+ let tcCombined = batchedTC?.combined?.all ?? null;
1346
+ if (!tcCombined && tcList.length > 1) {
1347
+ tcCombined = { input: 0, output: 0, cacheCreate: 0, cacheRead: 0 };
1348
+ for (const r of tcList) for (const k of Object.keys(tcCombined)) tcCombined[k] += r.p[k] ?? 0;
1349
+ }
1350
+ const subsTC = Object.entries(batchedTC?.per_platform ?? {})
1351
+ .filter(([k]) => k !== "multi")
1352
+ .reduce((n, [, pw]) => n + (pw.submission_count ?? 0), 0);
1353
+ const baseTC = Object.values(batchedTC?.per_platform ?? {})
1354
+ .map((pw) => pw.baseline_date)
1355
+ .filter(Boolean)
1356
+ .sort()[0];
1357
+ const tcRows = 2 + tcList.length + (tcCombined ? 1 : 0) + (subsTC ? 1 : 0);
1358
+ if (tcList.length && budget - used < tcRows) hidden.push("Token Composition");
1359
+ if (tcList.length && budget - used >= tcRows) {
1360
+ const showSplit = w >= 100;
1361
+ const NAME_W = 13;
1362
+ const splitW = 44; // "in 15.3M out 24.9M cw 238.7M cr 6.7B"
1363
+ const barW = showSplit ? Math.max(12, Math.min(36, w - 4 - NAME_W - 2 - splitW - 18)) : 50;
1364
+ const split = (p) =>
1365
+ `${paint(c.cyan, "in")} ${padStart(fmtTok(p.input ?? 0), 6)} ${paint(c.green, "out")} ${padStart(fmtTok(p.output ?? 0), 6)} ${paint(c.blue, "cw")} ${padStart(fmtTok(p.cacheCreate ?? 0), 6)} ${paint(c.boldGold, "cr")} ${padStart(fmtTok(p.cacheRead ?? 0), 6)}`;
1366
+ const row = (name, p, extra = "") =>
1367
+ emit(
1368
+ ` ${padEnd(name, NAME_W)}${tokenBar(p, barW)} ${showSplit ? split(p) + " " : ""}${bold(padStart(fmtTok(tot(p)), 6))}${extra}`,
1369
+ );
904
1370
  emit(` ${hr()}`);
905
1371
  emit(
906
- ` ${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")}`,
1372
+ ` ${bold("Token Composition")} ${dim(batchedTC ? "lifetime · archived submissions, never decreases" : "all-time · local logs")} ${dim("█")}${paint(c.cyan, "in")} ${dim("█")}${paint(c.blue, "cw")} ${dim("█")}${paint(c.boldGold, "cr")} ${dim("█")}${paint(c.green, "out")}`,
907
1373
  );
908
- for (const d of active) {
909
- if (used >= budget - boardLines) break;
910
- const all = d.windows?.find((w) => w.window === "all");
911
- if (!all) continue;
912
- emit(
913
- ` ${padEnd(platformColor(d.platform)(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)))}`,
914
- );
1374
+ for (const r of tcList) {
1375
+ if (used >= budget - 1) break;
1376
+ const d90 = r.d90 && w >= 132 ? dim(` ${fmtTok(tot(r.d90))} in 90d`) : r.src === "local" && batchedTC ? dim(" local") : "";
1377
+ row(platformColor(r.plat)(r.plat), r.p, d90);
915
1378
  }
916
- if (active.length > 1) {
917
- const cp = { input: 0, output: 0, cacheCreate: 0, cacheRead: 0 };
918
- for (const d of active) {
919
- const all = d.windows?.find((w) => w.window === "all");
920
- if (!all) continue;
921
- cp.input += all.pillars.input ?? 0;
922
- cp.output += all.pillars.output ?? 0;
923
- cp.cacheCreate += all.pillars.cacheCreate ?? 0;
924
- cp.cacheRead += all.pillars.cacheRead ?? 0;
925
- }
926
- emit(
927
- ` ${padEnd(bold(active.map((d) => d.platform).join("+")), 10)} ${tokenBar(cp, 50)} ${dim(fmtTok(cp.input + cp.output + cp.cacheCreate + cp.cacheRead))}`,
928
- );
1379
+ if (tcCombined && used < budget) {
1380
+ const d90 = batchedTC?.combined?.["90d"] && w >= 132 ? dim(` ${fmtTok(tot(batchedTC.combined["90d"]))} in 90d`) : "";
1381
+ row(bold("combined"), tcCombined, d90);
929
1382
  }
1383
+ if (subsTC && used < budget)
1384
+ emit(` ${dim(`${subsTC} submission${subsTC !== 1 ? "s" : ""} archived${baseTC ? ` · baseline ${String(baseTC).slice(0, 10)}` : ""}`)}`);
930
1385
  // Log-scale per-pillar breakdown — companion to the linear stacked bar above.
931
1386
  // The linear bar erases small pillars when one dominates by 1000× (MOSES-class:
932
1387
  // cacheRead 2.5B vs input 1.25M → input gets 0 width). The log view shows every
@@ -972,30 +1427,49 @@ function renderDashboard(data, status = "", scrollOffset = 0) {
972
1427
  return null;
973
1428
  })();
974
1429
 
975
- if (used < budget - 8) {
976
- // Compute the operator's combined "all" cascade for live Four Degrees.
977
- // TD.top is overridden with the live metrics so the "Top Evals" column
978
- // reflects the actual top operator instead of a static snapshot.
979
- const combinedAll = { input: 0, output: 0, cacheCreate: 0, cacheRead: 0 };
980
- for (const d of active) {
981
- const all = d.windows?.find((w) => w.window === "all");
982
- if (!all) continue;
983
- combinedAll.input += all.pillars.input ?? 0;
984
- combinedAll.output += all.pillars.output ?? 0;
985
- combinedAll.cacheCreate += all.pillars.cacheCreate ?? 0;
986
- combinedAll.cacheRead += all.pillars.cacheRead ?? 0;
987
- }
988
- const liveTop = cascadeFrom(combinedAll);
989
- const tdLive = { ...TD };
990
- if (liveTop && (liveTop.yield ?? 0) > 0) {
991
- tdLive.top = {
992
- yield: liveTop.yield,
993
- snr: liveTop.snr,
994
- vel: liveTop.velocity ?? TD.top.vel,
995
- lev: liveTop.leverage ?? TD.top.lev,
996
- d10: liveTop.dev10x ?? TD.top.d10,
997
- };
1430
+ // "Where you sit": field references + you on one Υ line, ordered by value,
1431
+ // spacing proportional to log distance (fits any width — labels never collide).
1432
+ function whereYouSitLine() {
1433
+ const youP = combinedAllPillars(active);
1434
+ const youCas = youP && cascadeFrom(youP);
1435
+ if (!(youCas?.yield > 0)) return null;
1436
+ const youArch = classifyArchetype({ velocity: youCas.velocity, leverage: youCas.leverage, pillars: youP });
1437
+ const ac = archetypeColor(youArch.key);
1438
+ const pts = [
1439
+ { v: TD.base.yield, s: dim(`AA ${fmtY(TD.base.yield)}`) },
1440
+ { v: TD.hcm.yield, s: `Field ${fmtY(TD.hcm.yield)}` },
1441
+ { v: TD.power.yield, s: `Power ${fmtY(TD.power.yield)}` },
1442
+ { v: TD.top.yield, s: gold(`Top ${fmtY(TD.top.yield)}`) },
1443
+ { v: youCas.yield, s: ac(bold(`▲ YOU ${fmtY(youCas.yield)}`)) },
1444
+ ].sort((x, y) => x.v - y.v);
1445
+ const lead = ` ${bold("Where you sit")} ${dim("Υ")} `;
1446
+ const labels = pts.reduce((n, pt) => n + visW(pt.s), 0);
1447
+ const avail = Math.max(pts.length - 1, w - visW(lead) - labels - 2 * (pts.length - 1) - 2);
1448
+ const lg = (v) => Math.log10(Math.max(v, 0.01));
1449
+ const span = Math.max(0.001, lg(pts[pts.length - 1].v) - lg(pts[0].v));
1450
+ let line = lead + pts[0].s;
1451
+ for (let i = 1; i < pts.length; i++) {
1452
+ const n = Math.max(1, Math.round(((lg(pts[i].v) - lg(pts[i - 1].v)) / span) * avail));
1453
+ line += ` ${dim("─".repeat(n))} ${pts[i].s}`;
998
1454
  }
1455
+ return line;
1456
+ }
1457
+
1458
+ // ── Lower report: two columns on wide terminals (owner 2026-10-15: "after all
1459
+ // the tokens load we should go into two columns"). Left = Your Read + Lifetime
1460
+ // Tokens, right = Four Degrees, then "Where you sit" full width underneath.
1461
+ const twoCol = w >= 130;
1462
+ const colW = Math.floor((w - 2) / 2);
1463
+ const parts = {};
1464
+ if (twoCol) {
1465
+ colBuf = [];
1466
+ _hrW = colW - 4;
1467
+ }
1468
+ if (used < budget - 8) {
1469
+ // Field references for Your Read + Four Degrees. Previously TD.top was
1470
+ // overwritten with the operator's own combined "all" cascade, so users were
1471
+ // compared against themselves; the published field values are used instead.
1472
+ const tdLive = TD;
999
1473
  // ── Custom insights — comparison vs the field + a concrete tip for the weakest metric
1000
1474
  emit(` ${hr()}`);
1001
1475
  emit(
@@ -1092,7 +1566,7 @@ function renderDashboard(data, status = "", scrollOffset = 0) {
1092
1566
  const hcmStr = `${aboveHcm}/${totalMetrics}`;
1093
1567
  const pwrStr = `${abovePower}/${totalMetrics}`;
1094
1568
  emit(
1095
- ` ${dim(`above HCM on ${bold(hcmStr)} · above Power on ${bold(pwrStr)}`)}`,
1569
+ ` ${dim(`above field on ${bold(hcmStr)} · above Power on ${bold(pwrStr)}`)}`,
1096
1570
  );
1097
1571
  }
1098
1572
  } else {
@@ -1101,15 +1575,22 @@ function renderDashboard(data, status = "", scrollOffset = 0) {
1101
1575
 
1102
1576
  // ── Four Degrees — AA baseline · Human Center of Mass · Power users · Top Evals
1103
1577
  // Matches signalaf.com/wiki/four-degrees (expanded from 3 → 4 degrees 2026-07-17).
1578
+ // Needs 12 rows (blank+hr+title+header+5 rows+note+blank+where-you-sit).
1579
+ if (twoCol) {
1580
+ parts.read = colBuf;
1581
+ colBuf = [];
1582
+ }
1583
+ if (!twoCol && budget - used < 12) hidden.push("Four Degrees");
1584
+ else {
1104
1585
  emit();
1105
1586
  emit(` ${hr()}`);
1106
1587
  emit(
1107
- ` ${bold("Four Degrees")} ${dim("AA · HCM · Power · Top Evals — signalaf.com/wiki")}`,
1588
+ ` ${bold("Four Degrees")} ${dim("AA · Field · Power · Top Evals — signalaf.com/wiki")}`,
1108
1589
  );
1109
1590
  const tdHead = [
1110
1591
  padEnd(hdr("Metric"), 10),
1111
1592
  padStart(hdr("AA base"), 9),
1112
- padStart(hdr("HCM"), 9),
1593
+ padStart(hdr("Field"), 9),
1113
1594
  padStart(hdr("Power"), 9),
1114
1595
  padStart(hdr("Top"), 9),
1115
1596
  ];
@@ -1134,15 +1615,36 @@ function renderDashboard(data, status = "", scrollOffset = 0) {
1134
1615
  );
1135
1616
  tdRow("10xDEV", tdLive.base.d10, tdLive.hcm.d10, tdLive.power.d10, tdLive.top.d10, (v) => v.toFixed(2));
1136
1617
  emit(
1137
- ` ${dim("Top live · HCM/Power calibrate with volume")}`,
1618
+ ` ${dim("Field = operator center of mass · published field references")}`,
1138
1619
  );
1620
+ // Where you sit — inline in the single-column layout (two-column puts it
1621
+ // full width under the columns).
1622
+ const youLine = !twoCol ? whereYouSitLine() : null;
1623
+ if (youLine && used < budget - 2) {
1624
+ emit();
1625
+ emit(youLine);
1626
+ }
1627
+ }
1628
+ } else hidden.push("Your Read · Four Degrees");
1629
+
1630
+ if (twoCol) {
1631
+ if (!parts.read) parts.read = colBuf;
1632
+ else parts.four = colBuf;
1633
+ colBuf = [];
1139
1634
  }
1140
1635
 
1141
1636
  // ── Lifetime Tokens — submission-based rolled-forward totals (never decrease)
1142
1637
  // Shows when the operator is signed in and has submission history. The batched
1143
1638
  // data comes from the server (computeBatchedWindows over snapshot_submissions).
1144
1639
  const batched = data?.batchedData;
1145
- if (batched && used < budget - 4) {
1640
+ const lifetimeRows = batched
1641
+ ? 4 + Object.keys(batched.per_platform ?? {}).filter((k) => k !== "multi").length + 2
1642
+ : 0;
1643
+ // Lifetime is now shown inside Token Composition (per-platform lifetime rows);
1644
+ // this standalone section only renders if that section had to be hidden.
1645
+ const lifetimeMerged = !hidden.includes("Token Composition");
1646
+ if (batched && !lifetimeMerged && !twoCol && budget - used < lifetimeRows) hidden.push("Lifetime Tokens");
1647
+ if (batched && !lifetimeMerged && (twoCol || budget - used >= lifetimeRows)) {
1146
1648
  const fmtT = (n) => {
1147
1649
  if (n >= 1e9) return (n / 1e9).toFixed(1) + "B";
1148
1650
  if (n >= 1e6) return (n / 1e6).toFixed(1) + "M";
@@ -1183,6 +1685,34 @@ function renderDashboard(data, status = "", scrollOffset = 0) {
1183
1685
  }
1184
1686
  }
1185
1687
 
1688
+ if (twoCol) {
1689
+ parts.life = colBuf;
1690
+ colBuf = null;
1691
+ _hrW = null;
1692
+ const trim = (a) => {
1693
+ const x = [...(a ?? [])];
1694
+ while (x.length && !stripAnsi(x[0]).trim()) x.shift();
1695
+ while (x.length && !stripAnsi(x[x.length - 1]).trim()) x.pop();
1696
+ return x;
1697
+ };
1698
+ // Lifetime's own divider line separates it from Your Read — no blank row.
1699
+ const left = [...trim(parts.read), ...trim(parts.life)];
1700
+ const right = trim(parts.four);
1701
+ const rows = Math.max(left.length, right.length);
1702
+ const fit = (ln) => padEnd(ansiTrunc(ln ?? "", colW), colW);
1703
+ const where = whereYouSitLine();
1704
+ const room = budget - used - (where ? 2 : 0) - (hidden.length ? 1 : 0) - (status ? 1 : 0);
1705
+ const shown = Math.min(rows, Math.max(0, room));
1706
+ for (let i = 0; i < shown; i++) emit(`${fit(left[i])} ${fit(right[i])}`.replace(/\s+$/, ""));
1707
+ if (shown < rows) hidden.push("rest of the report");
1708
+ if (where && used < budget - 1) {
1709
+ emit();
1710
+ emit(where);
1711
+ }
1712
+ }
1713
+
1714
+ if (hidden.length && used < budget)
1715
+ emit(` ${dim(`▾ ${hidden.join(" · ")} below — make the window taller to see`)}`);
1186
1716
  if (status && used < budget) emit(` ${dim(status)}`);
1187
1717
  }
1188
1718
 
@@ -1238,6 +1768,9 @@ const TREND_METRICS = [
1238
1768
  : null,
1239
1769
  fmt: (v) => {
1240
1770
  const i = v.input || 1;
1771
+ // Narrow terminals: compact "1.1K:1:4.0" (fits the 10-col window cells,
1772
+ // the spaced form overflowed at 80×24).
1773
+ if (W() < 120) return `${fmtLev(v.cacheRead / i)}:1:${(v.output / i).toFixed(1)}`;
1241
1774
  return `${(v.cacheRead / i).toFixed(1)} : 1 : ${(v.output / i).toFixed(2)}`;
1242
1775
  },
1243
1776
  },
@@ -1270,6 +1803,11 @@ function renderTrends(data, subIdx = 0) {
1270
1803
  emit(
1271
1804
  ` ${bold("Trends")} ${dim("every metric across windows (7d → all)")} ${subBar}`,
1272
1805
  );
1806
+ // PERF: Trends paints as soon as ANY platform is in (claude lands first); say so while the
1807
+ // rest of the progressive scan is still filling dashData.active.
1808
+ if (data?._loading && active?.length) {
1809
+ emit(` ${dim(`more platforms loading… (${active.length} in so far)`)}`);
1810
+ }
1273
1811
  emit(` ${dim("·".repeat(Math.min(W() - 4, 70)))}`);
1274
1812
  emit();
1275
1813
 
@@ -1329,27 +1867,20 @@ function renderTrends(data, subIdx = 0) {
1329
1867
  // FIX G1: window column headers (mirror renderCompare) — one header row, then
1330
1868
  // values in fixed-width columns below. No per-value `w:` prefix.
1331
1869
  emit(` ${trendHeader()}`);
1870
+ // Metrics that aren't computed client-side yet (m.stub) + Rank are listed
1871
+ // on one "coming later" line instead of rows stuck on "calculating…".
1872
+ const live = TREND_METRICS.filter((m) => !m.stub);
1332
1873
  let mi = 0;
1333
- for (; mi < TREND_METRICS.length; mi++) {
1334
- const m = TREND_METRICS[mi];
1874
+ for (; mi < live.length; mi++) {
1875
+ const m = live[mi];
1335
1876
  if (used >= budget - 2) break;
1336
- if (m.stub) {
1337
- emit(
1338
- ` ${padEnd(dim(m.label), 10)} ${dim(" calculating… (" + m.stub + ")")}`,
1339
- );
1340
- } else {
1341
- emit(
1342
- ` ${padEnd(cyan(m.label), 10)} ${metricSpark(d, m.pick, m.fmt)}`,
1343
- );
1344
- }
1345
- }
1346
- trendsDropped += TREND_METRICS.length - mi; // metrics this block couldn't fit
1347
- // rank trend — wired later; honest placeholder for now
1348
- if (used < budget - 1)
1349
1877
  emit(
1350
- ` ${padEnd(dim("Rank"), 10)} ${dim(" calculating… (rank history wires post-ingest)")}`,
1878
+ ` ${padEnd(cyan(m.label), 10)} ${metricSpark(d, m.pick, m.fmt)}`,
1351
1879
  );
1352
- else trendsDropped += 1;
1880
+ }
1881
+ trendsDropped += live.length - mi; // metrics this block couldn't fit
1882
+ const later = [...TREND_METRICS.filter((m) => m.stub).map((m) => m.label), "Rank"];
1883
+ if (used < budget - 1) emit(` ${dim(`coming later: ${later.join(" · ")}`)}`);
1353
1884
  emit();
1354
1885
  }
1355
1886
  // A3: surface the dropped count instead of hiding it.
@@ -1358,11 +1889,17 @@ function renderTrends(data, subIdx = 0) {
1358
1889
  if (trendsDropped > 0)
1359
1890
  emit(` ${dim(` +${trendsDropped} more — resize to see more`)}`);
1360
1891
  } else {
1361
- // Field
1362
- emit(` ${dim(" Field-wide trends — calculating…")}`);
1892
+ // Field — where your build sits among the 10 build archetypes.
1893
+ const p = combinedAllPillars(active);
1894
+ const you = p ? { lev: (p.cacheRead ?? 0) / p.input, vel: (p.output ?? 0) / p.input } : null;
1895
+ const arch = p ? classifyArchetype({ velocity: you.vel, leverage: you.lev, pillars: p }) : null;
1363
1896
  emit(
1364
- ` ${dim(" (needs per-window board history; materializes once live ingest lands)")}`,
1897
+ ` ${bold("Field map")} ${arch ? `${dim("you:")} ${archetypeColor(arch.key)(bold(arch.name))} ${dim(fmtOpRatio(p))}` : dim("reading your cascade…")}`,
1365
1898
  );
1899
+ emit();
1900
+ for (const ln of fieldMapLines(you, W(), budget - used)) emit(ln);
1901
+ emit();
1902
+ emit(` ${dim("Field-wide trends over time: coming later (needs per-window board history)")}`);
1366
1903
  }
1367
1904
  }
1368
1905
 
@@ -1375,15 +1912,20 @@ function renderCompare(data) {
1375
1912
  const platform = data?.platform ?? "claude";
1376
1913
  const WINS = ["7d", "30d", "90d", "all"];
1377
1914
  const w = W();
1378
- // Compare tab needs more vertical space — 4 sources × 4 pillars = 16 rows
1379
- // + headers + cascade metrics. Use full terminal height (not H()-4) and
1380
- // fall back to a generous fixed minimum when rows is undefined (--render mode).
1381
- const budget = Math.max(H() - 2, 40);
1915
+ // Real height (tab bar + footer = 4 rows). The old 40-row floor rendered rows
1916
+ // the frame then clipped, which the layout audit flags as silently dropped.
1917
+ // When content runs out of room the last row says so instead.
1918
+ const budget = Math.max(6, H() - 4);
1382
1919
  let used = 0;
1920
+ let clipped = false;
1383
1921
  const emit = (s = "") => {
1384
- if (used < budget) {
1922
+ if (used < budget - 1) {
1385
1923
  writeln(s);
1386
1924
  used++;
1925
+ } else if (!clipped) {
1926
+ clipped = true;
1927
+ writeln(` ${dim("▾ more below — make the window taller to see")}`);
1928
+ used++;
1387
1929
  }
1388
1930
  };
1389
1931
 
@@ -1425,12 +1967,18 @@ function renderCompare(data) {
1425
1967
  emit(
1426
1968
  ` ${bold("Source Comparison")} ${dim(`platform: ${platform}`)} ${dim(`· ${sourceList}`)}`,
1427
1969
  );
1970
+ // PERF: cached result → say how old it is and whether a refresh is in flight.
1971
+ if (data?._cachedAt) {
1972
+ const ago = fmtAgo(Date.now() - data._cachedAt);
1973
+ const tail = data._refreshing
1974
+ ? "refreshing…"
1975
+ : data._refreshFailed
1976
+ ? "refresh failed · [R] retries"
1977
+ : "[R] refreshes";
1978
+ emit(` ${dim(`updated ${ago} · ${tail}`)}`);
1979
+ }
1428
1980
 
1429
- if (!tpData) {
1430
- emit(
1431
- ` ${dim("tokenpull: no JSONL data found — check ~/.claude/projects/")}`,
1432
- );
1433
- } else {
1981
+ if (tpData) {
1434
1982
  const all = tpPillars["all"];
1435
1983
  if (all) {
1436
1984
  emit(
@@ -1441,22 +1989,35 @@ function renderCompare(data) {
1441
1989
  // FIX (2026-08-18): surface WHY external verifiers are absent so the user knows
1442
1990
  // it's a missing-tool issue, not a bug. Each verifier is null when its CLI/DB
1443
1991
  // isn't installed or returned no data for this platform.
1444
- const missing = [];
1445
- if (!cc) missing.push("ccusage (npm i -g ccusage)");
1446
- if (!td) missing.push("token-dashboard (~/.claude/token-dashboard.db)");
1447
- if (!ts) missing.push("tokscale (not installed, or scan timed out)");
1448
- if (missing.length)
1449
- for (const ln of wrapDim(`no data from: ${missing.join(" · ")}`)) emit(ln);
1992
+ // Source checklist: ✓ found / ✗ missing + how to get it. Only shown when
1993
+ // something is missing (a full house needs no explanation).
1994
+ const srcStatus = [
1995
+ { ok: !!tpData, name: "tokenpull", how: "reads your local session logs (~/.claude/projects/)" },
1996
+ { ok: !!cc, name: "ccusage", how: "npm i -g ccusage" },
1997
+ { ok: !!td, name: "token-dashboard", how: "~/.claude/token-dashboard.db" },
1998
+ { ok: !!ts, name: "tokscale", how: "not installed, or scan timed out" },
1999
+ ];
2000
+ if (srcStatus.some((x) => !x.ok)) {
2001
+ // Short terminals: one line (names only) so the comparison table keeps its rows.
2002
+ if (budget - used < 16) {
2003
+ emit(
2004
+ ` ${dim("sources:")} ${srcStatus.map((x) => `${x.ok ? green("✓") : paint(c.red, "✗")} ${x.ok ? x.name : dim(x.name)}`).join(" ")}`,
2005
+ );
2006
+ } else {
2007
+ emit();
2008
+ for (const x of srcStatus)
2009
+ emit(
2010
+ ` ${x.ok ? green("✓") : paint(c.red, "✗")} ${padEnd(x.ok ? x.name : dim(x.name), 16)} ${dim(x.ok ? "found" : x.how)}`,
2011
+ );
2012
+ }
2013
+ }
1450
2014
  emit();
1451
2015
 
1452
2016
  // FIX (2026-08-18): if ALL sources are empty (tokenpull has no windows AND all
1453
2017
  // verifiers are null), show a clear empty-state message instead of bare headers
1454
2018
  // with zero data rows.
1455
2019
  if (SOURCES.length === 0) {
1456
- for (const ln of wrapDim(
1457
- "no source data available for this platform — install a verifier or check ~/.claude/projects/",
1458
- ))
1459
- emit(ln);
2020
+ emit(` ${dim(`nothing to compare for ${platform} yet — [P] tries another platform`)}`);
1460
2021
  return;
1461
2022
  }
1462
2023
 
@@ -1535,7 +2096,11 @@ function renderCompare(data) {
1535
2096
  }
1536
2097
  }
1537
2098
 
1538
- if (used < budget - 4) {
2099
+ // Whole-section rule: hr + title + header + dots + one row per source.
2100
+ const metricsRows = 4 + SOURCES.length;
2101
+ if (budget - used < metricsRows) {
2102
+ if (used < budget) emit(` ${dim("▾ Cascade Metrics below — make the window taller to see")}`);
2103
+ } else {
1539
2104
  emit(` ${hr()}`);
1540
2105
  emit(
1541
2106
  ` ${bold("Cascade Metrics")} ${dim("all-time · computed from each source")}`,
@@ -1585,6 +2150,7 @@ function renderSubmissions(
1585
2150
  window = "30d",
1586
2151
  filterCodename = null,
1587
2152
  highlightCodename = null,
2153
+ uniqueOnly = false,
1588
2154
  ) {
1589
2155
  const rows =
1590
2156
  boardData?.submissions ??
@@ -1618,9 +2184,10 @@ function renderSubmissions(
1618
2184
  : "";
1619
2185
  emit();
1620
2186
  // Shorten the header on narrow terminals to avoid overflow.
2187
+ const scope = uniqueOnly ? "best per operator" : "all submissions";
1621
2188
  const boardHeader = w < 90
1622
- ? `window: ${window} · ranked by Υ Yield`
1623
- : `window: ${window} · all submissions ranked by Υ Yield · signalaf.com`;
2189
+ ? `window: ${window} · ${uniqueOnly ? "best per operator" : "ranked by Υ Yield"}`
2190
+ : `window: ${window} · ${scope} ranked by Υ Yield · signalaf.com`;
1624
2191
  emit(
1625
2192
  ` ${bold("Submissions")} ${dim(boardHeader)}${modeLabel}`,
1626
2193
  );
@@ -1632,6 +2199,17 @@ function renderSubmissions(
1632
2199
  }
1633
2200
 
1634
2201
  let sorted = [...rows].sort((a, b) => (gYield(b) ?? 0) - (gYield(a) ?? 0));
2202
+ // [U]: one row per operator — their best submission (rows are already
2203
+ // sorted by Υ, so the first one seen is the best).
2204
+ if (uniqueOnly) {
2205
+ const seen = new Set();
2206
+ sorted = sorted.filter((e) => {
2207
+ const key = (gName(e) ?? "").toLowerCase();
2208
+ if (seen.has(key)) return false;
2209
+ seen.add(key);
2210
+ return true;
2211
+ });
2212
+ }
1635
2213
  if (filterCodename) {
1636
2214
  sorted = sorted.filter(
1637
2215
  (e) => (gName(e) ?? "").toLowerCase() === filterCodename.toLowerCase(),
@@ -1644,14 +2222,16 @@ function renderSubmissions(
1644
2222
  }
1645
2223
 
1646
2224
  // op_ratio may arrive as a preformatted string ("3.5 : 1 : 0.50") or a number — render either.
1647
- const fmtOp = (v) =>
1648
- v == null
1649
- ? "—"
1650
- : typeof v === "string"
1651
- ? v
1652
- : typeof v === "number"
1653
- ? v.toFixed(2)
1654
- : String(v);
2225
+ // "200958:1:525.9" → "200,958 : 1 : 525.9" (same shape as the Dashboard ratio).
2226
+ const fmtOp = (v) => {
2227
+ if (v == null) return "—";
2228
+ if (typeof v === "number") return v.toFixed(2);
2229
+ const parts = String(v).split(":").map((x) => x.trim());
2230
+ if (parts.length !== 3 || parts.some((x) => x === "" || isNaN(Number(x)))) return String(v);
2231
+ const [cr, inp, out] = parts.map(Number);
2232
+ const crS = cr >= 1000 ? Math.round(cr).toLocaleString("en-US") : String(parts[0]);
2233
+ return `${crS} : ${inp} : ${parts[2]}`;
2234
+ };
1655
2235
 
1656
2236
  // Responsive column hiding — the full table is 106 cols wide. Hide columns
1657
2237
  // at narrower terminals so rows never overflow/wrap/garble.
@@ -1661,6 +2241,10 @@ function renderSubmissions(
1661
2241
  // w < 90: hide Tokens + Class + Op Ratio, shorten Codename 20→16 → 61 cols
1662
2242
  const showTokens = w >= 120;
1663
2243
  const showClass = w >= 100;
2244
+ // Wide terminals: a log-scale Υ bar per row so the gaps read at a glance.
2245
+ const showBar = w >= 132;
2246
+ const BAR_W = 16;
2247
+ const maxYLog = Math.max(1, ...rows.map((e) => Math.log10(Math.max(gYield(e) ?? 1, 1))));
1664
2248
  const showOpRatio = w >= 90;
1665
2249
  const nameWidth = w < 90 ? 16 : 20;
1666
2250
 
@@ -1670,12 +2254,13 @@ function renderSubmissions(
1670
2254
  padEnd(hdr("Platform"), 10),
1671
2255
  padEnd(hdr("Win"), 6),
1672
2256
  padStart(hdr("Υ Yield"), 9),
1673
- ...(showOpRatio ? [padEnd(hdr("Op Ratio"), 18)] : []),
2257
+ ...(showOpRatio ? [padEnd(hdr("Op Ratio"), 20)] : []),
1674
2258
  ...(showClass ? [padEnd(hdr("Class"), 12)] : []),
1675
2259
  ...(showTokens ? [padStart(hdr("Tokens"), 9)] : []),
2260
+ ...(showBar ? [padEnd(hdr("Υ (log)"), BAR_W)] : []),
1676
2261
  ];
1677
2262
  emit(` ${SH.join(" ")}`);
1678
- emit(` ${dim("·".repeat(Math.min(w - 4, 100)))}`);
2263
+ emit(` ${dim("·".repeat(Math.min(w - 4, 102)))}`);
1679
2264
 
1680
2265
  let shown = 0;
1681
2266
  for (let idx = 0; idx < sorted.length; idx++) {
@@ -1708,9 +2293,12 @@ function renderSubmissions(
1708
2293
  padEnd(platformColor(gPlat(e))(trunc(gPlat(e), 10)), 10),
1709
2294
  padEnd(dim(trunc(gWin(e), 6)), 6),
1710
2295
  yld,
1711
- ...(showOpRatio ? [padEnd(dim(fmtOp(gOpRatio(e))), 18)] : []),
2296
+ ...(showOpRatio ? [padEnd(dim(fmtOp(gOpRatio(e))), 20)] : []),
1712
2297
  ...(showClass ? [cls] : []),
1713
2298
  ...(showTokens ? [padStart(tk != null ? fmtTok(tk) : "—", 9)] : []),
2299
+ ...(showBar
2300
+ ? [padEnd(logBar(Math.max(yv ?? 0, 1), maxYLog, BAR_W, idx === 0 ? c.boldGold : isYou ? c.boldCyan : c.green).bar, BAR_W)]
2301
+ : []),
1714
2302
  ];
1715
2303
  emit(` ${cells.join(" ")}${youMark}`);
1716
2304
  shown++;
@@ -1733,12 +2321,13 @@ function renderBoard(
1733
2321
  window = "30d",
1734
2322
  filterCodename = null,
1735
2323
  highlightCodename = null,
2324
+ uniqueOnly = false,
1736
2325
  ) {
1737
2326
  // #8 (2026-06-27): when the data came from the NEW /api/v1/submissions endpoint, render ALL
1738
2327
  // submissions ranked (raw rows). Otherwise fall through to the existing leaderboard render
1739
2328
  // (pre-deploy fallback). loadBoardData tags the payload with `_source`.
1740
2329
  if (boardData?._source === "submissions") {
1741
- renderSubmissions(boardData, window, filterCodename, highlightCodename);
2330
+ renderSubmissions(boardData, window, filterCodename, highlightCodename, uniqueOnly);
1742
2331
  return;
1743
2332
  }
1744
2333
  const entries = boardData?.entries ?? boardData?.operators ?? boardData ?? [];
@@ -2116,7 +2705,7 @@ async function renderWatch(platform = "all", win = "all-windows") {
2116
2705
  // Renders a platform × window grid of what submit_verified WOULD send, using the
2117
2706
  // already-loaded dashData. [Enter] confirms → fires submit_verified per platform;
2118
2707
  // [Esc] cancels → back to the read tab. Mirrors the Connect focused-field pattern.
2119
- function renderSubmitPreview(dashData, id) {
2708
+ function renderSubmitPreview(dashData, id, page = 0) {
2120
2709
  const budget = H() - 4;
2121
2710
  let used = 0;
2122
2711
  const emit = (s = "") => {
@@ -2127,43 +2716,83 @@ function renderSubmitPreview(dashData, id) {
2127
2716
  };
2128
2717
  const WINS = ["7d", "30d", "90d", "all"];
2129
2718
  const COL_W = 14;
2719
+ const LBL_W = 14;
2130
2720
 
2131
2721
  emit();
2132
- emit(` ${bold("Submit Preview")} ${dim("what will publish to the board")}`);
2133
- emit(
2134
- ` ${dim(`signed in as ${id?.codename ?? "—"} · device ${id?.device_id?.slice(0, 8) ?? "—"}…`)}`,
2135
- );
2136
- emit();
2137
- // header row: platform | 7d | 30d | 90d | all
2138
- const hcols = [
2139
- padEnd(hdr("Platform"), 12),
2140
- ...WINS.map((w) => padStart(hdr(w), COL_W)),
2141
- ];
2142
- emit(` ${hcols.join(" ")}`);
2722
+ emit(` ${bold("Submit Preview")} ${dim("everything that will be sent, per platform × window")}`);
2143
2723
  emit(
2144
- ` ${dim("·".repeat(Math.min(W() - 4, 12 + WINS.length * (COL_W + 2))))}`,
2724
+ ` ${dim(`signed in as ${id?.codename ?? "—"} · device ${id?.device_id?.slice(0, 8) ?? "—"}… · ed25519-signed · token counts only, never prompt content`)}`,
2145
2725
  );
2146
2726
 
2147
- const active = dashData?.active ?? [];
2148
- for (const d of active) {
2149
- if (used >= budget - 3) break;
2150
- const cells = WINS.map((w) => {
2151
- const wd = d.windows?.find((x) => x.window === w);
2152
- if (!wd || wd.pillars.input + wd.pillars.output === 0)
2153
- return padStart(dim("—"), COL_W);
2154
- const cas = cascadeFrom(wd.pillars);
2155
- if (!cas) return padStart(dim("—"), COL_W);
2156
- return padStart(gold(fmtY(cas.yield)), COL_W);
2157
- });
2158
- emit(` ${padEnd(cyan(d.platform), 12)} ${cells.join(" ")}`);
2159
- }
2727
+ // One block per platform, plus the combined "multi" submission when 2+ are active
2728
+ // (mirrors the [Enter] handler: submit_verified per platform, then 'multi').
2729
+ const active = (dashData?.active ?? []).filter((d) =>
2730
+ d.windows?.some((w) => w.pillars && w.pillars.input + w.pillars.output > 0),
2731
+ );
2160
2732
  if (active.length === 0) {
2733
+ emit();
2161
2734
  emit(` ${dim(" no platforms with data — press [R] to refresh")}`);
2735
+ return { pages: 0 };
2736
+ }
2737
+ const blocks = active.map((d) => ({ platform: d.platform, windows: d.windows }));
2738
+ if (active.length >= 2) {
2739
+ const multi = WINS.map((wk) => {
2740
+ const sum = { input: 0, output: 0, cacheCreate: 0, cacheRead: 0 };
2741
+ let messages = 0;
2742
+ for (const d of active) {
2743
+ const w = d.windows?.find((x) => x.window === wk);
2744
+ if (!w?.pillars) continue;
2745
+ for (const k of Object.keys(sum)) sum[k] += w.pillars[k] ?? 0;
2746
+ messages += w.messages || 0;
2747
+ }
2748
+ return { window: wk, pillars: sum, messages };
2749
+ });
2750
+ blocks.push({ platform: "multi", windows: multi, combined: active.map((d) => d.platform).join("+") });
2751
+ }
2752
+
2753
+ const BLOCK_ROWS = 17;
2754
+ const perPage = Math.max(1, Math.floor((budget - used - 3) / BLOCK_ROWS));
2755
+ const pages = Math.ceil(blocks.length / perPage);
2756
+ const pg = Math.min(page, pages - 1);
2757
+ const cell = (v) => padStart(v, COL_W);
2758
+ const row = (label, vals) => emit(` ${padEnd(label, LBL_W)}${vals.map(cell).join("")}`);
2759
+
2760
+ for (const b of blocks.slice(pg * perPage, pg * perPage + perPage)) {
2761
+ const ws = WINS.map((wk) => {
2762
+ const w = b.windows?.find((x) => x.window === wk);
2763
+ const p = w?.pillars;
2764
+ if (!p || p.input + p.output === 0) return null; // empty windows are skipped by submit
2765
+ const cas = cascadeFrom(p);
2766
+ const rep = _computeReportBlock(p);
2767
+ const arch = cas ? classifyArchetype({ velocity: cas.velocity, leverage: cas.leverage, pillars: p }) : null;
2768
+ return { p, cas, rep, arch, messages: w.messages };
2769
+ });
2770
+ const pc = b.platform === "multi" ? (s) => bold(s) : platformColor(b.platform);
2771
+ emit();
2772
+ emit(
2773
+ ` ${pc(b.platform)}${b.combined ? dim(` combined: ${b.combined}`) : ""}${" ".repeat(Math.max(1, LBL_W + 2 - visW(b.platform) - (b.combined ? visW(` combined: ${b.combined}`) : 0)))}${WINS.map((wk) => cell(hdr(wk))).join("")}`,
2774
+ );
2775
+ const v = (f) => ws.map((x) => (x ? f(x) : dim("— skipped")));
2776
+ emit(` ${dim("sent · token pillars")}`);
2777
+ row("input", v((x) => fmtTok(x.p.input)));
2778
+ row("output", v((x) => fmtTok(x.p.output)));
2779
+ row("cache write", v((x) => fmtTok(x.p.cacheCreate ?? 0)));
2780
+ row("cache read", v((x) => fmtTok(x.p.cacheRead ?? 0)));
2781
+ row("messages", v((x) => (x.messages != null ? fmtTok(x.messages) : dim("—"))));
2782
+ emit(` ${dim("sent · report block")}`);
2783
+ row("mode", v((x) => `${x.rep.current_mode} ${Math.round((x.rep.mode_confidence ?? 0) * 100)}%`));
2784
+ row("health", v((x) => String(x.rep.health_score)));
2785
+ row("badges", v((x) => `${x.rep.badges?.earned_this_week?.length ?? 0} earned`));
2786
+ emit(` ${dim("what the board shows from these")}`);
2787
+ row("Υ yield", v((x) => gold(fmtY(x.cas?.yield))));
2788
+ row("op ratio", v((x) => fmtOpRatio(x.p).replace(/ : /g, ":")));
2789
+ row("class", v((x) => (CLS[x.cas?.class] ?? ((s) => s))(x.cas?.class ?? "—")));
2790
+ row("archetype", v((x) => (x.arch ? archetypeColor(x.arch.key)(x.arch.name) : "—")));
2162
2791
  }
2163
2792
  emit();
2164
- emit(
2165
- ` ${dim("[Enter]")} confirm + submit all platforms ${dim("[Esc]")} cancel`,
2166
- );
2793
+ if (pages > 1) emit(` ${dim(`page ${pg + 1}/${pages} · [P] next platform`)}`);
2794
+ emit(` ${dim("values are re-read when you press [Enter], so an active session may move them slightly")}`);
2795
+ return { pages };
2167
2796
  }
2168
2797
 
2169
2798
  // ── DEBUG: render a tab once (no TTY/alt-screen) + audit each line's visible
@@ -2471,7 +3100,10 @@ export async function runTui({
2471
3100
  let connectMsg = ""; // [6] Connect last-action message (signed in / invalid code)
2472
3101
  let submitMsg = ""; // [S] in-place submit result, shown in the read-tab footer
2473
3102
  let submitPreview = false; // FIX I1: [S] opens a preview grid before sending (see→confirm→send)
3103
+ let previewPage = 0; // submit preview: which page of platform blocks ([P] cycles)
3104
+ let previewPages = 0;
2474
3105
  let boardYouOnly = false; // FIX I2: [Y] toggles the Board to "just me" (hybrid model)
3106
+ let boardUnique = false; // [U] Board: every submission (default, unchanged) ↔ best row per operator
2475
3107
  let cascadeScroll = 0; // SCROLL-VIEW (2026-06-27): cascade-section scroll offset (Dashboard only)
2476
3108
  let ompEnabled = false; // Session-only Dashboard toggle; deliberately OFF on every launch.
2477
3109
 
@@ -2480,21 +3112,38 @@ export async function runTui({
2480
3112
  startBuffer();
2481
3113
  renderTabBar(activeTab);
2482
3114
 
2483
- const hint = ` ${dim("← → or 1-6")} switch tabs ${dim("[R]")} refresh ${dim("[Q]")} quit`;
2484
- const submitHint = ` ${dim("[S]")} submit to board · ${dim("[C]")} sign in`;
2485
- // Read-tab footer: hr + hint, plus the in-place submit result line when present.
2486
- const readFooter = (hintLine) =>
2487
- submitMsg
2488
- ? [` ${hr()}`, hintLine, ` ${submitMsg}`]
2489
- : [` ${hr()}`, hintLine];
3115
+ // Footer hints as prioritized items (1 = always keep … 3 = first to drop);
3116
+ // fitHints() wraps / compacts / drops them to fit the terminal width.
3117
+ const H_NAV = [
3118
+ { k: "← → or 1-6", label: "switch tabs", pri: 2 },
3119
+ { k: "[R]", label: "refresh", pri: 2 },
3120
+ { k: "[Q]", label: "quit", pri: 1 },
3121
+ ];
3122
+ const H_SUBMIT = [
3123
+ { k: "[S]", label: "submit to board", pri: 2 },
3124
+ { k: "[C]", label: "sign in", pri: 3 },
3125
+ ];
3126
+ // Read-tab footer: hr + hints, plus the in-place submit result line when present.
3127
+ const readFooter = (items) => {
3128
+ const lines = fitHints(items);
3129
+ return submitMsg
3130
+ ? [` ${hr()}`, ...lines, ` ${submitMsg}`]
3131
+ : [` ${hr()}`, ...lines];
3132
+ };
2490
3133
 
2491
3134
  // FIX I1: submit preview is a focused overlay — renders INSTEAD of the tab
2492
3135
  // content, with its own footer. [Enter] sends, [Esc] cancels.
2493
3136
  if (submitPreview) {
2494
- renderSubmitPreview(dashData, loadIdentity());
3137
+ const pv = renderSubmitPreview(dashData, loadIdentity(), previewPage);
3138
+ previewPages = pv?.pages ?? 0;
2495
3139
  setFooter([
2496
3140
  ` ${hr()}`,
2497
- ` ${dim("[Enter]")} submit all platforms ${dim("[Esc]")} cancel ${dim("[Q]")} quit`,
3141
+ ...fitHints([
3142
+ { k: "[Enter]", label: "submit all platforms", pri: 1 },
3143
+ ...(previewPages > 1 ? [{ k: "[P]", label: "next platform", pri: 1 }] : []),
3144
+ { k: "[Esc]", label: "cancel", pri: 1 },
3145
+ { k: "[Q]", label: "quit", pri: 1 },
3146
+ ]),
2498
3147
  ]);
2499
3148
  flushScreen();
2500
3149
  return;
@@ -2509,18 +3158,16 @@ export async function runTui({
2509
3158
  }
2510
3159
  // SCROLL-VIEW: show ↑↓ hint only when there are cascade rows to scroll
2511
3160
  const scrollHint =
2512
- dashData && Array.isArray(dashData.active)
2513
- ? cascadeScrollableCount(dashData.active) >
2514
- maxCascadeRowsFor(dashData.active)
2515
- ? ` ${dim("↑↓ scroll cascade")}`
2516
- : ""
2517
- : "";
3161
+ dashData && Array.isArray(dashData.active) &&
3162
+ cascadeScrollableCount(dashData.active) > maxCascadeRowsFor(dashData.active)
3163
+ ? [{ k: "↑↓", label: "scroll cascade", pri: 2 }]
3164
+ : [];
2518
3165
  const ompHint = !dashData?._ompAvailable
2519
- ? ""
3166
+ ? []
2520
3167
  : dashData._ompLoading
2521
- ? ` ${dim(ompEnabled ? "omp: on · scanning…" : "omp: off · finishing scan…")}`
2522
- : ` ${dim("[O]")} omp: ${ompEnabled ? "on" : "off"}`;
2523
- setFooter(readFooter(`${hint}${scrollHint}${ompHint}${submitHint}`));
3168
+ ? [{ k: ompEnabled ? "omp: on · scanning…" : "omp: off · finishing scan…", pri: 3 }]
3169
+ : [{ k: "[O]", label: `omp: ${ompEnabled ? "on" : "off"}`, pri: 3 }];
3170
+ setFooter(readFooter([...H_NAV, ...scrollHint, ...ompHint, ...H_SUBMIT]));
2524
3171
  } else if (activeTab === 1) {
2525
3172
  // Trends
2526
3173
  if (!dashData) {
@@ -2528,7 +3175,7 @@ export async function runTui({
2528
3175
  } else {
2529
3176
  renderTrends(dashData, trendSub);
2530
3177
  }
2531
- setFooter(readFooter(`${hint} ${dim("[T]")} view${submitHint}`));
3178
+ setFooter(readFooter([...H_NAV, { k: "[T]", label: "view", pri: 1 }, ...H_SUBMIT]));
2532
3179
  } else if (activeTab === 2) {
2533
3180
  // Compare
2534
3181
  if (!compareData) {
@@ -2545,7 +3192,7 @@ export async function runTui({
2545
3192
  renderCompare(compareData);
2546
3193
  }
2547
3194
  setFooter(
2548
- readFooter(`${hint} ${dim("[P]")} switch platform${submitHint}`),
3195
+ readFooter([...H_NAV, { k: "[P]", label: "switch platform", pri: 1 }, ...H_SUBMIT]),
2549
3196
  );
2550
3197
  } else if (activeTab === 3) {
2551
3198
  // Board
@@ -2561,33 +3208,106 @@ export async function runTui({
2561
3208
  boardWindow,
2562
3209
  boardYouOnly ? id?.codename : null,
2563
3210
  id?.codename ?? null,
3211
+ boardUnique,
2564
3212
  );
2565
3213
  }
2566
3214
  const youHint = isSignedIn(loadIdentity())
2567
- ? ` ${dim("[Y]")} ${boardYouOnly ? "all" : "just me"}`
2568
- : "";
3215
+ ? [{ k: "[Y]", label: boardYouOnly ? "all" : "just me", pri: 2 }]
3216
+ : [];
2569
3217
  setFooter(
2570
- readFooter(`${hint} ${dim("[W]")} window${youHint}${submitHint}`),
3218
+ readFooter([
3219
+ ...H_NAV,
3220
+ { k: "[W]", label: "window", pri: 1 },
3221
+ { k: "[U]", label: boardUnique ? "all submissions" : "best per operator", pri: 2 },
3222
+ ...youHint,
3223
+ ...H_SUBMIT,
3224
+ ]),
2571
3225
  );
2572
3226
  } else if (activeTab === 4) {
2573
3227
  // Watch
2574
3228
  renderWatchInfo(watchPlatform, watchWindow, watchRefresh);
2575
- setFooter(readFooter(`${hint}${submitHint}`));
3229
+ setFooter(readFooter([...H_NAV, ...H_SUBMIT]));
2576
3230
  } else if (activeTab === 5) {
2577
3231
  // Connect
2578
3232
  const connectId = loadIdentity();
2579
3233
  renderConnect(connectId, codeBuf, connectMsg);
2580
- const signOutHint = isSignedIn(connectId)
2581
- ? ` ${dim("[X]")} sign out`
2582
- : " " + dim("[X]") + " reset device";
2583
3234
  setFooter([
2584
3235
  ` ${hr()}`,
2585
- ` ${dim("[Enter]")} sign in ${dim("[Esc]")} clear/back${signOutHint} ${dim("← →")} tabs ${dim("[Q]")} quit`,
3236
+ ...fitHints([
3237
+ { k: "[Enter]", label: "sign in", pri: 1 },
3238
+ { k: "[Esc]", label: "clear/back", pri: 2 },
3239
+ { k: "[X]", label: isSignedIn(connectId) ? "sign out" : "reset device", pri: 2 },
3240
+ { k: "← →", label: "tabs", pri: 2 },
3241
+ { k: "[Q]", label: "quit", pri: 1 },
3242
+ ]),
2586
3243
  ]);
2587
3244
  }
2588
3245
  flushScreen();
2589
3246
  };
2590
3247
 
3248
+ // ── Compare loader (PERF 2026-10-15). Cached result for the platform → paint it at once with an
3249
+ // "updated Ns ago · refreshing…" note and refresh in the background; no cache → the original
3250
+ // animated "pulling fresh sources…" spinner. A newer request (platform switch, [R]) supersedes
3251
+ // an older in-flight one: stale results are cached but never painted over the newer view.
3252
+ let compareReq = 0;
3253
+ let compareAgoTimer = null;
3254
+ let compareInflight = null; // platform of the newest in-flight pull, or null
3255
+ const openCompare = async (label = "pulling fresh sources…") => {
3256
+ const req = ++compareReq;
3257
+ const plat = platform;
3258
+ const cached = getCachedCompare(plat);
3259
+ compareInflight = plat;
3260
+ let spinTimer = null;
3261
+ if (compareAgoTimer) clearInterval(compareAgoTimer);
3262
+ compareAgoTimer = null;
3263
+ if (cached) {
3264
+ compareData = { ...cached.data, _cachedAt: cached.at, _refreshing: true };
3265
+ await redraw();
3266
+ // keep the "updated Ns ago" note honest while the refresh runs
3267
+ compareAgoTimer = setInterval(() => {
3268
+ if (activeTab === 2 && req === compareReq && compareData?._refreshing)
3269
+ redraw().catch(() => {});
3270
+ }, 1000);
3271
+ } else {
3272
+ compareData = null;
3273
+ await redraw(); // show the spinner line before the (slow) fresh pull
3274
+ // Animated spinner with elapsed timer — the fresh verifier pull takes
3275
+ // 5-20s+ (ccusage CLI scans 1,700+ JSONL files). Without animation the
3276
+ // user sees a frozen "pulling fresh sources…" line and thinks it hung.
3277
+ const SPIN_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
3278
+ const BEAR_FRAMES = ["ʕ•ᴥ•ʔ", "ʕ•ᴥ•ʔノ", "ʕ•ᴥ•ʔ", "ʕ•ᴥ•ʔノ"];
3279
+ const pullStart = Date.now();
3280
+ let spinIdx = 0;
3281
+ spinTimer = setInterval(() => {
3282
+ if (activeTab !== 2 || req !== compareReq) return; // switched away — don't repaint
3283
+ const elapsed = ((Date.now() - pullStart) / 1000).toFixed(1);
3284
+ const spin = SPIN_FRAMES[spinIdx % SPIN_FRAMES.length];
3285
+ const bear = BEAR_FRAMES[spinIdx % BEAR_FRAMES.length];
3286
+ spinIdx++;
3287
+ // Repaint just the spinner line in-place (line 4 of the frame)
3288
+ process.stdout.write(
3289
+ GOTO(4, 1) + ERLINE +
3290
+ ` ${gold(spin)} ${dim(label)} ${bold(`${elapsed}s`)} ${dim(`·`)} ${gold(bear)}`,
3291
+ );
3292
+ }, 200);
3293
+ }
3294
+ const result = await loadCompareData(plat).catch(() => null);
3295
+ if (spinTimer) clearInterval(spinTimer);
3296
+ if (_compareHasData(result)) putCachedCompare(plat, result);
3297
+ if (req !== compareReq) return; // superseded by a newer Compare request
3298
+ compareInflight = null;
3299
+ if (compareAgoTimer) clearInterval(compareAgoTimer);
3300
+ compareAgoTimer = null;
3301
+ if (_compareHasData(result) || !cached) {
3302
+ compareData = result;
3303
+ status = `last refreshed ${new Date().toLocaleTimeString("en-US", { hour12: false })}`;
3304
+ } else {
3305
+ // Refresh came back empty/failed — keep showing the cached result, flagged as such.
3306
+ compareData = { ...cached.data, _cachedAt: cached.at, _refreshFailed: true };
3307
+ }
3308
+ if (activeTab === 2) await redraw();
3309
+ };
3310
+
2591
3311
  // ── Initial data load
2592
3312
  const loadAll = async () => {
2593
3313
  // A2 (2026-06-27): paint the frame + a clear "reading your cascade…" status IMMEDIATELY,
@@ -2615,7 +3335,16 @@ export async function runTui({
2615
3335
  // FIX 0: progressive Dashboard load — claude first (fast), render it, THEN fill
2616
3336
  // the remaining ALL_PLATFORMS entries in the background + redraw. The user sees their cascade
2617
3337
  // within ~1 read instead of a 7s blank "loading dashboard…".
2618
- dashData = await loadDashboardData({ includeOmp: ompEnabled }).catch((e) => {
3338
+ const placeholder = dashData;
3339
+ dashData = await loadDashboardData({
3340
+ includeOmp: ompEnabled,
3341
+ onPrimary: async (primary) => {
3342
+ // Still the placeholder from this loadAll? Paint claude now (Trends + cascade).
3343
+ if (dashData !== placeholder) return;
3344
+ placeholder.active = primary;
3345
+ await redraw();
3346
+ },
3347
+ }).catch((e) => {
2619
3348
  status = `dashboard error: ${e.message}`;
2620
3349
  return null;
2621
3350
  });
@@ -2904,7 +3633,7 @@ export async function runTui({
2904
3633
  didMulti = false;
2905
3634
  for (const p of platforms) {
2906
3635
  try {
2907
- const out = await callTool("submit_verified", { platform: p });
3636
+ const out = await callTool("submit_verified", { platform: p, confirm: true });
2908
3637
  const ws = out.windows || [];
2909
3638
  const received = ws.filter((w) => w.status === "received");
2910
3639
  const verified = received.filter(
@@ -2924,6 +3653,7 @@ export async function runTui({
2924
3653
  try {
2925
3654
  const out = await callTool("submit_verified", {
2926
3655
  platform: "multi",
3656
+ confirm: true,
2927
3657
  });
2928
3658
  const ws = out.windows || [];
2929
3659
  const received = ws.filter((w) => w.status === "received");
@@ -2957,6 +3687,11 @@ export async function runTui({
2957
3687
  await redraw();
2958
3688
  return;
2959
3689
  }
3690
+ if ((key === "p" || key === "P") && previewPages > 1) {
3691
+ previewPage = (previewPage + 1) % previewPages;
3692
+ await redraw();
3693
+ return;
3694
+ }
2960
3695
  return; // swallow all other keys while the preview is open
2961
3696
  }
2962
3697
 
@@ -3108,35 +3843,15 @@ export async function runTui({
3108
3843
  // ASYNC FIX (2026-06-27): with execFile (async), the event loop keeps running during the
3109
3844
  // pull — key presses are NOT blocked. Guard: only render the result if the user is still
3110
3845
  // on the Compare tab (they may have switched away during the pull).
3111
- if (switched && activeTab === 2 && !compareData) {
3846
+ if (switched && activeTab === 2 && (!compareData || compareData.platform !== platform)) {
3112
3847
  submitMsg = "";
3113
- await redraw(); // show the spinner line before the (slow) fresh pull
3114
- // Animated spinner with elapsed timer — the fresh verifier pull takes
3115
- // 5-20s (ccusage CLI scans 1,700+ JSONL files). Without animation the
3116
- // user sees a frozen "pulling fresh sources…" line and thinks it hung.
3117
- const SPIN_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
3118
- const BEAR_FRAMES = ["ʕ•ᴥ•ʔ", "ʕ•ᴥ•ʔノ", "ʕ•ᴥ•ʔ", "ʕ•ᴥ•ʔノ"];
3119
- const pullStart = Date.now();
3120
- let spinIdx = 0;
3121
- const spinTimer = setInterval(() => {
3122
- if (activeTab !== 2) return; // user switched away — don't repaint
3123
- const elapsed = ((Date.now() - pullStart) / 1000).toFixed(1);
3124
- const spin = SPIN_FRAMES[spinIdx % SPIN_FRAMES.length];
3125
- const bear = BEAR_FRAMES[spinIdx % BEAR_FRAMES.length];
3126
- spinIdx++;
3127
- // Repaint just the spinner line in-place (line 4 of the frame)
3128
- process.stdout.write(
3129
- GOTO(4, 1) + ERLINE +
3130
- ` ${gold(spin)} ${dim(`pulling fresh sources…`)} ${bold(`${elapsed}s`)} ${dim(`·`)} ${gold(bear)}`,
3131
- );
3132
- }, 200);
3133
- const result = await loadCompareData(platform).catch(() => null);
3134
- clearInterval(spinTimer);
3135
- if (activeTab === 2) {
3136
- // still on Compare? render the result
3137
- compareData = result;
3848
+ // Left and came back mid-pull? Re-show its view instead of starting a second scan.
3849
+ if (compareInflight === platform) {
3138
3850
  await redraw();
3851
+ return;
3139
3852
  }
3853
+ // Not awaited: the background refresh must not block key handling.
3854
+ openCompare().catch(() => {});
3140
3855
  return;
3141
3856
  }
3142
3857
 
@@ -3184,34 +3899,10 @@ export async function runTui({
3184
3899
  // Compare: real platforms only (no 'all' — Compare is per-platform by design).
3185
3900
  const idx = CYCLE_PLATFORMS.indexOf(platform);
3186
3901
  platform = CYCLE_PLATFORMS[(idx + 1) % CYCLE_PLATFORMS.length];
3187
- // #9: clearing compareData makes redraw show the "pulling fresh sources…" spinner
3188
- // for the new platform while the fresh on-demand verifier pull runs.
3189
- compareData = null;
3190
3902
  status = `loading ${platform}…`;
3191
- await redraw();
3192
- // Animated spinner (same as the initial Compare load — see above)
3193
- const SPIN_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
3194
- const BEAR_FRAMES = ["ʕ•ᴥ•ʔ", "ʕ•ᴥ•ʔノ", "ʕ•ᴥ•ʔ", "ʕ•ᴥ•ʔノ"];
3195
- const pullStart = Date.now();
3196
- let spinIdx = 0;
3197
- const spinTimer = setInterval(() => {
3198
- if (activeTab !== 2) return;
3199
- const elapsed = ((Date.now() - pullStart) / 1000).toFixed(1);
3200
- const spin = SPIN_FRAMES[spinIdx % SPIN_FRAMES.length];
3201
- const bear = BEAR_FRAMES[spinIdx % BEAR_FRAMES.length];
3202
- spinIdx++;
3203
- process.stdout.write(
3204
- GOTO(4, 1) + ERLINE +
3205
- ` ${gold(spin)} ${dim(`pulling ${platform}…`)} ${bold(`${elapsed}s`)} ${dim(`·`)} ${gold(bear)}`,
3206
- );
3207
- }, 200);
3208
- // ASYNC FIX: guard — only render if still on Compare (user may have switched away)
3209
- const result = await loadCompareData(platform).catch(() => null);
3210
- clearInterval(spinTimer);
3211
- if (activeTab === 2) {
3212
- compareData = result;
3213
- status = `last refreshed ${new Date().toLocaleTimeString("en-US", { hour12: false })}`;
3214
- }
3903
+ // Cached → instant paint + background refresh; else the spinner for the new platform.
3904
+ openCompare(`pulling ${platform}…`).catch(() => {});
3905
+ return;
3215
3906
  } else {
3216
3907
  // A5: Watch [P] cycles 'all' → claude → codex → … → 'all' (optional focus; default 'all').
3217
3908
  const WATCH_PLAT_CYCLE = ["all", ...CYCLE_PLATFORMS];
@@ -3275,15 +3966,8 @@ export async function runTui({
3275
3966
  // #9: on the Compare tab, [R] re-runs the FRESH verifier pull for the current platform
3276
3967
  // (loadAll no longer touches Compare). Clearing compareData shows the spinner first.
3277
3968
  if (activeTab === 2) {
3278
- compareData = null;
3279
- await redraw();
3280
- // ASYNC FIX: guard — only render if still on Compare
3281
- const result = await loadCompareData(platform).catch(() => null);
3282
- if (activeTab === 2) {
3283
- compareData = result;
3284
- status = `last refreshed ${new Date().toLocaleTimeString("en-US", { hour12: false })}`;
3285
- await redraw();
3286
- }
3969
+ // [R] forces a fresh pull. Any cached result stays on screen, marked "refreshing…".
3970
+ openCompare().catch(() => {});
3287
3971
  return;
3288
3972
  }
3289
3973
  await redraw();
@@ -3305,6 +3989,7 @@ export async function runTui({
3305
3989
  return;
3306
3990
  }
3307
3991
  submitPreview = true;
3992
+ previewPage = 0;
3308
3993
  submitMsg = "";
3309
3994
  await redraw();
3310
3995
  return;
@@ -3318,6 +4003,12 @@ export async function runTui({
3318
4003
  return;
3319
4004
  }
3320
4005
 
4006
+ if (k === "u" && activeTab === 3) {
4007
+ boardUnique = !boardUnique;
4008
+ await redraw();
4009
+ return;
4010
+ }
4011
+
3321
4012
  if (k === "w" && activeTab === 3) {
3322
4013
  // Board tab (now index 3)
3323
4014
  const windows = ["7d", "30d", "90d", "all"];