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.
- package/LICENSE +21 -0
- package/README.md +384 -0
- package/bin/swarm.mjs +45 -0
- package/dist/agents/aider.d.ts +12 -0
- package/dist/agents/aider.js +182 -0
- package/dist/agents/claude-code.d.ts +9 -0
- package/dist/agents/claude-code.js +216 -0
- package/dist/agents/codex.d.ts +14 -0
- package/dist/agents/codex.js +193 -0
- package/dist/agents/direct-llm.d.ts +9 -0
- package/dist/agents/direct-llm.js +78 -0
- package/dist/agents/mock.d.ts +9 -0
- package/dist/agents/mock.js +77 -0
- package/dist/agents/opencode.d.ts +23 -0
- package/dist/agents/opencode.js +571 -0
- package/dist/agents/provider.d.ts +11 -0
- package/dist/agents/provider.js +31 -0
- package/dist/cli.d.ts +15 -0
- package/dist/cli.js +285 -0
- package/dist/compression/compressor.d.ts +28 -0
- package/dist/compression/compressor.js +265 -0
- package/dist/config.d.ts +42 -0
- package/dist/config.js +170 -0
- package/dist/core/repl.d.ts +69 -0
- package/dist/core/repl.js +336 -0
- package/dist/core/rlm.d.ts +63 -0
- package/dist/core/rlm.js +409 -0
- package/dist/core/runtime.py +335 -0
- package/dist/core/types.d.ts +131 -0
- package/dist/core/types.js +19 -0
- package/dist/env.d.ts +10 -0
- package/dist/env.js +75 -0
- package/dist/interactive-swarm.d.ts +20 -0
- package/dist/interactive-swarm.js +1041 -0
- package/dist/interactive.d.ts +10 -0
- package/dist/interactive.js +1765 -0
- package/dist/main.d.ts +15 -0
- package/dist/main.js +242 -0
- package/dist/mcp/server.d.ts +15 -0
- package/dist/mcp/server.js +72 -0
- package/dist/mcp/session.d.ts +73 -0
- package/dist/mcp/session.js +184 -0
- package/dist/mcp/tools.d.ts +15 -0
- package/dist/mcp/tools.js +377 -0
- package/dist/memory/episodic.d.ts +132 -0
- package/dist/memory/episodic.js +390 -0
- package/dist/prompts/orchestrator.d.ts +5 -0
- package/dist/prompts/orchestrator.js +191 -0
- package/dist/routing/model-router.d.ts +130 -0
- package/dist/routing/model-router.js +515 -0
- package/dist/swarm.d.ts +14 -0
- package/dist/swarm.js +557 -0
- package/dist/threads/cache.d.ts +58 -0
- package/dist/threads/cache.js +198 -0
- package/dist/threads/manager.d.ts +85 -0
- package/dist/threads/manager.js +659 -0
- package/dist/ui/banner.d.ts +14 -0
- package/dist/ui/banner.js +42 -0
- package/dist/ui/dashboard.d.ts +33 -0
- package/dist/ui/dashboard.js +151 -0
- package/dist/ui/index.d.ts +10 -0
- package/dist/ui/index.js +11 -0
- package/dist/ui/log.d.ts +39 -0
- package/dist/ui/log.js +126 -0
- package/dist/ui/onboarding.d.ts +14 -0
- package/dist/ui/onboarding.js +518 -0
- package/dist/ui/spinner.d.ts +25 -0
- package/dist/ui/spinner.js +113 -0
- package/dist/ui/summary.d.ts +18 -0
- package/dist/ui/summary.js +113 -0
- package/dist/ui/theme.d.ts +63 -0
- package/dist/ui/theme.js +97 -0
- package/dist/viewer.d.ts +12 -0
- package/dist/viewer.js +1284 -0
- package/dist/worktree/manager.d.ts +45 -0
- package/dist/worktree/manager.js +266 -0
- package/dist/worktree/merge.d.ts +28 -0
- package/dist/worktree/merge.js +138 -0
- 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
|