swarm-code 0.1.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.
Files changed (79) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +384 -0
  3. package/bin/swarm.mjs +45 -0
  4. package/dist/agents/aider.d.ts +12 -0
  5. package/dist/agents/aider.js +182 -0
  6. package/dist/agents/claude-code.d.ts +9 -0
  7. package/dist/agents/claude-code.js +216 -0
  8. package/dist/agents/codex.d.ts +14 -0
  9. package/dist/agents/codex.js +193 -0
  10. package/dist/agents/direct-llm.d.ts +9 -0
  11. package/dist/agents/direct-llm.js +78 -0
  12. package/dist/agents/mock.d.ts +9 -0
  13. package/dist/agents/mock.js +77 -0
  14. package/dist/agents/opencode.d.ts +23 -0
  15. package/dist/agents/opencode.js +571 -0
  16. package/dist/agents/provider.d.ts +11 -0
  17. package/dist/agents/provider.js +31 -0
  18. package/dist/cli.d.ts +15 -0
  19. package/dist/cli.js +285 -0
  20. package/dist/compression/compressor.d.ts +28 -0
  21. package/dist/compression/compressor.js +265 -0
  22. package/dist/config.d.ts +42 -0
  23. package/dist/config.js +170 -0
  24. package/dist/core/repl.d.ts +69 -0
  25. package/dist/core/repl.js +336 -0
  26. package/dist/core/rlm.d.ts +63 -0
  27. package/dist/core/rlm.js +409 -0
  28. package/dist/core/runtime.py +335 -0
  29. package/dist/core/types.d.ts +131 -0
  30. package/dist/core/types.js +19 -0
  31. package/dist/env.d.ts +10 -0
  32. package/dist/env.js +75 -0
  33. package/dist/interactive-swarm.d.ts +20 -0
  34. package/dist/interactive-swarm.js +1041 -0
  35. package/dist/interactive.d.ts +10 -0
  36. package/dist/interactive.js +1765 -0
  37. package/dist/main.d.ts +15 -0
  38. package/dist/main.js +242 -0
  39. package/dist/mcp/server.d.ts +15 -0
  40. package/dist/mcp/server.js +72 -0
  41. package/dist/mcp/session.d.ts +73 -0
  42. package/dist/mcp/session.js +184 -0
  43. package/dist/mcp/tools.d.ts +15 -0
  44. package/dist/mcp/tools.js +377 -0
  45. package/dist/memory/episodic.d.ts +132 -0
  46. package/dist/memory/episodic.js +390 -0
  47. package/dist/prompts/orchestrator.d.ts +5 -0
  48. package/dist/prompts/orchestrator.js +191 -0
  49. package/dist/routing/model-router.d.ts +130 -0
  50. package/dist/routing/model-router.js +515 -0
  51. package/dist/swarm.d.ts +14 -0
  52. package/dist/swarm.js +557 -0
  53. package/dist/threads/cache.d.ts +58 -0
  54. package/dist/threads/cache.js +198 -0
  55. package/dist/threads/manager.d.ts +85 -0
  56. package/dist/threads/manager.js +659 -0
  57. package/dist/ui/banner.d.ts +14 -0
  58. package/dist/ui/banner.js +42 -0
  59. package/dist/ui/dashboard.d.ts +33 -0
  60. package/dist/ui/dashboard.js +151 -0
  61. package/dist/ui/index.d.ts +10 -0
  62. package/dist/ui/index.js +11 -0
  63. package/dist/ui/log.d.ts +39 -0
  64. package/dist/ui/log.js +126 -0
  65. package/dist/ui/onboarding.d.ts +14 -0
  66. package/dist/ui/onboarding.js +518 -0
  67. package/dist/ui/spinner.d.ts +25 -0
  68. package/dist/ui/spinner.js +113 -0
  69. package/dist/ui/summary.d.ts +18 -0
  70. package/dist/ui/summary.js +113 -0
  71. package/dist/ui/theme.d.ts +63 -0
  72. package/dist/ui/theme.js +97 -0
  73. package/dist/viewer.d.ts +12 -0
  74. package/dist/viewer.js +1284 -0
  75. package/dist/worktree/manager.d.ts +45 -0
  76. package/dist/worktree/manager.js +266 -0
  77. package/dist/worktree/merge.d.ts +28 -0
  78. package/dist/worktree/merge.js +138 -0
  79. package/package.json +69 -0
package/dist/viewer.js ADDED
@@ -0,0 +1,1284 @@
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 os from "node:os";
14
+ import * as path from "node:path";
15
+ // ── ANSI helpers ────────────────────────────────────────────────────────────
16
+ const c = {
17
+ reset: "\x1b[0m",
18
+ bold: "\x1b[1m",
19
+ dim: "\x1b[2m",
20
+ italic: "\x1b[3m",
21
+ underline: "\x1b[4m",
22
+ inverse: "\x1b[7m",
23
+ red: "\x1b[31m",
24
+ green: "\x1b[32m",
25
+ yellow: "\x1b[33m",
26
+ blue: "\x1b[34m",
27
+ magenta: "\x1b[35m",
28
+ cyan: "\x1b[36m",
29
+ white: "\x1b[37m",
30
+ gray: "\x1b[90m",
31
+ bgBlue: "\x1b[44m",
32
+ bgCyan: "\x1b[46m",
33
+ bgGray: "\x1b[100m",
34
+ clearScreen: "\x1b[2J",
35
+ cursorHome: "\x1b[H",
36
+ hideCursor: "\x1b[?25l",
37
+ showCursor: "\x1b[?25h",
38
+ altScreenOn: "\x1b[?1049h",
39
+ altScreenOff: "\x1b[?1049l",
40
+ };
41
+ function W(...args) {
42
+ process.stdout.write(args.join(""));
43
+ }
44
+ // ── Layout helpers ──────────────────────────────────────────────────────────
45
+ function getWidth() {
46
+ return Math.min(process.stdout.columns || 80, 120);
47
+ }
48
+ function getHeight() {
49
+ return process.stdout.rows || 24;
50
+ }
51
+ function hline(ch = "━", color = c.cyan) {
52
+ return `${color}${ch.repeat(getWidth())}${c.reset}`;
53
+ }
54
+ function centeredHeader(text, color = c.cyan) {
55
+ const w = getWidth();
56
+ const stripped = text.replace(/\x1b\[[0-9;]*m/g, "");
57
+ const pad = Math.max(0, w - stripped.length - 4);
58
+ const left = Math.floor(pad / 2);
59
+ const right = pad - left;
60
+ return `${color}${"━".repeat(left)} ${text}${color} ${"━".repeat(right)}${c.reset}`;
61
+ }
62
+ function boxed(title, content, color) {
63
+ const w = getWidth() - 4;
64
+ const display = content;
65
+ W(` ${color}${c.bold}${title}${c.reset}\n`);
66
+ W(` ${color}┌${"─".repeat(w)}┐${c.reset}\n`);
67
+ for (const line of display.split("\n")) {
68
+ const stripped = line.replace(/\x1b\[[0-9;]*m/g, "");
69
+ const padding = Math.max(0, w - stripped.length - 1);
70
+ W(` ${color}│${c.reset} ${line}${" ".repeat(padding)}${color}│${c.reset}\n`);
71
+ }
72
+ W(` ${color}└${"─".repeat(w)}┘${c.reset}\n`);
73
+ }
74
+ function kvLine(key, value) {
75
+ W(` ${c.gray}${key}:${c.reset} ${value}\n`);
76
+ }
77
+ function formatSize(chars) {
78
+ if (chars >= 1_000_000)
79
+ return `${(chars / 1_000_000).toFixed(1)}M`;
80
+ if (chars >= 1000)
81
+ return `${(chars / 1000).toFixed(1)}K`;
82
+ return `${chars}`;
83
+ }
84
+ /** Parse only the metadata fields from a trajectory JSON (not the full iterations array). */
85
+ function parseTrajectoryMeta(filePath) {
86
+ try {
87
+ const raw = fs.readFileSync(filePath, "utf-8");
88
+ // Quick partial parse: extract just the top-level fields we need for the list
89
+ const data = JSON.parse(raw);
90
+ return {
91
+ query: data.query,
92
+ iterations: data.iterations ? new Array(data.iterations.length) : [],
93
+ result: data.result ? { completed: data.result.completed } : null,
94
+ };
95
+ }
96
+ catch {
97
+ return undefined;
98
+ }
99
+ }
100
+ function listTrajectories() {
101
+ // Check both ~/.rlm/trajectories/ (default) and ./trajectories/ (legacy/local)
102
+ const homeDir = path.join(os.homedir(), ".rlm", "trajectories");
103
+ const localDir = path.resolve(process.cwd(), "trajectories");
104
+ const dir = fs.existsSync(homeDir) ? homeDir : localDir;
105
+ if (!fs.existsSync(dir))
106
+ return [];
107
+ return fs
108
+ .readdirSync(dir)
109
+ .filter((f) => f.endsWith(".json"))
110
+ .map((f) => {
111
+ const full = path.join(dir, f);
112
+ const stat = fs.statSync(full);
113
+ const traj = parseTrajectoryMeta(full);
114
+ return { name: f, path: full, size: stat.size, mtime: stat.mtime, traj };
115
+ })
116
+ .sort((a, b) => b.mtime.getTime() - a.mtime.getTime()); // newest first
117
+ }
118
+ async function pickFile(files) {
119
+ return new Promise((resolve) => {
120
+ let selected = 0;
121
+ const maxVisible = Math.min(files.length, getHeight() - 10);
122
+ function render() {
123
+ W(c.cursorHome, c.clearScreen, c.hideCursor);
124
+ W(`\n${hline()}\n`);
125
+ W(`${centeredHeader(`${c.bold}${c.white}RLM Trajectory Viewer${c.reset}`)}\n`);
126
+ W(`${hline()}\n\n`);
127
+ W(` ${c.bold}Select a trajectory:${c.reset} ${c.dim}(up/down navigate, enter select, q quit)${c.reset}\n\n`);
128
+ const scrollStart = Math.max(0, selected - Math.floor(maxVisible / 2));
129
+ const scrollEnd = Math.min(files.length, scrollStart + maxVisible);
130
+ for (let i = scrollStart; i < scrollEnd; i++) {
131
+ const f = files[i];
132
+ const isSel = i === selected;
133
+ const sizeKB = (f.size / 1024).toFixed(1);
134
+ // Extract info from trajectory data
135
+ const steps = f.traj?.iterations?.length ?? 0;
136
+ const completed = f.traj?.result?.completed;
137
+ const status = completed === true ? `${c.green}done${c.reset}` : completed === false ? `${c.yellow}partial${c.reset}` : "";
138
+ const queryPreview = f.traj?.query
139
+ ? f.traj.query.length > 40
140
+ ? `${f.traj.query.slice(0, 37)}...`
141
+ : f.traj.query
142
+ : "";
143
+ // Date from filename
144
+ const dateMatch = f.name.match(/(\d{4}-\d{2}-\d{2})T(\d{2})-(\d{2})/);
145
+ const dateStr = dateMatch ? `${dateMatch[1]} ${dateMatch[2]}:${dateMatch[3]}` : f.name;
146
+ const prefix = isSel ? `${c.cyan}${c.bold} > ` : ` `;
147
+ const nameColor = isSel ? `${c.cyan}${c.bold}` : c.white;
148
+ W(`${prefix}${nameColor}${dateStr}${c.reset}`);
149
+ W(` ${c.dim}${sizeKB}KB${c.reset}`);
150
+ W(` ${c.dim}${steps} step${steps !== 1 ? "s" : ""}${c.reset}`);
151
+ if (status)
152
+ W(` ${status}`);
153
+ if (queryPreview)
154
+ W(` ${c.dim}${queryPreview}${c.reset}`);
155
+ W(`\n`);
156
+ }
157
+ if (files.length > maxVisible) {
158
+ W(`\n ${c.dim}${scrollStart > 0 ? "^ more above" : ""} ${scrollEnd < files.length ? "v more below" : ""}${c.reset}\n`);
159
+ }
160
+ W(`\n`);
161
+ }
162
+ render();
163
+ process.stdin.setRawMode(true);
164
+ process.stdin.resume();
165
+ process.stdin.setEncoding("utf8");
166
+ function onKey(key) {
167
+ if (key === "\x1b[A") {
168
+ selected = Math.max(0, selected - 1);
169
+ render();
170
+ }
171
+ else if (key === "\x1b[B") {
172
+ selected = Math.min(files.length - 1, selected + 1);
173
+ render();
174
+ }
175
+ else if (key === "\r" || key === "\n") {
176
+ process.stdin.removeListener("data", onKey);
177
+ process.stdin.setRawMode(false);
178
+ process.stdin.pause();
179
+ resolve(files[selected].path);
180
+ }
181
+ else if (key === "q" || key === "\x03") {
182
+ W(c.showCursor, c.altScreenOff);
183
+ process.exit(0);
184
+ }
185
+ }
186
+ process.stdin.on("data", onKey);
187
+ });
188
+ }
189
+ function buildIterLine(step, isSelected) {
190
+ const isFinal = step.hasFinal;
191
+ const elapsed = (step.elapsedMs / 1000).toFixed(1);
192
+ const sqCount = step.subQueries.length;
193
+ const bullet = isFinal ? `${c.green}${c.bold}*${c.reset}` : `${c.blue}o${c.reset}`;
194
+ const sel = isSelected ? `${c.inverse}${c.cyan}` : "";
195
+ const codeLen = step.code ? step.code.split("\n").length : 0;
196
+ const outLen = step.stdout ? step.stdout.split("\n").length : 0;
197
+ const sqInfo = sqCount > 0 ? ` | ${c.magenta}${sqCount} sub-quer${sqCount !== 1 ? "ies" : "y"}${c.reset}` : "";
198
+ const errInfo = step.stderr ? ` | ${c.red}stderr${c.reset}` : "";
199
+ let line = ` ${sel} ${bullet} ${c.bold}Iteration ${step.iteration}${c.reset}`;
200
+ line += `${sel ? c.reset : ""} ${c.dim}${elapsed}s${c.reset}`;
201
+ line += ` | ${c.green}${codeLen}L code${c.reset} | ${c.yellow}${outLen}L output${c.reset}${sqInfo}${errInfo}`;
202
+ if (isFinal)
203
+ line += ` | ${c.green}${c.bold}FINAL${c.reset}`;
204
+ return line;
205
+ }
206
+ function renderOverview(state) {
207
+ const { traj } = state;
208
+ const w = getWidth();
209
+ const h = getHeight();
210
+ // Build all lines into a buffer
211
+ const buf = [];
212
+ // Header
213
+ buf.push(``);
214
+ buf.push(hline());
215
+ buf.push(centeredHeader(`${c.bold}${c.white}RLM Trajectory Viewer${c.reset}`));
216
+ buf.push(hline());
217
+ buf.push(``);
218
+ buf.push(` ${c.gray}Model :${c.reset} ${c.bold}${traj.model}${c.reset}`);
219
+ buf.push(` ${c.gray}Query :${c.reset} ${c.yellow}${traj.query}${c.reset}`);
220
+ buf.push(` ${c.gray}Context :${c.reset} ${traj.contextLength.toLocaleString()} chars | ${traj.contextLines.toLocaleString()} lines`);
221
+ 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}`}`);
222
+ buf.push(``);
223
+ buf.push(` ${c.bold}Iterations${c.reset} ${c.dim}(${traj.iterations.length} total)${c.reset}`);
224
+ buf.push(``);
225
+ const headerSize = buf.length;
226
+ const footerSize = 2;
227
+ const answerSize = traj.result ? 4 : 0;
228
+ const iterBudget = h - headerSize - footerSize - answerSize;
229
+ // Build iteration lines (each iteration = summary + separator)
230
+ const flatLines = [];
231
+ const iterStartOffsets = [];
232
+ for (let i = 0; i < traj.iterations.length; i++) {
233
+ const step = traj.iterations[i];
234
+ const isSel = i === state.iterIdx;
235
+ iterStartOffsets.push(flatLines.length);
236
+ flatLines.push(buildIterLine(step, isSel));
237
+ if (i < traj.iterations.length - 1) {
238
+ flatLines.push(` ${c.dim} |${c.reset}`);
239
+ }
240
+ }
241
+ // Scroll so selected iteration is visible
242
+ const selStart = iterStartOffsets[state.iterIdx] ?? 0;
243
+ let scrollY = Math.max(0, selStart - 2);
244
+ // If everything fits, no scroll needed
245
+ if (flatLines.length <= iterBudget) {
246
+ scrollY = 0;
247
+ }
248
+ const showFrom = scrollY;
249
+ const showTo = Math.min(flatLines.length, scrollY + iterBudget);
250
+ if (showFrom > 0) {
251
+ buf.push(` ${c.dim} ^ more above${c.reset}`);
252
+ }
253
+ for (let i = showFrom; i < showTo; i++) {
254
+ buf.push(flatLines[i]);
255
+ }
256
+ if (showTo < flatLines.length) {
257
+ buf.push(` ${c.dim} | more below${c.reset}`);
258
+ }
259
+ // Answer preview
260
+ if (traj.result) {
261
+ buf.push(`${c.green}${"─".repeat(w)}${c.reset}`);
262
+ buf.push(` ${c.green}${c.bold}Answer Preview:${c.reset}`);
263
+ const preview = traj.result.answer.split("\n")[0] || "";
264
+ buf.push(` ${c.white}${preview}${c.reset}`);
265
+ if (traj.result.answer.split("\n").length > 1) {
266
+ buf.push(` ${c.dim}... (press 'r' to see full result)${c.reset}`);
267
+ }
268
+ }
269
+ // Render
270
+ W(c.cursorHome, c.clearScreen, c.hideCursor);
271
+ for (const l of buf)
272
+ W(`${l}\n`);
273
+ // Footer
274
+ W(`${hline("─", c.gray)}\n`);
275
+ const swarmHint = traj.swarm ? ` ${c.dim}t${c.reset} threads` : "";
276
+ W(` ${c.dim}up/down${c.reset} select ${c.dim}enter${c.reset} view ${c.dim}r${c.reset} result${swarmHint} ${c.dim}q${c.reset} quit\n`);
277
+ }
278
+ function buildIterationContent(step, traj) {
279
+ const lines = [];
280
+ const w = getWidth() - 4;
281
+ // Title
282
+ lines.push(``);
283
+ lines.push(hline());
284
+ const finalTag = step.hasFinal ? ` ${c.green}${c.bold}FINAL${c.reset}` : "";
285
+ lines.push(centeredHeader(`${c.bold}${c.white}Iteration ${step.iteration} / ${traj.iterations.length}${c.reset}${finalTag}`));
286
+ lines.push(hline());
287
+ lines.push(``);
288
+ // Metadata
289
+ const elapsed = (step.elapsedMs / 1000).toFixed(1);
290
+ lines.push(` ${c.gray}Elapsed :${c.reset} ${elapsed}s`);
291
+ lines.push(` ${c.gray}Sub-queries:${c.reset} ${step.subQueries.length}`);
292
+ lines.push(` ${c.gray}Has Final :${c.reset} ${step.hasFinal ? `${c.green}yes${c.reset}` : `${c.gray}no${c.reset}`}`);
293
+ lines.push(``);
294
+ // Code
295
+ if (step.code) {
296
+ lines.push(` ${c.green}${c.bold}Generated Code${c.reset}`);
297
+ lines.push(` ${c.green}┌${"─".repeat(w)}┐${c.reset}`);
298
+ for (const cl of syntaxHighlight(step.code).split("\n")) {
299
+ const stripped = cl.replace(/\x1b\[[0-9;]*m/g, "");
300
+ const padding = Math.max(0, w - stripped.length - 1);
301
+ lines.push(` ${c.green}│${c.reset} ${cl}${" ".repeat(padding)}${c.green}│${c.reset}`);
302
+ }
303
+ lines.push(` ${c.green}└${"─".repeat(w)}┘${c.reset}`);
304
+ lines.push(``);
305
+ }
306
+ // REPL Output
307
+ if (step.stdout) {
308
+ lines.push(` ${c.yellow}${c.bold}REPL Output${c.reset}`);
309
+ lines.push(` ${c.yellow}┌${"─".repeat(w)}┐${c.reset}`);
310
+ for (const ol of step.stdout.split("\n")) {
311
+ const stripped = ol.replace(/\x1b\[[0-9;]*m/g, "");
312
+ const padding = Math.max(0, w - stripped.length - 1);
313
+ lines.push(` ${c.yellow}│${c.reset} ${ol}${" ".repeat(padding)}${c.yellow}│${c.reset}`);
314
+ }
315
+ lines.push(` ${c.yellow}└${"─".repeat(w)}┘${c.reset}`);
316
+ lines.push(``);
317
+ }
318
+ // Stderr
319
+ if (step.stderr) {
320
+ lines.push(` ${c.red}${c.bold}Stderr${c.reset}`);
321
+ lines.push(` ${c.red}┌${"─".repeat(w)}┐${c.reset}`);
322
+ for (const el of step.stderr.split("\n")) {
323
+ const stripped = el.replace(/\x1b\[[0-9;]*m/g, "");
324
+ const padding = Math.max(0, w - stripped.length - 1);
325
+ lines.push(` ${c.red}│${c.reset} ${el}${" ".repeat(padding)}${c.red}│${c.reset}`);
326
+ }
327
+ lines.push(` ${c.red}└${"─".repeat(w)}┘${c.reset}`);
328
+ lines.push(``);
329
+ }
330
+ // Sub-queries
331
+ if (step.subQueries.length > 0) {
332
+ lines.push(` ${c.magenta}${c.bold}Sub-queries (${step.subQueries.length})${c.reset} ${c.dim}press 's' for details${c.reset}`);
333
+ for (const sq of step.subQueries) {
334
+ const instrPreview = sq.instruction.length > 60 ? `${sq.instruction.slice(0, 57)}...` : sq.instruction;
335
+ const sqElapsed = sq.elapsedMs ? ` ${c.dim}${(sq.elapsedMs / 1000).toFixed(1)}s${c.reset}` : "";
336
+ lines.push(` ${c.magenta}#${sq.index}${c.reset} ${c.dim}(${formatSize(sq.contextLength)})${c.reset}${sqElapsed} ${instrPreview}`);
337
+ }
338
+ lines.push(``);
339
+ }
340
+ return lines;
341
+ }
342
+ function renderIteration(state) {
343
+ const { traj, iterIdx } = state;
344
+ const step = traj.iterations[iterIdx];
345
+ if (!step)
346
+ return;
347
+ const allLines = buildIterationContent(step, traj);
348
+ const h = getHeight();
349
+ const footerSize = 2;
350
+ const viewable = h - footerSize;
351
+ // Clamp scrollY
352
+ const maxScroll = Math.max(0, allLines.length - viewable);
353
+ if (state.scrollY > maxScroll)
354
+ state.scrollY = maxScroll;
355
+ if (state.scrollY < 0)
356
+ state.scrollY = 0;
357
+ const from = state.scrollY;
358
+ // Reserve lines for scroll indicators when needed
359
+ const hasScrollUp = from > 0;
360
+ const hasScrollDown = from + viewable < allLines.length;
361
+ const contentLines = viewable - (hasScrollUp ? 1 : 0) - (hasScrollDown ? 1 : 0);
362
+ const to = Math.min(allLines.length, from + contentLines);
363
+ W(c.cursorHome, c.clearScreen, c.hideCursor);
364
+ if (hasScrollUp) {
365
+ W(` ${c.dim}^ scroll up (${from} lines above)${c.reset}\n`);
366
+ }
367
+ for (let i = from; i < to; i++)
368
+ W(`${allLines[i]}\n`);
369
+ if (hasScrollDown) {
370
+ W(` ${c.dim}v scroll down (${allLines.length - to} lines below)${c.reset}\n`);
371
+ }
372
+ // Footer
373
+ const hints = [];
374
+ if (step.userMessage)
375
+ hints.push(`${c.dim}i${c.reset} input`);
376
+ if (step.rawResponse)
377
+ hints.push(`${c.dim}l${c.reset} response`);
378
+ if (step.systemPrompt || traj.iterations[0]?.systemPrompt)
379
+ hints.push(`${c.dim}p${c.reset} prompt`);
380
+ W(`${hline("─", c.gray)}\n`);
381
+ W(` ${c.dim}esc${c.reset} back `);
382
+ W(`${c.dim}up/down${c.reset} scroll `);
383
+ W(`${c.dim}n/N${c.reset} next/prev`);
384
+ if (step.subQueries.length > 0)
385
+ W(`${c.dim}s${c.reset} sub-queries `);
386
+ for (const hint of hints)
387
+ W(`${hint} `);
388
+ W(`${c.dim}r${c.reset} result `);
389
+ W(`${c.dim}q${c.reset} quit\n`);
390
+ }
391
+ function renderResult(state) {
392
+ const { traj } = state;
393
+ const result = traj.result;
394
+ W(c.cursorHome, c.clearScreen, c.hideCursor);
395
+ W(`\n${hline("━", c.green)}\n`);
396
+ W(`${centeredHeader(`${c.bold}${c.white}Final Result${c.reset}`, c.green)}\n`);
397
+ W(`${hline("━", c.green)}\n\n`);
398
+ if (!result) {
399
+ W(` ${c.red}${c.bold}No result available${c.reset} — the run may have been interrupted.\n`);
400
+ }
401
+ else {
402
+ kvLine("Completed ", result.completed ? `${c.green}yes${c.reset}` : `${c.red}no${c.reset}`);
403
+ kvLine("Iterations ", `${result.iterations}`);
404
+ kvLine("Sub-queries ", `${result.totalSubQueries}`);
405
+ kvLine("Duration ", `${(traj.totalElapsedMs / 1000).toFixed(1)}s`);
406
+ W(`\n`);
407
+ boxed("Answer", result.answer, c.green);
408
+ }
409
+ W(`\n${hline("─", c.gray)}\n`);
410
+ W(` ${c.dim}esc${c.reset} back `);
411
+ W(`${c.dim}q${c.reset} quit\n`);
412
+ }
413
+ function renderSubQueries(state) {
414
+ const { traj, iterIdx } = state;
415
+ const step = traj.iterations[iterIdx];
416
+ if (!step)
417
+ return;
418
+ const h = getHeight();
419
+ // Build buffer
420
+ const buf = [];
421
+ buf.push(``);
422
+ buf.push(hline("━", c.magenta));
423
+ buf.push(centeredHeader(`${c.bold}${c.white}Sub-queries — Iteration ${step.iteration}${c.reset}`, c.magenta));
424
+ buf.push(hline("━", c.magenta));
425
+ buf.push(``);
426
+ if (step.subQueries.length === 0) {
427
+ buf.push(` ${c.dim}No sub-queries in this iteration.${c.reset}`);
428
+ }
429
+ else {
430
+ // Clamp subQueryIdx
431
+ if (state.subQueryIdx >= step.subQueries.length)
432
+ state.subQueryIdx = step.subQueries.length - 1;
433
+ if (state.subQueryIdx < 0)
434
+ state.subQueryIdx = 0;
435
+ const headerSize = buf.length;
436
+ const footerSize = 2;
437
+ const listBudget = h - headerSize - footerSize;
438
+ // Build list lines (each sub-query = 2 lines: summary + separator)
439
+ const listLines = [];
440
+ const sqStartOffsets = [];
441
+ for (let i = 0; i < step.subQueries.length; i++) {
442
+ const sq = step.subQueries[i];
443
+ const isSel = i === state.subQueryIdx;
444
+ const sqElapsed = sq.elapsedMs ? `${(sq.elapsedMs / 1000).toFixed(1)}s` : "";
445
+ const instrPreview = sq.instruction.length > 50 ? `${sq.instruction.slice(0, 47)}...` : sq.instruction;
446
+ sqStartOffsets.push(listLines.length);
447
+ const sel = isSel ? `${c.inverse}${c.magenta}` : "";
448
+ const prefix = isSel ? `${c.magenta}${c.bold} > ` : ` `;
449
+ let line = `${prefix}${sel}#${sq.index}${c.reset}`;
450
+ line += ` ${c.dim}${sqElapsed}${c.reset}`;
451
+ line += ` ${c.dim}${formatSize(sq.contextLength)} in, ${formatSize(sq.resultLength)} out${c.reset}`;
452
+ line += ` ${instrPreview}`;
453
+ listLines.push(line);
454
+ if (i < step.subQueries.length - 1) {
455
+ listLines.push(` ${c.dim} |${c.reset}`);
456
+ }
457
+ }
458
+ // Scroll so selected sub-query is visible
459
+ const selStart = sqStartOffsets[state.subQueryIdx] ?? 0;
460
+ let scrollY = Math.max(0, selStart - 2);
461
+ if (listLines.length <= listBudget)
462
+ scrollY = 0;
463
+ const showFrom = scrollY;
464
+ const showTo = Math.min(listLines.length, scrollY + listBudget);
465
+ if (showFrom > 0) {
466
+ buf.push(` ${c.dim} ^ more above${c.reset}`);
467
+ }
468
+ for (let i = showFrom; i < showTo; i++) {
469
+ buf.push(listLines[i]);
470
+ }
471
+ if (showTo < listLines.length) {
472
+ buf.push(` ${c.dim} | more below${c.reset}`);
473
+ }
474
+ }
475
+ // Render
476
+ W(c.cursorHome, c.clearScreen, c.hideCursor);
477
+ for (const l of buf)
478
+ W(`${l}\n`);
479
+ // Footer
480
+ W(`${hline("─", c.gray)}\n`);
481
+ 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`);
482
+ }
483
+ function renderSubQueryDetail(state) {
484
+ const { traj, iterIdx } = state;
485
+ const step = traj.iterations[iterIdx];
486
+ if (!step)
487
+ return;
488
+ // Clamp subQueryIdx
489
+ if (state.subQueryIdx >= step.subQueries.length)
490
+ state.subQueryIdx = step.subQueries.length - 1;
491
+ if (state.subQueryIdx < 0)
492
+ state.subQueryIdx = 0;
493
+ const sq = step.subQueries[state.subQueryIdx];
494
+ if (!sq)
495
+ return;
496
+ const w = getWidth() - 4;
497
+ const h = getHeight();
498
+ // Build all content lines
499
+ const allLines = [];
500
+ allLines.push(``);
501
+ allLines.push(hline("━", c.magenta));
502
+ allLines.push(centeredHeader(`${c.bold}${c.white}Sub-query #${sq.index} — Iteration ${step.iteration}${c.reset}`, c.magenta));
503
+ allLines.push(hline("━", c.magenta));
504
+ allLines.push(``);
505
+ // Metadata
506
+ const sqElapsed = sq.elapsedMs ? `${(sq.elapsedMs / 1000).toFixed(1)}s` : "n/a";
507
+ allLines.push(` ${c.gray}Elapsed :${c.reset} ${sqElapsed}`);
508
+ allLines.push(` ${c.gray}Context length:${c.reset} ${formatSize(sq.contextLength)} chars`);
509
+ allLines.push(` ${c.gray}Result length :${c.reset} ${formatSize(sq.resultLength)} chars`);
510
+ allLines.push(` ${c.gray}Position :${c.reset} ${state.subQueryIdx + 1} of ${step.subQueries.length}`);
511
+ allLines.push(``);
512
+ // Full instruction (boxed, no truncation)
513
+ allLines.push(` ${c.magenta}${c.bold}Instruction${c.reset}`);
514
+ allLines.push(` ${c.magenta}┌${"─".repeat(w)}┐${c.reset}`);
515
+ for (const line of sq.instruction.split("\n")) {
516
+ const stripped = line.replace(/\x1b\[[0-9;]*m/g, "");
517
+ const padding = Math.max(0, w - stripped.length - 1);
518
+ allLines.push(` ${c.magenta}│${c.reset} ${line}${" ".repeat(padding)}${c.magenta}│${c.reset}`);
519
+ }
520
+ allLines.push(` ${c.magenta}└${"─".repeat(w)}┘${c.reset}`);
521
+ allLines.push(``);
522
+ // Full result preview (boxed, no truncation)
523
+ allLines.push(` ${c.cyan}${c.bold}Result Preview${c.reset}`);
524
+ allLines.push(` ${c.cyan}┌${"─".repeat(w)}┐${c.reset}`);
525
+ for (const line of sq.resultPreview.split("\n")) {
526
+ const stripped = line.replace(/\x1b\[[0-9;]*m/g, "");
527
+ const padding = Math.max(0, w - stripped.length - 1);
528
+ allLines.push(` ${c.cyan}│${c.reset} ${line}${" ".repeat(padding)}${c.cyan}│${c.reset}`);
529
+ }
530
+ allLines.push(` ${c.cyan}└${"─".repeat(w)}┘${c.reset}`);
531
+ allLines.push(``);
532
+ // Scrollable rendering
533
+ const footerSize = 2;
534
+ const viewable = h - footerSize;
535
+ const maxScroll = Math.max(0, allLines.length - viewable);
536
+ if (state.scrollY > maxScroll)
537
+ state.scrollY = maxScroll;
538
+ if (state.scrollY < 0)
539
+ state.scrollY = 0;
540
+ const from = state.scrollY;
541
+ const hasScrollUp = from > 0;
542
+ const hasScrollDown = from + viewable < allLines.length;
543
+ const contentLines = viewable - (hasScrollUp ? 1 : 0) - (hasScrollDown ? 1 : 0);
544
+ const to = Math.min(allLines.length, from + contentLines);
545
+ W(c.cursorHome, c.clearScreen, c.hideCursor);
546
+ if (hasScrollUp) {
547
+ W(` ${c.dim}^ scroll up (${from} lines above)${c.reset}\n`);
548
+ }
549
+ for (let i = from; i < to; i++)
550
+ W(`${allLines[i]}\n`);
551
+ if (hasScrollDown) {
552
+ W(` ${c.dim}v scroll down (${allLines.length - to} lines below)${c.reset}\n`);
553
+ }
554
+ // Footer
555
+ W(`${hline("─", c.gray)}\n`);
556
+ W(` ${c.dim}up/down${c.reset} scroll ${c.dim}n/N${c.reset} next/prev ${c.dim}esc${c.reset} back ${c.dim}q${c.reset} quit\n`);
557
+ }
558
+ function renderLlmInput(state) {
559
+ const { traj, iterIdx } = state;
560
+ const step = traj.iterations[iterIdx];
561
+ if (!step)
562
+ return;
563
+ W(c.cursorHome, c.clearScreen, c.hideCursor);
564
+ W(`\n${hline("━", c.blue)}\n`);
565
+ W(`${centeredHeader(`${c.bold}${c.white}LLM Input — Iteration ${step.iteration}${c.reset}`, c.blue)}\n`);
566
+ W(`${hline("━", c.blue)}\n\n`);
567
+ if (step.userMessage) {
568
+ kvLine("Length", `${step.userMessage.length.toLocaleString()} chars`);
569
+ W(`\n`);
570
+ boxed("User Message", step.userMessage, c.blue);
571
+ }
572
+ else {
573
+ W(` ${c.dim}No user message recorded for this iteration.${c.reset}\n`);
574
+ }
575
+ W(`\n${hline("─", c.gray)}\n`);
576
+ W(` ${c.dim}esc${c.reset} back `);
577
+ W(`${c.dim}q${c.reset} quit\n`);
578
+ }
579
+ function renderLlmResponse(state) {
580
+ const { traj, iterIdx } = state;
581
+ const step = traj.iterations[iterIdx];
582
+ if (!step)
583
+ return;
584
+ W(c.cursorHome, c.clearScreen, c.hideCursor);
585
+ W(`\n${hline("━", c.green)}\n`);
586
+ W(`${centeredHeader(`${c.bold}${c.white}LLM Response — Iteration ${step.iteration}${c.reset}`, c.green)}\n`);
587
+ W(`${hline("━", c.green)}\n\n`);
588
+ if (step.rawResponse) {
589
+ kvLine("Length", `${step.rawResponse.length.toLocaleString()} chars`);
590
+ W(`\n`);
591
+ boxed("Full LLM Response", step.rawResponse, c.green);
592
+ }
593
+ else {
594
+ W(` ${c.dim}No response recorded for this iteration.${c.reset}\n`);
595
+ }
596
+ W(`\n${hline("─", c.gray)}\n`);
597
+ W(` ${c.dim}esc${c.reset} back `);
598
+ W(`${c.dim}q${c.reset} quit\n`);
599
+ }
600
+ function renderSystemPrompt(state) {
601
+ const { traj, iterIdx } = state;
602
+ const step = traj.iterations[iterIdx];
603
+ if (!step)
604
+ return;
605
+ W(c.cursorHome, c.clearScreen, c.hideCursor);
606
+ W(`\n${hline("━", c.cyan)}\n`);
607
+ W(`${centeredHeader(`${c.bold}${c.white}System Prompt${c.reset}`, c.cyan)}\n`);
608
+ W(`${hline("━", c.cyan)}\n\n`);
609
+ const sysPrompt = step.systemPrompt || traj.iterations[0]?.systemPrompt;
610
+ if (sysPrompt) {
611
+ boxed("System Prompt", sysPrompt, c.cyan);
612
+ }
613
+ else {
614
+ W(` ${c.dim}System prompt not recorded in this trajectory.${c.reset}\n`);
615
+ }
616
+ W(`\n${hline("─", c.gray)}\n`);
617
+ W(` ${c.dim}esc${c.reset} back `);
618
+ W(`${c.dim}q${c.reset} quit\n`);
619
+ }
620
+ // ── Swarm view helpers ──────────────────────────────────────────────────────
621
+ /** Build a timing bar: filled blocks for elapsed portion, empty for remainder. */
622
+ function timingBar(durationMs, maxDurationMs, barWidth) {
623
+ if (maxDurationMs <= 0)
624
+ return "░".repeat(barWidth);
625
+ const filled = Math.min(barWidth, Math.max(1, Math.round((durationMs / maxDurationMs) * barWidth)));
626
+ const empty = barWidth - filled;
627
+ return "█".repeat(filled) + "░".repeat(empty);
628
+ }
629
+ /** Get status color for a thread. */
630
+ function statusColor(status) {
631
+ switch (status) {
632
+ case "completed":
633
+ return c.green;
634
+ case "failed":
635
+ return c.red;
636
+ case "cache_hit":
637
+ return c.yellow;
638
+ case "cancelled":
639
+ return c.gray;
640
+ default:
641
+ return c.white;
642
+ }
643
+ }
644
+ /** Get status label for a thread. */
645
+ function statusLabel(status) {
646
+ switch (status) {
647
+ case "cache_hit":
648
+ return "CACHED";
649
+ default:
650
+ return status.toUpperCase();
651
+ }
652
+ }
653
+ /** Get the flat ordered thread list (grouped by iteration). */
654
+ function getFlatThreadList(swarm) {
655
+ const byIteration = new Map();
656
+ for (const t of swarm.threads) {
657
+ const group = byIteration.get(t.iteration) || [];
658
+ group.push(t);
659
+ byIteration.set(t.iteration, group);
660
+ }
661
+ const iterations = [...byIteration.keys()].sort((a, b) => a - b);
662
+ const flat = [];
663
+ for (const iter of iterations) {
664
+ flat.push(...byIteration.get(iter));
665
+ }
666
+ return flat;
667
+ }
668
+ // ── Swarm view ──────────────────────────────────────────────────────────────
669
+ function renderSwarmView(state) {
670
+ const { traj } = state;
671
+ const swarm = traj.swarm;
672
+ const h = getHeight();
673
+ const buf = [];
674
+ // Header
675
+ buf.push(``);
676
+ buf.push(hline("━", c.cyan));
677
+ buf.push(centeredHeader(`${c.bold}${c.white}Swarm Thread DAG${c.reset}`, c.cyan));
678
+ buf.push(hline("━", c.cyan));
679
+ buf.push(``);
680
+ if (!swarm || swarm.threads.length === 0) {
681
+ buf.push(` ${c.dim}No swarm thread data in this trajectory.${c.reset}`);
682
+ buf.push(` ${c.dim}(Run in swarm mode to generate thread data)${c.reset}`);
683
+ W(c.cursorHome, c.clearScreen, c.hideCursor);
684
+ for (const l of buf)
685
+ W(`${l}\n`);
686
+ W(`\n${hline("─", c.gray)}\n`);
687
+ W(` ${c.dim}esc${c.reset} back ${c.dim}q${c.reset} quit\n`);
688
+ return;
689
+ }
690
+ // Compute stats
691
+ const completed = swarm.threads.filter((t) => t.status === "completed").length;
692
+ const failed = swarm.threads.filter((t) => t.status === "failed").length;
693
+ const cached = swarm.threads.filter((t) => t.status === "cache_hit").length;
694
+ const cancelled = swarm.threads.filter((t) => t.status === "cancelled").length;
695
+ const aggregateDuration = swarm.threads.reduce((s, t) => s + t.durationMs, 0);
696
+ const maxDurationMs = Math.max(...swarm.threads.map((t) => t.durationMs), 1);
697
+ // Estimate wall-clock time: sum of max-duration per iteration
698
+ const byIteration = new Map();
699
+ for (const t of swarm.threads) {
700
+ const group = byIteration.get(t.iteration) || [];
701
+ group.push(t);
702
+ byIteration.set(t.iteration, group);
703
+ }
704
+ const iterations = [...byIteration.keys()].sort((a, b) => a - b);
705
+ let wallClockMs = 0;
706
+ for (const iter of iterations) {
707
+ const threads = byIteration.get(iter);
708
+ wallClockMs += Math.max(...threads.map((t) => t.durationMs));
709
+ }
710
+ // Summary stats
711
+ buf.push(` ${c.gray}Threads :${c.reset} ${swarm.threads.length} total ${c.green}${completed} ok${c.reset} ${failed > 0 ? `${c.red}${failed} fail${c.reset} ` : ""}${cached > 0 ? `${c.yellow}${cached} cached${c.reset} ` : ""}${cancelled > 0 ? `${c.gray}${cancelled} cancelled${c.reset} ` : ""}`);
712
+ buf.push(` ${c.gray}Cost :${c.reset} $${swarm.totalCostUsd.toFixed(4)}`);
713
+ buf.push(` ${c.gray}Time :${c.reset} ${(wallClockMs / 1000).toFixed(1)}s wall / ${(aggregateDuration / 1000).toFixed(1)}s aggregate ${c.dim}(${aggregateDuration > 0 ? (aggregateDuration / Math.max(wallClockMs, 1)).toFixed(1) : "1.0"}x parallelism)${c.reset}`);
714
+ if (swarm.cacheStats.hits > 0) {
715
+ buf.push(` ${c.gray}Cache :${c.reset} ${swarm.cacheStats.hits} hits, saved ${(swarm.cacheStats.savedMs / 1000).toFixed(1)}s / $${swarm.cacheStats.savedUsd.toFixed(4)}`);
716
+ }
717
+ if (swarm.episodeCount !== undefined) {
718
+ buf.push(` ${c.gray}Episodes:${c.reset} ${swarm.episodeCount} in memory`);
719
+ }
720
+ buf.push(``);
721
+ // Build flat thread list for navigation
722
+ const flatThreads = getFlatThreadList(swarm);
723
+ // Clamp swarmThreadIdx
724
+ if (state.swarmThreadIdx >= flatThreads.length)
725
+ state.swarmThreadIdx = flatThreads.length - 1;
726
+ if (state.swarmThreadIdx < 0)
727
+ state.swarmThreadIdx = 0;
728
+ // Determine timing bar width (fits within terminal minus prefix overhead)
729
+ const barWidth = 10;
730
+ // Build DAG lines — each thread produces 2-3 lines, iteration headers produce 1 line + connector
731
+ const dagLines = [];
732
+ const threadLineOffsets = []; // maps flat thread index -> dagLines offset
733
+ let flatIdx = 0;
734
+ for (let iterPos = 0; iterPos < iterations.length; iterPos++) {
735
+ const iter = iterations[iterPos];
736
+ const threads = byIteration.get(iter);
737
+ // Iteration header with dependency info
738
+ const allDeps = new Set();
739
+ for (const t of threads) {
740
+ if (t.dependsOn) {
741
+ for (const d of t.dependsOn)
742
+ allDeps.add(d);
743
+ }
744
+ }
745
+ // Filter deps to only those from prior iterations
746
+ const priorThreadIds = new Set();
747
+ for (let pi = 0; pi < iterPos; pi++) {
748
+ const priorIter = iterations[pi];
749
+ for (const pt of byIteration.get(priorIter)) {
750
+ priorThreadIds.add(pt.threadId);
751
+ }
752
+ }
753
+ const externalDeps = [...allDeps].filter((d) => priorThreadIds.has(d));
754
+ const depSuffix = externalDeps.length > 0
755
+ ? ` ${c.dim}(depends on: ${externalDeps.map((d) => d.slice(0, 8)).join(", ")})${c.reset}`
756
+ : "";
757
+ dagLines.push(` ${c.cyan}${c.bold}Iteration ${iter}${c.reset}${depSuffix}`);
758
+ for (let ti = 0; ti < threads.length; ti++) {
759
+ const t = threads[ti];
760
+ const isSelected = flatIdx === state.swarmThreadIdx;
761
+ const isLast = ti === threads.length - 1;
762
+ const connector = isLast ? "└─" : "├─";
763
+ const subConnector = isLast ? " " : "│ ";
764
+ const tag = t.threadId.slice(0, 8);
765
+ const sColor = statusColor(t.status);
766
+ const sLabel = statusLabel(t.status).padEnd(9);
767
+ const bar = timingBar(t.durationMs, maxDurationMs, barWidth);
768
+ const duration = `${(t.durationMs / 1000).toFixed(1)}s`.padStart(6);
769
+ const cost = `$${t.estimatedCostUsd.toFixed(4)}`;
770
+ const highlight = isSelected ? `${c.inverse}` : "";
771
+ const highlightEnd = isSelected ? `${c.reset}` : "";
772
+ // Thread main line
773
+ threadLineOffsets.push(dagLines.length);
774
+ const mainLine = ` ${connector} ${highlight}${c.dim}${tag}${c.reset}${highlightEnd} ${sColor}${sLabel}${c.reset} ${sColor}${bar}${c.reset} ${c.dim}${duration} ${cost}${c.reset} ${t.agent}/${c.bold}${t.model}${c.reset}`;
775
+ dagLines.push(mainLine);
776
+ // Task description line
777
+ const taskPreview = t.task.length > 65 ? `${t.task.slice(0, 62)}...` : t.task;
778
+ const fileCount = t.filesChanged.length;
779
+ const fileSuffix = fileCount > 0 ? ` ${c.dim}(${fileCount} file${fileCount !== 1 ? "s" : ""})${c.reset}` : "";
780
+ dagLines.push(` ${subConnector} └─ ${taskPreview}${fileSuffix}`);
781
+ flatIdx++;
782
+ }
783
+ // Inter-iteration connector
784
+ if (iterPos < iterations.length - 1) {
785
+ dagLines.push(` ${c.dim}│${c.reset}`);
786
+ }
787
+ }
788
+ // Merge events
789
+ if (swarm.mergeEvents.length > 0) {
790
+ dagLines.push(``);
791
+ dagLines.push(` ${c.magenta}${c.bold}Merge Results${c.reset}`);
792
+ for (const m of swarm.mergeEvents) {
793
+ const icon = m.success ? `${c.green}+${c.reset}` : `${c.red}x${c.reset}`;
794
+ dagLines.push(` ${icon} ${m.branch}: ${m.message}`);
795
+ }
796
+ }
797
+ // Cost breakdown panel (toggled with 'c')
798
+ if (state.showCostBreakdown) {
799
+ dagLines.push(``);
800
+ dagLines.push(` ${c.yellow}${c.bold}Cost Breakdown by Agent${c.reset}`);
801
+ dagLines.push(` ${c.yellow}${"─".repeat(40)}${c.reset}`);
802
+ const costByAgent = new Map();
803
+ for (const t of swarm.threads) {
804
+ const key = `${t.agent}/${t.model}`;
805
+ const entry = costByAgent.get(key) || { cost: 0, count: 0, durationMs: 0 };
806
+ entry.cost += t.estimatedCostUsd;
807
+ entry.count++;
808
+ entry.durationMs += t.durationMs;
809
+ costByAgent.set(key, entry);
810
+ }
811
+ const sorted = [...costByAgent.entries()].sort((a, b) => b[1].cost - a[1].cost);
812
+ for (const [agent, stats] of sorted) {
813
+ const pct = swarm.totalCostUsd > 0 ? ((stats.cost / swarm.totalCostUsd) * 100).toFixed(0) : "0";
814
+ dagLines.push(` ${c.bold}${agent}${c.reset} ${c.dim}${stats.count} thread${stats.count !== 1 ? "s" : ""}${c.reset} $${stats.cost.toFixed(4)} ${c.dim}(${pct}%)${c.reset} ${c.dim}${(stats.durationMs / 1000).toFixed(1)}s${c.reset}`);
815
+ }
816
+ dagLines.push(` ${c.yellow}${"─".repeat(40)}${c.reset}`);
817
+ dagLines.push(` ${c.bold}Total${c.reset} ${swarm.threads.length} threads $${swarm.totalCostUsd.toFixed(4)}`);
818
+ }
819
+ // Calculate scrolling for DAG content
820
+ const headerSize = buf.length;
821
+ const footerSize = 2;
822
+ const dagBudget = h - headerSize - footerSize;
823
+ // Scroll so selected thread is visible
824
+ const selLineOffset = threadLineOffsets[state.swarmThreadIdx] ?? 0;
825
+ let scrollY = Math.max(0, selLineOffset - Math.floor(dagBudget / 2));
826
+ if (dagLines.length <= dagBudget)
827
+ scrollY = 0;
828
+ // Reserve lines for scroll indicators when needed
829
+ const hasScrollUp = scrollY > 0;
830
+ const hasScrollDown = scrollY + dagBudget < dagLines.length;
831
+ const contentBudget = dagBudget - (hasScrollUp ? 1 : 0) - (hasScrollDown ? 1 : 0);
832
+ const showFrom = scrollY;
833
+ const showTo = Math.min(dagLines.length, scrollY + contentBudget);
834
+ if (hasScrollUp) {
835
+ buf.push(` ${c.dim}^ more above (${showFrom} lines)${c.reset}`);
836
+ }
837
+ for (let i = showFrom; i < showTo; i++) {
838
+ buf.push(dagLines[i]);
839
+ }
840
+ if (hasScrollDown) {
841
+ buf.push(` ${c.dim}v more below (${dagLines.length - showTo} lines)${c.reset}`);
842
+ }
843
+ // Render
844
+ W(c.cursorHome, c.clearScreen, c.hideCursor);
845
+ for (const l of buf)
846
+ W(`${l}\n`);
847
+ // Footer
848
+ W(`${hline("─", c.gray)}\n`);
849
+ W(` ${c.dim}up/down${c.reset} select ${c.dim}enter${c.reset} detail ${c.dim}c${c.reset} cost ${c.dim}m${c.reset} merges ${c.dim}esc${c.reset} back ${c.dim}q${c.reset} quit\n`);
850
+ }
851
+ // ── Swarm thread detail ─────────────────────────────────────────────────────
852
+ function renderSwarmThreadDetail(state) {
853
+ const { traj } = state;
854
+ const swarm = traj.swarm;
855
+ if (!swarm || swarm.threads.length === 0)
856
+ return;
857
+ const flatThreads = getFlatThreadList(swarm);
858
+ if (state.swarmThreadIdx >= flatThreads.length)
859
+ state.swarmThreadIdx = flatThreads.length - 1;
860
+ if (state.swarmThreadIdx < 0)
861
+ state.swarmThreadIdx = 0;
862
+ const t = flatThreads[state.swarmThreadIdx];
863
+ if (!t)
864
+ return;
865
+ const w = getWidth() - 4;
866
+ const h = getHeight();
867
+ const maxDurationMs = Math.max(...swarm.threads.map((th) => th.durationMs), 1);
868
+ const barWidth = 20;
869
+ // Build all content lines
870
+ const allLines = [];
871
+ allLines.push(``);
872
+ allLines.push(hline("━", c.cyan));
873
+ allLines.push(centeredHeader(`${c.bold}${c.white}Thread ${t.threadId.slice(0, 8)} — ${statusLabel(t.status)}${c.reset}`, c.cyan));
874
+ allLines.push(hline("━", c.cyan));
875
+ allLines.push(``);
876
+ // Status with color + timing bar
877
+ const sColor = statusColor(t.status);
878
+ const bar = timingBar(t.durationMs, maxDurationMs, barWidth);
879
+ allLines.push(` ${c.gray}Status :${c.reset} ${sColor}${c.bold}${statusLabel(t.status)}${c.reset} ${sColor}${bar}${c.reset}`);
880
+ allLines.push(` ${c.gray}Thread ID:${c.reset} ${t.threadId}`);
881
+ allLines.push(` ${c.gray}Iteration:${c.reset} ${t.iteration}`);
882
+ allLines.push(` ${c.gray}Agent :${c.reset} ${t.agent}`);
883
+ allLines.push(` ${c.gray}Model :${c.reset} ${c.bold}${t.model}${c.reset}`);
884
+ if (t.slot) {
885
+ allLines.push(` ${c.gray}Slot :${c.reset} ${t.slot}`);
886
+ }
887
+ allLines.push(` ${c.gray}Duration :${c.reset} ${(t.durationMs / 1000).toFixed(1)}s ${c.dim}(${t.durationMs}ms)${c.reset}`);
888
+ allLines.push(` ${c.gray}Cost :${c.reset} $${t.estimatedCostUsd.toFixed(4)}`);
889
+ allLines.push(``);
890
+ // Full task description (boxed)
891
+ allLines.push(` ${c.cyan}${c.bold}Task${c.reset}`);
892
+ allLines.push(` ${c.cyan}┌${"─".repeat(w)}┐${c.reset}`);
893
+ for (const line of t.task.split("\n")) {
894
+ const stripped = line.replace(/\x1b\[[0-9;]*m/g, "");
895
+ const padding = Math.max(0, w - stripped.length - 1);
896
+ allLines.push(` ${c.cyan}│${c.reset} ${line}${" ".repeat(padding)}${c.cyan}│${c.reset}`);
897
+ }
898
+ allLines.push(` ${c.cyan}└${"─".repeat(w)}┘${c.reset}`);
899
+ allLines.push(``);
900
+ // Dependencies
901
+ if (t.dependsOn && t.dependsOn.length > 0) {
902
+ allLines.push(` ${c.magenta}${c.bold}Dependencies${c.reset} ${c.dim}(${t.dependsOn.length} thread${t.dependsOn.length !== 1 ? "s" : ""} provided context)${c.reset}`);
903
+ for (const depId of t.dependsOn) {
904
+ const depThread = swarm.threads.find((th) => th.threadId === depId);
905
+ if (depThread) {
906
+ const depColor = statusColor(depThread.status);
907
+ const depTaskPreview = depThread.task.length > 50 ? `${depThread.task.slice(0, 47)}...` : depThread.task;
908
+ allLines.push(` ${depColor}${depId.slice(0, 8)}${c.reset} ${depColor}${statusLabel(depThread.status)}${c.reset} ${c.dim}${depTaskPreview}${c.reset}`);
909
+ }
910
+ else {
911
+ allLines.push(` ${c.dim}${depId.slice(0, 8)} (not found in thread list)${c.reset}`);
912
+ }
913
+ }
914
+ allLines.push(``);
915
+ }
916
+ // Downstream (threads that depend on this one)
917
+ const downstream = swarm.threads.filter((th) => th.dependsOn?.includes(t.threadId));
918
+ if (downstream.length > 0) {
919
+ allLines.push(` ${c.blue}${c.bold}Downstream${c.reset} ${c.dim}(${downstream.length} thread${downstream.length !== 1 ? "s" : ""} depend on this)${c.reset}`);
920
+ for (const ds of downstream) {
921
+ const dsColor = statusColor(ds.status);
922
+ const dsTaskPreview = ds.task.length > 50 ? `${ds.task.slice(0, 47)}...` : ds.task;
923
+ allLines.push(` ${dsColor}${ds.threadId.slice(0, 8)}${c.reset} ${dsColor}${statusLabel(ds.status)}${c.reset} ${c.dim}iter ${ds.iteration}${c.reset} ${c.dim}${dsTaskPreview}${c.reset}`);
924
+ }
925
+ allLines.push(``);
926
+ }
927
+ // Files changed
928
+ if (t.filesChanged.length > 0) {
929
+ allLines.push(` ${c.green}${c.bold}Files Changed${c.reset} ${c.dim}(${t.filesChanged.length})${c.reset}`);
930
+ for (const f of t.filesChanged) {
931
+ allLines.push(` ${c.green}+${c.reset} ${f}`);
932
+ }
933
+ allLines.push(``);
934
+ }
935
+ else {
936
+ allLines.push(` ${c.dim}No files changed.${c.reset}`);
937
+ allLines.push(``);
938
+ }
939
+ // Position info
940
+ const posInFlat = state.swarmThreadIdx + 1;
941
+ allLines.push(` ${c.dim}Thread ${posInFlat} of ${flatThreads.length}${c.reset}`);
942
+ // Scrollable rendering
943
+ const footerSize = 2;
944
+ const viewable = h - footerSize;
945
+ const maxScroll = Math.max(0, allLines.length - viewable);
946
+ if (state.scrollY > maxScroll)
947
+ state.scrollY = maxScroll;
948
+ if (state.scrollY < 0)
949
+ state.scrollY = 0;
950
+ const from = state.scrollY;
951
+ const hasScrollUp = from > 0;
952
+ const hasScrollDown = from + viewable < allLines.length;
953
+ const contentLines = viewable - (hasScrollUp ? 1 : 0) - (hasScrollDown ? 1 : 0);
954
+ const to = Math.min(allLines.length, from + contentLines);
955
+ W(c.cursorHome, c.clearScreen, c.hideCursor);
956
+ if (hasScrollUp) {
957
+ W(` ${c.dim}^ scroll up (${from} lines above)${c.reset}\n`);
958
+ }
959
+ for (let i = from; i < to; i++)
960
+ W(`${allLines[i]}\n`);
961
+ if (hasScrollDown) {
962
+ W(` ${c.dim}v scroll down (${allLines.length - to} lines below)${c.reset}\n`);
963
+ }
964
+ // Footer
965
+ W(`${hline("─", c.gray)}\n`);
966
+ W(` ${c.dim}up/down${c.reset} scroll ${c.dim}n/N${c.reset} next/prev ${c.dim}esc${c.reset} back ${c.dim}q${c.reset} quit\n`);
967
+ }
968
+ // ── Minimal syntax highlighting ─────────────────────────────────────────────
969
+ function syntaxHighlight(code) {
970
+ return code
971
+ .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}`)
972
+ .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}`)
973
+ .replace(/(#.*)$/gm, `${c.gray}$1${c.reset}`)
974
+ .replace(/("""[\s\S]*?"""|'''[\s\S]*?'''|"[^"]*"|'[^']*')/g, `${c.yellow}$1${c.reset}`)
975
+ .replace(/\b(llm_query|async_llm_query|context|FINAL|FINAL_VAR)\b/g, `${c.green}${c.bold}$1${c.reset}`);
976
+ }
977
+ // ── Main interactive loop ───────────────────────────────────────────────────
978
+ async function main() {
979
+ // Enter alternate screen buffer so output never scrolls the main terminal
980
+ W(c.altScreenOn);
981
+ // Ensure we always restore terminal on exit (alt screen, cursor, raw mode)
982
+ const cleanup = () => {
983
+ try {
984
+ process.stdin.setRawMode(false);
985
+ }
986
+ catch { }
987
+ W(c.showCursor, c.altScreenOff);
988
+ };
989
+ process.on("exit", cleanup);
990
+ process.on("SIGINT", () => {
991
+ cleanup();
992
+ process.exit(0);
993
+ });
994
+ process.on("SIGTERM", () => {
995
+ cleanup();
996
+ process.exit(0);
997
+ });
998
+ let filePath = process.argv[2];
999
+ if (!filePath) {
1000
+ const files = listTrajectories();
1001
+ if (files.length === 0) {
1002
+ console.error(`${c.red}No trajectory files found in ~/.rlm/trajectories/${c.reset}\nRun a query first to generate trajectories.`);
1003
+ process.exit(1);
1004
+ }
1005
+ filePath = await pickFile(files);
1006
+ }
1007
+ // Load trajectory
1008
+ if (!fs.existsSync(filePath)) {
1009
+ console.error(`${c.red}File not found: ${filePath}${c.reset}`);
1010
+ process.exit(1);
1011
+ }
1012
+ let traj;
1013
+ try {
1014
+ traj = JSON.parse(fs.readFileSync(filePath, "utf-8"));
1015
+ }
1016
+ catch (err) {
1017
+ console.error(`${c.red}Could not parse trajectory file: ${err.message}${c.reset}`);
1018
+ process.exit(1);
1019
+ }
1020
+ if (!traj.iterations || traj.iterations.length === 0) {
1021
+ console.error(`${c.red}Trajectory has no iterations (empty run).${c.reset}`);
1022
+ process.exit(1);
1023
+ }
1024
+ // State
1025
+ const state = {
1026
+ mode: "overview",
1027
+ iterIdx: 0,
1028
+ subQueryIdx: 0,
1029
+ scrollY: 0,
1030
+ traj,
1031
+ swarmThreadIdx: 0,
1032
+ showCostBreakdown: false,
1033
+ };
1034
+ function render() {
1035
+ switch (state.mode) {
1036
+ case "overview":
1037
+ renderOverview(state);
1038
+ break;
1039
+ case "iteration":
1040
+ renderIteration(state);
1041
+ break;
1042
+ case "result":
1043
+ renderResult(state);
1044
+ break;
1045
+ case "subqueries":
1046
+ renderSubQueries(state);
1047
+ break;
1048
+ case "subqueryDetail":
1049
+ renderSubQueryDetail(state);
1050
+ break;
1051
+ case "llmInput":
1052
+ renderLlmInput(state);
1053
+ break;
1054
+ case "llmResponse":
1055
+ renderLlmResponse(state);
1056
+ break;
1057
+ case "systemPrompt":
1058
+ renderSystemPrompt(state);
1059
+ break;
1060
+ case "swarm":
1061
+ renderSwarmView(state);
1062
+ break;
1063
+ case "swarmThreadDetail":
1064
+ renderSwarmThreadDetail(state);
1065
+ break;
1066
+ }
1067
+ }
1068
+ render();
1069
+ // Key handling
1070
+ process.stdin.setRawMode(true);
1071
+ process.stdin.resume();
1072
+ process.stdin.setEncoding("utf8");
1073
+ process.stdin.on("data", (key) => {
1074
+ const maxIter = traj.iterations.length - 1;
1075
+ switch (state.mode) {
1076
+ case "overview":
1077
+ if (key === "\x1b[A") {
1078
+ state.iterIdx = Math.max(0, state.iterIdx - 1);
1079
+ }
1080
+ else if (key === "\x1b[B") {
1081
+ state.iterIdx = Math.min(maxIter, state.iterIdx + 1);
1082
+ }
1083
+ else if (key === "\r" || key === "\n" || key === "\x1b[C") {
1084
+ // Drill into iteration detail
1085
+ state.mode = "iteration";
1086
+ state.scrollY = 0;
1087
+ }
1088
+ else if (key === "r") {
1089
+ state.mode = "result";
1090
+ }
1091
+ else if (key === "t" && traj.swarm) {
1092
+ state.mode = "swarm";
1093
+ }
1094
+ else if (key === "q" || key === "\x03") {
1095
+ W(c.showCursor, "\n");
1096
+ process.exit(0);
1097
+ }
1098
+ break;
1099
+ case "iteration":
1100
+ if (key === "\x1b[A") {
1101
+ state.scrollY = Math.max(0, state.scrollY - 3);
1102
+ }
1103
+ else if (key === "\x1b[B") {
1104
+ state.scrollY += 3;
1105
+ }
1106
+ else if (key === "n") {
1107
+ if (state.iterIdx < maxIter) {
1108
+ state.iterIdx++;
1109
+ state.scrollY = 0;
1110
+ }
1111
+ }
1112
+ else if (key === "N") {
1113
+ if (state.iterIdx > 0) {
1114
+ state.iterIdx--;
1115
+ state.scrollY = 0;
1116
+ }
1117
+ }
1118
+ else if (key === "\x1b[D" || key === "\x1b" || key === "b") {
1119
+ state.mode = "overview";
1120
+ state.scrollY = 0;
1121
+ }
1122
+ else if (key === "s" && traj.iterations[state.iterIdx]?.subQueries.length > 0) {
1123
+ state.mode = "subqueries";
1124
+ state.subQueryIdx = 0;
1125
+ }
1126
+ else if (key === "i") {
1127
+ state.mode = "llmInput";
1128
+ }
1129
+ else if (key === "l") {
1130
+ state.mode = "llmResponse";
1131
+ }
1132
+ else if (key === "p") {
1133
+ state.mode = "systemPrompt";
1134
+ }
1135
+ else if (key === "r") {
1136
+ state.mode = "result";
1137
+ }
1138
+ else if (key === "q" || key === "\x03") {
1139
+ W(c.showCursor, "\n");
1140
+ process.exit(0);
1141
+ }
1142
+ break;
1143
+ case "result":
1144
+ if (key === "\x1b[D" || key === "\x1b" || key === "b") {
1145
+ state.mode = "overview";
1146
+ }
1147
+ else if (key === "q" || key === "\x03") {
1148
+ W(c.showCursor, "\n");
1149
+ process.exit(0);
1150
+ }
1151
+ break;
1152
+ case "subqueries": {
1153
+ const sqCount = traj.iterations[state.iterIdx]?.subQueries.length ?? 0;
1154
+ if (key === "\x1b[A") {
1155
+ state.subQueryIdx = Math.max(0, state.subQueryIdx - 1);
1156
+ }
1157
+ else if (key === "\x1b[B") {
1158
+ state.subQueryIdx = Math.min(sqCount - 1, state.subQueryIdx + 1);
1159
+ }
1160
+ else if (key === "\r" || key === "\n" || key === "\x1b[C") {
1161
+ state.mode = "subqueryDetail";
1162
+ state.scrollY = 0;
1163
+ }
1164
+ else if (key === "\x1b[D" || key === "\x1b" || key === "b") {
1165
+ state.mode = "iteration";
1166
+ }
1167
+ else if (key === "q" || key === "\x03") {
1168
+ W(c.showCursor, "\n");
1169
+ process.exit(0);
1170
+ }
1171
+ break;
1172
+ }
1173
+ case "subqueryDetail": {
1174
+ const sqMax = (traj.iterations[state.iterIdx]?.subQueries.length ?? 1) - 1;
1175
+ if (key === "\x1b[A") {
1176
+ state.scrollY = Math.max(0, state.scrollY - 3);
1177
+ }
1178
+ else if (key === "\x1b[B") {
1179
+ state.scrollY += 3;
1180
+ }
1181
+ else if (key === "n" || key === "\x1b[C") {
1182
+ if (state.subQueryIdx < sqMax) {
1183
+ state.subQueryIdx++;
1184
+ state.scrollY = 0;
1185
+ }
1186
+ }
1187
+ else if (key === "p" || key === "N") {
1188
+ if (state.subQueryIdx > 0) {
1189
+ state.subQueryIdx--;
1190
+ state.scrollY = 0;
1191
+ }
1192
+ }
1193
+ else if (key === "\x1b[D" || key === "\x1b" || key === "b") {
1194
+ state.mode = "subqueries";
1195
+ state.scrollY = 0;
1196
+ }
1197
+ else if (key === "q" || key === "\x03") {
1198
+ W(c.showCursor, "\n");
1199
+ process.exit(0);
1200
+ }
1201
+ break;
1202
+ }
1203
+ case "llmInput":
1204
+ case "llmResponse":
1205
+ case "systemPrompt":
1206
+ if (key === "\x1b[D" || key === "\x1b" || key === "b") {
1207
+ state.mode = "iteration";
1208
+ }
1209
+ else if (key === "q" || key === "\x03") {
1210
+ W(c.showCursor, "\n");
1211
+ process.exit(0);
1212
+ }
1213
+ break;
1214
+ case "swarm": {
1215
+ const swarmThreadCount = traj.swarm ? getFlatThreadList(traj.swarm).length : 0;
1216
+ if (key === "\x1b[A") {
1217
+ state.swarmThreadIdx = Math.max(0, state.swarmThreadIdx - 1);
1218
+ }
1219
+ else if (key === "\x1b[B") {
1220
+ state.swarmThreadIdx = Math.min(swarmThreadCount - 1, state.swarmThreadIdx + 1);
1221
+ }
1222
+ else if (key === "\r" || key === "\n" || key === "\x1b[C") {
1223
+ if (swarmThreadCount > 0) {
1224
+ state.mode = "swarmThreadDetail";
1225
+ state.scrollY = 0;
1226
+ }
1227
+ }
1228
+ else if (key === "c") {
1229
+ state.showCostBreakdown = !state.showCostBreakdown;
1230
+ }
1231
+ else if (key === "m") {
1232
+ // Scroll to merge events section (jump selection to last thread)
1233
+ state.swarmThreadIdx = Math.max(0, swarmThreadCount - 1);
1234
+ }
1235
+ else if (key === "\x1b[D" || key === "\x1b" || key === "b") {
1236
+ state.mode = "overview";
1237
+ }
1238
+ else if (key === "q" || key === "\x03") {
1239
+ W(c.showCursor, "\n");
1240
+ process.exit(0);
1241
+ }
1242
+ break;
1243
+ }
1244
+ case "swarmThreadDetail": {
1245
+ const stMax = traj.swarm ? getFlatThreadList(traj.swarm).length - 1 : 0;
1246
+ if (key === "\x1b[A") {
1247
+ state.scrollY = Math.max(0, state.scrollY - 3);
1248
+ }
1249
+ else if (key === "\x1b[B") {
1250
+ state.scrollY += 3;
1251
+ }
1252
+ else if (key === "n" || key === "\x1b[C") {
1253
+ if (state.swarmThreadIdx < stMax) {
1254
+ state.swarmThreadIdx++;
1255
+ state.scrollY = 0;
1256
+ }
1257
+ }
1258
+ else if (key === "N") {
1259
+ if (state.swarmThreadIdx > 0) {
1260
+ state.swarmThreadIdx--;
1261
+ state.scrollY = 0;
1262
+ }
1263
+ }
1264
+ else if (key === "\x1b[D" || key === "\x1b" || key === "b") {
1265
+ state.mode = "swarm";
1266
+ state.scrollY = 0;
1267
+ }
1268
+ else if (key === "q" || key === "\x03") {
1269
+ W(c.showCursor, "\n");
1270
+ process.exit(0);
1271
+ }
1272
+ break;
1273
+ }
1274
+ }
1275
+ render();
1276
+ });
1277
+ // (cleanup handler already registered at top of main)
1278
+ }
1279
+ main().catch((err) => {
1280
+ W(c.showCursor, c.altScreenOff);
1281
+ console.error(`Fatal: ${err}`);
1282
+ process.exit(1);
1283
+ });
1284
+ //# sourceMappingURL=viewer.js.map