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.
- package/.parallel-codex/config.example.toml +136 -3
- package/README.md +299 -21
- package/dist/bootstrap.js +37 -17
- package/dist/cli-args.js +34 -3
- package/dist/cli-help.js +20 -0
- package/dist/cli-startup-preflight.js +18 -0
- package/dist/cli-startup-recovery.js +82 -0
- package/dist/cli-workspace-picker.js +330 -0
- package/dist/cli-workspace-transition.js +33 -0
- package/dist/cli-workspace.js +7 -71
- package/dist/cli.js +234 -24
- package/dist/core/collaboration-timeline.js +268 -0
- package/dist/core/config-errors.js +14 -0
- package/dist/core/config.js +297 -109
- package/dist/core/file-store.js +119 -6
- package/dist/core/lease-finalization.js +22 -0
- package/dist/core/paths.js +7 -0
- package/dist/core/process-identity.js +48 -0
- package/dist/core/process-mutation-turn.js +128 -0
- package/dist/core/process-ownership.js +276 -0
- package/dist/core/process-tree.js +90 -0
- package/dist/core/router-audit.js +155 -0
- package/dist/core/router-redaction.js +31 -0
- package/dist/core/router.js +462 -35
- package/dist/core/session-index.js +412 -88
- package/dist/core/session-manager.js +1110 -40
- package/dist/core/task-session-details.js +175 -0
- package/dist/core/task-state-machine.js +18 -0
- package/dist/core/workspace-commit-recovery.js +118 -0
- package/dist/core/workspace.js +19 -11
- package/dist/doctor.js +373 -34
- package/dist/domain/schemas.js +142 -7
- package/dist/orchestrator/collaboration-channel.js +289 -6
- package/dist/orchestrator/feature-plan.js +70 -0
- package/dist/orchestrator/final-acceptance.js +86 -0
- package/dist/orchestrator/judge-artifacts.js +236 -0
- package/dist/orchestrator/orchestrator.js +2086 -203
- package/dist/orchestrator/prompts.js +168 -5
- package/dist/orchestrator/supervisor-summary.js +56 -2
- package/dist/orchestrator/workspace-sandbox.js +927 -0
- package/dist/tui/App.js +3187 -161
- package/dist/tui/AppShell.js +196 -25
- package/dist/tui/CollaborationTimelineView.js +327 -0
- package/dist/tui/FeatureBoardView.js +232 -0
- package/dist/tui/InputBar.js +581 -57
- package/dist/tui/RouterDiagnosticsView.js +469 -0
- package/dist/tui/StatusBar.js +610 -57
- package/dist/tui/StatusDetailView.js +164 -0
- package/dist/tui/TaskSessionDetailView.js +222 -0
- package/dist/tui/TaskSessionsView.js +210 -0
- package/dist/tui/TerminalOutput.js +53 -9
- package/dist/tui/WorkerOutputView.js +1404 -161
- package/dist/tui/WorkerOverviewView.js +269 -0
- package/dist/tui/chat-history.js +25 -0
- package/dist/tui/chat-input.js +67 -19
- package/dist/tui/chat-paste.js +76 -0
- package/dist/tui/display-width.js +41 -3
- package/dist/tui/incremental-text-file.js +101 -0
- package/dist/tui/keyboard.js +49 -0
- package/dist/tui/markdown-text.js +14 -0
- package/dist/tui/raw-input-decoder.js +3 -0
- package/dist/tui/scrolling.js +2 -1
- package/dist/tui/status-line.js +360 -11
- package/dist/tui/task-memory.js +13 -1
- package/dist/tui/task-result.js +105 -0
- package/dist/tui/terminal-screen.js +13 -1
- package/dist/tui/theme-contrast.js +144 -0
- package/dist/tui/theme-preview.js +109 -0
- package/dist/tui/theme.js +158 -0
- package/dist/version.js +1 -1
- package/dist/workers/capabilities.js +213 -0
- package/dist/workers/live-probe.js +177 -0
- package/dist/workers/mock-adapter.js +73 -8
- package/dist/workers/native-attach.js +106 -16
- package/dist/workers/process-adapter.js +572 -77
- package/dist/workers/provider.js +26 -0
- package/dist/workers/registry.js +12 -20
- 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 {
|
|
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 {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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 [
|
|
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(
|
|
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 [
|
|
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
|
|
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
|
|
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 =
|
|
57
|
-
const
|
|
58
|
-
|
|
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
|
-
|
|
79
|
-
}, [
|
|
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
|
|
99
|
-
|
|
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
|
-
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
|
147
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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,
|
|
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 === "
|
|
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
|
-
|
|
199
|
-
setView(
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
|
|
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,
|
|
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(
|
|
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
|
-
|
|
244
|
-
|
|
245
|
-
if (update.exit) {
|
|
246
|
-
exitRef.current();
|
|
1413
|
+
if (chunk === "\u0012" && !busyRef.current) {
|
|
1414
|
+
void retryRef.current();
|
|
247
1415
|
return;
|
|
248
1416
|
}
|
|
249
|
-
|
|
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
|
-
|
|
253
|
-
|
|
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
|
-
|
|
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,
|
|
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(
|
|
1548
|
+
setAttachError(NO_WORKERS_ATTACH_MESSAGE);
|
|
333
1549
|
return;
|
|
334
1550
|
}
|
|
335
1551
|
void attachSelectedWorker(worker);
|
|
336
1552
|
}
|
|
337
|
-
}, { isActive: view
|
|
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 (
|
|
342
|
-
?
|
|
343
|
-
|
|
344
|
-
config,
|
|
345
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
1624
|
+
if (attachIsCurrent()) {
|
|
1625
|
+
setAttachError(error.message);
|
|
1626
|
+
}
|
|
383
1627
|
}
|
|
384
1628
|
});
|
|
385
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
]);
|
|
2455
|
+
if (collaborationLoadSequenceRef.current === sequence) {
|
|
2456
|
+
setCollaborationError(error instanceof Error ? error.message : String(error));
|
|
2457
|
+
}
|
|
467
2458
|
}
|
|
468
2459
|
finally {
|
|
469
|
-
|
|
470
|
-
|
|
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
|
-
|
|
474
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
486
|
-
|
|
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
|
-
|
|
489
|
-
|
|
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
|
|
494
|
-
const
|
|
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
|
|
501
|
-
|
|
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
|
|
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
|
-
?
|
|
511
|
-
: [
|
|
512
|
-
fitted.forEach((
|
|
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 =
|
|
525
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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 -
|
|
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
|
-
`
|
|
688
|
-
`
|
|
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
|
+
}
|