@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.
- package/CHANGELOG.md +255 -0
- package/README.md +42 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +24 -2
- package/dist/cli.js.map +1 -1
- package/dist/commands/acp.d.ts.map +1 -1
- package/dist/commands/acp.js +6 -1
- package/dist/commands/acp.js.map +1 -1
- package/dist/commands/run-stream.d.ts.map +1 -1
- package/dist/commands/run-stream.js +9 -1
- package/dist/commands/run-stream.js.map +1 -1
- package/dist/commands/run.d.ts.map +1 -1
- package/dist/commands/run.js +14 -11
- package/dist/commands/run.js.map +1 -1
- package/dist/config/load.d.ts.map +1 -1
- package/dist/config/load.js +21 -0
- package/dist/config/load.js.map +1 -1
- package/dist/config/schema.d.ts +11 -4
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js.map +1 -1
- package/dist/doctor/checks/chain.d.ts.map +1 -1
- package/dist/doctor/checks/chain.js +11 -5
- package/dist/doctor/checks/chain.js.map +1 -1
- package/dist/doctor/checks/credentials.d.ts.map +1 -1
- package/dist/doctor/checks/credentials.js +5 -1
- package/dist/doctor/checks/credentials.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/integrations/providers/access.d.ts +7 -0
- package/dist/integrations/providers/access.d.ts.map +1 -0
- package/dist/integrations/providers/access.js +17 -0
- package/dist/integrations/providers/access.js.map +1 -0
- package/dist/integrations/providers/discover.d.ts +17 -54
- package/dist/integrations/providers/discover.d.ts.map +1 -1
- package/dist/integrations/providers/discover.js +46 -58
- package/dist/integrations/providers/discover.js.map +1 -1
- package/dist/integrations/providers/harness-credentials.d.ts +14 -0
- package/dist/integrations/providers/harness-credentials.d.ts.map +1 -1
- package/dist/integrations/providers/harness-credentials.js +46 -0
- package/dist/integrations/providers/harness-credentials.js.map +1 -1
- package/dist/integrations/providers/model-reasoning.d.ts +4 -0
- package/dist/integrations/providers/model-reasoning.d.ts.map +1 -0
- package/dist/integrations/providers/model-reasoning.js +46 -0
- package/dist/integrations/providers/model-reasoning.js.map +1 -0
- package/dist/integrations/providers/register.d.ts +1 -1
- package/dist/integrations/providers/register.d.ts.map +1 -1
- package/dist/integrations/providers/register.js +11 -1
- package/dist/integrations/providers/register.js.map +1 -1
- package/dist/integrations/providers/registry.d.ts +4 -4
- package/dist/integrations/providers/registry.d.ts.map +1 -1
- package/dist/integrations/providers/registry.js +23 -0
- package/dist/integrations/providers/registry.js.map +1 -1
- package/dist/integrations/sessions/context-inventory.d.ts +4 -0
- package/dist/integrations/sessions/context-inventory.d.ts.map +1 -0
- package/dist/integrations/sessions/context-inventory.js +44 -0
- package/dist/integrations/sessions/context-inventory.js.map +1 -0
- package/dist/integrations/sessions/conversation-search.d.ts +60 -0
- package/dist/integrations/sessions/conversation-search.d.ts.map +1 -0
- package/dist/integrations/sessions/conversation-search.js +461 -0
- package/dist/integrations/sessions/conversation-search.js.map +1 -0
- package/dist/integrations/sessions/task-context.d.ts +4 -0
- package/dist/integrations/sessions/task-context.d.ts.map +1 -0
- package/dist/integrations/sessions/task-context.js +73 -0
- package/dist/integrations/sessions/task-context.js.map +1 -0
- package/dist/integrations/subagents/activity.d.ts +3 -1
- package/dist/integrations/subagents/activity.d.ts.map +1 -1
- package/dist/integrations/subagents/activity.js +20 -2
- package/dist/integrations/subagents/activity.js.map +1 -1
- package/dist/integrations/subagents/runtime.d.ts +7 -4
- package/dist/integrations/subagents/runtime.d.ts.map +1 -1
- package/dist/integrations/subagents/runtime.js +105 -17
- package/dist/integrations/subagents/runtime.js.map +1 -1
- package/dist/memory/io.d.ts +12 -0
- package/dist/memory/io.d.ts.map +1 -0
- package/dist/memory/io.js +177 -0
- package/dist/memory/io.js.map +1 -0
- package/dist/memory/presentation.d.ts +4 -1
- package/dist/memory/presentation.d.ts.map +1 -1
- package/dist/memory/presentation.js +16 -2
- package/dist/memory/presentation.js.map +1 -1
- package/dist/memory/store.d.ts +17 -3
- package/dist/memory/store.d.ts.map +1 -1
- package/dist/memory/store.js +61 -35
- package/dist/memory/store.js.map +1 -1
- package/dist/output-schema.d.ts +4 -0
- package/dist/output-schema.d.ts.map +1 -0
- package/dist/output-schema.js +18 -0
- package/dist/output-schema.js.map +1 -0
- package/dist/tui/AgentExplorer.d.ts +13 -2
- package/dist/tui/AgentExplorer.d.ts.map +1 -1
- package/dist/tui/AgentExplorer.js +85 -23
- package/dist/tui/AgentExplorer.js.map +1 -1
- package/dist/tui/App.d.ts.map +1 -1
- package/dist/tui/App.js +454 -64
- package/dist/tui/App.js.map +1 -1
- package/dist/tui/BrandHeader.d.ts +1 -4
- package/dist/tui/BrandHeader.d.ts.map +1 -1
- package/dist/tui/BrandHeader.js +2 -6
- package/dist/tui/BrandHeader.js.map +1 -1
- package/dist/tui/Composer.d.ts +2 -1
- package/dist/tui/Composer.d.ts.map +1 -1
- package/dist/tui/Composer.js +9 -2
- package/dist/tui/Composer.js.map +1 -1
- package/dist/tui/PermissionOverlay.d.ts +1 -1
- package/dist/tui/PermissionOverlay.d.ts.map +1 -1
- package/dist/tui/PermissionOverlay.js +8 -1
- package/dist/tui/PermissionOverlay.js.map +1 -1
- package/dist/tui/Picker.d.ts.map +1 -1
- package/dist/tui/Picker.js +94 -21
- package/dist/tui/Picker.js.map +1 -1
- package/dist/tui/TextPrompt.d.ts +3 -1
- package/dist/tui/TextPrompt.d.ts.map +1 -1
- package/dist/tui/TextPrompt.js +55 -9
- package/dist/tui/TextPrompt.js.map +1 -1
- package/dist/tui/Transcript.d.ts.map +1 -1
- package/dist/tui/Transcript.js +1 -1
- package/dist/tui/Transcript.js.map +1 -1
- package/dist/tui/agent.d.ts +18 -3
- package/dist/tui/agent.d.ts.map +1 -1
- package/dist/tui/agent.js +174 -58
- package/dist/tui/agent.js.map +1 -1
- package/dist/tui/model-choices.d.ts +3 -1
- package/dist/tui/model-choices.d.ts.map +1 -1
- package/dist/tui/model-choices.js +12 -1
- package/dist/tui/model-choices.js.map +1 -1
- package/dist/tui/model-search.d.ts +4 -0
- package/dist/tui/model-search.d.ts.map +1 -0
- package/dist/tui/model-search.js +11 -0
- package/dist/tui/model-search.js.map +1 -0
- package/dist/tui/model-selection-intent.d.ts +8 -0
- package/dist/tui/model-selection-intent.d.ts.map +1 -0
- package/dist/tui/model-selection-intent.js +96 -0
- package/dist/tui/model-selection-intent.js.map +1 -0
- package/dist/tui/model-switch-tool.d.ts +7 -0
- package/dist/tui/model-switch-tool.d.ts.map +1 -0
- package/dist/tui/model-switch-tool.js +74 -0
- package/dist/tui/model-switch-tool.js.map +1 -0
- package/dist/tui/model-switch.d.ts +35 -0
- package/dist/tui/model-switch.d.ts.map +1 -0
- package/dist/tui/model-switch.js +106 -0
- package/dist/tui/model-switch.js.map +1 -0
- package/dist/tui/permission-review.d.ts.map +1 -1
- package/dist/tui/permission-review.js +25 -12
- package/dist/tui/permission-review.js.map +1 -1
- package/dist/tui/run-interruption.d.ts +7 -0
- package/dist/tui/run-interruption.d.ts.map +1 -1
- package/dist/tui/run-interruption.js +41 -0
- package/dist/tui/run-interruption.js.map +1 -1
- package/dist/tui/slashCommands.d.ts.map +1 -1
- package/dist/tui/slashCommands.js +2 -1
- package/dist/tui/slashCommands.js.map +1 -1
- package/dist/tui/types.d.ts +3 -1
- package/dist/tui/types.d.ts.map +1 -1
- 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 {
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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
|
-
|
|
1764
|
-
//
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
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
|
-
}, [
|
|
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 (
|
|
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,
|
|
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
|
-
|
|
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
|
-
}, [
|
|
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
|
|
3600
|
-
pushMessage('system',
|
|
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
|
|
3988
|
-
pushMessage('system',
|
|
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
|
-
?
|
|
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,
|
|
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
|
|
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 (
|
|
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 === '
|
|
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 === '
|
|
5279
|
-
? '
|
|
5280
|
-
:
|
|
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,
|
|
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 &&
|
|
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:
|