parallel-codex-tui 0.1.4 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.parallel-codex/config.example.toml +46 -0
- package/README.md +76 -17
- package/dist/cli-startup-preflight.js +18 -0
- package/dist/cli-startup-recovery.js +13 -1
- package/dist/cli.js +18 -6
- package/dist/core/collaboration-timeline.js +9 -2
- package/dist/core/config.js +161 -103
- package/dist/core/router.js +1 -1
- package/dist/core/session-index.js +234 -61
- package/dist/core/session-manager.js +25 -1
- package/dist/core/task-session-details.js +175 -0
- package/dist/core/task-state-machine.js +10 -9
- package/dist/doctor.js +58 -39
- package/dist/domain/schemas.js +15 -1
- package/dist/orchestrator/collaboration-channel.js +35 -3
- package/dist/orchestrator/final-acceptance.js +86 -0
- package/dist/orchestrator/orchestrator.js +405 -69
- package/dist/orchestrator/prompts.js +42 -3
- package/dist/orchestrator/workspace-sandbox.js +16 -0
- package/dist/tui/App.js +401 -52
- package/dist/tui/AppShell.js +9 -3
- package/dist/tui/FeatureBoardView.js +7 -2
- package/dist/tui/InputBar.js +82 -15
- package/dist/tui/StatusBar.js +1 -1
- package/dist/tui/StatusDetailView.js +164 -0
- package/dist/tui/TaskSessionDetailView.js +222 -0
- package/dist/tui/TaskSessionsView.js +5 -2
- package/dist/tui/WorkerOutputView.js +1 -0
- package/dist/tui/WorkerOverviewView.js +23 -4
- package/dist/tui/keyboard.js +3 -0
- package/dist/tui/status-line.js +42 -0
- package/dist/version.js +1 -1
- package/dist/workers/capabilities.js +4 -3
- package/dist/workers/live-probe.js +4 -3
- package/dist/workers/mock-adapter.js +37 -5
- package/dist/workers/native-attach.js +32 -17
- package/dist/workers/process-adapter.js +2 -0
- package/dist/workers/provider.js +26 -0
- package/dist/workers/registry.js +12 -22
- package/package.json +7 -1
package/dist/tui/App.js
CHANGED
|
@@ -5,7 +5,7 @@ import { Box, Text, useApp, useInput, useStdin } from "ink";
|
|
|
5
5
|
import { Lexer } from "marked";
|
|
6
6
|
import { readJson } from "../core/file-store.js";
|
|
7
7
|
import { WorkerStatusSchema } from "../domain/schemas.js";
|
|
8
|
-
import { effectiveWorkerWatchdog, formatRoutePendingStatus,
|
|
8
|
+
import { effectiveWorkerWatchdog, formatRoutePendingStatus, formatRoutePendingSummaryStatus, formatRouteStatus, formatRouteSummaryStatus, formatStatusLine, formatWorkerRuntimeStatus } from "./status-line.js";
|
|
9
9
|
import { applyChatInputChunk, insertChatPaste } from "./chat-input.js";
|
|
10
10
|
import { chatRequestHistory, navigateChatDraftHistory } from "./chat-history.js";
|
|
11
11
|
import { createChatPasteDecoder } from "./chat-paste.js";
|
|
@@ -18,7 +18,7 @@ import { TerminalOutput } from "./TerminalOutput.js";
|
|
|
18
18
|
import { NativeTerminalScreen } from "./terminal-screen.js";
|
|
19
19
|
import { WorkerOutputView } from "./WorkerOutputView.js";
|
|
20
20
|
import { compactEndByDisplayWidth, displayWidth, wrapByDisplayWidth } from "./display-width.js";
|
|
21
|
-
import { isAttachShortcut, isExitShortcut, isLogsShortcut, isNewTaskShortcut, isRouterDiagnosticsShortcut, isTaskResultShortcut, isTaskSessionsShortcut, isWorkerOverviewShortcut, isWorkerSearchShortcut, isWorkspaceShortcut, mouseScrollDelta, rawHistoryDelta, rawPageScrollDelta, scrollDelta, workerLogJumpKind } from "./keyboard.js";
|
|
21
|
+
import { isAttachShortcut, isExitShortcut, isLogsShortcut, isNewTaskShortcut, isRouterDiagnosticsShortcut, isStatusDetailsShortcut, isTaskResultShortcut, isTaskSessionsShortcut, isWorkerOverviewShortcut, isWorkerSearchShortcut, isWorkspaceShortcut, mouseScrollDelta, rawHistoryDelta, rawPageScrollDelta, scrollDelta, workerLogJumpKind } from "./keyboard.js";
|
|
22
22
|
import { createRawInputDecoder, tokenizeRawInput } from "./raw-input-decoder.js";
|
|
23
23
|
import { decodeHtmlEntities } from "./markdown-text.js";
|
|
24
24
|
import { configureTuiTheme, TUI_THEME } from "./theme.js";
|
|
@@ -28,8 +28,11 @@ import { moveWorkerSelection, WorkerOverviewView } from "./WorkerOverviewView.js
|
|
|
28
28
|
import { FeatureBoardView, moveFeatureBoardSelection } from "./FeatureBoardView.js";
|
|
29
29
|
import { CollaborationTimelineView, collaborationSelectionScrollOffset, collaborationTimelineEvents, moveCollaborationEventSelection, nextCollaborationFeatureIndex } from "./CollaborationTimelineView.js";
|
|
30
30
|
import { moveTaskSessionSelection, TaskSessionsView } from "./TaskSessionsView.js";
|
|
31
|
+
import { moveTaskSessionDetailSelection, TaskSessionDetailView } from "./TaskSessionDetailView.js";
|
|
31
32
|
import { latestTaskResultMessageIndex, parseTaskResultSummary } from "./task-result.js";
|
|
32
|
-
import { buildNativeAttachLaunch, startNativeAttachProcess } from "../workers/native-attach.js";
|
|
33
|
+
import { buildNativeAttachLaunch, buildNativeForkLaunch, startNativeAttachProcess } from "../workers/native-attach.js";
|
|
34
|
+
import { assignableWorkerProviderIds, workerProviderLabel, workerProviders } from "../workers/provider.js";
|
|
35
|
+
import { StatusDetailView } from "./StatusDetailView.js";
|
|
33
36
|
export function routeDecisionChatMessage(route) {
|
|
34
37
|
return `route · ${route.mode} · ${route.source ?? "router"}\n${route.reason.trim()}`;
|
|
35
38
|
}
|
|
@@ -44,7 +47,7 @@ const EMPTY_WORKER_NAVIGATION_TARGETS = {
|
|
|
44
47
|
errorOffsets: [],
|
|
45
48
|
diffOffsets: []
|
|
46
49
|
};
|
|
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 }) {
|
|
50
|
+
export function App({ config, orchestrator, cwd, initialTaskId = null, initialRoute = null, initialWorkers, initialCanRetryTask = false, initialMessages = [], persistChatMessage, workspaceChoices = [], switchWorkspace, loadRouterDiagnostics, loadTaskSessions, loadTaskSessionDetails, renameTaskSession, setTaskSessionArchived, deleteTaskSession, exportTaskSession, loadCollaborationTimeline, activateTaskSession, prepareNativeAttach, prepareNativeFork, startNativeAttach, shutdownSignal }) {
|
|
48
51
|
configureTuiTheme({
|
|
49
52
|
theme: config.ui.theme,
|
|
50
53
|
colors: config.ui.colors
|
|
@@ -95,9 +98,13 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
|
|
|
95
98
|
const [taskSessionsNotice, setTaskSessionsNotice] = useState(null);
|
|
96
99
|
const [taskSessionsIncludeArchived, setTaskSessionsIncludeArchived] = useState(false);
|
|
97
100
|
const [taskSessionAction, setTaskSessionAction] = useState(null);
|
|
101
|
+
const [taskSessionDetails, setTaskSessionDetails] = useState(null);
|
|
102
|
+
const [taskSessionDetailSelectedWorkerIndex, setTaskSessionDetailSelectedWorkerIndex] = useState(0);
|
|
103
|
+
const [taskSessionDetailNotice, setTaskSessionDetailNotice] = useState(null);
|
|
98
104
|
const [collaborationTimeline, setCollaborationTimeline] = useState(null);
|
|
99
105
|
const [featureBoardSelectedIndex, setFeatureBoardSelectedIndex] = useState(0);
|
|
100
106
|
const [featureCancelPrompt, setFeatureCancelPrompt] = useState(null);
|
|
107
|
+
const [featureAssignmentPrompt, setFeatureAssignmentPrompt] = useState(null);
|
|
101
108
|
const [featureBoardNotice, setFeatureBoardNotice] = useState(null);
|
|
102
109
|
const [collaborationLoading, setCollaborationLoading] = useState(false);
|
|
103
110
|
const [collaborationError, setCollaborationError] = useState(null);
|
|
@@ -121,6 +128,7 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
|
|
|
121
128
|
const inputCursorRef = useRef(inputCursor);
|
|
122
129
|
const viewRef = useRef(view);
|
|
123
130
|
const busyRef = useRef(busy);
|
|
131
|
+
const canRetryTaskRef = useRef(initialCanRetryTask);
|
|
124
132
|
const routeFallbackPromptRef = useRef(null);
|
|
125
133
|
const routeFallbackResolverRef = useRef(null);
|
|
126
134
|
const workersRef = useRef(workers);
|
|
@@ -137,9 +145,12 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
|
|
|
137
145
|
const taskSessionsLoadingRef = useRef(taskSessionsLoading);
|
|
138
146
|
const taskSessionsIncludeArchivedRef = useRef(taskSessionsIncludeArchived);
|
|
139
147
|
const taskSessionActionRef = useRef(taskSessionAction);
|
|
148
|
+
const taskSessionDetailsRef = useRef(null);
|
|
149
|
+
const taskSessionDetailSelectedWorkerIndexRef = useRef(0);
|
|
140
150
|
const collaborationTimelineRef = useRef(null);
|
|
141
151
|
const featureBoardSelectedIndexRef = useRef(0);
|
|
142
152
|
const featureCancelPromptRef = useRef(null);
|
|
153
|
+
const featureAssignmentPromptRef = useRef(null);
|
|
143
154
|
const collaborationFeatureIndexRef = useRef(-1);
|
|
144
155
|
const collaborationSelectedEventIdRef = useRef(null);
|
|
145
156
|
const collaborationDetailOpenRef = useRef(false);
|
|
@@ -157,6 +168,7 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
|
|
|
157
168
|
const openTaskSessionsRef = useRef(openTaskSessions);
|
|
158
169
|
const openFeatureBoardRef = useRef(openFeatureBoard);
|
|
159
170
|
const confirmFeatureCancellationRef = useRef(confirmFeatureCancellation);
|
|
171
|
+
const reassignSelectedFeatureRef = useRef(reassignSelectedFeature);
|
|
160
172
|
const openCollaborationTimelineRef = useRef(openCollaborationTimeline);
|
|
161
173
|
const refreshCollaborationTimelineRef = useRef(refreshCollaborationTimeline);
|
|
162
174
|
const activateSelectedTaskSessionRef = useRef(activateSelectedTaskSession);
|
|
@@ -165,11 +177,15 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
|
|
|
165
177
|
const archiveSelectedTaskSessionRef = useRef(archiveSelectedTaskSession);
|
|
166
178
|
const deleteSelectedTaskSessionRef = useRef(deleteSelectedTaskSession);
|
|
167
179
|
const exportSelectedTaskSessionRef = useRef(exportSelectedTaskSession);
|
|
180
|
+
const openSelectedTaskSessionDetailsRef = useRef(openSelectedTaskSessionDetails);
|
|
181
|
+
const refreshTaskSessionDetailsRef = useRef(refreshTaskSessionDetails);
|
|
182
|
+
const openTaskSessionDetailWorkerRef = useRef(openTaskSessionDetailWorker);
|
|
168
183
|
const workspaceReturnViewRef = useRef("chat");
|
|
169
184
|
const routerReturnViewRef = useRef("chat");
|
|
170
185
|
const workerOverviewReturnViewRef = useRef("chat");
|
|
171
186
|
const taskSessionsReturnViewRef = useRef("chat");
|
|
172
187
|
const collaborationReturnViewRef = useRef("workers");
|
|
188
|
+
const statusReturnViewRef = useRef("chat");
|
|
173
189
|
const collaborationLoadSequenceRef = useRef(0);
|
|
174
190
|
const routerLoadSequenceRef = useRef(0);
|
|
175
191
|
const taskSessionsLoadSequenceRef = useRef(0);
|
|
@@ -183,34 +199,31 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
|
|
|
183
199
|
const contentHeight = appContentHeight(terminalSize.rows, Boolean(attachError), config.ui.showStatusBar);
|
|
184
200
|
const outputHeight = Math.max(1, contentHeight);
|
|
185
201
|
const terminalWidth = terminalSize.columns;
|
|
186
|
-
const workerActivityPolicies = useMemo(() => ({
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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]);
|
|
202
|
+
const workerActivityPolicies = useMemo(() => Object.fromEntries(workerProviders(config).map(({ id, config: provider }) => [id, {
|
|
203
|
+
timeoutMs: provider.timeoutMs,
|
|
204
|
+
idleTimeoutMs: provider.idleTimeoutMs,
|
|
205
|
+
firstOutputTimeoutMs: provider.firstOutputTimeoutMs
|
|
206
|
+
}])), [config.workers]);
|
|
207
|
+
const assignableProviderIds = useMemo(() => assignableWorkerProviderIds(config), [config.workers]);
|
|
203
208
|
const selectedBoardFeature = collaborationTimeline?.features[featureBoardSelectedIndex];
|
|
209
|
+
const selectedTaskSessionDetailWorker = taskSessionDetails?.workers[taskSessionDetailSelectedWorkerIndex];
|
|
210
|
+
const taskSessionDetailHasNative = Boolean(selectedTaskSessionDetailWorker?.nativeSession && !taskSessionDetails?.task.archived_at);
|
|
211
|
+
const taskSessionDetailCanFork = Boolean(taskSessionDetailHasNative
|
|
212
|
+
&& selectedTaskSessionDetailWorker
|
|
213
|
+
&& (config.workers[selectedTaskSessionDetailWorker.engine]?.interactive.forkArgs.length ?? 0) > 0);
|
|
204
214
|
const featureCanCancel = busy && (selectedBoardFeature?.state === "actor_running"
|
|
205
215
|
|| selectedBoardFeature?.state === "critic_running");
|
|
216
|
+
const featureCanReassign = assignableProviderIds.length > 1
|
|
217
|
+
&& !busy
|
|
218
|
+
&& canRetryTask
|
|
219
|
+
&& selectedBoardFeature?.state !== "approved";
|
|
206
220
|
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
221
|
const visibleRouteStatus = routePending
|
|
212
222
|
? formatRoutePendingStatus(routePending, routeElapsedMs)
|
|
213
223
|
: formatRouteStatus(lastRoute);
|
|
224
|
+
const visibleRouteSummary = routePending
|
|
225
|
+
? formatRoutePendingSummaryStatus(routePending, routeElapsedMs)
|
|
226
|
+
: formatRouteSummaryStatus(lastRoute);
|
|
214
227
|
const visibleTaskStatus = routePending && !activeTaskId ? "" : formatStatusLine(visibleStatus);
|
|
215
228
|
const workerRefreshKey = workers.map((worker) => `${worker.id}\u0000${worker.statusPath}`).join("\u0001");
|
|
216
229
|
const taskResultMessageIndex = useMemo(() => activeTaskId ? latestTaskResultMessageIndex(messages, activeTaskId) : -1, [activeTaskId, messages]);
|
|
@@ -245,6 +258,9 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
|
|
|
245
258
|
useEffect(() => {
|
|
246
259
|
busyRef.current = busy;
|
|
247
260
|
}, [busy]);
|
|
261
|
+
useEffect(() => {
|
|
262
|
+
canRetryTaskRef.current = canRetryTask;
|
|
263
|
+
}, [canRetryTask]);
|
|
248
264
|
useEffect(() => {
|
|
249
265
|
taskResultExpandedRef.current = taskResultExpanded;
|
|
250
266
|
}, [taskResultExpanded]);
|
|
@@ -281,6 +297,12 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
|
|
|
281
297
|
useEffect(() => {
|
|
282
298
|
taskSessionActionRef.current = taskSessionAction;
|
|
283
299
|
}, [taskSessionAction]);
|
|
300
|
+
useEffect(() => {
|
|
301
|
+
taskSessionDetailsRef.current = taskSessionDetails;
|
|
302
|
+
}, [taskSessionDetails]);
|
|
303
|
+
useEffect(() => {
|
|
304
|
+
taskSessionDetailSelectedWorkerIndexRef.current = taskSessionDetailSelectedWorkerIndex;
|
|
305
|
+
}, [taskSessionDetailSelectedWorkerIndex]);
|
|
284
306
|
useEffect(() => {
|
|
285
307
|
featureBoardSelectedIndexRef.current = featureBoardSelectedIndex;
|
|
286
308
|
}, [featureBoardSelectedIndex]);
|
|
@@ -341,6 +363,9 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
|
|
|
341
363
|
archiveSelectedTaskSessionRef.current = archiveSelectedTaskSession;
|
|
342
364
|
deleteSelectedTaskSessionRef.current = deleteSelectedTaskSession;
|
|
343
365
|
exportSelectedTaskSessionRef.current = exportSelectedTaskSession;
|
|
366
|
+
openSelectedTaskSessionDetailsRef.current = openSelectedTaskSessionDetails;
|
|
367
|
+
refreshTaskSessionDetailsRef.current = refreshTaskSessionDetails;
|
|
368
|
+
openTaskSessionDetailWorkerRef.current = openTaskSessionDetailWorker;
|
|
344
369
|
});
|
|
345
370
|
useEffect(() => {
|
|
346
371
|
if ((view !== "collaboration" && view !== "features") || !activeTaskId || !loadCollaborationTimeline) {
|
|
@@ -608,11 +633,18 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
|
|
|
608
633
|
setTaskSessionsError(null);
|
|
609
634
|
setSelectedTaskSessionIndex(nextIndex);
|
|
610
635
|
};
|
|
636
|
+
const moveSelectedTaskSessionDetailWorker = (delta, wrap = false) => {
|
|
637
|
+
const nextIndex = moveTaskSessionDetailSelection(taskSessionDetailSelectedWorkerIndexRef.current, delta, taskSessionDetailsRef.current?.workers.length ?? 0, wrap);
|
|
638
|
+
taskSessionDetailSelectedWorkerIndexRef.current = nextIndex;
|
|
639
|
+
setTaskSessionDetailNotice(null);
|
|
640
|
+
setTaskSessionDetailSelectedWorkerIndex(nextIndex);
|
|
641
|
+
};
|
|
611
642
|
const moveSelectedFeature = (delta, wrap = false) => {
|
|
612
643
|
const nextIndex = moveFeatureBoardSelection(featureBoardSelectedIndexRef.current, delta, collaborationTimelineRef.current?.features.length ?? 0, wrap);
|
|
613
644
|
featureBoardSelectedIndexRef.current = nextIndex;
|
|
614
645
|
setCollaborationError(null);
|
|
615
646
|
updateFeatureCancelPrompt(null);
|
|
647
|
+
updateFeatureAssignmentPrompt(null);
|
|
616
648
|
setFeatureBoardNotice(null);
|
|
617
649
|
setFeatureBoardSelectedIndex(nextIndex);
|
|
618
650
|
};
|
|
@@ -657,6 +689,13 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
|
|
|
657
689
|
exitRef.current();
|
|
658
690
|
return;
|
|
659
691
|
}
|
|
692
|
+
if (fallbackChunks.some((fallbackChunk) => isStatusDetailsShortcut(fallbackChunk, {}))) {
|
|
693
|
+
statusReturnViewRef.current = "chat";
|
|
694
|
+
viewRef.current = "status";
|
|
695
|
+
setAttachError(null);
|
|
696
|
+
setView("status");
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
660
699
|
for (const fallbackChunk of fallbackChunks) {
|
|
661
700
|
if (fallbackChunk === "\x1b") {
|
|
662
701
|
settleRouteFallbackChoice("cancel");
|
|
@@ -680,12 +719,72 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
|
|
|
680
719
|
if (currentView === "workspace") {
|
|
681
720
|
return;
|
|
682
721
|
}
|
|
722
|
+
if (currentView === "status") {
|
|
723
|
+
if (isExitShortcut(chunk, {})) {
|
|
724
|
+
activeRunControllerRef.current?.abort();
|
|
725
|
+
exitRef.current();
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
if (isStatusDetailsShortcut(chunk, {}) || chunk === "\x1b") {
|
|
729
|
+
viewRef.current = statusReturnViewRef.current;
|
|
730
|
+
setView(statusReturnViewRef.current);
|
|
731
|
+
}
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
if (isStatusDetailsShortcut(chunk, {})) {
|
|
735
|
+
statusReturnViewRef.current = currentView;
|
|
736
|
+
viewRef.current = "status";
|
|
737
|
+
setAttachError(null);
|
|
738
|
+
setView("status");
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
683
741
|
if (currentView === "sessions") {
|
|
684
742
|
if (isExitShortcut(chunk, {})) {
|
|
685
743
|
activeRunControllerRef.current?.abort();
|
|
686
744
|
exitRef.current();
|
|
687
745
|
return;
|
|
688
746
|
}
|
|
747
|
+
if (taskSessionDetailsRef.current) {
|
|
748
|
+
if (isTaskSessionsShortcut(chunk, {}) || chunk === "\x1b") {
|
|
749
|
+
taskSessionDetailsRef.current = null;
|
|
750
|
+
taskSessionDetailSelectedWorkerIndexRef.current = 0;
|
|
751
|
+
setTaskSessionDetails(null);
|
|
752
|
+
setTaskSessionDetailSelectedWorkerIndex(0);
|
|
753
|
+
setTaskSessionDetailNotice(null);
|
|
754
|
+
setTaskSessionsError(null);
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
if (taskSessionsLoadingRef.current) {
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
if (chunk === "r" || chunk === "R") {
|
|
761
|
+
void refreshTaskSessionDetailsRef.current();
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
if (chunk === "c" || chunk === "C" || chunk === "\u000f") {
|
|
765
|
+
void openTaskSessionDetailWorkerRef.current("resume");
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
if (chunk === "b" || chunk === "B") {
|
|
769
|
+
void openTaskSessionDetailWorkerRef.current("fork");
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
if (chunk === "\r" || chunk === "\n") {
|
|
773
|
+
void openTaskSessionDetailWorkerRef.current("logs");
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
if (chunk === "\t") {
|
|
777
|
+
moveSelectedTaskSessionDetailWorker(1, true);
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
const detailSelectionDelta = -(rawHistoryDelta(chunk)
|
|
781
|
+
+ rawPageScrollDelta(chunk, Math.max(1, outputHeight - 2))
|
|
782
|
+
+ mouseScrollDelta(chunk, 1));
|
|
783
|
+
if (detailSelectionDelta !== 0) {
|
|
784
|
+
moveSelectedTaskSessionDetailWorker(detailSelectionDelta);
|
|
785
|
+
}
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
689
788
|
const sessionAction = taskSessionActionRef.current;
|
|
690
789
|
if (sessionAction) {
|
|
691
790
|
if (chunk === "\x1b") {
|
|
@@ -787,6 +886,10 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
|
|
|
787
886
|
void exportSelectedTaskSessionRef.current();
|
|
788
887
|
return;
|
|
789
888
|
}
|
|
889
|
+
if (chunk === "i" || chunk === "I") {
|
|
890
|
+
void openSelectedTaskSessionDetailsRef.current();
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
790
893
|
if (chunk === "h" || chunk === "H") {
|
|
791
894
|
const includeArchived = !taskSessionsIncludeArchivedRef.current;
|
|
792
895
|
taskSessionsIncludeArchivedRef.current = includeArchived;
|
|
@@ -826,13 +929,33 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
|
|
|
826
929
|
setFeatureBoardNotice(null);
|
|
827
930
|
return;
|
|
828
931
|
}
|
|
829
|
-
if (featureChunk === "x" || featureChunk === "X")
|
|
932
|
+
if ((pendingCancel.action === "cancel" && (featureChunk === "x" || featureChunk === "X"))
|
|
933
|
+
|| (pendingCancel.action === "pause" && (featureChunk === "p" || featureChunk === "P"))) {
|
|
830
934
|
void confirmFeatureCancellationRef.current(pendingCancel);
|
|
831
935
|
return;
|
|
832
936
|
}
|
|
833
937
|
}
|
|
834
938
|
return;
|
|
835
939
|
}
|
|
940
|
+
const pendingAssignment = featureAssignmentPromptRef.current;
|
|
941
|
+
if (pendingAssignment) {
|
|
942
|
+
for (const featureChunk of featureChunks) {
|
|
943
|
+
if (featureChunk === "\x1b" || featureChunk === "m" || featureChunk === "M") {
|
|
944
|
+
updateFeatureAssignmentPrompt(null);
|
|
945
|
+
setFeatureBoardNotice(null);
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
948
|
+
if (featureChunk === "a" || featureChunk === "A") {
|
|
949
|
+
void reassignSelectedFeatureRef.current(pendingAssignment, "actor");
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
if (featureChunk === "c" || featureChunk === "C") {
|
|
953
|
+
void reassignSelectedFeatureRef.current(pendingAssignment, "critic");
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
836
959
|
for (const featureChunk of featureChunks) {
|
|
837
960
|
if (featureChunk === "\x1b" || isWorkerOverviewShortcut(featureChunk, {})) {
|
|
838
961
|
setCollaborationError(null);
|
|
@@ -855,11 +978,43 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
|
|
|
855
978
|
setFeatureBoardNotice("Selected feature has no active Actor/Critic to cancel.");
|
|
856
979
|
return;
|
|
857
980
|
}
|
|
858
|
-
const prompt = { taskId, featureId: feature.id, title: feature.title };
|
|
981
|
+
const prompt = { action: "cancel", taskId, featureId: feature.id, title: feature.title };
|
|
859
982
|
updateFeatureCancelPrompt(prompt);
|
|
860
983
|
setFeatureBoardNotice(`Cancel ${feature.title}? Active peers will finish; integration stays blocked.`);
|
|
861
984
|
return;
|
|
862
985
|
}
|
|
986
|
+
if (featureChunk === "p" || featureChunk === "P") {
|
|
987
|
+
const taskId = activeTaskIdRef.current;
|
|
988
|
+
const feature = collaborationTimelineRef.current?.features[featureBoardSelectedIndexRef.current];
|
|
989
|
+
const pausable = busyRef.current
|
|
990
|
+
&& (feature?.state === "actor_running" || feature?.state === "critic_running");
|
|
991
|
+
if (!taskId || !feature || !pausable) {
|
|
992
|
+
setFeatureBoardNotice("Selected feature has no active Actor/Critic to pause.");
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
995
|
+
const prompt = { action: "pause", taskId, featureId: feature.id, title: feature.title };
|
|
996
|
+
updateFeatureCancelPrompt(prompt);
|
|
997
|
+
setFeatureBoardNotice(`Pause ${feature.title}? Completed peer checkpoints will be kept.`);
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1000
|
+
if (featureChunk === "m" || featureChunk === "M") {
|
|
1001
|
+
const taskId = activeTaskIdRef.current;
|
|
1002
|
+
const feature = collaborationTimelineRef.current?.features[featureBoardSelectedIndexRef.current];
|
|
1003
|
+
const reassignable = !busyRef.current
|
|
1004
|
+
&& canRetryTaskRef.current
|
|
1005
|
+
&& feature?.state !== "approved";
|
|
1006
|
+
if (!taskId || !feature || !reassignable) {
|
|
1007
|
+
setFeatureBoardNotice("Pause or stop the task before reassigning this Feature.");
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
1010
|
+
updateFeatureAssignmentPrompt({
|
|
1011
|
+
taskId,
|
|
1012
|
+
featureId: feature.id,
|
|
1013
|
+
title: feature.title
|
|
1014
|
+
});
|
|
1015
|
+
setFeatureBoardNotice(`Reassign ${feature.title} · A Actor (${feature.actorEngine ?? config.pairing.actor}) · C Critic (${feature.criticEngine ?? config.pairing.critic})`);
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
863
1018
|
if (featureChunk === "r" || featureChunk === "R") {
|
|
864
1019
|
setFeatureBoardNotice(null);
|
|
865
1020
|
void refreshCollaborationTimelineRef.current(false);
|
|
@@ -1396,18 +1551,19 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
|
|
|
1396
1551
|
void attachSelectedWorker(worker);
|
|
1397
1552
|
}
|
|
1398
1553
|
}, { isActive: view === "worker" && !workerSearch.open });
|
|
1399
|
-
async function attachSelectedWorker(worker) {
|
|
1554
|
+
async function attachSelectedWorker(worker, mode = "resume") {
|
|
1400
1555
|
const requestSequence = nativeAttachRequestSequenceRef.current + 1;
|
|
1401
1556
|
nativeAttachRequestSequenceRef.current = requestSequence;
|
|
1402
1557
|
const attachIsCurrent = () => (mountedRef.current && nativeAttachRequestSequenceRef.current === requestSequence);
|
|
1403
1558
|
setAttachError(null);
|
|
1404
1559
|
try {
|
|
1405
|
-
const launch = await (
|
|
1406
|
-
?
|
|
1407
|
-
|
|
1408
|
-
config,
|
|
1409
|
-
|
|
1410
|
-
|
|
1560
|
+
const launch = await (mode === "fork"
|
|
1561
|
+
? prepareNativeFork
|
|
1562
|
+
? prepareNativeFork(worker)
|
|
1563
|
+
: buildNativeForkLaunch({ config, worker })
|
|
1564
|
+
: prepareNativeAttach
|
|
1565
|
+
? prepareNativeAttach(worker)
|
|
1566
|
+
: buildNativeAttachLaunch({ config, worker }));
|
|
1411
1567
|
if (!attachIsCurrent()) {
|
|
1412
1568
|
return;
|
|
1413
1569
|
}
|
|
@@ -1445,7 +1601,12 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
|
|
|
1445
1601
|
if (!attachIsCurrent()) {
|
|
1446
1602
|
return;
|
|
1447
1603
|
}
|
|
1448
|
-
|
|
1604
|
+
const failureHint = nativeAttachFailureHint(screen.snapshot(), code, worker.engine);
|
|
1605
|
+
const closeOutput = [
|
|
1606
|
+
nativeAttachExitLine(code, screen.dimensions().cols),
|
|
1607
|
+
...(failureHint ? [failureHint] : [])
|
|
1608
|
+
].join("\r\n");
|
|
1609
|
+
void screen.write(`\r\n${closeOutput}\r\n`).then(() => {
|
|
1449
1610
|
if (!attachIsCurrent()) {
|
|
1450
1611
|
return;
|
|
1451
1612
|
}
|
|
@@ -1705,11 +1866,20 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
|
|
|
1705
1866
|
setRouteAnnouncement(null);
|
|
1706
1867
|
setLastRoute(null);
|
|
1707
1868
|
try {
|
|
1708
|
-
const
|
|
1869
|
+
const selectedPausedFeature = viewRef.current === "features"
|
|
1870
|
+
? collaborationTimelineRef.current?.features[featureBoardSelectedIndexRef.current]
|
|
1871
|
+
: null;
|
|
1872
|
+
const retryInput = {
|
|
1709
1873
|
taskId,
|
|
1710
1874
|
cwd,
|
|
1711
1875
|
...createRunCallbacks(controller, { announceRoute: false })
|
|
1712
|
-
}
|
|
1876
|
+
};
|
|
1877
|
+
const result = selectedPausedFeature?.state === "paused"
|
|
1878
|
+
? await orchestrator.resumeFeature({
|
|
1879
|
+
...retryInput,
|
|
1880
|
+
featureId: selectedPausedFeature.id
|
|
1881
|
+
})
|
|
1882
|
+
: await orchestrator.retryTask(retryInput);
|
|
1713
1883
|
activeTaskIdRef.current = taskId;
|
|
1714
1884
|
setActiveTaskId(taskId);
|
|
1715
1885
|
setActiveMode("complex");
|
|
@@ -1833,6 +2003,11 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
|
|
|
1833
2003
|
setAttachError(null);
|
|
1834
2004
|
setTaskSessionsError(null);
|
|
1835
2005
|
setTaskSessionsNotice(null);
|
|
2006
|
+
taskSessionDetailsRef.current = null;
|
|
2007
|
+
taskSessionDetailSelectedWorkerIndexRef.current = 0;
|
|
2008
|
+
setTaskSessionDetails(null);
|
|
2009
|
+
setTaskSessionDetailSelectedWorkerIndex(0);
|
|
2010
|
+
setTaskSessionDetailNotice(null);
|
|
1836
2011
|
updateTaskSessionAction(null);
|
|
1837
2012
|
await refreshTaskSessions(activeTaskIdRef.current);
|
|
1838
2013
|
}
|
|
@@ -1943,7 +2118,7 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
|
|
|
1943
2118
|
return exportTaskSession(selected.id);
|
|
1944
2119
|
}, (path) => `Exported · ${path}`, selected.id);
|
|
1945
2120
|
}
|
|
1946
|
-
async function
|
|
2121
|
+
async function openSelectedTaskSessionDetails() {
|
|
1947
2122
|
if (busyRef.current || taskSessionsLoadingRef.current) {
|
|
1948
2123
|
return;
|
|
1949
2124
|
}
|
|
@@ -1951,9 +2126,103 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
|
|
|
1951
2126
|
if (!selected) {
|
|
1952
2127
|
return;
|
|
1953
2128
|
}
|
|
2129
|
+
taskSessionsLoadingRef.current = true;
|
|
2130
|
+
setTaskSessionsLoading(true);
|
|
2131
|
+
setTaskSessionsError(null);
|
|
2132
|
+
setTaskSessionDetailNotice(null);
|
|
2133
|
+
const sequence = taskSessionsLoadSequenceRef.current + 1;
|
|
2134
|
+
taskSessionsLoadSequenceRef.current = sequence;
|
|
2135
|
+
try {
|
|
2136
|
+
if (!loadTaskSessionDetails) {
|
|
2137
|
+
throw new Error("Task session details are unavailable");
|
|
2138
|
+
}
|
|
2139
|
+
const details = await loadTaskSessionDetails(selected);
|
|
2140
|
+
if (taskSessionsLoadSequenceRef.current !== sequence) {
|
|
2141
|
+
return;
|
|
2142
|
+
}
|
|
2143
|
+
taskSessionDetailsRef.current = details;
|
|
2144
|
+
taskSessionDetailSelectedWorkerIndexRef.current = 0;
|
|
2145
|
+
setTaskSessionDetails(details);
|
|
2146
|
+
setTaskSessionDetailSelectedWorkerIndex(0);
|
|
2147
|
+
}
|
|
2148
|
+
catch (error) {
|
|
2149
|
+
if (taskSessionsLoadSequenceRef.current === sequence) {
|
|
2150
|
+
setTaskSessionsError(error instanceof Error ? error.message : String(error));
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
2153
|
+
finally {
|
|
2154
|
+
if (taskSessionsLoadSequenceRef.current === sequence) {
|
|
2155
|
+
taskSessionsLoadingRef.current = false;
|
|
2156
|
+
setTaskSessionsLoading(false);
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
async function refreshTaskSessionDetails() {
|
|
2161
|
+
const current = taskSessionDetailsRef.current;
|
|
2162
|
+
if (!current || taskSessionsLoadingRef.current || !loadTaskSessionDetails) {
|
|
2163
|
+
return;
|
|
2164
|
+
}
|
|
2165
|
+
const selectedWorkerId = current.workers[taskSessionDetailSelectedWorkerIndexRef.current]?.id;
|
|
2166
|
+
taskSessionsLoadingRef.current = true;
|
|
2167
|
+
setTaskSessionsLoading(true);
|
|
2168
|
+
setTaskSessionsError(null);
|
|
2169
|
+
const sequence = taskSessionsLoadSequenceRef.current + 1;
|
|
2170
|
+
taskSessionsLoadSequenceRef.current = sequence;
|
|
2171
|
+
try {
|
|
2172
|
+
const details = await loadTaskSessionDetails(current.task);
|
|
2173
|
+
if (taskSessionsLoadSequenceRef.current !== sequence) {
|
|
2174
|
+
return;
|
|
2175
|
+
}
|
|
2176
|
+
const selectedIndex = selectedWorkerId
|
|
2177
|
+
? Math.max(0, details.workers.findIndex((worker) => worker.id === selectedWorkerId))
|
|
2178
|
+
: 0;
|
|
2179
|
+
taskSessionDetailsRef.current = details;
|
|
2180
|
+
taskSessionDetailSelectedWorkerIndexRef.current = selectedIndex;
|
|
2181
|
+
setTaskSessionDetails(details);
|
|
2182
|
+
setTaskSessionDetailSelectedWorkerIndex(selectedIndex);
|
|
2183
|
+
setTaskSessionDetailNotice("Session hierarchy refreshed");
|
|
2184
|
+
}
|
|
2185
|
+
catch (error) {
|
|
2186
|
+
if (taskSessionsLoadSequenceRef.current === sequence) {
|
|
2187
|
+
setTaskSessionsError(error instanceof Error ? error.message : String(error));
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2190
|
+
finally {
|
|
2191
|
+
if (taskSessionsLoadSequenceRef.current === sequence) {
|
|
2192
|
+
taskSessionsLoadingRef.current = false;
|
|
2193
|
+
setTaskSessionsLoading(false);
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
async function openTaskSessionDetailWorker(mode) {
|
|
2198
|
+
const details = taskSessionDetailsRef.current;
|
|
2199
|
+
const detailWorker = details?.workers[taskSessionDetailSelectedWorkerIndexRef.current];
|
|
2200
|
+
if (!details || !detailWorker || taskSessionsLoadingRef.current) {
|
|
2201
|
+
return;
|
|
2202
|
+
}
|
|
2203
|
+
if (mode !== "logs" && !detailWorker.nativeSession) {
|
|
2204
|
+
setTaskSessionDetailNotice(`No native session recorded for ${sessionDetailWorkerLabel(detailWorker)}`);
|
|
2205
|
+
return;
|
|
2206
|
+
}
|
|
2207
|
+
const worker = await activateTaskSessionRecord(details.task, detailWorker.id, "worker");
|
|
2208
|
+
if (worker && mode !== "logs") {
|
|
2209
|
+
await attachSelectedWorkerRef.current(worker, mode === "fork" ? "fork" : "resume");
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2212
|
+
async function activateSelectedTaskSession() {
|
|
2213
|
+
const selected = taskSessionsRef.current[selectedTaskSessionIndexRef.current];
|
|
2214
|
+
if (!selected) {
|
|
2215
|
+
return;
|
|
2216
|
+
}
|
|
2217
|
+
await activateTaskSessionRecord(selected, null, "chat");
|
|
2218
|
+
}
|
|
2219
|
+
async function activateTaskSessionRecord(selected, preferredWorkerId, targetView) {
|
|
2220
|
+
if (busyRef.current || taskSessionsLoadingRef.current) {
|
|
2221
|
+
return null;
|
|
2222
|
+
}
|
|
1954
2223
|
if (selected.archived_at) {
|
|
1955
2224
|
setTaskSessionsError(`Unarchive ${selected.title} before restoring it`);
|
|
1956
|
-
return;
|
|
2225
|
+
return null;
|
|
1957
2226
|
}
|
|
1958
2227
|
taskSessionsLoadingRef.current = true;
|
|
1959
2228
|
setTaskSessionsLoading(true);
|
|
@@ -1966,7 +2235,7 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
|
|
|
1966
2235
|
}
|
|
1967
2236
|
const restored = await activateTaskSession(selected.id);
|
|
1968
2237
|
if (taskSessionsLoadSequenceRef.current !== sequence) {
|
|
1969
|
-
return;
|
|
2238
|
+
return null;
|
|
1970
2239
|
}
|
|
1971
2240
|
if (!restored) {
|
|
1972
2241
|
throw new Error(`Task session not found: ${selected.id}`);
|
|
@@ -1981,19 +2250,28 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
|
|
|
1981
2250
|
setTaskResultExpanded(latestTaskResultMessageIndex(messagesRef.current, restored.taskId) >= 0);
|
|
1982
2251
|
workersRef.current = restored.workers;
|
|
1983
2252
|
setWorkers(restored.workers);
|
|
1984
|
-
|
|
1985
|
-
|
|
2253
|
+
const preferredWorkerIndex = preferredWorkerId
|
|
2254
|
+
? restored.workers.findIndex((worker) => worker.id === preferredWorkerId)
|
|
2255
|
+
: -1;
|
|
2256
|
+
if (preferredWorkerId && preferredWorkerIndex < 0) {
|
|
2257
|
+
throw new Error(`Worker is no longer available: ${preferredWorkerId}`);
|
|
2258
|
+
}
|
|
2259
|
+
const nextWorkerIndex = preferredWorkerIndex >= 0 ? preferredWorkerIndex : 0;
|
|
2260
|
+
selectedWorkerIndexRef.current = nextWorkerIndex;
|
|
2261
|
+
setSelectedWorkerIndex(nextWorkerIndex);
|
|
1986
2262
|
setWorkerScrollOffset(0);
|
|
1987
2263
|
autoSelectedFailedWorkerRef.current = false;
|
|
1988
2264
|
userSelectedWorkerRef.current = false;
|
|
1989
2265
|
setAttachError(null);
|
|
1990
|
-
viewRef.current =
|
|
1991
|
-
setView(
|
|
2266
|
+
viewRef.current = targetView;
|
|
2267
|
+
setView(targetView);
|
|
2268
|
+
return restored.workers[nextWorkerIndex] ?? null;
|
|
1992
2269
|
}
|
|
1993
2270
|
catch (error) {
|
|
1994
2271
|
if (taskSessionsLoadSequenceRef.current === sequence) {
|
|
1995
2272
|
setTaskSessionsError(error instanceof Error ? error.message : String(error));
|
|
1996
2273
|
}
|
|
2274
|
+
return null;
|
|
1997
2275
|
}
|
|
1998
2276
|
finally {
|
|
1999
2277
|
if (taskSessionsLoadSequenceRef.current === sequence) {
|
|
@@ -2006,17 +2284,51 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
|
|
|
2006
2284
|
featureCancelPromptRef.current = next;
|
|
2007
2285
|
setFeatureCancelPrompt(next);
|
|
2008
2286
|
}
|
|
2287
|
+
function updateFeatureAssignmentPrompt(next) {
|
|
2288
|
+
featureAssignmentPromptRef.current = next;
|
|
2289
|
+
setFeatureAssignmentPrompt(next);
|
|
2290
|
+
}
|
|
2291
|
+
async function reassignSelectedFeature(prompt, role) {
|
|
2292
|
+
const feature = collaborationTimelineRef.current?.features.find((item) => item.id === prompt.featureId);
|
|
2293
|
+
if (!feature) {
|
|
2294
|
+
setFeatureBoardNotice(`Feature no longer exists: ${prompt.featureId}`);
|
|
2295
|
+
updateFeatureAssignmentPrompt(null);
|
|
2296
|
+
return;
|
|
2297
|
+
}
|
|
2298
|
+
const current = role === "actor"
|
|
2299
|
+
? feature.actorEngine ?? config.pairing.actor
|
|
2300
|
+
: feature.criticEngine ?? config.pairing.critic;
|
|
2301
|
+
const engine = nextAssignableFeatureEngine(current, assignableProviderIds);
|
|
2302
|
+
try {
|
|
2303
|
+
await orchestrator.reassignFeature({
|
|
2304
|
+
taskId: prompt.taskId,
|
|
2305
|
+
featureId: prompt.featureId,
|
|
2306
|
+
role,
|
|
2307
|
+
engine
|
|
2308
|
+
});
|
|
2309
|
+
setFeatureBoardNotice(`${role === "actor" ? "Actor" : "Critic"} reassigned to ${featureEngineDisplay(config, engine)} · Ctrl+R resumes the task.`);
|
|
2310
|
+
await refreshCollaborationTimeline(false);
|
|
2311
|
+
}
|
|
2312
|
+
catch (error) {
|
|
2313
|
+
setFeatureBoardNotice(`Reassign failed for ${prompt.title}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
2009
2316
|
async function confirmFeatureCancellation(prompt) {
|
|
2010
2317
|
updateFeatureCancelPrompt(null);
|
|
2011
2318
|
setAttachError(null);
|
|
2012
2319
|
try {
|
|
2013
|
-
const result =
|
|
2320
|
+
const result = prompt.action === "pause"
|
|
2321
|
+
? await orchestrator.pauseFeature(prompt.taskId, prompt.featureId)
|
|
2322
|
+
: await orchestrator.cancelFeature(prompt.taskId, prompt.featureId);
|
|
2014
2323
|
setFeatureBoardNotice(result.requested
|
|
2015
|
-
?
|
|
2324
|
+
? prompt.action === "pause"
|
|
2325
|
+
? `Pause requested for ${prompt.title} · ${result.role ?? "worker"} stopping; checkpoints stay resumable.`
|
|
2326
|
+
: `Cancellation requested for ${prompt.title} · ${result.role ?? "worker"} stopping; active peers will finish.`
|
|
2016
2327
|
: `No active Actor/Critic for ${prompt.title}; refresh and try again.`);
|
|
2017
2328
|
}
|
|
2018
2329
|
catch (error) {
|
|
2019
|
-
|
|
2330
|
+
const action = prompt.action === "pause" ? "Pause" : "Cancel";
|
|
2331
|
+
setFeatureBoardNotice(`${action} failed for ${prompt.title}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2020
2332
|
}
|
|
2021
2333
|
}
|
|
2022
2334
|
async function openFeatureBoard() {
|
|
@@ -2032,6 +2344,7 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
|
|
|
2032
2344
|
setAttachError(null);
|
|
2033
2345
|
setCollaborationError(null);
|
|
2034
2346
|
updateFeatureCancelPrompt(null);
|
|
2347
|
+
updateFeatureAssignmentPrompt(null);
|
|
2035
2348
|
setFeatureBoardNotice(null);
|
|
2036
2349
|
collaborationTimelineRef.current = null;
|
|
2037
2350
|
setCollaborationTimeline(null);
|
|
@@ -2219,19 +2532,19 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
|
|
|
2219
2532
|
if (view === "workspace") {
|
|
2220
2533
|
return (_jsx(Box, { flexDirection: "column", height: terminalSize.rows, children: _jsx(WorkspacePicker, { cwd: cwd, choices: workspaceChoices, terminalHeight: terminalSize.rows, terminalWidth: terminalWidth, onCancel: closeWorkspacePicker, onSelect: (workspace) => void selectWorkspace(workspace) }) }));
|
|
2221
2534
|
}
|
|
2222
|
-
return (_jsx(AppShell, { view: view, cwd: cwd, taskId: activeTaskId, statusText: [visibleTaskStatus,
|
|
2535
|
+
return (_jsx(AppShell, { view: view, cwd: cwd, taskId: activeTaskId, statusText: [visibleTaskStatus, visibleRouteSummary].filter(Boolean).join(" | "), contentHeight: contentHeight, showStatusBar: config.ui.showStatusBar, input: _jsx(InputBar, { mode: view === "worker" && workerSearch.open ? "worker-search" : view, ready: inputReady, busy: busy, routeFallback: Boolean(routeFallbackPrompt), collaborationDetail: collaborationDetailOpen, collaborationUnresolved: collaborationUnresolvedOnly, collaborationBack: collaborationReturnViewRef.current, featureCanCancel: featureCanCancel, featureCanPause: featureCanCancel, featureCanReassign: featureCanReassign, featureCancelConfirm: featureCancelPrompt?.action === "cancel", featurePauseConfirm: featureCancelPrompt?.action === "pause", featureAssignment: Boolean(featureAssignmentPrompt), taskSessionAction: taskSessionAction?.type === "rename"
|
|
2223
2536
|
? { type: "rename", value: taskSessionAction.value, cursor: taskSessionAction.cursor }
|
|
2224
2537
|
: taskSessionAction?.type === "delete"
|
|
2225
2538
|
? { 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"
|
|
2539
|
+
: null, taskSessionsIncludeArchived: taskSessionsIncludeArchived, taskSessionDetail: Boolean(taskSessionDetails), taskSessionDetailHasNative: taskSessionDetailHasNative, taskSessionDetailCanFork: taskSessionDetailCanFork, canRetry: canRetryTask, hasWorkers: workers.length > 0, hasActiveTask: Boolean(activeTaskId), hasTaskResult: hasTaskResult, taskResultExpanded: taskResultExpanded, chatScrollOffset: chatScrollOffset, chatMaxScrollOffset: chatMaxScrollOffset, nativeClosed: view === "native" && nativeAttach?.closedCode !== null, searchMatchIndex: workerSearch.matchIndex, searchMatchCount: workerNavigationTargets.searchOffsets.length, value: workerSearch.open && view === "worker"
|
|
2227
2540
|
? workerSearch.query
|
|
2228
|
-
: view === "native" || view === "router" || view === "workers" || view === "features" || view === "sessions" || view === "collaboration" ? "" : input, cursor: workerSearch.open && view === "worker"
|
|
2541
|
+
: view === "native" || view === "router" || view === "workers" || view === "features" || view === "sessions" || view === "collaboration" || view === "status" ? "" : input, cursor: workerSearch.open && view === "worker"
|
|
2229
2542
|
? 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"
|
|
2543
|
+
: view === "chat" ? inputCursor : undefined, terminalWidth: terminalWidth, onChange: view === "native" ? setNativeInput : setInput, onSubmit: view === "native" ? undefined : submit }), error: attachError, children: view === "status" ? (_jsx(StatusDetailView, { cwd: cwd, taskId: activeTaskId, mode: activeMode, busy: busy, canRetry: canRetryTask, taskStatus: visibleTaskStatus, routeStatus: visibleRouteStatus, routeReason: routePending ? undefined : lastRoute?.reason, workers: workers, selectedWorkerIndex: selectedWorkerIndex, height: contentHeight, terminalWidth: terminalWidth })) : view === "native" ? (_jsx(NativeAttachView, { attach: nativeAttach, viewportHeight: contentHeight })) : view === "sessions" ? (taskSessionDetails ? (_jsx(TaskSessionDetailView, { details: taskSessionDetails, selectedWorkerIndex: taskSessionDetailSelectedWorkerIndex, loading: taskSessionsLoading, error: taskSessionsError, notice: taskSessionDetailNotice, height: contentHeight, terminalWidth: terminalWidth })) : (_jsx(TaskSessionsView, { tasks: taskSessions, activeTaskId: activeTaskId, selectedIndex: selectedTaskSessionIndex, includeArchived: taskSessionsIncludeArchived, notice: taskSessionsNotice, action: taskSessionAction?.type === "rename"
|
|
2231
2544
|
? { type: "rename", title: taskSessionAction.title }
|
|
2232
2545
|
: taskSessionAction?.type === "delete"
|
|
2233
2546
|
? { 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 }) => {
|
|
2547
|
+
: null, loading: taskSessionsLoading, error: taskSessionsError, height: contentHeight, terminalWidth: terminalWidth }))) : view === "features" ? (_jsx(FeatureBoardView, { timeline: collaborationTimeline, selectedIndex: featureBoardSelectedIndex, loading: collaborationLoading, error: collaborationError, notice: featureBoardNotice, height: contentHeight, terminalWidth: terminalWidth })) : view === "collaboration" ? (_jsx(CollaborationTimelineView, { timeline: collaborationTimeline, featureIndex: collaborationFeatureIndex, selectedEventId: collaborationSelectedEventId, detailOpen: collaborationDetailOpen, unresolvedOnly: collaborationUnresolvedOnly, loading: collaborationLoading, error: collaborationError, scrollOffset: collaborationScrollOffset, height: contentHeight, terminalWidth: terminalWidth, onViewportChange: ({ offset, maxOffset }) => {
|
|
2235
2548
|
collaborationMaxScrollOffsetRef.current = maxOffset;
|
|
2236
2549
|
setCollaborationMaxScrollOffset(maxOffset);
|
|
2237
2550
|
if (offset !== collaborationScrollOffset) {
|
|
@@ -3046,6 +3359,21 @@ function compactChatTaskId(taskId) {
|
|
|
3046
3359
|
}
|
|
3047
3360
|
return taskId.startsWith("task-") ? taskId.slice("task-".length) : taskId;
|
|
3048
3361
|
}
|
|
3362
|
+
export function nextAssignableFeatureEngine(current, providers = ["codex", "claude"]) {
|
|
3363
|
+
const available = [...new Set(providers)];
|
|
3364
|
+
if (available.length === 0) {
|
|
3365
|
+
return current;
|
|
3366
|
+
}
|
|
3367
|
+
const currentIndex = available.indexOf(current);
|
|
3368
|
+
return available[(currentIndex + 1 + available.length) % available.length] ?? current;
|
|
3369
|
+
}
|
|
3370
|
+
function sessionDetailWorkerLabel(worker) {
|
|
3371
|
+
const role = `${worker.role.slice(0, 1).toUpperCase()}${worker.role.slice(1)}`;
|
|
3372
|
+
return `${role} (${worker.engine})${worker.featureTitle ? ` · ${worker.featureTitle}` : ""}`;
|
|
3373
|
+
}
|
|
3374
|
+
function featureEngineDisplay(config, engine) {
|
|
3375
|
+
return workerProviderLabel(config, engine);
|
|
3376
|
+
}
|
|
3049
3377
|
function compactChatText(text, maxLength) {
|
|
3050
3378
|
if (maxLength <= 5) {
|
|
3051
3379
|
return takeChatTextStartByDisplayWidth(text, maxLength);
|
|
@@ -3364,6 +3692,27 @@ export function nativeAttachExitLine(code, nativeTerminalCols) {
|
|
|
3364
3692
|
];
|
|
3365
3693
|
return firstNativeTitleThatFits(candidates, contentWidth);
|
|
3366
3694
|
}
|
|
3695
|
+
export function nativeAttachFailureHint(output, code, providerId = "codex") {
|
|
3696
|
+
if (code === 0) {
|
|
3697
|
+
return null;
|
|
3698
|
+
}
|
|
3699
|
+
if (/effective permissions do not allow additional writable roots|error adding directories|read-only sandbox/i.test(output)) {
|
|
3700
|
+
return `fix · set workers.${providerId}.interactive.args sandbox to workspace-write or danger-full-access, then reattach`;
|
|
3701
|
+
}
|
|
3702
|
+
if (/not inside a trusted directory|trusted directory/i.test(output)) {
|
|
3703
|
+
return "fix · open Codex once in this workspace and approve directory trust, then reattach";
|
|
3704
|
+
}
|
|
3705
|
+
if (/unexpected argument ['\"]?--skip-git-repo-check|--skip-git-repo-check.*not found/i.test(output)) {
|
|
3706
|
+
return `fix · remove --skip-git-repo-check from workers.${providerId}.interactive.args, then reattach`;
|
|
3707
|
+
}
|
|
3708
|
+
if (/stdin is not a terminal/i.test(output)) {
|
|
3709
|
+
return "fix · native attach requires its embedded PTY; reopen it with Ctrl+O instead of piping codex resume";
|
|
3710
|
+
}
|
|
3711
|
+
if (/permission denied|\bEACCES\b|\bEPERM\b/i.test(output)) {
|
|
3712
|
+
return "fix · grant the terminal read/write access to the workspace and .parallel-codex directories, then reattach";
|
|
3713
|
+
}
|
|
3714
|
+
return null;
|
|
3715
|
+
}
|
|
3367
3716
|
function withNativeTitleSuffix(candidates, suffix) {
|
|
3368
3717
|
const cleanSuffix = suffix?.trim();
|
|
3369
3718
|
if (!cleanSuffix) {
|