parallel-codex-tui 0.1.3 → 0.1.5

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