@namzu/cli 20.0.0 → 22.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 +371 -0
- package/README.md +71 -10
- 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 +15 -12
- package/dist/commands/run.js.map +1 -1
- package/dist/config/load.d.ts.map +1 -1
- package/dist/config/load.js +44 -2
- package/dist/config/load.js.map +1 -1
- package/dist/config/schema.d.ts +24 -13
- 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 +7 -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/installation.d.ts +5 -0
- package/dist/installation.d.ts.map +1 -0
- package/dist/installation.js +48 -0
- package/dist/installation.js.map +1 -0
- 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/current-credential.d.ts +4 -0
- package/dist/integrations/providers/current-credential.d.ts.map +1 -0
- package/dist/integrations/providers/current-credential.js +45 -0
- package/dist/integrations/providers/current-credential.js.map +1 -0
- package/dist/integrations/providers/discover.d.ts +28 -54
- package/dist/integrations/providers/discover.d.ts.map +1 -1
- package/dist/integrations/providers/discover.js +62 -58
- package/dist/integrations/providers/discover.js.map +1 -1
- package/dist/integrations/providers/gemini-credentials.d.ts +13 -0
- package/dist/integrations/providers/gemini-credentials.d.ts.map +1 -0
- package/dist/integrations/providers/gemini-credentials.js +148 -0
- package/dist/integrations/providers/gemini-credentials.js.map +1 -0
- 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 +16 -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 +33 -0
- package/dist/integrations/providers/registry.js.map +1 -1
- package/dist/integrations/providers/setup.d.ts +49 -0
- package/dist/integrations/providers/setup.d.ts.map +1 -0
- package/dist/integrations/providers/setup.js +105 -0
- package/dist/integrations/providers/setup.js.map +1 -0
- 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/store.d.ts +1 -0
- package/dist/integrations/sessions/store.d.ts.map +1 -1
- package/dist/integrations/sessions/store.js +17 -4
- package/dist/integrations/sessions/store.js.map +1 -1
- 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 +32 -5
- package/dist/integrations/subagents/activity.js.map +1 -1
- package/dist/integrations/subagents/history.d.ts +26 -0
- package/dist/integrations/subagents/history.d.ts.map +1 -0
- package/dist/integrations/subagents/history.js +76 -0
- package/dist/integrations/subagents/history.js.map +1 -0
- package/dist/integrations/subagents/model-effort.d.ts +4 -0
- package/dist/integrations/subagents/model-effort.d.ts.map +1 -0
- package/dist/integrations/subagents/model-effort.js +12 -0
- package/dist/integrations/subagents/model-effort.js.map +1 -0
- package/dist/integrations/subagents/runtime.d.ts +27 -7
- package/dist/integrations/subagents/runtime.d.ts.map +1 -1
- package/dist/integrations/subagents/runtime.js +305 -35
- package/dist/integrations/subagents/runtime.js.map +1 -1
- package/dist/integrations/web/search.d.ts +11 -0
- package/dist/integrations/web/search.d.ts.map +1 -0
- package/dist/integrations/web/search.js +73 -0
- package/dist/integrations/web/search.js.map +1 -0
- 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 +694 -150
- 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 +4 -8
- 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 +26 -3
- package/dist/tui/Composer.js.map +1 -1
- package/dist/tui/PermissionOverlay.d.ts +5 -2
- package/dist/tui/PermissionOverlay.d.ts.map +1 -1
- package/dist/tui/PermissionOverlay.js +14 -6
- package/dist/tui/PermissionOverlay.js.map +1 -1
- package/dist/tui/Picker.d.ts +2 -1
- package/dist/tui/Picker.d.ts.map +1 -1
- package/dist/tui/Picker.js +104 -24
- package/dist/tui/Picker.js.map +1 -1
- package/dist/tui/ProviderSetup.d.ts +6 -0
- package/dist/tui/ProviderSetup.d.ts.map +1 -0
- package/dist/tui/ProviderSetup.js +125 -0
- package/dist/tui/ProviderSetup.js.map +1 -0
- package/dist/tui/ResumePicker.d.ts.map +1 -1
- package/dist/tui/ResumePicker.js +5 -3
- package/dist/tui/ResumePicker.js.map +1 -1
- package/dist/tui/StatusPanel.d.ts +5 -0
- package/dist/tui/StatusPanel.d.ts.map +1 -0
- package/dist/tui/StatusPanel.js +11 -0
- package/dist/tui/StatusPanel.js.map +1 -0
- 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/ToolOutputViewer.d.ts +11 -0
- package/dist/tui/ToolOutputViewer.d.ts.map +1 -0
- package/dist/tui/ToolOutputViewer.js +39 -0
- package/dist/tui/ToolOutputViewer.js.map +1 -0
- package/dist/tui/Transcript.d.ts.map +1 -1
- package/dist/tui/Transcript.js +10 -4
- package/dist/tui/Transcript.js.map +1 -1
- package/dist/tui/agent.d.ts +24 -3
- package/dist/tui/agent.d.ts.map +1 -1
- package/dist/tui/agent.js +362 -97
- package/dist/tui/agent.js.map +1 -1
- package/dist/tui/live-window.d.ts.map +1 -1
- package/dist/tui/live-window.js +5 -1
- package/dist/tui/live-window.js.map +1 -1
- package/dist/tui/logo.d.ts +4 -4
- package/dist/tui/logo.d.ts.map +1 -1
- package/dist/tui/logo.js +4 -4
- package/dist/tui/logo.js.map +1 -1
- package/dist/tui/model-catalogue-view.d.ts +3 -0
- package/dist/tui/model-catalogue-view.d.ts.map +1 -0
- package/dist/tui/model-catalogue-view.js +50 -0
- package/dist/tui/model-catalogue-view.js.map +1 -0
- 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 +2 -0
- package/dist/tui/permission-review.d.ts.map +1 -1
- package/dist/tui/permission-review.js +82 -15
- 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 +7 -0
- package/dist/tui/slashCommands.d.ts.map +1 -1
- package/dist/tui/slashCommands.js +77 -2
- package/dist/tui/slashCommands.js.map +1 -1
- package/dist/tui/status-card.d.ts +3 -0
- package/dist/tui/status-card.d.ts.map +1 -0
- package/dist/tui/status-card.js +30 -0
- package/dist/tui/status-card.js.map +1 -0
- package/dist/tui/status-panel-layout.d.ts +11 -0
- package/dist/tui/status-panel-layout.d.ts.map +1 -0
- package/dist/tui/status-panel-layout.js +42 -0
- package/dist/tui/status-panel-layout.js.map +1 -0
- package/dist/tui/types.d.ts +7 -1
- package/dist/tui/types.d.ts.map +1 -1
- package/package.json +9 -7
package/dist/tui/App.js
CHANGED
|
@@ -11,6 +11,9 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
11
11
|
* 4. Otherwise show <Picker/>. An explicit picker choice is persisted;
|
|
12
12
|
* automatic reuse is not mistaken for a durable operator preference.
|
|
13
13
|
*/
|
|
14
|
+
import { discoverProviders } from '../integrations/providers/discover.js';
|
|
15
|
+
import { ProviderSetup } from './ProviderSetup.js';
|
|
16
|
+
import { ToolOutputViewer } from './ToolOutputViewer.js';
|
|
14
17
|
import { join, relative } from 'node:path';
|
|
15
18
|
import { DEFAULT_MAX_GOAL_ROUNDS, DefaultPathBuilder, DiskMessageFeedbackStore, GoalRoundLimitError, HostCommandRegistry, RunCancelled, SessionGoalActivation, StaleGoalError, asSessionId, createAssistantMessage, createUserMessage, generateRunId, generateSessionId, isCompactionMessage, kernelHostCommands, } from '@namzu/sdk';
|
|
16
19
|
import { Box, Text, useApp, useInput, useStdout, useWindowSize } from 'ink';
|
|
@@ -25,12 +28,12 @@ import { appendMessages, archiveConversation, forkConversation, forkConversation
|
|
|
25
28
|
import { conversationMarkdown, writeConversationExport, } from '../integrations/sessions/transcript-export.js';
|
|
26
29
|
import { isTrusted, trustDir } from '../integrations/trust/store.js';
|
|
27
30
|
import { checkUpdates } from '../integrations/updates.js';
|
|
28
|
-
import { renderMemoryReport } from '../memory/presentation.js';
|
|
29
|
-
import {
|
|
31
|
+
import { renderMemoryReport, renderMemorySaveResult } from '../memory/presentation.js';
|
|
32
|
+
import { appendMemoryWithStatus, readMemory } from '../memory/store.js';
|
|
30
33
|
import { effectivePermissionMode, permissionModeDescription, permissionModeLabel, } from '../permissions/mode.js';
|
|
31
34
|
import { composeSkillsPrompt, discoverSkills, loadSkillBody } from '../skills/store.js';
|
|
32
35
|
import { discoverUserCommands } from '../user-commands/store.js';
|
|
33
|
-
import { AgentCockpit, AgentTaskPanel, AgentTranscript, activeSubagentCohorts, agentCockpitIsWide, agentPhasePageSize, agentPhases, agentPickerPageSize, agentTranscriptPageSize, maxAgentTranscriptTailOffset, } from './AgentExplorer.js';
|
|
36
|
+
import { AgentCockpit, AgentTaskPanel, AgentTranscript, activeSubagentCohorts, agentCockpitIsWide, agentPhasePageSize, agentPhases, agentPickerPageSize, agentTranscriptPageSize, agentWorkflowPageSize, agentWorkflows, maxAgentTranscriptTailOffset, } from './AgentExplorer.js';
|
|
34
37
|
import { BrandHeader } from './BrandHeader.js';
|
|
35
38
|
import { ChoicePicker } from './ChoicePicker.js';
|
|
36
39
|
import { Composer, suggestionWindowSize, } from './Composer.js';
|
|
@@ -44,9 +47,10 @@ import { ResumePicker } from './ResumePicker.js';
|
|
|
44
47
|
import { StatusBar } from './StatusBar.js';
|
|
45
48
|
import { TaskList } from './TaskList.js';
|
|
46
49
|
import { TextPrompt } from './TextPrompt.js';
|
|
50
|
+
import { modelCatalogueView } from './model-catalogue-view.js';
|
|
47
51
|
import { Transcript, willCollapse } from './Transcript.js';
|
|
48
52
|
import { TrustPrompt } from './TrustPrompt.js';
|
|
49
|
-
import { createAgentSession, probeAgentSession, } from './agent.js';
|
|
53
|
+
import { createAgentSession, describeProviderModels, probeAgentSession, } from './agent.js';
|
|
50
54
|
import { choicePickerWindowSize, filterChoiceOptions, moveChoiceSelection, } from './choice-selection.js';
|
|
51
55
|
import { keepRecentRows } from './compact-transcript.js';
|
|
52
56
|
import { approvalIsDeliberate } from './consent-timing.js';
|
|
@@ -55,6 +59,8 @@ import { copyTargetsForResponse } from './copy-targets.js';
|
|
|
55
59
|
import { editablePrompts } from './edit-prompts.js';
|
|
56
60
|
import { editDraftInExternalEditor } from './external-editor.js';
|
|
57
61
|
import { liveWindow } from './live-window.js';
|
|
62
|
+
import { resolveModelSwitch, } from './model-switch.js';
|
|
63
|
+
import { parseModelSelectionIntent, resolveModelSelectionIntent } from './model-selection-intent.js';
|
|
58
64
|
import { describeCodexDeviceLoginStart, describeLoginOutcome, describeLoginStart, describeLogout, describeProviderLogout, } from './login-prompt.js';
|
|
59
65
|
import { selectPrimaryProvider } from './provider-selection.js';
|
|
60
66
|
import { eraseLastChoiceGrapheme } from './terminal-choice-text.js';
|
|
@@ -62,7 +68,7 @@ import { renderCheckpoints, renderRestore } from '../checkpoints/store.js';
|
|
|
62
68
|
import { expandFileMentions, listMentionableFiles } from './mentions.js';
|
|
63
69
|
import { openInBrowser } from './open-browser.js';
|
|
64
70
|
import { buildPermissionReview, buildPermissionSummary, permissionReviewPageRows, permissionReviewRefusal, permissionReviewRows, } from './permission-review.js';
|
|
65
|
-
import { describeRunInterruption } from './run-interruption.js';
|
|
71
|
+
import { describeRunInterruption, describeRunStop } from './run-interruption.js';
|
|
66
72
|
import { moveSelection } from './selection-window.js';
|
|
67
73
|
import { describeShellEscape, describeShellEscapeForModel, runShellEscape, shellEscapeCommand, } from './shell-escape.js';
|
|
68
74
|
import { baseBranchReviewPrompt, commitReviewPrompt, hostCommandNames, kernelCommandDescriptors, mergeHostCommands, renderOutcome, reviewPrompt, runSlash, } from './slashCommands.js';
|
|
@@ -275,6 +281,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
275
281
|
const [messages, setMessages] = useState([]);
|
|
276
282
|
/** Plain, copy-friendly rendering of the same retained transcript rows. */
|
|
277
283
|
const [rawOutput, setRawOutput] = useState(false);
|
|
284
|
+
const [outputViewer, setOutputViewer] = useState(null);
|
|
278
285
|
/**
|
|
279
286
|
* The conversation sent to the model, in the SDK's own lossless shape.
|
|
280
287
|
*
|
|
@@ -531,6 +538,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
531
538
|
copyPickerCommittedRef.current = copyPicker;
|
|
532
539
|
}, [copyPicker]);
|
|
533
540
|
/** Finite slash-command choice owned synchronously until apply or cancel. */
|
|
541
|
+
const [providerSetup, setProviderSetup] = useState(false);
|
|
534
542
|
const [choicePicker, setChoicePickerState] = useState(null);
|
|
535
543
|
const choicePickerRef = useRef(null);
|
|
536
544
|
const [choiceQuery, setChoiceQuery] = useState('');
|
|
@@ -587,6 +595,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
587
595
|
agentSurfaceCommittedRef.current = agentSurface;
|
|
588
596
|
}, [agentSurface]);
|
|
589
597
|
const replaceSubagents = useCallback((next) => {
|
|
598
|
+
const previous = subagentsRef.current;
|
|
590
599
|
subagentsRef.current = next;
|
|
591
600
|
setSubagentsState(next);
|
|
592
601
|
const surface = agentSurfaceRef.current;
|
|
@@ -599,7 +608,9 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
599
608
|
if (next.some((agent) => agent.viewId === surface.selectedId))
|
|
600
609
|
return;
|
|
601
610
|
const phases = agentPhases(next);
|
|
602
|
-
const
|
|
611
|
+
const previousGroupId = previous.find((agent) => agent.viewId === surface.selectedId)?.workflowGroupId;
|
|
612
|
+
const sameWorkflow = agentWorkflows(next).find((workflow) => workflow.id === previousGroupId);
|
|
613
|
+
const fallbackPhase = phases.find((phase) => phase.id === surface.selectedPhaseId) ?? sameWorkflow?.phases[0] ?? phases[0];
|
|
603
614
|
const fallbackAgent = fallbackPhase?.agents[0];
|
|
604
615
|
if (!fallbackPhase || !fallbackAgent) {
|
|
605
616
|
setAgentSurface(null);
|
|
@@ -614,7 +625,9 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
614
625
|
}, [setAgentSurface]);
|
|
615
626
|
const openAgentCockpit = useCallback(() => {
|
|
616
627
|
const agents = subagentsRef.current;
|
|
617
|
-
const
|
|
628
|
+
const workflows = agentWorkflows(agents);
|
|
629
|
+
const currentWorkflow = [...workflows].reverse().find((workflow) => workflow.agents.some((agent) => agent.status === 'starting' || agent.status === 'queued' || agent.status === 'working')) ?? workflows.at(-1);
|
|
630
|
+
const firstPhase = currentWorkflow?.phases[0];
|
|
618
631
|
const firstAgent = firstPhase?.agents[0];
|
|
619
632
|
if (!firstPhase || !firstAgent)
|
|
620
633
|
return false;
|
|
@@ -622,7 +635,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
622
635
|
kind: 'cockpit',
|
|
623
636
|
selectedPhaseId: firstPhase.id,
|
|
624
637
|
selectedId: firstAgent.viewId,
|
|
625
|
-
focus: 'agents',
|
|
638
|
+
focus: workflows.length > 1 ? 'workflows' : 'agents',
|
|
626
639
|
});
|
|
627
640
|
return true;
|
|
628
641
|
}, [setAgentSurface]);
|
|
@@ -632,6 +645,18 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
632
645
|
const abortRef = useRef(null);
|
|
633
646
|
/** Identity of the turn allowed to notify when it settles. */
|
|
634
647
|
const activeTurnTokenRef = useRef(null);
|
|
648
|
+
/** A tool can reserve a switch; only the owning turn's finalizer may publish it. */
|
|
649
|
+
const pendingModelSwitchRef = useRef(null);
|
|
650
|
+
const [modelSwitchVersion, setModelSwitchVersion] = useState(0);
|
|
651
|
+
const cancelPendingModelSwitch = useCallback(() => {
|
|
652
|
+
const pending = pendingModelSwitchRef.current;
|
|
653
|
+
if (!pending)
|
|
654
|
+
return false;
|
|
655
|
+
pendingModelSwitchRef.current = null;
|
|
656
|
+
pending.controller.abort(new Error('The pending model switch was cancelled.'));
|
|
657
|
+
setModelSwitchVersion((version) => version + 1);
|
|
658
|
+
return true;
|
|
659
|
+
}, []);
|
|
635
660
|
/** Exact live-input owner; absent before provider admission and after settlement. */
|
|
636
661
|
const activeTurnInboxRef = useRef(null);
|
|
637
662
|
/** A broken terminal notification is reported once, not after every turn. */
|
|
@@ -689,6 +714,18 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
689
714
|
setActiveTools([]);
|
|
690
715
|
}, []);
|
|
691
716
|
const permissionResolveRef = useRef(null);
|
|
717
|
+
const permissionQueueRef = useRef([]);
|
|
718
|
+
const [queuedPermissionCount, setQueuedPermissionCount] = useState(0);
|
|
719
|
+
useEffect(() => () => {
|
|
720
|
+
const decision = {
|
|
721
|
+
kind: 'reject',
|
|
722
|
+
feedback: 'The approval screen closed.',
|
|
723
|
+
};
|
|
724
|
+
permissionResolveRef.current?.(decision);
|
|
725
|
+
permissionResolveRef.current = null;
|
|
726
|
+
for (const pending of permissionQueueRef.current.splice(0))
|
|
727
|
+
pending.resolve(decision);
|
|
728
|
+
}, []);
|
|
692
729
|
/**
|
|
693
730
|
* When the pending prompt took the screen, or `null` with none open.
|
|
694
731
|
*
|
|
@@ -727,13 +764,14 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
727
764
|
* hook never ran.
|
|
728
765
|
*/
|
|
729
766
|
const closeAndExit = useCallback(() => {
|
|
767
|
+
cancelPendingModelSwitch();
|
|
730
768
|
const closing = session?.close() ?? Promise.resolve();
|
|
731
769
|
const bounded = Promise.race([
|
|
732
770
|
closing,
|
|
733
771
|
new Promise((resolve) => setTimeout(resolve, SESSION_CLOSE_ON_EXIT_MS).unref?.()),
|
|
734
772
|
]);
|
|
735
773
|
void bounded.catch(() => undefined).finally(() => exit());
|
|
736
|
-
}, [exit, session]);
|
|
774
|
+
}, [cancelPendingModelSwitch, exit, session]);
|
|
737
775
|
const exitWithSummary = useCallback(() => {
|
|
738
776
|
onExitSummary?.({
|
|
739
777
|
...(conversationMaterializedRef.current && scopeRef.current?.sessionId
|
|
@@ -747,6 +785,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
747
785
|
const goalDriveInFlightRef = useRef(false);
|
|
748
786
|
const [goalDriveVersion, setGoalDriveVersion] = useState(0);
|
|
749
787
|
const [goalStatus, setGoalStatus] = useState(null);
|
|
788
|
+
const pendingGoalResumeRef = useRef(null);
|
|
750
789
|
const wakeGoalDriver = useCallback(() => setGoalDriveVersion((version) => version + 1), []);
|
|
751
790
|
/**
|
|
752
791
|
* Conversation writes in the order the operator produced them.
|
|
@@ -837,6 +876,19 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
837
876
|
settledRef.current = 0;
|
|
838
877
|
setResetKey((k) => k + 1);
|
|
839
878
|
}, [stdout.isTTY, writeStdout]);
|
|
879
|
+
const previousTerminalRef = useRef(terminal);
|
|
880
|
+
useEffect(() => {
|
|
881
|
+
const previous = previousTerminalRef.current;
|
|
882
|
+
previousTerminalRef.current = terminal;
|
|
883
|
+
if (externalEditorRequestRef.current === null &&
|
|
884
|
+
(terminal.columns < previous.columns || terminal.rows < previous.rows)) {
|
|
885
|
+
// A contraction can reflow live rows into native scrollback before Ink
|
|
886
|
+
// receives resize. Its old row count cannot erase those rows. Rebuild
|
|
887
|
+
// Namzu's normal-buffer view at the committed size, retaining the same
|
|
888
|
+
// messages, draft and selected child. Regular frames remain incremental.
|
|
889
|
+
resetTranscript();
|
|
890
|
+
}
|
|
891
|
+
}, [terminal, resetTranscript]);
|
|
840
892
|
const requestExternalEditor = useCallback((seed) => {
|
|
841
893
|
if (phase !== 'ready' ||
|
|
842
894
|
state !== 'idle' ||
|
|
@@ -907,7 +959,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
907
959
|
externalEditorRequestRef.current = null;
|
|
908
960
|
request.controller.abort(new Error('the terminal session closed'));
|
|
909
961
|
}, []);
|
|
910
|
-
const pushMessage = useCallback((role, content, pending = false, glyph, detail, glyphColor, meta) => {
|
|
962
|
+
const pushMessage = useCallback((role, content, pending = false, glyph, detail, glyphColor, meta, activity) => {
|
|
911
963
|
const id = nextId();
|
|
912
964
|
setMessages((prev) => [
|
|
913
965
|
...prev,
|
|
@@ -920,6 +972,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
920
972
|
detail,
|
|
921
973
|
glyphColor,
|
|
922
974
|
meta,
|
|
975
|
+
activity,
|
|
923
976
|
// Numbered only if this body will actually be COLLAPSED — the
|
|
924
977
|
// number exists to be read off a hint, and a body that fits
|
|
925
978
|
// prints no hint. Numbering every body instead would leave gaps
|
|
@@ -930,7 +983,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
930
983
|
//
|
|
931
984
|
// Derived from `prev` rather than a counter, so the number is a
|
|
932
985
|
// fact about the transcript rather than a second record of it.
|
|
933
|
-
...(willCollapse(detail)
|
|
986
|
+
...((activity && detail?.length) || willCollapse(detail)
|
|
934
987
|
? {
|
|
935
988
|
detailRef: prev.filter((m) => m.detailRef !== undefined).length + 1,
|
|
936
989
|
}
|
|
@@ -939,6 +992,29 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
939
992
|
]);
|
|
940
993
|
return id;
|
|
941
994
|
}, [nextId]);
|
|
995
|
+
const reportedAgentsRef = useRef(new Set());
|
|
996
|
+
useEffect(() => {
|
|
997
|
+
reportedAgentsRef.current.clear();
|
|
998
|
+
}, [session]);
|
|
999
|
+
useEffect(() => {
|
|
1000
|
+
const current = session?.subagents?.getSnapshot() ?? [];
|
|
1001
|
+
for (const agent of subagents) {
|
|
1002
|
+
if (!current.some((item) => item.viewId === agent.viewId &&
|
|
1003
|
+
item.workflowId === agent.workflowId &&
|
|
1004
|
+
item.status === agent.status))
|
|
1005
|
+
continue;
|
|
1006
|
+
if (!['completed', 'failed', 'cancelled'].includes(agent.status))
|
|
1007
|
+
continue;
|
|
1008
|
+
if (reportedAgentsRef.current.has(agent.viewId))
|
|
1009
|
+
continue;
|
|
1010
|
+
reportedAgentsRef.current.add(agent.viewId);
|
|
1011
|
+
const completed = agent.status === 'completed';
|
|
1012
|
+
const status = completed
|
|
1013
|
+
? 'Completed'
|
|
1014
|
+
: (agent.latestActivity ?? (agent.status === 'cancelled' ? 'Cancelled' : 'Failed'));
|
|
1015
|
+
pushMessage('tool', `${agent.description} · ${status}`, false, completed ? '✓' : '✗', undefined, completed ? theme.status.ok : theme.status.error, 'ctrl+t · agent details');
|
|
1016
|
+
}
|
|
1017
|
+
}, [subagents, pushMessage, session]);
|
|
942
1018
|
const applyPermissionMode = useCallback((mode) => {
|
|
943
1019
|
if (!session?.hasProvider) {
|
|
944
1020
|
pushMessage('system', 'Choose a model before changing permissions.');
|
|
@@ -963,15 +1039,19 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
963
1039
|
setPermissionModeState(mode);
|
|
964
1040
|
pushMessage('system', `Permissions: ${permissionModeLabel(mode)} for this session. ${permissionModeDescription(mode)}`);
|
|
965
1041
|
}, [hasUnsettledTurn, pushMessage, session, state]);
|
|
966
|
-
const applyReasoningEffort = useCallback((effort) => {
|
|
1042
|
+
const applyReasoningEffort = useCallback((effort, selectedSession) => {
|
|
967
1043
|
if (!session?.hasProvider) {
|
|
968
1044
|
pushMessage('system', 'No active session — pick a provider before changing reasoning effort.');
|
|
969
1045
|
return;
|
|
970
1046
|
}
|
|
1047
|
+
if (selectedSession !== undefined && selectedSession !== session) {
|
|
1048
|
+
pushMessage('system', 'Reasoning effort was not changed: the selected model is no longer active.');
|
|
1049
|
+
return;
|
|
1050
|
+
}
|
|
971
1051
|
if (state !== 'idle' ||
|
|
972
1052
|
abortRef.current !== null ||
|
|
973
1053
|
hasUnsettledTurn() ||
|
|
974
|
-
queuedRef.current.length > 0 ||
|
|
1054
|
+
(queuedRef.current.length > 0 && selectedSession !== session) ||
|
|
975
1055
|
permissionResolveRef.current !== null ||
|
|
976
1056
|
compactingRef.current) {
|
|
977
1057
|
pushMessage('system', 'Reasoning effort was not changed: wait for the active turn, prompt, compaction, and queued work to settle.');
|
|
@@ -979,7 +1059,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
979
1059
|
}
|
|
980
1060
|
if (effort === undefined) {
|
|
981
1061
|
setReasoningEffort(undefined);
|
|
982
|
-
pushMessage('system', 'Reasoning
|
|
1062
|
+
pushMessage('system', 'Reasoning: provider default.');
|
|
983
1063
|
return;
|
|
984
1064
|
}
|
|
985
1065
|
if (!session.reasoningEffortLevels?.includes(effort)) {
|
|
@@ -987,7 +1067,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
987
1067
|
return;
|
|
988
1068
|
}
|
|
989
1069
|
setReasoningEffort(effort);
|
|
990
|
-
pushMessage('system', `Reasoning
|
|
1070
|
+
pushMessage('system', `Reasoning: ${effort} (this session).`);
|
|
991
1071
|
}, [hasUnsettledTurn, pushMessage, session, setReasoningEffort, state]);
|
|
992
1072
|
const stepReasoningEffort = useCallback((direction) => {
|
|
993
1073
|
const levels = session?.reasoningEffortLevels;
|
|
@@ -1412,6 +1492,11 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1412
1492
|
return;
|
|
1413
1493
|
}
|
|
1414
1494
|
setChoicePicker(null);
|
|
1495
|
+
if (picker.kind === 'resume-goal') {
|
|
1496
|
+
if (value === 'resume' && scopeRef.current?.sessionId === picker.sessionId)
|
|
1497
|
+
commandPickerSubmitRef.current('/goal resume');
|
|
1498
|
+
return;
|
|
1499
|
+
}
|
|
1415
1500
|
if (picker.kind === 'command') {
|
|
1416
1501
|
const command = value;
|
|
1417
1502
|
if (command.problem) {
|
|
@@ -1477,7 +1562,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1477
1562
|
removeStoredCredential(value);
|
|
1478
1563
|
return;
|
|
1479
1564
|
}
|
|
1480
|
-
applyReasoningEffort(value);
|
|
1565
|
+
applyReasoningEffort(value, picker.selectedSession);
|
|
1481
1566
|
}, [
|
|
1482
1567
|
activateSkill,
|
|
1483
1568
|
advanceQueueContinuation,
|
|
@@ -1609,6 +1694,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1609
1694
|
lastCompletedOutputRef.current = persistedOutput
|
|
1610
1695
|
? { text: persistedOutput, provenance: 'persisted' }
|
|
1611
1696
|
: null;
|
|
1697
|
+
pendingGoalResumeRef.current = requestedConversationId;
|
|
1612
1698
|
initialConversationIdRef.current = undefined;
|
|
1613
1699
|
conversationMaterializedRef.current = true;
|
|
1614
1700
|
}
|
|
@@ -1672,7 +1758,8 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1672
1758
|
materializationRef.current = null;
|
|
1673
1759
|
}
|
|
1674
1760
|
}, [ensureSessions]);
|
|
1675
|
-
const hydrateSession = useCallback(async (prefs, detectedNow,
|
|
1761
|
+
const hydrateSession = useCallback(async (prefs, detectedNow, options = {}) => {
|
|
1762
|
+
const { signal, persistSelection = false, announce = false } = options;
|
|
1676
1763
|
if (signal?.aborted)
|
|
1677
1764
|
return;
|
|
1678
1765
|
const scope = await ensureSessions();
|
|
@@ -1682,22 +1769,27 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1682
1769
|
const s = await createAgentSession(prefs, detectedNow, {
|
|
1683
1770
|
scope,
|
|
1684
1771
|
cwd: activeCtx.cwd,
|
|
1772
|
+
...(activeCtx.structuredOutput ? { structuredOutput: activeCtx.structuredOutput } : {}),
|
|
1685
1773
|
...(activeCtx.additionalDirectories
|
|
1686
1774
|
? { additionalDirectories: activeCtx.additionalDirectories }
|
|
1687
1775
|
: {}),
|
|
1688
1776
|
...(sessionsRef.current ? { stateRoot: sessionsRef.current.root } : {}),
|
|
1689
1777
|
enableComputerUse: true,
|
|
1690
1778
|
rules: activeCtx.rules,
|
|
1691
|
-
...(sessionsRef.current
|
|
1779
|
+
...(sessionsRef.current
|
|
1780
|
+
? { sessionGoals: sessionsRef.current.goals, conversationSessions: sessionsRef.current }
|
|
1781
|
+
: {}),
|
|
1692
1782
|
...(activeCtx.mcpServers ? { mcpServers: activeCtx.mcpServers } : {}),
|
|
1693
1783
|
...(activeCtx.plugins ? { plugins: activeCtx.plugins } : {}),
|
|
1694
1784
|
...(activeCtx.web ? { web: activeCtx.web } : {}),
|
|
1695
1785
|
...(activeCtx.hooks ? { hooks: activeCtx.hooks } : {}),
|
|
1696
1786
|
...(activeCtx.compaction ? { compaction: activeCtx.compaction } : {}),
|
|
1787
|
+
...(activeCtx.memory ? { memory: activeCtx.memory } : {}),
|
|
1697
1788
|
...(activeCtx.sandbox ? { sandbox: activeCtx.sandbox } : {}),
|
|
1698
1789
|
// Somebody is at this terminal, so the model may ask them one
|
|
1699
1790
|
// question when a decision is genuinely theirs.
|
|
1700
1791
|
askUser: true,
|
|
1792
|
+
allowModelSwitch: true,
|
|
1701
1793
|
});
|
|
1702
1794
|
if (signal?.aborted) {
|
|
1703
1795
|
void s.close();
|
|
@@ -1716,6 +1808,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1716
1808
|
}
|
|
1717
1809
|
let commands;
|
|
1718
1810
|
try {
|
|
1811
|
+
options.beforePublish?.();
|
|
1719
1812
|
commands = discoverUserCommands({ cwd: activeCtx.cwd, reserved: hostCommandNames() });
|
|
1720
1813
|
// Construction and validation precede the saved change. There is no
|
|
1721
1814
|
// await between this atomic write and publishing the usable session.
|
|
@@ -1741,9 +1834,16 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1741
1834
|
// Re-hydration (a provider switch via /model) builds a second session;
|
|
1742
1835
|
// without this the first one's tool-server child processes stay alive
|
|
1743
1836
|
// for the rest of the TUI's life.
|
|
1837
|
+
const previousInstructionFiles = previousSessionRef.current?.instructionFiles;
|
|
1744
1838
|
void previousSessionRef.current?.close();
|
|
1745
1839
|
previousSessionRef.current = s;
|
|
1746
1840
|
setSession(s);
|
|
1841
|
+
if (options.chooseReasoningEffort && s.reasoningEffortLevels?.length) {
|
|
1842
|
+
// Own input before publishing ready or releasing a paused queue. The
|
|
1843
|
+
// menu closes only after choosing an effort or keeping the new default.
|
|
1844
|
+
setSelectedChoice(0);
|
|
1845
|
+
setChoicePicker(reasoningEffortPicker(s, undefined, true) ?? null);
|
|
1846
|
+
}
|
|
1747
1847
|
setUserCommands(commands);
|
|
1748
1848
|
const mentionLoadOwner = {};
|
|
1749
1849
|
mentionLoadOwnerRef.current = mentionLoadOwner;
|
|
@@ -1760,13 +1860,11 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1760
1860
|
if (s.hasProvider) {
|
|
1761
1861
|
setTranscriptOwned(true);
|
|
1762
1862
|
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`);
|
|
1863
|
+
// Startup identity already lives in the footer. Confirm an explicit
|
|
1864
|
+
// picker selection, but do not add a routine connection log to chat.
|
|
1865
|
+
if (announce) {
|
|
1866
|
+
pushMessage('system', `Connected to ${s.providerSummary}${s.modelSummary ? ` · ${s.modelSummary}` : ''}`);
|
|
1867
|
+
}
|
|
1770
1868
|
// Before the rest: a limitation the operator accepted once and has
|
|
1771
1869
|
// been living with since is the thing they are least likely to
|
|
1772
1870
|
// remember and most likely to be surprised by.
|
|
@@ -1778,7 +1876,9 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1778
1876
|
// they did not expect — an instructions file in a parent directory
|
|
1779
1877
|
// they forgot about is exactly the thing that makes the agent
|
|
1780
1878
|
// behave oddly for no visible reason.
|
|
1781
|
-
if (s.instructionFiles.length > 0
|
|
1879
|
+
if (s.instructionFiles.length > 0 &&
|
|
1880
|
+
JSON.stringify([...s.instructionFiles].sort()) !==
|
|
1881
|
+
JSON.stringify(previousInstructionFiles ? [...previousInstructionFiles].sort() : undefined)) {
|
|
1782
1882
|
pushMessage('system', `Project instructions: ${s.instructionFiles.map((p) => relative(activeCtx.cwd, p) || p).join(', ')}`);
|
|
1783
1883
|
}
|
|
1784
1884
|
for (const skip of s.skippedInstructionFiles) {
|
|
@@ -1799,7 +1899,15 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1799
1899
|
if (s.errorHint)
|
|
1800
1900
|
pushMessage('system', s.errorHint);
|
|
1801
1901
|
}
|
|
1802
|
-
}, [
|
|
1902
|
+
}, [
|
|
1903
|
+
appLifetime.signal,
|
|
1904
|
+
advanceQueueContinuation,
|
|
1905
|
+
ensureSessions,
|
|
1906
|
+
pushMessage,
|
|
1907
|
+
setChoicePicker,
|
|
1908
|
+
setReasoningEffort,
|
|
1909
|
+
setSelectedChoice,
|
|
1910
|
+
]);
|
|
1803
1911
|
/**
|
|
1804
1912
|
* Sign in to a subscription without leaving namzu.
|
|
1805
1913
|
*
|
|
@@ -1835,7 +1943,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1835
1943
|
pushMessage('system', describeLoginOutcome(outcome));
|
|
1836
1944
|
if (outcome.ok) {
|
|
1837
1945
|
setPickerInitialView('providers');
|
|
1838
|
-
await runProbeRef.current?.();
|
|
1946
|
+
await runProbeRef.current?.({ announce: true });
|
|
1839
1947
|
}
|
|
1840
1948
|
return;
|
|
1841
1949
|
}
|
|
@@ -1894,7 +2002,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1894
2002
|
setPickerNotice(describeLoginOutcome(outcome));
|
|
1895
2003
|
if (outcome.ok) {
|
|
1896
2004
|
setPickerInitialView('providers');
|
|
1897
|
-
await runProbeRef.current?.(signal);
|
|
2005
|
+
await runProbeRef.current?.({ signal, announce: true });
|
|
1898
2006
|
}
|
|
1899
2007
|
return 'finished';
|
|
1900
2008
|
}
|
|
@@ -1954,11 +2062,12 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1954
2062
|
setPickerNotice(describeLoginOutcome(outcome));
|
|
1955
2063
|
if (outcome.ok) {
|
|
1956
2064
|
setPickerInitialView('providers');
|
|
1957
|
-
await runProbeRef.current?.(signal);
|
|
2065
|
+
await runProbeRef.current?.({ signal, announce: true });
|
|
1958
2066
|
}
|
|
1959
2067
|
return 'finished';
|
|
1960
2068
|
}, []);
|
|
1961
|
-
const runProbe = useCallback(async (
|
|
2069
|
+
const runProbe = useCallback(async (options = {}) => {
|
|
2070
|
+
const { signal, announce = false } = options;
|
|
1962
2071
|
try {
|
|
1963
2072
|
if (signal?.aborted)
|
|
1964
2073
|
return;
|
|
@@ -2011,7 +2120,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2011
2120
|
if (probe.preferences) {
|
|
2012
2121
|
setPickerDetected(null);
|
|
2013
2122
|
setPickerSelectionKind('provider-and-model');
|
|
2014
|
-
await hydrateSession(probe.preferences, probe.detected, signal);
|
|
2123
|
+
await hydrateSession(probe.preferences, probe.detected, { signal, announce });
|
|
2015
2124
|
return;
|
|
2016
2125
|
}
|
|
2017
2126
|
const signedIn = signedInSubscriptionProviders(probe.detected);
|
|
@@ -2029,7 +2138,10 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2029
2138
|
// not strand first run on the disabled unhealthy screen.
|
|
2030
2139
|
const admissionSignal = signal ?? appLifetime.signal;
|
|
2031
2140
|
try {
|
|
2032
|
-
await hydrateSession(automaticPreferences, probe.detected,
|
|
2141
|
+
await hydrateSession(automaticPreferences, probe.detected, {
|
|
2142
|
+
signal: admissionSignal,
|
|
2143
|
+
announce,
|
|
2144
|
+
});
|
|
2033
2145
|
return;
|
|
2034
2146
|
}
|
|
2035
2147
|
catch (error) {
|
|
@@ -2047,7 +2159,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2047
2159
|
setPickerDetected(signedIn);
|
|
2048
2160
|
setPickerSelectionKind('signed-in-subscription');
|
|
2049
2161
|
setPickerInitialView('providers');
|
|
2050
|
-
setPickerNotice('
|
|
2162
|
+
setPickerNotice('Multiple model sessions are already signed in on this device. Choose which one Namzu should use.');
|
|
2051
2163
|
setPhase('picker');
|
|
2052
2164
|
return;
|
|
2053
2165
|
}
|
|
@@ -2117,6 +2229,13 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2117
2229
|
// terminal scrollback; the live tail stays deliberately small so an activity
|
|
2118
2230
|
// tick never repaints the whole conversation.
|
|
2119
2231
|
//
|
|
2232
|
+
useEffect(() => {
|
|
2233
|
+
setOutputViewer(null);
|
|
2234
|
+
}, [resetKey]);
|
|
2235
|
+
useEffect(() => {
|
|
2236
|
+
if (permission !== null)
|
|
2237
|
+
setOutputViewer(null);
|
|
2238
|
+
}, [permission]);
|
|
2120
2239
|
// A ref, and mutated during render, because the split has to be MONOTONIC:
|
|
2121
2240
|
// a row that has been printed to scrollback can never come back, and `max`
|
|
2122
2241
|
// is idempotent, so a repeated render reaches the same answer.
|
|
@@ -2132,9 +2251,9 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2132
2251
|
// Freeze the parent's Static floor while it is open; otherwise a parent turn
|
|
2133
2252
|
// settling in the background prints through the child screen. Returning
|
|
2134
2253
|
// advances the floor once and emits those finalized rows exactly once.
|
|
2135
|
-
if (agentSurface === null)
|
|
2254
|
+
if (agentSurface === null && outputViewer === null)
|
|
2136
2255
|
settledRef.current = window.settled;
|
|
2137
|
-
const renderedSettled = agentSurface === null ? window.settled : settledRef.current;
|
|
2256
|
+
const renderedSettled = agentSurface === null && outputViewer === null ? window.settled : settledRef.current;
|
|
2138
2257
|
// One merged vocabulary for the session: this host's own commands plus
|
|
2139
2258
|
// whatever the kernel's registry reports. Built here so `/help`, the
|
|
2140
2259
|
// autocomplete and the dispatcher all answer from the same list — three
|
|
@@ -2200,6 +2319,9 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2200
2319
|
instructionFiles: session?.instructionFiles ?? [],
|
|
2201
2320
|
userCommands,
|
|
2202
2321
|
configDebug: ctx.configDebug ?? null,
|
|
2322
|
+
webSearchSummary: session?.webSearchSummary,
|
|
2323
|
+
columns: terminal.columns,
|
|
2324
|
+
sessionId: scopeRef.current?.sessionId,
|
|
2203
2325
|
};
|
|
2204
2326
|
// `/resume`: open the picker with this folder's recent conversations.
|
|
2205
2327
|
const doResume = useCallback(async () => {
|
|
@@ -2244,6 +2366,19 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2244
2366
|
setState('thinking');
|
|
2245
2367
|
resolve(decision);
|
|
2246
2368
|
}
|
|
2369
|
+
if (decision.kind === 'approve-all') {
|
|
2370
|
+
for (const pending of permissionQueueRef.current.splice(0))
|
|
2371
|
+
pending.resolve(decision);
|
|
2372
|
+
}
|
|
2373
|
+
const next = permissionQueueRef.current.shift();
|
|
2374
|
+
setQueuedPermissionCount(permissionQueueRef.current.length);
|
|
2375
|
+
if (next) {
|
|
2376
|
+
permissionResolveRef.current = next.resolve;
|
|
2377
|
+
permissionOpenedAtRef.current = Date.now();
|
|
2378
|
+
setPermissionDetailsOpen(!next.permission.summary.complete);
|
|
2379
|
+
setPermission(next.permission);
|
|
2380
|
+
setState('awaiting-permission');
|
|
2381
|
+
}
|
|
2247
2382
|
}, [setPermissionDetailsOpen, setPermissionReviewOffset]);
|
|
2248
2383
|
/**
|
|
2249
2384
|
* Stop the running turn, and hand the screen back in a usable state.
|
|
@@ -2264,11 +2399,19 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2264
2399
|
* sends at that prompt, for the same reason.
|
|
2265
2400
|
*/
|
|
2266
2401
|
const interruptTurn = useCallback(() => {
|
|
2402
|
+
const cancelledSwitch = cancelPendingModelSwitch();
|
|
2403
|
+
for (const pending of permissionQueueRef.current.splice(0)) {
|
|
2404
|
+
pending.resolve({ kind: 'reject', feedback: 'User interrupted.' });
|
|
2405
|
+
}
|
|
2406
|
+
setQueuedPermissionCount(0);
|
|
2267
2407
|
if (permissionResolveRef.current)
|
|
2268
2408
|
resolvePermission({ kind: 'reject', feedback: 'User interrupted.' });
|
|
2269
2409
|
const ac = abortRef.current;
|
|
2270
|
-
if (!ac)
|
|
2271
|
-
|
|
2410
|
+
if (!ac) {
|
|
2411
|
+
if (cancelledSwitch)
|
|
2412
|
+
discardQueued();
|
|
2413
|
+
return cancelledSwitch;
|
|
2414
|
+
}
|
|
2272
2415
|
ac.abort(new RunCancelled('user'));
|
|
2273
2416
|
const activeSessionId = scopeRef.current?.sessionId;
|
|
2274
2417
|
if (activeSessionId)
|
|
@@ -2284,7 +2427,14 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2284
2427
|
clearActiveTools();
|
|
2285
2428
|
setState('idle');
|
|
2286
2429
|
return true;
|
|
2287
|
-
}, [
|
|
2430
|
+
}, [
|
|
2431
|
+
cancelPendingModelSwitch,
|
|
2432
|
+
clearActiveTools,
|
|
2433
|
+
discardQueued,
|
|
2434
|
+
goalActivation,
|
|
2435
|
+
resolvePermission,
|
|
2436
|
+
wakeGoalDriver,
|
|
2437
|
+
]);
|
|
2288
2438
|
/**
|
|
2289
2439
|
* Load the chosen conversation into the transcript and continue in it.
|
|
2290
2440
|
*
|
|
@@ -2357,6 +2507,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2357
2507
|
discardQueued();
|
|
2358
2508
|
goalActivation.clear();
|
|
2359
2509
|
wakeGoalDriver();
|
|
2510
|
+
cancelPendingModelSwitch();
|
|
2360
2511
|
conversationGenRef.current += 1;
|
|
2361
2512
|
resetSubagentActivity();
|
|
2362
2513
|
session?.resetTaskStore?.();
|
|
@@ -2373,6 +2524,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2373
2524
|
lastCompletedOutputRef.current = persistedOutput
|
|
2374
2525
|
? { text: persistedOutput, provenance: 'persisted' }
|
|
2375
2526
|
: null;
|
|
2527
|
+
pendingGoalResumeRef.current = conv.id;
|
|
2376
2528
|
scope.sessionId = conv.id; // new turns now attribute to the resumed session
|
|
2377
2529
|
conversationMaterializedRef.current = true;
|
|
2378
2530
|
pushMessage('system', `Resumed: ${conv.title}`);
|
|
@@ -2393,6 +2545,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2393
2545
|
interruptTurn,
|
|
2394
2546
|
nextId,
|
|
2395
2547
|
pushMessage,
|
|
2548
|
+
cancelPendingModelSwitch,
|
|
2396
2549
|
resetSubagentActivity,
|
|
2397
2550
|
session,
|
|
2398
2551
|
resetTranscript,
|
|
@@ -2433,6 +2586,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2433
2586
|
discardQueued();
|
|
2434
2587
|
goalActivation.clear();
|
|
2435
2588
|
wakeGoalDriver();
|
|
2589
|
+
cancelPendingModelSwitch();
|
|
2436
2590
|
conversationGenRef.current += 1;
|
|
2437
2591
|
resetSubagentActivity();
|
|
2438
2592
|
session?.resetTaskStore?.();
|
|
@@ -2474,6 +2628,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2474
2628
|
goalActivation,
|
|
2475
2629
|
interruptTurn,
|
|
2476
2630
|
pushMessage,
|
|
2631
|
+
cancelPendingModelSwitch,
|
|
2477
2632
|
resetSubagentActivity,
|
|
2478
2633
|
session,
|
|
2479
2634
|
resetTranscript,
|
|
@@ -2722,6 +2877,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2722
2877
|
const readablePrefix = editList
|
|
2723
2878
|
.slice(0, target.userOrdinal)
|
|
2724
2879
|
.map((prompt) => prompt.displayText);
|
|
2880
|
+
cancelPendingModelSwitch();
|
|
2725
2881
|
conversationGenRef.current += 1;
|
|
2726
2882
|
resetSubagentActivity();
|
|
2727
2883
|
session?.resetTaskStore?.();
|
|
@@ -2765,6 +2921,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2765
2921
|
hasUnsettledTurn,
|
|
2766
2922
|
nextId,
|
|
2767
2923
|
pushMessage,
|
|
2924
|
+
cancelPendingModelSwitch,
|
|
2768
2925
|
resetSubagentActivity,
|
|
2769
2926
|
session,
|
|
2770
2927
|
resetTranscript,
|
|
@@ -2782,6 +2939,14 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2782
2939
|
}
|
|
2783
2940
|
const summary = buildPermissionSummary(review.text);
|
|
2784
2941
|
return new Promise((resolve) => {
|
|
2942
|
+
if (permissionResolveRef.current) {
|
|
2943
|
+
permissionQueueRef.current.push({
|
|
2944
|
+
permission: { ...req, review: review.text, summary },
|
|
2945
|
+
resolve,
|
|
2946
|
+
});
|
|
2947
|
+
setQueuedPermissionCount(permissionQueueRef.current.length);
|
|
2948
|
+
return;
|
|
2949
|
+
}
|
|
2785
2950
|
permissionResolveRef.current = resolve;
|
|
2786
2951
|
permissionOpenedAtRef.current = Date.now();
|
|
2787
2952
|
setPermissionReviewOffset(0);
|
|
@@ -2860,6 +3025,11 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2860
3025
|
}
|
|
2861
3026
|
switch (event.kind) {
|
|
2862
3027
|
case 'delta': {
|
|
3028
|
+
if (event.messageId && st.sourceMessageId !== event.messageId) {
|
|
3029
|
+
if (st.sourceMessageId !== undefined)
|
|
3030
|
+
closeAssistant();
|
|
3031
|
+
st.sourceMessageId = event.messageId;
|
|
3032
|
+
}
|
|
2863
3033
|
setState('thinking');
|
|
2864
3034
|
if (event.messageId && event.runId && st.sessionId) {
|
|
2865
3035
|
lastAssistantMessage.current = {
|
|
@@ -2904,11 +3074,19 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2904
3074
|
closeAssistant();
|
|
2905
3075
|
setThinking(null);
|
|
2906
3076
|
setState('tool');
|
|
3077
|
+
const waitingAgent = event.toolName === 'wait_for_task' && event.taskId
|
|
3078
|
+
? subagentsRef.current.find((agent) => agent.taskId === event.taskId &&
|
|
3079
|
+
(event.runId === undefined || agent.workflowId === event.runId))
|
|
3080
|
+
: undefined;
|
|
2907
3081
|
const tool = {
|
|
3082
|
+
...(waitingAgent ? { taskId: waitingAgent.taskId } : {}),
|
|
2908
3083
|
id: event.toolUseId,
|
|
2909
3084
|
...(event.runId ? { runId: event.runId } : {}),
|
|
2910
3085
|
toolName: event.toolName,
|
|
2911
|
-
|
|
3086
|
+
activity: event.activity,
|
|
3087
|
+
label: waitingAgent
|
|
3088
|
+
? `Waiting · ${waitingAgent.description}`
|
|
3089
|
+
: formatToolCall(event.toolName, event.summary, event.standalone),
|
|
2912
3090
|
startedAt: Date.now(),
|
|
2913
3091
|
detail: event.detail,
|
|
2914
3092
|
};
|
|
@@ -2952,6 +3130,15 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2952
3130
|
activeToolsRef.current = [...running.slice(0, i), ...running.slice(i + 1)];
|
|
2953
3131
|
setActiveTools(activeToolsRef.current);
|
|
2954
3132
|
}
|
|
3133
|
+
// Lifecycle updates own completion rows, even when the model never waits.
|
|
3134
|
+
// Keep unknown waits and errors visible; never hide an unrepresented task.
|
|
3135
|
+
const inspectableWait = done?.taskId && subagentsRef.current.some((agent) => agent.taskId === done.taskId &&
|
|
3136
|
+
(event.runId === undefined || agent.workflowId === event.runId) &&
|
|
3137
|
+
agent.transcript.some((row) => row.kind === 'assistant' && row.text.trim().length > 0));
|
|
3138
|
+
if (!event.isError && inspectableWait && event.toolName === 'wait_for_task') {
|
|
3139
|
+
setState(activeToolsRef.current.length > 0 ? 'tool' : 'thinking');
|
|
3140
|
+
break;
|
|
3141
|
+
}
|
|
2955
3142
|
const representedSuccessfulAgent = !event.isError &&
|
|
2956
3143
|
event.toolName.toLowerCase() === 'agent' &&
|
|
2957
3144
|
subagentsRef.current.some((agent) => agent.toolUseId === event.toolUseId &&
|
|
@@ -2964,6 +3151,22 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2964
3151
|
setState(activeToolsRef.current.length > 0 ? 'tool' : 'thinking');
|
|
2965
3152
|
break;
|
|
2966
3153
|
}
|
|
3154
|
+
if (!event.isError && done?.activity === 'exploration') {
|
|
3155
|
+
// Preserve the result, including the summary removed from detail by the adapter.
|
|
3156
|
+
const output = event.output !== undefined
|
|
3157
|
+
? event.output.split('\n')
|
|
3158
|
+
: [...(event.summary ? [event.summary] : []), ...(event.detail ?? [])];
|
|
3159
|
+
pushMessage('tool', done.label, false, '└', output, theme.text.muted, output.length > 0 ? 'ctrl+o output' : undefined, 'exploration');
|
|
3160
|
+
setState(activeToolsRef.current.length > 0 ? 'tool' : 'thinking');
|
|
3161
|
+
break;
|
|
3162
|
+
}
|
|
3163
|
+
const catalogue = !event.isError && event.toolName === 'agent_models' && event.output !== undefined
|
|
3164
|
+
? modelCatalogueView(event.output) : undefined;
|
|
3165
|
+
if (catalogue !== undefined) {
|
|
3166
|
+
pushMessage('tool', catalogue, false, '✓', event.output?.split('\n'), theme.status.ok, 'ctrl+o details', 'catalogue');
|
|
3167
|
+
setState(activeToolsRef.current.length > 0 ? 'tool' : 'thinking');
|
|
3168
|
+
break;
|
|
3169
|
+
}
|
|
2967
3170
|
pushMessage('tool', done?.label ?? formatToolCall(event.toolName, event.summary), false, event.isError ? '✗' : '✓', done?.detail, event.isError ? theme.status.error : theme.status.ok, event.durationMs !== undefined
|
|
2968
3171
|
? formatElapsed(event.durationMs)
|
|
2969
3172
|
: done
|
|
@@ -3066,12 +3269,12 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3066
3269
|
pushMessage('system', event.text, false, '⇄');
|
|
3067
3270
|
break;
|
|
3068
3271
|
case 'capability-warning':
|
|
3069
|
-
pushMessage('system', `Capability warning (${event.capability}${event.contentSource === 'tool-result' ? ' tool result' : ''}): ${event.text}`, false, '
|
|
3272
|
+
pushMessage('system', `Capability warning (${event.capability}${event.contentSource === 'tool-result' ? ' tool result' : ''}): ${event.text}`, false, '!');
|
|
3070
3273
|
break;
|
|
3071
3274
|
case 'history-repair':
|
|
3072
|
-
pushMessage('system', `History warning (${event.source}): ${event.text}`, false, '
|
|
3275
|
+
pushMessage('system', `History warning (${event.source}): ${event.text}`, false, '!');
|
|
3073
3276
|
break;
|
|
3074
|
-
case 'done':
|
|
3277
|
+
case 'done': {
|
|
3075
3278
|
// `run_completed` is not synonymous with success: budgets,
|
|
3076
3279
|
// cancellation and output guardrails arrive through this event too.
|
|
3077
3280
|
// Missing remains a normal end for older producers, matching the
|
|
@@ -3084,7 +3287,11 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3084
3287
|
outcome: st.completed ? 'completed' : 'stopped',
|
|
3085
3288
|
};
|
|
3086
3289
|
closeAssistant();
|
|
3290
|
+
const stopNotice = describeRunStop(event.stopReason);
|
|
3291
|
+
if (stopNotice)
|
|
3292
|
+
pushMessage('system', stopNotice, false, '■');
|
|
3087
3293
|
break;
|
|
3294
|
+
}
|
|
3088
3295
|
case 'paused':
|
|
3089
3296
|
// Paused is a recoverable SDK verdict, not success and not failure.
|
|
3090
3297
|
// Conversation evidence has no paused member, so it records the
|
|
@@ -3101,7 +3308,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3101
3308
|
st.outcome = event.message === 'aborted' ? 'cancelled' : 'failed';
|
|
3102
3309
|
if (event.message !== 'aborted') {
|
|
3103
3310
|
st.notification = { kind: 'turn-settled', outcome: 'failed' };
|
|
3104
|
-
pushMessage('system', describeRunInterruption(event), false, '
|
|
3311
|
+
pushMessage('system', describeRunInterruption(event), false, '!');
|
|
3105
3312
|
}
|
|
3106
3313
|
else
|
|
3107
3314
|
st.notification = null;
|
|
@@ -3109,6 +3316,14 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3109
3316
|
}
|
|
3110
3317
|
}, [appendToMessage, finalizeMessage, flushStream, pushMessage]);
|
|
3111
3318
|
const runTurn = useCallback(async (prompt) => {
|
|
3319
|
+
// A passive queue effect can still hold the previous render's session
|
|
3320
|
+
// while a replacement is being committed. Keep FIFO ownership until
|
|
3321
|
+
// the render and the resource-owning session agree.
|
|
3322
|
+
const pendingAtAdmission = pendingModelSwitchRef.current;
|
|
3323
|
+
if (pendingAtAdmission || previousSessionRef.current !== session) {
|
|
3324
|
+
replaceQueued([prompt, ...queuedRef.current]);
|
|
3325
|
+
return;
|
|
3326
|
+
}
|
|
3112
3327
|
if (!session || !session.hasProvider) {
|
|
3113
3328
|
pushMessage('system', session?.errorHint ?? 'Agent is not ready yet — give it a moment.');
|
|
3114
3329
|
return;
|
|
@@ -3232,6 +3447,72 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3232
3447
|
const turnGeneration = conversationGenRef.current;
|
|
3233
3448
|
unsettledTurnGenerationsRef.current.set(turnToken, turnGeneration);
|
|
3234
3449
|
const stillHere = () => conversationGenRef.current === turnGeneration;
|
|
3450
|
+
const requestModelSwitch = async (request, requestSignal) => {
|
|
3451
|
+
if (!currentProvider ||
|
|
3452
|
+
!stillHere() ||
|
|
3453
|
+
ac.signal.aborted ||
|
|
3454
|
+
requestSignal?.aborted ||
|
|
3455
|
+
appLifetime.signal.aborted ||
|
|
3456
|
+
activeTurnTokenRef.current !== turnToken ||
|
|
3457
|
+
previousSessionRef.current !== session) {
|
|
3458
|
+
return { kind: 'rejected', reason: 'The requesting turn is no longer active.' };
|
|
3459
|
+
}
|
|
3460
|
+
const accepted = pendingModelSwitchRef.current;
|
|
3461
|
+
if (accepted?.turnToken === turnToken &&
|
|
3462
|
+
!accepted.signal.aborted &&
|
|
3463
|
+
accepted.selection?.model === request.model.trim() &&
|
|
3464
|
+
(!request.provider || accepted.selection.id === request.provider.trim())) {
|
|
3465
|
+
return { kind: 'pending', selection: accepted.selection };
|
|
3466
|
+
}
|
|
3467
|
+
// A later request supersedes an earlier lookup even if its model list
|
|
3468
|
+
// resolves first. Cancellation never changes the active provider.
|
|
3469
|
+
cancelPendingModelSwitch();
|
|
3470
|
+
const controller = new AbortController();
|
|
3471
|
+
const pending = {
|
|
3472
|
+
turnToken,
|
|
3473
|
+
generation: turnGeneration,
|
|
3474
|
+
sessionId: scopeRef.current?.sessionId,
|
|
3475
|
+
ownerSession: session,
|
|
3476
|
+
controller,
|
|
3477
|
+
// A tool's lifetime can end before the turn. Once accepted, the
|
|
3478
|
+
// switch belongs to the turn and host, not that completed tool call.
|
|
3479
|
+
signal: AbortSignal.any([ac.signal, appLifetime.signal, controller.signal]),
|
|
3480
|
+
};
|
|
3481
|
+
pendingModelSwitchRef.current = pending;
|
|
3482
|
+
setModelSwitchVersion((version) => version + 1);
|
|
3483
|
+
try {
|
|
3484
|
+
const result = await resolveModelSwitch(request, {
|
|
3485
|
+
currentProvider,
|
|
3486
|
+
detected,
|
|
3487
|
+
describeModels: describeProviderModels,
|
|
3488
|
+
signal: requestSignal ? AbortSignal.any([pending.signal, requestSignal]) : pending.signal,
|
|
3489
|
+
});
|
|
3490
|
+
if (pendingModelSwitchRef.current !== pending ||
|
|
3491
|
+
pending.signal.aborted ||
|
|
3492
|
+
requestSignal?.aborted ||
|
|
3493
|
+
!stillHere() ||
|
|
3494
|
+
activeTurnTokenRef.current !== turnToken) {
|
|
3495
|
+
return { kind: 'rejected', reason: 'The model switch was cancelled or superseded.' };
|
|
3496
|
+
}
|
|
3497
|
+
if (result.kind === 'rejected')
|
|
3498
|
+
return result;
|
|
3499
|
+
pending.selection = result.selection;
|
|
3500
|
+
return { kind: 'pending', selection: result.selection };
|
|
3501
|
+
}
|
|
3502
|
+
catch (error) {
|
|
3503
|
+
return {
|
|
3504
|
+
kind: 'rejected',
|
|
3505
|
+
reason: pending.signal.aborted
|
|
3506
|
+
? 'The model switch was cancelled.'
|
|
3507
|
+
: `Could not resolve the requested model: ${error instanceof Error ? error.message : String(error)}`,
|
|
3508
|
+
};
|
|
3509
|
+
}
|
|
3510
|
+
finally {
|
|
3511
|
+
if (!pending.selection && pendingModelSwitchRef.current === pending) {
|
|
3512
|
+
cancelPendingModelSwitch();
|
|
3513
|
+
}
|
|
3514
|
+
}
|
|
3515
|
+
};
|
|
3235
3516
|
let inboxOpen = true;
|
|
3236
3517
|
const inboxEntries = [];
|
|
3237
3518
|
const inputWaiters = new Set();
|
|
@@ -3368,6 +3649,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3368
3649
|
// The mode above decides whether this callback is consulted.
|
|
3369
3650
|
onPermission: askPermission,
|
|
3370
3651
|
onQuestion: askQuestion,
|
|
3652
|
+
onModelSwitch: requestModelSwitch,
|
|
3371
3653
|
inboundMessages: () => inbox.drain(),
|
|
3372
3654
|
waitForInbound: (signal) => inbox.waitForInbound(signal),
|
|
3373
3655
|
extraSystem: [composeSkillsPrompt(activeSkills), drainIdleJobNotices(), drainOperatorShell()]
|
|
@@ -3480,6 +3762,10 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3480
3762
|
abortRef.current = null;
|
|
3481
3763
|
if (ownsTurn)
|
|
3482
3764
|
activeTurnTokenRef.current = null;
|
|
3765
|
+
permissionResolveRef.current?.({ kind: 'reject', feedback: 'The owning turn ended.' });
|
|
3766
|
+
for (const pending of permissionQueueRef.current.splice(0))
|
|
3767
|
+
pending.resolve({ kind: 'reject', feedback: 'The owning turn ended.' });
|
|
3768
|
+
setQueuedPermissionCount(0);
|
|
3483
3769
|
permissionResolveRef.current = null;
|
|
3484
3770
|
permissionOpenedAtRef.current = null;
|
|
3485
3771
|
setPermission(null);
|
|
@@ -3549,15 +3835,76 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3549
3835
|
// operation that sees no current-generation entries can now await that
|
|
3550
3836
|
// tail without a turn appearing behind its read later.
|
|
3551
3837
|
unsettledTurnGenerationsRef.current.delete(turnToken);
|
|
3838
|
+
const pending = pendingModelSwitchRef.current;
|
|
3839
|
+
if (pending?.turnToken === turnToken && pending.selection) {
|
|
3840
|
+
const selection = pending.selection;
|
|
3841
|
+
const ownsSwitch = () => pendingModelSwitchRef.current === pending &&
|
|
3842
|
+
conversationGenRef.current === pending.generation &&
|
|
3843
|
+
scopeRef.current?.sessionId === pending.sessionId &&
|
|
3844
|
+
previousSessionRef.current === pending.ownerSession &&
|
|
3845
|
+
!pending.signal.aborted;
|
|
3846
|
+
const beforePublish = () => {
|
|
3847
|
+
if (!ownsSwitch() ||
|
|
3848
|
+
conversationMutationRef.current ||
|
|
3849
|
+
hasUnsettledTurn(pending.generation) ||
|
|
3850
|
+
abortRef.current ||
|
|
3851
|
+
compactingRef.current ||
|
|
3852
|
+
exportingRef.current) {
|
|
3853
|
+
throw new Error('The active conversation changed before the model switch could complete.');
|
|
3854
|
+
}
|
|
3855
|
+
if (pending.ownerSession.subagents
|
|
3856
|
+
?.getSnapshot()
|
|
3857
|
+
.some((agent) => agent.status === 'starting' ||
|
|
3858
|
+
agent.status === 'queued' ||
|
|
3859
|
+
agent.status === 'working')) {
|
|
3860
|
+
throw new Error('Delegated agents are still active. Wait for them to finish before switching models.');
|
|
3861
|
+
}
|
|
3862
|
+
if (pending.ownerSession.jobs?.().some((job) => job.status === 'running')) {
|
|
3863
|
+
throw new Error('Background jobs are still running. Wait for them to finish before switching models.');
|
|
3864
|
+
}
|
|
3865
|
+
};
|
|
3866
|
+
try {
|
|
3867
|
+
await persistenceTailRef.current;
|
|
3868
|
+
beforePublish();
|
|
3869
|
+
await hydrateSession(selectPrimaryProvider(savedPrefsRef.current, selection), detected, {
|
|
3870
|
+
signal: pending.signal,
|
|
3871
|
+
persistSelection: false,
|
|
3872
|
+
beforePublish,
|
|
3873
|
+
});
|
|
3874
|
+
if (pendingModelSwitchRef.current === pending &&
|
|
3875
|
+
!pending.signal.aborted &&
|
|
3876
|
+
previousSessionRef.current !== pending.ownerSession) {
|
|
3877
|
+
pushMessage('system', `Switched to ${selection.id} · ${selection.model} for this conversation.`);
|
|
3878
|
+
}
|
|
3879
|
+
}
|
|
3880
|
+
catch (error) {
|
|
3881
|
+
if (ownsSwitch()) {
|
|
3882
|
+
pushMessage('system', `Model switch failed: ${error instanceof Error ? error.message : String(error)} The current model is unchanged.`);
|
|
3883
|
+
}
|
|
3884
|
+
}
|
|
3885
|
+
finally {
|
|
3886
|
+
if (pendingModelSwitchRef.current === pending) {
|
|
3887
|
+
pendingModelSwitchRef.current = null;
|
|
3888
|
+
setModelSwitchVersion((version) => version + 1);
|
|
3889
|
+
wakeGoalDriver();
|
|
3890
|
+
}
|
|
3891
|
+
}
|
|
3892
|
+
}
|
|
3552
3893
|
}
|
|
3553
3894
|
}, [
|
|
3554
3895
|
activeSkills,
|
|
3896
|
+
appLifetime.signal,
|
|
3555
3897
|
applyEvent,
|
|
3898
|
+
cancelPendingModelSwitch,
|
|
3556
3899
|
clearActiveTools,
|
|
3900
|
+
currentProvider,
|
|
3557
3901
|
ctx.cwd,
|
|
3902
|
+
detected,
|
|
3558
3903
|
finalizeMessage,
|
|
3559
3904
|
flushStream,
|
|
3560
3905
|
goalActivation,
|
|
3906
|
+
hasUnsettledTurn,
|
|
3907
|
+
hydrateSession,
|
|
3561
3908
|
materializeConversation,
|
|
3562
3909
|
onPermission,
|
|
3563
3910
|
pushMessage,
|
|
@@ -3567,6 +3914,109 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3567
3914
|
setQueuePause,
|
|
3568
3915
|
wakeGoalDriver,
|
|
3569
3916
|
]);
|
|
3917
|
+
const selectModelIntent = useCallback(async (query) => {
|
|
3918
|
+
if (!session ||
|
|
3919
|
+
!currentProvider ||
|
|
3920
|
+
state !== 'idle' ||
|
|
3921
|
+
phase !== 'ready' ||
|
|
3922
|
+
abortRef.current ||
|
|
3923
|
+
hasUnsettledTurn() ||
|
|
3924
|
+
pendingModelSwitchRef.current ||
|
|
3925
|
+
queuedRef.current.length > 0 ||
|
|
3926
|
+
permissionResolveRef.current ||
|
|
3927
|
+
compactingRef.current ||
|
|
3928
|
+
conversationMutationRef.current ||
|
|
3929
|
+
exportingRef.current ||
|
|
3930
|
+
goalDriveInFlightRef.current) {
|
|
3931
|
+
pushMessage('system', 'Model selection is unavailable until the active work and queue reach a stable boundary.');
|
|
3932
|
+
return;
|
|
3933
|
+
}
|
|
3934
|
+
const controller = new AbortController();
|
|
3935
|
+
const pending = {
|
|
3936
|
+
turnToken: {},
|
|
3937
|
+
generation: conversationGenRef.current,
|
|
3938
|
+
sessionId: scopeRef.current?.sessionId,
|
|
3939
|
+
ownerSession: session,
|
|
3940
|
+
controller,
|
|
3941
|
+
signal: AbortSignal.any([appLifetime.signal, controller.signal]),
|
|
3942
|
+
};
|
|
3943
|
+
pendingModelSwitchRef.current = pending;
|
|
3944
|
+
setModelSwitchVersion((version) => version + 1);
|
|
3945
|
+
const owns = () => pendingModelSwitchRef.current === pending &&
|
|
3946
|
+
!pending.signal.aborted &&
|
|
3947
|
+
conversationGenRef.current === pending.generation &&
|
|
3948
|
+
scopeRef.current?.sessionId === pending.sessionId &&
|
|
3949
|
+
previousSessionRef.current === session;
|
|
3950
|
+
const beforePublish = () => {
|
|
3951
|
+
if (!owns() ||
|
|
3952
|
+
abortRef.current ||
|
|
3953
|
+
hasUnsettledTurn() ||
|
|
3954
|
+
conversationMutationRef.current ||
|
|
3955
|
+
compactingRef.current ||
|
|
3956
|
+
exportingRef.current ||
|
|
3957
|
+
permissionResolveRef.current ||
|
|
3958
|
+
session.jobs?.().some((job) => job.status === 'running') ||
|
|
3959
|
+
session.subagents
|
|
3960
|
+
?.getSnapshot()
|
|
3961
|
+
.some((agent) => ['starting', 'queued', 'working'].includes(agent.status))) {
|
|
3962
|
+
throw new Error('Active work or the conversation changed before model selection could complete.');
|
|
3963
|
+
}
|
|
3964
|
+
};
|
|
3965
|
+
try {
|
|
3966
|
+
beforePublish();
|
|
3967
|
+
pushMessage('system', `Model requested: ${query}. Checking available catalogues…`);
|
|
3968
|
+
const resolved = await resolveModelSelectionIntent(query, {
|
|
3969
|
+
currentProvider,
|
|
3970
|
+
detected,
|
|
3971
|
+
describeModels: describeProviderModels,
|
|
3972
|
+
signal: pending.signal,
|
|
3973
|
+
});
|
|
3974
|
+
if (!owns())
|
|
3975
|
+
return;
|
|
3976
|
+
if (resolved.kind === 'rejected') {
|
|
3977
|
+
pushMessage('system', resolved.reason +
|
|
3978
|
+
(resolved.choices?.length
|
|
3979
|
+
? ` Choices: ${resolved.choices.map((choice) => `${choice.provider}/${choice.model}`).join(', ')}.`
|
|
3980
|
+
: ''));
|
|
3981
|
+
return;
|
|
3982
|
+
}
|
|
3983
|
+
pending.selection = resolved.selection;
|
|
3984
|
+
await persistenceTailRef.current;
|
|
3985
|
+
beforePublish();
|
|
3986
|
+
await hydrateSession(selectPrimaryProvider(savedPrefsRef.current, resolved.selection), detected, {
|
|
3987
|
+
signal: pending.signal,
|
|
3988
|
+
persistSelection: false,
|
|
3989
|
+
beforePublish,
|
|
3990
|
+
});
|
|
3991
|
+
if (pendingModelSwitchRef.current === pending &&
|
|
3992
|
+
!pending.signal.aborted &&
|
|
3993
|
+
previousSessionRef.current !== session) {
|
|
3994
|
+
pushMessage('system', `Switched to ${resolved.selection.id} · ${resolved.selection.model} for this conversation.`);
|
|
3995
|
+
}
|
|
3996
|
+
}
|
|
3997
|
+
catch (error) {
|
|
3998
|
+
if (owns())
|
|
3999
|
+
pushMessage('system', `Model selection failed: ${error instanceof Error ? error.message : String(error)} The current model is unchanged.`);
|
|
4000
|
+
}
|
|
4001
|
+
finally {
|
|
4002
|
+
if (pendingModelSwitchRef.current === pending) {
|
|
4003
|
+
pendingModelSwitchRef.current = null;
|
|
4004
|
+
setModelSwitchVersion((version) => version + 1);
|
|
4005
|
+
wakeGoalDriver();
|
|
4006
|
+
}
|
|
4007
|
+
}
|
|
4008
|
+
}, [
|
|
4009
|
+
session,
|
|
4010
|
+
currentProvider,
|
|
4011
|
+
state,
|
|
4012
|
+
phase,
|
|
4013
|
+
hasUnsettledTurn,
|
|
4014
|
+
appLifetime.signal,
|
|
4015
|
+
pushMessage,
|
|
4016
|
+
detected,
|
|
4017
|
+
hydrateSession,
|
|
4018
|
+
wakeGoalDriver,
|
|
4019
|
+
]);
|
|
3570
4020
|
const handleSubmit = useCallback((value, attachments, mode = 'submit') => {
|
|
3571
4021
|
if (goalCommandInFlightRef.current) {
|
|
3572
4022
|
pushMessage('system', 'A goal command is still reaching durable session state. Wait for its result before sending another command or prompt.');
|
|
@@ -3590,14 +4040,19 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3590
4040
|
return;
|
|
3591
4041
|
}
|
|
3592
4042
|
setHistory((prev) => [...prev, value]);
|
|
4043
|
+
const selectionIntent = !attachments?.length ? parseModelSelectionIntent(value) : undefined;
|
|
4044
|
+
if (selectionIntent) {
|
|
4045
|
+
void selectModelIntent(selectionIntent.query);
|
|
4046
|
+
return;
|
|
4047
|
+
}
|
|
3593
4048
|
// `#` remembers, `!` runs — neither is a prompt. Both are the
|
|
3594
4049
|
// operator acting directly, the way other coding agents spell it,
|
|
3595
4050
|
// and both leave a row the model reads on its next turn.
|
|
3596
4051
|
if (value.startsWith('#') && value.slice(1).trim().length > 0) {
|
|
3597
4052
|
const note = value.slice(1).trim();
|
|
3598
4053
|
try {
|
|
3599
|
-
const
|
|
3600
|
-
pushMessage('system',
|
|
4054
|
+
const saved = appendMemoryWithStatus(note, { scope: 'project', cwd: ctx.cwd });
|
|
4055
|
+
pushMessage('system', renderMemorySaveResult(saved, note));
|
|
3601
4056
|
}
|
|
3602
4057
|
catch (err) {
|
|
3603
4058
|
pushMessage('system', `Could not save memory: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -3634,7 +4089,16 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3634
4089
|
if (slash) {
|
|
3635
4090
|
switch (slash.kind) {
|
|
3636
4091
|
case 'message':
|
|
3637
|
-
|
|
4092
|
+
if (slash.statusRows) {
|
|
4093
|
+
setMessages((previous) => [...previous, {
|
|
4094
|
+
id: nextId(),
|
|
4095
|
+
role: slash.role,
|
|
4096
|
+
content: slash.content,
|
|
4097
|
+
statusRows: slash.statusRows,
|
|
4098
|
+
}]);
|
|
4099
|
+
}
|
|
4100
|
+
else
|
|
4101
|
+
pushMessage(slash.role, slash.content);
|
|
3638
4102
|
return;
|
|
3639
4103
|
case 'command-picker': {
|
|
3640
4104
|
if (slash.commands.length === 0) {
|
|
@@ -3664,6 +4128,13 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3664
4128
|
}
|
|
3665
4129
|
return;
|
|
3666
4130
|
}
|
|
4131
|
+
case 'provider-setup':
|
|
4132
|
+
if (state !== 'idle' || permission || choicePickerRef.current) {
|
|
4133
|
+
pushMessage('system', 'Provider setup is available once the active turn and prompts finish.');
|
|
4134
|
+
return;
|
|
4135
|
+
}
|
|
4136
|
+
setProviderSetup(true);
|
|
4137
|
+
return;
|
|
3667
4138
|
case 'settings-picker': {
|
|
3668
4139
|
const commands = [
|
|
3669
4140
|
{
|
|
@@ -3685,16 +4156,22 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3685
4156
|
description: `${permissionModeLabel(effectivePermissionMode(permissionModeRef.current, session?.approvalLatched() ?? false))} · this session.`,
|
|
3686
4157
|
},
|
|
3687
4158
|
{
|
|
3688
|
-
name: 'status
|
|
3689
|
-
label: '
|
|
4159
|
+
name: 'status',
|
|
4160
|
+
label: 'Web & session',
|
|
4161
|
+
description: `${session?.webSearchSummary ?? 'Not resolved'} · view tools, workspace and usage.`,
|
|
4162
|
+
},
|
|
4163
|
+
{ name: 'setup', label: 'Provider setup', description: 'Check installed CLIs and access; connect or install without leaving Namzu.' },
|
|
4164
|
+
{
|
|
4165
|
+
name: 'config sources',
|
|
4166
|
+
label: 'Setting sources',
|
|
3690
4167
|
description: 'Show which configuration files and overrides are in use.',
|
|
3691
4168
|
},
|
|
3692
4169
|
];
|
|
3693
4170
|
setSelectedChoice(0);
|
|
3694
4171
|
setChoicePicker({
|
|
3695
4172
|
kind: 'command',
|
|
3696
|
-
title: '
|
|
3697
|
-
notice: '
|
|
4173
|
+
title: 'Configuration',
|
|
4174
|
+
notice: 'Model, reasoning and permission changes apply to this session.',
|
|
3698
4175
|
values: commands,
|
|
3699
4176
|
options: commands.map((command) => ({
|
|
3700
4177
|
label: command.label,
|
|
@@ -3866,6 +4343,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3866
4343
|
return;
|
|
3867
4344
|
case 'repick':
|
|
3868
4345
|
if (state !== 'idle' ||
|
|
4346
|
+
pendingModelSwitchRef.current ||
|
|
3869
4347
|
abortRef.current !== null ||
|
|
3870
4348
|
hasUnsettledTurn() ||
|
|
3871
4349
|
(queuedRef.current.length > 0 && queuePauseRef.current === null) ||
|
|
@@ -3934,17 +4412,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3934
4412
|
const values = [undefined, ...levels];
|
|
3935
4413
|
const current = reasoningEffortRef.current;
|
|
3936
4414
|
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
|
-
});
|
|
4415
|
+
setChoicePicker(reasoningEffortPicker(session, current) ?? null);
|
|
3948
4416
|
return;
|
|
3949
4417
|
}
|
|
3950
4418
|
case 'login':
|
|
@@ -3984,8 +4452,8 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3984
4452
|
}
|
|
3985
4453
|
case 'remember':
|
|
3986
4454
|
try {
|
|
3987
|
-
const
|
|
3988
|
-
pushMessage('system',
|
|
4455
|
+
const saved = appendMemoryWithStatus(slash.text, { scope: slash.scope, cwd: ctx.cwd });
|
|
4456
|
+
pushMessage('system', renderMemorySaveResult(saved, slash.text));
|
|
3989
4457
|
}
|
|
3990
4458
|
catch (err) {
|
|
3991
4459
|
pushMessage('system', `Could not save memory: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -4011,7 +4479,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4011
4479
|
// Dropping it silently would leave someone wondering where a
|
|
4012
4480
|
// file they can see on disk went.
|
|
4013
4481
|
s.problem
|
|
4014
|
-
?
|
|
4482
|
+
? `! ${s.name} — ${s.problem}`
|
|
4015
4483
|
: `${activeNames.has(s.name) ? '● ' : '○ '}${s.name} — ${s.description}`);
|
|
4016
4484
|
pushMessage('system', `Skills (● active):\n ${lines.join('\n ')}`);
|
|
4017
4485
|
return;
|
|
@@ -4425,6 +4893,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4425
4893
|
resetTranscript,
|
|
4426
4894
|
runConversationExport,
|
|
4427
4895
|
session,
|
|
4896
|
+
selectModelIntent,
|
|
4428
4897
|
setChoicePicker,
|
|
4429
4898
|
setCopyPicker,
|
|
4430
4899
|
setReasoningEffort,
|
|
@@ -4462,6 +4931,19 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4462
4931
|
return;
|
|
4463
4932
|
}
|
|
4464
4933
|
setGoalStatus(goal);
|
|
4934
|
+
if (pendingGoalResumeRef.current === sessionId && phase === 'ready' && !choicePickerRef.current) {
|
|
4935
|
+
pendingGoalResumeRef.current = null;
|
|
4936
|
+
if (goal && (goal.phase === 'active' || goal.phase === 'paused')) {
|
|
4937
|
+
setSelectedChoice(0);
|
|
4938
|
+
setChoicePicker({ kind: 'resume-goal', title: 'Continue the saved goal?',
|
|
4939
|
+
notice: goal.objective, sessionId, values: ['resume', 'later'],
|
|
4940
|
+
options: [
|
|
4941
|
+
{ label: 'Resume goal', description: 'Continue automatic work toward this objective.' },
|
|
4942
|
+
{ label: 'Not now', description: 'Keep automatic work stopped; resume later with /goal resume.' },
|
|
4943
|
+
],
|
|
4944
|
+
});
|
|
4945
|
+
}
|
|
4946
|
+
}
|
|
4465
4947
|
})
|
|
4466
4948
|
.catch(() => {
|
|
4467
4949
|
if (!disposed && conversationGenRef.current === generation)
|
|
@@ -4470,7 +4952,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4470
4952
|
return () => {
|
|
4471
4953
|
disposed = true;
|
|
4472
4954
|
};
|
|
4473
|
-
}, [goalDriveVersion, phase, session]);
|
|
4955
|
+
}, [goalDriveVersion, phase, session, setChoicePicker]);
|
|
4474
4956
|
// Admit automatic work only at a whole-App durable boundary. This effect
|
|
4475
4957
|
// reserves; it never starts a turn itself. The one queue pump below remains
|
|
4476
4958
|
// the sole starter, which is what keeps a human prompt that arrives during
|
|
@@ -4488,6 +4970,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4488
4970
|
queuedRef.current.length > 0 ||
|
|
4489
4971
|
abortRef.current ||
|
|
4490
4972
|
hasUnsettledTurn() ||
|
|
4973
|
+
pendingModelSwitchRef.current ||
|
|
4491
4974
|
conversationMutationRef.current ||
|
|
4492
4975
|
exportingRef.current ||
|
|
4493
4976
|
compactingRef.current ||
|
|
@@ -4515,6 +4998,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4515
4998
|
state !== 'idle' ||
|
|
4516
4999
|
abortRef.current ||
|
|
4517
5000
|
hasUnsettledTurn(generation) ||
|
|
5001
|
+
pendingModelSwitchRef.current ||
|
|
4518
5002
|
conversationMutationRef.current ||
|
|
4519
5003
|
exportingRef.current ||
|
|
4520
5004
|
compactingRef.current ||
|
|
@@ -4540,6 +5024,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4540
5024
|
// or goal command is different: it revoked this scheduling boundary.
|
|
4541
5025
|
if (conversationGenRef.current !== generation ||
|
|
4542
5026
|
scopeRef.current?.sessionId !== sessionId ||
|
|
5027
|
+
pendingModelSwitchRef.current ||
|
|
4543
5028
|
conversationMutationRef.current ||
|
|
4544
5029
|
goalCommandInFlightRef.current ||
|
|
4545
5030
|
!goalActivation.isArmed(sessionId, armed)) {
|
|
@@ -4572,6 +5057,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4572
5057
|
enqueueQueued,
|
|
4573
5058
|
goalActivation,
|
|
4574
5059
|
goalDriveVersion,
|
|
5060
|
+
modelSwitchVersion,
|
|
4575
5061
|
hasUnsettledTurn,
|
|
4576
5062
|
phase,
|
|
4577
5063
|
pushMessage,
|
|
@@ -4592,6 +5078,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4592
5078
|
queuePauseRef.current !== null ||
|
|
4593
5079
|
abortRef.current ||
|
|
4594
5080
|
hasUnsettledTurn() ||
|
|
5081
|
+
pendingModelSwitchRef.current ||
|
|
4595
5082
|
conversationMutationRef.current ||
|
|
4596
5083
|
exportingRef.current ||
|
|
4597
5084
|
compactingRef.current ||
|
|
@@ -4611,6 +5098,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4611
5098
|
state,
|
|
4612
5099
|
phase,
|
|
4613
5100
|
queued,
|
|
5101
|
+
modelSwitchVersion,
|
|
4614
5102
|
queuePause,
|
|
4615
5103
|
textPrompt,
|
|
4616
5104
|
choicePicker,
|
|
@@ -4635,7 +5123,12 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4635
5123
|
}, [phase, ctx.version, pushMessage]);
|
|
4636
5124
|
const hydrateFromPicker = useCallback(async (prefs, detectedNow, signal, options = {}) => {
|
|
4637
5125
|
try {
|
|
4638
|
-
await hydrateSession(prefs, detectedNow,
|
|
5126
|
+
await hydrateSession(prefs, detectedNow, {
|
|
5127
|
+
signal,
|
|
5128
|
+
persistSelection: options.persistSelection,
|
|
5129
|
+
announce: true,
|
|
5130
|
+
chooseReasoningEffort: options.chooseReasoningEffort,
|
|
5131
|
+
});
|
|
4639
5132
|
}
|
|
4640
5133
|
catch (err) {
|
|
4641
5134
|
// A superseded choice no longer owns even its failure message. Its
|
|
@@ -4687,6 +5180,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4687
5180
|
// Only a real refusal broadens back to the general, sign-in-capable picker.
|
|
4688
5181
|
void hydrateFromPicker(prefs, detected, signal, {
|
|
4689
5182
|
revealAllOnFailure: true,
|
|
5183
|
+
chooseReasoningEffort: selection.model !== undefined,
|
|
4690
5184
|
persistSelection: detected.find((provider) => provider.entry.id === selection.provider)?.source.kind !==
|
|
4691
5185
|
'session',
|
|
4692
5186
|
});
|
|
@@ -4720,6 +5214,8 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4720
5214
|
exitWithSummary();
|
|
4721
5215
|
}, [session, exitWithSummary]);
|
|
4722
5216
|
useInput((input, key) => {
|
|
5217
|
+
if (outputViewer !== null)
|
|
5218
|
+
return;
|
|
4723
5219
|
// Startup has refused admission, so there is no draft or active turn to
|
|
4724
5220
|
// protect with the ready screen's two-press exit ladder.
|
|
4725
5221
|
if (phase === 'unhealthy') {
|
|
@@ -4840,9 +5336,9 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4840
5336
|
if (permission && (key.pageUp || key.pageDown || key.home || key.end)) {
|
|
4841
5337
|
const source = permissionDetailsOpenRef.current
|
|
4842
5338
|
? permission.review
|
|
4843
|
-
: permission.summary.text;
|
|
5339
|
+
: (permission.summary.compactText ?? permission.summary.text);
|
|
4844
5340
|
const count = permissionReviewRows(source, terminal.columns).length;
|
|
4845
|
-
const pageRows = permissionReviewPageRows(terminal.rows);
|
|
5341
|
+
const pageRows = Math.max(1, permissionReviewPageRows(terminal.rows) - (permission.runId ? 1 : 0));
|
|
4846
5342
|
const maxOffset = Math.max(0, count - pageRows);
|
|
4847
5343
|
const current = Math.min(permissionReviewOffsetRef.current, maxOffset);
|
|
4848
5344
|
const next = key.home
|
|
@@ -4858,8 +5354,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4858
5354
|
// Refusing is never deferred or gated — it is the direction a
|
|
4859
5355
|
// mistake is recoverable in, so it answers on the first press.
|
|
4860
5356
|
if (key.ctrl && input === 'c') {
|
|
4861
|
-
|
|
4862
|
-
abortRef.current?.abort(new RunCancelled('user'));
|
|
5357
|
+
interruptTurn();
|
|
4863
5358
|
return;
|
|
4864
5359
|
}
|
|
4865
5360
|
if (ch === 'n' || key.escape) {
|
|
@@ -4905,7 +5400,10 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4905
5400
|
return;
|
|
4906
5401
|
const agents = subagentsRef.current;
|
|
4907
5402
|
if (agentView.kind === 'cockpit') {
|
|
4908
|
-
const
|
|
5403
|
+
const workflows = agentWorkflows(agents);
|
|
5404
|
+
const workflowIndex = Math.max(0, workflows.findIndex((workflow) => workflow.phases.some((phase) => phase.id === agentView.selectedPhaseId)));
|
|
5405
|
+
const workflow = workflows[workflowIndex];
|
|
5406
|
+
const phases = workflow?.phases ?? [];
|
|
4909
5407
|
const phaseIndex = Math.max(0, phases.findIndex((phase) => phase.id === agentView.selectedPhaseId));
|
|
4910
5408
|
const phase = phases[phaseIndex];
|
|
4911
5409
|
const phaseAgents = phase?.agents ?? [];
|
|
@@ -4915,11 +5413,27 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4915
5413
|
setAgentSurface(null);
|
|
4916
5414
|
return;
|
|
4917
5415
|
}
|
|
4918
|
-
if (
|
|
5416
|
+
if ((key.ctrl && input === 'c') || input.toLowerCase() === 'q') {
|
|
4919
5417
|
setAgentSurface(null);
|
|
4920
5418
|
return;
|
|
4921
5419
|
}
|
|
5420
|
+
if (key.escape) {
|
|
5421
|
+
setAgentSurface(workflows.length > 1 && agentView.focus !== 'workflows'
|
|
5422
|
+
? { ...agentView, focus: 'workflows' }
|
|
5423
|
+
: null);
|
|
5424
|
+
return;
|
|
5425
|
+
}
|
|
4922
5426
|
if (key.leftArrow || key.rightArrow || key.tab) {
|
|
5427
|
+
if (agentView.focus === 'workflows') {
|
|
5428
|
+
if (!key.leftArrow) {
|
|
5429
|
+
setAgentSurface({ ...agentView, focus: phases.length > 1 ? 'phases' : 'agents' });
|
|
5430
|
+
}
|
|
5431
|
+
return;
|
|
5432
|
+
}
|
|
5433
|
+
if (key.leftArrow && agentView.focus === 'phases' && workflows.length > 1) {
|
|
5434
|
+
setAgentSurface({ ...agentView, focus: 'workflows' });
|
|
5435
|
+
return;
|
|
5436
|
+
}
|
|
4923
5437
|
const focus = key.leftArrow
|
|
4924
5438
|
? 'phases'
|
|
4925
5439
|
: key.rightArrow
|
|
@@ -4931,6 +5445,10 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4931
5445
|
return;
|
|
4932
5446
|
}
|
|
4933
5447
|
if (key.return) {
|
|
5448
|
+
if (agentView.focus === 'workflows') {
|
|
5449
|
+
setAgentSurface({ ...agentView, focus: phases.length > 1 ? 'phases' : 'agents' });
|
|
5450
|
+
return;
|
|
5451
|
+
}
|
|
4934
5452
|
if (agentView.focus === 'phases') {
|
|
4935
5453
|
setAgentSurface({ ...agentView, focus: 'agents' });
|
|
4936
5454
|
return;
|
|
@@ -4956,7 +5474,19 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4956
5474
|
: key.upArrow
|
|
4957
5475
|
? 'previous'
|
|
4958
5476
|
: 'next';
|
|
4959
|
-
if (agentView.focus === '
|
|
5477
|
+
if (agentView.focus === 'workflows') {
|
|
5478
|
+
const next = moveSelection(workflowIndex, workflows.length, movement, agentWorkflowPageSize(terminal.rows));
|
|
5479
|
+
const targetPhase = workflows[next]?.phases[0];
|
|
5480
|
+
const targetAgent = targetPhase?.agents[0];
|
|
5481
|
+
if (targetPhase && targetAgent) {
|
|
5482
|
+
setAgentSurface({
|
|
5483
|
+
...agentView,
|
|
5484
|
+
selectedPhaseId: targetPhase.id,
|
|
5485
|
+
selectedId: targetAgent.viewId,
|
|
5486
|
+
});
|
|
5487
|
+
}
|
|
5488
|
+
}
|
|
5489
|
+
else if (agentView.focus === 'phases') {
|
|
4960
5490
|
const next = moveSelection(phaseIndex, phases.length, movement, agentPhasePageSize(terminal.rows, agentCockpitIsWide(terminal.columns)));
|
|
4961
5491
|
const targetPhase = phases[next];
|
|
4962
5492
|
const targetAgent = targetPhase?.agents[0];
|
|
@@ -5139,69 +5669,32 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
5139
5669
|
}
|
|
5140
5670
|
// Esc interrupts a running turn (Ctrl+C stays reserved for exit). Mirrors
|
|
5141
5671
|
// the Ctrl+C interrupt path: abort, drop the queue, one "Interrupted." line.
|
|
5142
|
-
if (key.escape && abortRef.current) {
|
|
5672
|
+
if (key.escape && (abortRef.current || pendingModelSwitchRef.current)) {
|
|
5143
5673
|
interruptTurn();
|
|
5144
5674
|
pushMessage('system', 'Interrupted.');
|
|
5145
5675
|
return;
|
|
5146
5676
|
}
|
|
5147
|
-
//
|
|
5148
|
-
//
|
|
5149
|
-
// It was advertised — on every collapsed body — as toggling full
|
|
5150
|
-
// expansion for everything, and in that use it did nothing: finalized
|
|
5151
|
-
// rows went through `<Static>`, which renders `items.slice(index)` and
|
|
5152
|
-
// calls the render function only for items it has not emitted yet, so
|
|
5153
|
-
// output already on screen was beyond its reach. Measured, with a
|
|
5154
|
-
// twelve-line body up: pressing it produced one further frame whose
|
|
5155
|
-
// transcript region was byte-identical to the one before.
|
|
5156
|
-
//
|
|
5157
|
-
// The rows at the end of the transcript are now drawn live rather than
|
|
5158
|
-
// printed once, so for those the key does what it always claimed —
|
|
5159
|
-
// flipping the flag re-renders the row where it already is. It reaches
|
|
5160
|
-
// exactly as far back as the window does, and says so when that is
|
|
5161
|
-
// nowhere. It does not quietly fall back to appending a copy: `/expand`
|
|
5162
|
-
// is that, deliberately and visibly, and a key that sometimes redraws
|
|
5163
|
-
// in place and sometimes prints a second copy further down would be two
|
|
5164
|
-
// commands wearing one name.
|
|
5677
|
+
// Small expansions stay in place. Older or oversized bodies open a viewer.
|
|
5165
5678
|
if (key.ctrl && input === 'o') {
|
|
5166
|
-
|
|
5167
|
-
const
|
|
5168
|
-
|
|
5169
|
-
|
|
5170
|
-
|
|
5171
|
-
|
|
5172
|
-
|
|
5173
|
-
|
|
5174
|
-
|
|
5175
|
-
|
|
5176
|
-
|
|
5177
|
-
|
|
5178
|
-
|
|
5179
|
-
|
|
5180
|
-
|
|
5181
|
-
|
|
5182
|
-
|
|
5183
|
-
|
|
5184
|
-
|
|
5185
|
-
];
|
|
5186
|
-
}
|
|
5187
|
-
return [
|
|
5188
|
-
...prev,
|
|
5189
|
-
{
|
|
5190
|
-
id,
|
|
5191
|
-
role: 'system',
|
|
5192
|
-
content: `${block.content} — in full (${block.detail?.length ?? 0} lines)`,
|
|
5193
|
-
glyph: '⤢',
|
|
5194
|
-
detail: block.detail,
|
|
5195
|
-
detailExpanded: true,
|
|
5196
|
-
},
|
|
5197
|
-
];
|
|
5198
|
-
}
|
|
5199
|
-
// Expanded unless they are all open already, in which case this
|
|
5200
|
-
// closes them again — the toggle it was always described as.
|
|
5201
|
-
const expanding = collapsible.some((m) => m.detailExpanded !== true);
|
|
5202
|
-
const ids = new Set(collapsible.map((m) => m.id));
|
|
5203
|
-
return prev.map((m) => (ids.has(m.id) ? { ...m, detailExpanded: expanding } : m));
|
|
5204
|
-
});
|
|
5679
|
+
const live = messages.filter((m) => !m.pending).slice(settledRef.current);
|
|
5680
|
+
const collapsible = live.filter((m) => (m.activity && (m.detail?.length ?? 0) > 0) || willCollapse(m.detail));
|
|
5681
|
+
const blocks = messages.filter((m) => m.detailRef !== undefined && (m.detail?.length ?? 0) > 0);
|
|
5682
|
+
const block = collapsible.at(-1) ?? blocks.at(-1);
|
|
5683
|
+
if (!block) {
|
|
5684
|
+
pushMessage('system', 'Nothing to expand yet. Ctrl+O opens retained tool output when available.');
|
|
5685
|
+
return;
|
|
5686
|
+
}
|
|
5687
|
+
const expanding = collapsible.some((m) => m.detailExpanded !== true);
|
|
5688
|
+
const ids = new Set(collapsible.map((m) => m.id));
|
|
5689
|
+
const proposed = messages.map((m) => ids.has(m.id) ? { ...m, detailExpanded: expanding } : m);
|
|
5690
|
+
const projected = liveWindow({ messages: proposed.filter((m) => !m.pending), rows: terminal.rows,
|
|
5691
|
+
columns: terminal.columns, furnitureRows: LIVE_FURNITURE_ROWS + taskFurniture + toolFurniture,
|
|
5692
|
+
settled: settledRef.current, raw: rawOutput });
|
|
5693
|
+
if (collapsible.length === 0 || (expanding && projected.settled > settledRef.current)) {
|
|
5694
|
+
setOutputViewer(block);
|
|
5695
|
+
}
|
|
5696
|
+
else
|
|
5697
|
+
setMessages(proposed);
|
|
5205
5698
|
return;
|
|
5206
5699
|
}
|
|
5207
5700
|
if (key.ctrl && input === 'c') {
|
|
@@ -5226,7 +5719,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
5226
5719
|
// Active on every phase. It was gated off during the picker, which is what
|
|
5227
5720
|
// left that screen without a usable exit; the picker branch above returns
|
|
5228
5721
|
// immediately, so the Picker still owns everything except Ctrl+C.
|
|
5229
|
-
{ isActive: externalEditorRequest === null });
|
|
5722
|
+
{ isActive: externalEditorRequest === null && !providerSetup });
|
|
5230
5723
|
// Background is left natural — we inherit the terminal's own background
|
|
5231
5724
|
// and only theme the foreground. Forcing
|
|
5232
5725
|
// a filled bg left mismatched patches around bordered areas, so we don't.
|
|
@@ -5243,10 +5736,15 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
5243
5736
|
!(tool.runId
|
|
5244
5737
|
? representedSubagentTools.has(JSON.stringify([tool.runId, tool.id]))
|
|
5245
5738
|
: representedUnscopedSubagentToolUseIds.has(tool.id)));
|
|
5739
|
+
const outputBlocks = outputViewer
|
|
5740
|
+
? messages.filter((m) => m.detailRef !== undefined && (m.detail?.length ?? 0) > 0)
|
|
5741
|
+
: [];
|
|
5742
|
+
const outputIndex = outputBlocks.findIndex((m) => m.id === outputViewer?.id);
|
|
5246
5743
|
const selectedSubagent = agentSurface
|
|
5247
5744
|
? subagents.find((agent) => agent.viewId === agentSurface.selectedId)
|
|
5248
5745
|
: undefined;
|
|
5249
|
-
const lifecycleOwnsViewport =
|
|
5746
|
+
const lifecycleOwnsViewport = outputViewer !== null ||
|
|
5747
|
+
phase === 'trust' ||
|
|
5250
5748
|
phase === 'resume' ||
|
|
5251
5749
|
phase === 'edit' ||
|
|
5252
5750
|
phase === 'picker' ||
|
|
@@ -5260,7 +5758,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
5260
5758
|
choicePicker === null &&
|
|
5261
5759
|
copyPicker === null &&
|
|
5262
5760
|
agentSurface === null
|
|
5263
|
-
? goalStatusLabel(goalStatus)
|
|
5761
|
+
? goalStatusLabel(goalStatus, goalStatus ? goalActivation.isArmed(goalStatus.sessionId, goalStatus) : false)
|
|
5264
5762
|
: null;
|
|
5265
5763
|
const statusHint = conversationMutation === 'fork'
|
|
5266
5764
|
? 'forking conversation — input is paused'
|
|
@@ -5275,9 +5773,11 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
5275
5773
|
: permission
|
|
5276
5774
|
? hintForPhase(phase, state, session?.hasProvider === true)
|
|
5277
5775
|
: agentSurface?.kind === 'cockpit'
|
|
5278
|
-
? agentSurface.focus === '
|
|
5279
|
-
? '
|
|
5280
|
-
:
|
|
5776
|
+
? agentSurface.focus === 'workflows'
|
|
5777
|
+
? 'workflows — enter select · esc return'
|
|
5778
|
+
: agentSurface.focus === 'phases'
|
|
5779
|
+
? 'agent phases — enter agents · esc return'
|
|
5780
|
+
: 'agents — enter inspect · left phases · esc return'
|
|
5281
5781
|
: agentSurface?.kind === 'transcript'
|
|
5282
5782
|
? 'observing agent — esc agents · q parent'
|
|
5283
5783
|
: textPrompt
|
|
@@ -5291,8 +5791,21 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
5291
5791
|
: goalStatus && phase === 'ready' && state === 'idle'
|
|
5292
5792
|
? undefined
|
|
5293
5793
|
: hintForPhase(phase, state, session?.hasProvider === true, composerHasDraft);
|
|
5794
|
+
const permissionOwner = permission?.runId
|
|
5795
|
+
? subagents.find((agent) => agent.runId === permission.runId)
|
|
5796
|
+
: undefined;
|
|
5797
|
+
const permissionSourceLabel = permission?.runId
|
|
5798
|
+
? permissionOwner ? `Agent: ${permissionOwner.description}` : `Run: ${permission.runId}`
|
|
5799
|
+
: undefined;
|
|
5294
5800
|
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,
|
|
5801
|
+
return (_jsx(Box, { flexDirection: "column", display: externalEditorRequest ? 'none' : 'flex', children: _jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(TranscriptFrame, { children: _jsx(Transcript, { messages: finalized, pending: messages.find((m) => m.pending) ?? null, state: state, settled: transcriptOwned ? renderedSettled : 0, resetKey: resetKey, raw: rawOutput, hyperlinks: hyperlinks, showLive: !lifecycleOwnsViewport, header: transcriptOwned ? (_jsx(BrandHeader, { version: ctx.version, permissionMode: displayedPermissionMode })) : undefined }) }), providerSetup ? (_jsx(ProviderSetup, { cwd: ctx.cwd, onClose: () => setProviderSetup(false), onConnect: () => {
|
|
5802
|
+
setProviderSetup(false);
|
|
5803
|
+
setPickerInitialView('providers');
|
|
5804
|
+
setPickerDetected(null);
|
|
5805
|
+
setPickerSelectionKind('provider-and-model');
|
|
5806
|
+
setPhase('picker');
|
|
5807
|
+
void discoverProviders({ skipProbes: true }).then(setPickerDetected).catch((error) => setPickerNotice(String(error)));
|
|
5808
|
+
} })) : 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, onSetup: () => setProviderSetup(true), onCredential: handleTypedCredential, onLogin: startLoginFromPicker, onLoginComplete: finishLoginFromPicker, keyEntryFor: keyEntryFor, notice: pickerNotice })) : (_jsxs(_Fragment, { children: [agentSurface === null && outputViewer === 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 && outputViewer === 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, queuedCount: queuedPermissionCount, sourceLabel: permissionSourceLabel, 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 || outputViewer !== null), onSubmit: submitTextPrompt, onCancel: cancelTextPrompt }, textPrompt.token)) : permission === null && agentSurface === null && outputViewer === 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 && outputViewer === null && copyPicker ? (_jsx(CopyPicker, { targets: copyPicker.targets, selected: selectedCopy })) : null, _jsxs(ComposerFrame, { working: state === 'thinking' || state === 'tool' || visibleActiveTools.length > 0, focus: phase === 'ready' &&
|
|
5296
5809
|
state !== 'awaiting-permission' &&
|
|
5297
5810
|
!compacting &&
|
|
5298
5811
|
externalEditorRequest === null &&
|
|
@@ -5300,40 +5813,48 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
5300
5813
|
textPrompt === null &&
|
|
5301
5814
|
choicePicker === null &&
|
|
5302
5815
|
copyPicker === null &&
|
|
5303
|
-
agentSurface === null, hidden: permission !== null ||
|
|
5816
|
+
agentSurface === null && outputViewer === null, hidden: permission !== null ||
|
|
5304
5817
|
textPrompt !== null ||
|
|
5305
5818
|
choicePicker !== null ||
|
|
5306
5819
|
copyPicker !== null ||
|
|
5307
|
-
agentSurface
|
|
5820
|
+
(agentSurface !== null || outputViewer !== null), children: [pendingSteers.length > 0 &&
|
|
5308
5821
|
permission === null &&
|
|
5309
5822
|
textPrompt === null &&
|
|
5310
5823
|
choicePicker === null &&
|
|
5311
5824
|
copyPicker === null &&
|
|
5312
|
-
agentSurface === null ? (_jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: theme.accent.user, children: ["\u21B3 ", pendingSteers.length, " message", pendingSteers.length > 1 ? 's' : '', " steering the active turn \u2014 waiting for its next response boundary"] }) })) : null, queued.length > 0 &&
|
|
5825
|
+
agentSurface === null && outputViewer === null ? (_jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: theme.accent.user, children: ["\u21B3 ", pendingSteers.length, " message", pendingSteers.length > 1 ? 's' : '', " steering the active turn \u2014 waiting for its next response boundary"] }) })) : null, queued.length > 0 &&
|
|
5313
5826
|
permission === null &&
|
|
5314
5827
|
textPrompt === null &&
|
|
5315
5828
|
choicePicker === null &&
|
|
5316
5829
|
copyPicker === null &&
|
|
5317
|
-
agentSurface === null ? (_jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: theme.text.muted, children: [queuePause ? '⏸' : '⏎', " ", queued.length, " message", queued.length > 1 ? 's' : '', " queued \u2014", ' ', queuePause
|
|
5830
|
+
agentSurface === null && outputViewer === null ? (_jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: theme.text.muted, children: [queuePause ? '⏸' : '⏎', " ", queued.length, " message", queued.length > 1 ? 's' : '', " queued \u2014", ' ', queuePause
|
|
5318
5831
|
? queuePause.outcome === 'paused'
|
|
5319
5832
|
? 'held after a resumable run paused; wait for recovery, change model, or send a message to release it'
|
|
5320
5833
|
: `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:
|
|
5834
|
+
: 'sending when ready'] }) })) : null, _jsx(Composer, { reasoningEffortLevels: session?.reasoningEffortLevels, permissionMode: displayedPermissionMode, onCycleMode: cyclePermissionMode, disabled: outputViewer !== null ||
|
|
5835
|
+
phase !== 'ready' ||
|
|
5322
5836
|
state === 'awaiting-permission' ||
|
|
5323
5837
|
compacting ||
|
|
5324
5838
|
conversationMutation !== null ||
|
|
5325
5839
|
externalEditorRequest !== null ||
|
|
5326
|
-
agentSurface !== null, hidden: permission !== null ||
|
|
5840
|
+
(agentSurface !== null || outputViewer !== null), hidden: permission !== null ||
|
|
5327
5841
|
textPrompt !== null ||
|
|
5328
5842
|
choicePicker !== null ||
|
|
5329
5843
|
copyPicker !== null ||
|
|
5330
|
-
agentSurface
|
|
5844
|
+
(agentSurface !== null || outputViewer !== null),
|
|
5331
5845
|
// A turn is running, so Esc is the interrupt and not
|
|
5332
5846
|
// the composer's clear.
|
|
5333
|
-
escapeInterrupts: !compacting &&
|
|
5847
|
+
escapeInterrupts: !compacting &&
|
|
5848
|
+
(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
5849
|
...command,
|
|
5335
5850
|
description: command.unavailable?.(slashCtx) ?? command.description,
|
|
5336
|
-
})), 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] })),
|
|
5851
|
+
})), mentionCandidates: mentionCandidates, history: history })] }), permission === null && agentSurface === null && outputViewer === null && liveSubagents.length > 0 ? (_jsx(AgentTaskPanel, { agents: liveSubagents, terminalRows: terminal.rows, terminalColumns: terminal.columns })) : permission === null && agentSurface?.kind === 'cockpit' ? (_jsx(AgentCockpit, { agents: subagents, selectedPhaseId: agentSurface.selectedPhaseId, selectedId: agentSurface.selectedId, focus: agentSurface.focus, terminalRows: terminal.rows, terminalColumns: terminal.columns })) : permission === null && agentSurface?.kind === 'transcript' && selectedSubagent ? (_jsx(AgentTranscript, { agent: selectedSubagent, tailOffset: agentSurface.tailOffset, terminalRows: terminal.rows, terminalColumns: terminal.columns })) : null] })), outputViewer && permission === null ? (_jsx(ToolOutputViewer, { onPrevious: () => {
|
|
5852
|
+
if (outputIndex > 0)
|
|
5853
|
+
setOutputViewer(outputBlocks[outputIndex - 1]);
|
|
5854
|
+
}, onNext: () => {
|
|
5855
|
+
if (outputIndex >= 0 && outputIndex + 1 < outputBlocks.length)
|
|
5856
|
+
setOutputViewer(outputBlocks[outputIndex + 1]);
|
|
5857
|
+
}, title: outputViewer.content, lines: outputViewer.detail ?? [], rows: terminal.rows, columns: terminal.columns, onClose: () => setOutputViewer(null) }, outputViewer.id)) : null, _jsx(Box, { paddingTop: 1, children: _jsx(StatusBar, { cwd: ctx.cwd, provider: session?.providerSummary ?? null, model: session?.modelSummary ?? null, effort: reasoningEffort ?? 'default', goal: statusGoal, state: state, hint: statusHint }) })] }) }));
|
|
5337
5858
|
}
|
|
5338
5859
|
/** One line for a background job that ended, for the transcript and the next turn. */
|
|
5339
5860
|
function describeJobExit(job) {
|
|
@@ -5392,12 +5913,12 @@ hasDraft = false) {
|
|
|
5392
5913
|
// screen; only state-specific actions belong in the footer.
|
|
5393
5914
|
return '';
|
|
5394
5915
|
}
|
|
5395
|
-
function goalStatusLabel(goal) {
|
|
5916
|
+
function goalStatusLabel(goal, armed) {
|
|
5396
5917
|
if (!goal)
|
|
5397
5918
|
return null;
|
|
5398
5919
|
switch (goal.phase) {
|
|
5399
5920
|
case 'active':
|
|
5400
|
-
return 'Pursuing goal';
|
|
5921
|
+
return armed ? 'Pursuing goal' : 'Goal paused (/goal resume)';
|
|
5401
5922
|
case 'paused':
|
|
5402
5923
|
return 'Goal paused (/goal resume)';
|
|
5403
5924
|
case 'blocked':
|
|
@@ -5443,6 +5964,29 @@ function permissionPicker(mode, approvedAll, more = false) {
|
|
|
5443
5964
|
}),
|
|
5444
5965
|
};
|
|
5445
5966
|
}
|
|
5967
|
+
function reasoningEffortPicker(session, current, afterModelSelection = false) {
|
|
5968
|
+
const levels = session.reasoningEffortLevels;
|
|
5969
|
+
if (!session.hasProvider || levels === undefined)
|
|
5970
|
+
return undefined;
|
|
5971
|
+
const values = [undefined, ...levels];
|
|
5972
|
+
return {
|
|
5973
|
+
kind: 'reasoning-effort',
|
|
5974
|
+
title: `Select Reasoning Level for ${session.modelSummary ?? 'current model'}`,
|
|
5975
|
+
...(afterModelSelection
|
|
5976
|
+
? {
|
|
5977
|
+
selectedSession: session,
|
|
5978
|
+
notice: 'Model selected. Esc keeps this model at its default effort.',
|
|
5979
|
+
}
|
|
5980
|
+
: {}),
|
|
5981
|
+
values,
|
|
5982
|
+
options: values.map((effort) => ({
|
|
5983
|
+
label: effort ?? 'default',
|
|
5984
|
+
description: reasoningEffortDescription(effort),
|
|
5985
|
+
current: effort === current,
|
|
5986
|
+
default: effort === undefined,
|
|
5987
|
+
})),
|
|
5988
|
+
};
|
|
5989
|
+
}
|
|
5446
5990
|
function reasoningEffortDescription(effort) {
|
|
5447
5991
|
switch (effort) {
|
|
5448
5992
|
case undefined:
|