@viraatdas/rudder 0.5.12 → 0.6.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 +134 -108
- 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/auth.js +25 -15
- package/dist/auth.js.map +1 -1
- package/dist/native/rudder-native +0 -0
- package/dist/repl.js +11 -3
- package/dist/repl.js.map +1 -1
- package/dist/run-manager.d.ts +1 -1
- package/dist/run-manager.js +19 -11
- package/dist/run-manager.js.map +1 -1
- package/dist/state.d.ts +8 -1
- package/dist/state.js +39 -0
- package/dist/state.js.map +1 -1
- package/dist/tmux-dashboard.js +162 -40
- package/dist/tmux-dashboard.js.map +1 -1
- package/dist/tui.js +205 -21
- package/dist/tui.js.map +1 -1
- package/package.json +1 -1
package/dist/tui.js
CHANGED
|
@@ -4,9 +4,10 @@ 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 { permissionAttentionFromOutput } from "./agent-attention.js";
|
|
7
8
|
import { currentBranch, findRepoRoot, hasChanges } 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";
|
|
11
12
|
import { pathExists, shortenHome } from "./util.js";
|
|
12
13
|
const INTERACTIVE_BACKENDS = ["claude", "codex"];
|
|
@@ -58,9 +59,11 @@ function RudderTui({ defaults }) {
|
|
|
58
59
|
const [commandMenuIndex, setCommandMenuIndex] = useState(0);
|
|
59
60
|
const [discoveredModels, setDiscoveredModels] = useState([]);
|
|
60
61
|
const [deletePrompt, setDeletePrompt] = useState(null);
|
|
62
|
+
const [mergePrompt, setMergePrompt] = useState(null);
|
|
63
|
+
const [conflictPrompt, setConflictPrompt] = useState(null);
|
|
61
64
|
const [submitting, setSubmitting] = useState(false);
|
|
62
65
|
const [preferencesLoaded, setPreferencesLoaded] = useState(false);
|
|
63
|
-
const
|
|
66
|
+
const notifiedAlerts = useRef(null);
|
|
64
67
|
const refresh = useCallback(async () => {
|
|
65
68
|
const root = findRepoRoot();
|
|
66
69
|
const [nextConfig, nextBranch, nextRuns] = await Promise.all([
|
|
@@ -71,7 +74,7 @@ function RudderTui({ defaults }) {
|
|
|
71
74
|
setRepoRoot(root);
|
|
72
75
|
setConfig(nextConfig);
|
|
73
76
|
setBranch(nextBranch);
|
|
74
|
-
|
|
77
|
+
notifyRunAlerts(nextRuns, notifiedAlerts);
|
|
75
78
|
setRuns(nextRuns);
|
|
76
79
|
if (!preferencesLoaded) {
|
|
77
80
|
setBackend(toInteractiveBackend(defaults.backend ?? nextConfig.lastUsedBackend ?? nextConfig.defaultBackend));
|
|
@@ -199,6 +202,107 @@ function RudderTui({ defaults }) {
|
|
|
199
202
|
setNotice(error instanceof Error ? error.message : String(error));
|
|
200
203
|
}
|
|
201
204
|
}, [deletePrompt, refresh]);
|
|
205
|
+
const requestMergeRun = useCallback((runOverride, allowDirty = false) => {
|
|
206
|
+
const run = runOverride ?? selectedRun;
|
|
207
|
+
if (!run) {
|
|
208
|
+
setNotice("No agent selected");
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (!canMerge(run)) {
|
|
212
|
+
setNotice(`${shortId(run.id)} is not ready to merge`);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
setDeletePrompt(null);
|
|
216
|
+
setConflictPrompt(null);
|
|
217
|
+
setMergePrompt({
|
|
218
|
+
kind: "selected",
|
|
219
|
+
runId: run.id,
|
|
220
|
+
label: truncate(run.task, 48),
|
|
221
|
+
allowDirty,
|
|
222
|
+
});
|
|
223
|
+
setNotice(`Merge ${shortId(run.id)}? press y to confirm or n to cancel`);
|
|
224
|
+
}, [selectedRun]);
|
|
225
|
+
const requestMergeAll = useCallback((allowDirty = false) => {
|
|
226
|
+
const ready = runs.filter(canMerge);
|
|
227
|
+
if (ready.length === 0) {
|
|
228
|
+
setNotice("No completed worktree runs ready to merge");
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
setDeletePrompt(null);
|
|
232
|
+
setConflictPrompt(null);
|
|
233
|
+
setMergePrompt({ kind: "all", runIds: ready.map((run) => run.id), allowDirty });
|
|
234
|
+
setNotice(`Merge ${ready.length} run${ready.length === 1 ? "" : "s"}? press y to confirm or n to cancel`);
|
|
235
|
+
}, [runs]);
|
|
236
|
+
const confirmMerge = useCallback(async () => {
|
|
237
|
+
if (!mergePrompt) {
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
const prompt = mergePrompt;
|
|
241
|
+
setMergePrompt(null);
|
|
242
|
+
try {
|
|
243
|
+
if (prompt.kind === "selected") {
|
|
244
|
+
const merged = await mergeRun(prompt.runId, prompt.allowDirty, { silent: true });
|
|
245
|
+
if (merged.merge?.status === "conflict") {
|
|
246
|
+
const files = merged.merge.conflictedFiles ?? [];
|
|
247
|
+
setConflictPrompt({ runId: prompt.runId, files });
|
|
248
|
+
setNotice(`Merge conflict in ${files.length || "unknown"} file${files.length === 1 ? "" : "s"}; press y for AI help or n for manual`);
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
setNotice(`Merged ${shortId(prompt.runId)}`);
|
|
252
|
+
}
|
|
253
|
+
await refresh();
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
let mergedCount = 0;
|
|
257
|
+
for (const runId of prompt.runIds) {
|
|
258
|
+
const merged = await mergeRun(runId, prompt.allowDirty, { silent: true });
|
|
259
|
+
if (merged.merge?.status === "conflict") {
|
|
260
|
+
const files = merged.merge.conflictedFiles ?? [];
|
|
261
|
+
setConflictPrompt({ runId, files });
|
|
262
|
+
setNotice(`Merge all stopped after ${mergedCount}: conflict in ${shortId(runId)}; press y for AI help or n for manual`);
|
|
263
|
+
await refresh();
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
mergedCount += 1;
|
|
267
|
+
}
|
|
268
|
+
setNotice(`Merged ${mergedCount} run${mergedCount === 1 ? "" : "s"}`);
|
|
269
|
+
await refresh();
|
|
270
|
+
}
|
|
271
|
+
catch (error) {
|
|
272
|
+
setNotice(error instanceof Error ? error.message : String(error));
|
|
273
|
+
await refresh();
|
|
274
|
+
}
|
|
275
|
+
}, [mergePrompt, refresh]);
|
|
276
|
+
const startConflictResolver = useCallback(async () => {
|
|
277
|
+
if (!conflictPrompt) {
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
const files = conflictPrompt.files.length ? conflictPrompt.files.join("\n") : "(git did not report conflicted files)";
|
|
281
|
+
const task = [
|
|
282
|
+
"Read RUDDER.md first. A git merge stopped with conflicts in this checkout.",
|
|
283
|
+
`Conflicted files:\n${files}`,
|
|
284
|
+
"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.",
|
|
285
|
+
].join("\n\n");
|
|
286
|
+
try {
|
|
287
|
+
const run = await startRun({
|
|
288
|
+
task,
|
|
289
|
+
backend,
|
|
290
|
+
model,
|
|
291
|
+
detach: true,
|
|
292
|
+
worktree: false,
|
|
293
|
+
silent: true,
|
|
294
|
+
view: "shell",
|
|
295
|
+
});
|
|
296
|
+
setConflictPrompt(null);
|
|
297
|
+
setSelectedRunId(run.id);
|
|
298
|
+
setExpandedRunIds((current) => new Set(current).add(run.id));
|
|
299
|
+
setNotice(`Started AI merge-conflict resolver ${shortId(run.id)}`);
|
|
300
|
+
await refresh();
|
|
301
|
+
}
|
|
302
|
+
catch (error) {
|
|
303
|
+
setNotice(error instanceof Error ? error.message : String(error));
|
|
304
|
+
}
|
|
305
|
+
}, [backend, conflictPrompt, model, refresh]);
|
|
202
306
|
const copySelectedTranscript = useCallback(async (runOverride) => {
|
|
203
307
|
const run = runOverride ?? selectedRun;
|
|
204
308
|
if (!run) {
|
|
@@ -229,7 +333,9 @@ function RudderTui({ defaults }) {
|
|
|
229
333
|
return;
|
|
230
334
|
case "backend":
|
|
231
335
|
if (isInteractiveBackend(args[0])) {
|
|
232
|
-
|
|
336
|
+
const nextBackend = args[0];
|
|
337
|
+
chooseBackend(nextBackend, setBackend, setModel, setNotice);
|
|
338
|
+
setConfig(await rememberBackendSelection({ backend: nextBackend }));
|
|
233
339
|
}
|
|
234
340
|
else {
|
|
235
341
|
setNotice("Usage: /backend claude|codex");
|
|
@@ -267,6 +373,11 @@ function RudderTui({ defaults }) {
|
|
|
267
373
|
else {
|
|
268
374
|
const nextModel = args.join(" ");
|
|
269
375
|
setModel(nextModel);
|
|
376
|
+
setConfig(await rememberBackendSelection({
|
|
377
|
+
backend,
|
|
378
|
+
model: nextModel,
|
|
379
|
+
updateModel: true,
|
|
380
|
+
}));
|
|
270
381
|
setNotice(`Model ${nextModel}`);
|
|
271
382
|
}
|
|
272
383
|
setInput("");
|
|
@@ -289,11 +400,11 @@ function RudderTui({ defaults }) {
|
|
|
289
400
|
setInput("");
|
|
290
401
|
return;
|
|
291
402
|
case "merge":
|
|
292
|
-
|
|
403
|
+
requestMergeRun(resolveUiRun(runs, args[0] ?? selectedRun?.id), args.includes("--allow-dirty"));
|
|
293
404
|
setInput("");
|
|
294
405
|
return;
|
|
295
406
|
case "merge-all":
|
|
296
|
-
|
|
407
|
+
requestMergeAll(args.includes("--allow-dirty"));
|
|
297
408
|
setInput("");
|
|
298
409
|
return;
|
|
299
410
|
case "clear":
|
|
@@ -305,7 +416,7 @@ function RudderTui({ defaults }) {
|
|
|
305
416
|
setNotice(`Unknown command: /${command}`);
|
|
306
417
|
setInput("");
|
|
307
418
|
}
|
|
308
|
-
}, [app, backend, copySelectedTranscript, refresh, requestDeleteSelectedRun, runs, selectedRun?.id]);
|
|
419
|
+
}, [app, backend, copySelectedTranscript, refresh, requestDeleteSelectedRun, requestMergeAll, requestMergeRun, runs, selectedRun?.id]);
|
|
309
420
|
const selectModelOption = useCallback((index) => {
|
|
310
421
|
const option = modelOptions[index];
|
|
311
422
|
if (!option) {
|
|
@@ -315,7 +426,14 @@ function RudderTui({ defaults }) {
|
|
|
315
426
|
setModelMenuOpen(false);
|
|
316
427
|
setModelMenuIndex(index);
|
|
317
428
|
setNotice(option.value ? `Model ${option.value}` : "Using backend default model");
|
|
318
|
-
|
|
429
|
+
void rememberBackendSelection({
|
|
430
|
+
backend,
|
|
431
|
+
model: option.value,
|
|
432
|
+
updateModel: true,
|
|
433
|
+
}).then(setConfig).catch((error) => {
|
|
434
|
+
setNotice(error instanceof Error ? error.message : String(error));
|
|
435
|
+
});
|
|
436
|
+
}, [backend, modelOptions]);
|
|
319
437
|
const selectCommandOption = useCallback((index) => {
|
|
320
438
|
const option = commandOptions[index];
|
|
321
439
|
if (!option) {
|
|
@@ -355,6 +473,30 @@ function RudderTui({ defaults }) {
|
|
|
355
473
|
}
|
|
356
474
|
return;
|
|
357
475
|
}
|
|
476
|
+
if (mergePrompt) {
|
|
477
|
+
if (key.escape || value === "n" || value === "N") {
|
|
478
|
+
setMergePrompt(null);
|
|
479
|
+
setNotice("Merge cancelled");
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
if (value === "y" || value === "Y") {
|
|
483
|
+
void confirmMerge();
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
if (conflictPrompt) {
|
|
489
|
+
if (key.escape || value === "n" || value === "N") {
|
|
490
|
+
setConflictPrompt(null);
|
|
491
|
+
setNotice("Resolve the merge conflicts manually, then commit");
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
if (value === "y" || value === "Y") {
|
|
495
|
+
void startConflictResolver();
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
358
500
|
if (deletePrompt) {
|
|
359
501
|
if (key.escape) {
|
|
360
502
|
setDeletePrompt(null);
|
|
@@ -532,11 +674,11 @@ function RudderTui({ defaults }) {
|
|
|
532
674
|
return;
|
|
533
675
|
}
|
|
534
676
|
if (input.length === 0 && value === "m" && selectedRun) {
|
|
535
|
-
|
|
677
|
+
requestMergeRun(selectedRun);
|
|
536
678
|
return;
|
|
537
679
|
}
|
|
538
680
|
if (input.length === 0 && value === "M") {
|
|
539
|
-
|
|
681
|
+
requestMergeAll();
|
|
540
682
|
return;
|
|
541
683
|
}
|
|
542
684
|
if (input.length === 0 && value === "d") {
|
|
@@ -565,7 +707,7 @@ function RudderTui({ defaults }) {
|
|
|
565
707
|
const railWidth = Math.min(42, Math.max(30, Math.floor(width * 0.34)));
|
|
566
708
|
const detailWidth = Math.max(30, width - railWidth - 1);
|
|
567
709
|
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"
|
|
710
|
+
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
711
|
? _jsx(StatusDock, { notice: notice })
|
|
570
712
|
: _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
713
|
}
|
|
@@ -579,13 +721,13 @@ function RunRail(props) {
|
|
|
579
721
|
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
722
|
}
|
|
581
723
|
function RunCard(props) {
|
|
582
|
-
const tone =
|
|
724
|
+
const tone = runStatusColor(props.run);
|
|
583
725
|
const label = props.selected ? (props.targeted ? ">>" : "> ") : " ";
|
|
584
726
|
const task = truncate(props.run.task, Math.max(12, props.width - 14));
|
|
585
727
|
const progress = completionPercent(props.run);
|
|
586
728
|
const summary = truncate(agentRailSummary(props.run), Math.max(12, props.width - 7));
|
|
587
729
|
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
|
|
730
|
+
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
731
|
}
|
|
590
732
|
function DetailPane(props) {
|
|
591
733
|
if (!props.run) {
|
|
@@ -594,7 +736,7 @@ function DetailPane(props) {
|
|
|
594
736
|
const composerHeight = props.focused ? 3 : 0;
|
|
595
737
|
const outputHeight = Math.max(5, props.height - 6 - composerHeight);
|
|
596
738
|
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" :
|
|
739
|
+
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
740
|
}
|
|
599
741
|
function WorkerComposer(props) {
|
|
600
742
|
const active = isActive(props.run.status);
|
|
@@ -642,6 +784,18 @@ function DeletePromptBox(props) {
|
|
|
642
784
|
: "d delete run, Esc cancel";
|
|
643
785
|
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
786
|
}
|
|
787
|
+
function MergePromptBox(props) {
|
|
788
|
+
const contentWidth = Math.max(24, props.width - 4);
|
|
789
|
+
const subject = props.prompt.kind === "selected"
|
|
790
|
+
? `merge ${shortId(props.prompt.runId)} ${props.prompt.label}`
|
|
791
|
+
: `merge ${props.prompt.runIds.length} completed run${props.prompt.runIds.length === 1 ? "" : "s"}`;
|
|
792
|
+
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) }), _jsx(Text, { color: "gray", children: fitLine("press y to merge, n to cancel", contentWidth) })] }));
|
|
793
|
+
}
|
|
794
|
+
function MergeConflictPromptBox(props) {
|
|
795
|
+
const contentWidth = Math.max(24, props.width - 4);
|
|
796
|
+
const files = props.prompt.files.length ? props.prompt.files.join(", ") : "unknown files";
|
|
797
|
+
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) })] }));
|
|
798
|
+
}
|
|
645
799
|
function PromptDock(props) {
|
|
646
800
|
const label = props.targetRun ? `${isActive(props.targetRun.status) ? "interrupt" : "agent"} ${shortId(props.targetRun.id)}` : "task";
|
|
647
801
|
const showTextLabel = !props.focused || Boolean(props.targetRun);
|
|
@@ -665,6 +819,7 @@ async function loadUiRuns(repoRoot) {
|
|
|
665
819
|
output,
|
|
666
820
|
events,
|
|
667
821
|
work: buildWork(events, run),
|
|
822
|
+
attention: permissionAttentionFromOutput(output),
|
|
668
823
|
};
|
|
669
824
|
}));
|
|
670
825
|
}
|
|
@@ -853,25 +1008,32 @@ function modelForBackend(backend, config) {
|
|
|
853
1008
|
return undefined;
|
|
854
1009
|
}
|
|
855
1010
|
if (backend === "claude") {
|
|
856
|
-
return config.backends.claude?.model
|
|
1011
|
+
return config.backends.claude?.model;
|
|
857
1012
|
}
|
|
858
1013
|
if (backend === "codex") {
|
|
859
1014
|
return config.backends.codex?.model;
|
|
860
1015
|
}
|
|
861
1016
|
return config.backends.acpx?.model;
|
|
862
1017
|
}
|
|
863
|
-
function
|
|
864
|
-
const
|
|
1018
|
+
function notifyRunAlerts(runs, ref) {
|
|
1019
|
+
const terminal = new Set(runs.filter((run) => isTerminal(run.status)).map((run) => run.id));
|
|
1020
|
+
const permission = new Set(runs.filter((run) => runNeedsPermission(run)).map((run) => run.id));
|
|
865
1021
|
if (!ref.current) {
|
|
866
|
-
ref.current =
|
|
1022
|
+
ref.current = { terminal, permission };
|
|
867
1023
|
return;
|
|
868
1024
|
}
|
|
869
1025
|
for (const run of runs) {
|
|
870
|
-
if (isTerminal(run.status) && !ref.current.has(run.id)) {
|
|
1026
|
+
if (isTerminal(run.status) && !ref.current.terminal.has(run.id)) {
|
|
871
1027
|
playCompletionSound();
|
|
872
|
-
ref.current.add(run.id);
|
|
1028
|
+
ref.current.terminal.add(run.id);
|
|
1029
|
+
}
|
|
1030
|
+
if (runNeedsPermission(run) && !ref.current.permission.has(run.id)) {
|
|
1031
|
+
playCompletionSound();
|
|
1032
|
+
ref.current.permission.add(run.id);
|
|
873
1033
|
}
|
|
874
1034
|
}
|
|
1035
|
+
ref.current.terminal = terminal;
|
|
1036
|
+
ref.current.permission = permission;
|
|
875
1037
|
}
|
|
876
1038
|
function playCompletionSound() {
|
|
877
1039
|
try {
|
|
@@ -953,6 +1115,12 @@ function statusColor(status) {
|
|
|
953
1115
|
}
|
|
954
1116
|
return "cyan";
|
|
955
1117
|
}
|
|
1118
|
+
function runStatusColor(run) {
|
|
1119
|
+
return runNeedsPermission(run) ? "yellow" : statusColor(run.status);
|
|
1120
|
+
}
|
|
1121
|
+
function runNeedsPermission(run) {
|
|
1122
|
+
return isActive(run.status) && run.attention.needsPermission;
|
|
1123
|
+
}
|
|
956
1124
|
function toneColor(tone) {
|
|
957
1125
|
if (tone === "success") {
|
|
958
1126
|
return "green";
|
|
@@ -984,6 +1152,9 @@ function workGlyph(tone) {
|
|
|
984
1152
|
return "--";
|
|
985
1153
|
}
|
|
986
1154
|
function completionPercent(run) {
|
|
1155
|
+
if (runNeedsPermission(run)) {
|
|
1156
|
+
return 90;
|
|
1157
|
+
}
|
|
987
1158
|
if (run.status === "merged") {
|
|
988
1159
|
return 100;
|
|
989
1160
|
}
|
|
@@ -1027,6 +1198,9 @@ function canMerge(run) {
|
|
|
1027
1198
|
return run.status === "completed" && run.worktree.enabled;
|
|
1028
1199
|
}
|
|
1029
1200
|
function runSummary(run) {
|
|
1201
|
+
if (runNeedsPermission(run)) {
|
|
1202
|
+
return run.attention.summary ? `needs permission: ${run.attention.summary}` : "needs permission";
|
|
1203
|
+
}
|
|
1030
1204
|
const latestWork = run.work.at(-1);
|
|
1031
1205
|
if (run.status === "steering") {
|
|
1032
1206
|
return "auto-steering after completion";
|
|
@@ -1041,6 +1215,9 @@ function runSummary(run) {
|
|
|
1041
1215
|
return run.status;
|
|
1042
1216
|
}
|
|
1043
1217
|
function workerStateLabel(run) {
|
|
1218
|
+
if (runNeedsPermission(run)) {
|
|
1219
|
+
return "needs permission";
|
|
1220
|
+
}
|
|
1044
1221
|
if (run.status === "completed") {
|
|
1045
1222
|
return canMerge(run) ? "done m merge" : "done";
|
|
1046
1223
|
}
|
|
@@ -1059,6 +1236,9 @@ function workerStateLabel(run) {
|
|
|
1059
1236
|
return "running";
|
|
1060
1237
|
}
|
|
1061
1238
|
function agentRailSummary(run) {
|
|
1239
|
+
if (runNeedsPermission(run)) {
|
|
1240
|
+
return run.attention.summary ?? "waiting for permission";
|
|
1241
|
+
}
|
|
1062
1242
|
const output = summarizeOutput(run.output);
|
|
1063
1243
|
if (output) {
|
|
1064
1244
|
return output;
|
|
@@ -1077,7 +1257,11 @@ function summarizeOutput(output) {
|
|
|
1077
1257
|
const sentence = normalized.match(/^.{24,180}?[.!?](?:\s|$)/)?.[0]?.trim();
|
|
1078
1258
|
return sentence || normalized.slice(0, 180);
|
|
1079
1259
|
}
|
|
1080
|
-
function statusWord(
|
|
1260
|
+
function statusWord(run) {
|
|
1261
|
+
if (runNeedsPermission(run)) {
|
|
1262
|
+
return "permission:";
|
|
1263
|
+
}
|
|
1264
|
+
const status = run.status;
|
|
1081
1265
|
if (status === "completed" || status === "merged") {
|
|
1082
1266
|
return "done:";
|
|
1083
1267
|
}
|