@viraatdas/rudder 1.0.5 → 1.0.7
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 +369 -23
- package/dist/agent-attention.d.ts +5 -0
- package/dist/agent-attention.js +54 -0
- package/dist/agent-attention.js.map +1 -0
- package/dist/backends.js +58 -10
- package/dist/backends.js.map +1 -1
- package/dist/brain.js +3 -3
- package/dist/brain.js.map +1 -1
- package/dist/cloud.d.ts +9 -0
- package/dist/cloud.js +2255 -0
- package/dist/cloud.js.map +1 -0
- package/dist/main.js +112 -2
- package/dist/main.js.map +1 -1
- package/dist/migration.d.ts +82 -0
- package/dist/migration.js +174 -0
- package/dist/migration.js.map +1 -0
- package/dist/native/rudder-native +0 -0
- package/dist/native-agents.d.ts +1 -0
- package/dist/native-agents.js +126 -8
- package/dist/native-agents.js.map +1 -1
- package/dist/plan-mode.d.ts +2 -0
- package/dist/plan-mode.js +22 -0
- package/dist/plan-mode.js.map +1 -0
- package/dist/repl.js +11 -3
- package/dist/repl.js.map +1 -1
- package/dist/run-manager.d.ts +11 -1
- package/dist/run-manager.js +164 -19
- package/dist/run-manager.js.map +1 -1
- package/dist/state.d.ts +10 -1
- package/dist/state.js +51 -1
- package/dist/state.js.map +1 -1
- package/dist/task-summary.d.ts +5 -0
- package/dist/task-summary.js +128 -0
- package/dist/task-summary.js.map +1 -0
- package/dist/tmux-dashboard.js +248 -64
- package/dist/tmux-dashboard.js.map +1 -1
- package/dist/tmux.js +1 -0
- package/dist/tmux.js.map +1 -1
- package/dist/tui.js +228 -46
- package/dist/tui.js.map +1 -1
- package/dist/types.d.ts +23 -0
- package/dist/util.d.ts +1 -0
- package/dist/util.js +23 -1
- package/dist/util.js.map +1 -1
- package/package.json +4 -2
package/dist/tui.js
CHANGED
|
@@ -4,10 +4,12 @@ import { spawn } from "node:child_process";
|
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
6
6
|
import { Box, Text, render, useApp, useInput, useWindowSize } from "ink";
|
|
7
|
-
import {
|
|
7
|
+
import { permissionAttentionFromOutput } from "./agent-attention.js";
|
|
8
|
+
import { currentBranch, findRepoRoot } from "./git.js";
|
|
8
9
|
import { discoverModelOptions, fallbackModelOptions } from "./models.js";
|
|
9
|
-
import { eventsPath, listRuns, loadConfig, outputPath, } from "./state.js";
|
|
10
|
+
import { eventsPath, listRuns, loadConfig, outputPath, rememberBackendSelection, } from "./state.js";
|
|
10
11
|
import { continueRun, deleteRun, mergeRun, startRun, stopRun } from "./run-manager.js";
|
|
12
|
+
import { taskDisplayLabel } from "./task-summary.js";
|
|
11
13
|
import { pathExists, shortenHome } from "./util.js";
|
|
12
14
|
const INTERACTIVE_BACKENDS = ["claude", "codex"];
|
|
13
15
|
const COMPLETION_SOUND = fileURLToPath(new URL("../assets/sounds/ping.mp3", import.meta.url));
|
|
@@ -19,7 +21,7 @@ const COMMANDS = [
|
|
|
19
21
|
{ name: "new", detail: "return to new-agent mode", insert: "/new" },
|
|
20
22
|
{ name: "worktree", detail: "toggle worktree policy", insert: "/worktree " },
|
|
21
23
|
{ name: "stop", detail: "stop the selected run", insert: "/stop" },
|
|
22
|
-
{ name: "delete", detail: "delete selected run
|
|
24
|
+
{ name: "delete", detail: "delete selected run and its worktree", insert: "/delete" },
|
|
23
25
|
{ name: "copy", detail: "copy selected worker transcript", insert: "/copy" },
|
|
24
26
|
{ name: "merge", detail: "merge the selected completed worktree", insert: "/merge" },
|
|
25
27
|
{ name: "merge-all", detail: "merge all completed worktrees", insert: "/merge-all" },
|
|
@@ -58,9 +60,11 @@ function RudderTui({ defaults }) {
|
|
|
58
60
|
const [commandMenuIndex, setCommandMenuIndex] = useState(0);
|
|
59
61
|
const [discoveredModels, setDiscoveredModels] = useState([]);
|
|
60
62
|
const [deletePrompt, setDeletePrompt] = useState(null);
|
|
63
|
+
const [mergePrompt, setMergePrompt] = useState(null);
|
|
64
|
+
const [conflictPrompt, setConflictPrompt] = useState(null);
|
|
61
65
|
const [submitting, setSubmitting] = useState(false);
|
|
62
66
|
const [preferencesLoaded, setPreferencesLoaded] = useState(false);
|
|
63
|
-
const
|
|
67
|
+
const notifiedAlerts = useRef(null);
|
|
64
68
|
const refresh = useCallback(async () => {
|
|
65
69
|
const root = findRepoRoot();
|
|
66
70
|
const [nextConfig, nextBranch, nextRuns] = await Promise.all([
|
|
@@ -71,7 +75,7 @@ function RudderTui({ defaults }) {
|
|
|
71
75
|
setRepoRoot(root);
|
|
72
76
|
setConfig(nextConfig);
|
|
73
77
|
setBranch(nextBranch);
|
|
74
|
-
|
|
78
|
+
notifyRunAlerts(nextRuns, notifiedAlerts);
|
|
75
79
|
setRuns(nextRuns);
|
|
76
80
|
if (!preferencesLoaded) {
|
|
77
81
|
setBackend(toInteractiveBackend(defaults.backend ?? nextConfig.lastUsedBackend ?? nextConfig.defaultBackend));
|
|
@@ -168,30 +172,26 @@ function RudderTui({ defaults }) {
|
|
|
168
172
|
setSubmitting(false);
|
|
169
173
|
}
|
|
170
174
|
}, [backend, model, refresh, submitting, targetRun, worktreeMode]);
|
|
171
|
-
const requestDeleteSelectedRun = useCallback(
|
|
175
|
+
const requestDeleteSelectedRun = useCallback((runOverride) => {
|
|
172
176
|
const run = runOverride ?? selectedRun;
|
|
173
177
|
if (!run) {
|
|
174
178
|
setNotice("No agent selected");
|
|
175
179
|
return;
|
|
176
180
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
setDeletePrompt({ runId: run.id, canMerge: mergeable });
|
|
180
|
-
setNotice(mergeable
|
|
181
|
-
? `Delete ${shortId(run.id)}? press m to merge first, d to discard, Esc to cancel`
|
|
182
|
-
: `Delete ${shortId(run.id)}? press d to confirm, Esc to cancel`);
|
|
181
|
+
setDeletePrompt({ runId: run.id });
|
|
182
|
+
setNotice(`Delete ${shortId(run.id)}? press d to confirm, Esc to cancel`);
|
|
183
183
|
}, [selectedRun]);
|
|
184
|
-
const confirmDelete = useCallback(async (
|
|
184
|
+
const confirmDelete = useCallback(async () => {
|
|
185
185
|
if (!deletePrompt) {
|
|
186
186
|
return;
|
|
187
187
|
}
|
|
188
188
|
const runId = deletePrompt.runId;
|
|
189
189
|
try {
|
|
190
|
-
await deleteRun(runId, {
|
|
190
|
+
await deleteRun(runId, { force: true, silent: true });
|
|
191
191
|
setDeletePrompt(null);
|
|
192
192
|
setSelectedRunId(undefined);
|
|
193
193
|
setTargetRunId(undefined);
|
|
194
|
-
setNotice(
|
|
194
|
+
setNotice(`Deleted ${shortId(runId)}`);
|
|
195
195
|
await refresh();
|
|
196
196
|
}
|
|
197
197
|
catch (error) {
|
|
@@ -199,6 +199,107 @@ function RudderTui({ defaults }) {
|
|
|
199
199
|
setNotice(error instanceof Error ? error.message : String(error));
|
|
200
200
|
}
|
|
201
201
|
}, [deletePrompt, refresh]);
|
|
202
|
+
const requestMergeRun = useCallback((runOverride, allowDirty = false) => {
|
|
203
|
+
const run = runOverride ?? selectedRun;
|
|
204
|
+
if (!run) {
|
|
205
|
+
setNotice("No agent selected");
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
if (!canMerge(run)) {
|
|
209
|
+
setNotice(`${shortId(run.id)} is not ready to merge`);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
setDeletePrompt(null);
|
|
213
|
+
setConflictPrompt(null);
|
|
214
|
+
setMergePrompt({
|
|
215
|
+
kind: "selected",
|
|
216
|
+
runId: run.id,
|
|
217
|
+
label: truncate(taskDisplayLabel(run, 48), 48),
|
|
218
|
+
allowDirty,
|
|
219
|
+
});
|
|
220
|
+
setNotice(`Merge ${shortId(run.id)}? press y to confirm or n to cancel`);
|
|
221
|
+
}, [selectedRun]);
|
|
222
|
+
const requestMergeAll = useCallback((allowDirty = false) => {
|
|
223
|
+
const ready = runs.filter(canMerge);
|
|
224
|
+
if (ready.length === 0) {
|
|
225
|
+
setNotice("No completed worktree runs ready to merge");
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
setDeletePrompt(null);
|
|
229
|
+
setConflictPrompt(null);
|
|
230
|
+
setMergePrompt({ kind: "all", runIds: ready.map((run) => run.id), allowDirty });
|
|
231
|
+
setNotice(`Merge ${ready.length} run${ready.length === 1 ? "" : "s"}? press y to confirm or n to cancel`);
|
|
232
|
+
}, [runs]);
|
|
233
|
+
const confirmMerge = useCallback(async () => {
|
|
234
|
+
if (!mergePrompt) {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
const prompt = mergePrompt;
|
|
238
|
+
setMergePrompt(null);
|
|
239
|
+
try {
|
|
240
|
+
if (prompt.kind === "selected") {
|
|
241
|
+
const merged = await mergeRun(prompt.runId, prompt.allowDirty, { silent: true });
|
|
242
|
+
if (merged.merge?.status === "conflict") {
|
|
243
|
+
const files = merged.merge.conflictedFiles ?? [];
|
|
244
|
+
setConflictPrompt({ runId: prompt.runId, files });
|
|
245
|
+
setNotice(`Merge conflict in ${files.length || "unknown"} file${files.length === 1 ? "" : "s"}; press y for AI help or n for manual`);
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
setNotice(`Merged ${shortId(prompt.runId)}`);
|
|
249
|
+
}
|
|
250
|
+
await refresh();
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
let mergedCount = 0;
|
|
254
|
+
for (const runId of prompt.runIds) {
|
|
255
|
+
const merged = await mergeRun(runId, prompt.allowDirty, { silent: true });
|
|
256
|
+
if (merged.merge?.status === "conflict") {
|
|
257
|
+
const files = merged.merge.conflictedFiles ?? [];
|
|
258
|
+
setConflictPrompt({ runId, files });
|
|
259
|
+
setNotice(`Merge all stopped after ${mergedCount}: conflict in ${shortId(runId)}; press y for AI help or n for manual`);
|
|
260
|
+
await refresh();
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
mergedCount += 1;
|
|
264
|
+
}
|
|
265
|
+
setNotice(`Merged ${mergedCount} run${mergedCount === 1 ? "" : "s"}`);
|
|
266
|
+
await refresh();
|
|
267
|
+
}
|
|
268
|
+
catch (error) {
|
|
269
|
+
setNotice(error instanceof Error ? error.message : String(error));
|
|
270
|
+
await refresh();
|
|
271
|
+
}
|
|
272
|
+
}, [mergePrompt, refresh]);
|
|
273
|
+
const startConflictResolver = useCallback(async () => {
|
|
274
|
+
if (!conflictPrompt) {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
const files = conflictPrompt.files.length ? conflictPrompt.files.join("\n") : "(git did not report conflicted files)";
|
|
278
|
+
const task = [
|
|
279
|
+
"Read RUDDER.md first. A git merge stopped with conflicts in this checkout.",
|
|
280
|
+
`Conflicted files:\n${files}`,
|
|
281
|
+
"Resolve the merge conflicts, keep the intended changes from both sides where appropriate, run relevant checks if possible, and report what changed. Do not abort the merge unless resolving is impossible.",
|
|
282
|
+
].join("\n\n");
|
|
283
|
+
try {
|
|
284
|
+
const run = await startRun({
|
|
285
|
+
task,
|
|
286
|
+
backend,
|
|
287
|
+
model,
|
|
288
|
+
detach: true,
|
|
289
|
+
worktree: false,
|
|
290
|
+
silent: true,
|
|
291
|
+
view: "shell",
|
|
292
|
+
});
|
|
293
|
+
setConflictPrompt(null);
|
|
294
|
+
setSelectedRunId(run.id);
|
|
295
|
+
setExpandedRunIds((current) => new Set(current).add(run.id));
|
|
296
|
+
setNotice(`Started AI merge-conflict resolver ${shortId(run.id)}`);
|
|
297
|
+
await refresh();
|
|
298
|
+
}
|
|
299
|
+
catch (error) {
|
|
300
|
+
setNotice(error instanceof Error ? error.message : String(error));
|
|
301
|
+
}
|
|
302
|
+
}, [backend, conflictPrompt, model, refresh]);
|
|
202
303
|
const copySelectedTranscript = useCallback(async (runOverride) => {
|
|
203
304
|
const run = runOverride ?? selectedRun;
|
|
204
305
|
if (!run) {
|
|
@@ -229,7 +330,9 @@ function RudderTui({ defaults }) {
|
|
|
229
330
|
return;
|
|
230
331
|
case "backend":
|
|
231
332
|
if (isInteractiveBackend(args[0])) {
|
|
232
|
-
|
|
333
|
+
const nextBackend = args[0];
|
|
334
|
+
chooseBackend(nextBackend, setBackend, setModel, setNotice);
|
|
335
|
+
setConfig(await rememberBackendSelection({ backend: nextBackend }));
|
|
233
336
|
}
|
|
234
337
|
else {
|
|
235
338
|
setNotice("Usage: /backend claude|codex");
|
|
@@ -267,6 +370,11 @@ function RudderTui({ defaults }) {
|
|
|
267
370
|
else {
|
|
268
371
|
const nextModel = args.join(" ");
|
|
269
372
|
setModel(nextModel);
|
|
373
|
+
setConfig(await rememberBackendSelection({
|
|
374
|
+
backend,
|
|
375
|
+
model: nextModel,
|
|
376
|
+
updateModel: true,
|
|
377
|
+
}));
|
|
270
378
|
setNotice(`Model ${nextModel}`);
|
|
271
379
|
}
|
|
272
380
|
setInput("");
|
|
@@ -289,11 +397,11 @@ function RudderTui({ defaults }) {
|
|
|
289
397
|
setInput("");
|
|
290
398
|
return;
|
|
291
399
|
case "merge":
|
|
292
|
-
|
|
400
|
+
requestMergeRun(resolveUiRun(runs, args[0] ?? selectedRun?.id), args.includes("--allow-dirty"));
|
|
293
401
|
setInput("");
|
|
294
402
|
return;
|
|
295
403
|
case "merge-all":
|
|
296
|
-
|
|
404
|
+
requestMergeAll(args.includes("--allow-dirty"));
|
|
297
405
|
setInput("");
|
|
298
406
|
return;
|
|
299
407
|
case "clear":
|
|
@@ -305,7 +413,7 @@ function RudderTui({ defaults }) {
|
|
|
305
413
|
setNotice(`Unknown command: /${command}`);
|
|
306
414
|
setInput("");
|
|
307
415
|
}
|
|
308
|
-
}, [app, backend, copySelectedTranscript, refresh, requestDeleteSelectedRun, runs, selectedRun?.id]);
|
|
416
|
+
}, [app, backend, copySelectedTranscript, refresh, requestDeleteSelectedRun, requestMergeAll, requestMergeRun, runs, selectedRun?.id]);
|
|
309
417
|
const selectModelOption = useCallback((index) => {
|
|
310
418
|
const option = modelOptions[index];
|
|
311
419
|
if (!option) {
|
|
@@ -315,7 +423,14 @@ function RudderTui({ defaults }) {
|
|
|
315
423
|
setModelMenuOpen(false);
|
|
316
424
|
setModelMenuIndex(index);
|
|
317
425
|
setNotice(option.value ? `Model ${option.value}` : "Using backend default model");
|
|
318
|
-
|
|
426
|
+
void rememberBackendSelection({
|
|
427
|
+
backend,
|
|
428
|
+
model: option.value,
|
|
429
|
+
updateModel: true,
|
|
430
|
+
}).then(setConfig).catch((error) => {
|
|
431
|
+
setNotice(error instanceof Error ? error.message : String(error));
|
|
432
|
+
});
|
|
433
|
+
}, [backend, modelOptions]);
|
|
319
434
|
const selectCommandOption = useCallback((index) => {
|
|
320
435
|
const option = commandOptions[index];
|
|
321
436
|
if (!option) {
|
|
@@ -355,18 +470,38 @@ function RudderTui({ defaults }) {
|
|
|
355
470
|
}
|
|
356
471
|
return;
|
|
357
472
|
}
|
|
473
|
+
if (mergePrompt) {
|
|
474
|
+
if (key.escape || value === "n" || value === "N") {
|
|
475
|
+
setMergePrompt(null);
|
|
476
|
+
setNotice("Merge cancelled");
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
if (value === "y" || value === "Y") {
|
|
480
|
+
void confirmMerge();
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
if (conflictPrompt) {
|
|
486
|
+
if (key.escape || value === "n" || value === "N") {
|
|
487
|
+
setConflictPrompt(null);
|
|
488
|
+
setNotice("Resolve the merge conflicts manually, then commit");
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
if (value === "y" || value === "Y") {
|
|
492
|
+
void startConflictResolver();
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
358
497
|
if (deletePrompt) {
|
|
359
498
|
if (key.escape) {
|
|
360
499
|
setDeletePrompt(null);
|
|
361
500
|
setNotice("Delete cancelled");
|
|
362
501
|
return;
|
|
363
502
|
}
|
|
364
|
-
if (value === "
|
|
365
|
-
void confirmDelete(
|
|
366
|
-
return;
|
|
367
|
-
}
|
|
368
|
-
if (value === "d" || key.delete || key.backspace) {
|
|
369
|
-
void confirmDelete(false);
|
|
503
|
+
if (value === "d") {
|
|
504
|
+
void confirmDelete();
|
|
370
505
|
return;
|
|
371
506
|
}
|
|
372
507
|
return;
|
|
@@ -532,11 +667,11 @@ function RudderTui({ defaults }) {
|
|
|
532
667
|
return;
|
|
533
668
|
}
|
|
534
669
|
if (input.length === 0 && value === "m" && selectedRun) {
|
|
535
|
-
|
|
670
|
+
requestMergeRun(selectedRun);
|
|
536
671
|
return;
|
|
537
672
|
}
|
|
538
673
|
if (input.length === 0 && value === "M") {
|
|
539
|
-
|
|
674
|
+
requestMergeAll();
|
|
540
675
|
return;
|
|
541
676
|
}
|
|
542
677
|
if (input.length === 0 && value === "d") {
|
|
@@ -565,7 +700,7 @@ function RudderTui({ defaults }) {
|
|
|
565
700
|
const railWidth = Math.min(42, Math.max(30, Math.floor(width * 0.34)));
|
|
566
701
|
const detailWidth = Math.max(30, width - railWidth - 1);
|
|
567
702
|
const detailHeight = Math.max(8, height - 8);
|
|
568
|
-
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: targetRun?.id, width: railWidth, expandedRunIds: expandedRunIds, focused: focusPane === "agents" }), _jsx(Box, { flexDirection: "column", flexGrow: 1, marginLeft: 1, children: _jsx(DetailPane, { run: selectedRun, width: detailWidth, height: detailHeight, expanded: selectedExpanded, transcriptExpanded: transcriptExpanded, focused: focusPane === "worker", input: focusPane === "worker" ? input : "", submitting: submitting }) })] }), helpOpen ? _jsx(Help, {}) : null, modelMenuOpen ? _jsx(ModelMenu, { backend: backend, options: modelOptions, selectedIndex: modelMenuIndex, currentModel: model, width: width }) : null, commandMenuOpen ? _jsx(CommandMenu, { options: commandOptions, selectedIndex: commandMenuIndex, width: width }) : null, deletePrompt ? _jsx(DeletePromptBox, { prompt: deletePrompt, width: width }) : null, focusPane === "worker"
|
|
703
|
+
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: targetRun?.id, width: railWidth, expandedRunIds: expandedRunIds, focused: focusPane === "agents" }), _jsx(Box, { flexDirection: "column", flexGrow: 1, marginLeft: 1, children: _jsx(DetailPane, { run: selectedRun, width: detailWidth, height: detailHeight, expanded: selectedExpanded, transcriptExpanded: transcriptExpanded, focused: focusPane === "worker", input: focusPane === "worker" ? input : "", submitting: submitting }) })] }), helpOpen ? _jsx(Help, {}) : null, modelMenuOpen ? _jsx(ModelMenu, { backend: backend, options: modelOptions, selectedIndex: modelMenuIndex, currentModel: model, width: width }) : null, commandMenuOpen ? _jsx(CommandMenu, { options: commandOptions, selectedIndex: commandMenuIndex, width: width }) : null, mergePrompt ? _jsx(MergePromptBox, { prompt: mergePrompt, width: width }) : null, conflictPrompt ? _jsx(MergeConflictPromptBox, { prompt: conflictPrompt, width: width }) : null, deletePrompt ? _jsx(DeletePromptBox, { prompt: deletePrompt, width: width }) : null, focusPane === "worker"
|
|
569
704
|
? _jsx(StatusDock, { notice: notice })
|
|
570
705
|
: _jsx(PromptDock, { input: input, backend: backend, model: model ?? modelForBackend(backend, config), notice: notice, submitting: submitting, targetRun: targetRun, focused: focusPane === "task" }), _jsx(Footer, { focusPane: focusPane })] }));
|
|
571
706
|
}
|
|
@@ -579,13 +714,13 @@ function RunRail(props) {
|
|
|
579
714
|
return (_jsxs(Box, { flexDirection: "column", width: props.width, borderStyle: props.focused ? "double" : "single", borderColor: props.focused ? "cyan" : "gray", paddingX: 1, children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Box, { children: [props.focused ? _jsx(FocusPill, { label: "focus" }) : null, _jsx(Text, { bold: true, color: props.focused ? "cyan" : undefined, 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)))] }));
|
|
580
715
|
}
|
|
581
716
|
function RunCard(props) {
|
|
582
|
-
const tone =
|
|
717
|
+
const tone = runStatusColor(props.run);
|
|
583
718
|
const label = props.selected ? (props.targeted ? ">>" : "> ") : " ";
|
|
584
|
-
const task = truncate(props.run
|
|
719
|
+
const task = truncate(taskDisplayLabel(props.run, 80), Math.max(12, props.width - 14));
|
|
585
720
|
const progress = completionPercent(props.run);
|
|
586
721
|
const summary = truncate(agentRailSummary(props.run), Math.max(12, props.width - 7));
|
|
587
722
|
const meta = `${progressBar(progress)} ${progress}%`;
|
|
588
|
-
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: [" ", statusWord(props.run
|
|
723
|
+
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: [" ", statusWord(props.run), " ", props.targeted ? "editing " : "", summary] })] }));
|
|
589
724
|
}
|
|
590
725
|
function DetailPane(props) {
|
|
591
726
|
if (!props.run) {
|
|
@@ -594,7 +729,7 @@ function DetailPane(props) {
|
|
|
594
729
|
const composerHeight = props.focused ? 3 : 0;
|
|
595
730
|
const outputHeight = Math.max(5, props.height - 6 - composerHeight);
|
|
596
731
|
const contentWidth = Math.max(10, props.width - 4);
|
|
597
|
-
return (_jsxs(Box, { width: props.width, height: props.height, borderStyle: props.focused ? "double" : "single", borderColor: props.focused ? "cyan" :
|
|
732
|
+
return (_jsxs(Box, { width: props.width, height: props.height, borderStyle: props.focused ? "double" : "single", borderColor: props.focused ? "cyan" : runStatusColor(props.run), paddingX: 1, flexDirection: "column", children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Box, { children: [props.focused ? _jsx(FocusPill, { label: "focus" }) : null, _jsx(Text, { bold: true, color: props.focused ? "cyan" : undefined, children: " worker" })] }), _jsx(Text, { color: runStatusColor(props.run), children: workerStateLabel(props.run) })] }), _jsx(Text, { wrap: "truncate", color: "gray", children: fitLine(props.run.task, contentWidth) }), _jsx(Box, { flexDirection: "column", marginTop: 1, minHeight: 0, children: _jsx(Box, { height: outputHeight, overflow: "hidden", flexDirection: "column", children: tailLines(props.run.output, outputHeight).map((line, index) => (_jsx(Text, { wrap: "truncate", children: line || " " }, index))) }) }), props.focused ? _jsx(WorkerComposer, { run: props.run, input: props.input, submitting: props.submitting, width: contentWidth }) : null] }));
|
|
598
733
|
}
|
|
599
734
|
function WorkerComposer(props) {
|
|
600
735
|
const active = isActive(props.run.status);
|
|
@@ -604,7 +739,7 @@ function WorkerComposer(props) {
|
|
|
604
739
|
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, borderStyle: "single", borderColor: "cyan", paddingX: 1, children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Text, { children: [_jsx(FocusPill, { label: label }), _jsxs(Text, { color: props.submitting ? "yellow" : "cyan", children: [" ", props.submitting ? "sending" : shortId(props.run.id)] }), _jsxs(Text, { children: [" ", truncate(value, Math.max(8, props.width - 28))] }), _jsx(Text, { color: "cyan", children: "_" })] }), _jsx(Text, { color: "gray", children: active ? "running" : "resumable" })] }), _jsx(Text, { color: "gray", children: fitLine(`${helper}. Tab changes pane, Esc returns to task.`, props.width) })] }));
|
|
605
740
|
}
|
|
606
741
|
function Help() {
|
|
607
|
-
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: "Tab" }), " focus agents/worker/task ", _jsx(Text, { color: "cyan", children: "Enter" }), " submit focused input ", _jsx(Text, { color: "cyan", children: "j/k" }), " select run"] }), _jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: "worker focus" }), " type to selected agent; running agents are interrupted on Enter ", _jsx(Text, { color: "cyan", children: "n" }), " new task ", _jsx(Text, { color: "cyan", children: "x" }), " expand ", _jsx(Text, { color: "cyan", children: "l" }), " transcript"] }), _jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: "o" }), " model picker ", _jsx(Text, { color: "cyan", children: "/" }), " command search ", _jsx(Text, { color: "cyan", children: "
|
|
742
|
+
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: "Tab" }), " focus agents/worker/task ", _jsx(Text, { color: "cyan", children: "Enter" }), " submit focused input ", _jsx(Text, { color: "cyan", children: "j/k" }), " select run"] }), _jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: "worker focus" }), " type to selected agent; running agents are interrupted on Enter ", _jsx(Text, { color: "cyan", children: "n" }), " new task ", _jsx(Text, { color: "cyan", children: "x" }), " expand ", _jsx(Text, { color: "cyan", children: "l" }), " transcript"] }), _jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: "o" }), " model picker ", _jsx(Text, { color: "cyan", children: "/" }), " command search ", _jsx(Text, { color: "cyan", children: "dd" }), " delete ", _jsx(Text, { color: "cyan", children: "y" }), " copy transcript ", _jsx(Text, { color: "cyan", children: "s" }), " stop ", _jsx(Text, { color: "cyan", children: "m/M" }), " merge"] }), _jsx(Text, { color: "gray", children: "Slash: /backend claude|codex, /model, /model <name>, /agent, /interrupt, /new, /worktree, /stop, /delete, /copy, /merge, /merge-all, /exit" })] }));
|
|
608
743
|
}
|
|
609
744
|
function FocusPill(props) {
|
|
610
745
|
return (_jsxs(Text, { backgroundColor: "cyan", color: "black", bold: true, children: [" ", props.label.toUpperCase(), " "] }));
|
|
@@ -637,11 +772,25 @@ function CommandMenu(props) {
|
|
|
637
772
|
}
|
|
638
773
|
function DeletePromptBox(props) {
|
|
639
774
|
const contentWidth = Math.max(24, props.width - 4);
|
|
640
|
-
const action =
|
|
641
|
-
? "m merge first, d discard run, Esc cancel"
|
|
642
|
-
: "d delete run, Esc cancel";
|
|
775
|
+
const action = "d delete run + worktree, Esc cancel";
|
|
643
776
|
return (_jsx(Box, { width: props.width, borderStyle: "double", borderColor: "yellow", paddingX: 1, children: _jsx(Text, { color: "yellow", bold: true, children: fitLine(`delete ${shortId(props.prompt.runId)}? ${action}`, contentWidth) }) }));
|
|
644
777
|
}
|
|
778
|
+
function MergePromptBox(props) {
|
|
779
|
+
const contentWidth = Math.max(24, props.width - 4);
|
|
780
|
+
const subject = props.prompt.kind === "selected"
|
|
781
|
+
? `merge ${shortId(props.prompt.runId)} ${props.prompt.label}`
|
|
782
|
+
: `merge ${props.prompt.runIds.length} completed run${props.prompt.runIds.length === 1 ? "" : "s"}`;
|
|
783
|
+
const prefix = "press ";
|
|
784
|
+
const action = "y to merge";
|
|
785
|
+
const suffix = ", n to cancel";
|
|
786
|
+
const hint = fitLine(`${prefix}${action}${suffix}`, contentWidth);
|
|
787
|
+
return (_jsxs(Box, { width: props.width, borderStyle: "double", borderColor: "yellow", paddingX: 1, flexDirection: "column", children: [_jsx(Text, { color: "yellow", bold: true, children: fitLine(subject, contentWidth) }), _jsxs(Text, { children: [_jsx(Text, { color: "gray", children: hint.slice(0, prefix.length) }), _jsx(Text, { color: "red", bold: true, children: hint.slice(prefix.length, prefix.length + action.length) }), _jsx(Text, { color: "gray", children: hint.slice(prefix.length + action.length) })] })] }));
|
|
788
|
+
}
|
|
789
|
+
function MergeConflictPromptBox(props) {
|
|
790
|
+
const contentWidth = Math.max(24, props.width - 4);
|
|
791
|
+
const files = props.prompt.files.length ? props.prompt.files.join(", ") : "unknown files";
|
|
792
|
+
return (_jsxs(Box, { width: props.width, borderStyle: "double", borderColor: "red", paddingX: 1, flexDirection: "column", children: [_jsx(Text, { color: "red", bold: true, children: fitLine(`merge conflict ${shortId(props.prompt.runId)}`, contentWidth) }), _jsx(Text, { color: "gray", children: fitLine(files, contentWidth) }), _jsx(Text, { color: "gray", children: fitLine("press y for AI help, n to handle manually", contentWidth) })] }));
|
|
793
|
+
}
|
|
645
794
|
function PromptDock(props) {
|
|
646
795
|
const label = props.targetRun ? `${isActive(props.targetRun.status) ? "interrupt" : "agent"} ${shortId(props.targetRun.id)}` : "task";
|
|
647
796
|
const showTextLabel = !props.focused || Boolean(props.targetRun);
|
|
@@ -651,7 +800,7 @@ function StatusDock(props) {
|
|
|
651
800
|
return (_jsxs(Box, { borderStyle: "single", borderColor: "gray", paddingX: 1, justifyContent: "space-between", children: [_jsx(Text, { color: "gray", children: "worker input is active inside the selected agent pane" }), _jsx(Text, { color: "gray", children: props.notice })] }));
|
|
652
801
|
}
|
|
653
802
|
function Footer(props) {
|
|
654
|
-
return (_jsx(Box, { children: _jsxs(Text, { color: "gray", children: ["focus:", props.focusPane, " Tab focus / commands o model n new c worker
|
|
803
|
+
return (_jsx(Box, { children: _jsxs(Text, { color: "gray", children: ["focus:", props.focusPane, " Tab focus / commands o model n new c worker dd delete y copy m/M merge ? help"] }) }));
|
|
655
804
|
}
|
|
656
805
|
async function loadUiRuns(repoRoot) {
|
|
657
806
|
const runs = await listRuns(repoRoot);
|
|
@@ -665,6 +814,7 @@ async function loadUiRuns(repoRoot) {
|
|
|
665
814
|
output,
|
|
666
815
|
events,
|
|
667
816
|
work: buildWork(events, run),
|
|
817
|
+
attention: permissionAttentionFromOutput(output),
|
|
668
818
|
};
|
|
669
819
|
}));
|
|
670
820
|
}
|
|
@@ -853,25 +1003,32 @@ function modelForBackend(backend, config) {
|
|
|
853
1003
|
return undefined;
|
|
854
1004
|
}
|
|
855
1005
|
if (backend === "claude") {
|
|
856
|
-
return config.backends.claude?.model
|
|
1006
|
+
return config.backends.claude?.model;
|
|
857
1007
|
}
|
|
858
1008
|
if (backend === "codex") {
|
|
859
1009
|
return config.backends.codex?.model;
|
|
860
1010
|
}
|
|
861
1011
|
return config.backends.acpx?.model;
|
|
862
1012
|
}
|
|
863
|
-
function
|
|
864
|
-
const
|
|
1013
|
+
function notifyRunAlerts(runs, ref) {
|
|
1014
|
+
const terminal = new Set(runs.filter((run) => isTerminal(run.status)).map((run) => run.id));
|
|
1015
|
+
const permission = new Set(runs.filter((run) => runNeedsPermission(run)).map((run) => run.id));
|
|
865
1016
|
if (!ref.current) {
|
|
866
|
-
ref.current =
|
|
1017
|
+
ref.current = { terminal, permission };
|
|
867
1018
|
return;
|
|
868
1019
|
}
|
|
869
1020
|
for (const run of runs) {
|
|
870
|
-
if (isTerminal(run.status) && !ref.current.has(run.id)) {
|
|
1021
|
+
if (isTerminal(run.status) && !ref.current.terminal.has(run.id)) {
|
|
1022
|
+
playCompletionSound();
|
|
1023
|
+
ref.current.terminal.add(run.id);
|
|
1024
|
+
}
|
|
1025
|
+
if (runNeedsPermission(run) && !ref.current.permission.has(run.id)) {
|
|
871
1026
|
playCompletionSound();
|
|
872
|
-
ref.current.add(run.id);
|
|
1027
|
+
ref.current.permission.add(run.id);
|
|
873
1028
|
}
|
|
874
1029
|
}
|
|
1030
|
+
ref.current.terminal = terminal;
|
|
1031
|
+
ref.current.permission = permission;
|
|
875
1032
|
}
|
|
876
1033
|
function playCompletionSound() {
|
|
877
1034
|
try {
|
|
@@ -953,6 +1110,12 @@ function statusColor(status) {
|
|
|
953
1110
|
}
|
|
954
1111
|
return "cyan";
|
|
955
1112
|
}
|
|
1113
|
+
function runStatusColor(run) {
|
|
1114
|
+
return runNeedsPermission(run) ? "yellow" : statusColor(run.status);
|
|
1115
|
+
}
|
|
1116
|
+
function runNeedsPermission(run) {
|
|
1117
|
+
return isActive(run.status) && run.attention.needsPermission;
|
|
1118
|
+
}
|
|
956
1119
|
function toneColor(tone) {
|
|
957
1120
|
if (tone === "success") {
|
|
958
1121
|
return "green";
|
|
@@ -984,6 +1147,9 @@ function workGlyph(tone) {
|
|
|
984
1147
|
return "--";
|
|
985
1148
|
}
|
|
986
1149
|
function completionPercent(run) {
|
|
1150
|
+
if (runNeedsPermission(run)) {
|
|
1151
|
+
return 90;
|
|
1152
|
+
}
|
|
987
1153
|
if (run.status === "merged") {
|
|
988
1154
|
return 100;
|
|
989
1155
|
}
|
|
@@ -1027,6 +1193,9 @@ function canMerge(run) {
|
|
|
1027
1193
|
return run.status === "completed" && run.worktree.enabled;
|
|
1028
1194
|
}
|
|
1029
1195
|
function runSummary(run) {
|
|
1196
|
+
if (runNeedsPermission(run)) {
|
|
1197
|
+
return run.attention.summary ? `needs permission: ${run.attention.summary}` : "needs permission";
|
|
1198
|
+
}
|
|
1030
1199
|
const latestWork = run.work.at(-1);
|
|
1031
1200
|
if (run.status === "steering") {
|
|
1032
1201
|
return "auto-steering after completion";
|
|
@@ -1041,6 +1210,9 @@ function runSummary(run) {
|
|
|
1041
1210
|
return run.status;
|
|
1042
1211
|
}
|
|
1043
1212
|
function workerStateLabel(run) {
|
|
1213
|
+
if (runNeedsPermission(run)) {
|
|
1214
|
+
return "needs permission";
|
|
1215
|
+
}
|
|
1044
1216
|
if (run.status === "completed") {
|
|
1045
1217
|
return canMerge(run) ? "done m merge" : "done";
|
|
1046
1218
|
}
|
|
@@ -1059,6 +1231,9 @@ function workerStateLabel(run) {
|
|
|
1059
1231
|
return "running";
|
|
1060
1232
|
}
|
|
1061
1233
|
function agentRailSummary(run) {
|
|
1234
|
+
if (runNeedsPermission(run)) {
|
|
1235
|
+
return run.attention.summary ?? "waiting for permission";
|
|
1236
|
+
}
|
|
1062
1237
|
const output = summarizeOutput(run.output);
|
|
1063
1238
|
if (output) {
|
|
1064
1239
|
return output;
|
|
@@ -1077,8 +1252,15 @@ function summarizeOutput(output) {
|
|
|
1077
1252
|
const sentence = normalized.match(/^.{24,180}?[.!?](?:\s|$)/)?.[0]?.trim();
|
|
1078
1253
|
return sentence || normalized.slice(0, 180);
|
|
1079
1254
|
}
|
|
1080
|
-
function statusWord(
|
|
1081
|
-
if (
|
|
1255
|
+
function statusWord(run) {
|
|
1256
|
+
if (runNeedsPermission(run)) {
|
|
1257
|
+
return "permission:";
|
|
1258
|
+
}
|
|
1259
|
+
const status = run.status;
|
|
1260
|
+
if (status === "merged") {
|
|
1261
|
+
return "merged:";
|
|
1262
|
+
}
|
|
1263
|
+
if (status === "completed") {
|
|
1082
1264
|
return "done:";
|
|
1083
1265
|
}
|
|
1084
1266
|
if (status === "failed" || status === "merge-conflict") {
|