@namzu/cli 20.0.0 → 21.0.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 (155) hide show
  1. package/CHANGELOG.md +255 -0
  2. package/README.md +42 -2
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +24 -2
  5. package/dist/cli.js.map +1 -1
  6. package/dist/commands/acp.d.ts.map +1 -1
  7. package/dist/commands/acp.js +6 -1
  8. package/dist/commands/acp.js.map +1 -1
  9. package/dist/commands/run-stream.d.ts.map +1 -1
  10. package/dist/commands/run-stream.js +9 -1
  11. package/dist/commands/run-stream.js.map +1 -1
  12. package/dist/commands/run.d.ts.map +1 -1
  13. package/dist/commands/run.js +14 -11
  14. package/dist/commands/run.js.map +1 -1
  15. package/dist/config/load.d.ts.map +1 -1
  16. package/dist/config/load.js +21 -0
  17. package/dist/config/load.js.map +1 -1
  18. package/dist/config/schema.d.ts +11 -4
  19. package/dist/config/schema.d.ts.map +1 -1
  20. package/dist/config/schema.js.map +1 -1
  21. package/dist/doctor/checks/chain.d.ts.map +1 -1
  22. package/dist/doctor/checks/chain.js +11 -5
  23. package/dist/doctor/checks/chain.js.map +1 -1
  24. package/dist/doctor/checks/credentials.d.ts.map +1 -1
  25. package/dist/doctor/checks/credentials.js +5 -1
  26. package/dist/doctor/checks/credentials.js.map +1 -1
  27. package/dist/index.d.ts +1 -1
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js.map +1 -1
  30. package/dist/integrations/providers/access.d.ts +7 -0
  31. package/dist/integrations/providers/access.d.ts.map +1 -0
  32. package/dist/integrations/providers/access.js +17 -0
  33. package/dist/integrations/providers/access.js.map +1 -0
  34. package/dist/integrations/providers/discover.d.ts +17 -54
  35. package/dist/integrations/providers/discover.d.ts.map +1 -1
  36. package/dist/integrations/providers/discover.js +46 -58
  37. package/dist/integrations/providers/discover.js.map +1 -1
  38. package/dist/integrations/providers/harness-credentials.d.ts +14 -0
  39. package/dist/integrations/providers/harness-credentials.d.ts.map +1 -1
  40. package/dist/integrations/providers/harness-credentials.js +46 -0
  41. package/dist/integrations/providers/harness-credentials.js.map +1 -1
  42. package/dist/integrations/providers/model-reasoning.d.ts +4 -0
  43. package/dist/integrations/providers/model-reasoning.d.ts.map +1 -0
  44. package/dist/integrations/providers/model-reasoning.js +46 -0
  45. package/dist/integrations/providers/model-reasoning.js.map +1 -0
  46. package/dist/integrations/providers/register.d.ts +1 -1
  47. package/dist/integrations/providers/register.d.ts.map +1 -1
  48. package/dist/integrations/providers/register.js +11 -1
  49. package/dist/integrations/providers/register.js.map +1 -1
  50. package/dist/integrations/providers/registry.d.ts +4 -4
  51. package/dist/integrations/providers/registry.d.ts.map +1 -1
  52. package/dist/integrations/providers/registry.js +23 -0
  53. package/dist/integrations/providers/registry.js.map +1 -1
  54. package/dist/integrations/sessions/context-inventory.d.ts +4 -0
  55. package/dist/integrations/sessions/context-inventory.d.ts.map +1 -0
  56. package/dist/integrations/sessions/context-inventory.js +44 -0
  57. package/dist/integrations/sessions/context-inventory.js.map +1 -0
  58. package/dist/integrations/sessions/conversation-search.d.ts +60 -0
  59. package/dist/integrations/sessions/conversation-search.d.ts.map +1 -0
  60. package/dist/integrations/sessions/conversation-search.js +461 -0
  61. package/dist/integrations/sessions/conversation-search.js.map +1 -0
  62. package/dist/integrations/sessions/task-context.d.ts +4 -0
  63. package/dist/integrations/sessions/task-context.d.ts.map +1 -0
  64. package/dist/integrations/sessions/task-context.js +73 -0
  65. package/dist/integrations/sessions/task-context.js.map +1 -0
  66. package/dist/integrations/subagents/activity.d.ts +3 -1
  67. package/dist/integrations/subagents/activity.d.ts.map +1 -1
  68. package/dist/integrations/subagents/activity.js +20 -2
  69. package/dist/integrations/subagents/activity.js.map +1 -1
  70. package/dist/integrations/subagents/runtime.d.ts +7 -4
  71. package/dist/integrations/subagents/runtime.d.ts.map +1 -1
  72. package/dist/integrations/subagents/runtime.js +105 -17
  73. package/dist/integrations/subagents/runtime.js.map +1 -1
  74. package/dist/memory/io.d.ts +12 -0
  75. package/dist/memory/io.d.ts.map +1 -0
  76. package/dist/memory/io.js +177 -0
  77. package/dist/memory/io.js.map +1 -0
  78. package/dist/memory/presentation.d.ts +4 -1
  79. package/dist/memory/presentation.d.ts.map +1 -1
  80. package/dist/memory/presentation.js +16 -2
  81. package/dist/memory/presentation.js.map +1 -1
  82. package/dist/memory/store.d.ts +17 -3
  83. package/dist/memory/store.d.ts.map +1 -1
  84. package/dist/memory/store.js +61 -35
  85. package/dist/memory/store.js.map +1 -1
  86. package/dist/output-schema.d.ts +4 -0
  87. package/dist/output-schema.d.ts.map +1 -0
  88. package/dist/output-schema.js +18 -0
  89. package/dist/output-schema.js.map +1 -0
  90. package/dist/tui/AgentExplorer.d.ts +13 -2
  91. package/dist/tui/AgentExplorer.d.ts.map +1 -1
  92. package/dist/tui/AgentExplorer.js +85 -23
  93. package/dist/tui/AgentExplorer.js.map +1 -1
  94. package/dist/tui/App.d.ts.map +1 -1
  95. package/dist/tui/App.js +454 -64
  96. package/dist/tui/App.js.map +1 -1
  97. package/dist/tui/BrandHeader.d.ts +1 -4
  98. package/dist/tui/BrandHeader.d.ts.map +1 -1
  99. package/dist/tui/BrandHeader.js +2 -6
  100. package/dist/tui/BrandHeader.js.map +1 -1
  101. package/dist/tui/Composer.d.ts +2 -1
  102. package/dist/tui/Composer.d.ts.map +1 -1
  103. package/dist/tui/Composer.js +9 -2
  104. package/dist/tui/Composer.js.map +1 -1
  105. package/dist/tui/PermissionOverlay.d.ts +1 -1
  106. package/dist/tui/PermissionOverlay.d.ts.map +1 -1
  107. package/dist/tui/PermissionOverlay.js +8 -1
  108. package/dist/tui/PermissionOverlay.js.map +1 -1
  109. package/dist/tui/Picker.d.ts.map +1 -1
  110. package/dist/tui/Picker.js +94 -21
  111. package/dist/tui/Picker.js.map +1 -1
  112. package/dist/tui/TextPrompt.d.ts +3 -1
  113. package/dist/tui/TextPrompt.d.ts.map +1 -1
  114. package/dist/tui/TextPrompt.js +55 -9
  115. package/dist/tui/TextPrompt.js.map +1 -1
  116. package/dist/tui/Transcript.d.ts.map +1 -1
  117. package/dist/tui/Transcript.js +1 -1
  118. package/dist/tui/Transcript.js.map +1 -1
  119. package/dist/tui/agent.d.ts +18 -3
  120. package/dist/tui/agent.d.ts.map +1 -1
  121. package/dist/tui/agent.js +174 -58
  122. package/dist/tui/agent.js.map +1 -1
  123. package/dist/tui/model-choices.d.ts +3 -1
  124. package/dist/tui/model-choices.d.ts.map +1 -1
  125. package/dist/tui/model-choices.js +12 -1
  126. package/dist/tui/model-choices.js.map +1 -1
  127. package/dist/tui/model-search.d.ts +4 -0
  128. package/dist/tui/model-search.d.ts.map +1 -0
  129. package/dist/tui/model-search.js +11 -0
  130. package/dist/tui/model-search.js.map +1 -0
  131. package/dist/tui/model-selection-intent.d.ts +8 -0
  132. package/dist/tui/model-selection-intent.d.ts.map +1 -0
  133. package/dist/tui/model-selection-intent.js +96 -0
  134. package/dist/tui/model-selection-intent.js.map +1 -0
  135. package/dist/tui/model-switch-tool.d.ts +7 -0
  136. package/dist/tui/model-switch-tool.d.ts.map +1 -0
  137. package/dist/tui/model-switch-tool.js +74 -0
  138. package/dist/tui/model-switch-tool.js.map +1 -0
  139. package/dist/tui/model-switch.d.ts +35 -0
  140. package/dist/tui/model-switch.d.ts.map +1 -0
  141. package/dist/tui/model-switch.js +106 -0
  142. package/dist/tui/model-switch.js.map +1 -0
  143. package/dist/tui/permission-review.d.ts.map +1 -1
  144. package/dist/tui/permission-review.js +25 -12
  145. package/dist/tui/permission-review.js.map +1 -1
  146. package/dist/tui/run-interruption.d.ts +7 -0
  147. package/dist/tui/run-interruption.d.ts.map +1 -1
  148. package/dist/tui/run-interruption.js +41 -0
  149. package/dist/tui/run-interruption.js.map +1 -1
  150. package/dist/tui/slashCommands.d.ts.map +1 -1
  151. package/dist/tui/slashCommands.js +2 -1
  152. package/dist/tui/slashCommands.js.map +1 -1
  153. package/dist/tui/types.d.ts +3 -1
  154. package/dist/tui/types.d.ts.map +1 -1
  155. package/package.json +8 -7
package/dist/tui/App.js CHANGED
@@ -25,12 +25,12 @@ import { appendMessages, archiveConversation, forkConversation, forkConversation
25
25
  import { conversationMarkdown, writeConversationExport, } from '../integrations/sessions/transcript-export.js';
26
26
  import { isTrusted, trustDir } from '../integrations/trust/store.js';
27
27
  import { checkUpdates } from '../integrations/updates.js';
28
- import { renderMemoryReport } from '../memory/presentation.js';
29
- import { appendMemory, readMemory } from '../memory/store.js';
28
+ import { renderMemoryReport, renderMemorySaveResult } from '../memory/presentation.js';
29
+ import { appendMemoryWithStatus, readMemory } from '../memory/store.js';
30
30
  import { effectivePermissionMode, permissionModeDescription, permissionModeLabel, } from '../permissions/mode.js';
31
31
  import { composeSkillsPrompt, discoverSkills, loadSkillBody } from '../skills/store.js';
32
32
  import { discoverUserCommands } from '../user-commands/store.js';
33
- import { AgentCockpit, AgentTaskPanel, AgentTranscript, activeSubagentCohorts, agentCockpitIsWide, agentPhasePageSize, agentPhases, agentPickerPageSize, agentTranscriptPageSize, maxAgentTranscriptTailOffset, } from './AgentExplorer.js';
33
+ import { AgentCockpit, AgentTaskPanel, AgentTranscript, activeSubagentCohorts, agentCockpitIsWide, agentPhasePageSize, agentPhases, agentPickerPageSize, agentTranscriptPageSize, agentWorkflowPageSize, agentWorkflows, maxAgentTranscriptTailOffset, } from './AgentExplorer.js';
34
34
  import { BrandHeader } from './BrandHeader.js';
35
35
  import { ChoicePicker } from './ChoicePicker.js';
36
36
  import { Composer, suggestionWindowSize, } from './Composer.js';
@@ -46,7 +46,7 @@ import { TaskList } from './TaskList.js';
46
46
  import { TextPrompt } from './TextPrompt.js';
47
47
  import { Transcript, willCollapse } from './Transcript.js';
48
48
  import { TrustPrompt } from './TrustPrompt.js';
49
- import { createAgentSession, probeAgentSession, } from './agent.js';
49
+ import { createAgentSession, describeProviderModels, probeAgentSession, } from './agent.js';
50
50
  import { choicePickerWindowSize, filterChoiceOptions, moveChoiceSelection, } from './choice-selection.js';
51
51
  import { keepRecentRows } from './compact-transcript.js';
52
52
  import { approvalIsDeliberate } from './consent-timing.js';
@@ -55,6 +55,8 @@ import { copyTargetsForResponse } from './copy-targets.js';
55
55
  import { editablePrompts } from './edit-prompts.js';
56
56
  import { editDraftInExternalEditor } from './external-editor.js';
57
57
  import { liveWindow } from './live-window.js';
58
+ import { resolveModelSwitch, } from './model-switch.js';
59
+ import { parseModelSelectionIntent, resolveModelSelectionIntent } from './model-selection-intent.js';
58
60
  import { describeCodexDeviceLoginStart, describeLoginOutcome, describeLoginStart, describeLogout, describeProviderLogout, } from './login-prompt.js';
59
61
  import { selectPrimaryProvider } from './provider-selection.js';
60
62
  import { eraseLastChoiceGrapheme } from './terminal-choice-text.js';
@@ -62,7 +64,7 @@ import { renderCheckpoints, renderRestore } from '../checkpoints/store.js';
62
64
  import { expandFileMentions, listMentionableFiles } from './mentions.js';
63
65
  import { openInBrowser } from './open-browser.js';
64
66
  import { buildPermissionReview, buildPermissionSummary, permissionReviewPageRows, permissionReviewRefusal, permissionReviewRows, } from './permission-review.js';
65
- import { describeRunInterruption } from './run-interruption.js';
67
+ import { describeRunInterruption, describeRunStop } from './run-interruption.js';
66
68
  import { moveSelection } from './selection-window.js';
67
69
  import { describeShellEscape, describeShellEscapeForModel, runShellEscape, shellEscapeCommand, } from './shell-escape.js';
68
70
  import { baseBranchReviewPrompt, commitReviewPrompt, hostCommandNames, kernelCommandDescriptors, mergeHostCommands, renderOutcome, reviewPrompt, runSlash, } from './slashCommands.js';
@@ -587,6 +589,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
587
589
  agentSurfaceCommittedRef.current = agentSurface;
588
590
  }, [agentSurface]);
589
591
  const replaceSubagents = useCallback((next) => {
592
+ const previous = subagentsRef.current;
590
593
  subagentsRef.current = next;
591
594
  setSubagentsState(next);
592
595
  const surface = agentSurfaceRef.current;
@@ -599,7 +602,9 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
599
602
  if (next.some((agent) => agent.viewId === surface.selectedId))
600
603
  return;
601
604
  const phases = agentPhases(next);
602
- const fallbackPhase = phases.find((phase) => phase.id === surface.selectedPhaseId) ?? phases[0];
605
+ const previousGroupId = previous.find((agent) => agent.viewId === surface.selectedId)?.workflowGroupId;
606
+ const sameWorkflow = agentWorkflows(next).find((workflow) => workflow.id === previousGroupId);
607
+ const fallbackPhase = phases.find((phase) => phase.id === surface.selectedPhaseId) ?? sameWorkflow?.phases[0] ?? phases[0];
603
608
  const fallbackAgent = fallbackPhase?.agents[0];
604
609
  if (!fallbackPhase || !fallbackAgent) {
605
610
  setAgentSurface(null);
@@ -614,7 +619,9 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
614
619
  }, [setAgentSurface]);
615
620
  const openAgentCockpit = useCallback(() => {
616
621
  const agents = subagentsRef.current;
617
- const firstPhase = agentPhases(agents)[0];
622
+ const workflows = agentWorkflows(agents);
623
+ const currentWorkflow = [...workflows].reverse().find((workflow) => workflow.agents.some((agent) => agent.status === 'starting' || agent.status === 'queued' || agent.status === 'working')) ?? workflows.at(-1);
624
+ const firstPhase = currentWorkflow?.phases[0];
618
625
  const firstAgent = firstPhase?.agents[0];
619
626
  if (!firstPhase || !firstAgent)
620
627
  return false;
@@ -622,7 +629,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
622
629
  kind: 'cockpit',
623
630
  selectedPhaseId: firstPhase.id,
624
631
  selectedId: firstAgent.viewId,
625
- focus: 'agents',
632
+ focus: workflows.length > 1 ? 'workflows' : 'agents',
626
633
  });
627
634
  return true;
628
635
  }, [setAgentSurface]);
@@ -632,6 +639,18 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
632
639
  const abortRef = useRef(null);
633
640
  /** Identity of the turn allowed to notify when it settles. */
634
641
  const activeTurnTokenRef = useRef(null);
642
+ /** A tool can reserve a switch; only the owning turn's finalizer may publish it. */
643
+ const pendingModelSwitchRef = useRef(null);
644
+ const [modelSwitchVersion, setModelSwitchVersion] = useState(0);
645
+ const cancelPendingModelSwitch = useCallback(() => {
646
+ const pending = pendingModelSwitchRef.current;
647
+ if (!pending)
648
+ return false;
649
+ pendingModelSwitchRef.current = null;
650
+ pending.controller.abort(new Error('The pending model switch was cancelled.'));
651
+ setModelSwitchVersion((version) => version + 1);
652
+ return true;
653
+ }, []);
635
654
  /** Exact live-input owner; absent before provider admission and after settlement. */
636
655
  const activeTurnInboxRef = useRef(null);
637
656
  /** A broken terminal notification is reported once, not after every turn. */
@@ -727,13 +746,14 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
727
746
  * hook never ran.
728
747
  */
729
748
  const closeAndExit = useCallback(() => {
749
+ cancelPendingModelSwitch();
730
750
  const closing = session?.close() ?? Promise.resolve();
731
751
  const bounded = Promise.race([
732
752
  closing,
733
753
  new Promise((resolve) => setTimeout(resolve, SESSION_CLOSE_ON_EXIT_MS).unref?.()),
734
754
  ]);
735
755
  void bounded.catch(() => undefined).finally(() => exit());
736
- }, [exit, session]);
756
+ }, [cancelPendingModelSwitch, exit, session]);
737
757
  const exitWithSummary = useCallback(() => {
738
758
  onExitSummary?.({
739
759
  ...(conversationMaterializedRef.current && scopeRef.current?.sessionId
@@ -837,6 +857,19 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
837
857
  settledRef.current = 0;
838
858
  setResetKey((k) => k + 1);
839
859
  }, [stdout.isTTY, writeStdout]);
860
+ const previousTerminalRef = useRef(terminal);
861
+ useEffect(() => {
862
+ const previous = previousTerminalRef.current;
863
+ previousTerminalRef.current = terminal;
864
+ if (externalEditorRequestRef.current === null &&
865
+ (terminal.columns < previous.columns || terminal.rows < previous.rows)) {
866
+ // A contraction can reflow live rows into native scrollback before Ink
867
+ // receives resize. Its old row count cannot erase those rows. Rebuild
868
+ // Namzu's normal-buffer view at the committed size, retaining the same
869
+ // messages, draft and selected child. Regular frames remain incremental.
870
+ resetTranscript();
871
+ }
872
+ }, [terminal, resetTranscript]);
840
873
  const requestExternalEditor = useCallback((seed) => {
841
874
  if (phase !== 'ready' ||
842
875
  state !== 'idle' ||
@@ -963,15 +996,19 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
963
996
  setPermissionModeState(mode);
964
997
  pushMessage('system', `Permissions: ${permissionModeLabel(mode)} for this session. ${permissionModeDescription(mode)}`);
965
998
  }, [hasUnsettledTurn, pushMessage, session, state]);
966
- const applyReasoningEffort = useCallback((effort) => {
999
+ const applyReasoningEffort = useCallback((effort, selectedSession) => {
967
1000
  if (!session?.hasProvider) {
968
1001
  pushMessage('system', 'No active session — pick a provider before changing reasoning effort.');
969
1002
  return;
970
1003
  }
1004
+ if (selectedSession !== undefined && selectedSession !== session) {
1005
+ pushMessage('system', 'Reasoning effort was not changed: the selected model is no longer active.');
1006
+ return;
1007
+ }
971
1008
  if (state !== 'idle' ||
972
1009
  abortRef.current !== null ||
973
1010
  hasUnsettledTurn() ||
974
- queuedRef.current.length > 0 ||
1011
+ (queuedRef.current.length > 0 && selectedSession !== session) ||
975
1012
  permissionResolveRef.current !== null ||
976
1013
  compactingRef.current) {
977
1014
  pushMessage('system', 'Reasoning effort was not changed: wait for the active turn, prompt, compaction, and queued work to settle.');
@@ -979,7 +1016,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
979
1016
  }
980
1017
  if (effort === undefined) {
981
1018
  setReasoningEffort(undefined);
982
- pushMessage('system', 'Reasoning effort reset to the provider default for future main-query turns.');
1019
+ pushMessage('system', 'Reasoning: provider default.');
983
1020
  return;
984
1021
  }
985
1022
  if (!session.reasoningEffortLevels?.includes(effort)) {
@@ -987,7 +1024,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
987
1024
  return;
988
1025
  }
989
1026
  setReasoningEffort(effort);
990
- pushMessage('system', `Reasoning effort changed to ${effort} for future main-query turns in this session.`);
1027
+ pushMessage('system', `Reasoning: ${effort} (this session).`);
991
1028
  }, [hasUnsettledTurn, pushMessage, session, setReasoningEffort, state]);
992
1029
  const stepReasoningEffort = useCallback((direction) => {
993
1030
  const levels = session?.reasoningEffortLevels;
@@ -1477,7 +1514,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1477
1514
  removeStoredCredential(value);
1478
1515
  return;
1479
1516
  }
1480
- applyReasoningEffort(value);
1517
+ applyReasoningEffort(value, picker.selectedSession);
1481
1518
  }, [
1482
1519
  activateSkill,
1483
1520
  advanceQueueContinuation,
@@ -1672,7 +1709,8 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1672
1709
  materializationRef.current = null;
1673
1710
  }
1674
1711
  }, [ensureSessions]);
1675
- const hydrateSession = useCallback(async (prefs, detectedNow, signal, persistSelection = false) => {
1712
+ const hydrateSession = useCallback(async (prefs, detectedNow, options = {}) => {
1713
+ const { signal, persistSelection = false, announce = false } = options;
1676
1714
  if (signal?.aborted)
1677
1715
  return;
1678
1716
  const scope = await ensureSessions();
@@ -1682,22 +1720,27 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1682
1720
  const s = await createAgentSession(prefs, detectedNow, {
1683
1721
  scope,
1684
1722
  cwd: activeCtx.cwd,
1723
+ ...(activeCtx.structuredOutput ? { structuredOutput: activeCtx.structuredOutput } : {}),
1685
1724
  ...(activeCtx.additionalDirectories
1686
1725
  ? { additionalDirectories: activeCtx.additionalDirectories }
1687
1726
  : {}),
1688
1727
  ...(sessionsRef.current ? { stateRoot: sessionsRef.current.root } : {}),
1689
1728
  enableComputerUse: true,
1690
1729
  rules: activeCtx.rules,
1691
- ...(sessionsRef.current ? { sessionGoals: sessionsRef.current.goals } : {}),
1730
+ ...(sessionsRef.current
1731
+ ? { sessionGoals: sessionsRef.current.goals, conversationSessions: sessionsRef.current }
1732
+ : {}),
1692
1733
  ...(activeCtx.mcpServers ? { mcpServers: activeCtx.mcpServers } : {}),
1693
1734
  ...(activeCtx.plugins ? { plugins: activeCtx.plugins } : {}),
1694
1735
  ...(activeCtx.web ? { web: activeCtx.web } : {}),
1695
1736
  ...(activeCtx.hooks ? { hooks: activeCtx.hooks } : {}),
1696
1737
  ...(activeCtx.compaction ? { compaction: activeCtx.compaction } : {}),
1738
+ ...(activeCtx.memory ? { memory: activeCtx.memory } : {}),
1697
1739
  ...(activeCtx.sandbox ? { sandbox: activeCtx.sandbox } : {}),
1698
1740
  // Somebody is at this terminal, so the model may ask them one
1699
1741
  // question when a decision is genuinely theirs.
1700
1742
  askUser: true,
1743
+ allowModelSwitch: true,
1701
1744
  });
1702
1745
  if (signal?.aborted) {
1703
1746
  void s.close();
@@ -1716,6 +1759,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1716
1759
  }
1717
1760
  let commands;
1718
1761
  try {
1762
+ options.beforePublish?.();
1719
1763
  commands = discoverUserCommands({ cwd: activeCtx.cwd, reserved: hostCommandNames() });
1720
1764
  // Construction and validation precede the saved change. There is no
1721
1765
  // await between this atomic write and publishing the usable session.
@@ -1744,6 +1788,12 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1744
1788
  void previousSessionRef.current?.close();
1745
1789
  previousSessionRef.current = s;
1746
1790
  setSession(s);
1791
+ if (options.chooseReasoningEffort && s.reasoningEffortLevels?.length) {
1792
+ // Own input before publishing ready or releasing a paused queue. The
1793
+ // menu closes only after choosing an effort or keeping the new default.
1794
+ setSelectedChoice(0);
1795
+ setChoicePicker(reasoningEffortPicker(s, undefined, true) ?? null);
1796
+ }
1747
1797
  setUserCommands(commands);
1748
1798
  const mentionLoadOwner = {};
1749
1799
  mentionLoadOwnerRef.current = mentionLoadOwner;
@@ -1760,13 +1810,11 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1760
1810
  if (s.hasProvider) {
1761
1811
  setTranscriptOwned(true);
1762
1812
  setPhase('ready');
1763
- pushMessage('system',
1764
- // Counted here, at connect. The number is deliberately the one true
1765
- // at this moment rather than the one that will be true after the
1766
- // first turn registers the deferred tools — a line reporting that a
1767
- // connection just happened should describe the connection that just
1768
- // happened. `/tools` is the present-tense question and asks later.
1769
- `Connected to ${s.providerSummary}${s.modelSummary ? ` · ${s.modelSummary}` : ''} · ${s.toolNames().length} tools`);
1813
+ // Startup identity already lives in the footer. Confirm an explicit
1814
+ // picker selection, but do not add a routine connection log to chat.
1815
+ if (announce) {
1816
+ pushMessage('system', `Connected to ${s.providerSummary}${s.modelSummary ? ` · ${s.modelSummary}` : ''}`);
1817
+ }
1770
1818
  // Before the rest: a limitation the operator accepted once and has
1771
1819
  // been living with since is the thing they are least likely to
1772
1820
  // remember and most likely to be surprised by.
@@ -1799,7 +1847,15 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1799
1847
  if (s.errorHint)
1800
1848
  pushMessage('system', s.errorHint);
1801
1849
  }
1802
- }, [appLifetime.signal, advanceQueueContinuation, ensureSessions, pushMessage, setReasoningEffort]);
1850
+ }, [
1851
+ appLifetime.signal,
1852
+ advanceQueueContinuation,
1853
+ ensureSessions,
1854
+ pushMessage,
1855
+ setChoicePicker,
1856
+ setReasoningEffort,
1857
+ setSelectedChoice,
1858
+ ]);
1803
1859
  /**
1804
1860
  * Sign in to a subscription without leaving namzu.
1805
1861
  *
@@ -1835,7 +1891,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1835
1891
  pushMessage('system', describeLoginOutcome(outcome));
1836
1892
  if (outcome.ok) {
1837
1893
  setPickerInitialView('providers');
1838
- await runProbeRef.current?.();
1894
+ await runProbeRef.current?.({ announce: true });
1839
1895
  }
1840
1896
  return;
1841
1897
  }
@@ -1894,7 +1950,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1894
1950
  setPickerNotice(describeLoginOutcome(outcome));
1895
1951
  if (outcome.ok) {
1896
1952
  setPickerInitialView('providers');
1897
- await runProbeRef.current?.(signal);
1953
+ await runProbeRef.current?.({ signal, announce: true });
1898
1954
  }
1899
1955
  return 'finished';
1900
1956
  }
@@ -1954,11 +2010,12 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1954
2010
  setPickerNotice(describeLoginOutcome(outcome));
1955
2011
  if (outcome.ok) {
1956
2012
  setPickerInitialView('providers');
1957
- await runProbeRef.current?.(signal);
2013
+ await runProbeRef.current?.({ signal, announce: true });
1958
2014
  }
1959
2015
  return 'finished';
1960
2016
  }, []);
1961
- const runProbe = useCallback(async (signal) => {
2017
+ const runProbe = useCallback(async (options = {}) => {
2018
+ const { signal, announce = false } = options;
1962
2019
  try {
1963
2020
  if (signal?.aborted)
1964
2021
  return;
@@ -2011,7 +2068,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2011
2068
  if (probe.preferences) {
2012
2069
  setPickerDetected(null);
2013
2070
  setPickerSelectionKind('provider-and-model');
2014
- await hydrateSession(probe.preferences, probe.detected, signal);
2071
+ await hydrateSession(probe.preferences, probe.detected, { signal, announce });
2015
2072
  return;
2016
2073
  }
2017
2074
  const signedIn = signedInSubscriptionProviders(probe.detected);
@@ -2029,7 +2086,10 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2029
2086
  // not strand first run on the disabled unhealthy screen.
2030
2087
  const admissionSignal = signal ?? appLifetime.signal;
2031
2088
  try {
2032
- await hydrateSession(automaticPreferences, probe.detected, admissionSignal);
2089
+ await hydrateSession(automaticPreferences, probe.detected, {
2090
+ signal: admissionSignal,
2091
+ announce,
2092
+ });
2033
2093
  return;
2034
2094
  }
2035
2095
  catch (error) {
@@ -2264,11 +2324,15 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2264
2324
  * sends at that prompt, for the same reason.
2265
2325
  */
2266
2326
  const interruptTurn = useCallback(() => {
2327
+ const cancelledSwitch = cancelPendingModelSwitch();
2267
2328
  if (permissionResolveRef.current)
2268
2329
  resolvePermission({ kind: 'reject', feedback: 'User interrupted.' });
2269
2330
  const ac = abortRef.current;
2270
- if (!ac)
2271
- return false;
2331
+ if (!ac) {
2332
+ if (cancelledSwitch)
2333
+ discardQueued();
2334
+ return cancelledSwitch;
2335
+ }
2272
2336
  ac.abort(new RunCancelled('user'));
2273
2337
  const activeSessionId = scopeRef.current?.sessionId;
2274
2338
  if (activeSessionId)
@@ -2284,7 +2348,14 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2284
2348
  clearActiveTools();
2285
2349
  setState('idle');
2286
2350
  return true;
2287
- }, [clearActiveTools, discardQueued, goalActivation, resolvePermission, wakeGoalDriver]);
2351
+ }, [
2352
+ cancelPendingModelSwitch,
2353
+ clearActiveTools,
2354
+ discardQueued,
2355
+ goalActivation,
2356
+ resolvePermission,
2357
+ wakeGoalDriver,
2358
+ ]);
2288
2359
  /**
2289
2360
  * Load the chosen conversation into the transcript and continue in it.
2290
2361
  *
@@ -2357,6 +2428,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2357
2428
  discardQueued();
2358
2429
  goalActivation.clear();
2359
2430
  wakeGoalDriver();
2431
+ cancelPendingModelSwitch();
2360
2432
  conversationGenRef.current += 1;
2361
2433
  resetSubagentActivity();
2362
2434
  session?.resetTaskStore?.();
@@ -2393,6 +2465,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2393
2465
  interruptTurn,
2394
2466
  nextId,
2395
2467
  pushMessage,
2468
+ cancelPendingModelSwitch,
2396
2469
  resetSubagentActivity,
2397
2470
  session,
2398
2471
  resetTranscript,
@@ -2433,6 +2506,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2433
2506
  discardQueued();
2434
2507
  goalActivation.clear();
2435
2508
  wakeGoalDriver();
2509
+ cancelPendingModelSwitch();
2436
2510
  conversationGenRef.current += 1;
2437
2511
  resetSubagentActivity();
2438
2512
  session?.resetTaskStore?.();
@@ -2474,6 +2548,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2474
2548
  goalActivation,
2475
2549
  interruptTurn,
2476
2550
  pushMessage,
2551
+ cancelPendingModelSwitch,
2477
2552
  resetSubagentActivity,
2478
2553
  session,
2479
2554
  resetTranscript,
@@ -2722,6 +2797,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2722
2797
  const readablePrefix = editList
2723
2798
  .slice(0, target.userOrdinal)
2724
2799
  .map((prompt) => prompt.displayText);
2800
+ cancelPendingModelSwitch();
2725
2801
  conversationGenRef.current += 1;
2726
2802
  resetSubagentActivity();
2727
2803
  session?.resetTaskStore?.();
@@ -2765,6 +2841,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2765
2841
  hasUnsettledTurn,
2766
2842
  nextId,
2767
2843
  pushMessage,
2844
+ cancelPendingModelSwitch,
2768
2845
  resetSubagentActivity,
2769
2846
  session,
2770
2847
  resetTranscript,
@@ -3066,12 +3143,12 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3066
3143
  pushMessage('system', event.text, false, '⇄');
3067
3144
  break;
3068
3145
  case 'capability-warning':
3069
- pushMessage('system', `Capability warning (${event.capability}${event.contentSource === 'tool-result' ? ' tool result' : ''}): ${event.text}`, false, '⚠');
3146
+ pushMessage('system', `Capability warning (${event.capability}${event.contentSource === 'tool-result' ? ' tool result' : ''}): ${event.text}`, false, '!');
3070
3147
  break;
3071
3148
  case 'history-repair':
3072
- pushMessage('system', `History warning (${event.source}): ${event.text}`, false, '⚠');
3149
+ pushMessage('system', `History warning (${event.source}): ${event.text}`, false, '!');
3073
3150
  break;
3074
- case 'done':
3151
+ case 'done': {
3075
3152
  // `run_completed` is not synonymous with success: budgets,
3076
3153
  // cancellation and output guardrails arrive through this event too.
3077
3154
  // Missing remains a normal end for older producers, matching the
@@ -3084,7 +3161,11 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3084
3161
  outcome: st.completed ? 'completed' : 'stopped',
3085
3162
  };
3086
3163
  closeAssistant();
3164
+ const stopNotice = describeRunStop(event.stopReason);
3165
+ if (stopNotice)
3166
+ pushMessage('system', stopNotice, false, '■');
3087
3167
  break;
3168
+ }
3088
3169
  case 'paused':
3089
3170
  // Paused is a recoverable SDK verdict, not success and not failure.
3090
3171
  // Conversation evidence has no paused member, so it records the
@@ -3101,7 +3182,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3101
3182
  st.outcome = event.message === 'aborted' ? 'cancelled' : 'failed';
3102
3183
  if (event.message !== 'aborted') {
3103
3184
  st.notification = { kind: 'turn-settled', outcome: 'failed' };
3104
- pushMessage('system', describeRunInterruption(event), false, '⚠');
3185
+ pushMessage('system', describeRunInterruption(event), false, '!');
3105
3186
  }
3106
3187
  else
3107
3188
  st.notification = null;
@@ -3109,6 +3190,14 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3109
3190
  }
3110
3191
  }, [appendToMessage, finalizeMessage, flushStream, pushMessage]);
3111
3192
  const runTurn = useCallback(async (prompt) => {
3193
+ // A passive queue effect can still hold the previous render's session
3194
+ // while a replacement is being committed. Keep FIFO ownership until
3195
+ // the render and the resource-owning session agree.
3196
+ const pendingAtAdmission = pendingModelSwitchRef.current;
3197
+ if (pendingAtAdmission || previousSessionRef.current !== session) {
3198
+ replaceQueued([prompt, ...queuedRef.current]);
3199
+ return;
3200
+ }
3112
3201
  if (!session || !session.hasProvider) {
3113
3202
  pushMessage('system', session?.errorHint ?? 'Agent is not ready yet — give it a moment.');
3114
3203
  return;
@@ -3232,6 +3321,72 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3232
3321
  const turnGeneration = conversationGenRef.current;
3233
3322
  unsettledTurnGenerationsRef.current.set(turnToken, turnGeneration);
3234
3323
  const stillHere = () => conversationGenRef.current === turnGeneration;
3324
+ const requestModelSwitch = async (request, requestSignal) => {
3325
+ if (!currentProvider ||
3326
+ !stillHere() ||
3327
+ ac.signal.aborted ||
3328
+ requestSignal?.aborted ||
3329
+ appLifetime.signal.aborted ||
3330
+ activeTurnTokenRef.current !== turnToken ||
3331
+ previousSessionRef.current !== session) {
3332
+ return { kind: 'rejected', reason: 'The requesting turn is no longer active.' };
3333
+ }
3334
+ const accepted = pendingModelSwitchRef.current;
3335
+ if (accepted?.turnToken === turnToken &&
3336
+ !accepted.signal.aborted &&
3337
+ accepted.selection?.model === request.model.trim() &&
3338
+ (!request.provider || accepted.selection.id === request.provider.trim())) {
3339
+ return { kind: 'pending', selection: accepted.selection };
3340
+ }
3341
+ // A later request supersedes an earlier lookup even if its model list
3342
+ // resolves first. Cancellation never changes the active provider.
3343
+ cancelPendingModelSwitch();
3344
+ const controller = new AbortController();
3345
+ const pending = {
3346
+ turnToken,
3347
+ generation: turnGeneration,
3348
+ sessionId: scopeRef.current?.sessionId,
3349
+ ownerSession: session,
3350
+ controller,
3351
+ // A tool's lifetime can end before the turn. Once accepted, the
3352
+ // switch belongs to the turn and host, not that completed tool call.
3353
+ signal: AbortSignal.any([ac.signal, appLifetime.signal, controller.signal]),
3354
+ };
3355
+ pendingModelSwitchRef.current = pending;
3356
+ setModelSwitchVersion((version) => version + 1);
3357
+ try {
3358
+ const result = await resolveModelSwitch(request, {
3359
+ currentProvider,
3360
+ detected,
3361
+ describeModels: describeProviderModels,
3362
+ signal: requestSignal ? AbortSignal.any([pending.signal, requestSignal]) : pending.signal,
3363
+ });
3364
+ if (pendingModelSwitchRef.current !== pending ||
3365
+ pending.signal.aborted ||
3366
+ requestSignal?.aborted ||
3367
+ !stillHere() ||
3368
+ activeTurnTokenRef.current !== turnToken) {
3369
+ return { kind: 'rejected', reason: 'The model switch was cancelled or superseded.' };
3370
+ }
3371
+ if (result.kind === 'rejected')
3372
+ return result;
3373
+ pending.selection = result.selection;
3374
+ return { kind: 'pending', selection: result.selection };
3375
+ }
3376
+ catch (error) {
3377
+ return {
3378
+ kind: 'rejected',
3379
+ reason: pending.signal.aborted
3380
+ ? 'The model switch was cancelled.'
3381
+ : `Could not resolve the requested model: ${error instanceof Error ? error.message : String(error)}`,
3382
+ };
3383
+ }
3384
+ finally {
3385
+ if (!pending.selection && pendingModelSwitchRef.current === pending) {
3386
+ cancelPendingModelSwitch();
3387
+ }
3388
+ }
3389
+ };
3235
3390
  let inboxOpen = true;
3236
3391
  const inboxEntries = [];
3237
3392
  const inputWaiters = new Set();
@@ -3368,6 +3523,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3368
3523
  // The mode above decides whether this callback is consulted.
3369
3524
  onPermission: askPermission,
3370
3525
  onQuestion: askQuestion,
3526
+ onModelSwitch: requestModelSwitch,
3371
3527
  inboundMessages: () => inbox.drain(),
3372
3528
  waitForInbound: (signal) => inbox.waitForInbound(signal),
3373
3529
  extraSystem: [composeSkillsPrompt(activeSkills), drainIdleJobNotices(), drainOperatorShell()]
@@ -3549,15 +3705,76 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3549
3705
  // operation that sees no current-generation entries can now await that
3550
3706
  // tail without a turn appearing behind its read later.
3551
3707
  unsettledTurnGenerationsRef.current.delete(turnToken);
3708
+ const pending = pendingModelSwitchRef.current;
3709
+ if (pending?.turnToken === turnToken && pending.selection) {
3710
+ const selection = pending.selection;
3711
+ const ownsSwitch = () => pendingModelSwitchRef.current === pending &&
3712
+ conversationGenRef.current === pending.generation &&
3713
+ scopeRef.current?.sessionId === pending.sessionId &&
3714
+ previousSessionRef.current === pending.ownerSession &&
3715
+ !pending.signal.aborted;
3716
+ const beforePublish = () => {
3717
+ if (!ownsSwitch() ||
3718
+ conversationMutationRef.current ||
3719
+ hasUnsettledTurn(pending.generation) ||
3720
+ abortRef.current ||
3721
+ compactingRef.current ||
3722
+ exportingRef.current) {
3723
+ throw new Error('The active conversation changed before the model switch could complete.');
3724
+ }
3725
+ if (pending.ownerSession.subagents
3726
+ ?.getSnapshot()
3727
+ .some((agent) => agent.status === 'starting' ||
3728
+ agent.status === 'queued' ||
3729
+ agent.status === 'working')) {
3730
+ throw new Error('Delegated agents are still active. Wait for them to finish before switching models.');
3731
+ }
3732
+ if (pending.ownerSession.jobs?.().some((job) => job.status === 'running')) {
3733
+ throw new Error('Background jobs are still running. Wait for them to finish before switching models.');
3734
+ }
3735
+ };
3736
+ try {
3737
+ await persistenceTailRef.current;
3738
+ beforePublish();
3739
+ await hydrateSession(selectPrimaryProvider(savedPrefsRef.current, selection), detected, {
3740
+ signal: pending.signal,
3741
+ persistSelection: false,
3742
+ beforePublish,
3743
+ });
3744
+ if (pendingModelSwitchRef.current === pending &&
3745
+ !pending.signal.aborted &&
3746
+ previousSessionRef.current !== pending.ownerSession) {
3747
+ pushMessage('system', `Switched to ${selection.id} · ${selection.model} for this conversation.`);
3748
+ }
3749
+ }
3750
+ catch (error) {
3751
+ if (ownsSwitch()) {
3752
+ pushMessage('system', `Model switch failed: ${error instanceof Error ? error.message : String(error)} The current model is unchanged.`);
3753
+ }
3754
+ }
3755
+ finally {
3756
+ if (pendingModelSwitchRef.current === pending) {
3757
+ pendingModelSwitchRef.current = null;
3758
+ setModelSwitchVersion((version) => version + 1);
3759
+ wakeGoalDriver();
3760
+ }
3761
+ }
3762
+ }
3552
3763
  }
3553
3764
  }, [
3554
3765
  activeSkills,
3766
+ appLifetime.signal,
3555
3767
  applyEvent,
3768
+ cancelPendingModelSwitch,
3556
3769
  clearActiveTools,
3770
+ currentProvider,
3557
3771
  ctx.cwd,
3772
+ detected,
3558
3773
  finalizeMessage,
3559
3774
  flushStream,
3560
3775
  goalActivation,
3776
+ hasUnsettledTurn,
3777
+ hydrateSession,
3561
3778
  materializeConversation,
3562
3779
  onPermission,
3563
3780
  pushMessage,
@@ -3567,6 +3784,109 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3567
3784
  setQueuePause,
3568
3785
  wakeGoalDriver,
3569
3786
  ]);
3787
+ const selectModelIntent = useCallback(async (query) => {
3788
+ if (!session ||
3789
+ !currentProvider ||
3790
+ state !== 'idle' ||
3791
+ phase !== 'ready' ||
3792
+ abortRef.current ||
3793
+ hasUnsettledTurn() ||
3794
+ pendingModelSwitchRef.current ||
3795
+ queuedRef.current.length > 0 ||
3796
+ permissionResolveRef.current ||
3797
+ compactingRef.current ||
3798
+ conversationMutationRef.current ||
3799
+ exportingRef.current ||
3800
+ goalDriveInFlightRef.current) {
3801
+ pushMessage('system', 'Model selection is unavailable until the active work and queue reach a stable boundary.');
3802
+ return;
3803
+ }
3804
+ const controller = new AbortController();
3805
+ const pending = {
3806
+ turnToken: {},
3807
+ generation: conversationGenRef.current,
3808
+ sessionId: scopeRef.current?.sessionId,
3809
+ ownerSession: session,
3810
+ controller,
3811
+ signal: AbortSignal.any([appLifetime.signal, controller.signal]),
3812
+ };
3813
+ pendingModelSwitchRef.current = pending;
3814
+ setModelSwitchVersion((version) => version + 1);
3815
+ const owns = () => pendingModelSwitchRef.current === pending &&
3816
+ !pending.signal.aborted &&
3817
+ conversationGenRef.current === pending.generation &&
3818
+ scopeRef.current?.sessionId === pending.sessionId &&
3819
+ previousSessionRef.current === session;
3820
+ const beforePublish = () => {
3821
+ if (!owns() ||
3822
+ abortRef.current ||
3823
+ hasUnsettledTurn() ||
3824
+ conversationMutationRef.current ||
3825
+ compactingRef.current ||
3826
+ exportingRef.current ||
3827
+ permissionResolveRef.current ||
3828
+ session.jobs?.().some((job) => job.status === 'running') ||
3829
+ session.subagents
3830
+ ?.getSnapshot()
3831
+ .some((agent) => ['starting', 'queued', 'working'].includes(agent.status))) {
3832
+ throw new Error('Active work or the conversation changed before model selection could complete.');
3833
+ }
3834
+ };
3835
+ try {
3836
+ beforePublish();
3837
+ pushMessage('system', `Model requested: ${query}. Checking available catalogues…`);
3838
+ const resolved = await resolveModelSelectionIntent(query, {
3839
+ currentProvider,
3840
+ detected,
3841
+ describeModels: describeProviderModels,
3842
+ signal: pending.signal,
3843
+ });
3844
+ if (!owns())
3845
+ return;
3846
+ if (resolved.kind === 'rejected') {
3847
+ pushMessage('system', resolved.reason +
3848
+ (resolved.choices?.length
3849
+ ? ` Choices: ${resolved.choices.map((choice) => `${choice.provider}/${choice.model}`).join(', ')}.`
3850
+ : ''));
3851
+ return;
3852
+ }
3853
+ pending.selection = resolved.selection;
3854
+ await persistenceTailRef.current;
3855
+ beforePublish();
3856
+ await hydrateSession(selectPrimaryProvider(savedPrefsRef.current, resolved.selection), detected, {
3857
+ signal: pending.signal,
3858
+ persistSelection: false,
3859
+ beforePublish,
3860
+ });
3861
+ if (pendingModelSwitchRef.current === pending &&
3862
+ !pending.signal.aborted &&
3863
+ previousSessionRef.current !== session) {
3864
+ pushMessage('system', `Switched to ${resolved.selection.id} · ${resolved.selection.model} for this conversation.`);
3865
+ }
3866
+ }
3867
+ catch (error) {
3868
+ if (owns())
3869
+ pushMessage('system', `Model selection failed: ${error instanceof Error ? error.message : String(error)} The current model is unchanged.`);
3870
+ }
3871
+ finally {
3872
+ if (pendingModelSwitchRef.current === pending) {
3873
+ pendingModelSwitchRef.current = null;
3874
+ setModelSwitchVersion((version) => version + 1);
3875
+ wakeGoalDriver();
3876
+ }
3877
+ }
3878
+ }, [
3879
+ session,
3880
+ currentProvider,
3881
+ state,
3882
+ phase,
3883
+ hasUnsettledTurn,
3884
+ appLifetime.signal,
3885
+ pushMessage,
3886
+ detected,
3887
+ hydrateSession,
3888
+ wakeGoalDriver,
3889
+ ]);
3570
3890
  const handleSubmit = useCallback((value, attachments, mode = 'submit') => {
3571
3891
  if (goalCommandInFlightRef.current) {
3572
3892
  pushMessage('system', 'A goal command is still reaching durable session state. Wait for its result before sending another command or prompt.');
@@ -3590,14 +3910,19 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3590
3910
  return;
3591
3911
  }
3592
3912
  setHistory((prev) => [...prev, value]);
3913
+ const selectionIntent = !attachments?.length ? parseModelSelectionIntent(value) : undefined;
3914
+ if (selectionIntent) {
3915
+ void selectModelIntent(selectionIntent.query);
3916
+ return;
3917
+ }
3593
3918
  // `#` remembers, `!` runs — neither is a prompt. Both are the
3594
3919
  // operator acting directly, the way other coding agents spell it,
3595
3920
  // and both leave a row the model reads on its next turn.
3596
3921
  if (value.startsWith('#') && value.slice(1).trim().length > 0) {
3597
3922
  const note = value.slice(1).trim();
3598
3923
  try {
3599
- const path = appendMemory(note, { scope: 'project', cwd: ctx.cwd });
3600
- pushMessage('system', `Remembered for this project (${path}): ${note}`);
3924
+ const saved = appendMemoryWithStatus(note, { scope: 'project', cwd: ctx.cwd });
3925
+ pushMessage('system', renderMemorySaveResult(saved, note));
3601
3926
  }
3602
3927
  catch (err) {
3603
3928
  pushMessage('system', `Could not save memory: ${err instanceof Error ? err.message : String(err)}`);
@@ -3866,6 +4191,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3866
4191
  return;
3867
4192
  case 'repick':
3868
4193
  if (state !== 'idle' ||
4194
+ pendingModelSwitchRef.current ||
3869
4195
  abortRef.current !== null ||
3870
4196
  hasUnsettledTurn() ||
3871
4197
  (queuedRef.current.length > 0 && queuePauseRef.current === null) ||
@@ -3934,17 +4260,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3934
4260
  const values = [undefined, ...levels];
3935
4261
  const current = reasoningEffortRef.current;
3936
4262
  setSelectedChoice(Math.max(0, values.findIndex((value) => value === current)));
3937
- setChoicePicker({
3938
- kind: 'reasoning-effort',
3939
- title: `Select Reasoning Level for ${session.modelSummary ?? 'current model'}`,
3940
- values,
3941
- options: values.map((effort) => ({
3942
- label: effort ?? 'default',
3943
- description: reasoningEffortDescription(effort),
3944
- current: effort === current,
3945
- default: effort === undefined,
3946
- })),
3947
- });
4263
+ setChoicePicker(reasoningEffortPicker(session, current) ?? null);
3948
4264
  return;
3949
4265
  }
3950
4266
  case 'login':
@@ -3984,8 +4300,8 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3984
4300
  }
3985
4301
  case 'remember':
3986
4302
  try {
3987
- const path = appendMemory(slash.text, { scope: slash.scope, cwd: ctx.cwd });
3988
- pushMessage('system', `Remembered ${slash.scope === 'user' ? 'for every project' : 'for this project'} (${path}): ${slash.text}`);
4303
+ const saved = appendMemoryWithStatus(slash.text, { scope: slash.scope, cwd: ctx.cwd });
4304
+ pushMessage('system', renderMemorySaveResult(saved, slash.text));
3989
4305
  }
3990
4306
  catch (err) {
3991
4307
  pushMessage('system', `Could not save memory: ${err instanceof Error ? err.message : String(err)}`);
@@ -4011,7 +4327,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4011
4327
  // Dropping it silently would leave someone wondering where a
4012
4328
  // file they can see on disk went.
4013
4329
  s.problem
4014
- ? `⚠ ${s.name} — ${s.problem}`
4330
+ ? `! ${s.name} — ${s.problem}`
4015
4331
  : `${activeNames.has(s.name) ? '● ' : '○ '}${s.name} — ${s.description}`);
4016
4332
  pushMessage('system', `Skills (● active):\n ${lines.join('\n ')}`);
4017
4333
  return;
@@ -4425,6 +4741,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4425
4741
  resetTranscript,
4426
4742
  runConversationExport,
4427
4743
  session,
4744
+ selectModelIntent,
4428
4745
  setChoicePicker,
4429
4746
  setCopyPicker,
4430
4747
  setReasoningEffort,
@@ -4488,6 +4805,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4488
4805
  queuedRef.current.length > 0 ||
4489
4806
  abortRef.current ||
4490
4807
  hasUnsettledTurn() ||
4808
+ pendingModelSwitchRef.current ||
4491
4809
  conversationMutationRef.current ||
4492
4810
  exportingRef.current ||
4493
4811
  compactingRef.current ||
@@ -4515,6 +4833,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4515
4833
  state !== 'idle' ||
4516
4834
  abortRef.current ||
4517
4835
  hasUnsettledTurn(generation) ||
4836
+ pendingModelSwitchRef.current ||
4518
4837
  conversationMutationRef.current ||
4519
4838
  exportingRef.current ||
4520
4839
  compactingRef.current ||
@@ -4540,6 +4859,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4540
4859
  // or goal command is different: it revoked this scheduling boundary.
4541
4860
  if (conversationGenRef.current !== generation ||
4542
4861
  scopeRef.current?.sessionId !== sessionId ||
4862
+ pendingModelSwitchRef.current ||
4543
4863
  conversationMutationRef.current ||
4544
4864
  goalCommandInFlightRef.current ||
4545
4865
  !goalActivation.isArmed(sessionId, armed)) {
@@ -4572,6 +4892,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4572
4892
  enqueueQueued,
4573
4893
  goalActivation,
4574
4894
  goalDriveVersion,
4895
+ modelSwitchVersion,
4575
4896
  hasUnsettledTurn,
4576
4897
  phase,
4577
4898
  pushMessage,
@@ -4592,6 +4913,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4592
4913
  queuePauseRef.current !== null ||
4593
4914
  abortRef.current ||
4594
4915
  hasUnsettledTurn() ||
4916
+ pendingModelSwitchRef.current ||
4595
4917
  conversationMutationRef.current ||
4596
4918
  exportingRef.current ||
4597
4919
  compactingRef.current ||
@@ -4611,6 +4933,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4611
4933
  state,
4612
4934
  phase,
4613
4935
  queued,
4936
+ modelSwitchVersion,
4614
4937
  queuePause,
4615
4938
  textPrompt,
4616
4939
  choicePicker,
@@ -4635,7 +4958,12 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4635
4958
  }, [phase, ctx.version, pushMessage]);
4636
4959
  const hydrateFromPicker = useCallback(async (prefs, detectedNow, signal, options = {}) => {
4637
4960
  try {
4638
- await hydrateSession(prefs, detectedNow, signal, options.persistSelection);
4961
+ await hydrateSession(prefs, detectedNow, {
4962
+ signal,
4963
+ persistSelection: options.persistSelection,
4964
+ announce: true,
4965
+ chooseReasoningEffort: options.chooseReasoningEffort,
4966
+ });
4639
4967
  }
4640
4968
  catch (err) {
4641
4969
  // A superseded choice no longer owns even its failure message. Its
@@ -4687,6 +5015,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4687
5015
  // Only a real refusal broadens back to the general, sign-in-capable picker.
4688
5016
  void hydrateFromPicker(prefs, detected, signal, {
4689
5017
  revealAllOnFailure: true,
5018
+ chooseReasoningEffort: selection.model !== undefined,
4690
5019
  persistSelection: detected.find((provider) => provider.entry.id === selection.provider)?.source.kind !==
4691
5020
  'session',
4692
5021
  });
@@ -4905,7 +5234,10 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4905
5234
  return;
4906
5235
  const agents = subagentsRef.current;
4907
5236
  if (agentView.kind === 'cockpit') {
4908
- const phases = agentPhases(agents);
5237
+ const workflows = agentWorkflows(agents);
5238
+ const workflowIndex = Math.max(0, workflows.findIndex((workflow) => workflow.phases.some((phase) => phase.id === agentView.selectedPhaseId)));
5239
+ const workflow = workflows[workflowIndex];
5240
+ const phases = workflow?.phases ?? [];
4909
5241
  const phaseIndex = Math.max(0, phases.findIndex((phase) => phase.id === agentView.selectedPhaseId));
4910
5242
  const phase = phases[phaseIndex];
4911
5243
  const phaseAgents = phase?.agents ?? [];
@@ -4915,11 +5247,27 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4915
5247
  setAgentSurface(null);
4916
5248
  return;
4917
5249
  }
4918
- if (key.escape || (key.ctrl && input === 'c') || input.toLowerCase() === 'q') {
5250
+ if ((key.ctrl && input === 'c') || input.toLowerCase() === 'q') {
4919
5251
  setAgentSurface(null);
4920
5252
  return;
4921
5253
  }
5254
+ if (key.escape) {
5255
+ setAgentSurface(workflows.length > 1 && agentView.focus !== 'workflows'
5256
+ ? { ...agentView, focus: 'workflows' }
5257
+ : null);
5258
+ return;
5259
+ }
4922
5260
  if (key.leftArrow || key.rightArrow || key.tab) {
5261
+ if (agentView.focus === 'workflows') {
5262
+ if (!key.leftArrow) {
5263
+ setAgentSurface({ ...agentView, focus: phases.length > 1 ? 'phases' : 'agents' });
5264
+ }
5265
+ return;
5266
+ }
5267
+ if (key.leftArrow && agentView.focus === 'phases' && workflows.length > 1) {
5268
+ setAgentSurface({ ...agentView, focus: 'workflows' });
5269
+ return;
5270
+ }
4923
5271
  const focus = key.leftArrow
4924
5272
  ? 'phases'
4925
5273
  : key.rightArrow
@@ -4931,6 +5279,10 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4931
5279
  return;
4932
5280
  }
4933
5281
  if (key.return) {
5282
+ if (agentView.focus === 'workflows') {
5283
+ setAgentSurface({ ...agentView, focus: phases.length > 1 ? 'phases' : 'agents' });
5284
+ return;
5285
+ }
4934
5286
  if (agentView.focus === 'phases') {
4935
5287
  setAgentSurface({ ...agentView, focus: 'agents' });
4936
5288
  return;
@@ -4956,7 +5308,19 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4956
5308
  : key.upArrow
4957
5309
  ? 'previous'
4958
5310
  : 'next';
4959
- if (agentView.focus === 'phases') {
5311
+ if (agentView.focus === 'workflows') {
5312
+ const next = moveSelection(workflowIndex, workflows.length, movement, agentWorkflowPageSize(terminal.rows));
5313
+ const targetPhase = workflows[next]?.phases[0];
5314
+ const targetAgent = targetPhase?.agents[0];
5315
+ if (targetPhase && targetAgent) {
5316
+ setAgentSurface({
5317
+ ...agentView,
5318
+ selectedPhaseId: targetPhase.id,
5319
+ selectedId: targetAgent.viewId,
5320
+ });
5321
+ }
5322
+ }
5323
+ else if (agentView.focus === 'phases') {
4960
5324
  const next = moveSelection(phaseIndex, phases.length, movement, agentPhasePageSize(terminal.rows, agentCockpitIsWide(terminal.columns)));
4961
5325
  const targetPhase = phases[next];
4962
5326
  const targetAgent = targetPhase?.agents[0];
@@ -5139,7 +5503,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
5139
5503
  }
5140
5504
  // Esc interrupts a running turn (Ctrl+C stays reserved for exit). Mirrors
5141
5505
  // the Ctrl+C interrupt path: abort, drop the queue, one "Interrupted." line.
5142
- if (key.escape && abortRef.current) {
5506
+ if (key.escape && (abortRef.current || pendingModelSwitchRef.current)) {
5143
5507
  interruptTurn();
5144
5508
  pushMessage('system', 'Interrupted.');
5145
5509
  return;
@@ -5275,9 +5639,11 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
5275
5639
  : permission
5276
5640
  ? hintForPhase(phase, state, session?.hasProvider === true)
5277
5641
  : agentSurface?.kind === 'cockpit'
5278
- ? agentSurface.focus === 'phases'
5279
- ? 'agent phases — enter agents · esc return'
5280
- : 'agents — enter inspect · left phases · esc return'
5642
+ ? agentSurface.focus === 'workflows'
5643
+ ? 'workflows — enter select · esc return'
5644
+ : agentSurface.focus === 'phases'
5645
+ ? 'agent phases — enter agents · esc return'
5646
+ : 'agents — enter inspect · left phases · esc return'
5281
5647
  : agentSurface?.kind === 'transcript'
5282
5648
  ? 'observing agent — esc agents · q parent'
5283
5649
  : textPrompt
@@ -5292,7 +5658,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
5292
5658
  ? undefined
5293
5659
  : hintForPhase(phase, state, session?.hasProvider === true, composerHasDraft);
5294
5660
  const displayedPermissionMode = effectivePermissionMode(permissionMode, session?.approvalLatched() ?? false);
5295
- 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, provider: session?.providerSummary, model: session?.modelSummary, permissionMode: displayedPermissionMode, cwd: ctx.cwd })) : undefined }) }), phase === 'trust' ? (_jsx(TrustPrompt, { cwd: ctx.cwd })) : phase === 'unhealthy' ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: theme.status.error, bold: true, children: "Startup stopped" }), _jsx(Text, { color: theme.text.secondary, children: "Resolve the error above, then restart Namzu." })] })) : phase === 'resume' ? (_jsx(ResumePicker, { conversations: resumeList, selected: selectedResume })) : phase === 'edit' ? (_jsx(EditPromptPicker, { prompts: editList, selected: selectedEdit })) : phase === 'picker' ? (_jsx(Picker, { detected: pickerDetected ?? detected, currentProvider: currentProvider, selectionKind: pickerSelectionKind, currentModel: session?.modelSummary ?? null, initialView: pickerInitialView, onSubmit: handlePickerSubmit, onCancel: handlePickerCancel, onCredential: handleTypedCredential, onLogin: startLoginFromPicker, onLoginComplete: finishLoginFromPicker, keyEntryFor: keyEntryFor, notice: pickerNotice })) : (_jsxs(_Fragment, { children: [agentSurface === null ? (_jsx(LiveActivity, { compact: compactWork, activeTools: visibleActiveTools, working: state === 'thinking' || state === 'tool', interruptible: abortRef.current !== null, animate: stdout.isTTY === true, thinking: thinking })) : null, agentSurface === null && permission === null ? (_jsx(TaskList, { tasks: tasks, compact: compactWork })) : null, permission ? (_jsx(PermissionOverlay, { toolCalls: permission.toolCalls, review: permission.review, summary: permission.summary, detailsOpen: permissionDetailsOpen, reviewOffset: permissionReviewOffset, choice: permissionChoice, columns: terminal.columns, rows: terminal.rows })) : null, textPrompt ? (_jsx(TextPrompt, { title: textPrompt.title, placeholder: textPrompt.placeholder, initialValue: textPrompt.initialValue, emptyNotice: textPrompt.emptyNotice, hidden: permission !== null || agentSurface !== null, onSubmit: submitTextPrompt, onCancel: cancelTextPrompt }, textPrompt.token)) : permission === null && agentSurface === null && choicePicker ? (_jsx(ChoicePicker, { columns: Math.max(1, (terminal.columns ?? 80) - 2), title: choicePicker.title, notice: choicePicker.notice, options: filterChoiceOptions(choicePicker.options, choiceQuery), query: choicePickerSearchable(choicePicker) ? choiceQuery : undefined, selected: selectedChoice, windowSize: choicePicker.kind === 'command' ? choicePicker.windowSize : undefined })) : permission === null && agentSurface === null && copyPicker ? (_jsx(CopyPicker, { targets: copyPicker.targets, selected: selectedCopy })) : null, _jsxs(ComposerFrame, { working: state === 'thinking' || state === 'tool' || visibleActiveTools.length > 0, focus: phase === 'ready' &&
5661
+ 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 }) }), phase === 'trust' ? (_jsx(TrustPrompt, { cwd: ctx.cwd })) : phase === 'unhealthy' ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: theme.status.error, bold: true, children: "Startup stopped" }), _jsx(Text, { color: theme.text.secondary, children: "Resolve the error above, then restart Namzu." })] })) : phase === 'resume' ? (_jsx(ResumePicker, { conversations: resumeList, selected: selectedResume })) : phase === 'edit' ? (_jsx(EditPromptPicker, { prompts: editList, selected: selectedEdit })) : phase === 'picker' ? (_jsx(Picker, { detected: pickerDetected ?? detected, currentProvider: currentProvider, selectionKind: pickerSelectionKind, currentModel: session?.modelSummary ?? null, initialView: pickerInitialView, onSubmit: handlePickerSubmit, onCancel: handlePickerCancel, onCredential: handleTypedCredential, onLogin: startLoginFromPicker, onLoginComplete: finishLoginFromPicker, keyEntryFor: keyEntryFor, notice: pickerNotice })) : (_jsxs(_Fragment, { children: [agentSurface === null ? (_jsx(LiveActivity, { compact: compactWork, activeTools: visibleActiveTools, working: state === 'thinking' || state === 'tool', interruptible: abortRef.current !== null, animate: stdout.isTTY === true, thinking: thinking })) : null, agentSurface === null && permission === null ? (_jsx(TaskList, { tasks: tasks, compact: compactWork })) : null, permission ? (_jsx(PermissionOverlay, { toolCalls: permission.toolCalls, review: permission.review, summary: permission.summary, detailsOpen: permissionDetailsOpen, reviewOffset: permissionReviewOffset, choice: permissionChoice, columns: terminal.columns, rows: terminal.rows })) : null, textPrompt ? (_jsx(TextPrompt, { columns: Math.max(1, (terminal.columns ?? 80) - 2), title: textPrompt.title, placeholder: textPrompt.placeholder, initialValue: textPrompt.initialValue, emptyNotice: textPrompt.emptyNotice, hidden: permission !== null || agentSurface !== null, onSubmit: submitTextPrompt, onCancel: cancelTextPrompt }, textPrompt.token)) : permission === null && agentSurface === null && choicePicker ? (_jsx(ChoicePicker, { columns: Math.max(1, (terminal.columns ?? 80) - 2), title: choicePicker.title, notice: choicePicker.notice, options: filterChoiceOptions(choicePicker.options, choiceQuery), query: choicePickerSearchable(choicePicker) ? choiceQuery : undefined, selected: selectedChoice, windowSize: choicePicker.kind === 'command' ? choicePicker.windowSize : undefined })) : permission === null && agentSurface === null && copyPicker ? (_jsx(CopyPicker, { targets: copyPicker.targets, selected: selectedCopy })) : null, _jsxs(ComposerFrame, { working: state === 'thinking' || state === 'tool' || visibleActiveTools.length > 0, focus: phase === 'ready' &&
5296
5662
  state !== 'awaiting-permission' &&
5297
5663
  !compacting &&
5298
5664
  externalEditorRequest === null &&
@@ -5318,7 +5684,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
5318
5684
  ? queuePause.outcome === 'paused'
5319
5685
  ? 'held after a resumable run paused; wait for recovery, change model, or send a message to release it'
5320
5686
  : `paused after a ${queuePause.outcome} turn; send a message or change model to continue`
5321
- : 'sending when ready'] }) })) : null, _jsx(Composer, { permissionMode: displayedPermissionMode, onCycleMode: cyclePermissionMode, disabled: phase !== 'ready' ||
5687
+ : 'sending when ready'] }) })) : null, _jsx(Composer, { reasoningEffortLevels: session?.reasoningEffortLevels, permissionMode: displayedPermissionMode, onCycleMode: cyclePermissionMode, disabled: phase !== 'ready' ||
5322
5688
  state === 'awaiting-permission' ||
5323
5689
  compacting ||
5324
5690
  conversationMutation !== null ||
@@ -5330,7 +5696,8 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
5330
5696
  agentSurface?.kind === 'transcript',
5331
5697
  // A turn is running, so Esc is the interrupt and not
5332
5698
  // the composer's clear.
5333
- escapeInterrupts: !compacting && (state === 'thinking' || state === 'tool'), 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) => ({
5699
+ escapeInterrupts: !compacting &&
5700
+ (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) => ({
5334
5701
  ...command,
5335
5702
  description: command.unavailable?.(slashCtx) ?? command.description,
5336
5703
  })), mentionCandidates: mentionCandidates, history: history })] }), permission === null && agentSurface === 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] })), _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 }) })] }) }));
@@ -5443,6 +5810,29 @@ function permissionPicker(mode, approvedAll, more = false) {
5443
5810
  }),
5444
5811
  };
5445
5812
  }
5813
+ function reasoningEffortPicker(session, current, afterModelSelection = false) {
5814
+ const levels = session.reasoningEffortLevels;
5815
+ if (!session.hasProvider || levels === undefined)
5816
+ return undefined;
5817
+ const values = [undefined, ...levels];
5818
+ return {
5819
+ kind: 'reasoning-effort',
5820
+ title: `Select Reasoning Level for ${session.modelSummary ?? 'current model'}`,
5821
+ ...(afterModelSelection
5822
+ ? {
5823
+ selectedSession: session,
5824
+ notice: 'Model selected. Esc keeps this model at its default effort.',
5825
+ }
5826
+ : {}),
5827
+ values,
5828
+ options: values.map((effort) => ({
5829
+ label: effort ?? 'default',
5830
+ description: reasoningEffortDescription(effort),
5831
+ current: effort === current,
5832
+ default: effort === undefined,
5833
+ })),
5834
+ };
5835
+ }
5446
5836
  function reasoningEffortDescription(effort) {
5447
5837
  switch (effort) {
5448
5838
  case undefined: