@viraatdas/rudder 1.0.6 → 1.0.8
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 +364 -26
- 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.d.ts +1 -0
- package/dist/backends.js +59 -11
- 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 +99 -1
- package/dist/state.js.map +1 -1
- package/dist/task-summary.d.ts +6 -0
- package/dist/task-summary.js +186 -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 +24 -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/tmux-dashboard.js
CHANGED
|
@@ -1,20 +1,28 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
|
+
import fsp from "node:fs/promises";
|
|
3
4
|
import { fileURLToPath } from "node:url";
|
|
4
5
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
5
6
|
import { Box, Text, render, useInput, useWindowSize } from "ink";
|
|
7
|
+
import { permissionAttentionFromOutput } from "./agent-attention.js";
|
|
6
8
|
import { discoverEffortOptions, fallbackEffortOptions } from "./effort.js";
|
|
7
|
-
import { currentBranch, findRepoRoot
|
|
9
|
+
import { currentBranch, findRepoRoot } from "./git.js";
|
|
8
10
|
import { discoverModelOptions, fallbackModelOptions } from "./models.js";
|
|
9
|
-
import { startNativeRun, deleteRun, mergeRun, reconcileNativeTerminals, stopRun } from "./run-manager.js";
|
|
10
|
-
import { listRuns, loadConfig,
|
|
11
|
+
import { startNativePlan, startNativeRun, deleteRun, mergeRun, reconcileNativeTerminals, stopRun } from "./run-manager.js";
|
|
12
|
+
import { listRuns, loadConfig, outputPath, rememberBackendSelection } from "./state.js";
|
|
11
13
|
import { loadTmuxDashboardState, updateTmuxDashboardState, } from "./tmux-state.js";
|
|
12
14
|
import { detachClient, resizePane, selectPane } from "./tmux.js";
|
|
15
|
+
import { taskDisplayLabel } from "./task-summary.js";
|
|
13
16
|
import { shortenHome } from "./util.js";
|
|
14
17
|
const COMPLETION_SOUND = fileURLToPath(new URL("../assets/sounds/ping.mp3", import.meta.url));
|
|
18
|
+
const ATTENTION_TAIL_BYTES = 64 * 1024;
|
|
19
|
+
const TASK_HISTORY_LIMIT = 100;
|
|
15
20
|
const SLASH_COMMANDS = [
|
|
16
21
|
{ label: "/backend claude", detail: "use Claude Code for new tasks", value: "/backend claude" },
|
|
17
22
|
{ label: "/backend codex", detail: "use Codex for new tasks", value: "/backend codex" },
|
|
23
|
+
{ label: "/plan", detail: "toggle Rudder read-only plan mode", value: "/plan" },
|
|
24
|
+
{ label: "/plan <task>", detail: "plan one task without toggling", value: "/plan ", complete: "/plan " },
|
|
25
|
+
{ label: "/run <task>", detail: "start implementation even when plan mode is on", value: "/run ", complete: "/run " },
|
|
18
26
|
{ label: "/model", detail: "pick from available models", value: "/model" },
|
|
19
27
|
{ label: "/model <id>", detail: "set model for new tasks", value: "/model ", complete: "/model " },
|
|
20
28
|
{ label: "/clear", detail: "clear the task input", value: "/clear" },
|
|
@@ -51,14 +59,14 @@ function AgentPane({ defaults }) {
|
|
|
51
59
|
const [selectedRunId, setSelectedRunId] = useState();
|
|
52
60
|
const [notice, setNotice] = useState("");
|
|
53
61
|
const [deleteIntent, setDeleteIntent] = useState(null);
|
|
54
|
-
const
|
|
62
|
+
const alertRef = useRef(null);
|
|
55
63
|
const refresh = useCallback(async () => {
|
|
56
64
|
const root = findRepoRoot();
|
|
57
65
|
await reconcileNativeTerminals(root).catch(() => undefined);
|
|
58
66
|
const [nextBranch, nextConfig, nextRuns, state] = await Promise.all([
|
|
59
67
|
currentBranch(root),
|
|
60
68
|
loadConfig(),
|
|
61
|
-
|
|
69
|
+
loadAgentPaneRuns(root),
|
|
62
70
|
loadTmuxDashboardState(root, defaults.tmuxSessionName),
|
|
63
71
|
]);
|
|
64
72
|
setRepoRoot(root);
|
|
@@ -73,7 +81,7 @@ function AgentPane({ defaults }) {
|
|
|
73
81
|
return () => clearInterval(timer);
|
|
74
82
|
}, [refresh]);
|
|
75
83
|
useEffect(() => {
|
|
76
|
-
|
|
84
|
+
notifyRunAlerts(runs, alertRef);
|
|
77
85
|
}, [runs]);
|
|
78
86
|
const selectedIndex = Math.max(0, runs.findIndex((run) => run.id === selectedRunId));
|
|
79
87
|
const selectedRun = runs[selectedIndex];
|
|
@@ -95,16 +103,6 @@ function AgentPane({ defaults }) {
|
|
|
95
103
|
setNotice("");
|
|
96
104
|
return;
|
|
97
105
|
}
|
|
98
|
-
if (chunk === "m" && deleteIntent.canMerge) {
|
|
99
|
-
void deleteRun(deleteIntent.runId, { mergeFirst: true, force: true, silent: true })
|
|
100
|
-
.then(() => {
|
|
101
|
-
setDeleteIntent(null);
|
|
102
|
-
setNotice(`deleted ${shortId(deleteIntent.runId)}`);
|
|
103
|
-
return refresh();
|
|
104
|
-
})
|
|
105
|
-
.catch((error) => setNotice(error instanceof Error ? error.message : String(error)));
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
106
|
if (chunk === "d") {
|
|
109
107
|
void deleteRun(deleteIntent.runId, { force: true, silent: true })
|
|
110
108
|
.then(() => {
|
|
@@ -131,7 +129,12 @@ function AgentPane({ defaults }) {
|
|
|
131
129
|
}
|
|
132
130
|
if (chunk === "m" && selectedRun) {
|
|
133
131
|
void mergeRun(selectedRun.id, false, { silent: true })
|
|
134
|
-
.then(() =>
|
|
132
|
+
.then((merged) => {
|
|
133
|
+
setNotice(merged.merge?.status === "conflict"
|
|
134
|
+
? `merge conflict ${shortId(selectedRun.id)}`
|
|
135
|
+
: `merged ${shortId(selectedRun.id)}`);
|
|
136
|
+
return refresh();
|
|
137
|
+
})
|
|
135
138
|
.catch((error) => setNotice(error instanceof Error ? error.message : String(error)));
|
|
136
139
|
return;
|
|
137
140
|
}
|
|
@@ -140,14 +143,8 @@ function AgentPane({ defaults }) {
|
|
|
140
143
|
return;
|
|
141
144
|
}
|
|
142
145
|
if (chunk === "d" && selectedRun) {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
setDeleteIntent({ runId: selectedRun.id, canMerge: selectedRun.worktree.enabled && canMerge });
|
|
146
|
-
setNotice(selectedRun.worktree.enabled && canMerge
|
|
147
|
-
? "delete? press m to merge then delete, d to delete, Esc cancel"
|
|
148
|
-
: "delete? press d to delete, Esc cancel");
|
|
149
|
-
})
|
|
150
|
-
.catch((error) => setNotice(error instanceof Error ? error.message : String(error)));
|
|
146
|
+
setDeleteIntent({ runId: selectedRun.id });
|
|
147
|
+
setNotice("delete? press d to delete run + worktree, Esc cancel");
|
|
151
148
|
return;
|
|
152
149
|
}
|
|
153
150
|
if (chunk === "q") {
|
|
@@ -157,7 +154,7 @@ function AgentPane({ defaults }) {
|
|
|
157
154
|
const width = Math.max(24, size.columns);
|
|
158
155
|
const maxRuns = Math.max(1, Math.floor((size.rows - 5) / 3));
|
|
159
156
|
const visibleRuns = runs.slice(0, maxRuns);
|
|
160
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "rudder" }), _jsx(Text, { color: "gray", children: summarize(`${shortenHome(repoRoot)} ${branch}`, width) }), _jsxs(Text, { children: ["agents ", _jsxs(Text, { color: "gray", children: [runs.length, " runs"] })] }), visibleRuns.length === 0 ? _jsx(Text, { color: "gray", children: "No agents yet." }) : visibleRuns.map((run) => (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: run.id === selectedRun?.id ? "cyan" : taskColor(run), children: [run.id === selectedRun?.id ? "> " : " ", summarize(run
|
|
157
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "rudder" }), _jsx(Text, { color: "gray", children: summarize(`${shortenHome(repoRoot)} ${branch}`, width) }), _jsxs(Text, { children: ["agents ", _jsxs(Text, { color: "gray", children: [runs.length, " runs"] })] }), visibleRuns.length === 0 ? _jsx(Text, { color: "gray", children: "No agents yet." }) : visibleRuns.map((run) => (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: run.id === selectedRun?.id ? "cyan" : taskColor(run), children: [run.id === selectedRun?.id ? "> " : " ", summarize(taskDisplayLabel(run, 80), width - 3)] }), _jsxs(Text, { children: [_jsxs(Text, { color: runStatusColor(run), children: [" ", statusMark(run)] }), _jsxs(Text, { color: "gray", children: [" ", run.backend, " "] }), _jsx(Text, { color: "magenta", children: modelLabel(run, config) })] })] }, run.id))), notice ? _jsx(Text, { color: deleteIntent ? "red" : "yellow", children: summarize(notice, width) }) : null, _jsx(Text, { color: "gray", children: "j/k select Enter focus m merge dd delete" })] }));
|
|
161
158
|
}
|
|
162
159
|
function TaskPane({ defaults }) {
|
|
163
160
|
const [repoRoot, setRepoRoot] = useState(() => findRepoRoot());
|
|
@@ -166,7 +163,11 @@ function TaskPane({ defaults }) {
|
|
|
166
163
|
const [model, setModel] = useState(defaults.model);
|
|
167
164
|
const [effort, setEffort] = useState();
|
|
168
165
|
const [input, setInput] = useState("");
|
|
166
|
+
const [planMode, setPlanMode] = useState(false);
|
|
169
167
|
const inputRef = useRef("");
|
|
168
|
+
const taskHistoryRef = useRef([]);
|
|
169
|
+
const taskHistoryIndexRef = useRef(null);
|
|
170
|
+
const taskHistoryDraftRef = useRef("");
|
|
170
171
|
const [notice, setNotice] = useState("");
|
|
171
172
|
const [submitting, setSubmitting] = useState(false);
|
|
172
173
|
const [commandIndex, setCommandIndex] = useState(0);
|
|
@@ -220,6 +221,58 @@ function TaskPane({ defaults }) {
|
|
|
220
221
|
inputRef.current = value;
|
|
221
222
|
setInput(value);
|
|
222
223
|
}, []);
|
|
224
|
+
const resetTaskHistoryNavigation = useCallback(() => {
|
|
225
|
+
taskHistoryIndexRef.current = null;
|
|
226
|
+
taskHistoryDraftRef.current = "";
|
|
227
|
+
}, []);
|
|
228
|
+
const editTaskInput = useCallback((next) => {
|
|
229
|
+
resetTaskHistoryNavigation();
|
|
230
|
+
setTaskInput(next);
|
|
231
|
+
}, [resetTaskHistoryNavigation, setTaskInput]);
|
|
232
|
+
const rememberTaskHistory = useCallback((value) => {
|
|
233
|
+
const trimmed = value.trim();
|
|
234
|
+
if (!trimmed) {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
const history = taskHistoryRef.current;
|
|
238
|
+
history.push(trimmed);
|
|
239
|
+
if (history.length > TASK_HISTORY_LIMIT) {
|
|
240
|
+
history.splice(0, history.length - TASK_HISTORY_LIMIT);
|
|
241
|
+
}
|
|
242
|
+
resetTaskHistoryNavigation();
|
|
243
|
+
}, [resetTaskHistoryNavigation]);
|
|
244
|
+
const showTaskHistory = useCallback((direction) => {
|
|
245
|
+
const history = taskHistoryRef.current;
|
|
246
|
+
if (!history.length) {
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
if (direction === "previous") {
|
|
250
|
+
const current = taskHistoryIndexRef.current;
|
|
251
|
+
if (current === null) {
|
|
252
|
+
taskHistoryDraftRef.current = inputRef.current;
|
|
253
|
+
taskHistoryIndexRef.current = history.length - 1;
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
taskHistoryIndexRef.current = Math.max(0, Math.min(current, history.length - 1) - 1);
|
|
257
|
+
}
|
|
258
|
+
setTaskInput(history[taskHistoryIndexRef.current] ?? "");
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
const current = taskHistoryIndexRef.current;
|
|
262
|
+
if (current === null) {
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
if (current + 1 < history.length) {
|
|
266
|
+
taskHistoryIndexRef.current = current + 1;
|
|
267
|
+
setTaskInput(history[taskHistoryIndexRef.current] ?? "");
|
|
268
|
+
return true;
|
|
269
|
+
}
|
|
270
|
+
taskHistoryIndexRef.current = null;
|
|
271
|
+
const draft = taskHistoryDraftRef.current;
|
|
272
|
+
taskHistoryDraftRef.current = "";
|
|
273
|
+
setTaskInput(draft);
|
|
274
|
+
return true;
|
|
275
|
+
}, [setTaskInput]);
|
|
223
276
|
useEffect(() => {
|
|
224
277
|
setCommandIndex(0);
|
|
225
278
|
}, [input]);
|
|
@@ -270,6 +323,7 @@ function TaskPane({ defaults }) {
|
|
|
270
323
|
await submit(resolvedCommand.value);
|
|
271
324
|
return;
|
|
272
325
|
}
|
|
326
|
+
rememberTaskHistory(task);
|
|
273
327
|
if (task === "/model") {
|
|
274
328
|
setTaskInput("");
|
|
275
329
|
setNotice("");
|
|
@@ -279,11 +333,42 @@ function TaskPane({ defaults }) {
|
|
|
279
333
|
setPendingModel(null);
|
|
280
334
|
return;
|
|
281
335
|
}
|
|
336
|
+
if (task === "/plan") {
|
|
337
|
+
setPlanMode((current) => {
|
|
338
|
+
const next = !current;
|
|
339
|
+
setNotice(next ? "Plan mode on: Enter starts a read-only planner" : "Plan mode off");
|
|
340
|
+
return next;
|
|
341
|
+
});
|
|
342
|
+
setTaskInput("");
|
|
343
|
+
setModelPickerOpen(false);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
if (task.startsWith("/plan ")) {
|
|
347
|
+
const planTask = task.slice("/plan ".length).trim();
|
|
348
|
+
if (!planTask) {
|
|
349
|
+
setNotice("Usage: /plan <task>");
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
await startPlanner(planTask);
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
if (task.startsWith("/run ")) {
|
|
356
|
+
const runTask = task.slice("/run ".length).trim();
|
|
357
|
+
if (!runTask) {
|
|
358
|
+
setNotice("Usage: /run <task>");
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
await startWorker(runTask);
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
282
364
|
if (task.startsWith("/model ")) {
|
|
283
365
|
const nextModel = task.slice("/model ".length).trim() || undefined;
|
|
284
366
|
setModel(nextModel);
|
|
285
367
|
setEffort(undefined);
|
|
286
|
-
await updateBackendDefaults(repoRoot, defaults.tmuxSessionName, backend, nextModel, undefined
|
|
368
|
+
await updateBackendDefaults(repoRoot, defaults.tmuxSessionName, backend, nextModel, undefined, {
|
|
369
|
+
updateModel: true,
|
|
370
|
+
updateEffort: true,
|
|
371
|
+
});
|
|
287
372
|
setTaskInput("");
|
|
288
373
|
setNotice("");
|
|
289
374
|
return;
|
|
@@ -294,7 +379,10 @@ function TaskPane({ defaults }) {
|
|
|
294
379
|
setModel(undefined);
|
|
295
380
|
const nextEffort = effortForBackend(nextBackend, config);
|
|
296
381
|
setEffort(nextEffort);
|
|
297
|
-
await updateBackendDefaults(repoRoot, defaults.tmuxSessionName, nextBackend, undefined, nextEffort
|
|
382
|
+
await updateBackendDefaults(repoRoot, defaults.tmuxSessionName, nextBackend, undefined, nextEffort, {
|
|
383
|
+
updateModel: false,
|
|
384
|
+
updateEffort: false,
|
|
385
|
+
});
|
|
298
386
|
setTaskInput("");
|
|
299
387
|
setNotice("");
|
|
300
388
|
setModelPickerOpen(false);
|
|
@@ -307,7 +395,7 @@ function TaskPane({ defaults }) {
|
|
|
307
395
|
}
|
|
308
396
|
if (task === "/help") {
|
|
309
397
|
setTaskInput("");
|
|
310
|
-
setNotice("/
|
|
398
|
+
setNotice("/plan toggles read-only planning, /run bypasses it, /backend claude|codex, /model");
|
|
311
399
|
return;
|
|
312
400
|
}
|
|
313
401
|
if (task === "/detach") {
|
|
@@ -318,6 +406,13 @@ function TaskPane({ defaults }) {
|
|
|
318
406
|
setNotice("Unknown command. Type / to see commands.");
|
|
319
407
|
return;
|
|
320
408
|
}
|
|
409
|
+
if (planMode) {
|
|
410
|
+
await startPlanner(task);
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
await startWorker(task);
|
|
414
|
+
}, [backend, config, defaults.tmuxSessionName, effort, effortOptionsFor, model, modelIndex, modelOptions, modelPickerStep, pendingModel, planMode, rememberTaskHistory, repoRoot, setTaskInput, submitting]);
|
|
415
|
+
async function startWorker(task) {
|
|
321
416
|
const state = await loadTmuxDashboardState(repoRoot, defaults.tmuxSessionName);
|
|
322
417
|
if (!state) {
|
|
323
418
|
setNotice("Rudder tmux state is missing. Reopen rudder.");
|
|
@@ -345,7 +440,36 @@ function TaskPane({ defaults }) {
|
|
|
345
440
|
finally {
|
|
346
441
|
setSubmitting(false);
|
|
347
442
|
}
|
|
348
|
-
}
|
|
443
|
+
}
|
|
444
|
+
async function startPlanner(task) {
|
|
445
|
+
const state = await loadTmuxDashboardState(repoRoot, defaults.tmuxSessionName);
|
|
446
|
+
if (!state) {
|
|
447
|
+
setNotice("Rudder tmux state is missing. Reopen rudder.");
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
setSubmitting(true);
|
|
451
|
+
try {
|
|
452
|
+
const run = await startNativePlan({
|
|
453
|
+
task,
|
|
454
|
+
backend,
|
|
455
|
+
model,
|
|
456
|
+
effort,
|
|
457
|
+
tmuxSessionName: defaults.tmuxSessionName,
|
|
458
|
+
workerPaneId: state.workerPaneId,
|
|
459
|
+
focus: true,
|
|
460
|
+
silent: true,
|
|
461
|
+
});
|
|
462
|
+
await updateTmuxDashboardState(repoRoot, defaults.tmuxSessionName, { selectedRunId: run.id, backend, model, effort });
|
|
463
|
+
setTaskInput("");
|
|
464
|
+
setNotice("Read-only planner started");
|
|
465
|
+
}
|
|
466
|
+
catch (error) {
|
|
467
|
+
setNotice(error instanceof Error ? error.message : String(error));
|
|
468
|
+
}
|
|
469
|
+
finally {
|
|
470
|
+
setSubmitting(false);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
349
473
|
useInput((chunk, key) => {
|
|
350
474
|
if (key.ctrl && chunk === "c") {
|
|
351
475
|
void detachClient(defaults.tmuxSessionName);
|
|
@@ -401,7 +525,10 @@ function TaskPane({ defaults }) {
|
|
|
401
525
|
setBackend(nextBackend);
|
|
402
526
|
setModel(nextModel);
|
|
403
527
|
setEffort(nextEffort);
|
|
404
|
-
void updateBackendDefaults(repoRoot, defaults.tmuxSessionName, nextBackend, nextModel, nextEffort
|
|
528
|
+
void updateBackendDefaults(repoRoot, defaults.tmuxSessionName, nextBackend, nextModel, nextEffort, {
|
|
529
|
+
updateModel: true,
|
|
530
|
+
updateEffort: true,
|
|
531
|
+
});
|
|
405
532
|
setModelPickerOpen(false);
|
|
406
533
|
setModelPickerStep("model");
|
|
407
534
|
setPendingModel(null);
|
|
@@ -415,18 +542,26 @@ function TaskPane({ defaults }) {
|
|
|
415
542
|
if (returnIndex >= 0 && !key.ctrl && !key.meta) {
|
|
416
543
|
const beforeReturn = stripControlInput(chunk.slice(0, returnIndex));
|
|
417
544
|
if (beforeReturn) {
|
|
418
|
-
|
|
545
|
+
editTaskInput((current) => current + beforeReturn);
|
|
419
546
|
}
|
|
420
547
|
void submit();
|
|
421
548
|
return;
|
|
422
549
|
}
|
|
423
550
|
if (isLineClear(chunk, key)) {
|
|
424
|
-
|
|
551
|
+
editTaskInput("");
|
|
425
552
|
setNotice("");
|
|
426
553
|
return;
|
|
427
554
|
}
|
|
428
555
|
if (isWordDelete(chunk, key)) {
|
|
429
|
-
|
|
556
|
+
editTaskInput((current) => deletePreviousWord(current));
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
if (taskHistoryIndexRef.current !== null && key.upArrow) {
|
|
560
|
+
showTaskHistory("previous");
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
if (taskHistoryIndexRef.current !== null && key.downArrow) {
|
|
564
|
+
showTaskHistory("next");
|
|
430
565
|
return;
|
|
431
566
|
}
|
|
432
567
|
if (commandMenuOpen) {
|
|
@@ -439,7 +574,7 @@ function TaskPane({ defaults }) {
|
|
|
439
574
|
return;
|
|
440
575
|
}
|
|
441
576
|
if (key.escape) {
|
|
442
|
-
|
|
577
|
+
editTaskInput("");
|
|
443
578
|
setNotice("");
|
|
444
579
|
return;
|
|
445
580
|
}
|
|
@@ -448,7 +583,7 @@ function TaskPane({ defaults }) {
|
|
|
448
583
|
if (commandMenuOpen) {
|
|
449
584
|
const command = commandOptions[commandIndex];
|
|
450
585
|
if (command?.complete) {
|
|
451
|
-
|
|
586
|
+
editTaskInput(command.complete);
|
|
452
587
|
setNotice("");
|
|
453
588
|
return;
|
|
454
589
|
}
|
|
@@ -460,19 +595,28 @@ function TaskPane({ defaults }) {
|
|
|
460
595
|
void submit();
|
|
461
596
|
return;
|
|
462
597
|
}
|
|
598
|
+
if (key.upArrow) {
|
|
599
|
+
showTaskHistory("previous");
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
if (key.downArrow) {
|
|
603
|
+
showTaskHistory("next");
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
463
606
|
if (key.backspace || key.delete || chunk === "\u007f" || chunk === "\b") {
|
|
464
|
-
|
|
607
|
+
editTaskInput((current) => current.slice(0, -1));
|
|
465
608
|
return;
|
|
466
609
|
}
|
|
467
610
|
if (chunk && !key.ctrl && !key.meta) {
|
|
468
|
-
|
|
611
|
+
editTaskInput((current) => current + chunk);
|
|
469
612
|
}
|
|
470
613
|
});
|
|
471
614
|
const configured = model || (backend === "claude" ? config?.backends.claude?.model : config?.backends.codex?.model) || "default";
|
|
472
615
|
const configuredEffort = effort || effortForBackend(backend, config) || "auto";
|
|
616
|
+
const entryMode = planMode ? "plan" : "run";
|
|
473
617
|
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { color: "cyan", bold: true, children: "TASK" }), " ", input, _jsx(Text, { color: "cyan", children: "_" }), submitting ? _jsx(Text, { color: "gray", children: " starting..." }) : null, !submitting && notice ? _jsxs(Text, { color: "yellow", children: [" ", notice] }) : null] }), commandMenuOpen ? (_jsx(CommandMenu, { commands: commandOptions, selected: commandIndex })) : modelPickerOpen ? (modelPickerStep === "effort"
|
|
474
618
|
? _jsx(EffortMenu, { option: pendingModel ?? modelOptions[modelIndex], selected: effortIndex, backend: toNativeBackend(pendingModel?.backend ?? backend), options: effortOptionsFor(toNativeBackend(pendingModel?.backend ?? backend)) })
|
|
475
|
-
: _jsx(ModelMenu, { options: modelOptions, selected: modelIndex, backend: backend })) : (_jsxs(Text, { color: "gray", children: ["Enter
|
|
619
|
+
: _jsx(ModelMenu, { options: modelOptions, selected: modelIndex, backend: backend })) : (_jsxs(Text, { color: "gray", children: ["Enter ", entryMode, " Up/Down history Tab focus pane /plan /run ", backend, " ", configured, " ", configuredEffort] }))] }));
|
|
476
620
|
}
|
|
477
621
|
function CommandMenu({ commands, selected }) {
|
|
478
622
|
const command = commands[Math.max(0, Math.min(selected, commands.length - 1))];
|
|
@@ -497,6 +641,35 @@ function WorkerIdle(_props) {
|
|
|
497
641
|
function toNativeBackend(backend) {
|
|
498
642
|
return backend === "codex" ? "codex" : "claude";
|
|
499
643
|
}
|
|
644
|
+
async function loadAgentPaneRuns(repoRoot) {
|
|
645
|
+
const runs = await listRuns(repoRoot);
|
|
646
|
+
return await Promise.all(runs.map(async (run) => {
|
|
647
|
+
const output = await readTailIfExists(outputPath(repoRoot, run.id));
|
|
648
|
+
return {
|
|
649
|
+
...run,
|
|
650
|
+
attention: permissionAttentionFromOutput(output),
|
|
651
|
+
};
|
|
652
|
+
}));
|
|
653
|
+
}
|
|
654
|
+
async function readTailIfExists(file) {
|
|
655
|
+
const handle = await fsp.open(file, "r").catch(() => null);
|
|
656
|
+
if (!handle) {
|
|
657
|
+
return "";
|
|
658
|
+
}
|
|
659
|
+
try {
|
|
660
|
+
const stat = await handle.stat();
|
|
661
|
+
const length = Math.min(stat.size, ATTENTION_TAIL_BYTES);
|
|
662
|
+
const buffer = Buffer.alloc(length);
|
|
663
|
+
await handle.read(buffer, 0, length, Math.max(0, stat.size - length));
|
|
664
|
+
return buffer.toString("utf8");
|
|
665
|
+
}
|
|
666
|
+
catch {
|
|
667
|
+
return "";
|
|
668
|
+
}
|
|
669
|
+
finally {
|
|
670
|
+
await handle.close().catch(() => undefined);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
500
673
|
async function focusSelectedWorker(repoRoot, tmuxSessionName, run) {
|
|
501
674
|
if (run?.terminal?.paneId) {
|
|
502
675
|
await selectPane(run.terminal.paneId).catch(() => undefined);
|
|
@@ -507,22 +680,35 @@ async function focusSelectedWorker(repoRoot, tmuxSessionName, run) {
|
|
|
507
680
|
await selectPane(state.workerPaneId).catch(() => undefined);
|
|
508
681
|
}
|
|
509
682
|
}
|
|
510
|
-
function
|
|
511
|
-
const
|
|
683
|
+
function notifyRunAlerts(runs, ref) {
|
|
684
|
+
const terminal = new Set(runs.filter((run) => isTerminalRun(run)).map((run) => run.id));
|
|
685
|
+
const permission = new Set(runs.filter((run) => runNeedsPermission(run)).map((run) => run.id));
|
|
512
686
|
if (!ref.current) {
|
|
513
|
-
ref.current =
|
|
687
|
+
ref.current = { terminal, permission };
|
|
514
688
|
return;
|
|
515
689
|
}
|
|
516
690
|
for (const run of runs) {
|
|
517
|
-
if (isTerminalRun(run) && !ref.current.has(run.id)) {
|
|
691
|
+
if (isTerminalRun(run) && !ref.current.terminal.has(run.id)) {
|
|
518
692
|
playCompletionSound();
|
|
519
|
-
ref.current.add(run.id);
|
|
693
|
+
ref.current.terminal.add(run.id);
|
|
694
|
+
}
|
|
695
|
+
if (runNeedsPermission(run) && !ref.current.permission.has(run.id)) {
|
|
696
|
+
playCompletionSound();
|
|
697
|
+
ref.current.permission.add(run.id);
|
|
520
698
|
}
|
|
521
699
|
}
|
|
700
|
+
ref.current.terminal = terminal;
|
|
701
|
+
ref.current.permission = permission;
|
|
522
702
|
}
|
|
523
703
|
function isTerminalRun(run) {
|
|
524
704
|
return ["completed", "failed", "cancelled", "merged", "merge-conflict"].includes(run.status);
|
|
525
705
|
}
|
|
706
|
+
function isActiveRun(run) {
|
|
707
|
+
return ["created", "running", "steering", "verifying"].includes(run.status);
|
|
708
|
+
}
|
|
709
|
+
function runNeedsPermission(run) {
|
|
710
|
+
return isActiveRun(run) && run.attention.needsPermission;
|
|
711
|
+
}
|
|
526
712
|
function playCompletionSound() {
|
|
527
713
|
try {
|
|
528
714
|
const player = process.platform === "darwin" ? "afplay" : "ffplay";
|
|
@@ -538,6 +724,10 @@ function playCompletionSound() {
|
|
|
538
724
|
}
|
|
539
725
|
}
|
|
540
726
|
function statusMark(run) {
|
|
727
|
+
if (runNeedsPermission(run))
|
|
728
|
+
return "needs permission";
|
|
729
|
+
if (run.mode === "plan" && isActiveRun(run))
|
|
730
|
+
return "planning";
|
|
541
731
|
if (run.status === "merged")
|
|
542
732
|
return "merged";
|
|
543
733
|
if (run.status === "completed")
|
|
@@ -550,6 +740,9 @@ function statusMark(run) {
|
|
|
550
740
|
return "running";
|
|
551
741
|
return "queued";
|
|
552
742
|
}
|
|
743
|
+
function runStatusColor(run) {
|
|
744
|
+
return runNeedsPermission(run) ? "yellow" : statusColor(run);
|
|
745
|
+
}
|
|
553
746
|
function statusColor(run) {
|
|
554
747
|
if (run.status === "merged" || run.status === "completed")
|
|
555
748
|
return "green";
|
|
@@ -571,7 +764,8 @@ function modelLabel(run, config) {
|
|
|
571
764
|
: config?.backends.codex?.model)
|
|
572
765
|
?? "default";
|
|
573
766
|
const effort = run.effort ?? effortForBackend(toNativeBackend(run.backend), config);
|
|
574
|
-
|
|
767
|
+
const mode = run.mode === "plan" ? "plan " : "";
|
|
768
|
+
return summarize(`${mode}${model} ${effort ?? "auto"}`, 18);
|
|
575
769
|
}
|
|
576
770
|
function modelForBackend(backend, config) {
|
|
577
771
|
return backend === "claude" ? config?.backends.claude?.model : config?.backends.codex?.model;
|
|
@@ -582,25 +776,15 @@ function effortForBackend(backend, config) {
|
|
|
582
776
|
}
|
|
583
777
|
return config?.backends.codex?.reasoningEffort ?? config?.backends.codex?.effort;
|
|
584
778
|
}
|
|
585
|
-
async function updateBackendDefaults(repoRoot, tmuxSessionName, backend, model, effort) {
|
|
779
|
+
async function updateBackendDefaults(repoRoot, tmuxSessionName, backend, model, effort, options) {
|
|
586
780
|
await updateTmuxDashboardState(repoRoot, tmuxSessionName, { backend, model, effort });
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
};
|
|
595
|
-
}
|
|
596
|
-
else {
|
|
597
|
-
config.backends.codex = {
|
|
598
|
-
...(config.backends.codex ?? {}),
|
|
599
|
-
model,
|
|
600
|
-
reasoningEffort: effort,
|
|
601
|
-
};
|
|
602
|
-
}
|
|
603
|
-
await saveConfig(config);
|
|
781
|
+
await rememberBackendSelection({
|
|
782
|
+
backend,
|
|
783
|
+
model,
|
|
784
|
+
effort,
|
|
785
|
+
updateModel: options.updateModel,
|
|
786
|
+
updateEffort: options.updateEffort,
|
|
787
|
+
});
|
|
604
788
|
}
|
|
605
789
|
function withBackend(options, backend) {
|
|
606
790
|
return options.map((option) => ({ ...option, backend }));
|
|
@@ -618,7 +802,7 @@ function isExactRunnableCommand(input) {
|
|
|
618
802
|
return SLASH_COMMANDS.some((command) => !command.complete && command.value === trimmed);
|
|
619
803
|
}
|
|
620
804
|
function resolveSlashCommand(input) {
|
|
621
|
-
if (!input.startsWith("/") || input.startsWith("/model ")) {
|
|
805
|
+
if (!input.startsWith("/") || input.startsWith("/model ") || input.startsWith("/plan ") || input.startsWith("/run ")) {
|
|
622
806
|
return undefined;
|
|
623
807
|
}
|
|
624
808
|
if (isExactRunnableCommand(input)) {
|