parallel-codex-tui 0.2.10 → 0.3.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
@@ -5,6 +5,7 @@ import { Box, Text, useApp, useInput, useStdin } from "ink";
5
5
  import { Lexer } from "marked";
6
6
  import { copyTextToClipboard } from "../core/clipboard.js";
7
7
  import { readJson } from "../core/file-store.js";
8
+ import { CONFIGURABLE_ROLES, configuredRoleSelection } from "../core/role-configuration.js";
8
9
  import { WorkerStatusSchema } from "../domain/schemas.js";
9
10
  import { effectiveWorkerWatchdog, formatRoutePendingStatus, formatRoutePendingSummaryStatus, formatRouteStatus, formatRouteSummaryStatus, formatStatusLine, formatWorkerRuntimeStatus } from "./status-line.js";
10
11
  import { applyChatInputChunk, insertChatPaste } from "./chat-input.js";
@@ -19,7 +20,7 @@ import { TerminalOutput } from "./TerminalOutput.js";
19
20
  import { NativeTerminalScreen } from "./terminal-screen.js";
20
21
  import { WorkerOutputView } from "./WorkerOutputView.js";
21
22
  import { compactEndByDisplayWidth, displayWidth, wrapByDisplayWidth } from "./display-width.js";
22
- import { isAttachShortcut, isCopyShortcut, isDiagnosticsShortcut, isExitShortcut, isLogsShortcut, isNewConversationShortcut, isRouterDiagnosticsShortcut, isStatusDetailsShortcut, isTaskResultShortcut, isTaskSessionsShortcut, isWorkerOverviewShortcut, isWorkerSearchShortcut, isWorkspaceShortcut, mouseScrollDelta, rawChatScrollArrowDelta, rawHistoryDelta, rawPageScrollDelta, rawPlainArrowDelta, scrollDelta, workerLogJumpKind } from "./keyboard.js";
23
+ import { isAttachShortcut, isCopyShortcut, isDiagnosticsShortcut, isExitShortcut, isLogsShortcut, isNewConversationShortcut, isRoleConfigurationShortcut, isRouterDiagnosticsShortcut, isStatusDetailsShortcut, isTaskResultShortcut, isTaskSessionsShortcut, isWorkerOverviewShortcut, isWorkerSearchShortcut, isWorkspaceShortcut, mouseScrollDelta, rawChatScrollArrowDelta, rawHistoryDelta, rawHorizontalArrowDelta, rawPageScrollDelta, rawPlainArrowDelta, scrollDelta, workerLogJumpKind } from "./keyboard.js";
23
24
  import { createRawInputDecoder, tokenizeRawInput } from "./raw-input-decoder.js";
24
25
  import { decodeHtmlEntities } from "./markdown-text.js";
25
26
  import { configureTuiTheme, TUI_THEME } from "./theme.js";
@@ -35,6 +36,8 @@ import { latestTaskResultMessageIndex, parseTaskResultSummary } from "./task-res
35
36
  import { buildNativeAttachLaunch, buildNativeForkLaunch, startNativeAttachProcess } from "../workers/native-attach.js";
36
37
  import { assignableWorkerProviderIds, workerProviderLabel, workerProviders } from "../workers/provider.js";
37
38
  import { StatusDetailView } from "./StatusDetailView.js";
39
+ import { RoleConfigurationView } from "./RoleConfigurationView.js";
40
+ import { cycleRoleProvider, moveRoleConfigurationSelection, nextRoleConfigurationScope, roleConfigurationScopeHasOverride, roleConfigurationSelectionForScope, selectedConfigurableRole, updateRoleModel } from "./role-configuration-state.js";
38
41
  import { runtimeConfigChange } from "./runtime-config-state.js";
39
42
  export function routeDecisionChatMessage(route) {
40
43
  return `route · ${route.mode} · ${route.source ?? "router"}\n${route.reason.trim()}`;
@@ -77,6 +80,15 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
77
80
  const [canRetryTask, setCanRetryTask] = useState(initialCanRetryTask);
78
81
  const [attachError, setAttachError] = useState(null);
79
82
  const [configChange, setConfigChange] = useState(null);
83
+ const [roleConfigurationSnapshot, setRoleConfigurationSnapshot] = useState(null);
84
+ const [roleConfigurationDraft, setRoleConfigurationDraft] = useState(null);
85
+ const [roleConfigurationScope, setRoleConfigurationScope] = useState("next");
86
+ const [roleConfigurationSelectedIndex, setRoleConfigurationSelectedIndex] = useState(0);
87
+ const [roleConfigurationLoading, setRoleConfigurationLoading] = useState(false);
88
+ const [roleConfigurationSaving, setRoleConfigurationSaving] = useState(false);
89
+ const [roleConfigurationNotice, setRoleConfigurationNotice] = useState(null);
90
+ const [roleConfigurationError, setRoleConfigurationError] = useState(null);
91
+ const [roleModelEdit, setRoleModelEdit] = useState(null);
80
92
  const [clipboardNotice, setClipboardNotice] = useState(null);
81
93
  const [nativeInput, setNativeInput] = useState("");
82
94
  const [workerScrollOffset, setWorkerScrollOffset] = useState(0);
@@ -169,6 +181,13 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
169
181
  const featureBoardSelectedIndexRef = useRef(0);
170
182
  const featureCancelPromptRef = useRef(null);
171
183
  const featureAssignmentPromptRef = useRef(null);
184
+ const roleConfigurationSnapshotRef = useRef(null);
185
+ const roleConfigurationDraftRef = useRef(null);
186
+ const roleConfigurationScopeRef = useRef("next");
187
+ const roleConfigurationSelectedIndexRef = useRef(0);
188
+ const roleConfigurationLoadingRef = useRef(false);
189
+ const roleConfigurationSavingRef = useRef(false);
190
+ const roleModelEditRef = useRef(null);
172
191
  const collaborationFeatureIndexRef = useRef(-1);
173
192
  const collaborationSelectedEventIdRef = useRef(null);
174
193
  const collaborationDetailOpenRef = useRef(false);
@@ -188,6 +207,9 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
188
207
  const confirmFeatureCancellationRef = useRef(confirmFeatureCancellation);
189
208
  const reassignSelectedFeatureRef = useRef(reassignSelectedFeature);
190
209
  const openCollaborationTimelineRef = useRef(openCollaborationTimeline);
210
+ const openRoleConfigurationRef = useRef(openRoleConfiguration);
211
+ const applyRoleConfigurationRef = useRef(applyRoleConfiguration);
212
+ const clearRoleConfigurationRef = useRef(clearRoleConfiguration);
191
213
  const refreshCollaborationTimelineRef = useRef(refreshCollaborationTimeline);
192
214
  const activateSelectedTaskSessionRef = useRef(activateSelectedTaskSession);
193
215
  const refreshTaskSessionsRef = useRef(refreshTaskSessions);
@@ -213,6 +235,8 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
213
235
  const taskSessionsReturnViewRef = useRef("chat");
214
236
  const collaborationReturnViewRef = useRef("workers");
215
237
  const statusReturnViewRef = useRef("chat");
238
+ const roleConfigurationReturnViewRef = useRef("chat");
239
+ const roleConfigurationLoadSequenceRef = useRef(0);
216
240
  const collaborationLoadSequenceRef = useRef(0);
217
241
  const routerLoadSequenceRef = useRef(0);
218
242
  const taskSessionsLoadSequenceRef = useRef(0);
@@ -256,6 +280,14 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
256
280
  const taskResultMessageIndex = useMemo(() => activeTaskId ? latestTaskResultMessageIndex(messages, activeTaskId) : -1, [activeTaskId, messages]);
257
281
  const visibleChatMessages = useMemo(() => routeAnnouncement ? [...messages, routeAnnouncement] : messages, [messages, routeAnnouncement]);
258
282
  const hasTaskResult = taskResultMessageIndex >= 0;
283
+ const activeRoleSelection = roleConfigurationSnapshot
284
+ ? activeTaskId
285
+ ? roleConfigurationSnapshot.activeTurn
286
+ ?? roleConfigurationSnapshot.task
287
+ ?? roleConfigurationSnapshot.future
288
+ : roleConfigurationSnapshot.future
289
+ : configuredRoleSelection(config);
290
+ const roleConfigurationHasOverride = roleConfigurationScopeHasOverride(roleConfigurationSnapshot, roleConfigurationScope);
259
291
  useEffect(() => {
260
292
  inputRef.current = input;
261
293
  }, [input]);
@@ -325,6 +357,54 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
325
357
  useEffect(() => {
326
358
  canRetryTaskRef.current = canRetryTask;
327
359
  }, [canRetryTask]);
360
+ useEffect(() => {
361
+ roleConfigurationSnapshotRef.current = roleConfigurationSnapshot;
362
+ }, [roleConfigurationSnapshot]);
363
+ useEffect(() => {
364
+ roleConfigurationDraftRef.current = roleConfigurationDraft;
365
+ }, [roleConfigurationDraft]);
366
+ useEffect(() => {
367
+ roleConfigurationScopeRef.current = roleConfigurationScope;
368
+ }, [roleConfigurationScope]);
369
+ useEffect(() => {
370
+ roleConfigurationSelectedIndexRef.current = roleConfigurationSelectedIndex;
371
+ }, [roleConfigurationSelectedIndex]);
372
+ useEffect(() => {
373
+ roleConfigurationLoadingRef.current = roleConfigurationLoading;
374
+ }, [roleConfigurationLoading]);
375
+ useEffect(() => {
376
+ roleConfigurationSavingRef.current = roleConfigurationSaving;
377
+ }, [roleConfigurationSaving]);
378
+ useEffect(() => {
379
+ roleModelEditRef.current = roleModelEdit;
380
+ }, [roleModelEdit]);
381
+ useEffect(() => {
382
+ let active = true;
383
+ const sequence = roleConfigurationLoadSequenceRef.current + 1;
384
+ roleConfigurationLoadSequenceRef.current = sequence;
385
+ const snapshotPromise = typeof orchestrator.roleConfigurationSnapshot === "function"
386
+ ? orchestrator.roleConfigurationSnapshot(activeTaskId)
387
+ : Promise.resolve(fallbackRoleConfigurationSnapshot(config));
388
+ void snapshotPromise.then((snapshot) => {
389
+ if (!active || roleConfigurationLoadSequenceRef.current !== sequence) {
390
+ return;
391
+ }
392
+ roleConfigurationSnapshotRef.current = snapshot;
393
+ setRoleConfigurationSnapshot(snapshot);
394
+ if (viewRef.current === "roles") {
395
+ const draft = roleConfigurationSelectionForScope(snapshot, roleConfigurationScopeRef.current);
396
+ roleConfigurationDraftRef.current = draft;
397
+ setRoleConfigurationDraft(draft);
398
+ }
399
+ }).catch((error) => {
400
+ if (active && roleConfigurationLoadSequenceRef.current === sequence) {
401
+ setRoleConfigurationError(error instanceof Error ? error.message : String(error));
402
+ }
403
+ });
404
+ return () => {
405
+ active = false;
406
+ };
407
+ }, [activeTaskId, orchestrator]);
328
408
  useEffect(() => {
329
409
  taskResultExpandedRef.current = taskResultExpanded;
330
410
  }, [taskResultExpanded]);
@@ -433,6 +513,9 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
433
513
  openWorkerOverviewRef.current = openWorkerOverview;
434
514
  openTaskSessionsRef.current = openTaskSessions;
435
515
  openFeatureBoardRef.current = openFeatureBoard;
516
+ openRoleConfigurationRef.current = openRoleConfiguration;
517
+ applyRoleConfigurationRef.current = applyRoleConfiguration;
518
+ clearRoleConfigurationRef.current = clearRoleConfiguration;
436
519
  confirmFeatureCancellationRef.current = confirmFeatureCancellation;
437
520
  openCollaborationTimelineRef.current = openCollaborationTimeline;
438
521
  refreshCollaborationTimelineRef.current = refreshCollaborationTimeline;
@@ -828,6 +911,137 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
828
911
  if (currentView === "workspace") {
829
912
  return;
830
913
  }
914
+ if (currentView !== "native"
915
+ && tokenizeRawInput(chunk).some((inputChunk) => isRoleConfigurationShortcut(inputChunk, {}))
916
+ && !busyRef.current) {
917
+ if (currentView === "roles") {
918
+ roleModelEditRef.current = null;
919
+ setRoleModelEdit(null);
920
+ viewRef.current = roleConfigurationReturnViewRef.current;
921
+ setView(roleConfigurationReturnViewRef.current);
922
+ }
923
+ else {
924
+ void openRoleConfigurationRef.current();
925
+ }
926
+ return;
927
+ }
928
+ if (currentView === "roles") {
929
+ const roleChunks = tokenizeRawInput(chunk);
930
+ if (roleChunks.some((roleChunk) => isExitShortcut(roleChunk, {}))) {
931
+ activeRunControllerRef.current?.abort();
932
+ exitRef.current();
933
+ return;
934
+ }
935
+ if (roleChunks.some((roleChunk) => isStatusDetailsShortcut(roleChunk, {}))) {
936
+ statusReturnViewRef.current = "roles";
937
+ viewRef.current = "status";
938
+ setView("status");
939
+ return;
940
+ }
941
+ for (const roleChunk of roleChunks) {
942
+ const edit = roleModelEditRef.current;
943
+ if (edit) {
944
+ if (roleChunk === "\x1b") {
945
+ roleModelEditRef.current = null;
946
+ setRoleModelEdit(null);
947
+ continue;
948
+ }
949
+ const update = applyChatInputChunk(edit.value, roleChunk, edit.cursor);
950
+ if (update.submit !== null) {
951
+ const draft = roleConfigurationDraftRef.current;
952
+ if (draft) {
953
+ const nextDraft = updateRoleModel(draft, edit.role, update.submit);
954
+ roleConfigurationDraftRef.current = nextDraft;
955
+ setRoleConfigurationDraft(nextDraft);
956
+ }
957
+ roleModelEditRef.current = null;
958
+ setRoleModelEdit(null);
959
+ }
960
+ else {
961
+ const nextEdit = { ...edit, value: update.value, cursor: update.cursor };
962
+ roleModelEditRef.current = nextEdit;
963
+ setRoleModelEdit(nextEdit);
964
+ }
965
+ continue;
966
+ }
967
+ if (roleChunk === "\x1b") {
968
+ viewRef.current = roleConfigurationReturnViewRef.current;
969
+ setView(roleConfigurationReturnViewRef.current);
970
+ return;
971
+ }
972
+ if (roleConfigurationLoadingRef.current || roleConfigurationSavingRef.current) {
973
+ continue;
974
+ }
975
+ if (roleChunk === "\t") {
976
+ const nextScope = nextRoleConfigurationScope(roleConfigurationScopeRef.current, 1, Boolean(activeTaskIdRef.current));
977
+ roleConfigurationScopeRef.current = nextScope;
978
+ setRoleConfigurationScope(nextScope);
979
+ const snapshot = roleConfigurationSnapshotRef.current;
980
+ if (snapshot) {
981
+ const draft = roleConfigurationSelectionForScope(snapshot, nextScope);
982
+ roleConfigurationDraftRef.current = draft;
983
+ setRoleConfigurationDraft(draft);
984
+ }
985
+ setRoleConfigurationNotice(null);
986
+ setRoleConfigurationError(null);
987
+ continue;
988
+ }
989
+ const selectionDelta = -(rawHistoryDelta(roleChunk)
990
+ + rawPageScrollDelta(roleChunk, CONFIGURABLE_ROLES.length)
991
+ + mouseScrollDelta(roleChunk, 1));
992
+ if (selectionDelta !== 0) {
993
+ const nextIndex = moveRoleConfigurationSelection(roleConfigurationSelectedIndexRef.current, selectionDelta);
994
+ roleConfigurationSelectedIndexRef.current = nextIndex;
995
+ setRoleConfigurationSelectedIndex(nextIndex);
996
+ continue;
997
+ }
998
+ const providerDelta = rawHorizontalArrowDelta(roleChunk)
999
+ || (roleChunk === "]" ? 1 : roleChunk === "[" ? -1 : 0);
1000
+ if (providerDelta !== 0) {
1001
+ const snapshot = roleConfigurationSnapshotRef.current;
1002
+ const draft = roleConfigurationDraftRef.current;
1003
+ if (snapshot && draft) {
1004
+ const nextDraft = cycleRoleProvider(draft, selectedConfigurableRole(roleConfigurationSelectedIndexRef.current), snapshot.providers, providerDelta);
1005
+ roleConfigurationDraftRef.current = nextDraft;
1006
+ setRoleConfigurationDraft(nextDraft);
1007
+ setRoleConfigurationNotice(null);
1008
+ setRoleConfigurationError(null);
1009
+ }
1010
+ continue;
1011
+ }
1012
+ if (roleChunk === "m" || roleChunk === "M") {
1013
+ const role = selectedConfigurableRole(roleConfigurationSelectedIndexRef.current);
1014
+ const value = roleConfigurationDraftRef.current?.[role].model ?? "";
1015
+ const editState = {
1016
+ role,
1017
+ value,
1018
+ cursor: Array.from(value).length
1019
+ };
1020
+ roleModelEditRef.current = editState;
1021
+ setRoleModelEdit(editState);
1022
+ continue;
1023
+ }
1024
+ if (roleChunk === "\r" || roleChunk === "\n") {
1025
+ void applyRoleConfigurationRef.current();
1026
+ return;
1027
+ }
1028
+ if (roleChunk === "x" || roleChunk === "X") {
1029
+ void clearRoleConfigurationRef.current();
1030
+ return;
1031
+ }
1032
+ if (roleChunk === "r" || roleChunk === "R") {
1033
+ const snapshot = roleConfigurationSnapshotRef.current;
1034
+ if (snapshot) {
1035
+ const draft = roleConfigurationSelectionForScope(snapshot, roleConfigurationScopeRef.current);
1036
+ roleConfigurationDraftRef.current = draft;
1037
+ setRoleConfigurationDraft(draft);
1038
+ setRoleConfigurationNotice("Draft restored from saved configuration");
1039
+ setRoleConfigurationError(null);
1040
+ }
1041
+ }
1042
+ }
1043
+ return;
1044
+ }
831
1045
  if (currentView === "status") {
832
1046
  if (isExitShortcut(chunk, {})) {
833
1047
  activeRunControllerRef.current?.abort();
@@ -1841,6 +2055,15 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
1841
2055
  .join("\n")
1842
2056
  };
1843
2057
  }
2058
+ if (currentView === "roles") {
2059
+ const roles = roleConfigurationDraftRef.current;
2060
+ return {
2061
+ label: "role configuration",
2062
+ text: roles
2063
+ ? CONFIGURABLE_ROLES.map((role) => `${role} · ${roles[role].engine} · ${roles[role].model || "default"}`).join("\n")
2064
+ : ""
2065
+ };
2066
+ }
1844
2067
  if (currentView === "workers") {
1845
2068
  return {
1846
2069
  label: "workers",
@@ -2187,6 +2410,7 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
2187
2410
  cwd,
2188
2411
  taskId: target.taskId,
2189
2412
  route: followUpRoute?.route,
2413
+ roleSelection: followUpRoute?.roleSelection,
2190
2414
  ...callbacks
2191
2415
  })
2192
2416
  : target.kind === "task-question"
@@ -2195,6 +2419,7 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
2195
2419
  cwd,
2196
2420
  taskId: target.taskId,
2197
2421
  route: followUpRoute?.route,
2422
+ roleSelection: followUpRoute?.roleSelection,
2198
2423
  ...callbacks
2199
2424
  })
2200
2425
  : await orchestrator.handleRequest({
@@ -2209,6 +2434,7 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
2209
2434
  setCanRetryTask(nextMemory.activeTaskId
2210
2435
  ? await orchestrator.canRetryTask(nextMemory.activeTaskId)
2211
2436
  : false);
2437
+ await refreshRoleConfigurationState(nextMemory.activeTaskId);
2212
2438
  setRouteAnnouncement(null);
2213
2439
  await appendVisibleMessage({ from: "system", text: result.summary }, nextMemory.activeTaskId ?? undefined);
2214
2440
  if (result.mode === "complex" && parseTaskResultSummary(result.summary)) {
@@ -2221,6 +2447,7 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
2221
2447
  if (retryTaskId) {
2222
2448
  setCanRetryTask(await orchestrator.canRetryTask(retryTaskId));
2223
2449
  }
2450
+ await refreshRoleConfigurationState(retryTaskId);
2224
2451
  await appendVisibleMessage({
2225
2452
  from: "system",
2226
2453
  text: isAbortError(error) ? "cancelled · request stopped" : error instanceof Error ? error.message : String(error)
@@ -2268,6 +2495,7 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
2268
2495
  setActiveTaskId(taskId);
2269
2496
  setActiveMode("complex");
2270
2497
  setCanRetryTask(false);
2498
+ await refreshRoleConfigurationState(taskId);
2271
2499
  setRouteAnnouncement(null);
2272
2500
  await appendVisibleMessage({ from: "system", text: result.summary }, taskId);
2273
2501
  if (parseTaskResultSummary(result.summary)) {
@@ -2277,6 +2505,7 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
2277
2505
  catch (error) {
2278
2506
  setRouteAnnouncement(null);
2279
2507
  setCanRetryTask(await orchestrator.canRetryTask(taskId));
2508
+ await refreshRoleConfigurationState(taskId);
2280
2509
  await appendVisibleMessage({
2281
2510
  from: "system",
2282
2511
  text: isAbortError(error) ? "cancelled · retry stopped" : error instanceof Error ? error.message : String(error)
@@ -2305,6 +2534,7 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
2305
2534
  return;
2306
2535
  }
2307
2536
  resetActiveTaskUiForMainConversation();
2537
+ await refreshRoleConfigurationState(null);
2308
2538
  await appendVisibleMessage({ from: "system", text: "new conversation · ready" });
2309
2539
  }
2310
2540
  function resetActiveTaskUiForMainConversation() {
@@ -3070,6 +3300,153 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
3070
3300
  setAttachError(null);
3071
3301
  setView("workers");
3072
3302
  }
3303
+ async function refreshRoleConfigurationState(taskId) {
3304
+ try {
3305
+ const snapshot = typeof orchestrator.roleConfigurationSnapshot === "function"
3306
+ ? await orchestrator.roleConfigurationSnapshot(taskId)
3307
+ : fallbackRoleConfigurationSnapshot(config);
3308
+ roleConfigurationSnapshotRef.current = snapshot;
3309
+ setRoleConfigurationSnapshot(snapshot);
3310
+ if (viewRef.current === "roles") {
3311
+ const draft = roleConfigurationSelectionForScope(snapshot, roleConfigurationScopeRef.current);
3312
+ roleConfigurationDraftRef.current = draft;
3313
+ setRoleConfigurationDraft(draft);
3314
+ }
3315
+ }
3316
+ catch (error) {
3317
+ setRoleConfigurationError(error instanceof Error ? error.message : String(error));
3318
+ }
3319
+ }
3320
+ async function openRoleConfiguration() {
3321
+ const currentView = viewRef.current;
3322
+ if (busyRef.current || currentView === "native" || currentView === "workspace") {
3323
+ return;
3324
+ }
3325
+ const returnView = currentView === "roles" ? roleConfigurationReturnViewRef.current : currentView;
3326
+ roleConfigurationReturnViewRef.current = returnView;
3327
+ const scope = roleConfigurationScopeRef.current === "task" && !activeTaskIdRef.current
3328
+ ? "next"
3329
+ : roleConfigurationScopeRef.current;
3330
+ roleConfigurationScopeRef.current = scope;
3331
+ setRoleConfigurationScope(scope);
3332
+ roleModelEditRef.current = null;
3333
+ setRoleModelEdit(null);
3334
+ setRoleConfigurationNotice(null);
3335
+ setRoleConfigurationError(null);
3336
+ roleConfigurationLoadingRef.current = true;
3337
+ setRoleConfigurationLoading(true);
3338
+ viewRef.current = "roles";
3339
+ setView("roles");
3340
+ const sequence = roleConfigurationLoadSequenceRef.current + 1;
3341
+ roleConfigurationLoadSequenceRef.current = sequence;
3342
+ try {
3343
+ const snapshot = typeof orchestrator.roleConfigurationSnapshot === "function"
3344
+ ? await orchestrator.roleConfigurationSnapshot(activeTaskIdRef.current)
3345
+ : fallbackRoleConfigurationSnapshot(config);
3346
+ if (!mountedRef.current || roleConfigurationLoadSequenceRef.current !== sequence) {
3347
+ return;
3348
+ }
3349
+ roleConfigurationSnapshotRef.current = snapshot;
3350
+ setRoleConfigurationSnapshot(snapshot);
3351
+ const draft = roleConfigurationSelectionForScope(snapshot, scope);
3352
+ roleConfigurationDraftRef.current = draft;
3353
+ setRoleConfigurationDraft(draft);
3354
+ }
3355
+ catch (error) {
3356
+ if (mountedRef.current && roleConfigurationLoadSequenceRef.current === sequence) {
3357
+ setRoleConfigurationError(error instanceof Error ? error.message : String(error));
3358
+ }
3359
+ }
3360
+ finally {
3361
+ if (mountedRef.current && roleConfigurationLoadSequenceRef.current === sequence) {
3362
+ roleConfigurationLoadingRef.current = false;
3363
+ setRoleConfigurationLoading(false);
3364
+ }
3365
+ }
3366
+ }
3367
+ async function applyRoleConfiguration() {
3368
+ const draft = roleConfigurationDraftRef.current;
3369
+ if (!draft || roleConfigurationSavingRef.current || roleConfigurationLoadingRef.current) {
3370
+ return;
3371
+ }
3372
+ const scope = roleConfigurationScopeRef.current;
3373
+ const taskId = activeTaskIdRef.current;
3374
+ if (scope === "task" && !taskId) {
3375
+ setRoleConfigurationError("No active Task · choose next request or future requests");
3376
+ return;
3377
+ }
3378
+ roleConfigurationSavingRef.current = true;
3379
+ setRoleConfigurationSaving(true);
3380
+ setRoleConfigurationNotice(null);
3381
+ setRoleConfigurationError(null);
3382
+ try {
3383
+ if (typeof orchestrator.updateRoleConfiguration !== "function") {
3384
+ throw new Error("Role configuration is unavailable in this runtime");
3385
+ }
3386
+ const snapshot = await orchestrator.updateRoleConfiguration({
3387
+ scope,
3388
+ roles: draft,
3389
+ taskId
3390
+ });
3391
+ roleConfigurationSnapshotRef.current = snapshot;
3392
+ setRoleConfigurationSnapshot(snapshot);
3393
+ const saved = roleConfigurationSelectionForScope(snapshot, scope);
3394
+ roleConfigurationDraftRef.current = saved;
3395
+ setRoleConfigurationDraft(saved);
3396
+ setRoleConfigurationNotice(scope === "next"
3397
+ ? "Saved · the next request will consume this matrix once"
3398
+ : scope === "task"
3399
+ ? "Saved · current Task retries and follow-ups will use this matrix"
3400
+ : "Saved · future requests now use this default matrix");
3401
+ if (scope === "task" && taskId) {
3402
+ void refreshCollaborationTimelineRef.current(false);
3403
+ }
3404
+ }
3405
+ catch (error) {
3406
+ setRoleConfigurationError(error instanceof Error ? error.message : String(error));
3407
+ }
3408
+ finally {
3409
+ roleConfigurationSavingRef.current = false;
3410
+ setRoleConfigurationSaving(false);
3411
+ }
3412
+ }
3413
+ async function clearRoleConfiguration() {
3414
+ if (roleConfigurationSavingRef.current || roleConfigurationLoadingRef.current) {
3415
+ return;
3416
+ }
3417
+ const scope = roleConfigurationScopeRef.current;
3418
+ const taskId = activeTaskIdRef.current;
3419
+ if (scope === "task" && !taskId) {
3420
+ setRoleConfigurationError("No active Task · choose next request or future requests");
3421
+ return;
3422
+ }
3423
+ roleConfigurationSavingRef.current = true;
3424
+ setRoleConfigurationSaving(true);
3425
+ setRoleConfigurationNotice(null);
3426
+ setRoleConfigurationError(null);
3427
+ try {
3428
+ if (typeof orchestrator.clearRoleConfiguration !== "function") {
3429
+ throw new Error("Role configuration is unavailable in this runtime");
3430
+ }
3431
+ const snapshot = await orchestrator.clearRoleConfiguration(scope, taskId);
3432
+ roleConfigurationSnapshotRef.current = snapshot;
3433
+ setRoleConfigurationSnapshot(snapshot);
3434
+ const inherited = roleConfigurationSelectionForScope(snapshot, scope);
3435
+ roleConfigurationDraftRef.current = inherited;
3436
+ setRoleConfigurationDraft(inherited);
3437
+ setRoleConfigurationNotice(scope === "future" ? "Reset · future requests inherit config.toml" : "Reset · scope now inherits future defaults");
3438
+ if (scope === "task" && taskId) {
3439
+ void refreshCollaborationTimelineRef.current(false);
3440
+ }
3441
+ }
3442
+ catch (error) {
3443
+ setRoleConfigurationError(error instanceof Error ? error.message : String(error));
3444
+ }
3445
+ finally {
3446
+ roleConfigurationSavingRef.current = false;
3447
+ setRoleConfigurationSaving(false);
3448
+ }
3449
+ }
3073
3450
  function openWorkspacePicker() {
3074
3451
  const currentView = viewRef.current;
3075
3452
  if (busyRef.current || !switchWorkspace || currentView === "native" || currentView === "workspace") {
@@ -3113,11 +3490,11 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
3113
3490
  ? { type: "delete", title: taskSessionAction.title }
3114
3491
  : null, taskSessionsIncludeArchived: sessionCenterMode === "conversations"
3115
3492
  ? mainConversationsIncludeArchived
3116
- : 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, value: workerSearch.open && view === "worker"
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"
3117
3494
  ? workerSearch.query
3118
- : view === "native" || view === "router" || view === "workers" || view === "features" || view === "sessions" || view === "collaboration" || view === "status" ? "" : input, cursor: workerSearch.open && view === "worker"
3495
+ : view === "native" || view === "router" || view === "workers" || view === "features" || view === "sessions" || view === "collaboration" || view === "status" || view === "roles" ? "" : input, cursor: workerSearch.open && view === "worker"
3119
3496
  ? workerSearch.cursor
3120
- : view === "chat" ? inputCursor : undefined, terminalWidth: terminalWidth, onChange: view === "native" ? setNativeInput : setInput, onSubmit: view === "native" ? undefined : submit }), error: attachError, children: view === "status" ? (_jsx(StatusDetailView, { cwd: cwd, taskId: activeTaskId, mode: activeMode, busy: busy, canRetry: canRetryTask, taskStatus: visibleTaskStatus, routeStatus: visibleRouteStatus, routeReason: routePending ? undefined : lastRoute?.reason, pairing: config.pairing, 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"
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"
3121
3498
  ? { type: "rename", title: mainConversationAction.title }
3122
3499
  : mainConversationAction?.type === "delete"
3123
3500
  ? { type: "delete", title: mainConversationAction.title }
@@ -3154,6 +3531,22 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
3154
3531
  }
3155
3532
  } })) }));
3156
3533
  }
3534
+ function fallbackRoleConfigurationSnapshot(config) {
3535
+ const roles = configuredRoleSelection(config);
3536
+ return {
3537
+ baseline: roles,
3538
+ future: configuredRoleSelection(config),
3539
+ next: null,
3540
+ task: null,
3541
+ activeTurn: null,
3542
+ providers: workerProviders(config).map(({ id, config: provider }) => ({
3543
+ id,
3544
+ model: provider.model.name,
3545
+ modelProvider: provider.model.provider,
3546
+ assignable: provider.assignable
3547
+ }))
3548
+ };
3549
+ }
3157
3550
  function readTerminalSize() {
3158
3551
  return {
3159
3552
  columns: Math.max(1, Math.trunc(process.stdout.columns || 120)),
@@ -236,6 +236,9 @@ function viewLabel(view) {
236
236
  return shortViewLabel(view);
237
237
  }
238
238
  function shortcutHint(view) {
239
+ if (view === "roles") {
240
+ return "^E back";
241
+ }
239
242
  if (view === "status") {
240
243
  return "^S back";
241
244
  }
@@ -245,6 +248,9 @@ function shortcutHint(view) {
245
248
  return "^S status · ^C exit";
246
249
  }
247
250
  function shortViewLabel(view) {
251
+ if (view === "roles") {
252
+ return "roles";
253
+ }
248
254
  if (view === "status") {
249
255
  return "status";
250
256
  }
@@ -272,7 +278,7 @@ function shortViewLabel(view) {
272
278
  return "chat";
273
279
  }
274
280
  function shortShortcutHint(view) {
275
- return view === "native" ? "^]" : view === "status" ? "^S" : "^C";
281
+ return view === "native" ? "^]" : view === "status" ? "^S" : view === "roles" ? "^E" : "^C";
276
282
  }
277
283
  function compactHeaderProject(cwd, maxLength) {
278
284
  const project = basename(cwd) || cwd;
@@ -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, 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, 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 }) {
6
6
  const terminalWidth = providedTerminalWidth ?? process.stdout.columns ?? 120;
7
7
  const fillRail = providedTerminalWidth !== undefined || typeof process.stdout.columns === "number";
8
8
  if (clipboardNotice) {
@@ -19,6 +19,17 @@ export function InputBar({ mode, ready = true, busy = false, routeFallback = fal
19
19
  const textWidth = displayWidth(`${prefix}${display.before}|${display.after}${suffix}`);
20
20
  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 }), suffix ? _jsx(Text, { backgroundColor: TUI_THEME.rail, color: TUI_THEME.muted, children: suffix }) : null] }));
21
21
  }
22
+ if (mode === "roles") {
23
+ if (roleEditingModel) {
24
+ const prefix = `${roleEditingModel.role} model > `;
25
+ const valueWidth = Math.max(1, terminalWidth - displayWidth(prefix) - 3);
26
+ const display = chatInputDisplayParts(roleEditingModel.value, roleEditingModel.cursor, valueWidth);
27
+ const textWidth = displayWidth(`${prefix}${display.before}|${display.after}`);
28
+ 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 })] }));
29
+ }
30
+ const hints = roleConfigurationInputHints(terminalWidth, roleScope, roleCanApply, roleSaving, roleHasOverride);
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
+ }
22
33
  if (mode === "status") {
23
34
  const hints = statusDetailInputHints(terminalWidth);
24
35
  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] }));
@@ -422,14 +433,27 @@ function workerInputHints(width) {
422
433
  }
423
434
  function statusDetailInputHints(width) {
424
435
  return selectInputHints(width, [
425
- { label: "status", detail: " · ^X diagnostics · ^S/Esc back · ^C exit" },
426
- { label: "status", detail: " · ^X diag · ^S/Esc back · ^C exit" },
436
+ { label: "status", detail: " · ^E roles · ^X diagnostics · ^S/Esc back · ^C exit" },
437
+ { label: "status", detail: " · ^E roles · ^X diag · ^S/Esc back · ^C exit" },
427
438
  { label: "status", detail: " · ^S back · ^C exit" },
428
439
  { label: "status", detail: " · ^S back" },
429
440
  { label: "status", detail: "" },
430
441
  { label: "st", detail: "" }
431
442
  ]);
432
443
  }
444
+ function roleConfigurationInputHints(width, scope, canApply, saving, hasOverride) {
445
+ const label = saving ? "roles · saving" : `roles · ${scope}`;
446
+ const apply = canApply ? "Enter apply" : "task unavailable";
447
+ const reset = hasOverride ? " · X reset" : "";
448
+ return selectInputHints(width, [
449
+ { label, detail: ` · Tab scope · Up/Dn role · Left/Right provider · M model · ${apply}${reset} · ^E/Esc back` },
450
+ { label, detail: ` · Tab scope · Up/Dn · Left/Right provider · M model · ${apply}${reset} · Esc back` },
451
+ { label, detail: ` · Tab · Up/Dn · Left/Right · M model · ${apply}${reset}` },
452
+ { label, detail: ` · Tab · arrows · M · ${canApply ? "Enter" : "no task"}` },
453
+ { label: "roles", detail: " · Tab · arrows · M · Enter" },
454
+ { label: "roles", detail: "" }
455
+ ]);
456
+ }
433
457
  function workerOverviewInputHints(width) {
434
458
  if (width < 16) {
435
459
  return { label: "wrk", detail: "" };