@namzu/cli 18.1.0 → 19.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 +5898 -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 +24 -13
- package/dist/integrations/subagents/runtime.d.ts.map +1 -1
- package/dist/integrations/subagents/runtime.js +282 -122
- package/dist/integrations/subagents/runtime.js.map +1 -1
- 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 +12 -45
- package/dist/permissions/mode.d.ts.map +1 -1
- package/dist/permissions/mode.js +18 -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.map +1 -1
- package/dist/tui/AgentExplorer.js +14 -18
- package/dist/tui/AgentExplorer.js.map +1 -1
- package/dist/tui/App.d.ts.map +1 -1
- package/dist/tui/App.js +606 -207
- 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 +17 -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 +4 -1
- package/dist/tui/Composer.d.ts.map +1 -1
- package/dist/tui/Composer.js +4 -4
- 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 +108 -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 +79 -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 +3 -3
- package/dist/tui/Transcript.d.ts.map +1 -1
- package/dist/tui/Transcript.js +16 -34
- package/dist/tui/Transcript.js.map +1 -1
- package/dist/tui/agent.d.ts +113 -50
- package/dist/tui/agent.d.ts.map +1 -1
- package/dist/tui/agent.js +512 -293
- 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 +77 -31
- package/dist/tui/slashCommands.d.ts.map +1 -1
- package/dist/tui/slashCommands.js +343 -303
- 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 +6 -1
- package/dist/tui/types.d.ts.map +1 -1
- package/package.json +6 -4
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';
|
|
@@ -26,23 +26,27 @@ import { conversationMarkdown, writeConversationExport, } from '../integrations/
|
|
|
26
26
|
import { isTrusted, trustDir } from '../integrations/trust/store.js';
|
|
27
27
|
import { checkUpdates } from '../integrations/updates.js';
|
|
28
28
|
import { appendMemory, composeMemoryPrompt, readMemory } from '../memory/store.js';
|
|
29
|
+
import { permissionModeDescription } from '../permissions/mode.js';
|
|
29
30
|
import { composeSkillsPrompt, discoverSkills, loadSkillBody } from '../skills/store.js';
|
|
30
31
|
import { discoverUserCommands } from '../user-commands/store.js';
|
|
31
32
|
import { AgentCockpit, AgentTaskPanel, AgentTranscript, activeSubagentCohorts, agentCockpitIsWide, agentPhasePageSize, agentPhases, agentPickerPageSize, agentTranscriptPageSize, maxAgentTranscriptTailOffset, } from './AgentExplorer.js';
|
|
33
|
+
import { BrandHeader } from './BrandHeader.js';
|
|
32
34
|
import { ChoicePicker } from './ChoicePicker.js';
|
|
33
35
|
import { Composer, suggestionWindowSize, } from './Composer.js';
|
|
36
|
+
import { ComposerFrame } from './ComposerFrame.js';
|
|
34
37
|
import { CopyPicker } from './CopyPicker.js';
|
|
35
38
|
import { EditPromptPicker } from './EditPromptPicker.js';
|
|
36
39
|
import { LiveActivity, formatElapsed } from './LiveActivity.js';
|
|
37
|
-
import { TaskList } from './TaskList.js';
|
|
38
40
|
import { PermissionOverlay } from './PermissionOverlay.js';
|
|
39
41
|
import { Picker } from './Picker.js';
|
|
40
42
|
import { ResumePicker } from './ResumePicker.js';
|
|
41
43
|
import { StatusBar } from './StatusBar.js';
|
|
44
|
+
import { TaskList } from './TaskList.js';
|
|
42
45
|
import { TextPrompt } from './TextPrompt.js';
|
|
43
46
|
import { Transcript, willCollapse } from './Transcript.js';
|
|
44
47
|
import { TrustPrompt } from './TrustPrompt.js';
|
|
45
48
|
import { createAgentSession, probeAgentSession, } from './agent.js';
|
|
49
|
+
import { choicePickerWindowSize, filterChoiceOptions, moveChoiceSelection, } from './choice-selection.js';
|
|
46
50
|
import { keepRecentRows } from './compact-transcript.js';
|
|
47
51
|
import { approvalIsDeliberate } from './consent-timing.js';
|
|
48
52
|
import { planTurnPublication } from './conversation-history.js';
|
|
@@ -51,12 +55,15 @@ import { editablePrompts } from './edit-prompts.js';
|
|
|
51
55
|
import { editDraftInExternalEditor } from './external-editor.js';
|
|
52
56
|
import { liveWindow } from './live-window.js';
|
|
53
57
|
import { describeCodexDeviceLoginStart, describeLoginOutcome, describeLoginStart, describeLogout, describeProviderLogout, } from './login-prompt.js';
|
|
54
|
-
import {
|
|
58
|
+
import { selectPrimaryProvider } from './provider-selection.js';
|
|
59
|
+
import { eraseLastChoiceGrapheme } from './terminal-choice-text.js';
|
|
60
|
+
import { renderCheckpoints, renderRestore } from '../checkpoints/store.js';
|
|
55
61
|
import { expandFileMentions, listMentionableFiles } from './mentions.js';
|
|
56
62
|
import { openInBrowser } from './open-browser.js';
|
|
57
|
-
import {
|
|
63
|
+
import { buildPermissionReview, buildPermissionSummary, permissionReviewPageRows, permissionReviewRefusal, permissionReviewRows, } from './permission-review.js';
|
|
58
64
|
import { describeRunInterruption } from './run-interruption.js';
|
|
59
65
|
import { moveSelection } from './selection-window.js';
|
|
66
|
+
import { describeShellEscape, describeShellEscapeForModel, runShellEscape, shellEscapeCommand, } from './shell-escape.js';
|
|
60
67
|
import { baseBranchReviewPrompt, commitReviewPrompt, hostCommandNames, kernelCommandDescriptors, mergeHostCommands, renderOutcome, reviewPrompt, runSlash, } from './slashCommands.js';
|
|
61
68
|
import { splitCompleteBlocks, splitSafeCut } from './stream-blocks.js';
|
|
62
69
|
import { terminalSupportsHyperlinks } from './terminal-hyperlinks.js';
|
|
@@ -64,6 +71,8 @@ import { theme } from './theme.js';
|
|
|
64
71
|
import { useSelectionIndex } from './use-selection-index.js';
|
|
65
72
|
import { renderWorkspaceDiff, workspaceDiff } from './workspace-diff.js';
|
|
66
73
|
import { listReviewBranches, listReviewCommits, reviewMergeBase, } from './workspace-review.js';
|
|
74
|
+
/** How long `/exit` waits for the session to close before leaving anyway. */
|
|
75
|
+
const SESSION_CLOSE_ON_EXIT_MS = 5_000;
|
|
67
76
|
/** The value a question row carries: an option id, or the free-text escape. */
|
|
68
77
|
const FREE_TEXT_ANSWER = '__free_text__';
|
|
69
78
|
/**
|
|
@@ -305,7 +314,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
305
314
|
const [pickerDetected, setPickerDetected] = useState(null);
|
|
306
315
|
/** Whether Enter accepts the provider directly or continues to model choice. */
|
|
307
316
|
const [pickerSelectionKind, setPickerSelectionKind] = useState('provider-and-model');
|
|
308
|
-
/**
|
|
317
|
+
/** Active preferences, or the saved chain awaiting its missing credential. */
|
|
309
318
|
const savedPrefsRef = useRef(null);
|
|
310
319
|
/**
|
|
311
320
|
* Why the picker is open, drawn ON the picker.
|
|
@@ -345,6 +354,15 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
345
354
|
permissionReviewOffsetRef.current = next;
|
|
346
355
|
setPermissionReviewOffsetState(next);
|
|
347
356
|
}, []);
|
|
357
|
+
// Which of the three answers the cursor is on. Opens on "Yes", as the
|
|
358
|
+
// prompt an operator knows from elsewhere does; Enter confirms it only
|
|
359
|
+
// after the same settle window `y` and `a` wait out.
|
|
360
|
+
const [permissionChoice, setPermissionChoiceState] = useState(0);
|
|
361
|
+
const permissionChoiceRef = useRef(0);
|
|
362
|
+
const setPermissionChoice = useCallback((next) => {
|
|
363
|
+
permissionChoiceRef.current = next;
|
|
364
|
+
setPermissionChoiceState(next);
|
|
365
|
+
}, []);
|
|
348
366
|
// The launch bypass is an INITIAL selection, not permanent authority. A
|
|
349
367
|
// typed /permissions command can narrow it back to prompt/strict without
|
|
350
368
|
// rebuilding the provider, tools, plugins, or sandbox.
|
|
@@ -366,7 +384,62 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
366
384
|
/** Git-admitted file paths cached once per successful session hydration. */
|
|
367
385
|
const [mentionCandidates, setMentionCandidates] = useState([]);
|
|
368
386
|
const mentionLoadOwnerRef = useRef(null);
|
|
387
|
+
// What compaction has done this session, summed over passes; `/context`
|
|
388
|
+
// prints it. Reset with the session, never by a pass that declined.
|
|
389
|
+
const [compactionTally, setCompactionTally] = useState({
|
|
390
|
+
passes: 0,
|
|
391
|
+
clearedResults: 0,
|
|
392
|
+
stubbedNarrations: 0,
|
|
393
|
+
summaries: 0,
|
|
394
|
+
reclaimedTokens: 0,
|
|
395
|
+
});
|
|
396
|
+
// Jobs that ended while no turn was running. The kernel's notice needs a
|
|
397
|
+
// tool result to ride on, so between turns the exit is held here and
|
|
398
|
+
// handed to the next send as part of the system text; the transcript
|
|
399
|
+
// row is written at once.
|
|
400
|
+
const idleJobNoticesRef = useRef([]);
|
|
401
|
+
const drainIdleJobNotices = () => {
|
|
402
|
+
if (idleJobNoticesRef.current.length === 0)
|
|
403
|
+
return undefined;
|
|
404
|
+
const lines = idleJobNoticesRef.current;
|
|
405
|
+
idleJobNoticesRef.current = [];
|
|
406
|
+
return `Background jobs that ended since your last turn:\n${lines.map((l) => `- ${l}`).join('\n')}`;
|
|
407
|
+
};
|
|
408
|
+
// Commands the operator ran with `!` since the last turn. The model did
|
|
409
|
+
// not see them happen; it reads them here on its next turn.
|
|
410
|
+
const operatorShellRef = useRef([]);
|
|
411
|
+
const drainOperatorShell = () => {
|
|
412
|
+
if (operatorShellRef.current.length === 0)
|
|
413
|
+
return undefined;
|
|
414
|
+
const blocks = operatorShellRef.current;
|
|
415
|
+
operatorShellRef.current = [];
|
|
416
|
+
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')}`;
|
|
417
|
+
};
|
|
418
|
+
useEffect(() => {
|
|
419
|
+
if (!session?.onJobExit)
|
|
420
|
+
return;
|
|
421
|
+
return session.onJobExit((job) => {
|
|
422
|
+
if (abortRef.current !== null)
|
|
423
|
+
return;
|
|
424
|
+
const text = describeJobExit({
|
|
425
|
+
jobId: job.id,
|
|
426
|
+
command: job.command,
|
|
427
|
+
status: job.status === 'killed' ? 'killed' : 'exited',
|
|
428
|
+
...(job.exitCode !== undefined ? { exitCode: job.exitCode } : {}),
|
|
429
|
+
...(job.signal ? { signal: job.signal } : {}),
|
|
430
|
+
});
|
|
431
|
+
idleJobNoticesRef.current.push(text);
|
|
432
|
+
pushMessage('system', text, false, '⚙');
|
|
433
|
+
});
|
|
434
|
+
}, [session]);
|
|
369
435
|
const [usage, setUsage] = useState(null);
|
|
436
|
+
/**
|
|
437
|
+
* The model's current reasoning, last line only, for the live region.
|
|
438
|
+
* Never appended to a message: the kernel keeps reasoning out of its
|
|
439
|
+
* own transcript, and this exists so a long think reads as work rather
|
|
440
|
+
* than as silence. Cleared by the first text or tool call that follows.
|
|
441
|
+
*/
|
|
442
|
+
const [thinking, setThinking] = useState(null);
|
|
370
443
|
// Tools currently executing — rendered live (spinner + elapsed) below the
|
|
371
444
|
// transcript, then committed as static lines on completion.
|
|
372
445
|
const [activeTools, setActiveTools] = useState([]);
|
|
@@ -459,6 +532,8 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
459
532
|
/** Finite slash-command choice owned synchronously until apply or cancel. */
|
|
460
533
|
const [choicePicker, setChoicePickerState] = useState(null);
|
|
461
534
|
const choicePickerRef = useRef(null);
|
|
535
|
+
const [choiceQuery, setChoiceQuery] = useState('');
|
|
536
|
+
const choiceQueryRef = useRef('');
|
|
462
537
|
/**
|
|
463
538
|
* Exact chooser React has committed. The synchronous owner ref is needed to
|
|
464
539
|
* block queue/goal work immediately, but it must not accept the Return key
|
|
@@ -469,6 +544,8 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
469
544
|
const { selection: selectedChoice, selectionRef: selectedChoiceRef, setSelection: setSelectedChoice, } = useSelectionIndex(0);
|
|
470
545
|
const setChoicePicker = useCallback((next) => {
|
|
471
546
|
choicePickerRef.current = next;
|
|
547
|
+
choiceQueryRef.current = '';
|
|
548
|
+
setChoiceQuery('');
|
|
472
549
|
if (next === null)
|
|
473
550
|
choicePickerCommittedRef.current = null;
|
|
474
551
|
setChoicePickerState(next);
|
|
@@ -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);
|
|
@@ -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') {
|
|
@@ -1504,6 +1603,9 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1504
1603
|
else {
|
|
1505
1604
|
// Provider/tool startup needs stable project/tenant/topic ids, not a
|
|
1506
1605
|
// durable conversation. The cursor is replaced on first admitted use.
|
|
1606
|
+
// The conversation's id, chosen now; the conversation is written
|
|
1607
|
+
// under it at first durable use. Nothing that saw this id before
|
|
1608
|
+
// that moment — a hook, a log line, the screen — is later wrong.
|
|
1507
1609
|
sessionId = generateSessionId();
|
|
1508
1610
|
}
|
|
1509
1611
|
sessionsRef.current = sessions;
|
|
@@ -1531,11 +1633,15 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1531
1633
|
return undefined;
|
|
1532
1634
|
conversationMutationRef.current = 'materialize';
|
|
1533
1635
|
try {
|
|
1534
|
-
const
|
|
1636
|
+
const written = await startConversation(sessions, scope.sessionId);
|
|
1535
1637
|
if (scopeRef.current !== scope || conversationMaterializedRef.current) {
|
|
1536
1638
|
throw new Error('the active conversation changed while its first session was published');
|
|
1537
1639
|
}
|
|
1538
|
-
|
|
1640
|
+
// The store creates the conversation under the id chosen at open,
|
|
1641
|
+
// so this is the same id; a store that cannot honour a chosen id
|
|
1642
|
+
// is followed rather than contradicted.
|
|
1643
|
+
if (written !== scope.sessionId)
|
|
1644
|
+
scope.sessionId = written;
|
|
1539
1645
|
conversationMaterializedRef.current = true;
|
|
1540
1646
|
return scope;
|
|
1541
1647
|
}
|
|
@@ -1554,7 +1660,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1554
1660
|
materializationRef.current = null;
|
|
1555
1661
|
}
|
|
1556
1662
|
}, [ensureSessions]);
|
|
1557
|
-
const hydrateSession = useCallback(async (prefs, detectedNow, signal) => {
|
|
1663
|
+
const hydrateSession = useCallback(async (prefs, detectedNow, signal, persistSelection = false) => {
|
|
1558
1664
|
if (signal?.aborted)
|
|
1559
1665
|
return;
|
|
1560
1666
|
const scope = await ensureSessions();
|
|
@@ -1564,15 +1670,18 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1564
1670
|
const s = await createAgentSession(prefs, detectedNow, {
|
|
1565
1671
|
scope,
|
|
1566
1672
|
cwd: activeCtx.cwd,
|
|
1567
|
-
...(
|
|
1568
|
-
? {
|
|
1673
|
+
...(activeCtx.additionalDirectories
|
|
1674
|
+
? { additionalDirectories: activeCtx.additionalDirectories }
|
|
1569
1675
|
: {}),
|
|
1676
|
+
...(sessionsRef.current ? { stateRoot: sessionsRef.current.root } : {}),
|
|
1570
1677
|
enableComputerUse: true,
|
|
1571
1678
|
rules: activeCtx.rules,
|
|
1572
1679
|
...(sessionsRef.current ? { sessionGoals: sessionsRef.current.goals } : {}),
|
|
1573
1680
|
...(activeCtx.mcpServers ? { mcpServers: activeCtx.mcpServers } : {}),
|
|
1574
1681
|
...(activeCtx.plugins ? { plugins: activeCtx.plugins } : {}),
|
|
1575
1682
|
...(activeCtx.web ? { web: activeCtx.web } : {}),
|
|
1683
|
+
...(activeCtx.hooks ? { hooks: activeCtx.hooks } : {}),
|
|
1684
|
+
...(activeCtx.compaction ? { compaction: activeCtx.compaction } : {}),
|
|
1576
1685
|
...(activeCtx.sandbox ? { sandbox: activeCtx.sandbox } : {}),
|
|
1577
1686
|
// Somebody is at this terminal, so the model may ask them one
|
|
1578
1687
|
// question when a decision is genuinely theirs.
|
|
@@ -1593,6 +1702,24 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1593
1702
|
}
|
|
1594
1703
|
throw new Error(reason);
|
|
1595
1704
|
}
|
|
1705
|
+
let commands;
|
|
1706
|
+
try {
|
|
1707
|
+
commands = discoverUserCommands({ cwd: activeCtx.cwd, reserved: hostCommandNames() });
|
|
1708
|
+
// Construction and validation precede the saved change. There is no
|
|
1709
|
+
// await between this atomic write and publishing the usable session.
|
|
1710
|
+
if (persistSelection)
|
|
1711
|
+
writePreferences(prefs);
|
|
1712
|
+
}
|
|
1713
|
+
catch (error) {
|
|
1714
|
+
try {
|
|
1715
|
+
await s.close();
|
|
1716
|
+
}
|
|
1717
|
+
catch {
|
|
1718
|
+
// Preserve the admission/save error and the still-active old session.
|
|
1719
|
+
}
|
|
1720
|
+
throw error;
|
|
1721
|
+
}
|
|
1722
|
+
savedPrefsRef.current = prefs;
|
|
1596
1723
|
// A picker-owned provider/model change is one state transition. Clear the
|
|
1597
1724
|
// old model's effort selection before publishing the replacement session
|
|
1598
1725
|
// or releasing any paused queue. Failed and superseded candidates returned
|
|
@@ -1605,13 +1732,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1605
1732
|
void previousSessionRef.current?.close();
|
|
1606
1733
|
previousSessionRef.current = s;
|
|
1607
1734
|
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
|
-
}));
|
|
1735
|
+
setUserCommands(commands);
|
|
1615
1736
|
const mentionLoadOwner = {};
|
|
1616
1737
|
mentionLoadOwnerRef.current = mentionLoadOwner;
|
|
1617
1738
|
void listMentionableFiles(activeCtx.cwd, appLifetime.signal).then((files) => {
|
|
@@ -1973,6 +2094,13 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1973
2094
|
}
|
|
1974
2095
|
}, [activateTrustedProject, pushMessage, runProbe]);
|
|
1975
2096
|
const finalized = messages.filter((m) => !m.pending);
|
|
2097
|
+
// Activity and the plan share the terminal with the draft. Collapse the two
|
|
2098
|
+
// lists together only when their full previews would crowd the input area.
|
|
2099
|
+
const fullTaskFurniture = tasks.length === 0 ? 0 : Math.min(tasks.length, 8) + 3;
|
|
2100
|
+
const fullToolFurniture = activeTools.length === 0 ? 0 : Math.min(activeTools.length, 3) * 2 + 2;
|
|
2101
|
+
const compactWork = LIVE_FURNITURE_ROWS + fullTaskFurniture + fullToolFurniture >= terminal.rows;
|
|
2102
|
+
const taskFurniture = compactWork && fullTaskFurniture > 0 ? 2 : fullTaskFurniture;
|
|
2103
|
+
const toolFurniture = compactWork && fullToolFurniture > 0 ? 2 : fullToolFurniture;
|
|
1976
2104
|
// How much of the transcript is still redrawable. The rest belongs to native
|
|
1977
2105
|
// terminal scrollback; the live tail stays deliberately small so an activity
|
|
1978
2106
|
// tick never repaints the whole conversation.
|
|
@@ -1984,7 +2112,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1984
2112
|
messages: finalized,
|
|
1985
2113
|
rows: terminal.rows,
|
|
1986
2114
|
columns: terminal.columns,
|
|
1987
|
-
furnitureRows: LIVE_FURNITURE_ROWS,
|
|
2115
|
+
furnitureRows: LIVE_FURNITURE_ROWS + taskFurniture + toolFurniture,
|
|
1988
2116
|
settled: settledRef.current,
|
|
1989
2117
|
raw: rawOutput,
|
|
1990
2118
|
});
|
|
@@ -2003,6 +2131,17 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2003
2131
|
const hostCommands = mergeHostCommands(kernelCommandDescriptors());
|
|
2004
2132
|
const slashCtx = {
|
|
2005
2133
|
cwd: ctx.cwd,
|
|
2134
|
+
jobs: () => session?.jobs?.() ?? [],
|
|
2135
|
+
hooks: () => session?.hooks,
|
|
2136
|
+
directories: () => session?.directories?.list() ?? [],
|
|
2137
|
+
compaction: session
|
|
2138
|
+
? {
|
|
2139
|
+
strategy: ctx.compaction?.strategy ?? 'salience',
|
|
2140
|
+
softTarget: 0.5,
|
|
2141
|
+
triggerThreshold: 0.7,
|
|
2142
|
+
...compactionTally,
|
|
2143
|
+
}
|
|
2144
|
+
: null,
|
|
2006
2145
|
builtins: hostCommands,
|
|
2007
2146
|
lastAssistantMessageId: () => lastAssistantMessage.current?.messageId ?? null,
|
|
2008
2147
|
// Called when `/tools` renders, not read here — the same shape, and the
|
|
@@ -2081,6 +2220,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2081
2220
|
permissionOpenedAtRef.current = null;
|
|
2082
2221
|
setPermissionReviewOffset(0);
|
|
2083
2222
|
setPermissionDetailsOpen(false);
|
|
2223
|
+
setPermissionChoice(0);
|
|
2084
2224
|
setPermission(null);
|
|
2085
2225
|
if (resolve) {
|
|
2086
2226
|
// The provider resumes asynchronously after the decision. Leaving the
|
|
@@ -2207,6 +2347,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2207
2347
|
wakeGoalDriver();
|
|
2208
2348
|
conversationGenRef.current += 1;
|
|
2209
2349
|
resetSubagentActivity();
|
|
2350
|
+
session?.resetTaskStore?.();
|
|
2210
2351
|
activeTurnTokenRef.current = null;
|
|
2211
2352
|
resetTranscript();
|
|
2212
2353
|
setMessages(restored);
|
|
@@ -2241,6 +2382,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2241
2382
|
nextId,
|
|
2242
2383
|
pushMessage,
|
|
2243
2384
|
resetSubagentActivity,
|
|
2385
|
+
session,
|
|
2244
2386
|
resetTranscript,
|
|
2245
2387
|
wakeGoalDriver,
|
|
2246
2388
|
]);
|
|
@@ -2281,6 +2423,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2281
2423
|
wakeGoalDriver();
|
|
2282
2424
|
conversationGenRef.current += 1;
|
|
2283
2425
|
resetSubagentActivity();
|
|
2426
|
+
session?.resetTaskStore?.();
|
|
2284
2427
|
activeTurnTokenRef.current = null;
|
|
2285
2428
|
modelHistoryRef.current = [];
|
|
2286
2429
|
lastAssistantMessage.current = null;
|
|
@@ -2320,6 +2463,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2320
2463
|
interruptTurn,
|
|
2321
2464
|
pushMessage,
|
|
2322
2465
|
resetSubagentActivity,
|
|
2466
|
+
session,
|
|
2323
2467
|
resetTranscript,
|
|
2324
2468
|
wakeGoalDriver,
|
|
2325
2469
|
]);
|
|
@@ -2382,9 +2526,12 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2382
2526
|
}
|
|
2383
2527
|
const scope = scopeRef.current;
|
|
2384
2528
|
if (!scope || scope.sessionId !== prompt.sessionId) {
|
|
2385
|
-
pushMessage('system',
|
|
2386
|
-
|
|
2387
|
-
|
|
2529
|
+
pushMessage('system', 'The conversation changed while this editor was open. Reopen the command in the current conversation.');
|
|
2530
|
+
return;
|
|
2531
|
+
}
|
|
2532
|
+
if (prompt.kind === 'goal-create' || prompt.kind === 'goal-edit') {
|
|
2533
|
+
if (value.trim())
|
|
2534
|
+
commandPickerSubmitRef.current(`/goal ${prompt.kind === 'goal-create' ? 'set' : 'edit'} ${value.trim()}`);
|
|
2388
2535
|
return;
|
|
2389
2536
|
}
|
|
2390
2537
|
if (prompt.kind === 'export-file') {
|
|
@@ -2461,6 +2608,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2461
2608
|
// is reloaded or reset. Only where the NEXT turn is written changes.
|
|
2462
2609
|
scope.sessionId = forked.id;
|
|
2463
2610
|
resetSubagentActivity();
|
|
2611
|
+
session?.resetTaskStore?.();
|
|
2464
2612
|
goalActivation.clear();
|
|
2465
2613
|
wakeGoalDriver();
|
|
2466
2614
|
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 +2626,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2478
2626
|
materializeConversation,
|
|
2479
2627
|
pushMessage,
|
|
2480
2628
|
resetSubagentActivity,
|
|
2629
|
+
session,
|
|
2481
2630
|
wakeGoalDriver,
|
|
2482
2631
|
]);
|
|
2483
2632
|
/** Open the durable prompt picker after the composer's second empty Esc. */
|
|
@@ -2502,6 +2651,33 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2502
2651
|
editCommittedRef.current = false;
|
|
2503
2652
|
setPhase('edit');
|
|
2504
2653
|
}, [hasUnsettledTurn, messages, pushMessage, state]);
|
|
2654
|
+
/** `/restore`: list the file checkpoints, or put the tree back to before a turn. */
|
|
2655
|
+
const doRestore = useCallback(async (turn) => {
|
|
2656
|
+
const store = session?.checkpoints;
|
|
2657
|
+
if (!store) {
|
|
2658
|
+
pushMessage('system', 'This session keeps no file checkpoints.');
|
|
2659
|
+
return;
|
|
2660
|
+
}
|
|
2661
|
+
if (turn === undefined) {
|
|
2662
|
+
pushMessage('system', renderCheckpoints(store.list(), ctxRef.current.cwd));
|
|
2663
|
+
return;
|
|
2664
|
+
}
|
|
2665
|
+
if (abortRef.current || state !== 'idle' || hasUnsettledTurn()) {
|
|
2666
|
+
pushMessage('system', 'A turn is still running. Wait for it, or interrupt it, before restoring files.');
|
|
2667
|
+
return;
|
|
2668
|
+
}
|
|
2669
|
+
try {
|
|
2670
|
+
const report = await store.restore(turn);
|
|
2671
|
+
const text = renderRestore(report, ctxRef.current.cwd);
|
|
2672
|
+
pushMessage('system', text);
|
|
2673
|
+
// The model's picture of those files is now wrong; it reads
|
|
2674
|
+
// this before its next turn.
|
|
2675
|
+
operatorShellRef.current.push(`/restore ${turn}\n${text}`);
|
|
2676
|
+
}
|
|
2677
|
+
catch (err) {
|
|
2678
|
+
pushMessage('system', err instanceof Error ? err.message : String(err));
|
|
2679
|
+
}
|
|
2680
|
+
}, [hasUnsettledTurn, pushMessage, session, state]);
|
|
2505
2681
|
/** Fork before the selected prompt, then reopen that prompt in the composer. */
|
|
2506
2682
|
const confirmPromptEdit = useCallback(async (target) => {
|
|
2507
2683
|
if (editCommittedRef.current || conversationMutationRef.current)
|
|
@@ -2536,6 +2712,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2536
2712
|
.map((prompt) => prompt.displayText);
|
|
2537
2713
|
conversationGenRef.current += 1;
|
|
2538
2714
|
resetSubagentActivity();
|
|
2715
|
+
session?.resetTaskStore?.();
|
|
2539
2716
|
activeTurnTokenRef.current = null;
|
|
2540
2717
|
goalActivation.clear();
|
|
2541
2718
|
wakeGoalDriver();
|
|
@@ -2577,6 +2754,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2577
2754
|
nextId,
|
|
2578
2755
|
pushMessage,
|
|
2579
2756
|
resetSubagentActivity,
|
|
2757
|
+
session,
|
|
2580
2758
|
resetTranscript,
|
|
2581
2759
|
wakeGoalDriver,
|
|
2582
2760
|
]);
|
|
@@ -2654,6 +2832,20 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2654
2832
|
st.assistantId = null;
|
|
2655
2833
|
}
|
|
2656
2834
|
};
|
|
2835
|
+
if (event.kind !== 'usage' && 'budget' in event && event.budget) {
|
|
2836
|
+
const budget = event.budget;
|
|
2837
|
+
const own = st.lastUsage;
|
|
2838
|
+
setUsage((previous) => ({
|
|
2839
|
+
totalTokens: budget.ownTokens,
|
|
2840
|
+
budget,
|
|
2841
|
+
cost: {
|
|
2842
|
+
...(own?.cost ?? { totalCost: 0, cacheDiscount: 0, unpricedTokens: 0 }),
|
|
2843
|
+
unpricedTokens: (own?.cost.unpricedTokens ?? 0) +
|
|
2844
|
+
Math.max(0, budget.ownTokens - (own?.totalTokens ?? 0)),
|
|
2845
|
+
},
|
|
2846
|
+
...(own && previous?.context ? { context: previous.context } : {}),
|
|
2847
|
+
}));
|
|
2848
|
+
}
|
|
2657
2849
|
switch (event.kind) {
|
|
2658
2850
|
case 'delta': {
|
|
2659
2851
|
setState('thinking');
|
|
@@ -2668,6 +2860,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2668
2860
|
// turn: the wait before the first token is the model's, and
|
|
2669
2861
|
// counting it would release the opening sentence on arrival.
|
|
2670
2862
|
st.releasedAt ??= Date.now();
|
|
2863
|
+
setThinking(null);
|
|
2671
2864
|
st.text += event.text;
|
|
2672
2865
|
// Held, not appended. Appending each delta is what produced text
|
|
2673
2866
|
// that types itself out — nothing animates it, but a few
|
|
@@ -2697,6 +2890,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2697
2890
|
}
|
|
2698
2891
|
case 'tool-start': {
|
|
2699
2892
|
closeAssistant();
|
|
2893
|
+
setThinking(null);
|
|
2700
2894
|
setState('tool');
|
|
2701
2895
|
const tool = {
|
|
2702
2896
|
id: event.toolUseId,
|
|
@@ -2771,8 +2965,36 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2771
2965
|
break;
|
|
2772
2966
|
}
|
|
2773
2967
|
case 'usage':
|
|
2774
|
-
|
|
2968
|
+
st.lastUsage = event;
|
|
2969
|
+
setUsage({
|
|
2970
|
+
totalTokens: event.totalTokens,
|
|
2971
|
+
...(event.budget ? { budget: event.budget } : {}),
|
|
2972
|
+
cost: event.cost,
|
|
2973
|
+
...(event.contextTokens !== undefined && event.contextWindowTokens !== undefined
|
|
2974
|
+
? {
|
|
2975
|
+
context: {
|
|
2976
|
+
tokens: event.contextTokens,
|
|
2977
|
+
windowTokens: event.contextWindowTokens,
|
|
2978
|
+
measured: event.contextMeasuredBy === 'provider',
|
|
2979
|
+
windowAssumed: event.windowSource !== 'provider' && event.windowSource !== 'config',
|
|
2980
|
+
},
|
|
2981
|
+
}
|
|
2982
|
+
: {}),
|
|
2983
|
+
});
|
|
2984
|
+
break;
|
|
2985
|
+
case 'reasoning': {
|
|
2986
|
+
if (event.done)
|
|
2987
|
+
break;
|
|
2988
|
+
setState('thinking');
|
|
2989
|
+
// The last line only, bounded: a screen-wide row that changes as
|
|
2990
|
+
// the model writes is a spinner with words, not a transcript.
|
|
2991
|
+
setThinking((prev) => {
|
|
2992
|
+
const joined = `${prev ?? ''}${event.text}`;
|
|
2993
|
+
const lastLine = joined.slice(joined.lastIndexOf('\n') + 1);
|
|
2994
|
+
return lastLine.length > 160 ? `…${lastLine.slice(-159)}` : lastLine;
|
|
2995
|
+
});
|
|
2775
2996
|
break;
|
|
2997
|
+
}
|
|
2776
2998
|
case 'task': {
|
|
2777
2999
|
// The live list gets every change; the transcript records the
|
|
2778
3000
|
// opening and the close, as it did before the list existed.
|
|
@@ -2801,6 +3023,11 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2801
3023
|
}
|
|
2802
3024
|
break;
|
|
2803
3025
|
}
|
|
3026
|
+
case 'job':
|
|
3027
|
+
// The kernel saw it end during a turn; the model reads the notice
|
|
3028
|
+
// on its next tool result, the operator reads this row.
|
|
3029
|
+
pushMessage('system', describeJobExit(event), false, '⚙');
|
|
3030
|
+
break;
|
|
2804
3031
|
case 'context':
|
|
2805
3032
|
// Into the TRANSCRIPT, not a status line. A status indicator is
|
|
2806
3033
|
// present while nothing is happening and gone afterwards, so
|
|
@@ -2808,6 +3035,15 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2808
3035
|
// looking at was compacted. The event is a fact about the
|
|
2809
3036
|
// conversation and belongs in its record.
|
|
2810
3037
|
pushMessage('system', event.text, false, event.shed ? '⌫' : '⌦');
|
|
3038
|
+
if (event.shed) {
|
|
3039
|
+
setCompactionTally((tally) => ({
|
|
3040
|
+
passes: tally.passes + 1,
|
|
3041
|
+
clearedResults: tally.clearedResults + (event.cleared ?? 0),
|
|
3042
|
+
stubbedNarrations: tally.stubbedNarrations + (event.stubbed ?? 0),
|
|
3043
|
+
summaries: tally.summaries + (event.summarised ? 1 : 0),
|
|
3044
|
+
reclaimedTokens: tally.reclaimedTokens + (event.reclaimedTokens ?? 0),
|
|
3045
|
+
}));
|
|
3046
|
+
}
|
|
2811
3047
|
break;
|
|
2812
3048
|
case 'provider-fallback':
|
|
2813
3049
|
// The transcript, for the reason compaction is in the transcript
|
|
@@ -3094,7 +3330,9 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3094
3330
|
onPermission: askPermission,
|
|
3095
3331
|
onQuestion: askQuestion,
|
|
3096
3332
|
inboundMessages: () => inbox.drain(),
|
|
3097
|
-
extraSystem: composeSkillsPrompt(activeSkills)
|
|
3333
|
+
extraSystem: [composeSkillsPrompt(activeSkills), drainIdleJobNotices(), drainOperatorShell()]
|
|
3334
|
+
.filter((part) => Boolean(part))
|
|
3335
|
+
.join('\n\n') || undefined,
|
|
3098
3336
|
onConversationMessages: (messages) => {
|
|
3099
3337
|
// State-only: opaque reasoning/signatures must reach the next
|
|
3100
3338
|
// provider and durable store without becoming transcript text.
|
|
@@ -3312,6 +3550,41 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3312
3550
|
return;
|
|
3313
3551
|
}
|
|
3314
3552
|
setHistory((prev) => [...prev, value]);
|
|
3553
|
+
// `#` remembers, `!` runs — neither is a prompt. Both are the
|
|
3554
|
+
// operator acting directly, the way other coding agents spell it,
|
|
3555
|
+
// and both leave a row the model reads on its next turn.
|
|
3556
|
+
if (value.startsWith('#') && value.slice(1).trim().length > 0) {
|
|
3557
|
+
const note = value.slice(1).trim();
|
|
3558
|
+
try {
|
|
3559
|
+
const path = appendMemory(note, { scope: 'project', cwd: ctx.cwd });
|
|
3560
|
+
pushMessage('system', `Remembered for this project (${path}): ${note}`);
|
|
3561
|
+
}
|
|
3562
|
+
catch (err) {
|
|
3563
|
+
pushMessage('system', `Could not save memory: ${err instanceof Error ? err.message : String(err)}`);
|
|
3564
|
+
}
|
|
3565
|
+
return;
|
|
3566
|
+
}
|
|
3567
|
+
const escaped = shellEscapeCommand(value);
|
|
3568
|
+
if (escaped !== null) {
|
|
3569
|
+
pushMessage('user', value);
|
|
3570
|
+
const rowId = pushMessage('tool', `! ${escaped}`, true, '…');
|
|
3571
|
+
void runShellEscape(escaped, { cwd: ctx.cwd }).then((result) => {
|
|
3572
|
+
operatorShellRef.current.push(describeShellEscapeForModel(escaped, result));
|
|
3573
|
+
const ok = result.exitCode === 0 && !result.timedOut;
|
|
3574
|
+
const detail = result.output.length > 0 ? result.output.replace(/\n$/u, '').split('\n') : undefined;
|
|
3575
|
+
setMessages((prev) => prev.map((m) => m.id === rowId
|
|
3576
|
+
? {
|
|
3577
|
+
...m,
|
|
3578
|
+
content: describeShellEscape(escaped, result),
|
|
3579
|
+
pending: false,
|
|
3580
|
+
glyph: ok ? '✓' : '✗',
|
|
3581
|
+
...(ok ? {} : { glyphColor: theme.status.error }),
|
|
3582
|
+
...(detail ? { detail } : {}),
|
|
3583
|
+
}
|
|
3584
|
+
: m));
|
|
3585
|
+
});
|
|
3586
|
+
return;
|
|
3587
|
+
}
|
|
3315
3588
|
// What actually gets sent. A `prompt` action replaces it with text the
|
|
3316
3589
|
// command composed, and then takes the ordinary send path below —
|
|
3317
3590
|
// including the queue — so a command-driven turn is not a second way
|
|
@@ -3339,6 +3612,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3339
3612
|
description: command.problem
|
|
3340
3613
|
? `Unavailable: ${command.problem}`
|
|
3341
3614
|
: command.description,
|
|
3615
|
+
disabledReason: command.problem,
|
|
3342
3616
|
})),
|
|
3343
3617
|
windowSize: suggestionWindowSize(terminal.rows),
|
|
3344
3618
|
});
|
|
@@ -3346,8 +3620,154 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3346
3620
|
}
|
|
3347
3621
|
case 'agent-cockpit': {
|
|
3348
3622
|
if (!openAgentCockpit()) {
|
|
3349
|
-
pushMessage('system',
|
|
3623
|
+
pushMessage('system', `No delegated agents in this conversation.${session?.agentIds.length ? `\nAvailable: ${session.agentIds.join(', ')}` : '\nNo agents are configured.'}`);
|
|
3624
|
+
}
|
|
3625
|
+
return;
|
|
3626
|
+
}
|
|
3627
|
+
case 'settings-picker': {
|
|
3628
|
+
const commands = [
|
|
3629
|
+
{
|
|
3630
|
+
name: 'model',
|
|
3631
|
+
description: `${session?.providerSummary ?? 'No provider'} · ${session?.modelSummary ?? 'No model'}. Change model or provider.`,
|
|
3632
|
+
},
|
|
3633
|
+
{
|
|
3634
|
+
name: 'effort',
|
|
3635
|
+
description: `${reasoningEffortRef.current ?? session?.reasoningEffortDefault ?? 'Model default'} · applies to future turns in this session.`,
|
|
3636
|
+
...(session?.reasoningEffortLevels === undefined
|
|
3637
|
+
? { problem: 'This model does not publish reasoning choices.' }
|
|
3638
|
+
: {}),
|
|
3639
|
+
},
|
|
3640
|
+
{
|
|
3641
|
+
name: 'permissions',
|
|
3642
|
+
description: `${permissionModeRef.current} · applies to future turns in this session.`,
|
|
3643
|
+
},
|
|
3644
|
+
{
|
|
3645
|
+
name: 'status config',
|
|
3646
|
+
description: 'Show which configuration files and overrides are in use.',
|
|
3647
|
+
},
|
|
3648
|
+
];
|
|
3649
|
+
setSelectedChoice(0);
|
|
3650
|
+
setChoicePicker({
|
|
3651
|
+
kind: 'command',
|
|
3652
|
+
title: 'Settings',
|
|
3653
|
+
notice: 'Select a setting to view or change it.',
|
|
3654
|
+
values: commands,
|
|
3655
|
+
options: commands.map((command) => ({
|
|
3656
|
+
label: `/${command.name}`,
|
|
3657
|
+
description: command.description,
|
|
3658
|
+
disabledReason: command.problem,
|
|
3659
|
+
})),
|
|
3660
|
+
windowSize: 7,
|
|
3661
|
+
});
|
|
3662
|
+
return;
|
|
3663
|
+
}
|
|
3664
|
+
case 'goal-editor': {
|
|
3665
|
+
const scope = scopeRef.current;
|
|
3666
|
+
if (!scope || !sessionsRef.current) {
|
|
3667
|
+
pushMessage('system', 'Goals require conversation history to be available.');
|
|
3668
|
+
return;
|
|
3350
3669
|
}
|
|
3670
|
+
textPromptTokenRef.current += 1;
|
|
3671
|
+
setTextPrompt({
|
|
3672
|
+
token: textPromptTokenRef.current,
|
|
3673
|
+
kind: slash.edit ? 'goal-edit' : 'goal-create',
|
|
3674
|
+
title: slash.edit ? 'Edit goal' : 'Set a goal',
|
|
3675
|
+
placeholder: `Up to ${goalStatus?.maxGoalRounds ?? DEFAULT_MAX_GOAL_ROUNDS} automatic turns; /goal pause stops continuation.`,
|
|
3676
|
+
emptyNotice: 'Describe the result you want, or press Esc to cancel.',
|
|
3677
|
+
initialValue: slash.edit ? (goalStatus?.objective ?? '') : '',
|
|
3678
|
+
sessionId: scope.sessionId,
|
|
3679
|
+
});
|
|
3680
|
+
return;
|
|
3681
|
+
}
|
|
3682
|
+
case 'goal-picker': {
|
|
3683
|
+
const generation = conversationGenRef.current;
|
|
3684
|
+
const request = {};
|
|
3685
|
+
setSelectedChoice(-1);
|
|
3686
|
+
setChoicePicker({
|
|
3687
|
+
kind: 'command',
|
|
3688
|
+
title: 'Goal',
|
|
3689
|
+
notice: 'Loading goal…',
|
|
3690
|
+
values: [],
|
|
3691
|
+
options: [],
|
|
3692
|
+
windowSize: 7,
|
|
3693
|
+
request,
|
|
3694
|
+
});
|
|
3695
|
+
void (async () => {
|
|
3696
|
+
try {
|
|
3697
|
+
const sessions = sessionsRef.current;
|
|
3698
|
+
const scope = scopeRef.current;
|
|
3699
|
+
if (!sessions || !scope) {
|
|
3700
|
+
setChoicePicker(null);
|
|
3701
|
+
pushMessage('system', 'Goals require conversation history to be available.');
|
|
3702
|
+
return;
|
|
3703
|
+
}
|
|
3704
|
+
const goal = conversationMaterializedRef.current
|
|
3705
|
+
? await sessions.goals.getGoal(scope.sessionId, sessions.tenantId)
|
|
3706
|
+
: null;
|
|
3707
|
+
if (appLifetime.signal.aborted ||
|
|
3708
|
+
conversationGenRef.current !== generation ||
|
|
3709
|
+
choicePickerRef.current?.request !== request)
|
|
3710
|
+
return;
|
|
3711
|
+
setGoalStatus(goal);
|
|
3712
|
+
const commands = goal
|
|
3713
|
+
? [
|
|
3714
|
+
{
|
|
3715
|
+
name: 'goal status',
|
|
3716
|
+
description: 'View goal progress and continuation status.',
|
|
3717
|
+
},
|
|
3718
|
+
{ name: 'goal edit', description: 'Change the objective.' },
|
|
3719
|
+
...(goal.phase === 'complete'
|
|
3720
|
+
? []
|
|
3721
|
+
: [
|
|
3722
|
+
goal.phase === 'active' && goalActivation.isArmed(scope.sessionId, goal)
|
|
3723
|
+
? {
|
|
3724
|
+
name: 'goal pause',
|
|
3725
|
+
description: 'Stop automatic continuation after the current turn.',
|
|
3726
|
+
}
|
|
3727
|
+
: {
|
|
3728
|
+
name: 'goal resume',
|
|
3729
|
+
description: 'Continue working automatically toward this goal.',
|
|
3730
|
+
},
|
|
3731
|
+
]),
|
|
3732
|
+
{
|
|
3733
|
+
name: 'goal clear',
|
|
3734
|
+
description: 'Remove this goal and stop its automatic continuation.',
|
|
3735
|
+
},
|
|
3736
|
+
]
|
|
3737
|
+
: [
|
|
3738
|
+
{
|
|
3739
|
+
name: 'goal set',
|
|
3740
|
+
description: 'Set an objective and start automatic continuation.',
|
|
3741
|
+
},
|
|
3742
|
+
];
|
|
3743
|
+
setSelectedChoice(0);
|
|
3744
|
+
setChoicePicker({
|
|
3745
|
+
kind: 'command',
|
|
3746
|
+
back: choicePickerRef.current?.back,
|
|
3747
|
+
title: 'Goal',
|
|
3748
|
+
notice: goal
|
|
3749
|
+
? `${goal.phase} · ${goal.roundsAdmitted}/${goal.maxGoalRounds} automatic turns · ${goal.objective}`
|
|
3750
|
+
: `No goal set for this conversation. Up to ${DEFAULT_MAX_GOAL_ROUNDS} automatic turns when started.`,
|
|
3751
|
+
values: commands,
|
|
3752
|
+
options: commands.map((command) => ({
|
|
3753
|
+
label: `/${command.name}`,
|
|
3754
|
+
description: command.description,
|
|
3755
|
+
})),
|
|
3756
|
+
windowSize: 7,
|
|
3757
|
+
});
|
|
3758
|
+
}
|
|
3759
|
+
catch (error) {
|
|
3760
|
+
if (!appLifetime.signal.aborted &&
|
|
3761
|
+
conversationGenRef.current === generation &&
|
|
3762
|
+
choicePickerRef.current?.request === request) {
|
|
3763
|
+
setChoicePicker(null);
|
|
3764
|
+
pushMessage('system', `Could not load goal: ${error instanceof Error ? error.message : String(error)}`);
|
|
3765
|
+
}
|
|
3766
|
+
}
|
|
3767
|
+
finally {
|
|
3768
|
+
wakeGoalDriver();
|
|
3769
|
+
}
|
|
3770
|
+
})();
|
|
3351
3771
|
return;
|
|
3352
3772
|
}
|
|
3353
3773
|
case 'clear-screen':
|
|
@@ -3419,7 +3839,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3419
3839
|
setKeyEntryFor(null);
|
|
3420
3840
|
setPickerDetected(null);
|
|
3421
3841
|
setPickerSelectionKind('provider-and-model');
|
|
3422
|
-
setPickerInitialView('
|
|
3842
|
+
setPickerInitialView('models');
|
|
3423
3843
|
setPhase('picker');
|
|
3424
3844
|
return;
|
|
3425
3845
|
case 'permission-mode': {
|
|
@@ -3486,6 +3906,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3486
3906
|
label: effort ?? 'default',
|
|
3487
3907
|
description: reasoningEffortDescription(effort),
|
|
3488
3908
|
current: effort === current,
|
|
3909
|
+
default: effort === undefined,
|
|
3489
3910
|
})),
|
|
3490
3911
|
});
|
|
3491
3912
|
return;
|
|
@@ -3527,16 +3948,17 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3527
3948
|
}
|
|
3528
3949
|
case 'remember':
|
|
3529
3950
|
try {
|
|
3530
|
-
appendMemory(slash.text);
|
|
3531
|
-
pushMessage('system', `Remembered: ${slash.text}`);
|
|
3951
|
+
const path = appendMemory(slash.text, { scope: slash.scope, cwd: ctx.cwd });
|
|
3952
|
+
pushMessage('system', `Remembered ${slash.scope === 'user' ? 'for every project' : 'for this project'} (${path}): ${slash.text}`);
|
|
3532
3953
|
}
|
|
3533
3954
|
catch (err) {
|
|
3534
3955
|
pushMessage('system', `Could not save memory: ${err instanceof Error ? err.message : String(err)}`);
|
|
3535
3956
|
}
|
|
3536
3957
|
return;
|
|
3537
3958
|
case 'show-memory': {
|
|
3538
|
-
const mem = composeMemoryPrompt(readMemory());
|
|
3539
|
-
pushMessage('system', mem ??
|
|
3959
|
+
const mem = composeMemoryPrompt(readMemory(undefined, ctx.cwd));
|
|
3960
|
+
pushMessage('system', mem ??
|
|
3961
|
+
'Nothing remembered yet. #note or /memory <text> saves a fact about this project (.namzu/MEMORY.md); /memory --user <text> saves one for every project (~/.namzu/MEMORY.md).');
|
|
3540
3962
|
return;
|
|
3541
3963
|
}
|
|
3542
3964
|
case 'list-skills': {
|
|
@@ -3595,79 +4017,19 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3595
4017
|
case 'fork':
|
|
3596
4018
|
void doFork();
|
|
3597
4019
|
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
|
-
];
|
|
4020
|
+
case 'restore':
|
|
4021
|
+
void doRestore(slash.turn);
|
|
4022
|
+
return;
|
|
4023
|
+
case 'add-dir': {
|
|
4024
|
+
const dirs = session?.directories;
|
|
4025
|
+
if (!dirs) {
|
|
4026
|
+
pushMessage('system', 'No session yet; /add-dir works once one has started.');
|
|
4027
|
+
return;
|
|
4028
|
+
}
|
|
4029
|
+
void dirs.add(slash.path).then((result) => {
|
|
4030
|
+
pushMessage('system', result.added
|
|
4031
|
+
? `Added ${result.path}. The file tools reach it by absolute path from the next turn; a sandboxed run binds it read-write.`
|
|
4032
|
+
: `Not added: ${result.path} — ${result.reason ?? 'refused'}`);
|
|
3671
4033
|
});
|
|
3672
4034
|
return;
|
|
3673
4035
|
}
|
|
@@ -3702,9 +4064,17 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3702
4064
|
await materializeConversation();
|
|
3703
4065
|
const durableSessions = sessionsRef.current;
|
|
3704
4066
|
const runScope = scopeRef.current;
|
|
4067
|
+
const taskStore = slash.name === 'tasks' ? session?.currentTaskStore?.() : undefined;
|
|
4068
|
+
if (slash.name === 'tasks' && !taskStore) {
|
|
4069
|
+
pushMessage('system', session?.currentTaskStore
|
|
4070
|
+
? 'No task list is available yet for this conversation.'
|
|
4071
|
+
: 'Task listing is unavailable in this session.');
|
|
4072
|
+
return;
|
|
4073
|
+
}
|
|
3705
4074
|
const registry = new HostCommandRegistry();
|
|
3706
4075
|
registry.register(kernelHostCommands({
|
|
3707
4076
|
allowedAgentIds: session?.agentIds ?? [],
|
|
4077
|
+
...(taskStore ? { taskStore } : {}),
|
|
3708
4078
|
...(durableSessions && runScope && conversationMaterializedRef.current
|
|
3709
4079
|
? {
|
|
3710
4080
|
goal: {
|
|
@@ -4004,11 +4374,15 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4004
4374
|
advanceQueueContinuation,
|
|
4005
4375
|
applyPermissionMode,
|
|
4006
4376
|
applyReasoningEffort,
|
|
4377
|
+
appLifetime,
|
|
4007
4378
|
ctx.cwd,
|
|
4008
4379
|
doResume,
|
|
4009
4380
|
enqueueQueued,
|
|
4010
4381
|
exitWithSummary,
|
|
4011
4382
|
hasUnsettledTurn,
|
|
4383
|
+
goalActivation,
|
|
4384
|
+
goalStatus,
|
|
4385
|
+
hostCommands,
|
|
4012
4386
|
nextId,
|
|
4013
4387
|
openAgentCockpit,
|
|
4014
4388
|
pushMessage,
|
|
@@ -4016,6 +4390,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4016
4390
|
removeStoredCredential,
|
|
4017
4391
|
resetTranscript,
|
|
4018
4392
|
runConversationExport,
|
|
4393
|
+
session,
|
|
4019
4394
|
setChoicePicker,
|
|
4020
4395
|
setCopyPicker,
|
|
4021
4396
|
setReasoningEffort,
|
|
@@ -4224,9 +4599,9 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4224
4599
|
pushMessage('system', `⬆ Update available:\n${lines.join('\n')}`);
|
|
4225
4600
|
});
|
|
4226
4601
|
}, [phase, ctx.version, pushMessage]);
|
|
4227
|
-
const hydrateFromPicker = useCallback(async (prefs, detectedNow, signal,
|
|
4602
|
+
const hydrateFromPicker = useCallback(async (prefs, detectedNow, signal, options = {}) => {
|
|
4228
4603
|
try {
|
|
4229
|
-
await hydrateSession(prefs, detectedNow, signal);
|
|
4604
|
+
await hydrateSession(prefs, detectedNow, signal, options.persistSelection);
|
|
4230
4605
|
}
|
|
4231
4606
|
catch (err) {
|
|
4232
4607
|
// A superseded choice no longer owns even its failure message. Its
|
|
@@ -4234,7 +4609,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4234
4609
|
// choice stays on the picker with the actionable construction error.
|
|
4235
4610
|
if (signal.aborted)
|
|
4236
4611
|
return;
|
|
4237
|
-
if (revealAllOnFailure) {
|
|
4612
|
+
if (options.revealAllOnFailure) {
|
|
4238
4613
|
setPickerDetected(null);
|
|
4239
4614
|
setPickerSelectionKind('provider-and-model');
|
|
4240
4615
|
setPickerInitialView('providers');
|
|
@@ -4259,48 +4634,29 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4259
4634
|
setPickerNotice(null);
|
|
4260
4635
|
// The disposition, not the key. `credential` never reaches a message.
|
|
4261
4636
|
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.
|
|
4637
|
+
// Supplying a credential changes the primary's access, not unrelated
|
|
4638
|
+
// fallbacks, model pins, delegation or capability preferences.
|
|
4268
4639
|
const saved = savedPrefsRef.current;
|
|
4269
4640
|
const prefs = saved && primaryProvider(saved).id === credential.entry.id
|
|
4270
4641
|
? saved
|
|
4271
|
-
: {
|
|
4272
|
-
version: 3,
|
|
4273
|
-
providers: [{ id: credential.entry.id }],
|
|
4274
|
-
subagents: { active: [] },
|
|
4275
|
-
};
|
|
4642
|
+
: selectPrimaryProvider(saved, { id: credential.entry.id });
|
|
4276
4643
|
void hydrateFromPicker(prefs, next, signal);
|
|
4277
4644
|
}, [detected, hydrateFromPicker, pushMessage]);
|
|
4278
4645
|
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
|
-
};
|
|
4646
|
+
const prefs = selectPrimaryProvider(savedPrefsRef.current, {
|
|
4647
|
+
id: selection.provider,
|
|
4648
|
+
...(selection.model !== undefined ? { model: selection.model } : {}),
|
|
4649
|
+
});
|
|
4291
4650
|
setKeyEntryFor(null);
|
|
4292
4651
|
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
4652
|
// Keep a narrowed signed-in roster stable while construction is pending.
|
|
4301
4653
|
// Only a real refusal broadens back to the general, sign-in-capable picker.
|
|
4302
|
-
void hydrateFromPicker(prefs, detected, signal,
|
|
4303
|
-
|
|
4654
|
+
void hydrateFromPicker(prefs, detected, signal, {
|
|
4655
|
+
revealAllOnFailure: true,
|
|
4656
|
+
persistSelection: detected.find((provider) => provider.entry.id === selection.provider)?.source.kind !==
|
|
4657
|
+
'session',
|
|
4658
|
+
});
|
|
4659
|
+
}, [detected, hydrateFromPicker]);
|
|
4304
4660
|
/**
|
|
4305
4661
|
* Leaving the picker returns to whatever was on screen before it.
|
|
4306
4662
|
*
|
|
@@ -4330,6 +4686,13 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4330
4686
|
exitWithSummary();
|
|
4331
4687
|
}, [session, exitWithSummary]);
|
|
4332
4688
|
useInput((input, key) => {
|
|
4689
|
+
// Startup has refused admission, so there is no draft or active turn to
|
|
4690
|
+
// protect with the ready screen's two-press exit ladder.
|
|
4691
|
+
if (phase === 'unhealthy') {
|
|
4692
|
+
if (key.escape || (key.ctrl && input === 'c'))
|
|
4693
|
+
exitWithSummary();
|
|
4694
|
+
return;
|
|
4695
|
+
}
|
|
4333
4696
|
// The provider picker owns its own keyboard — arrows, digits, enter,
|
|
4334
4697
|
// esc and the typed-key screen are all its business. Ctrl+C is not: it
|
|
4335
4698
|
// is the one key that must work on every screen, and this handler used
|
|
@@ -4347,12 +4710,11 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4347
4710
|
exitWithSummary();
|
|
4348
4711
|
return;
|
|
4349
4712
|
}
|
|
4350
|
-
// Previous-prompt picker
|
|
4351
|
-
// older prompts, matching the second Esc that opened it; q is the cancel.
|
|
4713
|
+
// Previous-prompt picker uses the same cancel key as other selectors.
|
|
4352
4714
|
if (phase === 'edit') {
|
|
4353
4715
|
if (editCommittedRef.current)
|
|
4354
4716
|
return;
|
|
4355
|
-
if ((key.ctrl && input === 'c') || input.toLowerCase() === 'q') {
|
|
4717
|
+
if (key.escape || (key.ctrl && input === 'c') || input.toLowerCase() === 'q') {
|
|
4356
4718
|
setEditList([]);
|
|
4357
4719
|
setPhase('ready');
|
|
4358
4720
|
return;
|
|
@@ -4361,7 +4723,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4361
4723
|
setSelectedEdit((index) => moveSelection(index, editList.length, key.home ? 'first' : key.end ? 'last' : key.pageUp ? 'previous-page' : 'next-page'));
|
|
4362
4724
|
return;
|
|
4363
4725
|
}
|
|
4364
|
-
if (key.
|
|
4726
|
+
if (key.leftArrow || key.upArrow) {
|
|
4365
4727
|
setSelectedEdit((index) => moveSelection(index, editList.length, 'previous'));
|
|
4366
4728
|
return;
|
|
4367
4729
|
}
|
|
@@ -4435,25 +4797,27 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4435
4797
|
setPermissionDetailsOpen(!permissionDetailsOpenRef.current);
|
|
4436
4798
|
return;
|
|
4437
4799
|
}
|
|
4438
|
-
if (permission &&
|
|
4439
|
-
|
|
4800
|
+
if (permission && (key.upArrow || key.downArrow)) {
|
|
4801
|
+
const current = permissionChoiceRef.current;
|
|
4802
|
+
const next = key.upArrow ? Math.max(0, current - 1) : Math.min(2, current + 1);
|
|
4803
|
+
setPermissionChoice(next);
|
|
4804
|
+
return;
|
|
4805
|
+
}
|
|
4806
|
+
if (permission && (key.pageUp || key.pageDown || key.home || key.end)) {
|
|
4440
4807
|
const source = permissionDetailsOpenRef.current
|
|
4441
4808
|
? permission.review
|
|
4442
4809
|
: permission.summary.text;
|
|
4443
4810
|
const count = permissionReviewRows(source, terminal.columns).length;
|
|
4444
|
-
const
|
|
4811
|
+
const pageRows = permissionReviewPageRows(terminal.rows);
|
|
4812
|
+
const maxOffset = Math.max(0, count - pageRows);
|
|
4445
4813
|
const current = Math.min(permissionReviewOffsetRef.current, maxOffset);
|
|
4446
4814
|
const next = key.home
|
|
4447
4815
|
? 0
|
|
4448
4816
|
: key.end
|
|
4449
4817
|
? maxOffset
|
|
4450
4818
|
: 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);
|
|
4819
|
+
? Math.max(0, current - pageRows)
|
|
4820
|
+
: Math.min(maxOffset, current + pageRows);
|
|
4457
4821
|
setPermissionReviewOffset(next);
|
|
4458
4822
|
return;
|
|
4459
4823
|
}
|
|
@@ -4468,18 +4832,21 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4468
4832
|
resolvePermission({ kind: 'reject' });
|
|
4469
4833
|
return;
|
|
4470
4834
|
}
|
|
4471
|
-
//
|
|
4472
|
-
//
|
|
4473
|
-
//
|
|
4474
|
-
//
|
|
4475
|
-
// operator never looked at.
|
|
4476
|
-
// they must arrive after the prompt has been up long enough to read.
|
|
4835
|
+
// Every approval waits out the settle window: `y`, `a`, a digit and
|
|
4836
|
+
// Enter alike. Enter confirms the answer the cursor is on, which
|
|
4837
|
+
// opens on "Yes" — the prompt an operator knows from elsewhere — so
|
|
4838
|
+
// the window is what stands between an in-flight Enter and a call
|
|
4839
|
+
// the operator never looked at. Refusals above never wait.
|
|
4477
4840
|
if (!approvalIsDeliberate(permissionOpenedAtRef.current, Date.now()))
|
|
4478
4841
|
return;
|
|
4479
|
-
|
|
4842
|
+
const numbered = ch === '1' ? 0 : ch === '2' ? 1 : ch === '3' ? 2 : null;
|
|
4843
|
+
const chosen = key.return ? permissionChoiceRef.current : numbered;
|
|
4844
|
+
if (ch === 'y' || chosen === 0)
|
|
4480
4845
|
resolvePermission({ kind: 'approve' });
|
|
4481
|
-
else if (ch === 'a')
|
|
4846
|
+
else if (ch === 'a' || chosen === 1)
|
|
4482
4847
|
resolvePermission({ kind: 'approve-all' });
|
|
4848
|
+
else if (chosen === 2)
|
|
4849
|
+
resolvePermission({ kind: 'reject' });
|
|
4483
4850
|
return;
|
|
4484
4851
|
}
|
|
4485
4852
|
// The active child count already advertises this key beside Working.
|
|
@@ -4629,10 +4996,17 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4629
4996
|
// menu. Only the exact object React committed may consume a choice.
|
|
4630
4997
|
if (choicePickerCommittedRef.current !== picker)
|
|
4631
4998
|
return;
|
|
4632
|
-
const
|
|
4999
|
+
const searchable = choicePickerSearchable(picker);
|
|
5000
|
+
const options = filterChoiceOptions(picker.options, choiceQueryRef.current);
|
|
5001
|
+
const applyVisibleChoice = (index) => {
|
|
5002
|
+
const option = options[index];
|
|
5003
|
+
if (option)
|
|
5004
|
+
applyChoiceSelection(picker.options.indexOf(option));
|
|
5005
|
+
};
|
|
4633
5006
|
if (key.escape || (key.ctrl && input === 'c')) {
|
|
4634
5007
|
reviewChoiceInFlightRef.current = null;
|
|
4635
|
-
|
|
5008
|
+
setSelectedChoice(0);
|
|
5009
|
+
setChoicePicker(key.escape ? (picker.back ?? null) : null);
|
|
4636
5010
|
// Esc leaves the question unanswered and the turn running; the
|
|
4637
5011
|
// tool tells the model so. Ctrl+C is the operator saying stop.
|
|
4638
5012
|
if (picker.kind === 'user-question') {
|
|
@@ -4641,26 +5015,42 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4641
5015
|
return;
|
|
4642
5016
|
}
|
|
4643
5017
|
if (key.home || key.end || key.pageUp || key.pageDown) {
|
|
4644
|
-
const pageSize =
|
|
4645
|
-
|
|
5018
|
+
const pageSize = choicePickerWindowSize({
|
|
5019
|
+
rows: terminal.rows,
|
|
5020
|
+
columns: Math.max(1, (terminal.columns ?? 80) - 2),
|
|
5021
|
+
searchable,
|
|
5022
|
+
notice: Boolean(picker.notice),
|
|
5023
|
+
selectedDescription: options.some((option) => Boolean(option.selectedDescription || option.disabledReason)),
|
|
5024
|
+
windowSize: picker.kind === 'command' ? picker.windowSize : undefined,
|
|
5025
|
+
});
|
|
5026
|
+
setSelectedChoice((index) => moveChoiceSelection(options, index, key.home ? 'first' : key.end ? 'last' : key.pageUp ? 'previous-page' : 'next-page', pageSize));
|
|
4646
5027
|
return;
|
|
4647
5028
|
}
|
|
4648
5029
|
if (key.upArrow) {
|
|
4649
|
-
setSelectedChoice((index) =>
|
|
5030
|
+
setSelectedChoice((index) => moveChoiceSelection(options, index, 'previous'));
|
|
4650
5031
|
return;
|
|
4651
5032
|
}
|
|
4652
5033
|
if (key.downArrow) {
|
|
4653
|
-
setSelectedChoice((index) =>
|
|
5034
|
+
setSelectedChoice((index) => moveChoiceSelection(options, index, 'next'));
|
|
4654
5035
|
return;
|
|
4655
5036
|
}
|
|
4656
5037
|
if (key.return) {
|
|
4657
|
-
|
|
5038
|
+
applyVisibleChoice(selectedChoiceRef.current);
|
|
5039
|
+
return;
|
|
5040
|
+
}
|
|
5041
|
+
if (searchable && !key.ctrl && !key.meta && (key.backspace || key.delete || input)) {
|
|
5042
|
+
const next = key.backspace || key.delete
|
|
5043
|
+
? eraseLastChoiceGrapheme(choiceQueryRef.current)
|
|
5044
|
+
: choiceQueryRef.current + input.replace(/\p{Cc}/gu, '');
|
|
5045
|
+
choiceQueryRef.current = next;
|
|
5046
|
+
setChoiceQuery(next);
|
|
5047
|
+
setSelectedChoice(moveChoiceSelection(filterChoiceOptions(picker.options, next), 0, 'first'));
|
|
4658
5048
|
return;
|
|
4659
5049
|
}
|
|
4660
5050
|
if (/^[1-9]$/.test(input)) {
|
|
4661
5051
|
const index = Number(input) - 1;
|
|
4662
5052
|
if (options[index])
|
|
4663
|
-
|
|
5053
|
+
applyVisibleChoice(index);
|
|
4664
5054
|
}
|
|
4665
5055
|
return;
|
|
4666
5056
|
}
|
|
@@ -4739,19 +5129,36 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4739
5129
|
// in place and sometimes prints a second copy further down would be two
|
|
4740
5130
|
// commands wearing one name.
|
|
4741
5131
|
if (key.ctrl && input === 'o') {
|
|
4742
|
-
// Taken outside the updater so both branches use the same one
|
|
4743
|
-
// `/expand` does.
|
|
5132
|
+
// Taken outside the updater so both branches use the same one.
|
|
4744
5133
|
const id = nextId();
|
|
4745
5134
|
setMessages((prev) => {
|
|
4746
5135
|
const live = prev.filter((m) => !m.pending).slice(settledRef.current);
|
|
4747
5136
|
const collapsible = live.filter((m) => willCollapse(m.detail));
|
|
4748
5137
|
if (collapsible.length === 0) {
|
|
5138
|
+
// Nothing redrawable is collapsed, so this reprints the most recent
|
|
5139
|
+
// collapsed body as a new row — the rows above have been printed
|
|
5140
|
+
// to the terminal's own scrollback, which cannot be rewritten.
|
|
5141
|
+
const blocks = prev.filter((m) => m.detailRef !== undefined);
|
|
5142
|
+
const block = blocks[blocks.length - 1];
|
|
5143
|
+
if (!block) {
|
|
5144
|
+
return [
|
|
5145
|
+
...prev,
|
|
5146
|
+
{
|
|
5147
|
+
id,
|
|
5148
|
+
role: 'system',
|
|
5149
|
+
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.',
|
|
5150
|
+
},
|
|
5151
|
+
];
|
|
5152
|
+
}
|
|
4749
5153
|
return [
|
|
4750
5154
|
...prev,
|
|
4751
5155
|
{
|
|
4752
5156
|
id,
|
|
4753
5157
|
role: 'system',
|
|
4754
|
-
content:
|
|
5158
|
+
content: `${block.content} — in full (${block.detail?.length ?? 0} lines)`,
|
|
5159
|
+
glyph: '⤢',
|
|
5160
|
+
detail: block.detail,
|
|
5161
|
+
detailExpanded: true,
|
|
4755
5162
|
},
|
|
4756
5163
|
];
|
|
4757
5164
|
}
|
|
@@ -4840,21 +5247,28 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4840
5247
|
: agentSurface?.kind === 'transcript'
|
|
4841
5248
|
? 'observing agent — esc agents · q parent'
|
|
4842
5249
|
: textPrompt
|
|
4843
|
-
? '
|
|
5250
|
+
? 'editor open · enter apply · esc cancel'
|
|
4844
5251
|
: choicePicker
|
|
4845
|
-
?
|
|
5252
|
+
? choicePickerSearchable(choicePicker)
|
|
5253
|
+
? 'type to filter · ↑↓ select · enter apply · esc back'
|
|
5254
|
+
: '↑↓ / 1–9 select · enter apply · esc back'
|
|
4846
5255
|
: copyPicker
|
|
4847
5256
|
? 'copy target open — ↑↓ / 1–9 select · esc cancel'
|
|
4848
5257
|
: goalStatus && phase === 'ready' && state === 'idle'
|
|
4849
5258
|
? undefined
|
|
4850
5259
|
: hintForPhase(phase, state, session?.hasProvider === true, composerHasDraft);
|
|
4851
|
-
return (_jsx(Box, { flexDirection: "column", display: externalEditorRequest ? 'none' : 'flex', children: _jsxs(Box, { flexDirection: "column", paddingX: 1, children: [
|
|
4852
|
-
|
|
5260
|
+
return (_jsx(Box, { flexDirection: "column", display: externalEditorRequest ? 'none' : 'flex', children: _jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(TranscriptFrame, { children: _jsx(Transcript, { messages: finalized, pending: messages.find((m) => m.pending) ?? null, state: state, settled: transcriptOwned ? renderedSettled : 0, resetKey: resetKey, raw: rawOutput, hyperlinks: hyperlinks, showLive: !lifecycleOwnsViewport, header: transcriptOwned ? (_jsx(BrandHeader, { version: ctx.version, provider: session?.providerSummary, model: session?.modelSummary, permissionMode: permissionMode, cwd: ctx.cwd })) : undefined }) }), phase === 'trust' ? (_jsx(TrustPrompt, { cwd: ctx.cwd })) : phase === 'unhealthy' ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: theme.status.error, bold: true, children: "Startup stopped" }), _jsx(Text, { color: theme.text.secondary, children: "Resolve the error above, then restart Namzu." })] })) : phase === 'resume' ? (_jsx(ResumePicker, { conversations: resumeList, selected: selectedResume })) : phase === 'edit' ? (_jsx(EditPromptPicker, { prompts: editList, selected: selectedEdit })) : phase === 'picker' ? (_jsx(Picker, { detected: pickerDetected ?? detected, currentProvider: currentProvider, selectionKind: pickerSelectionKind, currentModel: session?.modelSummary ?? null, initialView: pickerInitialView, onSubmit: handlePickerSubmit, onCancel: handlePickerCancel, onCredential: handleTypedCredential, onLogin: startLoginFromPicker, onLoginComplete: finishLoginFromPicker, keyEntryFor: keyEntryFor, notice: pickerNotice })) : (_jsxs(_Fragment, { children: [agentSurface === null ? (_jsx(LiveActivity, { compact: compactWork, activeTools: visibleActiveTools, working: state === 'thinking' || state === 'tool', interruptible: abortRef.current !== null, animate: stdout.isTTY === true, thinking: thinking })) : null, agentSurface === null && permission === null ? (_jsx(TaskList, { tasks: tasks, compact: compactWork })) : null, permission ? (_jsx(PermissionOverlay, { toolCalls: permission.toolCalls, review: permission.review, summary: permission.summary, detailsOpen: permissionDetailsOpen, reviewOffset: permissionReviewOffset, choice: permissionChoice, columns: terminal.columns, rows: terminal.rows })) : null, textPrompt ? (_jsx(TextPrompt, { title: textPrompt.title, placeholder: textPrompt.placeholder, initialValue: textPrompt.initialValue, emptyNotice: textPrompt.emptyNotice, hidden: permission !== null || agentSurface !== null, onSubmit: submitTextPrompt, onCancel: cancelTextPrompt }, textPrompt.token)) : permission === null && agentSurface === null && choicePicker ? (_jsx(ChoicePicker, { columns: Math.max(1, (terminal.columns ?? 80) - 2), title: choicePicker.title, notice: choicePicker.notice, options: filterChoiceOptions(choicePicker.options, choiceQuery), query: choicePickerSearchable(choicePicker) ? choiceQuery : undefined, selected: selectedChoice, windowSize: choicePicker.kind === 'command' ? choicePicker.windowSize : undefined })) : permission === null && agentSurface === null && copyPicker ? (_jsx(CopyPicker, { targets: copyPicker.targets, selected: selectedCopy })) : null, _jsxs(ComposerFrame, { working: state === 'thinking' || state === 'tool' || visibleActiveTools.length > 0, focus: phase === 'ready' &&
|
|
5261
|
+
state !== 'awaiting-permission' &&
|
|
5262
|
+
!compacting &&
|
|
5263
|
+
externalEditorRequest === null &&
|
|
4853
5264
|
conversationMutation === null &&
|
|
4854
5265
|
textPrompt === null &&
|
|
4855
5266
|
choicePicker === null &&
|
|
4856
5267
|
copyPicker === null &&
|
|
4857
|
-
agentSurface === null, hidden: permission !== null ||
|
|
5268
|
+
agentSurface === null, hidden: permission !== null ||
|
|
5269
|
+
textPrompt !== null ||
|
|
5270
|
+
choicePicker !== null ||
|
|
5271
|
+
copyPicker !== null, children: [pendingSteers.length > 0 &&
|
|
4858
5272
|
permission === null &&
|
|
4859
5273
|
textPrompt === null &&
|
|
4860
5274
|
choicePicker === null &&
|
|
@@ -4879,26 +5293,22 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4879
5293
|
copyPicker !== null,
|
|
4880
5294
|
// A turn is running, so Esc is the interrupt and not
|
|
4881
5295
|
// 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,
|
|
5296
|
+
escapeInterrupts: !compacting && (state === 'thinking' || state === 'tool'), onSubmit: handleSubmit, onNotice: (text) => pushMessage('system', text), onStepReasoningEffort: stepReasoningEffort, onExternalEdit: requestExternalEditor, onEditPrevious: openPromptEditor, draftToRestore: composerDraft, onDraftRestored: (token) => setComposerDraft((draft) => (draft?.token === token ? null : draft)), onDraftPresenceChange: setComposerHasDraft, onOpenAgentPanel: openAgentCockpit, userCommands: userCommands, builtins: hostCommands.map((command) => ({
|
|
5297
|
+
...command,
|
|
5298
|
+
description: command.unavailable?.(slashCtx) ?? command.description,
|
|
5299
|
+
})), mentionCandidates: mentionCandidates, history: history })] }), permission === null && agentSurface === null && liveSubagents.length > 0 ? (_jsx(AgentTaskPanel, { agents: liveSubagents, terminalRows: terminal.rows, terminalColumns: terminal.columns })) : permission === null && agentSurface?.kind === 'cockpit' ? (_jsx(AgentCockpit, { agents: liveSubagents, selectedPhaseId: agentSurface.selectedPhaseId, selectedId: agentSurface.selectedId, focus: agentSurface.focus, terminalRows: terminal.rows, terminalColumns: terminal.columns })) : permission === null && agentSurface?.kind === 'transcript' && selectedSubagent ? (_jsx(AgentTranscript, { agent: selectedSubagent, tailOffset: agentSurface.tailOffset, terminalRows: terminal.rows, terminalColumns: terminal.columns })) : null] })), _jsx(Box, { paddingTop: 1, children: _jsx(StatusBar, { cwd: ctx.cwd, provider: session?.providerSummary ?? null, model: session?.modelSummary ?? null, effort: reasoningEffort ?? 'default', goal: statusGoal, state: state, hint: statusHint }) })] }) }));
|
|
4883
5300
|
}
|
|
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] }));
|
|
5301
|
+
/** One line for a background job that ended, for the transcript and the next turn. */
|
|
5302
|
+
function describeJobExit(job) {
|
|
5303
|
+
const outcome = job.status === 'killed'
|
|
5304
|
+
? 'was stopped'
|
|
5305
|
+
: `exited ${job.exitCode ?? '?'}${job.signal ? ` (${job.signal})` : ''}`;
|
|
5306
|
+
return `background job ${job.jobId} (${job.command}) ${outcome}`;
|
|
4892
5307
|
}
|
|
4893
5308
|
function TranscriptFrame({ children }) {
|
|
4894
5309
|
// Borderless, edge-to-edge — the message glyph gutter provides structure.
|
|
4895
5310
|
return _jsx(Box, { flexDirection: "column", children: children });
|
|
4896
5311
|
}
|
|
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
5312
|
// Tool call label: the tool name title-cased, then its most identifying
|
|
4903
5313
|
// argument — `Bash(ls -la)`, `Read(file.ts)`. A bare tool name in a transcript
|
|
4904
5314
|
// of forty calls says nothing about which one this was.
|
|
@@ -4921,7 +5331,7 @@ hasDraft = false) {
|
|
|
4921
5331
|
if (phase === 'resume')
|
|
4922
5332
|
return '↑↓ navigate · enter resume · esc cancel';
|
|
4923
5333
|
if (phase === 'edit')
|
|
4924
|
-
return '
|
|
5334
|
+
return '← older · → newer · enter fork and edit · esc cancel';
|
|
4925
5335
|
if (phase === 'probing')
|
|
4926
5336
|
return 'discovering providers…';
|
|
4927
5337
|
// Esc does two different things here depending on how the picker was reached,
|
|
@@ -4933,11 +5343,11 @@ hasDraft = false) {
|
|
|
4933
5343
|
: '↑↓ navigate · enter accept · esc or Ctrl+C exit';
|
|
4934
5344
|
}
|
|
4935
5345
|
if (phase === 'unhealthy')
|
|
4936
|
-
return 'Ctrl+C
|
|
5346
|
+
return 'esc or Ctrl+C exit';
|
|
4937
5347
|
// Enter is absent because Enter decides nothing here, deliberately — see the
|
|
4938
5348
|
// permission gate in the key handler above.
|
|
4939
5349
|
if (state === 'awaiting-permission')
|
|
4940
|
-
return '
|
|
5350
|
+
return '↑↓ select · enter confirm · esc decline';
|
|
4941
5351
|
if (state !== 'idle')
|
|
4942
5352
|
return hasDraft ? 'enter steer · tab queue · esc interrupt' : '';
|
|
4943
5353
|
// The composer already points to /help. Keeping the complete key legend in
|
|
@@ -4963,19 +5373,8 @@ function goalStatusLabel(goal) {
|
|
|
4963
5373
|
function labelOfOption(question, optionId) {
|
|
4964
5374
|
return question.options.find((option) => option.id === optionId)?.label ?? optionId;
|
|
4965
5375
|
}
|
|
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
|
-
}
|
|
5376
|
+
function choicePickerSearchable(picker) {
|
|
5377
|
+
return ['command', 'skill', 'review-branch', 'review-commit'].includes(picker.kind);
|
|
4979
5378
|
}
|
|
4980
5379
|
function permissionPickerNotice(session) {
|
|
4981
5380
|
const notices = [];
|