@multiclawcli/dashboard 0.4.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/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 +642 -114
- package/package.json +9 -8
- package/dist/tui.d.ts +0 -299
package/dist/tui.js
CHANGED
|
@@ -3,8 +3,8 @@ import { render } from "ink";
|
|
|
3
3
|
import { createElement } from "react";
|
|
4
4
|
|
|
5
5
|
// src/tui/App.tsx
|
|
6
|
-
import * as
|
|
7
|
-
import { isAbortError, Orchestrator } from "@multiclawcli/core";
|
|
6
|
+
import * as path2 from "path";
|
|
7
|
+
import { describeResumePlan, isAbortError, Orchestrator } from "@multiclawcli/core";
|
|
8
8
|
import { Box as Box5, Text as Text5, useApp, useInput, useWindowSize } from "ink";
|
|
9
9
|
import { useCallback, useEffect, useMemo, useReducer, useRef, useState } from "react";
|
|
10
10
|
|
|
@@ -15,7 +15,8 @@ var COMMANDS = [
|
|
|
15
15
|
name: "focus",
|
|
16
16
|
args: "<id>",
|
|
17
17
|
description: "Focus an agent by id",
|
|
18
|
-
availability: "always"
|
|
18
|
+
availability: "always",
|
|
19
|
+
completes: "agent"
|
|
19
20
|
},
|
|
20
21
|
{ name: "clear", description: "Clear the focused agent's log", availability: "always" },
|
|
21
22
|
{
|
|
@@ -23,6 +24,46 @@ var COMMANDS = [
|
|
|
23
24
|
description: "Browse finished turns",
|
|
24
25
|
availability: "always"
|
|
25
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
|
+
},
|
|
26
67
|
{
|
|
27
68
|
name: "cancel",
|
|
28
69
|
description: "Cancel the running turn and its queue",
|
|
@@ -35,6 +76,16 @@ function availableCommands(running) {
|
|
|
35
76
|
const phase = running ? "running" : "idle";
|
|
36
77
|
return COMMANDS.filter((c) => c.availability === "always" || c.availability === phase);
|
|
37
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
|
+
}
|
|
38
89
|
function matchCommands(query, running) {
|
|
39
90
|
const q = query.trim().toLowerCase();
|
|
40
91
|
const pool = availableCommands(running);
|
|
@@ -60,16 +111,117 @@ function parseCommand(text) {
|
|
|
60
111
|
const [name = "", ...args] = text.slice(1).trim().split(/\s+/);
|
|
61
112
|
return name ? { name, args } : void 0;
|
|
62
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
|
+
}
|
|
63
142
|
|
|
64
143
|
// src/tui/components/chrome.tsx
|
|
65
144
|
import { Box, Text } from "ink";
|
|
66
145
|
|
|
67
146
|
// src/tui/format.ts
|
|
147
|
+
import stringWidth from "string-width";
|
|
68
148
|
function truncate(text, max) {
|
|
69
149
|
if (max <= 0) return "";
|
|
70
150
|
if (text.length <= max) return text;
|
|
71
151
|
return `${text.slice(0, Math.max(0, max - 1))}\u2026`;
|
|
72
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
|
+
}
|
|
73
225
|
function formatDuration(ms) {
|
|
74
226
|
if (!Number.isFinite(ms) || ms < 0) return "\u2014";
|
|
75
227
|
if (ms < 6e4) return `${(ms / 1e3).toFixed(1)}s`;
|
|
@@ -86,6 +238,22 @@ function circled(n) {
|
|
|
86
238
|
function padRight(text, width) {
|
|
87
239
|
return text.length >= width ? text : text + " ".repeat(width - text.length);
|
|
88
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
|
+
}
|
|
89
257
|
|
|
90
258
|
// src/tui/layout.ts
|
|
91
259
|
var MIN_COLUMNS = 40;
|
|
@@ -129,18 +297,23 @@ function computeLayout(input) {
|
|
|
129
297
|
queueRows + menuRows + 1 + // prompt
|
|
130
298
|
1;
|
|
131
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);
|
|
132
304
|
return {
|
|
133
305
|
columns,
|
|
134
306
|
rows,
|
|
135
307
|
compact,
|
|
136
|
-
showTeam
|
|
308
|
+
showTeam,
|
|
137
309
|
showRail,
|
|
138
310
|
railRows,
|
|
139
311
|
welcome,
|
|
140
312
|
bodyHeight,
|
|
141
313
|
// The stream spends one of the body's rows on its own header.
|
|
142
314
|
logViewport: Math.max(1, bodyHeight - 1),
|
|
143
|
-
teamWidth
|
|
315
|
+
teamWidth,
|
|
316
|
+
streamWidth,
|
|
144
317
|
menuRows,
|
|
145
318
|
queueRows
|
|
146
319
|
};
|
|
@@ -160,6 +333,9 @@ var STATUS = {
|
|
|
160
333
|
retrying: { glyph: "\u21BB", color: COLOR.warn, label: "retrying" },
|
|
161
334
|
success: { glyph: "\u2713", color: COLOR.ok, label: "done" },
|
|
162
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" },
|
|
163
339
|
skipped: { glyph: "\u2298", color: COLOR.muted, dim: true, label: "skipped" }
|
|
164
340
|
};
|
|
165
341
|
var SPINNER = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
@@ -187,7 +363,16 @@ var GLYPH = {
|
|
|
187
363
|
/** The leader is asking for a decision rather than reporting one. */
|
|
188
364
|
ask: "?",
|
|
189
365
|
/** A mid-turn warning that rides alongside the outcome rather than hiding it. */
|
|
190
|
-
warn: "\u26A0"
|
|
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"
|
|
191
376
|
};
|
|
192
377
|
|
|
193
378
|
// src/tui/components/chrome.tsx
|
|
@@ -304,7 +489,8 @@ var initialState = {
|
|
|
304
489
|
leaderIds: [],
|
|
305
490
|
autoFollow: true,
|
|
306
491
|
scroll: 0,
|
|
307
|
-
history: []
|
|
492
|
+
history: [],
|
|
493
|
+
verbose: false
|
|
308
494
|
};
|
|
309
495
|
function stripAnsi(text) {
|
|
310
496
|
return text.replace(ANSI_PATTERN, "");
|
|
@@ -346,19 +532,20 @@ function seedAgents(config) {
|
|
|
346
532
|
}
|
|
347
533
|
return agents;
|
|
348
534
|
}
|
|
349
|
-
function createInitialState(config) {
|
|
535
|
+
function createInitialState(config, history = []) {
|
|
350
536
|
const agents = seedAgents(config);
|
|
351
537
|
return {
|
|
352
538
|
phase: "idle",
|
|
353
539
|
name: config.name,
|
|
354
|
-
turn: 0,
|
|
540
|
+
turn: history[history.length - 1]?.turn ?? 0,
|
|
355
541
|
stages: [],
|
|
356
542
|
leaderIds: leaderAgentIds(config),
|
|
357
543
|
agents,
|
|
358
544
|
focusId: Object.keys(agents)[0],
|
|
359
545
|
autoFollow: true,
|
|
360
546
|
scroll: 0,
|
|
361
|
-
history
|
|
547
|
+
history,
|
|
548
|
+
verbose: false
|
|
362
549
|
};
|
|
363
550
|
}
|
|
364
551
|
function mergeStageAgents(agents, stages) {
|
|
@@ -393,6 +580,7 @@ function reduceEvent(state, event) {
|
|
|
393
580
|
phase: "running",
|
|
394
581
|
name: event.payload.name,
|
|
395
582
|
requirement: event.payload.requirement ?? state.requirement,
|
|
583
|
+
reason: event.payload.selectionReason,
|
|
396
584
|
stages: event.payload.stages,
|
|
397
585
|
agents: mergeStageAgents(state.agents, event.payload.stages)
|
|
398
586
|
};
|
|
@@ -441,7 +629,8 @@ function reduceEvent(state, event) {
|
|
|
441
629
|
};
|
|
442
630
|
}
|
|
443
631
|
case "agent:output": {
|
|
444
|
-
const { agentId, chunk } = event.payload;
|
|
632
|
+
const { agentId, chunk, kind } = event.payload;
|
|
633
|
+
if (kind === "tool-detail" && !state.verbose) return state;
|
|
445
634
|
const existing = state.agents[agentId];
|
|
446
635
|
if (!existing) return state;
|
|
447
636
|
return {
|
|
@@ -455,20 +644,19 @@ function reduceEvent(state, event) {
|
|
|
455
644
|
case "agent:complete":
|
|
456
645
|
case "agent:failed": {
|
|
457
646
|
const result = event.payload;
|
|
458
|
-
const
|
|
459
|
-
|
|
460
|
-
const next = existing ? {
|
|
461
|
-
...existing,
|
|
462
|
-
status,
|
|
463
|
-
duration: result.duration,
|
|
464
|
-
attempts: result.attempts,
|
|
465
|
-
error: result.error
|
|
466
|
-
} : blankAgent(result.agentId, result.agentName, {
|
|
467
|
-
status,
|
|
647
|
+
const patch = {
|
|
648
|
+
status: result.status,
|
|
468
649
|
duration: result.duration,
|
|
469
650
|
attempts: result.attempts,
|
|
470
|
-
error: result.error
|
|
471
|
-
|
|
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);
|
|
472
660
|
return { ...state, agents: { ...state.agents, [result.agentId]: next } };
|
|
473
661
|
}
|
|
474
662
|
case "agent:skipped": {
|
|
@@ -495,6 +683,7 @@ function reduceEvent(state, event) {
|
|
|
495
683
|
phase: "complete",
|
|
496
684
|
success: event.payload.success,
|
|
497
685
|
totalDuration: event.payload.totalDuration,
|
|
686
|
+
totalCostUsd: event.payload.totalCostUsd,
|
|
498
687
|
endedAt: epoch(event.timestamp) ?? Date.now(),
|
|
499
688
|
reply: event.payload.reply
|
|
500
689
|
};
|
|
@@ -502,9 +691,12 @@ function reduceEvent(state, event) {
|
|
|
502
691
|
return state;
|
|
503
692
|
}
|
|
504
693
|
}
|
|
505
|
-
function
|
|
694
|
+
function wrapLog(logs, width) {
|
|
695
|
+
return logs.split("\n").flatMap((line) => wrapLine(line, width));
|
|
696
|
+
}
|
|
697
|
+
function focusedRowCount(turn, width) {
|
|
506
698
|
const agent = turn.focusId ? turn.agents[turn.focusId] : void 0;
|
|
507
|
-
return agent?.logs ? agent.logs
|
|
699
|
+
return agent?.logs ? wrapLog(agent.logs, width).length : 0;
|
|
508
700
|
}
|
|
509
701
|
function snapshot(state) {
|
|
510
702
|
return {
|
|
@@ -521,6 +713,8 @@ function snapshot(state) {
|
|
|
521
713
|
scroll: state.scroll,
|
|
522
714
|
success: state.success,
|
|
523
715
|
totalDuration: state.totalDuration,
|
|
716
|
+
totalCostUsd: state.totalCostUsd,
|
|
717
|
+
reason: state.reason,
|
|
524
718
|
warning: state.warning,
|
|
525
719
|
reply: state.reply
|
|
526
720
|
};
|
|
@@ -533,9 +727,12 @@ function mapShown(state, fn) {
|
|
|
533
727
|
const next = fn(state);
|
|
534
728
|
return next === state ? state : { ...state, ...next };
|
|
535
729
|
}
|
|
730
|
+
function recordedTurns(state) {
|
|
731
|
+
return [...state.history, ...state.phase === "complete" ? [snapshot(state)] : []];
|
|
732
|
+
}
|
|
536
733
|
function conversationTurns(state) {
|
|
537
734
|
const turns = [];
|
|
538
|
-
for (const turn of
|
|
735
|
+
for (const turn of recordedTurns(state)) {
|
|
539
736
|
if (turn.phase !== "complete" || !turn.requirement) continue;
|
|
540
737
|
turns.push(
|
|
541
738
|
turn.reply ? { requirement: turn.requirement, reply: turn.reply } : { requirement: turn.requirement }
|
|
@@ -549,7 +746,7 @@ function tuiReducer(state, action) {
|
|
|
549
746
|
return reduceEvent(state, action.event);
|
|
550
747
|
case "turn:start": {
|
|
551
748
|
const ids = Object.keys(action.agents);
|
|
552
|
-
const history = state.
|
|
749
|
+
const history = state.requirement !== void 0 ? [...state.history, snapshot(state)].slice(-MAX_HISTORY) : state.history;
|
|
553
750
|
return {
|
|
554
751
|
phase: "running",
|
|
555
752
|
name: state.name,
|
|
@@ -563,11 +760,15 @@ function tuiReducer(state, action) {
|
|
|
563
760
|
autoFollow: true,
|
|
564
761
|
scroll: 0,
|
|
565
762
|
history,
|
|
566
|
-
// A new requirement replaces the previous
|
|
567
|
-
// make the old
|
|
568
|
-
// a past turn ends for the same
|
|
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,
|
|
569
768
|
reply: void 0,
|
|
570
|
-
view: void 0
|
|
769
|
+
view: void 0,
|
|
770
|
+
// A console setting, carried across turns rather than reset with them.
|
|
771
|
+
verbose: state.verbose
|
|
571
772
|
};
|
|
572
773
|
}
|
|
573
774
|
case "focus:cycle":
|
|
@@ -587,11 +788,35 @@ function tuiReducer(state, action) {
|
|
|
587
788
|
});
|
|
588
789
|
case "stream:scroll":
|
|
589
790
|
return mapShown(state, (turn) => {
|
|
590
|
-
const
|
|
591
|
-
const maxScroll = Math.max(0,
|
|
791
|
+
const rows = focusedRowCount(turn, action.width);
|
|
792
|
+
const maxScroll = Math.max(0, rows - Math.max(1, action.viewport));
|
|
592
793
|
const next = Math.min(Math.max(0, turn.scroll + action.delta), maxScroll);
|
|
593
794
|
return next === turn.scroll ? turn : { ...turn, scroll: next };
|
|
594
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
|
+
);
|
|
595
820
|
case "stream:clear":
|
|
596
821
|
return mapShown(state, (turn) => {
|
|
597
822
|
const id = turn.focusId;
|
|
@@ -605,6 +830,8 @@ function tuiReducer(state, action) {
|
|
|
605
830
|
}
|
|
606
831
|
case "view:close":
|
|
607
832
|
return state.view ? { ...state, view: void 0 } : state;
|
|
833
|
+
case "verbose:toggle":
|
|
834
|
+
return { ...state, verbose: !state.verbose };
|
|
608
835
|
}
|
|
609
836
|
}
|
|
610
837
|
function stageStatus(stage, agents) {
|
|
@@ -613,9 +840,15 @@ function stageStatus(stage, agents) {
|
|
|
613
840
|
if (statuses.some((s) => s === "running" || s === "retrying")) return "running";
|
|
614
841
|
if (statuses.length > 0 && statuses.every((s) => s === "success")) return "success";
|
|
615
842
|
if (statuses.length > 0 && statuses.every((s) => s === "skipped")) return "skipped";
|
|
843
|
+
if (statuses.length > 0 && statuses.every((s) => s === "cancelled")) return "cancelled";
|
|
616
844
|
return "pending";
|
|
617
845
|
}
|
|
618
|
-
var SETTLED = /* @__PURE__ */ new Set([
|
|
846
|
+
var SETTLED = /* @__PURE__ */ new Set([
|
|
847
|
+
"success",
|
|
848
|
+
"failed",
|
|
849
|
+
"cancelled",
|
|
850
|
+
"skipped"
|
|
851
|
+
]);
|
|
619
852
|
function progress(turn) {
|
|
620
853
|
const agents = Object.values(turn.agents);
|
|
621
854
|
return { done: agents.filter((a) => SETTLED.has(a.status)).length, total: agents.length };
|
|
@@ -632,15 +865,15 @@ function turnElapsed(turn, now) {
|
|
|
632
865
|
const end = turn.phase === "running" ? now : turn.endedAt ?? now;
|
|
633
866
|
return Math.max(0, end - turn.startedAt);
|
|
634
867
|
}
|
|
635
|
-
function visibleLog(turn, viewport) {
|
|
868
|
+
function visibleLog(turn, viewport, width) {
|
|
636
869
|
const agent = turn.focusId ? turn.agents[turn.focusId] : void 0;
|
|
637
|
-
const all = agent?.logs ? agent.logs
|
|
870
|
+
const all = agent?.logs ? wrapLog(agent.logs, width) : [];
|
|
638
871
|
const height = Math.max(1, viewport);
|
|
639
872
|
const maxScroll = Math.max(0, all.length - height);
|
|
640
873
|
const scrolledBack = Math.min(Math.max(0, turn.scroll), maxScroll);
|
|
641
874
|
const end = all.length - scrolledBack;
|
|
642
875
|
return {
|
|
643
|
-
|
|
876
|
+
rows: all.slice(Math.max(0, end - height), end),
|
|
644
877
|
scrolledBack,
|
|
645
878
|
following: scrolledBack === 0
|
|
646
879
|
};
|
|
@@ -672,9 +905,9 @@ var HELP_GROUPS = [
|
|
|
672
905
|
title: "commands",
|
|
673
906
|
rows: [
|
|
674
907
|
["/", "Command menu"],
|
|
675
|
-
["/history", "Browse finished turns"],
|
|
676
908
|
["?", "This help"],
|
|
677
|
-
["ctrl+c", "Cancel the turn and its queue \xB7 again to quit"]
|
|
909
|
+
["ctrl+c", "Cancel the turn and its queue \xB7 again to quit"],
|
|
910
|
+
["/<command>", `All ${COMMANDS.length} commands \u2014 type / to browse`]
|
|
678
911
|
]
|
|
679
912
|
}
|
|
680
913
|
];
|
|
@@ -684,7 +917,7 @@ var HISTORY_CHROME_ROWS = 4;
|
|
|
684
917
|
function historyCapacity(maxRows) {
|
|
685
918
|
return Math.max(1, Math.floor((maxRows - HISTORY_CHROME_ROWS) / TURN_ROWS));
|
|
686
919
|
}
|
|
687
|
-
function
|
|
920
|
+
function listWindowStart(selected, total, capacity) {
|
|
688
921
|
const maxStart = Math.max(0, total - capacity);
|
|
689
922
|
return Math.min(Math.max(0, Math.min(selected, selected - capacity + 1)), maxStart);
|
|
690
923
|
}
|
|
@@ -722,7 +955,7 @@ function HistoryOverlay({
|
|
|
722
955
|
maxRows
|
|
723
956
|
}) {
|
|
724
957
|
const capacity = historyCapacity(maxRows);
|
|
725
|
-
const start =
|
|
958
|
+
const start = listWindowStart(selected, turns.length, capacity);
|
|
726
959
|
const visible = turns.slice(start, start + capacity);
|
|
727
960
|
const range = turns.length > capacity ? ` \xB7 showing ${start + 1}\u2013${start + visible.length} of ${turns.length}` : "";
|
|
728
961
|
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", flexShrink: 0, paddingLeft: 2, paddingTop: 1, width, children: [
|
|
@@ -803,7 +1036,11 @@ function TeamRow({
|
|
|
803
1036
|
glyph,
|
|
804
1037
|
" ",
|
|
805
1038
|
agent.icon ? `${agent.icon} ` : "",
|
|
806
|
-
agent.name
|
|
1039
|
+
agent.name,
|
|
1040
|
+
agent.taskTitle ? /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
|
|
1041
|
+
" ",
|
|
1042
|
+
agent.taskTitle
|
|
1043
|
+
] }) : null
|
|
807
1044
|
]
|
|
808
1045
|
}
|
|
809
1046
|
) }),
|
|
@@ -856,18 +1093,19 @@ function Divider() {
|
|
|
856
1093
|
function StreamPanel({
|
|
857
1094
|
turn,
|
|
858
1095
|
height,
|
|
1096
|
+
width,
|
|
859
1097
|
now,
|
|
860
1098
|
spinner
|
|
861
1099
|
}) {
|
|
862
1100
|
const agent = turn.focusId ? turn.agents[turn.focusId] : void 0;
|
|
863
1101
|
const viewport = Math.max(1, height - 1);
|
|
864
|
-
const window = visibleLog(turn, viewport);
|
|
1102
|
+
const window = visibleLog(turn, viewport, width);
|
|
865
1103
|
const style = agent ? STATUS[agent.status] : void 0;
|
|
866
1104
|
const elapsed = agent ? agentElapsed(agent, now) : void 0;
|
|
867
1105
|
return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", flexGrow: 1, height, paddingLeft: 1, children: [
|
|
868
1106
|
/* @__PURE__ */ jsxs3(Box3, { justifyContent: "space-between", children: [
|
|
869
|
-
/* @__PURE__ */
|
|
870
|
-
/* @__PURE__ */ jsx3(Text3, { bold: true,
|
|
1107
|
+
/* @__PURE__ */ jsx3(Box3, { flexShrink: 1, children: /* @__PURE__ */ jsxs3(Text3, { wrap: "truncate-end", children: [
|
|
1108
|
+
/* @__PURE__ */ jsx3(Text3, { bold: true, children: agent?.name ?? "\u2014" }),
|
|
871
1109
|
agent?.model ? /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
|
|
872
1110
|
" \xB7 ",
|
|
873
1111
|
agent.model
|
|
@@ -875,8 +1113,23 @@ function StreamPanel({
|
|
|
875
1113
|
agent && style ? /* @__PURE__ */ jsxs3(Fragment, { children: [
|
|
876
1114
|
/* @__PURE__ */ jsx3(Text3, { dimColor: true, children: " \xB7 " }),
|
|
877
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)
|
|
878
1131
|
] }) : null
|
|
879
|
-
] }),
|
|
1132
|
+
] }) }),
|
|
880
1133
|
/* @__PURE__ */ jsxs3(Box3, { flexShrink: 0, children: [
|
|
881
1134
|
elapsed === void 0 ? null : /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
|
|
882
1135
|
formatDuration(elapsed),
|
|
@@ -885,11 +1138,28 @@ function StreamPanel({
|
|
|
885
1138
|
/* @__PURE__ */ jsx3(Text3, { color: window.following ? COLOR.accent : COLOR.warn, children: window.following ? GLYPH.live : `${GLYPH.paused} ${window.scrolledBack}\u2191` })
|
|
886
1139
|
] })
|
|
887
1140
|
] }),
|
|
888
|
-
window.
|
|
889
|
-
//
|
|
890
|
-
//
|
|
891
|
-
|
|
892
|
-
|
|
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
|
+
)
|
|
893
1163
|
] });
|
|
894
1164
|
}
|
|
895
1165
|
function ReplyPanel({ reply, width }) {
|
|
@@ -909,6 +1179,19 @@ function ReplyPanel({ reply, width }) {
|
|
|
909
1179
|
] })
|
|
910
1180
|
);
|
|
911
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
|
+
}
|
|
912
1195
|
function WelcomePanel({ turn, width }) {
|
|
913
1196
|
const agents = Object.values(turn.agents);
|
|
914
1197
|
const leaderId = turn.leaderIds[0];
|
|
@@ -951,10 +1234,11 @@ function Prompt({
|
|
|
951
1234
|
caretVisible,
|
|
952
1235
|
placeholder
|
|
953
1236
|
}) {
|
|
1237
|
+
const shown = value.replace(/\r/g, "").replace(/\n/g, GLYPH.newline);
|
|
954
1238
|
const ghost = value === "";
|
|
955
|
-
const before =
|
|
956
|
-
const cell = ghost ? placeholder.slice(0, 1) :
|
|
957
|
-
const tail = ghost ? placeholder.slice(1) :
|
|
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);
|
|
958
1242
|
return /* @__PURE__ */ jsxs4(Box4, { flexShrink: 0, children: [
|
|
959
1243
|
/* @__PURE__ */ jsxs4(Text4, { color: COLOR.accent, children: [
|
|
960
1244
|
GLYPH.caret,
|
|
@@ -965,14 +1249,10 @@ function Prompt({
|
|
|
965
1249
|
/* @__PURE__ */ jsx4(Text4, { dimColor: ghost, children: tail })
|
|
966
1250
|
] });
|
|
967
1251
|
}
|
|
968
|
-
function CommandMenu({
|
|
969
|
-
const
|
|
970
|
-
|
|
971
|
-
...commands.map((c) => c.name.length + (c.args ? c.args.length + 1 : 0))
|
|
972
|
-
);
|
|
973
|
-
return /* @__PURE__ */ jsx4(Box4, { flexDirection: "column", paddingLeft: 2, children: commands.map((command, index) => {
|
|
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) => {
|
|
974
1255
|
const active = index === selected;
|
|
975
|
-
const label = command.args ? `${command.name} ${command.args}` : command.name;
|
|
976
1256
|
return /* @__PURE__ */ jsxs4(
|
|
977
1257
|
Text4,
|
|
978
1258
|
{
|
|
@@ -981,17 +1261,82 @@ function CommandMenu({ commands, selected }) {
|
|
|
981
1261
|
wrap: "truncate-end",
|
|
982
1262
|
children: [
|
|
983
1263
|
active ? GLYPH.caret : " ",
|
|
984
|
-
" /",
|
|
985
|
-
padRight(label, nameWidth),
|
|
986
1264
|
" ",
|
|
987
|
-
|
|
1265
|
+
padRight(row.label, labelWidth),
|
|
1266
|
+
" ",
|
|
1267
|
+
row.description
|
|
988
1268
|
]
|
|
989
1269
|
},
|
|
990
|
-
|
|
1270
|
+
row.key
|
|
991
1271
|
);
|
|
992
1272
|
}) });
|
|
993
1273
|
}
|
|
994
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
|
+
|
|
995
1340
|
// src/tui/App.tsx
|
|
996
1341
|
import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
997
1342
|
var FLUSH_INTERVAL_MS = 80;
|
|
@@ -1000,6 +1345,19 @@ var BLINK_INTERVAL_MS = 530;
|
|
|
1000
1345
|
var INPUT_PLACEHOLDER = "describe what the team should do\u2026";
|
|
1001
1346
|
var RUNNING_PLACEHOLDER = "type to queue a follow-up\u2026";
|
|
1002
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
|
+
}
|
|
1003
1361
|
function StatusLine({
|
|
1004
1362
|
state,
|
|
1005
1363
|
running,
|
|
@@ -1037,8 +1395,10 @@ function StatusLine({
|
|
|
1037
1395
|
if (state.phase === "complete") {
|
|
1038
1396
|
const time = state.totalDuration === void 0 ? "" : ` ${formatDuration(state.totalDuration)}`;
|
|
1039
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)}`;
|
|
1040
1399
|
return /* @__PURE__ */ jsxs5(Text5, { color: state.success ? COLOR.ok : COLOR.error, wrap: "truncate-end", children: [
|
|
1041
1400
|
outcome2,
|
|
1401
|
+
cost,
|
|
1042
1402
|
state.warning ? ` \xB7 ${GLYPH.warn} ${state.warning}` : ""
|
|
1043
1403
|
] });
|
|
1044
1404
|
}
|
|
@@ -1053,7 +1413,11 @@ function StatusLine({
|
|
|
1053
1413
|
function App({ config, onTurnStart }) {
|
|
1054
1414
|
const { exit } = useApp();
|
|
1055
1415
|
const { columns, rows } = useWindowSize();
|
|
1056
|
-
const [state, dispatch] = useReducer(
|
|
1416
|
+
const [state, dispatch] = useReducer(
|
|
1417
|
+
tuiReducer,
|
|
1418
|
+
config,
|
|
1419
|
+
(c) => createInitialState(c, loadHistory(c.workDir))
|
|
1420
|
+
);
|
|
1057
1421
|
const stateRef = useRef(state);
|
|
1058
1422
|
stateRef.current = state;
|
|
1059
1423
|
const [line, setLine] = useState(EMPTY_LINE);
|
|
@@ -1092,8 +1456,19 @@ function App({ config, onTurnStart }) {
|
|
|
1092
1456
|
const timer = setInterval(() => setCaretOn((on) => !on), BLINK_INTERVAL_MS);
|
|
1093
1457
|
return () => clearInterval(timer);
|
|
1094
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]);
|
|
1095
1470
|
const startTurn = useCallback(
|
|
1096
|
-
async (requirement) => {
|
|
1471
|
+
async (requirement, opts) => {
|
|
1097
1472
|
setError(void 0);
|
|
1098
1473
|
setCancelled(false);
|
|
1099
1474
|
setNotice(void 0);
|
|
@@ -1106,14 +1481,16 @@ function App({ config, onTurnStart }) {
|
|
|
1106
1481
|
startedAt: Date.now()
|
|
1107
1482
|
});
|
|
1108
1483
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
1109
|
-
const logDir =
|
|
1484
|
+
const logDir = path2.join(config.workDir, ".multiclaw", "runs", `run-${stamp}`, "logs");
|
|
1110
1485
|
const runConfig = {
|
|
1111
1486
|
...config,
|
|
1112
1487
|
context: { ...config.context, requirement },
|
|
1113
1488
|
// An answer only reads as a requirement next to the question it answers,
|
|
1114
1489
|
// so the leader is handed the turns behind this one as well.
|
|
1115
1490
|
history: earlierTurns,
|
|
1116
|
-
logDir
|
|
1491
|
+
logDir,
|
|
1492
|
+
// Only a retry carries this; a normal turn lets the leader route.
|
|
1493
|
+
...opts?.resume ? { resume: opts.resume } : {}
|
|
1117
1494
|
};
|
|
1118
1495
|
const controller = new AbortController();
|
|
1119
1496
|
abortRef.current = controller;
|
|
@@ -1145,9 +1522,49 @@ function App({ config, onTurnStart }) {
|
|
|
1145
1522
|
setQueue(rest);
|
|
1146
1523
|
void startTurn(next);
|
|
1147
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
|
+
});
|
|
1148
1561
|
const runCommand = useCallback(
|
|
1149
1562
|
(name, args) => {
|
|
1150
1563
|
setLine(EMPTY_LINE);
|
|
1564
|
+
if (!canRun(name, running)) {
|
|
1565
|
+
setNotice(unavailableReason(name, running));
|
|
1566
|
+
return;
|
|
1567
|
+
}
|
|
1151
1568
|
const shown2 = state.view ?? state;
|
|
1152
1569
|
switch (name) {
|
|
1153
1570
|
case "agents":
|
|
@@ -1156,17 +1573,18 @@ function App({ config, onTurnStart }) {
|
|
|
1156
1573
|
);
|
|
1157
1574
|
return;
|
|
1158
1575
|
case "focus": {
|
|
1159
|
-
const
|
|
1160
|
-
if (!
|
|
1576
|
+
const input = args[0];
|
|
1577
|
+
if (!input) {
|
|
1161
1578
|
setNotice("usage: /focus <agent id>");
|
|
1162
1579
|
return;
|
|
1163
1580
|
}
|
|
1164
|
-
|
|
1165
|
-
|
|
1581
|
+
const resolved = resolveAgentId(input, Object.keys(shown2.agents));
|
|
1582
|
+
if ("error" in resolved) {
|
|
1583
|
+
setNotice(resolved.error);
|
|
1166
1584
|
return;
|
|
1167
1585
|
}
|
|
1168
|
-
dispatch({ type: "focus:set", agentId: id });
|
|
1169
|
-
setNotice(`focused ${id}`);
|
|
1586
|
+
dispatch({ type: "focus:set", agentId: resolved.id });
|
|
1587
|
+
setNotice(`focused ${resolved.id}`);
|
|
1170
1588
|
return;
|
|
1171
1589
|
}
|
|
1172
1590
|
case "clear":
|
|
@@ -1186,6 +1604,97 @@ function App({ config, onTurnStart }) {
|
|
|
1186
1604
|
setHistoryCursor(0);
|
|
1187
1605
|
setHistoryOpen(true);
|
|
1188
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;
|
|
1189
1698
|
case "cancel": {
|
|
1190
1699
|
if (!abortRef.current) {
|
|
1191
1700
|
setNotice("nothing to cancel");
|
|
@@ -1209,7 +1718,7 @@ function App({ config, onTurnStart }) {
|
|
|
1209
1718
|
setNotice(`unknown command: /${name}`);
|
|
1210
1719
|
}
|
|
1211
1720
|
},
|
|
1212
|
-
[config, exit, queue.length, state]
|
|
1721
|
+
[config, exit, layout.logViewport, layout.streamWidth, queue.length, running, state, startTurn]
|
|
1213
1722
|
);
|
|
1214
1723
|
const submit = useCallback(() => {
|
|
1215
1724
|
const text = line.text.trim();
|
|
@@ -1249,31 +1758,9 @@ function App({ config, onTurnStart }) {
|
|
|
1249
1758
|
const moveTo = useCallback((cursor) => {
|
|
1250
1759
|
setLine((l) => ({ ...l, cursor }));
|
|
1251
1760
|
}, []);
|
|
1252
|
-
const
|
|
1253
|
-
const
|
|
1254
|
-
|
|
1255
|
-
[query, running]
|
|
1256
|
-
);
|
|
1257
|
-
const menuIndex = menuCursor.query === query ? menuCursor.index : 0;
|
|
1258
|
-
const selected = matches.length === 0 ? 0 : Math.min(menuIndex, matches.length - 1);
|
|
1259
|
-
const menuOpen = !helpOpen && !historyOpen && query !== void 0 && matches.length > 0;
|
|
1260
|
-
const historyTurns = useMemo(() => [...state.history].reverse(), [state.history]);
|
|
1261
|
-
const historyIndex = historyTurns.length === 0 ? 0 : Math.min(historyCursor, historyTurns.length - 1);
|
|
1262
|
-
const shown = state.view ?? state;
|
|
1263
|
-
const layout = computeLayout({
|
|
1264
|
-
columns,
|
|
1265
|
-
rows,
|
|
1266
|
-
turn: state.turn,
|
|
1267
|
-
overlay: helpOpen || historyOpen,
|
|
1268
|
-
menuRows: menuOpen ? matches.length : 0,
|
|
1269
|
-
queueRows: queue.length,
|
|
1270
|
-
notice: notice !== void 0,
|
|
1271
|
-
viewing: state.view !== void 0,
|
|
1272
|
-
hasRail: shown.leaderIds.length > 0 || shown.stages.length > 0,
|
|
1273
|
-
railCells: railCellWidths(shown)
|
|
1274
|
-
});
|
|
1275
|
-
const menuWindow = matches.slice(0, layout.menuRows);
|
|
1276
|
-
const menuSelected = Math.min(selected, Math.max(0, menuWindow.length - 1));
|
|
1761
|
+
const windowStart = listWindowStart(selected, paletteTotal, layout.menuRows);
|
|
1762
|
+
const paletteWindow = allRows.slice(windowStart, windowStart + layout.menuRows);
|
|
1763
|
+
const paletteSelected = selected - windowStart;
|
|
1277
1764
|
const now = Date.now();
|
|
1278
1765
|
const spinner = SPINNER[tick % SPINNER.length];
|
|
1279
1766
|
const handleKey = (char, key) => {
|
|
@@ -1340,13 +1827,13 @@ function App({ config, onTurnStart }) {
|
|
|
1340
1827
|
dispatch({ type: "focus:cycle", delta: key.shift ? -1 : 1 });
|
|
1341
1828
|
return;
|
|
1342
1829
|
}
|
|
1343
|
-
if (
|
|
1830
|
+
if (paletteOpen) {
|
|
1344
1831
|
if (key.upArrow) {
|
|
1345
|
-
setMenuCursor({ query, index: (menuIndex - 1 +
|
|
1832
|
+
setMenuCursor({ query: paletteKey, index: (menuIndex - 1 + paletteTotal) % paletteTotal });
|
|
1346
1833
|
return;
|
|
1347
1834
|
}
|
|
1348
1835
|
if (key.downArrow) {
|
|
1349
|
-
setMenuCursor({ query, index: (menuIndex + 1) %
|
|
1836
|
+
setMenuCursor({ query: paletteKey, index: (menuIndex + 1) % paletteTotal });
|
|
1350
1837
|
return;
|
|
1351
1838
|
}
|
|
1352
1839
|
if (key.escape) {
|
|
@@ -1354,24 +1841,45 @@ function App({ config, onTurnStart }) {
|
|
|
1354
1841
|
return;
|
|
1355
1842
|
}
|
|
1356
1843
|
if (key.return) {
|
|
1357
|
-
runCommand(
|
|
1844
|
+
if (arg) runCommand(arg.command.name, [allRows[selected].key]);
|
|
1845
|
+
else runCommand(matches[selected].name, []);
|
|
1358
1846
|
return;
|
|
1359
1847
|
}
|
|
1360
1848
|
}
|
|
1361
1849
|
if (key.upArrow) {
|
|
1362
|
-
dispatch({
|
|
1850
|
+
dispatch({
|
|
1851
|
+
type: "stream:scroll",
|
|
1852
|
+
delta: 1,
|
|
1853
|
+
viewport: layout.logViewport,
|
|
1854
|
+
width: layout.streamWidth
|
|
1855
|
+
});
|
|
1363
1856
|
return;
|
|
1364
1857
|
}
|
|
1365
1858
|
if (key.downArrow) {
|
|
1366
|
-
dispatch({
|
|
1859
|
+
dispatch({
|
|
1860
|
+
type: "stream:scroll",
|
|
1861
|
+
delta: -1,
|
|
1862
|
+
viewport: layout.logViewport,
|
|
1863
|
+
width: layout.streamWidth
|
|
1864
|
+
});
|
|
1367
1865
|
return;
|
|
1368
1866
|
}
|
|
1369
1867
|
if (key.pageUp) {
|
|
1370
|
-
dispatch({
|
|
1868
|
+
dispatch({
|
|
1869
|
+
type: "stream:scroll",
|
|
1870
|
+
delta: layout.logViewport,
|
|
1871
|
+
viewport: layout.logViewport,
|
|
1872
|
+
width: layout.streamWidth
|
|
1873
|
+
});
|
|
1371
1874
|
return;
|
|
1372
1875
|
}
|
|
1373
1876
|
if (key.pageDown) {
|
|
1374
|
-
dispatch({
|
|
1877
|
+
dispatch({
|
|
1878
|
+
type: "stream:scroll",
|
|
1879
|
+
delta: -layout.logViewport,
|
|
1880
|
+
viewport: layout.logViewport,
|
|
1881
|
+
width: layout.streamWidth
|
|
1882
|
+
});
|
|
1375
1883
|
return;
|
|
1376
1884
|
}
|
|
1377
1885
|
if (char === "?" && line.text === "" && !running) {
|
|
@@ -1399,6 +1907,10 @@ function App({ config, onTurnStart }) {
|
|
|
1399
1907
|
return;
|
|
1400
1908
|
}
|
|
1401
1909
|
if (key.escape) {
|
|
1910
|
+
if (error) {
|
|
1911
|
+
setError(void 0);
|
|
1912
|
+
return;
|
|
1913
|
+
}
|
|
1402
1914
|
if (state.view) {
|
|
1403
1915
|
dispatch({ type: "view:close" });
|
|
1404
1916
|
return;
|
|
@@ -1415,12 +1927,12 @@ function App({ config, onTurnStart }) {
|
|
|
1415
1927
|
deleteForward();
|
|
1416
1928
|
return;
|
|
1417
1929
|
}
|
|
1418
|
-
if (char && !key.ctrl && !key.meta) insert(char.replace(
|
|
1930
|
+
if (char && !key.ctrl && !key.meta) insert(char.replace(/\r\n?/g, "\n"));
|
|
1419
1931
|
};
|
|
1420
1932
|
const handlerRef = useRef(handleKey);
|
|
1421
1933
|
handlerRef.current = handleKey;
|
|
1422
1934
|
useInput((char, key) => handlerRef.current(char, key));
|
|
1423
|
-
const keys = helpOpen ? "esc close" : historyOpen ? "\u2191\u2193 select \xB7 enter open \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";
|
|
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";
|
|
1424
1936
|
return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", height: layout.rows, children: [
|
|
1425
1937
|
/* @__PURE__ */ jsx5(
|
|
1426
1938
|
Header,
|
|
@@ -1437,6 +1949,7 @@ function App({ config, onTurnStart }) {
|
|
|
1437
1949
|
// the chrome below it off the screen.
|
|
1438
1950
|
/* @__PURE__ */ jsx5(Box5, { marginTop: 1, height: layout.railRows, overflow: "hidden", flexShrink: 0, children: /* @__PURE__ */ jsx5(PipelineRail, { turn: shown, spinner }) })
|
|
1439
1951
|
) : null,
|
|
1952
|
+
shown.reason ? /* @__PURE__ */ jsx5(Text5, { dimColor: true, wrap: "truncate-end", children: ` reason: ${shown.reason}` }) : null,
|
|
1440
1953
|
/* @__PURE__ */ jsx5(
|
|
1441
1954
|
Box5,
|
|
1442
1955
|
{
|
|
@@ -1453,12 +1966,26 @@ function App({ config, onTurnStart }) {
|
|
|
1453
1966
|
width: layout.columns,
|
|
1454
1967
|
maxRows: layout.bodyHeight
|
|
1455
1968
|
}
|
|
1456
|
-
) : layout.welcome ? /* @__PURE__ */ jsx5(WelcomePanel, { turn: state, width: layout.columns }) :
|
|
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: [
|
|
1457
1975
|
layout.showTeam ? /* @__PURE__ */ jsxs5(Fragment2, { children: [
|
|
1458
1976
|
/* @__PURE__ */ jsx5(TeamPanel, { turn: shown, width: layout.teamWidth, now, spinner }),
|
|
1459
1977
|
/* @__PURE__ */ jsx5(Divider, {})
|
|
1460
1978
|
] }) : null,
|
|
1461
|
-
/* @__PURE__ */ jsx5(
|
|
1979
|
+
/* @__PURE__ */ jsx5(
|
|
1980
|
+
StreamPanel,
|
|
1981
|
+
{
|
|
1982
|
+
turn: shown,
|
|
1983
|
+
height: layout.bodyHeight,
|
|
1984
|
+
width: layout.streamWidth,
|
|
1985
|
+
now,
|
|
1986
|
+
spinner
|
|
1987
|
+
}
|
|
1988
|
+
)
|
|
1462
1989
|
] })
|
|
1463
1990
|
}
|
|
1464
1991
|
),
|
|
@@ -1468,7 +1995,7 @@ function App({ config, onTurnStart }) {
|
|
|
1468
1995
|
notice
|
|
1469
1996
|
] }) : null,
|
|
1470
1997
|
/* @__PURE__ */ jsx5(QueueStrip, { items: queue, max: layout.queueRows }),
|
|
1471
|
-
|
|
1998
|
+
paletteOpen ? /* @__PURE__ */ jsx5(CommandMenu, { rows: paletteWindow, selected: paletteSelected }) : null,
|
|
1472
1999
|
/* @__PURE__ */ jsx5(
|
|
1473
2000
|
Prompt,
|
|
1474
2001
|
{
|
|
@@ -1537,6 +2064,7 @@ export {
|
|
|
1537
2064
|
measureRailRows,
|
|
1538
2065
|
plannedAgents,
|
|
1539
2066
|
progress,
|
|
2067
|
+
recordedTurns,
|
|
1540
2068
|
reduceEvent,
|
|
1541
2069
|
seedAgents,
|
|
1542
2070
|
stageStatus,
|