@namzu/cli 18.0.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 +13 -1
- package/dist/cli.js.map +1 -1
- package/dist/commands/acp.d.ts.map +1 -1
- package/dist/commands/acp.js +3 -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 +34 -13
- package/dist/commands/run-stream.js.map +1 -1
- package/dist/commands/run.d.ts.map +1 -1
- package/dist/commands/run.js +133 -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 +122 -2
- package/dist/config/load.js.map +1 -1
- package/dist/config/schema.d.ts +92 -0
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js.map +1 -1
- package/dist/context/doctrine.d.ts +22 -0
- package/dist/context/doctrine.d.ts.map +1 -0
- package/dist/context/doctrine.js +22 -0
- package/dist/context/doctrine.js.map +1 -0
- 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/context/turn-snapshot.d.ts +59 -0
- package/dist/context/turn-snapshot.d.ts.map +1 -0
- package/dist/context/turn-snapshot.js +122 -0
- package/dist/context/turn-snapshot.js.map +1 -0
- 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 +27 -3
- package/dist/integrations/subagents/runtime.d.ts.map +1 -1
- package/dist/integrations/subagents/runtime.js +302 -82
- 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 -20
- package/dist/permissions/mode.d.ts.map +1 -1
- package/dist/permissions/mode.js +18 -3
- 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 +843 -208
- 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 +9 -1
- package/dist/tui/Composer.d.ts.map +1 -1
- package/dist/tui/Composer.js +21 -5
- 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 +118 -11
- 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 +32 -0
- package/dist/tui/TaskList.d.ts.map +1 -0
- package/dist/tui/TaskList.js +67 -0
- package/dist/tui/TaskList.js.map +1 -0
- 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 +152 -48
- package/dist/tui/agent.d.ts.map +1 -1
- package/dist/tui/agent.js +622 -256
- 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 +199 -10
- 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/stream-blocks.d.ts +24 -0
- package/dist/tui/stream-blocks.d.ts.map +1 -1
- package/dist/tui/stream-blocks.js +74 -0
- package/dist/tui/stream-blocks.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 +8 -1
- package/dist/tui/types.d.ts.map +1 -1
- package/package.json +8 -6
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,11 +26,14 @@ 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';
|
|
@@ -38,10 +41,12 @@ import { PermissionOverlay } from './PermissionOverlay.js';
|
|
|
38
41
|
import { Picker } from './Picker.js';
|
|
39
42
|
import { ResumePicker } from './ResumePicker.js';
|
|
40
43
|
import { StatusBar } from './StatusBar.js';
|
|
44
|
+
import { TaskList } from './TaskList.js';
|
|
41
45
|
import { TextPrompt } from './TextPrompt.js';
|
|
42
46
|
import { Transcript, willCollapse } from './Transcript.js';
|
|
43
47
|
import { TrustPrompt } from './TrustPrompt.js';
|
|
44
48
|
import { createAgentSession, probeAgentSession, } from './agent.js';
|
|
49
|
+
import { choicePickerWindowSize, filterChoiceOptions, moveChoiceSelection, } from './choice-selection.js';
|
|
45
50
|
import { keepRecentRows } from './compact-transcript.js';
|
|
46
51
|
import { approvalIsDeliberate } from './consent-timing.js';
|
|
47
52
|
import { planTurnPublication } from './conversation-history.js';
|
|
@@ -50,19 +55,48 @@ import { editablePrompts } from './edit-prompts.js';
|
|
|
50
55
|
import { editDraftInExternalEditor } from './external-editor.js';
|
|
51
56
|
import { liveWindow } from './live-window.js';
|
|
52
57
|
import { describeCodexDeviceLoginStart, describeLoginOutcome, describeLoginStart, describeLogout, describeProviderLogout, } from './login-prompt.js';
|
|
53
|
-
import {
|
|
58
|
+
import { selectPrimaryProvider } from './provider-selection.js';
|
|
59
|
+
import { eraseLastChoiceGrapheme } from './terminal-choice-text.js';
|
|
60
|
+
import { renderCheckpoints, renderRestore } from '../checkpoints/store.js';
|
|
54
61
|
import { expandFileMentions, listMentionableFiles } from './mentions.js';
|
|
55
62
|
import { openInBrowser } from './open-browser.js';
|
|
56
|
-
import {
|
|
63
|
+
import { buildPermissionReview, buildPermissionSummary, permissionReviewPageRows, permissionReviewRefusal, permissionReviewRows, } from './permission-review.js';
|
|
57
64
|
import { describeRunInterruption } from './run-interruption.js';
|
|
58
65
|
import { moveSelection } from './selection-window.js';
|
|
66
|
+
import { describeShellEscape, describeShellEscapeForModel, runShellEscape, shellEscapeCommand, } from './shell-escape.js';
|
|
59
67
|
import { baseBranchReviewPrompt, commitReviewPrompt, hostCommandNames, kernelCommandDescriptors, mergeHostCommands, renderOutcome, reviewPrompt, runSlash, } from './slashCommands.js';
|
|
60
|
-
import { splitCompleteBlocks } from './stream-blocks.js';
|
|
68
|
+
import { splitCompleteBlocks, splitSafeCut } from './stream-blocks.js';
|
|
61
69
|
import { terminalSupportsHyperlinks } from './terminal-hyperlinks.js';
|
|
62
70
|
import { theme } from './theme.js';
|
|
63
71
|
import { useSelectionIndex } from './use-selection-index.js';
|
|
64
72
|
import { renderWorkspaceDiff, workspaceDiff } from './workspace-diff.js';
|
|
65
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;
|
|
76
|
+
/** The value a question row carries: an option id, or the free-text escape. */
|
|
77
|
+
const FREE_TEXT_ANSWER = '__free_text__';
|
|
78
|
+
/**
|
|
79
|
+
* The streaming assistant bubble, carried across events within one turn.
|
|
80
|
+
*
|
|
81
|
+
* `text` is everything the model has said and is what gets persisted;
|
|
82
|
+
* `pending` is the part not yet on screen, held until it forms a whole block.
|
|
83
|
+
* The two are separate because they answer different questions — what was
|
|
84
|
+
* said, and what has been shown — and conflating them is how a reply gets
|
|
85
|
+
* saved with a paragraph the operator never saw, or shown twice.
|
|
86
|
+
*/
|
|
87
|
+
/**
|
|
88
|
+
* How long held reply text may wait before a sentence of it is shown.
|
|
89
|
+
*
|
|
90
|
+
* Blocks are released as they complete (`splitCompleteBlocks`), which keeps a
|
|
91
|
+
* fast reply landing a paragraph at a time. A slow one — a long paragraph
|
|
92
|
+
* arriving over twenty seconds — completes no block until its last character
|
|
93
|
+
* and was invisible for its whole length. Past this interval the pending
|
|
94
|
+
* text is released to its last safe cut instead (`splitSafeCut`): a sentence
|
|
95
|
+
* or a line, never a half-word, never inside a fence. Long enough that a
|
|
96
|
+
* fast stream never reaches it; short enough that a slow one reads as
|
|
97
|
+
* writing rather than as silence.
|
|
98
|
+
*/
|
|
99
|
+
const STREAM_RELEASE_MS = 250;
|
|
66
100
|
/** Last non-empty assistant text in a durable conversation, newest first. */
|
|
67
101
|
function latestAssistantOutput(messages) {
|
|
68
102
|
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
@@ -280,7 +314,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
280
314
|
const [pickerDetected, setPickerDetected] = useState(null);
|
|
281
315
|
/** Whether Enter accepts the provider directly or continues to model choice. */
|
|
282
316
|
const [pickerSelectionKind, setPickerSelectionKind] = useState('provider-and-model');
|
|
283
|
-
/**
|
|
317
|
+
/** Active preferences, or the saved chain awaiting its missing credential. */
|
|
284
318
|
const savedPrefsRef = useRef(null);
|
|
285
319
|
/**
|
|
286
320
|
* Why the picker is open, drawn ON the picker.
|
|
@@ -292,6 +326,22 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
292
326
|
*/
|
|
293
327
|
const [pickerNotice, setPickerNotice] = useState(null);
|
|
294
328
|
const [permission, setPermission] = useState(null);
|
|
329
|
+
/**
|
|
330
|
+
* The question currently on screen and the promise the tool is waiting
|
|
331
|
+
* on. Resolved exactly once: by a chosen row, by free text, by Esc (skip)
|
|
332
|
+
* or by Ctrl+C (abort). A second resolution is a no-op, not a second
|
|
333
|
+
* answer.
|
|
334
|
+
*/
|
|
335
|
+
const questionRef = useRef(null);
|
|
336
|
+
const resolveQuestion = useCallback((answer) => {
|
|
337
|
+
const pending = questionRef.current;
|
|
338
|
+
if (!pending?.resolve)
|
|
339
|
+
return;
|
|
340
|
+
const resolve = pending.resolve;
|
|
341
|
+
pending.resolve = null;
|
|
342
|
+
questionRef.current = null;
|
|
343
|
+
resolve(answer);
|
|
344
|
+
}, []);
|
|
295
345
|
const [permissionReviewOffset, setPermissionReviewOffsetState] = useState(0);
|
|
296
346
|
const permissionReviewOffsetRef = useRef(0);
|
|
297
347
|
const [permissionDetailsOpen, setPermissionDetailsOpenState] = useState(false);
|
|
@@ -304,6 +354,15 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
304
354
|
permissionReviewOffsetRef.current = next;
|
|
305
355
|
setPermissionReviewOffsetState(next);
|
|
306
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
|
+
}, []);
|
|
307
366
|
// The launch bypass is an INITIAL selection, not permanent authority. A
|
|
308
367
|
// typed /permissions command can narrow it back to prompt/strict without
|
|
309
368
|
// rebuilding the provider, tools, plugins, or sandbox.
|
|
@@ -325,10 +384,74 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
325
384
|
/** Git-admitted file paths cached once per successful session hydration. */
|
|
326
385
|
const [mentionCandidates, setMentionCandidates] = useState([]);
|
|
327
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]);
|
|
328
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);
|
|
329
443
|
// Tools currently executing — rendered live (spinner + elapsed) below the
|
|
330
444
|
// transcript, then committed as static lines on completion.
|
|
331
445
|
const [activeTools, setActiveTools] = useState([]);
|
|
446
|
+
/**
|
|
447
|
+
* The model's plan for the current request, in creation order, updated in
|
|
448
|
+
* place on every status change. Cleared when the next request begins —
|
|
449
|
+
* a plan is scoped to the request it was made for, and one that outlived
|
|
450
|
+
* it would read as work still pending on a turn that already ended.
|
|
451
|
+
*/
|
|
452
|
+
const [tasks, setTasks] = useState([]);
|
|
453
|
+
/** Ids already seen this request, so an opening row is written once. */
|
|
454
|
+
const knownTaskIdsRef = useRef(new Set());
|
|
332
455
|
// Bumped to reset the <Static> transcript log (on /clear, /clear-screen and /resume).
|
|
333
456
|
const [resetKey, setResetKey] = useState(0);
|
|
334
457
|
/**
|
|
@@ -409,6 +532,8 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
409
532
|
/** Finite slash-command choice owned synchronously until apply or cancel. */
|
|
410
533
|
const [choicePicker, setChoicePickerState] = useState(null);
|
|
411
534
|
const choicePickerRef = useRef(null);
|
|
535
|
+
const [choiceQuery, setChoiceQuery] = useState('');
|
|
536
|
+
const choiceQueryRef = useRef('');
|
|
412
537
|
/**
|
|
413
538
|
* Exact chooser React has committed. The synchronous owner ref is needed to
|
|
414
539
|
* block queue/goal work immediately, but it must not accept the Return key
|
|
@@ -419,6 +544,8 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
419
544
|
const { selection: selectedChoice, selectionRef: selectedChoiceRef, setSelection: setSelectedChoice, } = useSelectionIndex(0);
|
|
420
545
|
const setChoicePicker = useCallback((next) => {
|
|
421
546
|
choicePickerRef.current = next;
|
|
547
|
+
choiceQueryRef.current = '';
|
|
548
|
+
setChoiceQuery('');
|
|
422
549
|
if (next === null)
|
|
423
550
|
choicePickerCommittedRef.current = null;
|
|
424
551
|
setChoicePickerState(next);
|
|
@@ -592,14 +719,29 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
592
719
|
*/
|
|
593
720
|
const conversationMaterializedRef = useRef(false);
|
|
594
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]);
|
|
595
737
|
const exitWithSummary = useCallback(() => {
|
|
596
738
|
onExitSummary?.({
|
|
597
739
|
...(conversationMaterializedRef.current && scopeRef.current?.sessionId
|
|
598
740
|
? { conversationId: scopeRef.current.sessionId }
|
|
599
741
|
: {}),
|
|
600
742
|
});
|
|
601
|
-
|
|
602
|
-
}, [
|
|
743
|
+
closeAndExit();
|
|
744
|
+
}, [closeAndExit, onExitSummary]);
|
|
603
745
|
/** Durable active state is not permission to spend turns after a restart. */
|
|
604
746
|
const [goalActivation] = useState(() => new SessionGoalActivation());
|
|
605
747
|
const goalDriveInFlightRef = useRef(false);
|
|
@@ -988,7 +1130,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
988
1130
|
archived = true;
|
|
989
1131
|
goalActivation.clear();
|
|
990
1132
|
onExitSummary?.({});
|
|
991
|
-
|
|
1133
|
+
closeAndExit();
|
|
992
1134
|
}
|
|
993
1135
|
catch (error) {
|
|
994
1136
|
pushMessage('system', `Could not archive this conversation: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -1027,6 +1169,20 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1027
1169
|
}
|
|
1028
1170
|
return { sessions, sessionId };
|
|
1029
1171
|
}, [hasUnsettledTurn, pushMessage, state]);
|
|
1172
|
+
/**
|
|
1173
|
+
* Shift+Tab: `prompt` → `accept-edits` → `plan` → `prompt`. Three stops
|
|
1174
|
+
* only — `auto` and `strict` are deliberate choices made by name in
|
|
1175
|
+
* `/permissions`, not stops on a key an operator presses on reflex. From
|
|
1176
|
+
* either of those the key returns to `prompt`, which is the direction a
|
|
1177
|
+
* reflex should fall. The change goes through the same gate `/permissions`
|
|
1178
|
+
* uses, so it is refused while a turn is active, and the refusal is
|
|
1179
|
+
* explained on screen.
|
|
1180
|
+
*/
|
|
1181
|
+
const cyclePermissionMode = useCallback(() => {
|
|
1182
|
+
const current = permissionModeRef.current;
|
|
1183
|
+
const next = current === 'prompt' ? 'accept-edits' : current === 'accept-edits' ? 'plan' : 'prompt';
|
|
1184
|
+
applyPermissionMode(next);
|
|
1185
|
+
}, [applyPermissionMode]);
|
|
1030
1186
|
const runConversationExport = useCallback((destination) => {
|
|
1031
1187
|
const source = stableExportSource();
|
|
1032
1188
|
if (!source)
|
|
@@ -1083,6 +1239,8 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1083
1239
|
const value = picker.values[index];
|
|
1084
1240
|
if (index < 0 || index >= picker.values.length)
|
|
1085
1241
|
return;
|
|
1242
|
+
if (picker.options[index]?.disabledReason)
|
|
1243
|
+
return;
|
|
1086
1244
|
if (picker.kind === 'review-preset') {
|
|
1087
1245
|
if (reviewChoiceInFlightRef.current)
|
|
1088
1246
|
return;
|
|
@@ -1113,6 +1271,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1113
1271
|
setSelectedChoice(0);
|
|
1114
1272
|
setChoicePicker({
|
|
1115
1273
|
kind: 'review-branch',
|
|
1274
|
+
back: picker,
|
|
1116
1275
|
title: 'Select a base branch',
|
|
1117
1276
|
notice: `Current branch: ${listing.current}`,
|
|
1118
1277
|
values: listing.branches,
|
|
@@ -1137,6 +1296,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1137
1296
|
setSelectedChoice(0);
|
|
1138
1297
|
setChoicePicker({
|
|
1139
1298
|
kind: 'review-commit',
|
|
1299
|
+
back: picker,
|
|
1140
1300
|
title: 'Select a commit to review',
|
|
1141
1301
|
values: commits,
|
|
1142
1302
|
options: commits.map((commit) => ({
|
|
@@ -1209,6 +1369,39 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1209
1369
|
})();
|
|
1210
1370
|
return;
|
|
1211
1371
|
}
|
|
1372
|
+
if (picker.kind === 'user-question') {
|
|
1373
|
+
const pending = questionRef.current;
|
|
1374
|
+
setChoicePicker(null);
|
|
1375
|
+
if (!pending)
|
|
1376
|
+
return;
|
|
1377
|
+
if (value === FREE_TEXT_ANSWER) {
|
|
1378
|
+
// The scope on a text prompt is what its other kinds check
|
|
1379
|
+
// against a conversation switch; a question belongs to the turn
|
|
1380
|
+
// that asked it, and the turn holds the promise.
|
|
1381
|
+
const scope = scopeRef.current;
|
|
1382
|
+
if (!scope) {
|
|
1383
|
+
resolveQuestion({ kind: 'skip' });
|
|
1384
|
+
return;
|
|
1385
|
+
}
|
|
1386
|
+
textPromptTokenRef.current += 1;
|
|
1387
|
+
setTextPrompt({
|
|
1388
|
+
token: textPromptTokenRef.current,
|
|
1389
|
+
kind: 'user-question',
|
|
1390
|
+
title: pending.question.question,
|
|
1391
|
+
placeholder: 'Type your answer and press Enter · Esc skips',
|
|
1392
|
+
emptyNotice: 'An empty answer is a skip. Press Esc to skip.',
|
|
1393
|
+
initialValue: '',
|
|
1394
|
+
sessionId: scope.sessionId,
|
|
1395
|
+
});
|
|
1396
|
+
return;
|
|
1397
|
+
}
|
|
1398
|
+
// The tool's own result row carries the answer the model saw; this
|
|
1399
|
+
// row is the operator's, so a reader of the transcript sees the
|
|
1400
|
+
// question and the choice together where it happened.
|
|
1401
|
+
pushMessage('system', `Answered "${pending.question.question}": ${labelOfOption(pending.question, value)}`, false, '?');
|
|
1402
|
+
resolveQuestion({ kind: 'answer', selectedOptionIds: [value] });
|
|
1403
|
+
return;
|
|
1404
|
+
}
|
|
1212
1405
|
if (picker.kind === 'review-commit') {
|
|
1213
1406
|
advanceQueueContinuation();
|
|
1214
1407
|
enqueueQueued({
|
|
@@ -1226,6 +1419,9 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1226
1419
|
return;
|
|
1227
1420
|
}
|
|
1228
1421
|
commandPickerSubmitRef.current(`/${command.name}`);
|
|
1422
|
+
const child = choicePickerRef.current;
|
|
1423
|
+
if (child)
|
|
1424
|
+
setChoicePicker({ ...child, back: picker });
|
|
1229
1425
|
return;
|
|
1230
1426
|
}
|
|
1231
1427
|
if (picker.kind === 'archive-conversation') {
|
|
@@ -1348,12 +1544,35 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1348
1544
|
* if every exit releases it; this is the function every exit calls.
|
|
1349
1545
|
*/
|
|
1350
1546
|
const flushStream = useCallback((st) => {
|
|
1547
|
+
if (st.releaseTimer) {
|
|
1548
|
+
clearTimeout(st.releaseTimer);
|
|
1549
|
+
st.releaseTimer = undefined;
|
|
1550
|
+
}
|
|
1351
1551
|
if (!st.pending || st.pending.length === 0)
|
|
1352
1552
|
return;
|
|
1353
1553
|
const id = st.assistantId ?? pushMessage('assistant', '', true);
|
|
1354
1554
|
st.assistantId = id;
|
|
1355
1555
|
appendToMessage(id, st.pending);
|
|
1356
1556
|
st.pending = '';
|
|
1557
|
+
st.releasedAt = Date.now();
|
|
1558
|
+
}, [appendToMessage, pushMessage]);
|
|
1559
|
+
/**
|
|
1560
|
+
* Release held text to its last safe cut — a sentence or a line — when it
|
|
1561
|
+
* has waited longer than `STREAM_RELEASE_MS`. Called from the delta path
|
|
1562
|
+
* with the clock, and from the timer the delta path arms so a stream that
|
|
1563
|
+
* goes quiet mid-paragraph still shows what it has.
|
|
1564
|
+
*/
|
|
1565
|
+
const releaseStalled = useCallback((st) => {
|
|
1566
|
+
if (!st.pending || st.pending.length === 0)
|
|
1567
|
+
return;
|
|
1568
|
+
const { ready, rest } = splitSafeCut(st.pending);
|
|
1569
|
+
if (ready.length === 0)
|
|
1570
|
+
return;
|
|
1571
|
+
const id = st.assistantId ?? pushMessage('assistant', '', true);
|
|
1572
|
+
st.assistantId = id;
|
|
1573
|
+
st.pending = rest;
|
|
1574
|
+
appendToMessage(id, ready);
|
|
1575
|
+
st.releasedAt = Date.now();
|
|
1357
1576
|
}, [appendToMessage, pushMessage]);
|
|
1358
1577
|
const finalizeMessage = useCallback((id, finalContent) => {
|
|
1359
1578
|
setMessages((prev) => prev.map((m) => m.id === id ? { ...m, content: finalContent ?? m.content, pending: false } : m));
|
|
@@ -1384,6 +1603,9 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1384
1603
|
else {
|
|
1385
1604
|
// Provider/tool startup needs stable project/tenant/topic ids, not a
|
|
1386
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.
|
|
1387
1609
|
sessionId = generateSessionId();
|
|
1388
1610
|
}
|
|
1389
1611
|
sessionsRef.current = sessions;
|
|
@@ -1411,11 +1633,15 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1411
1633
|
return undefined;
|
|
1412
1634
|
conversationMutationRef.current = 'materialize';
|
|
1413
1635
|
try {
|
|
1414
|
-
const
|
|
1636
|
+
const written = await startConversation(sessions, scope.sessionId);
|
|
1415
1637
|
if (scopeRef.current !== scope || conversationMaterializedRef.current) {
|
|
1416
1638
|
throw new Error('the active conversation changed while its first session was published');
|
|
1417
1639
|
}
|
|
1418
|
-
|
|
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;
|
|
1419
1645
|
conversationMaterializedRef.current = true;
|
|
1420
1646
|
return scope;
|
|
1421
1647
|
}
|
|
@@ -1434,7 +1660,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1434
1660
|
materializationRef.current = null;
|
|
1435
1661
|
}
|
|
1436
1662
|
}, [ensureSessions]);
|
|
1437
|
-
const hydrateSession = useCallback(async (prefs, detectedNow, signal) => {
|
|
1663
|
+
const hydrateSession = useCallback(async (prefs, detectedNow, signal, persistSelection = false) => {
|
|
1438
1664
|
if (signal?.aborted)
|
|
1439
1665
|
return;
|
|
1440
1666
|
const scope = await ensureSessions();
|
|
@@ -1444,15 +1670,22 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1444
1670
|
const s = await createAgentSession(prefs, detectedNow, {
|
|
1445
1671
|
scope,
|
|
1446
1672
|
cwd: activeCtx.cwd,
|
|
1447
|
-
...(
|
|
1448
|
-
? {
|
|
1673
|
+
...(activeCtx.additionalDirectories
|
|
1674
|
+
? { additionalDirectories: activeCtx.additionalDirectories }
|
|
1449
1675
|
: {}),
|
|
1676
|
+
...(sessionsRef.current ? { stateRoot: sessionsRef.current.root } : {}),
|
|
1450
1677
|
enableComputerUse: true,
|
|
1451
1678
|
rules: activeCtx.rules,
|
|
1452
1679
|
...(sessionsRef.current ? { sessionGoals: sessionsRef.current.goals } : {}),
|
|
1453
1680
|
...(activeCtx.mcpServers ? { mcpServers: activeCtx.mcpServers } : {}),
|
|
1454
1681
|
...(activeCtx.plugins ? { plugins: activeCtx.plugins } : {}),
|
|
1682
|
+
...(activeCtx.web ? { web: activeCtx.web } : {}),
|
|
1683
|
+
...(activeCtx.hooks ? { hooks: activeCtx.hooks } : {}),
|
|
1684
|
+
...(activeCtx.compaction ? { compaction: activeCtx.compaction } : {}),
|
|
1455
1685
|
...(activeCtx.sandbox ? { sandbox: activeCtx.sandbox } : {}),
|
|
1686
|
+
// Somebody is at this terminal, so the model may ask them one
|
|
1687
|
+
// question when a decision is genuinely theirs.
|
|
1688
|
+
askUser: true,
|
|
1456
1689
|
});
|
|
1457
1690
|
if (signal?.aborted) {
|
|
1458
1691
|
void s.close();
|
|
@@ -1469,6 +1702,24 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1469
1702
|
}
|
|
1470
1703
|
throw new Error(reason);
|
|
1471
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;
|
|
1472
1723
|
// A picker-owned provider/model change is one state transition. Clear the
|
|
1473
1724
|
// old model's effort selection before publishing the replacement session
|
|
1474
1725
|
// or releasing any paused queue. Failed and superseded candidates returned
|
|
@@ -1481,13 +1732,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1481
1732
|
void previousSessionRef.current?.close();
|
|
1482
1733
|
previousSessionRef.current = s;
|
|
1483
1734
|
setSession(s);
|
|
1484
|
-
setUserCommands(
|
|
1485
|
-
cwd: activeCtx.cwd,
|
|
1486
|
-
// Builtins are reserved: a `help.md` must not take over `/help`.
|
|
1487
|
-
// Passing the names here is what lets the loader tell its author
|
|
1488
|
-
// the file is shadowed instead of leaving it silently unused.
|
|
1489
|
-
reserved: hostCommandNames(),
|
|
1490
|
-
}));
|
|
1735
|
+
setUserCommands(commands);
|
|
1491
1736
|
const mentionLoadOwner = {};
|
|
1492
1737
|
mentionLoadOwnerRef.current = mentionLoadOwner;
|
|
1493
1738
|
void listMentionableFiles(activeCtx.cwd, appLifetime.signal).then((files) => {
|
|
@@ -1849,6 +2094,13 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1849
2094
|
}
|
|
1850
2095
|
}, [activateTrustedProject, pushMessage, runProbe]);
|
|
1851
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;
|
|
1852
2104
|
// How much of the transcript is still redrawable. The rest belongs to native
|
|
1853
2105
|
// terminal scrollback; the live tail stays deliberately small so an activity
|
|
1854
2106
|
// tick never repaints the whole conversation.
|
|
@@ -1860,7 +2112,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1860
2112
|
messages: finalized,
|
|
1861
2113
|
rows: terminal.rows,
|
|
1862
2114
|
columns: terminal.columns,
|
|
1863
|
-
furnitureRows: LIVE_FURNITURE_ROWS,
|
|
2115
|
+
furnitureRows: LIVE_FURNITURE_ROWS + taskFurniture + toolFurniture,
|
|
1864
2116
|
settled: settledRef.current,
|
|
1865
2117
|
raw: rawOutput,
|
|
1866
2118
|
});
|
|
@@ -1879,6 +2131,17 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1879
2131
|
const hostCommands = mergeHostCommands(kernelCommandDescriptors());
|
|
1880
2132
|
const slashCtx = {
|
|
1881
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,
|
|
1882
2145
|
builtins: hostCommands,
|
|
1883
2146
|
lastAssistantMessageId: () => lastAssistantMessage.current?.messageId ?? null,
|
|
1884
2147
|
// Called when `/tools` renders, not read here — the same shape, and the
|
|
@@ -1957,6 +2220,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1957
2220
|
permissionOpenedAtRef.current = null;
|
|
1958
2221
|
setPermissionReviewOffset(0);
|
|
1959
2222
|
setPermissionDetailsOpen(false);
|
|
2223
|
+
setPermissionChoice(0);
|
|
1960
2224
|
setPermission(null);
|
|
1961
2225
|
if (resolve) {
|
|
1962
2226
|
// The provider resumes asynchronously after the decision. Leaving the
|
|
@@ -2083,6 +2347,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2083
2347
|
wakeGoalDriver();
|
|
2084
2348
|
conversationGenRef.current += 1;
|
|
2085
2349
|
resetSubagentActivity();
|
|
2350
|
+
session?.resetTaskStore?.();
|
|
2086
2351
|
activeTurnTokenRef.current = null;
|
|
2087
2352
|
resetTranscript();
|
|
2088
2353
|
setMessages(restored);
|
|
@@ -2117,6 +2382,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2117
2382
|
nextId,
|
|
2118
2383
|
pushMessage,
|
|
2119
2384
|
resetSubagentActivity,
|
|
2385
|
+
session,
|
|
2120
2386
|
resetTranscript,
|
|
2121
2387
|
wakeGoalDriver,
|
|
2122
2388
|
]);
|
|
@@ -2157,6 +2423,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2157
2423
|
wakeGoalDriver();
|
|
2158
2424
|
conversationGenRef.current += 1;
|
|
2159
2425
|
resetSubagentActivity();
|
|
2426
|
+
session?.resetTaskStore?.();
|
|
2160
2427
|
activeTurnTokenRef.current = null;
|
|
2161
2428
|
modelHistoryRef.current = [];
|
|
2162
2429
|
lastAssistantMessage.current = null;
|
|
@@ -2196,6 +2463,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2196
2463
|
interruptTurn,
|
|
2197
2464
|
pushMessage,
|
|
2198
2465
|
resetSubagentActivity,
|
|
2466
|
+
session,
|
|
2199
2467
|
resetTranscript,
|
|
2200
2468
|
wakeGoalDriver,
|
|
2201
2469
|
]);
|
|
@@ -2243,11 +2511,27 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2243
2511
|
if (!prompt)
|
|
2244
2512
|
return;
|
|
2245
2513
|
setTextPrompt(null);
|
|
2514
|
+
if (prompt.kind === 'user-question') {
|
|
2515
|
+
const text = value.trim();
|
|
2516
|
+
const pending = questionRef.current;
|
|
2517
|
+
if (text.length === 0) {
|
|
2518
|
+
resolveQuestion({ kind: 'skip' });
|
|
2519
|
+
return;
|
|
2520
|
+
}
|
|
2521
|
+
if (pending) {
|
|
2522
|
+
pushMessage('system', `Answered "${pending.question.question}": ${text}`, false, '?');
|
|
2523
|
+
}
|
|
2524
|
+
resolveQuestion({ kind: 'answer', selectedOptionIds: [], freeText: text });
|
|
2525
|
+
return;
|
|
2526
|
+
}
|
|
2246
2527
|
const scope = scopeRef.current;
|
|
2247
2528
|
if (!scope || scope.sessionId !== prompt.sessionId) {
|
|
2248
|
-
pushMessage('system',
|
|
2249
|
-
|
|
2250
|
-
|
|
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()}`);
|
|
2251
2535
|
return;
|
|
2252
2536
|
}
|
|
2253
2537
|
if (prompt.kind === 'export-file') {
|
|
@@ -2275,8 +2559,11 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2275
2559
|
queueMicrotask(() => {
|
|
2276
2560
|
if (textPromptRef.current === prompt)
|
|
2277
2561
|
setTextPrompt(null);
|
|
2562
|
+
// A cancelled free-text answer is a skipped question, not a hung tool.
|
|
2563
|
+
if (prompt?.kind === 'user-question')
|
|
2564
|
+
resolveQuestion({ kind: 'skip' });
|
|
2278
2565
|
});
|
|
2279
|
-
}, [setTextPrompt]);
|
|
2566
|
+
}, [resolveQuestion, setTextPrompt]);
|
|
2280
2567
|
/**
|
|
2281
2568
|
* `/fork`: continue in a copy, leaving this conversation where it is.
|
|
2282
2569
|
*
|
|
@@ -2321,6 +2608,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2321
2608
|
// is reloaded or reset. Only where the NEXT turn is written changes.
|
|
2322
2609
|
scope.sessionId = forked.id;
|
|
2323
2610
|
resetSubagentActivity();
|
|
2611
|
+
session?.resetTaskStore?.();
|
|
2324
2612
|
goalActivation.clear();
|
|
2325
2613
|
wakeGoalDriver();
|
|
2326
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.`);
|
|
@@ -2338,6 +2626,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2338
2626
|
materializeConversation,
|
|
2339
2627
|
pushMessage,
|
|
2340
2628
|
resetSubagentActivity,
|
|
2629
|
+
session,
|
|
2341
2630
|
wakeGoalDriver,
|
|
2342
2631
|
]);
|
|
2343
2632
|
/** Open the durable prompt picker after the composer's second empty Esc. */
|
|
@@ -2362,6 +2651,33 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2362
2651
|
editCommittedRef.current = false;
|
|
2363
2652
|
setPhase('edit');
|
|
2364
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]);
|
|
2365
2681
|
/** Fork before the selected prompt, then reopen that prompt in the composer. */
|
|
2366
2682
|
const confirmPromptEdit = useCallback(async (target) => {
|
|
2367
2683
|
if (editCommittedRef.current || conversationMutationRef.current)
|
|
@@ -2396,6 +2712,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2396
2712
|
.map((prompt) => prompt.displayText);
|
|
2397
2713
|
conversationGenRef.current += 1;
|
|
2398
2714
|
resetSubagentActivity();
|
|
2715
|
+
session?.resetTaskStore?.();
|
|
2399
2716
|
activeTurnTokenRef.current = null;
|
|
2400
2717
|
goalActivation.clear();
|
|
2401
2718
|
wakeGoalDriver();
|
|
@@ -2437,6 +2754,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2437
2754
|
nextId,
|
|
2438
2755
|
pushMessage,
|
|
2439
2756
|
resetSubagentActivity,
|
|
2757
|
+
session,
|
|
2440
2758
|
resetTranscript,
|
|
2441
2759
|
wakeGoalDriver,
|
|
2442
2760
|
]);
|
|
@@ -2461,6 +2779,39 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2461
2779
|
sendTerminalNotification({ kind: 'approval-required' });
|
|
2462
2780
|
});
|
|
2463
2781
|
}, [sendTerminalNotification, setPermissionDetailsOpen, setPermissionReviewOffset]);
|
|
2782
|
+
/**
|
|
2783
|
+
* `ask_user_question`, on screen. The question becomes a finite chooser —
|
|
2784
|
+
* the model's options, then "Something else…" when it allowed free text —
|
|
2785
|
+
* and the tool's promise waits on the row the operator picks. One question
|
|
2786
|
+
* at a time: the tool is not concurrency-safe, so a second cannot arrive
|
|
2787
|
+
* while the first is up.
|
|
2788
|
+
*/
|
|
2789
|
+
const askQuestion = useCallback((question) => new Promise((resolve) => {
|
|
2790
|
+
questionRef.current = { question, resolve };
|
|
2791
|
+
const values = [
|
|
2792
|
+
...question.options.map((option) => option.id),
|
|
2793
|
+
...(question.allowFreeText ? [FREE_TEXT_ANSWER] : []),
|
|
2794
|
+
];
|
|
2795
|
+
setSelectedChoice(0);
|
|
2796
|
+
setChoicePicker({
|
|
2797
|
+
kind: 'user-question',
|
|
2798
|
+
title: question.question,
|
|
2799
|
+
notice: question.header
|
|
2800
|
+
? `${question.header} · Enter answers · Esc skips`
|
|
2801
|
+
: 'Enter answers · Esc skips',
|
|
2802
|
+
values,
|
|
2803
|
+
options: [
|
|
2804
|
+
...question.options.map((option) => ({
|
|
2805
|
+
label: option.label,
|
|
2806
|
+
description: option.description ?? '',
|
|
2807
|
+
})),
|
|
2808
|
+
...(question.allowFreeText
|
|
2809
|
+
? [{ label: 'Something else…', description: 'Answer in your own words' }]
|
|
2810
|
+
: []),
|
|
2811
|
+
],
|
|
2812
|
+
});
|
|
2813
|
+
sendTerminalNotification({ kind: 'approval-required' });
|
|
2814
|
+
}), [sendTerminalNotification, setChoicePicker, setSelectedChoice]);
|
|
2464
2815
|
// Render one agent event onto the transcript. Shared by the local turn loop
|
|
2465
2816
|
// and the daemon-attach poller, so both paths produce identical output.
|
|
2466
2817
|
// `st` carries the streaming-assistant bubble id + accumulated text across
|
|
@@ -2481,6 +2832,20 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2481
2832
|
st.assistantId = null;
|
|
2482
2833
|
}
|
|
2483
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
|
+
}
|
|
2484
2849
|
switch (event.kind) {
|
|
2485
2850
|
case 'delta': {
|
|
2486
2851
|
setState('thinking');
|
|
@@ -2491,6 +2856,11 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2491
2856
|
sessionId: st.sessionId,
|
|
2492
2857
|
};
|
|
2493
2858
|
}
|
|
2859
|
+
// The release clock starts with the first character, not with the
|
|
2860
|
+
// turn: the wait before the first token is the model's, and
|
|
2861
|
+
// counting it would release the opening sentence on arrival.
|
|
2862
|
+
st.releasedAt ??= Date.now();
|
|
2863
|
+
setThinking(null);
|
|
2494
2864
|
st.text += event.text;
|
|
2495
2865
|
// Held, not appended. Appending each delta is what produced text
|
|
2496
2866
|
// that types itself out — nothing animates it, but a few
|
|
@@ -2501,11 +2871,26 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2501
2871
|
if (ready.length > 0) {
|
|
2502
2872
|
st.pending = rest;
|
|
2503
2873
|
appendToMessage(ensureAssistant(), ready);
|
|
2874
|
+
st.releasedAt = Date.now();
|
|
2875
|
+
}
|
|
2876
|
+
// A block is the preferred unit; a sentence is the fallback for
|
|
2877
|
+
// a paragraph that is taking longer than an operator will wait
|
|
2878
|
+
// for it. The timer covers the stream going quiet between two
|
|
2879
|
+
// deltas, which the clock check alone cannot see.
|
|
2880
|
+
if (st.releaseTimer)
|
|
2881
|
+
clearTimeout(st.releaseTimer);
|
|
2882
|
+
if (Date.now() - (st.releasedAt ?? 0) >= STREAM_RELEASE_MS) {
|
|
2883
|
+
releaseStalled(st);
|
|
2504
2884
|
}
|
|
2885
|
+
st.releaseTimer = setTimeout(() => {
|
|
2886
|
+
st.releaseTimer = undefined;
|
|
2887
|
+
releaseStalled(st);
|
|
2888
|
+
}, STREAM_RELEASE_MS);
|
|
2505
2889
|
break;
|
|
2506
2890
|
}
|
|
2507
2891
|
case 'tool-start': {
|
|
2508
2892
|
closeAssistant();
|
|
2893
|
+
setThinking(null);
|
|
2509
2894
|
setState('tool');
|
|
2510
2895
|
const tool = {
|
|
2511
2896
|
id: event.toolUseId,
|
|
@@ -2580,10 +2965,68 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2580
2965
|
break;
|
|
2581
2966
|
}
|
|
2582
2967
|
case 'usage':
|
|
2583
|
-
|
|
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
|
+
});
|
|
2584
2984
|
break;
|
|
2585
|
-
case '
|
|
2586
|
-
|
|
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
|
+
});
|
|
2996
|
+
break;
|
|
2997
|
+
}
|
|
2998
|
+
case 'task': {
|
|
2999
|
+
// The live list gets every change; the transcript records the
|
|
3000
|
+
// opening and the close, as it did before the list existed.
|
|
3001
|
+
// Decided from a ref, not inside the state updater: React runs
|
|
3002
|
+
// updaters lazily, so a flag set there is still unset when the
|
|
3003
|
+
// transcript row below is chosen.
|
|
3004
|
+
const isNew = !knownTaskIdsRef.current.has(event.taskId);
|
|
3005
|
+
knownTaskIdsRef.current.add(event.taskId);
|
|
3006
|
+
const item = {
|
|
3007
|
+
id: event.taskId,
|
|
3008
|
+
subject: event.subject,
|
|
3009
|
+
status: event.status,
|
|
3010
|
+
};
|
|
3011
|
+
setTasks((prev) => {
|
|
3012
|
+
const index = prev.findIndex((task) => task.id === item.id);
|
|
3013
|
+
return index < 0 ? [...prev, item] : prev.map((task, i) => (i === index ? item : task));
|
|
3014
|
+
});
|
|
3015
|
+
if (event.status === 'completed') {
|
|
3016
|
+
pushMessage('tool', event.subject, false, '☑');
|
|
3017
|
+
}
|
|
3018
|
+
else if (event.status === 'failed') {
|
|
3019
|
+
pushMessage('tool', event.subject, false, '☒');
|
|
3020
|
+
}
|
|
3021
|
+
else if (isNew) {
|
|
3022
|
+
pushMessage('tool', event.subject, false, '☐');
|
|
3023
|
+
}
|
|
3024
|
+
break;
|
|
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, '⚙');
|
|
2587
3030
|
break;
|
|
2588
3031
|
case 'context':
|
|
2589
3032
|
// Into the TRANSCRIPT, not a status line. A status indicator is
|
|
@@ -2592,6 +3035,15 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2592
3035
|
// looking at was compacted. The event is a fact about the
|
|
2593
3036
|
// conversation and belongs in its record.
|
|
2594
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
|
+
}
|
|
2595
3047
|
break;
|
|
2596
3048
|
case 'provider-fallback':
|
|
2597
3049
|
// The transcript, for the reason compaction is in the transcript
|
|
@@ -2721,6 +3173,12 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2721
3173
|
else {
|
|
2722
3174
|
pushMessage('user', text, false, undefined, undefined, undefined, humanPromptMeta(attached.length, attachments));
|
|
2723
3175
|
}
|
|
3176
|
+
// A plan is scoped to the request it was made for. Cleared here, at
|
|
3177
|
+
// the start of the next one, rather than when the previous turn
|
|
3178
|
+
// ended: a finished list stays on screen until the operator moves on,
|
|
3179
|
+
// which is the moment it has told them everything it can.
|
|
3180
|
+
setTasks([]);
|
|
3181
|
+
knownTaskIdsRef.current = new Set();
|
|
2724
3182
|
// The model interleaves text → tool → text across iterations; `applyEvent`
|
|
2725
3183
|
// renders each one in order.
|
|
2726
3184
|
const st = {
|
|
@@ -2870,8 +3328,11 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2870
3328
|
...(goalRound ? { goalRound } : {}),
|
|
2871
3329
|
// The mode above decides whether this callback is consulted.
|
|
2872
3330
|
onPermission: askPermission,
|
|
3331
|
+
onQuestion: askQuestion,
|
|
2873
3332
|
inboundMessages: () => inbox.drain(),
|
|
2874
|
-
extraSystem: composeSkillsPrompt(activeSkills)
|
|
3333
|
+
extraSystem: [composeSkillsPrompt(activeSkills), drainIdleJobNotices(), drainOperatorShell()]
|
|
3334
|
+
.filter((part) => Boolean(part))
|
|
3335
|
+
.join('\n\n') || undefined,
|
|
2875
3336
|
onConversationMessages: (messages) => {
|
|
2876
3337
|
// State-only: opaque reasoning/signatures must reach the next
|
|
2877
3338
|
// provider and durable store without becoming transcript text.
|
|
@@ -3089,6 +3550,41 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3089
3550
|
return;
|
|
3090
3551
|
}
|
|
3091
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
|
+
}
|
|
3092
3588
|
// What actually gets sent. A `prompt` action replaces it with text the
|
|
3093
3589
|
// command composed, and then takes the ordinary send path below —
|
|
3094
3590
|
// including the queue — so a command-driven turn is not a second way
|
|
@@ -3116,6 +3612,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3116
3612
|
description: command.problem
|
|
3117
3613
|
? `Unavailable: ${command.problem}`
|
|
3118
3614
|
: command.description,
|
|
3615
|
+
disabledReason: command.problem,
|
|
3119
3616
|
})),
|
|
3120
3617
|
windowSize: suggestionWindowSize(terminal.rows),
|
|
3121
3618
|
});
|
|
@@ -3123,8 +3620,154 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3123
3620
|
}
|
|
3124
3621
|
case 'agent-cockpit': {
|
|
3125
3622
|
if (!openAgentCockpit()) {
|
|
3126
|
-
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;
|
|
3127
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
|
+
})();
|
|
3128
3771
|
return;
|
|
3129
3772
|
}
|
|
3130
3773
|
case 'clear-screen':
|
|
@@ -3196,7 +3839,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3196
3839
|
setKeyEntryFor(null);
|
|
3197
3840
|
setPickerDetected(null);
|
|
3198
3841
|
setPickerSelectionKind('provider-and-model');
|
|
3199
|
-
setPickerInitialView('
|
|
3842
|
+
setPickerInitialView('models');
|
|
3200
3843
|
setPhase('picker');
|
|
3201
3844
|
return;
|
|
3202
3845
|
case 'permission-mode': {
|
|
@@ -3218,7 +3861,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3218
3861
|
pushMessage('system', 'Permission choices are unavailable until the active turn, prompt, compaction, queued work, or embedded-session approval latch can be safely settled.');
|
|
3219
3862
|
return;
|
|
3220
3863
|
}
|
|
3221
|
-
const values = ['prompt', 'auto', 'strict'];
|
|
3864
|
+
const values = ['prompt', 'accept-edits', 'auto', 'strict', 'plan'];
|
|
3222
3865
|
setSelectedChoice(Math.max(0, values.indexOf(permissionModeRef.current)));
|
|
3223
3866
|
setChoicePicker({
|
|
3224
3867
|
kind: 'permission-mode',
|
|
@@ -3263,6 +3906,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3263
3906
|
label: effort ?? 'default',
|
|
3264
3907
|
description: reasoningEffortDescription(effort),
|
|
3265
3908
|
current: effort === current,
|
|
3909
|
+
default: effort === undefined,
|
|
3266
3910
|
})),
|
|
3267
3911
|
});
|
|
3268
3912
|
return;
|
|
@@ -3304,16 +3948,17 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3304
3948
|
}
|
|
3305
3949
|
case 'remember':
|
|
3306
3950
|
try {
|
|
3307
|
-
appendMemory(slash.text);
|
|
3308
|
-
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}`);
|
|
3309
3953
|
}
|
|
3310
3954
|
catch (err) {
|
|
3311
3955
|
pushMessage('system', `Could not save memory: ${err instanceof Error ? err.message : String(err)}`);
|
|
3312
3956
|
}
|
|
3313
3957
|
return;
|
|
3314
3958
|
case 'show-memory': {
|
|
3315
|
-
const mem = composeMemoryPrompt(readMemory());
|
|
3316
|
-
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).');
|
|
3317
3962
|
return;
|
|
3318
3963
|
}
|
|
3319
3964
|
case 'list-skills': {
|
|
@@ -3372,79 +4017,19 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3372
4017
|
case 'fork':
|
|
3373
4018
|
void doFork();
|
|
3374
4019
|
return;
|
|
3375
|
-
case '
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
// shapes with a hand-written copy between them — the geometry
|
|
3389
|
-
// `docs/conventions/one-site-is-not-every-site.md` names as the
|
|
3390
|
-
// hardest kind to spot, adopted here to solve a staleness problem
|
|
3391
|
-
// the updater solves without it.
|
|
3392
|
-
//
|
|
3393
|
-
// The id is taken outside so both branches use the same one.
|
|
3394
|
-
const id = nextId();
|
|
3395
|
-
setMessages((prev) => {
|
|
3396
|
-
const blocks = prev.filter((m) => m.detailRef !== undefined);
|
|
3397
|
-
const say = (content) => ({
|
|
3398
|
-
id,
|
|
3399
|
-
role: 'system',
|
|
3400
|
-
content,
|
|
3401
|
-
});
|
|
3402
|
-
if (blocks.length === 0) {
|
|
3403
|
-
return [
|
|
3404
|
-
...prev,
|
|
3405
|
-
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.'),
|
|
3406
|
-
];
|
|
3407
|
-
}
|
|
3408
|
-
const block = slash.which === 'last'
|
|
3409
|
-
? blocks[blocks.length - 1]
|
|
3410
|
-
: blocks.find((m) => m.detailRef === slash.which);
|
|
3411
|
-
if (!block) {
|
|
3412
|
-
// Names the range rather than only refusing: the operator's
|
|
3413
|
-
// next move is to type a different number, and they should
|
|
3414
|
-
// not have to guess which ones exist.
|
|
3415
|
-
return [
|
|
3416
|
-
...prev,
|
|
3417
|
-
say(`No collapsed output numbered ${slash.which}. This conversation has ${blocks.length === 1
|
|
3418
|
-
? 'one, numbered 1'
|
|
3419
|
-
: `${blocks.length}, numbered 1 to ${blocks.length}`}.`),
|
|
3420
|
-
];
|
|
3421
|
-
}
|
|
3422
|
-
// A NEW row carrying the same lines, flagged to print them all.
|
|
3423
|
-
// Not a mutation of the row that collapsed them: that row was
|
|
3424
|
-
// handed to <Static>, which emits an item once and calls the
|
|
3425
|
-
// render function only for items it has not emitted yet, so
|
|
3426
|
-
// nothing decided now can reach it. Appending is the only
|
|
3427
|
-
// expansion this architecture has.
|
|
3428
|
-
//
|
|
3429
|
-
// The lines travel as `detail` rather than flattened into the
|
|
3430
|
-
// content so the `▏` rule and the +/- diff colouring survive.
|
|
3431
|
-
// An expansion that showed LESS than the collapsed form would
|
|
3432
|
-
// be a strange thing to have asked for.
|
|
3433
|
-
//
|
|
3434
|
-
// It carries no `detailRef` of its own: it is already in full,
|
|
3435
|
-
// so there is nothing for a number to offer, and giving it one
|
|
3436
|
-
// would let `/expand` produce a third copy of the same output.
|
|
3437
|
-
return [
|
|
3438
|
-
...prev,
|
|
3439
|
-
{
|
|
3440
|
-
id,
|
|
3441
|
-
role: 'system',
|
|
3442
|
-
content: `${block.content} — in full (${block.detail?.length ?? 0} lines)`,
|
|
3443
|
-
glyph: '⤢',
|
|
3444
|
-
detail: block.detail,
|
|
3445
|
-
detailExpanded: true,
|
|
3446
|
-
},
|
|
3447
|
-
];
|
|
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'}`);
|
|
3448
4033
|
});
|
|
3449
4034
|
return;
|
|
3450
4035
|
}
|
|
@@ -3479,9 +4064,17 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3479
4064
|
await materializeConversation();
|
|
3480
4065
|
const durableSessions = sessionsRef.current;
|
|
3481
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
|
+
}
|
|
3482
4074
|
const registry = new HostCommandRegistry();
|
|
3483
4075
|
registry.register(kernelHostCommands({
|
|
3484
4076
|
allowedAgentIds: session?.agentIds ?? [],
|
|
4077
|
+
...(taskStore ? { taskStore } : {}),
|
|
3485
4078
|
...(durableSessions && runScope && conversationMaterializedRef.current
|
|
3486
4079
|
? {
|
|
3487
4080
|
goal: {
|
|
@@ -3781,11 +4374,15 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3781
4374
|
advanceQueueContinuation,
|
|
3782
4375
|
applyPermissionMode,
|
|
3783
4376
|
applyReasoningEffort,
|
|
4377
|
+
appLifetime,
|
|
3784
4378
|
ctx.cwd,
|
|
3785
4379
|
doResume,
|
|
3786
4380
|
enqueueQueued,
|
|
3787
4381
|
exitWithSummary,
|
|
3788
4382
|
hasUnsettledTurn,
|
|
4383
|
+
goalActivation,
|
|
4384
|
+
goalStatus,
|
|
4385
|
+
hostCommands,
|
|
3789
4386
|
nextId,
|
|
3790
4387
|
openAgentCockpit,
|
|
3791
4388
|
pushMessage,
|
|
@@ -3793,6 +4390,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3793
4390
|
removeStoredCredential,
|
|
3794
4391
|
resetTranscript,
|
|
3795
4392
|
runConversationExport,
|
|
4393
|
+
session,
|
|
3796
4394
|
setChoicePicker,
|
|
3797
4395
|
setCopyPicker,
|
|
3798
4396
|
setReasoningEffort,
|
|
@@ -4001,9 +4599,9 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4001
4599
|
pushMessage('system', `⬆ Update available:\n${lines.join('\n')}`);
|
|
4002
4600
|
});
|
|
4003
4601
|
}, [phase, ctx.version, pushMessage]);
|
|
4004
|
-
const hydrateFromPicker = useCallback(async (prefs, detectedNow, signal,
|
|
4602
|
+
const hydrateFromPicker = useCallback(async (prefs, detectedNow, signal, options = {}) => {
|
|
4005
4603
|
try {
|
|
4006
|
-
await hydrateSession(prefs, detectedNow, signal);
|
|
4604
|
+
await hydrateSession(prefs, detectedNow, signal, options.persistSelection);
|
|
4007
4605
|
}
|
|
4008
4606
|
catch (err) {
|
|
4009
4607
|
// A superseded choice no longer owns even its failure message. Its
|
|
@@ -4011,7 +4609,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4011
4609
|
// choice stays on the picker with the actionable construction error.
|
|
4012
4610
|
if (signal.aborted)
|
|
4013
4611
|
return;
|
|
4014
|
-
if (revealAllOnFailure) {
|
|
4612
|
+
if (options.revealAllOnFailure) {
|
|
4015
4613
|
setPickerDetected(null);
|
|
4016
4614
|
setPickerSelectionKind('provider-and-model');
|
|
4017
4615
|
setPickerInitialView('providers');
|
|
@@ -4036,48 +4634,29 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4036
4634
|
setPickerNotice(null);
|
|
4037
4635
|
// The disposition, not the key. `credential` never reaches a message.
|
|
4038
4636
|
pushMessage('system', disposition);
|
|
4039
|
-
//
|
|
4040
|
-
//
|
|
4041
|
-
// from the id alone would have been a quiet demotion in the one case
|
|
4042
|
-
// this routing creates: an operator whose file pins a model, and whose
|
|
4043
|
-
// only problem was a missing secret, would have been moved onto the
|
|
4044
|
-
// registry default for supplying it.
|
|
4637
|
+
// Supplying a credential changes the primary's access, not unrelated
|
|
4638
|
+
// fallbacks, model pins, delegation or capability preferences.
|
|
4045
4639
|
const saved = savedPrefsRef.current;
|
|
4046
4640
|
const prefs = saved && primaryProvider(saved).id === credential.entry.id
|
|
4047
4641
|
? saved
|
|
4048
|
-
: {
|
|
4049
|
-
version: 3,
|
|
4050
|
-
providers: [{ id: credential.entry.id }],
|
|
4051
|
-
subagents: { active: [] },
|
|
4052
|
-
};
|
|
4642
|
+
: selectPrimaryProvider(saved, { id: credential.entry.id });
|
|
4053
4643
|
void hydrateFromPicker(prefs, next, signal);
|
|
4054
4644
|
}, [detected, hydrateFromPicker, pushMessage]);
|
|
4055
4645
|
const handlePickerSubmit = useCallback((selection, signal) => {
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
providers: [
|
|
4061
|
-
{
|
|
4062
|
-
id: selection.provider,
|
|
4063
|
-
...(selection.model !== undefined ? { model: selection.model } : {}),
|
|
4064
|
-
},
|
|
4065
|
-
],
|
|
4066
|
-
subagents: { active: [] },
|
|
4067
|
-
};
|
|
4646
|
+
const prefs = selectPrimaryProvider(savedPrefsRef.current, {
|
|
4647
|
+
id: selection.provider,
|
|
4648
|
+
...(selection.model !== undefined ? { model: selection.model } : {}),
|
|
4649
|
+
});
|
|
4068
4650
|
setKeyEntryFor(null);
|
|
4069
4651
|
setPickerNotice(null);
|
|
4070
|
-
try {
|
|
4071
|
-
writePreferences(prefs);
|
|
4072
|
-
}
|
|
4073
|
-
catch (err) {
|
|
4074
|
-
pushMessage('system', `Could not save preferences: ${err instanceof Error ? err.message : String(err)}`);
|
|
4075
|
-
return;
|
|
4076
|
-
}
|
|
4077
4652
|
// Keep a narrowed signed-in roster stable while construction is pending.
|
|
4078
4653
|
// Only a real refusal broadens back to the general, sign-in-capable picker.
|
|
4079
|
-
void hydrateFromPicker(prefs, detected, signal,
|
|
4080
|
-
|
|
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]);
|
|
4081
4660
|
/**
|
|
4082
4661
|
* Leaving the picker returns to whatever was on screen before it.
|
|
4083
4662
|
*
|
|
@@ -4107,6 +4686,13 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4107
4686
|
exitWithSummary();
|
|
4108
4687
|
}, [session, exitWithSummary]);
|
|
4109
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
|
+
}
|
|
4110
4696
|
// The provider picker owns its own keyboard — arrows, digits, enter,
|
|
4111
4697
|
// esc and the typed-key screen are all its business. Ctrl+C is not: it
|
|
4112
4698
|
// is the one key that must work on every screen, and this handler used
|
|
@@ -4124,12 +4710,11 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4124
4710
|
exitWithSummary();
|
|
4125
4711
|
return;
|
|
4126
4712
|
}
|
|
4127
|
-
// Previous-prompt picker
|
|
4128
|
-
// 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.
|
|
4129
4714
|
if (phase === 'edit') {
|
|
4130
4715
|
if (editCommittedRef.current)
|
|
4131
4716
|
return;
|
|
4132
|
-
if ((key.ctrl && input === 'c') || input.toLowerCase() === 'q') {
|
|
4717
|
+
if (key.escape || (key.ctrl && input === 'c') || input.toLowerCase() === 'q') {
|
|
4133
4718
|
setEditList([]);
|
|
4134
4719
|
setPhase('ready');
|
|
4135
4720
|
return;
|
|
@@ -4138,7 +4723,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4138
4723
|
setSelectedEdit((index) => moveSelection(index, editList.length, key.home ? 'first' : key.end ? 'last' : key.pageUp ? 'previous-page' : 'next-page'));
|
|
4139
4724
|
return;
|
|
4140
4725
|
}
|
|
4141
|
-
if (key.
|
|
4726
|
+
if (key.leftArrow || key.upArrow) {
|
|
4142
4727
|
setSelectedEdit((index) => moveSelection(index, editList.length, 'previous'));
|
|
4143
4728
|
return;
|
|
4144
4729
|
}
|
|
@@ -4212,25 +4797,27 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4212
4797
|
setPermissionDetailsOpen(!permissionDetailsOpenRef.current);
|
|
4213
4798
|
return;
|
|
4214
4799
|
}
|
|
4215
|
-
if (permission &&
|
|
4216
|
-
|
|
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)) {
|
|
4217
4807
|
const source = permissionDetailsOpenRef.current
|
|
4218
4808
|
? permission.review
|
|
4219
4809
|
: permission.summary.text;
|
|
4220
4810
|
const count = permissionReviewRows(source, terminal.columns).length;
|
|
4221
|
-
const
|
|
4811
|
+
const pageRows = permissionReviewPageRows(terminal.rows);
|
|
4812
|
+
const maxOffset = Math.max(0, count - pageRows);
|
|
4222
4813
|
const current = Math.min(permissionReviewOffsetRef.current, maxOffset);
|
|
4223
4814
|
const next = key.home
|
|
4224
4815
|
? 0
|
|
4225
4816
|
: key.end
|
|
4226
4817
|
? maxOffset
|
|
4227
4818
|
: key.pageUp
|
|
4228
|
-
? Math.max(0, current -
|
|
4229
|
-
:
|
|
4230
|
-
? Math.min(maxOffset, current + PERMISSION_REVIEW_PAGE_ROWS)
|
|
4231
|
-
: key.upArrow
|
|
4232
|
-
? Math.max(0, current - 1)
|
|
4233
|
-
: Math.min(maxOffset, current + 1);
|
|
4819
|
+
? Math.max(0, current - pageRows)
|
|
4820
|
+
: Math.min(maxOffset, current + pageRows);
|
|
4234
4821
|
setPermissionReviewOffset(next);
|
|
4235
4822
|
return;
|
|
4236
4823
|
}
|
|
@@ -4245,18 +4832,21 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4245
4832
|
resolvePermission({ kind: 'reject' });
|
|
4246
4833
|
return;
|
|
4247
4834
|
}
|
|
4248
|
-
//
|
|
4249
|
-
//
|
|
4250
|
-
//
|
|
4251
|
-
//
|
|
4252
|
-
// operator never looked at.
|
|
4253
|
-
// 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.
|
|
4254
4840
|
if (!approvalIsDeliberate(permissionOpenedAtRef.current, Date.now()))
|
|
4255
4841
|
return;
|
|
4256
|
-
|
|
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)
|
|
4257
4845
|
resolvePermission({ kind: 'approve' });
|
|
4258
|
-
else if (ch === 'a')
|
|
4846
|
+
else if (ch === 'a' || chosen === 1)
|
|
4259
4847
|
resolvePermission({ kind: 'approve-all' });
|
|
4848
|
+
else if (chosen === 2)
|
|
4849
|
+
resolvePermission({ kind: 'reject' });
|
|
4260
4850
|
return;
|
|
4261
4851
|
}
|
|
4262
4852
|
// The active child count already advertises this key beside Working.
|
|
@@ -4406,33 +4996,61 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4406
4996
|
// menu. Only the exact object React committed may consume a choice.
|
|
4407
4997
|
if (choicePickerCommittedRef.current !== picker)
|
|
4408
4998
|
return;
|
|
4409
|
-
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
|
+
};
|
|
4410
5006
|
if (key.escape || (key.ctrl && input === 'c')) {
|
|
4411
5007
|
reviewChoiceInFlightRef.current = null;
|
|
4412
|
-
|
|
5008
|
+
setSelectedChoice(0);
|
|
5009
|
+
setChoicePicker(key.escape ? (picker.back ?? null) : null);
|
|
5010
|
+
// Esc leaves the question unanswered and the turn running; the
|
|
5011
|
+
// tool tells the model so. Ctrl+C is the operator saying stop.
|
|
5012
|
+
if (picker.kind === 'user-question') {
|
|
5013
|
+
resolveQuestion({ kind: key.escape ? 'skip' : 'abort' });
|
|
5014
|
+
}
|
|
4413
5015
|
return;
|
|
4414
5016
|
}
|
|
4415
5017
|
if (key.home || key.end || key.pageUp || key.pageDown) {
|
|
4416
|
-
const pageSize =
|
|
4417
|
-
|
|
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));
|
|
4418
5027
|
return;
|
|
4419
5028
|
}
|
|
4420
5029
|
if (key.upArrow) {
|
|
4421
|
-
setSelectedChoice((index) =>
|
|
5030
|
+
setSelectedChoice((index) => moveChoiceSelection(options, index, 'previous'));
|
|
4422
5031
|
return;
|
|
4423
5032
|
}
|
|
4424
5033
|
if (key.downArrow) {
|
|
4425
|
-
setSelectedChoice((index) =>
|
|
5034
|
+
setSelectedChoice((index) => moveChoiceSelection(options, index, 'next'));
|
|
4426
5035
|
return;
|
|
4427
5036
|
}
|
|
4428
5037
|
if (key.return) {
|
|
4429
|
-
|
|
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'));
|
|
4430
5048
|
return;
|
|
4431
5049
|
}
|
|
4432
5050
|
if (/^[1-9]$/.test(input)) {
|
|
4433
5051
|
const index = Number(input) - 1;
|
|
4434
5052
|
if (options[index])
|
|
4435
|
-
|
|
5053
|
+
applyVisibleChoice(index);
|
|
4436
5054
|
}
|
|
4437
5055
|
return;
|
|
4438
5056
|
}
|
|
@@ -4511,19 +5129,36 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4511
5129
|
// in place and sometimes prints a second copy further down would be two
|
|
4512
5130
|
// commands wearing one name.
|
|
4513
5131
|
if (key.ctrl && input === 'o') {
|
|
4514
|
-
// Taken outside the updater so both branches use the same one
|
|
4515
|
-
// `/expand` does.
|
|
5132
|
+
// Taken outside the updater so both branches use the same one.
|
|
4516
5133
|
const id = nextId();
|
|
4517
5134
|
setMessages((prev) => {
|
|
4518
5135
|
const live = prev.filter((m) => !m.pending).slice(settledRef.current);
|
|
4519
5136
|
const collapsible = live.filter((m) => willCollapse(m.detail));
|
|
4520
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
|
+
}
|
|
4521
5153
|
return [
|
|
4522
5154
|
...prev,
|
|
4523
5155
|
{
|
|
4524
5156
|
id,
|
|
4525
5157
|
role: 'system',
|
|
4526
|
-
content:
|
|
5158
|
+
content: `${block.content} — in full (${block.detail?.length ?? 0} lines)`,
|
|
5159
|
+
glyph: '⤢',
|
|
5160
|
+
detail: block.detail,
|
|
5161
|
+
detailExpanded: true,
|
|
4527
5162
|
},
|
|
4528
5163
|
];
|
|
4529
5164
|
}
|
|
@@ -4612,21 +5247,28 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4612
5247
|
: agentSurface?.kind === 'transcript'
|
|
4613
5248
|
? 'observing agent — esc agents · q parent'
|
|
4614
5249
|
: textPrompt
|
|
4615
|
-
? '
|
|
5250
|
+
? 'editor open · enter apply · esc cancel'
|
|
4616
5251
|
: choicePicker
|
|
4617
|
-
?
|
|
5252
|
+
? choicePickerSearchable(choicePicker)
|
|
5253
|
+
? 'type to filter · ↑↓ select · enter apply · esc back'
|
|
5254
|
+
: '↑↓ / 1–9 select · enter apply · esc back'
|
|
4618
5255
|
: copyPicker
|
|
4619
5256
|
? 'copy target open — ↑↓ / 1–9 select · esc cancel'
|
|
4620
5257
|
: goalStatus && phase === 'ready' && state === 'idle'
|
|
4621
5258
|
? undefined
|
|
4622
5259
|
: hintForPhase(phase, state, session?.hasProvider === true, composerHasDraft);
|
|
4623
|
-
return (_jsx(Box, { flexDirection: "column", display: externalEditorRequest ? 'none' : 'flex', children: _jsxs(Box, { flexDirection: "column", paddingX: 1, children: [
|
|
4624
|
-
|
|
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 &&
|
|
4625
5264
|
conversationMutation === null &&
|
|
4626
5265
|
textPrompt === null &&
|
|
4627
5266
|
choicePicker === null &&
|
|
4628
5267
|
copyPicker === null &&
|
|
4629
|
-
agentSurface === null, hidden: permission !== null ||
|
|
5268
|
+
agentSurface === null, hidden: permission !== null ||
|
|
5269
|
+
textPrompt !== null ||
|
|
5270
|
+
choicePicker !== null ||
|
|
5271
|
+
copyPicker !== null, children: [pendingSteers.length > 0 &&
|
|
4630
5272
|
permission === null &&
|
|
4631
5273
|
textPrompt === null &&
|
|
4632
5274
|
choicePicker === null &&
|
|
@@ -4640,7 +5282,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4640
5282
|
? queuePause.outcome === 'paused'
|
|
4641
5283
|
? 'held after a resumable run paused; wait for recovery, change model, or send a message to release it'
|
|
4642
5284
|
: `paused after a ${queuePause.outcome} turn; send a message or change model to continue`
|
|
4643
|
-
: 'sending when ready'] }) })) : null, _jsx(Composer, { disabled: phase !== 'ready' ||
|
|
5285
|
+
: 'sending when ready'] }) })) : null, _jsx(Composer, { permissionMode: permissionMode, onCycleMode: cyclePermissionMode, disabled: phase !== 'ready' ||
|
|
4644
5286
|
state === 'awaiting-permission' ||
|
|
4645
5287
|
compacting ||
|
|
4646
5288
|
conversationMutation !== null ||
|
|
@@ -4651,26 +5293,22 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4651
5293
|
copyPicker !== null,
|
|
4652
5294
|
// A turn is running, so Esc is the interrupt and not
|
|
4653
5295
|
// the composer's clear.
|
|
4654
|
-
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 }) })] }) }));
|
|
4655
5300
|
}
|
|
4656
|
-
|
|
4657
|
-
|
|
4658
|
-
const
|
|
4659
|
-
|
|
4660
|
-
|
|
4661
|
-
|
|
4662
|
-
const prettyCwd = home && cwd.startsWith(home) ? `~${cwd.slice(home.length)}` : cwd;
|
|
4663
|
-
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}`;
|
|
4664
5307
|
}
|
|
4665
5308
|
function TranscriptFrame({ children }) {
|
|
4666
5309
|
// Borderless, edge-to-edge — the message glyph gutter provides structure.
|
|
4667
5310
|
return _jsx(Box, { flexDirection: "column", children: children });
|
|
4668
5311
|
}
|
|
4669
|
-
function ComposerFrame({ focus, hidden = false, children, }) {
|
|
4670
|
-
// Input-field look: a rounded rule above and below the composer, no side
|
|
4671
|
-
// borders, so the input reads as a field rather than a heavy box.
|
|
4672
|
-
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 }));
|
|
4673
|
-
}
|
|
4674
5312
|
// Tool call label: the tool name title-cased, then its most identifying
|
|
4675
5313
|
// argument — `Bash(ls -la)`, `Read(file.ts)`. A bare tool name in a transcript
|
|
4676
5314
|
// of forty calls says nothing about which one this was.
|
|
@@ -4693,7 +5331,7 @@ hasDraft = false) {
|
|
|
4693
5331
|
if (phase === 'resume')
|
|
4694
5332
|
return '↑↓ navigate · enter resume · esc cancel';
|
|
4695
5333
|
if (phase === 'edit')
|
|
4696
|
-
return '
|
|
5334
|
+
return '← older · → newer · enter fork and edit · esc cancel';
|
|
4697
5335
|
if (phase === 'probing')
|
|
4698
5336
|
return 'discovering providers…';
|
|
4699
5337
|
// Esc does two different things here depending on how the picker was reached,
|
|
@@ -4705,11 +5343,11 @@ hasDraft = false) {
|
|
|
4705
5343
|
: '↑↓ navigate · enter accept · esc or Ctrl+C exit';
|
|
4706
5344
|
}
|
|
4707
5345
|
if (phase === 'unhealthy')
|
|
4708
|
-
return 'Ctrl+C
|
|
5346
|
+
return 'esc or Ctrl+C exit';
|
|
4709
5347
|
// Enter is absent because Enter decides nothing here, deliberately — see the
|
|
4710
5348
|
// permission gate in the key handler above.
|
|
4711
5349
|
if (state === 'awaiting-permission')
|
|
4712
|
-
return '
|
|
5350
|
+
return '↑↓ select · enter confirm · esc decline';
|
|
4713
5351
|
if (state !== 'idle')
|
|
4714
5352
|
return hasDraft ? 'enter steer · tab queue · esc interrupt' : '';
|
|
4715
5353
|
// The composer already points to /help. Keeping the complete key legend in
|
|
@@ -4731,15 +5369,12 @@ function goalStatusLabel(goal) {
|
|
|
4731
5369
|
return 'Goal achieved';
|
|
4732
5370
|
}
|
|
4733
5371
|
}
|
|
4734
|
-
|
|
4735
|
-
|
|
4736
|
-
|
|
4737
|
-
|
|
4738
|
-
|
|
4739
|
-
|
|
4740
|
-
case 'strict':
|
|
4741
|
-
return 'Refuse undecided calls; explicit allow rules still work';
|
|
4742
|
-
}
|
|
5372
|
+
/** The label the operator saw for the option id they chose. */
|
|
5373
|
+
function labelOfOption(question, optionId) {
|
|
5374
|
+
return question.options.find((option) => option.id === optionId)?.label ?? optionId;
|
|
5375
|
+
}
|
|
5376
|
+
function choicePickerSearchable(picker) {
|
|
5377
|
+
return ['command', 'skill', 'review-branch', 'review-commit'].includes(picker.kind);
|
|
4743
5378
|
}
|
|
4744
5379
|
function permissionPickerNotice(session) {
|
|
4745
5380
|
const notices = [];
|