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.
Files changed (40) hide show
  1. package/.parallel-codex/config.example.toml +46 -0
  2. package/README.md +76 -17
  3. package/dist/cli-startup-preflight.js +18 -0
  4. package/dist/cli-startup-recovery.js +13 -1
  5. package/dist/cli.js +18 -6
  6. package/dist/core/collaboration-timeline.js +9 -2
  7. package/dist/core/config.js +161 -103
  8. package/dist/core/router.js +1 -1
  9. package/dist/core/session-index.js +234 -61
  10. package/dist/core/session-manager.js +25 -1
  11. package/dist/core/task-session-details.js +175 -0
  12. package/dist/core/task-state-machine.js +10 -9
  13. package/dist/doctor.js +58 -39
  14. package/dist/domain/schemas.js +15 -1
  15. package/dist/orchestrator/collaboration-channel.js +35 -3
  16. package/dist/orchestrator/final-acceptance.js +86 -0
  17. package/dist/orchestrator/orchestrator.js +405 -69
  18. package/dist/orchestrator/prompts.js +42 -3
  19. package/dist/orchestrator/workspace-sandbox.js +16 -0
  20. package/dist/tui/App.js +401 -52
  21. package/dist/tui/AppShell.js +9 -3
  22. package/dist/tui/FeatureBoardView.js +7 -2
  23. package/dist/tui/InputBar.js +82 -15
  24. package/dist/tui/StatusBar.js +1 -1
  25. package/dist/tui/StatusDetailView.js +164 -0
  26. package/dist/tui/TaskSessionDetailView.js +222 -0
  27. package/dist/tui/TaskSessionsView.js +5 -2
  28. package/dist/tui/WorkerOutputView.js +1 -0
  29. package/dist/tui/WorkerOverviewView.js +23 -4
  30. package/dist/tui/keyboard.js +3 -0
  31. package/dist/tui/status-line.js +42 -0
  32. package/dist/version.js +1 -1
  33. package/dist/workers/capabilities.js +4 -3
  34. package/dist/workers/live-probe.js +4 -3
  35. package/dist/workers/mock-adapter.js +37 -5
  36. package/dist/workers/native-attach.js +32 -17
  37. package/dist/workers/process-adapter.js +2 -0
  38. package/dist/workers/provider.js +26 -0
  39. package/dist/workers/registry.js +12 -22
  40. 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, formatSelectedWorkerStatus, formatRouteStatus, formatStatusLine, formatWorkerRuntimeStatus } from "./status-line.js";
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
- 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]);
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 (prepareNativeAttach
1406
- ? prepareNativeAttach(worker)
1407
- : buildNativeAttachLaunch({
1408
- config,
1409
- worker
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
- void screen.write(`\r\n${nativeAttachExitLine(code, screen.dimensions().cols)}\r\n`).then(() => {
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 result = await orchestrator.retryTask({
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 activateSelectedTaskSession() {
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
- selectedWorkerIndexRef.current = 0;
1985
- setSelectedWorkerIndex(0);
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 = "chat";
1991
- setView("chat");
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 = await orchestrator.cancelFeature(prompt.taskId, prompt.featureId);
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
- ? `Cancellation requested for ${prompt.title} · ${result.role ?? "worker"} stopping; active peers will finish.`
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
- setFeatureBoardNotice(`Cancel failed for ${prompt.title}: ${error instanceof Error ? error.message : String(error)}`);
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, 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"
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) {