parallel-codex-tui 0.3.2 → 0.4.0

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/dist/tui/App.js CHANGED
@@ -7,6 +7,7 @@ import { copyTextToClipboard } from "../core/clipboard.js";
7
7
  import { readJson } from "../core/file-store.js";
8
8
  import { CONFIGURABLE_ROLES, configuredRoleSelection } from "../core/role-configuration.js";
9
9
  import { WorkerStatusSchema } from "../domain/schemas.js";
10
+ import { isSupervisorDetachedError } from "../supervisor/client.js";
10
11
  import { effectiveWorkerWatchdog, formatRoutePendingStatus, formatRoutePendingSummaryStatus, formatRouteStatus, formatRouteSummaryStatus, formatStatusLine, formatWorkerRuntimeStatus } from "./status-line.js";
11
12
  import { applyChatInputChunk, insertChatPaste } from "./chat-input.js";
12
13
  import { chatRequestHistory, navigateChatDraftHistory } from "./chat-history.js";
@@ -62,6 +63,7 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
62
63
  const [inputCursor, setInputCursor] = useState(0);
63
64
  const [inputReady, setInputReady] = useState(false);
64
65
  const [terminalSize, setTerminalSize] = useState(readTerminalSize);
66
+ const [, setBackgroundRunRevision] = useState(0);
65
67
  const [messages, setMessages] = useState(() => [...initialMessages]);
66
68
  const [taskResultExpanded, setTaskResultExpanded] = useState(() => (Boolean(initialTaskId) && latestTaskResultMessageIndex(initialMessages, initialTaskId) >= 0));
67
69
  const [busy, setBusy] = useState(false);
@@ -148,6 +150,7 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
148
150
  const copyTextByViewRef = useRef({ chat: "", worker: "" });
149
151
  const messagesRef = useRef([...initialMessages]);
150
152
  const activeRunControllerRef = useRef(null);
153
+ const backgroundRestoreStartedRef = useRef(false);
151
154
  const activeTaskIdRef = useRef(initialTaskId);
152
155
  const nativeInputRef = useRef(nativeInput);
153
156
  const inputRef = useRef(input);
@@ -270,6 +273,8 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
270
273
  && !busy
271
274
  && canRetryTask
272
275
  && selectedBoardFeature?.state !== "approved";
276
+ const backgroundRunAttached = orchestrator.backgroundRunAttached?.() ?? false;
277
+ const backgroundRunControllable = orchestrator.backgroundRunControllable?.() ?? backgroundRunAttached;
273
278
  const visibleStatus = statusLineWithWorkerRefs(status, workers);
274
279
  const visibleRouteStatus = routePending
275
280
  ? formatRoutePendingStatus(routePending, routeElapsedMs)
@@ -290,6 +295,9 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
290
295
  : roleConfigurationSnapshot.future
291
296
  : configuredRoleSelection(config);
292
297
  const roleConfigurationHasOverride = roleConfigurationScopeHasOverride(roleConfigurationSnapshot, roleConfigurationScope);
298
+ useEffect(() => orchestrator.subscribeBackgroundRunState?.(() => {
299
+ setBackgroundRunRevision((current) => current + 1);
300
+ }), [orchestrator]);
293
301
  useEffect(() => {
294
302
  inputRef.current = input;
295
303
  }, [input]);
@@ -633,6 +641,70 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
633
641
  active = false;
634
642
  };
635
643
  }, [activeTaskId, initialTaskId, initialWorkers, orchestrator]);
644
+ useEffect(() => {
645
+ if (!orchestrator.restorePendingRun || backgroundRestoreStartedRef.current) {
646
+ return;
647
+ }
648
+ backgroundRestoreStartedRef.current = true;
649
+ const controller = new AbortController();
650
+ activeRunControllerRef.current = controller;
651
+ let active = true;
652
+ async function restoreBackgroundRun() {
653
+ busyRef.current = true;
654
+ setBusy(true);
655
+ try {
656
+ const result = await orchestrator.restorePendingRun?.({
657
+ cwd,
658
+ ...createRunCallbacks(controller, { announceRoute: false })
659
+ });
660
+ if (!active || !result) {
661
+ return;
662
+ }
663
+ const taskId = result.taskId ?? activeTaskIdRef.current;
664
+ activeTaskIdRef.current = taskId;
665
+ setActiveTaskId(taskId);
666
+ setActiveMode(taskId ? "complex" : result.mode);
667
+ setCanRetryTask(taskId ? await orchestrator.canRetryTask(taskId) : false);
668
+ await refreshRoleConfigurationState(taskId);
669
+ if (!messagesRef.current.some((message) => message.from === "system" && message.text === result.summary && message.taskId === (taskId ?? undefined))) {
670
+ await appendVisibleMessage({ from: "system", text: result.summary }, taskId ?? undefined, { persist: false });
671
+ }
672
+ if (result.mode === "complex" && parseTaskResultSummary(result.summary)) {
673
+ setTaskResultExpanded(true);
674
+ }
675
+ await orchestrator.acknowledgeBackgroundRun?.();
676
+ }
677
+ catch (error) {
678
+ if (!active || isSupervisorDetachedError(error)) {
679
+ return;
680
+ }
681
+ const taskId = activeTaskIdRef.current;
682
+ const text = isAbortError(error)
683
+ ? "cancelled · request stopped"
684
+ : error instanceof Error ? error.message : String(error);
685
+ if (!messagesRef.current.some((message) => message.from === "system" && message.text === text && message.taskId === (taskId ?? undefined))) {
686
+ await appendVisibleMessage({ from: "system", text }, taskId ?? undefined, { persist: false });
687
+ }
688
+ await orchestrator.acknowledgeBackgroundRun?.();
689
+ setCanRetryTask(taskId ? await orchestrator.canRetryTask(taskId) : false);
690
+ }
691
+ finally {
692
+ if (activeRunControllerRef.current === controller) {
693
+ activeRunControllerRef.current = null;
694
+ }
695
+ if (active) {
696
+ setRoutePending(null);
697
+ setRouteAnnouncement(null);
698
+ busyRef.current = false;
699
+ setBusy(false);
700
+ }
701
+ }
702
+ }
703
+ void restoreBackgroundRun();
704
+ return () => {
705
+ active = false;
706
+ };
707
+ }, [cwd, orchestrator]);
636
708
  useEffect(() => {
637
709
  if (workers.length === 0 || !status) {
638
710
  return;
@@ -768,8 +840,7 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
768
840
  process.stdout.write("\x1b[?2004h\x1b[?1006l\x1b[?1003l\x1b[?1002l\x1b[?1000l");
769
841
  const commitChatInputUpdate = (update, previousValue, previousCursor) => {
770
842
  if (update.exit) {
771
- activeRunControllerRef.current?.abort();
772
- exitRef.current();
843
+ requestAppExit();
773
844
  return false;
774
845
  }
775
846
  if (busyRef.current) {
@@ -882,8 +953,7 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
882
953
  if (currentView === "chat" && routeFallbackPromptRef.current) {
883
954
  const fallbackChunks = tokenizeRawInput(chunk);
884
955
  if (fallbackChunks.some((fallbackChunk) => isExitShortcut(fallbackChunk, {}))) {
885
- activeRunControllerRef.current?.abort();
886
- exitRef.current();
956
+ requestAppExit();
887
957
  return;
888
958
  }
889
959
  if (fallbackChunks.some((fallbackChunk) => isStatusDetailsShortcut(fallbackChunk, {}))) {
@@ -933,8 +1003,7 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
933
1003
  if (currentView === "roles") {
934
1004
  const roleChunks = tokenizeRawInput(chunk);
935
1005
  if (roleChunks.some((roleChunk) => isExitShortcut(roleChunk, {}))) {
936
- activeRunControllerRef.current?.abort();
937
- exitRef.current();
1006
+ requestAppExit();
938
1007
  return;
939
1008
  }
940
1009
  if (roleChunks.some((roleChunk) => isStatusDetailsShortcut(roleChunk, {}))) {
@@ -1049,8 +1118,7 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
1049
1118
  }
1050
1119
  if (currentView === "status") {
1051
1120
  if (isExitShortcut(chunk, {})) {
1052
- activeRunControllerRef.current?.abort();
1053
- exitRef.current();
1121
+ requestAppExit();
1054
1122
  return;
1055
1123
  }
1056
1124
  if (isStatusDetailsShortcut(chunk, {}) || chunk === "\x1b") {
@@ -1068,8 +1136,7 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
1068
1136
  }
1069
1137
  if (currentView === "sessions") {
1070
1138
  if (isExitShortcut(chunk, {})) {
1071
- activeRunControllerRef.current?.abort();
1072
- exitRef.current();
1139
+ requestAppExit();
1073
1140
  return;
1074
1141
  }
1075
1142
  if (taskSessionDetailsRef.current) {
@@ -1422,8 +1489,7 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
1422
1489
  if (currentView === "features") {
1423
1490
  const featureChunks = tokenizeRawInput(chunk);
1424
1491
  if (featureChunks.some((featureChunk) => isExitShortcut(featureChunk, {}))) {
1425
- activeRunControllerRef.current?.abort();
1426
- exitRef.current();
1492
+ requestAppExit();
1427
1493
  return;
1428
1494
  }
1429
1495
  const pendingCancel = featureCancelPromptRef.current;
@@ -1575,8 +1641,7 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
1575
1641
  if (currentView === "collaboration") {
1576
1642
  const timelineChunks = tokenizeRawInput(chunk);
1577
1643
  if (timelineChunks.some((timelineChunk) => isExitShortcut(timelineChunk, {}))) {
1578
- activeRunControllerRef.current?.abort();
1579
- exitRef.current();
1644
+ requestAppExit();
1580
1645
  return;
1581
1646
  }
1582
1647
  for (const timelineChunk of timelineChunks) {
@@ -1666,8 +1731,7 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
1666
1731
  }
1667
1732
  if (currentView === "workers") {
1668
1733
  if (isExitShortcut(chunk, {})) {
1669
- activeRunControllerRef.current?.abort();
1670
- exitRef.current();
1734
+ requestAppExit();
1671
1735
  return;
1672
1736
  }
1673
1737
  if (isWorkerOverviewShortcut(chunk, {}) || chunk === "\x1b") {
@@ -1730,8 +1794,7 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
1730
1794
  if (currentView === "router") {
1731
1795
  const routerChunks = tokenizeRawInput(chunk);
1732
1796
  if (routerChunks.some((routerChunk) => isExitShortcut(routerChunk, {}))) {
1733
- activeRunControllerRef.current?.abort();
1734
- exitRef.current();
1797
+ requestAppExit();
1735
1798
  return;
1736
1799
  }
1737
1800
  for (const routerChunk of routerChunks) {
@@ -1768,8 +1831,7 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
1768
1831
  if (currentView === "worker") {
1769
1832
  for (const workerChunk of tokenizeRawInput(chunk)) {
1770
1833
  if (isExitShortcut(workerChunk, {})) {
1771
- activeRunControllerRef.current?.abort();
1772
- exitRef.current();
1834
+ requestAppExit();
1773
1835
  return;
1774
1836
  }
1775
1837
  if (workerSearchRef.current.open) {
@@ -2024,6 +2086,7 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
2024
2086
  nativeAttachRequestSequenceRef.current += 1;
2025
2087
  nativeAttachRef.current?.process.kill();
2026
2088
  nativeAttachRef.current = null;
2089
+ orchestrator.detachBackgroundRuns?.();
2027
2090
  activeRunControllerRef.current?.abort();
2028
2091
  collaborationLoadSequenceRef.current += 1;
2029
2092
  routerLoadSequenceRef.current += 1;
@@ -2039,8 +2102,7 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
2039
2102
  return;
2040
2103
  }
2041
2104
  const shutdown = () => {
2042
- activeRunControllerRef.current?.abort();
2043
- exitRef.current();
2105
+ requestAppExit();
2044
2106
  };
2045
2107
  if (shutdownSignal.aborted) {
2046
2108
  shutdown();
@@ -2049,6 +2111,11 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
2049
2111
  shutdownSignal.addEventListener("abort", shutdown, { once: true });
2050
2112
  return () => shutdownSignal.removeEventListener("abort", shutdown);
2051
2113
  }, [shutdownSignal]);
2114
+ function requestAppExit() {
2115
+ orchestrator.detachBackgroundRuns?.();
2116
+ activeRunControllerRef.current?.abort();
2117
+ exitRef.current();
2118
+ }
2052
2119
  async function copyVisibleView() {
2053
2120
  const payload = visibleClipboardPayload(viewRef.current);
2054
2121
  if (!payload?.text.trim()) {
@@ -2196,8 +2263,7 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
2196
2263
  useInput((inputKey, key) => {
2197
2264
  if (view === "worker") {
2198
2265
  if (isExitShortcut(inputKey, key)) {
2199
- activeRunControllerRef.current?.abort();
2200
- exitRef.current();
2266
+ requestAppExit();
2201
2267
  return;
2202
2268
  }
2203
2269
  if (isNewConversationShortcut(inputKey, key) && !busy) {
@@ -2419,14 +2485,14 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
2419
2485
  function settleRouteFallbackChoice(choice) {
2420
2486
  routeFallbackResolverRef.current?.(choice);
2421
2487
  }
2422
- async function appendVisibleMessage(message, taskId) {
2488
+ async function appendVisibleMessage(message, taskId, options = {}) {
2423
2489
  const visibleMessage = taskId ? { ...message, taskId } : message;
2424
2490
  setMessages((current) => {
2425
2491
  const next = [...current, visibleMessage];
2426
2492
  messagesRef.current = next;
2427
2493
  return next;
2428
2494
  });
2429
- if (!persistChatMessage) {
2495
+ if (!persistChatMessage || options.persist === false) {
2430
2496
  return;
2431
2497
  }
2432
2498
  try {
@@ -2513,13 +2579,17 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
2513
2579
  : false);
2514
2580
  await refreshRoleConfigurationState(nextMemory.activeTaskId);
2515
2581
  setRouteAnnouncement(null);
2516
- await appendVisibleMessage({ from: "system", text: result.summary }, nextMemory.activeTaskId ?? undefined);
2582
+ await appendVisibleMessage({ from: "system", text: result.summary }, nextMemory.activeTaskId ?? undefined, { persist: !orchestrator.persistsRunResults });
2583
+ await orchestrator.acknowledgeBackgroundRun?.();
2517
2584
  if (result.mode === "complex" && parseTaskResultSummary(result.summary)) {
2518
2585
  setTaskResultExpanded(true);
2519
2586
  }
2520
2587
  }
2521
2588
  catch (error) {
2522
2589
  setRouteAnnouncement(null);
2590
+ if (isSupervisorDetachedError(error)) {
2591
+ return;
2592
+ }
2523
2593
  const retryTaskId = activeTaskIdRef.current;
2524
2594
  if (retryTaskId) {
2525
2595
  setCanRetryTask(await orchestrator.canRetryTask(retryTaskId));
@@ -2528,7 +2598,8 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
2528
2598
  await appendVisibleMessage({
2529
2599
  from: "system",
2530
2600
  text: isAbortError(error) ? "cancelled · request stopped" : error instanceof Error ? error.message : String(error)
2531
- }, retryTaskId ?? undefined);
2601
+ }, retryTaskId ?? undefined, { persist: !orchestrator.persistsRunResults });
2602
+ await orchestrator.acknowledgeBackgroundRun?.();
2532
2603
  }
2533
2604
  finally {
2534
2605
  if (activeRunControllerRef.current === controller) {
@@ -2574,19 +2645,24 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
2574
2645
  setCanRetryTask(false);
2575
2646
  await refreshRoleConfigurationState(taskId);
2576
2647
  setRouteAnnouncement(null);
2577
- await appendVisibleMessage({ from: "system", text: result.summary }, taskId);
2648
+ await appendVisibleMessage({ from: "system", text: result.summary }, taskId, { persist: !orchestrator.persistsRunResults });
2649
+ await orchestrator.acknowledgeBackgroundRun?.();
2578
2650
  if (parseTaskResultSummary(result.summary)) {
2579
2651
  setTaskResultExpanded(true);
2580
2652
  }
2581
2653
  }
2582
2654
  catch (error) {
2583
2655
  setRouteAnnouncement(null);
2656
+ if (isSupervisorDetachedError(error)) {
2657
+ return;
2658
+ }
2584
2659
  setCanRetryTask(await orchestrator.canRetryTask(taskId));
2585
2660
  await refreshRoleConfigurationState(taskId);
2586
2661
  await appendVisibleMessage({
2587
2662
  from: "system",
2588
2663
  text: isAbortError(error) ? "cancelled · retry stopped" : error instanceof Error ? error.message : String(error)
2589
- }, taskId);
2664
+ }, taskId, { persist: !orchestrator.persistsRunResults });
2665
+ await orchestrator.acknowledgeBackgroundRun?.();
2590
2666
  }
2591
2667
  finally {
2592
2668
  if (activeRunControllerRef.current === controller) {
@@ -3574,7 +3650,7 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
3574
3650
  if (view === "workspace") {
3575
3651
  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) }) }));
3576
3652
  }
3577
- return (_jsx(AppShell, { view: view, cwd: cwd, taskId: activeTaskId, statusText: [visibleTaskStatus, visibleRouteSummary, configChange?.compact].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), featureEditingModel: featureAssignmentPrompt?.modelEdit ?? null, taskSessionAction: sessionCenterMode === "conversations"
3653
+ return (_jsx(AppShell, { view: view, cwd: cwd, taskId: activeTaskId, busy: busy, detachable: backgroundRunAttached, controllable: backgroundRunControllable, statusText: [visibleTaskStatus, visibleRouteSummary, configChange?.compact].filter(Boolean).join(" | "), contentHeight: contentHeight, showStatusBar: config.ui.showStatusBar, input: _jsx(InputBar, { mode: view === "worker" && workerSearch.open ? "worker-search" : view, ready: inputReady, busy: busy, busyDetachable: backgroundRunAttached, busyControllable: backgroundRunControllable, 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), featureEditingModel: featureAssignmentPrompt?.modelEdit ?? null, taskSessionAction: sessionCenterMode === "conversations"
3578
3654
  ? mainConversationAction?.type === "rename"
3579
3655
  ? { type: "rename", value: mainConversationAction.value, cursor: mainConversationAction.cursor }
3580
3656
  : mainConversationAction?.type === "delete"
@@ -6,8 +6,8 @@ import { compactEndByDisplayWidth, displayWidth } from "./display-width.js";
6
6
  import { TUI_THEME } from "./theme.js";
7
7
  const APP_HEADER_ROOMY_SEPARATOR = " · ";
8
8
  const APP_HEADER_COMPACT_SEPARATOR = " ";
9
- export function AppShell({ view, cwd, taskId, statusText, contentHeight = 20, terminalWidth = process.stdout.columns || 120, showStatusBar = true, children, input, error = null }) {
10
- const header = headerParts({ view, cwd, taskId, terminalWidth });
9
+ export function AppShell({ view, cwd, taskId, statusText, contentHeight = 20, terminalWidth = process.stdout.columns || 120, showStatusBar = true, busy = false, detachable = false, controllable = true, children, input, error = null }) {
10
+ const header = headerParts({ view, cwd, taskId, terminalWidth, busy, detachable, controllable });
11
11
  const headerSegments = headerDisplaySegments(header);
12
12
  const headerSeparatorText = headerSeparator(terminalWidth);
13
13
  const headerLeadingWidth = terminalWidth > 1 ? 1 : 0;
@@ -186,7 +186,11 @@ function headerParts(input) {
186
186
  view: nano ? "" : input.terminalWidth <= 24 && input.view === "native" ? "nat" : narrow ? shortViewLabel(input.view) : viewLabel(input.view),
187
187
  task: showTask ? ultraNarrow ? ultraCompactTaskId(task) : narrow ? task : `#${task}` : "",
188
188
  project: tiny || ultraNarrow ? "" : compactHeaderProject(input.cwd, veryNarrow ? 10 : narrow ? 16 : 40),
189
- shortcut: narrow ? shortShortcutHint(input.view) : shortcutHint(input.view)
189
+ shortcut: input.busy && input.detachable
190
+ ? input.controllable
191
+ ? narrow ? "^C detach" : "Esc stop · ^C detach"
192
+ : narrow ? "^C detach" : "observe · ^C detach"
193
+ : narrow ? shortShortcutHint(input.view) : shortcutHint(input.view)
190
194
  }, contentWidth, separator);
191
195
  }
192
196
  function fitHeaderParts(parts, contentWidth, separator) {
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
2
2
  import { Box, Text } from "ink";
3
3
  import { compactEndByDisplayWidth, compactTailByDisplayWidth, displayWidth } from "./display-width.js";
4
4
  import { TUI_THEME } from "./theme.js";
5
- export function InputBar({ mode, ready = true, busy = false, routeFallback = false, collaborationDetail = false, collaborationUnresolved = false, collaborationBack = "workers", featureCanCancel = false, featureCanPause = false, featureCanReassign = false, featureCancelConfirm = false, featurePauseConfirm = false, featureAssignment = false, featureEditingModel = null, taskSessionAction = null, taskSessionsIncludeArchived = false, taskSessionQuery = "", mainConversationSessions = false, taskSessionDetail = false, taskSessionDetailHasNative = false, taskSessionDetailCanFork = false, canRetry = false, hasWorkers = false, hasActiveTask = false, hasTaskResult = false, taskResultExpanded = false, chatScrollOffset = 0, chatMaxScrollOffset = 0, nativeClosed = false, searchMatchIndex = 0, searchMatchCount = 0, clipboardNotice = null, roleScope = "next", roleEditingModel = null, roleCanApply = true, roleSaving = false, roleHasOverride = false, value, cursor, terminalWidth: providedTerminalWidth, onChange, onSubmit }) {
5
+ export function InputBar({ mode, ready = true, busy = false, busyDetachable = false, busyControllable = true, routeFallback = false, collaborationDetail = false, collaborationUnresolved = false, collaborationBack = "workers", featureCanCancel = false, featureCanPause = false, featureCanReassign = false, featureCancelConfirm = false, featurePauseConfirm = false, featureAssignment = false, featureEditingModel = null, taskSessionAction = null, taskSessionsIncludeArchived = false, taskSessionQuery = "", mainConversationSessions = false, taskSessionDetail = false, taskSessionDetailHasNative = false, taskSessionDetailCanFork = false, canRetry = false, hasWorkers = false, hasActiveTask = false, hasTaskResult = false, taskResultExpanded = false, chatScrollOffset = 0, chatMaxScrollOffset = 0, nativeClosed = false, searchMatchIndex = 0, searchMatchCount = 0, clipboardNotice = null, roleScope = "next", roleEditingModel = null, roleCanApply = true, roleSaving = false, roleHasOverride = false, value, cursor, terminalWidth: providedTerminalWidth, onChange, onSubmit }) {
6
6
  const terminalWidth = providedTerminalWidth ?? process.stdout.columns ?? 120;
7
7
  const fillRail = providedTerminalWidth !== undefined || typeof process.stdout.columns === "number";
8
8
  if (clipboardNotice) {
@@ -113,7 +113,7 @@ export function InputBar({ mode, ready = true, busy = false, routeFallback = fal
113
113
  const hints = routeFallbackInputHints(terminalWidth);
114
114
  return (_jsxs(InputRail, { terminalWidth: terminalWidth, textWidth: displayWidth(`${hints.label}${hints.detail}`), fill: fillRail, children: [_jsx(Text, { backgroundColor: TUI_THEME.rail, color: TUI_THEME.warning, bold: true, children: hints.label }), hints.detail ? _jsx(Text, { backgroundColor: TUI_THEME.rail, color: TUI_THEME.text, children: hints.detail }) : null] }));
115
115
  }
116
- const busyText = chatBusyDisplayValue(terminalWidth);
116
+ const busyText = chatBusyDisplayValue(terminalWidth, busyDetachable, busyControllable);
117
117
  const prompt = busy ? "run" : ">";
118
118
  if (busy) {
119
119
  return (_jsxs(InputRail, { terminalWidth: terminalWidth, textWidth: displayWidth(busyText ? `${prompt} ${busyText}` : prompt), fill: fillRail, children: [_jsx(Text, { backgroundColor: TUI_THEME.rail, color: TUI_THEME.warning, bold: true, children: prompt }), busyText ? (_jsxs(_Fragment, { children: [_jsx(Text, { backgroundColor: TUI_THEME.rail, children: " " }), _jsx(Text, { backgroundColor: TUI_THEME.rail, color: TUI_THEME.warning, children: busyText })] })) : null] }));
@@ -370,13 +370,19 @@ function selectChatPlaceholder(terminalWidth, candidates) {
370
370
  function chatPlaceholderValueWidth(terminalWidth) {
371
371
  return Math.max(1, terminalWidth - (terminalWidth >= 10 ? 6 : 4));
372
372
  }
373
- export function chatBusyDisplayValue(terminalWidth) {
373
+ export function chatBusyDisplayValue(terminalWidth, detachable = false, controllable = true) {
374
374
  if (terminalWidth < 14) {
375
375
  return "";
376
376
  }
377
377
  if (terminalWidth < 22) {
378
378
  return "busy";
379
379
  }
380
+ if (detachable && !controllable) {
381
+ return terminalWidth >= 34 ? "observing · ^C detach" : "^C detach";
382
+ }
383
+ if (terminalWidth >= 48 && detachable) {
384
+ return "working · Esc stop · ^C detach";
385
+ }
380
386
  if (terminalWidth >= 34) {
381
387
  return "working · Esc stop";
382
388
  }
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const version = "0.3.2";
1
+ export const version = "0.4.0";
@@ -10,6 +10,7 @@ export class MockWorkerAdapter {
10
10
  await spec.onNativeSession?.(nativeSessionId);
11
11
  await setStatus(spec, "running", "mock-running", `${spec.role} mock worker running`, nativeSessionId);
12
12
  await appendText(spec.outputLogPath, `[mock:${spec.role}] started\n`);
13
+ await waitForConfiguredMockDelay(spec);
13
14
  if (spec.role === "judge") {
14
15
  if (spec.prompt.includes("# Role:") && spec.prompt.includes("· Final acceptance")) {
15
16
  const criterionIds = promptJsonArray(spec.prompt, "Required acceptance criterion ids");
@@ -67,6 +68,23 @@ export class MockWorkerAdapter {
67
68
  };
68
69
  }
69
70
  }
71
+ async function waitForConfiguredMockDelay(spec) {
72
+ const configured = Number(spec.modelConfig?.env?.PCT_MOCK_DELAY_MS ?? 0);
73
+ const delayMs = Number.isFinite(configured) ? Math.min(10000, Math.max(0, Math.trunc(configured))) : 0;
74
+ if (delayMs === 0 || spec.signal?.aborted) {
75
+ return;
76
+ }
77
+ await new Promise((resolve) => {
78
+ const timer = setTimeout(finish, delayMs);
79
+ const signal = spec.signal;
80
+ function finish() {
81
+ clearTimeout(timer);
82
+ signal?.removeEventListener("abort", finish);
83
+ resolve();
84
+ }
85
+ signal?.addEventListener("abort", finish, { once: true });
86
+ });
87
+ }
70
88
  async function cancelMockWorker(spec, nativeSessionId) {
71
89
  await appendText(spec.outputLogPath, "[mock] cancelled\n");
72
90
  await setStatus(spec, "cancelled", "mock-cancelled", `${spec.role} mock worker cancelled`, nativeSessionId);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "parallel-codex-tui",
3
- "version": "0.3.2",
3
+ "version": "0.4.0",
4
4
  "description": "A TypeScript TUI wrapper for routed parallel coding with Codex and Claude workers.",
5
5
  "license": "MIT",
6
6
  "type": "module",