@namzu/cli 25.0.1 → 25.1.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.
Files changed (60) hide show
  1. package/CHANGELOG.md +217 -0
  2. package/dist/context/doctrine.d.ts +4 -2
  3. package/dist/context/doctrine.d.ts.map +1 -1
  4. package/dist/context/doctrine.js +4 -2
  5. package/dist/context/doctrine.js.map +1 -1
  6. package/dist/integrations/mcp/servers.d.ts +30 -0
  7. package/dist/integrations/mcp/servers.d.ts.map +1 -1
  8. package/dist/integrations/mcp/servers.js +32 -1
  9. package/dist/integrations/mcp/servers.js.map +1 -1
  10. package/dist/integrations/subagents/activity.d.ts +285 -11
  11. package/dist/integrations/subagents/activity.d.ts.map +1 -1
  12. package/dist/integrations/subagents/activity.js +334 -31
  13. package/dist/integrations/subagents/activity.js.map +1 -1
  14. package/dist/integrations/subagents/replay.d.ts +46 -0
  15. package/dist/integrations/subagents/replay.d.ts.map +1 -0
  16. package/dist/integrations/subagents/replay.js +193 -0
  17. package/dist/integrations/subagents/replay.js.map +1 -0
  18. package/dist/integrations/subagents/runs.d.ts +72 -0
  19. package/dist/integrations/subagents/runs.d.ts.map +1 -0
  20. package/dist/integrations/subagents/runs.js +155 -0
  21. package/dist/integrations/subagents/runs.js.map +1 -0
  22. package/dist/integrations/subagents/runtime.d.ts +21 -0
  23. package/dist/integrations/subagents/runtime.d.ts.map +1 -1
  24. package/dist/integrations/subagents/runtime.js +121 -2
  25. package/dist/integrations/subagents/runtime.js.map +1 -1
  26. package/dist/tui/AgentExplorer.d.ts +46 -2
  27. package/dist/tui/AgentExplorer.d.ts.map +1 -1
  28. package/dist/tui/AgentExplorer.js +261 -22
  29. package/dist/tui/AgentExplorer.js.map +1 -1
  30. package/dist/tui/App.d.ts.map +1 -1
  31. package/dist/tui/App.js +448 -28
  32. package/dist/tui/App.js.map +1 -1
  33. package/dist/tui/ChoicePicker.d.ts +2 -0
  34. package/dist/tui/ChoicePicker.d.ts.map +1 -1
  35. package/dist/tui/ChoicePicker.js +3 -1
  36. package/dist/tui/ChoicePicker.js.map +1 -1
  37. package/dist/tui/Composer.d.ts +10 -7
  38. package/dist/tui/Composer.d.ts.map +1 -1
  39. package/dist/tui/Composer.js +2 -7
  40. package/dist/tui/Composer.js.map +1 -1
  41. package/dist/tui/StatusBar.d.ts +56 -10
  42. package/dist/tui/StatusBar.d.ts.map +1 -1
  43. package/dist/tui/StatusBar.js +106 -35
  44. package/dist/tui/StatusBar.js.map +1 -1
  45. package/dist/tui/agent.d.ts +38 -1
  46. package/dist/tui/agent.d.ts.map +1 -1
  47. package/dist/tui/agent.js +25 -1
  48. package/dist/tui/agent.js.map +1 -1
  49. package/dist/tui/choice-selection.d.ts +3 -1
  50. package/dist/tui/choice-selection.d.ts.map +1 -1
  51. package/dist/tui/choice-selection.js +2 -2
  52. package/dist/tui/choice-selection.js.map +1 -1
  53. package/dist/tui/permission-review.d.ts.map +1 -1
  54. package/dist/tui/permission-review.js +14 -2
  55. package/dist/tui/permission-review.js.map +1 -1
  56. package/dist/tui/slashCommands.d.ts +40 -0
  57. package/dist/tui/slashCommands.d.ts.map +1 -1
  58. package/dist/tui/slashCommands.js +62 -3
  59. package/dist/tui/slashCommands.js.map +1 -1
  60. package/package.json +2 -2
package/dist/tui/App.js CHANGED
@@ -29,6 +29,8 @@ import { terminalNotificationEnabled, writeTerminalNotification, } from '../inte
29
29
  import { beginCodexDeviceLogin, beginSubscriptionLogin, clearAllStoredCredentials, clearStoredCodexCredential, clearStoredSubscriptionCredential, credentialsPath, parsePastedInput, primaryProvider, readStoredCodexCredential, readStoredSubscriptionCredential, signedInSubscriptionProviders, writePreferences, } from '../integrations/providers/index.js';
30
30
  import { appendMessages, archiveConversation, forkConversation, forkConversationBeforeUser, listRecent, loadConversation, loadResumableConversation, openSessions, replaceConversation, requireWritableConversation, setTitle, startConversation, titleOf, } from '../integrations/sessions/store.js';
31
31
  import { conversationMarkdown, writeConversationExport, } from '../integrations/sessions/transcript-export.js';
32
+ import { liveOrchestrationRuns } from '../integrations/subagents/runs.js';
33
+ import { NARRATION_TOOL_NAME } from '../integrations/subagents/runtime.js';
32
34
  import { isTrusted, trustDir } from '../integrations/trust/store.js';
33
35
  import { checkUpdates } from '../integrations/updates.js';
34
36
  import { renderMemoryReport, renderMemorySaveResult } from '../memory/presentation.js';
@@ -36,7 +38,7 @@ import { appendMemoryWithStatus, readMemory } from '../memory/store.js';
36
38
  import { effectivePermissionMode, permissionModeDescription, permissionModeLabel, } from '../permissions/mode.js';
37
39
  import { composeSkillsPrompt, discoverSkills, loadSkillBody } from '../skills/store.js';
38
40
  import { discoverUserCommands } from '../user-commands/store.js';
39
- import { AgentCockpit, AgentTaskPanel, AgentTranscript, activeSubagentCohorts, agentCockpitIsWide, agentPhasePageSize, agentPhases, agentPickerPageSize, agentTranscriptPageSize, agentWorkflowPageSize, agentWorkflows, maxAgentTranscriptTailOffset, } from './AgentExplorer.js';
41
+ import { AgentCockpit, AgentNarrationBand, AgentTaskPanel, AgentTranscript, activeSubagentCohorts, agentCockpitIsWide, agentPhasePageSize, agentPhases, agentPickerPageSize, agentTranscriptPageSize, agentWorkflowPageSize, agentWorkflows, maxAgentTranscriptTailOffset, } from './AgentExplorer.js';
40
42
  import { BrandHeader } from './BrandHeader.js';
41
43
  import { ChoicePicker } from './ChoicePicker.js';
42
44
  import { emptyPluginReport, pluginDetails, pluginOption, pluginStartupState } from './plugin-view.js';
@@ -76,7 +78,7 @@ import { buildPermissionReview, buildPermissionSummary, permissionReviewPageRows
76
78
  import { describeRunInterruption, describeRunStop } from './run-interruption.js';
77
79
  import { moveSelection } from './selection-window.js';
78
80
  import { describeShellEscape, describeShellEscapeForModel, runShellEscape, shellEscapeCommand, } from './shell-escape.js';
79
- import { baseBranchReviewPrompt, commitReviewPrompt, hostCommandNames, kernelCommandDescriptors, mergeHostCommands, renderOutcome, reviewPrompt, runSlash, } from './slashCommands.js';
81
+ import { baseBranchReviewPrompt, combineOrchestrationRuns, commitReviewPrompt, hostCommandNames, kernelCommandDescriptors, mergeHostCommands, renderAgentRuns, renderOutcome, reviewPrompt, runSlash, } from './slashCommands.js';
80
82
  import { splitCompleteBlocks, splitSafeCut } from './stream-blocks.js';
81
83
  import { terminalSupportsHyperlinks } from './terminal-hyperlinks.js';
82
84
  import { theme } from './theme.js';
@@ -387,6 +389,15 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
387
389
  reasoningEffortRef.current = next;
388
390
  setReasoningEffortState(next);
389
391
  }, []);
392
+ // A session setting layered above effort, not a level a provider publishes
393
+ // — see docs/cli/slash-commands.md's /effort section. In-memory and
394
+ // per-session like reasoningEffort above; no preferences file involved.
395
+ const [orchestrateMode, setOrchestrateModeState] = useState(false);
396
+ const orchestrateModeRef = useRef(false);
397
+ const setOrchestrateMode = useCallback((next) => {
398
+ orchestrateModeRef.current = next;
399
+ setOrchestrateModeState(next);
400
+ }, []);
390
401
  const [activeSkills, setActiveSkills] = useState([]);
391
402
  // Read when the session comes up rather than on every keystroke: the
392
403
  // autocomplete dropdown consults this on each character, and a `readdirSync`
@@ -586,6 +597,13 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
586
597
  /** Bounded child-run projection published by the current AgentSession. */
587
598
  const [subagents, setSubagentsState] = useState([]);
588
599
  const subagentsRef = useRef([]);
600
+ /**
601
+ * The parent's own commentary on the work it is coordinating, from the same
602
+ * monitor and the same subscription as the rows below it. Read through the
603
+ * optional reader, so a source that publishes no narration is simply a
604
+ * session with none.
605
+ */
606
+ const [narration, setNarration] = useState([]);
589
607
  const [agentSurface, setAgentSurfaceState] = useState(null);
590
608
  const agentSurfaceRef = useRef(null);
591
609
  const agentSurfaceCommittedRef = useRef(null);
@@ -604,7 +622,30 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
604
622
  useEffect(() => {
605
623
  agentSurfaceCommittedRef.current = agentSurface;
606
624
  }, [agentSurface]);
607
- const replaceSubagents = useCallback((next) => {
625
+ /**
626
+ * Children of this conversation's earlier runs, rebuilt from saved
627
+ * evidence, kept apart from the live monitor's own snapshot.
628
+ *
629
+ * A ref and not state because it is an input to `replaceSubagents`, which
630
+ * runs from the monitor's subscription: holding it as state would make
631
+ * every saved row arrive one render after the live rows it belongs beside.
632
+ */
633
+ const savedSubagentsRef = useRef([]);
634
+ /** Revokes an in-flight evidence read whose session or conversation is gone. */
635
+ const savedSubagentsTokenRef = useRef(0);
636
+ const withSavedSubagents = useCallback((live) => {
637
+ const saved = savedSubagentsRef.current;
638
+ if (saved.length === 0)
639
+ return live;
640
+ // A child the live monitor still holds wins over its own saved copy:
641
+ // both describe one run, and only one of the two is attached to
642
+ // anything. Saved rows land after the live ones, so a cohort that is
643
+ // still running keeps the top of the list.
644
+ const liveRunIds = new Set(live.map((agent) => agent.runId).filter((id) => id !== undefined));
645
+ return [...live, ...saved.filter((agent) => !agent.runId || !liveRunIds.has(agent.runId))];
646
+ }, []);
647
+ const replaceSubagents = useCallback((incoming) => {
648
+ const next = withSavedSubagents(incoming);
608
649
  const previous = subagentsRef.current;
609
650
  subagentsRef.current = next;
610
651
  setSubagentsState(next);
@@ -632,8 +673,40 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
632
673
  selectedId: fallbackAgent.viewId,
633
674
  focus: surface.kind === 'cockpit' ? surface.focus : surface.returnFocus,
634
675
  });
635
- }, [setAgentSurface]);
636
- const openAgentCockpit = useCallback(() => {
676
+ }, [setAgentSurface, withSavedSubagents]);
677
+ /**
678
+ * Re-reads what this conversation's finished children left on disk.
679
+ *
680
+ * Called when a session is installed, whenever the conversation the scope
681
+ * names changes — a resume is the whole point of this: the children worth
682
+ * opening belong to the runs of the conversation just switched TO — and
683
+ * whenever the cockpit is opened, which is what recovers a child the live
684
+ * monitor's eighty-agent bound has since evicted. All three are reads of
685
+ * directories that are already there; nothing is created, and a failure
686
+ * leaves the live rows untouched.
687
+ */
688
+ const hydrateSavedChildren = useCallback(async () => {
689
+ const read = session?.savedChildren;
690
+ if (!read)
691
+ return;
692
+ const token = ++savedSubagentsTokenRef.current;
693
+ let saved;
694
+ try {
695
+ saved = await read();
696
+ }
697
+ catch {
698
+ // Saved evidence is an extra view over a directory that may not exist
699
+ // yet, or may not be readable. Not finding it is not a reason to
700
+ // disturb a session whose live children are fine.
701
+ return;
702
+ }
703
+ if (token !== savedSubagentsTokenRef.current)
704
+ return;
705
+ savedSubagentsRef.current = saved;
706
+ replaceSubagents(session?.subagents?.getSnapshot() ?? []);
707
+ }, [replaceSubagents, session]);
708
+ /** Opens the cockpit on the rows already on screen; false when there are none. */
709
+ const openCockpitFromCurrentRows = useCallback(() => {
637
710
  const agents = subagentsRef.current;
638
711
  const workflows = agentWorkflows(agents);
639
712
  const currentWorkflow = [...workflows].reverse().find((workflow) => workflow.agents.some((agent) => agent.status === 'starting' || agent.status === 'queued' || agent.status === 'working')) ?? workflows.at(-1);
@@ -649,6 +722,33 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
649
722
  });
650
723
  return true;
651
724
  }, [setAgentSurface]);
725
+ /** The most recent cockpit request, so an older one cannot report on its behalf. */
726
+ const cockpitOpenTokenRef = useRef(0);
727
+ /**
728
+ * Ctrl+T and `/agents`: open the cockpit, and when nothing is on screen
729
+ * yet, WAIT for the saved evidence before answering.
730
+ *
731
+ * The waiting is the point. A conversation's children may exist only on
732
+ * disk — evicted by the eighty-agent bound, or written by a process that
733
+ * has since exited — and that read is asynchronous. Answering from the
734
+ * rows already up would make the first press after a resume report an
735
+ * absence that the read, still in flight, was about to disprove. Rows that
736
+ * ARE up open immediately and refresh the saved ones behind the screen, so
737
+ * the common case pays nothing for this.
738
+ */
739
+ const openAgentCockpit = useCallback(async () => {
740
+ if (openCockpitFromCurrentRows()) {
741
+ void hydrateSavedChildren();
742
+ return true;
743
+ }
744
+ const token = ++cockpitOpenTokenRef.current;
745
+ await hydrateSavedChildren();
746
+ // A later request owns the screen now. Reporting `false` here would put
747
+ // an empty-state line under a cockpit that request is about to open.
748
+ if (token !== cockpitOpenTokenRef.current)
749
+ return true;
750
+ return openCockpitFromCurrentRows();
751
+ }, [hydrateSavedChildren, openCockpitFromCurrentRows]);
652
752
  /** One git-backed review choice may be resolving while its visible picker remains authoritative. */
653
753
  const reviewChoiceInFlightRef = useRef(null);
654
754
  const exitArmedRef = useRef(false);
@@ -703,17 +803,32 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
703
803
  useEffect(() => {
704
804
  const source = session?.subagents;
705
805
  setAgentSurface(null);
806
+ savedSubagentsRef.current = [];
807
+ void hydrateSavedChildren();
706
808
  if (!source) {
707
809
  replaceSubagents([]);
810
+ setNarration([]);
708
811
  return;
709
812
  }
710
- const refresh = () => replaceSubagents(source.getSnapshot());
813
+ const refresh = () => {
814
+ replaceSubagents(source.getSnapshot());
815
+ setNarration(source.getNarration?.() ?? []);
816
+ };
711
817
  refresh();
712
818
  return source.subscribe(refresh);
713
- }, [replaceSubagents, session, setAgentSurface]);
819
+ }, [hydrateSavedChildren, replaceSubagents, session, setAgentSurface]);
714
820
  const resetSubagentActivity = useCallback(() => {
715
821
  session?.subagents?.reset();
716
822
  setAgentSurface(null);
823
+ // The monitor clears its own commentary on reset; this drops the copy
824
+ // already on screen in the same pass as the rows, so the band cannot
825
+ // outlive the conversation it was written in.
826
+ setNarration([]);
827
+ // A new conversation starts with no children on screen, saved ones
828
+ // included: the evidence stays on disk, but it belongs to the runs of
829
+ // the conversation that was just left behind.
830
+ savedSubagentsTokenRef.current += 1;
831
+ savedSubagentsRef.current = [];
717
832
  replaceSubagents([]);
718
833
  }, [replaceSubagents, session, setAgentSurface]);
719
834
  // Source of truth for in-flight tools (the event loop runs across renders, so
@@ -1002,6 +1117,92 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1002
1117
  ]);
1003
1118
  return id;
1004
1119
  }, [nextId]);
1120
+ /**
1121
+ * `/agents runs`: opens a picker immediately with a disabled loading row,
1122
+ * then replaces it once the live snapshot and the disk walk both resolve.
1123
+ *
1124
+ * The composer stays reachable the whole time — nothing here awaits before
1125
+ * the first `setChoicePicker`. A stale read is dropped by comparing
1126
+ * `choicePickerRef.current` against the exact loading-picker object this
1127
+ * call put there (the idiom the `/review` pickers already use below),
1128
+ * rather than a counter: Escape and every other picker-replacing path
1129
+ * already route through `setChoicePicker`, so they retire this object as a
1130
+ * side effect and need no separate token bump of their own. That covers
1131
+ * both "the operator left this picker" (any other object is current now,
1132
+ * `null` included) and "opened it again" (a second call's own object is
1133
+ * current instead).
1134
+ */
1135
+ const openAgentRunsPicker = useCallback(() => {
1136
+ setSelectedChoice(0);
1137
+ const pending = {
1138
+ kind: 'agent-runs',
1139
+ title: 'Orchestration runs',
1140
+ values: ['loading'],
1141
+ options: [
1142
+ { label: 'Loading…', description: 'Reading saved evidence…', disabledReason: 'Loading…' },
1143
+ ],
1144
+ };
1145
+ setChoicePicker(pending);
1146
+ void (async () => {
1147
+ const live = liveOrchestrationRuns(subagentsRef.current, Date.now());
1148
+ let finished = [];
1149
+ try {
1150
+ finished = (await session?.listOrchestrationRuns?.()) ?? [];
1151
+ }
1152
+ catch {
1153
+ // Disk evidence is optional here: a live-only listing is still an
1154
+ // honest answer, and the empty-history message below still applies
1155
+ // if that live half turns out empty too.
1156
+ }
1157
+ if (appLifetime.signal.aborted || choicePickerRef.current !== pending)
1158
+ return;
1159
+ const listing = combineOrchestrationRuns(live, finished);
1160
+ const empty = renderAgentRuns(listing);
1161
+ if (empty) {
1162
+ setChoicePicker(null);
1163
+ pushMessage('system', empty);
1164
+ return;
1165
+ }
1166
+ setChoicePicker({
1167
+ kind: 'agent-runs',
1168
+ title: 'Orchestration runs',
1169
+ notice: listing.omitted > 0
1170
+ ? `${listing.omitted} older run${listing.omitted === 1 ? '' : 's'} not shown.`
1171
+ : undefined,
1172
+ values: listing.runs,
1173
+ options: listing.runs.map(agentRunOption),
1174
+ });
1175
+ })();
1176
+ }, [appLifetime, pushMessage, session, setChoicePicker, setSelectedChoice]);
1177
+ /**
1178
+ * Opens an `/agents runs` selection straight into its first agent's
1179
+ * transcript — where the replayed banner actually lives, for a finished
1180
+ * run — rather than the phase/agent list Ctrl+T lands on.
1181
+ *
1182
+ * A finished run's evidence is hydrated first, since a run this operator
1183
+ * has not opened the cockpit for yet may not be in `subagents` at all.
1184
+ * A live run skips that: it is already there, and re-reading disk for a
1185
+ * run this process is still writing would risk the very half-written
1186
+ * evidence `listSavedOrchestrationRuns` exists to stay off of.
1187
+ */
1188
+ const openOrchestrationRun = useCallback(async (run) => {
1189
+ if (!run.live)
1190
+ await hydrateSavedChildren();
1191
+ const workflow = agentWorkflows(subagentsRef.current).find((candidate) => candidate.agents.some((agent) => agent.workflowId === run.id));
1192
+ const firstPhase = workflow?.phases[0];
1193
+ const firstAgent = firstPhase?.agents[0];
1194
+ if (!firstPhase || !firstAgent) {
1195
+ pushMessage('system', `Could not open "${run.name}" — its saved evidence is no longer available.`);
1196
+ return;
1197
+ }
1198
+ setAgentSurface({
1199
+ kind: 'transcript',
1200
+ selectedPhaseId: firstPhase.id,
1201
+ selectedId: firstAgent.viewId,
1202
+ returnFocus: 'agents',
1203
+ tailOffset: 0,
1204
+ });
1205
+ }, [hydrateSavedChildren, pushMessage, setAgentSurface]);
1005
1206
  const reportedAgentsRef = useRef(new Set());
1006
1207
  useEffect(() => {
1007
1208
  reportedAgentsRef.current.clear();
@@ -1025,6 +1226,38 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1025
1226
  pushMessage('tool', `${agent.description} · ${status}`, false, completed ? '✓' : '✗', undefined, completed ? theme.status.ok : theme.status.error, 'ctrl+t · agent details');
1026
1227
  }
1027
1228
  }, [subagents, pushMessage, session]);
1229
+ /**
1230
+ * A delivered `send_message` correction already lands its own row inside
1231
+ * the child's transcript (`SubagentActivityMonitor.recordMessage`); this
1232
+ * mirrors it into the main conversation so the operator sees what was
1233
+ * sent without drilling in. Deliberately a separate ref from
1234
+ * `reportedAgentsRef` above: that gate is exactly-once per terminal
1235
+ * agent, this one is exactly-once per delivered message, and folding the
1236
+ * two together would let one gate's reset silently double-report the
1237
+ * other's rows.
1238
+ */
1239
+ const reportedCorrectionsRef = useRef(new Set());
1240
+ useEffect(() => {
1241
+ reportedCorrectionsRef.current.clear();
1242
+ }, [session]);
1243
+ useEffect(() => {
1244
+ const current = session?.subagents?.getSnapshot() ?? [];
1245
+ for (const agent of subagents) {
1246
+ const live = current.find((item) => item.viewId === agent.viewId && item.workflowId === agent.workflowId);
1247
+ if (!live)
1248
+ continue;
1249
+ for (const row of agent.transcript) {
1250
+ if (row.kind !== 'system' || row.direction !== 'to-child')
1251
+ continue;
1252
+ if (!live.transcript.some((item) => item.id === row.id))
1253
+ continue;
1254
+ if (reportedCorrectionsRef.current.has(row.id))
1255
+ continue;
1256
+ reportedCorrectionsRef.current.add(row.id);
1257
+ pushMessage('tool', `${agent.description} · correction sent`, false, '→', [row.text]);
1258
+ }
1259
+ }
1260
+ }, [subagents, pushMessage, session]);
1028
1261
  const applyPermissionMode = useCallback((mode) => {
1029
1262
  if (!session?.hasProvider) {
1030
1263
  pushMessage('system', 'Choose a model before changing permissions.');
@@ -1079,6 +1312,47 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1079
1312
  setReasoningEffort(effort);
1080
1313
  pushMessage('system', `Reasoning: ${effort} (this session).`);
1081
1314
  }, [hasUnsettledTurn, pushMessage, session, setReasoningEffort, state]);
1315
+ /**
1316
+ * Turn the session's orchestrate mode on or off.
1317
+ *
1318
+ * On: pins effort to the model's highest published level (last entry of
1319
+ * `reasoningEffortLevels`, which every provider publishes low-to-high) and
1320
+ * strengthens delegation guidance for future turns via `SendOptions.orchestrate`
1321
+ * — see `applyReasoningEffort` above for the mirrored guard shape. When the
1322
+ * model publishes no exact menu, effort is left alone and the notice says
1323
+ * so explicitly rather than silently doing nothing.
1324
+ */
1325
+ const applyOrchestrateMode = useCallback((enabled, selectedSession) => {
1326
+ if (!session?.hasProvider) {
1327
+ pushMessage('system', 'No active session — pick a provider before turning orchestrate mode on.');
1328
+ return;
1329
+ }
1330
+ if (selectedSession !== undefined && selectedSession !== session) {
1331
+ pushMessage('system', 'Orchestrate mode was not changed: the selected model is no longer active.');
1332
+ return;
1333
+ }
1334
+ if (state !== 'idle' ||
1335
+ abortRef.current !== null ||
1336
+ hasUnsettledTurn() ||
1337
+ (queuedRef.current.length > 0 && selectedSession !== session) ||
1338
+ permissionResolveRef.current !== null ||
1339
+ compactingRef.current) {
1340
+ pushMessage('system', 'Orchestrate mode was not changed: wait for the active turn, prompt, compaction, and queued work to settle.');
1341
+ return;
1342
+ }
1343
+ setOrchestrateMode(enabled);
1344
+ if (!enabled) {
1345
+ pushMessage('system', 'Orchestrate mode is off.');
1346
+ return;
1347
+ }
1348
+ const highest = highestReasoningEffort(session.reasoningEffortLevels);
1349
+ if (highest !== undefined) {
1350
+ setReasoningEffort(highest);
1351
+ pushMessage('system', `Orchestrate mode is on — effort pinned to ${highest} for ${session.modelSummary ?? 'this model'}, and delegation guidance is strengthened for this session.`);
1352
+ return;
1353
+ }
1354
+ pushMessage('system', `Orchestrate mode is on — ${session.modelSummary ?? 'this model'} does not publish an exact effort menu, so effort was left as is. Delegation guidance is still strengthened for this session.`);
1355
+ }, [hasUnsettledTurn, pushMessage, session, setOrchestrateMode, setReasoningEffort, state]);
1082
1356
  const stepReasoningEffort = useCallback((direction) => {
1083
1357
  const levels = session?.reasoningEffortLevels;
1084
1358
  if (!session?.hasProvider || levels === undefined || levels.length === 0) {
@@ -1583,6 +1857,12 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1583
1857
  setChoicePicker({ ...child, back: picker });
1584
1858
  return;
1585
1859
  }
1860
+ if (picker.kind === 'agent-runs') {
1861
+ if (value === 'loading')
1862
+ return;
1863
+ void openOrchestrationRun(value);
1864
+ return;
1865
+ }
1586
1866
  if (picker.kind === 'archive-conversation') {
1587
1867
  if (value === 'archive')
1588
1868
  archiveCurrentConversation();
@@ -1636,16 +1916,22 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1636
1916
  removeStoredCredential(value);
1637
1917
  return;
1638
1918
  }
1919
+ if (picker.kind === 'reasoning-effort' && value === ORCHESTRATE_MODE_VALUE) {
1920
+ applyOrchestrateMode(!orchestrateModeRef.current, picker.selectedSession);
1921
+ return;
1922
+ }
1639
1923
  applyReasoningEffort(value, picker.selectedSession);
1640
1924
  }, [
1641
1925
  activateSkill,
1642
1926
  advanceQueueContinuation,
1643
1927
  appLifetime.signal,
1928
+ applyOrchestrateMode,
1644
1929
  applyPermissionMode,
1645
1930
  applyReasoningEffort,
1646
1931
  archiveCurrentConversation,
1647
1932
  ctx.cwd,
1648
1933
  enqueueQueued,
1934
+ openOrchestrationRun,
1649
1935
  pushMessage,
1650
1936
  recordFeedback,
1651
1937
  removeStoredCredential,
@@ -1905,9 +2191,12 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1905
2191
  // A picker-owned provider/model change is one state transition. Clear the
1906
2192
  // old model's effort selection before publishing the replacement session
1907
2193
  // or releasing any paused queue. Failed and superseded candidates returned
1908
- // above, so they leave the current session selection untouched.
1909
- if (signal !== undefined)
1910
- setReasoningEffort(undefined);
2194
+ // above, so they leave the current session selection untouched. Orchestrate
2195
+ // mode survives the switch: re-pin to the new model's highest published
2196
+ // level instead of clearing, exactly as it pinned when first turned on.
2197
+ if (signal !== undefined) {
2198
+ setReasoningEffort(orchestrateModeRef.current ? highestReasoningEffort(s.reasoningEffortLevels) : undefined);
2199
+ }
1911
2200
  // Re-hydration (a provider switch via /model) builds a second session;
1912
2201
  // without this the first one's tool-server child processes stay alive
1913
2202
  // for the rest of the TUI's life.
@@ -1915,7 +2204,9 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1915
2204
  void previousSessionRef.current?.close();
1916
2205
  previousSessionRef.current = s;
1917
2206
  setSession(s);
1918
- if (options.chooseReasoningEffort && s.reasoningEffortLevels?.length) {
2207
+ // Orchestrate mode already re-pinned effort above; reopening this picker
2208
+ // would ask the operator to redo a choice the mode just made for them.
2209
+ if (!orchestrateModeRef.current && options.chooseReasoningEffort && s.reasoningEffortLevels?.length) {
1919
2210
  // Own input before publishing ready or releasing a paused queue. The
1920
2211
  // menu closes only after choosing an effort or keeping the new default.
1921
2212
  setSelectedChoice(0);
@@ -2603,6 +2894,10 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2603
2894
  : null;
2604
2895
  pendingGoalResumeRef.current = conv.id;
2605
2896
  scope.sessionId = conv.id; // new turns now attribute to the resumed session
2897
+ // The scope now names the resumed conversation, so its finished
2898
+ // children are readable. Without this the first Ctrl+T after a resume
2899
+ // would have nothing to find.
2900
+ void hydrateSavedChildren();
2606
2901
  conversationMaterializedRef.current = true;
2607
2902
  pushMessage('system', `Resumed: ${conv.title}`);
2608
2903
  if (discardedQueued > 0) {
@@ -2623,6 +2918,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2623
2918
  nextId,
2624
2919
  pushMessage,
2625
2920
  cancelPendingModelSwitch,
2921
+ hydrateSavedChildren,
2626
2922
  resetSubagentActivity,
2627
2923
  session,
2628
2924
  resetTranscript,
@@ -2674,6 +2970,11 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2674
2970
  if (sourceScope && targetSessionId) {
2675
2971
  sourceScope.sessionId = targetSessionId;
2676
2972
  conversationMaterializedRef.current = true;
2973
+ // A fresh conversation owns no runs yet, so this normally reads
2974
+ // nothing. It runs anyway because the rule is "the scope changed,
2975
+ // re-read", and an exception here is how the resume path lost
2976
+ // its own re-read.
2977
+ void hydrateSavedChildren();
2677
2978
  }
2678
2979
  if (clearScreen) {
2679
2980
  setMessages([]);
@@ -2706,6 +3007,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2706
3007
  interruptTurn,
2707
3008
  pushMessage,
2708
3009
  cancelPendingModelSwitch,
3010
+ hydrateSavedChildren,
2709
3011
  resetSubagentActivity,
2710
3012
  session,
2711
3013
  resetTranscript,
@@ -2858,6 +3160,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2858
3160
  // is reloaded or reset. Only where the NEXT turn is written changes.
2859
3161
  scope.sessionId = forked.id;
2860
3162
  resetSubagentActivity();
3163
+ void hydrateSavedChildren();
2861
3164
  session?.resetTaskStore?.();
2862
3165
  goalActivation.clear();
2863
3166
  wakeGoalDriver();
@@ -2875,6 +3178,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2875
3178
  hasUnsettledTurn,
2876
3179
  materializeConversation,
2877
3180
  pushMessage,
3181
+ hydrateSavedChildren,
2878
3182
  resetSubagentActivity,
2879
3183
  session,
2880
3184
  wakeGoalDriver,
@@ -2968,6 +3272,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2968
3272
  goalActivation.clear();
2969
3273
  wakeGoalDriver();
2970
3274
  scope.sessionId = forked.id;
3275
+ void hydrateSavedChildren();
2971
3276
  modelHistoryRef.current = forked.messages;
2972
3277
  lastAssistantMessage.current = null;
2973
3278
  const persistedOutput = latestAssistantOutput(forked.messages);
@@ -3005,6 +3310,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3005
3310
  nextId,
3006
3311
  pushMessage,
3007
3312
  cancelPendingModelSwitch,
3313
+ hydrateSavedChildren,
3008
3314
  resetSubagentActivity,
3009
3315
  session,
3010
3316
  resetTranscript,
@@ -3229,6 +3535,17 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3229
3535
  event.toolName.toLowerCase() === 'agent' &&
3230
3536
  subagentsRef.current.some((agent) => agent.toolUseId === event.toolUseId &&
3231
3537
  (event.runId === undefined || agent.workflowId === event.runId));
3538
+ // The band above the rail already carries a narrated line, in the
3539
+ // parent's own words, from the moment it is written. Printing the
3540
+ // call that wrote it would show the same sentence twice, the
3541
+ // second time as protocol — the same duplicate-protocol case as
3542
+ // the represented Agent call just below, and refused the same
3543
+ // way. A REFUSED line keeps its row: nothing reached the screen,
3544
+ // and that row is the only thing that says so.
3545
+ if (!event.isError && event.toolName === NARRATION_TOOL_NAME) {
3546
+ setState(activeToolsRef.current.length > 0 ? 'tool' : 'thinking');
3547
+ break;
3548
+ }
3232
3549
  if (representedSuccessfulAgent) {
3233
3550
  // The automatic panel already owned this successful child from
3234
3551
  // start through settlement. Re-emitting its internal Agent call as
@@ -3684,6 +4001,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3684
4001
  activeTurnInboxRef.current = inbox;
3685
4002
  const turnPermissionMode = permissionModeRef.current;
3686
4003
  const turnReasoningEffort = reasoningEffortRef.current;
4004
+ const turnOrchestrateMode = orchestrateModeRef.current;
3687
4005
  const turnRunLimits = resolveRunGuards(ctxRef.current.limits, runLimitsOverrideRef.current);
3688
4006
  // Always carry the guarded callback. `auto` and `strict` decide before
3689
4007
  // calling it in makeResumeHandler; retaining it is what lets a session
@@ -3744,6 +4062,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3744
4062
  permissionMode: turnPermissionMode,
3745
4063
  limits: turnRunLimits,
3746
4064
  ...(turnReasoningEffort !== undefined ? { effort: turnReasoningEffort } : {}),
4065
+ ...(turnOrchestrateMode ? { orchestrate: true } : {}),
3747
4066
  ...(goalRound ? { goalRound } : {}),
3748
4067
  // The mode above decides whether this callback is consulted.
3749
4068
  onPermission: askPermission,
@@ -4224,9 +4543,20 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4224
4543
  return;
4225
4544
  }
4226
4545
  case 'agent-cockpit': {
4227
- if (!openAgentCockpit()) {
4228
- pushMessage('system', `No delegated agents in this conversation.${session?.agentIds.length ? `\nAvailable: ${session.agentIds.join(', ')}` : '\nNo agents are configured.'}`);
4229
- }
4546
+ // Reported after the read, not before it: the answer to "are
4547
+ // there any" may be on disk, and a conversation whose evidence
4548
+ // is sitting in its own directory must never be told it
4549
+ // delegated nothing.
4550
+ const agentIds = session?.agentIds ?? [];
4551
+ void openAgentCockpit().then((opened) => {
4552
+ if (opened)
4553
+ return;
4554
+ pushMessage('system', `No delegated agents in this conversation.${agentIds.length ? `\nAvailable: ${agentIds.join(', ')}` : '\nNo agents are configured.'}`);
4555
+ });
4556
+ return;
4557
+ }
4558
+ case 'agent-runs': {
4559
+ openAgentRunsPicker();
4230
4560
  return;
4231
4561
  }
4232
4562
  case 'provider-setup':
@@ -4563,7 +4893,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4563
4893
  const values = [undefined, ...levels];
4564
4894
  const current = reasoningEffortRef.current;
4565
4895
  setSelectedChoice(Math.max(0, values.findIndex((value) => value === current)));
4566
- setChoicePicker(reasoningEffortPicker(session, current) ?? null);
4896
+ setChoicePicker(reasoningEffortPicker(session, current, false, orchestrateModeRef.current) ?? null);
4567
4897
  return;
4568
4898
  }
4569
4899
  case 'login':
@@ -4981,6 +5311,11 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4981
5311
  : 'Raw output mode off — rich transcript rendering restored.');
4982
5312
  return;
4983
5313
  }
5314
+ case 'orchestrate-mode': {
5315
+ const enabled = slash.enabled === 'toggle' ? !orchestrateModeRef.current : slash.enabled;
5316
+ applyOrchestrateMode(enabled);
5317
+ return;
5318
+ }
4984
5319
  case 'export-picker': {
4985
5320
  if (!stableExportSource())
4986
5321
  return;
@@ -5063,6 +5398,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
5063
5398
  hostCommands,
5064
5399
  nextId,
5065
5400
  openAgentCockpit,
5401
+ openAgentRunsPicker,
5066
5402
  pushMessage,
5067
5403
  rawOutput,
5068
5404
  removeStoredCredential,
@@ -5563,8 +5899,14 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
5563
5899
  setAgentSurface(null);
5564
5900
  return;
5565
5901
  }
5566
- if (openAgentCockpit())
5902
+ if (openCockpitFromCurrentRows()) {
5903
+ void hydrateSavedChildren();
5567
5904
  return;
5905
+ }
5906
+ // Nothing on screen yet. The conversation's children may exist only
5907
+ // as saved evidence, so the request outlives that read rather than
5908
+ // being answered by an empty snapshot.
5909
+ void openAgentCockpit();
5568
5910
  }
5569
5911
  // Child observation owns navigation, but never outranks consent: the
5570
5912
  // permission branch above preempts it if a child or parent call asks.
@@ -5702,7 +6044,9 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
5702
6044
  return;
5703
6045
  }
5704
6046
  if (key.home || key.end || key.pageUp || key.pageDown || key.upArrow || key.downArrow) {
5705
- const page = agentTranscriptPageSize(terminal.rows);
6047
+ // Reserving the replayed banner's row here too keeps one PgDn a
6048
+ // screenful of what is actually visible.
6049
+ const page = agentTranscriptPageSize(terminal.rows, selected.replayed ? 1 : 0);
5706
6050
  const max = maxAgentTranscriptTailOffset(selected, terminal.rows, terminal.columns);
5707
6051
  const offset = Math.min(agentView.tailOffset, max);
5708
6052
  const next = key.home
@@ -5976,6 +6320,17 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
5976
6320
  ? permissionOwner ? `Agent: ${permissionOwner.description}` : `Run: ${permission.runId}`
5977
6321
  : undefined;
5978
6322
  const displayedPermissionMode = effectivePermissionMode(permissionMode, session?.approvalLatched() ?? false);
6323
+ // True exactly when the phase ternary below renders its final branch — the
6324
+ // normal composer view, as opposed to provider setup or a lifecycle
6325
+ // picker/prompt. The agent rail is a feature of that view: computing this
6326
+ // once here lets its render site sit after the footer, below, while still
6327
+ // never appearing during trust/resume/edit/picker/setup phases.
6328
+ const showComposerSurface = !providerSetup &&
6329
+ phase !== 'trust' &&
6330
+ phase !== 'unhealthy' &&
6331
+ phase !== 'resume' &&
6332
+ phase !== 'edit' &&
6333
+ phase !== 'picker';
5979
6334
  return (_jsx(Box, { flexDirection: "column", display: externalEditorRequest ? 'none' : 'flex', children: _jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(TranscriptFrame, { children: _jsx(Transcript, { messages: finalized, pending: messages.find((m) => m.pending) ?? null, state: state, settled: transcriptOwned ? renderedSettled : 0, resetKey: resetKey, raw: rawOutput, hyperlinks: hyperlinks, showLive: !lifecycleOwnsViewport, header: transcriptOwned ? (_jsx(BrandHeader, { version: ctx.version, permissionMode: displayedPermissionMode })) : undefined }) }), providerSetup ? (_jsx(ProviderSetup, { cwd: ctx.cwd, onClose: () => setProviderSetup(false), onConnect: () => {
5980
6335
  setProviderSetup(false);
5981
6336
  setPickerInitialView('providers');
@@ -6009,7 +6364,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
6009
6364
  ? queuePause.outcome === 'paused'
6010
6365
  ? 'held after a resumable run paused; wait for recovery, change model, or send a message to release it'
6011
6366
  : `paused after a ${queuePause.outcome} turn; send a message or change model to continue`
6012
- : 'sending when ready'] }) })) : null, _jsx(Composer, { reasoningEffortLevels: session?.reasoningEffortLevels, permissionMode: displayedPermissionMode, onCycleMode: cyclePermissionMode, disabled: outputViewer !== null ||
6367
+ : 'sending when ready'] }) })) : null, _jsx(Composer, { reasoningEffortLevels: session?.reasoningEffortLevels, onCycleMode: cyclePermissionMode, disabled: outputViewer !== null ||
6013
6368
  phase !== 'ready' ||
6014
6369
  state === 'awaiting-permission' ||
6015
6370
  compacting ||
@@ -6026,13 +6381,23 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
6026
6381
  (state === 'thinking' || state === 'tool' || pendingModelSwitchRef.current !== null), onSubmit: handleSubmit, onNotice: (text) => pushMessage('system', text), onStepReasoningEffort: stepReasoningEffort, onExternalEdit: requestExternalEditor, onEditPrevious: openPromptEditor, draftToRestore: composerDraft, onDraftRestored: (token) => setComposerDraft((draft) => (draft?.token === token ? null : draft)), onDraftPresenceChange: setComposerHasDraft, onOpenAgentPanel: openAgentCockpit, userCommands: userCommands, builtins: hostCommands.map((command) => ({
6027
6382
  ...command,
6028
6383
  description: command.unavailable?.(slashCtx) ?? command.description,
6029
- })), mentionCandidates: mentionCandidates, history: history })] }), permission === null && agentSurface === null && outputViewer === null && liveSubagents.length > 0 ? (_jsx(AgentTaskPanel, { agents: liveSubagents, terminalRows: terminal.rows, terminalColumns: terminal.columns })) : permission === null && agentSurface?.kind === 'cockpit' ? (_jsx(AgentCockpit, { agents: subagents, selectedPhaseId: agentSurface.selectedPhaseId, selectedId: agentSurface.selectedId, focus: agentSurface.focus, terminalRows: terminal.rows, terminalColumns: terminal.columns })) : permission === null && agentSurface?.kind === 'transcript' && selectedSubagent ? (_jsx(AgentTranscript, { agent: selectedSubagent, tailOffset: agentSurface.tailOffset, terminalRows: terminal.rows, terminalColumns: terminal.columns })) : null] })), outputViewer && permission === null ? (_jsx(ToolOutputViewer, { onPrevious: () => {
6384
+ })), mentionCandidates: mentionCandidates, history: history })] })] })), _jsx(StatusBar, { cwd: ctx.cwd, provider: session?.providerSummary ?? null, model: session?.modelSummary ?? null, effort: reasoningEffort, orchestrate: orchestrateMode, goal: statusGoal, state: state, hint: statusHint, permissionMode: displayedPermissionMode, canCycleMode: true }), showComposerSurface &&
6385
+ permission === null &&
6386
+ agentSurface === null &&
6387
+ outputViewer === null ? (_jsx(AgentNarrationBand, { lines: narration })) : null, showComposerSurface &&
6388
+ permission === null &&
6389
+ agentSurface === null &&
6390
+ outputViewer === null &&
6391
+ liveSubagents.length > 0 ? (_jsx(AgentTaskPanel, { agents: liveSubagents, terminalRows: terminal.rows, terminalColumns: terminal.columns })) : showComposerSurface && permission === null && agentSurface?.kind === 'cockpit' ? (_jsx(AgentCockpit, { agents: subagents, selectedPhaseId: agentSurface.selectedPhaseId, selectedId: agentSurface.selectedId, focus: agentSurface.focus, terminalRows: terminal.rows, terminalColumns: terminal.columns })) : showComposerSurface &&
6392
+ permission === null &&
6393
+ agentSurface?.kind === 'transcript' &&
6394
+ selectedSubagent ? (_jsx(AgentTranscript, { agent: selectedSubagent, tailOffset: agentSurface.tailOffset, terminalRows: terminal.rows, terminalColumns: terminal.columns })) : null, outputViewer && permission === null ? (_jsx(ToolOutputViewer, { onPrevious: () => {
6030
6395
  if (outputIndex > 0)
6031
6396
  setOutputViewer(outputBlocks[outputIndex - 1]);
6032
6397
  }, onNext: () => {
6033
6398
  if (outputIndex >= 0 && outputIndex + 1 < outputBlocks.length)
6034
6399
  setOutputViewer(outputBlocks[outputIndex + 1]);
6035
- }, title: outputViewer.content, lines: outputViewer.detail ?? [], rows: terminal.rows, columns: terminal.columns, onClose: () => setOutputViewer(null) }, outputViewer.id)) : null, _jsx(Box, { paddingTop: 1, children: _jsx(StatusBar, { cwd: ctx.cwd, provider: session?.providerSummary ?? null, model: session?.modelSummary ?? null, effort: reasoningEffort ?? 'default', goal: statusGoal, state: state, hint: statusHint }) })] }) }));
6400
+ }, title: outputViewer.content, lines: outputViewer.detail ?? [], rows: terminal.rows, columns: terminal.columns, onClose: () => setOutputViewer(null) }, outputViewer.id)) : null] }) }));
6036
6401
  }
6037
6402
  /** One line for a background job that ended, for the transcript and the next turn. */
6038
6403
  function describeJobExit(job) {
@@ -6109,6 +6474,33 @@ function goalStatusLabel(goal, armed) {
6109
6474
  function labelOfOption(question, optionId) {
6110
6475
  return question.options.find((option) => option.id === optionId)?.label ?? optionId;
6111
6476
  }
6477
+ /** `/agents runs`' one-line-per-run text, mirroring `/jobs`' own column shape. */
6478
+ function agentRunOption(run) {
6479
+ const agents = `${run.agentsDone}/${run.agentsTotal} agent${run.agentsTotal === 1 ? '' : 's'}`;
6480
+ const tokens = `${run.tokensTotal.toLocaleString('en-US')} tokens`;
6481
+ const columns = [
6482
+ relativeRunStart(run.startedAt),
6483
+ run.phases.join(', '),
6484
+ agents,
6485
+ tokens,
6486
+ formatElapsed(run.elapsedMs),
6487
+ ];
6488
+ if (run.live)
6489
+ columns.push('live');
6490
+ return { label: run.name, description: columns.join(' · ') };
6491
+ }
6492
+ /** `just now` / `12m ago` / `3h ago` / `2d ago` — the same buckets a conversation's own resume picker shows. */
6493
+ function relativeRunStart(startedAt) {
6494
+ const minutes = Math.round((Date.now() - startedAt) / 60_000);
6495
+ if (minutes < 1)
6496
+ return 'just now';
6497
+ if (minutes < 60)
6498
+ return `${minutes}m ago`;
6499
+ const hours = Math.round(minutes / 60);
6500
+ if (hours < 24)
6501
+ return `${hours}h ago`;
6502
+ return `${Math.round(hours / 24)}d ago`;
6503
+ }
6112
6504
  function choicePickerSearchable(picker) {
6113
6505
  return ['command', 'skill', 'plugin', 'review-branch', 'review-commit'].includes(picker.kind);
6114
6506
  }
@@ -6190,11 +6582,27 @@ function permissionPicker(mode, approvedAll, more = false) {
6190
6582
  }),
6191
6583
  };
6192
6584
  }
6193
- function reasoningEffortPicker(session, current, afterModelSelection = false) {
6585
+ /**
6586
+ * The picker's sentinel for the orchestrate-mode row, below the rule.
6587
+ *
6588
+ * A plain string outside the `ReasoningEffort` union by construction — never
6589
+ * added to any model's published menu — so it can share the same picker
6590
+ * without ever being mistaken for a level `/effort <token>` could select.
6591
+ */
6592
+ const ORCHESTRATE_MODE_VALUE = 'orchestrate';
6593
+ /** Last entry of a low-to-high menu every provider publishes in that order; `undefined` when none is known. */
6594
+ function highestReasoningEffort(levels) {
6595
+ return levels && levels.length > 0 ? levels[levels.length - 1] : undefined;
6596
+ }
6597
+ function reasoningEffortPicker(session, current, afterModelSelection = false, orchestrateOn = false) {
6194
6598
  const levels = session.reasoningEffortLevels;
6195
6599
  if (!session.hasProvider || levels === undefined)
6196
6600
  return undefined;
6197
- const values = [undefined, ...levels];
6601
+ const effortValues = [undefined, ...levels];
6602
+ const values = [
6603
+ ...effortValues,
6604
+ ORCHESTRATE_MODE_VALUE,
6605
+ ];
6198
6606
  return {
6199
6607
  kind: 'reasoning-effort',
6200
6608
  title: `Select Reasoning Level for ${session.modelSummary ?? 'current model'}`,
@@ -6205,12 +6613,24 @@ function reasoningEffortPicker(session, current, afterModelSelection = false) {
6205
6613
  }
6206
6614
  : {}),
6207
6615
  values,
6208
- options: values.map((effort) => ({
6209
- label: effort ?? 'default',
6210
- description: reasoningEffortDescription(effort),
6211
- current: effort === current,
6212
- default: effort === undefined,
6213
- })),
6616
+ options: [
6617
+ ...effortValues.map((effort) => ({
6618
+ label: effort ?? 'default',
6619
+ description: reasoningEffortDescription(effort),
6620
+ current: effort === current,
6621
+ default: effort === undefined,
6622
+ })),
6623
+ {
6624
+ // Below a rule, visually apart from the levels above: a session
6625
+ // setting, not a sixth level the provider published.
6626
+ label: 'orchestrate',
6627
+ description: orchestrateOn
6628
+ ? 'On · highest level and delegate by default'
6629
+ : 'Off · highest level and delegate by default',
6630
+ current: orchestrateOn,
6631
+ ruleBefore: true,
6632
+ },
6633
+ ],
6214
6634
  };
6215
6635
  }
6216
6636
  function reasoningEffortDescription(effort) {