@viraatdas/rudder 0.1.2 → 0.3.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 +51 -17
- package/dist/brain.js +12 -3
- package/dist/brain.js.map +1 -1
- package/dist/main.js +22 -2
- package/dist/main.js.map +1 -1
- package/dist/run-manager.d.ts +14 -4
- package/dist/run-manager.js +255 -66
- package/dist/run-manager.js.map +1 -1
- package/dist/state.d.ts +1 -0
- package/dist/state.js +7 -0
- package/dist/state.js.map +1 -1
- package/dist/tui.d.ts +9 -0
- package/dist/tui.js +721 -0
- package/dist/tui.js.map +1 -0
- package/dist/types.d.ts +14 -2
- package/package.json +6 -1
package/dist/tui.js
ADDED
|
@@ -0,0 +1,721 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import fsp from "node:fs/promises";
|
|
3
|
+
import { useCallback, useEffect, useState } from "react";
|
|
4
|
+
import { Box, Text, render, useApp, useInput, useWindowSize } from "ink";
|
|
5
|
+
import { currentBranch, findRepoRoot } from "./git.js";
|
|
6
|
+
import { eventsPath, listRuns, loadConfig, outputPath, } from "./state.js";
|
|
7
|
+
import { continueRun, mergeRun, startRun, stopRun } from "./run-manager.js";
|
|
8
|
+
import { pathExists, shortenHome } from "./util.js";
|
|
9
|
+
const BACKENDS = ["claude", "codex", "acpx"];
|
|
10
|
+
export async function runInteractiveTui(defaults) {
|
|
11
|
+
const instance = render(_jsx(RudderTui, { defaults: defaults ?? {} }), {
|
|
12
|
+
alternateScreen: true,
|
|
13
|
+
exitOnCtrlC: false,
|
|
14
|
+
maxFps: 60,
|
|
15
|
+
});
|
|
16
|
+
await instance.waitUntilExit();
|
|
17
|
+
}
|
|
18
|
+
function RudderTui({ defaults }) {
|
|
19
|
+
const app = useApp();
|
|
20
|
+
const size = useWindowSize();
|
|
21
|
+
const [repoRoot, setRepoRoot] = useState(() => findRepoRoot());
|
|
22
|
+
const [branch, setBranch] = useState("HEAD");
|
|
23
|
+
const [config, setConfig] = useState(null);
|
|
24
|
+
const [backend, setBackend] = useState(defaults.backend ?? "claude");
|
|
25
|
+
const [model, setModel] = useState(defaults.model);
|
|
26
|
+
const [worktreeMode, setWorktreeMode] = useState(defaults.worktree === false ? "auto" : "always");
|
|
27
|
+
const [runs, setRuns] = useState([]);
|
|
28
|
+
const [selectedRunId, setSelectedRunId] = useState();
|
|
29
|
+
const [targetRunId, setTargetRunId] = useState();
|
|
30
|
+
const [expandedRunIds, setExpandedRunIds] = useState(new Set());
|
|
31
|
+
const [transcriptExpanded, setTranscriptExpanded] = useState(false);
|
|
32
|
+
const [input, setInput] = useState("");
|
|
33
|
+
const [notice, setNotice] = useState("Ready");
|
|
34
|
+
const [helpOpen, setHelpOpen] = useState(false);
|
|
35
|
+
const [submitting, setSubmitting] = useState(false);
|
|
36
|
+
const [preferencesLoaded, setPreferencesLoaded] = useState(false);
|
|
37
|
+
const refresh = useCallback(async () => {
|
|
38
|
+
const root = findRepoRoot();
|
|
39
|
+
const [nextConfig, nextBranch, nextRuns] = await Promise.all([
|
|
40
|
+
loadConfig(),
|
|
41
|
+
currentBranch(root),
|
|
42
|
+
loadUiRuns(root),
|
|
43
|
+
]);
|
|
44
|
+
setRepoRoot(root);
|
|
45
|
+
setConfig(nextConfig);
|
|
46
|
+
setBranch(nextBranch);
|
|
47
|
+
setRuns(nextRuns);
|
|
48
|
+
if (!preferencesLoaded) {
|
|
49
|
+
setBackend(defaults.backend ?? nextConfig.lastUsedBackend ?? nextConfig.defaultBackend);
|
|
50
|
+
setModel(defaults.model);
|
|
51
|
+
setPreferencesLoaded(true);
|
|
52
|
+
}
|
|
53
|
+
setSelectedRunId((current) => current ?? nextRuns[0]?.id);
|
|
54
|
+
}, [defaults.backend, defaults.model, preferencesLoaded]);
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
void refresh();
|
|
57
|
+
const timer = setInterval(() => {
|
|
58
|
+
void refresh();
|
|
59
|
+
}, input ? 1500 : 900);
|
|
60
|
+
return () => clearInterval(timer);
|
|
61
|
+
}, [input, refresh]);
|
|
62
|
+
const selectedIndex = Math.max(0, runs.findIndex((run) => run.id === selectedRunId));
|
|
63
|
+
const selectedRun = runs[selectedIndex];
|
|
64
|
+
const targetRun = targetRunId ? runs.find((run) => run.id === targetRunId) : undefined;
|
|
65
|
+
const activeCount = runs.filter((run) => isActive(run.status)).length;
|
|
66
|
+
const selectedExpanded = Boolean(selectedRun && expandedRunIds.has(selectedRun.id));
|
|
67
|
+
const submitTask = useCallback(async (task) => {
|
|
68
|
+
const trimmed = task.trim();
|
|
69
|
+
if (!trimmed || submitting) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
setSubmitting(true);
|
|
73
|
+
if (targetRun) {
|
|
74
|
+
setNotice(`Sending to ${shortId(targetRun.id)}...`);
|
|
75
|
+
try {
|
|
76
|
+
const run = await continueRun({
|
|
77
|
+
runId: targetRun.id,
|
|
78
|
+
prompt: trimmed,
|
|
79
|
+
silent: true,
|
|
80
|
+
});
|
|
81
|
+
setInput("");
|
|
82
|
+
setSelectedRunId(run.id);
|
|
83
|
+
setExpandedRunIds((current) => new Set(current).add(run.id));
|
|
84
|
+
setNotice(`Sent to ${shortId(run.id)}`);
|
|
85
|
+
await refresh();
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
setNotice(error instanceof Error ? error.message : String(error));
|
|
89
|
+
}
|
|
90
|
+
finally {
|
|
91
|
+
setSubmitting(false);
|
|
92
|
+
}
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
setNotice(`Starting ${backend}...`);
|
|
96
|
+
try {
|
|
97
|
+
const run = await startRun({
|
|
98
|
+
task: trimmed,
|
|
99
|
+
backend,
|
|
100
|
+
model,
|
|
101
|
+
detach: true,
|
|
102
|
+
worktree: worktreeMode === "always",
|
|
103
|
+
silent: true,
|
|
104
|
+
view: "shell",
|
|
105
|
+
});
|
|
106
|
+
setInput("");
|
|
107
|
+
setSelectedRunId(run.id);
|
|
108
|
+
setExpandedRunIds((current) => new Set(current).add(run.id));
|
|
109
|
+
setNotice(`Started ${run.id}`);
|
|
110
|
+
await refresh();
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
setNotice(error instanceof Error ? error.message : String(error));
|
|
114
|
+
}
|
|
115
|
+
finally {
|
|
116
|
+
setSubmitting(false);
|
|
117
|
+
}
|
|
118
|
+
}, [backend, model, refresh, submitting, targetRun, worktreeMode]);
|
|
119
|
+
const handleCommand = useCallback(async (line) => {
|
|
120
|
+
const [command = "", ...args] = line.slice(1).trim().split(/\s+/).filter(Boolean);
|
|
121
|
+
switch (command) {
|
|
122
|
+
case "":
|
|
123
|
+
case "help":
|
|
124
|
+
case "?":
|
|
125
|
+
setHelpOpen((value) => !value);
|
|
126
|
+
setInput("");
|
|
127
|
+
return;
|
|
128
|
+
case "q":
|
|
129
|
+
case "quit":
|
|
130
|
+
case "exit":
|
|
131
|
+
app.exit();
|
|
132
|
+
return;
|
|
133
|
+
case "backend":
|
|
134
|
+
if (isBackend(args[0])) {
|
|
135
|
+
chooseBackend(args[0], setBackend, setModel, setNotice);
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
setNotice("Usage: /backend claude|codex|acpx");
|
|
139
|
+
}
|
|
140
|
+
setInput("");
|
|
141
|
+
return;
|
|
142
|
+
case "agent":
|
|
143
|
+
case "continue": {
|
|
144
|
+
const run = resolveUiRun(runs, args[0] ?? selectedRun?.id);
|
|
145
|
+
if (run) {
|
|
146
|
+
setTargetRunId(run.id);
|
|
147
|
+
setSelectedRunId(run.id);
|
|
148
|
+
setNotice(`Typing to ${shortId(run.id)}`);
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
setNotice("No agent selected");
|
|
152
|
+
}
|
|
153
|
+
setInput("");
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
case "new":
|
|
157
|
+
setTargetRunId(undefined);
|
|
158
|
+
setNotice("Typing starts a new agent");
|
|
159
|
+
setInput("");
|
|
160
|
+
return;
|
|
161
|
+
case "model":
|
|
162
|
+
setModel(args.join(" ") || undefined);
|
|
163
|
+
setNotice(args.length ? `Model ${args.join(" ")}` : "Using backend default model");
|
|
164
|
+
setInput("");
|
|
165
|
+
return;
|
|
166
|
+
case "worktree":
|
|
167
|
+
setWorktreeMode(args[0] === "always" || args[0] === "on" ? "always" : "auto");
|
|
168
|
+
setNotice(`Worktrees ${args[0] === "always" || args[0] === "on" ? "always" : "auto"}`);
|
|
169
|
+
setInput("");
|
|
170
|
+
return;
|
|
171
|
+
case "stop":
|
|
172
|
+
await runAction(args[0] ?? selectedRun?.id, async (id) => stopRun(id, { silent: true }), "Stopped", setNotice, refresh);
|
|
173
|
+
setInput("");
|
|
174
|
+
return;
|
|
175
|
+
case "merge":
|
|
176
|
+
await runAction(args[0] ?? selectedRun?.id, async (id) => mergeRun(id, args.includes("--allow-dirty"), { silent: true }), "Merged", setNotice, refresh);
|
|
177
|
+
setInput("");
|
|
178
|
+
return;
|
|
179
|
+
case "merge-all":
|
|
180
|
+
await mergeReadyRuns(runs, args.includes("--allow-dirty"), setNotice, refresh);
|
|
181
|
+
setInput("");
|
|
182
|
+
return;
|
|
183
|
+
case "clear":
|
|
184
|
+
setExpandedRunIds(new Set());
|
|
185
|
+
setNotice("Collapsed all runs");
|
|
186
|
+
setInput("");
|
|
187
|
+
return;
|
|
188
|
+
default:
|
|
189
|
+
setNotice(`Unknown command: /${command}`);
|
|
190
|
+
setInput("");
|
|
191
|
+
}
|
|
192
|
+
}, [app, refresh, runs, selectedRun?.id]);
|
|
193
|
+
useInput((value, key) => {
|
|
194
|
+
if (key.ctrl && value === "c") {
|
|
195
|
+
app.exit();
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (key.escape) {
|
|
199
|
+
if (input) {
|
|
200
|
+
setInput("");
|
|
201
|
+
}
|
|
202
|
+
else if (helpOpen) {
|
|
203
|
+
setHelpOpen(false);
|
|
204
|
+
}
|
|
205
|
+
else if (transcriptExpanded) {
|
|
206
|
+
setTranscriptExpanded(false);
|
|
207
|
+
}
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
if (key.tab || value === "\t") {
|
|
211
|
+
cycleBackend(backend, setBackend, setModel, setNotice);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
if (key.upArrow || (input.length === 0 && value === "k")) {
|
|
215
|
+
selectRelative(runs, selectedRunId, -1, setSelectedRunId);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (key.downArrow || (input.length === 0 && value === "j")) {
|
|
219
|
+
selectRelative(runs, selectedRunId, 1, setSelectedRunId);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
if (key.pageUp) {
|
|
223
|
+
selectRelative(runs, selectedRunId, -5, setSelectedRunId);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
if (key.pageDown) {
|
|
227
|
+
selectRelative(runs, selectedRunId, 5, setSelectedRunId);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
if (key.return) {
|
|
231
|
+
if (input.trim().startsWith("/")) {
|
|
232
|
+
void handleCommand(input);
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
void submitTask(input);
|
|
236
|
+
}
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
if ((key.meta && (key.backspace || key.delete)) || value === "\u001b\u007f" || value === "\u001b\b") {
|
|
240
|
+
setInput((current) => deletePreviousWord(current));
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
if (key.ctrl && value === "w") {
|
|
244
|
+
setInput((current) => deletePreviousWord(current));
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
if (key.backspace || key.delete || value === "\u007f" || value === "\b") {
|
|
248
|
+
setInput((current) => current.slice(0, -1));
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
if (input.length === 0 && value === "q") {
|
|
252
|
+
app.exit();
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
if (input.length === 0 && value === "?") {
|
|
256
|
+
setHelpOpen((current) => !current);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
if (input.length === 0 && value === "r") {
|
|
260
|
+
void refresh();
|
|
261
|
+
setNotice("Refreshed");
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
if (input.length === 0 && value === "b") {
|
|
265
|
+
cycleBackend(backend, setBackend, setModel, setNotice);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
if (input.length === 0 && value === "c" && selectedRun) {
|
|
269
|
+
setTargetRunId(selectedRun.id);
|
|
270
|
+
setNotice(`Typing to ${shortId(selectedRun.id)}`);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
if (input.length === 0 && value === "n") {
|
|
274
|
+
setTargetRunId(undefined);
|
|
275
|
+
setNotice("Typing starts a new agent");
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
if (input.length === 0 && value === "w") {
|
|
279
|
+
setWorktreeMode((current) => current === "auto" ? "always" : "auto");
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
if (input.length === 0 && value === "x" && selectedRun) {
|
|
283
|
+
setExpandedRunIds((current) => toggleSet(current, selectedRun.id));
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
if (input.length === 0 && value === "l" && selectedRun) {
|
|
287
|
+
setTranscriptExpanded((current) => !current);
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
if (input.length === 0 && value === "s" && selectedRun) {
|
|
291
|
+
void runAction(selectedRun.id, async (id) => stopRun(id, { silent: true }), "Stopped", setNotice, refresh);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
if (input.length === 0 && value === "m" && selectedRun) {
|
|
295
|
+
void runAction(selectedRun.id, async (id) => mergeRun(id, false, { silent: true }), "Merged", setNotice, refresh);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
if (input.length === 0 && value === "M") {
|
|
299
|
+
void mergeReadyRuns(runs, false, setNotice, refresh);
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
if (isPrintable(value)) {
|
|
303
|
+
setInput((current) => current + value);
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
const width = Math.max(80, size.columns);
|
|
307
|
+
const height = Math.max(24, size.rows);
|
|
308
|
+
const railWidth = Math.min(42, Math.max(30, Math.floor(width * 0.34)));
|
|
309
|
+
const detailWidth = Math.max(30, width - railWidth - 1);
|
|
310
|
+
const detailHeight = Math.max(8, height - 8);
|
|
311
|
+
return (_jsxs(Box, { flexDirection: "column", width: width, height: height, children: [_jsx(Header, { width: width, repoRoot: repoRoot, branch: branch, backend: backend, model: model ?? modelForBackend(backend, config), activeCount: activeCount, worktreeMode: worktreeMode }), _jsxs(Box, { flexGrow: 1, minHeight: 0, children: [_jsx(RunRail, { runs: runs, selectedRunId: selectedRun?.id, targetRunId: targetRunId, width: railWidth, expandedRunIds: expandedRunIds }), _jsx(Box, { flexDirection: "column", flexGrow: 1, marginLeft: 1, children: _jsx(DetailPane, { run: selectedRun, width: detailWidth, height: detailHeight, expanded: selectedExpanded, transcriptExpanded: transcriptExpanded }) })] }), helpOpen ? _jsx(Help, {}) : null, _jsx(PromptDock, { input: input, backend: backend, model: model ?? modelForBackend(backend, config), notice: notice, submitting: submitting, targetRun: targetRun }), _jsx(Footer, {})] }));
|
|
312
|
+
}
|
|
313
|
+
function Header(props) {
|
|
314
|
+
const contentWidth = Math.max(10, props.width - 4);
|
|
315
|
+
const label = `rudder ${shortenHome(props.repoRoot)} ${props.branch} | ${props.backend}${props.model ? ` ${props.model}` : ""} | worktree:${props.worktreeMode} active:${props.activeCount}`;
|
|
316
|
+
return (_jsx(Box, { borderStyle: "single", borderColor: "cyan", paddingX: 1, children: _jsx(Text, { bold: true, color: "cyan", children: fitLine(label, contentWidth) }) }));
|
|
317
|
+
}
|
|
318
|
+
function RunRail(props) {
|
|
319
|
+
const visible = props.runs.slice(0, 12);
|
|
320
|
+
return (_jsxs(Box, { flexDirection: "column", width: props.width, borderStyle: "single", borderColor: "gray", paddingX: 1, children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsx(Text, { bold: true, children: "agents" }), _jsxs(Text, { color: "gray", children: [props.runs.length, " runs"] })] }), visible.length === 0 ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", children: "No runs yet. Type a task below." }) })) : null, visible.map((run) => (_jsx(RunCard, { run: run, selected: run.id === props.selectedRunId, targeted: run.id === props.targetRunId, expanded: props.expandedRunIds.has(run.id), width: props.width - 4 }, run.id)))] }));
|
|
321
|
+
}
|
|
322
|
+
function RunCard(props) {
|
|
323
|
+
const tone = statusColor(props.run.status);
|
|
324
|
+
const label = props.selected ? ">" : " ";
|
|
325
|
+
const task = truncate(props.run.task, Math.max(12, props.width - 14));
|
|
326
|
+
const progress = completionPercent(props.run);
|
|
327
|
+
const summary = truncate(runSummary(props.run), Math.max(12, props.width - 8));
|
|
328
|
+
const meta = `${progressBar(progress)} ${progress}%`;
|
|
329
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { wrap: "truncate", color: props.selected ? "white" : "gray", bold: props.selected, children: [label, " ", meta, " ", statusGlyph(props.run.status), " ", props.run.backend, " ", task] }), _jsxs(Text, { wrap: "truncate", color: tone, children: [" ", props.run.worktree.enabled ? "wt" : "co", " ", props.targeted ? "typing " : "", summary] })] }));
|
|
330
|
+
}
|
|
331
|
+
function DetailPane(props) {
|
|
332
|
+
if (!props.run) {
|
|
333
|
+
return (_jsx(Box, { width: props.width, height: props.height, borderStyle: "single", borderColor: "gray", paddingX: 1, flexDirection: "column", children: _jsx(Text, { color: "gray", children: "No agent selected." }) }));
|
|
334
|
+
}
|
|
335
|
+
const workLimit = props.expanded ? 10 : 5;
|
|
336
|
+
const outputHeight = props.transcriptExpanded ? Math.max(8, props.height - 7) : Math.max(5, Math.floor(props.height * 0.45));
|
|
337
|
+
const contentWidth = Math.max(10, props.width - 4);
|
|
338
|
+
const progress = completionPercent(props.run);
|
|
339
|
+
return (_jsxs(Box, { width: props.width, height: props.height, borderStyle: "single", borderColor: statusColor(props.run.status), paddingX: 1, flexDirection: "column", children: [_jsx(Text, { children: " " }), _jsx(Text, { wrap: "truncate", color: statusColor(props.run.status), children: fitLine(`${props.run.status} ${progress}% ${props.run.backend} ${shortId(props.run.id)} ${props.run.task}`, contentWidth) }), _jsx(Text, { wrap: "truncate", color: "gray", children: fitLine(props.run.worktree.enabled ? shortenHome(props.run.worktree.path) : "current checkout", contentWidth) }), _jsx(Text, { wrap: "truncate", color: canMerge(props.run) ? "green" : "gray", children: fitLine(canMerge(props.run) ? "[m] merge this [M] merge all ready" : runSummary(props.run), contentWidth) }), !props.transcriptExpanded ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { bold: true, children: "work" }), props.run.work.slice(-workLimit).map((item, index) => (_jsx(Text, { color: toneColor(item.tone), wrap: "truncate", children: formatWorkLine(item, contentWidth) }, `${item.label}-${index}`))), props.run.work.length === 0 ? _jsx(Text, { color: "gray", children: "No worker events yet." }) : null] })) : null, _jsxs(Box, { flexDirection: "column", marginTop: 1, minHeight: 0, children: [_jsx(Text, { bold: true, children: "transcript" }), _jsx(Box, { height: outputHeight, overflow: "hidden", flexDirection: "column", children: tailLines(props.run.output, outputHeight).map((line, index) => (_jsx(Text, { wrap: "truncate", children: line || " " }, index))) })] })] }));
|
|
340
|
+
}
|
|
341
|
+
function Help() {
|
|
342
|
+
return (_jsxs(Box, { borderStyle: "single", borderColor: "yellow", paddingX: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, color: "yellow", children: "keys" }), _jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: "Enter" }), " submit task or slash command ", _jsx(Text, { color: "cyan", children: "Tab" }), " switch backend ", _jsx(Text, { color: "cyan", children: "j/k" }), " or arrows select run"] }), _jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: "x" }), " expand/collapse ", _jsx(Text, { color: "cyan", children: "l" }), " transcript ", _jsx(Text, { color: "cyan", children: "c" }), " type to selected ", _jsx(Text, { color: "cyan", children: "n" }), " new agent"] }), _jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: "w" }), " worktree auto/always ", _jsx(Text, { color: "cyan", children: "s" }), " stop ", _jsx(Text, { color: "cyan", children: "m" }), " merge selected ", _jsx(Text, { color: "cyan", children: "M" }), " merge all ready"] }), _jsx(Text, { color: "gray", children: "Slash: /backend claude|codex|acpx, /model <name>, /agent, /new, /worktree, /stop, /merge, /merge-all, /exit" })] }));
|
|
343
|
+
}
|
|
344
|
+
function PromptDock(props) {
|
|
345
|
+
const label = props.targetRun ? `agent ${shortId(props.targetRun.id)}` : "task";
|
|
346
|
+
return (_jsxs(Box, { borderStyle: "single", borderColor: "cyan", paddingX: 1, justifyContent: "space-between", children: [_jsxs(Text, { children: [_jsx(Text, { color: props.submitting ? "yellow" : "cyan", children: props.submitting ? "starting" : label }), _jsxs(Text, { children: [" ", props.input] }), _jsx(Text, { color: "cyan", children: "_" })] }), _jsxs(Text, { color: "gray", children: [props.notice, " ", props.backend, props.model ? ` ${props.model}` : ""] })] }));
|
|
347
|
+
}
|
|
348
|
+
function Footer() {
|
|
349
|
+
return (_jsx(Box, { children: _jsx(Text, { color: "gray", children: "Enter submit Tab backend c continue n new j/k m/M merge ? help q quit" }) }));
|
|
350
|
+
}
|
|
351
|
+
async function loadUiRuns(repoRoot) {
|
|
352
|
+
const runs = await listRuns(repoRoot);
|
|
353
|
+
return await Promise.all(runs.map(async (run) => {
|
|
354
|
+
const [output, events] = await Promise.all([
|
|
355
|
+
readTextIfExists(outputPath(repoRoot, run.id)),
|
|
356
|
+
readEvents(repoRoot, run.id),
|
|
357
|
+
]);
|
|
358
|
+
return {
|
|
359
|
+
...run,
|
|
360
|
+
output,
|
|
361
|
+
events,
|
|
362
|
+
work: buildWork(events, run),
|
|
363
|
+
};
|
|
364
|
+
}));
|
|
365
|
+
}
|
|
366
|
+
async function readTextIfExists(file) {
|
|
367
|
+
if (!(await pathExists(file))) {
|
|
368
|
+
return "";
|
|
369
|
+
}
|
|
370
|
+
return await fsp.readFile(file, "utf8").catch(() => "");
|
|
371
|
+
}
|
|
372
|
+
async function readEvents(repoRoot, runId) {
|
|
373
|
+
const file = eventsPath(repoRoot, runId);
|
|
374
|
+
const raw = await readTextIfExists(file);
|
|
375
|
+
return raw
|
|
376
|
+
.split(/\r?\n/)
|
|
377
|
+
.map((line) => line.trim())
|
|
378
|
+
.filter(Boolean)
|
|
379
|
+
.map((line) => {
|
|
380
|
+
try {
|
|
381
|
+
return JSON.parse(line);
|
|
382
|
+
}
|
|
383
|
+
catch {
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
386
|
+
})
|
|
387
|
+
.filter((event) => Boolean(event));
|
|
388
|
+
}
|
|
389
|
+
function buildWork(events, run) {
|
|
390
|
+
const items = [];
|
|
391
|
+
for (const event of events) {
|
|
392
|
+
if (event.type === "run.created") {
|
|
393
|
+
items.push({ label: run.worktree.enabled ? "worktree prepared" : "checkout claimed", detail: event.message, tone: "info" });
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
if (event.type === "run.continued") {
|
|
397
|
+
items.push({ label: "user follow-up", detail: event.message, tone: "info" });
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
if (event.type === "steerer.waiting") {
|
|
401
|
+
items.push({ label: "auto-steering wait", detail: "10s grace period", tone: "warning" });
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
if (event.type === "steerer.prompt") {
|
|
405
|
+
items.push({ label: "auto-steering", detail: "review prompt sent", tone: "info" });
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
if (event.type === "planner.spec") {
|
|
409
|
+
items.push({ label: "planner contract", detail: "acceptance criteria generated", tone: "info" });
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
if (event.type === "run.started") {
|
|
413
|
+
items.push({ label: "worker started", detail: objectField(event.data, "command") ?? run.backend, tone: "info" });
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
if (event.type === "backend.output") {
|
|
417
|
+
const tool = toolSummary(event.data);
|
|
418
|
+
if (tool) {
|
|
419
|
+
items.push(tool);
|
|
420
|
+
}
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
if (event.type === "backend.error") {
|
|
424
|
+
items.push({ label: "backend error", detail: event.message, tone: "danger" });
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
if (event.type === "verifier.result") {
|
|
428
|
+
const missing = missingCount(event.data);
|
|
429
|
+
items.push({ label: "verifier", detail: missing ? `${missing} missing item${missing === 1 ? "" : "s"}` : "accepted", tone: missing ? "warning" : "success" });
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
if (event.type === "run.completed") {
|
|
433
|
+
items.push({ label: "completed", detail: event.message, tone: "success" });
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
if (event.type === "run.failed") {
|
|
437
|
+
items.push({ label: "failed", detail: event.message, tone: "danger" });
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
if (event.type === "run.cancelled") {
|
|
441
|
+
items.push({ label: "cancelled", detail: event.message, tone: "warning" });
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
if (event.type === "merge.result") {
|
|
445
|
+
items.push({ label: "merge", detail: event.message, tone: event.message?.includes("conflict") ? "warning" : "success" });
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
return compactWork(items);
|
|
449
|
+
}
|
|
450
|
+
function toolSummary(data) {
|
|
451
|
+
if (!isRecord(data) || data.type !== "stream_event" || !isRecord(data.event)) {
|
|
452
|
+
return null;
|
|
453
|
+
}
|
|
454
|
+
const event = data.event;
|
|
455
|
+
if (event.type === "content_block_start" && isRecord(event.content_block)) {
|
|
456
|
+
const block = event.content_block;
|
|
457
|
+
if (block.type === "tool_use") {
|
|
458
|
+
return { label: "tool", detail: typeof block.name === "string" ? block.name : "tool_use", tone: "muted" };
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
return null;
|
|
462
|
+
}
|
|
463
|
+
function compactWork(items) {
|
|
464
|
+
const compacted = [];
|
|
465
|
+
for (const item of items) {
|
|
466
|
+
const last = compacted.at(-1);
|
|
467
|
+
if (last && last.label === item.label && last.detail === item.detail && last.tone === item.tone) {
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
compacted.push(item);
|
|
471
|
+
}
|
|
472
|
+
return compacted;
|
|
473
|
+
}
|
|
474
|
+
function formatWorkLine(item, width) {
|
|
475
|
+
const raw = `${workGlyph(item.tone)} ${item.label}${item.detail ? ` ${item.detail}` : ""}`;
|
|
476
|
+
return fitLine(raw, width);
|
|
477
|
+
}
|
|
478
|
+
function fitLine(value, width) {
|
|
479
|
+
return truncate(value, width).padEnd(width, " ");
|
|
480
|
+
}
|
|
481
|
+
async function runAction(runId, action, success, setNotice, refresh) {
|
|
482
|
+
if (!runId) {
|
|
483
|
+
setNotice("No run selected");
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
try {
|
|
487
|
+
await action(runId);
|
|
488
|
+
setNotice(`${success} ${shortId(runId)}`);
|
|
489
|
+
await refresh();
|
|
490
|
+
}
|
|
491
|
+
catch (error) {
|
|
492
|
+
setNotice(error instanceof Error ? error.message : String(error));
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
async function mergeReadyRuns(runs, allowDirty, setNotice, refresh) {
|
|
496
|
+
const ready = runs.filter(canMerge);
|
|
497
|
+
if (ready.length === 0) {
|
|
498
|
+
setNotice("No completed worktree runs ready to merge");
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
setNotice(`Merging ${ready.length} run${ready.length === 1 ? "" : "s"}...`);
|
|
502
|
+
let merged = 0;
|
|
503
|
+
for (const run of ready) {
|
|
504
|
+
await mergeRun(run.id, allowDirty, { silent: true });
|
|
505
|
+
merged += 1;
|
|
506
|
+
}
|
|
507
|
+
setNotice(`Merged ${merged} run${merged === 1 ? "" : "s"}`);
|
|
508
|
+
await refresh();
|
|
509
|
+
}
|
|
510
|
+
function selectRelative(runs, selectedRunId, delta, setSelectedRunId) {
|
|
511
|
+
if (runs.length === 0) {
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
const index = Math.max(0, runs.findIndex((run) => run.id === selectedRunId));
|
|
515
|
+
const next = Math.min(runs.length - 1, Math.max(0, index + delta));
|
|
516
|
+
setSelectedRunId(runs[next]?.id);
|
|
517
|
+
}
|
|
518
|
+
function cycleBackend(current, setBackend, setModel, setNotice) {
|
|
519
|
+
const index = BACKENDS.indexOf(current);
|
|
520
|
+
chooseBackend(BACKENDS[(index + 1) % BACKENDS.length] ?? "claude", setBackend, setModel, setNotice);
|
|
521
|
+
}
|
|
522
|
+
function chooseBackend(backend, setBackend, setModel, setNotice) {
|
|
523
|
+
setBackend(backend);
|
|
524
|
+
setModel(undefined);
|
|
525
|
+
setNotice(`Backend ${backend}`);
|
|
526
|
+
}
|
|
527
|
+
function toggleSet(current, value) {
|
|
528
|
+
const next = new Set(current);
|
|
529
|
+
if (next.has(value)) {
|
|
530
|
+
next.delete(value);
|
|
531
|
+
}
|
|
532
|
+
else {
|
|
533
|
+
next.add(value);
|
|
534
|
+
}
|
|
535
|
+
return next;
|
|
536
|
+
}
|
|
537
|
+
function modelForBackend(backend, config) {
|
|
538
|
+
if (!config) {
|
|
539
|
+
return undefined;
|
|
540
|
+
}
|
|
541
|
+
if (backend === "claude") {
|
|
542
|
+
return config.backends.claude?.model;
|
|
543
|
+
}
|
|
544
|
+
if (backend === "codex") {
|
|
545
|
+
return config.backends.codex?.model;
|
|
546
|
+
}
|
|
547
|
+
return config.backends.acpx?.model;
|
|
548
|
+
}
|
|
549
|
+
function resolveUiRun(runs, runId) {
|
|
550
|
+
if (!runId) {
|
|
551
|
+
return undefined;
|
|
552
|
+
}
|
|
553
|
+
return runs.find((run) => run.id === runId || run.id.startsWith(runId));
|
|
554
|
+
}
|
|
555
|
+
function isBackend(value) {
|
|
556
|
+
return value === "claude" || value === "codex" || value === "acpx";
|
|
557
|
+
}
|
|
558
|
+
function isPrintable(value) {
|
|
559
|
+
return value.length > 0 && !/[\u0000-\u001f\u007f]/.test(value);
|
|
560
|
+
}
|
|
561
|
+
function isActive(status) {
|
|
562
|
+
return status === "created" || status === "running" || status === "steering" || status === "verifying";
|
|
563
|
+
}
|
|
564
|
+
function statusGlyph(status) {
|
|
565
|
+
if (status === "completed" || status === "merged") {
|
|
566
|
+
return "ok";
|
|
567
|
+
}
|
|
568
|
+
if (status === "failed" || status === "merge-conflict") {
|
|
569
|
+
return "!!";
|
|
570
|
+
}
|
|
571
|
+
if (status === "cancelled") {
|
|
572
|
+
return "--";
|
|
573
|
+
}
|
|
574
|
+
return "..";
|
|
575
|
+
}
|
|
576
|
+
function statusColor(status) {
|
|
577
|
+
if (status === "completed" || status === "merged") {
|
|
578
|
+
return "green";
|
|
579
|
+
}
|
|
580
|
+
if (status === "failed" || status === "merge-conflict") {
|
|
581
|
+
return "red";
|
|
582
|
+
}
|
|
583
|
+
if (status === "cancelled") {
|
|
584
|
+
return "yellow";
|
|
585
|
+
}
|
|
586
|
+
if (status === "verifying" || status === "steering") {
|
|
587
|
+
return "magenta";
|
|
588
|
+
}
|
|
589
|
+
return "cyan";
|
|
590
|
+
}
|
|
591
|
+
function toneColor(tone) {
|
|
592
|
+
if (tone === "success") {
|
|
593
|
+
return "green";
|
|
594
|
+
}
|
|
595
|
+
if (tone === "warning") {
|
|
596
|
+
return "yellow";
|
|
597
|
+
}
|
|
598
|
+
if (tone === "danger") {
|
|
599
|
+
return "red";
|
|
600
|
+
}
|
|
601
|
+
if (tone === "info") {
|
|
602
|
+
return "cyan";
|
|
603
|
+
}
|
|
604
|
+
return "gray";
|
|
605
|
+
}
|
|
606
|
+
function workGlyph(tone) {
|
|
607
|
+
if (tone === "success") {
|
|
608
|
+
return "ok";
|
|
609
|
+
}
|
|
610
|
+
if (tone === "warning") {
|
|
611
|
+
return "??";
|
|
612
|
+
}
|
|
613
|
+
if (tone === "danger") {
|
|
614
|
+
return "!!";
|
|
615
|
+
}
|
|
616
|
+
if (tone === "info") {
|
|
617
|
+
return "->";
|
|
618
|
+
}
|
|
619
|
+
return "--";
|
|
620
|
+
}
|
|
621
|
+
function completionPercent(run) {
|
|
622
|
+
if (run.status === "merged") {
|
|
623
|
+
return 100;
|
|
624
|
+
}
|
|
625
|
+
if (run.status === "completed") {
|
|
626
|
+
return 95;
|
|
627
|
+
}
|
|
628
|
+
if (run.status === "merge-conflict") {
|
|
629
|
+
return 85;
|
|
630
|
+
}
|
|
631
|
+
if (run.status === "failed" || run.status === "cancelled") {
|
|
632
|
+
return 50;
|
|
633
|
+
}
|
|
634
|
+
if (run.status === "steering") {
|
|
635
|
+
return 88;
|
|
636
|
+
}
|
|
637
|
+
if (run.status === "verifying") {
|
|
638
|
+
return 82;
|
|
639
|
+
}
|
|
640
|
+
const labels = new Set(run.work.map((item) => item.label));
|
|
641
|
+
let value = 8;
|
|
642
|
+
if (labels.has("worktree prepared") || labels.has("checkout claimed")) {
|
|
643
|
+
value = 18;
|
|
644
|
+
}
|
|
645
|
+
if (labels.has("planner contract")) {
|
|
646
|
+
value = 28;
|
|
647
|
+
}
|
|
648
|
+
if (labels.has("worker started")) {
|
|
649
|
+
value = 45;
|
|
650
|
+
}
|
|
651
|
+
if (run.output.trim()) {
|
|
652
|
+
value = Math.max(value, 60);
|
|
653
|
+
}
|
|
654
|
+
return value;
|
|
655
|
+
}
|
|
656
|
+
function progressBar(percent) {
|
|
657
|
+
const width = 6;
|
|
658
|
+
const filled = Math.max(0, Math.min(width, Math.round((percent / 100) * width)));
|
|
659
|
+
return `[${"#".repeat(filled)}${"-".repeat(width - filled)}]`;
|
|
660
|
+
}
|
|
661
|
+
function canMerge(run) {
|
|
662
|
+
return run.status === "completed" && run.worktree.enabled;
|
|
663
|
+
}
|
|
664
|
+
function runSummary(run) {
|
|
665
|
+
const latestWork = run.work.at(-1);
|
|
666
|
+
if (run.status === "steering") {
|
|
667
|
+
return "auto-steering after completion";
|
|
668
|
+
}
|
|
669
|
+
if (latestWork) {
|
|
670
|
+
return `${latestWork.label}${latestWork.detail ? `: ${latestWork.detail}` : ""}`;
|
|
671
|
+
}
|
|
672
|
+
const latestLine = tailLines(run.output, 1)[0];
|
|
673
|
+
if (latestLine && latestLine !== "No transcript yet.") {
|
|
674
|
+
return latestLine;
|
|
675
|
+
}
|
|
676
|
+
return run.status;
|
|
677
|
+
}
|
|
678
|
+
function deletePreviousWord(value) {
|
|
679
|
+
return value.replace(/\s+$/, "").replace(/\S+$/, "");
|
|
680
|
+
}
|
|
681
|
+
function missingCount(data) {
|
|
682
|
+
if (!isRecord(data) || !Array.isArray(data.missing)) {
|
|
683
|
+
return 0;
|
|
684
|
+
}
|
|
685
|
+
return data.missing.length;
|
|
686
|
+
}
|
|
687
|
+
function objectField(data, key) {
|
|
688
|
+
return isRecord(data) && typeof data[key] === "string" ? data[key] : undefined;
|
|
689
|
+
}
|
|
690
|
+
function isRecord(value) {
|
|
691
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
692
|
+
}
|
|
693
|
+
function tailLines(text, maxLines) {
|
|
694
|
+
const normalized = text.replace(/\r/g, "");
|
|
695
|
+
const lines = normalized.includes("\n") ? normalized.split("\n") : chunkLine(normalized, 96);
|
|
696
|
+
const visible = lines.filter((line, index) => line.length > 0 || index < lines.length - 1).slice(-Math.max(1, maxLines));
|
|
697
|
+
return visible.length ? visible : ["No transcript yet."];
|
|
698
|
+
}
|
|
699
|
+
function chunkLine(text, width) {
|
|
700
|
+
if (!text) {
|
|
701
|
+
return [];
|
|
702
|
+
}
|
|
703
|
+
const chunks = [];
|
|
704
|
+
for (let i = 0; i < text.length; i += width) {
|
|
705
|
+
chunks.push(text.slice(i, i + width));
|
|
706
|
+
}
|
|
707
|
+
return chunks;
|
|
708
|
+
}
|
|
709
|
+
function shortId(runId) {
|
|
710
|
+
return runId.slice(0, 14);
|
|
711
|
+
}
|
|
712
|
+
function truncate(value, width) {
|
|
713
|
+
if (value.length <= width) {
|
|
714
|
+
return value;
|
|
715
|
+
}
|
|
716
|
+
if (width <= 1) {
|
|
717
|
+
return value.slice(0, width);
|
|
718
|
+
}
|
|
719
|
+
return `${value.slice(0, Math.max(0, width - 3))}...`;
|
|
720
|
+
}
|
|
721
|
+
//# sourceMappingURL=tui.js.map
|