@viraatdas/rudder 2.10.8 → 2.10.9

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.
@@ -1,890 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { spawn } from "node:child_process";
3
- import fsp from "node:fs/promises";
4
- import { fileURLToPath } from "node:url";
5
- import { useCallback, useEffect, useMemo, useRef, useState } from "react";
6
- import { Box, Text, render, useInput, useWindowSize } from "ink";
7
- import { permissionAttentionFromOutput } from "./agent-attention.js";
8
- import { discoverEffortOptions, fallbackEffortOptions } from "./effort.js";
9
- import { currentBranch, findRepoRoot } from "./git.js";
10
- import { discoverModelOptions, fallbackModelOptions } from "./models.js";
11
- import { startNativePlan, startNativeRun, deleteRun, mergeRun, reconcileNativeTerminals, stopRun, syncRun } from "./run-manager.js";
12
- import { listRuns, loadConfig, outputPath, rememberBackendSelection } from "./state.js";
13
- import { loadTmuxDashboardState, updateTmuxDashboardState, } from "./tmux-state.js";
14
- import { detachClient, resizePane, selectPane } from "./tmux.js";
15
- import { taskDisplayLabel } from "./task-summary.js";
16
- import { shortenHome } from "./util.js";
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;
20
- const SLASH_COMMANDS = [
21
- { label: "/backend claude", detail: "use Claude Code for new tasks", value: "/backend claude" },
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 " },
26
- { label: "/sync", detail: "rebase the selected worktree without merging", value: "/sync" },
27
- { label: "/sync <run>", detail: "rebase a worktree without merging", value: "/sync ", complete: "/sync " },
28
- { label: "/model", detail: "pick from available models", value: "/model" },
29
- { label: "/model <id>", detail: "set model for new tasks", value: "/model ", complete: "/model " },
30
- { label: "/clear", detail: "clear the task input", value: "/clear" },
31
- { label: "/help", detail: "show available task commands", value: "/help" },
32
- { label: "/detach", detail: "detach the tmux session", value: "/detach" },
33
- ];
34
- export async function runTmuxAgentPane(defaults) {
35
- const instance = render(_jsx(AgentPane, { defaults: defaults }), {
36
- exitOnCtrlC: false,
37
- maxFps: 20,
38
- });
39
- await instance.waitUntilExit();
40
- }
41
- export async function runTmuxTaskPane(defaults) {
42
- const instance = render(_jsx(TaskPane, { defaults: defaults }), {
43
- exitOnCtrlC: false,
44
- maxFps: 30,
45
- });
46
- await instance.waitUntilExit();
47
- }
48
- export async function runTmuxWorkerIdle(defaults) {
49
- const instance = render(_jsx(WorkerIdle, { defaults: defaults }), {
50
- exitOnCtrlC: false,
51
- maxFps: 10,
52
- });
53
- await instance.waitUntilExit();
54
- }
55
- function AgentPane({ defaults }) {
56
- const size = useWindowSize();
57
- const [repoRoot, setRepoRoot] = useState(() => findRepoRoot());
58
- const [branch, setBranch] = useState("HEAD");
59
- const [config, setConfig] = useState(null);
60
- const [runs, setRuns] = useState([]);
61
- const [selectedRunId, setSelectedRunId] = useState();
62
- const [notice, setNotice] = useState("");
63
- const [deleteIntent, setDeleteIntent] = useState(null);
64
- const alertRef = useRef(null);
65
- const refresh = useCallback(async () => {
66
- const root = findRepoRoot();
67
- await reconcileNativeTerminals(root).catch(() => undefined);
68
- const [nextBranch, nextConfig, nextRuns, state] = await Promise.all([
69
- currentBranch(root),
70
- loadConfig(),
71
- loadAgentPaneRuns(root),
72
- loadTmuxDashboardState(root, defaults.tmuxSessionName),
73
- ]);
74
- setRepoRoot(root);
75
- setBranch(nextBranch);
76
- setConfig(nextConfig);
77
- setRuns(nextRuns);
78
- setSelectedRunId((current) => state?.selectedRunId ?? current ?? nextRuns[0]?.id);
79
- }, [defaults.tmuxSessionName]);
80
- useEffect(() => {
81
- void refresh();
82
- const timer = setInterval(() => void refresh(), 1000);
83
- return () => clearInterval(timer);
84
- }, [refresh]);
85
- useEffect(() => {
86
- notifyRunAlerts(runs, alertRef);
87
- }, [runs]);
88
- const selectedIndex = Math.max(0, runs.findIndex((run) => run.id === selectedRunId));
89
- const selectedRun = runs[selectedIndex];
90
- const selectRun = useCallback(async (run) => {
91
- if (!run) {
92
- return;
93
- }
94
- setSelectedRunId(run.id);
95
- await updateTmuxDashboardState(repoRoot, defaults.tmuxSessionName, { selectedRunId: run.id });
96
- }, [defaults.tmuxSessionName, repoRoot]);
97
- useInput((chunk, key) => {
98
- if (key.ctrl && chunk === "c") {
99
- void detachClient(defaults.tmuxSessionName);
100
- return;
101
- }
102
- if (deleteIntent) {
103
- if (key.escape) {
104
- setDeleteIntent(null);
105
- setNotice("");
106
- return;
107
- }
108
- if (chunk === "d") {
109
- void deleteRun(deleteIntent.runId, { force: true, silent: true })
110
- .then(() => {
111
- setDeleteIntent(null);
112
- setNotice(`deleted ${shortId(deleteIntent.runId)}`);
113
- return refresh();
114
- })
115
- .catch((error) => setNotice(error instanceof Error ? error.message : String(error)));
116
- return;
117
- }
118
- return;
119
- }
120
- if (key.upArrow || chunk === "k") {
121
- void selectRun(runs[Math.max(0, selectedIndex - 1)]);
122
- return;
123
- }
124
- if (key.downArrow || chunk === "j") {
125
- void selectRun(runs[Math.min(runs.length - 1, selectedIndex + 1)]);
126
- return;
127
- }
128
- if (key.return || chunk === "f") {
129
- void focusSelectedWorker(repoRoot, defaults.tmuxSessionName, selectedRun);
130
- return;
131
- }
132
- if (chunk === "m" && selectedRun) {
133
- void mergeRun(selectedRun.id, false, { silent: true })
134
- .then((merged) => {
135
- if (merged.merge?.status === "conflict") {
136
- setNotice(`merge conflict ${shortId(selectedRun.id)}`);
137
- }
138
- else if (merged.merge?.status === "merged") {
139
- setNotice(`merged ${shortId(selectedRun.id)}`);
140
- }
141
- else {
142
- setNotice(`merge failed ${shortId(selectedRun.id)}: ${merged.merge?.error ?? "unknown error"}`);
143
- }
144
- return refresh();
145
- })
146
- .catch((error) => setNotice(error instanceof Error ? error.message : String(error)));
147
- return;
148
- }
149
- if (chunk === "u" && selectedRun) {
150
- void syncRun(selectedRun.id, { silent: true })
151
- .then((synced) => {
152
- setNotice(syncNotice(synced));
153
- return refresh();
154
- })
155
- .catch((error) => setNotice(error instanceof Error ? error.message : String(error)));
156
- return;
157
- }
158
- if (chunk === "s" && selectedRun) {
159
- void stopRun(selectedRun.id, { silent: true }).then(refresh);
160
- return;
161
- }
162
- if (chunk === "d" && selectedRun) {
163
- setDeleteIntent({ runId: selectedRun.id });
164
- setNotice("delete? press d to delete run + worktree, Esc cancel");
165
- return;
166
- }
167
- if (chunk === "q") {
168
- void detachClient(defaults.tmuxSessionName);
169
- }
170
- });
171
- const width = Math.max(24, size.columns);
172
- const maxRuns = Math.max(1, Math.floor((size.rows - 5) / 3));
173
- const visibleRuns = runs.slice(0, maxRuns);
174
- 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 u sync m merge dd delete" })] }));
175
- }
176
- function TaskPane({ defaults }) {
177
- const [repoRoot, setRepoRoot] = useState(() => findRepoRoot());
178
- const [config, setConfig] = useState(null);
179
- const [backend, setBackend] = useState(toNativeBackend(defaults.backend ?? "claude"));
180
- const [model, setModel] = useState(defaults.model);
181
- const [effort, setEffort] = useState();
182
- const [input, setInput] = useState("");
183
- const [planMode, setPlanMode] = useState(false);
184
- const inputRef = useRef("");
185
- const taskHistoryRef = useRef([]);
186
- const taskHistoryIndexRef = useRef(null);
187
- const taskHistoryDraftRef = useRef("");
188
- const [notice, setNotice] = useState("");
189
- const [submitting, setSubmitting] = useState(false);
190
- const [commandIndex, setCommandIndex] = useState(0);
191
- const [modelPickerOpen, setModelPickerOpen] = useState(false);
192
- const [modelPickerStep, setModelPickerStep] = useState("model");
193
- const [modelIndex, setModelIndex] = useState(0);
194
- const [effortIndex, setEffortIndex] = useState(0);
195
- const [pendingModel, setPendingModel] = useState(null);
196
- const [claudeModels, setClaudeModels] = useState([]);
197
- const [codexModels, setCodexModels] = useState([]);
198
- const [claudeEfforts, setClaudeEfforts] = useState([]);
199
- const [codexEfforts, setCodexEfforts] = useState([]);
200
- const [taskPaneId, setTaskPaneId] = useState();
201
- const refresh = useCallback(async () => {
202
- const root = findRepoRoot();
203
- const [nextConfig, state] = await Promise.all([
204
- loadConfig(),
205
- loadTmuxDashboardState(root, defaults.tmuxSessionName),
206
- ]);
207
- setRepoRoot(root);
208
- setConfig(nextConfig);
209
- const nextBackend = state?.backend ?? toNativeBackend(defaults.backend ?? nextConfig.lastUsedBackend ?? nextConfig.defaultBackend);
210
- setBackend(nextBackend);
211
- setModel(state?.model ?? defaults.model);
212
- setEffort(state?.effort ?? effortForBackend(nextBackend, nextConfig));
213
- setTaskPaneId(state?.taskPaneId);
214
- }, [defaults.backend, defaults.model, defaults.tmuxSessionName]);
215
- useEffect(() => {
216
- void refresh();
217
- const timer = setInterval(() => void refresh(), 1500);
218
- return () => clearInterval(timer);
219
- }, [refresh]);
220
- const commandOptions = useMemo(() => filterSlashCommands(input), [input]);
221
- const commandMenuOpen = !modelPickerOpen && input.startsWith("/") && !isExactRunnableCommand(input) && commandOptions.length > 0;
222
- const claudeDefault = config?.backends.claude?.model;
223
- const codexDefault = config?.backends.codex?.model;
224
- const modelOptions = useMemo(() => {
225
- const primary = backend === "claude"
226
- ? withBackend(claudeModels.length ? claudeModels : fallbackModelOptions("claude", claudeDefault), "claude")
227
- : withBackend(codexModels.length ? codexModels : fallbackModelOptions("codex", codexDefault), "codex");
228
- const secondary = backend === "claude"
229
- ? withBackend(codexModels.length ? codexModels : fallbackModelOptions("codex", codexDefault), "codex")
230
- : withBackend(claudeModels.length ? claudeModels : fallbackModelOptions("claude", claudeDefault), "claude");
231
- return [...primary, ...secondary];
232
- }, [backend, claudeDefault, claudeModels, codexDefault, codexModels]);
233
- const effortOptionsFor = useCallback((nextBackend) => (nextBackend === "claude"
234
- ? (claudeEfforts.length ? claudeEfforts : fallbackEffortOptions("claude"))
235
- : (codexEfforts.length ? codexEfforts : fallbackEffortOptions("codex"))), [claudeEfforts, codexEfforts]);
236
- const setTaskInput = useCallback((next) => {
237
- const value = typeof next === "function" ? next(inputRef.current) : next;
238
- inputRef.current = value;
239
- setInput(value);
240
- }, []);
241
- const resetTaskHistoryNavigation = useCallback(() => {
242
- taskHistoryIndexRef.current = null;
243
- taskHistoryDraftRef.current = "";
244
- }, []);
245
- const editTaskInput = useCallback((next) => {
246
- resetTaskHistoryNavigation();
247
- setTaskInput(next);
248
- }, [resetTaskHistoryNavigation, setTaskInput]);
249
- const rememberTaskHistory = useCallback((value) => {
250
- const trimmed = value.trim();
251
- if (!trimmed) {
252
- return;
253
- }
254
- const history = taskHistoryRef.current;
255
- history.push(trimmed);
256
- if (history.length > TASK_HISTORY_LIMIT) {
257
- history.splice(0, history.length - TASK_HISTORY_LIMIT);
258
- }
259
- resetTaskHistoryNavigation();
260
- }, [resetTaskHistoryNavigation]);
261
- const showTaskHistory = useCallback((direction) => {
262
- const history = taskHistoryRef.current;
263
- if (!history.length) {
264
- return false;
265
- }
266
- if (direction === "previous") {
267
- const current = taskHistoryIndexRef.current;
268
- if (current === null) {
269
- taskHistoryDraftRef.current = inputRef.current;
270
- taskHistoryIndexRef.current = history.length - 1;
271
- }
272
- else {
273
- taskHistoryIndexRef.current = Math.max(0, Math.min(current, history.length - 1) - 1);
274
- }
275
- setTaskInput(history[taskHistoryIndexRef.current] ?? "");
276
- return true;
277
- }
278
- const current = taskHistoryIndexRef.current;
279
- if (current === null) {
280
- return false;
281
- }
282
- if (current + 1 < history.length) {
283
- taskHistoryIndexRef.current = current + 1;
284
- setTaskInput(history[taskHistoryIndexRef.current] ?? "");
285
- return true;
286
- }
287
- taskHistoryIndexRef.current = null;
288
- const draft = taskHistoryDraftRef.current;
289
- taskHistoryDraftRef.current = "";
290
- setTaskInput(draft);
291
- return true;
292
- }, [setTaskInput]);
293
- useEffect(() => {
294
- setCommandIndex(0);
295
- }, [input]);
296
- useEffect(() => {
297
- let cancelled = false;
298
- void Promise.all([
299
- discoverModelOptions("claude", claudeDefault).catch(() => fallbackModelOptions("claude", claudeDefault)),
300
- discoverModelOptions("codex", codexDefault).catch(() => fallbackModelOptions("codex", codexDefault)),
301
- ]).then(([nextClaudeModels, nextCodexModels]) => {
302
- if (!cancelled) {
303
- setClaudeModels(nextClaudeModels);
304
- setCodexModels(nextCodexModels);
305
- setModelIndex(0);
306
- }
307
- });
308
- return () => {
309
- cancelled = true;
310
- };
311
- }, [claudeDefault, codexDefault]);
312
- useEffect(() => {
313
- let cancelled = false;
314
- void Promise.all([
315
- discoverEffortOptions("claude").catch(() => fallbackEffortOptions("claude")),
316
- discoverEffortOptions("codex").catch(() => fallbackEffortOptions("codex")),
317
- ]).then(([nextClaudeEfforts, nextCodexEfforts]) => {
318
- if (!cancelled) {
319
- setClaudeEfforts(nextClaudeEfforts);
320
- setCodexEfforts(nextCodexEfforts);
321
- }
322
- });
323
- return () => {
324
- cancelled = true;
325
- };
326
- }, []);
327
- useEffect(() => {
328
- if (!taskPaneId) {
329
- return;
330
- }
331
- void resizePane(taskPaneId, modelPickerOpen ? 10 : 3);
332
- }, [modelPickerOpen, taskPaneId]);
333
- const submit = useCallback(async (override) => {
334
- const task = (override ?? inputRef.current).trim();
335
- if (!task || submitting) {
336
- return;
337
- }
338
- const resolvedCommand = resolveSlashCommand(task);
339
- if (resolvedCommand && resolvedCommand.value !== task && !resolvedCommand.complete) {
340
- await submit(resolvedCommand.value);
341
- return;
342
- }
343
- rememberTaskHistory(task);
344
- if (task === "/model") {
345
- setTaskInput("");
346
- setNotice("");
347
- setModelPickerOpen(true);
348
- setModelPickerStep("model");
349
- setModelIndex(0);
350
- setPendingModel(null);
351
- return;
352
- }
353
- if (task === "/plan") {
354
- setPlanMode((current) => {
355
- const next = !current;
356
- setNotice(next ? "Plan mode on: Enter starts a read-only planner" : "Plan mode off");
357
- return next;
358
- });
359
- setTaskInput("");
360
- setModelPickerOpen(false);
361
- return;
362
- }
363
- if (task.startsWith("/plan ")) {
364
- const planTask = task.slice("/plan ".length).trim();
365
- if (!planTask) {
366
- setNotice("Usage: /plan <task>");
367
- return;
368
- }
369
- await startPlanner(planTask);
370
- return;
371
- }
372
- if (task.startsWith("/run ")) {
373
- const runTask = task.slice("/run ".length).trim();
374
- if (!runTask) {
375
- setNotice("Usage: /run <task>");
376
- return;
377
- }
378
- await startWorker(runTask);
379
- return;
380
- }
381
- if (task === "/sync" || task.startsWith("/sync ")) {
382
- await syncSelectedRun(task.slice("/sync".length).trim());
383
- return;
384
- }
385
- if (task.startsWith("/model ")) {
386
- const nextModel = task.slice("/model ".length).trim() || undefined;
387
- setModel(nextModel);
388
- setEffort(undefined);
389
- await updateBackendDefaults(repoRoot, defaults.tmuxSessionName, backend, nextModel, undefined, {
390
- updateModel: true,
391
- updateEffort: true,
392
- });
393
- setTaskInput("");
394
- setNotice("");
395
- return;
396
- }
397
- if (task === "/backend claude" || task === "/backend codex") {
398
- const nextBackend = task.endsWith("codex") ? "codex" : "claude";
399
- setBackend(nextBackend);
400
- setModel(undefined);
401
- const nextEffort = effortForBackend(nextBackend, config);
402
- setEffort(nextEffort);
403
- await updateBackendDefaults(repoRoot, defaults.tmuxSessionName, nextBackend, undefined, nextEffort, {
404
- updateModel: false,
405
- updateEffort: false,
406
- });
407
- setTaskInput("");
408
- setNotice("");
409
- setModelPickerOpen(false);
410
- return;
411
- }
412
- if (task === "/clear") {
413
- setTaskInput("");
414
- setNotice("");
415
- return;
416
- }
417
- if (task === "/help") {
418
- setTaskInput("");
419
- setNotice("/plan toggles read-only planning, /run bypasses it, /sync rebases a worktree, /backend claude|codex, /model");
420
- return;
421
- }
422
- if (task === "/detach") {
423
- await detachClient(defaults.tmuxSessionName);
424
- return;
425
- }
426
- if (task.startsWith("/")) {
427
- setNotice("Unknown command. Type / to see commands.");
428
- return;
429
- }
430
- if (planMode) {
431
- await startPlanner(task);
432
- return;
433
- }
434
- await startWorker(task);
435
- }, [backend, config, defaults.tmuxSessionName, effort, effortOptionsFor, model, modelIndex, modelOptions, modelPickerStep, pendingModel, planMode, rememberTaskHistory, repoRoot, setTaskInput, submitting]);
436
- async function startWorker(task) {
437
- const state = await loadTmuxDashboardState(repoRoot, defaults.tmuxSessionName);
438
- if (!state) {
439
- setNotice("Rudder tmux state is missing. Reopen rudder.");
440
- return;
441
- }
442
- setSubmitting(true);
443
- try {
444
- const run = await startNativeRun({
445
- task,
446
- backend,
447
- model,
448
- effort,
449
- tmuxSessionName: defaults.tmuxSessionName,
450
- workerPaneId: state.workerPaneId,
451
- focus: true,
452
- silent: true,
453
- });
454
- await updateTmuxDashboardState(repoRoot, defaults.tmuxSessionName, { selectedRunId: run.id, backend, model, effort });
455
- setTaskInput("");
456
- setNotice("");
457
- }
458
- catch (error) {
459
- setNotice(error instanceof Error ? error.message : String(error));
460
- }
461
- finally {
462
- setSubmitting(false);
463
- }
464
- }
465
- async function startPlanner(task) {
466
- const state = await loadTmuxDashboardState(repoRoot, defaults.tmuxSessionName);
467
- if (!state) {
468
- setNotice("Rudder tmux state is missing. Reopen rudder.");
469
- return;
470
- }
471
- setSubmitting(true);
472
- try {
473
- const run = await startNativePlan({
474
- task,
475
- backend,
476
- model,
477
- effort,
478
- tmuxSessionName: defaults.tmuxSessionName,
479
- workerPaneId: state.workerPaneId,
480
- focus: true,
481
- silent: true,
482
- });
483
- await updateTmuxDashboardState(repoRoot, defaults.tmuxSessionName, { selectedRunId: run.id, backend, model, effort });
484
- setTaskInput("");
485
- setNotice("Read-only planner started");
486
- }
487
- catch (error) {
488
- setNotice(error instanceof Error ? error.message : String(error));
489
- }
490
- finally {
491
- setSubmitting(false);
492
- }
493
- }
494
- async function syncSelectedRun(runId) {
495
- const state = await loadTmuxDashboardState(repoRoot, defaults.tmuxSessionName);
496
- const targetRunId = runId || state?.selectedRunId;
497
- if (!targetRunId) {
498
- setNotice("Usage: /sync <run>");
499
- return;
500
- }
501
- setSubmitting(true);
502
- try {
503
- const synced = await syncRun(targetRunId, { silent: true });
504
- setTaskInput("");
505
- setNotice(syncNotice(synced));
506
- }
507
- catch (error) {
508
- setNotice(error instanceof Error ? error.message : String(error));
509
- }
510
- finally {
511
- setSubmitting(false);
512
- }
513
- }
514
- useInput((chunk, key) => {
515
- if (key.ctrl && chunk === "c") {
516
- void detachClient(defaults.tmuxSessionName);
517
- return;
518
- }
519
- if (modelPickerOpen) {
520
- if (key.escape) {
521
- if (modelPickerStep === "effort") {
522
- setModelPickerStep("model");
523
- setPendingModel(null);
524
- }
525
- else {
526
- setModelPickerOpen(false);
527
- }
528
- setNotice("");
529
- return;
530
- }
531
- const activeBackend = toNativeBackend(pendingModel?.backend ?? backend);
532
- const effortOptions = effortOptionsFor(activeBackend);
533
- if (key.upArrow || chunk === "k") {
534
- if (modelPickerStep === "effort") {
535
- setEffortIndex((current) => Math.max(0, current - 1));
536
- }
537
- else {
538
- setModelIndex((current) => Math.max(0, current - 1));
539
- }
540
- return;
541
- }
542
- if (key.downArrow || chunk === "j") {
543
- if (modelPickerStep === "effort") {
544
- setEffortIndex((current) => Math.min(effortOptions.length - 1, current + 1));
545
- }
546
- else {
547
- setModelIndex((current) => Math.min(modelOptions.length - 1, current + 1));
548
- }
549
- return;
550
- }
551
- if (key.return) {
552
- if (modelPickerStep === "model") {
553
- const option = modelOptions[modelIndex] ?? { label: "Default", value: undefined, backend };
554
- const nextBackend = toNativeBackend(option.backend ?? backend);
555
- const currentEffort = effortForBackend(nextBackend, config);
556
- const nextEffortOptions = effortOptionsFor(nextBackend);
557
- setPendingModel(option);
558
- setEffortIndex(Math.max(0, nextEffortOptions.findIndex((candidate) => candidate.value === currentEffort)));
559
- setModelPickerStep("effort");
560
- return;
561
- }
562
- const option = pendingModel ?? modelOptions[modelIndex] ?? { label: "Default", value: undefined, backend };
563
- const nextBackend = toNativeBackend(option.backend ?? backend);
564
- const nextModel = option.value;
565
- const nextEffort = effortOptions[effortIndex]?.value;
566
- setBackend(nextBackend);
567
- setModel(nextModel);
568
- setEffort(nextEffort);
569
- void updateBackendDefaults(repoRoot, defaults.tmuxSessionName, nextBackend, nextModel, nextEffort, {
570
- updateModel: true,
571
- updateEffort: true,
572
- });
573
- setModelPickerOpen(false);
574
- setModelPickerStep("model");
575
- setPendingModel(null);
576
- setTaskInput("");
577
- setNotice("");
578
- return;
579
- }
580
- return;
581
- }
582
- const returnIndex = chunk.search(/[\r\n]/);
583
- if (returnIndex >= 0 && !key.ctrl && !key.meta) {
584
- const beforeReturn = stripControlInput(chunk.slice(0, returnIndex));
585
- if (beforeReturn) {
586
- editTaskInput((current) => current + beforeReturn);
587
- }
588
- void submit();
589
- return;
590
- }
591
- if (isLineClear(chunk, key)) {
592
- editTaskInput("");
593
- setNotice("");
594
- return;
595
- }
596
- if (isWordDelete(chunk, key)) {
597
- editTaskInput((current) => deletePreviousWord(current));
598
- return;
599
- }
600
- if (taskHistoryIndexRef.current !== null && key.upArrow) {
601
- showTaskHistory("previous");
602
- return;
603
- }
604
- if (taskHistoryIndexRef.current !== null && key.downArrow) {
605
- showTaskHistory("next");
606
- return;
607
- }
608
- if (commandMenuOpen) {
609
- if (key.upArrow || chunk === "k") {
610
- setCommandIndex((current) => Math.max(0, current - 1));
611
- return;
612
- }
613
- if (key.downArrow || chunk === "j") {
614
- setCommandIndex((current) => Math.min(commandOptions.length - 1, current + 1));
615
- return;
616
- }
617
- if (key.escape) {
618
- editTaskInput("");
619
- setNotice("");
620
- return;
621
- }
622
- }
623
- if (key.return) {
624
- if (commandMenuOpen) {
625
- const command = commandOptions[commandIndex];
626
- if (command?.complete) {
627
- editTaskInput(command.complete);
628
- setNotice("");
629
- return;
630
- }
631
- if (command) {
632
- void submit(command.value);
633
- return;
634
- }
635
- }
636
- void submit();
637
- return;
638
- }
639
- if (key.upArrow) {
640
- showTaskHistory("previous");
641
- return;
642
- }
643
- if (key.downArrow) {
644
- showTaskHistory("next");
645
- return;
646
- }
647
- if (key.backspace || key.delete || chunk === "\u007f" || chunk === "\b") {
648
- editTaskInput((current) => current.slice(0, -1));
649
- return;
650
- }
651
- if (chunk && !key.ctrl && !key.meta) {
652
- editTaskInput((current) => current + chunk);
653
- }
654
- });
655
- const configured = model || (backend === "claude" ? config?.backends.claude?.model : config?.backends.codex?.model) || "default";
656
- const configuredEffort = effort || effortForBackend(backend, config) || "auto";
657
- const entryMode = planMode ? "plan" : "run";
658
- 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"
659
- ? _jsx(EffortMenu, { option: pendingModel ?? modelOptions[modelIndex], selected: effortIndex, backend: toNativeBackend(pendingModel?.backend ?? backend), options: effortOptionsFor(toNativeBackend(pendingModel?.backend ?? backend)) })
660
- : _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] }))] }));
661
- }
662
- function CommandMenu({ commands, selected }) {
663
- const command = commands[Math.max(0, Math.min(selected, commands.length - 1))];
664
- return _jsx(Text, { color: "cyan", children: command ? `> ${command.label} ${command.detail}` : "No command" });
665
- }
666
- function ModelMenu({ options, selected, backend }) {
667
- const start = Math.max(0, Math.min(selected - 2, Math.max(0, options.length - 7)));
668
- const visible = options.slice(start, start + 7);
669
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "gray", children: "Pick a model. Claude and Codex are both listed." }), visible.map((option, index) => {
670
- const absoluteIndex = start + index;
671
- const optionBackend = toNativeBackend(option.backend ?? backend);
672
- return (_jsxs(Text, { color: absoluteIndex === selected ? "cyan" : "gray", children: [absoluteIndex === selected ? "> " : " ", _jsx(Text, { color: optionBackend === "claude" ? "cyan" : "green", children: optionBackend }), " ", option.label, option.detail ? ` ${option.detail}` : ""] }, `${optionBackend}-${option.value ?? "default"}-${absoluteIndex}`));
673
- })] }));
674
- }
675
- function EffortMenu({ option, selected, backend, options }) {
676
- const modelName = option?.label ?? "Default";
677
- return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "gray", children: ["Pick effort for ", backend, " ", modelName, ". Esc goes back."] }), options.map((candidate, index) => (_jsxs(Text, { color: index === selected ? "cyan" : "gray", children: [index === selected ? "> " : " ", _jsx(Text, { color: candidate.value === "xhigh" || candidate.value === "max" ? "yellow" : undefined, children: candidate.label }), candidate.detail ? ` ${candidate.detail}` : ""] }, candidate.value ?? "auto")))] }));
678
- }
679
- function WorkerIdle(_props) {
680
- return _jsx(Box, {});
681
- }
682
- function toNativeBackend(backend) {
683
- return backend === "codex" ? "codex" : "claude";
684
- }
685
- async function loadAgentPaneRuns(repoRoot) {
686
- const runs = await listRuns(repoRoot);
687
- return await Promise.all(runs.map(async (run) => {
688
- const output = await readTailIfExists(outputPath(repoRoot, run.id));
689
- return {
690
- ...run,
691
- attention: permissionAttentionFromOutput(output),
692
- };
693
- }));
694
- }
695
- async function readTailIfExists(file) {
696
- const handle = await fsp.open(file, "r").catch(() => null);
697
- if (!handle) {
698
- return "";
699
- }
700
- try {
701
- const stat = await handle.stat();
702
- const length = Math.min(stat.size, ATTENTION_TAIL_BYTES);
703
- const buffer = Buffer.alloc(length);
704
- await handle.read(buffer, 0, length, Math.max(0, stat.size - length));
705
- return buffer.toString("utf8");
706
- }
707
- catch {
708
- return "";
709
- }
710
- finally {
711
- await handle.close().catch(() => undefined);
712
- }
713
- }
714
- async function focusSelectedWorker(repoRoot, tmuxSessionName, run) {
715
- if (run?.terminal?.paneId) {
716
- await selectPane(run.terminal.paneId).catch(() => undefined);
717
- return;
718
- }
719
- const state = await loadTmuxDashboardState(repoRoot, tmuxSessionName);
720
- if (state?.workerPaneId) {
721
- await selectPane(state.workerPaneId).catch(() => undefined);
722
- }
723
- }
724
- function notifyRunAlerts(runs, ref) {
725
- const terminal = new Set(runs.filter((run) => isTerminalRun(run)).map((run) => run.id));
726
- const permission = new Set(runs.filter((run) => runNeedsPermission(run)).map((run) => run.id));
727
- if (!ref.current) {
728
- ref.current = { terminal, permission };
729
- return;
730
- }
731
- for (const run of runs) {
732
- if (isTerminalRun(run) && !ref.current.terminal.has(run.id)) {
733
- playCompletionSound();
734
- ref.current.terminal.add(run.id);
735
- }
736
- if (runNeedsPermission(run) && !ref.current.permission.has(run.id)) {
737
- playCompletionSound();
738
- ref.current.permission.add(run.id);
739
- }
740
- }
741
- ref.current.terminal = terminal;
742
- ref.current.permission = permission;
743
- }
744
- function isTerminalRun(run) {
745
- return ["completed", "failed", "cancelled", "merged", "merge-conflict"].includes(run.status);
746
- }
747
- function isActiveRun(run) {
748
- return ["created", "running", "steering", "verifying"].includes(run.status);
749
- }
750
- function runNeedsPermission(run) {
751
- return isActiveRun(run) && run.attention.needsPermission;
752
- }
753
- function playCompletionSound() {
754
- try {
755
- const player = process.platform === "darwin" ? "afplay" : "ffplay";
756
- const args = process.platform === "darwin"
757
- ? [COMPLETION_SOUND]
758
- : ["-nodisp", "-autoexit", "-loglevel", "quiet", COMPLETION_SOUND];
759
- const child = spawn(player, args, { detached: true, stdio: "ignore" });
760
- child.on("error", () => process.stdout.write("\u0007"));
761
- child.unref();
762
- }
763
- catch {
764
- process.stdout.write("\u0007");
765
- }
766
- }
767
- function statusMark(run) {
768
- if (runNeedsPermission(run))
769
- return "needs permission";
770
- if (run.mode === "plan" && isActiveRun(run))
771
- return "planning";
772
- if (run.status === "merged")
773
- return "merged";
774
- if (run.status === "completed")
775
- return "done";
776
- if (run.status === "failed" || run.status === "merge-conflict")
777
- return "failed";
778
- if (run.status === "cancelled")
779
- return "stopped";
780
- if (run.status === "running" || run.status === "steering" || run.status === "verifying")
781
- return "running";
782
- return "queued";
783
- }
784
- function runStatusColor(run) {
785
- return runNeedsPermission(run) ? "yellow" : statusColor(run);
786
- }
787
- function statusColor(run) {
788
- if (run.status === "merged" || run.status === "completed")
789
- return "green";
790
- if (run.status === "failed" || run.status === "merge-conflict")
791
- return "red";
792
- if (run.status === "cancelled")
793
- return "yellow";
794
- if (run.status === "running" || run.status === "steering" || run.status === "verifying")
795
- return "yellow";
796
- return "gray";
797
- }
798
- function taskColor(run) {
799
- return undefined;
800
- }
801
- function modelLabel(run, config) {
802
- const model = run.model
803
- ?? (run.backend === "claude"
804
- ? config?.backends.claude?.model
805
- : config?.backends.codex?.model)
806
- ?? "default";
807
- const effort = run.effort ?? effortForBackend(toNativeBackend(run.backend), config);
808
- const mode = run.mode === "plan" ? "plan " : "";
809
- return summarize(`${mode}${model} ${effort ?? "auto"}`, 18);
810
- }
811
- function modelForBackend(backend, config) {
812
- return backend === "claude" ? config?.backends.claude?.model : config?.backends.codex?.model;
813
- }
814
- function effortForBackend(backend, config) {
815
- if (backend === "claude") {
816
- return config?.backends.claude?.effort;
817
- }
818
- return config?.backends.codex?.reasoningEffort ?? config?.backends.codex?.effort;
819
- }
820
- async function updateBackendDefaults(repoRoot, tmuxSessionName, backend, model, effort, options) {
821
- await updateTmuxDashboardState(repoRoot, tmuxSessionName, { backend, model, effort });
822
- await rememberBackendSelection({
823
- backend,
824
- model,
825
- effort,
826
- updateModel: options.updateModel,
827
- updateEffort: options.updateEffort,
828
- });
829
- }
830
- function withBackend(options, backend) {
831
- return options.map((option) => ({ ...option, backend }));
832
- }
833
- function filterSlashCommands(input) {
834
- if (!input.startsWith("/")) {
835
- return [];
836
- }
837
- const query = input.toLowerCase();
838
- const matches = SLASH_COMMANDS.filter((command) => command.label.toLowerCase().startsWith(query));
839
- return matches.length ? matches : SLASH_COMMANDS.filter((command) => command.label.toLowerCase().includes(query.slice(1)));
840
- }
841
- function isExactRunnableCommand(input) {
842
- const trimmed = input.trim();
843
- return SLASH_COMMANDS.some((command) => !command.complete && command.value === trimmed);
844
- }
845
- function resolveSlashCommand(input) {
846
- if (!input.startsWith("/") || input.startsWith("/model ") || input.startsWith("/plan ") || input.startsWith("/run ") || input.startsWith("/sync ")) {
847
- return undefined;
848
- }
849
- if (isExactRunnableCommand(input)) {
850
- return undefined;
851
- }
852
- return filterSlashCommands(input)[0];
853
- }
854
- function isLineClear(chunk, key) {
855
- return (key.ctrl && chunk === "u") || chunk === "\u0015" || chunk === "\u001b\u0015";
856
- }
857
- function isWordDelete(chunk, key) {
858
- return Boolean((key.ctrl && chunk === "w") ||
859
- chunk === "\u0017" ||
860
- chunk === "\u001b\u007f" ||
861
- chunk === "\u001b\b" ||
862
- (key.meta && (key.backspace || key.delete || chunk === "\u007f" || chunk === "\b")));
863
- }
864
- function deletePreviousWord(value) {
865
- return value.trimEnd().replace(/\s*\S+$/, "");
866
- }
867
- function stripControlInput(value) {
868
- return value.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, "");
869
- }
870
- function shortId(id) {
871
- return id.slice(0, 14);
872
- }
873
- function syncNotice(run) {
874
- if (run.sync?.status === "synced") {
875
- return `synced ${shortId(run.id)}`;
876
- }
877
- if (run.sync?.status === "conflict") {
878
- const count = run.sync.conflictedFiles?.length || "unknown";
879
- return `sync conflict ${shortId(run.id)} (${count} file${count === 1 ? "" : "s"}); resolve in worktree and run git rebase --continue`;
880
- }
881
- return `sync failed ${shortId(run.id)}`;
882
- }
883
- function summarize(value, width) {
884
- const clean = value.replace(/\s+/g, " ").trim();
885
- if (clean.length <= width) {
886
- return clean;
887
- }
888
- return `${clean.slice(0, Math.max(0, width - 1))}…`;
889
- }
890
- //# sourceMappingURL=tmux-dashboard.js.map