@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.
- package/CHANGELOG.md +308 -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 +15 -6
- package/dist/integrations/subagents/runtime.d.ts.map +1 -1
- package/dist/integrations/subagents/runtime.js +274 -46
- 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 +12 -0
- package/dist/memory/presentation.d.ts.map +1 -0
- package/dist/memory/presentation.js +51 -0
- package/dist/memory/presentation.js.map +1 -0
- 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/permissions/mode.d.ts +3 -0
- package/dist/permissions/mode.d.ts.map +1 -1
- package/dist/permissions/mode.js +18 -0
- package/dist/permissions/mode.js.map +1 -1
- package/dist/tui/AgentExplorer.d.ts +13 -2
- package/dist/tui/AgentExplorer.d.ts.map +1 -1
- package/dist/tui/AgentExplorer.js +93 -23
- package/dist/tui/AgentExplorer.js.map +1 -1
- package/dist/tui/App.d.ts.map +1 -1
- package/dist/tui/App.js +554 -107
- package/dist/tui/App.js.map +1 -1
- package/dist/tui/BrandHeader.d.ts +2 -5
- package/dist/tui/BrandHeader.d.ts.map +1 -1
- package/dist/tui/BrandHeader.js +3 -6
- package/dist/tui/BrandHeader.js.map +1 -1
- package/dist/tui/Composer.d.ts +3 -2
- package/dist/tui/Composer.d.ts.map +1 -1
- package/dist/tui/Composer.js +11 -9
- package/dist/tui/Composer.js.map +1 -1
- package/dist/tui/ComposerFrame.d.ts.map +1 -1
- package/dist/tui/ComposerFrame.js +1 -1
- package/dist/tui/ComposerFrame.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 +12 -4
- package/dist/tui/PermissionOverlay.js.map +1 -1
- package/dist/tui/Picker.d.ts.map +1 -1
- package/dist/tui/Picker.js +97 -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 +1 -9
- package/dist/tui/Transcript.d.ts.map +1 -1
- package/dist/tui/Transcript.js +32 -44
- package/dist/tui/Transcript.js.map +1 -1
- package/dist/tui/agent.d.ts +21 -4
- package/dist/tui/agent.d.ts.map +1 -1
- package/dist/tui/agent.js +207 -78
- 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 +5 -0
- package/dist/tui/slashCommands.d.ts.map +1 -1
- package/dist/tui/slashCommands.js +188 -15
- package/dist/tui/slashCommands.js.map +1 -1
- package/dist/tui/types.d.ts +6 -13
- package/dist/tui/types.d.ts.map +1 -1
- 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 {
|
|
29
|
-
import {
|
|
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
|
-
|
|
595
|
-
if (activeCohorts.length === 0) {
|
|
598
|
+
if (next.length === 0) {
|
|
596
599
|
setAgentSurface(null);
|
|
597
600
|
return;
|
|
598
601
|
}
|
|
599
|
-
if (
|
|
602
|
+
if (next.some((agent) => agent.viewId === surface.selectedId))
|
|
600
603
|
return;
|
|
601
|
-
const phases = agentPhases(
|
|
602
|
-
const
|
|
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 =
|
|
617
|
-
const
|
|
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', '
|
|
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', '
|
|
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', '
|
|
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', `
|
|
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;
|
|
@@ -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,
|
|
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
|
|
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
|
-
|
|
1752
|
-
//
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
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
|
-
}, [
|
|
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 (
|
|
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,
|
|
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
|
-
|
|
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
|
-
}, [
|
|
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
|
|
3560
|
-
pushMessage('system',
|
|
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
|
-
|
|
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:
|
|
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', '
|
|
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',
|
|
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
|
|
3865
|
-
setSelectedChoice(Math.max(0,
|
|
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
|
|
3952
|
-
pushMessage('system',
|
|
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
|
-
|
|
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
|
-
?
|
|
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,
|
|
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 =
|
|
5235
|
+
const agents = subagentsRef.current;
|
|
4873
5236
|
if (agentView.kind === 'cockpit') {
|
|
4874
|
-
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 ?? [];
|
|
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 (
|
|
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 === '
|
|
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
|
-
?
|
|
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 === '
|
|
5245
|
-
? '
|
|
5246
|
-
:
|
|
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
|
-
|
|
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
|
|
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:
|
|
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 &&
|
|
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:
|
|
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
|
|
5380
|
-
const
|
|
5381
|
-
|
|
5382
|
-
|
|
5383
|
-
|
|
5384
|
-
|
|
5385
|
-
|
|
5386
|
-
|
|
5387
|
-
|
|
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) {
|