parallel-codex-tui 0.3.0 → 0.3.2

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
@@ -119,6 +119,7 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
119
119
  const [taskSessionsError, setTaskSessionsError] = useState(null);
120
120
  const [taskSessionsNotice, setTaskSessionsNotice] = useState(null);
121
121
  const [taskSessionsIncludeArchived, setTaskSessionsIncludeArchived] = useState(false);
122
+ const [taskSessionQuery, setTaskSessionQuery] = useState("");
122
123
  const [taskSessionAction, setTaskSessionAction] = useState(null);
123
124
  const [taskSessionDetails, setTaskSessionDetails] = useState(null);
124
125
  const [taskSessionDetailSelectedWorkerIndex, setTaskSessionDetailSelectedWorkerIndex] = useState(0);
@@ -174,6 +175,7 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
174
175
  const mainConversationActionRef = useRef(mainConversationAction);
175
176
  const taskSessionsLoadingRef = useRef(taskSessionsLoading);
176
177
  const taskSessionsIncludeArchivedRef = useRef(taskSessionsIncludeArchived);
178
+ const taskSessionQueryRef = useRef("");
177
179
  const taskSessionActionRef = useRef(taskSessionAction);
178
180
  const taskSessionDetailsRef = useRef(null);
179
181
  const taskSessionDetailSelectedWorkerIndexRef = useRef(0);
@@ -264,7 +266,7 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
264
266
  && (config.workers[selectedTaskSessionDetailWorker.engine]?.interactive.forkArgs.length ?? 0) > 0);
265
267
  const featureCanCancel = busy && (selectedBoardFeature?.state === "actor_running"
266
268
  || selectedBoardFeature?.state === "critic_running");
267
- const featureCanReassign = assignableProviderIds.length > 1
269
+ const featureCanReassign = assignableProviderIds.length > 0
268
270
  && !busy
269
271
  && canRetryTask
270
272
  && selectedBoardFeature?.state !== "approved";
@@ -453,6 +455,9 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
453
455
  useEffect(() => {
454
456
  taskSessionsIncludeArchivedRef.current = taskSessionsIncludeArchived;
455
457
  }, [taskSessionsIncludeArchived]);
458
+ useEffect(() => {
459
+ taskSessionQueryRef.current = taskSessionQuery;
460
+ }, [taskSessionQuery]);
456
461
  useEffect(() => {
457
462
  taskSessionActionRef.current = taskSessionAction;
458
463
  }, [taskSessionAction]);
@@ -1246,10 +1251,38 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
1246
1251
  const sessionAction = taskSessionActionRef.current;
1247
1252
  if (sessionAction) {
1248
1253
  if (chunk === "\x1b") {
1254
+ if (sessionAction.type === "search") {
1255
+ const preferred = taskSessionsRef.current[selectedTaskSessionIndexRef.current]?.id ?? null;
1256
+ updateTaskSessionAction(null);
1257
+ setTaskSessionsError(null);
1258
+ void refreshTaskSessionsRef.current(preferred, sessionAction.previousQuery);
1259
+ return;
1260
+ }
1249
1261
  updateTaskSessionAction(null);
1250
1262
  setTaskSessionsError(null);
1251
1263
  return;
1252
1264
  }
1265
+ if (sessionAction.type === "search") {
1266
+ const update = applyChatInputChunk(sessionAction.value, chunk, sessionAction.cursor);
1267
+ const preferred = taskSessionsRef.current[selectedTaskSessionIndexRef.current]?.id ?? null;
1268
+ if (update.submit !== null) {
1269
+ updateTaskSessionAction(null);
1270
+ setTaskSessionsError(null);
1271
+ void refreshTaskSessionsRef.current(preferred, update.submit);
1272
+ }
1273
+ else {
1274
+ updateTaskSessionAction({
1275
+ ...sessionAction,
1276
+ value: update.value,
1277
+ cursor: update.cursor
1278
+ });
1279
+ if (update.value !== sessionAction.value) {
1280
+ setTaskSessionsError(null);
1281
+ void refreshTaskSessionsRef.current(preferred, update.value);
1282
+ }
1283
+ }
1284
+ return;
1285
+ }
1253
1286
  if (taskSessionsLoadingRef.current) {
1254
1287
  return;
1255
1288
  }
@@ -1286,6 +1319,17 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
1286
1319
  if (taskSessionsLoadingRef.current) {
1287
1320
  return;
1288
1321
  }
1322
+ if (isWorkerSearchShortcut(chunk, {})) {
1323
+ updateTaskSessionAction({
1324
+ type: "search",
1325
+ value: taskSessionQueryRef.current,
1326
+ cursor: Array.from(taskSessionQueryRef.current).length,
1327
+ previousQuery: taskSessionQueryRef.current
1328
+ });
1329
+ setTaskSessionsError(null);
1330
+ setTaskSessionsNotice(null);
1331
+ return;
1332
+ }
1289
1333
  if (isRouterDiagnosticsShortcut(chunk, {})) {
1290
1334
  void openRouterDiagnosticsRef.current();
1291
1335
  return;
@@ -1303,6 +1347,11 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
1303
1347
  return;
1304
1348
  }
1305
1349
  const selectedSession = taskSessionsRef.current[selectedTaskSessionIndexRef.current];
1350
+ if ((chunk === "x" || chunk === "X") && taskSessionQueryRef.current.trim()) {
1351
+ void refreshTaskSessionsRef.current(selectedSession?.id ?? null, "");
1352
+ setTaskSessionsNotice("Task search cleared");
1353
+ return;
1354
+ }
1306
1355
  if (chunk === "r" || chunk === "R") {
1307
1356
  if (selectedSession) {
1308
1357
  updateTaskSessionAction({
@@ -1396,6 +1445,24 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
1396
1445
  const pendingAssignment = featureAssignmentPromptRef.current;
1397
1446
  if (pendingAssignment) {
1398
1447
  for (const featureChunk of featureChunks) {
1448
+ const modelEdit = featureAssignmentPromptRef.current?.modelEdit;
1449
+ if (modelEdit) {
1450
+ if (featureChunk === "\x1b") {
1451
+ updateFeatureAssignmentPrompt({ ...pendingAssignment, modelEdit: undefined });
1452
+ continue;
1453
+ }
1454
+ const update = applyChatInputChunk(modelEdit.value, featureChunk, modelEdit.cursor);
1455
+ if (update.submit !== null) {
1456
+ void reassignSelectedFeatureRef.current(pendingAssignment, modelEdit.role, update.submit);
1457
+ }
1458
+ else {
1459
+ updateFeatureAssignmentPrompt({
1460
+ ...pendingAssignment,
1461
+ modelEdit: { ...modelEdit, value: update.value, cursor: update.cursor }
1462
+ });
1463
+ }
1464
+ continue;
1465
+ }
1399
1466
  if (featureChunk === "\x1b" || featureChunk === "m" || featureChunk === "M") {
1400
1467
  updateFeatureAssignmentPrompt(null);
1401
1468
  setFeatureBoardNotice(null);
@@ -1409,6 +1476,16 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
1409
1476
  void reassignSelectedFeatureRef.current(pendingAssignment, "critic");
1410
1477
  return;
1411
1478
  }
1479
+ if (featureChunk === "1" || featureChunk === "2") {
1480
+ const feature = collaborationTimelineRef.current?.features.find((item) => item.id === pendingAssignment.featureId);
1481
+ const role = featureChunk === "1" ? "actor" : "critic";
1482
+ const value = role === "actor" ? feature?.actorModel ?? "" : feature?.criticModel ?? "";
1483
+ updateFeatureAssignmentPrompt({
1484
+ ...pendingAssignment,
1485
+ modelEdit: { role, value, cursor: Array.from(value).length }
1486
+ });
1487
+ return;
1488
+ }
1412
1489
  }
1413
1490
  return;
1414
1491
  }
@@ -1468,7 +1545,7 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
1468
1545
  featureId: feature.id,
1469
1546
  title: feature.title
1470
1547
  });
1471
- setFeatureBoardNotice(`Reassign ${feature.title} · A Actor (${feature.actorEngine ?? config.pairing.actor}) · C Critic (${feature.criticEngine ?? config.pairing.critic})`);
1548
+ setFeatureBoardNotice(`Reassign ${feature.title} · A Actor (${featureTargetDisplay(config, feature.actorEngine ?? config.pairing.actor, feature.actorModel)}) · C Critic (${featureTargetDisplay(config, feature.criticEngine ?? config.pairing.critic, feature.criticModel)})`);
1472
1549
  return;
1473
1550
  }
1474
1551
  if (featureChunk === "r" || featureChunk === "R") {
@@ -2628,11 +2705,16 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
2628
2705
  setTaskSessionDetailNotice(null);
2629
2706
  updateTaskSessionAction(null);
2630
2707
  updateMainConversationAction(null);
2708
+ taskSessionQueryRef.current = "";
2709
+ setTaskSessionQuery("");
2631
2710
  sessionCenterModeRef.current = "tasks";
2632
2711
  setSessionCenterMode("tasks");
2633
2712
  await refreshTaskSessions(activeTaskIdRef.current);
2634
2713
  }
2635
- async function refreshTaskSessions(preferredTaskId = null) {
2714
+ async function refreshTaskSessions(preferredTaskId = null, query = taskSessionQueryRef.current) {
2715
+ const normalizedQuery = query.replace(/[\u0000-\u001f\u007f]/g, " ").slice(0, 1000);
2716
+ taskSessionQueryRef.current = normalizedQuery;
2717
+ setTaskSessionQuery(normalizedQuery);
2636
2718
  taskSessionsLoadingRef.current = true;
2637
2719
  setTaskSessionsLoading(true);
2638
2720
  const sequence = taskSessionsLoadSequenceRef.current + 1;
@@ -2642,7 +2724,8 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
2642
2724
  throw new Error("Task sessions are unavailable");
2643
2725
  }
2644
2726
  const tasks = await loadTaskSessions({
2645
- includeArchived: taskSessionsIncludeArchivedRef.current
2727
+ includeArchived: taskSessionsIncludeArchivedRef.current,
2728
+ ...(normalizedQuery.trim() ? { query: normalizedQuery } : {})
2646
2729
  });
2647
2730
  if (taskSessionsLoadSequenceRef.current !== sequence) {
2648
2731
  return;
@@ -3087,7 +3170,7 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
3087
3170
  featureAssignmentPromptRef.current = next;
3088
3171
  setFeatureAssignmentPrompt(next);
3089
3172
  }
3090
- async function reassignSelectedFeature(prompt, role) {
3173
+ async function reassignSelectedFeature(prompt, role, model) {
3091
3174
  const feature = collaborationTimelineRef.current?.features.find((item) => item.id === prompt.featureId);
3092
3175
  if (!feature) {
3093
3176
  setFeatureBoardNotice(`Feature no longer exists: ${prompt.featureId}`);
@@ -3097,15 +3180,22 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
3097
3180
  const current = role === "actor"
3098
3181
  ? feature.actorEngine ?? config.pairing.actor
3099
3182
  : feature.criticEngine ?? config.pairing.critic;
3100
- const engine = nextAssignableFeatureEngine(current, assignableProviderIds);
3183
+ const engine = model === undefined
3184
+ ? nextAssignableFeatureEngine(current, assignableProviderIds)
3185
+ : current;
3101
3186
  try {
3102
- await orchestrator.reassignFeature({
3187
+ const result = await orchestrator.reassignFeature({
3103
3188
  taskId: prompt.taskId,
3104
3189
  featureId: prompt.featureId,
3105
3190
  role,
3106
- engine
3191
+ engine,
3192
+ ...(model === undefined ? {} : { model })
3107
3193
  });
3108
- setFeatureBoardNotice(`${role === "actor" ? "Actor" : "Critic"} reassigned to ${featureEngineDisplay(config, engine)} · Ctrl+R resumes the task.`);
3194
+ updateFeatureAssignmentPrompt({ ...prompt, modelEdit: undefined });
3195
+ const assignedModel = role === "actor"
3196
+ ? result.assignment.actor_model
3197
+ : result.assignment.critic_model;
3198
+ setFeatureBoardNotice(`${role === "actor" ? "Actor" : "Critic"} reassigned to ${featureTargetDisplay(config, engine, assignedModel)} · Ctrl+R resumes the task.`);
3109
3199
  await refreshCollaborationTimeline(false);
3110
3200
  }
3111
3201
  catch (error) {
@@ -3383,6 +3473,12 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
3383
3473
  if (typeof orchestrator.updateRoleConfiguration !== "function") {
3384
3474
  throw new Error("Role configuration is unavailable in this runtime");
3385
3475
  }
3476
+ if (typeof orchestrator.validateRoleConfiguration === "function") {
3477
+ const validation = await orchestrator.validateRoleConfiguration(draft);
3478
+ if (!validation.ok) {
3479
+ throw new Error(formatRoleConfigurationPreflightFailure(validation.lines));
3480
+ }
3481
+ }
3386
3482
  const snapshot = await orchestrator.updateRoleConfiguration({
3387
3483
  scope,
3388
3484
  roles: draft,
@@ -3478,7 +3574,7 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
3478
3574
  if (view === "workspace") {
3479
3575
  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) }) }));
3480
3576
  }
3481
- 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), taskSessionAction: sessionCenterMode === "conversations"
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"
3482
3578
  ? mainConversationAction?.type === "rename"
3483
3579
  ? { type: "rename", value: mainConversationAction.value, cursor: mainConversationAction.cursor }
3484
3580
  : mainConversationAction?.type === "delete"
@@ -3488,17 +3584,19 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
3488
3584
  ? { type: "rename", value: taskSessionAction.value, cursor: taskSessionAction.cursor }
3489
3585
  : taskSessionAction?.type === "delete"
3490
3586
  ? { type: "delete", title: taskSessionAction.title }
3491
- : null, taskSessionsIncludeArchived: sessionCenterMode === "conversations"
3587
+ : taskSessionAction?.type === "search"
3588
+ ? { type: "search", value: taskSessionAction.value, cursor: taskSessionAction.cursor }
3589
+ : null, taskSessionsIncludeArchived: sessionCenterMode === "conversations"
3492
3590
  ? mainConversationsIncludeArchived
3493
- : 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"
3591
+ : 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"
3494
3592
  ? workerSearch.query
3495
3593
  : view === "native" || view === "router" || view === "workers" || view === "features" || view === "sessions" || view === "collaboration" || view === "status" || view === "roles" ? "" : input, cursor: workerSearch.open && view === "worker"
3496
3594
  ? workerSearch.cursor
3497
- : view === "chat" ? inputCursor : undefined, terminalWidth: terminalWidth, onChange: view === "native" ? setNativeInput : setInput, onSubmit: view === "native" ? undefined : submit }), error: attachError, children: view === "roles" ? (_jsx(RoleConfigurationView, { snapshot: roleConfigurationSnapshot, draft: roleConfigurationDraft, scope: roleConfigurationScope, selectedRoleIndex: roleConfigurationSelectedIndex, loading: roleConfigurationLoading, saving: roleConfigurationSaving, notice: roleConfigurationNotice, error: roleConfigurationError, hasActiveTask: Boolean(activeTaskId), height: contentHeight, terminalWidth: terminalWidth })) : view === "status" ? (_jsx(StatusDetailView, { cwd: cwd, taskId: activeTaskId, mode: activeMode, busy: busy, canRetry: canRetryTask, taskStatus: visibleTaskStatus, routeStatus: visibleRouteStatus, routeReason: routePending ? undefined : lastRoute?.reason, pairing: config.pairing, roleSelection: activeRoleSelection, configStatus: configChange?.detail, configRestartRequired: configChange?.kind === "restart", 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 })) : sessionCenterMode === "conversations" ? (_jsx(MainConversationsView, { conversations: mainConversations, selectedIndex: selectedMainConversationIndex, includeArchived: mainConversationsIncludeArchived, notice: taskSessionsNotice, action: mainConversationAction?.type === "rename"
3595
+ : view === "chat" ? inputCursor : undefined, terminalWidth: terminalWidth, onChange: view === "native" ? setNativeInput : setInput, onSubmit: view === "native" ? undefined : submit }), error: attachError, children: view === "roles" ? (_jsx(RoleConfigurationView, { snapshot: roleConfigurationSnapshot, draft: roleConfigurationDraft, scope: roleConfigurationScope, selectedRoleIndex: roleConfigurationSelectedIndex, loading: roleConfigurationLoading, saving: roleConfigurationSaving, notice: roleConfigurationNotice, error: roleConfigurationError, hasActiveTask: Boolean(activeTaskId), height: contentHeight, terminalWidth: terminalWidth })) : view === "status" ? (_jsx(StatusDetailView, { cwd: cwd, taskId: activeTaskId, mode: activeMode, busy: busy, canRetry: canRetryTask, taskStatus: visibleTaskStatus, routeStatus: visibleRouteStatus, routeReason: routePending ? undefined : lastRoute?.reason, pairing: config.pairing, roleSelection: activeRoleSelection, roleConfigurationSnapshot: roleConfigurationSnapshot, configStatus: configChange?.detail, configRestartRequired: configChange?.kind === "restart", 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 })) : sessionCenterMode === "conversations" ? (_jsx(MainConversationsView, { conversations: mainConversations, selectedIndex: selectedMainConversationIndex, includeArchived: mainConversationsIncludeArchived, notice: taskSessionsNotice, action: mainConversationAction?.type === "rename"
3498
3596
  ? { type: "rename", title: mainConversationAction.title }
3499
3597
  : mainConversationAction?.type === "delete"
3500
3598
  ? { type: "delete", title: mainConversationAction.title }
3501
- : 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"
3599
+ : 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"
3502
3600
  ? { type: "rename", title: taskSessionAction.title }
3503
3601
  : taskSessionAction?.type === "delete"
3504
3602
  ? { type: "delete", title: taskSessionAction.title }
@@ -4356,6 +4454,10 @@ function sessionDetailWorkerLabel(worker) {
4356
4454
  function featureEngineDisplay(config, engine) {
4357
4455
  return workerProviderLabel(config, engine);
4358
4456
  }
4457
+ function featureTargetDisplay(config, engine, model) {
4458
+ const effectiveModel = model?.trim() || config.workers[engine]?.model.name || "default";
4459
+ return `${featureEngineDisplay(config, engine)}/${effectiveModel}`;
4460
+ }
4359
4461
  function compactChatText(text, maxLength) {
4360
4462
  if (maxLength <= 5) {
4361
4463
  return takeChatTextStartByDisplayWidth(text, maxLength);
@@ -4745,6 +4847,15 @@ function compactNativeAttachRole(label) {
4745
4847
  function compactNativeSessionForTitle(sessionId, maxLength) {
4746
4848
  return compactEndByDisplayWidth(sessionId, Math.min(maxLength, 16));
4747
4849
  }
4850
+ export function formatRoleConfigurationPreflightFailure(lines) {
4851
+ const clean = lines
4852
+ .map((line) => line.replace(/[\u0000-\u001f\u007f]/g, " ").replace(/\s+/g, " ").trim())
4853
+ .filter(Boolean);
4854
+ const actionable = clean.filter((line) => (/\b(?:denied|error|failed|invalid|missing|unavailable|unsupported)\b/i.test(line)
4855
+ || /\bnot (?:found|supported|available)\b/i.test(line)));
4856
+ const detail = (actionable.length > 0 ? actionable : clean).slice(0, 4);
4857
+ return ["Provider preflight failed", ...detail].join(" · ");
4858
+ }
4748
4859
  function isAbortError(error) {
4749
4860
  return error instanceof Error && error.name === "AbortError";
4750
4861
  }
@@ -142,7 +142,10 @@ function featureBoardEvidenceText(timeline, feature, width) {
142
142
  ? `deps ${dependencies.map((item) => safeFeatureBoardText(item.title)).join(", ")}`
143
143
  : "independent";
144
144
  const assignmentText = feature.actorEngine && feature.criticEngine
145
- ? `actor ${feature.actorEngine} · critic ${feature.criticEngine}`
145
+ ? [
146
+ `actor ${feature.actorEngine}/${feature.actorModel?.trim() || "default"}`,
147
+ `critic ${feature.criticEngine}/${feature.criticModel?.trim() || "default"}`
148
+ ].join(" · ")
146
149
  : "";
147
150
  const evidence = feature.latestFinding
148
151
  ? `finding · ${safeFeatureBoardText(feature.latestFinding)}`
@@ -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, 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, 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) {
@@ -30,11 +30,25 @@ export function InputBar({ mode, ready = true, busy = false, routeFallback = fal
30
30
  const hints = roleConfigurationInputHints(terminalWidth, roleScope, roleCanApply, roleSaving, roleHasOverride);
31
31
  return (_jsxs(InputRail, { terminalWidth: terminalWidth, textWidth: displayWidth(`${hints.label}${hints.detail}`), fill: fillRail, children: [_jsx(Text, { backgroundColor: TUI_THEME.rail, color: roleSaving ? TUI_THEME.warning : TUI_THEME.accent, bold: true, children: hints.label }), hints.detail ? _jsx(Text, { backgroundColor: TUI_THEME.rail, color: TUI_THEME.muted, children: hints.detail }) : null] }));
32
32
  }
33
+ if (mode === "features" && featureEditingModel) {
34
+ const prefix = `${featureEditingModel.role} model > `;
35
+ const valueWidth = Math.max(1, terminalWidth - displayWidth(prefix) - 3);
36
+ const display = chatInputDisplayParts(featureEditingModel.value, featureEditingModel.cursor, valueWidth);
37
+ const textWidth = displayWidth(`${prefix}${display.before}|${display.after}`);
38
+ 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 })] }));
39
+ }
33
40
  if (mode === "status") {
34
41
  const hints = statusDetailInputHints(terminalWidth);
35
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] }));
36
43
  }
37
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
+ }
38
52
  if (taskSessionAction?.type === "rename") {
39
53
  const prefix = terminalWidth < 12 ? "> " : "rename > ";
40
54
  const valueWidth = Math.max(1, terminalWidth - displayWidth(prefix) - 3);
@@ -54,7 +68,7 @@ export function InputBar({ mode, ready = true, busy = false, routeFallback = fal
54
68
  const hints = mainConversationSessionsInputHints(terminalWidth, taskSessionsIncludeArchived);
55
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] }));
56
70
  }
57
- const hints = taskSessionsInputHints(terminalWidth, taskSessionsIncludeArchived);
71
+ const hints = taskSessionsInputHints(terminalWidth, taskSessionsIncludeArchived, Boolean(taskSessionQuery.trim()));
58
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] }));
59
73
  }
60
74
  if (mode === "collaboration") {
@@ -481,9 +495,9 @@ function workerOverviewInputHints(width) {
481
495
  function featureBoardInputHints(width, options) {
482
496
  if (options.assignment) {
483
497
  return selectInputHints(width, [
484
- { label: "assign provider", detail: " · A Actor · C Critic · M/Esc done" },
485
- { label: "assign", detail: " · A Actor · C Critic · Esc done" },
486
- { label: "assign", detail: " · A actor · C critic" },
498
+ { label: "assign", detail: " · A/C provider · 1/2 model · M/Esc done" },
499
+ { label: "assign", detail: " · A/C provider · 1/2 model · Esc done" },
500
+ { label: "assign", detail: " · A/C provider · 1/2 model" },
487
501
  { label: "provider", detail: " · A · C · Esc" },
488
502
  { label: "provider", detail: "" },
489
503
  { label: "M", detail: "" }
@@ -589,10 +603,11 @@ function collaborationDetailInputHints(width) {
589
603
  { label: "e", detail: "" }
590
604
  ]);
591
605
  }
592
- function taskSessionsInputHints(width, includeArchived) {
606
+ function taskSessionsInputHints(width, includeArchived, hasQuery) {
593
607
  const archivedAction = includeArchived ? "H hide archived" : "H archived";
608
+ const searchAction = hasQuery ? "^F edit · X clear" : "^F find";
594
609
  return selectInputHints(width, [
595
- { label: "sessions", detail: ` · Up/Dn select · Enter restore · C conversations · I inspect · R rename · A archive · D delete · E export · ${archivedAction} · Esc back` },
610
+ { label: "sessions", detail: ` · Up/Dn select · Enter restore · C conversations · I inspect · R rename · A archive · D delete · E export · ${archivedAction} · Esc back · ${searchAction}` },
596
611
  { label: "sessions", detail: " · Up/Dn select · Enter restore · C conversations · I inspect · R rename · A archive · D delete · E export · Esc back" },
597
612
  { label: "sessions", detail: " · Up/Dn select · Enter restore · C chats · I inspect · R rename · A archive · D delete · Esc back" },
598
613
  { label: "sessions", detail: " · Up/Dn select · Enter restore · C chats · I inspect · R rename · A archive · Esc back" },
@@ -24,10 +24,10 @@ export function statusDetailDisplayLines(input, width, height) {
24
24
  text: fitStatusDetailText(`workspace · ${basename(input.cwd) || input.cwd} · ${input.cwd}`, safeWidth),
25
25
  tone: "muted"
26
26
  },
27
- {
28
- text: fitStatusDetailText(rolePairingDetail(input.pairing, input.roleSelection), safeWidth),
29
- tone: "text"
30
- }
27
+ ...roleMatrixDetailLines(input).map((line) => ({
28
+ text: fitStatusDetailText(line.text, safeWidth),
29
+ tone: line.tone
30
+ }))
31
31
  ];
32
32
  if (input.configStatus?.trim()) {
33
33
  lines.push({
@@ -59,15 +59,36 @@ export function statusDetailDisplayLines(input, width, height) {
59
59
  }
60
60
  return lines.slice(0, maxLines);
61
61
  }
62
- function rolePairingDetail(pairing, selection) {
62
+ function roleMatrixDetail(label, pairing, selection) {
63
63
  return [
64
- "active roles",
64
+ label,
65
65
  rolePairingEntry("main", selection?.main.engine ?? pairing.main, selection?.main.model),
66
66
  rolePairingEntry("judge", selection?.judge.engine ?? pairing.judge, selection?.judge.model),
67
67
  rolePairingEntry("actor", selection?.actor.engine ?? pairing.actor, selection?.actor.model),
68
68
  rolePairingEntry("critic", selection?.critic.engine ?? pairing.critic, selection?.critic.model)
69
69
  ].join(" · ");
70
70
  }
71
+ function roleMatrixDetailLines(input) {
72
+ const snapshot = input.roleConfigurationSnapshot;
73
+ const lines = [{
74
+ text: roleMatrixDetail("actual roles", input.pairing, input.roleSelection),
75
+ tone: "text"
76
+ }];
77
+ if (!snapshot) {
78
+ return lines;
79
+ }
80
+ const next = snapshot.next ?? snapshot.task ?? snapshot.future;
81
+ const source = snapshot.next ? "one shot" : snapshot.task ? "task default" : "future default";
82
+ lines.push({
83
+ text: roleMatrixDetail(`next roles (${source})`, input.pairing, next),
84
+ tone: snapshot.next ? "active" : "muted"
85
+ });
86
+ lines.push({
87
+ text: roleMatrixDetail("future roles", input.pairing, snapshot.future),
88
+ tone: "muted"
89
+ });
90
+ return lines;
91
+ }
71
92
  function rolePairingEntry(role, engine, model) {
72
93
  return [role, engine, model?.trim()].filter(Boolean).join("/");
73
94
  }
@@ -2,13 +2,14 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box, Text } from "ink";
3
3
  import { compactEndByDisplayWidth, displayWidth } from "./display-width.js";
4
4
  import { TUI_THEME } from "./theme.js";
5
- export function TaskSessionsView({ tasks, activeTaskId, selectedIndex, includeArchived = false, notice = null, action = null, loading = false, error = null, height = 20, terminalWidth = process.stdout.columns || 120 }) {
5
+ export function TaskSessionsView({ tasks, activeTaskId, selectedIndex, includeArchived = false, query = "", notice = null, action = null, loading = false, error = null, height = 20, terminalWidth = process.stdout.columns || 120 }) {
6
6
  const viewportHeight = Math.max(1, height);
7
7
  const width = taskSessionsContentWidth(terminalWidth);
8
8
  const lines = taskSessionsDisplayLines(tasks, activeTaskId, selectedIndex, viewportHeight, terminalWidth, {
9
9
  loading,
10
10
  error,
11
11
  includeArchived,
12
+ query,
12
13
  notice,
13
14
  action
14
15
  });
@@ -21,7 +22,10 @@ export function taskSessionsDisplayLines(tasks, activeTaskId, selectedIndex, hei
21
22
  const lines = [
22
23
  {
23
24
  text: fitTaskSessionCandidates([
24
- state.includeArchived ? "Task sessions · archived shown" : "Task sessions",
25
+ taskSessionsHeading(Boolean(state.includeArchived), state.query ?? ""),
26
+ ...((state.query ?? "").trim()
27
+ ? [`Find · ${safeTaskSessionText(state.query ?? "")}`, `/ ${safeTaskSessionText(state.query ?? "")}`]
28
+ : []),
25
29
  state.includeArchived ? "Sessions · all" : "Sessions",
26
30
  "Tasks",
27
31
  "T"
@@ -30,7 +34,7 @@ export function taskSessionsDisplayLines(tasks, activeTaskId, selectedIndex, hei
30
34
  }
31
35
  ];
32
36
  if (viewportHeight >= 3) {
33
- lines.push({ text: taskSessionSummary(tasks, width), tone: "muted" });
37
+ lines.push({ text: taskSessionSummary(tasks, width, state.query ?? ""), tone: "muted" });
34
38
  }
35
39
  if (viewportHeight >= 4 && state.action) {
36
40
  lines.push({
@@ -58,7 +62,10 @@ export function taskSessionsDisplayLines(tasks, activeTaskId, selectedIndex, hei
58
62
  }
59
63
  if (tasks.length === 0) {
60
64
  if (slots > 0) {
61
- lines.push({ text: fitTaskSessionText("No saved task sessions", width), tone: "muted" });
65
+ lines.push({
66
+ text: fitTaskSessionText(state.query?.trim() ? "No matching task sessions" : "No saved task sessions", width),
67
+ tone: "muted"
68
+ });
62
69
  }
63
70
  return lines;
64
71
  }
@@ -94,7 +101,7 @@ function TaskSessionRow({ line, width }) {
94
101
  const theme = taskSessionLineTheme(line.tone);
95
102
  return (_jsxs(Text, { children: [_jsx(Text, { ...theme, children: line.text }), trailingWidth > 0 ? _jsx(Text, { backgroundColor: TUI_THEME.surface, children: " ".repeat(trailingWidth) }) : null] }));
96
103
  }
97
- function taskSessionSummary(tasks, width) {
104
+ function taskSessionSummary(tasks, width, query) {
98
105
  const counts = new Map();
99
106
  let archived = 0;
100
107
  for (const task of tasks) {
@@ -112,11 +119,16 @@ function taskSessionSummary(tasks, width) {
112
119
  if (archived > 0) {
113
120
  parts.push(`${archived} archived`);
114
121
  }
122
+ const searching = Boolean(query.trim());
123
+ const prefix = searching
124
+ ? `${tasks.length} ${tasks.length === 1 ? "match" : "matches"}`
125
+ : `${tasks.length} ${tasks.length === 1 ? "task" : "tasks"}`;
126
+ const compactPrefix = searching ? `${tasks.length} matches` : `${tasks.length} tasks`;
115
127
  return fitTaskSessionCandidates([
116
- [`${tasks.length} ${tasks.length === 1 ? "task" : "tasks"}`, ...parts].join(" · "),
117
- `${tasks.length} tasks · ${counts.get("running") ?? 0} active · ${counts.get("failed") ?? 0} failed`,
118
- `${tasks.length} tasks`,
119
- `${tasks.length}t`
128
+ [prefix, ...parts].join(" · "),
129
+ `${compactPrefix} · ${counts.get("running") ?? 0} active · ${counts.get("failed") ?? 0} failed`,
130
+ compactPrefix,
131
+ `${tasks.length}${searching ? "m" : "t"}`
120
132
  ], width);
121
133
  }
122
134
  function taskSessionRowText(task, selected, active, width) {
@@ -130,7 +142,9 @@ function taskSessionRowText(task, selected, active, width) {
130
142
  const workers = `${task.workerCount} ${task.workerCount === 1 ? "worker" : "workers"}`;
131
143
  const native = `${task.nativeSessionCount} native`;
132
144
  const compactId = task.id.replace(/^task-/, "#");
145
+ const match = safeTaskSessionText(task.searchMatch?.summary ?? "");
133
146
  return fitTaskSessionCandidates([
147
+ ...(match ? [[marker + title, status, match].join(" · ")] : []),
134
148
  [marker + title, status, turns, workers, native, date].join(" · "),
135
149
  [marker + title, status, turns, workers, native].join(" · "),
136
150
  [marker + title, status, turns, workers].join(" · "),
@@ -139,6 +153,13 @@ function taskSessionRowText(task, selected, active, width) {
139
153
  marker.trimEnd()
140
154
  ], width);
141
155
  }
156
+ function taskSessionsHeading(includeArchived, query) {
157
+ const scope = includeArchived ? "archived shown" : "active";
158
+ const cleanQuery = safeTaskSessionText(query);
159
+ return cleanQuery
160
+ ? `Task sessions · ${scope} · find ${cleanQuery}`
161
+ : includeArchived ? "Task sessions · archived shown" : "Task sessions";
162
+ }
142
163
  function taskSessionWindowStart(selected, count, visibleCount) {
143
164
  if (visibleCount <= 0 || count <= visibleCount) {
144
165
  return 0;
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const version = "0.3.0";
1
+ export const version = "0.3.2";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "parallel-codex-tui",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "A TypeScript TUI wrapper for routed parallel coding with Codex and Claude workers.",
5
5
  "license": "MIT",
6
6
  "type": "module",