parallel-codex-tui 0.3.1 → 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);
@@ -119,6 +121,7 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
119
121
  const [taskSessionsError, setTaskSessionsError] = useState(null);
120
122
  const [taskSessionsNotice, setTaskSessionsNotice] = useState(null);
121
123
  const [taskSessionsIncludeArchived, setTaskSessionsIncludeArchived] = useState(false);
124
+ const [taskSessionQuery, setTaskSessionQuery] = useState("");
122
125
  const [taskSessionAction, setTaskSessionAction] = useState(null);
123
126
  const [taskSessionDetails, setTaskSessionDetails] = useState(null);
124
127
  const [taskSessionDetailSelectedWorkerIndex, setTaskSessionDetailSelectedWorkerIndex] = useState(0);
@@ -147,6 +150,7 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
147
150
  const copyTextByViewRef = useRef({ chat: "", worker: "" });
148
151
  const messagesRef = useRef([...initialMessages]);
149
152
  const activeRunControllerRef = useRef(null);
153
+ const backgroundRestoreStartedRef = useRef(false);
150
154
  const activeTaskIdRef = useRef(initialTaskId);
151
155
  const nativeInputRef = useRef(nativeInput);
152
156
  const inputRef = useRef(input);
@@ -174,6 +178,7 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
174
178
  const mainConversationActionRef = useRef(mainConversationAction);
175
179
  const taskSessionsLoadingRef = useRef(taskSessionsLoading);
176
180
  const taskSessionsIncludeArchivedRef = useRef(taskSessionsIncludeArchived);
181
+ const taskSessionQueryRef = useRef("");
177
182
  const taskSessionActionRef = useRef(taskSessionAction);
178
183
  const taskSessionDetailsRef = useRef(null);
179
184
  const taskSessionDetailSelectedWorkerIndexRef = useRef(0);
@@ -268,6 +273,8 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
268
273
  && !busy
269
274
  && canRetryTask
270
275
  && selectedBoardFeature?.state !== "approved";
276
+ const backgroundRunAttached = orchestrator.backgroundRunAttached?.() ?? false;
277
+ const backgroundRunControllable = orchestrator.backgroundRunControllable?.() ?? backgroundRunAttached;
271
278
  const visibleStatus = statusLineWithWorkerRefs(status, workers);
272
279
  const visibleRouteStatus = routePending
273
280
  ? formatRoutePendingStatus(routePending, routeElapsedMs)
@@ -288,6 +295,9 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
288
295
  : roleConfigurationSnapshot.future
289
296
  : configuredRoleSelection(config);
290
297
  const roleConfigurationHasOverride = roleConfigurationScopeHasOverride(roleConfigurationSnapshot, roleConfigurationScope);
298
+ useEffect(() => orchestrator.subscribeBackgroundRunState?.(() => {
299
+ setBackgroundRunRevision((current) => current + 1);
300
+ }), [orchestrator]);
291
301
  useEffect(() => {
292
302
  inputRef.current = input;
293
303
  }, [input]);
@@ -453,6 +463,9 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
453
463
  useEffect(() => {
454
464
  taskSessionsIncludeArchivedRef.current = taskSessionsIncludeArchived;
455
465
  }, [taskSessionsIncludeArchived]);
466
+ useEffect(() => {
467
+ taskSessionQueryRef.current = taskSessionQuery;
468
+ }, [taskSessionQuery]);
456
469
  useEffect(() => {
457
470
  taskSessionActionRef.current = taskSessionAction;
458
471
  }, [taskSessionAction]);
@@ -628,6 +641,70 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
628
641
  active = false;
629
642
  };
630
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]);
631
708
  useEffect(() => {
632
709
  if (workers.length === 0 || !status) {
633
710
  return;
@@ -763,8 +840,7 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
763
840
  process.stdout.write("\x1b[?2004h\x1b[?1006l\x1b[?1003l\x1b[?1002l\x1b[?1000l");
764
841
  const commitChatInputUpdate = (update, previousValue, previousCursor) => {
765
842
  if (update.exit) {
766
- activeRunControllerRef.current?.abort();
767
- exitRef.current();
843
+ requestAppExit();
768
844
  return false;
769
845
  }
770
846
  if (busyRef.current) {
@@ -877,8 +953,7 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
877
953
  if (currentView === "chat" && routeFallbackPromptRef.current) {
878
954
  const fallbackChunks = tokenizeRawInput(chunk);
879
955
  if (fallbackChunks.some((fallbackChunk) => isExitShortcut(fallbackChunk, {}))) {
880
- activeRunControllerRef.current?.abort();
881
- exitRef.current();
956
+ requestAppExit();
882
957
  return;
883
958
  }
884
959
  if (fallbackChunks.some((fallbackChunk) => isStatusDetailsShortcut(fallbackChunk, {}))) {
@@ -928,8 +1003,7 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
928
1003
  if (currentView === "roles") {
929
1004
  const roleChunks = tokenizeRawInput(chunk);
930
1005
  if (roleChunks.some((roleChunk) => isExitShortcut(roleChunk, {}))) {
931
- activeRunControllerRef.current?.abort();
932
- exitRef.current();
1006
+ requestAppExit();
933
1007
  return;
934
1008
  }
935
1009
  if (roleChunks.some((roleChunk) => isStatusDetailsShortcut(roleChunk, {}))) {
@@ -1044,8 +1118,7 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
1044
1118
  }
1045
1119
  if (currentView === "status") {
1046
1120
  if (isExitShortcut(chunk, {})) {
1047
- activeRunControllerRef.current?.abort();
1048
- exitRef.current();
1121
+ requestAppExit();
1049
1122
  return;
1050
1123
  }
1051
1124
  if (isStatusDetailsShortcut(chunk, {}) || chunk === "\x1b") {
@@ -1063,8 +1136,7 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
1063
1136
  }
1064
1137
  if (currentView === "sessions") {
1065
1138
  if (isExitShortcut(chunk, {})) {
1066
- activeRunControllerRef.current?.abort();
1067
- exitRef.current();
1139
+ requestAppExit();
1068
1140
  return;
1069
1141
  }
1070
1142
  if (taskSessionDetailsRef.current) {
@@ -1246,10 +1318,38 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
1246
1318
  const sessionAction = taskSessionActionRef.current;
1247
1319
  if (sessionAction) {
1248
1320
  if (chunk === "\x1b") {
1321
+ if (sessionAction.type === "search") {
1322
+ const preferred = taskSessionsRef.current[selectedTaskSessionIndexRef.current]?.id ?? null;
1323
+ updateTaskSessionAction(null);
1324
+ setTaskSessionsError(null);
1325
+ void refreshTaskSessionsRef.current(preferred, sessionAction.previousQuery);
1326
+ return;
1327
+ }
1249
1328
  updateTaskSessionAction(null);
1250
1329
  setTaskSessionsError(null);
1251
1330
  return;
1252
1331
  }
1332
+ if (sessionAction.type === "search") {
1333
+ const update = applyChatInputChunk(sessionAction.value, chunk, sessionAction.cursor);
1334
+ const preferred = taskSessionsRef.current[selectedTaskSessionIndexRef.current]?.id ?? null;
1335
+ if (update.submit !== null) {
1336
+ updateTaskSessionAction(null);
1337
+ setTaskSessionsError(null);
1338
+ void refreshTaskSessionsRef.current(preferred, update.submit);
1339
+ }
1340
+ else {
1341
+ updateTaskSessionAction({
1342
+ ...sessionAction,
1343
+ value: update.value,
1344
+ cursor: update.cursor
1345
+ });
1346
+ if (update.value !== sessionAction.value) {
1347
+ setTaskSessionsError(null);
1348
+ void refreshTaskSessionsRef.current(preferred, update.value);
1349
+ }
1350
+ }
1351
+ return;
1352
+ }
1253
1353
  if (taskSessionsLoadingRef.current) {
1254
1354
  return;
1255
1355
  }
@@ -1286,6 +1386,17 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
1286
1386
  if (taskSessionsLoadingRef.current) {
1287
1387
  return;
1288
1388
  }
1389
+ if (isWorkerSearchShortcut(chunk, {})) {
1390
+ updateTaskSessionAction({
1391
+ type: "search",
1392
+ value: taskSessionQueryRef.current,
1393
+ cursor: Array.from(taskSessionQueryRef.current).length,
1394
+ previousQuery: taskSessionQueryRef.current
1395
+ });
1396
+ setTaskSessionsError(null);
1397
+ setTaskSessionsNotice(null);
1398
+ return;
1399
+ }
1289
1400
  if (isRouterDiagnosticsShortcut(chunk, {})) {
1290
1401
  void openRouterDiagnosticsRef.current();
1291
1402
  return;
@@ -1303,6 +1414,11 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
1303
1414
  return;
1304
1415
  }
1305
1416
  const selectedSession = taskSessionsRef.current[selectedTaskSessionIndexRef.current];
1417
+ if ((chunk === "x" || chunk === "X") && taskSessionQueryRef.current.trim()) {
1418
+ void refreshTaskSessionsRef.current(selectedSession?.id ?? null, "");
1419
+ setTaskSessionsNotice("Task search cleared");
1420
+ return;
1421
+ }
1306
1422
  if (chunk === "r" || chunk === "R") {
1307
1423
  if (selectedSession) {
1308
1424
  updateTaskSessionAction({
@@ -1373,8 +1489,7 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
1373
1489
  if (currentView === "features") {
1374
1490
  const featureChunks = tokenizeRawInput(chunk);
1375
1491
  if (featureChunks.some((featureChunk) => isExitShortcut(featureChunk, {}))) {
1376
- activeRunControllerRef.current?.abort();
1377
- exitRef.current();
1492
+ requestAppExit();
1378
1493
  return;
1379
1494
  }
1380
1495
  const pendingCancel = featureCancelPromptRef.current;
@@ -1526,8 +1641,7 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
1526
1641
  if (currentView === "collaboration") {
1527
1642
  const timelineChunks = tokenizeRawInput(chunk);
1528
1643
  if (timelineChunks.some((timelineChunk) => isExitShortcut(timelineChunk, {}))) {
1529
- activeRunControllerRef.current?.abort();
1530
- exitRef.current();
1644
+ requestAppExit();
1531
1645
  return;
1532
1646
  }
1533
1647
  for (const timelineChunk of timelineChunks) {
@@ -1617,8 +1731,7 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
1617
1731
  }
1618
1732
  if (currentView === "workers") {
1619
1733
  if (isExitShortcut(chunk, {})) {
1620
- activeRunControllerRef.current?.abort();
1621
- exitRef.current();
1734
+ requestAppExit();
1622
1735
  return;
1623
1736
  }
1624
1737
  if (isWorkerOverviewShortcut(chunk, {}) || chunk === "\x1b") {
@@ -1681,8 +1794,7 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
1681
1794
  if (currentView === "router") {
1682
1795
  const routerChunks = tokenizeRawInput(chunk);
1683
1796
  if (routerChunks.some((routerChunk) => isExitShortcut(routerChunk, {}))) {
1684
- activeRunControllerRef.current?.abort();
1685
- exitRef.current();
1797
+ requestAppExit();
1686
1798
  return;
1687
1799
  }
1688
1800
  for (const routerChunk of routerChunks) {
@@ -1719,8 +1831,7 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
1719
1831
  if (currentView === "worker") {
1720
1832
  for (const workerChunk of tokenizeRawInput(chunk)) {
1721
1833
  if (isExitShortcut(workerChunk, {})) {
1722
- activeRunControllerRef.current?.abort();
1723
- exitRef.current();
1834
+ requestAppExit();
1724
1835
  return;
1725
1836
  }
1726
1837
  if (workerSearchRef.current.open) {
@@ -1975,6 +2086,7 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
1975
2086
  nativeAttachRequestSequenceRef.current += 1;
1976
2087
  nativeAttachRef.current?.process.kill();
1977
2088
  nativeAttachRef.current = null;
2089
+ orchestrator.detachBackgroundRuns?.();
1978
2090
  activeRunControllerRef.current?.abort();
1979
2091
  collaborationLoadSequenceRef.current += 1;
1980
2092
  routerLoadSequenceRef.current += 1;
@@ -1990,8 +2102,7 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
1990
2102
  return;
1991
2103
  }
1992
2104
  const shutdown = () => {
1993
- activeRunControllerRef.current?.abort();
1994
- exitRef.current();
2105
+ requestAppExit();
1995
2106
  };
1996
2107
  if (shutdownSignal.aborted) {
1997
2108
  shutdown();
@@ -2000,6 +2111,11 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
2000
2111
  shutdownSignal.addEventListener("abort", shutdown, { once: true });
2001
2112
  return () => shutdownSignal.removeEventListener("abort", shutdown);
2002
2113
  }, [shutdownSignal]);
2114
+ function requestAppExit() {
2115
+ orchestrator.detachBackgroundRuns?.();
2116
+ activeRunControllerRef.current?.abort();
2117
+ exitRef.current();
2118
+ }
2003
2119
  async function copyVisibleView() {
2004
2120
  const payload = visibleClipboardPayload(viewRef.current);
2005
2121
  if (!payload?.text.trim()) {
@@ -2147,8 +2263,7 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
2147
2263
  useInput((inputKey, key) => {
2148
2264
  if (view === "worker") {
2149
2265
  if (isExitShortcut(inputKey, key)) {
2150
- activeRunControllerRef.current?.abort();
2151
- exitRef.current();
2266
+ requestAppExit();
2152
2267
  return;
2153
2268
  }
2154
2269
  if (isNewConversationShortcut(inputKey, key) && !busy) {
@@ -2370,14 +2485,14 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
2370
2485
  function settleRouteFallbackChoice(choice) {
2371
2486
  routeFallbackResolverRef.current?.(choice);
2372
2487
  }
2373
- async function appendVisibleMessage(message, taskId) {
2488
+ async function appendVisibleMessage(message, taskId, options = {}) {
2374
2489
  const visibleMessage = taskId ? { ...message, taskId } : message;
2375
2490
  setMessages((current) => {
2376
2491
  const next = [...current, visibleMessage];
2377
2492
  messagesRef.current = next;
2378
2493
  return next;
2379
2494
  });
2380
- if (!persistChatMessage) {
2495
+ if (!persistChatMessage || options.persist === false) {
2381
2496
  return;
2382
2497
  }
2383
2498
  try {
@@ -2464,13 +2579,17 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
2464
2579
  : false);
2465
2580
  await refreshRoleConfigurationState(nextMemory.activeTaskId);
2466
2581
  setRouteAnnouncement(null);
2467
- 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?.();
2468
2584
  if (result.mode === "complex" && parseTaskResultSummary(result.summary)) {
2469
2585
  setTaskResultExpanded(true);
2470
2586
  }
2471
2587
  }
2472
2588
  catch (error) {
2473
2589
  setRouteAnnouncement(null);
2590
+ if (isSupervisorDetachedError(error)) {
2591
+ return;
2592
+ }
2474
2593
  const retryTaskId = activeTaskIdRef.current;
2475
2594
  if (retryTaskId) {
2476
2595
  setCanRetryTask(await orchestrator.canRetryTask(retryTaskId));
@@ -2479,7 +2598,8 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
2479
2598
  await appendVisibleMessage({
2480
2599
  from: "system",
2481
2600
  text: isAbortError(error) ? "cancelled · request stopped" : error instanceof Error ? error.message : String(error)
2482
- }, retryTaskId ?? undefined);
2601
+ }, retryTaskId ?? undefined, { persist: !orchestrator.persistsRunResults });
2602
+ await orchestrator.acknowledgeBackgroundRun?.();
2483
2603
  }
2484
2604
  finally {
2485
2605
  if (activeRunControllerRef.current === controller) {
@@ -2525,19 +2645,24 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
2525
2645
  setCanRetryTask(false);
2526
2646
  await refreshRoleConfigurationState(taskId);
2527
2647
  setRouteAnnouncement(null);
2528
- await appendVisibleMessage({ from: "system", text: result.summary }, taskId);
2648
+ await appendVisibleMessage({ from: "system", text: result.summary }, taskId, { persist: !orchestrator.persistsRunResults });
2649
+ await orchestrator.acknowledgeBackgroundRun?.();
2529
2650
  if (parseTaskResultSummary(result.summary)) {
2530
2651
  setTaskResultExpanded(true);
2531
2652
  }
2532
2653
  }
2533
2654
  catch (error) {
2534
2655
  setRouteAnnouncement(null);
2656
+ if (isSupervisorDetachedError(error)) {
2657
+ return;
2658
+ }
2535
2659
  setCanRetryTask(await orchestrator.canRetryTask(taskId));
2536
2660
  await refreshRoleConfigurationState(taskId);
2537
2661
  await appendVisibleMessage({
2538
2662
  from: "system",
2539
2663
  text: isAbortError(error) ? "cancelled · retry stopped" : error instanceof Error ? error.message : String(error)
2540
- }, taskId);
2664
+ }, taskId, { persist: !orchestrator.persistsRunResults });
2665
+ await orchestrator.acknowledgeBackgroundRun?.();
2541
2666
  }
2542
2667
  finally {
2543
2668
  if (activeRunControllerRef.current === controller) {
@@ -2656,11 +2781,16 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
2656
2781
  setTaskSessionDetailNotice(null);
2657
2782
  updateTaskSessionAction(null);
2658
2783
  updateMainConversationAction(null);
2784
+ taskSessionQueryRef.current = "";
2785
+ setTaskSessionQuery("");
2659
2786
  sessionCenterModeRef.current = "tasks";
2660
2787
  setSessionCenterMode("tasks");
2661
2788
  await refreshTaskSessions(activeTaskIdRef.current);
2662
2789
  }
2663
- async function refreshTaskSessions(preferredTaskId = null) {
2790
+ async function refreshTaskSessions(preferredTaskId = null, query = taskSessionQueryRef.current) {
2791
+ const normalizedQuery = query.replace(/[\u0000-\u001f\u007f]/g, " ").slice(0, 1000);
2792
+ taskSessionQueryRef.current = normalizedQuery;
2793
+ setTaskSessionQuery(normalizedQuery);
2664
2794
  taskSessionsLoadingRef.current = true;
2665
2795
  setTaskSessionsLoading(true);
2666
2796
  const sequence = taskSessionsLoadSequenceRef.current + 1;
@@ -2670,7 +2800,8 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
2670
2800
  throw new Error("Task sessions are unavailable");
2671
2801
  }
2672
2802
  const tasks = await loadTaskSessions({
2673
- includeArchived: taskSessionsIncludeArchivedRef.current
2803
+ includeArchived: taskSessionsIncludeArchivedRef.current,
2804
+ ...(normalizedQuery.trim() ? { query: normalizedQuery } : {})
2674
2805
  });
2675
2806
  if (taskSessionsLoadSequenceRef.current !== sequence) {
2676
2807
  return;
@@ -3519,7 +3650,7 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
3519
3650
  if (view === "workspace") {
3520
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) }) }));
3521
3652
  }
3522
- 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"
3523
3654
  ? mainConversationAction?.type === "rename"
3524
3655
  ? { type: "rename", value: mainConversationAction.value, cursor: mainConversationAction.cursor }
3525
3656
  : mainConversationAction?.type === "delete"
@@ -3529,9 +3660,11 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
3529
3660
  ? { type: "rename", value: taskSessionAction.value, cursor: taskSessionAction.cursor }
3530
3661
  : taskSessionAction?.type === "delete"
3531
3662
  ? { type: "delete", title: taskSessionAction.title }
3532
- : null, taskSessionsIncludeArchived: sessionCenterMode === "conversations"
3663
+ : taskSessionAction?.type === "search"
3664
+ ? { type: "search", value: taskSessionAction.value, cursor: taskSessionAction.cursor }
3665
+ : null, taskSessionsIncludeArchived: sessionCenterMode === "conversations"
3533
3666
  ? mainConversationsIncludeArchived
3534
- : taskSessionsIncludeArchived, mainConversationSessions: sessionCenterMode === "conversations" && !taskSessionDetails, 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, clipboardNotice: clipboardNotice, roleScope: roleConfigurationScope, roleEditingModel: roleModelEdit, roleCanApply: roleConfigurationScope !== "task" || Boolean(activeTaskId), roleSaving: roleConfigurationSaving, roleHasOverride: roleConfigurationHasOverride, value: workerSearch.open && view === "worker"
3667
+ : taskSessionsIncludeArchived, taskSessionQuery: taskSessionQuery, mainConversationSessions: sessionCenterMode === "conversations" && !taskSessionDetails, 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, clipboardNotice: clipboardNotice, roleScope: roleConfigurationScope, roleEditingModel: roleModelEdit, roleCanApply: roleConfigurationScope !== "task" || Boolean(activeTaskId), roleSaving: roleConfigurationSaving, roleHasOverride: roleConfigurationHasOverride, value: workerSearch.open && view === "worker"
3535
3668
  ? workerSearch.query
3536
3669
  : view === "native" || view === "router" || view === "workers" || view === "features" || view === "sessions" || view === "collaboration" || view === "status" || view === "roles" ? "" : input, cursor: workerSearch.open && view === "worker"
3537
3670
  ? workerSearch.cursor
@@ -3539,7 +3672,7 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
3539
3672
  ? { type: "rename", title: mainConversationAction.title }
3540
3673
  : mainConversationAction?.type === "delete"
3541
3674
  ? { type: "delete", title: mainConversationAction.title }
3542
- : null, loading: taskSessionsLoading, error: taskSessionsError, height: contentHeight, terminalWidth: terminalWidth })) : (_jsx(TaskSessionsView, { tasks: taskSessions, activeTaskId: activeTaskId, selectedIndex: selectedTaskSessionIndex, includeArchived: taskSessionsIncludeArchived, notice: taskSessionsNotice, action: taskSessionAction?.type === "rename"
3675
+ : null, loading: taskSessionsLoading, error: taskSessionsError, height: contentHeight, terminalWidth: terminalWidth })) : (_jsx(TaskSessionsView, { tasks: taskSessions, activeTaskId: activeTaskId, selectedIndex: selectedTaskSessionIndex, includeArchived: taskSessionsIncludeArchived, query: taskSessionQuery, notice: taskSessionsNotice, action: taskSessionAction?.type === "rename"
3543
3676
  ? { type: "rename", title: taskSessionAction.title }
3544
3677
  : taskSessionAction?.type === "delete"
3545
3678
  ? { type: "delete", title: taskSessionAction.title }
@@ -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, 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) {
@@ -42,6 +42,13 @@ export function InputBar({ mode, ready = true, busy = false, routeFallback = fal
42
42
  return (_jsxs(InputRail, { terminalWidth: terminalWidth, textWidth: displayWidth(`${hints.label}${hints.detail}`), fill: fillRail, children: [_jsx(Text, { backgroundColor: TUI_THEME.rail, color: TUI_THEME.accent, bold: true, children: hints.label }), hints.detail ? _jsx(Text, { backgroundColor: TUI_THEME.rail, color: TUI_THEME.muted, children: hints.detail }) : null] }));
43
43
  }
44
44
  if (mode === "sessions") {
45
+ if (taskSessionAction?.type === "search") {
46
+ const prefix = terminalWidth < 12 ? "/ " : "find > ";
47
+ const valueWidth = Math.max(1, terminalWidth - displayWidth(prefix) - 3);
48
+ const display = chatInputDisplayParts(taskSessionAction.value, taskSessionAction.cursor, valueWidth);
49
+ const textWidth = displayWidth(`${prefix}${display.before}|${display.after}`);
50
+ return (_jsxs(InputRail, { terminalWidth: terminalWidth, textWidth: textWidth, fill: fillRail, children: [_jsx(Text, { backgroundColor: TUI_THEME.rail, color: TUI_THEME.accent, bold: true, children: prefix }), _jsx(Text, { backgroundColor: TUI_THEME.rail, color: TUI_THEME.text, children: display.before }), _jsx(Text, { backgroundColor: TUI_THEME.rail, color: TUI_THEME.accent, bold: true, children: "|" }), _jsx(Text, { backgroundColor: TUI_THEME.rail, color: TUI_THEME.text, children: display.after })] }));
51
+ }
45
52
  if (taskSessionAction?.type === "rename") {
46
53
  const prefix = terminalWidth < 12 ? "> " : "rename > ";
47
54
  const valueWidth = Math.max(1, terminalWidth - displayWidth(prefix) - 3);
@@ -61,7 +68,7 @@ export function InputBar({ mode, ready = true, busy = false, routeFallback = fal
61
68
  const hints = mainConversationSessionsInputHints(terminalWidth, taskSessionsIncludeArchived);
62
69
  return (_jsxs(InputRail, { terminalWidth: terminalWidth, textWidth: displayWidth(`${hints.label}${hints.detail}`), fill: fillRail, children: [_jsx(Text, { backgroundColor: TUI_THEME.rail, color: TUI_THEME.accent, bold: true, children: hints.label }), hints.detail ? _jsx(Text, { backgroundColor: TUI_THEME.rail, color: TUI_THEME.muted, children: hints.detail }) : null] }));
63
70
  }
64
- const hints = taskSessionsInputHints(terminalWidth, taskSessionsIncludeArchived);
71
+ const hints = taskSessionsInputHints(terminalWidth, taskSessionsIncludeArchived, Boolean(taskSessionQuery.trim()));
65
72
  return (_jsxs(InputRail, { terminalWidth: terminalWidth, textWidth: displayWidth(`${hints.label}${hints.detail}`), fill: fillRail, children: [_jsx(Text, { backgroundColor: TUI_THEME.rail, color: TUI_THEME.accent, bold: true, children: hints.label }), hints.detail ? _jsx(Text, { backgroundColor: TUI_THEME.rail, color: TUI_THEME.muted, children: hints.detail }) : null] }));
66
73
  }
67
74
  if (mode === "collaboration") {
@@ -106,7 +113,7 @@ export function InputBar({ mode, ready = true, busy = false, routeFallback = fal
106
113
  const hints = routeFallbackInputHints(terminalWidth);
107
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] }));
108
115
  }
109
- const busyText = chatBusyDisplayValue(terminalWidth);
116
+ const busyText = chatBusyDisplayValue(terminalWidth, busyDetachable, busyControllable);
110
117
  const prompt = busy ? "run" : ">";
111
118
  if (busy) {
112
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] }));
@@ -363,13 +370,19 @@ function selectChatPlaceholder(terminalWidth, candidates) {
363
370
  function chatPlaceholderValueWidth(terminalWidth) {
364
371
  return Math.max(1, terminalWidth - (terminalWidth >= 10 ? 6 : 4));
365
372
  }
366
- export function chatBusyDisplayValue(terminalWidth) {
373
+ export function chatBusyDisplayValue(terminalWidth, detachable = false, controllable = true) {
367
374
  if (terminalWidth < 14) {
368
375
  return "";
369
376
  }
370
377
  if (terminalWidth < 22) {
371
378
  return "busy";
372
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
+ }
373
386
  if (terminalWidth >= 34) {
374
387
  return "working · Esc stop";
375
388
  }
@@ -596,10 +609,11 @@ function collaborationDetailInputHints(width) {
596
609
  { label: "e", detail: "" }
597
610
  ]);
598
611
  }
599
- function taskSessionsInputHints(width, includeArchived) {
612
+ function taskSessionsInputHints(width, includeArchived, hasQuery) {
600
613
  const archivedAction = includeArchived ? "H hide archived" : "H archived";
614
+ const searchAction = hasQuery ? "^F edit · X clear" : "^F find";
601
615
  return selectInputHints(width, [
602
- { label: "sessions", detail: ` · Up/Dn select · Enter restore · C conversations · I inspect · R rename · A archive · D delete · E export · ${archivedAction} · Esc back` },
616
+ { label: "sessions", detail: ` · Up/Dn select · Enter restore · C conversations · I inspect · R rename · A archive · D delete · E export · ${archivedAction} · Esc back · ${searchAction}` },
603
617
  { label: "sessions", detail: " · Up/Dn select · Enter restore · C conversations · I inspect · R rename · A archive · D delete · E export · Esc back" },
604
618
  { label: "sessions", detail: " · Up/Dn select · Enter restore · C chats · I inspect · R rename · A archive · D delete · Esc back" },
605
619
  { label: "sessions", detail: " · Up/Dn select · Enter restore · C chats · I inspect · R rename · A archive · Esc back" },