rlm-cli 0.2.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/dist/viewer.js ADDED
@@ -0,0 +1,828 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * RLM Trajectory Viewer — interactive TUI for browsing saved trajectory JSON files.
4
+ *
5
+ * Navigate through iterations with arrow keys, view code, REPL output,
6
+ * sub-queries, and the final answer in a beautifully formatted display.
7
+ *
8
+ * Usage:
9
+ * rlm viewer # pick from list
10
+ * rlm viewer trajectories/file.json # open specific file
11
+ */
12
+ import * as fs from "node:fs";
13
+ import * as path from "node:path";
14
+ // ── ANSI helpers ────────────────────────────────────────────────────────────
15
+ const c = {
16
+ reset: "\x1b[0m",
17
+ bold: "\x1b[1m",
18
+ dim: "\x1b[2m",
19
+ italic: "\x1b[3m",
20
+ underline: "\x1b[4m",
21
+ inverse: "\x1b[7m",
22
+ red: "\x1b[31m",
23
+ green: "\x1b[32m",
24
+ yellow: "\x1b[33m",
25
+ blue: "\x1b[34m",
26
+ magenta: "\x1b[35m",
27
+ cyan: "\x1b[36m",
28
+ white: "\x1b[37m",
29
+ gray: "\x1b[90m",
30
+ bgBlue: "\x1b[44m",
31
+ bgCyan: "\x1b[46m",
32
+ bgGray: "\x1b[100m",
33
+ clearScreen: "\x1b[2J",
34
+ cursorHome: "\x1b[H",
35
+ hideCursor: "\x1b[?25l",
36
+ showCursor: "\x1b[?25h",
37
+ altScreenOn: "\x1b[?1049h",
38
+ altScreenOff: "\x1b[?1049l",
39
+ };
40
+ function W(...args) {
41
+ process.stdout.write(args.join(""));
42
+ }
43
+ // ── Layout helpers ──────────────────────────────────────────────────────────
44
+ function getWidth() {
45
+ return Math.min(process.stdout.columns || 80, 120);
46
+ }
47
+ function getHeight() {
48
+ return process.stdout.rows || 24;
49
+ }
50
+ function hline(ch = "━", color = c.cyan) {
51
+ return `${color}${ch.repeat(getWidth())}${c.reset}`;
52
+ }
53
+ function centeredHeader(text, color = c.cyan) {
54
+ const w = getWidth();
55
+ const stripped = text.replace(/\x1b\[[0-9;]*m/g, "");
56
+ const pad = Math.max(0, w - stripped.length - 4);
57
+ const left = Math.floor(pad / 2);
58
+ const right = pad - left;
59
+ return `${color}${"━".repeat(left)} ${text}${color} ${"━".repeat(right)}${c.reset}`;
60
+ }
61
+ function boxed(title, content, color) {
62
+ const w = getWidth() - 4;
63
+ const display = content;
64
+ W(` ${color}${c.bold}${title}${c.reset}\n`);
65
+ W(` ${color}┌${"─".repeat(w)}┐${c.reset}\n`);
66
+ for (const line of display.split("\n")) {
67
+ const stripped = line.replace(/\x1b\[[0-9;]*m/g, "");
68
+ const padding = Math.max(0, w - stripped.length - 1);
69
+ W(` ${color}│${c.reset} ${line}${" ".repeat(padding)}${color}│${c.reset}\n`);
70
+ }
71
+ W(` ${color}└${"─".repeat(w)}┘${c.reset}\n`);
72
+ }
73
+ function kvLine(key, value) {
74
+ W(` ${c.gray}${key}:${c.reset} ${value}\n`);
75
+ }
76
+ function formatSize(chars) {
77
+ if (chars >= 1_000_000)
78
+ return `${(chars / 1_000_000).toFixed(1)}M`;
79
+ if (chars >= 1000)
80
+ return `${(chars / 1000).toFixed(1)}K`;
81
+ return `${chars}`;
82
+ }
83
+ function listTrajectories() {
84
+ const dir = path.resolve(process.cwd(), "trajectories");
85
+ if (!fs.existsSync(dir))
86
+ return [];
87
+ return fs
88
+ .readdirSync(dir)
89
+ .filter((f) => f.endsWith(".json"))
90
+ .map((f) => {
91
+ const full = path.join(dir, f);
92
+ const stat = fs.statSync(full);
93
+ let traj;
94
+ try {
95
+ traj = JSON.parse(fs.readFileSync(full, "utf-8"));
96
+ }
97
+ catch { /* skip */ }
98
+ return { name: f, path: full, size: stat.size, mtime: stat.mtime, traj };
99
+ })
100
+ .sort((a, b) => b.mtime.getTime() - a.mtime.getTime()); // newest first
101
+ }
102
+ async function pickFile(files) {
103
+ return new Promise((resolve) => {
104
+ let selected = 0;
105
+ const maxVisible = Math.min(files.length, getHeight() - 10);
106
+ function render() {
107
+ W(c.cursorHome, c.clearScreen, c.hideCursor);
108
+ W(`\n${hline()}\n`);
109
+ W(`${centeredHeader(`${c.bold}${c.white}RLM Trajectory Viewer${c.reset}`)}\n`);
110
+ W(`${hline()}\n\n`);
111
+ W(` ${c.bold}Select a trajectory:${c.reset} ${c.dim}(up/down navigate, enter select, q quit)${c.reset}\n\n`);
112
+ const scrollStart = Math.max(0, selected - Math.floor(maxVisible / 2));
113
+ const scrollEnd = Math.min(files.length, scrollStart + maxVisible);
114
+ for (let i = scrollStart; i < scrollEnd; i++) {
115
+ const f = files[i];
116
+ const isSel = i === selected;
117
+ const sizeKB = (f.size / 1024).toFixed(1);
118
+ // Extract info from trajectory data
119
+ const steps = f.traj?.iterations?.length ?? 0;
120
+ const completed = f.traj?.result?.completed;
121
+ const status = completed === true ? `${c.green}done${c.reset}` : completed === false ? `${c.yellow}partial${c.reset}` : "";
122
+ const queryPreview = f.traj?.query
123
+ ? (f.traj.query.length > 40 ? f.traj.query.slice(0, 37) + "..." : f.traj.query)
124
+ : "";
125
+ // Date from filename
126
+ const dateMatch = f.name.match(/(\d{4}-\d{2}-\d{2})T(\d{2})-(\d{2})/);
127
+ const dateStr = dateMatch ? `${dateMatch[1]} ${dateMatch[2]}:${dateMatch[3]}` : f.name;
128
+ const prefix = isSel ? `${c.cyan}${c.bold} > ` : ` `;
129
+ const nameColor = isSel ? `${c.cyan}${c.bold}` : c.white;
130
+ W(`${prefix}${nameColor}${dateStr}${c.reset}`);
131
+ W(` ${c.dim}${sizeKB}KB${c.reset}`);
132
+ W(` ${c.dim}${steps} step${steps !== 1 ? "s" : ""}${c.reset}`);
133
+ if (status)
134
+ W(` ${status}`);
135
+ if (queryPreview)
136
+ W(` ${c.dim}${queryPreview}${c.reset}`);
137
+ W(`\n`);
138
+ }
139
+ if (files.length > maxVisible) {
140
+ W(`\n ${c.dim}${scrollStart > 0 ? "^ more above" : ""} ${scrollEnd < files.length ? "v more below" : ""}${c.reset}\n`);
141
+ }
142
+ W(`\n`);
143
+ }
144
+ render();
145
+ process.stdin.setRawMode(true);
146
+ process.stdin.resume();
147
+ process.stdin.setEncoding("utf8");
148
+ function onKey(key) {
149
+ if (key === "\x1b[A") {
150
+ selected = Math.max(0, selected - 1);
151
+ render();
152
+ }
153
+ else if (key === "\x1b[B") {
154
+ selected = Math.min(files.length - 1, selected + 1);
155
+ render();
156
+ }
157
+ else if (key === "\r" || key === "\n") {
158
+ process.stdin.removeListener("data", onKey);
159
+ process.stdin.setRawMode(false);
160
+ process.stdin.pause();
161
+ resolve(files[selected].path);
162
+ }
163
+ else if (key === "q" || key === "\x03") {
164
+ W(c.showCursor, c.altScreenOff);
165
+ process.exit(0);
166
+ }
167
+ }
168
+ process.stdin.on("data", onKey);
169
+ });
170
+ }
171
+ function buildIterLine(step, isSelected) {
172
+ const isFinal = step.hasFinal;
173
+ const elapsed = (step.elapsedMs / 1000).toFixed(1);
174
+ const sqCount = step.subQueries.length;
175
+ const bullet = isFinal ? `${c.green}${c.bold}*${c.reset}` : `${c.blue}o${c.reset}`;
176
+ const sel = isSelected ? `${c.inverse}${c.cyan}` : "";
177
+ const codeLen = step.code ? step.code.split("\n").length : 0;
178
+ const outLen = step.stdout ? step.stdout.split("\n").length : 0;
179
+ const sqInfo = sqCount > 0 ? ` | ${c.magenta}${sqCount} sub-quer${sqCount !== 1 ? "ies" : "y"}${c.reset}` : "";
180
+ const errInfo = step.stderr ? ` | ${c.red}stderr${c.reset}` : "";
181
+ let line = ` ${sel} ${bullet} ${c.bold}Iteration ${step.iteration}${c.reset}`;
182
+ line += `${sel ? c.reset : ""} ${c.dim}${elapsed}s${c.reset}`;
183
+ line += ` | ${c.green}${codeLen}L code${c.reset} | ${c.yellow}${outLen}L output${c.reset}${sqInfo}${errInfo}`;
184
+ if (isFinal)
185
+ line += ` | ${c.green}${c.bold}FINAL${c.reset}`;
186
+ return line;
187
+ }
188
+ function renderOverview(state) {
189
+ const { traj } = state;
190
+ const w = getWidth();
191
+ const h = getHeight();
192
+ // Build all lines into a buffer
193
+ const buf = [];
194
+ // Header
195
+ buf.push(``);
196
+ buf.push(hline());
197
+ buf.push(centeredHeader(`${c.bold}${c.white}RLM Trajectory Viewer${c.reset}`));
198
+ buf.push(hline());
199
+ buf.push(``);
200
+ buf.push(` ${c.gray}Model :${c.reset} ${c.bold}${traj.model}${c.reset}`);
201
+ buf.push(` ${c.gray}Query :${c.reset} ${c.yellow}${traj.query}${c.reset}`);
202
+ buf.push(` ${c.gray}Context :${c.reset} ${traj.contextLength.toLocaleString()} chars | ${traj.contextLines.toLocaleString()} lines`);
203
+ buf.push(` ${c.gray}Duration:${c.reset} ${(traj.totalElapsedMs / 1000).toFixed(1)}s ${c.gray}|${c.reset} ${traj.result?.completed ? `${c.green}Completed${c.reset}` : `${c.red}Incomplete${c.reset}`}`);
204
+ buf.push(``);
205
+ buf.push(` ${c.bold}Iterations${c.reset} ${c.dim}(${traj.iterations.length} total)${c.reset}`);
206
+ buf.push(``);
207
+ const headerSize = buf.length;
208
+ const footerSize = 2;
209
+ const answerSize = traj.result ? 4 : 0;
210
+ const iterBudget = h - headerSize - footerSize - answerSize;
211
+ // Build iteration lines (each iteration = summary + separator)
212
+ const flatLines = [];
213
+ const iterStartOffsets = [];
214
+ for (let i = 0; i < traj.iterations.length; i++) {
215
+ const step = traj.iterations[i];
216
+ const isSel = i === state.iterIdx;
217
+ iterStartOffsets.push(flatLines.length);
218
+ flatLines.push(buildIterLine(step, isSel));
219
+ if (i < traj.iterations.length - 1) {
220
+ flatLines.push(` ${c.dim} |${c.reset}`);
221
+ }
222
+ }
223
+ // Scroll so selected iteration is visible
224
+ const selStart = iterStartOffsets[state.iterIdx] ?? 0;
225
+ let scrollY = Math.max(0, selStart - 2);
226
+ // If everything fits, no scroll needed
227
+ if (flatLines.length <= iterBudget) {
228
+ scrollY = 0;
229
+ }
230
+ const showFrom = scrollY;
231
+ const showTo = Math.min(flatLines.length, scrollY + iterBudget);
232
+ if (showFrom > 0) {
233
+ buf.push(` ${c.dim} ^ more above${c.reset}`);
234
+ }
235
+ for (let i = showFrom; i < showTo; i++) {
236
+ buf.push(flatLines[i]);
237
+ }
238
+ if (showTo < flatLines.length) {
239
+ buf.push(` ${c.dim} | more below${c.reset}`);
240
+ }
241
+ // Answer preview
242
+ if (traj.result) {
243
+ buf.push(`${c.green}${"─".repeat(w)}${c.reset}`);
244
+ buf.push(` ${c.green}${c.bold}Answer Preview:${c.reset}`);
245
+ const preview = traj.result.answer.split("\n")[0] || "";
246
+ buf.push(` ${c.white}${preview}${c.reset}`);
247
+ if (traj.result.answer.split("\n").length > 1) {
248
+ buf.push(` ${c.dim}... (press 'r' to see full result)${c.reset}`);
249
+ }
250
+ }
251
+ // Render
252
+ W(c.cursorHome, c.clearScreen, c.hideCursor);
253
+ for (const l of buf)
254
+ W(l + "\n");
255
+ // Footer
256
+ W(hline("─", c.gray) + "\n");
257
+ W(` ${c.dim}up/down${c.reset} select ${c.dim}enter${c.reset} view ${c.dim}r${c.reset} result ${c.dim}q${c.reset} quit\n`);
258
+ }
259
+ function buildIterationContent(step, traj) {
260
+ const lines = [];
261
+ const w = getWidth() - 4;
262
+ // Title
263
+ lines.push(``);
264
+ lines.push(hline());
265
+ const finalTag = step.hasFinal ? ` ${c.green}${c.bold}FINAL${c.reset}` : "";
266
+ lines.push(centeredHeader(`${c.bold}${c.white}Iteration ${step.iteration} / ${traj.iterations.length}${c.reset}${finalTag}`));
267
+ lines.push(hline());
268
+ lines.push(``);
269
+ // Metadata
270
+ const elapsed = (step.elapsedMs / 1000).toFixed(1);
271
+ lines.push(` ${c.gray}Elapsed :${c.reset} ${elapsed}s`);
272
+ lines.push(` ${c.gray}Sub-queries:${c.reset} ${step.subQueries.length}`);
273
+ lines.push(` ${c.gray}Has Final :${c.reset} ${step.hasFinal ? `${c.green}yes${c.reset}` : `${c.gray}no${c.reset}`}`);
274
+ lines.push(``);
275
+ // Code
276
+ if (step.code) {
277
+ lines.push(` ${c.green}${c.bold}Generated Code${c.reset}`);
278
+ lines.push(` ${c.green}┌${"─".repeat(w)}┐${c.reset}`);
279
+ for (const cl of syntaxHighlight(step.code).split("\n")) {
280
+ const stripped = cl.replace(/\x1b\[[0-9;]*m/g, "");
281
+ const padding = Math.max(0, w - stripped.length - 1);
282
+ lines.push(` ${c.green}│${c.reset} ${cl}${" ".repeat(padding)}${c.green}│${c.reset}`);
283
+ }
284
+ lines.push(` ${c.green}└${"─".repeat(w)}┘${c.reset}`);
285
+ lines.push(``);
286
+ }
287
+ // REPL Output
288
+ if (step.stdout) {
289
+ lines.push(` ${c.yellow}${c.bold}REPL Output${c.reset}`);
290
+ lines.push(` ${c.yellow}┌${"─".repeat(w)}┐${c.reset}`);
291
+ for (const ol of step.stdout.split("\n")) {
292
+ const stripped = ol.replace(/\x1b\[[0-9;]*m/g, "");
293
+ const padding = Math.max(0, w - stripped.length - 1);
294
+ lines.push(` ${c.yellow}│${c.reset} ${ol}${" ".repeat(padding)}${c.yellow}│${c.reset}`);
295
+ }
296
+ lines.push(` ${c.yellow}└${"─".repeat(w)}┘${c.reset}`);
297
+ lines.push(``);
298
+ }
299
+ // Stderr
300
+ if (step.stderr) {
301
+ lines.push(` ${c.red}${c.bold}Stderr${c.reset}`);
302
+ lines.push(` ${c.red}┌${"─".repeat(w)}┐${c.reset}`);
303
+ for (const el of step.stderr.split("\n")) {
304
+ const stripped = el.replace(/\x1b\[[0-9;]*m/g, "");
305
+ const padding = Math.max(0, w - stripped.length - 1);
306
+ lines.push(` ${c.red}│${c.reset} ${el}${" ".repeat(padding)}${c.red}│${c.reset}`);
307
+ }
308
+ lines.push(` ${c.red}└${"─".repeat(w)}┘${c.reset}`);
309
+ lines.push(``);
310
+ }
311
+ // Sub-queries
312
+ if (step.subQueries.length > 0) {
313
+ lines.push(` ${c.magenta}${c.bold}Sub-queries (${step.subQueries.length})${c.reset} ${c.dim}press 's' for details${c.reset}`);
314
+ for (const sq of step.subQueries) {
315
+ const instrPreview = sq.instruction.length > 60 ? sq.instruction.slice(0, 57) + "..." : sq.instruction;
316
+ const sqElapsed = sq.elapsedMs ? ` ${c.dim}${(sq.elapsedMs / 1000).toFixed(1)}s${c.reset}` : "";
317
+ lines.push(` ${c.magenta}#${sq.index}${c.reset} ${c.dim}(${formatSize(sq.contextLength)})${c.reset}${sqElapsed} ${instrPreview}`);
318
+ }
319
+ lines.push(``);
320
+ }
321
+ return lines;
322
+ }
323
+ function renderIteration(state) {
324
+ const { traj, iterIdx } = state;
325
+ const step = traj.iterations[iterIdx];
326
+ if (!step)
327
+ return;
328
+ const allLines = buildIterationContent(step, traj);
329
+ const h = getHeight();
330
+ const footerSize = 2;
331
+ const viewable = h - footerSize;
332
+ // Clamp scrollY
333
+ const maxScroll = Math.max(0, allLines.length - viewable);
334
+ if (state.scrollY > maxScroll)
335
+ state.scrollY = maxScroll;
336
+ if (state.scrollY < 0)
337
+ state.scrollY = 0;
338
+ const from = state.scrollY;
339
+ const to = Math.min(allLines.length, from + viewable);
340
+ W(c.cursorHome, c.clearScreen, c.hideCursor);
341
+ // Scroll indicator at top
342
+ if (from > 0) {
343
+ W(` ${c.dim}^ scroll up (${from} lines above)${c.reset}\n`);
344
+ for (let i = from + 1; i < to; i++)
345
+ W(allLines[i] + "\n");
346
+ }
347
+ else {
348
+ for (let i = from; i < to; i++)
349
+ W(allLines[i] + "\n");
350
+ }
351
+ if (to < allLines.length) {
352
+ // Replace last visible line with scroll indicator
353
+ W(` ${c.dim}v scroll down (${allLines.length - to} lines below)${c.reset}\n`);
354
+ }
355
+ // Footer
356
+ const hints = [];
357
+ if (step.userMessage)
358
+ hints.push(`${c.dim}i${c.reset} input`);
359
+ if (step.rawResponse)
360
+ hints.push(`${c.dim}l${c.reset} response`);
361
+ if (step.systemPrompt || traj.iterations[0]?.systemPrompt)
362
+ hints.push(`${c.dim}p${c.reset} prompt`);
363
+ W(hline("─", c.gray) + "\n");
364
+ W(` ${c.dim}esc${c.reset} back `);
365
+ W(`${c.dim}up/down${c.reset} scroll `);
366
+ W(`${c.dim}n/p${c.reset} next/prev `);
367
+ if (step.subQueries.length > 0)
368
+ W(`${c.dim}s${c.reset} sub-queries `);
369
+ for (const hint of hints)
370
+ W(`${hint} `);
371
+ W(`${c.dim}r${c.reset} result `);
372
+ W(`${c.dim}q${c.reset} quit\n`);
373
+ }
374
+ function renderResult(state) {
375
+ const { traj } = state;
376
+ const result = traj.result;
377
+ W(c.cursorHome, c.clearScreen, c.hideCursor);
378
+ W(`\n${hline("━", c.green)}\n`);
379
+ W(`${centeredHeader(`${c.bold}${c.white}Final Result${c.reset}`, c.green)}\n`);
380
+ W(`${hline("━", c.green)}\n\n`);
381
+ if (!result) {
382
+ W(` ${c.red}${c.bold}No result available${c.reset} — the run may have been interrupted.\n`);
383
+ }
384
+ else {
385
+ kvLine("Completed ", result.completed ? `${c.green}yes${c.reset}` : `${c.red}no${c.reset}`);
386
+ kvLine("Iterations ", `${result.iterations}`);
387
+ kvLine("Sub-queries ", `${result.totalSubQueries}`);
388
+ kvLine("Duration ", `${(traj.totalElapsedMs / 1000).toFixed(1)}s`);
389
+ W(`\n`);
390
+ boxed("Answer", result.answer, c.green);
391
+ }
392
+ W(`\n${hline("─", c.gray)}\n`);
393
+ W(` ${c.dim}esc${c.reset} back `);
394
+ W(`${c.dim}q${c.reset} quit\n`);
395
+ }
396
+ function renderSubQueries(state) {
397
+ const { traj, iterIdx } = state;
398
+ const step = traj.iterations[iterIdx];
399
+ if (!step)
400
+ return;
401
+ const h = getHeight();
402
+ // Build buffer
403
+ const buf = [];
404
+ buf.push(``);
405
+ buf.push(hline("━", c.magenta));
406
+ buf.push(centeredHeader(`${c.bold}${c.white}Sub-queries — Iteration ${step.iteration}${c.reset}`, c.magenta));
407
+ buf.push(hline("━", c.magenta));
408
+ buf.push(``);
409
+ if (step.subQueries.length === 0) {
410
+ buf.push(` ${c.dim}No sub-queries in this iteration.${c.reset}`);
411
+ }
412
+ else {
413
+ // Clamp subQueryIdx
414
+ if (state.subQueryIdx >= step.subQueries.length)
415
+ state.subQueryIdx = step.subQueries.length - 1;
416
+ if (state.subQueryIdx < 0)
417
+ state.subQueryIdx = 0;
418
+ const headerSize = buf.length;
419
+ const footerSize = 2;
420
+ const listBudget = h - headerSize - footerSize;
421
+ // Build list lines (each sub-query = 2 lines: summary + separator)
422
+ const listLines = [];
423
+ const sqStartOffsets = [];
424
+ for (let i = 0; i < step.subQueries.length; i++) {
425
+ const sq = step.subQueries[i];
426
+ const isSel = i === state.subQueryIdx;
427
+ const sqElapsed = sq.elapsedMs ? `${(sq.elapsedMs / 1000).toFixed(1)}s` : "";
428
+ const instrPreview = sq.instruction.length > 50 ? sq.instruction.slice(0, 47) + "..." : sq.instruction;
429
+ sqStartOffsets.push(listLines.length);
430
+ const sel = isSel ? `${c.inverse}${c.magenta}` : "";
431
+ const prefix = isSel ? `${c.magenta}${c.bold} > ` : ` `;
432
+ let line = `${prefix}${sel}#${sq.index}${c.reset}`;
433
+ line += ` ${c.dim}${sqElapsed}${c.reset}`;
434
+ line += ` ${c.dim}${formatSize(sq.contextLength)} in, ${formatSize(sq.resultLength)} out${c.reset}`;
435
+ line += ` ${instrPreview}`;
436
+ listLines.push(line);
437
+ if (i < step.subQueries.length - 1) {
438
+ listLines.push(` ${c.dim} |${c.reset}`);
439
+ }
440
+ }
441
+ // Scroll so selected sub-query is visible
442
+ const selStart = sqStartOffsets[state.subQueryIdx] ?? 0;
443
+ let scrollY = Math.max(0, selStart - 2);
444
+ if (listLines.length <= listBudget)
445
+ scrollY = 0;
446
+ const showFrom = scrollY;
447
+ const showTo = Math.min(listLines.length, scrollY + listBudget);
448
+ if (showFrom > 0) {
449
+ buf.push(` ${c.dim} ^ more above${c.reset}`);
450
+ }
451
+ for (let i = showFrom; i < showTo; i++) {
452
+ buf.push(listLines[i]);
453
+ }
454
+ if (showTo < listLines.length) {
455
+ buf.push(` ${c.dim} | more below${c.reset}`);
456
+ }
457
+ }
458
+ // Render
459
+ W(c.cursorHome, c.clearScreen, c.hideCursor);
460
+ for (const l of buf)
461
+ W(l + "\n");
462
+ // Footer
463
+ W(hline("─", c.gray) + "\n");
464
+ W(` ${c.dim}up/down${c.reset} select ${c.dim}enter${c.reset} view ${c.dim}esc${c.reset} back ${c.dim}q${c.reset} quit\n`);
465
+ }
466
+ function renderSubQueryDetail(state) {
467
+ const { traj, iterIdx } = state;
468
+ const step = traj.iterations[iterIdx];
469
+ if (!step)
470
+ return;
471
+ // Clamp subQueryIdx
472
+ if (state.subQueryIdx >= step.subQueries.length)
473
+ state.subQueryIdx = step.subQueries.length - 1;
474
+ if (state.subQueryIdx < 0)
475
+ state.subQueryIdx = 0;
476
+ const sq = step.subQueries[state.subQueryIdx];
477
+ if (!sq)
478
+ return;
479
+ const w = getWidth() - 4;
480
+ const h = getHeight();
481
+ // Build all content lines
482
+ const allLines = [];
483
+ allLines.push(``);
484
+ allLines.push(hline("━", c.magenta));
485
+ allLines.push(centeredHeader(`${c.bold}${c.white}Sub-query #${sq.index} — Iteration ${step.iteration}${c.reset}`, c.magenta));
486
+ allLines.push(hline("━", c.magenta));
487
+ allLines.push(``);
488
+ // Metadata
489
+ const sqElapsed = sq.elapsedMs ? `${(sq.elapsedMs / 1000).toFixed(1)}s` : "n/a";
490
+ allLines.push(` ${c.gray}Elapsed :${c.reset} ${sqElapsed}`);
491
+ allLines.push(` ${c.gray}Context length:${c.reset} ${formatSize(sq.contextLength)} chars`);
492
+ allLines.push(` ${c.gray}Result length :${c.reset} ${formatSize(sq.resultLength)} chars`);
493
+ allLines.push(` ${c.gray}Position :${c.reset} ${state.subQueryIdx + 1} of ${step.subQueries.length}`);
494
+ allLines.push(``);
495
+ // Full instruction (boxed, no truncation)
496
+ allLines.push(` ${c.magenta}${c.bold}Instruction${c.reset}`);
497
+ allLines.push(` ${c.magenta}┌${"─".repeat(w)}┐${c.reset}`);
498
+ for (const line of sq.instruction.split("\n")) {
499
+ const stripped = line.replace(/\x1b\[[0-9;]*m/g, "");
500
+ const padding = Math.max(0, w - stripped.length - 1);
501
+ allLines.push(` ${c.magenta}│${c.reset} ${line}${" ".repeat(padding)}${c.magenta}│${c.reset}`);
502
+ }
503
+ allLines.push(` ${c.magenta}└${"─".repeat(w)}┘${c.reset}`);
504
+ allLines.push(``);
505
+ // Full result preview (boxed, no truncation)
506
+ allLines.push(` ${c.cyan}${c.bold}Result Preview${c.reset}`);
507
+ allLines.push(` ${c.cyan}┌${"─".repeat(w)}┐${c.reset}`);
508
+ for (const line of sq.resultPreview.split("\n")) {
509
+ const stripped = line.replace(/\x1b\[[0-9;]*m/g, "");
510
+ const padding = Math.max(0, w - stripped.length - 1);
511
+ allLines.push(` ${c.cyan}│${c.reset} ${line}${" ".repeat(padding)}${c.cyan}│${c.reset}`);
512
+ }
513
+ allLines.push(` ${c.cyan}└${"─".repeat(w)}┘${c.reset}`);
514
+ allLines.push(``);
515
+ // Scrollable rendering
516
+ const footerSize = 2;
517
+ const viewable = h - footerSize;
518
+ const maxScroll = Math.max(0, allLines.length - viewable);
519
+ if (state.scrollY > maxScroll)
520
+ state.scrollY = maxScroll;
521
+ if (state.scrollY < 0)
522
+ state.scrollY = 0;
523
+ const from = state.scrollY;
524
+ const to = Math.min(allLines.length, from + viewable);
525
+ W(c.cursorHome, c.clearScreen, c.hideCursor);
526
+ if (from > 0) {
527
+ W(` ${c.dim}^ scroll up (${from} lines above)${c.reset}\n`);
528
+ for (let i = from + 1; i < to; i++)
529
+ W(allLines[i] + "\n");
530
+ }
531
+ else {
532
+ for (let i = from; i < to; i++)
533
+ W(allLines[i] + "\n");
534
+ }
535
+ if (to < allLines.length) {
536
+ W(` ${c.dim}v scroll down (${allLines.length - to} lines below)${c.reset}\n`);
537
+ }
538
+ // Footer
539
+ W(hline("─", c.gray) + "\n");
540
+ W(` ${c.dim}up/down${c.reset} scroll ${c.dim}n/p${c.reset} next/prev ${c.dim}esc${c.reset} back ${c.dim}q${c.reset} quit\n`);
541
+ }
542
+ function renderLlmInput(state) {
543
+ const { traj, iterIdx } = state;
544
+ const step = traj.iterations[iterIdx];
545
+ if (!step)
546
+ return;
547
+ W(c.cursorHome, c.clearScreen, c.hideCursor);
548
+ W(`\n${hline("━", c.blue)}\n`);
549
+ W(`${centeredHeader(`${c.bold}${c.white}LLM Input — Iteration ${step.iteration}${c.reset}`, c.blue)}\n`);
550
+ W(`${hline("━", c.blue)}\n\n`);
551
+ if (step.userMessage) {
552
+ kvLine("Length", `${step.userMessage.length.toLocaleString()} chars`);
553
+ W(`\n`);
554
+ boxed("User Message", step.userMessage, c.blue);
555
+ }
556
+ else {
557
+ W(` ${c.dim}No user message recorded for this iteration.${c.reset}\n`);
558
+ }
559
+ W(`\n${hline("─", c.gray)}\n`);
560
+ W(` ${c.dim}esc${c.reset} back `);
561
+ W(`${c.dim}q${c.reset} quit\n`);
562
+ }
563
+ function renderLlmResponse(state) {
564
+ const { traj, iterIdx } = state;
565
+ const step = traj.iterations[iterIdx];
566
+ if (!step)
567
+ return;
568
+ W(c.cursorHome, c.clearScreen, c.hideCursor);
569
+ W(`\n${hline("━", c.green)}\n`);
570
+ W(`${centeredHeader(`${c.bold}${c.white}LLM Response — Iteration ${step.iteration}${c.reset}`, c.green)}\n`);
571
+ W(`${hline("━", c.green)}\n\n`);
572
+ if (step.rawResponse) {
573
+ kvLine("Length", `${step.rawResponse.length.toLocaleString()} chars`);
574
+ W(`\n`);
575
+ boxed("Full LLM Response", step.rawResponse, c.green);
576
+ }
577
+ else {
578
+ W(` ${c.dim}No response recorded for this iteration.${c.reset}\n`);
579
+ }
580
+ W(`\n${hline("─", c.gray)}\n`);
581
+ W(` ${c.dim}esc${c.reset} back `);
582
+ W(`${c.dim}q${c.reset} quit\n`);
583
+ }
584
+ function renderSystemPrompt(state) {
585
+ const { traj, iterIdx } = state;
586
+ const step = traj.iterations[iterIdx];
587
+ if (!step)
588
+ return;
589
+ W(c.cursorHome, c.clearScreen, c.hideCursor);
590
+ W(`\n${hline("━", c.cyan)}\n`);
591
+ W(`${centeredHeader(`${c.bold}${c.white}System Prompt${c.reset}`, c.cyan)}\n`);
592
+ W(`${hline("━", c.cyan)}\n\n`);
593
+ const sysPrompt = step.systemPrompt || traj.iterations[0]?.systemPrompt;
594
+ if (sysPrompt) {
595
+ boxed("System Prompt", sysPrompt, c.cyan);
596
+ }
597
+ else {
598
+ W(` ${c.dim}System prompt not recorded in this trajectory.${c.reset}\n`);
599
+ }
600
+ W(`\n${hline("─", c.gray)}\n`);
601
+ W(` ${c.dim}esc${c.reset} back `);
602
+ W(`${c.dim}q${c.reset} quit\n`);
603
+ }
604
+ // ── Minimal syntax highlighting ─────────────────────────────────────────────
605
+ function syntaxHighlight(code) {
606
+ return code
607
+ .replace(/\b(import|from|def|class|return|if|elif|else|for|while|in|not|and|or|try|except|finally|with|as|raise|pass|break|continue|yield|lambda|True|False|None|await|async)\b/g, `${c.magenta}$1${c.reset}`)
608
+ .replace(/\b(print|len|range|enumerate|sorted|set|list|dict|str|int|float|type|isinstance|zip|map|filter)\b/g, `${c.cyan}$1${c.reset}`)
609
+ .replace(/(#.*)$/gm, `${c.gray}$1${c.reset}`)
610
+ .replace(/("""[\s\S]*?"""|'''[\s\S]*?'''|"[^"]*"|'[^']*')/g, `${c.yellow}$1${c.reset}`)
611
+ .replace(/\b(llm_query|async_llm_query|context|FINAL|FINAL_VAR)\b/g, `${c.green}${c.bold}$1${c.reset}`);
612
+ }
613
+ // ── Main interactive loop ───────────────────────────────────────────────────
614
+ async function main() {
615
+ // Enter alternate screen buffer so output never scrolls the main terminal
616
+ W(c.altScreenOn);
617
+ // Ensure we always leave alt screen on exit
618
+ const cleanup = () => W(c.showCursor, c.altScreenOff);
619
+ process.on("exit", cleanup);
620
+ let filePath = process.argv[2];
621
+ if (!filePath) {
622
+ const files = listTrajectories();
623
+ if (files.length === 0) {
624
+ console.error(`${c.red}No trajectory files found in ./trajectories/${c.reset}\nRun a query first to generate trajectories.`);
625
+ process.exit(1);
626
+ }
627
+ filePath = await pickFile(files);
628
+ }
629
+ // Load trajectory
630
+ if (!fs.existsSync(filePath)) {
631
+ console.error(`${c.red}File not found: ${filePath}${c.reset}`);
632
+ process.exit(1);
633
+ }
634
+ const traj = JSON.parse(fs.readFileSync(filePath, "utf-8"));
635
+ if (!traj.iterations || traj.iterations.length === 0) {
636
+ console.error(`${c.red}Trajectory has no iterations (empty run).${c.reset}`);
637
+ process.exit(1);
638
+ }
639
+ // State
640
+ const state = {
641
+ mode: "overview",
642
+ iterIdx: 0,
643
+ subQueryIdx: 0,
644
+ scrollY: 0,
645
+ traj,
646
+ };
647
+ function render() {
648
+ switch (state.mode) {
649
+ case "overview":
650
+ renderOverview(state);
651
+ break;
652
+ case "iteration":
653
+ renderIteration(state);
654
+ break;
655
+ case "result":
656
+ renderResult(state);
657
+ break;
658
+ case "subqueries":
659
+ renderSubQueries(state);
660
+ break;
661
+ case "subqueryDetail":
662
+ renderSubQueryDetail(state);
663
+ break;
664
+ case "llmInput":
665
+ renderLlmInput(state);
666
+ break;
667
+ case "llmResponse":
668
+ renderLlmResponse(state);
669
+ break;
670
+ case "systemPrompt":
671
+ renderSystemPrompt(state);
672
+ break;
673
+ }
674
+ }
675
+ render();
676
+ // Key handling
677
+ process.stdin.setRawMode(true);
678
+ process.stdin.resume();
679
+ process.stdin.setEncoding("utf8");
680
+ process.stdin.on("data", (key) => {
681
+ const maxIter = traj.iterations.length - 1;
682
+ switch (state.mode) {
683
+ case "overview":
684
+ if (key === "\x1b[A") {
685
+ state.iterIdx = Math.max(0, state.iterIdx - 1);
686
+ }
687
+ else if (key === "\x1b[B") {
688
+ state.iterIdx = Math.min(maxIter, state.iterIdx + 1);
689
+ }
690
+ else if (key === "\r" || key === "\n" || key === "\x1b[C") {
691
+ // Drill into iteration detail
692
+ state.mode = "iteration";
693
+ state.scrollY = 0;
694
+ }
695
+ else if (key === "r") {
696
+ state.mode = "result";
697
+ }
698
+ else if (key === "q" || key === "\x03") {
699
+ W(c.showCursor, "\n");
700
+ process.exit(0);
701
+ }
702
+ break;
703
+ case "iteration":
704
+ if (key === "\x1b[A") {
705
+ state.scrollY = Math.max(0, state.scrollY - 3);
706
+ }
707
+ else if (key === "\x1b[B") {
708
+ state.scrollY += 3;
709
+ }
710
+ else if (key === "n") {
711
+ if (state.iterIdx < maxIter) {
712
+ state.iterIdx++;
713
+ state.scrollY = 0;
714
+ }
715
+ }
716
+ else if (key === "N") {
717
+ if (state.iterIdx > 0) {
718
+ state.iterIdx--;
719
+ state.scrollY = 0;
720
+ }
721
+ }
722
+ else if (key === "\x1b[D" || key === "\x1b" || key === "b") {
723
+ state.mode = "overview";
724
+ state.scrollY = 0;
725
+ }
726
+ else if (key === "s" && traj.iterations[state.iterIdx]?.subQueries.length > 0) {
727
+ state.mode = "subqueries";
728
+ state.subQueryIdx = 0;
729
+ }
730
+ else if (key === "i") {
731
+ state.mode = "llmInput";
732
+ }
733
+ else if (key === "l") {
734
+ state.mode = "llmResponse";
735
+ }
736
+ else if (key === "p") {
737
+ state.mode = "systemPrompt";
738
+ }
739
+ else if (key === "r") {
740
+ state.mode = "result";
741
+ }
742
+ else if (key === "q" || key === "\x03") {
743
+ W(c.showCursor, "\n");
744
+ process.exit(0);
745
+ }
746
+ break;
747
+ case "result":
748
+ if (key === "\x1b[D" || key === "\x1b" || key === "b") {
749
+ state.mode = "overview";
750
+ }
751
+ else if (key === "q" || key === "\x03") {
752
+ W(c.showCursor, "\n");
753
+ process.exit(0);
754
+ }
755
+ break;
756
+ case "subqueries": {
757
+ const sqCount = traj.iterations[state.iterIdx]?.subQueries.length ?? 0;
758
+ if (key === "\x1b[A") {
759
+ state.subQueryIdx = Math.max(0, state.subQueryIdx - 1);
760
+ }
761
+ else if (key === "\x1b[B") {
762
+ state.subQueryIdx = Math.min(sqCount - 1, state.subQueryIdx + 1);
763
+ }
764
+ else if (key === "\r" || key === "\n" || key === "\x1b[C") {
765
+ state.mode = "subqueryDetail";
766
+ state.scrollY = 0;
767
+ }
768
+ else if (key === "\x1b[D" || key === "\x1b" || key === "b") {
769
+ state.mode = "iteration";
770
+ }
771
+ else if (key === "q" || key === "\x03") {
772
+ W(c.showCursor, "\n");
773
+ process.exit(0);
774
+ }
775
+ break;
776
+ }
777
+ case "subqueryDetail": {
778
+ const sqMax = (traj.iterations[state.iterIdx]?.subQueries.length ?? 1) - 1;
779
+ if (key === "\x1b[A") {
780
+ state.scrollY = Math.max(0, state.scrollY - 3);
781
+ }
782
+ else if (key === "\x1b[B") {
783
+ state.scrollY += 3;
784
+ }
785
+ else if (key === "n" || key === "\x1b[C") {
786
+ if (state.subQueryIdx < sqMax) {
787
+ state.subQueryIdx++;
788
+ state.scrollY = 0;
789
+ }
790
+ }
791
+ else if (key === "p" || key === "N") {
792
+ if (state.subQueryIdx > 0) {
793
+ state.subQueryIdx--;
794
+ state.scrollY = 0;
795
+ }
796
+ }
797
+ else if (key === "\x1b[D" || key === "\x1b" || key === "b") {
798
+ state.mode = "subqueries";
799
+ state.scrollY = 0;
800
+ }
801
+ else if (key === "q" || key === "\x03") {
802
+ W(c.showCursor, "\n");
803
+ process.exit(0);
804
+ }
805
+ break;
806
+ }
807
+ case "llmInput":
808
+ case "llmResponse":
809
+ case "systemPrompt":
810
+ if (key === "\x1b[D" || key === "\x1b" || key === "b") {
811
+ state.mode = "iteration";
812
+ }
813
+ else if (key === "q" || key === "\x03") {
814
+ W(c.showCursor, "\n");
815
+ process.exit(0);
816
+ }
817
+ break;
818
+ }
819
+ render();
820
+ });
821
+ // (cleanup handler already registered at top of main)
822
+ }
823
+ main().catch((err) => {
824
+ W(c.showCursor, c.altScreenOff);
825
+ console.error(`Fatal: ${err}`);
826
+ process.exit(1);
827
+ });
828
+ //# sourceMappingURL=viewer.js.map