@namzu/cli 18.1.0 → 20.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 +5951 -0
- package/README.md +23 -24
- package/dist/bin.js +12 -1
- package/dist/bin.js.map +1 -1
- package/dist/checkpoints/store.d.ts +75 -0
- package/dist/checkpoints/store.d.ts.map +1 -0
- package/dist/checkpoints/store.js +177 -0
- package/dist/checkpoints/store.js.map +1 -0
- package/dist/checkpoints/wrap.d.ts +15 -0
- package/dist/checkpoints/wrap.d.ts.map +1 -0
- package/dist/checkpoints/wrap.js +37 -0
- package/dist/checkpoints/wrap.js.map +1 -0
- package/dist/cli.d.ts +6 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +12 -1
- package/dist/cli.js.map +1 -1
- package/dist/commands/acp.d.ts.map +1 -1
- package/dist/commands/acp.js +2 -0
- package/dist/commands/acp.js.map +1 -1
- package/dist/commands/drain.d.ts.map +1 -1
- package/dist/commands/drain.js +15 -2
- package/dist/commands/drain.js.map +1 -1
- package/dist/commands/provider-wait.d.ts +44 -0
- package/dist/commands/provider-wait.d.ts.map +1 -0
- package/dist/commands/provider-wait.js +65 -0
- package/dist/commands/provider-wait.js.map +1 -0
- package/dist/commands/run-flags.d.ts +13 -1
- package/dist/commands/run-flags.d.ts.map +1 -1
- package/dist/commands/run-flags.js +41 -0
- package/dist/commands/run-flags.js.map +1 -1
- package/dist/commands/run-stream.d.ts.map +1 -1
- package/dist/commands/run-stream.js +33 -13
- package/dist/commands/run-stream.js.map +1 -1
- package/dist/commands/run.d.ts.map +1 -1
- package/dist/commands/run.js +132 -78
- package/dist/commands/run.js.map +1 -1
- package/dist/commands/stdin.d.ts +17 -0
- package/dist/commands/stdin.d.ts.map +1 -0
- package/dist/commands/stdin.js +69 -0
- package/dist/commands/stdin.js.map +1 -0
- package/dist/config/load.d.ts.map +1 -1
- package/dist/config/load.js +107 -2
- package/dist/config/load.js.map +1 -1
- package/dist/config/schema.d.ts +80 -5
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js.map +1 -1
- package/dist/context/doctrine.d.ts +16 -38
- package/dist/context/doctrine.d.ts.map +1 -1
- package/dist/context/doctrine.js +16 -75
- package/dist/context/doctrine.js.map +1 -1
- package/dist/context/environment.d.ts +2 -0
- package/dist/context/environment.d.ts.map +1 -1
- package/dist/context/environment.js +3 -0
- package/dist/context/environment.js.map +1 -1
- package/dist/integrations/mcp/servers.d.ts +19 -0
- package/dist/integrations/mcp/servers.d.ts.map +1 -1
- package/dist/integrations/mcp/servers.js +23 -2
- package/dist/integrations/mcp/servers.js.map +1 -1
- package/dist/integrations/plugins/runtime.d.ts +8 -2
- package/dist/integrations/plugins/runtime.d.ts.map +1 -1
- package/dist/integrations/plugins/runtime.js +24 -12
- package/dist/integrations/plugins/runtime.js.map +1 -1
- package/dist/integrations/providers/chain-capabilities.js +1 -1
- package/dist/integrations/providers/chain-capabilities.js.map +1 -1
- package/dist/integrations/providers/credential-store.d.ts +1 -1
- package/dist/integrations/providers/credential-store.js +1 -1
- package/dist/integrations/providers/harness-credentials.d.ts.map +1 -1
- package/dist/integrations/providers/harness-credentials.js +7 -0
- package/dist/integrations/providers/harness-credentials.js.map +1 -1
- package/dist/integrations/providers/registry.d.ts +2 -2
- package/dist/integrations/providers/registry.js +2 -2
- package/dist/integrations/sessions/store.d.ts +14 -22
- package/dist/integrations/sessions/store.d.ts.map +1 -1
- package/dist/integrations/sessions/store.js +78 -175
- package/dist/integrations/sessions/store.js.map +1 -1
- package/dist/integrations/sessions/transcript-export.d.ts.map +1 -1
- package/dist/integrations/sessions/transcript-export.js +6 -2
- package/dist/integrations/sessions/transcript-export.js.map +1 -1
- package/dist/integrations/state/identity.d.ts +21 -0
- package/dist/integrations/state/identity.d.ts.map +1 -0
- package/dist/integrations/state/identity.js +49 -0
- package/dist/integrations/state/identity.js.map +1 -0
- package/dist/integrations/state/immutable-json.d.ts +10 -0
- package/dist/integrations/state/immutable-json.d.ts.map +1 -0
- package/dist/integrations/state/immutable-json.js +38 -0
- package/dist/integrations/state/immutable-json.js.map +1 -0
- package/dist/integrations/state/project.d.ts +10 -0
- package/dist/integrations/state/project.d.ts.map +1 -0
- package/dist/integrations/state/project.js +15 -0
- package/dist/integrations/state/project.js.map +1 -0
- package/dist/integrations/state/report.d.ts.map +1 -1
- package/dist/integrations/state/report.js +42 -12
- package/dist/integrations/state/report.js.map +1 -1
- package/dist/integrations/subagents/definitions.d.ts +19 -0
- package/dist/integrations/subagents/definitions.d.ts.map +1 -0
- package/dist/integrations/subagents/definitions.js +26 -0
- package/dist/integrations/subagents/definitions.js.map +1 -0
- package/dist/integrations/subagents/parent.d.ts +13 -0
- package/dist/integrations/subagents/parent.d.ts.map +1 -0
- package/dist/integrations/subagents/parent.js +57 -0
- package/dist/integrations/subagents/parent.js.map +1 -0
- package/dist/integrations/subagents/runtime.d.ts +33 -16
- package/dist/integrations/subagents/runtime.d.ts.map +1 -1
- package/dist/integrations/subagents/runtime.js +450 -150
- package/dist/integrations/subagents/runtime.js.map +1 -1
- package/dist/memory/presentation.d.ts +9 -0
- package/dist/memory/presentation.d.ts.map +1 -0
- package/dist/memory/presentation.js +37 -0
- package/dist/memory/presentation.js.map +1 -0
- package/dist/memory/store.d.ts +35 -15
- package/dist/memory/store.d.ts.map +1 -1
- package/dist/memory/store.js +79 -25
- package/dist/memory/store.js.map +1 -1
- package/dist/permissions/mode.d.ts +15 -45
- package/dist/permissions/mode.d.ts.map +1 -1
- package/dist/permissions/mode.js +36 -13
- package/dist/permissions/mode.js.map +1 -1
- package/dist/release-notes.d.ts +20 -0
- package/dist/release-notes.d.ts.map +1 -0
- package/dist/release-notes.js +64 -0
- package/dist/release-notes.js.map +1 -0
- package/dist/tui/AgentExplorer.d.ts +1 -1
- package/dist/tui/AgentExplorer.d.ts.map +1 -1
- package/dist/tui/AgentExplorer.js +25 -21
- package/dist/tui/AgentExplorer.js.map +1 -1
- package/dist/tui/App.d.ts.map +1 -1
- package/dist/tui/App.js +701 -245
- package/dist/tui/App.js.map +1 -1
- package/dist/tui/BrandHeader.d.ts +12 -0
- package/dist/tui/BrandHeader.d.ts.map +1 -0
- package/dist/tui/BrandHeader.js +18 -0
- package/dist/tui/BrandHeader.js.map +1 -0
- package/dist/tui/ChoicePicker.d.ts +11 -1
- package/dist/tui/ChoicePicker.d.ts.map +1 -1
- package/dist/tui/ChoicePicker.js +46 -13
- package/dist/tui/ChoicePicker.js.map +1 -1
- package/dist/tui/Composer.d.ts +5 -2
- package/dist/tui/Composer.d.ts.map +1 -1
- package/dist/tui/Composer.js +6 -11
- package/dist/tui/Composer.js.map +1 -1
- package/dist/tui/ComposerFrame.d.ts +10 -0
- package/dist/tui/ComposerFrame.d.ts.map +1 -0
- package/dist/tui/ComposerFrame.js +93 -0
- package/dist/tui/ComposerFrame.js.map +1 -0
- package/dist/tui/EditPromptPicker.d.ts.map +1 -1
- package/dist/tui/EditPromptPicker.js +1 -1
- package/dist/tui/EditPromptPicker.js.map +1 -1
- package/dist/tui/LiveActivity.d.ts +12 -5
- package/dist/tui/LiveActivity.d.ts.map +1 -1
- package/dist/tui/LiveActivity.js +17 -32
- package/dist/tui/LiveActivity.js.map +1 -1
- package/dist/tui/PermissionOverlay.d.ts +26 -2
- package/dist/tui/PermissionOverlay.d.ts.map +1 -1
- package/dist/tui/PermissionOverlay.js +109 -16
- package/dist/tui/PermissionOverlay.js.map +1 -1
- package/dist/tui/Picker.d.ts +2 -2
- package/dist/tui/Picker.d.ts.map +1 -1
- package/dist/tui/Picker.js +82 -52
- package/dist/tui/Picker.js.map +1 -1
- package/dist/tui/StatusBar.d.ts.map +1 -1
- package/dist/tui/StatusBar.js +5 -2
- package/dist/tui/StatusBar.js.map +1 -1
- package/dist/tui/TaskList.d.ts +3 -1
- package/dist/tui/TaskList.d.ts.map +1 -1
- package/dist/tui/TaskList.js +13 -4
- package/dist/tui/TaskList.js.map +1 -1
- package/dist/tui/Transcript.d.ts +4 -12
- package/dist/tui/Transcript.d.ts.map +1 -1
- package/dist/tui/Transcript.js +45 -75
- package/dist/tui/Transcript.js.map +1 -1
- package/dist/tui/agent.d.ts +116 -51
- package/dist/tui/agent.d.ts.map +1 -1
- package/dist/tui/agent.js +539 -307
- package/dist/tui/agent.js.map +1 -1
- package/dist/tui/choice-selection.d.ts +17 -0
- package/dist/tui/choice-selection.d.ts.map +1 -0
- package/dist/tui/choice-selection.js +38 -0
- package/dist/tui/choice-selection.js.map +1 -0
- package/dist/tui/exit-summary.d.ts +7 -1
- package/dist/tui/exit-summary.d.ts.map +1 -1
- package/dist/tui/exit-summary.js +12 -3
- package/dist/tui/exit-summary.js.map +1 -1
- package/dist/tui/index.d.ts +3 -1
- package/dist/tui/index.d.ts.map +1 -1
- package/dist/tui/index.js +5 -3
- package/dist/tui/index.js.map +1 -1
- package/dist/tui/logo.d.ts +7 -22
- package/dist/tui/logo.d.ts.map +1 -1
- package/dist/tui/logo.js +8 -34
- package/dist/tui/logo.js.map +1 -1
- package/dist/tui/model-choices.d.ts.map +1 -1
- package/dist/tui/model-choices.js +21 -6
- package/dist/tui/model-choices.js.map +1 -1
- package/dist/tui/permission-review.d.ts +15 -4
- package/dist/tui/permission-review.d.ts.map +1 -1
- package/dist/tui/permission-review.js +48 -9
- package/dist/tui/permission-review.js.map +1 -1
- package/dist/tui/provider-selection.d.ts +4 -0
- package/dist/tui/provider-selection.d.ts.map +1 -0
- package/dist/tui/provider-selection.js +16 -0
- package/dist/tui/provider-selection.js.map +1 -0
- package/dist/tui/run-interruption.d.ts +3 -2
- package/dist/tui/run-interruption.d.ts.map +1 -1
- package/dist/tui/run-interruption.js +2 -1
- package/dist/tui/run-interruption.js.map +1 -1
- package/dist/tui/shell-escape.d.ts +42 -0
- package/dist/tui/shell-escape.d.ts.map +1 -0
- package/dist/tui/shell-escape.js +110 -0
- package/dist/tui/shell-escape.js.map +1 -0
- package/dist/tui/slashCommands.d.ts +82 -31
- package/dist/tui/slashCommands.d.ts.map +1 -1
- package/dist/tui/slashCommands.js +518 -306
- package/dist/tui/slashCommands.js.map +1 -1
- package/dist/tui/terminal-choice-text.d.ts +8 -0
- package/dist/tui/terminal-choice-text.d.ts.map +1 -0
- package/dist/tui/terminal-choice-text.js +37 -0
- package/dist/tui/terminal-choice-text.js.map +1 -0
- package/dist/tui/theme.d.ts +3 -4
- package/dist/tui/theme.d.ts.map +1 -1
- package/dist/tui/theme.js +16 -17
- package/dist/tui/theme.js.map +1 -1
- package/dist/tui/types.d.ts +9 -13
- package/dist/tui/types.d.ts.map +1 -1
- package/package.json +7 -5
package/dist/tui/App.js
CHANGED
|
@@ -12,7 +12,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
12
12
|
* automatic reuse is not mistaken for a durable operator preference.
|
|
13
13
|
*/
|
|
14
14
|
import { join, relative } from 'node:path';
|
|
15
|
-
import { DefaultPathBuilder, DiskMessageFeedbackStore, GoalRoundLimitError, HostCommandRegistry, RunCancelled, SessionGoalActivation, StaleGoalError, asSessionId, createAssistantMessage, createUserMessage, generateRunId, generateSessionId, isCompactionMessage, kernelHostCommands, } from '@namzu/sdk';
|
|
15
|
+
import { DEFAULT_MAX_GOAL_ROUNDS, DefaultPathBuilder, DiskMessageFeedbackStore, GoalRoundLimitError, HostCommandRegistry, RunCancelled, SessionGoalActivation, StaleGoalError, asSessionId, createAssistantMessage, createUserMessage, generateRunId, generateSessionId, isCompactionMessage, kernelHostCommands, } from '@namzu/sdk';
|
|
16
16
|
import { Box, Text, useApp, useInput, useStdout, useWindowSize } from 'ink';
|
|
17
17
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
18
18
|
import { pinTrustedProjectPath, resolveTrustedProjectContext, } from '../config/trusted-project-context.js';
|
|
@@ -25,24 +25,29 @@ import { appendMessages, archiveConversation, forkConversation, forkConversation
|
|
|
25
25
|
import { conversationMarkdown, writeConversationExport, } from '../integrations/sessions/transcript-export.js';
|
|
26
26
|
import { isTrusted, trustDir } from '../integrations/trust/store.js';
|
|
27
27
|
import { checkUpdates } from '../integrations/updates.js';
|
|
28
|
-
import {
|
|
28
|
+
import { renderMemoryReport } from '../memory/presentation.js';
|
|
29
|
+
import { appendMemory, readMemory } from '../memory/store.js';
|
|
30
|
+
import { effectivePermissionMode, permissionModeDescription, permissionModeLabel, } from '../permissions/mode.js';
|
|
29
31
|
import { composeSkillsPrompt, discoverSkills, loadSkillBody } from '../skills/store.js';
|
|
30
32
|
import { discoverUserCommands } from '../user-commands/store.js';
|
|
31
33
|
import { AgentCockpit, AgentTaskPanel, AgentTranscript, activeSubagentCohorts, agentCockpitIsWide, agentPhasePageSize, agentPhases, agentPickerPageSize, agentTranscriptPageSize, maxAgentTranscriptTailOffset, } from './AgentExplorer.js';
|
|
34
|
+
import { BrandHeader } from './BrandHeader.js';
|
|
32
35
|
import { ChoicePicker } from './ChoicePicker.js';
|
|
33
36
|
import { Composer, suggestionWindowSize, } from './Composer.js';
|
|
37
|
+
import { ComposerFrame } from './ComposerFrame.js';
|
|
34
38
|
import { CopyPicker } from './CopyPicker.js';
|
|
35
39
|
import { EditPromptPicker } from './EditPromptPicker.js';
|
|
36
40
|
import { LiveActivity, formatElapsed } from './LiveActivity.js';
|
|
37
|
-
import { TaskList } from './TaskList.js';
|
|
38
41
|
import { PermissionOverlay } from './PermissionOverlay.js';
|
|
39
42
|
import { Picker } from './Picker.js';
|
|
40
43
|
import { ResumePicker } from './ResumePicker.js';
|
|
41
44
|
import { StatusBar } from './StatusBar.js';
|
|
45
|
+
import { TaskList } from './TaskList.js';
|
|
42
46
|
import { TextPrompt } from './TextPrompt.js';
|
|
43
47
|
import { Transcript, willCollapse } from './Transcript.js';
|
|
44
48
|
import { TrustPrompt } from './TrustPrompt.js';
|
|
45
49
|
import { createAgentSession, probeAgentSession, } from './agent.js';
|
|
50
|
+
import { choicePickerWindowSize, filterChoiceOptions, moveChoiceSelection, } from './choice-selection.js';
|
|
46
51
|
import { keepRecentRows } from './compact-transcript.js';
|
|
47
52
|
import { approvalIsDeliberate } from './consent-timing.js';
|
|
48
53
|
import { planTurnPublication } from './conversation-history.js';
|
|
@@ -51,12 +56,15 @@ import { editablePrompts } from './edit-prompts.js';
|
|
|
51
56
|
import { editDraftInExternalEditor } from './external-editor.js';
|
|
52
57
|
import { liveWindow } from './live-window.js';
|
|
53
58
|
import { describeCodexDeviceLoginStart, describeLoginOutcome, describeLoginStart, describeLogout, describeProviderLogout, } from './login-prompt.js';
|
|
54
|
-
import {
|
|
59
|
+
import { selectPrimaryProvider } from './provider-selection.js';
|
|
60
|
+
import { eraseLastChoiceGrapheme } from './terminal-choice-text.js';
|
|
61
|
+
import { renderCheckpoints, renderRestore } from '../checkpoints/store.js';
|
|
55
62
|
import { expandFileMentions, listMentionableFiles } from './mentions.js';
|
|
56
63
|
import { openInBrowser } from './open-browser.js';
|
|
57
|
-
import {
|
|
64
|
+
import { buildPermissionReview, buildPermissionSummary, permissionReviewPageRows, permissionReviewRefusal, permissionReviewRows, } from './permission-review.js';
|
|
58
65
|
import { describeRunInterruption } from './run-interruption.js';
|
|
59
66
|
import { moveSelection } from './selection-window.js';
|
|
67
|
+
import { describeShellEscape, describeShellEscapeForModel, runShellEscape, shellEscapeCommand, } from './shell-escape.js';
|
|
60
68
|
import { baseBranchReviewPrompt, commitReviewPrompt, hostCommandNames, kernelCommandDescriptors, mergeHostCommands, renderOutcome, reviewPrompt, runSlash, } from './slashCommands.js';
|
|
61
69
|
import { splitCompleteBlocks, splitSafeCut } from './stream-blocks.js';
|
|
62
70
|
import { terminalSupportsHyperlinks } from './terminal-hyperlinks.js';
|
|
@@ -64,6 +72,8 @@ import { theme } from './theme.js';
|
|
|
64
72
|
import { useSelectionIndex } from './use-selection-index.js';
|
|
65
73
|
import { renderWorkspaceDiff, workspaceDiff } from './workspace-diff.js';
|
|
66
74
|
import { listReviewBranches, listReviewCommits, reviewMergeBase, } from './workspace-review.js';
|
|
75
|
+
/** How long `/exit` waits for the session to close before leaving anyway. */
|
|
76
|
+
const SESSION_CLOSE_ON_EXIT_MS = 5_000;
|
|
67
77
|
/** The value a question row carries: an option id, or the free-text escape. */
|
|
68
78
|
const FREE_TEXT_ANSWER = '__free_text__';
|
|
69
79
|
/**
|
|
@@ -305,7 +315,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
305
315
|
const [pickerDetected, setPickerDetected] = useState(null);
|
|
306
316
|
/** Whether Enter accepts the provider directly or continues to model choice. */
|
|
307
317
|
const [pickerSelectionKind, setPickerSelectionKind] = useState('provider-and-model');
|
|
308
|
-
/**
|
|
318
|
+
/** Active preferences, or the saved chain awaiting its missing credential. */
|
|
309
319
|
const savedPrefsRef = useRef(null);
|
|
310
320
|
/**
|
|
311
321
|
* Why the picker is open, drawn ON the picker.
|
|
@@ -345,6 +355,15 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
345
355
|
permissionReviewOffsetRef.current = next;
|
|
346
356
|
setPermissionReviewOffsetState(next);
|
|
347
357
|
}, []);
|
|
358
|
+
// Which of the three answers the cursor is on. Opens on "Yes", as the
|
|
359
|
+
// prompt an operator knows from elsewhere does; Enter confirms it only
|
|
360
|
+
// after the same settle window `y` and `a` wait out.
|
|
361
|
+
const [permissionChoice, setPermissionChoiceState] = useState(0);
|
|
362
|
+
const permissionChoiceRef = useRef(0);
|
|
363
|
+
const setPermissionChoice = useCallback((next) => {
|
|
364
|
+
permissionChoiceRef.current = next;
|
|
365
|
+
setPermissionChoiceState(next);
|
|
366
|
+
}, []);
|
|
348
367
|
// The launch bypass is an INITIAL selection, not permanent authority. A
|
|
349
368
|
// typed /permissions command can narrow it back to prompt/strict without
|
|
350
369
|
// rebuilding the provider, tools, plugins, or sandbox.
|
|
@@ -366,7 +385,62 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
366
385
|
/** Git-admitted file paths cached once per successful session hydration. */
|
|
367
386
|
const [mentionCandidates, setMentionCandidates] = useState([]);
|
|
368
387
|
const mentionLoadOwnerRef = useRef(null);
|
|
388
|
+
// What compaction has done this session, summed over passes; `/context`
|
|
389
|
+
// prints it. Reset with the session, never by a pass that declined.
|
|
390
|
+
const [compactionTally, setCompactionTally] = useState({
|
|
391
|
+
passes: 0,
|
|
392
|
+
clearedResults: 0,
|
|
393
|
+
stubbedNarrations: 0,
|
|
394
|
+
summaries: 0,
|
|
395
|
+
reclaimedTokens: 0,
|
|
396
|
+
});
|
|
397
|
+
// Jobs that ended while no turn was running. The kernel's notice needs a
|
|
398
|
+
// tool result to ride on, so between turns the exit is held here and
|
|
399
|
+
// handed to the next send as part of the system text; the transcript
|
|
400
|
+
// row is written at once.
|
|
401
|
+
const idleJobNoticesRef = useRef([]);
|
|
402
|
+
const drainIdleJobNotices = () => {
|
|
403
|
+
if (idleJobNoticesRef.current.length === 0)
|
|
404
|
+
return undefined;
|
|
405
|
+
const lines = idleJobNoticesRef.current;
|
|
406
|
+
idleJobNoticesRef.current = [];
|
|
407
|
+
return `Background jobs that ended since your last turn:\n${lines.map((l) => `- ${l}`).join('\n')}`;
|
|
408
|
+
};
|
|
409
|
+
// Commands the operator ran with `!` since the last turn. The model did
|
|
410
|
+
// not see them happen; it reads them here on its next turn.
|
|
411
|
+
const operatorShellRef = useRef([]);
|
|
412
|
+
const drainOperatorShell = () => {
|
|
413
|
+
if (operatorShellRef.current.length === 0)
|
|
414
|
+
return undefined;
|
|
415
|
+
const blocks = operatorShellRef.current;
|
|
416
|
+
operatorShellRef.current = [];
|
|
417
|
+
return `What the operator did outside you since your last turn — commands they ran in their own shell, files they put back — with the output:\n\n${blocks.join('\n\n')}`;
|
|
418
|
+
};
|
|
419
|
+
useEffect(() => {
|
|
420
|
+
if (!session?.onJobExit)
|
|
421
|
+
return;
|
|
422
|
+
return session.onJobExit((job) => {
|
|
423
|
+
if (abortRef.current !== null)
|
|
424
|
+
return;
|
|
425
|
+
const text = describeJobExit({
|
|
426
|
+
jobId: job.id,
|
|
427
|
+
command: job.command,
|
|
428
|
+
status: job.status === 'killed' ? 'killed' : 'exited',
|
|
429
|
+
...(job.exitCode !== undefined ? { exitCode: job.exitCode } : {}),
|
|
430
|
+
...(job.signal ? { signal: job.signal } : {}),
|
|
431
|
+
});
|
|
432
|
+
idleJobNoticesRef.current.push(text);
|
|
433
|
+
pushMessage('system', text, false, '⚙');
|
|
434
|
+
});
|
|
435
|
+
}, [session]);
|
|
369
436
|
const [usage, setUsage] = useState(null);
|
|
437
|
+
/**
|
|
438
|
+
* The model's current reasoning, last line only, for the live region.
|
|
439
|
+
* Never appended to a message: the kernel keeps reasoning out of its
|
|
440
|
+
* own transcript, and this exists so a long think reads as work rather
|
|
441
|
+
* than as silence. Cleared by the first text or tool call that follows.
|
|
442
|
+
*/
|
|
443
|
+
const [thinking, setThinking] = useState(null);
|
|
370
444
|
// Tools currently executing — rendered live (spinner + elapsed) below the
|
|
371
445
|
// transcript, then committed as static lines on completion.
|
|
372
446
|
const [activeTools, setActiveTools] = useState([]);
|
|
@@ -459,6 +533,8 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
459
533
|
/** Finite slash-command choice owned synchronously until apply or cancel. */
|
|
460
534
|
const [choicePicker, setChoicePickerState] = useState(null);
|
|
461
535
|
const choicePickerRef = useRef(null);
|
|
536
|
+
const [choiceQuery, setChoiceQuery] = useState('');
|
|
537
|
+
const choiceQueryRef = useRef('');
|
|
462
538
|
/**
|
|
463
539
|
* Exact chooser React has committed. The synchronous owner ref is needed to
|
|
464
540
|
* block queue/goal work immediately, but it must not accept the Return key
|
|
@@ -469,6 +545,8 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
469
545
|
const { selection: selectedChoice, selectionRef: selectedChoiceRef, setSelection: setSelectedChoice, } = useSelectionIndex(0);
|
|
470
546
|
const setChoicePicker = useCallback((next) => {
|
|
471
547
|
choicePickerRef.current = next;
|
|
548
|
+
choiceQueryRef.current = '';
|
|
549
|
+
setChoiceQuery('');
|
|
472
550
|
if (next === null)
|
|
473
551
|
choicePickerCommittedRef.current = null;
|
|
474
552
|
setChoicePickerState(next);
|
|
@@ -514,14 +592,13 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
514
592
|
const surface = agentSurfaceRef.current;
|
|
515
593
|
if (!surface)
|
|
516
594
|
return;
|
|
517
|
-
|
|
518
|
-
if (activeCohorts.length === 0) {
|
|
595
|
+
if (next.length === 0) {
|
|
519
596
|
setAgentSurface(null);
|
|
520
597
|
return;
|
|
521
598
|
}
|
|
522
|
-
if (
|
|
599
|
+
if (next.some((agent) => agent.viewId === surface.selectedId))
|
|
523
600
|
return;
|
|
524
|
-
const phases = agentPhases(
|
|
601
|
+
const phases = agentPhases(next);
|
|
525
602
|
const fallbackPhase = phases.find((phase) => phase.id === surface.selectedPhaseId) ?? phases[0];
|
|
526
603
|
const fallbackAgent = fallbackPhase?.agents[0];
|
|
527
604
|
if (!fallbackPhase || !fallbackAgent) {
|
|
@@ -536,7 +613,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
536
613
|
});
|
|
537
614
|
}, [setAgentSurface]);
|
|
538
615
|
const openAgentCockpit = useCallback(() => {
|
|
539
|
-
const agents =
|
|
616
|
+
const agents = subagentsRef.current;
|
|
540
617
|
const firstPhase = agentPhases(agents)[0];
|
|
541
618
|
const firstAgent = firstPhase?.agents[0];
|
|
542
619
|
if (!firstPhase || !firstAgent)
|
|
@@ -642,14 +719,29 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
642
719
|
*/
|
|
643
720
|
const conversationMaterializedRef = useRef(false);
|
|
644
721
|
const materializationRef = useRef(null);
|
|
722
|
+
/**
|
|
723
|
+
* The session owns processes — MCP servers, background jobs, the hooks'
|
|
724
|
+
* `session_end` — and Ink's exit does not close it. So the session is
|
|
725
|
+
* closed first, bounded, and the process leaves after: a server started
|
|
726
|
+
* with `run_in_background` used to outlive `/exit`, and a `session_end`
|
|
727
|
+
* hook never ran.
|
|
728
|
+
*/
|
|
729
|
+
const closeAndExit = useCallback(() => {
|
|
730
|
+
const closing = session?.close() ?? Promise.resolve();
|
|
731
|
+
const bounded = Promise.race([
|
|
732
|
+
closing,
|
|
733
|
+
new Promise((resolve) => setTimeout(resolve, SESSION_CLOSE_ON_EXIT_MS).unref?.()),
|
|
734
|
+
]);
|
|
735
|
+
void bounded.catch(() => undefined).finally(() => exit());
|
|
736
|
+
}, [exit, session]);
|
|
645
737
|
const exitWithSummary = useCallback(() => {
|
|
646
738
|
onExitSummary?.({
|
|
647
739
|
...(conversationMaterializedRef.current && scopeRef.current?.sessionId
|
|
648
740
|
? { conversationId: scopeRef.current.sessionId }
|
|
649
741
|
: {}),
|
|
650
742
|
});
|
|
651
|
-
|
|
652
|
-
}, [
|
|
743
|
+
closeAndExit();
|
|
744
|
+
}, [closeAndExit, onExitSummary]);
|
|
653
745
|
/** Durable active state is not permission to spend turns after a restart. */
|
|
654
746
|
const [goalActivation] = useState(() => new SessionGoalActivation());
|
|
655
747
|
const goalDriveInFlightRef = useRef(false);
|
|
@@ -849,7 +941,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
849
941
|
}, [nextId]);
|
|
850
942
|
const applyPermissionMode = useCallback((mode) => {
|
|
851
943
|
if (!session?.hasProvider) {
|
|
852
|
-
pushMessage('system', '
|
|
944
|
+
pushMessage('system', 'Choose a model before changing permissions.');
|
|
853
945
|
return;
|
|
854
946
|
}
|
|
855
947
|
if (state !== 'idle' ||
|
|
@@ -858,18 +950,18 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
858
950
|
queuedRef.current.length > 0 ||
|
|
859
951
|
permissionResolveRef.current !== null ||
|
|
860
952
|
compactingRef.current) {
|
|
861
|
-
pushMessage('system', '
|
|
953
|
+
pushMessage('system', 'Permissions were not changed. Finish or stop the current work first.');
|
|
862
954
|
return;
|
|
863
955
|
}
|
|
864
956
|
if (!session.resetApprovalLatch) {
|
|
865
|
-
pushMessage('system', '
|
|
957
|
+
pushMessage('system', 'This session cannot reset approvals. Reconnect before changing permissions.');
|
|
866
958
|
return;
|
|
867
959
|
}
|
|
868
960
|
session.resetApprovalLatch();
|
|
869
961
|
permissionModeRef.current = mode;
|
|
870
962
|
permissionModeSourceRef.current = 'session';
|
|
871
963
|
setPermissionModeState(mode);
|
|
872
|
-
pushMessage('system', `
|
|
964
|
+
pushMessage('system', `Permissions: ${permissionModeLabel(mode)} for this session. ${permissionModeDescription(mode)}`);
|
|
873
965
|
}, [hasUnsettledTurn, pushMessage, session, state]);
|
|
874
966
|
const applyReasoningEffort = useCallback((effort) => {
|
|
875
967
|
if (!session?.hasProvider) {
|
|
@@ -1038,7 +1130,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1038
1130
|
archived = true;
|
|
1039
1131
|
goalActivation.clear();
|
|
1040
1132
|
onExitSummary?.({});
|
|
1041
|
-
|
|
1133
|
+
closeAndExit();
|
|
1042
1134
|
}
|
|
1043
1135
|
catch (error) {
|
|
1044
1136
|
pushMessage('system', `Could not archive this conversation: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -1147,6 +1239,8 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1147
1239
|
const value = picker.values[index];
|
|
1148
1240
|
if (index < 0 || index >= picker.values.length)
|
|
1149
1241
|
return;
|
|
1242
|
+
if (picker.options[index]?.disabledReason)
|
|
1243
|
+
return;
|
|
1150
1244
|
if (picker.kind === 'review-preset') {
|
|
1151
1245
|
if (reviewChoiceInFlightRef.current)
|
|
1152
1246
|
return;
|
|
@@ -1177,6 +1271,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1177
1271
|
setSelectedChoice(0);
|
|
1178
1272
|
setChoicePicker({
|
|
1179
1273
|
kind: 'review-branch',
|
|
1274
|
+
back: picker,
|
|
1180
1275
|
title: 'Select a base branch',
|
|
1181
1276
|
notice: `Current branch: ${listing.current}`,
|
|
1182
1277
|
values: listing.branches,
|
|
@@ -1201,6 +1296,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1201
1296
|
setSelectedChoice(0);
|
|
1202
1297
|
setChoicePicker({
|
|
1203
1298
|
kind: 'review-commit',
|
|
1299
|
+
back: picker,
|
|
1204
1300
|
title: 'Select a commit to review',
|
|
1205
1301
|
values: commits,
|
|
1206
1302
|
options: commits.map((commit) => ({
|
|
@@ -1323,6 +1419,9 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1323
1419
|
return;
|
|
1324
1420
|
}
|
|
1325
1421
|
commandPickerSubmitRef.current(`/${command.name}`);
|
|
1422
|
+
const child = choicePickerRef.current;
|
|
1423
|
+
if (child)
|
|
1424
|
+
setChoicePicker({ ...child, back: picker });
|
|
1326
1425
|
return;
|
|
1327
1426
|
}
|
|
1328
1427
|
if (picker.kind === 'archive-conversation') {
|
|
@@ -1353,6 +1452,16 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1353
1452
|
return;
|
|
1354
1453
|
}
|
|
1355
1454
|
if (picker.kind === 'permission-mode') {
|
|
1455
|
+
if (value === 'more') {
|
|
1456
|
+
const child = permissionPicker(permissionModeRef.current, session?.approvalLatched() ?? false, true);
|
|
1457
|
+
setSelectedChoice(Math.max(0, child.options.findIndex((option) => option.current)));
|
|
1458
|
+
setChoicePicker({ ...child, back: picker });
|
|
1459
|
+
return;
|
|
1460
|
+
}
|
|
1461
|
+
if (value === 'rules') {
|
|
1462
|
+
commandPickerSubmitRef.current('/permissions details');
|
|
1463
|
+
return;
|
|
1464
|
+
}
|
|
1356
1465
|
applyPermissionMode(value);
|
|
1357
1466
|
return;
|
|
1358
1467
|
}
|
|
@@ -1383,7 +1492,9 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1383
1492
|
removeStoredCredential,
|
|
1384
1493
|
runConversationExport,
|
|
1385
1494
|
setChoicePicker,
|
|
1495
|
+
setSelectedChoice,
|
|
1386
1496
|
setTextPrompt,
|
|
1497
|
+
session,
|
|
1387
1498
|
]);
|
|
1388
1499
|
const sendCopyRequest = useCallback((index) => {
|
|
1389
1500
|
const picker = copyPickerRef.current;
|
|
@@ -1504,6 +1615,9 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1504
1615
|
else {
|
|
1505
1616
|
// Provider/tool startup needs stable project/tenant/topic ids, not a
|
|
1506
1617
|
// durable conversation. The cursor is replaced on first admitted use.
|
|
1618
|
+
// The conversation's id, chosen now; the conversation is written
|
|
1619
|
+
// under it at first durable use. Nothing that saw this id before
|
|
1620
|
+
// that moment — a hook, a log line, the screen — is later wrong.
|
|
1507
1621
|
sessionId = generateSessionId();
|
|
1508
1622
|
}
|
|
1509
1623
|
sessionsRef.current = sessions;
|
|
@@ -1531,11 +1645,15 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1531
1645
|
return undefined;
|
|
1532
1646
|
conversationMutationRef.current = 'materialize';
|
|
1533
1647
|
try {
|
|
1534
|
-
const
|
|
1648
|
+
const written = await startConversation(sessions, scope.sessionId);
|
|
1535
1649
|
if (scopeRef.current !== scope || conversationMaterializedRef.current) {
|
|
1536
1650
|
throw new Error('the active conversation changed while its first session was published');
|
|
1537
1651
|
}
|
|
1538
|
-
|
|
1652
|
+
// The store creates the conversation under the id chosen at open,
|
|
1653
|
+
// so this is the same id; a store that cannot honour a chosen id
|
|
1654
|
+
// is followed rather than contradicted.
|
|
1655
|
+
if (written !== scope.sessionId)
|
|
1656
|
+
scope.sessionId = written;
|
|
1539
1657
|
conversationMaterializedRef.current = true;
|
|
1540
1658
|
return scope;
|
|
1541
1659
|
}
|
|
@@ -1554,7 +1672,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1554
1672
|
materializationRef.current = null;
|
|
1555
1673
|
}
|
|
1556
1674
|
}, [ensureSessions]);
|
|
1557
|
-
const hydrateSession = useCallback(async (prefs, detectedNow, signal) => {
|
|
1675
|
+
const hydrateSession = useCallback(async (prefs, detectedNow, signal, persistSelection = false) => {
|
|
1558
1676
|
if (signal?.aborted)
|
|
1559
1677
|
return;
|
|
1560
1678
|
const scope = await ensureSessions();
|
|
@@ -1564,15 +1682,18 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1564
1682
|
const s = await createAgentSession(prefs, detectedNow, {
|
|
1565
1683
|
scope,
|
|
1566
1684
|
cwd: activeCtx.cwd,
|
|
1567
|
-
...(
|
|
1568
|
-
? {
|
|
1685
|
+
...(activeCtx.additionalDirectories
|
|
1686
|
+
? { additionalDirectories: activeCtx.additionalDirectories }
|
|
1569
1687
|
: {}),
|
|
1688
|
+
...(sessionsRef.current ? { stateRoot: sessionsRef.current.root } : {}),
|
|
1570
1689
|
enableComputerUse: true,
|
|
1571
1690
|
rules: activeCtx.rules,
|
|
1572
1691
|
...(sessionsRef.current ? { sessionGoals: sessionsRef.current.goals } : {}),
|
|
1573
1692
|
...(activeCtx.mcpServers ? { mcpServers: activeCtx.mcpServers } : {}),
|
|
1574
1693
|
...(activeCtx.plugins ? { plugins: activeCtx.plugins } : {}),
|
|
1575
1694
|
...(activeCtx.web ? { web: activeCtx.web } : {}),
|
|
1695
|
+
...(activeCtx.hooks ? { hooks: activeCtx.hooks } : {}),
|
|
1696
|
+
...(activeCtx.compaction ? { compaction: activeCtx.compaction } : {}),
|
|
1576
1697
|
...(activeCtx.sandbox ? { sandbox: activeCtx.sandbox } : {}),
|
|
1577
1698
|
// Somebody is at this terminal, so the model may ask them one
|
|
1578
1699
|
// question when a decision is genuinely theirs.
|
|
@@ -1593,6 +1714,24 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1593
1714
|
}
|
|
1594
1715
|
throw new Error(reason);
|
|
1595
1716
|
}
|
|
1717
|
+
let commands;
|
|
1718
|
+
try {
|
|
1719
|
+
commands = discoverUserCommands({ cwd: activeCtx.cwd, reserved: hostCommandNames() });
|
|
1720
|
+
// Construction and validation precede the saved change. There is no
|
|
1721
|
+
// await between this atomic write and publishing the usable session.
|
|
1722
|
+
if (persistSelection)
|
|
1723
|
+
writePreferences(prefs);
|
|
1724
|
+
}
|
|
1725
|
+
catch (error) {
|
|
1726
|
+
try {
|
|
1727
|
+
await s.close();
|
|
1728
|
+
}
|
|
1729
|
+
catch {
|
|
1730
|
+
// Preserve the admission/save error and the still-active old session.
|
|
1731
|
+
}
|
|
1732
|
+
throw error;
|
|
1733
|
+
}
|
|
1734
|
+
savedPrefsRef.current = prefs;
|
|
1596
1735
|
// A picker-owned provider/model change is one state transition. Clear the
|
|
1597
1736
|
// old model's effort selection before publishing the replacement session
|
|
1598
1737
|
// or releasing any paused queue. Failed and superseded candidates returned
|
|
@@ -1605,13 +1744,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1605
1744
|
void previousSessionRef.current?.close();
|
|
1606
1745
|
previousSessionRef.current = s;
|
|
1607
1746
|
setSession(s);
|
|
1608
|
-
setUserCommands(
|
|
1609
|
-
cwd: activeCtx.cwd,
|
|
1610
|
-
// Builtins are reserved: a `help.md` must not take over `/help`.
|
|
1611
|
-
// Passing the names here is what lets the loader tell its author
|
|
1612
|
-
// the file is shadowed instead of leaving it silently unused.
|
|
1613
|
-
reserved: hostCommandNames(),
|
|
1614
|
-
}));
|
|
1747
|
+
setUserCommands(commands);
|
|
1615
1748
|
const mentionLoadOwner = {};
|
|
1616
1749
|
mentionLoadOwnerRef.current = mentionLoadOwner;
|
|
1617
1750
|
void listMentionableFiles(activeCtx.cwd, appLifetime.signal).then((files) => {
|
|
@@ -1973,6 +2106,13 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1973
2106
|
}
|
|
1974
2107
|
}, [activateTrustedProject, pushMessage, runProbe]);
|
|
1975
2108
|
const finalized = messages.filter((m) => !m.pending);
|
|
2109
|
+
// Activity and the plan share the terminal with the draft. Collapse the two
|
|
2110
|
+
// lists together only when their full previews would crowd the input area.
|
|
2111
|
+
const fullTaskFurniture = tasks.length === 0 ? 0 : Math.min(tasks.length, 8) + 3;
|
|
2112
|
+
const fullToolFurniture = activeTools.length === 0 ? 0 : Math.min(activeTools.length, 3) * 2 + 2;
|
|
2113
|
+
const compactWork = LIVE_FURNITURE_ROWS + fullTaskFurniture + fullToolFurniture >= terminal.rows;
|
|
2114
|
+
const taskFurniture = compactWork && fullTaskFurniture > 0 ? 2 : fullTaskFurniture;
|
|
2115
|
+
const toolFurniture = compactWork && fullToolFurniture > 0 ? 2 : fullToolFurniture;
|
|
1976
2116
|
// How much of the transcript is still redrawable. The rest belongs to native
|
|
1977
2117
|
// terminal scrollback; the live tail stays deliberately small so an activity
|
|
1978
2118
|
// tick never repaints the whole conversation.
|
|
@@ -1984,7 +2124,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1984
2124
|
messages: finalized,
|
|
1985
2125
|
rows: terminal.rows,
|
|
1986
2126
|
columns: terminal.columns,
|
|
1987
|
-
furnitureRows: LIVE_FURNITURE_ROWS,
|
|
2127
|
+
furnitureRows: LIVE_FURNITURE_ROWS + taskFurniture + toolFurniture,
|
|
1988
2128
|
settled: settledRef.current,
|
|
1989
2129
|
raw: rawOutput,
|
|
1990
2130
|
});
|
|
@@ -2003,6 +2143,17 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2003
2143
|
const hostCommands = mergeHostCommands(kernelCommandDescriptors());
|
|
2004
2144
|
const slashCtx = {
|
|
2005
2145
|
cwd: ctx.cwd,
|
|
2146
|
+
jobs: () => session?.jobs?.() ?? [],
|
|
2147
|
+
hooks: () => session?.hooks,
|
|
2148
|
+
directories: () => session?.directories?.list() ?? [],
|
|
2149
|
+
compaction: session
|
|
2150
|
+
? {
|
|
2151
|
+
strategy: ctx.compaction?.strategy ?? 'salience',
|
|
2152
|
+
softTarget: 0.5,
|
|
2153
|
+
triggerThreshold: 0.7,
|
|
2154
|
+
...compactionTally,
|
|
2155
|
+
}
|
|
2156
|
+
: null,
|
|
2006
2157
|
builtins: hostCommands,
|
|
2007
2158
|
lastAssistantMessageId: () => lastAssistantMessage.current?.messageId ?? null,
|
|
2008
2159
|
// Called when `/tools` renders, not read here — the same shape, and the
|
|
@@ -2081,6 +2232,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2081
2232
|
permissionOpenedAtRef.current = null;
|
|
2082
2233
|
setPermissionReviewOffset(0);
|
|
2083
2234
|
setPermissionDetailsOpen(false);
|
|
2235
|
+
setPermissionChoice(0);
|
|
2084
2236
|
setPermission(null);
|
|
2085
2237
|
if (resolve) {
|
|
2086
2238
|
// The provider resumes asynchronously after the decision. Leaving the
|
|
@@ -2207,6 +2359,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2207
2359
|
wakeGoalDriver();
|
|
2208
2360
|
conversationGenRef.current += 1;
|
|
2209
2361
|
resetSubagentActivity();
|
|
2362
|
+
session?.resetTaskStore?.();
|
|
2210
2363
|
activeTurnTokenRef.current = null;
|
|
2211
2364
|
resetTranscript();
|
|
2212
2365
|
setMessages(restored);
|
|
@@ -2241,6 +2394,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2241
2394
|
nextId,
|
|
2242
2395
|
pushMessage,
|
|
2243
2396
|
resetSubagentActivity,
|
|
2397
|
+
session,
|
|
2244
2398
|
resetTranscript,
|
|
2245
2399
|
wakeGoalDriver,
|
|
2246
2400
|
]);
|
|
@@ -2281,6 +2435,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2281
2435
|
wakeGoalDriver();
|
|
2282
2436
|
conversationGenRef.current += 1;
|
|
2283
2437
|
resetSubagentActivity();
|
|
2438
|
+
session?.resetTaskStore?.();
|
|
2284
2439
|
activeTurnTokenRef.current = null;
|
|
2285
2440
|
modelHistoryRef.current = [];
|
|
2286
2441
|
lastAssistantMessage.current = null;
|
|
@@ -2320,6 +2475,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2320
2475
|
interruptTurn,
|
|
2321
2476
|
pushMessage,
|
|
2322
2477
|
resetSubagentActivity,
|
|
2478
|
+
session,
|
|
2323
2479
|
resetTranscript,
|
|
2324
2480
|
wakeGoalDriver,
|
|
2325
2481
|
]);
|
|
@@ -2382,9 +2538,12 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2382
2538
|
}
|
|
2383
2539
|
const scope = scopeRef.current;
|
|
2384
2540
|
if (!scope || scope.sessionId !== prompt.sessionId) {
|
|
2385
|
-
pushMessage('system',
|
|
2386
|
-
|
|
2387
|
-
|
|
2541
|
+
pushMessage('system', 'The conversation changed while this editor was open. Reopen the command in the current conversation.');
|
|
2542
|
+
return;
|
|
2543
|
+
}
|
|
2544
|
+
if (prompt.kind === 'goal-create' || prompt.kind === 'goal-edit') {
|
|
2545
|
+
if (value.trim())
|
|
2546
|
+
commandPickerSubmitRef.current(`/goal ${prompt.kind === 'goal-create' ? 'set' : 'edit'} ${value.trim()}`);
|
|
2388
2547
|
return;
|
|
2389
2548
|
}
|
|
2390
2549
|
if (prompt.kind === 'export-file') {
|
|
@@ -2461,6 +2620,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2461
2620
|
// is reloaded or reset. Only where the NEXT turn is written changes.
|
|
2462
2621
|
scope.sessionId = forked.id;
|
|
2463
2622
|
resetSubagentActivity();
|
|
2623
|
+
session?.resetTaskStore?.();
|
|
2464
2624
|
goalActivation.clear();
|
|
2465
2625
|
wakeGoalDriver();
|
|
2466
2626
|
pushMessage('system', `Forked into "${forked.title}" — ${forked.copied} message(s) copied. This screen continues in the copy; ${original} is unchanged and still in /resume.`);
|
|
@@ -2478,6 +2638,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2478
2638
|
materializeConversation,
|
|
2479
2639
|
pushMessage,
|
|
2480
2640
|
resetSubagentActivity,
|
|
2641
|
+
session,
|
|
2481
2642
|
wakeGoalDriver,
|
|
2482
2643
|
]);
|
|
2483
2644
|
/** Open the durable prompt picker after the composer's second empty Esc. */
|
|
@@ -2502,6 +2663,33 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2502
2663
|
editCommittedRef.current = false;
|
|
2503
2664
|
setPhase('edit');
|
|
2504
2665
|
}, [hasUnsettledTurn, messages, pushMessage, state]);
|
|
2666
|
+
/** `/restore`: list the file checkpoints, or put the tree back to before a turn. */
|
|
2667
|
+
const doRestore = useCallback(async (turn) => {
|
|
2668
|
+
const store = session?.checkpoints;
|
|
2669
|
+
if (!store) {
|
|
2670
|
+
pushMessage('system', 'This session keeps no file checkpoints.');
|
|
2671
|
+
return;
|
|
2672
|
+
}
|
|
2673
|
+
if (turn === undefined) {
|
|
2674
|
+
pushMessage('system', renderCheckpoints(store.list(), ctxRef.current.cwd));
|
|
2675
|
+
return;
|
|
2676
|
+
}
|
|
2677
|
+
if (abortRef.current || state !== 'idle' || hasUnsettledTurn()) {
|
|
2678
|
+
pushMessage('system', 'A turn is still running. Wait for it, or interrupt it, before restoring files.');
|
|
2679
|
+
return;
|
|
2680
|
+
}
|
|
2681
|
+
try {
|
|
2682
|
+
const report = await store.restore(turn);
|
|
2683
|
+
const text = renderRestore(report, ctxRef.current.cwd);
|
|
2684
|
+
pushMessage('system', text);
|
|
2685
|
+
// The model's picture of those files is now wrong; it reads
|
|
2686
|
+
// this before its next turn.
|
|
2687
|
+
operatorShellRef.current.push(`/restore ${turn}\n${text}`);
|
|
2688
|
+
}
|
|
2689
|
+
catch (err) {
|
|
2690
|
+
pushMessage('system', err instanceof Error ? err.message : String(err));
|
|
2691
|
+
}
|
|
2692
|
+
}, [hasUnsettledTurn, pushMessage, session, state]);
|
|
2505
2693
|
/** Fork before the selected prompt, then reopen that prompt in the composer. */
|
|
2506
2694
|
const confirmPromptEdit = useCallback(async (target) => {
|
|
2507
2695
|
if (editCommittedRef.current || conversationMutationRef.current)
|
|
@@ -2536,6 +2724,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2536
2724
|
.map((prompt) => prompt.displayText);
|
|
2537
2725
|
conversationGenRef.current += 1;
|
|
2538
2726
|
resetSubagentActivity();
|
|
2727
|
+
session?.resetTaskStore?.();
|
|
2539
2728
|
activeTurnTokenRef.current = null;
|
|
2540
2729
|
goalActivation.clear();
|
|
2541
2730
|
wakeGoalDriver();
|
|
@@ -2577,6 +2766,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2577
2766
|
nextId,
|
|
2578
2767
|
pushMessage,
|
|
2579
2768
|
resetSubagentActivity,
|
|
2769
|
+
session,
|
|
2580
2770
|
resetTranscript,
|
|
2581
2771
|
wakeGoalDriver,
|
|
2582
2772
|
]);
|
|
@@ -2654,6 +2844,20 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2654
2844
|
st.assistantId = null;
|
|
2655
2845
|
}
|
|
2656
2846
|
};
|
|
2847
|
+
if (event.kind !== 'usage' && 'budget' in event && event.budget) {
|
|
2848
|
+
const budget = event.budget;
|
|
2849
|
+
const own = st.lastUsage;
|
|
2850
|
+
setUsage((previous) => ({
|
|
2851
|
+
totalTokens: budget.ownTokens,
|
|
2852
|
+
budget,
|
|
2853
|
+
cost: {
|
|
2854
|
+
...(own?.cost ?? { totalCost: 0, cacheDiscount: 0, unpricedTokens: 0 }),
|
|
2855
|
+
unpricedTokens: (own?.cost.unpricedTokens ?? 0) +
|
|
2856
|
+
Math.max(0, budget.ownTokens - (own?.totalTokens ?? 0)),
|
|
2857
|
+
},
|
|
2858
|
+
...(own && previous?.context ? { context: previous.context } : {}),
|
|
2859
|
+
}));
|
|
2860
|
+
}
|
|
2657
2861
|
switch (event.kind) {
|
|
2658
2862
|
case 'delta': {
|
|
2659
2863
|
setState('thinking');
|
|
@@ -2668,6 +2872,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2668
2872
|
// turn: the wait before the first token is the model's, and
|
|
2669
2873
|
// counting it would release the opening sentence on arrival.
|
|
2670
2874
|
st.releasedAt ??= Date.now();
|
|
2875
|
+
setThinking(null);
|
|
2671
2876
|
st.text += event.text;
|
|
2672
2877
|
// Held, not appended. Appending each delta is what produced text
|
|
2673
2878
|
// that types itself out — nothing animates it, but a few
|
|
@@ -2697,6 +2902,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2697
2902
|
}
|
|
2698
2903
|
case 'tool-start': {
|
|
2699
2904
|
closeAssistant();
|
|
2905
|
+
setThinking(null);
|
|
2700
2906
|
setState('tool');
|
|
2701
2907
|
const tool = {
|
|
2702
2908
|
id: event.toolUseId,
|
|
@@ -2771,8 +2977,36 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2771
2977
|
break;
|
|
2772
2978
|
}
|
|
2773
2979
|
case 'usage':
|
|
2774
|
-
|
|
2980
|
+
st.lastUsage = event;
|
|
2981
|
+
setUsage({
|
|
2982
|
+
totalTokens: event.totalTokens,
|
|
2983
|
+
...(event.budget ? { budget: event.budget } : {}),
|
|
2984
|
+
cost: event.cost,
|
|
2985
|
+
...(event.contextTokens !== undefined && event.contextWindowTokens !== undefined
|
|
2986
|
+
? {
|
|
2987
|
+
context: {
|
|
2988
|
+
tokens: event.contextTokens,
|
|
2989
|
+
windowTokens: event.contextWindowTokens,
|
|
2990
|
+
measured: event.contextMeasuredBy === 'provider',
|
|
2991
|
+
windowAssumed: event.windowSource !== 'provider' && event.windowSource !== 'config',
|
|
2992
|
+
},
|
|
2993
|
+
}
|
|
2994
|
+
: {}),
|
|
2995
|
+
});
|
|
2775
2996
|
break;
|
|
2997
|
+
case 'reasoning': {
|
|
2998
|
+
if (event.done)
|
|
2999
|
+
break;
|
|
3000
|
+
setState('thinking');
|
|
3001
|
+
// The last line only, bounded: a screen-wide row that changes as
|
|
3002
|
+
// the model writes is a spinner with words, not a transcript.
|
|
3003
|
+
setThinking((prev) => {
|
|
3004
|
+
const joined = `${prev ?? ''}${event.text}`;
|
|
3005
|
+
const lastLine = joined.slice(joined.lastIndexOf('\n') + 1);
|
|
3006
|
+
return lastLine.length > 160 ? `…${lastLine.slice(-159)}` : lastLine;
|
|
3007
|
+
});
|
|
3008
|
+
break;
|
|
3009
|
+
}
|
|
2776
3010
|
case 'task': {
|
|
2777
3011
|
// The live list gets every change; the transcript records the
|
|
2778
3012
|
// opening and the close, as it did before the list existed.
|
|
@@ -2801,6 +3035,11 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2801
3035
|
}
|
|
2802
3036
|
break;
|
|
2803
3037
|
}
|
|
3038
|
+
case 'job':
|
|
3039
|
+
// The kernel saw it end during a turn; the model reads the notice
|
|
3040
|
+
// on its next tool result, the operator reads this row.
|
|
3041
|
+
pushMessage('system', describeJobExit(event), false, '⚙');
|
|
3042
|
+
break;
|
|
2804
3043
|
case 'context':
|
|
2805
3044
|
// Into the TRANSCRIPT, not a status line. A status indicator is
|
|
2806
3045
|
// present while nothing is happening and gone afterwards, so
|
|
@@ -2808,6 +3047,15 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2808
3047
|
// looking at was compacted. The event is a fact about the
|
|
2809
3048
|
// conversation and belongs in its record.
|
|
2810
3049
|
pushMessage('system', event.text, false, event.shed ? '⌫' : '⌦');
|
|
3050
|
+
if (event.shed) {
|
|
3051
|
+
setCompactionTally((tally) => ({
|
|
3052
|
+
passes: tally.passes + 1,
|
|
3053
|
+
clearedResults: tally.clearedResults + (event.cleared ?? 0),
|
|
3054
|
+
stubbedNarrations: tally.stubbedNarrations + (event.stubbed ?? 0),
|
|
3055
|
+
summaries: tally.summaries + (event.summarised ? 1 : 0),
|
|
3056
|
+
reclaimedTokens: tally.reclaimedTokens + (event.reclaimedTokens ?? 0),
|
|
3057
|
+
}));
|
|
3058
|
+
}
|
|
2811
3059
|
break;
|
|
2812
3060
|
case 'provider-fallback':
|
|
2813
3061
|
// The transcript, for the reason compaction is in the transcript
|
|
@@ -2986,6 +3234,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2986
3234
|
const stillHere = () => conversationGenRef.current === turnGeneration;
|
|
2987
3235
|
let inboxOpen = true;
|
|
2988
3236
|
const inboxEntries = [];
|
|
3237
|
+
const inputWaiters = new Set();
|
|
2989
3238
|
const inbox = {
|
|
2990
3239
|
accept(input) {
|
|
2991
3240
|
if (!inboxOpen ||
|
|
@@ -2996,8 +3245,32 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2996
3245
|
return false;
|
|
2997
3246
|
inboxEntries.push(input);
|
|
2998
3247
|
setPendingSteers([...inboxEntries]);
|
|
3248
|
+
for (const wake of [...inputWaiters])
|
|
3249
|
+
wake();
|
|
2999
3250
|
return true;
|
|
3000
3251
|
},
|
|
3252
|
+
waitForInbound(signal) {
|
|
3253
|
+
if (signal.aborted)
|
|
3254
|
+
return Promise.reject(signal.reason);
|
|
3255
|
+
if (!inboxOpen || inboxEntries.length > 0)
|
|
3256
|
+
return Promise.resolve();
|
|
3257
|
+
return new Promise((resolve, reject) => {
|
|
3258
|
+
const cleanup = () => {
|
|
3259
|
+
inputWaiters.delete(wake);
|
|
3260
|
+
signal.removeEventListener('abort', abort);
|
|
3261
|
+
};
|
|
3262
|
+
const wake = () => {
|
|
3263
|
+
cleanup();
|
|
3264
|
+
resolve();
|
|
3265
|
+
};
|
|
3266
|
+
const abort = () => {
|
|
3267
|
+
cleanup();
|
|
3268
|
+
reject(signal.reason);
|
|
3269
|
+
};
|
|
3270
|
+
inputWaiters.add(wake);
|
|
3271
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
3272
|
+
});
|
|
3273
|
+
},
|
|
3001
3274
|
drain() {
|
|
3002
3275
|
if (!inboxOpen ||
|
|
3003
3276
|
ac.signal.aborted ||
|
|
@@ -3025,6 +3298,8 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3025
3298
|
},
|
|
3026
3299
|
close() {
|
|
3027
3300
|
inboxOpen = false;
|
|
3301
|
+
for (const wake of [...inputWaiters])
|
|
3302
|
+
wake();
|
|
3028
3303
|
return inboxEntries.splice(0, inboxEntries.length);
|
|
3029
3304
|
},
|
|
3030
3305
|
};
|
|
@@ -3094,7 +3369,10 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3094
3369
|
onPermission: askPermission,
|
|
3095
3370
|
onQuestion: askQuestion,
|
|
3096
3371
|
inboundMessages: () => inbox.drain(),
|
|
3097
|
-
|
|
3372
|
+
waitForInbound: (signal) => inbox.waitForInbound(signal),
|
|
3373
|
+
extraSystem: [composeSkillsPrompt(activeSkills), drainIdleJobNotices(), drainOperatorShell()]
|
|
3374
|
+
.filter((part) => Boolean(part))
|
|
3375
|
+
.join('\n\n') || undefined,
|
|
3098
3376
|
onConversationMessages: (messages) => {
|
|
3099
3377
|
// State-only: opaque reasoning/signatures must reach the next
|
|
3100
3378
|
// provider and durable store without becoming transcript text.
|
|
@@ -3312,6 +3590,41 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3312
3590
|
return;
|
|
3313
3591
|
}
|
|
3314
3592
|
setHistory((prev) => [...prev, value]);
|
|
3593
|
+
// `#` remembers, `!` runs — neither is a prompt. Both are the
|
|
3594
|
+
// operator acting directly, the way other coding agents spell it,
|
|
3595
|
+
// and both leave a row the model reads on its next turn.
|
|
3596
|
+
if (value.startsWith('#') && value.slice(1).trim().length > 0) {
|
|
3597
|
+
const note = value.slice(1).trim();
|
|
3598
|
+
try {
|
|
3599
|
+
const path = appendMemory(note, { scope: 'project', cwd: ctx.cwd });
|
|
3600
|
+
pushMessage('system', `Remembered for this project (${path}): ${note}`);
|
|
3601
|
+
}
|
|
3602
|
+
catch (err) {
|
|
3603
|
+
pushMessage('system', `Could not save memory: ${err instanceof Error ? err.message : String(err)}`);
|
|
3604
|
+
}
|
|
3605
|
+
return;
|
|
3606
|
+
}
|
|
3607
|
+
const escaped = shellEscapeCommand(value);
|
|
3608
|
+
if (escaped !== null) {
|
|
3609
|
+
pushMessage('user', value);
|
|
3610
|
+
const rowId = pushMessage('tool', `! ${escaped}`, true, '…');
|
|
3611
|
+
void runShellEscape(escaped, { cwd: ctx.cwd }).then((result) => {
|
|
3612
|
+
operatorShellRef.current.push(describeShellEscapeForModel(escaped, result));
|
|
3613
|
+
const ok = result.exitCode === 0 && !result.timedOut;
|
|
3614
|
+
const detail = result.output.length > 0 ? result.output.replace(/\n$/u, '').split('\n') : undefined;
|
|
3615
|
+
setMessages((prev) => prev.map((m) => m.id === rowId
|
|
3616
|
+
? {
|
|
3617
|
+
...m,
|
|
3618
|
+
content: describeShellEscape(escaped, result),
|
|
3619
|
+
pending: false,
|
|
3620
|
+
glyph: ok ? '✓' : '✗',
|
|
3621
|
+
...(ok ? {} : { glyphColor: theme.status.error }),
|
|
3622
|
+
...(detail ? { detail } : {}),
|
|
3623
|
+
}
|
|
3624
|
+
: m));
|
|
3625
|
+
});
|
|
3626
|
+
return;
|
|
3627
|
+
}
|
|
3315
3628
|
// What actually gets sent. A `prompt` action replaces it with text the
|
|
3316
3629
|
// command composed, and then takes the ordinary send path below —
|
|
3317
3630
|
// including the queue — so a command-driven turn is not a second way
|
|
@@ -3339,6 +3652,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3339
3652
|
description: command.problem
|
|
3340
3653
|
? `Unavailable: ${command.problem}`
|
|
3341
3654
|
: command.description,
|
|
3655
|
+
disabledReason: command.problem,
|
|
3342
3656
|
})),
|
|
3343
3657
|
windowSize: suggestionWindowSize(terminal.rows),
|
|
3344
3658
|
});
|
|
@@ -3346,10 +3660,160 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3346
3660
|
}
|
|
3347
3661
|
case 'agent-cockpit': {
|
|
3348
3662
|
if (!openAgentCockpit()) {
|
|
3349
|
-
pushMessage('system',
|
|
3663
|
+
pushMessage('system', `No delegated agents in this conversation.${session?.agentIds.length ? `\nAvailable: ${session.agentIds.join(', ')}` : '\nNo agents are configured.'}`);
|
|
3350
3664
|
}
|
|
3351
3665
|
return;
|
|
3352
3666
|
}
|
|
3667
|
+
case 'settings-picker': {
|
|
3668
|
+
const commands = [
|
|
3669
|
+
{
|
|
3670
|
+
name: 'model',
|
|
3671
|
+
label: 'Model',
|
|
3672
|
+
description: `${session?.providerSummary ?? 'No provider'} · ${session?.modelSummary ?? 'No model'}. Change model or provider.`,
|
|
3673
|
+
},
|
|
3674
|
+
{
|
|
3675
|
+
name: 'effort',
|
|
3676
|
+
label: 'Reasoning effort',
|
|
3677
|
+
description: `${reasoningEffortRef.current ?? session?.reasoningEffortDefault ?? 'Model default'} · applies to future turns in this session.`,
|
|
3678
|
+
...(session?.reasoningEffortLevels === undefined
|
|
3679
|
+
? { problem: 'This model does not publish reasoning choices.' }
|
|
3680
|
+
: {}),
|
|
3681
|
+
},
|
|
3682
|
+
{
|
|
3683
|
+
name: 'permissions',
|
|
3684
|
+
label: 'Permissions',
|
|
3685
|
+
description: `${permissionModeLabel(effectivePermissionMode(permissionModeRef.current, session?.approvalLatched() ?? false))} · this session.`,
|
|
3686
|
+
},
|
|
3687
|
+
{
|
|
3688
|
+
name: 'status config',
|
|
3689
|
+
label: 'Configuration',
|
|
3690
|
+
description: 'Show which configuration files and overrides are in use.',
|
|
3691
|
+
},
|
|
3692
|
+
];
|
|
3693
|
+
setSelectedChoice(0);
|
|
3694
|
+
setChoicePicker({
|
|
3695
|
+
kind: 'command',
|
|
3696
|
+
title: 'Settings',
|
|
3697
|
+
notice: 'Select a setting to view or change it.',
|
|
3698
|
+
values: commands,
|
|
3699
|
+
options: commands.map((command) => ({
|
|
3700
|
+
label: command.label,
|
|
3701
|
+
description: command.description,
|
|
3702
|
+
disabledReason: command.problem,
|
|
3703
|
+
})),
|
|
3704
|
+
windowSize: 7,
|
|
3705
|
+
});
|
|
3706
|
+
return;
|
|
3707
|
+
}
|
|
3708
|
+
case 'goal-editor': {
|
|
3709
|
+
const scope = scopeRef.current;
|
|
3710
|
+
if (!scope || !sessionsRef.current) {
|
|
3711
|
+
pushMessage('system', 'Goals require conversation history to be available.');
|
|
3712
|
+
return;
|
|
3713
|
+
}
|
|
3714
|
+
textPromptTokenRef.current += 1;
|
|
3715
|
+
setTextPrompt({
|
|
3716
|
+
token: textPromptTokenRef.current,
|
|
3717
|
+
kind: slash.edit ? 'goal-edit' : 'goal-create',
|
|
3718
|
+
title: slash.edit ? 'Edit goal' : 'Set a goal',
|
|
3719
|
+
placeholder: `Up to ${goalStatus?.maxGoalRounds ?? DEFAULT_MAX_GOAL_ROUNDS} automatic turns; /goal pause stops continuation.`,
|
|
3720
|
+
emptyNotice: 'Describe the result you want, or press Esc to cancel.',
|
|
3721
|
+
initialValue: slash.edit ? (goalStatus?.objective ?? '') : '',
|
|
3722
|
+
sessionId: scope.sessionId,
|
|
3723
|
+
});
|
|
3724
|
+
return;
|
|
3725
|
+
}
|
|
3726
|
+
case 'goal-picker': {
|
|
3727
|
+
const generation = conversationGenRef.current;
|
|
3728
|
+
const request = {};
|
|
3729
|
+
setSelectedChoice(-1);
|
|
3730
|
+
setChoicePicker({
|
|
3731
|
+
kind: 'command',
|
|
3732
|
+
title: 'Goal',
|
|
3733
|
+
notice: 'Loading goal…',
|
|
3734
|
+
values: [],
|
|
3735
|
+
options: [],
|
|
3736
|
+
windowSize: 7,
|
|
3737
|
+
request,
|
|
3738
|
+
});
|
|
3739
|
+
void (async () => {
|
|
3740
|
+
try {
|
|
3741
|
+
const sessions = sessionsRef.current;
|
|
3742
|
+
const scope = scopeRef.current;
|
|
3743
|
+
if (!sessions || !scope) {
|
|
3744
|
+
setChoicePicker(null);
|
|
3745
|
+
pushMessage('system', 'Goals require conversation history to be available.');
|
|
3746
|
+
return;
|
|
3747
|
+
}
|
|
3748
|
+
const goal = conversationMaterializedRef.current
|
|
3749
|
+
? await sessions.goals.getGoal(scope.sessionId, sessions.tenantId)
|
|
3750
|
+
: null;
|
|
3751
|
+
if (appLifetime.signal.aborted ||
|
|
3752
|
+
conversationGenRef.current !== generation ||
|
|
3753
|
+
choicePickerRef.current?.request !== request)
|
|
3754
|
+
return;
|
|
3755
|
+
setGoalStatus(goal);
|
|
3756
|
+
const commands = goal
|
|
3757
|
+
? [
|
|
3758
|
+
{
|
|
3759
|
+
name: 'goal status',
|
|
3760
|
+
description: 'View goal progress and continuation status.',
|
|
3761
|
+
},
|
|
3762
|
+
{ name: 'goal edit', description: 'Change the objective.' },
|
|
3763
|
+
...(goal.phase === 'complete'
|
|
3764
|
+
? []
|
|
3765
|
+
: [
|
|
3766
|
+
goal.phase === 'active' && goalActivation.isArmed(scope.sessionId, goal)
|
|
3767
|
+
? {
|
|
3768
|
+
name: 'goal pause',
|
|
3769
|
+
description: 'Stop automatic continuation after the current turn.',
|
|
3770
|
+
}
|
|
3771
|
+
: {
|
|
3772
|
+
name: 'goal resume',
|
|
3773
|
+
description: 'Continue working automatically toward this goal.',
|
|
3774
|
+
},
|
|
3775
|
+
]),
|
|
3776
|
+
{
|
|
3777
|
+
name: 'goal clear',
|
|
3778
|
+
description: 'Remove this goal and stop its automatic continuation.',
|
|
3779
|
+
},
|
|
3780
|
+
]
|
|
3781
|
+
: [
|
|
3782
|
+
{
|
|
3783
|
+
name: 'goal set',
|
|
3784
|
+
description: 'Set an objective and start automatic continuation.',
|
|
3785
|
+
},
|
|
3786
|
+
];
|
|
3787
|
+
setSelectedChoice(0);
|
|
3788
|
+
setChoicePicker({
|
|
3789
|
+
kind: 'command',
|
|
3790
|
+
back: choicePickerRef.current?.back,
|
|
3791
|
+
title: 'Goal',
|
|
3792
|
+
notice: goal
|
|
3793
|
+
? `${goal.phase} · ${goal.roundsAdmitted}/${goal.maxGoalRounds} automatic turns · ${goal.objective}`
|
|
3794
|
+
: `No goal set for this conversation. Up to ${DEFAULT_MAX_GOAL_ROUNDS} automatic turns when started.`,
|
|
3795
|
+
values: commands,
|
|
3796
|
+
options: commands.map((command) => ({
|
|
3797
|
+
label: `/${command.name}`,
|
|
3798
|
+
description: command.description,
|
|
3799
|
+
})),
|
|
3800
|
+
windowSize: 7,
|
|
3801
|
+
});
|
|
3802
|
+
}
|
|
3803
|
+
catch (error) {
|
|
3804
|
+
if (!appLifetime.signal.aborted &&
|
|
3805
|
+
conversationGenRef.current === generation &&
|
|
3806
|
+
choicePickerRef.current?.request === request) {
|
|
3807
|
+
setChoicePicker(null);
|
|
3808
|
+
pushMessage('system', `Could not load goal: ${error instanceof Error ? error.message : String(error)}`);
|
|
3809
|
+
}
|
|
3810
|
+
}
|
|
3811
|
+
finally {
|
|
3812
|
+
wakeGoalDriver();
|
|
3813
|
+
}
|
|
3814
|
+
})();
|
|
3815
|
+
return;
|
|
3816
|
+
}
|
|
3353
3817
|
case 'clear-screen':
|
|
3354
3818
|
setMessages([]);
|
|
3355
3819
|
resetTranscript();
|
|
@@ -3419,7 +3883,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3419
3883
|
setKeyEntryFor(null);
|
|
3420
3884
|
setPickerDetected(null);
|
|
3421
3885
|
setPickerSelectionKind('provider-and-model');
|
|
3422
|
-
setPickerInitialView('
|
|
3886
|
+
setPickerInitialView('models');
|
|
3423
3887
|
setPhase('picker');
|
|
3424
3888
|
return;
|
|
3425
3889
|
case 'permission-mode': {
|
|
@@ -3428,7 +3892,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3428
3892
|
}
|
|
3429
3893
|
case 'permission-mode-picker': {
|
|
3430
3894
|
if (!session?.hasProvider) {
|
|
3431
|
-
pushMessage('system', '
|
|
3895
|
+
pushMessage('system', 'Choose a model before changing permissions.');
|
|
3432
3896
|
return;
|
|
3433
3897
|
}
|
|
3434
3898
|
if (state !== 'idle' ||
|
|
@@ -3438,22 +3902,14 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3438
3902
|
permissionResolveRef.current !== null ||
|
|
3439
3903
|
compactingRef.current ||
|
|
3440
3904
|
!session.resetApprovalLatch) {
|
|
3441
|
-
pushMessage('system',
|
|
3905
|
+
pushMessage('system', !session.resetApprovalLatch
|
|
3906
|
+
? 'This session cannot reset approvals. Reconnect before changing permissions.'
|
|
3907
|
+
: 'Finish or stop the current work before changing permissions.');
|
|
3442
3908
|
return;
|
|
3443
3909
|
}
|
|
3444
|
-
const
|
|
3445
|
-
setSelectedChoice(Math.max(0,
|
|
3446
|
-
setChoicePicker(
|
|
3447
|
-
kind: 'permission-mode',
|
|
3448
|
-
title: 'Select Permission Mode',
|
|
3449
|
-
notice: permissionPickerNotice(session),
|
|
3450
|
-
values,
|
|
3451
|
-
options: values.map((mode) => ({
|
|
3452
|
-
label: mode,
|
|
3453
|
-
description: permissionModeDescription(mode),
|
|
3454
|
-
current: mode === permissionModeRef.current,
|
|
3455
|
-
})),
|
|
3456
|
-
});
|
|
3910
|
+
const picker = permissionPicker(permissionModeRef.current, session.approvalLatched());
|
|
3911
|
+
setSelectedChoice(Math.max(0, picker.options.findIndex((option) => option.current)));
|
|
3912
|
+
setChoicePicker(picker);
|
|
3457
3913
|
return;
|
|
3458
3914
|
}
|
|
3459
3915
|
case 'reasoning-effort': {
|
|
@@ -3486,6 +3942,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3486
3942
|
label: effort ?? 'default',
|
|
3487
3943
|
description: reasoningEffortDescription(effort),
|
|
3488
3944
|
current: effort === current,
|
|
3945
|
+
default: effort === undefined,
|
|
3489
3946
|
})),
|
|
3490
3947
|
});
|
|
3491
3948
|
return;
|
|
@@ -3527,16 +3984,15 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3527
3984
|
}
|
|
3528
3985
|
case 'remember':
|
|
3529
3986
|
try {
|
|
3530
|
-
appendMemory(slash.text);
|
|
3531
|
-
pushMessage('system', `Remembered: ${slash.text}`);
|
|
3987
|
+
const path = appendMemory(slash.text, { scope: slash.scope, cwd: ctx.cwd });
|
|
3988
|
+
pushMessage('system', `Remembered ${slash.scope === 'user' ? 'for every project' : 'for this project'} (${path}): ${slash.text}`);
|
|
3532
3989
|
}
|
|
3533
3990
|
catch (err) {
|
|
3534
3991
|
pushMessage('system', `Could not save memory: ${err instanceof Error ? err.message : String(err)}`);
|
|
3535
3992
|
}
|
|
3536
3993
|
return;
|
|
3537
3994
|
case 'show-memory': {
|
|
3538
|
-
|
|
3539
|
-
pushMessage('system', mem ?? 'Nothing remembered yet. Use /remember <text>, or edit ~/.namzu/MEMORY.md.');
|
|
3995
|
+
pushMessage('system', renderMemoryReport(readMemory(undefined, ctx.cwd), { cwd: ctx.cwd }));
|
|
3540
3996
|
return;
|
|
3541
3997
|
}
|
|
3542
3998
|
case 'list-skills': {
|
|
@@ -3595,79 +4051,19 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3595
4051
|
case 'fork':
|
|
3596
4052
|
void doFork();
|
|
3597
4053
|
return;
|
|
3598
|
-
case '
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
// shapes with a hand-written copy between them — the geometry
|
|
3612
|
-
// `docs/conventions/one-site-is-not-every-site.md` names as the
|
|
3613
|
-
// hardest kind to spot, adopted here to solve a staleness problem
|
|
3614
|
-
// the updater solves without it.
|
|
3615
|
-
//
|
|
3616
|
-
// The id is taken outside so both branches use the same one.
|
|
3617
|
-
const id = nextId();
|
|
3618
|
-
setMessages((prev) => {
|
|
3619
|
-
const blocks = prev.filter((m) => m.detailRef !== undefined);
|
|
3620
|
-
const say = (content) => ({
|
|
3621
|
-
id,
|
|
3622
|
-
role: 'system',
|
|
3623
|
-
content,
|
|
3624
|
-
});
|
|
3625
|
-
if (blocks.length === 0) {
|
|
3626
|
-
return [
|
|
3627
|
-
...prev,
|
|
3628
|
-
say('Nothing to expand yet. Tool output longer than six lines collapses with a "… +N lines · /expand n" hint, and that number is what this takes.'),
|
|
3629
|
-
];
|
|
3630
|
-
}
|
|
3631
|
-
const block = slash.which === 'last'
|
|
3632
|
-
? blocks[blocks.length - 1]
|
|
3633
|
-
: blocks.find((m) => m.detailRef === slash.which);
|
|
3634
|
-
if (!block) {
|
|
3635
|
-
// Names the range rather than only refusing: the operator's
|
|
3636
|
-
// next move is to type a different number, and they should
|
|
3637
|
-
// not have to guess which ones exist.
|
|
3638
|
-
return [
|
|
3639
|
-
...prev,
|
|
3640
|
-
say(`No collapsed output numbered ${slash.which}. This conversation has ${blocks.length === 1
|
|
3641
|
-
? 'one, numbered 1'
|
|
3642
|
-
: `${blocks.length}, numbered 1 to ${blocks.length}`}.`),
|
|
3643
|
-
];
|
|
3644
|
-
}
|
|
3645
|
-
// A NEW row carrying the same lines, flagged to print them all.
|
|
3646
|
-
// Not a mutation of the row that collapsed them: that row was
|
|
3647
|
-
// handed to <Static>, which emits an item once and calls the
|
|
3648
|
-
// render function only for items it has not emitted yet, so
|
|
3649
|
-
// nothing decided now can reach it. Appending is the only
|
|
3650
|
-
// expansion this architecture has.
|
|
3651
|
-
//
|
|
3652
|
-
// The lines travel as `detail` rather than flattened into the
|
|
3653
|
-
// content so the `▏` rule and the +/- diff colouring survive.
|
|
3654
|
-
// An expansion that showed LESS than the collapsed form would
|
|
3655
|
-
// be a strange thing to have asked for.
|
|
3656
|
-
//
|
|
3657
|
-
// It carries no `detailRef` of its own: it is already in full,
|
|
3658
|
-
// so there is nothing for a number to offer, and giving it one
|
|
3659
|
-
// would let `/expand` produce a third copy of the same output.
|
|
3660
|
-
return [
|
|
3661
|
-
...prev,
|
|
3662
|
-
{
|
|
3663
|
-
id,
|
|
3664
|
-
role: 'system',
|
|
3665
|
-
content: `${block.content} — in full (${block.detail?.length ?? 0} lines)`,
|
|
3666
|
-
glyph: '⤢',
|
|
3667
|
-
detail: block.detail,
|
|
3668
|
-
detailExpanded: true,
|
|
3669
|
-
},
|
|
3670
|
-
];
|
|
4054
|
+
case 'restore':
|
|
4055
|
+
void doRestore(slash.turn);
|
|
4056
|
+
return;
|
|
4057
|
+
case 'add-dir': {
|
|
4058
|
+
const dirs = session?.directories;
|
|
4059
|
+
if (!dirs) {
|
|
4060
|
+
pushMessage('system', 'No session yet; /add-dir works once one has started.');
|
|
4061
|
+
return;
|
|
4062
|
+
}
|
|
4063
|
+
void dirs.add(slash.path).then((result) => {
|
|
4064
|
+
pushMessage('system', result.added
|
|
4065
|
+
? `Added ${result.path}. The file tools reach it by absolute path from the next turn; a sandboxed run binds it read-write.`
|
|
4066
|
+
: `Not added: ${result.path} — ${result.reason ?? 'refused'}`);
|
|
3671
4067
|
});
|
|
3672
4068
|
return;
|
|
3673
4069
|
}
|
|
@@ -3702,9 +4098,17 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3702
4098
|
await materializeConversation();
|
|
3703
4099
|
const durableSessions = sessionsRef.current;
|
|
3704
4100
|
const runScope = scopeRef.current;
|
|
4101
|
+
const taskStore = slash.name === 'tasks' ? session?.currentTaskStore?.() : undefined;
|
|
4102
|
+
if (slash.name === 'tasks' && !taskStore) {
|
|
4103
|
+
pushMessage('system', session?.currentTaskStore
|
|
4104
|
+
? 'No task list is available yet for this conversation.'
|
|
4105
|
+
: 'Task listing is unavailable in this session.');
|
|
4106
|
+
return;
|
|
4107
|
+
}
|
|
3705
4108
|
const registry = new HostCommandRegistry();
|
|
3706
4109
|
registry.register(kernelHostCommands({
|
|
3707
4110
|
allowedAgentIds: session?.agentIds ?? [],
|
|
4111
|
+
...(taskStore ? { taskStore } : {}),
|
|
3708
4112
|
...(durableSessions && runScope && conversationMaterializedRef.current
|
|
3709
4113
|
? {
|
|
3710
4114
|
goal: {
|
|
@@ -4004,11 +4408,15 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4004
4408
|
advanceQueueContinuation,
|
|
4005
4409
|
applyPermissionMode,
|
|
4006
4410
|
applyReasoningEffort,
|
|
4411
|
+
appLifetime,
|
|
4007
4412
|
ctx.cwd,
|
|
4008
4413
|
doResume,
|
|
4009
4414
|
enqueueQueued,
|
|
4010
4415
|
exitWithSummary,
|
|
4011
4416
|
hasUnsettledTurn,
|
|
4417
|
+
goalActivation,
|
|
4418
|
+
goalStatus,
|
|
4419
|
+
hostCommands,
|
|
4012
4420
|
nextId,
|
|
4013
4421
|
openAgentCockpit,
|
|
4014
4422
|
pushMessage,
|
|
@@ -4016,6 +4424,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4016
4424
|
removeStoredCredential,
|
|
4017
4425
|
resetTranscript,
|
|
4018
4426
|
runConversationExport,
|
|
4427
|
+
session,
|
|
4019
4428
|
setChoicePicker,
|
|
4020
4429
|
setCopyPicker,
|
|
4021
4430
|
setReasoningEffort,
|
|
@@ -4224,9 +4633,9 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4224
4633
|
pushMessage('system', `⬆ Update available:\n${lines.join('\n')}`);
|
|
4225
4634
|
});
|
|
4226
4635
|
}, [phase, ctx.version, pushMessage]);
|
|
4227
|
-
const hydrateFromPicker = useCallback(async (prefs, detectedNow, signal,
|
|
4636
|
+
const hydrateFromPicker = useCallback(async (prefs, detectedNow, signal, options = {}) => {
|
|
4228
4637
|
try {
|
|
4229
|
-
await hydrateSession(prefs, detectedNow, signal);
|
|
4638
|
+
await hydrateSession(prefs, detectedNow, signal, options.persistSelection);
|
|
4230
4639
|
}
|
|
4231
4640
|
catch (err) {
|
|
4232
4641
|
// A superseded choice no longer owns even its failure message. Its
|
|
@@ -4234,7 +4643,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4234
4643
|
// choice stays on the picker with the actionable construction error.
|
|
4235
4644
|
if (signal.aborted)
|
|
4236
4645
|
return;
|
|
4237
|
-
if (revealAllOnFailure) {
|
|
4646
|
+
if (options.revealAllOnFailure) {
|
|
4238
4647
|
setPickerDetected(null);
|
|
4239
4648
|
setPickerSelectionKind('provider-and-model');
|
|
4240
4649
|
setPickerInitialView('providers');
|
|
@@ -4259,48 +4668,29 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4259
4668
|
setPickerNotice(null);
|
|
4260
4669
|
// The disposition, not the key. `credential` never reaches a message.
|
|
4261
4670
|
pushMessage('system', disposition);
|
|
4262
|
-
//
|
|
4263
|
-
//
|
|
4264
|
-
// from the id alone would have been a quiet demotion in the one case
|
|
4265
|
-
// this routing creates: an operator whose file pins a model, and whose
|
|
4266
|
-
// only problem was a missing secret, would have been moved onto the
|
|
4267
|
-
// registry default for supplying it.
|
|
4671
|
+
// Supplying a credential changes the primary's access, not unrelated
|
|
4672
|
+
// fallbacks, model pins, delegation or capability preferences.
|
|
4268
4673
|
const saved = savedPrefsRef.current;
|
|
4269
4674
|
const prefs = saved && primaryProvider(saved).id === credential.entry.id
|
|
4270
4675
|
? saved
|
|
4271
|
-
: {
|
|
4272
|
-
version: 3,
|
|
4273
|
-
providers: [{ id: credential.entry.id }],
|
|
4274
|
-
subagents: { active: [] },
|
|
4275
|
-
};
|
|
4676
|
+
: selectPrimaryProvider(saved, { id: credential.entry.id });
|
|
4276
4677
|
void hydrateFromPicker(prefs, next, signal);
|
|
4277
4678
|
}, [detected, hydrateFromPicker, pushMessage]);
|
|
4278
4679
|
const handlePickerSubmit = useCallback((selection, signal) => {
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
providers: [
|
|
4284
|
-
{
|
|
4285
|
-
id: selection.provider,
|
|
4286
|
-
...(selection.model !== undefined ? { model: selection.model } : {}),
|
|
4287
|
-
},
|
|
4288
|
-
],
|
|
4289
|
-
subagents: { active: [] },
|
|
4290
|
-
};
|
|
4680
|
+
const prefs = selectPrimaryProvider(savedPrefsRef.current, {
|
|
4681
|
+
id: selection.provider,
|
|
4682
|
+
...(selection.model !== undefined ? { model: selection.model } : {}),
|
|
4683
|
+
});
|
|
4291
4684
|
setKeyEntryFor(null);
|
|
4292
4685
|
setPickerNotice(null);
|
|
4293
|
-
try {
|
|
4294
|
-
writePreferences(prefs);
|
|
4295
|
-
}
|
|
4296
|
-
catch (err) {
|
|
4297
|
-
pushMessage('system', `Could not save preferences: ${err instanceof Error ? err.message : String(err)}`);
|
|
4298
|
-
return;
|
|
4299
|
-
}
|
|
4300
4686
|
// Keep a narrowed signed-in roster stable while construction is pending.
|
|
4301
4687
|
// Only a real refusal broadens back to the general, sign-in-capable picker.
|
|
4302
|
-
void hydrateFromPicker(prefs, detected, signal,
|
|
4303
|
-
|
|
4688
|
+
void hydrateFromPicker(prefs, detected, signal, {
|
|
4689
|
+
revealAllOnFailure: true,
|
|
4690
|
+
persistSelection: detected.find((provider) => provider.entry.id === selection.provider)?.source.kind !==
|
|
4691
|
+
'session',
|
|
4692
|
+
});
|
|
4693
|
+
}, [detected, hydrateFromPicker]);
|
|
4304
4694
|
/**
|
|
4305
4695
|
* Leaving the picker returns to whatever was on screen before it.
|
|
4306
4696
|
*
|
|
@@ -4330,6 +4720,13 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4330
4720
|
exitWithSummary();
|
|
4331
4721
|
}, [session, exitWithSummary]);
|
|
4332
4722
|
useInput((input, key) => {
|
|
4723
|
+
// Startup has refused admission, so there is no draft or active turn to
|
|
4724
|
+
// protect with the ready screen's two-press exit ladder.
|
|
4725
|
+
if (phase === 'unhealthy') {
|
|
4726
|
+
if (key.escape || (key.ctrl && input === 'c'))
|
|
4727
|
+
exitWithSummary();
|
|
4728
|
+
return;
|
|
4729
|
+
}
|
|
4333
4730
|
// The provider picker owns its own keyboard — arrows, digits, enter,
|
|
4334
4731
|
// esc and the typed-key screen are all its business. Ctrl+C is not: it
|
|
4335
4732
|
// is the one key that must work on every screen, and this handler used
|
|
@@ -4347,12 +4744,11 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4347
4744
|
exitWithSummary();
|
|
4348
4745
|
return;
|
|
4349
4746
|
}
|
|
4350
|
-
// Previous-prompt picker
|
|
4351
|
-
// older prompts, matching the second Esc that opened it; q is the cancel.
|
|
4747
|
+
// Previous-prompt picker uses the same cancel key as other selectors.
|
|
4352
4748
|
if (phase === 'edit') {
|
|
4353
4749
|
if (editCommittedRef.current)
|
|
4354
4750
|
return;
|
|
4355
|
-
if ((key.ctrl && input === 'c') || input.toLowerCase() === 'q') {
|
|
4751
|
+
if (key.escape || (key.ctrl && input === 'c') || input.toLowerCase() === 'q') {
|
|
4356
4752
|
setEditList([]);
|
|
4357
4753
|
setPhase('ready');
|
|
4358
4754
|
return;
|
|
@@ -4361,7 +4757,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4361
4757
|
setSelectedEdit((index) => moveSelection(index, editList.length, key.home ? 'first' : key.end ? 'last' : key.pageUp ? 'previous-page' : 'next-page'));
|
|
4362
4758
|
return;
|
|
4363
4759
|
}
|
|
4364
|
-
if (key.
|
|
4760
|
+
if (key.leftArrow || key.upArrow) {
|
|
4365
4761
|
setSelectedEdit((index) => moveSelection(index, editList.length, 'previous'));
|
|
4366
4762
|
return;
|
|
4367
4763
|
}
|
|
@@ -4435,25 +4831,27 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4435
4831
|
setPermissionDetailsOpen(!permissionDetailsOpenRef.current);
|
|
4436
4832
|
return;
|
|
4437
4833
|
}
|
|
4438
|
-
if (permission &&
|
|
4439
|
-
|
|
4834
|
+
if (permission && (key.upArrow || key.downArrow)) {
|
|
4835
|
+
const current = permissionChoiceRef.current;
|
|
4836
|
+
const next = key.upArrow ? Math.max(0, current - 1) : Math.min(2, current + 1);
|
|
4837
|
+
setPermissionChoice(next);
|
|
4838
|
+
return;
|
|
4839
|
+
}
|
|
4840
|
+
if (permission && (key.pageUp || key.pageDown || key.home || key.end)) {
|
|
4440
4841
|
const source = permissionDetailsOpenRef.current
|
|
4441
4842
|
? permission.review
|
|
4442
4843
|
: permission.summary.text;
|
|
4443
4844
|
const count = permissionReviewRows(source, terminal.columns).length;
|
|
4444
|
-
const
|
|
4845
|
+
const pageRows = permissionReviewPageRows(terminal.rows);
|
|
4846
|
+
const maxOffset = Math.max(0, count - pageRows);
|
|
4445
4847
|
const current = Math.min(permissionReviewOffsetRef.current, maxOffset);
|
|
4446
4848
|
const next = key.home
|
|
4447
4849
|
? 0
|
|
4448
4850
|
: key.end
|
|
4449
4851
|
? maxOffset
|
|
4450
4852
|
: key.pageUp
|
|
4451
|
-
? Math.max(0, current -
|
|
4452
|
-
:
|
|
4453
|
-
? Math.min(maxOffset, current + PERMISSION_REVIEW_PAGE_ROWS)
|
|
4454
|
-
: key.upArrow
|
|
4455
|
-
? Math.max(0, current - 1)
|
|
4456
|
-
: Math.min(maxOffset, current + 1);
|
|
4853
|
+
? Math.max(0, current - pageRows)
|
|
4854
|
+
: Math.min(maxOffset, current + pageRows);
|
|
4457
4855
|
setPermissionReviewOffset(next);
|
|
4458
4856
|
return;
|
|
4459
4857
|
}
|
|
@@ -4468,18 +4866,21 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4468
4866
|
resolvePermission({ kind: 'reject' });
|
|
4469
4867
|
return;
|
|
4470
4868
|
}
|
|
4471
|
-
//
|
|
4472
|
-
//
|
|
4473
|
-
//
|
|
4474
|
-
//
|
|
4475
|
-
// operator never looked at.
|
|
4476
|
-
// they must arrive after the prompt has been up long enough to read.
|
|
4869
|
+
// Every approval waits out the settle window: `y`, `a`, a digit and
|
|
4870
|
+
// Enter alike. Enter confirms the answer the cursor is on, which
|
|
4871
|
+
// opens on "Yes" — the prompt an operator knows from elsewhere — so
|
|
4872
|
+
// the window is what stands between an in-flight Enter and a call
|
|
4873
|
+
// the operator never looked at. Refusals above never wait.
|
|
4477
4874
|
if (!approvalIsDeliberate(permissionOpenedAtRef.current, Date.now()))
|
|
4478
4875
|
return;
|
|
4479
|
-
|
|
4876
|
+
const numbered = ch === '1' ? 0 : ch === '2' ? 1 : ch === '3' ? 2 : null;
|
|
4877
|
+
const chosen = key.return ? permissionChoiceRef.current : numbered;
|
|
4878
|
+
if (ch === 'y' || chosen === 0)
|
|
4480
4879
|
resolvePermission({ kind: 'approve' });
|
|
4481
|
-
else if (ch === 'a')
|
|
4880
|
+
else if (ch === 'a' || chosen === 1)
|
|
4482
4881
|
resolvePermission({ kind: 'approve-all' });
|
|
4882
|
+
else if (chosen === 2)
|
|
4883
|
+
resolvePermission({ kind: 'reject' });
|
|
4483
4884
|
return;
|
|
4484
4885
|
}
|
|
4485
4886
|
// The active child count already advertises this key beside Working.
|
|
@@ -4502,7 +4903,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4502
4903
|
// highlighted child before the list has appeared on screen.
|
|
4503
4904
|
if (agentSurfaceCommittedRef.current !== agentView)
|
|
4504
4905
|
return;
|
|
4505
|
-
const agents =
|
|
4906
|
+
const agents = subagentsRef.current;
|
|
4506
4907
|
if (agentView.kind === 'cockpit') {
|
|
4507
4908
|
const phases = agentPhases(agents);
|
|
4508
4909
|
const phaseIndex = Math.max(0, phases.findIndex((phase) => phase.id === agentView.selectedPhaseId));
|
|
@@ -4629,10 +5030,17 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4629
5030
|
// menu. Only the exact object React committed may consume a choice.
|
|
4630
5031
|
if (choicePickerCommittedRef.current !== picker)
|
|
4631
5032
|
return;
|
|
4632
|
-
const
|
|
5033
|
+
const searchable = choicePickerSearchable(picker);
|
|
5034
|
+
const options = filterChoiceOptions(picker.options, choiceQueryRef.current);
|
|
5035
|
+
const applyVisibleChoice = (index) => {
|
|
5036
|
+
const option = options[index];
|
|
5037
|
+
if (option)
|
|
5038
|
+
applyChoiceSelection(picker.options.indexOf(option));
|
|
5039
|
+
};
|
|
4633
5040
|
if (key.escape || (key.ctrl && input === 'c')) {
|
|
4634
5041
|
reviewChoiceInFlightRef.current = null;
|
|
4635
|
-
|
|
5042
|
+
setSelectedChoice(0);
|
|
5043
|
+
setChoicePicker(key.escape ? (picker.back ?? null) : null);
|
|
4636
5044
|
// Esc leaves the question unanswered and the turn running; the
|
|
4637
5045
|
// tool tells the model so. Ctrl+C is the operator saying stop.
|
|
4638
5046
|
if (picker.kind === 'user-question') {
|
|
@@ -4641,26 +5049,42 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4641
5049
|
return;
|
|
4642
5050
|
}
|
|
4643
5051
|
if (key.home || key.end || key.pageUp || key.pageDown) {
|
|
4644
|
-
const pageSize =
|
|
4645
|
-
|
|
5052
|
+
const pageSize = choicePickerWindowSize({
|
|
5053
|
+
rows: terminal.rows,
|
|
5054
|
+
columns: Math.max(1, (terminal.columns ?? 80) - 2),
|
|
5055
|
+
searchable,
|
|
5056
|
+
notice: Boolean(picker.notice),
|
|
5057
|
+
selectedDescription: options.some((option) => Boolean(option.selectedDescription || option.disabledReason)),
|
|
5058
|
+
windowSize: picker.kind === 'command' ? picker.windowSize : undefined,
|
|
5059
|
+
});
|
|
5060
|
+
setSelectedChoice((index) => moveChoiceSelection(options, index, key.home ? 'first' : key.end ? 'last' : key.pageUp ? 'previous-page' : 'next-page', pageSize));
|
|
4646
5061
|
return;
|
|
4647
5062
|
}
|
|
4648
5063
|
if (key.upArrow) {
|
|
4649
|
-
setSelectedChoice((index) =>
|
|
5064
|
+
setSelectedChoice((index) => moveChoiceSelection(options, index, 'previous'));
|
|
4650
5065
|
return;
|
|
4651
5066
|
}
|
|
4652
5067
|
if (key.downArrow) {
|
|
4653
|
-
setSelectedChoice((index) =>
|
|
5068
|
+
setSelectedChoice((index) => moveChoiceSelection(options, index, 'next'));
|
|
4654
5069
|
return;
|
|
4655
5070
|
}
|
|
4656
5071
|
if (key.return) {
|
|
4657
|
-
|
|
5072
|
+
applyVisibleChoice(selectedChoiceRef.current);
|
|
5073
|
+
return;
|
|
5074
|
+
}
|
|
5075
|
+
if (searchable && !key.ctrl && !key.meta && (key.backspace || key.delete || input)) {
|
|
5076
|
+
const next = key.backspace || key.delete
|
|
5077
|
+
? eraseLastChoiceGrapheme(choiceQueryRef.current)
|
|
5078
|
+
: choiceQueryRef.current + input.replace(/\p{Cc}/gu, '');
|
|
5079
|
+
choiceQueryRef.current = next;
|
|
5080
|
+
setChoiceQuery(next);
|
|
5081
|
+
setSelectedChoice(moveChoiceSelection(filterChoiceOptions(picker.options, next), 0, 'first'));
|
|
4658
5082
|
return;
|
|
4659
5083
|
}
|
|
4660
5084
|
if (/^[1-9]$/.test(input)) {
|
|
4661
5085
|
const index = Number(input) - 1;
|
|
4662
5086
|
if (options[index])
|
|
4663
|
-
|
|
5087
|
+
applyVisibleChoice(index);
|
|
4664
5088
|
}
|
|
4665
5089
|
return;
|
|
4666
5090
|
}
|
|
@@ -4739,19 +5163,36 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4739
5163
|
// in place and sometimes prints a second copy further down would be two
|
|
4740
5164
|
// commands wearing one name.
|
|
4741
5165
|
if (key.ctrl && input === 'o') {
|
|
4742
|
-
// Taken outside the updater so both branches use the same one
|
|
4743
|
-
// `/expand` does.
|
|
5166
|
+
// Taken outside the updater so both branches use the same one.
|
|
4744
5167
|
const id = nextId();
|
|
4745
5168
|
setMessages((prev) => {
|
|
4746
5169
|
const live = prev.filter((m) => !m.pending).slice(settledRef.current);
|
|
4747
5170
|
const collapsible = live.filter((m) => willCollapse(m.detail));
|
|
4748
5171
|
if (collapsible.length === 0) {
|
|
5172
|
+
// Nothing redrawable is collapsed, so this reprints the most recent
|
|
5173
|
+
// collapsed body as a new row — the rows above have been printed
|
|
5174
|
+
// to the terminal's own scrollback, which cannot be rewritten.
|
|
5175
|
+
const blocks = prev.filter((m) => m.detailRef !== undefined);
|
|
5176
|
+
const block = blocks[blocks.length - 1];
|
|
5177
|
+
if (!block) {
|
|
5178
|
+
return [
|
|
5179
|
+
...prev,
|
|
5180
|
+
{
|
|
5181
|
+
id,
|
|
5182
|
+
role: 'system',
|
|
5183
|
+
content: 'Nothing to expand yet. Tool output longer than six lines collapses with a "… +N lines · ctrl+o" hint; Ctrl+O opens it in place while it is still on screen, and reprints the most recent one in full once it has scrolled away.',
|
|
5184
|
+
},
|
|
5185
|
+
];
|
|
5186
|
+
}
|
|
4749
5187
|
return [
|
|
4750
5188
|
...prev,
|
|
4751
5189
|
{
|
|
4752
5190
|
id,
|
|
4753
5191
|
role: 'system',
|
|
4754
|
-
content:
|
|
5192
|
+
content: `${block.content} — in full (${block.detail?.length ?? 0} lines)`,
|
|
5193
|
+
glyph: '⤢',
|
|
5194
|
+
detail: block.detail,
|
|
5195
|
+
detailExpanded: true,
|
|
4755
5196
|
},
|
|
4756
5197
|
];
|
|
4757
5198
|
}
|
|
@@ -4803,7 +5244,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4803
5244
|
? representedSubagentTools.has(JSON.stringify([tool.runId, tool.id]))
|
|
4804
5245
|
: representedUnscopedSubagentToolUseIds.has(tool.id)));
|
|
4805
5246
|
const selectedSubagent = agentSurface
|
|
4806
|
-
?
|
|
5247
|
+
? subagents.find((agent) => agent.viewId === agentSurface.selectedId)
|
|
4807
5248
|
: undefined;
|
|
4808
5249
|
const lifecycleOwnsViewport = phase === 'trust' ||
|
|
4809
5250
|
phase === 'resume' ||
|
|
@@ -4840,21 +5281,30 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4840
5281
|
: agentSurface?.kind === 'transcript'
|
|
4841
5282
|
? 'observing agent — esc agents · q parent'
|
|
4842
5283
|
: textPrompt
|
|
4843
|
-
? '
|
|
5284
|
+
? 'editor open · enter apply · esc cancel'
|
|
4844
5285
|
: choicePicker
|
|
4845
|
-
?
|
|
5286
|
+
? choicePickerSearchable(choicePicker)
|
|
5287
|
+
? 'type to filter · ↑↓ select · enter apply · esc back'
|
|
5288
|
+
: '↑↓ / 1–9 select · enter apply · esc back'
|
|
4846
5289
|
: copyPicker
|
|
4847
5290
|
? 'copy target open — ↑↓ / 1–9 select · esc cancel'
|
|
4848
5291
|
: goalStatus && phase === 'ready' && state === 'idle'
|
|
4849
5292
|
? undefined
|
|
4850
5293
|
: hintForPhase(phase, state, session?.hasProvider === true, composerHasDraft);
|
|
4851
|
-
|
|
4852
|
-
|
|
5294
|
+
const displayedPermissionMode = effectivePermissionMode(permissionMode, session?.approvalLatched() ?? false);
|
|
5295
|
+
return (_jsx(Box, { flexDirection: "column", display: externalEditorRequest ? 'none' : 'flex', children: _jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(TranscriptFrame, { children: _jsx(Transcript, { messages: finalized, pending: messages.find((m) => m.pending) ?? null, state: state, settled: transcriptOwned ? renderedSettled : 0, resetKey: resetKey, raw: rawOutput, hyperlinks: hyperlinks, showLive: !lifecycleOwnsViewport, header: transcriptOwned ? (_jsx(BrandHeader, { version: ctx.version, provider: session?.providerSummary, model: session?.modelSummary, permissionMode: displayedPermissionMode, cwd: ctx.cwd })) : undefined }) }), phase === 'trust' ? (_jsx(TrustPrompt, { cwd: ctx.cwd })) : phase === 'unhealthy' ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: theme.status.error, bold: true, children: "Startup stopped" }), _jsx(Text, { color: theme.text.secondary, children: "Resolve the error above, then restart Namzu." })] })) : phase === 'resume' ? (_jsx(ResumePicker, { conversations: resumeList, selected: selectedResume })) : phase === 'edit' ? (_jsx(EditPromptPicker, { prompts: editList, selected: selectedEdit })) : phase === 'picker' ? (_jsx(Picker, { detected: pickerDetected ?? detected, currentProvider: currentProvider, selectionKind: pickerSelectionKind, currentModel: session?.modelSummary ?? null, initialView: pickerInitialView, onSubmit: handlePickerSubmit, onCancel: handlePickerCancel, onCredential: handleTypedCredential, onLogin: startLoginFromPicker, onLoginComplete: finishLoginFromPicker, keyEntryFor: keyEntryFor, notice: pickerNotice })) : (_jsxs(_Fragment, { children: [agentSurface === null ? (_jsx(LiveActivity, { compact: compactWork, activeTools: visibleActiveTools, working: state === 'thinking' || state === 'tool', interruptible: abortRef.current !== null, animate: stdout.isTTY === true, thinking: thinking })) : null, agentSurface === null && permission === null ? (_jsx(TaskList, { tasks: tasks, compact: compactWork })) : null, permission ? (_jsx(PermissionOverlay, { toolCalls: permission.toolCalls, review: permission.review, summary: permission.summary, detailsOpen: permissionDetailsOpen, reviewOffset: permissionReviewOffset, choice: permissionChoice, columns: terminal.columns, rows: terminal.rows })) : null, textPrompt ? (_jsx(TextPrompt, { title: textPrompt.title, placeholder: textPrompt.placeholder, initialValue: textPrompt.initialValue, emptyNotice: textPrompt.emptyNotice, hidden: permission !== null || agentSurface !== null, onSubmit: submitTextPrompt, onCancel: cancelTextPrompt }, textPrompt.token)) : permission === null && agentSurface === null && choicePicker ? (_jsx(ChoicePicker, { columns: Math.max(1, (terminal.columns ?? 80) - 2), title: choicePicker.title, notice: choicePicker.notice, options: filterChoiceOptions(choicePicker.options, choiceQuery), query: choicePickerSearchable(choicePicker) ? choiceQuery : undefined, selected: selectedChoice, windowSize: choicePicker.kind === 'command' ? choicePicker.windowSize : undefined })) : permission === null && agentSurface === null && copyPicker ? (_jsx(CopyPicker, { targets: copyPicker.targets, selected: selectedCopy })) : null, _jsxs(ComposerFrame, { working: state === 'thinking' || state === 'tool' || visibleActiveTools.length > 0, focus: phase === 'ready' &&
|
|
5296
|
+
state !== 'awaiting-permission' &&
|
|
5297
|
+
!compacting &&
|
|
5298
|
+
externalEditorRequest === null &&
|
|
4853
5299
|
conversationMutation === null &&
|
|
4854
5300
|
textPrompt === null &&
|
|
4855
5301
|
choicePicker === null &&
|
|
4856
5302
|
copyPicker === null &&
|
|
4857
|
-
agentSurface === null, hidden: permission !== null ||
|
|
5303
|
+
agentSurface === null, hidden: permission !== null ||
|
|
5304
|
+
textPrompt !== null ||
|
|
5305
|
+
choicePicker !== null ||
|
|
5306
|
+
copyPicker !== null ||
|
|
5307
|
+
agentSurface?.kind === 'transcript', children: [pendingSteers.length > 0 &&
|
|
4858
5308
|
permission === null &&
|
|
4859
5309
|
textPrompt === null &&
|
|
4860
5310
|
choicePicker === null &&
|
|
@@ -4868,7 +5318,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4868
5318
|
? queuePause.outcome === 'paused'
|
|
4869
5319
|
? 'held after a resumable run paused; wait for recovery, change model, or send a message to release it'
|
|
4870
5320
|
: `paused after a ${queuePause.outcome} turn; send a message or change model to continue`
|
|
4871
|
-
: 'sending when ready'] }) })) : null, _jsx(Composer, { permissionMode:
|
|
5321
|
+
: 'sending when ready'] }) })) : null, _jsx(Composer, { permissionMode: displayedPermissionMode, onCycleMode: cyclePermissionMode, disabled: phase !== 'ready' ||
|
|
4872
5322
|
state === 'awaiting-permission' ||
|
|
4873
5323
|
compacting ||
|
|
4874
5324
|
conversationMutation !== null ||
|
|
@@ -4876,29 +5326,26 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4876
5326
|
agentSurface !== null, hidden: permission !== null ||
|
|
4877
5327
|
textPrompt !== null ||
|
|
4878
5328
|
choicePicker !== null ||
|
|
4879
|
-
copyPicker !== null
|
|
5329
|
+
copyPicker !== null ||
|
|
5330
|
+
agentSurface?.kind === 'transcript',
|
|
4880
5331
|
// A turn is running, so Esc is the interrupt and not
|
|
4881
5332
|
// the composer's clear.
|
|
4882
|
-
escapeInterrupts: !compacting && (state === 'thinking' || state === 'tool'), onSubmit: handleSubmit, onNotice: (text) => pushMessage('system', text), onStepReasoningEffort: stepReasoningEffort, onExternalEdit: requestExternalEditor, onEditPrevious: openPromptEditor, draftToRestore: composerDraft, onDraftRestored: (token) => setComposerDraft((draft) => (draft?.token === token ? null : draft)), onDraftPresenceChange: setComposerHasDraft, onOpenAgentPanel: openAgentCockpit, userCommands: userCommands,
|
|
5333
|
+
escapeInterrupts: !compacting && (state === 'thinking' || state === 'tool'), onSubmit: handleSubmit, onNotice: (text) => pushMessage('system', text), onStepReasoningEffort: stepReasoningEffort, onExternalEdit: requestExternalEditor, onEditPrevious: openPromptEditor, draftToRestore: composerDraft, onDraftRestored: (token) => setComposerDraft((draft) => (draft?.token === token ? null : draft)), onDraftPresenceChange: setComposerHasDraft, onOpenAgentPanel: openAgentCockpit, userCommands: userCommands, builtins: hostCommands.map((command) => ({
|
|
5334
|
+
...command,
|
|
5335
|
+
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] })), _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 }) })] }) }));
|
|
4883
5337
|
}
|
|
4884
|
-
|
|
4885
|
-
|
|
4886
|
-
const
|
|
4887
|
-
|
|
4888
|
-
|
|
4889
|
-
|
|
4890
|
-
const prettyCwd = home && cwd.startsWith(home) ? `~${cwd.slice(home.length)}` : cwd;
|
|
4891
|
-
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, paddingTop: 1, paddingBottom: 1, children: [_jsxs(Box, { flexDirection: "row", children: [wide ? (_jsx(Box, { flexDirection: "column", marginRight: 2, children: NAMZU_WORDMARK.map((line, i) => (_jsx(Text, { color: NAMZU_WORDMARK_GRADIENT[i], bold: true, children: line }, `wm-${i}`))) })) : (_jsxs(Text, { color: NAMZU_MARK_COLOR, children: [NAMZU_MARK, " "] })), _jsxs(Box, { flexDirection: "column", marginTop: wide ? 1 : 0, children: [_jsxs(Text, { children: [_jsx(Text, { color: theme.text.secondary, children: "Cogitave" }), wide ? null : (_jsxs(Text, { color: NAMZU_MARK_COLOR, bold: true, children: [' ', "Namzu"] })), _jsxs(Text, { color: theme.text.muted, children: [" v", version] })] }), _jsx(Text, { color: theme.text.secondary, children: provider ? `${provider}${model ? ` · ${model}` : ''}` : 'the agent in your terminal' }), _jsx(Text, { color: theme.text.muted, children: prettyCwd })] })] }), permissionMode === 'auto' ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.status.error, bold: true, children: "\u26A0 launched in auto permission mode \u2014 undecided tools run without asking until /permissions changes it" }) })) : null] }));
|
|
5338
|
+
/** One line for a background job that ended, for the transcript and the next turn. */
|
|
5339
|
+
function describeJobExit(job) {
|
|
5340
|
+
const outcome = job.status === 'killed'
|
|
5341
|
+
? 'was stopped'
|
|
5342
|
+
: `exited ${job.exitCode ?? '?'}${job.signal ? ` (${job.signal})` : ''}`;
|
|
5343
|
+
return `background job ${job.jobId} (${job.command}) ${outcome}`;
|
|
4892
5344
|
}
|
|
4893
5345
|
function TranscriptFrame({ children }) {
|
|
4894
5346
|
// Borderless, edge-to-edge — the message glyph gutter provides structure.
|
|
4895
5347
|
return _jsx(Box, { flexDirection: "column", children: children });
|
|
4896
5348
|
}
|
|
4897
|
-
function ComposerFrame({ focus, hidden = false, children, }) {
|
|
4898
|
-
// Input-field look: a rounded rule above and below the composer, no side
|
|
4899
|
-
// borders, so the input reads as a field rather than a heavy box.
|
|
4900
|
-
return (_jsx(Box, { flexDirection: "column", ...(hidden ? {} : { borderStyle: 'round' }), borderTop: true, borderBottom: true, borderLeft: false, borderRight: false, borderColor: focus ? theme.border.focus : theme.border.default, marginTop: hidden ? 0 : 1, children: children }));
|
|
4901
|
-
}
|
|
4902
5349
|
// Tool call label: the tool name title-cased, then its most identifying
|
|
4903
5350
|
// argument — `Bash(ls -la)`, `Read(file.ts)`. A bare tool name in a transcript
|
|
4904
5351
|
// of forty calls says nothing about which one this was.
|
|
@@ -4921,7 +5368,7 @@ hasDraft = false) {
|
|
|
4921
5368
|
if (phase === 'resume')
|
|
4922
5369
|
return '↑↓ navigate · enter resume · esc cancel';
|
|
4923
5370
|
if (phase === 'edit')
|
|
4924
|
-
return '
|
|
5371
|
+
return '← older · → newer · enter fork and edit · esc cancel';
|
|
4925
5372
|
if (phase === 'probing')
|
|
4926
5373
|
return 'discovering providers…';
|
|
4927
5374
|
// Esc does two different things here depending on how the picker was reached,
|
|
@@ -4933,11 +5380,11 @@ hasDraft = false) {
|
|
|
4933
5380
|
: '↑↓ navigate · enter accept · esc or Ctrl+C exit';
|
|
4934
5381
|
}
|
|
4935
5382
|
if (phase === 'unhealthy')
|
|
4936
|
-
return 'Ctrl+C
|
|
5383
|
+
return 'esc or Ctrl+C exit';
|
|
4937
5384
|
// Enter is absent because Enter decides nothing here, deliberately — see the
|
|
4938
5385
|
// permission gate in the key handler above.
|
|
4939
5386
|
if (state === 'awaiting-permission')
|
|
4940
|
-
return '
|
|
5387
|
+
return '↑↓ select · enter confirm · esc decline';
|
|
4941
5388
|
if (state !== 'idle')
|
|
4942
5389
|
return hasDraft ? 'enter steer · tab queue · esc interrupt' : '';
|
|
4943
5390
|
// The composer already points to /help. Keeping the complete key legend in
|
|
@@ -4963,29 +5410,38 @@ function goalStatusLabel(goal) {
|
|
|
4963
5410
|
function labelOfOption(question, optionId) {
|
|
4964
5411
|
return question.options.find((option) => option.id === optionId)?.label ?? optionId;
|
|
4965
5412
|
}
|
|
4966
|
-
function
|
|
4967
|
-
|
|
4968
|
-
case 'prompt':
|
|
4969
|
-
return 'Ask before an undecided tool call runs';
|
|
4970
|
-
case 'accept-edits':
|
|
4971
|
-
return 'Approve file edits and writes without asking; shell and everything else still ask';
|
|
4972
|
-
case 'plan':
|
|
4973
|
-
return 'Read-only: the agent explores and presents a plan; leave plan mode to carry it out';
|
|
4974
|
-
case 'auto':
|
|
4975
|
-
return 'Approve undecided calls unless a rule or safety gate refuses';
|
|
4976
|
-
case 'strict':
|
|
4977
|
-
return 'Refuse undecided calls; explicit allow rules still work';
|
|
4978
|
-
}
|
|
5413
|
+
function choicePickerSearchable(picker) {
|
|
5414
|
+
return ['command', 'skill', 'review-branch', 'review-commit'].includes(picker.kind);
|
|
4979
5415
|
}
|
|
4980
|
-
function
|
|
4981
|
-
const
|
|
4982
|
-
|
|
4983
|
-
|
|
4984
|
-
|
|
4985
|
-
|
|
4986
|
-
|
|
4987
|
-
|
|
4988
|
-
|
|
5416
|
+
function permissionPicker(mode, approvedAll, more = false) {
|
|
5417
|
+
const current = effectivePermissionMode(mode, approvedAll);
|
|
5418
|
+
const values = more
|
|
5419
|
+
? ['auto', 'strict', 'rules']
|
|
5420
|
+
: ['prompt', 'accept-edits', 'plan', 'more'];
|
|
5421
|
+
return {
|
|
5422
|
+
kind: 'permission-mode',
|
|
5423
|
+
title: more ? 'More permission options' : 'Permissions',
|
|
5424
|
+
notice: `Current: ${permissionModeLabel(current)} · this session`,
|
|
5425
|
+
values,
|
|
5426
|
+
options: values.map((value) => {
|
|
5427
|
+
if (value === 'more')
|
|
5428
|
+
return {
|
|
5429
|
+
label: 'More options',
|
|
5430
|
+
description: 'Other approval modes and permission rules',
|
|
5431
|
+
current: current === 'auto' || current === 'strict',
|
|
5432
|
+
};
|
|
5433
|
+
if (value === 'rules')
|
|
5434
|
+
return {
|
|
5435
|
+
label: 'View rules',
|
|
5436
|
+
description: 'Show the permission rules in use',
|
|
5437
|
+
};
|
|
5438
|
+
return {
|
|
5439
|
+
label: permissionModeLabel(value),
|
|
5440
|
+
description: permissionModeDescription(value),
|
|
5441
|
+
current: value === current,
|
|
5442
|
+
};
|
|
5443
|
+
}),
|
|
5444
|
+
};
|
|
4989
5445
|
}
|
|
4990
5446
|
function reasoningEffortDescription(effort) {
|
|
4991
5447
|
switch (effort) {
|