@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.
- package/README.md +66 -2
- package/dist/tui/App.d.ts +10 -0
- package/dist/tui/commands.d.ts +122 -0
- package/dist/tui/components/chrome.d.ts +58 -0
- package/dist/tui/components/overlays.d.ts +64 -0
- package/dist/tui/components/panels.d.ts +72 -0
- package/dist/tui/components/prompt.d.ts +36 -0
- package/dist/tui/format.d.ts +65 -0
- package/dist/tui/history.d.ts +34 -0
- package/dist/tui/index.d.ts +17 -0
- package/dist/tui/layout.d.ts +87 -0
- package/dist/tui/state.d.ts +292 -0
- package/dist/tui/theme.d.ts +66 -0
- package/dist/tui.js +2076 -0
- package/package.json +20 -28
- package/components.json +0 -16
- package/index.html +0 -12
- package/postcss.config.js +0 -3
- package/src/client/App.tsx +0 -96
- package/src/client/components/AgentCard.tsx +0 -60
- package/src/client/components/LogPanel.tsx +0 -43
- package/src/client/components/PipelineBar.tsx +0 -234
- package/src/client/components/ui/badge.tsx +0 -28
- package/src/client/components/ui/button.tsx +0 -43
- package/src/client/components/ui/card.tsx +0 -22
- package/src/client/components/ui/scroll-area.tsx +0 -43
- package/src/client/components/ui/separator.tsx +0 -23
- package/src/client/hooks/useWebSocket.ts +0 -33
- package/src/client/index.css +0 -31
- package/src/client/lib/utils.ts +0 -6
- package/src/client/main.tsx +0 -8
- package/src/client/store/useOrchestrationStore.ts +0 -132
- package/src/server/DashboardServer.ts +0 -56
- package/src/server/WebSocketHub.ts +0 -35
- package/src/server/index.ts +0 -2
- package/tailwind.config.js +0 -50
- package/tsconfig.json +0 -12
- package/tsconfig.server.json +0 -9
- 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
|
+
};
|