@multiclawcli/dashboard 0.3.2 → 0.5.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 (39) hide show
  1. package/README.md +66 -2
  2. package/dist/tui/App.d.ts +10 -0
  3. package/dist/tui/commands.d.ts +122 -0
  4. package/dist/tui/components/chrome.d.ts +58 -0
  5. package/dist/tui/components/overlays.d.ts +64 -0
  6. package/dist/tui/components/panels.d.ts +72 -0
  7. package/dist/tui/components/prompt.d.ts +36 -0
  8. package/dist/tui/format.d.ts +65 -0
  9. package/dist/tui/history.d.ts +34 -0
  10. package/dist/tui/index.d.ts +17 -0
  11. package/dist/tui/layout.d.ts +87 -0
  12. package/dist/tui/state.d.ts +292 -0
  13. package/dist/tui/theme.d.ts +66 -0
  14. package/dist/tui.js +2076 -0
  15. package/package.json +20 -28
  16. package/components.json +0 -16
  17. package/index.html +0 -12
  18. package/postcss.config.js +0 -3
  19. package/src/client/App.tsx +0 -96
  20. package/src/client/components/AgentCard.tsx +0 -60
  21. package/src/client/components/LogPanel.tsx +0 -43
  22. package/src/client/components/PipelineBar.tsx +0 -234
  23. package/src/client/components/ui/badge.tsx +0 -28
  24. package/src/client/components/ui/button.tsx +0 -43
  25. package/src/client/components/ui/card.tsx +0 -22
  26. package/src/client/components/ui/scroll-area.tsx +0 -43
  27. package/src/client/components/ui/separator.tsx +0 -23
  28. package/src/client/hooks/useWebSocket.ts +0 -33
  29. package/src/client/index.css +0 -31
  30. package/src/client/lib/utils.ts +0 -6
  31. package/src/client/main.tsx +0 -8
  32. package/src/client/store/useOrchestrationStore.ts +0 -132
  33. package/src/server/DashboardServer.ts +0 -56
  34. package/src/server/WebSocketHub.ts +0 -35
  35. package/src/server/index.ts +0 -2
  36. package/tailwind.config.js +0 -50
  37. package/tsconfig.json +0 -12
  38. package/tsconfig.server.json +0 -9
  39. package/vite.config.ts +0 -25
package/dist/tui.js ADDED
@@ -0,0 +1,2076 @@
1
+ // src/tui/index.ts
2
+ import { render } from "ink";
3
+ import { createElement } from "react";
4
+
5
+ // src/tui/App.tsx
6
+ import * as path2 from "path";
7
+ import { describeResumePlan, isAbortError, Orchestrator } from "@multiclawcli/core";
8
+ import { Box as Box5, Text as Text5, useApp, useInput, useWindowSize } from "ink";
9
+ import { useCallback, useEffect, useMemo, useReducer, useRef, useState } from "react";
10
+
11
+ // src/tui/commands.ts
12
+ var COMMANDS = [
13
+ { name: "agents", description: "List the configured team", availability: "always" },
14
+ {
15
+ name: "focus",
16
+ args: "<id>",
17
+ description: "Focus an agent by id",
18
+ availability: "always",
19
+ completes: "agent"
20
+ },
21
+ { name: "clear", description: "Clear the focused agent's log", availability: "always" },
22
+ {
23
+ name: "history",
24
+ description: "Browse finished turns",
25
+ availability: "always"
26
+ },
27
+ {
28
+ name: "verbose",
29
+ description: "Toggle tool detail in the stream",
30
+ availability: "always"
31
+ },
32
+ {
33
+ name: "retry",
34
+ args: "[id]",
35
+ description: "Re-run a failed agent (or all of them)",
36
+ availability: "idle"
37
+ },
38
+ {
39
+ name: "replay",
40
+ args: "[turn]",
41
+ description: "Run an earlier turn again",
42
+ availability: "idle"
43
+ },
44
+ {
45
+ name: "fork",
46
+ args: "[turn]",
47
+ description: "Edit an earlier turn's requirement",
48
+ availability: "always"
49
+ },
50
+ {
51
+ name: "export",
52
+ args: "[turn] [path]",
53
+ description: "Export a finished turn as Markdown",
54
+ availability: "always"
55
+ },
56
+ {
57
+ name: "search",
58
+ args: "<text>",
59
+ description: "Search the focused stream",
60
+ availability: "always"
61
+ },
62
+ {
63
+ name: "latest",
64
+ description: "Jump the stream to latest output",
65
+ availability: "always"
66
+ },
67
+ {
68
+ name: "cancel",
69
+ description: "Cancel the running turn and its queue",
70
+ availability: "running"
71
+ },
72
+ { name: "help", description: "Show the key map", availability: "always" },
73
+ { name: "quit", description: "Exit the console", availability: "always" }
74
+ ];
75
+ function availableCommands(running) {
76
+ const phase = running ? "running" : "idle";
77
+ return COMMANDS.filter((c) => c.availability === "always" || c.availability === phase);
78
+ }
79
+ function canRun(name, running) {
80
+ const command = COMMANDS.find((c) => c.name === name);
81
+ if (!command) return true;
82
+ return availableCommands(running).includes(command);
83
+ }
84
+ function unavailableReason(name, running) {
85
+ const command = COMMANDS.find((c) => c.name === name);
86
+ if (!command || availableCommands(running).includes(command)) return void 0;
87
+ return running ? `/${name} waits for the current turn to finish` : `/${name} only applies while a turn is running`;
88
+ }
89
+ function matchCommands(query, running) {
90
+ const q = query.trim().toLowerCase();
91
+ const pool = availableCommands(running);
92
+ if (!q) return pool;
93
+ const byName = [];
94
+ const byDescription = [];
95
+ for (const command of pool) {
96
+ if (command.name.startsWith(q)) byName.push(command);
97
+ else if (command.description.toLowerCase().includes(q)) byDescription.push(command);
98
+ }
99
+ return [...byName, ...byDescription];
100
+ }
101
+ function isCommandLine(text) {
102
+ return text.startsWith("/");
103
+ }
104
+ function menuQuery(text) {
105
+ if (!isCommandLine(text)) return void 0;
106
+ const rest = text.slice(1);
107
+ return /\s/.test(rest) ? void 0 : rest;
108
+ }
109
+ function parseCommand(text) {
110
+ if (!isCommandLine(text)) return void 0;
111
+ const [name = "", ...args] = text.slice(1).trim().split(/\s+/);
112
+ return name ? { name, args } : void 0;
113
+ }
114
+ function paletteRows(commands) {
115
+ return commands.map((command) => ({
116
+ key: command.name,
117
+ label: command.args ? `/${command.name} ${command.args}` : `/${command.name}`,
118
+ description: command.description
119
+ }));
120
+ }
121
+ function argQuery(text) {
122
+ if (!isCommandLine(text)) return void 0;
123
+ const match = /^(\S+)\s+(\S*)$/.exec(text);
124
+ if (!match) return void 0;
125
+ const command = COMMANDS.find((c) => c.name === match[1].slice(1));
126
+ if (!command?.completes) return void 0;
127
+ return { command, partial: match[2] };
128
+ }
129
+ function completeAgents(partial, ids, limit = 8) {
130
+ const query = partial.toLowerCase();
131
+ const matches = query ? ids.filter((id) => id.toLowerCase().startsWith(query)) : [...ids];
132
+ return matches.slice(0, limit);
133
+ }
134
+ function resolveAgentId(input, ids) {
135
+ if (ids.includes(input)) return { id: input };
136
+ const query = input.toLowerCase();
137
+ const hits = ids.filter((id) => id.toLowerCase().startsWith(query));
138
+ if (hits.length === 1) return { id: hits[0] };
139
+ if (hits.length === 0) return { error: `unknown agent: ${input}` };
140
+ return { error: `ambiguous: ${hits.join(", ")}` };
141
+ }
142
+
143
+ // src/tui/components/chrome.tsx
144
+ import { Box, Text } from "ink";
145
+
146
+ // src/tui/format.ts
147
+ import stringWidth from "string-width";
148
+ function truncate(text, max) {
149
+ if (max <= 0) return "";
150
+ if (text.length <= max) return text;
151
+ return `${text.slice(0, Math.max(0, max - 1))}\u2026`;
152
+ }
153
+ function displayWidth(text) {
154
+ return stringWidth(text);
155
+ }
156
+ function wrapLine(line, width) {
157
+ const limit = Math.max(1, width);
158
+ if (line === "") return [""];
159
+ const rows = [];
160
+ let row = "";
161
+ let empty = true;
162
+ const flush = () => {
163
+ if (!empty) rows.push(row);
164
+ row = "";
165
+ empty = true;
166
+ };
167
+ const overflow = (word) => {
168
+ const chunks = splitByWidth(word, limit);
169
+ for (const chunk of chunks.slice(0, -1)) rows.push(chunk);
170
+ row = chunks[chunks.length - 1];
171
+ empty = row === "";
172
+ };
173
+ for (const word of line.split(" ")) {
174
+ const candidate = empty ? word : `${row} ${word}`;
175
+ if (displayWidth(candidate) <= limit) {
176
+ row = candidate;
177
+ empty = false;
178
+ continue;
179
+ }
180
+ const head = breakableHead(empty ? "" : row, word, limit);
181
+ if (head === 0) {
182
+ flush();
183
+ overflow(word);
184
+ continue;
185
+ }
186
+ rows.push(empty ? word.slice(0, head) : `${row} ${word.slice(0, head)}`);
187
+ overflow(word.slice(head));
188
+ }
189
+ flush();
190
+ return rows;
191
+ }
192
+ var CLOSERS = "\u3002\uFF0C\u3001\uFF1B\uFF1A\uFF1F\uFF01\uFF09\uFF3D\uFF5D\u300D\u300F\u3011\u300B\u3009\u201D\u2019\u2026\u2014\xB7";
193
+ function breakableHead(row, word, limit) {
194
+ const chars = [...word];
195
+ let used = row === "" ? 0 : displayWidth(row) + 1;
196
+ let head = 0;
197
+ for (let i = 0; i < chars.length; i++) {
198
+ const charWidth = displayWidth(chars[i]);
199
+ if (used + charWidth > limit) break;
200
+ used += charWidth;
201
+ const next = chars[i + 1];
202
+ if (next !== void 0 && CLOSERS.includes(next)) continue;
203
+ if (charWidth >= 2 || next !== void 0 && displayWidth(next) >= 2) head = i + 1;
204
+ }
205
+ return head;
206
+ }
207
+ function splitByWidth(text, width) {
208
+ const chunks = [];
209
+ let chunk = "";
210
+ let used = 0;
211
+ for (const char of text) {
212
+ const charWidth = displayWidth(char);
213
+ if (chunk !== "" && used + charWidth > width) {
214
+ chunks.push(chunk);
215
+ chunk = char;
216
+ used = charWidth;
217
+ } else {
218
+ chunk += char;
219
+ used += charWidth;
220
+ }
221
+ }
222
+ chunks.push(chunk);
223
+ return chunks;
224
+ }
225
+ function formatDuration(ms) {
226
+ if (!Number.isFinite(ms) || ms < 0) return "\u2014";
227
+ if (ms < 6e4) return `${(ms / 1e3).toFixed(1)}s`;
228
+ const totalSeconds = Math.round(ms / 1e3);
229
+ const minutes = Math.floor(totalSeconds / 60);
230
+ if (minutes < 60) return `${minutes}m ${String(totalSeconds % 60).padStart(2, "0")}s`;
231
+ const hours = Math.floor(minutes / 60);
232
+ return `${hours}h ${String(minutes % 60).padStart(2, "0")}m`;
233
+ }
234
+ var CIRCLED = "\u2460\u2461\u2462\u2463\u2464\u2465\u2466\u2467\u2468\u2469\u246A\u246B\u246C\u246D\u246E\u246F\u2470\u2471\u2472\u2473";
235
+ function circled(n) {
236
+ return CIRCLED[n - 1] ?? `(${n})`;
237
+ }
238
+ function padRight(text, width) {
239
+ return text.length >= width ? text : text + " ".repeat(width - text.length);
240
+ }
241
+ function oneDecimal(value) {
242
+ const fixed = value.toFixed(1);
243
+ return fixed.endsWith(".0") ? fixed.slice(0, -2) : fixed;
244
+ }
245
+ function formatTokens(n) {
246
+ if (!Number.isFinite(n) || n < 0) return "\u2014";
247
+ if (n < 1e3) return String(Math.round(n));
248
+ if (n < 1e6) return `${oneDecimal(n / 1e3)}k`;
249
+ return `${oneDecimal(n / 1e6)}M`;
250
+ }
251
+ function formatCost(usd) {
252
+ if (!Number.isFinite(usd) || usd < 0) return "\u2014";
253
+ if (usd === 0) return "$0.00";
254
+ if (usd < 0.01) return "<$0.01";
255
+ return `$${usd.toFixed(2)}`;
256
+ }
257
+
258
+ // src/tui/layout.ts
259
+ var MIN_COLUMNS = 40;
260
+ var MIN_ROWS = 10;
261
+ var COMPACT_COLUMNS = 72;
262
+ var RAIL_MIN_ROWS = 16;
263
+ var MAX_RAIL_ROWS = 2;
264
+ var MAX_MENU_ROWS = 7;
265
+ var MAX_QUEUE_ROWS = 3;
266
+ var MIN_BODY_ROWS = 3;
267
+ function measureRailRows(cells, columns) {
268
+ if (!cells || cells.length === 0) return 0;
269
+ if (columns <= 0) return MAX_RAIL_ROWS;
270
+ let rows = 1;
271
+ let used = 0;
272
+ for (const cell of cells) {
273
+ if (used > 0 && used + cell > columns) {
274
+ rows += 1;
275
+ if (rows >= MAX_RAIL_ROWS) return MAX_RAIL_ROWS;
276
+ used = cell;
277
+ } else {
278
+ used += cell;
279
+ }
280
+ }
281
+ return rows;
282
+ }
283
+ function computeLayout(input) {
284
+ const columns = Math.max(MIN_COLUMNS, input.columns);
285
+ const rows = Math.max(MIN_ROWS, input.rows);
286
+ const compact = columns < COMPACT_COLUMNS;
287
+ const welcome = !input.overlay && input.turn === 0;
288
+ const showRail = !welcome && !input.overlay && rows >= RAIL_MIN_ROWS && input.hasRail;
289
+ const menuRows = Math.min(Math.max(0, input.menuRows), MAX_MENU_ROWS);
290
+ const queueRows = Math.min(Math.max(0, input.queueRows ?? 0), MAX_QUEUE_ROWS);
291
+ const railRows = showRail ? Math.min(MAX_RAIL_ROWS, Math.max(1, measureRailRows(input.railCells, columns))) : 0;
292
+ const reserved = 1 + // header
293
+ 1 + // rule under the header
294
+ (railRows > 0 ? railRows + 1 : 0) + // pipeline rail plus the gap above it
295
+ 1 + // gap before the body
296
+ (input.notice ? 1 : 0) + (input.viewing ? 1 : 0) + // "viewing turn N" banner
297
+ queueRows + menuRows + 1 + // prompt
298
+ 1;
299
+ const bodyHeight = Math.max(MIN_BODY_ROWS, rows - reserved);
300
+ const showTeam = !compact;
301
+ const teamWidth = Math.min(30, Math.max(18, Math.floor(columns * 0.24)));
302
+ const streamChrome = (showTeam ? teamWidth + 1 : 0) + 1;
303
+ const streamWidth = Math.max(1, columns - streamChrome - 1);
304
+ return {
305
+ columns,
306
+ rows,
307
+ compact,
308
+ showTeam,
309
+ showRail,
310
+ railRows,
311
+ welcome,
312
+ bodyHeight,
313
+ // The stream spends one of the body's rows on its own header.
314
+ logViewport: Math.max(1, bodyHeight - 1),
315
+ teamWidth,
316
+ streamWidth,
317
+ menuRows,
318
+ queueRows
319
+ };
320
+ }
321
+
322
+ // src/tui/theme.ts
323
+ var COLOR = {
324
+ accent: "cyan",
325
+ ok: "green",
326
+ warn: "yellow",
327
+ error: "red",
328
+ muted: "gray"
329
+ };
330
+ var STATUS = {
331
+ pending: { glyph: "\u25CB", color: COLOR.muted, dim: true, label: "queued" },
332
+ running: { glyph: "\u25D0", color: COLOR.accent, label: "running" },
333
+ retrying: { glyph: "\u21BB", color: COLOR.warn, label: "retrying" },
334
+ success: { glyph: "\u2713", color: COLOR.ok, label: "done" },
335
+ failed: { glyph: "\u2717", color: COLOR.error, label: "failed" },
336
+ // Stopped, not broken: a filled square reads as "halted" and shares the muted
337
+ // treatment of a skip, so a Ctrl-C does not paint the roster red.
338
+ cancelled: { glyph: "\u25A0", color: COLOR.muted, dim: true, label: "cancelled" },
339
+ skipped: { glyph: "\u2298", color: COLOR.muted, dim: true, label: "skipped" }
340
+ };
341
+ var SPINNER = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
342
+ var GLYPH = {
343
+ /** Marks the console itself in the header and the welcome screen. */
344
+ brand: "\u25C6",
345
+ /** The leader runs before the pipeline exists, so it gets its own mark. */
346
+ leader: "\u25C6",
347
+ agent: "\u25C7",
348
+ /** Points at the focused row in the team list. */
349
+ caret: "\u203A",
350
+ /** Pipeline flow between stages. */
351
+ connector: "\u2500\u2500\u25B8",
352
+ /** The stream is tailing; new output appears as it arrives. */
353
+ live: "\u25BC",
354
+ /** The stream is parked while the user reads back. */
355
+ paused: "\u23F8",
356
+ /**
357
+ * A follow-up waiting for the team to finish.
358
+ *
359
+ * Latin-1 on purpose: it renders as exactly one cell in every terminal, which
360
+ * a pictograph like ⏳ does not.
361
+ */
362
+ queued: "\xBB",
363
+ /** The leader is asking for a decision rather than reporting one. */
364
+ ask: "?",
365
+ /** A mid-turn warning that rides alongside the outcome rather than hiding it. */
366
+ warn: "\u26A0",
367
+ /**
368
+ * A pasted line break, drawn in the one-line prompt.
369
+ *
370
+ * MUST be exactly one column wide. `Prompt` maps `\n` to this glyph 1:1 — one
371
+ * character in, one character out — so its caret index stays valid; a glyph
372
+ * that rendered as two columns would push the caret off by one for every
373
+ * newline before it. If a terminal disagrees, this single glyph is the fix.
374
+ */
375
+ newline: "\u21B5"
376
+ };
377
+
378
+ // src/tui/components/chrome.tsx
379
+ import { jsx, jsxs } from "react/jsx-runtime";
380
+ function Header({ name, right }) {
381
+ return /* @__PURE__ */ jsxs(Box, { justifyContent: "space-between", flexShrink: 0, children: [
382
+ /* @__PURE__ */ jsx(Box, { flexShrink: 1, children: /* @__PURE__ */ jsxs(Text, { wrap: "truncate-end", children: [
383
+ /* @__PURE__ */ jsxs(Text, { color: COLOR.accent, bold: true, children: [
384
+ GLYPH.brand,
385
+ " multiclaw"
386
+ ] }),
387
+ name ? /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
388
+ " \xB7 ",
389
+ name
390
+ ] }) : null
391
+ ] }) }),
392
+ /* @__PURE__ */ jsx(Box, { flexShrink: 0, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: right }) })
393
+ ] });
394
+ }
395
+ function Rule({ width }) {
396
+ return /* @__PURE__ */ jsx(Box, { flexShrink: 0, children: /* @__PURE__ */ jsx(Text, { color: COLOR.muted, dimColor: true, children: "\u2500".repeat(Math.max(0, width)) }) });
397
+ }
398
+ function Chip({ agent, fallbackName, spinner }) {
399
+ const status = agent?.status ?? "pending";
400
+ const style = STATUS[status];
401
+ const glyph = status === "running" ? spinner : style.glyph;
402
+ return /* @__PURE__ */ jsx(Box, { marginRight: 1, children: /* @__PURE__ */ jsxs(Text, { color: style.color, dimColor: style.dim, children: [
403
+ glyph,
404
+ " ",
405
+ agent?.icon ? `${agent.icon} ` : "",
406
+ agent?.name ?? fallbackName
407
+ ] }) });
408
+ }
409
+ function railCellWidths(turn) {
410
+ const chip = (name, icon) => `G ${icon ? `${icon} ` : ""}${name}`.length + 1;
411
+ const cells = [];
412
+ for (const id of turn.leaderIds) {
413
+ const agent = turn.agents[id];
414
+ cells.push(chip(agent?.name ?? id, agent?.icon));
415
+ }
416
+ turn.stages.forEach((stage, index) => {
417
+ cells.push(` ${GLYPH.connector} `.length, `${circled(index + 1)} `.length);
418
+ for (const agent of stage.agents) {
419
+ cells.push(chip(agent.name, agent.icon));
420
+ }
421
+ });
422
+ return cells;
423
+ }
424
+ function PipelineRail({ turn, spinner }) {
425
+ if (turn.leaderIds.length === 0 && turn.stages.length === 0) return null;
426
+ const items = [];
427
+ for (const id of turn.leaderIds) {
428
+ items.push(
429
+ /* @__PURE__ */ jsx(Chip, { agent: turn.agents[id], fallbackName: id, spinner }, `leader-${id}`)
430
+ );
431
+ }
432
+ turn.stages.forEach((stage, index) => {
433
+ items.push(
434
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: ` ${GLYPH.connector} ` }, `flow-${stage.stageIndex}`),
435
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
436
+ circled(index + 1),
437
+ " "
438
+ ] }, `stage-${stage.stageIndex}`)
439
+ );
440
+ for (const agent of stage.agents) {
441
+ items.push(
442
+ /* @__PURE__ */ jsx(
443
+ Chip,
444
+ {
445
+ agent: turn.agents[agent.id],
446
+ fallbackName: agent.name,
447
+ spinner
448
+ },
449
+ agent.id
450
+ )
451
+ );
452
+ }
453
+ });
454
+ return /* @__PURE__ */ jsx(Box, { flexWrap: "wrap", children: items });
455
+ }
456
+ function HintBar({ status, keys }) {
457
+ return /* @__PURE__ */ jsxs(Box, { justifyContent: "space-between", flexShrink: 0, children: [
458
+ /* @__PURE__ */ jsx(Box, { flexShrink: 1, children: status }),
459
+ /* @__PURE__ */ jsx(Box, { flexShrink: 0, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: keys }) })
460
+ ] });
461
+ }
462
+ function QueueStrip({ items, max = MAX_QUEUE_ROWS }) {
463
+ if (items.length === 0 || max <= 0) return null;
464
+ const shown = items.slice(0, max);
465
+ const hidden = items.length - shown.length;
466
+ return /* @__PURE__ */ jsx(Box, { flexDirection: "column", flexShrink: 0, children: shown.map((text, index) => (
467
+ // biome-ignore lint/suspicious/noArrayIndexKey: a queue may hold the same sentence twice, so position is what tells the rows apart
468
+ /* @__PURE__ */ jsxs(Text, { wrap: "truncate-end", children: [
469
+ /* @__PURE__ */ jsx(Text, { color: COLOR.warn, children: index === 0 ? `${GLYPH.queued} ` : " " }),
470
+ /* @__PURE__ */ jsx(Text, { dimColor: index > 0, children: text }),
471
+ hidden > 0 && index === shown.length - 1 ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: ` (+${hidden} more)` }) : null
472
+ ] }, `${index}-${text}`)
473
+ )) });
474
+ }
475
+
476
+ // src/tui/components/overlays.tsx
477
+ import { Box as Box2, Text as Text2 } from "ink";
478
+
479
+ // src/tui/state.ts
480
+ var MAX_LOG_CHARS = 8e3;
481
+ var ANSI_PATTERN = /\x1b\[[0-9;]*[a-zA-Z]/g;
482
+ var MAX_HISTORY = 20;
483
+ var initialState = {
484
+ phase: "idle",
485
+ name: "",
486
+ turn: 0,
487
+ stages: [],
488
+ agents: {},
489
+ leaderIds: [],
490
+ autoFollow: true,
491
+ scroll: 0,
492
+ history: [],
493
+ verbose: false
494
+ };
495
+ function stripAnsi(text) {
496
+ return text.replace(ANSI_PATTERN, "");
497
+ }
498
+ function appendLog(previous, chunk) {
499
+ const next = previous + stripAnsi(chunk);
500
+ return next.length > MAX_LOG_CHARS ? next.slice(next.length - MAX_LOG_CHARS) : next;
501
+ }
502
+ function blankAgent(id, name, patch = {}) {
503
+ return { id, name, status: "pending", logs: "", ...patch };
504
+ }
505
+ function epoch(iso) {
506
+ const value = Date.parse(iso);
507
+ return Number.isNaN(value) ? void 0 : value;
508
+ }
509
+ function agentModel(agent) {
510
+ return agent.model ?? agent.models?.[agent.runtime ?? "acp"];
511
+ }
512
+ function activeLeader(config) {
513
+ return config.useLeader === false ? void 0 : config.leader;
514
+ }
515
+ function plannedAgents(config) {
516
+ const leader = activeLeader(config);
517
+ return leader ? [leader, ...config.agents] : config.agents;
518
+ }
519
+ function leaderAgentIds(config) {
520
+ const leader = activeLeader(config);
521
+ return leader ? [leader.id] : [];
522
+ }
523
+ function seedAgents(config) {
524
+ const agents = {};
525
+ for (const agent of plannedAgents(config)) {
526
+ agents[agent.id] = blankAgent(agent.id, agent.name, {
527
+ icon: agent.icon,
528
+ model: agentModel(agent),
529
+ description: agent.description,
530
+ taskTitle: agent.taskTitle
531
+ });
532
+ }
533
+ return agents;
534
+ }
535
+ function createInitialState(config, history = []) {
536
+ const agents = seedAgents(config);
537
+ return {
538
+ phase: "idle",
539
+ name: config.name,
540
+ turn: history[history.length - 1]?.turn ?? 0,
541
+ stages: [],
542
+ leaderIds: leaderAgentIds(config),
543
+ agents,
544
+ focusId: Object.keys(agents)[0],
545
+ autoFollow: true,
546
+ scroll: 0,
547
+ history,
548
+ verbose: false
549
+ };
550
+ }
551
+ function mergeStageAgents(agents, stages) {
552
+ const next = { ...agents };
553
+ for (const stage of stages) {
554
+ for (const a of stage.agents) {
555
+ const existing = next[a.id];
556
+ next[a.id] = existing ? {
557
+ ...existing,
558
+ name: a.name,
559
+ icon: a.icon,
560
+ model: a.model,
561
+ // The event's `description` is absent for a hand-rolled agent
562
+ // definition, so fall back rather than blanking a known role.
563
+ description: a.description ?? existing.description,
564
+ taskTitle: a.taskTitle
565
+ } : blankAgent(a.id, a.name, {
566
+ icon: a.icon,
567
+ model: a.model,
568
+ description: a.description,
569
+ taskTitle: a.taskTitle
570
+ });
571
+ }
572
+ }
573
+ return next;
574
+ }
575
+ function reduceEvent(state, event) {
576
+ switch (event.type) {
577
+ case "orchestration:start": {
578
+ return {
579
+ ...state,
580
+ phase: "running",
581
+ name: event.payload.name,
582
+ requirement: event.payload.requirement ?? state.requirement,
583
+ reason: event.payload.selectionReason,
584
+ stages: event.payload.stages,
585
+ agents: mergeStageAgents(state.agents, event.payload.stages)
586
+ };
587
+ }
588
+ // The architect's task plan is applied *after* the first stage, which can
589
+ // regroup the team. The console drew the pre-plan grouping from
590
+ // `orchestration:start`; this is how it learns the grouping that ran.
591
+ case "orchestration:stages": {
592
+ return {
593
+ ...state,
594
+ stages: event.payload.stages,
595
+ agents: mergeStageAgents(state.agents, event.payload.stages)
596
+ };
597
+ }
598
+ // Stage boundaries carry no state of their own — the per-agent statuses
599
+ // derived from them already drive the pipeline bar.
600
+ case "stage:start":
601
+ case "stage:complete":
602
+ return state;
603
+ case "agent:start": {
604
+ const { agentId, agentName, icon, model, taskTitle } = event.payload;
605
+ const existing = state.agents[agentId];
606
+ const startedAt = epoch(event.timestamp);
607
+ const next = existing ? {
608
+ ...existing,
609
+ name: agentName,
610
+ icon: icon ?? existing.icon,
611
+ model: model ?? existing.model,
612
+ taskTitle: taskTitle ?? existing.taskTitle,
613
+ status: "running",
614
+ startedAt: startedAt ?? existing.startedAt
615
+ } : blankAgent(agentId, agentName, {
616
+ icon,
617
+ model,
618
+ taskTitle,
619
+ status: "running",
620
+ startedAt
621
+ });
622
+ const takesFocus = state.autoFollow;
623
+ return {
624
+ ...state,
625
+ agents: { ...state.agents, [agentId]: next },
626
+ focusId: takesFocus ? agentId : state.focusId,
627
+ // A new agent takes the stream with it, so the window must follow.
628
+ scroll: takesFocus ? 0 : state.scroll
629
+ };
630
+ }
631
+ case "agent:output": {
632
+ const { agentId, chunk, kind } = event.payload;
633
+ if (kind === "tool-detail" && !state.verbose) return state;
634
+ const existing = state.agents[agentId];
635
+ if (!existing) return state;
636
+ return {
637
+ ...state,
638
+ agents: {
639
+ ...state.agents,
640
+ [agentId]: { ...existing, logs: appendLog(existing.logs, chunk) }
641
+ }
642
+ };
643
+ }
644
+ case "agent:complete":
645
+ case "agent:failed": {
646
+ const result = event.payload;
647
+ const patch = {
648
+ status: result.status,
649
+ duration: result.duration,
650
+ attempts: result.attempts,
651
+ error: result.error,
652
+ usage: result.usage,
653
+ costUsd: result.costUsd,
654
+ // Present only on the success path; a failed agent has no session to
655
+ // continue, so `/retry` re-runs it cold.
656
+ sessionId: result.sessionId
657
+ };
658
+ const existing = state.agents[result.agentId];
659
+ const next = existing ? { ...existing, ...patch } : blankAgent(result.agentId, result.agentName, patch);
660
+ return { ...state, agents: { ...state.agents, [result.agentId]: next } };
661
+ }
662
+ case "agent:skipped": {
663
+ const { agentId, agentName, icon } = event.payload;
664
+ const existing = state.agents[agentId];
665
+ const next = existing ? { ...existing, status: "skipped" } : blankAgent(agentId, agentName, { icon, status: "skipped" });
666
+ return { ...state, agents: { ...state.agents, [agentId]: next } };
667
+ }
668
+ case "agent:retrying": {
669
+ const { agentId, agentName, attempt, maxAttempts, error } = event.payload;
670
+ const note = `
671
+ [retry ${attempt}/${maxAttempts}] ${error}
672
+ `;
673
+ const existing = state.agents[agentId];
674
+ const retry = { attempt, max: maxAttempts };
675
+ const next = existing ? { ...existing, status: "retrying", retry, logs: appendLog(existing.logs, note) } : blankAgent(agentId, agentName, { status: "retrying", retry, logs: note });
676
+ return { ...state, agents: { ...state.agents, [agentId]: next } };
677
+ }
678
+ case "orchestration:warning":
679
+ return { ...state, warning: event.payload.message };
680
+ case "orchestration:complete":
681
+ return {
682
+ ...state,
683
+ phase: "complete",
684
+ success: event.payload.success,
685
+ totalDuration: event.payload.totalDuration,
686
+ totalCostUsd: event.payload.totalCostUsd,
687
+ endedAt: epoch(event.timestamp) ?? Date.now(),
688
+ reply: event.payload.reply
689
+ };
690
+ default:
691
+ return state;
692
+ }
693
+ }
694
+ function wrapLog(logs, width) {
695
+ return logs.split("\n").flatMap((line) => wrapLine(line, width));
696
+ }
697
+ function focusedRowCount(turn, width) {
698
+ const agent = turn.focusId ? turn.agents[turn.focusId] : void 0;
699
+ return agent?.logs ? wrapLog(agent.logs, width).length : 0;
700
+ }
701
+ function snapshot(state) {
702
+ return {
703
+ turn: state.turn,
704
+ requirement: state.requirement,
705
+ phase: state.phase,
706
+ startedAt: state.startedAt,
707
+ endedAt: state.endedAt,
708
+ stages: state.stages,
709
+ agents: state.agents,
710
+ leaderIds: state.leaderIds,
711
+ focusId: state.focusId,
712
+ autoFollow: state.autoFollow,
713
+ scroll: state.scroll,
714
+ success: state.success,
715
+ totalDuration: state.totalDuration,
716
+ totalCostUsd: state.totalCostUsd,
717
+ reason: state.reason,
718
+ warning: state.warning,
719
+ reply: state.reply
720
+ };
721
+ }
722
+ function mapShown(state, fn) {
723
+ if (state.view) {
724
+ const next2 = fn(state.view);
725
+ return next2 === state.view ? state : { ...state, view: next2 };
726
+ }
727
+ const next = fn(state);
728
+ return next === state ? state : { ...state, ...next };
729
+ }
730
+ function recordedTurns(state) {
731
+ return [...state.history, ...state.phase === "complete" ? [snapshot(state)] : []];
732
+ }
733
+ function conversationTurns(state) {
734
+ const turns = [];
735
+ for (const turn of recordedTurns(state)) {
736
+ if (turn.phase !== "complete" || !turn.requirement) continue;
737
+ turns.push(
738
+ turn.reply ? { requirement: turn.requirement, reply: turn.reply } : { requirement: turn.requirement }
739
+ );
740
+ }
741
+ return turns;
742
+ }
743
+ function tuiReducer(state, action) {
744
+ switch (action.type) {
745
+ case "event":
746
+ return reduceEvent(state, action.event);
747
+ case "turn:start": {
748
+ const ids = Object.keys(action.agents);
749
+ const history = state.requirement !== void 0 ? [...state.history, snapshot(state)].slice(-MAX_HISTORY) : state.history;
750
+ return {
751
+ phase: "running",
752
+ name: state.name,
753
+ turn: state.turn + 1,
754
+ requirement: action.requirement,
755
+ startedAt: action.startedAt,
756
+ stages: [],
757
+ leaderIds: action.leaderIds,
758
+ agents: action.agents,
759
+ focusId: ids[0],
760
+ autoFollow: true,
761
+ scroll: 0,
762
+ history,
763
+ // A new requirement also replaces the previous leader metadata; leaving
764
+ // it up would make the old answer or routing reason look like the
765
+ // response to the new request. Reading a past turn ends for the same
766
+ // reason — the panes belong to the run.
767
+ reason: void 0,
768
+ reply: void 0,
769
+ view: void 0,
770
+ // A console setting, carried across turns rather than reset with them.
771
+ verbose: state.verbose
772
+ };
773
+ }
774
+ case "focus:cycle":
775
+ return mapShown(state, (turn) => {
776
+ const ids = Object.keys(turn.agents);
777
+ if (ids.length === 0) return turn;
778
+ const current = turn.focusId ? ids.indexOf(turn.focusId) : -1;
779
+ const nextIndex = (current + action.delta + ids.length) % ids.length;
780
+ if (nextIndex === current) return turn;
781
+ return { ...turn, focusId: ids[nextIndex], autoFollow: false, scroll: 0 };
782
+ });
783
+ case "focus:set":
784
+ return mapShown(state, (turn) => {
785
+ if (!turn.agents[action.agentId]) return turn;
786
+ if (turn.focusId === action.agentId && !turn.autoFollow && turn.scroll === 0) return turn;
787
+ return { ...turn, focusId: action.agentId, autoFollow: false, scroll: 0 };
788
+ });
789
+ case "stream:scroll":
790
+ return mapShown(state, (turn) => {
791
+ const rows = focusedRowCount(turn, action.width);
792
+ const maxScroll = Math.max(0, rows - Math.max(1, action.viewport));
793
+ const next = Math.min(Math.max(0, turn.scroll + action.delta), maxScroll);
794
+ return next === turn.scroll ? turn : { ...turn, scroll: next };
795
+ });
796
+ case "stream:search":
797
+ return mapShown(state, (turn) => {
798
+ const agent = turn.focusId ? turn.agents[turn.focusId] : void 0;
799
+ if (!agent?.logs || action.query === "") return turn;
800
+ const rows = wrapLog(agent.logs, action.width);
801
+ const needle = action.query.toLowerCase();
802
+ let index = -1;
803
+ for (let i = rows.length - 1; i >= 0; i--) {
804
+ if (rows[i].toLowerCase().includes(needle)) {
805
+ index = i;
806
+ break;
807
+ }
808
+ }
809
+ if (index === -1) return turn;
810
+ const height = Math.max(1, action.viewport);
811
+ const maxScroll = Math.max(0, rows.length - height);
812
+ const scroll = Math.min(Math.max(0, rows.length - 1 - index), maxScroll);
813
+ return scroll === turn.scroll && !turn.autoFollow ? turn : { ...turn, scroll, autoFollow: false };
814
+ });
815
+ case "stream:latest":
816
+ return mapShown(
817
+ state,
818
+ (turn) => turn.scroll === 0 && turn.autoFollow ? turn : { ...turn, scroll: 0, autoFollow: true }
819
+ );
820
+ case "stream:clear":
821
+ return mapShown(state, (turn) => {
822
+ const id = turn.focusId;
823
+ const agent = id ? turn.agents[id] : void 0;
824
+ if (!id || !agent) return turn;
825
+ return { ...turn, agents: { ...turn.agents, [id]: { ...agent, logs: "" } }, scroll: 0 };
826
+ });
827
+ case "view:open": {
828
+ const record = state.history.find((turn) => turn.turn === action.turn);
829
+ return record ? { ...state, view: record } : state;
830
+ }
831
+ case "view:close":
832
+ return state.view ? { ...state, view: void 0 } : state;
833
+ case "verbose:toggle":
834
+ return { ...state, verbose: !state.verbose };
835
+ }
836
+ }
837
+ function stageStatus(stage, agents) {
838
+ const statuses = stage.agents.map((a) => agents[a.id]?.status ?? "pending");
839
+ if (statuses.some((s) => s === "failed")) return "failed";
840
+ if (statuses.some((s) => s === "running" || s === "retrying")) return "running";
841
+ if (statuses.length > 0 && statuses.every((s) => s === "success")) return "success";
842
+ if (statuses.length > 0 && statuses.every((s) => s === "skipped")) return "skipped";
843
+ if (statuses.length > 0 && statuses.every((s) => s === "cancelled")) return "cancelled";
844
+ return "pending";
845
+ }
846
+ var SETTLED = /* @__PURE__ */ new Set([
847
+ "success",
848
+ "failed",
849
+ "cancelled",
850
+ "skipped"
851
+ ]);
852
+ function progress(turn) {
853
+ const agents = Object.values(turn.agents);
854
+ return { done: agents.filter((a) => SETTLED.has(a.status)).length, total: agents.length };
855
+ }
856
+ function agentElapsed(agent, now) {
857
+ if (agent.status === "running" || agent.status === "retrying") {
858
+ return agent.startedAt === void 0 ? void 0 : Math.max(0, now - agent.startedAt);
859
+ }
860
+ return agent.duration;
861
+ }
862
+ function turnElapsed(turn, now) {
863
+ if (turn.totalDuration !== void 0) return turn.totalDuration;
864
+ if (turn.startedAt === void 0) return void 0;
865
+ const end = turn.phase === "running" ? now : turn.endedAt ?? now;
866
+ return Math.max(0, end - turn.startedAt);
867
+ }
868
+ function visibleLog(turn, viewport, width) {
869
+ const agent = turn.focusId ? turn.agents[turn.focusId] : void 0;
870
+ const all = agent?.logs ? wrapLog(agent.logs, width) : [];
871
+ const height = Math.max(1, viewport);
872
+ const maxScroll = Math.max(0, all.length - height);
873
+ const scrolledBack = Math.min(Math.max(0, turn.scroll), maxScroll);
874
+ const end = all.length - scrolledBack;
875
+ return {
876
+ rows: all.slice(Math.max(0, end - height), end),
877
+ scrolledBack,
878
+ following: scrolledBack === 0
879
+ };
880
+ }
881
+
882
+ // src/tui/components/overlays.tsx
883
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
884
+ var HELP_GROUPS = [
885
+ {
886
+ title: "prompt",
887
+ rows: [
888
+ ["enter", "Run the requirement"],
889
+ ["enter (busy)", "Queue it to run next"],
890
+ ["esc", "Clear the line, close an overlay"],
891
+ ["\u2190 \u2192 home end", "Move the caret"]
892
+ ]
893
+ },
894
+ {
895
+ title: "panes",
896
+ rows: [
897
+ ["tab \u21E7tab", "Next / previous agent"],
898
+ ["\u2191 \u2193", "Scroll the stream one line"],
899
+ ["pgup pgdn", "Scroll the stream a page"],
900
+ ["ctrl+l", "Clear the focused log"],
901
+ ["esc", "Stop reading a past turn"]
902
+ ]
903
+ },
904
+ {
905
+ title: "commands",
906
+ rows: [
907
+ ["/", "Command menu"],
908
+ ["?", "This help"],
909
+ ["ctrl+c", "Cancel the turn and its queue \xB7 again to quit"],
910
+ ["/<command>", `All ${COMMANDS.length} commands \u2014 type / to browse`]
911
+ ]
912
+ }
913
+ ];
914
+ var KEY_COLUMN = 16;
915
+ var TURN_ROWS = 3;
916
+ var HISTORY_CHROME_ROWS = 4;
917
+ function historyCapacity(maxRows) {
918
+ return Math.max(1, Math.floor((maxRows - HISTORY_CHROME_ROWS) / TURN_ROWS));
919
+ }
920
+ function listWindowStart(selected, total, capacity) {
921
+ const maxStart = Math.max(0, total - capacity);
922
+ return Math.min(Math.max(0, Math.min(selected, selected - capacity + 1)), maxStart);
923
+ }
924
+ function helpRows(compact = false) {
925
+ const gap = compact ? 0 : 1;
926
+ const groups = HELP_GROUPS.reduce((total, group) => total + gap + 1 + group.rows.length, 0);
927
+ return 1 + gap + groups + gap + 1;
928
+ }
929
+ function HelpOverlay({ width, maxRows }) {
930
+ const compact = helpRows(false) > maxRows;
931
+ const gap = compact ? 0 : 1;
932
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", flexShrink: 0, paddingLeft: 2, paddingTop: 1, width, children: [
933
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "? keys" }),
934
+ HELP_GROUPS.map((group) => /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginTop: gap, children: [
935
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: group.title }),
936
+ group.rows.map(([keys, description]) => /* @__PURE__ */ jsxs2(Box2, { children: [
937
+ /* @__PURE__ */ jsx2(Text2, { color: COLOR.accent, children: padRight(keys, KEY_COLUMN) }),
938
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, wrap: "truncate-end", children: description })
939
+ ] }, keys))
940
+ ] }, group.title)),
941
+ /* @__PURE__ */ jsx2(Box2, { marginTop: gap, children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "esc or ? to close" }) })
942
+ ] });
943
+ }
944
+ function outcome(turn) {
945
+ if (turn.reply) {
946
+ return turn.reply.mode === "ask" ? { text: `${GLYPH.ask} needs input`, color: COLOR.warn } : { text: `${STATUS.success.glyph} answered`, color: COLOR.ok };
947
+ }
948
+ if (turn.phase !== "complete") return { text: "interrupted", color: COLOR.warn };
949
+ return turn.success ? { text: `${STATUS.success.glyph} done`, color: COLOR.ok } : { text: `${STATUS.failed.glyph} failed`, color: COLOR.error };
950
+ }
951
+ function HistoryOverlay({
952
+ turns,
953
+ selected,
954
+ width,
955
+ maxRows
956
+ }) {
957
+ const capacity = historyCapacity(maxRows);
958
+ const start = listWindowStart(selected, turns.length, capacity);
959
+ const visible = turns.slice(start, start + capacity);
960
+ const range = turns.length > capacity ? ` \xB7 showing ${start + 1}\u2013${start + visible.length} of ${turns.length}` : "";
961
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", flexShrink: 0, paddingLeft: 2, paddingTop: 1, width, children: [
962
+ /* @__PURE__ */ jsxs2(Box2, { justifyContent: "space-between", children: [
963
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "HISTORY" }),
964
+ /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
965
+ turns.length,
966
+ " earlier"
967
+ ] })
968
+ ] }),
969
+ turns.length === 0 ? /* @__PURE__ */ jsx2(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "No earlier turns yet." }) }) : /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", marginTop: 1, children: visible.map((turn, offset) => {
970
+ const index = start + offset;
971
+ const focused = index === selected;
972
+ const state = outcome(turn);
973
+ const { done, total } = progress(turn);
974
+ const time = turn.totalDuration === void 0 ? "" : ` \xB7 ${formatDuration(turn.totalDuration)}`;
975
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginTop: index === 0 ? 0 : 1, children: [
976
+ /* @__PURE__ */ jsxs2(Box2, { children: [
977
+ /* @__PURE__ */ jsx2(Text2, { color: focused ? COLOR.accent : void 0, children: focused ? `${GLYPH.caret} ` : " " }),
978
+ /* @__PURE__ */ jsxs2(Text2, { bold: focused, color: focused ? COLOR.accent : void 0, children: [
979
+ "turn ",
980
+ turn.turn
981
+ ] }),
982
+ /* @__PURE__ */ jsxs2(Text2, { color: state.color, children: [
983
+ " \xB7 ",
984
+ state.text
985
+ ] }),
986
+ /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
987
+ time,
988
+ " \xB7 ",
989
+ done,
990
+ "/",
991
+ total,
992
+ " agents"
993
+ ] })
994
+ ] }),
995
+ /* @__PURE__ */ jsxs2(Text2, { dimColor: true, wrap: "truncate-end", children: [
996
+ " ",
997
+ truncate(turn.requirement ?? "(no requirement)", Math.max(8, width - 6))
998
+ ] })
999
+ ] }, turn.turn);
1000
+ }) }),
1001
+ /* @__PURE__ */ jsx2(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: `\u2191\u2193 select \xB7 enter open \xB7 esc close${range}` }) })
1002
+ ] });
1003
+ }
1004
+
1005
+ // src/tui/components/panels.tsx
1006
+ import { Box as Box3, Text as Text3 } from "ink";
1007
+ import { Fragment, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1008
+ function rightColumn(agent, now) {
1009
+ if (agent.status === "retrying" && agent.retry) {
1010
+ return { text: `retry ${agent.retry.attempt}/${agent.retry.max}`, color: COLOR.warn };
1011
+ }
1012
+ const elapsed = agentElapsed(agent, now);
1013
+ if (elapsed !== void 0) return { text: formatDuration(elapsed) };
1014
+ if (agent.status === "skipped") return { text: "skipped" };
1015
+ return { text: "\u2014" };
1016
+ }
1017
+ function TeamRow({
1018
+ agent,
1019
+ focused,
1020
+ now,
1021
+ spinner
1022
+ }) {
1023
+ const style = STATUS[agent.status];
1024
+ const glyph = agent.status === "running" ? spinner : style.glyph;
1025
+ const right = rightColumn(agent, now);
1026
+ return /* @__PURE__ */ jsxs3(Box3, { justifyContent: "space-between", children: [
1027
+ /* @__PURE__ */ jsx3(Box3, { flexShrink: 1, children: /* @__PURE__ */ jsxs3(
1028
+ Text3,
1029
+ {
1030
+ color: focused ? COLOR.accent : style.color,
1031
+ dimColor: !focused && style.dim,
1032
+ bold: focused,
1033
+ wrap: "truncate-end",
1034
+ children: [
1035
+ focused ? `${GLYPH.caret} ` : " ",
1036
+ glyph,
1037
+ " ",
1038
+ agent.icon ? `${agent.icon} ` : "",
1039
+ agent.name,
1040
+ agent.taskTitle ? /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
1041
+ " ",
1042
+ agent.taskTitle
1043
+ ] }) : null
1044
+ ]
1045
+ }
1046
+ ) }),
1047
+ /* @__PURE__ */ jsx3(Box3, { flexShrink: 0, children: /* @__PURE__ */ jsx3(Text3, { color: right.color, dimColor: right.color === void 0, children: right.text }) })
1048
+ ] });
1049
+ }
1050
+ function TeamPanel({
1051
+ turn,
1052
+ width,
1053
+ now,
1054
+ spinner
1055
+ }) {
1056
+ const ids = Object.keys(turn.agents);
1057
+ const { done, total } = progress(turn);
1058
+ return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", width, flexShrink: 0, paddingRight: 1, children: [
1059
+ /* @__PURE__ */ jsxs3(Box3, { justifyContent: "space-between", children: [
1060
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "TEAM" }),
1061
+ /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
1062
+ done,
1063
+ "/",
1064
+ total
1065
+ ] })
1066
+ ] }),
1067
+ ids.length === 0 ? /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "no agents configured" }) : ids.map((id) => /* @__PURE__ */ jsx3(
1068
+ TeamRow,
1069
+ {
1070
+ agent: turn.agents[id],
1071
+ focused: turn.focusId === id,
1072
+ now,
1073
+ spinner
1074
+ },
1075
+ id
1076
+ ))
1077
+ ] });
1078
+ }
1079
+ function Divider() {
1080
+ return /* @__PURE__ */ jsx3(
1081
+ Box3,
1082
+ {
1083
+ flexShrink: 0,
1084
+ borderStyle: "single",
1085
+ borderTop: false,
1086
+ borderBottom: false,
1087
+ borderRight: false,
1088
+ borderLeft: true,
1089
+ borderColor: COLOR.muted
1090
+ }
1091
+ );
1092
+ }
1093
+ function StreamPanel({
1094
+ turn,
1095
+ height,
1096
+ width,
1097
+ now,
1098
+ spinner
1099
+ }) {
1100
+ const agent = turn.focusId ? turn.agents[turn.focusId] : void 0;
1101
+ const viewport = Math.max(1, height - 1);
1102
+ const window = visibleLog(turn, viewport, width);
1103
+ const style = agent ? STATUS[agent.status] : void 0;
1104
+ const elapsed = agent ? agentElapsed(agent, now) : void 0;
1105
+ return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", flexGrow: 1, height, paddingLeft: 1, children: [
1106
+ /* @__PURE__ */ jsxs3(Box3, { justifyContent: "space-between", children: [
1107
+ /* @__PURE__ */ jsx3(Box3, { flexShrink: 1, children: /* @__PURE__ */ jsxs3(Text3, { wrap: "truncate-end", children: [
1108
+ /* @__PURE__ */ jsx3(Text3, { bold: true, children: agent?.name ?? "\u2014" }),
1109
+ agent?.model ? /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
1110
+ " \xB7 ",
1111
+ agent.model
1112
+ ] }) : null,
1113
+ agent && style ? /* @__PURE__ */ jsxs3(Fragment, { children: [
1114
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: " \xB7 " }),
1115
+ /* @__PURE__ */ jsx3(Text3, { color: style.color, dimColor: style.dim, children: agent.status === "running" ? `${spinner} ${style.label}` : style.label })
1116
+ ] }) : null,
1117
+ agent?.taskTitle ? /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
1118
+ " \xB7 ",
1119
+ agent.taskTitle
1120
+ ] }) : null,
1121
+ agent?.usage?.contextTokens !== void 0 && agent.usage.contextSize !== void 0 ? /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
1122
+ " ",
1123
+ "\xB7 ctx ",
1124
+ formatTokens(agent.usage.contextTokens),
1125
+ "/",
1126
+ formatTokens(agent.usage.contextSize)
1127
+ ] }) : null,
1128
+ agent?.costUsd !== void 0 ? /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
1129
+ " \xB7 ",
1130
+ formatCost(agent.costUsd)
1131
+ ] }) : null
1132
+ ] }) }),
1133
+ /* @__PURE__ */ jsxs3(Box3, { flexShrink: 0, children: [
1134
+ elapsed === void 0 ? null : /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
1135
+ formatDuration(elapsed),
1136
+ " "
1137
+ ] }),
1138
+ /* @__PURE__ */ jsx3(Text3, { color: window.following ? COLOR.accent : COLOR.warn, children: window.following ? GLYPH.live : `${GLYPH.paused} ${window.scrolledBack}\u2191` })
1139
+ ] })
1140
+ ] }),
1141
+ window.rows.length === 0 ? /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: turn.phase === "running" ? "waiting for output\u2026" : "no output yet" }) : (
1142
+ // The tail, already wrapped to `width` by `visibleLog` — the pane draws
1143
+ // rows, and one log line is several of them. `visibleLog` also picks
1144
+ // which rows, so the content here is exactly the pane's height and the
1145
+ // oldest rows are the ones already gone: nothing is left to clip, which
1146
+ // is why this box is not bottom-pinned. Pinning it would only leave a
1147
+ // gap under the header whenever an agent has not said much yet.
1148
+ //
1149
+ // Rows are rendered with `truncate-end` rather than letting Ink wrap
1150
+ // them again: they are supposed to fit, and if `width` was ever
1151
+ // optimistic the failure should be a shaved row, not a re-wrap that
1152
+ // overflows the pane and pushes the newest output out of sight.
1153
+ //
1154
+ // `flexShrink={0}` stops Yoga compressing the rows into each other,
1155
+ // and `overflow="hidden"` is the backstop if any of the above is ever
1156
+ // wrong.
1157
+ /* @__PURE__ */ jsx3(Box3, { flexDirection: "column", flexShrink: 0, height: viewport, overflow: "hidden", children: window.rows.map((row, index) => (
1158
+ // Rows are an append-only tail; position is the identity.
1159
+ // biome-ignore lint/suspicious/noArrayIndexKey: positional log tail
1160
+ /* @__PURE__ */ jsx3(Box3, { flexShrink: 0, children: /* @__PURE__ */ jsx3(Text3, { wrap: "truncate-end", children: row }) }, index)
1161
+ )) })
1162
+ )
1163
+ ] });
1164
+ }
1165
+ function ReplyPanel({ reply, width }) {
1166
+ const asking = reply.mode === "ask";
1167
+ return (
1168
+ // `flexShrink={0}`: the body must clip this, not compress it. A shrunken
1169
+ // column box folds its own text rows into each other before the body ever
1170
+ // gets a chance to hide the overflow.
1171
+ /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", flexShrink: 0, paddingLeft: 2, paddingTop: 1, width, children: [
1172
+ /* @__PURE__ */ jsxs3(Text3, { color: asking ? COLOR.warn : COLOR.accent, bold: true, children: [
1173
+ asking ? GLYPH.ask : GLYPH.leader,
1174
+ " ",
1175
+ asking ? "the team needs a decision" : "answered"
1176
+ ] }),
1177
+ /* @__PURE__ */ jsx3(Box3, { marginTop: 1, children: /* @__PURE__ */ jsx3(Text3, { children: reply.message }) }),
1178
+ /* @__PURE__ */ jsx3(Box3, { marginTop: 1, children: /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: asking ? "Answer below and press enter." : "Type a requirement and press enter." }) })
1179
+ ] })
1180
+ );
1181
+ }
1182
+ function ErrorPanel({ message, width }) {
1183
+ return (
1184
+ // `flexShrink={0}` — see ReplyPanel. A compressed column box folds its own
1185
+ // text rows into each other before the body gets a chance to clip them.
1186
+ /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", flexShrink: 0, paddingLeft: 2, paddingTop: 1, width, children: [
1187
+ /* @__PURE__ */ jsxs3(Text3, { color: COLOR.error, bold: true, children: [
1188
+ GLYPH.warn,
1189
+ " the run stopped"
1190
+ ] }),
1191
+ /* @__PURE__ */ jsx3(Box3, { marginTop: 1, children: /* @__PURE__ */ jsx3(Text3, { color: COLOR.error, children: message }) })
1192
+ ] })
1193
+ );
1194
+ }
1195
+ function WelcomePanel({ turn, width }) {
1196
+ const agents = Object.values(turn.agents);
1197
+ const leaderId = turn.leaderIds[0];
1198
+ const nameWidth = Math.min(24, Math.max(8, ...agents.map((a) => a.name.length)) + 2);
1199
+ return (
1200
+ // `flexShrink={0}` — see ReplyPanel. Compressing the roster folds one agent's
1201
+ // description onto the next row's, which reads as a corrupted screen.
1202
+ /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", flexShrink: 0, paddingLeft: 2, paddingTop: 1, width, children: [
1203
+ /* @__PURE__ */ jsxs3(Text3, { color: COLOR.accent, bold: true, children: [
1204
+ GLYPH.brand,
1205
+ " multiclaw"
1206
+ ] }),
1207
+ /* @__PURE__ */ jsx3(Box3, { marginTop: 1, children: /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "A team of agents, one prompt away." }) }),
1208
+ /* @__PURE__ */ jsx3(Box3, { flexDirection: "column", marginTop: 1, children: agents.map((agent) => {
1209
+ const focused = turn.focusId === agent.id;
1210
+ const leader = agent.id === leaderId;
1211
+ return /* @__PURE__ */ jsxs3(Box3, { children: [
1212
+ /* @__PURE__ */ jsxs3(Box3, { flexShrink: 0, children: [
1213
+ /* @__PURE__ */ jsx3(Text3, { color: focused ? COLOR.accent : void 0, children: focused ? `${GLYPH.caret} ` : " " }),
1214
+ /* @__PURE__ */ jsx3(Text3, { color: leader ? COLOR.accent : COLOR.muted, children: leader ? GLYPH.leader : GLYPH.agent }),
1215
+ /* @__PURE__ */ jsxs3(Text3, { bold: focused, color: focused ? COLOR.accent : void 0, children: [
1216
+ " ",
1217
+ padRight(truncate(agent.name, nameWidth - 2), nameWidth - 1)
1218
+ ] })
1219
+ ] }),
1220
+ /* @__PURE__ */ jsx3(Box3, { flexShrink: 1, children: /* @__PURE__ */ jsx3(Text3, { dimColor: true, wrap: "truncate-end", children: agent.taskTitle ?? agent.description ?? "" }) })
1221
+ ] }, agent.id);
1222
+ }) }),
1223
+ /* @__PURE__ */ jsx3(Box3, { marginTop: 1, children: /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "Type a requirement and press enter." }) })
1224
+ ] })
1225
+ );
1226
+ }
1227
+
1228
+ // src/tui/components/prompt.tsx
1229
+ import { Box as Box4, Text as Text4 } from "ink";
1230
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1231
+ function Prompt({
1232
+ value,
1233
+ cursor,
1234
+ caretVisible,
1235
+ placeholder
1236
+ }) {
1237
+ const shown = value.replace(/\r/g, "").replace(/\n/g, GLYPH.newline);
1238
+ const ghost = value === "";
1239
+ const before = shown.slice(0, cursor);
1240
+ const cell = ghost ? placeholder.slice(0, 1) : shown.slice(cursor, cursor + 1);
1241
+ const tail = ghost ? placeholder.slice(1) : shown.slice(cursor + 1);
1242
+ return /* @__PURE__ */ jsxs4(Box4, { flexShrink: 0, children: [
1243
+ /* @__PURE__ */ jsxs4(Text4, { color: COLOR.accent, children: [
1244
+ GLYPH.caret,
1245
+ " "
1246
+ ] }),
1247
+ /* @__PURE__ */ jsx4(Text4, { dimColor: ghost, children: before }),
1248
+ /* @__PURE__ */ jsx4(Text4, { inverse: caretVisible, dimColor: !caretVisible && ghost, children: cell || " " }),
1249
+ /* @__PURE__ */ jsx4(Text4, { dimColor: ghost, children: tail })
1250
+ ] });
1251
+ }
1252
+ function CommandMenu({ rows, selected }) {
1253
+ const labelWidth = Math.max(1, ...rows.map((row) => row.label.length));
1254
+ return /* @__PURE__ */ jsx4(Box4, { flexDirection: "column", paddingLeft: 2, children: rows.map((row, index) => {
1255
+ const active = index === selected;
1256
+ return /* @__PURE__ */ jsxs4(
1257
+ Text4,
1258
+ {
1259
+ color: active ? COLOR.accent : void 0,
1260
+ dimColor: !active,
1261
+ wrap: "truncate-end",
1262
+ children: [
1263
+ active ? GLYPH.caret : " ",
1264
+ " ",
1265
+ padRight(row.label, labelWidth),
1266
+ " ",
1267
+ row.description
1268
+ ]
1269
+ },
1270
+ row.key
1271
+ );
1272
+ }) });
1273
+ }
1274
+
1275
+ // src/tui/history.ts
1276
+ import * as fs from "fs";
1277
+ import * as path from "path";
1278
+ var CONSOLE_DIR = "console";
1279
+ var HISTORY_FILE = "history.json";
1280
+ var EXPORT_DIR = "exports";
1281
+ function historyPath(workDir) {
1282
+ return path.join(workDir, ".multiclaw", CONSOLE_DIR, HISTORY_FILE);
1283
+ }
1284
+ function isObject(value) {
1285
+ return typeof value === "object" && value !== null;
1286
+ }
1287
+ function isTurnView(value) {
1288
+ if (!isObject(value)) return false;
1289
+ const turn = value;
1290
+ return typeof turn.turn === "number" && typeof turn.phase === "string" && isObject(turn.agents) && Object.values(turn.agents).every(isObject) && Array.isArray(turn.stages) && turn.stages.every(isObject) && Array.isArray(turn.leaderIds);
1291
+ }
1292
+ function loadHistory(workDir) {
1293
+ try {
1294
+ const raw = fs.readFileSync(historyPath(workDir), "utf-8");
1295
+ const parsed = JSON.parse(raw);
1296
+ if (!Array.isArray(parsed)) return [];
1297
+ return parsed.filter(isTurnView).slice(-MAX_HISTORY);
1298
+ } catch {
1299
+ return [];
1300
+ }
1301
+ }
1302
+ async function saveHistory(workDir, turns) {
1303
+ const target = historyPath(workDir);
1304
+ await fs.promises.mkdir(path.dirname(target), { recursive: true });
1305
+ const tmp = `${target}.tmp`;
1306
+ await fs.promises.writeFile(tmp, JSON.stringify(turns), "utf-8");
1307
+ await fs.promises.rename(tmp, target);
1308
+ }
1309
+ function exportPath(workDir, turn) {
1310
+ return path.join(workDir, ".multiclaw", CONSOLE_DIR, EXPORT_DIR, `turn-${turn}.md`);
1311
+ }
1312
+ async function exportTurn(turn, filePath) {
1313
+ await fs.promises.mkdir(path.dirname(filePath), { recursive: true });
1314
+ await fs.promises.writeFile(filePath, formatTurnMarkdown(turn), "utf-8");
1315
+ }
1316
+ function formatTurnMarkdown(turn) {
1317
+ const lines = [`# Turn ${turn.turn}`, ""];
1318
+ if (turn.requirement) lines.push("## Requirement", "", turn.requirement, "");
1319
+ if (turn.reply) {
1320
+ lines.push("## Reply", "", `_${turn.reply.mode}_`, "", turn.reply.message, "");
1321
+ } else {
1322
+ lines.push("## Result", "", `Status: ${turn.success ? "success" : "failed"}`, "");
1323
+ }
1324
+ const agents = Object.values(turn.agents);
1325
+ if (agents.length > 0) {
1326
+ lines.push("## Agents", "");
1327
+ for (const agent of agents) {
1328
+ lines.push(`- ${agent.id}: ${agent.name} \u2014 ${agent.status}`);
1329
+ }
1330
+ lines.push("");
1331
+ lines.push("## Logs", "");
1332
+ for (const agent of agents) {
1333
+ lines.push(`### ${agent.id}: ${agent.name}`, "", "```", agent.logs ?? "", "```", "");
1334
+ }
1335
+ }
1336
+ return `${lines.join("\n").trimEnd()}
1337
+ `;
1338
+ }
1339
+
1340
+ // src/tui/App.tsx
1341
+ import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
1342
+ var FLUSH_INTERVAL_MS = 80;
1343
+ var SPINNER_INTERVAL_MS = 110;
1344
+ var BLINK_INTERVAL_MS = 530;
1345
+ var INPUT_PLACEHOLDER = "describe what the team should do\u2026";
1346
+ var RUNNING_PLACEHOLDER = "type to queue a follow-up\u2026";
1347
+ var EMPTY_LINE = { text: "", cursor: 0 };
1348
+ function pickRecordedTurn(state, raw, whenEmpty) {
1349
+ const turns = recordedTurns(state);
1350
+ if (turns.length === 0) return { error: whenEmpty };
1351
+ const target = raw === void 0 ? turns[turns.length - 1] : turns.find((t) => String(t.turn) === raw);
1352
+ if (!target) return { error: `no turn ${raw}` };
1353
+ return { record: target };
1354
+ }
1355
+ function pickTurn(state, raw, whenEmpty) {
1356
+ const target = pickRecordedTurn(state, raw, whenEmpty);
1357
+ if ("error" in target) return target;
1358
+ if (!target.record.requirement) return { error: `turn ${target.record.turn} has no requirement` };
1359
+ return { record: target.record, requirement: target.record.requirement };
1360
+ }
1361
+ function StatusLine({
1362
+ state,
1363
+ running,
1364
+ cancelled,
1365
+ error,
1366
+ now,
1367
+ spinner,
1368
+ queued
1369
+ }) {
1370
+ const { done, total } = progress(state);
1371
+ if (error)
1372
+ return /* @__PURE__ */ jsx5(Text5, { color: COLOR.error, wrap: "truncate-end", children: error });
1373
+ if (state.warning && state.phase !== "complete")
1374
+ return /* @__PURE__ */ jsx5(Text5, { color: COLOR.warn, wrap: "truncate-end", children: state.warning });
1375
+ if (cancelled && !running) return /* @__PURE__ */ jsx5(Text5, { color: COLOR.warn, children: "cancelled" });
1376
+ if (state.reply) {
1377
+ const asking = state.reply.mode === "ask";
1378
+ return /* @__PURE__ */ jsx5(Text5, { color: asking ? COLOR.warn : COLOR.ok, wrap: "truncate-end", children: asking ? `${GLYPH.ask} needs your input` : `${STATUS.success.glyph} answered` });
1379
+ }
1380
+ if (running) {
1381
+ const elapsed = turnElapsed(state, now);
1382
+ const time = elapsed === void 0 ? "" : ` ${formatDuration(elapsed)}`;
1383
+ return /* @__PURE__ */ jsxs5(Text5, { color: COLOR.accent, wrap: "truncate-end", children: [
1384
+ spinner,
1385
+ " running",
1386
+ time,
1387
+ " \xB7 ",
1388
+ done,
1389
+ "/",
1390
+ total,
1391
+ " agents",
1392
+ queued > 0 ? ` \xB7 ${queued} queued` : ""
1393
+ ] });
1394
+ }
1395
+ if (state.phase === "complete") {
1396
+ const time = state.totalDuration === void 0 ? "" : ` ${formatDuration(state.totalDuration)}`;
1397
+ const outcome2 = state.success ? `${STATUS.success.glyph} done${time}` : `${STATUS.failed.glyph} finished with failures${time}`;
1398
+ const cost = state.totalCostUsd === void 0 ? "" : ` \xB7 ${formatCost(state.totalCostUsd)}`;
1399
+ return /* @__PURE__ */ jsxs5(Text5, { color: state.success ? COLOR.ok : COLOR.error, wrap: "truncate-end", children: [
1400
+ outcome2,
1401
+ cost,
1402
+ state.warning ? ` \xB7 ${GLYPH.warn} ${state.warning}` : ""
1403
+ ] });
1404
+ }
1405
+ const stages = state.stages.length;
1406
+ return /* @__PURE__ */ jsxs5(Text5, { dimColor: true, wrap: "truncate-end", children: [
1407
+ "ready \xB7 ",
1408
+ total,
1409
+ " agents",
1410
+ stages > 0 ? ` \xB7 ${stages} stages` : ""
1411
+ ] });
1412
+ }
1413
+ function App({ config, onTurnStart }) {
1414
+ const { exit } = useApp();
1415
+ const { columns, rows } = useWindowSize();
1416
+ const [state, dispatch] = useReducer(
1417
+ tuiReducer,
1418
+ config,
1419
+ (c) => createInitialState(c, loadHistory(c.workDir))
1420
+ );
1421
+ const stateRef = useRef(state);
1422
+ stateRef.current = state;
1423
+ const [line, setLine] = useState(EMPTY_LINE);
1424
+ const [caretOn, setCaretOn] = useState(true);
1425
+ const [tick, setTick] = useState(0);
1426
+ const [running, setRunning] = useState(false);
1427
+ const [queue, setQueue] = useState([]);
1428
+ const [cancelled, setCancelled] = useState(false);
1429
+ const [error, setError] = useState();
1430
+ const [notice, setNotice] = useState();
1431
+ const [helpOpen, setHelpOpen] = useState(false);
1432
+ const [historyOpen, setHistoryOpen] = useState(false);
1433
+ const [historyCursor, setHistoryCursor] = useState(0);
1434
+ const [menuCursor, setMenuCursor] = useState({
1435
+ query: void 0,
1436
+ index: 0
1437
+ });
1438
+ const abortRef = useRef(null);
1439
+ const pending = useRef([]);
1440
+ const flush = useCallback(() => {
1441
+ const batch = pending.current;
1442
+ if (batch.length === 0) return;
1443
+ pending.current = [];
1444
+ for (const event of batch) dispatch({ type: "event", event });
1445
+ }, []);
1446
+ useEffect(() => {
1447
+ if (!running) return;
1448
+ const timer = setInterval(() => setTick((value) => value + 1), SPINNER_INTERVAL_MS);
1449
+ return () => clearInterval(timer);
1450
+ }, [running]);
1451
+ useEffect(() => {
1452
+ if (!running && line.text === "") {
1453
+ setCaretOn(true);
1454
+ return;
1455
+ }
1456
+ const timer = setInterval(() => setCaretOn((on) => !on), BLINK_INTERVAL_MS);
1457
+ return () => clearInterval(timer);
1458
+ }, [running, line.text]);
1459
+ const lastSaved = useRef(void 0);
1460
+ useEffect(() => {
1461
+ const turns = recordedTurns(stateRef.current).slice(-MAX_HISTORY);
1462
+ if (turns.length === 0) return;
1463
+ const json = JSON.stringify(turns);
1464
+ if (json === lastSaved.current) return;
1465
+ lastSaved.current = json;
1466
+ void saveHistory(config.workDir, turns).catch(
1467
+ (err) => setNotice(`history not saved: ${err.message}`)
1468
+ );
1469
+ }, [state.history, state.phase]);
1470
+ const startTurn = useCallback(
1471
+ async (requirement, opts) => {
1472
+ setError(void 0);
1473
+ setCancelled(false);
1474
+ setNotice(void 0);
1475
+ const earlierTurns = conversationTurns(stateRef.current);
1476
+ dispatch({
1477
+ type: "turn:start",
1478
+ requirement,
1479
+ agents: seedAgents(config),
1480
+ leaderIds: leaderAgentIds(config),
1481
+ startedAt: Date.now()
1482
+ });
1483
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
1484
+ const logDir = path2.join(config.workDir, ".multiclaw", "runs", `run-${stamp}`, "logs");
1485
+ const runConfig = {
1486
+ ...config,
1487
+ context: { ...config.context, requirement },
1488
+ // An answer only reads as a requirement next to the question it answers,
1489
+ // so the leader is handed the turns behind this one as well.
1490
+ history: earlierTurns,
1491
+ logDir,
1492
+ // Only a retry carries this; a normal turn lets the leader route.
1493
+ ...opts?.resume ? { resume: opts.resume } : {}
1494
+ };
1495
+ const controller = new AbortController();
1496
+ abortRef.current = controller;
1497
+ setRunning(true);
1498
+ const orchestrator = new Orchestrator(runConfig);
1499
+ const unsubscribe = orchestrator.eventBus.subscribe((event) => {
1500
+ pending.current.push(event);
1501
+ });
1502
+ onTurnStart?.(orchestrator.eventBus, logDir);
1503
+ const timer = setInterval(flush, FLUSH_INTERVAL_MS);
1504
+ try {
1505
+ await orchestrator.run({ signal: controller.signal });
1506
+ } catch (err) {
1507
+ if (isAbortError(err)) setCancelled(true);
1508
+ else setError(err.message);
1509
+ } finally {
1510
+ clearInterval(timer);
1511
+ flush();
1512
+ unsubscribe();
1513
+ abortRef.current = null;
1514
+ setRunning(false);
1515
+ }
1516
+ },
1517
+ [config, flush, onTurnStart]
1518
+ );
1519
+ useEffect(() => {
1520
+ if (running || queue.length === 0) return;
1521
+ const [next, ...rest] = queue;
1522
+ setQueue(rest);
1523
+ void startTurn(next);
1524
+ }, [running, queue, startTurn]);
1525
+ const query = menuQuery(line.text);
1526
+ const matches = useMemo(
1527
+ () => query === void 0 ? [] : matchCommands(query, running),
1528
+ [query, running]
1529
+ );
1530
+ const historyTurns = useMemo(() => [...state.history].reverse(), [state.history]);
1531
+ const historyIndex = historyTurns.length === 0 ? 0 : Math.min(historyCursor, historyTurns.length - 1);
1532
+ const shown = state.view ?? state;
1533
+ const arg = argQuery(line.text);
1534
+ const argRows = arg ? completeAgents(arg.partial, Object.keys(shown.agents)).map((id) => ({
1535
+ key: id,
1536
+ label: id,
1537
+ // The static role, not the per-turn task title: while completing, the
1538
+ // user is asking "which agent is this", not "what is it doing now".
1539
+ description: shown.agents[id]?.description ?? shown.agents[id]?.name ?? id
1540
+ })) : [];
1541
+ const allRows = arg ? argRows : paletteRows(matches);
1542
+ const paletteKey = arg ? `${arg.command.name} ${arg.partial}` : query;
1543
+ const paletteTotal = allRows.length;
1544
+ const menuIndex = menuCursor.query === paletteKey ? menuCursor.index : 0;
1545
+ const selected = paletteTotal === 0 ? 0 : Math.min(menuIndex, paletteTotal - 1);
1546
+ const menuOpen = !helpOpen && !historyOpen && query !== void 0 && matches.length > 0;
1547
+ const argOpen = !helpOpen && !historyOpen && arg !== void 0 && argRows.length > 0;
1548
+ const paletteOpen = menuOpen || argOpen;
1549
+ const layout = computeLayout({
1550
+ columns,
1551
+ rows,
1552
+ turn: state.turn,
1553
+ overlay: helpOpen || historyOpen,
1554
+ menuRows: paletteOpen ? paletteTotal : 0,
1555
+ queueRows: queue.length,
1556
+ notice: notice !== void 0,
1557
+ viewing: state.view !== void 0,
1558
+ hasRail: shown.leaderIds.length > 0 || shown.stages.length > 0,
1559
+ railCells: railCellWidths(shown)
1560
+ });
1561
+ const runCommand = useCallback(
1562
+ (name, args) => {
1563
+ setLine(EMPTY_LINE);
1564
+ if (!canRun(name, running)) {
1565
+ setNotice(unavailableReason(name, running));
1566
+ return;
1567
+ }
1568
+ const shown2 = state.view ?? state;
1569
+ switch (name) {
1570
+ case "agents":
1571
+ setNotice(
1572
+ `team: ${plannedAgents(config).map((agent) => agent.id).join(", ")}`
1573
+ );
1574
+ return;
1575
+ case "focus": {
1576
+ const input = args[0];
1577
+ if (!input) {
1578
+ setNotice("usage: /focus <agent id>");
1579
+ return;
1580
+ }
1581
+ const resolved = resolveAgentId(input, Object.keys(shown2.agents));
1582
+ if ("error" in resolved) {
1583
+ setNotice(resolved.error);
1584
+ return;
1585
+ }
1586
+ dispatch({ type: "focus:set", agentId: resolved.id });
1587
+ setNotice(`focused ${resolved.id}`);
1588
+ return;
1589
+ }
1590
+ case "clear":
1591
+ if (!shown2.focusId) {
1592
+ setNotice("nothing focused");
1593
+ return;
1594
+ }
1595
+ dispatch({ type: "stream:clear" });
1596
+ setNotice(`cleared ${shown2.focusId}`);
1597
+ return;
1598
+ case "history":
1599
+ if (state.history.length === 0) {
1600
+ setNotice("no earlier turns yet");
1601
+ return;
1602
+ }
1603
+ setNotice(void 0);
1604
+ setHistoryCursor(0);
1605
+ setHistoryOpen(true);
1606
+ return;
1607
+ case "verbose":
1608
+ dispatch({ type: "verbose:toggle" });
1609
+ setNotice(state.verbose ? "verbose off" : "verbose on \u2014 showing tool detail");
1610
+ return;
1611
+ case "retry": {
1612
+ if (state.phase !== "complete" || !state.requirement) {
1613
+ setNotice("nothing to retry yet");
1614
+ return;
1615
+ }
1616
+ let ids;
1617
+ if (args[0]) {
1618
+ const resolved = resolveAgentId(args[0], Object.keys(state.agents));
1619
+ if ("error" in resolved) {
1620
+ setNotice(resolved.error);
1621
+ return;
1622
+ }
1623
+ ids = [resolved.id];
1624
+ } else {
1625
+ ids = Object.values(state.agents).filter((agent) => agent.status === "failed").map((agent) => agent.id);
1626
+ }
1627
+ if (ids.length === 0) {
1628
+ setNotice("nothing failed");
1629
+ return;
1630
+ }
1631
+ const sessions = Object.fromEntries(
1632
+ ids.filter((id) => state.agents[id]?.sessionId).map((id) => [id, state.agents[id].sessionId])
1633
+ );
1634
+ void startTurn(state.requirement, {
1635
+ resume: {
1636
+ agents: ids,
1637
+ ...Object.keys(sessions).length > 0 ? { sessions } : {}
1638
+ }
1639
+ });
1640
+ setNotice(
1641
+ `retrying ${ids.join(", ")} \u2014 ${describeResumePlan({
1642
+ agents: ids,
1643
+ sessions,
1644
+ team: config.agents
1645
+ })}`
1646
+ );
1647
+ return;
1648
+ }
1649
+ case "replay": {
1650
+ const target = pickTurn(state, args[0], "nothing to replay yet");
1651
+ if ("error" in target) {
1652
+ setNotice(target.error);
1653
+ return;
1654
+ }
1655
+ void startTurn(target.requirement);
1656
+ setNotice(`replaying turn ${target.record.turn}`);
1657
+ return;
1658
+ }
1659
+ case "fork": {
1660
+ const target = pickTurn(state, args[0], "nothing to fork yet");
1661
+ if ("error" in target) {
1662
+ setNotice(target.error);
1663
+ return;
1664
+ }
1665
+ setLine({ text: target.requirement, cursor: target.requirement.length });
1666
+ setNotice(`editing turn ${target.record.turn} \u2014 enter to run`);
1667
+ return;
1668
+ }
1669
+ case "export": {
1670
+ const target = pickRecordedTurn(state, args[0], "nothing to export yet");
1671
+ if ("error" in target) {
1672
+ setNotice(target.error);
1673
+ return;
1674
+ }
1675
+ const file = args[1] ? path2.resolve(config.workDir, args[1]) : exportPath(config.workDir, target.record.turn);
1676
+ void exportTurn(target.record, file).then(() => setNotice(`exported turn ${target.record.turn} to ${file}`)).catch((err) => setNotice(`export failed: ${err.message}`));
1677
+ return;
1678
+ }
1679
+ case "search": {
1680
+ const text = args.join(" ");
1681
+ if (!text) {
1682
+ setNotice("usage: /search <text>");
1683
+ return;
1684
+ }
1685
+ dispatch({
1686
+ type: "stream:search",
1687
+ query: text,
1688
+ viewport: layout.logViewport,
1689
+ width: layout.streamWidth
1690
+ });
1691
+ setNotice(`searched for ${text}`);
1692
+ return;
1693
+ }
1694
+ case "latest":
1695
+ dispatch({ type: "stream:latest" });
1696
+ setNotice("showing latest output");
1697
+ return;
1698
+ case "cancel": {
1699
+ if (!abortRef.current) {
1700
+ setNotice("nothing to cancel");
1701
+ return;
1702
+ }
1703
+ const dropped = queue.length;
1704
+ abortRef.current.abort();
1705
+ setQueue([]);
1706
+ setCancelled(true);
1707
+ setNotice(dropped > 0 ? `cancelling\u2026 \xB7 dropped ${dropped} queued` : "cancelling\u2026");
1708
+ return;
1709
+ }
1710
+ case "help":
1711
+ setNotice(void 0);
1712
+ setHelpOpen(true);
1713
+ return;
1714
+ case "quit":
1715
+ exit();
1716
+ return;
1717
+ default:
1718
+ setNotice(`unknown command: /${name}`);
1719
+ }
1720
+ },
1721
+ [config, exit, layout.logViewport, layout.streamWidth, queue.length, running, state, startTurn]
1722
+ );
1723
+ const submit = useCallback(() => {
1724
+ const text = line.text.trim();
1725
+ if (!text) return;
1726
+ const command = parseCommand(text);
1727
+ if (command) {
1728
+ runCommand(command.name, command.args);
1729
+ return;
1730
+ }
1731
+ setLine(EMPTY_LINE);
1732
+ setNotice(void 0);
1733
+ if (running) {
1734
+ setQueue((pending2) => [...pending2, text]);
1735
+ return;
1736
+ }
1737
+ void startTurn(text);
1738
+ }, [line.text, runCommand, running, startTurn]);
1739
+ const insert = useCallback((chunk) => {
1740
+ setLine((l) => ({
1741
+ text: l.text.slice(0, l.cursor) + chunk + l.text.slice(l.cursor),
1742
+ cursor: l.cursor + chunk.length
1743
+ }));
1744
+ }, []);
1745
+ const backspace = useCallback(() => {
1746
+ setLine(
1747
+ (l) => l.cursor === 0 ? l : { text: l.text.slice(0, l.cursor - 1) + l.text.slice(l.cursor), cursor: l.cursor - 1 }
1748
+ );
1749
+ }, []);
1750
+ const deleteForward = useCallback(() => {
1751
+ setLine(
1752
+ (l) => l.cursor >= l.text.length ? l : { text: l.text.slice(0, l.cursor) + l.text.slice(l.cursor + 1), cursor: l.cursor }
1753
+ );
1754
+ }, []);
1755
+ const move = useCallback((delta) => {
1756
+ setLine((l) => ({ ...l, cursor: Math.min(Math.max(0, l.cursor + delta), l.text.length) }));
1757
+ }, []);
1758
+ const moveTo = useCallback((cursor) => {
1759
+ setLine((l) => ({ ...l, cursor }));
1760
+ }, []);
1761
+ const windowStart = listWindowStart(selected, paletteTotal, layout.menuRows);
1762
+ const paletteWindow = allRows.slice(windowStart, windowStart + layout.menuRows);
1763
+ const paletteSelected = selected - windowStart;
1764
+ const now = Date.now();
1765
+ const spinner = SPINNER[tick % SPINNER.length];
1766
+ const handleKey = (char, key) => {
1767
+ if (key.ctrl && char === "c") {
1768
+ if (helpOpen) {
1769
+ setHelpOpen(false);
1770
+ } else if (historyOpen) {
1771
+ setHistoryOpen(false);
1772
+ } else if (abortRef.current) {
1773
+ abortRef.current.abort();
1774
+ setNotice(queue.length > 0 ? `cancelling\u2026 \xB7 dropped ${queue.length} queued` : "cancelling\u2026");
1775
+ setQueue([]);
1776
+ setCancelled(true);
1777
+ } else if (line.text) {
1778
+ setLine(EMPTY_LINE);
1779
+ } else {
1780
+ exit();
1781
+ }
1782
+ return;
1783
+ }
1784
+ if (helpOpen) {
1785
+ if (key.escape || char === "?") setHelpOpen(false);
1786
+ return;
1787
+ }
1788
+ if (historyOpen) {
1789
+ if (key.escape) {
1790
+ setHistoryOpen(false);
1791
+ return;
1792
+ }
1793
+ if (historyTurns.length === 0) return;
1794
+ if (key.upArrow) {
1795
+ setHistoryCursor((historyIndex - 1 + historyTurns.length) % historyTurns.length);
1796
+ return;
1797
+ }
1798
+ if (key.downArrow) {
1799
+ setHistoryCursor((historyIndex + 1) % historyTurns.length);
1800
+ return;
1801
+ }
1802
+ if (key.return) {
1803
+ dispatch({ type: "view:open", turn: historyTurns[historyIndex].turn });
1804
+ setHistoryOpen(false);
1805
+ return;
1806
+ }
1807
+ return;
1808
+ }
1809
+ if (key.ctrl && char === "d") {
1810
+ if (!line.text) exit();
1811
+ return;
1812
+ }
1813
+ if (key.ctrl && char === "l") {
1814
+ dispatch({ type: "stream:clear" });
1815
+ setNotice(shown.focusId ? `cleared ${shown.focusId}` : "nothing focused");
1816
+ return;
1817
+ }
1818
+ if (key.ctrl && char === "a") {
1819
+ moveTo(0);
1820
+ return;
1821
+ }
1822
+ if (key.ctrl && char === "e") {
1823
+ setLine((l) => ({ ...l, cursor: l.text.length }));
1824
+ return;
1825
+ }
1826
+ if (key.tab) {
1827
+ dispatch({ type: "focus:cycle", delta: key.shift ? -1 : 1 });
1828
+ return;
1829
+ }
1830
+ if (paletteOpen) {
1831
+ if (key.upArrow) {
1832
+ setMenuCursor({ query: paletteKey, index: (menuIndex - 1 + paletteTotal) % paletteTotal });
1833
+ return;
1834
+ }
1835
+ if (key.downArrow) {
1836
+ setMenuCursor({ query: paletteKey, index: (menuIndex + 1) % paletteTotal });
1837
+ return;
1838
+ }
1839
+ if (key.escape) {
1840
+ setLine(EMPTY_LINE);
1841
+ return;
1842
+ }
1843
+ if (key.return) {
1844
+ if (arg) runCommand(arg.command.name, [allRows[selected].key]);
1845
+ else runCommand(matches[selected].name, []);
1846
+ return;
1847
+ }
1848
+ }
1849
+ if (key.upArrow) {
1850
+ dispatch({
1851
+ type: "stream:scroll",
1852
+ delta: 1,
1853
+ viewport: layout.logViewport,
1854
+ width: layout.streamWidth
1855
+ });
1856
+ return;
1857
+ }
1858
+ if (key.downArrow) {
1859
+ dispatch({
1860
+ type: "stream:scroll",
1861
+ delta: -1,
1862
+ viewport: layout.logViewport,
1863
+ width: layout.streamWidth
1864
+ });
1865
+ return;
1866
+ }
1867
+ if (key.pageUp) {
1868
+ dispatch({
1869
+ type: "stream:scroll",
1870
+ delta: layout.logViewport,
1871
+ viewport: layout.logViewport,
1872
+ width: layout.streamWidth
1873
+ });
1874
+ return;
1875
+ }
1876
+ if (key.pageDown) {
1877
+ dispatch({
1878
+ type: "stream:scroll",
1879
+ delta: -layout.logViewport,
1880
+ viewport: layout.logViewport,
1881
+ width: layout.streamWidth
1882
+ });
1883
+ return;
1884
+ }
1885
+ if (char === "?" && line.text === "" && !running) {
1886
+ setHelpOpen(true);
1887
+ return;
1888
+ }
1889
+ if (key.return) {
1890
+ submit();
1891
+ return;
1892
+ }
1893
+ if (key.leftArrow) {
1894
+ move(-1);
1895
+ return;
1896
+ }
1897
+ if (key.rightArrow) {
1898
+ move(1);
1899
+ return;
1900
+ }
1901
+ if (key.home) {
1902
+ moveTo(0);
1903
+ return;
1904
+ }
1905
+ if (key.end) {
1906
+ setLine((l) => ({ ...l, cursor: l.text.length }));
1907
+ return;
1908
+ }
1909
+ if (key.escape) {
1910
+ if (error) {
1911
+ setError(void 0);
1912
+ return;
1913
+ }
1914
+ if (state.view) {
1915
+ dispatch({ type: "view:close" });
1916
+ return;
1917
+ }
1918
+ setLine(EMPTY_LINE);
1919
+ setNotice(void 0);
1920
+ return;
1921
+ }
1922
+ if (key.backspace) {
1923
+ backspace();
1924
+ return;
1925
+ }
1926
+ if (key.delete) {
1927
+ deleteForward();
1928
+ return;
1929
+ }
1930
+ if (char && !key.ctrl && !key.meta) insert(char.replace(/\r\n?/g, "\n"));
1931
+ };
1932
+ const handlerRef = useRef(handleKey);
1933
+ handlerRef.current = handleKey;
1934
+ useInput((char, key) => handlerRef.current(char, key));
1935
+ const keys = helpOpen ? "esc close" : historyOpen ? "\u2191\u2193 select \xB7 enter open \xB7 esc close" : argOpen ? `\u2191\u2193 select \xB7 enter ${arg?.command.name} \xB7 esc close` : menuOpen ? "\u2191\u2193 select \xB7 enter run \xB7 esc close" : running ? "enter queues \xB7 ctrl+c cancel" : state.view ? "esc live \xB7 tab agent \xB7 ? help" : state.reply ? "enter sends \xB7 / commands \xB7 ? help" : "tab switch \xB7 / commands \xB7 ? help";
1936
+ return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", height: layout.rows, children: [
1937
+ /* @__PURE__ */ jsx5(
1938
+ Header,
1939
+ {
1940
+ name: state.name || config.name,
1941
+ right: state.view ? `turn ${shown.turn} \xB7 viewing` : `turn ${shown.turn} \xB7 ${running ? formatDuration(turnElapsed(state, now) ?? 0) : state.phase === "complete" ? "complete" : "ready"}`
1942
+ }
1943
+ ),
1944
+ /* @__PURE__ */ jsx5(Rule, { width: layout.columns }),
1945
+ config.useLeader === false ? /* @__PURE__ */ jsx5(Text5, { color: COLOR.warn, dimColor: true, wrap: "truncate-end", children: " \u26A0 leader off \u2014 every message runs the full team" }) : null,
1946
+ layout.showRail ? (
1947
+ // The rail wraps, so it can be taller than one row. Cap it at the height
1948
+ // the budget reserved and clip, rather than letting the extra rows push
1949
+ // the chrome below it off the screen.
1950
+ /* @__PURE__ */ jsx5(Box5, { marginTop: 1, height: layout.railRows, overflow: "hidden", flexShrink: 0, children: /* @__PURE__ */ jsx5(PipelineRail, { turn: shown, spinner }) })
1951
+ ) : null,
1952
+ shown.reason ? /* @__PURE__ */ jsx5(Text5, { dimColor: true, wrap: "truncate-end", children: ` reason: ${shown.reason}` }) : null,
1953
+ /* @__PURE__ */ jsx5(
1954
+ Box5,
1955
+ {
1956
+ marginTop: 1,
1957
+ height: layout.bodyHeight,
1958
+ flexShrink: 0,
1959
+ flexDirection: "column",
1960
+ overflow: "hidden",
1961
+ children: helpOpen ? /* @__PURE__ */ jsx5(HelpOverlay, { width: layout.columns, maxRows: layout.bodyHeight }) : historyOpen ? /* @__PURE__ */ jsx5(
1962
+ HistoryOverlay,
1963
+ {
1964
+ turns: historyTurns,
1965
+ selected: historyIndex,
1966
+ width: layout.columns,
1967
+ maxRows: layout.bodyHeight
1968
+ }
1969
+ ) : layout.welcome ? /* @__PURE__ */ jsx5(WelcomePanel, { turn: state, width: layout.columns }) : error ? (
1970
+ // The error wins over a reply: a run that stopped with an error is the
1971
+ // most urgent thing on screen, and a stale answer underneath it would
1972
+ // read as the outcome. The status line keeps a one-line version too.
1973
+ /* @__PURE__ */ jsx5(ErrorPanel, { message: error, width: layout.columns })
1974
+ ) : shown.reply ? /* @__PURE__ */ jsx5(ReplyPanel, { reply: shown.reply, width: layout.columns }) : /* @__PURE__ */ jsxs5(Box5, { height: layout.bodyHeight, flexShrink: 0, flexDirection: "row", children: [
1975
+ layout.showTeam ? /* @__PURE__ */ jsxs5(Fragment2, { children: [
1976
+ /* @__PURE__ */ jsx5(TeamPanel, { turn: shown, width: layout.teamWidth, now, spinner }),
1977
+ /* @__PURE__ */ jsx5(Divider, {})
1978
+ ] }) : null,
1979
+ /* @__PURE__ */ jsx5(
1980
+ StreamPanel,
1981
+ {
1982
+ turn: shown,
1983
+ height: layout.bodyHeight,
1984
+ width: layout.streamWidth,
1985
+ now,
1986
+ spinner
1987
+ }
1988
+ )
1989
+ ] })
1990
+ }
1991
+ ),
1992
+ state.view ? /* @__PURE__ */ jsx5(Text5, { color: COLOR.warn, wrap: "truncate-end", children: ` viewing turn ${shown.turn} \xB7 ${shown.requirement ?? "(no requirement)"} \xB7 esc to return` }) : null,
1993
+ notice ? /* @__PURE__ */ jsxs5(Text5, { dimColor: true, wrap: "truncate-end", children: [
1994
+ " ",
1995
+ notice
1996
+ ] }) : null,
1997
+ /* @__PURE__ */ jsx5(QueueStrip, { items: queue, max: layout.queueRows }),
1998
+ paletteOpen ? /* @__PURE__ */ jsx5(CommandMenu, { rows: paletteWindow, selected: paletteSelected }) : null,
1999
+ /* @__PURE__ */ jsx5(
2000
+ Prompt,
2001
+ {
2002
+ value: line.text,
2003
+ cursor: line.cursor,
2004
+ caretVisible: caretOn,
2005
+ placeholder: running ? RUNNING_PLACEHOLDER : INPUT_PLACEHOLDER
2006
+ }
2007
+ ),
2008
+ /* @__PURE__ */ jsx5(
2009
+ HintBar,
2010
+ {
2011
+ status: /* @__PURE__ */ jsx5(
2012
+ StatusLine,
2013
+ {
2014
+ state,
2015
+ running,
2016
+ cancelled,
2017
+ error,
2018
+ now,
2019
+ spinner,
2020
+ queued: queue.length
2021
+ }
2022
+ ),
2023
+ keys
2024
+ }
2025
+ )
2026
+ ] });
2027
+ }
2028
+
2029
+ // src/tui/index.ts
2030
+ async function startTui(options) {
2031
+ if (process.stdout.isTTY !== true) {
2032
+ throw new Error(
2033
+ 'The interactive TUI needs a TTY. Use `multiclaw run "<requirement>"` for headless execution.'
2034
+ );
2035
+ }
2036
+ const restore = () => process.stdout.write("\x1B[?25h\x1B[?1049l");
2037
+ process.stdout.write("\x1B[?1049h\x1B[?25l");
2038
+ process.on("exit", restore);
2039
+ try {
2040
+ const app = render(createElement(App, options), { exitOnCtrlC: false });
2041
+ await app.waitUntilExit();
2042
+ } finally {
2043
+ process.off("exit", restore);
2044
+ restore();
2045
+ }
2046
+ }
2047
+ export {
2048
+ COMPACT_COLUMNS,
2049
+ MAX_HISTORY,
2050
+ MAX_LOG_CHARS,
2051
+ MAX_MENU_ROWS,
2052
+ MAX_QUEUE_ROWS,
2053
+ MAX_RAIL_ROWS,
2054
+ MIN_COLUMNS,
2055
+ MIN_ROWS,
2056
+ RAIL_MIN_ROWS,
2057
+ agentElapsed,
2058
+ appendLog,
2059
+ computeLayout,
2060
+ conversationTurns,
2061
+ createInitialState,
2062
+ initialState,
2063
+ leaderAgentIds,
2064
+ measureRailRows,
2065
+ plannedAgents,
2066
+ progress,
2067
+ recordedTurns,
2068
+ reduceEvent,
2069
+ seedAgents,
2070
+ stageStatus,
2071
+ startTui,
2072
+ stripAnsi,
2073
+ tuiReducer,
2074
+ turnElapsed,
2075
+ visibleLog
2076
+ };