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