parallel-codex-tui 0.1.3 → 0.1.4

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.
Files changed (72) hide show
  1. package/.parallel-codex/config.example.toml +90 -3
  2. package/README.md +240 -21
  3. package/dist/bootstrap.js +37 -17
  4. package/dist/cli-args.js +34 -3
  5. package/dist/cli-help.js +20 -0
  6. package/dist/cli-startup-recovery.js +70 -0
  7. package/dist/cli-workspace-picker.js +330 -0
  8. package/dist/cli-workspace-transition.js +33 -0
  9. package/dist/cli-workspace.js +7 -71
  10. package/dist/cli.js +221 -23
  11. package/dist/core/collaboration-timeline.js +261 -0
  12. package/dist/core/config-errors.js +14 -0
  13. package/dist/core/config.js +154 -24
  14. package/dist/core/file-store.js +119 -6
  15. package/dist/core/lease-finalization.js +22 -0
  16. package/dist/core/paths.js +7 -0
  17. package/dist/core/process-identity.js +48 -0
  18. package/dist/core/process-mutation-turn.js +128 -0
  19. package/dist/core/process-ownership.js +276 -0
  20. package/dist/core/process-tree.js +90 -0
  21. package/dist/core/router-audit.js +155 -0
  22. package/dist/core/router-redaction.js +31 -0
  23. package/dist/core/router.js +462 -35
  24. package/dist/core/session-index.js +188 -37
  25. package/dist/core/session-manager.js +1086 -40
  26. package/dist/core/task-state-machine.js +17 -0
  27. package/dist/core/workspace-commit-recovery.js +118 -0
  28. package/dist/core/workspace.js +19 -11
  29. package/dist/doctor.js +343 -23
  30. package/dist/domain/schemas.js +127 -6
  31. package/dist/orchestrator/collaboration-channel.js +255 -4
  32. package/dist/orchestrator/feature-plan.js +70 -0
  33. package/dist/orchestrator/judge-artifacts.js +236 -0
  34. package/dist/orchestrator/orchestrator.js +1749 -202
  35. package/dist/orchestrator/prompts.js +126 -2
  36. package/dist/orchestrator/supervisor-summary.js +56 -2
  37. package/dist/orchestrator/workspace-sandbox.js +911 -0
  38. package/dist/tui/App.js +2830 -153
  39. package/dist/tui/AppShell.js +188 -23
  40. package/dist/tui/CollaborationTimelineView.js +327 -0
  41. package/dist/tui/FeatureBoardView.js +227 -0
  42. package/dist/tui/InputBar.js +514 -57
  43. package/dist/tui/RouterDiagnosticsView.js +469 -0
  44. package/dist/tui/StatusBar.js +610 -57
  45. package/dist/tui/TaskSessionsView.js +207 -0
  46. package/dist/tui/TerminalOutput.js +53 -9
  47. package/dist/tui/WorkerOutputView.js +1403 -161
  48. package/dist/tui/WorkerOverviewView.js +250 -0
  49. package/dist/tui/chat-history.js +25 -0
  50. package/dist/tui/chat-input.js +67 -19
  51. package/dist/tui/chat-paste.js +76 -0
  52. package/dist/tui/display-width.js +41 -3
  53. package/dist/tui/incremental-text-file.js +101 -0
  54. package/dist/tui/keyboard.js +46 -0
  55. package/dist/tui/markdown-text.js +14 -0
  56. package/dist/tui/raw-input-decoder.js +3 -0
  57. package/dist/tui/scrolling.js +2 -1
  58. package/dist/tui/status-line.js +318 -11
  59. package/dist/tui/task-memory.js +13 -1
  60. package/dist/tui/task-result.js +105 -0
  61. package/dist/tui/terminal-screen.js +13 -1
  62. package/dist/tui/theme-contrast.js +144 -0
  63. package/dist/tui/theme-preview.js +109 -0
  64. package/dist/tui/theme.js +158 -0
  65. package/dist/version.js +1 -1
  66. package/dist/workers/capabilities.js +212 -0
  67. package/dist/workers/live-probe.js +176 -0
  68. package/dist/workers/mock-adapter.js +39 -6
  69. package/dist/workers/native-attach.js +78 -3
  70. package/dist/workers/process-adapter.js +570 -77
  71. package/dist/workers/registry.js +4 -2
  72. package/package.json +5 -2
package/dist/tui/App.js CHANGED
@@ -1,70 +1,253 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useEffect, useRef, useState } from "react";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useEffect, useMemo, useRef, useState } from "react";
3
3
  import { basename } from "node:path";
4
4
  import { Box, Text, useApp, useInput, useStdin } from "ink";
5
+ import { Lexer } from "marked";
5
6
  import { readJson } from "../core/file-store.js";
6
7
  import { WorkerStatusSchema } from "../domain/schemas.js";
7
- import { formatSelectedWorkerStatus, formatStatusLine, formatWorkerRuntimeStatus } from "./status-line.js";
8
- import { applyChatInputChunk } from "./chat-input.js";
8
+ import { effectiveWorkerWatchdog, formatRoutePendingStatus, formatSelectedWorkerStatus, formatRouteStatus, formatStatusLine, formatWorkerRuntimeStatus } from "./status-line.js";
9
+ import { applyChatInputChunk, insertChatPaste } from "./chat-input.js";
10
+ import { chatRequestHistory, navigateChatDraftHistory } from "./chat-history.js";
11
+ import { createChatPasteDecoder } from "./chat-paste.js";
9
12
  import { AppShell } from "./AppShell.js";
10
13
  import { InputBar } from "./InputBar.js";
11
14
  import { applyNativeInputChunk } from "./native-input.js";
12
15
  import { nextScrollOffset } from "./scrolling.js";
13
- import { chooseSubmitTarget, nextSubmitMemoryState, shouldClearWorkersForSubmit } from "./task-memory.js";
16
+ import { chooseSubmitTarget, currentSubmitMemoryState, newTaskMemoryState, nextSubmitMemoryState, shouldClearWorkersForSubmit } from "./task-memory.js";
14
17
  import { TerminalOutput } from "./TerminalOutput.js";
15
18
  import { NativeTerminalScreen } from "./terminal-screen.js";
16
19
  import { WorkerOutputView } from "./WorkerOutputView.js";
17
20
  import { compactEndByDisplayWidth, displayWidth, wrapByDisplayWidth } from "./display-width.js";
18
- import { isAttachShortcut, isExitShortcut, isLogsShortcut, mouseScrollDelta, scrollDelta } from "./keyboard.js";
19
- import { createRawInputDecoder } from "./raw-input-decoder.js";
21
+ import { isAttachShortcut, isExitShortcut, isLogsShortcut, isNewTaskShortcut, isRouterDiagnosticsShortcut, isTaskResultShortcut, isTaskSessionsShortcut, isWorkerOverviewShortcut, isWorkerSearchShortcut, isWorkspaceShortcut, mouseScrollDelta, rawHistoryDelta, rawPageScrollDelta, scrollDelta, workerLogJumpKind } from "./keyboard.js";
22
+ import { createRawInputDecoder, tokenizeRawInput } from "./raw-input-decoder.js";
23
+ import { decodeHtmlEntities } from "./markdown-text.js";
24
+ import { configureTuiTheme, TUI_THEME } from "./theme.js";
25
+ import { WorkspacePicker } from "../cli-workspace-picker.js";
26
+ import { RouterDiagnosticsView, routerDiagnosticsPolicy } from "./RouterDiagnosticsView.js";
27
+ import { moveWorkerSelection, WorkerOverviewView } from "./WorkerOverviewView.js";
28
+ import { FeatureBoardView, moveFeatureBoardSelection } from "./FeatureBoardView.js";
29
+ import { CollaborationTimelineView, collaborationSelectionScrollOffset, collaborationTimelineEvents, moveCollaborationEventSelection, nextCollaborationFeatureIndex } from "./CollaborationTimelineView.js";
30
+ import { moveTaskSessionSelection, TaskSessionsView } from "./TaskSessionsView.js";
31
+ import { latestTaskResultMessageIndex, parseTaskResultSummary } from "./task-result.js";
20
32
  import { buildNativeAttachLaunch, startNativeAttachProcess } from "../workers/native-attach.js";
21
- const NATIVE_PANEL_BACKGROUND = "ansi256(235)";
22
- const NATIVE_PANEL_TITLE_BACKGROUND = "ansi256(24)";
23
- export function App({ config, orchestrator, cwd, initialTaskId = null, prepareNativeAttach, startNativeAttach }) {
33
+ export function routeDecisionChatMessage(route) {
34
+ return `route · ${route.mode} · ${route.source ?? "router"}\n${route.reason.trim()}`;
35
+ }
36
+ const NO_WORKERS_ATTACH_MESSAGE = "No workers yet · start a complex task before attaching";
37
+ const NO_WORKERS_LOGS_MESSAGE = "No workers yet · start a complex task before opening logs";
38
+ const NO_WORKERS_OVERVIEW_MESSAGE = "No workers yet · start a complex task before opening overview";
39
+ const NO_ACTIVE_COLLABORATION_MESSAGE = "No active task · restore a task before opening timeline";
40
+ const NO_ACTIVE_FEATURES_MESSAGE = "No active task · restore a task before opening features";
41
+ const EMPTY_WORKER_NAVIGATION_TARGETS = {
42
+ searchOffsets: [],
43
+ searchLineIndexes: [],
44
+ errorOffsets: [],
45
+ diffOffsets: []
46
+ };
47
+ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRoute = null, initialWorkers, initialCanRetryTask = false, initialMessages = [], persistChatMessage, workspaceChoices = [], switchWorkspace, loadRouterDiagnostics, loadTaskSessions, renameTaskSession, setTaskSessionArchived, deleteTaskSession, exportTaskSession, loadCollaborationTimeline, activateTaskSession, prepareNativeAttach, startNativeAttach, shutdownSignal }) {
48
+ configureTuiTheme({
49
+ theme: config.ui.theme,
50
+ colors: config.ui.colors
51
+ });
24
52
  const [input, setInput] = useState("");
25
- const [messages, setMessages] = useState([]);
53
+ const [inputCursor, setInputCursor] = useState(0);
54
+ const [inputReady, setInputReady] = useState(false);
55
+ const [terminalSize, setTerminalSize] = useState(readTerminalSize);
56
+ const [messages, setMessages] = useState(() => [...initialMessages]);
57
+ const [taskResultExpanded, setTaskResultExpanded] = useState(() => (Boolean(initialTaskId) && latestTaskResultMessageIndex(initialMessages, initialTaskId) >= 0));
26
58
  const [busy, setBusy] = useState(false);
27
- const [status, setStatus] = useState(initialTaskId ? { taskId: initialTaskId } : null);
59
+ const [status, setStatus] = useState(() => restoredWorkerStatusLine(initialTaskId, initialWorkers));
60
+ const [lastRoute, setLastRoute] = useState(initialRoute);
61
+ const [routePending, setRoutePending] = useState(null);
62
+ const [routeAnnouncement, setRouteAnnouncement] = useState(null);
63
+ const [routeElapsedMs, setRouteElapsedMs] = useState(0);
64
+ const [routeFallbackPrompt, setRouteFallbackPrompt] = useState(null);
28
65
  const [view, setView] = useState("chat");
29
- const [workers, setWorkers] = useState([]);
66
+ const [workers, setWorkers] = useState(() => [...(initialWorkers ?? [])]);
30
67
  const [selectedWorkerIndex, setSelectedWorkerIndex] = useState(0);
68
+ const [workerClockMs, setWorkerClockMs] = useState(() => Date.now());
31
69
  const [activeTaskId, setActiveTaskId] = useState(initialTaskId);
32
70
  const [activeMode, setActiveMode] = useState(initialTaskId ? "complex" : null);
71
+ const [canRetryTask, setCanRetryTask] = useState(initialCanRetryTask);
33
72
  const [attachError, setAttachError] = useState(null);
34
73
  const [nativeInput, setNativeInput] = useState("");
35
74
  const [workerScrollOffset, setWorkerScrollOffset] = useState(0);
36
- const [workerMaxScrollOffset, setWorkerMaxScrollOffset] = useState(0);
75
+ const [workerSearch, setWorkerSearch] = useState({
76
+ open: false,
77
+ query: "",
78
+ cursor: 0,
79
+ matchIndex: 0
80
+ });
81
+ const [workerNavigationTargets, setWorkerNavigationTargets] = useState(EMPTY_WORKER_NAVIGATION_TARGETS);
82
+ const [chatScrollOffset, setChatScrollOffset] = useState(0);
83
+ const [chatMaxScrollOffset, setChatMaxScrollOffset] = useState(0);
84
+ const [routerRecords, setRouterRecords] = useState([]);
85
+ const [routerPolicy, setRouterPolicy] = useState(() => routerDiagnosticsPolicy(config.router));
86
+ const [routerLoading, setRouterLoading] = useState(false);
87
+ const [routerError, setRouterError] = useState(null);
88
+ const [routerScope, setRouterScope] = useState("all");
89
+ const [routerScrollOffset, setRouterScrollOffset] = useState(0);
90
+ const [routerMaxScrollOffset, setRouterMaxScrollOffset] = useState(0);
91
+ const [taskSessions, setTaskSessions] = useState([]);
92
+ const [selectedTaskSessionIndex, setSelectedTaskSessionIndex] = useState(0);
93
+ const [taskSessionsLoading, setTaskSessionsLoading] = useState(false);
94
+ const [taskSessionsError, setTaskSessionsError] = useState(null);
95
+ const [taskSessionsNotice, setTaskSessionsNotice] = useState(null);
96
+ const [taskSessionsIncludeArchived, setTaskSessionsIncludeArchived] = useState(false);
97
+ const [taskSessionAction, setTaskSessionAction] = useState(null);
98
+ const [collaborationTimeline, setCollaborationTimeline] = useState(null);
99
+ const [featureBoardSelectedIndex, setFeatureBoardSelectedIndex] = useState(0);
100
+ const [featureCancelPrompt, setFeatureCancelPrompt] = useState(null);
101
+ const [featureBoardNotice, setFeatureBoardNotice] = useState(null);
102
+ const [collaborationLoading, setCollaborationLoading] = useState(false);
103
+ const [collaborationError, setCollaborationError] = useState(null);
104
+ const [collaborationFeatureIndex, setCollaborationFeatureIndex] = useState(-1);
105
+ const [collaborationSelectedEventId, setCollaborationSelectedEventId] = useState(null);
106
+ const [collaborationDetailOpen, setCollaborationDetailOpen] = useState(false);
107
+ const [collaborationUnresolvedOnly, setCollaborationUnresolvedOnly] = useState(false);
108
+ const [collaborationScrollOffset, setCollaborationScrollOffset] = useState(0);
109
+ const [collaborationMaxScrollOffset, setCollaborationMaxScrollOffset] = useState(0);
37
110
  const [nativeAttach, setNativeAttach] = useState(null);
38
111
  const { exit } = useApp();
39
112
  const { setRawMode, internal_eventEmitter: stdinEvents } = useStdin();
113
+ const mountedRef = useRef(true);
40
114
  const nativeAttachRef = useRef(nativeAttach);
115
+ const nativeAttachRequestSequenceRef = useRef(0);
116
+ const messagesRef = useRef([...initialMessages]);
117
+ const activeRunControllerRef = useRef(null);
118
+ const activeTaskIdRef = useRef(initialTaskId);
41
119
  const nativeInputRef = useRef(nativeInput);
42
120
  const inputRef = useRef(input);
121
+ const inputCursorRef = useRef(inputCursor);
43
122
  const viewRef = useRef(view);
44
123
  const busyRef = useRef(busy);
124
+ const routeFallbackPromptRef = useRef(null);
125
+ const routeFallbackResolverRef = useRef(null);
45
126
  const workersRef = useRef(workers);
46
127
  const selectedWorkerIndexRef = useRef(selectedWorkerIndex);
47
- const workerMaxScrollOffsetRef = useRef(workerMaxScrollOffset);
128
+ const workerSearchRef = useRef(workerSearch);
129
+ const workerNavigationTargetsRef = useRef(workerNavigationTargets);
130
+ const workerJumpIndexRef = useRef({ error: -1, diff: -1 });
131
+ const chatScrollOffsetRef = useRef(chatScrollOffset);
132
+ const chatMaxScrollOffsetRef = useRef(chatMaxScrollOffset);
133
+ const taskResultExpandedRef = useRef(taskResultExpanded);
134
+ const routerMaxScrollOffsetRef = useRef(routerMaxScrollOffset);
135
+ const taskSessionsRef = useRef(taskSessions);
136
+ const selectedTaskSessionIndexRef = useRef(selectedTaskSessionIndex);
137
+ const taskSessionsLoadingRef = useRef(taskSessionsLoading);
138
+ const taskSessionsIncludeArchivedRef = useRef(taskSessionsIncludeArchived);
139
+ const taskSessionActionRef = useRef(taskSessionAction);
140
+ const collaborationTimelineRef = useRef(null);
141
+ const featureBoardSelectedIndexRef = useRef(0);
142
+ const featureCancelPromptRef = useRef(null);
143
+ const collaborationFeatureIndexRef = useRef(-1);
144
+ const collaborationSelectedEventIdRef = useRef(null);
145
+ const collaborationDetailOpenRef = useRef(false);
146
+ const collaborationUnresolvedOnlyRef = useRef(false);
147
+ const collaborationMaxScrollOffsetRef = useRef(0);
48
148
  const autoSelectedFailedWorkerRef = useRef(false);
49
149
  const userSelectedWorkerRef = useRef(false);
50
150
  const attachSelectedWorkerRef = useRef(attachSelectedWorker);
51
151
  const submitRef = useRef(submit);
152
+ const retryRef = useRef(retryActiveTask);
153
+ const newTaskRef = useRef(startNewTask);
154
+ const openWorkspacePickerRef = useRef(openWorkspacePicker);
155
+ const openRouterDiagnosticsRef = useRef(openRouterDiagnostics);
156
+ const openWorkerOverviewRef = useRef(openWorkerOverview);
157
+ const openTaskSessionsRef = useRef(openTaskSessions);
158
+ const openFeatureBoardRef = useRef(openFeatureBoard);
159
+ const confirmFeatureCancellationRef = useRef(confirmFeatureCancellation);
160
+ const openCollaborationTimelineRef = useRef(openCollaborationTimeline);
161
+ const refreshCollaborationTimelineRef = useRef(refreshCollaborationTimeline);
162
+ const activateSelectedTaskSessionRef = useRef(activateSelectedTaskSession);
163
+ const refreshTaskSessionsRef = useRef(refreshTaskSessions);
164
+ const renameSelectedTaskSessionRef = useRef(renameSelectedTaskSession);
165
+ const archiveSelectedTaskSessionRef = useRef(archiveSelectedTaskSession);
166
+ const deleteSelectedTaskSessionRef = useRef(deleteSelectedTaskSession);
167
+ const exportSelectedTaskSessionRef = useRef(exportSelectedTaskSession);
168
+ const workspaceReturnViewRef = useRef("chat");
169
+ const routerReturnViewRef = useRef("chat");
170
+ const workerOverviewReturnViewRef = useRef("chat");
171
+ const taskSessionsReturnViewRef = useRef("chat");
172
+ const collaborationReturnViewRef = useRef("workers");
173
+ const collaborationLoadSequenceRef = useRef(0);
174
+ const routerLoadSequenceRef = useRef(0);
175
+ const taskSessionsLoadSequenceRef = useRef(0);
52
176
  const exitRef = useRef(exit);
53
177
  const rawInputDecoderRef = useRef(createRawInputDecoder());
54
- const contentHeight = appContentHeight(process.stdout.rows || 30, Boolean(attachError));
178
+ const chatPasteDecoderRef = useRef(createChatPasteDecoder());
179
+ const chatDraftHistoryRef = useRef({
180
+ offset: 0,
181
+ draft: { value: "", cursor: 0 }
182
+ });
183
+ const contentHeight = appContentHeight(terminalSize.rows, Boolean(attachError), config.ui.showStatusBar);
55
184
  const outputHeight = Math.max(1, contentHeight);
56
- const terminalWidth = process.stdout.columns || 120;
57
- const selectedWorkerStatus = formatSelectedWorkerStatus(status, selectedWorkerIndex);
58
- const visibleWorkerStatus = view === "chat" ? "" : selectedWorkerStatus;
185
+ const terminalWidth = terminalSize.columns;
186
+ const workerActivityPolicies = useMemo(() => ({
187
+ codex: {
188
+ timeoutMs: config.workers.codex.timeoutMs,
189
+ idleTimeoutMs: config.workers.codex.idleTimeoutMs,
190
+ firstOutputTimeoutMs: config.workers.codex.firstOutputTimeoutMs
191
+ },
192
+ claude: {
193
+ timeoutMs: config.workers.claude.timeoutMs,
194
+ idleTimeoutMs: config.workers.claude.idleTimeoutMs,
195
+ firstOutputTimeoutMs: config.workers.claude.firstOutputTimeoutMs
196
+ },
197
+ mock: {
198
+ timeoutMs: config.workers.mock.timeoutMs,
199
+ idleTimeoutMs: config.workers.mock.idleTimeoutMs,
200
+ firstOutputTimeoutMs: config.workers.mock.firstOutputTimeoutMs
201
+ }
202
+ }), [config.workers.claude, config.workers.codex, config.workers.mock]);
203
+ const selectedBoardFeature = collaborationTimeline?.features[featureBoardSelectedIndex];
204
+ const featureCanCancel = busy && (selectedBoardFeature?.state === "actor_running"
205
+ || selectedBoardFeature?.state === "critic_running");
206
+ const visibleStatus = statusLineWithWorkerRefs(status, workers);
207
+ const selectedWorkerStatus = formatSelectedWorkerStatus(visibleStatus, selectedWorkerIndex);
208
+ const visibleWorkerStatus = view === "chat" || view === "router" || view === "sessions" || view === "features" || view === "collaboration"
209
+ ? ""
210
+ : selectedWorkerStatus;
211
+ const visibleRouteStatus = routePending
212
+ ? formatRoutePendingStatus(routePending, routeElapsedMs)
213
+ : formatRouteStatus(lastRoute);
214
+ const visibleTaskStatus = routePending && !activeTaskId ? "" : formatStatusLine(visibleStatus);
215
+ const workerRefreshKey = workers.map((worker) => `${worker.id}\u0000${worker.statusPath}`).join("\u0001");
216
+ const taskResultMessageIndex = useMemo(() => activeTaskId ? latestTaskResultMessageIndex(messages, activeTaskId) : -1, [activeTaskId, messages]);
217
+ const visibleChatMessages = useMemo(() => routeAnnouncement ? [...messages, routeAnnouncement] : messages, [messages, routeAnnouncement]);
218
+ const hasTaskResult = taskResultMessageIndex >= 0;
59
219
  useEffect(() => {
60
220
  inputRef.current = input;
61
221
  }, [input]);
222
+ useEffect(() => {
223
+ const updateTerminalSize = () => {
224
+ const next = readTerminalSize();
225
+ setTerminalSize((current) => (current.columns === next.columns && current.rows === next.rows ? current : next));
226
+ };
227
+ process.stdout.on("resize", updateTerminalSize);
228
+ updateTerminalSize();
229
+ return () => {
230
+ process.stdout.off("resize", updateTerminalSize);
231
+ };
232
+ }, []);
233
+ useEffect(() => {
234
+ inputCursorRef.current = inputCursor;
235
+ }, [inputCursor]);
236
+ useEffect(() => {
237
+ messagesRef.current = messages;
238
+ }, [messages]);
239
+ useEffect(() => {
240
+ activeTaskIdRef.current = activeTaskId;
241
+ }, [activeTaskId]);
62
242
  useEffect(() => {
63
243
  viewRef.current = view;
64
244
  }, [view]);
65
245
  useEffect(() => {
66
246
  busyRef.current = busy;
67
247
  }, [busy]);
248
+ useEffect(() => {
249
+ taskResultExpandedRef.current = taskResultExpanded;
250
+ }, [taskResultExpanded]);
68
251
  useEffect(() => {
69
252
  exitRef.current = exit;
70
253
  }, [exit]);
@@ -75,38 +258,167 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, prepareNa
75
258
  selectedWorkerIndexRef.current = selectedWorkerIndex;
76
259
  }, [selectedWorkerIndex]);
77
260
  useEffect(() => {
78
- workerMaxScrollOffsetRef.current = workerMaxScrollOffset;
79
- }, [workerMaxScrollOffset]);
261
+ chatScrollOffsetRef.current = chatScrollOffset;
262
+ }, [chatScrollOffset]);
263
+ useEffect(() => {
264
+ chatMaxScrollOffsetRef.current = chatMaxScrollOffset;
265
+ }, [chatMaxScrollOffset]);
266
+ useEffect(() => {
267
+ routerMaxScrollOffsetRef.current = routerMaxScrollOffset;
268
+ }, [routerMaxScrollOffset]);
269
+ useEffect(() => {
270
+ taskSessionsRef.current = taskSessions;
271
+ }, [taskSessions]);
272
+ useEffect(() => {
273
+ selectedTaskSessionIndexRef.current = selectedTaskSessionIndex;
274
+ }, [selectedTaskSessionIndex]);
275
+ useEffect(() => {
276
+ taskSessionsLoadingRef.current = taskSessionsLoading;
277
+ }, [taskSessionsLoading]);
278
+ useEffect(() => {
279
+ taskSessionsIncludeArchivedRef.current = taskSessionsIncludeArchived;
280
+ }, [taskSessionsIncludeArchived]);
281
+ useEffect(() => {
282
+ taskSessionActionRef.current = taskSessionAction;
283
+ }, [taskSessionAction]);
284
+ useEffect(() => {
285
+ featureBoardSelectedIndexRef.current = featureBoardSelectedIndex;
286
+ }, [featureBoardSelectedIndex]);
287
+ useEffect(() => {
288
+ if (!featureCancelPrompt) {
289
+ return;
290
+ }
291
+ const selected = collaborationTimeline?.features[featureBoardSelectedIndex];
292
+ const stillActive = busy
293
+ && selected?.id === featureCancelPrompt.featureId
294
+ && (selected.state === "actor_running" || selected.state === "critic_running");
295
+ if (!stillActive) {
296
+ updateFeatureCancelPrompt(null);
297
+ setFeatureBoardNotice(null);
298
+ }
299
+ }, [busy, collaborationTimeline, featureBoardSelectedIndex, featureCancelPrompt]);
300
+ useEffect(() => {
301
+ collaborationSelectedEventIdRef.current = collaborationSelectedEventId;
302
+ }, [collaborationSelectedEventId]);
303
+ useEffect(() => {
304
+ collaborationDetailOpenRef.current = collaborationDetailOpen;
305
+ }, [collaborationDetailOpen]);
306
+ useEffect(() => {
307
+ collaborationUnresolvedOnlyRef.current = collaborationUnresolvedOnly;
308
+ }, [collaborationUnresolvedOnly]);
309
+ useEffect(() => {
310
+ chatScrollOffsetRef.current = 0;
311
+ setChatScrollOffset(0);
312
+ }, [messages.length]);
313
+ useEffect(() => {
314
+ if (!routePending || routePending.mode !== "auto") {
315
+ setRouteElapsedMs(0);
316
+ return;
317
+ }
318
+ const updateElapsed = () => {
319
+ setRouteElapsedMs(Math.min(routePending.timeoutMs, Date.now() - routePending.startedAtMs));
320
+ };
321
+ updateElapsed();
322
+ const interval = setInterval(updateElapsed, 250);
323
+ return () => clearInterval(interval);
324
+ }, [routePending]);
80
325
  useEffect(() => {
81
326
  attachSelectedWorkerRef.current = attachSelectedWorker;
82
327
  submitRef.current = submit;
328
+ retryRef.current = retryActiveTask;
329
+ newTaskRef.current = startNewTask;
330
+ openWorkspacePickerRef.current = openWorkspacePicker;
331
+ openRouterDiagnosticsRef.current = openRouterDiagnostics;
332
+ openWorkerOverviewRef.current = openWorkerOverview;
333
+ openTaskSessionsRef.current = openTaskSessions;
334
+ openFeatureBoardRef.current = openFeatureBoard;
335
+ confirmFeatureCancellationRef.current = confirmFeatureCancellation;
336
+ openCollaborationTimelineRef.current = openCollaborationTimeline;
337
+ refreshCollaborationTimelineRef.current = refreshCollaborationTimeline;
338
+ activateSelectedTaskSessionRef.current = activateSelectedTaskSession;
339
+ refreshTaskSessionsRef.current = refreshTaskSessions;
340
+ renameSelectedTaskSessionRef.current = renameSelectedTaskSession;
341
+ archiveSelectedTaskSessionRef.current = archiveSelectedTaskSession;
342
+ deleteSelectedTaskSessionRef.current = deleteSelectedTaskSession;
343
+ exportSelectedTaskSessionRef.current = exportSelectedTaskSession;
83
344
  });
345
+ useEffect(() => {
346
+ if ((view !== "collaboration" && view !== "features") || !activeTaskId || !loadCollaborationTimeline) {
347
+ return;
348
+ }
349
+ const interval = setInterval(() => {
350
+ void refreshCollaborationTimelineRef.current(false);
351
+ }, 1500);
352
+ return () => clearInterval(interval);
353
+ }, [activeTaskId, loadCollaborationTimeline, view]);
354
+ useEffect(() => {
355
+ if (view !== "workers") {
356
+ return;
357
+ }
358
+ const tick = () => setWorkerClockMs(Date.now());
359
+ tick();
360
+ const interval = setInterval(tick, 1000);
361
+ return () => clearInterval(interval);
362
+ }, [view]);
84
363
  useEffect(() => {
85
364
  nativeAttachRef.current = nativeAttach;
86
365
  }, [nativeAttach]);
366
+ useEffect(() => {
367
+ const screen = nativeAttach?.screen;
368
+ const nativeProcess = nativeAttach?.process;
369
+ if (view !== "native" || !screen || !nativeProcess) {
370
+ return;
371
+ }
372
+ const resizeNativeAttach = () => {
373
+ const cols = nativeAttachTerminalColumns(process.stdout.columns || 120);
374
+ const rows = nativeAttachTerminalRows(process.stdout.rows || 30, Boolean(attachError), config.ui.showStatusBar);
375
+ screen.resize(cols, rows);
376
+ nativeProcess.resize(cols, rows);
377
+ setNativeAttach((current) => current && current.screen === screen
378
+ ? {
379
+ ...current,
380
+ launch: { ...current.launch, cols, rows },
381
+ snapshot: screen.snapshot()
382
+ }
383
+ : current);
384
+ };
385
+ process.stdout.on("resize", resizeNativeAttach);
386
+ resizeNativeAttach();
387
+ return () => {
388
+ process.stdout.off("resize", resizeNativeAttach);
389
+ };
390
+ }, [attachError, config.ui.showStatusBar, nativeAttach?.process, nativeAttach?.screen, view]);
87
391
  useEffect(() => {
88
392
  nativeInputRef.current = nativeInput;
89
393
  }, [nativeInput]);
90
394
  useEffect(() => {
91
- if (!initialTaskId) {
395
+ if (!initialTaskId || activeTaskId !== initialTaskId || initialWorkers !== undefined) {
92
396
  return;
93
397
  }
94
398
  const taskId = initialTaskId;
95
399
  let active = true;
96
400
  async function loadInitialWorkers() {
97
401
  try {
98
- const restored = await orchestrator.listTaskWorkers(taskId);
99
- if (!active || restored.length === 0) {
402
+ const [restored, retryable] = await Promise.all([
403
+ orchestrator.listTaskWorkers(taskId),
404
+ orchestrator.canRetryTask(taskId)
405
+ ]);
406
+ if (!active || activeTaskIdRef.current !== taskId) {
407
+ return;
408
+ }
409
+ setCanRetryTask(retryable);
410
+ if (restored.length === 0) {
100
411
  return;
101
412
  }
102
413
  setWorkers(restored);
414
+ setStatus(restoredWorkerStatusLine(taskId, restored));
103
415
  selectedWorkerIndexRef.current = 0;
104
416
  autoSelectedFailedWorkerRef.current = false;
105
417
  userSelectedWorkerRef.current = false;
106
418
  setSelectedWorkerIndex(0);
107
419
  }
108
420
  catch (error) {
109
- if (active) {
421
+ if (active && activeTaskIdRef.current === taskId) {
110
422
  setAttachError(error instanceof Error ? error.message : String(error));
111
423
  }
112
424
  }
@@ -115,60 +427,126 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, prepareNa
115
427
  return () => {
116
428
  active = false;
117
429
  };
118
- }, [initialTaskId, orchestrator]);
430
+ }, [activeTaskId, initialTaskId, initialWorkers, orchestrator]);
119
431
  useEffect(() => {
120
432
  if (workers.length === 0 || !status) {
121
433
  return;
122
434
  }
123
435
  let active = true;
436
+ let refreshInFlight = false;
124
437
  async function refreshWorkerStatuses() {
125
- const updates = await Promise.all(workers.map(async (worker) => {
126
- try {
127
- const workerStatus = await readJson(worker.statusPath, WorkerStatusSchema);
128
- return {
129
- label: worker.label,
130
- role: worker.role,
131
- state: workerStatus.state,
132
- status: formatWorkerRuntimeStatus(workerStatus)
133
- };
134
- }
135
- catch {
136
- return null;
137
- }
138
- }));
139
- if (!active) {
438
+ if (refreshInFlight) {
140
439
  return;
141
440
  }
142
- setStatus((current) => {
143
- if (!current) {
144
- return current;
441
+ refreshInFlight = true;
442
+ try {
443
+ const updates = await Promise.all(workers.map(async (worker) => {
444
+ try {
445
+ const workerStatus = await readJson(worker.statusPath, WorkerStatusSchema);
446
+ return {
447
+ id: worker.id,
448
+ label: worker.label,
449
+ role: worker.role,
450
+ engine: worker.engine,
451
+ state: workerStatus.state,
452
+ status: formatWorkerRuntimeStatus(workerStatus),
453
+ runtimeStatus: workerStatus
454
+ };
455
+ }
456
+ catch {
457
+ return null;
458
+ }
459
+ }));
460
+ if (!active) {
461
+ return;
145
462
  }
146
- const next = { ...current };
147
- next.workers = updates
463
+ const currentWorkersById = new Map(workersRef.current.map((worker) => [worker.id, worker]));
464
+ const effectiveUpdates = updates.map((update) => {
465
+ if (!update) {
466
+ return null;
467
+ }
468
+ const currentWorker = currentWorkersById.get(update.id);
469
+ const currentRuntime = currentWorker?.runtimeStatus;
470
+ if (!currentWorker || !currentRuntime || !newerTerminalRuntime(currentRuntime, update.runtimeStatus)) {
471
+ return update;
472
+ }
473
+ return {
474
+ id: currentWorker.id,
475
+ label: currentWorker.label,
476
+ role: currentWorker.role,
477
+ engine: currentWorker.engine,
478
+ state: currentRuntime.state,
479
+ status: formatWorkerRuntimeStatus(currentRuntime),
480
+ runtimeStatus: currentRuntime
481
+ };
482
+ });
483
+ setStatus((current) => {
484
+ if (!current) {
485
+ return current;
486
+ }
487
+ const next = { ...current };
488
+ next.workers = effectiveUpdates
489
+ .filter((update) => update !== null)
490
+ .map((update) => ({
491
+ label: update.label,
492
+ status: update.status
493
+ }));
494
+ for (const update of effectiveUpdates) {
495
+ if (update) {
496
+ if (update.role === "main"
497
+ && terminalMainStatus(current.main)
498
+ && activeWorkerRuntime(update.runtimeStatus)) {
499
+ continue;
500
+ }
501
+ next[update.role] = update.status;
502
+ if (update.role === "main") {
503
+ next.mainEngine = update.engine;
504
+ next.mainProgress = mainWorkerProgress(update.runtimeStatus, workerActivityPolicies[update.engine]);
505
+ }
506
+ }
507
+ }
508
+ return next;
509
+ });
510
+ const runtimeStatusById = new Map(effectiveUpdates
148
511
  .filter((update) => update !== null)
149
- .map((update) => ({
150
- label: update.label,
151
- status: update.status
152
- }));
153
- for (const update of updates) {
154
- if (update) {
155
- next[update.role] = update.status;
512
+ .map((update) => [update.id, update.runtimeStatus]));
513
+ setWorkers((current) => {
514
+ let changed = false;
515
+ const next = current.map((worker) => {
516
+ const runtimeStatus = runtimeStatusById.get(worker.id);
517
+ if (!runtimeStatus
518
+ || sameWorkerRuntimeStatus(worker.runtimeStatus, runtimeStatus)
519
+ || newerTerminalRuntime(worker.runtimeStatus, runtimeStatus)) {
520
+ return worker;
521
+ }
522
+ changed = true;
523
+ return { ...worker, runtimeStatus };
524
+ });
525
+ if (changed) {
526
+ workersRef.current = next;
527
+ }
528
+ return changed ? next : current;
529
+ });
530
+ const failedWorkerIndex = effectiveUpdates.findIndex((update) => update?.state === "failed");
531
+ if (config.ui.autoOpenFailedWorker &&
532
+ failedWorkerIndex >= 0 &&
533
+ !autoSelectedFailedWorkerRef.current &&
534
+ !userSelectedWorkerRef.current) {
535
+ autoSelectedFailedWorkerRef.current = true;
536
+ selectedWorkerIndexRef.current = failedWorkerIndex;
537
+ setSelectedWorkerIndex(failedWorkerIndex);
538
+ setWorkerScrollOffset(0);
539
+ if (viewRef.current !== "native" &&
540
+ viewRef.current !== "router" &&
541
+ viewRef.current !== "sessions" &&
542
+ viewRef.current !== "workers" &&
543
+ viewRef.current !== "workspace") {
544
+ setView("worker");
156
545
  }
157
546
  }
158
- return next;
159
- });
160
- const failedWorkerIndex = updates.findIndex((update) => update?.state === "failed");
161
- if (config.ui.autoOpenFailedWorker &&
162
- failedWorkerIndex >= 0 &&
163
- !autoSelectedFailedWorkerRef.current &&
164
- !userSelectedWorkerRef.current) {
165
- autoSelectedFailedWorkerRef.current = true;
166
- selectedWorkerIndexRef.current = failedWorkerIndex;
167
- setSelectedWorkerIndex(failedWorkerIndex);
168
- setWorkerScrollOffset(0);
169
- if (viewRef.current !== "native") {
170
- setView("worker");
171
- }
547
+ }
548
+ finally {
549
+ refreshInFlight = false;
172
550
  }
173
551
  }
174
552
  void refreshWorkerStatuses();
@@ -179,45 +557,681 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, prepareNa
179
557
  active = false;
180
558
  clearInterval(interval);
181
559
  };
182
- }, [config.ui.autoOpenFailedWorker, status?.taskId, workers]);
560
+ }, [config.ui.autoOpenFailedWorker, status?.taskId, workerActivityPolicies, workerRefreshKey]);
183
561
  useEffect(() => {
184
562
  setRawMode(true);
185
- process.stdout.write("\x1b[?1000h\x1b[?1002h\x1b[?1006h");
563
+ process.stdout.write("\x1b[?2004h\x1b[?1000h\x1b[?1002h\x1b[?1006h");
564
+ const commitChatInputUpdate = (update, previousValue, previousCursor) => {
565
+ if (update.exit) {
566
+ activeRunControllerRef.current?.abort();
567
+ exitRef.current();
568
+ return false;
569
+ }
570
+ if (busyRef.current) {
571
+ return false;
572
+ }
573
+ if (taskResultExpandedRef.current
574
+ && update.value !== previousValue
575
+ && update.value.length > 0) {
576
+ taskResultExpandedRef.current = false;
577
+ setTaskResultExpanded(false);
578
+ chatScrollOffsetRef.current = 0;
579
+ setChatScrollOffset(0);
580
+ }
581
+ inputRef.current = update.value;
582
+ inputCursorRef.current = update.cursor;
583
+ setInput(update.value);
584
+ setInputCursor(update.cursor);
585
+ if (update.value !== previousValue || update.cursor !== previousCursor || update.submit !== null) {
586
+ chatDraftHistoryRef.current = {
587
+ offset: 0,
588
+ draft: { value: update.value, cursor: update.cursor }
589
+ };
590
+ }
591
+ if (update.submit !== null) {
592
+ void submitRef.current(update.submit);
593
+ return false;
594
+ }
595
+ return true;
596
+ };
597
+ const moveSelectedWorker = (delta, wrap = false) => {
598
+ const nextIndex = moveWorkerSelection(selectedWorkerIndexRef.current, delta, workersRef.current.length, wrap);
599
+ selectedWorkerIndexRef.current = nextIndex;
600
+ userSelectedWorkerRef.current = true;
601
+ setAttachError(null);
602
+ setSelectedWorkerIndex(nextIndex);
603
+ setWorkerScrollOffset(0);
604
+ };
605
+ const moveSelectedTaskSession = (delta, wrap = false) => {
606
+ const nextIndex = moveTaskSessionSelection(selectedTaskSessionIndexRef.current, delta, taskSessionsRef.current.length, wrap);
607
+ selectedTaskSessionIndexRef.current = nextIndex;
608
+ setTaskSessionsError(null);
609
+ setSelectedTaskSessionIndex(nextIndex);
610
+ };
611
+ const moveSelectedFeature = (delta, wrap = false) => {
612
+ const nextIndex = moveFeatureBoardSelection(featureBoardSelectedIndexRef.current, delta, collaborationTimelineRef.current?.features.length ?? 0, wrap);
613
+ featureBoardSelectedIndexRef.current = nextIndex;
614
+ setCollaborationError(null);
615
+ updateFeatureCancelPrompt(null);
616
+ setFeatureBoardNotice(null);
617
+ setFeatureBoardSelectedIndex(nextIndex);
618
+ };
619
+ const commitWorkerSearch = (next) => {
620
+ workerSearchRef.current = next;
621
+ setWorkerSearch(next);
622
+ };
623
+ const closeWorkerSearch = () => {
624
+ commitWorkerSearch({ ...workerSearchRef.current, open: false });
625
+ };
626
+ const cycleWorkerSearch = (delta) => {
627
+ const offsets = workerNavigationTargetsRef.current.searchOffsets;
628
+ if (offsets.length === 0) {
629
+ return;
630
+ }
631
+ const current = workerSearchRef.current;
632
+ const nextIndex = ((current.matchIndex + delta) % offsets.length + offsets.length) % offsets.length;
633
+ commitWorkerSearch({ ...current, matchIndex: nextIndex });
634
+ setWorkerScrollOffset(offsets[nextIndex] ?? 0);
635
+ };
636
+ const jumpWorkerLog = (kind) => {
637
+ const offsets = kind === "error"
638
+ ? workerNavigationTargetsRef.current.errorOffsets
639
+ : workerNavigationTargetsRef.current.diffOffsets;
640
+ if (offsets.length === 0) {
641
+ return;
642
+ }
643
+ const nextIndex = (workerJumpIndexRef.current[kind] + 1) % offsets.length;
644
+ workerJumpIndexRef.current[kind] = nextIndex;
645
+ setWorkerScrollOffset(offsets[nextIndex] ?? 0);
646
+ };
186
647
  const handleRawInput = (data) => {
187
648
  const chunk = rawInputDecoderRef.current.write(Buffer.isBuffer(data) ? data : String(data ?? ""));
188
649
  if (!chunk) {
189
650
  return;
190
651
  }
191
652
  const currentView = viewRef.current;
192
- if (currentView === "worker") {
653
+ if (currentView === "chat" && routeFallbackPromptRef.current) {
654
+ const fallbackChunks = tokenizeRawInput(chunk);
655
+ if (fallbackChunks.some((fallbackChunk) => isExitShortcut(fallbackChunk, {}))) {
656
+ activeRunControllerRef.current?.abort();
657
+ exitRef.current();
658
+ return;
659
+ }
660
+ for (const fallbackChunk of fallbackChunks) {
661
+ if (fallbackChunk === "\x1b") {
662
+ settleRouteFallbackChoice("cancel");
663
+ return;
664
+ }
665
+ if (fallbackChunk === "1" || fallbackChunk === "m" || fallbackChunk === "M") {
666
+ settleRouteFallbackChoice("main");
667
+ return;
668
+ }
669
+ if (fallbackChunk === "2" || fallbackChunk === "p" || fallbackChunk === "P") {
670
+ settleRouteFallbackChoice("parallel");
671
+ return;
672
+ }
673
+ if (fallbackChunk === "r" || fallbackChunk === "R") {
674
+ settleRouteFallbackChoice("retry");
675
+ return;
676
+ }
677
+ }
678
+ return;
679
+ }
680
+ if (currentView === "workspace") {
681
+ return;
682
+ }
683
+ if (currentView === "sessions") {
193
684
  if (isExitShortcut(chunk, {})) {
685
+ activeRunControllerRef.current?.abort();
194
686
  exitRef.current();
195
687
  return;
196
688
  }
197
- if (chunk === "\x1b") {
198
- userSelectedWorkerRef.current = true;
199
- setView("chat");
689
+ const sessionAction = taskSessionActionRef.current;
690
+ if (sessionAction) {
691
+ if (chunk === "\x1b") {
692
+ updateTaskSessionAction(null);
693
+ setTaskSessionsError(null);
694
+ return;
695
+ }
696
+ if (taskSessionsLoadingRef.current) {
697
+ return;
698
+ }
699
+ if (sessionAction.type === "rename") {
700
+ const update = applyChatInputChunk(sessionAction.value, chunk, sessionAction.cursor);
701
+ if (update.submit !== null) {
702
+ void renameSelectedTaskSessionRef.current({
703
+ ...sessionAction,
704
+ value: update.submit,
705
+ cursor: Array.from(update.submit).length
706
+ });
707
+ }
708
+ else {
709
+ updateTaskSessionAction({
710
+ ...sessionAction,
711
+ value: update.value,
712
+ cursor: update.cursor
713
+ });
714
+ }
715
+ return;
716
+ }
717
+ if (chunk === "d" || chunk === "D") {
718
+ void deleteSelectedTaskSessionRef.current(sessionAction);
719
+ }
720
+ return;
721
+ }
722
+ if (isTaskSessionsShortcut(chunk, {}) || chunk === "\x1b") {
723
+ setTaskSessionsError(null);
724
+ setTaskSessionsNotice(null);
725
+ viewRef.current = taskSessionsReturnViewRef.current;
726
+ setView(taskSessionsReturnViewRef.current);
727
+ return;
728
+ }
729
+ if (taskSessionsLoadingRef.current) {
730
+ return;
731
+ }
732
+ if (isRouterDiagnosticsShortcut(chunk, {})) {
733
+ void openRouterDiagnosticsRef.current();
734
+ return;
735
+ }
736
+ if (isWorkspaceShortcut(chunk, {}) && !busyRef.current) {
737
+ openWorkspacePickerRef.current();
738
+ return;
739
+ }
740
+ if (isNewTaskShortcut(chunk, {}) && !busyRef.current) {
741
+ if (activeTaskIdRef.current) {
742
+ void newTaskRef.current();
743
+ }
744
+ else {
745
+ viewRef.current = "chat";
746
+ setView("chat");
747
+ }
748
+ return;
749
+ }
750
+ const selectedSession = taskSessionsRef.current[selectedTaskSessionIndexRef.current];
751
+ if (chunk === "r" || chunk === "R") {
752
+ if (selectedSession) {
753
+ updateTaskSessionAction({
754
+ type: "rename",
755
+ taskId: selectedSession.id,
756
+ title: selectedSession.title,
757
+ value: selectedSession.title,
758
+ cursor: Array.from(selectedSession.title).length
759
+ });
760
+ setTaskSessionsError(null);
761
+ setTaskSessionsNotice(null);
762
+ }
763
+ return;
764
+ }
765
+ if (chunk === "a" || chunk === "A") {
766
+ void archiveSelectedTaskSessionRef.current();
767
+ return;
768
+ }
769
+ if (chunk === "d" || chunk === "D") {
770
+ if (selectedSession) {
771
+ if (selectedSession.id === activeTaskIdRef.current) {
772
+ setTaskSessionsError("Start a new task before deleting the active session");
773
+ }
774
+ else {
775
+ updateTaskSessionAction({
776
+ type: "delete",
777
+ taskId: selectedSession.id,
778
+ title: selectedSession.title
779
+ });
780
+ setTaskSessionsError(null);
781
+ setTaskSessionsNotice(null);
782
+ }
783
+ }
784
+ return;
785
+ }
786
+ if (chunk === "e" || chunk === "E") {
787
+ void exportSelectedTaskSessionRef.current();
788
+ return;
789
+ }
790
+ if (chunk === "h" || chunk === "H") {
791
+ const includeArchived = !taskSessionsIncludeArchivedRef.current;
792
+ taskSessionsIncludeArchivedRef.current = includeArchived;
793
+ setTaskSessionsIncludeArchived(includeArchived);
794
+ setTaskSessionsNotice(includeArchived ? "Archived sessions shown" : "Archived sessions hidden");
795
+ void refreshTaskSessionsRef.current(selectedSession?.id ?? null);
796
+ return;
797
+ }
798
+ if (chunk === "\r" || chunk === "\n") {
799
+ void activateSelectedTaskSessionRef.current();
800
+ return;
801
+ }
802
+ if (chunk === "\t") {
803
+ moveSelectedTaskSession(1, true);
804
+ return;
805
+ }
806
+ const selectionDelta = -(rawHistoryDelta(chunk)
807
+ + rawPageScrollDelta(chunk, Math.max(1, outputHeight - 2))
808
+ + mouseScrollDelta(chunk, 1));
809
+ if (selectionDelta !== 0) {
810
+ moveSelectedTaskSession(selectionDelta);
811
+ }
812
+ return;
813
+ }
814
+ if (currentView === "features") {
815
+ const featureChunks = tokenizeRawInput(chunk);
816
+ if (featureChunks.some((featureChunk) => isExitShortcut(featureChunk, {}))) {
817
+ activeRunControllerRef.current?.abort();
818
+ exitRef.current();
819
+ return;
820
+ }
821
+ const pendingCancel = featureCancelPromptRef.current;
822
+ if (pendingCancel) {
823
+ for (const featureChunk of featureChunks) {
824
+ if (featureChunk === "\x1b") {
825
+ updateFeatureCancelPrompt(null);
826
+ setFeatureBoardNotice(null);
827
+ return;
828
+ }
829
+ if (featureChunk === "x" || featureChunk === "X") {
830
+ void confirmFeatureCancellationRef.current(pendingCancel);
831
+ return;
832
+ }
833
+ }
834
+ return;
835
+ }
836
+ for (const featureChunk of featureChunks) {
837
+ if (featureChunk === "\x1b" || isWorkerOverviewShortcut(featureChunk, {})) {
838
+ setCollaborationError(null);
839
+ setFeatureBoardNotice(null);
840
+ viewRef.current = "workers";
841
+ setView("workers");
842
+ return;
843
+ }
844
+ if (featureChunk === "\u0012" && !busyRef.current) {
845
+ setFeatureBoardNotice(null);
846
+ void retryRef.current();
847
+ return;
848
+ }
849
+ if (featureChunk === "x" || featureChunk === "X") {
850
+ const taskId = activeTaskIdRef.current;
851
+ const feature = collaborationTimelineRef.current?.features[featureBoardSelectedIndexRef.current];
852
+ const cancellable = busyRef.current
853
+ && (feature?.state === "actor_running" || feature?.state === "critic_running");
854
+ if (!taskId || !feature || !cancellable) {
855
+ setFeatureBoardNotice("Selected feature has no active Actor/Critic to cancel.");
856
+ return;
857
+ }
858
+ const prompt = { taskId, featureId: feature.id, title: feature.title };
859
+ updateFeatureCancelPrompt(prompt);
860
+ setFeatureBoardNotice(`Cancel ${feature.title}? Active peers will finish; integration stays blocked.`);
861
+ return;
862
+ }
863
+ if (featureChunk === "r" || featureChunk === "R") {
864
+ setFeatureBoardNotice(null);
865
+ void refreshCollaborationTimelineRef.current(false);
866
+ continue;
867
+ }
868
+ if (featureChunk === "\r" || featureChunk === "\n" || featureChunk === "c" || featureChunk === "C") {
869
+ if ((collaborationTimelineRef.current?.features.length ?? 0) > 0) {
870
+ void openCollaborationTimelineRef.current(featureBoardSelectedIndexRef.current);
871
+ }
872
+ return;
873
+ }
874
+ if (featureChunk === "\t") {
875
+ moveSelectedFeature(1, true);
876
+ continue;
877
+ }
878
+ const selectionDelta = -(rawHistoryDelta(featureChunk)
879
+ + rawPageScrollDelta(featureChunk, Math.max(1, outputHeight - 2))
880
+ + mouseScrollDelta(featureChunk, 1));
881
+ if (selectionDelta !== 0) {
882
+ moveSelectedFeature(selectionDelta);
883
+ }
884
+ }
885
+ return;
886
+ }
887
+ if (currentView === "collaboration") {
888
+ const timelineChunks = tokenizeRawInput(chunk);
889
+ if (timelineChunks.some((timelineChunk) => isExitShortcut(timelineChunk, {}))) {
890
+ activeRunControllerRef.current?.abort();
891
+ exitRef.current();
892
+ return;
893
+ }
894
+ for (const timelineChunk of timelineChunks) {
895
+ if (collaborationDetailOpenRef.current && (timelineChunk === "\x1b" || timelineChunk === "\r" || timelineChunk === "\n")) {
896
+ collaborationDetailOpenRef.current = false;
897
+ setCollaborationDetailOpen(false);
898
+ collaborationMaxScrollOffsetRef.current = 0;
899
+ setCollaborationMaxScrollOffset(0);
900
+ setCollaborationScrollOffset(0);
901
+ continue;
902
+ }
903
+ if (collaborationDetailOpenRef.current) {
904
+ const detailDelta = mouseScrollDelta(timelineChunk, 3)
905
+ + rawPageScrollDelta(timelineChunk, Math.max(1, outputHeight - 3));
906
+ if (detailDelta !== 0) {
907
+ setCollaborationScrollOffset((current) => (nextScrollOffset(current, -detailDelta, collaborationMaxScrollOffsetRef.current)));
908
+ }
909
+ continue;
910
+ }
911
+ if (timelineChunk === "\x1b" || isWorkerOverviewShortcut(timelineChunk, {})) {
912
+ setCollaborationError(null);
913
+ viewRef.current = collaborationReturnViewRef.current;
914
+ setView(collaborationReturnViewRef.current);
915
+ return;
916
+ }
917
+ const scopedEvents = collaborationTimelineRef.current
918
+ ? collaborationTimelineEvents(collaborationTimelineRef.current, collaborationFeatureIndexRef.current, collaborationUnresolvedOnlyRef.current)
919
+ : [];
920
+ if (timelineChunk === "\r" || timelineChunk === "\n") {
921
+ const selectedId = collaborationSelectedEventIdRef.current ?? scopedEvents.at(-1)?.id ?? null;
922
+ if (selectedId) {
923
+ collaborationSelectedEventIdRef.current = selectedId;
924
+ setCollaborationSelectedEventId(selectedId);
925
+ collaborationDetailOpenRef.current = true;
926
+ setCollaborationDetailOpen(true);
927
+ collaborationMaxScrollOffsetRef.current = 0;
928
+ setCollaborationMaxScrollOffset(0);
929
+ setCollaborationScrollOffset(0);
930
+ }
931
+ continue;
932
+ }
933
+ if (timelineChunk === "\t") {
934
+ const nextIndex = nextCollaborationFeatureIndex(collaborationFeatureIndexRef.current, 1, collaborationTimelineRef.current?.features.length ?? 0);
935
+ collaborationFeatureIndexRef.current = nextIndex;
936
+ setCollaborationFeatureIndex(nextIndex);
937
+ collaborationSelectedEventIdRef.current = null;
938
+ setCollaborationSelectedEventId(null);
939
+ collaborationDetailOpenRef.current = false;
940
+ setCollaborationDetailOpen(false);
941
+ collaborationMaxScrollOffsetRef.current = 0;
942
+ setCollaborationMaxScrollOffset(0);
943
+ setCollaborationScrollOffset(0);
944
+ continue;
945
+ }
946
+ if (timelineChunk === "u" || timelineChunk === "U") {
947
+ const unresolved = !collaborationUnresolvedOnlyRef.current;
948
+ collaborationUnresolvedOnlyRef.current = unresolved;
949
+ setCollaborationUnresolvedOnly(unresolved);
950
+ collaborationSelectedEventIdRef.current = null;
951
+ setCollaborationSelectedEventId(null);
952
+ collaborationMaxScrollOffsetRef.current = 0;
953
+ setCollaborationMaxScrollOffset(0);
954
+ setCollaborationScrollOffset(0);
955
+ continue;
956
+ }
957
+ if (timelineChunk === "r" || timelineChunk === "R") {
958
+ void refreshCollaborationTimelineRef.current(false);
959
+ continue;
960
+ }
961
+ const eventDelta = rawHistoryDelta(timelineChunk);
962
+ if (eventDelta !== 0) {
963
+ const nextId = moveCollaborationEventSelection(scopedEvents, collaborationSelectedEventIdRef.current, -eventDelta);
964
+ collaborationSelectedEventIdRef.current = nextId;
965
+ setCollaborationSelectedEventId(nextId);
966
+ const lineHeight = (process.stdout.columns || 120) < 28 ? 1 : 2;
967
+ setCollaborationScrollOffset((current) => (nextScrollOffset(current, eventDelta * lineHeight, collaborationMaxScrollOffsetRef.current)));
968
+ continue;
969
+ }
970
+ const timelineDelta = mouseScrollDelta(timelineChunk, 3)
971
+ + rawPageScrollDelta(timelineChunk, Math.max(1, outputHeight - 3));
972
+ if (timelineDelta !== 0) {
973
+ setCollaborationScrollOffset((current) => (nextScrollOffset(current, timelineDelta, collaborationMaxScrollOffsetRef.current)));
974
+ }
975
+ }
976
+ return;
977
+ }
978
+ if (currentView === "workers") {
979
+ if (isExitShortcut(chunk, {})) {
980
+ activeRunControllerRef.current?.abort();
981
+ exitRef.current();
982
+ return;
983
+ }
984
+ if (isWorkerOverviewShortcut(chunk, {}) || chunk === "\x1b") {
985
+ setAttachError(null);
986
+ viewRef.current = workerOverviewReturnViewRef.current;
987
+ setView(workerOverviewReturnViewRef.current);
988
+ return;
989
+ }
990
+ if (isRouterDiagnosticsShortcut(chunk, {})) {
991
+ void openRouterDiagnosticsRef.current();
992
+ return;
993
+ }
994
+ if (isTaskSessionsShortcut(chunk, {}) && !busyRef.current) {
995
+ void openTaskSessionsRef.current();
996
+ return;
997
+ }
998
+ if (isWorkspaceShortcut(chunk, {}) && !busyRef.current) {
999
+ openWorkspacePickerRef.current();
200
1000
  return;
201
1001
  }
202
- const delta = mouseScrollDelta(chunk, 3);
203
- if (delta !== 0) {
204
- setWorkerScrollOffset((current) => nextScrollOffset(current, delta, workerMaxScrollOffsetRef.current));
1002
+ if (isNewTaskShortcut(chunk, {}) && !busyRef.current) {
1003
+ void newTaskRef.current();
1004
+ return;
1005
+ }
1006
+ if (chunk === "f" || chunk === "F") {
1007
+ void openFeatureBoardRef.current();
1008
+ return;
1009
+ }
1010
+ if (chunk === "c" || chunk === "C") {
1011
+ void openCollaborationTimelineRef.current();
1012
+ return;
1013
+ }
1014
+ if (isAttachShortcut(chunk, {})) {
1015
+ const worker = workersRef.current[selectedWorkerIndexRef.current];
1016
+ if (worker) {
1017
+ void attachSelectedWorkerRef.current(worker);
1018
+ }
1019
+ return;
1020
+ }
1021
+ if (isLogsShortcut(chunk, {}) || chunk === "\r" || chunk === "\n") {
1022
+ if (workersRef.current.length > 0) {
1023
+ viewRef.current = "worker";
1024
+ setView("worker");
1025
+ setWorkerScrollOffset(0);
1026
+ }
1027
+ return;
1028
+ }
1029
+ if (chunk === "\t") {
1030
+ moveSelectedWorker(1, true);
1031
+ return;
1032
+ }
1033
+ const selectionDelta = -(rawHistoryDelta(chunk)
1034
+ + rawPageScrollDelta(chunk, Math.max(1, outputHeight - 2))
1035
+ + mouseScrollDelta(chunk, 1));
1036
+ if (selectionDelta !== 0) {
1037
+ moveSelectedWorker(selectionDelta);
1038
+ }
1039
+ return;
1040
+ }
1041
+ if (currentView === "router") {
1042
+ const routerChunks = tokenizeRawInput(chunk);
1043
+ if (routerChunks.some((routerChunk) => isExitShortcut(routerChunk, {}))) {
1044
+ activeRunControllerRef.current?.abort();
1045
+ exitRef.current();
1046
+ return;
1047
+ }
1048
+ for (const routerChunk of routerChunks) {
1049
+ if (isRouterDiagnosticsShortcut(routerChunk, {})) {
1050
+ void openRouterDiagnosticsRef.current();
1051
+ continue;
1052
+ }
1053
+ if (isTaskSessionsShortcut(routerChunk, {}) && !busyRef.current) {
1054
+ void openTaskSessionsRef.current();
1055
+ return;
1056
+ }
1057
+ if (routerChunk === "\t") {
1058
+ setRouterScope((current) => current === "all" ? "workspace" : "all");
1059
+ routerMaxScrollOffsetRef.current = 0;
1060
+ setRouterMaxScrollOffset(0);
1061
+ setRouterScrollOffset(0);
1062
+ continue;
1063
+ }
1064
+ if (routerChunk === "\x1b") {
1065
+ setAttachError(null);
1066
+ viewRef.current = routerReturnViewRef.current;
1067
+ setView(routerReturnViewRef.current);
1068
+ return;
1069
+ }
1070
+ const routeDelta = mouseScrollDelta(routerChunk, 3)
1071
+ + rawPageScrollDelta(routerChunk, Math.max(1, outputHeight - 1));
1072
+ if (routeDelta !== 0) {
1073
+ setRouterScrollOffset((current) => (nextScrollOffset(current, -routeDelta, routerMaxScrollOffsetRef.current)));
1074
+ }
1075
+ }
1076
+ return;
1077
+ }
1078
+ if (currentView === "worker") {
1079
+ for (const workerChunk of tokenizeRawInput(chunk)) {
1080
+ if (isExitShortcut(workerChunk, {})) {
1081
+ activeRunControllerRef.current?.abort();
1082
+ exitRef.current();
1083
+ return;
1084
+ }
1085
+ if (workerSearchRef.current.open) {
1086
+ if (isWorkerSearchShortcut(workerChunk, {}) || workerChunk === "\x1b") {
1087
+ closeWorkerSearch();
1088
+ continue;
1089
+ }
1090
+ if (workerChunk === "\r" || workerChunk === "\n") {
1091
+ cycleWorkerSearch(1);
1092
+ continue;
1093
+ }
1094
+ const searchHistoryDelta = rawHistoryDelta(workerChunk);
1095
+ if (searchHistoryDelta !== 0) {
1096
+ cycleWorkerSearch(-searchHistoryDelta);
1097
+ continue;
1098
+ }
1099
+ const current = workerSearchRef.current;
1100
+ const update = applyChatInputChunk(current.query, workerChunk, current.cursor);
1101
+ const query = update.submit ?? update.value;
1102
+ const cursor = update.submit === null ? update.cursor : Array.from(query).length;
1103
+ commitWorkerSearch({
1104
+ open: true,
1105
+ query,
1106
+ cursor,
1107
+ matchIndex: query === current.query ? current.matchIndex : 0
1108
+ });
1109
+ continue;
1110
+ }
1111
+ if (isWorkerSearchShortcut(workerChunk, {})) {
1112
+ commitWorkerSearch({ ...workerSearchRef.current, open: true });
1113
+ continue;
1114
+ }
1115
+ const jumpKind = workerLogJumpKind(workerChunk);
1116
+ if (jumpKind) {
1117
+ jumpWorkerLog(jumpKind);
1118
+ continue;
1119
+ }
1120
+ if (isNewTaskShortcut(workerChunk, {}) && !busyRef.current) {
1121
+ void newTaskRef.current();
1122
+ return;
1123
+ }
1124
+ if (isWorkspaceShortcut(workerChunk, {}) && !busyRef.current) {
1125
+ openWorkspacePickerRef.current();
1126
+ return;
1127
+ }
1128
+ if (isRouterDiagnosticsShortcut(workerChunk, {})) {
1129
+ void openRouterDiagnosticsRef.current();
1130
+ return;
1131
+ }
1132
+ if (isTaskSessionsShortcut(workerChunk, {}) && !busyRef.current) {
1133
+ void openTaskSessionsRef.current();
1134
+ return;
1135
+ }
1136
+ if (isWorkerOverviewShortcut(workerChunk, {})) {
1137
+ openWorkerOverviewRef.current();
1138
+ return;
1139
+ }
1140
+ if (workerChunk === "\x1b") {
1141
+ userSelectedWorkerRef.current = true;
1142
+ setAttachError(null);
1143
+ setView("chat");
1144
+ return;
1145
+ }
1146
+ const delta = mouseScrollDelta(workerChunk, 3);
1147
+ if (delta !== 0) {
1148
+ setWorkerScrollOffset((current) => nextScrollOffset(current, delta, null));
1149
+ }
205
1150
  }
206
1151
  return;
207
1152
  }
208
1153
  if (currentView === "chat") {
1154
+ if (isWorkspaceShortcut(chunk, {}) && !busyRef.current) {
1155
+ openWorkspacePickerRef.current();
1156
+ return;
1157
+ }
1158
+ if (isRouterDiagnosticsShortcut(chunk, {})) {
1159
+ void openRouterDiagnosticsRef.current();
1160
+ return;
1161
+ }
1162
+ if (isTaskSessionsShortcut(chunk, {}) && !busyRef.current) {
1163
+ void openTaskSessionsRef.current();
1164
+ return;
1165
+ }
1166
+ if (isWorkerOverviewShortcut(chunk, {})) {
1167
+ openWorkerOverviewRef.current();
1168
+ return;
1169
+ }
1170
+ if (isTaskResultShortcut(chunk, {})
1171
+ && activeTaskIdRef.current
1172
+ && latestTaskResultMessageIndex(messagesRef.current, activeTaskIdRef.current) >= 0) {
1173
+ const expanded = !taskResultExpandedRef.current;
1174
+ taskResultExpandedRef.current = expanded;
1175
+ setTaskResultExpanded(expanded);
1176
+ chatScrollOffsetRef.current = 0;
1177
+ setChatScrollOffset(0);
1178
+ return;
1179
+ }
1180
+ const paste = chatPasteDecoderRef.current.write(chunk);
1181
+ if (paste.intercepted) {
1182
+ if (busyRef.current) {
1183
+ return;
1184
+ }
1185
+ for (const event of paste.events) {
1186
+ const previousValue = inputRef.current;
1187
+ const previousCursor = inputCursorRef.current;
1188
+ const update = event.kind === "paste"
1189
+ ? insertChatPaste(previousValue, event.text, previousCursor)
1190
+ : applyChatInputChunk(previousValue, event.text, previousCursor);
1191
+ if (!commitChatInputUpdate(update, previousValue, previousCursor)) {
1192
+ return;
1193
+ }
1194
+ }
1195
+ return;
1196
+ }
1197
+ if (isNewTaskShortcut(chunk, {}) && !busyRef.current) {
1198
+ void newTaskRef.current();
1199
+ return;
1200
+ }
209
1201
  if (chunk === "\x1b") {
1202
+ if (busyRef.current) {
1203
+ activeRunControllerRef.current?.abort();
1204
+ return;
1205
+ }
210
1206
  userSelectedWorkerRef.current = true;
1207
+ setAttachError(null);
211
1208
  setView("chat");
212
1209
  return;
213
1210
  }
214
1211
  const wheelDelta = mouseScrollDelta(chunk, 3);
215
- if (wheelDelta !== 0 && workersRef.current.length > 0) {
1212
+ const pageDelta = rawPageScrollDelta(chunk, Math.max(1, outputHeight - 1));
1213
+ const historyDelta = wheelDelta + pageDelta;
1214
+ if (historyDelta !== 0 && chatMaxScrollOffsetRef.current > 0) {
1215
+ setChatScrollOffset((current) => {
1216
+ const next = nextScrollOffset(current, taskResultExpandedRef.current ? -historyDelta : historyDelta, chatMaxScrollOffsetRef.current);
1217
+ chatScrollOffsetRef.current = next;
1218
+ return next;
1219
+ });
1220
+ return;
1221
+ }
1222
+ if (wheelDelta > 0 && workersRef.current.length > 0 && !taskResultExpandedRef.current) {
1223
+ setAttachError(null);
1224
+ viewRef.current = "worker";
216
1225
  setView("worker");
217
- setWorkerScrollOffset((current) => nextScrollOffset(current, wheelDelta, workerMaxScrollOffsetRef.current));
1226
+ setWorkerScrollOffset((current) => nextScrollOffset(current, wheelDelta, null));
218
1227
  return;
219
1228
  }
220
1229
  if (chunk === "\u0017") {
1230
+ if (workersRef.current.length === 0) {
1231
+ setAttachError(NO_WORKERS_LOGS_MESSAGE);
1232
+ return;
1233
+ }
1234
+ setAttachError(null);
221
1235
  setView("worker");
222
1236
  setWorkerScrollOffset(0);
223
1237
  return;
@@ -225,7 +1239,7 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, prepareNa
225
1239
  if (chunk === "\u000f") {
226
1240
  const worker = workersRef.current[selectedWorkerIndexRef.current];
227
1241
  if (!worker) {
228
- setAttachError("No worker selected. Run a complex task or wait for workers to load before attaching.");
1242
+ setAttachError(NO_WORKERS_ATTACH_MESSAGE);
229
1243
  return;
230
1244
  }
231
1245
  void attachSelectedWorkerRef.current(worker);
@@ -235,23 +1249,33 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, prepareNa
235
1249
  const nextIndex = (selectedWorkerIndexRef.current + 1) % workersRef.current.length;
236
1250
  userSelectedWorkerRef.current = true;
237
1251
  selectedWorkerIndexRef.current = nextIndex;
1252
+ setAttachError(null);
238
1253
  setSelectedWorkerIndex(nextIndex);
239
1254
  setView("worker");
240
1255
  setWorkerScrollOffset(0);
241
1256
  return;
242
1257
  }
243
- const update = applyChatInputChunk(inputRef.current, chunk);
244
- inputRef.current = update.value;
245
- if (update.exit) {
246
- exitRef.current();
1258
+ if (chunk === "\u0012" && !busyRef.current) {
1259
+ void retryRef.current();
247
1260
  return;
248
1261
  }
249
- if (!busyRef.current) {
1262
+ const draftHistoryDelta = rawHistoryDelta(chunk);
1263
+ if (draftHistoryDelta !== 0) {
1264
+ if (busyRef.current) {
1265
+ return;
1266
+ }
1267
+ const update = navigateChatDraftHistory(chatRequestHistory(messagesRef.current), { value: inputRef.current, cursor: inputCursorRef.current }, chatDraftHistoryRef.current, draftHistoryDelta);
1268
+ chatDraftHistoryRef.current = update.state;
1269
+ inputRef.current = update.value;
1270
+ inputCursorRef.current = update.cursor;
250
1271
  setInput(update.value);
1272
+ setInputCursor(update.cursor);
1273
+ return;
251
1274
  }
252
- if (update.submit !== null) {
253
- void submitRef.current(update.submit);
254
- }
1275
+ const previousValue = inputRef.current;
1276
+ const previousCursor = inputCursorRef.current;
1277
+ const update = applyChatInputChunk(previousValue, chunk, previousCursor);
1278
+ commitChatInputUpdate(update, previousValue, previousCursor);
255
1279
  return;
256
1280
  }
257
1281
  const nativeWheelDelta = mouseScrollDelta(chunk, Math.max(3, Math.floor(outputHeight / 2)));
@@ -267,6 +1291,7 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, prepareNa
267
1291
  }
268
1292
  const update = applyNativeInputChunk(nativeInputRef.current, chunk, outputHeight - 1);
269
1293
  if (update.exit) {
1294
+ nativeAttachRequestSequenceRef.current += 1;
270
1295
  nativeAttachRef.current?.process.kill();
271
1296
  nativeAttachRef.current = null;
272
1297
  nativeInputRef.current = "";
@@ -292,27 +1317,63 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, prepareNa
292
1317
  }
293
1318
  };
294
1319
  stdinEvents.on("input", handleRawInput);
1320
+ setInputReady(true);
295
1321
  return () => {
296
1322
  stdinEvents.removeListener("input", handleRawInput);
297
1323
  rawInputDecoderRef.current.end();
298
- process.stdout.write("\x1b[?1006l\x1b[?1002l\x1b[?1000l");
1324
+ chatPasteDecoderRef.current.reset();
1325
+ process.stdout.write("\x1b[?1006l\x1b[?1002l\x1b[?1000l\x1b[?2004l");
299
1326
  setRawMode(false);
300
1327
  };
301
1328
  }, [outputHeight, setRawMode, stdinEvents]);
1329
+ useEffect(() => {
1330
+ mountedRef.current = true;
1331
+ return () => {
1332
+ mountedRef.current = false;
1333
+ nativeAttachRequestSequenceRef.current += 1;
1334
+ nativeAttachRef.current?.process.kill();
1335
+ nativeAttachRef.current = null;
1336
+ activeRunControllerRef.current?.abort();
1337
+ collaborationLoadSequenceRef.current += 1;
1338
+ routerLoadSequenceRef.current += 1;
1339
+ taskSessionsLoadSequenceRef.current += 1;
1340
+ };
1341
+ }, []);
1342
+ useEffect(() => {
1343
+ if (!shutdownSignal) {
1344
+ return;
1345
+ }
1346
+ const shutdown = () => {
1347
+ activeRunControllerRef.current?.abort();
1348
+ exitRef.current();
1349
+ };
1350
+ if (shutdownSignal.aborted) {
1351
+ shutdown();
1352
+ return;
1353
+ }
1354
+ shutdownSignal.addEventListener("abort", shutdown, { once: true });
1355
+ return () => shutdownSignal.removeEventListener("abort", shutdown);
1356
+ }, [shutdownSignal]);
302
1357
  useInput((inputKey, key) => {
303
1358
  if (view === "worker") {
304
1359
  if (isExitShortcut(inputKey, key)) {
1360
+ activeRunControllerRef.current?.abort();
305
1361
  exitRef.current();
306
1362
  return;
307
1363
  }
1364
+ if (isNewTaskShortcut(inputKey, key) && !busy) {
1365
+ void startNewTask();
1366
+ return;
1367
+ }
308
1368
  const delta = scrollDelta(inputKey, key, outputHeight - 1);
309
1369
  if (delta !== 0) {
310
- setWorkerScrollOffset((current) => nextScrollOffset(current, delta, workerMaxScrollOffset));
1370
+ setWorkerScrollOffset((current) => nextScrollOffset(current, delta, null));
311
1371
  return;
312
1372
  }
313
1373
  }
314
1374
  if (key.escape) {
315
1375
  userSelectedWorkerRef.current = true;
1376
+ setAttachError(null);
316
1377
  setView("chat");
317
1378
  return;
318
1379
  }
@@ -329,13 +1390,16 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, prepareNa
329
1390
  if (isAttachShortcut(inputKey, key)) {
330
1391
  const worker = workers[selectedWorkerIndex];
331
1392
  if (!worker) {
332
- setAttachError("No worker selected. Run a complex task or wait for workers to load before attaching.");
1393
+ setAttachError(NO_WORKERS_ATTACH_MESSAGE);
333
1394
  return;
334
1395
  }
335
1396
  void attachSelectedWorker(worker);
336
1397
  }
337
- }, { isActive: view !== "native" && view !== "chat" });
1398
+ }, { isActive: view === "worker" && !workerSearch.open });
338
1399
  async function attachSelectedWorker(worker) {
1400
+ const requestSequence = nativeAttachRequestSequenceRef.current + 1;
1401
+ nativeAttachRequestSequenceRef.current = requestSequence;
1402
+ const attachIsCurrent = () => (mountedRef.current && nativeAttachRequestSequenceRef.current === requestSequence);
339
1403
  setAttachError(null);
340
1404
  try {
341
1405
  const launch = await (prepareNativeAttach
@@ -344,9 +1408,12 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, prepareNa
344
1408
  config,
345
1409
  worker
346
1410
  }));
1411
+ if (!attachIsCurrent()) {
1412
+ return;
1413
+ }
347
1414
  const terminalCols = process.stdout.columns || 120;
348
1415
  const nativeTerminalCols = nativeAttachTerminalColumns(terminalCols);
349
- const terminalRows = Math.max(1, contentHeight - 2);
1416
+ const terminalRows = nativeAttachTerminalRows(process.stdout.rows || 30, Boolean(attachError), config.ui.showStatusBar);
350
1417
  const screen = new NativeTerminalScreen({
351
1418
  cols: nativeTerminalCols,
352
1419
  rows: terminalRows
@@ -358,20 +1425,34 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, prepareNa
358
1425
  };
359
1426
  const processRef = (startNativeAttach ?? startNativeAttachProcess)(sizedLaunch, {
360
1427
  onOutput: (chunk) => {
1428
+ if (!attachIsCurrent()) {
1429
+ return;
1430
+ }
361
1431
  void screen.write(chunk).then(() => {
1432
+ if (!attachIsCurrent()) {
1433
+ return;
1434
+ }
362
1435
  setNativeAttach((current) => current && current.screen === screen
363
1436
  ? {
364
1437
  ...current,
1438
+ hasOutput: true,
365
1439
  snapshot: screen.snapshot()
366
1440
  }
367
1441
  : current);
368
1442
  });
369
1443
  },
370
1444
  onClose: (code) => {
371
- void screen.write(`\r\n${nativeAttachExitLine(code, nativeTerminalCols)}\r\n`).then(() => {
1445
+ if (!attachIsCurrent()) {
1446
+ return;
1447
+ }
1448
+ void screen.write(`\r\n${nativeAttachExitLine(code, screen.dimensions().cols)}\r\n`).then(() => {
1449
+ if (!attachIsCurrent()) {
1450
+ return;
1451
+ }
372
1452
  setNativeAttach((current) => current && current.screen === screen
373
1453
  ? {
374
1454
  ...current,
1455
+ hasOutput: true,
375
1456
  closedCode: code,
376
1457
  snapshot: screen.snapshot()
377
1458
  }
@@ -379,21 +1460,136 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, prepareNa
379
1460
  });
380
1461
  },
381
1462
  onError: (error) => {
382
- setAttachError(error.message);
1463
+ if (attachIsCurrent()) {
1464
+ setAttachError(error.message);
1465
+ }
383
1466
  }
384
1467
  });
385
- setNativeAttach({
1468
+ if (!attachIsCurrent()) {
1469
+ processRef.kill();
1470
+ return;
1471
+ }
1472
+ const nextNativeAttach = {
1473
+ hasOutput: false,
386
1474
  launch: sizedLaunch,
387
1475
  process: processRef,
388
1476
  screen,
389
1477
  snapshot: screen.snapshot(),
390
1478
  closedCode: null
391
- });
1479
+ };
1480
+ nativeAttachRef.current = nextNativeAttach;
1481
+ setNativeAttach(nextNativeAttach);
392
1482
  setNativeInput("");
393
1483
  setView("native");
394
1484
  }
395
1485
  catch (error) {
396
- setAttachError(error instanceof Error ? error.message : String(error));
1486
+ if (attachIsCurrent()) {
1487
+ setAttachError(error instanceof Error ? error.message : String(error));
1488
+ }
1489
+ }
1490
+ }
1491
+ function createRunCallbacks(controller, options = {}) {
1492
+ const announceRoute = options.announceRoute ?? true;
1493
+ return {
1494
+ signal: controller.signal,
1495
+ onRouteStart: (state) => {
1496
+ setRouteElapsedMs(0);
1497
+ setRoutePending({ ...state, startedAtMs: Date.now() });
1498
+ },
1499
+ onRouteProgress: (progress) => {
1500
+ setRoutePending((current) => current ? { ...current, phase: progress.phase } : current);
1501
+ },
1502
+ onRouteFallback: (fallback) => requestRouteFallbackChoice(fallback, controller.signal),
1503
+ onRoute: (route) => {
1504
+ setRoutePending(null);
1505
+ setLastRoute(route);
1506
+ if (announceRoute) {
1507
+ setRouteAnnouncement({
1508
+ from: "system",
1509
+ kind: "route",
1510
+ text: routeDecisionChatMessage(route),
1511
+ ...(activeTaskIdRef.current ? { taskId: activeTaskIdRef.current } : {})
1512
+ });
1513
+ chatScrollOffsetRef.current = 0;
1514
+ setChatScrollOffset(0);
1515
+ }
1516
+ },
1517
+ onStatus: (nextStatus) => {
1518
+ setStatus(nextStatus.main
1519
+ ? { ...nextStatus, mainEngine: config.pairing.main }
1520
+ : nextStatus);
1521
+ if (nextStatus.taskId !== "main") {
1522
+ activeTaskIdRef.current = nextStatus.taskId;
1523
+ setActiveTaskId(nextStatus.taskId);
1524
+ setActiveMode("complex");
1525
+ }
1526
+ },
1527
+ onWorker: (worker) => {
1528
+ setWorkers((current) => {
1529
+ const next = upsertWorker(current, worker);
1530
+ workersRef.current = next;
1531
+ return next;
1532
+ });
1533
+ if (worker.role === "main" && worker.runtimeStatus) {
1534
+ setStatus((current) => applyMainRuntimeStatus(current, worker, workerActivityPolicies[worker.engine]));
1535
+ }
1536
+ }
1537
+ };
1538
+ }
1539
+ function requestRouteFallbackChoice(fallback, signal) {
1540
+ routeFallbackResolverRef.current?.("cancel");
1541
+ setRoutePending(null);
1542
+ setLastRoute(fallback.route);
1543
+ routeFallbackPromptRef.current = fallback;
1544
+ setRouteFallbackPrompt(fallback);
1545
+ viewRef.current = "chat";
1546
+ setView("chat");
1547
+ return new Promise((resolve) => {
1548
+ let settled = false;
1549
+ const finish = (choice) => {
1550
+ if (settled) {
1551
+ return;
1552
+ }
1553
+ settled = true;
1554
+ signal.removeEventListener("abort", onAbort);
1555
+ const current = routeFallbackPromptRef.current;
1556
+ if (current === fallback) {
1557
+ routeFallbackPromptRef.current = null;
1558
+ routeFallbackResolverRef.current = null;
1559
+ setRouteFallbackPrompt(null);
1560
+ setLastRoute(previewRouteFallbackChoice(fallback.route, choice));
1561
+ }
1562
+ resolve(choice);
1563
+ };
1564
+ const onAbort = () => finish("cancel");
1565
+ routeFallbackResolverRef.current = finish;
1566
+ if (signal.aborted) {
1567
+ finish("cancel");
1568
+ }
1569
+ else {
1570
+ signal.addEventListener("abort", onAbort, { once: true });
1571
+ }
1572
+ });
1573
+ }
1574
+ function settleRouteFallbackChoice(choice) {
1575
+ routeFallbackResolverRef.current?.(choice);
1576
+ }
1577
+ async function appendVisibleMessage(message, taskId) {
1578
+ const visibleMessage = taskId ? { ...message, taskId } : message;
1579
+ setMessages((current) => {
1580
+ const next = [...current, visibleMessage];
1581
+ messagesRef.current = next;
1582
+ return next;
1583
+ });
1584
+ if (!persistChatMessage) {
1585
+ return;
1586
+ }
1587
+ try {
1588
+ await persistChatMessage(message, taskId);
1589
+ }
1590
+ catch (error) {
1591
+ const detail = error instanceof Error ? error.message : String(error);
1592
+ setAttachError(`Chat history write failed · ${detail}`);
397
1593
  }
398
1594
  }
399
1595
  async function submit(value) {
@@ -402,26 +1598,32 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, prepareNa
402
1598
  return;
403
1599
  }
404
1600
  inputRef.current = "";
1601
+ inputCursorRef.current = 0;
1602
+ chatDraftHistoryRef.current = {
1603
+ offset: 0,
1604
+ draft: { value: "", cursor: 0 }
1605
+ };
405
1606
  setInput("");
1607
+ setInputCursor(0);
1608
+ chatScrollOffsetRef.current = 0;
1609
+ setChatScrollOffset(0);
406
1610
  busyRef.current = true;
407
1611
  setBusy(true);
408
- setMessages((current) => [...current, { from: "user", text: request }]);
1612
+ setRoutePending(null);
1613
+ setRouteAnnouncement(null);
1614
+ setLastRoute(null);
1615
+ setTaskResultExpanded(false);
1616
+ const controller = new AbortController();
1617
+ activeRunControllerRef.current = controller;
1618
+ const memory = currentSubmitMemoryState(activeTaskIdRef.current, activeMode);
1619
+ await appendVisibleMessage({ from: "user", text: request }, memory.activeTaskId ?? undefined);
409
1620
  try {
410
- const callbacks = {
411
- onStatus: setStatus,
412
- onWorker: (worker) => {
413
- setWorkers((current) => upsertWorker(current, worker));
414
- }
415
- };
416
- const memory = {
417
- activeTaskId,
418
- activeMode
419
- };
420
- const followUpRoute = activeTaskId && activeMode === "complex"
1621
+ const callbacks = createRunCallbacks(controller);
1622
+ const followUpRoute = memory.activeTaskId && memory.activeMode === "complex"
421
1623
  ? await orchestrator.routeTaskFollowUp({
422
1624
  request,
423
1625
  cwd,
424
- taskId: activeTaskId,
1626
+ taskId: memory.activeTaskId,
425
1627
  ...callbacks
426
1628
  })
427
1629
  : undefined;
@@ -433,13 +1635,13 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, prepareNa
433
1635
  userSelectedWorkerRef.current = false;
434
1636
  setSelectedWorkerIndex(0);
435
1637
  setWorkerScrollOffset(0);
436
- setWorkerMaxScrollOffset(0);
437
1638
  }
438
1639
  const result = target.kind === "task-turn"
439
1640
  ? await orchestrator.handleTaskTurn({
440
1641
  request,
441
1642
  cwd,
442
1643
  taskId: target.taskId,
1644
+ route: followUpRoute?.route,
443
1645
  ...callbacks
444
1646
  })
445
1647
  : target.kind === "task-question"
@@ -447,6 +1649,7 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, prepareNa
447
1649
  request,
448
1650
  cwd,
449
1651
  taskId: target.taskId,
1652
+ route: followUpRoute?.route,
450
1653
  ...callbacks
451
1654
  })
452
1655
  : await orchestrator.handleRequest({
@@ -457,72 +1660,1345 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, prepareNa
457
1660
  const nextMemory = nextSubmitMemoryState(memory, target, result);
458
1661
  setActiveMode(nextMemory.activeMode);
459
1662
  setActiveTaskId(nextMemory.activeTaskId);
460
- setMessages((current) => [...current, { from: "system", text: result.summary }]);
1663
+ activeTaskIdRef.current = nextMemory.activeTaskId;
1664
+ setCanRetryTask(nextMemory.activeTaskId
1665
+ ? await orchestrator.canRetryTask(nextMemory.activeTaskId)
1666
+ : false);
1667
+ setRouteAnnouncement(null);
1668
+ await appendVisibleMessage({ from: "system", text: result.summary }, nextMemory.activeTaskId ?? undefined);
1669
+ if (result.mode === "complex" && parseTaskResultSummary(result.summary)) {
1670
+ setTaskResultExpanded(true);
1671
+ }
1672
+ }
1673
+ catch (error) {
1674
+ setRouteAnnouncement(null);
1675
+ const retryTaskId = activeTaskIdRef.current;
1676
+ if (retryTaskId) {
1677
+ setCanRetryTask(await orchestrator.canRetryTask(retryTaskId));
1678
+ }
1679
+ await appendVisibleMessage({
1680
+ from: "system",
1681
+ text: isAbortError(error) ? "cancelled · request stopped" : error instanceof Error ? error.message : String(error)
1682
+ }, retryTaskId ?? undefined);
1683
+ }
1684
+ finally {
1685
+ if (activeRunControllerRef.current === controller) {
1686
+ activeRunControllerRef.current = null;
1687
+ }
1688
+ setRoutePending(null);
1689
+ setRouteAnnouncement(null);
1690
+ busyRef.current = false;
1691
+ setBusy(false);
1692
+ }
1693
+ }
1694
+ async function retryActiveTask() {
1695
+ const taskId = activeTaskIdRef.current;
1696
+ if (!taskId || busyRef.current || !canRetryTask) {
1697
+ return;
1698
+ }
1699
+ const controller = new AbortController();
1700
+ activeRunControllerRef.current = controller;
1701
+ busyRef.current = true;
1702
+ setBusy(true);
1703
+ setCanRetryTask(false);
1704
+ setRoutePending(null);
1705
+ setRouteAnnouncement(null);
1706
+ setLastRoute(null);
1707
+ try {
1708
+ const result = await orchestrator.retryTask({
1709
+ taskId,
1710
+ cwd,
1711
+ ...createRunCallbacks(controller, { announceRoute: false })
1712
+ });
1713
+ activeTaskIdRef.current = taskId;
1714
+ setActiveTaskId(taskId);
1715
+ setActiveMode("complex");
1716
+ setCanRetryTask(false);
1717
+ setRouteAnnouncement(null);
1718
+ await appendVisibleMessage({ from: "system", text: result.summary }, taskId);
1719
+ if (parseTaskResultSummary(result.summary)) {
1720
+ setTaskResultExpanded(true);
1721
+ }
1722
+ }
1723
+ catch (error) {
1724
+ setRouteAnnouncement(null);
1725
+ setCanRetryTask(await orchestrator.canRetryTask(taskId));
1726
+ await appendVisibleMessage({
1727
+ from: "system",
1728
+ text: isAbortError(error) ? "cancelled · retry stopped" : error instanceof Error ? error.message : String(error)
1729
+ }, taskId);
1730
+ }
1731
+ finally {
1732
+ if (activeRunControllerRef.current === controller) {
1733
+ activeRunControllerRef.current = null;
1734
+ }
1735
+ setRoutePending(null);
1736
+ setRouteAnnouncement(null);
1737
+ busyRef.current = false;
1738
+ setBusy(false);
1739
+ }
1740
+ }
1741
+ async function startNewTask() {
1742
+ if (busyRef.current || !activeTaskIdRef.current) {
1743
+ return;
1744
+ }
1745
+ try {
1746
+ await activateTaskSession?.(null);
1747
+ }
1748
+ catch (error) {
1749
+ setAttachError(error instanceof Error ? error.message : String(error));
1750
+ return;
1751
+ }
1752
+ const nextMemory = newTaskMemoryState();
1753
+ activeTaskIdRef.current = nextMemory.activeTaskId;
1754
+ setActiveTaskId(nextMemory.activeTaskId);
1755
+ setActiveMode(nextMemory.activeMode);
1756
+ setCanRetryTask(false);
1757
+ setStatus(null);
1758
+ setRoutePending(null);
1759
+ setRouteAnnouncement(null);
1760
+ setLastRoute(null);
1761
+ setTaskResultExpanded(false);
1762
+ setWorkers([]);
1763
+ workersRef.current = [];
1764
+ selectedWorkerIndexRef.current = 0;
1765
+ setSelectedWorkerIndex(0);
1766
+ setWorkerScrollOffset(0);
1767
+ autoSelectedFailedWorkerRef.current = false;
1768
+ userSelectedWorkerRef.current = false;
1769
+ setAttachError(null);
1770
+ viewRef.current = "chat";
1771
+ setView("chat");
1772
+ chatDraftHistoryRef.current = {
1773
+ offset: 0,
1774
+ draft: { value: inputRef.current, cursor: inputCursorRef.current }
1775
+ };
1776
+ await appendVisibleMessage({ from: "system", text: "new task · ready" });
1777
+ }
1778
+ async function openRouterDiagnostics() {
1779
+ const currentView = viewRef.current;
1780
+ if (currentView === "native" || currentView === "workspace") {
1781
+ return;
1782
+ }
1783
+ if (currentView !== "router") {
1784
+ routerReturnViewRef.current = currentView === "worker" || currentView === "workers" || currentView === "sessions"
1785
+ ? currentView
1786
+ : "chat";
1787
+ }
1788
+ viewRef.current = "router";
1789
+ setView("router");
1790
+ setAttachError(null);
1791
+ setRouterError(null);
1792
+ setRouterLoading(true);
1793
+ setRouterScrollOffset(0);
1794
+ routerMaxScrollOffsetRef.current = 0;
1795
+ setRouterMaxScrollOffset(0);
1796
+ const sequence = routerLoadSequenceRef.current + 1;
1797
+ routerLoadSequenceRef.current = sequence;
1798
+ try {
1799
+ if (!loadRouterDiagnostics) {
1800
+ throw new Error("Router diagnostics are unavailable");
1801
+ }
1802
+ const diagnostics = await loadRouterDiagnostics();
1803
+ if (routerLoadSequenceRef.current !== sequence) {
1804
+ return;
1805
+ }
1806
+ setRouterRecords(diagnostics.records);
1807
+ setRouterPolicy(diagnostics.policy);
1808
+ }
1809
+ catch (error) {
1810
+ if (routerLoadSequenceRef.current === sequence) {
1811
+ setRouterError(error instanceof Error ? error.message : String(error));
1812
+ }
1813
+ }
1814
+ finally {
1815
+ if (routerLoadSequenceRef.current === sequence) {
1816
+ setRouterLoading(false);
1817
+ }
1818
+ }
1819
+ }
1820
+ async function openTaskSessions() {
1821
+ const currentView = viewRef.current;
1822
+ if (busyRef.current ||
1823
+ currentView === "native" ||
1824
+ currentView === "workspace" ||
1825
+ currentView === "sessions") {
1826
+ return;
1827
+ }
1828
+ taskSessionsReturnViewRef.current = currentView === "worker" || currentView === "workers" || currentView === "router"
1829
+ ? currentView
1830
+ : "chat";
1831
+ viewRef.current = "sessions";
1832
+ setView("sessions");
1833
+ setAttachError(null);
1834
+ setTaskSessionsError(null);
1835
+ setTaskSessionsNotice(null);
1836
+ updateTaskSessionAction(null);
1837
+ await refreshTaskSessions(activeTaskIdRef.current);
1838
+ }
1839
+ async function refreshTaskSessions(preferredTaskId = null) {
1840
+ taskSessionsLoadingRef.current = true;
1841
+ setTaskSessionsLoading(true);
1842
+ const sequence = taskSessionsLoadSequenceRef.current + 1;
1843
+ taskSessionsLoadSequenceRef.current = sequence;
1844
+ try {
1845
+ if (!loadTaskSessions) {
1846
+ throw new Error("Task sessions are unavailable");
1847
+ }
1848
+ const tasks = await loadTaskSessions({
1849
+ includeArchived: taskSessionsIncludeArchivedRef.current
1850
+ });
1851
+ if (taskSessionsLoadSequenceRef.current !== sequence) {
1852
+ return;
1853
+ }
1854
+ taskSessionsRef.current = tasks;
1855
+ setTaskSessions(tasks);
1856
+ const preferredIndex = preferredTaskId
1857
+ ? tasks.findIndex((task) => task.id === preferredTaskId)
1858
+ : -1;
1859
+ const selectedIndex = preferredIndex >= 0
1860
+ ? preferredIndex
1861
+ : Math.min(Math.max(0, selectedTaskSessionIndexRef.current), Math.max(0, tasks.length - 1));
1862
+ selectedTaskSessionIndexRef.current = selectedIndex;
1863
+ setSelectedTaskSessionIndex(selectedIndex);
1864
+ }
1865
+ catch (error) {
1866
+ if (taskSessionsLoadSequenceRef.current === sequence) {
1867
+ setTaskSessionsError(error instanceof Error ? error.message : String(error));
1868
+ }
1869
+ }
1870
+ finally {
1871
+ if (taskSessionsLoadSequenceRef.current === sequence) {
1872
+ taskSessionsLoadingRef.current = false;
1873
+ setTaskSessionsLoading(false);
1874
+ }
1875
+ }
1876
+ }
1877
+ function updateTaskSessionAction(next) {
1878
+ taskSessionActionRef.current = next;
1879
+ setTaskSessionAction(next);
1880
+ }
1881
+ async function runTaskSessionOperation(run, notice, preferredTaskId) {
1882
+ if (taskSessionsLoadingRef.current) {
1883
+ return;
1884
+ }
1885
+ taskSessionsLoadingRef.current = true;
1886
+ setTaskSessionsLoading(true);
1887
+ setTaskSessionsError(null);
1888
+ setTaskSessionsNotice(null);
1889
+ try {
1890
+ const result = await run();
1891
+ updateTaskSessionAction(null);
1892
+ setTaskSessionsNotice(typeof notice === "function" ? notice(result) : notice);
1893
+ await refreshTaskSessions(preferredTaskId);
1894
+ }
1895
+ catch (error) {
1896
+ setTaskSessionsError(error instanceof Error ? error.message : String(error));
1897
+ }
1898
+ finally {
1899
+ taskSessionsLoadingRef.current = false;
1900
+ setTaskSessionsLoading(false);
1901
+ }
1902
+ }
1903
+ async function renameSelectedTaskSession(action) {
1904
+ await runTaskSessionOperation(async () => {
1905
+ if (!renameTaskSession) {
1906
+ throw new Error("Task session rename is unavailable");
1907
+ }
1908
+ const title = action.value.trim();
1909
+ await renameTaskSession(action.taskId, title);
1910
+ return title;
1911
+ }, (title) => `Renamed · ${title}`, action.taskId);
1912
+ }
1913
+ async function archiveSelectedTaskSession() {
1914
+ const selected = taskSessionsRef.current[selectedTaskSessionIndexRef.current];
1915
+ if (!selected) {
1916
+ return;
1917
+ }
1918
+ const archived = !selected.archived_at;
1919
+ await runTaskSessionOperation(async () => {
1920
+ if (!setTaskSessionArchived) {
1921
+ throw new Error("Task session archive is unavailable");
1922
+ }
1923
+ await setTaskSessionArchived(selected.id, archived);
1924
+ }, archived ? `Archived · ${selected.title}` : `Unarchived · ${selected.title}`, selected.id);
1925
+ }
1926
+ async function deleteSelectedTaskSession(action) {
1927
+ await runTaskSessionOperation(async () => {
1928
+ if (!deleteTaskSession) {
1929
+ throw new Error("Task session deletion is unavailable");
1930
+ }
1931
+ await deleteTaskSession(action.taskId);
1932
+ }, `Deleted · ${action.title}`, null);
1933
+ }
1934
+ async function exportSelectedTaskSession() {
1935
+ const selected = taskSessionsRef.current[selectedTaskSessionIndexRef.current];
1936
+ if (!selected) {
1937
+ return;
1938
+ }
1939
+ await runTaskSessionOperation(async () => {
1940
+ if (!exportTaskSession) {
1941
+ throw new Error("Task session export is unavailable");
1942
+ }
1943
+ return exportTaskSession(selected.id);
1944
+ }, (path) => `Exported · ${path}`, selected.id);
1945
+ }
1946
+ async function activateSelectedTaskSession() {
1947
+ if (busyRef.current || taskSessionsLoadingRef.current) {
1948
+ return;
1949
+ }
1950
+ const selected = taskSessionsRef.current[selectedTaskSessionIndexRef.current];
1951
+ if (!selected) {
1952
+ return;
1953
+ }
1954
+ if (selected.archived_at) {
1955
+ setTaskSessionsError(`Unarchive ${selected.title} before restoring it`);
1956
+ return;
1957
+ }
1958
+ taskSessionsLoadingRef.current = true;
1959
+ setTaskSessionsLoading(true);
1960
+ setTaskSessionsError(null);
1961
+ const sequence = taskSessionsLoadSequenceRef.current + 1;
1962
+ taskSessionsLoadSequenceRef.current = sequence;
1963
+ try {
1964
+ if (!activateTaskSession) {
1965
+ throw new Error("Task session restore is unavailable");
1966
+ }
1967
+ const restored = await activateTaskSession(selected.id);
1968
+ if (taskSessionsLoadSequenceRef.current !== sequence) {
1969
+ return;
1970
+ }
1971
+ if (!restored) {
1972
+ throw new Error(`Task session not found: ${selected.id}`);
1973
+ }
1974
+ activeTaskIdRef.current = restored.taskId;
1975
+ setActiveTaskId(restored.taskId);
1976
+ setActiveMode("complex");
1977
+ setCanRetryTask(restored.canRetry);
1978
+ setStatus(restoredWorkerStatusLine(restored.taskId, restored.workers));
1979
+ setRoutePending(null);
1980
+ setLastRoute(restored.route);
1981
+ setTaskResultExpanded(latestTaskResultMessageIndex(messagesRef.current, restored.taskId) >= 0);
1982
+ workersRef.current = restored.workers;
1983
+ setWorkers(restored.workers);
1984
+ selectedWorkerIndexRef.current = 0;
1985
+ setSelectedWorkerIndex(0);
1986
+ setWorkerScrollOffset(0);
1987
+ autoSelectedFailedWorkerRef.current = false;
1988
+ userSelectedWorkerRef.current = false;
1989
+ setAttachError(null);
1990
+ viewRef.current = "chat";
1991
+ setView("chat");
1992
+ }
1993
+ catch (error) {
1994
+ if (taskSessionsLoadSequenceRef.current === sequence) {
1995
+ setTaskSessionsError(error instanceof Error ? error.message : String(error));
1996
+ }
1997
+ }
1998
+ finally {
1999
+ if (taskSessionsLoadSequenceRef.current === sequence) {
2000
+ taskSessionsLoadingRef.current = false;
2001
+ setTaskSessionsLoading(false);
2002
+ }
2003
+ }
2004
+ }
2005
+ function updateFeatureCancelPrompt(next) {
2006
+ featureCancelPromptRef.current = next;
2007
+ setFeatureCancelPrompt(next);
2008
+ }
2009
+ async function confirmFeatureCancellation(prompt) {
2010
+ updateFeatureCancelPrompt(null);
2011
+ setAttachError(null);
2012
+ try {
2013
+ const result = await orchestrator.cancelFeature(prompt.taskId, prompt.featureId);
2014
+ setFeatureBoardNotice(result.requested
2015
+ ? `Cancellation requested for ${prompt.title} · ${result.role ?? "worker"} stopping; active peers will finish.`
2016
+ : `No active Actor/Critic for ${prompt.title}; refresh and try again.`);
2017
+ }
2018
+ catch (error) {
2019
+ setFeatureBoardNotice(`Cancel failed for ${prompt.title}: ${error instanceof Error ? error.message : String(error)}`);
2020
+ }
2021
+ }
2022
+ async function openFeatureBoard() {
2023
+ if (viewRef.current !== "workers") {
2024
+ return;
2025
+ }
2026
+ if (!activeTaskIdRef.current) {
2027
+ setAttachError(NO_ACTIVE_FEATURES_MESSAGE);
2028
+ return;
2029
+ }
2030
+ viewRef.current = "features";
2031
+ setView("features");
2032
+ setAttachError(null);
2033
+ setCollaborationError(null);
2034
+ updateFeatureCancelPrompt(null);
2035
+ setFeatureBoardNotice(null);
2036
+ collaborationTimelineRef.current = null;
2037
+ setCollaborationTimeline(null);
2038
+ featureBoardSelectedIndexRef.current = 0;
2039
+ setFeatureBoardSelectedIndex(0);
2040
+ collaborationFeatureIndexRef.current = -1;
2041
+ setCollaborationFeatureIndex(-1);
2042
+ collaborationSelectedEventIdRef.current = null;
2043
+ setCollaborationSelectedEventId(null);
2044
+ collaborationDetailOpenRef.current = false;
2045
+ setCollaborationDetailOpen(false);
2046
+ collaborationUnresolvedOnlyRef.current = false;
2047
+ setCollaborationUnresolvedOnly(false);
2048
+ collaborationMaxScrollOffsetRef.current = 0;
2049
+ setCollaborationMaxScrollOffset(0);
2050
+ setCollaborationScrollOffset(0);
2051
+ await refreshCollaborationTimeline(true);
2052
+ }
2053
+ async function openCollaborationTimeline(featureIndex = -1) {
2054
+ const source = viewRef.current;
2055
+ if (source !== "workers" && source !== "features") {
2056
+ return;
2057
+ }
2058
+ if (!activeTaskIdRef.current) {
2059
+ setAttachError(NO_ACTIVE_COLLABORATION_MESSAGE);
2060
+ return;
2061
+ }
2062
+ collaborationReturnViewRef.current = source;
2063
+ updateFeatureCancelPrompt(null);
2064
+ setFeatureBoardNotice(null);
2065
+ viewRef.current = "collaboration";
2066
+ setView("collaboration");
2067
+ setAttachError(null);
2068
+ setCollaborationError(null);
2069
+ const existingTimeline = source === "features" ? collaborationTimelineRef.current : null;
2070
+ if (!existingTimeline) {
2071
+ collaborationTimelineRef.current = null;
2072
+ setCollaborationTimeline(null);
2073
+ }
2074
+ const nextFeatureIndex = featureIndex >= 0 && featureIndex < (existingTimeline?.features.length ?? 0)
2075
+ ? featureIndex
2076
+ : -1;
2077
+ collaborationFeatureIndexRef.current = nextFeatureIndex;
2078
+ setCollaborationFeatureIndex(nextFeatureIndex);
2079
+ collaborationSelectedEventIdRef.current = null;
2080
+ setCollaborationSelectedEventId(null);
2081
+ collaborationDetailOpenRef.current = false;
2082
+ setCollaborationDetailOpen(false);
2083
+ collaborationUnresolvedOnlyRef.current = false;
2084
+ setCollaborationUnresolvedOnly(false);
2085
+ collaborationMaxScrollOffsetRef.current = 0;
2086
+ setCollaborationMaxScrollOffset(0);
2087
+ setCollaborationScrollOffset(0);
2088
+ await refreshCollaborationTimeline(!existingTimeline);
2089
+ }
2090
+ async function refreshCollaborationTimeline(showLoading = true) {
2091
+ const taskId = activeTaskIdRef.current;
2092
+ if (!taskId) {
2093
+ setCollaborationError(NO_ACTIVE_COLLABORATION_MESSAGE);
2094
+ return;
2095
+ }
2096
+ const sequence = collaborationLoadSequenceRef.current + 1;
2097
+ collaborationLoadSequenceRef.current = sequence;
2098
+ if (showLoading) {
2099
+ setCollaborationLoading(true);
2100
+ }
2101
+ try {
2102
+ if (!loadCollaborationTimeline) {
2103
+ throw new Error("Collaboration timeline is unavailable");
2104
+ }
2105
+ const timeline = await loadCollaborationTimeline(taskId);
2106
+ if (collaborationLoadSequenceRef.current !== sequence) {
2107
+ return;
2108
+ }
2109
+ collaborationTimelineRef.current = timeline;
2110
+ setCollaborationTimeline(timeline);
2111
+ const currentBoardIndex = featureBoardSelectedIndexRef.current;
2112
+ const nextBoardIndex = timeline.features.length > 0
2113
+ ? Math.min(timeline.features.length - 1, Math.max(0, currentBoardIndex))
2114
+ : 0;
2115
+ if (nextBoardIndex !== currentBoardIndex) {
2116
+ featureBoardSelectedIndexRef.current = nextBoardIndex;
2117
+ setFeatureBoardSelectedIndex(nextBoardIndex);
2118
+ }
2119
+ const currentIndex = collaborationFeatureIndexRef.current;
2120
+ const nextIndex = currentIndex >= timeline.features.length ? -1 : currentIndex;
2121
+ if (nextIndex !== currentIndex) {
2122
+ collaborationFeatureIndexRef.current = nextIndex;
2123
+ setCollaborationFeatureIndex(nextIndex);
2124
+ setCollaborationScrollOffset(0);
2125
+ }
2126
+ const scopedEvents = collaborationTimelineEvents(timeline, nextIndex, collaborationUnresolvedOnlyRef.current);
2127
+ const selectedEventId = collaborationSelectedEventIdRef.current;
2128
+ if (selectedEventId && !scopedEvents.some((event) => event.id === selectedEventId)) {
2129
+ collaborationSelectedEventIdRef.current = null;
2130
+ setCollaborationSelectedEventId(null);
2131
+ collaborationDetailOpenRef.current = false;
2132
+ setCollaborationDetailOpen(false);
2133
+ setCollaborationScrollOffset(0);
2134
+ }
2135
+ else if (selectedEventId && !collaborationDetailOpenRef.current) {
2136
+ const minimumOffset = collaborationSelectionScrollOffset(scopedEvents, selectedEventId, process.stdout.columns || 120);
2137
+ setCollaborationScrollOffset((current) => Math.max(current, minimumOffset));
2138
+ }
2139
+ setCollaborationError(null);
461
2140
  }
462
2141
  catch (error) {
463
- setMessages((current) => [
464
- ...current,
465
- { from: "system", text: error instanceof Error ? error.message : String(error) }
466
- ]);
2142
+ if (collaborationLoadSequenceRef.current === sequence) {
2143
+ setCollaborationError(error instanceof Error ? error.message : String(error));
2144
+ }
467
2145
  }
468
2146
  finally {
469
- busyRef.current = false;
470
- setBusy(false);
2147
+ if (collaborationLoadSequenceRef.current === sequence) {
2148
+ setCollaborationLoading(false);
2149
+ }
2150
+ }
2151
+ }
2152
+ function updateWorkerNavigationTargets(next) {
2153
+ const previous = workerNavigationTargetsRef.current;
2154
+ workerNavigationTargetsRef.current = next;
2155
+ if (!sameNumberArray(previous.errorOffsets, next.errorOffsets)) {
2156
+ workerJumpIndexRef.current.error = -1;
2157
+ }
2158
+ if (!sameNumberArray(previous.diffOffsets, next.diffOffsets)) {
2159
+ workerJumpIndexRef.current.diff = -1;
2160
+ }
2161
+ if (!sameWorkerNavigationTargets(previous, next)) {
2162
+ setWorkerNavigationTargets(next);
2163
+ }
2164
+ const search = workerSearchRef.current;
2165
+ const matchIndex = next.searchOffsets.length > 0
2166
+ ? Math.min(next.searchOffsets.length - 1, Math.max(0, search.matchIndex))
2167
+ : 0;
2168
+ if (matchIndex !== search.matchIndex) {
2169
+ const updated = { ...search, matchIndex };
2170
+ workerSearchRef.current = updated;
2171
+ setWorkerSearch(updated);
2172
+ }
2173
+ if (search.open && search.query.trim() && next.searchOffsets.length > 0) {
2174
+ setWorkerScrollOffset(next.searchOffsets[matchIndex] ?? 0);
2175
+ }
2176
+ }
2177
+ function openWorkerOverview() {
2178
+ const currentView = viewRef.current;
2179
+ if (currentView !== "chat" && currentView !== "worker") {
2180
+ return;
2181
+ }
2182
+ if (workersRef.current.length === 0) {
2183
+ setAttachError(NO_WORKERS_OVERVIEW_MESSAGE);
2184
+ return;
2185
+ }
2186
+ workerOverviewReturnViewRef.current = currentView;
2187
+ viewRef.current = "workers";
2188
+ setAttachError(null);
2189
+ setView("workers");
2190
+ }
2191
+ function openWorkspacePicker() {
2192
+ const currentView = viewRef.current;
2193
+ if (busyRef.current || !switchWorkspace || currentView === "native" || currentView === "workspace") {
2194
+ return;
2195
+ }
2196
+ workspaceReturnViewRef.current = currentView === "worker" || currentView === "workers" || currentView === "sessions"
2197
+ ? currentView
2198
+ : "chat";
2199
+ setAttachError(null);
2200
+ setView("workspace");
2201
+ }
2202
+ function closeWorkspacePicker() {
2203
+ setAttachError(null);
2204
+ setView(workspaceReturnViewRef.current);
2205
+ }
2206
+ async function selectWorkspace(workspace) {
2207
+ if (!switchWorkspace || workspace === cwd) {
2208
+ closeWorkspacePicker();
2209
+ return;
2210
+ }
2211
+ try {
2212
+ await switchWorkspace(workspace);
2213
+ }
2214
+ catch (error) {
2215
+ setAttachError(error instanceof Error ? error.message : String(error));
2216
+ setView(workspaceReturnViewRef.current);
471
2217
  }
472
2218
  }
473
- return (_jsx(AppShell, { view: view, cwd: cwd, taskId: activeTaskId, statusText: `${formatStatusLine(status)}${visibleWorkerStatus ? ` | ${visibleWorkerStatus}` : ""}`, contentHeight: contentHeight, input: _jsx(InputBar, { mode: view, busy: busy, hasWorkers: workers.length > 0, nativeClosed: view === "native" && nativeAttach?.closedCode !== null, value: view === "native" ? "" : input, terminalWidth: terminalWidth, onChange: view === "native" ? setNativeInput : setInput, onSubmit: view === "native" ? undefined : submit }), error: attachError, children: view === "native" ? (_jsx(NativeAttachView, { attach: nativeAttach })) : view === "chat" ? (_jsx(ChatView, { messages: messages, cwd: cwd, activeTaskId: activeTaskId, terminalWidth: terminalWidth, viewportHeight: contentHeight })) : (_jsx(WorkerOutputView, { title: workerTitle(workers, selectedWorkerIndex), role: workers[selectedWorkerIndex]?.role, logPath: workers[selectedWorkerIndex]?.logPath ?? null, scrollOffset: workerScrollOffset, height: Math.max(1, outputHeight - 1), terminalWidth: terminalWidth, onViewportChange: ({ offset, maxOffset }) => {
474
- setWorkerMaxScrollOffset(maxOffset);
2219
+ if (view === "workspace") {
2220
+ return (_jsx(Box, { flexDirection: "column", height: terminalSize.rows, children: _jsx(WorkspacePicker, { cwd: cwd, choices: workspaceChoices, terminalHeight: terminalSize.rows, terminalWidth: terminalWidth, onCancel: closeWorkspacePicker, onSelect: (workspace) => void selectWorkspace(workspace) }) }));
2221
+ }
2222
+ return (_jsx(AppShell, { view: view, cwd: cwd, taskId: activeTaskId, statusText: [visibleTaskStatus, visibleWorkerStatus, visibleRouteStatus].filter(Boolean).join(" | "), contentHeight: contentHeight, showStatusBar: config.ui.showStatusBar, input: _jsx(InputBar, { mode: view === "worker" && workerSearch.open ? "worker-search" : view, ready: inputReady, busy: busy, routeFallback: Boolean(routeFallbackPrompt), collaborationDetail: collaborationDetailOpen, collaborationUnresolved: collaborationUnresolvedOnly, collaborationBack: collaborationReturnViewRef.current, featureCanCancel: featureCanCancel, featureCancelConfirm: Boolean(featureCancelPrompt), taskSessionAction: taskSessionAction?.type === "rename"
2223
+ ? { type: "rename", value: taskSessionAction.value, cursor: taskSessionAction.cursor }
2224
+ : taskSessionAction?.type === "delete"
2225
+ ? { type: "delete", title: taskSessionAction.title }
2226
+ : null, taskSessionsIncludeArchived: taskSessionsIncludeArchived, canRetry: canRetryTask, hasWorkers: workers.length > 0, hasActiveTask: Boolean(activeTaskId), hasTaskResult: hasTaskResult, taskResultExpanded: taskResultExpanded, chatScrollOffset: chatScrollOffset, chatMaxScrollOffset: chatMaxScrollOffset, nativeClosed: view === "native" && nativeAttach?.closedCode !== null, searchMatchIndex: workerSearch.matchIndex, searchMatchCount: workerNavigationTargets.searchOffsets.length, value: workerSearch.open && view === "worker"
2227
+ ? workerSearch.query
2228
+ : view === "native" || view === "router" || view === "workers" || view === "features" || view === "sessions" || view === "collaboration" ? "" : input, cursor: workerSearch.open && view === "worker"
2229
+ ? workerSearch.cursor
2230
+ : view === "chat" ? inputCursor : undefined, terminalWidth: terminalWidth, onChange: view === "native" ? setNativeInput : setInput, onSubmit: view === "native" ? undefined : submit }), error: attachError, children: view === "native" ? (_jsx(NativeAttachView, { attach: nativeAttach, viewportHeight: contentHeight })) : view === "sessions" ? (_jsx(TaskSessionsView, { tasks: taskSessions, activeTaskId: activeTaskId, selectedIndex: selectedTaskSessionIndex, includeArchived: taskSessionsIncludeArchived, notice: taskSessionsNotice, action: taskSessionAction?.type === "rename"
2231
+ ? { type: "rename", title: taskSessionAction.title }
2232
+ : taskSessionAction?.type === "delete"
2233
+ ? { type: "delete", title: taskSessionAction.title }
2234
+ : null, loading: taskSessionsLoading, error: taskSessionsError, height: contentHeight, terminalWidth: terminalWidth })) : view === "features" ? (_jsx(FeatureBoardView, { timeline: collaborationTimeline, selectedIndex: featureBoardSelectedIndex, loading: collaborationLoading, error: collaborationError, notice: featureBoardNotice, height: contentHeight, terminalWidth: terminalWidth })) : view === "collaboration" ? (_jsx(CollaborationTimelineView, { timeline: collaborationTimeline, featureIndex: collaborationFeatureIndex, selectedEventId: collaborationSelectedEventId, detailOpen: collaborationDetailOpen, unresolvedOnly: collaborationUnresolvedOnly, loading: collaborationLoading, error: collaborationError, scrollOffset: collaborationScrollOffset, height: contentHeight, terminalWidth: terminalWidth, onViewportChange: ({ offset, maxOffset }) => {
2235
+ collaborationMaxScrollOffsetRef.current = maxOffset;
2236
+ setCollaborationMaxScrollOffset(maxOffset);
2237
+ if (offset !== collaborationScrollOffset) {
2238
+ setCollaborationScrollOffset(offset);
2239
+ }
2240
+ } })) : view === "workers" ? (_jsx(WorkerOverviewView, { workers: workers, selectedIndex: selectedWorkerIndex, nowMs: workerClockMs, activityPolicies: workerActivityPolicies, height: contentHeight, terminalWidth: terminalWidth })) : view === "router" ? (_jsx(RouterDiagnosticsView, { records: routerRecords, policy: routerPolicy, currentWorkspace: cwd, scope: routerScope, loading: routerLoading, error: routerError, scrollOffset: routerScrollOffset, height: contentHeight, terminalWidth: terminalWidth, onViewportChange: ({ offset, maxOffset }) => {
2241
+ routerMaxScrollOffsetRef.current = maxOffset;
2242
+ setRouterMaxScrollOffset(maxOffset);
2243
+ if (offset !== routerScrollOffset) {
2244
+ setRouterScrollOffset(offset);
2245
+ }
2246
+ } })) : view === "chat" ? (_jsx(ChatView, { messages: visibleChatMessages, cwd: cwd, activeTaskId: activeTaskId, terminalWidth: terminalWidth, viewportHeight: contentHeight, scrollOffset: chatScrollOffset, expandedTaskResult: taskResultExpanded, onViewportChange: ({ offset, maxOffset }) => {
2247
+ chatMaxScrollOffsetRef.current = maxOffset;
2248
+ setChatMaxScrollOffset(maxOffset);
2249
+ if (offset !== chatScrollOffsetRef.current) {
2250
+ chatScrollOffsetRef.current = offset;
2251
+ setChatScrollOffset(offset);
2252
+ }
2253
+ } })) : (_jsx(WorkerOutputView, { title: workerTitle(workers, selectedWorkerIndex), role: workers[selectedWorkerIndex]?.role, featureId: workers[selectedWorkerIndex]?.featureId, logPath: workers[selectedWorkerIndex]?.logPath ?? null, scrollOffset: workerScrollOffset, searchQuery: workerSearch.open ? workerSearch.query : "", searchMatchIndex: workerSearch.matchIndex, height: Math.max(1, outputHeight - 1), terminalWidth: terminalWidth, onNavigationChange: updateWorkerNavigationTargets, onViewportChange: ({ offset }) => {
475
2254
  if (offset !== workerScrollOffset) {
476
2255
  setWorkerScrollOffset(offset);
477
2256
  }
478
2257
  } })) }));
479
2258
  }
480
- export function ChatView({ messages, cwd, activeTaskId, terminalWidth = process.stdout.columns || 120, viewportHeight }) {
2259
+ function readTerminalSize() {
2260
+ return {
2261
+ columns: Math.max(1, Math.trunc(process.stdout.columns || 120)),
2262
+ rows: Math.max(1, Math.trunc(process.stdout.rows || 30))
2263
+ };
2264
+ }
2265
+ export function ChatView({ messages, cwd, activeTaskId, terminalWidth = process.stdout.columns || 120, viewportHeight, scrollOffset = 0, expandedTaskResult = false, onViewportChange }) {
481
2266
  const height = viewportHeight ? Math.max(1, viewportHeight) : undefined;
2267
+ const viewport = useMemo(() => chatMessageViewport(messages, terminalWidth, height ?? 12, scrollOffset, {
2268
+ expandedTaskResult,
2269
+ taskId: activeTaskId
2270
+ }), [activeTaskId, expandedTaskResult, height, messages, scrollOffset, terminalWidth]);
2271
+ useEffect(() => {
2272
+ onViewportChange?.({
2273
+ offset: viewport.clampedOffset,
2274
+ maxOffset: viewport.maxOffset
2275
+ });
2276
+ }, [onViewportChange, viewport.clampedOffset, viewport.maxOffset]);
482
2277
  if (messages.length === 0) {
483
- return (_jsx(Box, { flexDirection: "column", height: height, justifyContent: height ? "flex-end" : undefined, children: _jsx(ChatEmptyState, { cwd: cwd, activeTaskId: activeTaskId, terminalWidth: terminalWidth }) }));
2278
+ const spacerLines = chatViewportSpacerLineCount(1, height);
2279
+ return (_jsxs(Box, { flexDirection: "column", height: height, children: [_jsx(ChatViewportSpacerLines, { count: spacerLines, terminalWidth: terminalWidth }), _jsx(ChatEmptyState, { cwd: cwd, activeTaskId: activeTaskId, terminalWidth: terminalWidth })] }));
2280
+ }
2281
+ const lines = viewport.lines;
2282
+ const spacerLines = chatViewportSpacerLineCount(lines.length, height);
2283
+ const topAligned = (expandedTaskResult && latestTaskResultMessageIndex(messages, activeTaskId) >= 0) || chatCompletionIsTopAligned(messages);
2284
+ return (_jsxs(Box, { flexDirection: "column", height: height, children: [!topAligned ? _jsx(ChatViewportSpacerLines, { count: spacerLines, terminalWidth: terminalWidth }) : null, lines.map((line, index) => (_jsx(ChatLine, { line: line, terminalWidth: terminalWidth }, `${line.from}-${index}`))), topAligned ? _jsx(ChatViewportSpacerLines, { count: spacerLines, terminalWidth: terminalWidth }) : null] }));
2285
+ }
2286
+ export function chatLineTheme(line) {
2287
+ const backgroundColor = chatLineBackgroundColor(line.background);
2288
+ if (line.from === "user") {
2289
+ return { backgroundColor, color: TUI_THEME.accent };
2290
+ }
2291
+ if (!line.text.trim()) {
2292
+ return { backgroundColor, color: TUI_THEME.muted };
2293
+ }
2294
+ return { backgroundColor, color: TUI_THEME.text };
2295
+ }
2296
+ export function chatSpanTheme(from, tone, background = "surface") {
2297
+ const baseColor = from === "user" ? TUI_THEME.accent : TUI_THEME.text;
2298
+ const backgroundColor = chatLineBackgroundColor(background);
2299
+ if (tone === "link") {
2300
+ return {
2301
+ backgroundColor,
2302
+ color: TUI_THEME.accent,
2303
+ underline: true
2304
+ };
2305
+ }
2306
+ if (tone === "code") {
2307
+ return {
2308
+ backgroundColor: TUI_THEME.rail,
2309
+ color: TUI_THEME.warning
2310
+ };
2311
+ }
2312
+ if (tone === "strong") {
2313
+ return {
2314
+ backgroundColor,
2315
+ bold: true,
2316
+ color: baseColor
2317
+ };
2318
+ }
2319
+ if (tone === "success") {
2320
+ return {
2321
+ backgroundColor,
2322
+ bold: true,
2323
+ color: TUI_THEME.success
2324
+ };
2325
+ }
2326
+ if (tone === "warning") {
2327
+ return {
2328
+ backgroundColor,
2329
+ bold: true,
2330
+ color: TUI_THEME.warning
2331
+ };
2332
+ }
2333
+ if (tone === "danger") {
2334
+ return {
2335
+ backgroundColor,
2336
+ bold: true,
2337
+ color: TUI_THEME.danger
2338
+ };
2339
+ }
2340
+ if (tone === "heading") {
2341
+ return {
2342
+ backgroundColor,
2343
+ bold: true,
2344
+ color: TUI_THEME.accent
2345
+ };
2346
+ }
2347
+ if (tone === "emphasis") {
2348
+ return {
2349
+ backgroundColor,
2350
+ color: baseColor,
2351
+ italic: true
2352
+ };
2353
+ }
2354
+ if (tone === "muted" || (tone === "prefix" && from === "system")) {
2355
+ return {
2356
+ backgroundColor,
2357
+ color: TUI_THEME.muted
2358
+ };
2359
+ }
2360
+ return {
2361
+ backgroundColor,
2362
+ color: baseColor
2363
+ };
2364
+ }
2365
+ function chatLineBackgroundColor(background) {
2366
+ return background === "rail" ? TUI_THEME.rail : TUI_THEME.surface;
2367
+ }
2368
+ export function chatLineTrailingFillWidth(line, terminalWidth) {
2369
+ return Math.max(0, chatContentWidth(terminalWidth) - displayWidth(chatLineDisplayText(line)));
2370
+ }
2371
+ export function chatMessageDisplayLines(messages, terminalWidth, maxLines = 12, options = {}) {
2372
+ return chatMessageViewport(messages, terminalWidth, maxLines, 0, options).lines;
2373
+ }
2374
+ export function chatMessageViewport(messages, terminalWidth, maxLines = 12, offsetFromBottom = 0, options = {}) {
2375
+ const contentWidth = chatContentWidth(terminalWidth);
2376
+ const expandedResultIndex = options.expandedTaskResult
2377
+ ? latestTaskResultMessageIndex(messages, options.taskId)
2378
+ : -1;
2379
+ const renderedByMessage = messages.map((message, index) => (chatSingleMessageDisplayLines(message, contentWidth, index === expandedResultIndex)));
2380
+ const viewportHeight = Math.max(1, maxLines);
2381
+ if (expandedResultIndex >= 0) {
2382
+ const requestIndex = taskResultRequestMessageIndex(messages, expandedResultIndex);
2383
+ const focused = [
2384
+ ...(requestIndex >= 0 ? renderedByMessage[requestIndex] ?? [] : []),
2385
+ ...(renderedByMessage[expandedResultIndex] ?? [])
2386
+ ];
2387
+ const maxOffset = Math.max(0, focused.length - viewportHeight);
2388
+ const clampedOffset = Math.min(Math.max(0, Math.trunc(offsetFromBottom)), maxOffset);
2389
+ return {
2390
+ lines: focused.slice(clampedOffset, clampedOffset + viewportHeight),
2391
+ clampedOffset,
2392
+ maxOffset
2393
+ };
2394
+ }
2395
+ const rendered = renderedByMessage.flat();
2396
+ const maxOffset = Math.max(0, rendered.length - viewportHeight);
2397
+ const clampedOffset = Math.min(Math.max(0, Math.trunc(offsetFromBottom)), maxOffset);
2398
+ const end = rendered.length - clampedOffset;
2399
+ const start = Math.max(0, end - viewportHeight);
2400
+ return {
2401
+ lines: rendered.slice(start, end),
2402
+ clampedOffset,
2403
+ maxOffset
2404
+ };
2405
+ }
2406
+ function taskResultRequestMessageIndex(messages, resultIndex) {
2407
+ const resultTaskId = messages[resultIndex]?.taskId;
2408
+ for (let index = resultIndex - 1; index >= 0; index -= 1) {
2409
+ const message = messages[index];
2410
+ if (!message || message.from !== "user") {
2411
+ continue;
2412
+ }
2413
+ if (!resultTaskId) {
2414
+ return message.taskId === undefined ? index : -1;
2415
+ }
2416
+ if (message.taskId === resultTaskId || message.taskId === undefined) {
2417
+ return index;
2418
+ }
2419
+ return -1;
484
2420
  }
485
- const lines = chatMessageDisplayLines(messages, terminalWidth, height ?? 12);
486
- return (_jsx(Box, { flexDirection: "column", height: height, justifyContent: height ? "flex-end" : undefined, children: lines.map((line, index) => (_jsx(Text, { color: line.from === "user" ? "cyan" : undefined, dimColor: line.from === "system" && !line.text.trim(), children: line.text || " " }, `${line.from}-${index}`))) }));
2421
+ return -1;
2422
+ }
2423
+ export function chatViewportBlankLineTheme() {
2424
+ return {
2425
+ backgroundColor: TUI_THEME.surface
2426
+ };
2427
+ }
2428
+ function ChatViewportSpacerLines({ count, terminalWidth }) {
2429
+ return (_jsx(_Fragment, { children: Array.from({ length: count }, (_, index) => (_jsx(Text, { ...chatViewportBlankLineTheme(), children: " ".repeat(chatViewportBlankLineWidth(terminalWidth)) }, `chat-spacer-${index}`))) }));
2430
+ }
2431
+ function chatViewportSpacerLineCount(contentLines, viewportHeight) {
2432
+ return viewportHeight ? Math.max(0, viewportHeight - contentLines) : 0;
2433
+ }
2434
+ function chatCompletionIsTopAligned(messages) {
2435
+ const latest = messages.at(-1);
2436
+ return latest?.from === "system" && compactSupervisorSummaryForChat(latest.text) !== null;
2437
+ }
2438
+ function chatViewportBlankLineWidth(terminalWidth) {
2439
+ return Math.max(1, chatContentWidth(terminalWidth));
2440
+ }
2441
+ function ChatLine({ line, terminalWidth }) {
2442
+ const theme = chatLineTheme(line);
2443
+ const fillWidth = chatLineTrailingFillWidth(line, terminalWidth);
2444
+ const spans = line.text && line.spans?.length ? line.spans : null;
2445
+ const backgroundColor = chatLineBackgroundColor(line.background);
2446
+ return (_jsxs(Text, { children: [spans
2447
+ ? spans.map((span, index) => (_jsx(Text, { ...chatSpanTheme(line.from, span.tone, line.background), children: span.text }, `${span.tone}-${index}`)))
2448
+ : _jsx(Text, { ...theme, children: chatLineDisplayText(line) }), fillWidth > 0 ? _jsx(Text, { backgroundColor: backgroundColor, children: " ".repeat(fillWidth) }) : null] }));
487
2449
  }
488
- export function chatMessageDisplayLines(messages, terminalWidth, maxLines = 12) {
489
- const contentWidth = Math.max(8, terminalWidth - 2);
490
- const rendered = messages.flatMap((message) => chatSingleMessageDisplayLines(message, contentWidth));
491
- return rendered.slice(-maxLines);
2450
+ function chatLineDisplayText(line) {
2451
+ return line.text || " ";
492
2452
  }
493
- function chatSingleMessageDisplayLines(message, contentWidth) {
494
- const rawLines = message.text.split(/\r?\n/);
2453
+ function chatContentWidth(terminalWidth) {
2454
+ return Math.max(8, terminalWidth - 2);
2455
+ }
2456
+ function chatSingleMessageDisplayLines(message, contentWidth, expandedTaskResult = false) {
2457
+ const rawLines = chatMessageMarkdownLines(message, expandedTaskResult, contentWidth);
495
2458
  const rendered = [];
496
2459
  rawLines.forEach((rawLine, rawIndex) => {
497
2460
  const isFirstRawLine = rawIndex === 0;
498
2461
  const firstPrefix = message.from === "user" && isFirstRawLine ? "> " : message.from === "user" ? " " : "";
499
2462
  const wrapWidth = Math.max(1, contentWidth - displayWidth(firstPrefix));
500
- const wrapped = wrapByDisplayWidth(rawLine, wrapWidth);
501
- wrapped.forEach((chunk, chunkIndex) => {
2463
+ const rawSpans = rawLine.spans;
2464
+ const visibleRawLine = chatSpanText(rawSpans);
2465
+ const wrapped = wrapChatSpans(rawSpans, wrapWidth);
2466
+ wrapped.forEach((chunkSpans, chunkIndex) => {
2467
+ const chunk = chatSpanText(chunkSpans);
502
2468
  const continuation = !isFirstRawLine || chunkIndex > 0;
503
- const prefix = message.from === "user"
504
- ? continuation
505
- ? " "
506
- : "> "
507
- : "";
2469
+ const prefix = chatLinePrefix(message.from, visibleRawLine, continuation, chunkIndex > 0, rawLine.continuationPrefix);
508
2470
  const lineWidth = Math.max(1, contentWidth - displayWidth(prefix));
509
2471
  const fitted = displayWidth(chunk) > lineWidth
510
- ? wrapByDisplayWidth(chunk, lineWidth)
511
- : [chunk];
512
- fitted.forEach((part, partIndex) => {
2472
+ ? wrapChatSpans(chunkSpans, lineWidth)
2473
+ : [chunkSpans];
2474
+ fitted.forEach((partSpans, partIndex) => {
2475
+ const part = chatSpanText(partSpans);
2476
+ const displaySpans = mergeChatSpans([
2477
+ ...(prefix ? [{ text: prefix, tone: "prefix" }] : []),
2478
+ ...partSpans
2479
+ ]);
513
2480
  rendered.push({
514
2481
  from: message.from,
515
2482
  text: `${prefix}${part}`,
516
- continuation: continuation || partIndex > 0
2483
+ continuation: continuation || partIndex > 0,
2484
+ spans: displaySpans,
2485
+ background: rawLine.background
517
2486
  });
518
2487
  });
519
2488
  });
520
2489
  });
521
2490
  return rendered;
522
2491
  }
2492
+ function chatMessageMarkdownLines(message, expandedTaskResult = false, contentWidth = Number.POSITIVE_INFINITY) {
2493
+ if (message.kind === "route") {
2494
+ return routeChatMessageLines(message.text);
2495
+ }
2496
+ if (message.from === "system" && expandedTaskResult) {
2497
+ const expanded = expandedSupervisorSummaryForChat(message.text, contentWidth);
2498
+ if (expanded) {
2499
+ return expanded;
2500
+ }
2501
+ }
2502
+ const compact = message.from === "system" ? compactSupervisorSummaryForChat(message.text) : null;
2503
+ if (compact) {
2504
+ return compact.map((line) => ({
2505
+ spans: compactChatSummarySpans(line),
2506
+ continuationPrefix: isCompactChatSummaryLine(line) ? " " : undefined
2507
+ }));
2508
+ }
2509
+ return chatMarkdownBlockLines(message.text);
2510
+ }
2511
+ function routeChatMessageLines(text) {
2512
+ const [header = "route", ...details] = text.split(/\r?\n/);
2513
+ return [
2514
+ {
2515
+ spans: routeChatHeaderSpans(header),
2516
+ background: "rail",
2517
+ continuationPrefix: " "
2518
+ },
2519
+ ...details.map((detail) => ({
2520
+ spans: mergeChatSpans([
2521
+ { text: " ", tone: "muted" },
2522
+ ...applyChatSpanTone(chatMarkdownSpans(detail), "muted")
2523
+ ]),
2524
+ background: "rail",
2525
+ continuationPrefix: " "
2526
+ }))
2527
+ ];
2528
+ }
2529
+ function routeChatHeaderSpans(header) {
2530
+ const match = header.match(/^route\s*·\s*(simple|complex)\s*·\s*(.+)$/i);
2531
+ if (!match?.[1] || !match[2]) {
2532
+ return applyChatSpanTone(chatMarkdownSpans(header), "heading");
2533
+ }
2534
+ const mode = match[1].toLowerCase();
2535
+ return [
2536
+ { text: "route", tone: "heading" },
2537
+ { text: " · ", tone: "muted" },
2538
+ { text: mode, tone: mode === "simple" ? "success" : "warning" },
2539
+ { text: ` · ${match[2].trim()}`, tone: "muted" }
2540
+ ];
2541
+ }
2542
+ function chatMarkdownBlockLines(text) {
2543
+ if (!text) {
2544
+ return [{ spans: [] }];
2545
+ }
2546
+ try {
2547
+ const lines = chatBlockTokensToLines(Lexer.lex(text));
2548
+ return lines.length > 0 ? lines : [{ spans: [] }];
2549
+ }
2550
+ catch {
2551
+ return text.split(/\r?\n/).map((line) => ({ spans: chatMarkdownSpans(line) }));
2552
+ }
2553
+ }
2554
+ function chatBlockTokensToLines(tokens) {
2555
+ return tokens.flatMap((token) => chatBlockTokenToLines(token));
2556
+ }
2557
+ function chatBlockTokenToLines(token) {
2558
+ if (token.type === "space") {
2559
+ return [{ spans: [] }];
2560
+ }
2561
+ if (token.type === "checkbox") {
2562
+ return [];
2563
+ }
2564
+ if (token.type === "heading") {
2565
+ return chatInlineMarkdownLines(tokenText(token)).map((line) => ({
2566
+ ...line,
2567
+ spans: applyChatSpanTone(line.spans, "heading")
2568
+ }));
2569
+ }
2570
+ if (token.type === "paragraph" || token.type === "text") {
2571
+ return chatInlineMarkdownLines(tokenText(token));
2572
+ }
2573
+ if (isChatListToken(token)) {
2574
+ return chatListTokenToLines(token);
2575
+ }
2576
+ if (token.type === "blockquote") {
2577
+ return chatBlockTokensToLines(tokenTokens(token)).map((line) => (chatMarkdownLineIsBlank(line)
2578
+ ? line
2579
+ : prependChatMarkdownLine(line, "│ ", "muted", " ")));
2580
+ }
2581
+ if (isChatCodeToken(token)) {
2582
+ return chatCodeTokenToLines(token);
2583
+ }
2584
+ if (token.type === "hr") {
2585
+ return [{ spans: [{ text: "· · ·", tone: "muted" }] }];
2586
+ }
2587
+ if (isChatTableToken(token)) {
2588
+ return chatTableTokenToLines(token);
2589
+ }
2590
+ if (token.type === "html") {
2591
+ return tokenText(token).split(/\r?\n/).map((line) => ({ spans: chatMarkdownSpans(line) }));
2592
+ }
2593
+ if (token.type === "def") {
2594
+ return [];
2595
+ }
2596
+ const nested = tokenTokens(token);
2597
+ if (nested.length > 0) {
2598
+ return chatBlockTokensToLines(nested);
2599
+ }
2600
+ return tokenText(token).split(/\r?\n/).map((line) => ({ spans: chatMarkdownSpans(line) }));
2601
+ }
2602
+ function chatInlineMarkdownLines(text) {
2603
+ return text.split(/\r?\n/).map((line) => ({ spans: chatMarkdownSpans(line) }));
2604
+ }
2605
+ function chatListTokenToLines(token) {
2606
+ const rendered = [];
2607
+ const start = typeof token.start === "number" ? token.start : 1;
2608
+ token.items.forEach((item, itemIndex) => {
2609
+ const marker = item.task
2610
+ ? item.checked ? "[x] " : "[ ] "
2611
+ : token.ordered ? `${start + itemIndex}. ` : "• ";
2612
+ const continuationPrefix = " ".repeat(displayWidth(marker));
2613
+ let hasItemContent = false;
2614
+ for (const itemToken of item.tokens) {
2615
+ if (isChatListToken(itemToken)) {
2616
+ const nested = chatListTokenToLines(itemToken);
2617
+ rendered.push(...nested.map((line) => (chatMarkdownLineIsBlank(line)
2618
+ ? line
2619
+ : prependChatMarkdownLine(line, continuationPrefix, "muted", continuationPrefix))));
2620
+ continue;
2621
+ }
2622
+ for (const line of chatBlockTokenToLines(itemToken)) {
2623
+ if (chatMarkdownLineIsBlank(line)) {
2624
+ rendered.push(line);
2625
+ continue;
2626
+ }
2627
+ if (!hasItemContent) {
2628
+ rendered.push(prependChatMarkdownLine(line, marker, "muted", continuationPrefix));
2629
+ hasItemContent = true;
2630
+ }
2631
+ else {
2632
+ rendered.push(prependChatMarkdownLine(line, continuationPrefix, "muted", continuationPrefix));
2633
+ }
2634
+ }
2635
+ }
2636
+ if (!hasItemContent && !item.tokens.some((itemToken) => itemToken.type === "list")) {
2637
+ rendered.push({ spans: [{ text: marker.trimEnd(), tone: "muted" }] });
2638
+ }
2639
+ });
2640
+ return rendered;
2641
+ }
2642
+ function chatCodeTokenToLines(token) {
2643
+ const rendered = [];
2644
+ const language = token.lang?.trim().split(/\s+/)[0] ?? "";
2645
+ if (language) {
2646
+ rendered.push({
2647
+ spans: [{ text: language, tone: "muted" }],
2648
+ background: "rail"
2649
+ });
2650
+ }
2651
+ const codeLines = token.text.split(/\r?\n/);
2652
+ for (const line of codeLines) {
2653
+ rendered.push({
2654
+ spans: mergeChatSpans([
2655
+ { text: "| ", tone: "muted" },
2656
+ ...(line ? [{ text: line, tone: "code" }] : [])
2657
+ ]),
2658
+ background: "rail",
2659
+ continuationPrefix: " "
2660
+ });
2661
+ }
2662
+ return rendered;
2663
+ }
2664
+ function chatTableTokenToLines(token) {
2665
+ const row = (cells, heading) => ({
2666
+ spans: mergeChatSpans([
2667
+ { text: "| ", tone: "muted" },
2668
+ ...cells.flatMap((cell, index) => [
2669
+ ...applyChatSpanTone(chatSpansFromTokens(cell.tokens, "text"), heading ? "strong" : "text"),
2670
+ { text: index === cells.length - 1 ? " |" : " | ", tone: "muted" }
2671
+ ])
2672
+ ]),
2673
+ background: "rail",
2674
+ continuationPrefix: " "
2675
+ });
2676
+ return [row(token.header, true), ...token.rows.map((cells) => row(cells, false))];
2677
+ }
2678
+ function prependChatMarkdownLine(line, prefix, tone, continuationPrefix) {
2679
+ return {
2680
+ ...line,
2681
+ spans: mergeChatSpans([{ text: prefix, tone }, ...line.spans]),
2682
+ continuationPrefix
2683
+ };
2684
+ }
2685
+ function chatMarkdownLineIsBlank(line) {
2686
+ return !chatSpanText(line.spans).trim();
2687
+ }
2688
+ function isChatListToken(token) {
2689
+ return token.type === "list" && "items" in token && Array.isArray(token.items);
2690
+ }
2691
+ function isChatCodeToken(token) {
2692
+ return token.type === "code" && "text" in token && typeof token.text === "string";
2693
+ }
2694
+ function isChatTableToken(token) {
2695
+ return (token.type === "table" &&
2696
+ "header" in token && Array.isArray(token.header) &&
2697
+ "rows" in token && Array.isArray(token.rows));
2698
+ }
2699
+ function chatMarkdownSpans(text) {
2700
+ if (!text) {
2701
+ return [];
2702
+ }
2703
+ try {
2704
+ return mergeChatSpans(chatSpansFromTokens(Lexer.lexInline(text), "text"));
2705
+ }
2706
+ catch {
2707
+ return [{ text, tone: "text" }];
2708
+ }
2709
+ }
2710
+ function chatSpansFromTokens(tokens, inheritedTone) {
2711
+ return tokens.flatMap((token) => chatSpansFromToken(token, inheritedTone));
2712
+ }
2713
+ function chatSpansFromToken(token, inheritedTone) {
2714
+ const nested = tokenTokens(token);
2715
+ if (token.type === "codespan" || token.type === "code") {
2716
+ return [{ text: tokenText(token), tone: "code" }];
2717
+ }
2718
+ if (token.type === "link" || token.type === "image") {
2719
+ const label = chatSpanText(chatSpansFromTokens(nested, "text")) || tokenText(token);
2720
+ const href = tokenHref(token);
2721
+ return mergeChatSpans([
2722
+ ...(label ? [{ text: label, tone: "link" }] : []),
2723
+ ...(href && isExternalChatLink(href) && href !== label
2724
+ ? [{ text: ` <${href}>`, tone: "muted" }]
2725
+ : [])
2726
+ ]);
2727
+ }
2728
+ if (token.type === "strong") {
2729
+ return applyChatSpanTone(chatSpansFromTokens(nested, inheritedTone), "strong");
2730
+ }
2731
+ if (token.type === "em") {
2732
+ return applyChatSpanTone(chatSpansFromTokens(nested, inheritedTone), "emphasis");
2733
+ }
2734
+ if (token.type === "del") {
2735
+ return applyChatSpanTone(chatSpansFromTokens(nested, inheritedTone), "muted");
2736
+ }
2737
+ if (token.type === "br") {
2738
+ return [{ text: " ", tone: inheritedTone }];
2739
+ }
2740
+ if (nested.length > 0) {
2741
+ return chatSpansFromTokens(nested, inheritedTone);
2742
+ }
2743
+ const text = token.type === "html"
2744
+ ? tokenText(token).replace(/<[^>]*>/g, "")
2745
+ : tokenText(token);
2746
+ return text ? [{ text: decodeHtmlEntities(text), tone: inheritedTone }] : [];
2747
+ }
2748
+ function tokenTokens(token) {
2749
+ return "tokens" in token && Array.isArray(token.tokens) ? token.tokens : [];
2750
+ }
2751
+ function tokenText(token) {
2752
+ if ("text" in token && typeof token.text === "string") {
2753
+ return token.text;
2754
+ }
2755
+ return typeof token.raw === "string" ? token.raw : "";
2756
+ }
2757
+ function tokenHref(token) {
2758
+ return "href" in token && typeof token.href === "string" ? token.href.trim() : "";
2759
+ }
2760
+ function applyChatSpanTone(spans, tone) {
2761
+ return spans.map((span) => ({
2762
+ ...span,
2763
+ tone: span.tone === "code" || span.tone === "link" ? span.tone : tone
2764
+ }));
2765
+ }
2766
+ function isExternalChatLink(href) {
2767
+ return /^(?:https?:|mailto:)/i.test(href);
2768
+ }
2769
+ function chatSpanText(spans) {
2770
+ return spans.map((span) => span.text).join("");
2771
+ }
2772
+ function mergeChatSpans(spans) {
2773
+ const merged = [];
2774
+ for (const span of spans) {
2775
+ if (!span.text) {
2776
+ continue;
2777
+ }
2778
+ const previous = merged.at(-1);
2779
+ if (previous?.tone === span.tone) {
2780
+ previous.text += span.text;
2781
+ }
2782
+ else {
2783
+ merged.push({ ...span });
2784
+ }
2785
+ }
2786
+ return merged;
2787
+ }
2788
+ function wrapChatSpans(spans, maxWidth) {
2789
+ const text = chatSpanText(spans);
2790
+ if (!text) {
2791
+ return [[]];
2792
+ }
2793
+ const chunks = wrapByDisplayWidth(text, maxWidth);
2794
+ let cursor = 0;
2795
+ return chunks.map((chunk) => {
2796
+ const found = text.indexOf(chunk, cursor);
2797
+ const start = found >= 0 ? found : cursor;
2798
+ const end = start + chunk.length;
2799
+ cursor = end;
2800
+ return sliceChatSpans(spans, start, end);
2801
+ });
2802
+ }
2803
+ function sliceChatSpans(spans, start, end) {
2804
+ const sliced = [];
2805
+ let offset = 0;
2806
+ for (const span of spans) {
2807
+ const spanStart = offset;
2808
+ const spanEnd = spanStart + span.text.length;
2809
+ offset = spanEnd;
2810
+ const overlapStart = Math.max(start, spanStart);
2811
+ const overlapEnd = Math.min(end, spanEnd);
2812
+ if (overlapStart < overlapEnd) {
2813
+ sliced.push({
2814
+ text: span.text.slice(overlapStart - spanStart, overlapEnd - spanStart),
2815
+ tone: span.tone
2816
+ });
2817
+ }
2818
+ }
2819
+ return mergeChatSpans(sliced);
2820
+ }
2821
+ function chatLinePrefix(from, rawLine, continuation, wrappedContinuation, markdownContinuationPrefix) {
2822
+ if (from === "user") {
2823
+ return continuation ? " " : "> ";
2824
+ }
2825
+ if (from === "system" && wrappedContinuation) {
2826
+ return markdownContinuationPrefix ?? (isCompactChatSummaryLine(rawLine) ? " " : "");
2827
+ }
2828
+ return "";
2829
+ }
2830
+ function expandedSupervisorSummaryForChat(text, contentWidth) {
2831
+ const result = parseTaskResultSummary(text);
2832
+ if (!result) {
2833
+ return null;
2834
+ }
2835
+ const outcome = taskResultOutcomeDisplay(result.outcome);
2836
+ const lines = [
2837
+ {
2838
+ spans: taskResultHeaderSpans(outcome, contentWidth),
2839
+ background: "rail",
2840
+ continuationPrefix: " "
2841
+ }
2842
+ ];
2843
+ const sections = [
2844
+ { key: "requirements", title: "Requirements" },
2845
+ { key: "implementation", title: "Implementation" },
2846
+ { key: "changes", title: "Changes", optional: true },
2847
+ { key: "review", title: "Review" },
2848
+ { key: "verification", title: "Verification", optional: true },
2849
+ { key: "findings", title: "Findings" }
2850
+ ];
2851
+ for (const section of sections) {
2852
+ if (section.optional && !result.sections[section.key].trim()) {
2853
+ continue;
2854
+ }
2855
+ lines.push({
2856
+ spans: [{ text: section.title, tone: "heading" }],
2857
+ background: "rail",
2858
+ continuationPrefix: " "
2859
+ });
2860
+ lines.push(...taskResultSectionMarkdownLines(result.sections[section.key], section.key, result.outcome));
2861
+ }
2862
+ return lines;
2863
+ }
2864
+ function taskResultSectionMarkdownLines(content, section, outcome) {
2865
+ if (!content.trim()) {
2866
+ return [{
2867
+ spans: [
2868
+ { text: " ", tone: "prefix" },
2869
+ { text: "none", tone: "muted" }
2870
+ ],
2871
+ continuationPrefix: " "
2872
+ }];
2873
+ }
2874
+ return chatMarkdownBlockLines(content)
2875
+ .filter((line) => !chatMarkdownLineIsBlank(line))
2876
+ .map((line) => {
2877
+ const text = chatSpanText(line.spans).trim();
2878
+ const decisionSection = section === "review" || section === "verification";
2879
+ const semanticTone = decisionSection && /^(?:Critic decision:\s*)?APPROVED\b/i.test(text)
2880
+ ? "success"
2881
+ : decisionSection && /^(?:Critic decision:\s*)?(?:REVISION_REQUIRED|REJECTED|FAILED)\b/i.test(text)
2882
+ ? "danger"
2883
+ : section === "findings" && outcome === "revision-required"
2884
+ ? "warning"
2885
+ : null;
2886
+ const spans = semanticTone ? applyChatSpanTone(line.spans, semanticTone) : line.spans;
2887
+ return prependChatMarkdownLine({ ...line, spans }, " ", "prefix", " ");
2888
+ });
2889
+ }
2890
+ function taskResultOutcomeDisplay(outcome) {
2891
+ if (outcome === "approved") {
2892
+ return { label: "APPROVED", shortLabel: "APPROVED", tone: "success" };
2893
+ }
2894
+ if (outcome === "revision-required") {
2895
+ return { label: "REVISION REQUIRED", shortLabel: "REVISION", tone: "danger" };
2896
+ }
2897
+ return { label: "COMPLETE", shortLabel: "COMPLETE", tone: "warning" };
2898
+ }
2899
+ function taskResultHeaderSpans(outcome, maxWidth) {
2900
+ const details = ["complex task completed", "complex", null];
2901
+ const titleWith = (detail, label) => (mergeChatSpans([
2902
+ { text: "done", tone: "success" },
2903
+ ...(detail
2904
+ ? [
2905
+ { text: " · ", tone: "muted" },
2906
+ { text: detail, tone: "text" }
2907
+ ]
2908
+ : []),
2909
+ { text: " · ", tone: "muted" },
2910
+ { text: label, tone: outcome.tone }
2911
+ ]));
2912
+ const candidates = [
2913
+ ...details.map((detail) => titleWith(detail, outcome.label)),
2914
+ [{ text: outcome.label, tone: outcome.tone }]
2915
+ ];
2916
+ if (outcome.shortLabel !== outcome.label) {
2917
+ candidates.push(...details.map((detail) => titleWith(detail, outcome.shortLabel)), [{ text: outcome.shortLabel, tone: outcome.tone }]);
2918
+ }
2919
+ const width = Math.max(1, Math.trunc(maxWidth));
2920
+ return candidates.find((candidate) => displayWidth(chatSpanText(candidate)) <= width)
2921
+ ?? [{ text: compactEndByDisplayWidth(outcome.shortLabel, width), tone: outcome.tone }];
2922
+ }
2923
+ function compactSupervisorSummaryForChat(text) {
2924
+ const result = parseTaskResultSummary(text);
2925
+ if (!result) {
2926
+ return null;
2927
+ }
2928
+ const sections = [
2929
+ { label: "requirements", value: result.sections.requirements },
2930
+ { label: "actor", value: result.sections.implementation },
2931
+ { label: "review", value: result.sections.review },
2932
+ { label: "findings", value: result.sections.findings }
2933
+ ];
2934
+ return [
2935
+ "done · complex task completed",
2936
+ ...sections.map((section) => (`${section.label} · ${chatSummarySectionValue(section.value.split(/\r?\n/))}`))
2937
+ ];
2938
+ }
2939
+ function chatSummarySectionValue(lines) {
2940
+ for (const line of lines) {
2941
+ const cleaned = cleanChatSummaryLine(line);
2942
+ if (cleaned && !isChatSummaryHeading(cleaned)) {
2943
+ return cleaned;
2944
+ }
2945
+ }
2946
+ return "none";
2947
+ }
2948
+ function cleanChatSummaryLine(line) {
2949
+ const cleaned = line
2950
+ .trim()
2951
+ .replace(/^#{1,6}\s+/, "")
2952
+ .replace(/^[-*]\s+/, "")
2953
+ .replace(/^\d+\.\s+/, "")
2954
+ .replace(/`([^`]+)`/g, "$1")
2955
+ .replace(/\*\*([^*]+)\*\*/g, "$1")
2956
+ .replace(/__([^_]+)__/g, "$1")
2957
+ .trim();
2958
+ return cleaned === "(empty)" ? "none" : cleaned;
2959
+ }
2960
+ function isChatSummaryHeading(line) {
2961
+ return /^(?:requirements|actor work|worklog|changed files|critic review|review|verification|critic findings):?$/i.test(line);
2962
+ }
2963
+ function isCompactChatSummaryLine(line) {
2964
+ return /^(?:done|requirements|actor|review|findings) · /i.test(line.trim());
2965
+ }
2966
+ function compactChatSummarySpans(line) {
2967
+ const match = line.match(/^([^·]+?)\s+·\s+(.+)$/u);
2968
+ if (!match) {
2969
+ return chatMarkdownSpans(line);
2970
+ }
2971
+ const label = (match[1] ?? "").trim();
2972
+ const value = (match[2] ?? "").trim();
2973
+ const labelTone = label === "done" ? "success" : "muted";
2974
+ const valueSpans = label === "review" && /^APPROVED\b/i.test(value)
2975
+ ? [{ text: value, tone: "success" }]
2976
+ : label === "findings" && /^none$/i.test(value)
2977
+ ? [{ text: value, tone: "muted" }]
2978
+ : chatMarkdownSpans(value);
2979
+ return mergeChatSpans([
2980
+ { text: label, tone: labelTone },
2981
+ { text: " · ", tone: "muted" },
2982
+ ...valueSpans
2983
+ ]);
2984
+ }
523
2985
  function ChatEmptyState({ cwd, activeTaskId, terminalWidth }) {
524
- const contentWidth = Math.max(8, terminalWidth - 2);
525
- return (_jsx(Box, { flexDirection: "column", children: _jsx(Text, { color: "green", bold: true, children: chatEmptyStateDisplayLine(cwd, activeTaskId, contentWidth) }) }));
2986
+ const contentWidth = chatContentWidth(terminalWidth);
2987
+ const line = chatEmptyStateDisplayLine(cwd, activeTaskId, contentWidth);
2988
+ const fillWidth = Math.max(0, contentWidth - displayWidth(line));
2989
+ return (_jsx(Box, { flexDirection: "column", children: _jsxs(Text, { children: [_jsx(Text, { ...chatEmptyStateTheme(), children: line }), fillWidth > 0 ? _jsx(Text, { backgroundColor: TUI_THEME.surface, children: " ".repeat(fillWidth) }) : null] }) }));
2990
+ }
2991
+ export function chatEmptyStateTrailingFillWidth(cwd, activeTaskId, terminalWidth) {
2992
+ const contentWidth = chatContentWidth(terminalWidth);
2993
+ const line = chatEmptyStateDisplayLine(cwd, activeTaskId, contentWidth);
2994
+ return Math.max(0, contentWidth - displayWidth(line));
2995
+ }
2996
+ export function chatEmptyStateTheme() {
2997
+ return {
2998
+ backgroundColor: TUI_THEME.surface,
2999
+ bold: true,
3000
+ color: TUI_THEME.success
3001
+ };
526
3002
  }
527
3003
  function chatEmptyStateDisplayLine(cwd, activeTaskId, contentWidth) {
528
3004
  const project = compactChatWorkspace(cwd);
@@ -530,6 +3006,9 @@ function chatEmptyStateDisplayLine(cwd, activeTaskId, contentWidth) {
530
3006
  if (contentWidth < 14) {
531
3007
  return compactChatText(project || task || "ready", contentWidth);
532
3008
  }
3009
+ if (contentWidth >= 38) {
3010
+ return "ready";
3011
+ }
533
3012
  const roomyPrefix = task ? `ready · ${project} · ` : "ready · ";
534
3013
  const roomyValue = task || project;
535
3014
  const roomy = `${roomyPrefix}${compactChatText(roomyValue, Math.max(1, contentWidth - displayWidth(roomyPrefix)))}`;
@@ -599,6 +3078,54 @@ function upsertWorker(workers, worker) {
599
3078
  }
600
3079
  return [...workers, worker];
601
3080
  }
3081
+ function restoredWorkerStatusLine(taskId, workers) {
3082
+ if (!taskId) {
3083
+ return null;
3084
+ }
3085
+ const state = { taskId };
3086
+ const restored = (workers ?? []).map((worker) => {
3087
+ return {
3088
+ role: worker.role,
3089
+ engine: worker.engine,
3090
+ label: worker.label,
3091
+ status: worker.runtimeStatus ? formatWorkerRuntimeStatus(worker.runtimeStatus) : "waiting"
3092
+ };
3093
+ });
3094
+ if (restored.length === 0) {
3095
+ return state;
3096
+ }
3097
+ state.workers = restored.map(({ label, status }) => ({ label, status }));
3098
+ for (const worker of restored) {
3099
+ state[worker.role] = worker.status;
3100
+ if (worker.role === "main") {
3101
+ state.mainEngine = worker.engine;
3102
+ }
3103
+ }
3104
+ return state;
3105
+ }
3106
+ export function statusLineWithWorkerRefs(status, workers) {
3107
+ if (!status || workers.length === 0) {
3108
+ return status;
3109
+ }
3110
+ const restored = restoredWorkerStatusLine(status.taskId, workers);
3111
+ if (!restored?.workers?.length) {
3112
+ return status;
3113
+ }
3114
+ const next = {
3115
+ ...status,
3116
+ workers: restored.workers
3117
+ };
3118
+ for (const role of ["judge", "actor", "critic"]) {
3119
+ if (restored[role]) {
3120
+ next[role] = restored[role];
3121
+ }
3122
+ }
3123
+ if (restored.main && (!terminalMainStatus(status.main) || terminalMainStatus(restored.main))) {
3124
+ next.main = restored.main;
3125
+ next.mainEngine = restored.mainEngine;
3126
+ }
3127
+ return next;
3128
+ }
602
3129
  function workerTitle(workers, selectedWorkerIndex) {
603
3130
  const worker = workers[selectedWorkerIndex];
604
3131
  if (!worker) {
@@ -606,42 +3133,177 @@ function workerTitle(workers, selectedWorkerIndex) {
606
3133
  }
607
3134
  return `${worker.label} output (${selectedWorkerIndex + 1}/${workers.length})`;
608
3135
  }
609
- export function appContentHeight(rows, hasError = false) {
3136
+ function sameWorkerRuntimeStatus(left, right) {
3137
+ if (!left || !right) {
3138
+ return left === right;
3139
+ }
3140
+ return left.worker_id === right.worker_id &&
3141
+ left.feature_id === right.feature_id &&
3142
+ left.feature_title === right.feature_title &&
3143
+ left.role === right.role &&
3144
+ left.engine === right.engine &&
3145
+ left.state === right.state &&
3146
+ left.phase === right.phase &&
3147
+ left.last_event_at === right.last_event_at &&
3148
+ left.summary === right.summary &&
3149
+ left.native_session_id === right.native_session_id;
3150
+ }
3151
+ function mainWorkerProgress(status, policy, nowMs = Date.now()) {
3152
+ const initialized = status.state === "idle" && status.phase === "initialized";
3153
+ if (!initialized && status.state !== "starting" && status.state !== "running") {
3154
+ return undefined;
3155
+ }
3156
+ const lastEventMs = Date.parse(status.last_event_at);
3157
+ const firstOutputTimeoutMs = effectiveWorkerWatchdog(policy?.firstOutputTimeoutMs, policy?.timeoutMs);
3158
+ const idleTimeoutMs = effectiveWorkerWatchdog(policy?.idleTimeoutMs, policy?.timeoutMs);
3159
+ return {
3160
+ phase: status.phase,
3161
+ elapsedMs: Number.isFinite(lastEventMs) ? Math.max(0, nowMs - lastEventMs) : 0,
3162
+ ...(firstOutputTimeoutMs
3163
+ ? { firstOutputTimeoutMs }
3164
+ : {}),
3165
+ ...(idleTimeoutMs
3166
+ ? { idleTimeoutMs }
3167
+ : {})
3168
+ };
3169
+ }
3170
+ function applyMainRuntimeStatus(current, worker, policy, nowMs = Date.now()) {
3171
+ const runtimeStatus = worker.runtimeStatus;
3172
+ if (!current || !runtimeStatus) {
3173
+ return current;
3174
+ }
3175
+ if (terminalMainStatus(current.main)
3176
+ && activeWorkerRuntime(runtimeStatus)
3177
+ && !recoveringWorkerRuntime(runtimeStatus)) {
3178
+ return current;
3179
+ }
3180
+ return {
3181
+ ...current,
3182
+ main: formatWorkerRuntimeStatus(runtimeStatus),
3183
+ mainEngine: worker.engine,
3184
+ mainProgress: mainWorkerProgress(runtimeStatus, policy, nowMs)
3185
+ };
3186
+ }
3187
+ function terminalMainStatus(status) {
3188
+ const state = status?.trim().split(/[\s/:·]/, 1)[0]?.toLowerCase();
3189
+ return state === "done"
3190
+ || state === "fail"
3191
+ || state === "failed"
3192
+ || state === "error"
3193
+ || state === "stop"
3194
+ || state === "cancelled"
3195
+ || state === "canceled";
3196
+ }
3197
+ function activeWorkerRuntime(status) {
3198
+ return status.state === "idle" || status.state === "starting" || status.state === "running";
3199
+ }
3200
+ function recoveringWorkerRuntime(status) {
3201
+ return activeWorkerRuntime(status) && status.phase === "native-resume-fallback";
3202
+ }
3203
+ function newerTerminalRuntime(current, incoming) {
3204
+ if (!current) {
3205
+ return false;
3206
+ }
3207
+ const currentTime = Date.parse(current.last_event_at);
3208
+ const incomingTime = Date.parse(incoming.last_event_at);
3209
+ if (!activeWorkerRuntime(current) && activeWorkerRuntime(incoming)) {
3210
+ if (recoveringWorkerRuntime(incoming)) {
3211
+ return Number.isFinite(currentTime)
3212
+ && Number.isFinite(incomingTime)
3213
+ && currentTime > incomingTime;
3214
+ }
3215
+ return true;
3216
+ }
3217
+ return Number.isFinite(currentTime)
3218
+ && Number.isFinite(incomingTime)
3219
+ && currentTime > incomingTime;
3220
+ }
3221
+ function sameWorkerNavigationTargets(left, right) {
3222
+ return sameNumberArray(left.searchOffsets, right.searchOffsets) &&
3223
+ sameNumberArray(left.searchLineIndexes, right.searchLineIndexes) &&
3224
+ sameNumberArray(left.errorOffsets, right.errorOffsets) &&
3225
+ sameNumberArray(left.diffOffsets, right.diffOffsets);
3226
+ }
3227
+ function sameNumberArray(left, right) {
3228
+ return left.length === right.length && left.every((value, index) => value === right[index]);
3229
+ }
3230
+ function previewRouteFallbackChoice(route, choice) {
3231
+ const mode = choice === "main" ? "simple" : choice === "parallel" ? "complex" : route.mode;
3232
+ return {
3233
+ ...route,
3234
+ mode,
3235
+ suggested_roles: mode === "complex" ? ["judge", "actor", "critic"] : [],
3236
+ router_fallback_resolution: choice === "cancel" ? "cancelled" : choice
3237
+ };
3238
+ }
3239
+ export function appContentHeight(rows, hasError = false, showStatusBar = true) {
610
3240
  const headerRows = 1;
611
3241
  const inputRows = 1;
612
- const statusRows = 1;
3242
+ const statusRows = showStatusBar ? 1 : 0;
613
3243
  const errorRows = hasError ? 1 : 0;
614
3244
  return Math.max(2, rows - headerRows - inputRows - statusRows - errorRows);
615
3245
  }
616
- function NativeAttachView({ attach }) {
3246
+ function NativeAttachView({ attach, viewportHeight }) {
617
3247
  if (!attach) {
618
- return _jsx(Text, { children: "Starting native attach..." });
3248
+ return _jsx(Text, { ...nativeAttachStartingTheme(), children: nativeAttachStartingText() });
619
3249
  }
620
3250
  const terminalWidth = process.stdout.columns || 120;
621
3251
  const panelWidth = nativeAttachPanelRailWidth(terminalWidth);
622
3252
  const scroll = attach.screen.scrollState();
623
3253
  const scrollLabel = nativeTerminalScrollDisplay(scroll.offset, scroll.maxOffset, terminalWidth);
624
3254
  const title = nativeAttachTitleDisplay(attach.launch.label, attach.launch.sessionId, attach.closedCode, panelWidth, scrollLabel);
625
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(NativeAttachTitleRail, { title: title, width: panelWidth }), _jsx(TerminalOutput, { lines: attach.screen.styledSnapshotLines({ showCursor: true }) })] }));
3255
+ const outputMinLines = Math.max(1, viewportHeight - 1);
3256
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(NativeAttachTitleRail, { title: title, width: panelWidth }), _jsx(TerminalOutput, { lines: attach.hasOutput ? attach.screen.styledSnapshotLines({ showCursor: true }) : [], minLines: outputMinLines, width: panelWidth })] }));
3257
+ }
3258
+ export function nativeAttachStartingTheme() {
3259
+ return {
3260
+ backgroundColor: TUI_THEME.surface,
3261
+ color: TUI_THEME.muted
3262
+ };
3263
+ }
3264
+ export function nativeAttachStartingText() {
3265
+ return "opening native session";
626
3266
  }
627
3267
  function NativeAttachTitleRail({ title, width }) {
628
3268
  const titleText = ` ${title} `;
3269
+ const segments = nativeAttachTitleSegments(title);
629
3270
  const renderWidth = typeof process.stdout.columns === "number"
630
3271
  ? width
631
3272
  : null;
632
3273
  const trailingWidth = renderWidth === null
633
3274
  ? 0
634
3275
  : Math.max(0, renderWidth - displayWidth(titleText));
635
- return (_jsxs(Box, { children: [_jsx(Text, { backgroundColor: NATIVE_PANEL_TITLE_BACKGROUND, color: "white", bold: true, children: titleText }), trailingWidth > 0 ? _jsx(Text, { backgroundColor: NATIVE_PANEL_BACKGROUND, children: " ".repeat(trailingWidth) }) : null] }));
3276
+ return (_jsxs(Box, { children: [_jsx(Text, { backgroundColor: TUI_THEME.chrome, children: " " }), segments.map((segment, index) => (_jsx(Text, { backgroundColor: TUI_THEME.chrome, color: segment.tone === "identity" ? TUI_THEME.accent : segment.tone === "danger" ? TUI_THEME.danger : TUI_THEME.muted, bold: segment.tone === "identity", children: segment.text }, `${segment.tone}-${index}`))), _jsx(Text, { backgroundColor: TUI_THEME.chrome, children: " " }), trailingWidth > 0 ? _jsx(Text, { backgroundColor: TUI_THEME.chrome, children: " ".repeat(trailingWidth) }) : null] }));
3277
+ }
3278
+ function nativeAttachTitleSegments(title) {
3279
+ const parts = title.split(" · ");
3280
+ if (parts.length > 1) {
3281
+ return parts.map((part, index) => ({
3282
+ text: index === 0 ? part : ` · ${part}`,
3283
+ tone: index === 0 ? "identity" : isNativeExitTitlePart(part) ? "danger" : "muted"
3284
+ }));
3285
+ }
3286
+ const exitMatch = title.match(/^(.*?)(\s+(?:exited\s+\d+|exit:\d+))$/);
3287
+ if (exitMatch?.[1]) {
3288
+ return [
3289
+ { text: exitMatch[1], tone: "identity" },
3290
+ { text: exitMatch[2] ?? "", tone: "danger" }
3291
+ ];
3292
+ }
3293
+ return [{ text: title, tone: isNativeExitTitlePart(title) ? "danger" : "identity" }];
3294
+ }
3295
+ function isNativeExitTitlePart(text) {
3296
+ return /^(?:exited\s+\d+|exit:\d+)$/.test(text.trim());
636
3297
  }
637
3298
  function nativeAttachPanelRailWidth(terminalWidth) {
638
3299
  const renderWidth = typeof process.stdout.columns === "number"
639
3300
  ? Math.max(1, Math.min(terminalWidth, process.stdout.columns))
640
3301
  : terminalWidth;
641
- return Math.max(1, renderWidth - 4);
3302
+ return Math.max(1, renderWidth - 2);
642
3303
  }
643
3304
  export function nativeAttachTitleDisplay(label, sessionId, closedCode, terminalWidth = process.stdout.columns || 120, scrollLabel = null) {
644
3305
  const exit = closedCode === null ? "" : `exit:${closedCode}`;
3306
+ const exitReadable = closedCode === null ? "" : `exited ${closedCode}`;
645
3307
  const contentWidth = Math.max(1, terminalWidth - 2);
646
3308
  if (terminalWidth < 24) {
647
3309
  return tinyNativeAttachTitle(label, exit ? ` ${exit}` : "", contentWidth);
@@ -650,9 +3312,18 @@ export function nativeAttachTitleDisplay(label, sessionId, closedCode, terminalW
650
3312
  const roleLabel = compactNativeAttachRole(label);
651
3313
  if (exit) {
652
3314
  return firstNativeTitleThatFits(withNativeTitleSuffix([
3315
+ ...(terminalWidth >= 32
3316
+ ? [
3317
+ `native ${compactLabel} · ${exitReadable}`,
3318
+ `native ${roleLabel} · ${exitReadable}`,
3319
+ `${roleLabel} ${exitReadable}`,
3320
+ exitReadable
3321
+ ]
3322
+ : []),
653
3323
  `native ${compactLabel} · ${exit}`,
654
3324
  `native ${roleLabel} · ${exit}`,
655
- `${roleLabel} ${exit}`
3325
+ `${roleLabel} ${exit}`,
3326
+ exit
656
3327
  ], scrollLabel), contentWidth);
657
3328
  }
658
3329
  const prefix = `native ${compactLabel}`;
@@ -681,11 +3352,14 @@ export function nativeTerminalScrollDisplay(offset, maxOffset, width) {
681
3352
  export function nativeAttachTerminalColumns(terminalWidth = process.stdout.columns || 120) {
682
3353
  return Math.max(1, terminalWidth - 2);
683
3354
  }
3355
+ export function nativeAttachTerminalRows(terminalRows = process.stdout.rows || 30, hasError = false, showStatusBar = true) {
3356
+ return Math.max(1, appContentHeight(terminalRows, hasError, showStatusBar) - 2);
3357
+ }
684
3358
  export function nativeAttachExitLine(code, nativeTerminalCols) {
685
3359
  const contentWidth = Math.max(1, nativeTerminalCols);
686
3360
  const candidates = [
687
- `[process exited with code ${code}]`,
688
- `[exit ${code}]`,
3361
+ `process exited · code ${code}`,
3362
+ `exit code ${code}`,
689
3363
  `exit:${code}`
690
3364
  ];
691
3365
  return firstNativeTitleThatFits(candidates, contentWidth);
@@ -740,3 +3414,6 @@ function compactNativeAttachRole(label) {
740
3414
  function compactNativeSessionForTitle(sessionId, maxLength) {
741
3415
  return compactEndByDisplayWidth(sessionId, Math.min(maxLength, 16));
742
3416
  }
3417
+ function isAbortError(error) {
3418
+ return error instanceof Error && error.name === "AbortError";
3419
+ }