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