@namzu/cli 19.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 (164) hide show
  1. package/CHANGELOG.md +308 -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 +15 -6
  71. package/dist/integrations/subagents/runtime.d.ts.map +1 -1
  72. package/dist/integrations/subagents/runtime.js +274 -46
  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 +12 -0
  79. package/dist/memory/presentation.d.ts.map +1 -0
  80. package/dist/memory/presentation.js +51 -0
  81. package/dist/memory/presentation.js.map +1 -0
  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/permissions/mode.d.ts +3 -0
  91. package/dist/permissions/mode.d.ts.map +1 -1
  92. package/dist/permissions/mode.js +18 -0
  93. package/dist/permissions/mode.js.map +1 -1
  94. package/dist/tui/AgentExplorer.d.ts +13 -2
  95. package/dist/tui/AgentExplorer.d.ts.map +1 -1
  96. package/dist/tui/AgentExplorer.js +93 -23
  97. package/dist/tui/AgentExplorer.js.map +1 -1
  98. package/dist/tui/App.d.ts.map +1 -1
  99. package/dist/tui/App.js +554 -107
  100. package/dist/tui/App.js.map +1 -1
  101. package/dist/tui/BrandHeader.d.ts +2 -5
  102. package/dist/tui/BrandHeader.d.ts.map +1 -1
  103. package/dist/tui/BrandHeader.js +3 -6
  104. package/dist/tui/BrandHeader.js.map +1 -1
  105. package/dist/tui/Composer.d.ts +3 -2
  106. package/dist/tui/Composer.d.ts.map +1 -1
  107. package/dist/tui/Composer.js +11 -9
  108. package/dist/tui/Composer.js.map +1 -1
  109. package/dist/tui/ComposerFrame.d.ts.map +1 -1
  110. package/dist/tui/ComposerFrame.js +1 -1
  111. package/dist/tui/ComposerFrame.js.map +1 -1
  112. package/dist/tui/PermissionOverlay.d.ts +1 -1
  113. package/dist/tui/PermissionOverlay.d.ts.map +1 -1
  114. package/dist/tui/PermissionOverlay.js +12 -4
  115. package/dist/tui/PermissionOverlay.js.map +1 -1
  116. package/dist/tui/Picker.d.ts.map +1 -1
  117. package/dist/tui/Picker.js +97 -21
  118. package/dist/tui/Picker.js.map +1 -1
  119. package/dist/tui/TextPrompt.d.ts +3 -1
  120. package/dist/tui/TextPrompt.d.ts.map +1 -1
  121. package/dist/tui/TextPrompt.js +55 -9
  122. package/dist/tui/TextPrompt.js.map +1 -1
  123. package/dist/tui/Transcript.d.ts +1 -9
  124. package/dist/tui/Transcript.d.ts.map +1 -1
  125. package/dist/tui/Transcript.js +32 -44
  126. package/dist/tui/Transcript.js.map +1 -1
  127. package/dist/tui/agent.d.ts +21 -4
  128. package/dist/tui/agent.d.ts.map +1 -1
  129. package/dist/tui/agent.js +207 -78
  130. package/dist/tui/agent.js.map +1 -1
  131. package/dist/tui/model-choices.d.ts +3 -1
  132. package/dist/tui/model-choices.d.ts.map +1 -1
  133. package/dist/tui/model-choices.js +12 -1
  134. package/dist/tui/model-choices.js.map +1 -1
  135. package/dist/tui/model-search.d.ts +4 -0
  136. package/dist/tui/model-search.d.ts.map +1 -0
  137. package/dist/tui/model-search.js +11 -0
  138. package/dist/tui/model-search.js.map +1 -0
  139. package/dist/tui/model-selection-intent.d.ts +8 -0
  140. package/dist/tui/model-selection-intent.d.ts.map +1 -0
  141. package/dist/tui/model-selection-intent.js +96 -0
  142. package/dist/tui/model-selection-intent.js.map +1 -0
  143. package/dist/tui/model-switch-tool.d.ts +7 -0
  144. package/dist/tui/model-switch-tool.d.ts.map +1 -0
  145. package/dist/tui/model-switch-tool.js +74 -0
  146. package/dist/tui/model-switch-tool.js.map +1 -0
  147. package/dist/tui/model-switch.d.ts +35 -0
  148. package/dist/tui/model-switch.d.ts.map +1 -0
  149. package/dist/tui/model-switch.js +106 -0
  150. package/dist/tui/model-switch.js.map +1 -0
  151. package/dist/tui/permission-review.d.ts.map +1 -1
  152. package/dist/tui/permission-review.js +25 -12
  153. package/dist/tui/permission-review.js.map +1 -1
  154. package/dist/tui/run-interruption.d.ts +7 -0
  155. package/dist/tui/run-interruption.d.ts.map +1 -1
  156. package/dist/tui/run-interruption.js +41 -0
  157. package/dist/tui/run-interruption.js.map +1 -1
  158. package/dist/tui/slashCommands.d.ts +5 -0
  159. package/dist/tui/slashCommands.d.ts.map +1 -1
  160. package/dist/tui/slashCommands.js +188 -15
  161. package/dist/tui/slashCommands.js.map +1 -1
  162. package/dist/tui/types.d.ts +6 -13
  163. package/dist/tui/types.d.ts.map +1 -1
  164. package/package.json +8 -7
package/dist/tui/App.js CHANGED
@@ -25,11 +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 { appendMemory, composeMemoryPrompt, readMemory } from '../memory/store.js';
29
- import { permissionModeDescription } from '../permissions/mode.js';
28
+ import { renderMemoryReport, renderMemorySaveResult } from '../memory/presentation.js';
29
+ import { appendMemoryWithStatus, readMemory } from '../memory/store.js';
30
+ import { effectivePermissionMode, permissionModeDescription, permissionModeLabel, } from '../permissions/mode.js';
30
31
  import { composeSkillsPrompt, discoverSkills, loadSkillBody } from '../skills/store.js';
31
32
  import { discoverUserCommands } from '../user-commands/store.js';
32
- 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';
33
34
  import { BrandHeader } from './BrandHeader.js';
34
35
  import { ChoicePicker } from './ChoicePicker.js';
35
36
  import { Composer, suggestionWindowSize, } from './Composer.js';
@@ -45,7 +46,7 @@ import { TaskList } from './TaskList.js';
45
46
  import { TextPrompt } from './TextPrompt.js';
46
47
  import { Transcript, willCollapse } from './Transcript.js';
47
48
  import { TrustPrompt } from './TrustPrompt.js';
48
- import { createAgentSession, probeAgentSession, } from './agent.js';
49
+ import { createAgentSession, describeProviderModels, probeAgentSession, } from './agent.js';
49
50
  import { choicePickerWindowSize, filterChoiceOptions, moveChoiceSelection, } from './choice-selection.js';
50
51
  import { keepRecentRows } from './compact-transcript.js';
51
52
  import { approvalIsDeliberate } from './consent-timing.js';
@@ -54,6 +55,8 @@ import { copyTargetsForResponse } from './copy-targets.js';
54
55
  import { editablePrompts } from './edit-prompts.js';
55
56
  import { editDraftInExternalEditor } from './external-editor.js';
56
57
  import { liveWindow } from './live-window.js';
58
+ import { resolveModelSwitch, } from './model-switch.js';
59
+ import { parseModelSelectionIntent, resolveModelSelectionIntent } from './model-selection-intent.js';
57
60
  import { describeCodexDeviceLoginStart, describeLoginOutcome, describeLoginStart, describeLogout, describeProviderLogout, } from './login-prompt.js';
58
61
  import { selectPrimaryProvider } from './provider-selection.js';
59
62
  import { eraseLastChoiceGrapheme } from './terminal-choice-text.js';
@@ -61,7 +64,7 @@ import { renderCheckpoints, renderRestore } from '../checkpoints/store.js';
61
64
  import { expandFileMentions, listMentionableFiles } from './mentions.js';
62
65
  import { openInBrowser } from './open-browser.js';
63
66
  import { buildPermissionReview, buildPermissionSummary, permissionReviewPageRows, permissionReviewRefusal, permissionReviewRows, } from './permission-review.js';
64
- import { describeRunInterruption } from './run-interruption.js';
67
+ import { describeRunInterruption, describeRunStop } from './run-interruption.js';
65
68
  import { moveSelection } from './selection-window.js';
66
69
  import { describeShellEscape, describeShellEscapeForModel, runShellEscape, shellEscapeCommand, } from './shell-escape.js';
67
70
  import { baseBranchReviewPrompt, commitReviewPrompt, hostCommandNames, kernelCommandDescriptors, mergeHostCommands, renderOutcome, reviewPrompt, runSlash, } from './slashCommands.js';
@@ -586,20 +589,22 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
586
589
  agentSurfaceCommittedRef.current = agentSurface;
587
590
  }, [agentSurface]);
588
591
  const replaceSubagents = useCallback((next) => {
592
+ const previous = subagentsRef.current;
589
593
  subagentsRef.current = next;
590
594
  setSubagentsState(next);
591
595
  const surface = agentSurfaceRef.current;
592
596
  if (!surface)
593
597
  return;
594
- const activeCohorts = activeSubagentCohorts(next);
595
- if (activeCohorts.length === 0) {
598
+ if (next.length === 0) {
596
599
  setAgentSurface(null);
597
600
  return;
598
601
  }
599
- if (activeCohorts.some((agent) => agent.viewId === surface.selectedId))
602
+ if (next.some((agent) => agent.viewId === surface.selectedId))
600
603
  return;
601
- const phases = agentPhases(activeCohorts);
602
- const fallbackPhase = phases.find((phase) => phase.id === surface.selectedPhaseId) ?? phases[0];
604
+ const phases = agentPhases(next);
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);
@@ -613,8 +618,10 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
613
618
  });
614
619
  }, [setAgentSurface]);
615
620
  const openAgentCockpit = useCallback(() => {
616
- const agents = activeSubagentCohorts(subagentsRef.current);
617
- const firstPhase = agentPhases(agents)[0];
621
+ const agents = subagentsRef.current;
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' ||
@@ -941,7 +974,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
941
974
  }, [nextId]);
942
975
  const applyPermissionMode = useCallback((mode) => {
943
976
  if (!session?.hasProvider) {
944
- pushMessage('system', 'No active session — pick a provider before changing permissions.');
977
+ pushMessage('system', 'Choose a model before changing permissions.');
945
978
  return;
946
979
  }
947
980
  if (state !== 'idle' ||
@@ -950,28 +983,32 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
950
983
  queuedRef.current.length > 0 ||
951
984
  permissionResolveRef.current !== null ||
952
985
  compactingRef.current) {
953
- pushMessage('system', 'Permission mode was not changed: wait for the active turn, prompt, compaction, and queued work to settle.');
986
+ pushMessage('system', 'Permissions were not changed. Finish or stop the current work first.');
954
987
  return;
955
988
  }
956
989
  if (!session.resetApprovalLatch) {
957
- pushMessage('system', 'Permission mode was not changed: this embedded session cannot revoke an earlier "approve all" choice. Reconnect it before changing modes.');
990
+ pushMessage('system', 'This session cannot reset approvals. Reconnect before changing permissions.');
958
991
  return;
959
992
  }
960
993
  session.resetApprovalLatch();
961
994
  permissionModeRef.current = mode;
962
995
  permissionModeSourceRef.current = 'session';
963
996
  setPermissionModeState(mode);
964
- pushMessage('system', `Permission mode changed to ${mode}. Any earlier "approve all" choice was revoked. Declarative deny rules and the built-in safety gate still take precedence.`);
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;
@@ -1452,6 +1489,16 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1452
1489
  return;
1453
1490
  }
1454
1491
  if (picker.kind === 'permission-mode') {
1492
+ if (value === 'more') {
1493
+ const child = permissionPicker(permissionModeRef.current, session?.approvalLatched() ?? false, true);
1494
+ setSelectedChoice(Math.max(0, child.options.findIndex((option) => option.current)));
1495
+ setChoicePicker({ ...child, back: picker });
1496
+ return;
1497
+ }
1498
+ if (value === 'rules') {
1499
+ commandPickerSubmitRef.current('/permissions details');
1500
+ return;
1501
+ }
1455
1502
  applyPermissionMode(value);
1456
1503
  return;
1457
1504
  }
@@ -1467,7 +1514,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1467
1514
  removeStoredCredential(value);
1468
1515
  return;
1469
1516
  }
1470
- applyReasoningEffort(value);
1517
+ applyReasoningEffort(value, picker.selectedSession);
1471
1518
  }, [
1472
1519
  activateSkill,
1473
1520
  advanceQueueContinuation,
@@ -1482,7 +1529,9 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1482
1529
  removeStoredCredential,
1483
1530
  runConversationExport,
1484
1531
  setChoicePicker,
1532
+ setSelectedChoice,
1485
1533
  setTextPrompt,
1534
+ session,
1486
1535
  ]);
1487
1536
  const sendCopyRequest = useCallback((index) => {
1488
1537
  const picker = copyPickerRef.current;
@@ -1660,7 +1709,8 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1660
1709
  materializationRef.current = null;
1661
1710
  }
1662
1711
  }, [ensureSessions]);
1663
- 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;
1664
1714
  if (signal?.aborted)
1665
1715
  return;
1666
1716
  const scope = await ensureSessions();
@@ -1670,22 +1720,27 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1670
1720
  const s = await createAgentSession(prefs, detectedNow, {
1671
1721
  scope,
1672
1722
  cwd: activeCtx.cwd,
1723
+ ...(activeCtx.structuredOutput ? { structuredOutput: activeCtx.structuredOutput } : {}),
1673
1724
  ...(activeCtx.additionalDirectories
1674
1725
  ? { additionalDirectories: activeCtx.additionalDirectories }
1675
1726
  : {}),
1676
1727
  ...(sessionsRef.current ? { stateRoot: sessionsRef.current.root } : {}),
1677
1728
  enableComputerUse: true,
1678
1729
  rules: activeCtx.rules,
1679
- ...(sessionsRef.current ? { sessionGoals: sessionsRef.current.goals } : {}),
1730
+ ...(sessionsRef.current
1731
+ ? { sessionGoals: sessionsRef.current.goals, conversationSessions: sessionsRef.current }
1732
+ : {}),
1680
1733
  ...(activeCtx.mcpServers ? { mcpServers: activeCtx.mcpServers } : {}),
1681
1734
  ...(activeCtx.plugins ? { plugins: activeCtx.plugins } : {}),
1682
1735
  ...(activeCtx.web ? { web: activeCtx.web } : {}),
1683
1736
  ...(activeCtx.hooks ? { hooks: activeCtx.hooks } : {}),
1684
1737
  ...(activeCtx.compaction ? { compaction: activeCtx.compaction } : {}),
1738
+ ...(activeCtx.memory ? { memory: activeCtx.memory } : {}),
1685
1739
  ...(activeCtx.sandbox ? { sandbox: activeCtx.sandbox } : {}),
1686
1740
  // Somebody is at this terminal, so the model may ask them one
1687
1741
  // question when a decision is genuinely theirs.
1688
1742
  askUser: true,
1743
+ allowModelSwitch: true,
1689
1744
  });
1690
1745
  if (signal?.aborted) {
1691
1746
  void s.close();
@@ -1704,6 +1759,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1704
1759
  }
1705
1760
  let commands;
1706
1761
  try {
1762
+ options.beforePublish?.();
1707
1763
  commands = discoverUserCommands({ cwd: activeCtx.cwd, reserved: hostCommandNames() });
1708
1764
  // Construction and validation precede the saved change. There is no
1709
1765
  // await between this atomic write and publishing the usable session.
@@ -1732,6 +1788,12 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1732
1788
  void previousSessionRef.current?.close();
1733
1789
  previousSessionRef.current = s;
1734
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
+ }
1735
1797
  setUserCommands(commands);
1736
1798
  const mentionLoadOwner = {};
1737
1799
  mentionLoadOwnerRef.current = mentionLoadOwner;
@@ -1748,13 +1810,11 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1748
1810
  if (s.hasProvider) {
1749
1811
  setTranscriptOwned(true);
1750
1812
  setPhase('ready');
1751
- pushMessage('system',
1752
- // Counted here, at connect. The number is deliberately the one true
1753
- // at this moment rather than the one that will be true after the
1754
- // first turn registers the deferred tools — a line reporting that a
1755
- // connection just happened should describe the connection that just
1756
- // happened. `/tools` is the present-tense question and asks later.
1757
- `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
+ }
1758
1818
  // Before the rest: a limitation the operator accepted once and has
1759
1819
  // been living with since is the thing they are least likely to
1760
1820
  // remember and most likely to be surprised by.
@@ -1787,7 +1847,15 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1787
1847
  if (s.errorHint)
1788
1848
  pushMessage('system', s.errorHint);
1789
1849
  }
1790
- }, [appLifetime.signal, advanceQueueContinuation, ensureSessions, pushMessage, setReasoningEffort]);
1850
+ }, [
1851
+ appLifetime.signal,
1852
+ advanceQueueContinuation,
1853
+ ensureSessions,
1854
+ pushMessage,
1855
+ setChoicePicker,
1856
+ setReasoningEffort,
1857
+ setSelectedChoice,
1858
+ ]);
1791
1859
  /**
1792
1860
  * Sign in to a subscription without leaving namzu.
1793
1861
  *
@@ -1823,7 +1891,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1823
1891
  pushMessage('system', describeLoginOutcome(outcome));
1824
1892
  if (outcome.ok) {
1825
1893
  setPickerInitialView('providers');
1826
- await runProbeRef.current?.();
1894
+ await runProbeRef.current?.({ announce: true });
1827
1895
  }
1828
1896
  return;
1829
1897
  }
@@ -1882,7 +1950,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1882
1950
  setPickerNotice(describeLoginOutcome(outcome));
1883
1951
  if (outcome.ok) {
1884
1952
  setPickerInitialView('providers');
1885
- await runProbeRef.current?.(signal);
1953
+ await runProbeRef.current?.({ signal, announce: true });
1886
1954
  }
1887
1955
  return 'finished';
1888
1956
  }
@@ -1942,11 +2010,12 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1942
2010
  setPickerNotice(describeLoginOutcome(outcome));
1943
2011
  if (outcome.ok) {
1944
2012
  setPickerInitialView('providers');
1945
- await runProbeRef.current?.(signal);
2013
+ await runProbeRef.current?.({ signal, announce: true });
1946
2014
  }
1947
2015
  return 'finished';
1948
2016
  }, []);
1949
- const runProbe = useCallback(async (signal) => {
2017
+ const runProbe = useCallback(async (options = {}) => {
2018
+ const { signal, announce = false } = options;
1950
2019
  try {
1951
2020
  if (signal?.aborted)
1952
2021
  return;
@@ -1999,7 +2068,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1999
2068
  if (probe.preferences) {
2000
2069
  setPickerDetected(null);
2001
2070
  setPickerSelectionKind('provider-and-model');
2002
- await hydrateSession(probe.preferences, probe.detected, signal);
2071
+ await hydrateSession(probe.preferences, probe.detected, { signal, announce });
2003
2072
  return;
2004
2073
  }
2005
2074
  const signedIn = signedInSubscriptionProviders(probe.detected);
@@ -2017,7 +2086,10 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2017
2086
  // not strand first run on the disabled unhealthy screen.
2018
2087
  const admissionSignal = signal ?? appLifetime.signal;
2019
2088
  try {
2020
- await hydrateSession(automaticPreferences, probe.detected, admissionSignal);
2089
+ await hydrateSession(automaticPreferences, probe.detected, {
2090
+ signal: admissionSignal,
2091
+ announce,
2092
+ });
2021
2093
  return;
2022
2094
  }
2023
2095
  catch (error) {
@@ -2252,11 +2324,15 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2252
2324
  * sends at that prompt, for the same reason.
2253
2325
  */
2254
2326
  const interruptTurn = useCallback(() => {
2327
+ const cancelledSwitch = cancelPendingModelSwitch();
2255
2328
  if (permissionResolveRef.current)
2256
2329
  resolvePermission({ kind: 'reject', feedback: 'User interrupted.' });
2257
2330
  const ac = abortRef.current;
2258
- if (!ac)
2259
- return false;
2331
+ if (!ac) {
2332
+ if (cancelledSwitch)
2333
+ discardQueued();
2334
+ return cancelledSwitch;
2335
+ }
2260
2336
  ac.abort(new RunCancelled('user'));
2261
2337
  const activeSessionId = scopeRef.current?.sessionId;
2262
2338
  if (activeSessionId)
@@ -2272,7 +2348,14 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2272
2348
  clearActiveTools();
2273
2349
  setState('idle');
2274
2350
  return true;
2275
- }, [clearActiveTools, discardQueued, goalActivation, resolvePermission, wakeGoalDriver]);
2351
+ }, [
2352
+ cancelPendingModelSwitch,
2353
+ clearActiveTools,
2354
+ discardQueued,
2355
+ goalActivation,
2356
+ resolvePermission,
2357
+ wakeGoalDriver,
2358
+ ]);
2276
2359
  /**
2277
2360
  * Load the chosen conversation into the transcript and continue in it.
2278
2361
  *
@@ -2345,6 +2428,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2345
2428
  discardQueued();
2346
2429
  goalActivation.clear();
2347
2430
  wakeGoalDriver();
2431
+ cancelPendingModelSwitch();
2348
2432
  conversationGenRef.current += 1;
2349
2433
  resetSubagentActivity();
2350
2434
  session?.resetTaskStore?.();
@@ -2381,6 +2465,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2381
2465
  interruptTurn,
2382
2466
  nextId,
2383
2467
  pushMessage,
2468
+ cancelPendingModelSwitch,
2384
2469
  resetSubagentActivity,
2385
2470
  session,
2386
2471
  resetTranscript,
@@ -2421,6 +2506,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2421
2506
  discardQueued();
2422
2507
  goalActivation.clear();
2423
2508
  wakeGoalDriver();
2509
+ cancelPendingModelSwitch();
2424
2510
  conversationGenRef.current += 1;
2425
2511
  resetSubagentActivity();
2426
2512
  session?.resetTaskStore?.();
@@ -2462,6 +2548,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2462
2548
  goalActivation,
2463
2549
  interruptTurn,
2464
2550
  pushMessage,
2551
+ cancelPendingModelSwitch,
2465
2552
  resetSubagentActivity,
2466
2553
  session,
2467
2554
  resetTranscript,
@@ -2710,6 +2797,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2710
2797
  const readablePrefix = editList
2711
2798
  .slice(0, target.userOrdinal)
2712
2799
  .map((prompt) => prompt.displayText);
2800
+ cancelPendingModelSwitch();
2713
2801
  conversationGenRef.current += 1;
2714
2802
  resetSubagentActivity();
2715
2803
  session?.resetTaskStore?.();
@@ -2753,6 +2841,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2753
2841
  hasUnsettledTurn,
2754
2842
  nextId,
2755
2843
  pushMessage,
2844
+ cancelPendingModelSwitch,
2756
2845
  resetSubagentActivity,
2757
2846
  session,
2758
2847
  resetTranscript,
@@ -3054,12 +3143,12 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3054
3143
  pushMessage('system', event.text, false, '⇄');
3055
3144
  break;
3056
3145
  case 'capability-warning':
3057
- 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, '!');
3058
3147
  break;
3059
3148
  case 'history-repair':
3060
- pushMessage('system', `History warning (${event.source}): ${event.text}`, false, '⚠');
3149
+ pushMessage('system', `History warning (${event.source}): ${event.text}`, false, '!');
3061
3150
  break;
3062
- case 'done':
3151
+ case 'done': {
3063
3152
  // `run_completed` is not synonymous with success: budgets,
3064
3153
  // cancellation and output guardrails arrive through this event too.
3065
3154
  // Missing remains a normal end for older producers, matching the
@@ -3072,7 +3161,11 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3072
3161
  outcome: st.completed ? 'completed' : 'stopped',
3073
3162
  };
3074
3163
  closeAssistant();
3164
+ const stopNotice = describeRunStop(event.stopReason);
3165
+ if (stopNotice)
3166
+ pushMessage('system', stopNotice, false, '■');
3075
3167
  break;
3168
+ }
3076
3169
  case 'paused':
3077
3170
  // Paused is a recoverable SDK verdict, not success and not failure.
3078
3171
  // Conversation evidence has no paused member, so it records the
@@ -3089,7 +3182,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3089
3182
  st.outcome = event.message === 'aborted' ? 'cancelled' : 'failed';
3090
3183
  if (event.message !== 'aborted') {
3091
3184
  st.notification = { kind: 'turn-settled', outcome: 'failed' };
3092
- pushMessage('system', describeRunInterruption(event), false, '⚠');
3185
+ pushMessage('system', describeRunInterruption(event), false, '!');
3093
3186
  }
3094
3187
  else
3095
3188
  st.notification = null;
@@ -3097,6 +3190,14 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3097
3190
  }
3098
3191
  }, [appendToMessage, finalizeMessage, flushStream, pushMessage]);
3099
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
+ }
3100
3201
  if (!session || !session.hasProvider) {
3101
3202
  pushMessage('system', session?.errorHint ?? 'Agent is not ready yet — give it a moment.');
3102
3203
  return;
@@ -3220,8 +3321,75 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3220
3321
  const turnGeneration = conversationGenRef.current;
3221
3322
  unsettledTurnGenerationsRef.current.set(turnToken, turnGeneration);
3222
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
+ };
3223
3390
  let inboxOpen = true;
3224
3391
  const inboxEntries = [];
3392
+ const inputWaiters = new Set();
3225
3393
  const inbox = {
3226
3394
  accept(input) {
3227
3395
  if (!inboxOpen ||
@@ -3232,8 +3400,32 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3232
3400
  return false;
3233
3401
  inboxEntries.push(input);
3234
3402
  setPendingSteers([...inboxEntries]);
3403
+ for (const wake of [...inputWaiters])
3404
+ wake();
3235
3405
  return true;
3236
3406
  },
3407
+ waitForInbound(signal) {
3408
+ if (signal.aborted)
3409
+ return Promise.reject(signal.reason);
3410
+ if (!inboxOpen || inboxEntries.length > 0)
3411
+ return Promise.resolve();
3412
+ return new Promise((resolve, reject) => {
3413
+ const cleanup = () => {
3414
+ inputWaiters.delete(wake);
3415
+ signal.removeEventListener('abort', abort);
3416
+ };
3417
+ const wake = () => {
3418
+ cleanup();
3419
+ resolve();
3420
+ };
3421
+ const abort = () => {
3422
+ cleanup();
3423
+ reject(signal.reason);
3424
+ };
3425
+ inputWaiters.add(wake);
3426
+ signal.addEventListener('abort', abort, { once: true });
3427
+ });
3428
+ },
3237
3429
  drain() {
3238
3430
  if (!inboxOpen ||
3239
3431
  ac.signal.aborted ||
@@ -3261,6 +3453,8 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3261
3453
  },
3262
3454
  close() {
3263
3455
  inboxOpen = false;
3456
+ for (const wake of [...inputWaiters])
3457
+ wake();
3264
3458
  return inboxEntries.splice(0, inboxEntries.length);
3265
3459
  },
3266
3460
  };
@@ -3329,7 +3523,9 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3329
3523
  // The mode above decides whether this callback is consulted.
3330
3524
  onPermission: askPermission,
3331
3525
  onQuestion: askQuestion,
3526
+ onModelSwitch: requestModelSwitch,
3332
3527
  inboundMessages: () => inbox.drain(),
3528
+ waitForInbound: (signal) => inbox.waitForInbound(signal),
3333
3529
  extraSystem: [composeSkillsPrompt(activeSkills), drainIdleJobNotices(), drainOperatorShell()]
3334
3530
  .filter((part) => Boolean(part))
3335
3531
  .join('\n\n') || undefined,
@@ -3509,15 +3705,76 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3509
3705
  // operation that sees no current-generation entries can now await that
3510
3706
  // tail without a turn appearing behind its read later.
3511
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
+ }
3512
3763
  }
3513
3764
  }, [
3514
3765
  activeSkills,
3766
+ appLifetime.signal,
3515
3767
  applyEvent,
3768
+ cancelPendingModelSwitch,
3516
3769
  clearActiveTools,
3770
+ currentProvider,
3517
3771
  ctx.cwd,
3772
+ detected,
3518
3773
  finalizeMessage,
3519
3774
  flushStream,
3520
3775
  goalActivation,
3776
+ hasUnsettledTurn,
3777
+ hydrateSession,
3521
3778
  materializeConversation,
3522
3779
  onPermission,
3523
3780
  pushMessage,
@@ -3527,6 +3784,109 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3527
3784
  setQueuePause,
3528
3785
  wakeGoalDriver,
3529
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
+ ]);
3530
3890
  const handleSubmit = useCallback((value, attachments, mode = 'submit') => {
3531
3891
  if (goalCommandInFlightRef.current) {
3532
3892
  pushMessage('system', 'A goal command is still reaching durable session state. Wait for its result before sending another command or prompt.');
@@ -3550,14 +3910,19 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3550
3910
  return;
3551
3911
  }
3552
3912
  setHistory((prev) => [...prev, value]);
3913
+ const selectionIntent = !attachments?.length ? parseModelSelectionIntent(value) : undefined;
3914
+ if (selectionIntent) {
3915
+ void selectModelIntent(selectionIntent.query);
3916
+ return;
3917
+ }
3553
3918
  // `#` remembers, `!` runs — neither is a prompt. Both are the
3554
3919
  // operator acting directly, the way other coding agents spell it,
3555
3920
  // and both leave a row the model reads on its next turn.
3556
3921
  if (value.startsWith('#') && value.slice(1).trim().length > 0) {
3557
3922
  const note = value.slice(1).trim();
3558
3923
  try {
3559
- const path = appendMemory(note, { scope: 'project', cwd: ctx.cwd });
3560
- pushMessage('system', `Remembered for this project (${path}): ${note}`);
3924
+ const saved = appendMemoryWithStatus(note, { scope: 'project', cwd: ctx.cwd });
3925
+ pushMessage('system', renderMemorySaveResult(saved, note));
3561
3926
  }
3562
3927
  catch (err) {
3563
3928
  pushMessage('system', `Could not save memory: ${err instanceof Error ? err.message : String(err)}`);
@@ -3628,10 +3993,12 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3628
3993
  const commands = [
3629
3994
  {
3630
3995
  name: 'model',
3996
+ label: 'Model',
3631
3997
  description: `${session?.providerSummary ?? 'No provider'} · ${session?.modelSummary ?? 'No model'}. Change model or provider.`,
3632
3998
  },
3633
3999
  {
3634
4000
  name: 'effort',
4001
+ label: 'Reasoning effort',
3635
4002
  description: `${reasoningEffortRef.current ?? session?.reasoningEffortDefault ?? 'Model default'} · applies to future turns in this session.`,
3636
4003
  ...(session?.reasoningEffortLevels === undefined
3637
4004
  ? { problem: 'This model does not publish reasoning choices.' }
@@ -3639,10 +4006,12 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3639
4006
  },
3640
4007
  {
3641
4008
  name: 'permissions',
3642
- description: `${permissionModeRef.current} · applies to future turns in this session.`,
4009
+ label: 'Permissions',
4010
+ description: `${permissionModeLabel(effectivePermissionMode(permissionModeRef.current, session?.approvalLatched() ?? false))} · this session.`,
3643
4011
  },
3644
4012
  {
3645
4013
  name: 'status config',
4014
+ label: 'Configuration',
3646
4015
  description: 'Show which configuration files and overrides are in use.',
3647
4016
  },
3648
4017
  ];
@@ -3653,7 +4022,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3653
4022
  notice: 'Select a setting to view or change it.',
3654
4023
  values: commands,
3655
4024
  options: commands.map((command) => ({
3656
- label: `/${command.name}`,
4025
+ label: command.label,
3657
4026
  description: command.description,
3658
4027
  disabledReason: command.problem,
3659
4028
  })),
@@ -3822,6 +4191,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3822
4191
  return;
3823
4192
  case 'repick':
3824
4193
  if (state !== 'idle' ||
4194
+ pendingModelSwitchRef.current ||
3825
4195
  abortRef.current !== null ||
3826
4196
  hasUnsettledTurn() ||
3827
4197
  (queuedRef.current.length > 0 && queuePauseRef.current === null) ||
@@ -3848,7 +4218,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3848
4218
  }
3849
4219
  case 'permission-mode-picker': {
3850
4220
  if (!session?.hasProvider) {
3851
- pushMessage('system', 'No active session — pick a provider before changing permissions.');
4221
+ pushMessage('system', 'Choose a model before changing permissions.');
3852
4222
  return;
3853
4223
  }
3854
4224
  if (state !== 'idle' ||
@@ -3858,22 +4228,14 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3858
4228
  permissionResolveRef.current !== null ||
3859
4229
  compactingRef.current ||
3860
4230
  !session.resetApprovalLatch) {
3861
- pushMessage('system', 'Permission choices are unavailable until the active turn, prompt, compaction, queued work, or embedded-session approval latch can be safely settled.');
4231
+ pushMessage('system', !session.resetApprovalLatch
4232
+ ? 'This session cannot reset approvals. Reconnect before changing permissions.'
4233
+ : 'Finish or stop the current work before changing permissions.');
3862
4234
  return;
3863
4235
  }
3864
- const values = ['prompt', 'accept-edits', 'auto', 'strict', 'plan'];
3865
- setSelectedChoice(Math.max(0, values.indexOf(permissionModeRef.current)));
3866
- setChoicePicker({
3867
- kind: 'permission-mode',
3868
- title: 'Select Permission Mode',
3869
- notice: permissionPickerNotice(session),
3870
- values,
3871
- options: values.map((mode) => ({
3872
- label: mode,
3873
- description: permissionModeDescription(mode),
3874
- current: mode === permissionModeRef.current,
3875
- })),
3876
- });
4236
+ const picker = permissionPicker(permissionModeRef.current, session.approvalLatched());
4237
+ setSelectedChoice(Math.max(0, picker.options.findIndex((option) => option.current)));
4238
+ setChoicePicker(picker);
3877
4239
  return;
3878
4240
  }
3879
4241
  case 'reasoning-effort': {
@@ -3898,17 +4260,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3898
4260
  const values = [undefined, ...levels];
3899
4261
  const current = reasoningEffortRef.current;
3900
4262
  setSelectedChoice(Math.max(0, values.findIndex((value) => value === current)));
3901
- setChoicePicker({
3902
- kind: 'reasoning-effort',
3903
- title: `Select Reasoning Level for ${session.modelSummary ?? 'current model'}`,
3904
- values,
3905
- options: values.map((effort) => ({
3906
- label: effort ?? 'default',
3907
- description: reasoningEffortDescription(effort),
3908
- current: effort === current,
3909
- default: effort === undefined,
3910
- })),
3911
- });
4263
+ setChoicePicker(reasoningEffortPicker(session, current) ?? null);
3912
4264
  return;
3913
4265
  }
3914
4266
  case 'login':
@@ -3948,17 +4300,15 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3948
4300
  }
3949
4301
  case 'remember':
3950
4302
  try {
3951
- const path = appendMemory(slash.text, { scope: slash.scope, cwd: ctx.cwd });
3952
- 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));
3953
4305
  }
3954
4306
  catch (err) {
3955
4307
  pushMessage('system', `Could not save memory: ${err instanceof Error ? err.message : String(err)}`);
3956
4308
  }
3957
4309
  return;
3958
4310
  case 'show-memory': {
3959
- const mem = composeMemoryPrompt(readMemory(undefined, ctx.cwd));
3960
- pushMessage('system', mem ??
3961
- 'Nothing remembered yet. #note or /memory <text> saves a fact about this project (.namzu/MEMORY.md); /memory --user <text> saves one for every project (~/.namzu/MEMORY.md).');
4311
+ pushMessage('system', renderMemoryReport(readMemory(undefined, ctx.cwd), { cwd: ctx.cwd }));
3962
4312
  return;
3963
4313
  }
3964
4314
  case 'list-skills': {
@@ -3977,7 +4327,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3977
4327
  // Dropping it silently would leave someone wondering where a
3978
4328
  // file they can see on disk went.
3979
4329
  s.problem
3980
- ? `⚠ ${s.name} — ${s.problem}`
4330
+ ? `! ${s.name} — ${s.problem}`
3981
4331
  : `${activeNames.has(s.name) ? '● ' : '○ '}${s.name} — ${s.description}`);
3982
4332
  pushMessage('system', `Skills (● active):\n ${lines.join('\n ')}`);
3983
4333
  return;
@@ -4391,6 +4741,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4391
4741
  resetTranscript,
4392
4742
  runConversationExport,
4393
4743
  session,
4744
+ selectModelIntent,
4394
4745
  setChoicePicker,
4395
4746
  setCopyPicker,
4396
4747
  setReasoningEffort,
@@ -4454,6 +4805,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4454
4805
  queuedRef.current.length > 0 ||
4455
4806
  abortRef.current ||
4456
4807
  hasUnsettledTurn() ||
4808
+ pendingModelSwitchRef.current ||
4457
4809
  conversationMutationRef.current ||
4458
4810
  exportingRef.current ||
4459
4811
  compactingRef.current ||
@@ -4481,6 +4833,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4481
4833
  state !== 'idle' ||
4482
4834
  abortRef.current ||
4483
4835
  hasUnsettledTurn(generation) ||
4836
+ pendingModelSwitchRef.current ||
4484
4837
  conversationMutationRef.current ||
4485
4838
  exportingRef.current ||
4486
4839
  compactingRef.current ||
@@ -4506,6 +4859,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4506
4859
  // or goal command is different: it revoked this scheduling boundary.
4507
4860
  if (conversationGenRef.current !== generation ||
4508
4861
  scopeRef.current?.sessionId !== sessionId ||
4862
+ pendingModelSwitchRef.current ||
4509
4863
  conversationMutationRef.current ||
4510
4864
  goalCommandInFlightRef.current ||
4511
4865
  !goalActivation.isArmed(sessionId, armed)) {
@@ -4538,6 +4892,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4538
4892
  enqueueQueued,
4539
4893
  goalActivation,
4540
4894
  goalDriveVersion,
4895
+ modelSwitchVersion,
4541
4896
  hasUnsettledTurn,
4542
4897
  phase,
4543
4898
  pushMessage,
@@ -4558,6 +4913,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4558
4913
  queuePauseRef.current !== null ||
4559
4914
  abortRef.current ||
4560
4915
  hasUnsettledTurn() ||
4916
+ pendingModelSwitchRef.current ||
4561
4917
  conversationMutationRef.current ||
4562
4918
  exportingRef.current ||
4563
4919
  compactingRef.current ||
@@ -4577,6 +4933,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4577
4933
  state,
4578
4934
  phase,
4579
4935
  queued,
4936
+ modelSwitchVersion,
4580
4937
  queuePause,
4581
4938
  textPrompt,
4582
4939
  choicePicker,
@@ -4601,7 +4958,12 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4601
4958
  }, [phase, ctx.version, pushMessage]);
4602
4959
  const hydrateFromPicker = useCallback(async (prefs, detectedNow, signal, options = {}) => {
4603
4960
  try {
4604
- 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
+ });
4605
4967
  }
4606
4968
  catch (err) {
4607
4969
  // A superseded choice no longer owns even its failure message. Its
@@ -4653,6 +5015,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4653
5015
  // Only a real refusal broadens back to the general, sign-in-capable picker.
4654
5016
  void hydrateFromPicker(prefs, detected, signal, {
4655
5017
  revealAllOnFailure: true,
5018
+ chooseReasoningEffort: selection.model !== undefined,
4656
5019
  persistSelection: detected.find((provider) => provider.entry.id === selection.provider)?.source.kind !==
4657
5020
  'session',
4658
5021
  });
@@ -4869,9 +5232,12 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4869
5232
  // highlighted child before the list has appeared on screen.
4870
5233
  if (agentSurfaceCommittedRef.current !== agentView)
4871
5234
  return;
4872
- const agents = activeSubagentCohorts(subagentsRef.current);
5235
+ const agents = subagentsRef.current;
4873
5236
  if (agentView.kind === 'cockpit') {
4874
- 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 ?? [];
4875
5241
  const phaseIndex = Math.max(0, phases.findIndex((phase) => phase.id === agentView.selectedPhaseId));
4876
5242
  const phase = phases[phaseIndex];
4877
5243
  const phaseAgents = phase?.agents ?? [];
@@ -4881,11 +5247,27 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4881
5247
  setAgentSurface(null);
4882
5248
  return;
4883
5249
  }
4884
- if (key.escape || (key.ctrl && input === 'c') || input.toLowerCase() === 'q') {
5250
+ if ((key.ctrl && input === 'c') || input.toLowerCase() === 'q') {
4885
5251
  setAgentSurface(null);
4886
5252
  return;
4887
5253
  }
5254
+ if (key.escape) {
5255
+ setAgentSurface(workflows.length > 1 && agentView.focus !== 'workflows'
5256
+ ? { ...agentView, focus: 'workflows' }
5257
+ : null);
5258
+ return;
5259
+ }
4888
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
+ }
4889
5271
  const focus = key.leftArrow
4890
5272
  ? 'phases'
4891
5273
  : key.rightArrow
@@ -4897,6 +5279,10 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4897
5279
  return;
4898
5280
  }
4899
5281
  if (key.return) {
5282
+ if (agentView.focus === 'workflows') {
5283
+ setAgentSurface({ ...agentView, focus: phases.length > 1 ? 'phases' : 'agents' });
5284
+ return;
5285
+ }
4900
5286
  if (agentView.focus === 'phases') {
4901
5287
  setAgentSurface({ ...agentView, focus: 'agents' });
4902
5288
  return;
@@ -4922,7 +5308,19 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4922
5308
  : key.upArrow
4923
5309
  ? 'previous'
4924
5310
  : 'next';
4925
- 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') {
4926
5324
  const next = moveSelection(phaseIndex, phases.length, movement, agentPhasePageSize(terminal.rows, agentCockpitIsWide(terminal.columns)));
4927
5325
  const targetPhase = phases[next];
4928
5326
  const targetAgent = targetPhase?.agents[0];
@@ -5105,7 +5503,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
5105
5503
  }
5106
5504
  // Esc interrupts a running turn (Ctrl+C stays reserved for exit). Mirrors
5107
5505
  // the Ctrl+C interrupt path: abort, drop the queue, one "Interrupted." line.
5108
- if (key.escape && abortRef.current) {
5506
+ if (key.escape && (abortRef.current || pendingModelSwitchRef.current)) {
5109
5507
  interruptTurn();
5110
5508
  pushMessage('system', 'Interrupted.');
5111
5509
  return;
@@ -5210,7 +5608,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
5210
5608
  ? representedSubagentTools.has(JSON.stringify([tool.runId, tool.id]))
5211
5609
  : representedUnscopedSubagentToolUseIds.has(tool.id)));
5212
5610
  const selectedSubagent = agentSurface
5213
- ? liveSubagents.find((agent) => agent.viewId === agentSurface.selectedId)
5611
+ ? subagents.find((agent) => agent.viewId === agentSurface.selectedId)
5214
5612
  : undefined;
5215
5613
  const lifecycleOwnsViewport = phase === 'trust' ||
5216
5614
  phase === 'resume' ||
@@ -5241,9 +5639,11 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
5241
5639
  : permission
5242
5640
  ? hintForPhase(phase, state, session?.hasProvider === true)
5243
5641
  : agentSurface?.kind === 'cockpit'
5244
- ? agentSurface.focus === 'phases'
5245
- ? 'agent phases — enter agents · esc return'
5246
- : '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'
5247
5647
  : agentSurface?.kind === 'transcript'
5248
5648
  ? 'observing agent — esc agents · q parent'
5249
5649
  : textPrompt
@@ -5257,7 +5657,8 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
5257
5657
  : goalStatus && phase === 'ready' && state === 'idle'
5258
5658
  ? undefined
5259
5659
  : hintForPhase(phase, state, session?.hasProvider === true, composerHasDraft);
5260
- 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: permissionMode, 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' &&
5660
+ const displayedPermissionMode = effectivePermissionMode(permissionMode, session?.approvalLatched() ?? false);
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' &&
5261
5662
  state !== 'awaiting-permission' &&
5262
5663
  !compacting &&
5263
5664
  externalEditorRequest === null &&
@@ -5268,7 +5669,8 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
5268
5669
  agentSurface === null, hidden: permission !== null ||
5269
5670
  textPrompt !== null ||
5270
5671
  choicePicker !== null ||
5271
- copyPicker !== null, children: [pendingSteers.length > 0 &&
5672
+ copyPicker !== null ||
5673
+ agentSurface?.kind === 'transcript', children: [pendingSteers.length > 0 &&
5272
5674
  permission === null &&
5273
5675
  textPrompt === null &&
5274
5676
  choicePicker === null &&
@@ -5282,7 +5684,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
5282
5684
  ? queuePause.outcome === 'paused'
5283
5685
  ? 'held after a resumable run paused; wait for recovery, change model, or send a message to release it'
5284
5686
  : `paused after a ${queuePause.outcome} turn; send a message or change model to continue`
5285
- : 'sending when ready'] }) })) : null, _jsx(Composer, { permissionMode: permissionMode, onCycleMode: cyclePermissionMode, disabled: phase !== 'ready' ||
5687
+ : 'sending when ready'] }) })) : null, _jsx(Composer, { reasoningEffortLevels: session?.reasoningEffortLevels, permissionMode: displayedPermissionMode, onCycleMode: cyclePermissionMode, disabled: phase !== 'ready' ||
5286
5688
  state === 'awaiting-permission' ||
5287
5689
  compacting ||
5288
5690
  conversationMutation !== null ||
@@ -5290,13 +5692,15 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
5290
5692
  agentSurface !== null, hidden: permission !== null ||
5291
5693
  textPrompt !== null ||
5292
5694
  choicePicker !== null ||
5293
- copyPicker !== null,
5695
+ copyPicker !== null ||
5696
+ agentSurface?.kind === 'transcript',
5294
5697
  // A turn is running, so Esc is the interrupt and not
5295
5698
  // the composer's clear.
5296
- 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) => ({
5297
5701
  ...command,
5298
5702
  description: command.unavailable?.(slashCtx) ?? command.description,
5299
- })), 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: liveSubagents, 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 }) })] }) }));
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 }) })] }) }));
5300
5704
  }
5301
5705
  /** One line for a background job that ended, for the transcript and the next turn. */
5302
5706
  function describeJobExit(job) {
@@ -5376,15 +5780,58 @@ function labelOfOption(question, optionId) {
5376
5780
  function choicePickerSearchable(picker) {
5377
5781
  return ['command', 'skill', 'review-branch', 'review-commit'].includes(picker.kind);
5378
5782
  }
5379
- function permissionPickerNotice(session) {
5380
- const notices = [];
5381
- if (session.approvalLatched()) {
5382
- notices.push('Approve all is active; applying any mode revokes it.');
5383
- }
5384
- const exempt = session.promptExemptTools();
5385
- if (exempt.length > 0)
5386
- notices.push(`Never prompted: ${exempt.join(', ')}.`);
5387
- return notices.length > 0 ? notices.join(' ') : undefined;
5783
+ function permissionPicker(mode, approvedAll, more = false) {
5784
+ const current = effectivePermissionMode(mode, approvedAll);
5785
+ const values = more
5786
+ ? ['auto', 'strict', 'rules']
5787
+ : ['prompt', 'accept-edits', 'plan', 'more'];
5788
+ return {
5789
+ kind: 'permission-mode',
5790
+ title: more ? 'More permission options' : 'Permissions',
5791
+ notice: `Current: ${permissionModeLabel(current)} · this session`,
5792
+ values,
5793
+ options: values.map((value) => {
5794
+ if (value === 'more')
5795
+ return {
5796
+ label: 'More options',
5797
+ description: 'Other approval modes and permission rules',
5798
+ current: current === 'auto' || current === 'strict',
5799
+ };
5800
+ if (value === 'rules')
5801
+ return {
5802
+ label: 'View rules',
5803
+ description: 'Show the permission rules in use',
5804
+ };
5805
+ return {
5806
+ label: permissionModeLabel(value),
5807
+ description: permissionModeDescription(value),
5808
+ current: value === current,
5809
+ };
5810
+ }),
5811
+ };
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
+ };
5388
5835
  }
5389
5836
  function reasoningEffortDescription(effort) {
5390
5837
  switch (effort) {