@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.
package/dist/tui.js DELETED
@@ -1,1443 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import fsp from "node:fs/promises";
3
- import { spawn } from "node:child_process";
4
- import { fileURLToPath } from "node:url";
5
- import { useCallback, useEffect, useMemo, useRef, useState } from "react";
6
- import { Box, Text, render, useApp, useInput, useWindowSize } from "ink";
7
- import { permissionAttentionFromOutput } from "./agent-attention.js";
8
- import { currentBranch, findRepoRoot } from "./git.js";
9
- import { discoverModelOptions, fallbackModelOptions } from "./models.js";
10
- import { eventsPath, listRuns, loadConfig, outputPath, rememberBackendSelection, } from "./state.js";
11
- import { continueRun, deleteRun, mergeRun, startRun, stopRun, syncRun } from "./run-manager.js";
12
- import { taskDisplayLabel } from "./task-summary.js";
13
- import { pathExists, shortenHome } from "./util.js";
14
- const INTERACTIVE_BACKENDS = ["claude", "codex"];
15
- const COMPLETION_SOUND = fileURLToPath(new URL("../assets/sounds/ping.mp3", import.meta.url));
16
- const COMMANDS = [
17
- { name: "backend", detail: "switch backend: claude or codex", insert: "/backend " },
18
- { name: "model", detail: "open model picker or set a custom model id", insert: "/model" },
19
- { name: "agent", detail: "send your next input to the selected agent", insert: "/agent" },
20
- { name: "interrupt", detail: "interrupt and redirect the selected running agent", insert: "/interrupt" },
21
- { name: "new", detail: "return to new-agent mode", insert: "/new" },
22
- { name: "worktree", detail: "toggle worktree policy", insert: "/worktree " },
23
- { name: "stop", detail: "stop the selected run", insert: "/stop" },
24
- { name: "delete", detail: "delete selected run and its worktree", insert: "/delete" },
25
- { name: "copy", detail: "copy selected worker transcript", insert: "/copy" },
26
- { name: "sync", detail: "rebase the selected worktree without merging", insert: "/sync" },
27
- { name: "merge", detail: "merge the selected completed worktree", insert: "/merge" },
28
- { name: "merge-all", detail: "merge all completed worktrees", insert: "/merge-all" },
29
- { name: "clear", detail: "collapse all run cards", insert: "/clear" },
30
- { name: "help", detail: "show key and slash command help", insert: "/help" },
31
- { name: "exit", detail: "quit Rudder", insert: "/exit" },
32
- ];
33
- export async function runInteractiveTui(defaults) {
34
- const instance = render(_jsx(RudderTui, { defaults: defaults ?? {} }), {
35
- alternateScreen: true,
36
- exitOnCtrlC: false,
37
- maxFps: 60,
38
- });
39
- await instance.waitUntilExit();
40
- }
41
- function RudderTui({ defaults }) {
42
- const app = useApp();
43
- const size = useWindowSize();
44
- const [repoRoot, setRepoRoot] = useState(() => findRepoRoot());
45
- const [branch, setBranch] = useState("HEAD");
46
- const [config, setConfig] = useState(null);
47
- const [backend, setBackend] = useState(toInteractiveBackend(defaults.backend ?? "claude"));
48
- const [model, setModel] = useState(defaults.model);
49
- const [worktreeMode, setWorktreeMode] = useState(defaults.worktree === false ? "auto" : "always");
50
- const [runs, setRuns] = useState([]);
51
- const [selectedRunId, setSelectedRunId] = useState();
52
- const [targetRunId, setTargetRunId] = useState();
53
- const [focusPane, setFocusPane] = useState("task");
54
- const [expandedRunIds, setExpandedRunIds] = useState(new Set());
55
- const [transcriptExpanded, setTranscriptExpanded] = useState(false);
56
- const [input, setInput] = useState("");
57
- const [notice, setNotice] = useState("Ready");
58
- const [helpOpen, setHelpOpen] = useState(false);
59
- const [modelMenuOpen, setModelMenuOpen] = useState(false);
60
- const [modelMenuIndex, setModelMenuIndex] = useState(0);
61
- const [commandMenuIndex, setCommandMenuIndex] = useState(0);
62
- const [discoveredModels, setDiscoveredModels] = useState([]);
63
- const [deletePrompt, setDeletePrompt] = useState(null);
64
- const [mergePrompt, setMergePrompt] = useState(null);
65
- const [conflictPrompt, setConflictPrompt] = useState(null);
66
- const [submitting, setSubmitting] = useState(false);
67
- const [preferencesLoaded, setPreferencesLoaded] = useState(false);
68
- const notifiedAlerts = useRef(null);
69
- const refresh = useCallback(async () => {
70
- const root = findRepoRoot();
71
- const [nextConfig, nextBranch, nextRuns] = await Promise.all([
72
- loadConfig(),
73
- currentBranch(root),
74
- loadUiRuns(root),
75
- ]);
76
- setRepoRoot(root);
77
- setConfig(nextConfig);
78
- setBranch(nextBranch);
79
- notifyRunAlerts(nextRuns, notifiedAlerts);
80
- setRuns(nextRuns);
81
- if (!preferencesLoaded) {
82
- setBackend(toInteractiveBackend(defaults.backend ?? nextConfig.lastUsedBackend ?? nextConfig.defaultBackend));
83
- setModel(defaults.model);
84
- setPreferencesLoaded(true);
85
- }
86
- setSelectedRunId((current) => current ?? nextRuns[0]?.id);
87
- }, [defaults.backend, defaults.model, preferencesLoaded]);
88
- // Read the latest refresh and the typing state through refs so the poll
89
- // timer is created once instead of being torn down on every keystroke (which
90
- // could starve the periodic refresh while the user types steadily).
91
- const refreshRef = useRef(refresh);
92
- useEffect(() => {
93
- refreshRef.current = refresh;
94
- }, [refresh]);
95
- const inputActiveRef = useRef(input.length > 0);
96
- useEffect(() => {
97
- inputActiveRef.current = input.length > 0;
98
- }, [input]);
99
- useEffect(() => {
100
- let cancelled = false;
101
- let timer;
102
- const schedule = () => {
103
- timer = setTimeout(async () => {
104
- if (cancelled) {
105
- return;
106
- }
107
- await refreshRef.current();
108
- if (!cancelled) {
109
- schedule();
110
- }
111
- }, inputActiveRef.current ? 1500 : 900);
112
- };
113
- void refreshRef.current();
114
- schedule();
115
- return () => {
116
- cancelled = true;
117
- clearTimeout(timer);
118
- };
119
- }, []);
120
- // Depend on the derived configured default (a string), not the whole config
121
- // object, which is reallocated on every refresh tick. Otherwise discovery
122
- // re-fires constantly and resets the menu index out from under the user.
123
- const configuredDefault = modelForBackend(backend, config);
124
- useEffect(() => {
125
- let cancelled = false;
126
- void discoverModelOptions(backend, configuredDefault)
127
- .then((options) => {
128
- if (!cancelled) {
129
- setDiscoveredModels(options);
130
- setModelMenuIndex(0);
131
- }
132
- })
133
- .catch(() => {
134
- if (!cancelled) {
135
- setDiscoveredModels(fallbackModelOptions(backend, configuredDefault));
136
- setModelMenuIndex(0);
137
- }
138
- });
139
- return () => {
140
- cancelled = true;
141
- };
142
- }, [backend, configuredDefault]);
143
- const selectedIndex = Math.max(0, runs.findIndex((run) => run.id === selectedRunId));
144
- const selectedRun = runs[selectedIndex];
145
- const targetRun = focusPane === "worker" && selectedRun ? selectedRun : targetRunId ? runs.find((run) => run.id === targetRunId) : undefined;
146
- const activeCount = runs.filter((run) => isActive(run.status)).length;
147
- const selectedExpanded = Boolean(selectedRun && expandedRunIds.has(selectedRun.id));
148
- const modelOptions = useMemo(() => discoveredModels.length ? discoveredModels : fallbackModelOptions(backend, modelForBackend(backend, config)), [backend, config, discoveredModels]);
149
- // Keep the highlighted option in range when the list shrinks asynchronously
150
- // (e.g. discovery falls back to a shorter list while the menu is open).
151
- useEffect(() => {
152
- setModelMenuIndex((index) => Math.min(index, Math.max(0, modelOptions.length - 1)));
153
- }, [modelOptions.length]);
154
- const commandOptions = useMemo(() => filterCommands(input), [input]);
155
- const commandMenuOpen = input.startsWith("/") && !modelMenuOpen && commandOptions.length > 0;
156
- const submitTask = useCallback(async (task) => {
157
- const trimmed = task.trim();
158
- if (!trimmed || submitting) {
159
- return;
160
- }
161
- setSubmitting(true);
162
- if (targetRun) {
163
- const interrupt = isActive(targetRun.status);
164
- setNotice(`${interrupt ? "Interrupting" : "Sending to"} ${shortId(targetRun.id)}...`);
165
- try {
166
- const run = await continueRun({
167
- runId: targetRun.id,
168
- prompt: trimmed,
169
- interrupt,
170
- silent: true,
171
- });
172
- setInput("");
173
- setSelectedRunId(run.id);
174
- setExpandedRunIds((current) => new Set(current).add(run.id));
175
- setNotice(`${interrupt ? "Interrupted" : "Sent to"} ${shortId(run.id)}`);
176
- await refresh();
177
- }
178
- catch (error) {
179
- setNotice(error instanceof Error ? error.message : String(error));
180
- }
181
- finally {
182
- setSubmitting(false);
183
- }
184
- return;
185
- }
186
- setNotice(`Starting ${backend}...`);
187
- try {
188
- const run = await startRun({
189
- task: trimmed,
190
- backend,
191
- model,
192
- detach: true,
193
- worktree: worktreeMode === "always",
194
- silent: true,
195
- view: "shell",
196
- });
197
- setInput("");
198
- setSelectedRunId(run.id);
199
- setExpandedRunIds((current) => new Set(current).add(run.id));
200
- setNotice(`Started ${run.id}`);
201
- await refresh();
202
- }
203
- catch (error) {
204
- setNotice(error instanceof Error ? error.message : String(error));
205
- }
206
- finally {
207
- setSubmitting(false);
208
- }
209
- }, [backend, model, refresh, submitting, targetRun, worktreeMode]);
210
- const requestDeleteSelectedRun = useCallback((runOverride) => {
211
- const run = runOverride ?? selectedRun;
212
- if (!run) {
213
- setNotice("No agent selected");
214
- return;
215
- }
216
- setDeletePrompt({ runId: run.id });
217
- setNotice(`Delete ${shortId(run.id)}? press d to confirm, Esc to cancel`);
218
- }, [selectedRun]);
219
- const confirmDelete = useCallback(async () => {
220
- if (!deletePrompt) {
221
- return;
222
- }
223
- const runId = deletePrompt.runId;
224
- try {
225
- await deleteRun(runId, { force: true, silent: true });
226
- setDeletePrompt(null);
227
- setSelectedRunId(undefined);
228
- setTargetRunId(undefined);
229
- setNotice(`Deleted ${shortId(runId)}`);
230
- await refresh();
231
- }
232
- catch (error) {
233
- setDeletePrompt(null);
234
- setNotice(error instanceof Error ? error.message : String(error));
235
- }
236
- }, [deletePrompt, refresh]);
237
- const requestMergeRun = useCallback((runOverride, allowDirty = false) => {
238
- const run = runOverride ?? selectedRun;
239
- if (!run) {
240
- setNotice("No agent selected");
241
- return;
242
- }
243
- if (!canMerge(run)) {
244
- setNotice(`${shortId(run.id)} is not ready to merge`);
245
- return;
246
- }
247
- setDeletePrompt(null);
248
- setConflictPrompt(null);
249
- setMergePrompt({
250
- kind: "selected",
251
- runId: run.id,
252
- label: truncate(taskDisplayLabel(run, 48), 48),
253
- allowDirty,
254
- });
255
- setNotice(`Merge ${shortId(run.id)}? press y to confirm or n to cancel`);
256
- }, [selectedRun]);
257
- const requestMergeAll = useCallback((allowDirty = false) => {
258
- const ready = runs.filter(canMerge);
259
- if (ready.length === 0) {
260
- setNotice("No completed worktree runs ready to merge");
261
- return;
262
- }
263
- setDeletePrompt(null);
264
- setConflictPrompt(null);
265
- setMergePrompt({ kind: "all", runIds: ready.map((run) => run.id), allowDirty });
266
- setNotice(`Merge ${ready.length} run${ready.length === 1 ? "" : "s"}? press y to confirm or n to cancel`);
267
- }, [runs]);
268
- const confirmMerge = useCallback(async () => {
269
- if (!mergePrompt) {
270
- return;
271
- }
272
- const prompt = mergePrompt;
273
- setMergePrompt(null);
274
- try {
275
- if (prompt.kind === "selected") {
276
- const merged = await mergeRun(prompt.runId, prompt.allowDirty, { silent: true });
277
- if (merged.merge?.status === "conflict") {
278
- const files = merged.merge.conflictedFiles ?? [];
279
- setConflictPrompt({ runId: prompt.runId, files });
280
- setNotice(`Merge conflict in ${files.length || "unknown"} file${files.length === 1 ? "" : "s"}; press y for AI help or n for manual`);
281
- }
282
- else if (merged.merge?.status === "merged") {
283
- setNotice(`Merged ${shortId(prompt.runId)}`);
284
- }
285
- else {
286
- setNotice(`Merge failed for ${shortId(prompt.runId)}: ${merged.merge?.error ?? "unknown error"}`);
287
- }
288
- await refresh();
289
- return;
290
- }
291
- let mergedCount = 0;
292
- for (const runId of prompt.runIds) {
293
- const merged = await mergeRun(runId, prompt.allowDirty, { silent: true });
294
- if (merged.merge?.status === "conflict") {
295
- const files = merged.merge.conflictedFiles ?? [];
296
- setConflictPrompt({ runId, files });
297
- setNotice(`Merge all stopped after ${mergedCount}: conflict in ${shortId(runId)}; press y for AI help or n for manual`);
298
- await refresh();
299
- return;
300
- }
301
- if (merged.merge?.status !== "merged") {
302
- setNotice(`Merge all stopped after ${mergedCount}: failed in ${shortId(runId)}: ${merged.merge?.error ?? "unknown error"}`);
303
- await refresh();
304
- return;
305
- }
306
- mergedCount += 1;
307
- }
308
- setNotice(`Merged ${mergedCount} run${mergedCount === 1 ? "" : "s"}`);
309
- await refresh();
310
- }
311
- catch (error) {
312
- setNotice(error instanceof Error ? error.message : String(error));
313
- await refresh();
314
- }
315
- }, [mergePrompt, refresh]);
316
- const startConflictResolver = useCallback(async () => {
317
- if (!conflictPrompt) {
318
- return;
319
- }
320
- const files = conflictPrompt.files.length ? conflictPrompt.files.join("\n") : "(git did not report conflicted files)";
321
- const task = [
322
- "Read RUDDER.md first. A git merge stopped with conflicts in this checkout.",
323
- `Conflicted files:\n${files}`,
324
- "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.",
325
- ].join("\n\n");
326
- try {
327
- const run = await startRun({
328
- task,
329
- backend,
330
- model,
331
- detach: true,
332
- worktree: false,
333
- silent: true,
334
- view: "shell",
335
- });
336
- setConflictPrompt(null);
337
- setSelectedRunId(run.id);
338
- setExpandedRunIds((current) => new Set(current).add(run.id));
339
- setNotice(`Started AI merge-conflict resolver ${shortId(run.id)}`);
340
- await refresh();
341
- }
342
- catch (error) {
343
- setNotice(error instanceof Error ? error.message : String(error));
344
- }
345
- }, [backend, conflictPrompt, model, refresh]);
346
- const copySelectedTranscript = useCallback(async (runOverride) => {
347
- const run = runOverride ?? selectedRun;
348
- if (!run) {
349
- setNotice("No agent selected");
350
- return;
351
- }
352
- try {
353
- await copyToClipboard(run.output);
354
- setNotice(`Copied transcript ${shortId(run.id)}`);
355
- }
356
- catch (error) {
357
- setNotice(error instanceof Error ? error.message : String(error));
358
- }
359
- }, [selectedRun]);
360
- const syncSelectedRun = useCallback(async (runOverride) => {
361
- const run = runOverride ?? selectedRun;
362
- if (!run) {
363
- setNotice("No agent selected");
364
- return;
365
- }
366
- try {
367
- const synced = await syncRun(run.id, { silent: true });
368
- setNotice(syncNotice(synced));
369
- await refresh();
370
- }
371
- catch (error) {
372
- setNotice(error instanceof Error ? error.message : String(error));
373
- await refresh();
374
- }
375
- }, [refresh, selectedRun]);
376
- const handleCommand = useCallback(async (line) => {
377
- const [command = "", ...args] = line.slice(1).trim().split(/\s+/).filter(Boolean);
378
- switch (command) {
379
- case "":
380
- case "help":
381
- case "?":
382
- setHelpOpen((value) => !value);
383
- setInput("");
384
- return;
385
- case "q":
386
- case "quit":
387
- case "exit":
388
- app.exit();
389
- return;
390
- case "backend":
391
- if (isInteractiveBackend(args[0])) {
392
- const nextBackend = args[0];
393
- chooseBackend(nextBackend, setBackend, setModel, setNotice);
394
- setConfig(await rememberBackendSelection({ backend: nextBackend }));
395
- }
396
- else {
397
- setNotice("Usage: /backend claude|codex");
398
- }
399
- setInput("");
400
- return;
401
- case "agent":
402
- case "continue":
403
- case "interrupt": {
404
- const run = resolveUiRun(runs, args[0] ?? selectedRun?.id);
405
- if (run) {
406
- setTargetRunId(run.id);
407
- setSelectedRunId(run.id);
408
- setFocusPane("worker");
409
- setNotice(`${isActive(run.status) ? "Esc/Enter will interrupt" : "Typing to"} ${shortId(run.id)}`);
410
- }
411
- else {
412
- setNotice("No agent selected");
413
- }
414
- setInput("");
415
- return;
416
- }
417
- case "new":
418
- setTargetRunId(undefined);
419
- setFocusPane("task");
420
- setNotice("Typing starts a new agent");
421
- setInput("");
422
- return;
423
- case "model":
424
- if (args.length === 0) {
425
- setModelMenuOpen(true);
426
- setModelMenuIndex(0);
427
- setNotice(`Pick a ${backend} model`);
428
- }
429
- else {
430
- const nextModel = args.join(" ");
431
- setModel(nextModel);
432
- setConfig(await rememberBackendSelection({
433
- backend,
434
- model: nextModel,
435
- updateModel: true,
436
- }));
437
- setNotice(`Model ${nextModel}`);
438
- }
439
- setInput("");
440
- return;
441
- case "worktree":
442
- setWorktreeMode(args[0] === "always" || args[0] === "on" ? "always" : "auto");
443
- setNotice(`Worktrees ${args[0] === "always" || args[0] === "on" ? "always" : "auto"}`);
444
- setInput("");
445
- return;
446
- case "stop":
447
- await runAction(args[0] ?? selectedRun?.id, async (id) => stopRun(id, { silent: true }), "Stopped", setNotice, refresh);
448
- setInput("");
449
- return;
450
- case "delete":
451
- await requestDeleteSelectedRun(resolveUiRun(runs, args[0] ?? selectedRun?.id));
452
- setInput("");
453
- return;
454
- case "copy":
455
- await copySelectedTranscript(resolveUiRun(runs, args[0] ?? selectedRun?.id));
456
- setInput("");
457
- return;
458
- case "sync":
459
- await syncSelectedRun(resolveUiRun(runs, args[0] ?? selectedRun?.id));
460
- setInput("");
461
- return;
462
- case "merge":
463
- requestMergeRun(resolveUiRun(runs, args[0] ?? selectedRun?.id), args.includes("--allow-dirty"));
464
- setInput("");
465
- return;
466
- case "merge-all":
467
- requestMergeAll(args.includes("--allow-dirty"));
468
- setInput("");
469
- return;
470
- case "clear":
471
- setExpandedRunIds(new Set());
472
- setNotice("Collapsed all runs");
473
- setInput("");
474
- return;
475
- default:
476
- setNotice(`Unknown command: /${command}`);
477
- setInput("");
478
- }
479
- }, [app, backend, copySelectedTranscript, refresh, requestDeleteSelectedRun, requestMergeAll, requestMergeRun, runs, selectedRun?.id, syncSelectedRun]);
480
- const selectModelOption = useCallback((index) => {
481
- const option = modelOptions[index];
482
- if (!option) {
483
- return;
484
- }
485
- setModel(option.value);
486
- setModelMenuOpen(false);
487
- setModelMenuIndex(index);
488
- setNotice(option.value ? `Model ${option.value}` : "Using backend default model");
489
- void rememberBackendSelection({
490
- backend,
491
- model: option.value,
492
- updateModel: true,
493
- }).then(setConfig).catch((error) => {
494
- setNotice(error instanceof Error ? error.message : String(error));
495
- });
496
- }, [backend, modelOptions]);
497
- const selectCommandOption = useCallback((index) => {
498
- const option = commandOptions[index];
499
- if (!option) {
500
- return;
501
- }
502
- setInput(option.insert.endsWith(" ") ? option.insert : option.insert);
503
- setCommandMenuIndex(index);
504
- if (!option.insert.endsWith(" ") && option.insert !== "/model") {
505
- setNotice(`Press Enter to run ${option.insert}`);
506
- }
507
- if (option.insert === "/model") {
508
- setInput("");
509
- setModelMenuOpen(true);
510
- setModelMenuIndex(0);
511
- setNotice(`Pick a ${backend} model`);
512
- }
513
- }, [backend, commandOptions]);
514
- useInput((value, key) => {
515
- if (key.ctrl && value === "c") {
516
- app.exit();
517
- return;
518
- }
519
- if ((key.meta && value === "1") || value === "\u001b1") {
520
- setFocusPane("agents");
521
- setModelMenuOpen(false);
522
- setInput("");
523
- setNotice("Agents focus: j/k or arrows select runs");
524
- return;
525
- }
526
- if ((key.meta && value === "2") || value === "\u001b2") {
527
- setFocusPane("worker");
528
- setModelMenuOpen(false);
529
- setInput("");
530
- if (selectedRun) {
531
- setNotice(`${isActive(selectedRun.status) ? "Worker focus: type redirect, Enter interrupts" : "Worker focus: type follow-up"} ${shortId(selectedRun.id)}`);
532
- }
533
- else {
534
- setNotice("No agent selected");
535
- }
536
- return;
537
- }
538
- if ((key.meta && value === "3") || value === "\u001b3") {
539
- setFocusPane("task");
540
- setModelMenuOpen(false);
541
- setInput("");
542
- setTargetRunId(undefined);
543
- setNotice("Task focus: type a new task");
544
- return;
545
- }
546
- if (key.tab || value === "\t") {
547
- return;
548
- }
549
- if (mergePrompt) {
550
- if (key.escape || value === "n" || value === "N") {
551
- setMergePrompt(null);
552
- setNotice("Merge cancelled");
553
- return;
554
- }
555
- if (value === "y" || value === "Y") {
556
- void confirmMerge();
557
- return;
558
- }
559
- return;
560
- }
561
- if (conflictPrompt) {
562
- if (key.escape || value === "n" || value === "N") {
563
- setConflictPrompt(null);
564
- setNotice("Resolve the merge conflicts manually, then commit");
565
- return;
566
- }
567
- if (value === "y" || value === "Y") {
568
- void startConflictResolver();
569
- return;
570
- }
571
- return;
572
- }
573
- if (deletePrompt) {
574
- if (key.escape) {
575
- setDeletePrompt(null);
576
- setNotice("Delete cancelled");
577
- return;
578
- }
579
- if (value === "d") {
580
- void confirmDelete();
581
- return;
582
- }
583
- return;
584
- }
585
- if (modelMenuOpen) {
586
- if (key.escape) {
587
- setModelMenuOpen(false);
588
- setNotice("Model unchanged");
589
- return;
590
- }
591
- if (key.upArrow || value === "k") {
592
- setModelMenuIndex((current) => Math.max(0, current - 1));
593
- return;
594
- }
595
- if (key.downArrow || value === "j") {
596
- setModelMenuIndex((current) => Math.min(modelOptions.length - 1, current + 1));
597
- return;
598
- }
599
- if (key.return) {
600
- selectModelOption(modelMenuIndex);
601
- return;
602
- }
603
- const numeric = Number(value);
604
- if (Number.isInteger(numeric) && numeric >= 1 && numeric <= modelOptions.length) {
605
- selectModelOption(numeric - 1);
606
- return;
607
- }
608
- return;
609
- }
610
- if (commandMenuOpen && input.startsWith("/")) {
611
- if (key.upArrow || value === "\u001b[A") {
612
- setCommandMenuIndex((current) => Math.max(0, current - 1));
613
- return;
614
- }
615
- if (key.downArrow || value === "\u001b[B") {
616
- setCommandMenuIndex((current) => Math.min(commandOptions.length - 1, current + 1));
617
- return;
618
- }
619
- }
620
- if (key.escape) {
621
- if (input) {
622
- setInput("");
623
- }
624
- else if (helpOpen) {
625
- setHelpOpen(false);
626
- }
627
- else if (transcriptExpanded) {
628
- setTranscriptExpanded(false);
629
- }
630
- else if (focusPane === "worker") {
631
- setTargetRunId(undefined);
632
- setFocusPane("task");
633
- setNotice("Task focus: type a new task");
634
- }
635
- else if (selectedRun) {
636
- setFocusPane("worker");
637
- setNotice(`${isActive(selectedRun.status) ? "Type redirect, Enter interrupts" : "Typing to"} ${shortId(selectedRun.id)}`);
638
- }
639
- return;
640
- }
641
- if (key.upArrow || (focusPane === "agents" && input.length === 0 && value === "k")) {
642
- selectRelative(runs, selectedRunId, -1, setSelectedRunId);
643
- if (focusPane === "task") {
644
- setFocusPane("agents");
645
- }
646
- return;
647
- }
648
- if (key.downArrow || (focusPane === "agents" && input.length === 0 && value === "j")) {
649
- selectRelative(runs, selectedRunId, 1, setSelectedRunId);
650
- if (focusPane === "task") {
651
- setFocusPane("agents");
652
- }
653
- return;
654
- }
655
- if (key.pageUp) {
656
- selectRelative(runs, selectedRunId, -5, setSelectedRunId);
657
- return;
658
- }
659
- if (key.pageDown) {
660
- selectRelative(runs, selectedRunId, 5, setSelectedRunId);
661
- return;
662
- }
663
- if (key.return) {
664
- if (commandMenuOpen && shouldSelectCommand(input, commandOptions)) {
665
- selectCommandOption(commandMenuIndex);
666
- }
667
- else if (input.trim().startsWith("/")) {
668
- void handleCommand(input);
669
- }
670
- else {
671
- void submitTask(input);
672
- }
673
- return;
674
- }
675
- if ((key.meta && (key.backspace || key.delete)) || value === "\u0015" || (key.ctrl && value === "u")) {
676
- setInput("");
677
- return;
678
- }
679
- if (value === "\u001b\u007f" || value === "\u001b\b") {
680
- setInput((current) => deletePreviousWord(current));
681
- return;
682
- }
683
- if (key.ctrl && value === "w") {
684
- setInput((current) => deletePreviousWord(current));
685
- return;
686
- }
687
- if (key.backspace || key.delete || value === "\u007f" || value === "\b") {
688
- setInput((current) => current.slice(0, -1));
689
- return;
690
- }
691
- if (focusPane !== "agents" && !input.startsWith("/")) {
692
- const text = normalizeInputText(value);
693
- if (text) {
694
- setInput((current) => current + text);
695
- setCommandMenuIndex(0);
696
- }
697
- return;
698
- }
699
- if (input.length === 0 && value === "q") {
700
- app.exit();
701
- return;
702
- }
703
- if (input.length === 0 && value === "?") {
704
- setHelpOpen((current) => !current);
705
- return;
706
- }
707
- if (input.length === 0 && value === "r") {
708
- void refresh();
709
- setNotice("Refreshed");
710
- return;
711
- }
712
- if (input.length === 0 && value === "o") {
713
- setModelMenuOpen(true);
714
- setModelMenuIndex(0);
715
- setNotice(`Pick a ${backend} model`);
716
- return;
717
- }
718
- if (input.length === 0 && value === "c" && selectedRun) {
719
- setFocusPane("worker");
720
- setNotice(`${isActive(selectedRun.status) ? "Type redirect, Enter interrupts" : "Typing to"} ${shortId(selectedRun.id)}`);
721
- return;
722
- }
723
- if (input.length === 0 && value === "n") {
724
- setTargetRunId(undefined);
725
- setFocusPane("task");
726
- setNotice("Typing starts a new agent");
727
- return;
728
- }
729
- if (input.length === 0 && value === "w") {
730
- setWorktreeMode((current) => current === "auto" ? "always" : "auto");
731
- return;
732
- }
733
- if (input.length === 0 && value === "x" && selectedRun) {
734
- setExpandedRunIds((current) => toggleSet(current, selectedRun.id));
735
- return;
736
- }
737
- if (input.length === 0 && value === "l" && selectedRun) {
738
- setTranscriptExpanded((current) => !current);
739
- return;
740
- }
741
- if (input.length === 0 && value === "s" && selectedRun) {
742
- void runAction(selectedRun.id, async (id) => stopRun(id, { silent: true }), "Stopped", setNotice, refresh);
743
- return;
744
- }
745
- if (input.length === 0 && value === "u" && selectedRun) {
746
- void syncSelectedRun();
747
- return;
748
- }
749
- if (input.length === 0 && value === "m" && selectedRun) {
750
- requestMergeRun(selectedRun);
751
- return;
752
- }
753
- if (input.length === 0 && value === "M") {
754
- requestMergeAll();
755
- return;
756
- }
757
- if (input.length === 0 && value === "d") {
758
- void requestDeleteSelectedRun();
759
- return;
760
- }
761
- if (input.length === 0 && value === "y") {
762
- void copySelectedTranscript();
763
- return;
764
- }
765
- if (focusPane === "agents" && value !== "/") {
766
- return;
767
- }
768
- if (focusPane === "worker" && !selectedRun && value !== "/") {
769
- setNotice("No agent selected");
770
- return;
771
- }
772
- const text = normalizeInputText(value);
773
- if (text) {
774
- setInput((current) => current + text);
775
- setCommandMenuIndex(0);
776
- }
777
- });
778
- const width = Math.max(80, size.columns);
779
- const height = Math.max(24, size.rows);
780
- const railWidth = Math.min(42, Math.max(30, Math.floor(width * 0.34)));
781
- const detailWidth = Math.max(30, width - railWidth - 1);
782
- const detailHeight = Math.max(8, height - 8);
783
- 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"
784
- ? _jsx(StatusDock, { notice: notice })
785
- : _jsx(PromptDock, { input: input, backend: backend, model: model ?? modelForBackend(backend, config), notice: notice, submitting: submitting, targetRun: targetRun, focused: focusPane === "task" }), _jsx(Footer, { focusPane: focusPane })] }));
786
- }
787
- function Header(props) {
788
- const contentWidth = Math.max(10, props.width - 4);
789
- const label = `rudder ${shortenHome(props.repoRoot)} ${props.branch} | ${props.backend}${props.model ? ` ${props.model}` : ""} | worktree:${props.worktreeMode} active:${props.activeCount}`;
790
- return (_jsx(Box, { borderStyle: "single", borderColor: "cyan", paddingX: 1, children: _jsx(Text, { bold: true, color: "cyan", children: fitLine(label, contentWidth) }) }));
791
- }
792
- function RunRail(props) {
793
- const visible = props.runs.slice(0, 12);
794
- 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)))] }));
795
- }
796
- function RunCard(props) {
797
- const tone = runStatusColor(props.run);
798
- const label = props.selected ? (props.targeted ? ">>" : "> ") : " ";
799
- const task = truncate(taskDisplayLabel(props.run, 80), Math.max(12, props.width - 14));
800
- const progress = completionPercent(props.run);
801
- const summary = truncate(agentRailSummary(props.run), Math.max(12, props.width - 7));
802
- const meta = `${progressBar(progress)} ${progress}%`;
803
- 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] })] }));
804
- }
805
- function DetailPane(props) {
806
- if (!props.run) {
807
- return (_jsx(Box, { width: props.width, height: props.height, borderStyle: props.focused ? "double" : "single", borderColor: props.focused ? "cyan" : "gray", paddingX: 1, flexDirection: "column", children: _jsx(Text, { color: "gray", children: "No agent selected." }) }));
808
- }
809
- const composerHeight = props.focused ? 3 : 0;
810
- const outputHeight = Math.max(5, props.height - 6 - composerHeight);
811
- const contentWidth = Math.max(10, props.width - 4);
812
- 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] }));
813
- }
814
- function WorkerComposer(props) {
815
- const active = isActive(props.run.status);
816
- const label = active ? "interrupt" : "agent";
817
- const helper = active ? "Enter interrupts and redirects this run" : "Enter continues this completed session";
818
- const value = props.input || (active ? "type a redirect..." : "type a follow-up...");
819
- return (_jsxs(Box, { flexDirection: "column", marginTop: 1, borderStyle: "single", borderColor: "cyan", paddingX: 1, children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Text, { children: [_jsx(FocusPill, { label: label }), _jsxs(Text, { color: props.submitting ? "yellow" : "cyan", children: [" ", props.submitting ? "sending" : shortId(props.run.id)] }), _jsxs(Text, { children: [" ", truncate(value, Math.max(8, props.width - 28))] }), _jsx(Text, { color: "cyan", children: "_" })] }), _jsx(Text, { color: "gray", children: active ? "running" : "resumable" })] }), _jsx(Text, { color: "gray", children: fitLine(`${helper}. Option-1/2/3 changes pane, Esc returns to task.`, props.width) })] }));
820
- }
821
- function Help() {
822
- return (_jsxs(Box, { borderStyle: "single", borderColor: "yellow", paddingX: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, color: "yellow", children: "keys" }), _jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: "Option-1/2/3" }), " focus agents/worker/task ", _jsx(Text, { color: "cyan", children: "Enter" }), " submit focused input ", _jsx(Text, { color: "cyan", children: "j/k" }), " select run"] }), _jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: "worker focus" }), " type to selected agent; running agents are interrupted on Enter ", _jsx(Text, { color: "cyan", children: "n" }), " new task ", _jsx(Text, { color: "cyan", children: "x" }), " expand ", _jsx(Text, { color: "cyan", children: "l" }), " transcript"] }), _jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: "o" }), " model picker ", _jsx(Text, { color: "cyan", children: "/" }), " command search ", _jsx(Text, { color: "cyan", children: "dd" }), " delete ", _jsx(Text, { color: "cyan", children: "y" }), " copy transcript ", _jsx(Text, { color: "cyan", children: "s" }), " stop ", _jsx(Text, { color: "cyan", children: "u" }), " sync ", _jsx(Text, { color: "cyan", children: "m/M" }), " merge"] }), _jsx(Text, { color: "gray", children: "Slash: /backend claude|codex, /model, /model <name>, /agent, /interrupt, /new, /worktree, /stop, /delete, /copy, /sync, /merge, /merge-all, /exit" })] }));
823
- }
824
- function FocusPill(props) {
825
- return (_jsxs(Text, { backgroundColor: "cyan", color: "black", bold: true, children: [" ", props.label.toUpperCase(), " "] }));
826
- }
827
- function ModelMenu(props) {
828
- const contentWidth = Math.max(24, props.width - 4);
829
- const start = visibleWindowStart(props.selectedIndex, props.options.length, 8);
830
- const visible = props.options.slice(start, start + 8);
831
- const hiddenBefore = start > 0;
832
- const hiddenAfter = start + visible.length < props.options.length;
833
- return (_jsxs(Box, { width: props.width, borderStyle: "single", borderColor: "magenta", paddingX: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, color: "magenta", children: fitLine(`model: ${props.backend}`, contentWidth) }), hiddenBefore ? _jsx(Text, { color: "gray", children: fitLine(" ...", contentWidth) }) : null, visible.map((option, localIndex) => {
834
- const index = start + localIndex;
835
- const selected = index === props.selectedIndex;
836
- const active = option.value === props.currentModel || (!option.value && !props.currentModel);
837
- const marker = active ? "* " : "";
838
- const line = `${selected ? "> " : " "}${index + 1}. ${marker}${option.label}${option.detail ? ` ${option.detail}` : ""}`;
839
- return (_jsx(Text, { color: selected ? "white" : "gray", bold: selected, wrap: "truncate", children: fitLine(line, contentWidth) }, `${option.label}-${index}`));
840
- }), hiddenAfter ? _jsx(Text, { color: "gray", children: fitLine(" ...", contentWidth) }) : null, _jsx(Text, { color: "gray", children: fitLine("Enter selects, Esc cancels, j/k or arrows move. Type /model <id> for custom.", contentWidth) })] }));
841
- }
842
- function CommandMenu(props) {
843
- const contentWidth = Math.max(24, props.width - 4);
844
- const start = visibleWindowStart(props.selectedIndex, props.options.length, 8);
845
- const visible = props.options.slice(start, start + 8);
846
- return (_jsxs(Box, { width: props.width, borderStyle: "single", borderColor: "blue", paddingX: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, color: "blue", children: fitLine("commands", contentWidth) }), visible.map((option, localIndex) => {
847
- const index = start + localIndex;
848
- const selected = index === props.selectedIndex;
849
- const line = `${selected ? "> " : " "}/${option.name.padEnd(12, " ")} ${option.detail}`;
850
- return (_jsx(Text, { color: selected ? "white" : "gray", bold: selected, wrap: "truncate", children: fitLine(line, contentWidth) }, option.name));
851
- }), _jsx(Text, { color: "gray", children: fitLine("Enter completes/runs selected command, arrows move, Option-1/2/3 changes pane focus.", contentWidth) })] }));
852
- }
853
- function DeletePromptBox(props) {
854
- const contentWidth = Math.max(24, props.width - 4);
855
- const action = "d delete run + worktree, Esc cancel";
856
- 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) }) }));
857
- }
858
- function MergePromptBox(props) {
859
- const contentWidth = Math.max(24, props.width - 4);
860
- const subject = props.prompt.kind === "selected"
861
- ? `merge ${shortId(props.prompt.runId)} ${props.prompt.label}`
862
- : `merge ${props.prompt.runIds.length} completed run${props.prompt.runIds.length === 1 ? "" : "s"}`;
863
- const prefix = "press ";
864
- const action = "y to merge";
865
- const suffix = ", n to cancel";
866
- const hint = fitLine(`${prefix}${action}${suffix}`, contentWidth);
867
- return (_jsxs(Box, { width: props.width, borderStyle: "double", borderColor: "yellow", paddingX: 1, flexDirection: "column", children: [_jsx(Text, { color: "yellow", bold: true, children: fitLine(subject, contentWidth) }), _jsxs(Text, { children: [_jsx(Text, { color: "gray", children: hint.slice(0, prefix.length) }), _jsx(Text, { color: "red", bold: true, children: hint.slice(prefix.length, prefix.length + action.length) }), _jsx(Text, { color: "gray", children: hint.slice(prefix.length + action.length) })] })] }));
868
- }
869
- function MergeConflictPromptBox(props) {
870
- const contentWidth = Math.max(24, props.width - 4);
871
- const files = props.prompt.files.length ? props.prompt.files.join(", ") : "unknown files";
872
- 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) })] }));
873
- }
874
- function PromptDock(props) {
875
- const label = props.targetRun ? `${isActive(props.targetRun.status) ? "interrupt" : "agent"} ${shortId(props.targetRun.id)}` : "task";
876
- const showTextLabel = !props.focused || Boolean(props.targetRun);
877
- return (_jsxs(Box, { borderStyle: props.focused ? "double" : "single", borderColor: props.focused ? "cyan" : props.targetRun ? "magenta" : "gray", paddingX: 1, justifyContent: "space-between", children: [_jsxs(Text, { children: [props.focused ? _jsx(FocusPill, { label: "task" }) : null, showTextLabel ? (_jsxs(Text, { color: props.submitting ? "yellow" : props.targetRun ? "magenta" : "cyan", children: [props.focused ? " " : "", props.submitting ? "starting" : label] })) : null, _jsxs(Text, { children: [" ", props.input] }), _jsx(Text, { color: "cyan", children: "_" })] }), _jsxs(Text, { color: "gray", children: [props.notice, " ", props.backend, props.model ? ` ${props.model}` : ""] })] }));
878
- }
879
- function StatusDock(props) {
880
- return (_jsxs(Box, { borderStyle: "single", borderColor: "gray", paddingX: 1, justifyContent: "space-between", children: [_jsx(Text, { color: "gray", children: "worker input is active inside the selected agent pane" }), _jsx(Text, { color: "gray", children: props.notice })] }));
881
- }
882
- function Footer(props) {
883
- return (_jsx(Box, { children: _jsxs(Text, { color: "gray", children: ["focus:", props.focusPane, " Opt-1/2/3 focus / commands o model n new c worker u sync dd delete y copy m/M merge ? help"] }) }));
884
- }
885
- async function loadUiRuns(repoRoot) {
886
- const runs = await listRuns(repoRoot);
887
- return await Promise.all(runs.map(async (run) => {
888
- const [output, events] = await Promise.all([
889
- readTextIfExists(outputPath(repoRoot, run.id)),
890
- readEvents(repoRoot, run.id),
891
- ]);
892
- return {
893
- ...run,
894
- output,
895
- events,
896
- work: buildWork(events, run),
897
- attention: permissionAttentionFromOutput(output),
898
- };
899
- }));
900
- }
901
- async function readTextIfExists(file) {
902
- if (!(await pathExists(file))) {
903
- return "";
904
- }
905
- return await fsp.readFile(file, "utf8").catch(() => "");
906
- }
907
- async function readEvents(repoRoot, runId) {
908
- const file = eventsPath(repoRoot, runId);
909
- const raw = await readTextIfExists(file);
910
- return raw
911
- .split(/\r?\n/)
912
- .map((line) => line.trim())
913
- .filter(Boolean)
914
- .map((line) => {
915
- try {
916
- return JSON.parse(line);
917
- }
918
- catch {
919
- return null;
920
- }
921
- })
922
- .filter((event) => Boolean(event));
923
- }
924
- function buildWork(events, run) {
925
- const items = [];
926
- for (const event of events) {
927
- if (event.type === "run.created") {
928
- continue;
929
- }
930
- if (event.type === "run.continued") {
931
- items.push({ label: "user follow-up", detail: event.message, tone: "info" });
932
- continue;
933
- }
934
- if (event.type === "steerer.waiting") {
935
- items.push({ label: "auto-steering wait", detail: "10s grace period", tone: "warning" });
936
- continue;
937
- }
938
- if (event.type === "steerer.prompt") {
939
- items.push({ label: "auto-steering", detail: "review prompt sent", tone: "info" });
940
- continue;
941
- }
942
- if (event.type === "planner.spec") {
943
- continue;
944
- }
945
- if (event.type === "run.started") {
946
- continue;
947
- }
948
- if (event.type === "backend.output") {
949
- const tool = toolSummary(event.data);
950
- if (tool) {
951
- items.push(tool);
952
- }
953
- continue;
954
- }
955
- if (event.type === "backend.error") {
956
- items.push({ label: "backend error", detail: event.message, tone: "danger" });
957
- continue;
958
- }
959
- if (event.type === "verifier.result") {
960
- const missing = missingCount(event.data);
961
- items.push({ label: "verifier", detail: missing ? `${missing} missing item${missing === 1 ? "" : "s"}` : "accepted", tone: missing ? "warning" : "success" });
962
- continue;
963
- }
964
- if (event.type === "run.completed") {
965
- items.push({ label: "completed", detail: event.message, tone: "success" });
966
- continue;
967
- }
968
- if (event.type === "run.failed") {
969
- items.push({ label: "failed", detail: event.message, tone: "danger" });
970
- continue;
971
- }
972
- if (event.type === "run.cancelled") {
973
- items.push({ label: "cancelled", detail: event.message, tone: "warning" });
974
- continue;
975
- }
976
- if (event.type === "merge.result") {
977
- const detail = event.message;
978
- const tone = detail?.includes("conflict") || detail?.includes("failed") ? "warning" : "success";
979
- items.push({ label: "merge", detail, tone });
980
- }
981
- if (event.type === "sync.result") {
982
- const detail = event.message;
983
- const tone = detail?.includes("conflict") || detail?.includes("failed") ? "warning" : "success";
984
- items.push({ label: "sync", detail, tone });
985
- }
986
- }
987
- return compactWork(items);
988
- }
989
- function toolSummary(data) {
990
- if (!isRecord(data) || data.type !== "stream_event" || !isRecord(data.event)) {
991
- return null;
992
- }
993
- const event = data.event;
994
- if (event.type === "content_block_start" && isRecord(event.content_block)) {
995
- const block = event.content_block;
996
- if (block.type === "tool_use") {
997
- return { label: "tool", detail: typeof block.name === "string" ? block.name : "tool_use", tone: "muted" };
998
- }
999
- }
1000
- return null;
1001
- }
1002
- function compactWork(items) {
1003
- const compacted = [];
1004
- for (const item of items) {
1005
- const last = compacted.at(-1);
1006
- if (last && last.label === item.label && last.detail === item.detail && last.tone === item.tone) {
1007
- continue;
1008
- }
1009
- compacted.push(item);
1010
- }
1011
- return compacted;
1012
- }
1013
- function formatWorkLine(item, width) {
1014
- const raw = `${workGlyph(item.tone)} ${item.label}${item.detail ? ` ${item.detail}` : ""}`;
1015
- return fitLine(raw, width);
1016
- }
1017
- function fitLine(value, width) {
1018
- return truncate(value, width).padEnd(width, " ");
1019
- }
1020
- async function runAction(runId, action, success, setNotice, refresh) {
1021
- if (!runId) {
1022
- setNotice("No run selected");
1023
- return;
1024
- }
1025
- try {
1026
- await action(runId);
1027
- setNotice(`${success} ${shortId(runId)}`);
1028
- await refresh();
1029
- }
1030
- catch (error) {
1031
- setNotice(error instanceof Error ? error.message : String(error));
1032
- }
1033
- }
1034
- async function mergeReadyRuns(runs, allowDirty, setNotice, refresh) {
1035
- const ready = runs.filter(canMerge);
1036
- if (ready.length === 0) {
1037
- setNotice("No completed worktree runs ready to merge");
1038
- return;
1039
- }
1040
- setNotice(`Merging ${ready.length} run${ready.length === 1 ? "" : "s"}...`);
1041
- let merged = 0;
1042
- for (const run of ready) {
1043
- const result = await mergeRun(run.id, allowDirty, { silent: true });
1044
- if (result.merge?.status === "conflict") {
1045
- const files = result.merge.conflictedFiles ?? [];
1046
- setNotice(`Merge all stopped after ${merged}: conflict in ${shortId(run.id)} (${files.length || "unknown"} file${files.length === 1 ? "" : "s"})`);
1047
- await refresh();
1048
- return;
1049
- }
1050
- if (result.merge?.status !== "merged") {
1051
- setNotice(`Merge all stopped after ${merged}: failed in ${shortId(run.id)}: ${result.merge?.error ?? "unknown error"}`);
1052
- await refresh();
1053
- return;
1054
- }
1055
- merged += 1;
1056
- }
1057
- setNotice(`Merged ${merged} run${merged === 1 ? "" : "s"}`);
1058
- await refresh();
1059
- }
1060
- function selectRelative(runs, selectedRunId, delta, setSelectedRunId) {
1061
- if (runs.length === 0) {
1062
- return;
1063
- }
1064
- const index = Math.max(0, runs.findIndex((run) => run.id === selectedRunId));
1065
- const next = Math.min(runs.length - 1, Math.max(0, index + delta));
1066
- setSelectedRunId(runs[next]?.id);
1067
- }
1068
- function chooseBackend(backend, setBackend, setModel, setNotice) {
1069
- setBackend(backend);
1070
- setModel(undefined);
1071
- setNotice(`Backend ${backend}`);
1072
- }
1073
- function visibleWindowStart(selectedIndex, total, windowSize) {
1074
- if (total <= windowSize) {
1075
- return 0;
1076
- }
1077
- const half = Math.floor(windowSize / 2);
1078
- return Math.max(0, Math.min(total - windowSize, selectedIndex - half));
1079
- }
1080
- function toggleSet(current, value) {
1081
- const next = new Set(current);
1082
- if (next.has(value)) {
1083
- next.delete(value);
1084
- }
1085
- else {
1086
- next.add(value);
1087
- }
1088
- return next;
1089
- }
1090
- function modelForBackend(backend, config) {
1091
- if (!config) {
1092
- return undefined;
1093
- }
1094
- if (backend === "claude") {
1095
- return config.backends.claude?.model;
1096
- }
1097
- if (backend === "codex") {
1098
- return config.backends.codex?.model;
1099
- }
1100
- return config.backends.acpx?.model;
1101
- }
1102
- function notifyRunAlerts(runs, ref) {
1103
- const terminal = new Set(runs.filter((run) => isTerminal(run.status)).map((run) => run.id));
1104
- const permission = new Set(runs.filter((run) => runNeedsPermission(run)).map((run) => run.id));
1105
- if (!ref.current) {
1106
- ref.current = { terminal, permission };
1107
- return;
1108
- }
1109
- for (const run of runs) {
1110
- if (isTerminal(run.status) && !ref.current.terminal.has(run.id)) {
1111
- playCompletionSound();
1112
- ref.current.terminal.add(run.id);
1113
- }
1114
- if (runNeedsPermission(run) && !ref.current.permission.has(run.id)) {
1115
- playCompletionSound();
1116
- ref.current.permission.add(run.id);
1117
- }
1118
- }
1119
- ref.current.terminal = terminal;
1120
- ref.current.permission = permission;
1121
- }
1122
- function playCompletionSound() {
1123
- try {
1124
- const player = process.platform === "darwin" ? "afplay" : "ffplay";
1125
- const args = process.platform === "darwin"
1126
- ? [COMPLETION_SOUND]
1127
- : ["-nodisp", "-autoexit", "-loglevel", "quiet", COMPLETION_SOUND];
1128
- const child = spawn(player, args, {
1129
- detached: true,
1130
- stdio: "ignore",
1131
- });
1132
- child.on("error", () => process.stdout.write("\u0007"));
1133
- child.unref();
1134
- }
1135
- catch {
1136
- process.stdout.write("\u0007");
1137
- }
1138
- }
1139
- function resolveUiRun(runs, runId) {
1140
- if (!runId) {
1141
- return undefined;
1142
- }
1143
- return runs.find((run) => run.id === runId || run.id.startsWith(runId));
1144
- }
1145
- function filterCommands(input) {
1146
- if (!input.startsWith("/")) {
1147
- return [];
1148
- }
1149
- const query = input.slice(1).trim().toLowerCase();
1150
- if (!query) {
1151
- return COMMANDS;
1152
- }
1153
- return COMMANDS.filter((command) => command.name.includes(query) || command.detail.toLowerCase().includes(query));
1154
- }
1155
- function shouldSelectCommand(input, options) {
1156
- const trimmed = input.trim();
1157
- if (!trimmed.startsWith("/") || trimmed.includes(" ")) {
1158
- return false;
1159
- }
1160
- const query = trimmed.slice(1);
1161
- return query.length === 0 || !options.some((option) => option.name === query);
1162
- }
1163
- function isInteractiveBackend(value) {
1164
- return value === "claude" || value === "codex";
1165
- }
1166
- function toInteractiveBackend(value) {
1167
- return value === "codex" ? "codex" : "claude";
1168
- }
1169
- function isActive(status) {
1170
- return status === "created" || status === "running" || status === "steering" || status === "verifying";
1171
- }
1172
- function isTerminal(status) {
1173
- return status === "completed" || status === "failed" || status === "cancelled" || status === "merged" || status === "merge-conflict";
1174
- }
1175
- function statusGlyph(status) {
1176
- if (status === "completed" || status === "merged") {
1177
- return "ok";
1178
- }
1179
- if (status === "failed" || status === "merge-conflict") {
1180
- return "!!";
1181
- }
1182
- if (status === "cancelled") {
1183
- return "--";
1184
- }
1185
- return "..";
1186
- }
1187
- function statusColor(status) {
1188
- if (status === "completed" || status === "merged") {
1189
- return "green";
1190
- }
1191
- if (status === "failed" || status === "merge-conflict") {
1192
- return "red";
1193
- }
1194
- if (status === "cancelled") {
1195
- return "yellow";
1196
- }
1197
- if (status === "verifying" || status === "steering") {
1198
- return "magenta";
1199
- }
1200
- return "cyan";
1201
- }
1202
- function runStatusColor(run) {
1203
- return runNeedsPermission(run) ? "yellow" : statusColor(run.status);
1204
- }
1205
- function runNeedsPermission(run) {
1206
- return isActive(run.status) && run.attention.needsPermission;
1207
- }
1208
- function toneColor(tone) {
1209
- if (tone === "success") {
1210
- return "green";
1211
- }
1212
- if (tone === "warning") {
1213
- return "yellow";
1214
- }
1215
- if (tone === "danger") {
1216
- return "red";
1217
- }
1218
- if (tone === "info") {
1219
- return "cyan";
1220
- }
1221
- return "gray";
1222
- }
1223
- function workGlyph(tone) {
1224
- if (tone === "success") {
1225
- return "ok";
1226
- }
1227
- if (tone === "warning") {
1228
- return "??";
1229
- }
1230
- if (tone === "danger") {
1231
- return "!!";
1232
- }
1233
- if (tone === "info") {
1234
- return "->";
1235
- }
1236
- return "--";
1237
- }
1238
- function completionPercent(run) {
1239
- if (runNeedsPermission(run)) {
1240
- return 90;
1241
- }
1242
- if (run.status === "merged") {
1243
- return 100;
1244
- }
1245
- if (run.status === "completed") {
1246
- return 95;
1247
- }
1248
- if (run.status === "merge-conflict") {
1249
- return 85;
1250
- }
1251
- if (run.status === "failed" || run.status === "cancelled") {
1252
- return 50;
1253
- }
1254
- if (run.status === "steering") {
1255
- return 88;
1256
- }
1257
- if (run.status === "verifying") {
1258
- return 82;
1259
- }
1260
- const labels = new Set(run.work.map((item) => item.label));
1261
- let value = 8;
1262
- if (labels.has("worktree prepared") || labels.has("checkout claimed")) {
1263
- value = 18;
1264
- }
1265
- if (labels.has("planner contract")) {
1266
- value = 28;
1267
- }
1268
- if (labels.has("worker started")) {
1269
- value = 45;
1270
- }
1271
- if (run.output.trim()) {
1272
- value = Math.max(value, 60);
1273
- }
1274
- return value;
1275
- }
1276
- function progressBar(percent) {
1277
- const width = 6;
1278
- const filled = Math.max(0, Math.min(width, Math.round((percent / 100) * width)));
1279
- return `[${"#".repeat(filled)}${"-".repeat(width - filled)}]`;
1280
- }
1281
- function canMerge(run) {
1282
- return (run.status === "completed" || (run.status === "merge-conflict" && run.merge?.conflictKind === "rebase")) && run.worktree.enabled;
1283
- }
1284
- function runSummary(run) {
1285
- if (runNeedsPermission(run)) {
1286
- return run.attention.summary ? `needs permission: ${run.attention.summary}` : "needs permission";
1287
- }
1288
- const latestWork = run.work.at(-1);
1289
- if (run.status === "steering") {
1290
- return "auto-steering after completion";
1291
- }
1292
- if (latestWork) {
1293
- return `${latestWork.label}${latestWork.detail ? `: ${latestWork.detail}` : ""}`;
1294
- }
1295
- const latestLine = tailLines(run.output, 1)[0];
1296
- if (latestLine && latestLine !== "No transcript yet.") {
1297
- return latestLine;
1298
- }
1299
- return run.status;
1300
- }
1301
- function workerStateLabel(run) {
1302
- if (runNeedsPermission(run)) {
1303
- return "needs permission";
1304
- }
1305
- if (run.status === "completed") {
1306
- return canMerge(run) ? "done u sync m merge" : "done";
1307
- }
1308
- if (run.status === "merged") {
1309
- return "merged";
1310
- }
1311
- if (run.status === "failed" || run.status === "merge-conflict") {
1312
- return "failed";
1313
- }
1314
- if (run.status === "cancelled") {
1315
- return "stopped";
1316
- }
1317
- if (run.status === "verifying" || run.status === "steering") {
1318
- return "checking";
1319
- }
1320
- return "running";
1321
- }
1322
- function syncNotice(run) {
1323
- if (run.sync?.status === "synced") {
1324
- return `Synced ${shortId(run.id)}`;
1325
- }
1326
- if (run.sync?.status === "conflict") {
1327
- const count = run.sync.conflictedFiles?.length || "unknown";
1328
- return `Sync conflict in ${count} file${count === 1 ? "" : "s"}; resolve in the worktree and run git rebase --continue`;
1329
- }
1330
- return `Sync failed: ${run.sync?.error ?? "unknown error"}`;
1331
- }
1332
- function agentRailSummary(run) {
1333
- if (runNeedsPermission(run)) {
1334
- return run.attention.summary ?? "waiting for permission";
1335
- }
1336
- const output = summarizeOutput(run.output);
1337
- if (output) {
1338
- return output;
1339
- }
1340
- const latestWork = run.work.at(-1);
1341
- if (latestWork) {
1342
- return `${latestWork.label}${latestWork.detail ? `: ${latestWork.detail}` : ""}`;
1343
- }
1344
- return run.currentPrompt && run.currentPrompt !== run.task ? run.currentPrompt : run.task;
1345
- }
1346
- function summarizeOutput(output) {
1347
- const normalized = output.replace(/\s+/g, " ").trim();
1348
- if (!normalized) {
1349
- return "";
1350
- }
1351
- const sentence = normalized.match(/^.{24,180}?[.!?](?:\s|$)/)?.[0]?.trim();
1352
- return sentence || normalized.slice(0, 180);
1353
- }
1354
- function statusWord(run) {
1355
- if (runNeedsPermission(run)) {
1356
- return "permission:";
1357
- }
1358
- const status = run.status;
1359
- if (status === "merged") {
1360
- return "merged:";
1361
- }
1362
- if (status === "completed") {
1363
- return "done:";
1364
- }
1365
- if (status === "failed" || status === "merge-conflict") {
1366
- return "failed:";
1367
- }
1368
- if (status === "cancelled") {
1369
- return "stopped:";
1370
- }
1371
- if (status === "steering" || status === "verifying") {
1372
- return "checking:";
1373
- }
1374
- return "running:";
1375
- }
1376
- function deletePreviousWord(value) {
1377
- return value.replace(/\s+$/, "").replace(/\S+$/, "");
1378
- }
1379
- function normalizeInputText(value) {
1380
- return value
1381
- .replace(/\x1b\[[0-9;]*[A-Za-z]/g, "")
1382
- .replace(/\r?\n/g, " ")
1383
- .replace(/\t/g, " ")
1384
- .replace(/[\u0000-\u0008\u000b-\u001f\u007f]/g, "");
1385
- }
1386
- async function copyToClipboard(text) {
1387
- if (!text.trim()) {
1388
- throw new Error("No transcript to copy");
1389
- }
1390
- const command = process.platform === "darwin" ? "pbcopy" : "xclip";
1391
- const args = process.platform === "darwin" ? [] : ["-selection", "clipboard"];
1392
- await new Promise((resolve, reject) => {
1393
- const child = spawn(command, args, { stdio: ["pipe", "ignore", "ignore"] });
1394
- child.on("error", reject);
1395
- child.on("close", (code) => {
1396
- if (code === 0) {
1397
- resolve();
1398
- }
1399
- else {
1400
- reject(new Error(`Clipboard command exited with ${code}`));
1401
- }
1402
- });
1403
- child.stdin.end(text);
1404
- });
1405
- }
1406
- function missingCount(data) {
1407
- if (!isRecord(data) || !Array.isArray(data.missing)) {
1408
- return 0;
1409
- }
1410
- return data.missing.length;
1411
- }
1412
- function isRecord(value) {
1413
- return Boolean(value && typeof value === "object" && !Array.isArray(value));
1414
- }
1415
- function tailLines(text, maxLines) {
1416
- const normalized = text.replace(/\r/g, "");
1417
- const lines = normalized.includes("\n") ? normalized.split("\n") : chunkLine(normalized, 96);
1418
- const visible = lines.filter((line, index) => line.length > 0 || index < lines.length - 1).slice(-Math.max(1, maxLines));
1419
- return visible.length ? visible : ["No transcript yet."];
1420
- }
1421
- function chunkLine(text, width) {
1422
- if (!text) {
1423
- return [];
1424
- }
1425
- const chunks = [];
1426
- for (let i = 0; i < text.length; i += width) {
1427
- chunks.push(text.slice(i, i + width));
1428
- }
1429
- return chunks;
1430
- }
1431
- function shortId(runId) {
1432
- return runId.slice(0, 14);
1433
- }
1434
- function truncate(value, width) {
1435
- if (value.length <= width) {
1436
- return value;
1437
- }
1438
- if (width <= 1) {
1439
- return value.slice(0, width);
1440
- }
1441
- return `${value.slice(0, Math.max(0, width - 3))}...`;
1442
- }
1443
- //# sourceMappingURL=tui.js.map