sigrank 1.0.46 → 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.
- package/CHANGELOG.md +14 -0
- package/manifest.json +1 -1
- package/package.json +1 -1
- package/presentation/tui-themes.mjs +43 -28
- package/presentation/tui.mjs +958 -268
package/presentation/tui.mjs
CHANGED
|
@@ -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 (
|
|
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
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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 =
|
|
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 =
|
|
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
|
|
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 >=
|
|
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
|
|
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 =
|
|
542
|
-
// power = Power users (median of top 100
|
|
543
|
-
// top = Top Evals (single
|
|
544
|
-
//
|
|
545
|
-
//
|
|
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,
|
|
548
|
-
hcm: { yield: 1.
|
|
549
|
-
power: { yield:
|
|
550
|
-
top: { yield:
|
|
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
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
const
|
|
575
|
-
|
|
576
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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)
|
|
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
|
|
903
|
-
|
|
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, "
|
|
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
|
|
909
|
-
if (used >= budget -
|
|
910
|
-
const
|
|
911
|
-
|
|
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 (
|
|
917
|
-
const
|
|
918
|
-
|
|
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
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
const
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
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
|
|
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 ·
|
|
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("
|
|
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("
|
|
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
|
-
|
|
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 <
|
|
1334
|
-
const m =
|
|
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(
|
|
1878
|
+
` ${padEnd(cyan(m.label), 10)} ${metricSpark(d, m.pick, m.fmt)}`,
|
|
1351
1879
|
);
|
|
1352
|
-
|
|
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
|
-
|
|
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
|
-
` ${
|
|
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
|
-
//
|
|
1379
|
-
//
|
|
1380
|
-
//
|
|
1381
|
-
const budget = Math.max(H() -
|
|
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 (
|
|
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
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
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
|
|
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
|
-
|
|
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} ·
|
|
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
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
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"),
|
|
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,
|
|
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))),
|
|
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("
|
|
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("·
|
|
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
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
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
|
-
?
|
|
2522
|
-
:
|
|
2523
|
-
setFooter(readFooter(
|
|
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(
|
|
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(
|
|
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
|
-
?
|
|
2568
|
-
:
|
|
3215
|
+
? [{ k: "[Y]", label: boardYouOnly ? "all" : "just me", pri: 2 }]
|
|
3216
|
+
: [];
|
|
2569
3217
|
setFooter(
|
|
2570
|
-
readFooter(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
});
|
|
@@ -2958,6 +3687,11 @@ export async function runTui({
|
|
|
2958
3687
|
await redraw();
|
|
2959
3688
|
return;
|
|
2960
3689
|
}
|
|
3690
|
+
if ((key === "p" || key === "P") && previewPages > 1) {
|
|
3691
|
+
previewPage = (previewPage + 1) % previewPages;
|
|
3692
|
+
await redraw();
|
|
3693
|
+
return;
|
|
3694
|
+
}
|
|
2961
3695
|
return; // swallow all other keys while the preview is open
|
|
2962
3696
|
}
|
|
2963
3697
|
|
|
@@ -3109,35 +3843,15 @@ export async function runTui({
|
|
|
3109
3843
|
// ASYNC FIX (2026-06-27): with execFile (async), the event loop keeps running during the
|
|
3110
3844
|
// pull — key presses are NOT blocked. Guard: only render the result if the user is still
|
|
3111
3845
|
// on the Compare tab (they may have switched away during the pull).
|
|
3112
|
-
if (switched && activeTab === 2 && !compareData) {
|
|
3846
|
+
if (switched && activeTab === 2 && (!compareData || compareData.platform !== platform)) {
|
|
3113
3847
|
submitMsg = "";
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
// 5-20s (ccusage CLI scans 1,700+ JSONL files). Without animation the
|
|
3117
|
-
// user sees a frozen "pulling fresh sources…" line and thinks it hung.
|
|
3118
|
-
const SPIN_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
3119
|
-
const BEAR_FRAMES = ["ʕ•ᴥ•ʔ", "ʕ•ᴥ•ʔノ", "ʕ•ᴥ•ʔ", "ʕ•ᴥ•ʔノ"];
|
|
3120
|
-
const pullStart = Date.now();
|
|
3121
|
-
let spinIdx = 0;
|
|
3122
|
-
const spinTimer = setInterval(() => {
|
|
3123
|
-
if (activeTab !== 2) return; // user switched away — don't repaint
|
|
3124
|
-
const elapsed = ((Date.now() - pullStart) / 1000).toFixed(1);
|
|
3125
|
-
const spin = SPIN_FRAMES[spinIdx % SPIN_FRAMES.length];
|
|
3126
|
-
const bear = BEAR_FRAMES[spinIdx % BEAR_FRAMES.length];
|
|
3127
|
-
spinIdx++;
|
|
3128
|
-
// Repaint just the spinner line in-place (line 4 of the frame)
|
|
3129
|
-
process.stdout.write(
|
|
3130
|
-
GOTO(4, 1) + ERLINE +
|
|
3131
|
-
` ${gold(spin)} ${dim(`pulling fresh sources…`)} ${bold(`${elapsed}s`)} ${dim(`·`)} ${gold(bear)}`,
|
|
3132
|
-
);
|
|
3133
|
-
}, 200);
|
|
3134
|
-
const result = await loadCompareData(platform).catch(() => null);
|
|
3135
|
-
clearInterval(spinTimer);
|
|
3136
|
-
if (activeTab === 2) {
|
|
3137
|
-
// still on Compare? render the result
|
|
3138
|
-
compareData = result;
|
|
3848
|
+
// Left and came back mid-pull? Re-show its view instead of starting a second scan.
|
|
3849
|
+
if (compareInflight === platform) {
|
|
3139
3850
|
await redraw();
|
|
3851
|
+
return;
|
|
3140
3852
|
}
|
|
3853
|
+
// Not awaited: the background refresh must not block key handling.
|
|
3854
|
+
openCompare().catch(() => {});
|
|
3141
3855
|
return;
|
|
3142
3856
|
}
|
|
3143
3857
|
|
|
@@ -3185,34 +3899,10 @@ export async function runTui({
|
|
|
3185
3899
|
// Compare: real platforms only (no 'all' — Compare is per-platform by design).
|
|
3186
3900
|
const idx = CYCLE_PLATFORMS.indexOf(platform);
|
|
3187
3901
|
platform = CYCLE_PLATFORMS[(idx + 1) % CYCLE_PLATFORMS.length];
|
|
3188
|
-
// #9: clearing compareData makes redraw show the "pulling fresh sources…" spinner
|
|
3189
|
-
// for the new platform while the fresh on-demand verifier pull runs.
|
|
3190
|
-
compareData = null;
|
|
3191
3902
|
status = `loading ${platform}…`;
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
const BEAR_FRAMES = ["ʕ•ᴥ•ʔ", "ʕ•ᴥ•ʔノ", "ʕ•ᴥ•ʔ", "ʕ•ᴥ•ʔノ"];
|
|
3196
|
-
const pullStart = Date.now();
|
|
3197
|
-
let spinIdx = 0;
|
|
3198
|
-
const spinTimer = setInterval(() => {
|
|
3199
|
-
if (activeTab !== 2) return;
|
|
3200
|
-
const elapsed = ((Date.now() - pullStart) / 1000).toFixed(1);
|
|
3201
|
-
const spin = SPIN_FRAMES[spinIdx % SPIN_FRAMES.length];
|
|
3202
|
-
const bear = BEAR_FRAMES[spinIdx % BEAR_FRAMES.length];
|
|
3203
|
-
spinIdx++;
|
|
3204
|
-
process.stdout.write(
|
|
3205
|
-
GOTO(4, 1) + ERLINE +
|
|
3206
|
-
` ${gold(spin)} ${dim(`pulling ${platform}…`)} ${bold(`${elapsed}s`)} ${dim(`·`)} ${gold(bear)}`,
|
|
3207
|
-
);
|
|
3208
|
-
}, 200);
|
|
3209
|
-
// ASYNC FIX: guard — only render if still on Compare (user may have switched away)
|
|
3210
|
-
const result = await loadCompareData(platform).catch(() => null);
|
|
3211
|
-
clearInterval(spinTimer);
|
|
3212
|
-
if (activeTab === 2) {
|
|
3213
|
-
compareData = result;
|
|
3214
|
-
status = `last refreshed ${new Date().toLocaleTimeString("en-US", { hour12: false })}`;
|
|
3215
|
-
}
|
|
3903
|
+
// Cached → instant paint + background refresh; else the spinner for the new platform.
|
|
3904
|
+
openCompare(`pulling ${platform}…`).catch(() => {});
|
|
3905
|
+
return;
|
|
3216
3906
|
} else {
|
|
3217
3907
|
// A5: Watch [P] cycles 'all' → claude → codex → … → 'all' (optional focus; default 'all').
|
|
3218
3908
|
const WATCH_PLAT_CYCLE = ["all", ...CYCLE_PLATFORMS];
|
|
@@ -3276,15 +3966,8 @@ export async function runTui({
|
|
|
3276
3966
|
// #9: on the Compare tab, [R] re-runs the FRESH verifier pull for the current platform
|
|
3277
3967
|
// (loadAll no longer touches Compare). Clearing compareData shows the spinner first.
|
|
3278
3968
|
if (activeTab === 2) {
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
// ASYNC FIX: guard — only render if still on Compare
|
|
3282
|
-
const result = await loadCompareData(platform).catch(() => null);
|
|
3283
|
-
if (activeTab === 2) {
|
|
3284
|
-
compareData = result;
|
|
3285
|
-
status = `last refreshed ${new Date().toLocaleTimeString("en-US", { hour12: false })}`;
|
|
3286
|
-
await redraw();
|
|
3287
|
-
}
|
|
3969
|
+
// [R] forces a fresh pull. Any cached result stays on screen, marked "refreshing…".
|
|
3970
|
+
openCompare().catch(() => {});
|
|
3288
3971
|
return;
|
|
3289
3972
|
}
|
|
3290
3973
|
await redraw();
|
|
@@ -3306,6 +3989,7 @@ export async function runTui({
|
|
|
3306
3989
|
return;
|
|
3307
3990
|
}
|
|
3308
3991
|
submitPreview = true;
|
|
3992
|
+
previewPage = 0;
|
|
3309
3993
|
submitMsg = "";
|
|
3310
3994
|
await redraw();
|
|
3311
3995
|
return;
|
|
@@ -3319,6 +4003,12 @@ export async function runTui({
|
|
|
3319
4003
|
return;
|
|
3320
4004
|
}
|
|
3321
4005
|
|
|
4006
|
+
if (k === "u" && activeTab === 3) {
|
|
4007
|
+
boardUnique = !boardUnique;
|
|
4008
|
+
await redraw();
|
|
4009
|
+
return;
|
|
4010
|
+
}
|
|
4011
|
+
|
|
3322
4012
|
if (k === "w" && activeTab === 3) {
|
|
3323
4013
|
// Board tab (now index 3)
|
|
3324
4014
|
const windows = ["7d", "30d", "90d", "all"];
|