@leanandmean/coding-agent 0.74.1-scramjet.6 → 0.74.1-scramjet.60
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 +32 -0
- package/LICENSE +202 -0
- package/README.md +21 -9
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +1 -1
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/initial-message.d.ts +1 -0
- package/dist/cli/initial-message.d.ts.map +1 -1
- package/dist/cli/initial-message.js +3 -0
- package/dist/cli/initial-message.js.map +1 -1
- package/dist/cli/list-models.d.ts +2 -0
- package/dist/cli/list-models.d.ts.map +1 -1
- package/dist/cli/list-models.js +4 -1
- package/dist/cli/list-models.js.map +1 -1
- package/dist/config.d.ts +9 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +33 -22
- package/dist/config.js.map +1 -1
- package/dist/core/agent-session-runtime.d.ts +12 -3
- package/dist/core/agent-session-runtime.d.ts.map +1 -1
- package/dist/core/agent-session-runtime.js +64 -38
- package/dist/core/agent-session-runtime.js.map +1 -1
- package/dist/core/agent-session-services.d.ts +1 -0
- package/dist/core/agent-session-services.d.ts.map +1 -1
- package/dist/core/agent-session-services.js +2 -0
- package/dist/core/agent-session-services.js.map +1 -1
- package/dist/core/agent-session.d.ts +86 -16
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +865 -181
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
- package/dist/core/compaction/branch-summarization.js +21 -17
- package/dist/core/compaction/branch-summarization.js.map +1 -1
- package/dist/core/compaction/compaction.d.ts +3 -5
- package/dist/core/compaction/compaction.d.ts.map +1 -1
- package/dist/core/compaction/compaction.js +43 -10
- package/dist/core/compaction/compaction.js.map +1 -1
- package/dist/core/exec.d.ts +12 -0
- package/dist/core/exec.d.ts.map +1 -1
- package/dist/core/exec.js +74 -28
- package/dist/core/exec.js.map +1 -1
- package/dist/core/export-html/template.js +4 -0
- package/dist/core/extensions/index.d.ts +1 -1
- package/dist/core/extensions/index.d.ts.map +1 -1
- package/dist/core/extensions/index.js.map +1 -1
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +7 -0
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/runner.d.ts +9 -5
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +162 -3
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/types.d.ts +110 -6
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/keybindings.d.ts +8 -0
- package/dist/core/keybindings.d.ts.map +1 -1
- package/dist/core/model-registry.d.ts +4 -0
- package/dist/core/model-registry.d.ts.map +1 -1
- package/dist/core/model-registry.js +179 -28
- package/dist/core/model-registry.js.map +1 -1
- package/dist/core/model-resolver.d.ts.map +1 -1
- package/dist/core/model-resolver.js +24 -6
- package/dist/core/model-resolver.js.map +1 -1
- package/dist/core/output-throughput-history.d.ts +25 -0
- package/dist/core/output-throughput-history.d.ts.map +1 -0
- package/dist/core/output-throughput-history.js +240 -0
- package/dist/core/output-throughput-history.js.map +1 -0
- package/dist/core/output-throughput.d.ts +43 -0
- package/dist/core/output-throughput.d.ts.map +1 -0
- package/dist/core/output-throughput.js +161 -0
- package/dist/core/output-throughput.js.map +1 -0
- package/dist/core/resource-loader.d.ts +6 -0
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +76 -21
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/scramjet-command-parser.d.ts +4 -0
- package/dist/core/scramjet-command-parser.d.ts.map +1 -1
- package/dist/core/scramjet-command-parser.js +72 -1
- package/dist/core/scramjet-command-parser.js.map +1 -1
- package/dist/core/sdk.d.ts +2 -0
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +61 -10
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-manager.d.ts +11 -3
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +46 -11
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/settings-manager.d.ts +18 -6
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +84 -16
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/skills.d.ts.map +1 -1
- package/dist/core/skills.js +6 -2
- package/dist/core/skills.js.map +1 -1
- package/dist/core/system-prompt.d.ts +2 -0
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +5 -2
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js +4 -3
- package/dist/core/tools/bash.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts +16 -0
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +38 -10
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/armin.d.ts +4 -2
- package/dist/modes/interactive/components/armin.d.ts.map +1 -1
- package/dist/modes/interactive/components/armin.js +14 -8
- package/dist/modes/interactive/components/armin.js.map +1 -1
- package/dist/modes/interactive/components/assistant-message.d.ts +3 -1
- package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/assistant-message.js +19 -8
- package/dist/modes/interactive/components/assistant-message.js.map +1 -1
- package/dist/modes/interactive/components/daxnuts.d.ts +4 -1
- package/dist/modes/interactive/components/daxnuts.d.ts.map +1 -1
- package/dist/modes/interactive/components/daxnuts.js +14 -6
- package/dist/modes/interactive/components/daxnuts.js.map +1 -1
- package/dist/modes/interactive/components/extension-editor.d.ts.map +1 -1
- package/dist/modes/interactive/components/extension-editor.js +5 -3
- package/dist/modes/interactive/components/extension-editor.js.map +1 -1
- package/dist/modes/interactive/components/extension-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/extension-selector.js +4 -3
- package/dist/modes/interactive/components/extension-selector.js.map +1 -1
- package/dist/modes/interactive/components/footer.d.ts +4 -0
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +22 -18
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/components/model-selector.d.ts +3 -1
- package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/model-selector.js +9 -3
- package/dist/modes/interactive/components/model-selector.js.map +1 -1
- package/dist/modes/interactive/components/scoped-models-selector.d.ts +3 -1
- package/dist/modes/interactive/components/scoped-models-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/scoped-models-selector.js +8 -2
- package/dist/modes/interactive/components/scoped-models-selector.js.map +1 -1
- package/dist/modes/interactive/components/settings-selector.d.ts +17 -3
- package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js +87 -16
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/interactive/components/thinking-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/thinking-selector.js +2 -1
- package/dist/modes/interactive/components/thinking-selector.js.map +1 -1
- package/dist/modes/interactive/components/tool-execution.d.ts +19 -1
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +129 -36
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/dist/modes/interactive/components/user-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/user-message.js +3 -2
- package/dist/modes/interactive/components/user-message.js.map +1 -1
- package/dist/modes/interactive/components/visual-truncate.d.ts.map +1 -1
- package/dist/modes/interactive/components/visual-truncate.js +2 -2
- package/dist/modes/interactive/components/visual-truncate.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +37 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +876 -175
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/spellcheck.d.ts +26 -0
- package/dist/modes/interactive/spellcheck.d.ts.map +1 -0
- package/dist/modes/interactive/spellcheck.js +144 -0
- package/dist/modes/interactive/spellcheck.js.map +1 -0
- package/dist/modes/interactive/theme/{dark.json → pi-dark.json} +3 -2
- package/dist/modes/interactive/theme/{light.json → pi-light.json} +27 -26
- package/dist/modes/interactive/theme/theme-schema.json +6 -1
- package/dist/modes/interactive/theme/theme.d.ts +24 -4
- package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
- package/dist/modes/interactive/theme/theme.js +123 -22
- package/dist/modes/interactive/theme/theme.js.map +1 -1
- package/dist/modes/print-mode.d.ts.map +1 -1
- package/dist/modes/print-mode.js +5 -0
- package/dist/modes/print-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-client.d.ts +3 -1
- package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-client.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +6 -1
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/package-manager-cli.d.ts.map +1 -1
- package/dist/package-manager-cli.js +33 -14
- package/dist/package-manager-cli.js.map +1 -1
- package/dist/utils/clipboard-native.d.ts +1 -0
- package/dist/utils/clipboard-native.d.ts.map +1 -1
- package/dist/utils/clipboard-native.js.map +1 -1
- package/dist/utils/clipboard.d.ts +6 -0
- package/dist/utils/clipboard.d.ts.map +1 -1
- package/dist/utils/clipboard.js +278 -10
- package/dist/utils/clipboard.js.map +1 -1
- package/dist/utils/tools-manager.d.ts +1 -1
- package/dist/utils/tools-manager.d.ts.map +1 -1
- package/dist/utils/tools-manager.js +7 -6
- package/dist/utils/tools-manager.js.map +1 -1
- package/dist/utils/version-check.d.ts +17 -5
- package/dist/utils/version-check.d.ts.map +1 -1
- package/dist/utils/version-check.js +88 -21
- package/dist/utils/version-check.js.map +1 -1
- package/docs/compaction.md +9 -3
- package/docs/custom-provider.md +13 -4
- package/docs/extensions.md +139 -21
- package/docs/keybindings.md +25 -1
- package/docs/models.md +34 -2
- package/docs/packages.md +2 -0
- package/docs/prompt-templates.md +2 -0
- package/docs/quickstart.md +1 -4
- package/docs/rpc.md +8 -6
- package/docs/sdk.md +22 -3
- package/docs/session-format.md +2 -1
- package/docs/settings.md +13 -3
- package/docs/skills.md +7 -5
- package/docs/terminal-setup.md +79 -2
- package/docs/terminal-verification.md +71 -0
- package/docs/themes.md +63 -10
- package/docs/tui.md +120 -25
- package/docs/usage.md +6 -4
- package/examples/extensions/border-status-editor.ts +2 -3
- package/examples/extensions/doom-overlay/doom-component.ts +3 -3
- package/examples/extensions/mac-system-theme.ts +3 -2
- package/examples/extensions/modal-editor.ts +13 -8
- package/examples/extensions/overlay-qa-tests.ts +22 -9
- package/examples/extensions/overlay-test.ts +5 -3
- package/examples/extensions/snake.ts +3 -1
- package/examples/extensions/space-invaders.ts +6 -3
- package/examples/extensions/subagent/index.ts +20 -17
- package/package.json +69 -58
|
@@ -6,8 +6,9 @@ import * as crypto from "node:crypto";
|
|
|
6
6
|
import * as fs from "node:fs";
|
|
7
7
|
import * as os from "node:os";
|
|
8
8
|
import * as path from "node:path";
|
|
9
|
+
import { stripVTControlCharacters } from "node:util";
|
|
9
10
|
import { getProviders, } from "@leanandmean/ai";
|
|
10
|
-
import { CombinedAutocompleteProvider, Container, fuzzyFilter, Loader, Markdown, matchesKey, ProcessTerminal, Spacer, setKeybindings, Text, TruncatedText, TUI, visibleWidth, } from "@leanandmean/tui";
|
|
11
|
+
import { CombinedAutocompleteProvider, Container, fuzzyFilter, isKeyRelease, Loader, Markdown, matchesKey, ProcessTerminal, Spacer, setKeybindings, Text, TruncatedText, TUI, TUI_KEYBINDINGS, visibleWidth, } from "@leanandmean/tui";
|
|
11
12
|
import { spawn, spawnSync } from "child_process";
|
|
12
13
|
import { APP_NAME, APP_TITLE, getAgentDir, getAuthPath, getDebugLogPath, getDocsPath, VERSION } from "../../config.js";
|
|
13
14
|
import { parseSkillBlock } from "../../core/agent-session.js";
|
|
@@ -18,12 +19,13 @@ import { createCompactionSummaryMessage } from "../../core/messages.js";
|
|
|
18
19
|
import { defaultModelPerProvider, findExactModelReferenceMatch, resolveModelScope } from "../../core/model-resolver.js";
|
|
19
20
|
import { DefaultPackageManager } from "../../core/package-manager.js";
|
|
20
21
|
import { BUILT_IN_PROVIDER_DISPLAY_NAMES } from "../../core/provider-display-names.js";
|
|
21
|
-
import {
|
|
22
|
+
import { describeRuntimeError } from "../../core/resource-loader.js";
|
|
23
|
+
import { indexScramjetCommandStarts, parseScramjetCommandBlock, restoreScramjetCommandInvocation, } from "../../core/scramjet-command-parser.js"; // SCRAMJET-DIVERGENCE: scramjet-command restoration (issues 82, 414)
|
|
22
24
|
import { formatMissingSessionCwdPrompt, MissingSessionCwdError } from "../../core/session-cwd.js";
|
|
23
25
|
import { SessionManager } from "../../core/session-manager.js";
|
|
24
26
|
import { BUILTIN_SLASH_COMMANDS } from "../../core/slash-commands.js";
|
|
25
27
|
import { getChangelogPath, getNewEntries, parseChangelog } from "../../utils/changelog.js";
|
|
26
|
-
import { copyToClipboard } from "../../utils/clipboard.js";
|
|
28
|
+
import { copyToClipboard, createWslClipboardReader, readClipboardText } from "../../utils/clipboard.js";
|
|
27
29
|
import { extensionForImageMimeType, readClipboardImage } from "../../utils/clipboard-image.js";
|
|
28
30
|
import { parseGitUrl } from "../../utils/git.js";
|
|
29
31
|
import { getCwdRelativePath } from "../../utils/paths.js";
|
|
@@ -57,7 +59,8 @@ import { ToolExecutionComponent } from "./components/tool-execution.js";
|
|
|
57
59
|
import { TreeSelectorComponent } from "./components/tree-selector.js";
|
|
58
60
|
import { UserMessageComponent } from "./components/user-message.js";
|
|
59
61
|
import { UserMessageSelectorComponent } from "./components/user-message-selector.js";
|
|
60
|
-
import {
|
|
62
|
+
import { NspellProvider } from "./spellcheck.js";
|
|
63
|
+
import { classifyBackgroundColor, detectThemeFromEnvironment, getAvailableThemes, getAvailableThemesWithPaths, getCurrentThemeName, getEditorTheme, getMarkdownTheme, getThemeByName, initTheme, onThemeChange, resolveThemeName, setRegisteredThemes, setTheme, setThemeInstance, stopThemeWatcher, Theme, theme, } from "./theme/theme.js";
|
|
61
64
|
function isExpandable(obj) {
|
|
62
65
|
return typeof obj === "object" && obj !== null && "setExpanded" in obj && typeof obj.setExpanded === "function";
|
|
63
66
|
}
|
|
@@ -106,11 +109,15 @@ export class InteractiveMode {
|
|
|
106
109
|
options;
|
|
107
110
|
runtimeHost;
|
|
108
111
|
ui;
|
|
112
|
+
tuiMode;
|
|
113
|
+
committedChatContainer;
|
|
109
114
|
chatContainer;
|
|
115
|
+
mutableChatComponents = new Set();
|
|
110
116
|
pendingMessagesContainer;
|
|
111
117
|
statusContainer;
|
|
112
118
|
defaultEditor;
|
|
113
119
|
editor;
|
|
120
|
+
editorAvailableRows;
|
|
114
121
|
editorComponentFactory;
|
|
115
122
|
autocompleteProvider;
|
|
116
123
|
autocompleteProviderWrappers = [];
|
|
@@ -126,6 +133,11 @@ export class InteractiveMode {
|
|
|
126
133
|
loadingAnimation = undefined;
|
|
127
134
|
workingMessage = undefined;
|
|
128
135
|
workingVisible = true;
|
|
136
|
+
// SCRAMJET-DIVERGENCE: title provider for extension-owned terminal titles.
|
|
137
|
+
titleProvider = undefined;
|
|
138
|
+
// SCRAMJET-DIVERGENCE: captured terminal classification (env-seeded in the constructor, updated
|
|
139
|
+
// by the OSC 11 query) used to re-resolve the default theme once extension themes register.
|
|
140
|
+
detectedClassification = undefined;
|
|
129
141
|
workingIndicatorOptions = undefined;
|
|
130
142
|
defaultWorkingMessage = "Working...";
|
|
131
143
|
defaultHiddenThinkingLabel = "Thinking...";
|
|
@@ -143,6 +155,11 @@ export class InteractiveMode {
|
|
|
143
155
|
streamingMessage = undefined;
|
|
144
156
|
// Tool execution tracking: toolCallId -> component
|
|
145
157
|
pendingTools = new Map();
|
|
158
|
+
pendingToolFinalizations = new Set();
|
|
159
|
+
agentRunGeneration = 0;
|
|
160
|
+
selectorOpenGeneration = 0;
|
|
161
|
+
pendingSelectorOpenGeneration;
|
|
162
|
+
layoutSettingsSettlementGeneration = 0;
|
|
146
163
|
// Tool output expansion state
|
|
147
164
|
toolOutputExpanded = false;
|
|
148
165
|
// Thinking block visibility state
|
|
@@ -173,6 +190,7 @@ export class InteractiveMode {
|
|
|
173
190
|
extensionSelector = undefined;
|
|
174
191
|
extensionInput = undefined;
|
|
175
192
|
extensionEditor = undefined;
|
|
193
|
+
customHeightAllocation;
|
|
176
194
|
extensionTerminalInputUnsubscribers = new Set();
|
|
177
195
|
// Extension widgets (components rendered above/below the editor)
|
|
178
196
|
extensionWidgetsAbove = new Map();
|
|
@@ -210,9 +228,30 @@ export class InteractiveMode {
|
|
|
210
228
|
await this.rebindCurrentSession();
|
|
211
229
|
});
|
|
212
230
|
this.version = VERSION;
|
|
213
|
-
this.
|
|
214
|
-
|
|
231
|
+
this.tuiMode = this.settingsManager.getTuiMode();
|
|
232
|
+
// SCRAMJET-DIVERGENCE: retained interactive rendering and injectable terminals.
|
|
233
|
+
this.ui = new TUI(options.terminal ?? new ProcessTerminal(), this.settingsManager.getShowHardwareCursor());
|
|
234
|
+
this.clipboardReader = this.tuiMode === "retained" ? createWslClipboardReader() : undefined;
|
|
235
|
+
this.ui.addLifecycleListener((event) => {
|
|
236
|
+
if (event === "start")
|
|
237
|
+
void this.clipboardReader?.start().catch(() => { });
|
|
238
|
+
else {
|
|
239
|
+
this.clipboardPasteGeneration++;
|
|
240
|
+
this.clipboardPastePending = undefined;
|
|
241
|
+
this.clipboardReader?.close();
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
this.ui.addInputListener((data) => {
|
|
245
|
+
if (!isKeyRelease(data) &&
|
|
246
|
+
data !== "\x1b[I" &&
|
|
247
|
+
!/^\x1b\[<34;\d{1,5};\d{1,5}M$/.test(data) &&
|
|
248
|
+
!/^\x1b\[6;\d+;\d+t$/.test(data) &&
|
|
249
|
+
!/^\x1b\[<\d+;\d+;\d+m$/.test(data))
|
|
250
|
+
this.clipboardPasteGeneration++;
|
|
251
|
+
return undefined;
|
|
252
|
+
});
|
|
215
253
|
this.headerContainer = new Container();
|
|
254
|
+
this.committedChatContainer = new Container();
|
|
216
255
|
this.chatContainer = new Container();
|
|
217
256
|
this.pendingMessagesContainer = new Container();
|
|
218
257
|
this.statusContainer = new Container();
|
|
@@ -227,6 +266,8 @@ export class InteractiveMode {
|
|
|
227
266
|
autocompleteMaxVisible,
|
|
228
267
|
});
|
|
229
268
|
this.editor = this.defaultEditor;
|
|
269
|
+
this.configureEditorHeight(this.editor);
|
|
270
|
+
this.defaultEditor.setSpellcheckProvider(new NspellProvider());
|
|
230
271
|
this.editorContainer = new Container();
|
|
231
272
|
this.editorContainer.addChild(this.editor);
|
|
232
273
|
this.footerDataProvider = new FooterDataProvider(this.sessionManager.getCwd());
|
|
@@ -236,8 +277,287 @@ export class InteractiveMode {
|
|
|
236
277
|
this.hideThinkingBlock = this.settingsManager.getHideThinkingBlock();
|
|
237
278
|
// Register themes from resource loader and initialize
|
|
238
279
|
setRegisteredThemes(this.session.resourceLoader.getThemes().themes);
|
|
280
|
+
// SCRAMJET-DIVERGENCE: capture the env classification so the post-extension-bind reapplication
|
|
281
|
+
// can honor COLORFGBG/Apple-Terminal-detected (never-OSC) terminals. Without this seed, those
|
|
282
|
+
// terminals would resolve the dark branch and clobber light terminals to scramjet-dark.
|
|
283
|
+
this.detectedClassification = detectThemeFromEnvironment()?.theme;
|
|
239
284
|
initTheme(this.settingsManager.getTheme(), true);
|
|
240
285
|
}
|
|
286
|
+
configureEditorHeight(editor) {
|
|
287
|
+
editor.setHeightLimit?.(() => ({
|
|
288
|
+
rows: Math.max(1, this.editorAvailableRows ?? this.ui.terminal.rows - 3),
|
|
289
|
+
text: Math.max(1, Math.floor((this.ui.terminal.rows * this.settingsManager.getEditorMaxHeightPercent()) / 100)),
|
|
290
|
+
}));
|
|
291
|
+
}
|
|
292
|
+
renderSessionGap(width, below) {
|
|
293
|
+
if (below === 0)
|
|
294
|
+
return "";
|
|
295
|
+
const forms = [
|
|
296
|
+
`Session: ${below} lines below · Ctrl+End: latest`,
|
|
297
|
+
`Session: ${below} lines below`,
|
|
298
|
+
`Session: ${below} below`,
|
|
299
|
+
`Session↓${below}`,
|
|
300
|
+
"Session ↓…",
|
|
301
|
+
];
|
|
302
|
+
return theme.fg("muted", forms.find((text) => visibleWidth(text) <= width) ?? "");
|
|
303
|
+
}
|
|
304
|
+
// SCRAMJET-DIVERGENCE: preserve production ownership while making mutable overflow browseable.
|
|
305
|
+
clipboardReader;
|
|
306
|
+
clipboardPastePending;
|
|
307
|
+
clipboardPasteGeneration = 0;
|
|
308
|
+
clipboardPasteOwner;
|
|
309
|
+
async pasteFromClipboard() {
|
|
310
|
+
const editor = this.editor;
|
|
311
|
+
const session = this.session;
|
|
312
|
+
const draft = editor.getText();
|
|
313
|
+
const generation = this.clipboardPasteGeneration;
|
|
314
|
+
const current = () => generation === this.clipboardPasteGeneration &&
|
|
315
|
+
this.editor === editor &&
|
|
316
|
+
this.session === session &&
|
|
317
|
+
editor.getText() === draft &&
|
|
318
|
+
this.editorContainer.children.length === 1 &&
|
|
319
|
+
this.editorContainer.children[0] === editor &&
|
|
320
|
+
this.ui.isComponentFocused(editor) &&
|
|
321
|
+
this.ui.isComponentVisible(this.editorContainer) &&
|
|
322
|
+
this.ui.isComponentRenderComplete(this.editorContainer);
|
|
323
|
+
if (!current())
|
|
324
|
+
return;
|
|
325
|
+
let pending;
|
|
326
|
+
try {
|
|
327
|
+
while (current() && !this.ui.isViewportFrameFlushed()) {
|
|
328
|
+
await this.ui.renderNow({ requireFlush: true });
|
|
329
|
+
}
|
|
330
|
+
if (!current())
|
|
331
|
+
return;
|
|
332
|
+
if (this.clipboardReader) {
|
|
333
|
+
await this.clipboardReader.start();
|
|
334
|
+
if (!current())
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
this.clipboardPastePending ??= this.clipboardReader ? this.clipboardReader.read() : readClipboardText();
|
|
338
|
+
pending = this.clipboardPastePending;
|
|
339
|
+
const text = await pending;
|
|
340
|
+
if (!current())
|
|
341
|
+
return;
|
|
342
|
+
const safeText = stripVTControlCharacters(text).replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f]/g, "");
|
|
343
|
+
editor.handleInput(`\x1b[200~${safeText}\x1b[201~`);
|
|
344
|
+
this.ui.requestRender();
|
|
345
|
+
}
|
|
346
|
+
catch (error) {
|
|
347
|
+
if (current())
|
|
348
|
+
this.showError(`Paste failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
349
|
+
}
|
|
350
|
+
finally {
|
|
351
|
+
if (pending && this.clipboardPastePending === pending)
|
|
352
|
+
this.clipboardPastePending = undefined;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
configureRetainedViewport() {
|
|
356
|
+
this.ui.configureViewport({
|
|
357
|
+
getBlocks: () => this.ui.children.flatMap((component) => {
|
|
358
|
+
if (component === this.editorContainer &&
|
|
359
|
+
this.clipboardPasteOwner !== this.editorContainer.children[0]) {
|
|
360
|
+
this.clipboardPasteOwner = this.editorContainer.children[0];
|
|
361
|
+
this.clipboardPasteGeneration++;
|
|
362
|
+
}
|
|
363
|
+
if (component instanceof Container &&
|
|
364
|
+
(component === this.committedChatContainer || component === this.chatContainer)) {
|
|
365
|
+
return component.children.map((child) => ({
|
|
366
|
+
component: child,
|
|
367
|
+
finalized: component === this.committedChatContainer,
|
|
368
|
+
}));
|
|
369
|
+
}
|
|
370
|
+
const dock = this.settingsManager.getDockEditor() &&
|
|
371
|
+
this.editorContainer.children.length > 0 &&
|
|
372
|
+
[
|
|
373
|
+
this.widgetContainerAbove,
|
|
374
|
+
this.editorContainer,
|
|
375
|
+
this.widgetContainerBelow,
|
|
376
|
+
this.customFooter ?? this.footer,
|
|
377
|
+
].includes(component);
|
|
378
|
+
return [
|
|
379
|
+
{
|
|
380
|
+
component,
|
|
381
|
+
finalized: false,
|
|
382
|
+
dock,
|
|
383
|
+
renderDockGap: component === this.widgetContainerAbove
|
|
384
|
+
? (width, below) => this.renderSessionGap(width, below)
|
|
385
|
+
: undefined,
|
|
386
|
+
fitHeight: component === this.editorContainer
|
|
387
|
+
? (rows) => {
|
|
388
|
+
this.editorAvailableRows = rows;
|
|
389
|
+
const allocation = this.customHeightAllocation;
|
|
390
|
+
if (allocation) {
|
|
391
|
+
if (this.editorContainer.children.length === 1 &&
|
|
392
|
+
this.editorContainer.children[0] === allocation.component) {
|
|
393
|
+
try {
|
|
394
|
+
allocation.notify(rows);
|
|
395
|
+
}
|
|
396
|
+
catch (error) {
|
|
397
|
+
allocation.fail(error);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
else
|
|
401
|
+
this.releaseCustomHeight();
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
: undefined,
|
|
405
|
+
},
|
|
406
|
+
];
|
|
407
|
+
}),
|
|
408
|
+
keybindings: this.keybindings,
|
|
409
|
+
copy: copyToClipboard,
|
|
410
|
+
requestPaste: () => void this.pasteFromClipboard(),
|
|
411
|
+
minimumSize: { columns: 12, rows: 3 },
|
|
412
|
+
handleBlockedInput: (data) => {
|
|
413
|
+
const selector = this.editorContainer.children[0];
|
|
414
|
+
if (selector instanceof SettingsSelectorComponent &&
|
|
415
|
+
this.ui.isComponentFocused(selector.getSettingsList()) &&
|
|
416
|
+
this.keybindings.matches(data, "tui.select.cancel")) {
|
|
417
|
+
selector.cancel();
|
|
418
|
+
}
|
|
419
|
+
else if (this.keybindings.matches(data, "app.interrupt")) {
|
|
420
|
+
this.restoreQueuedMessagesToEditor();
|
|
421
|
+
this.session.abortBash();
|
|
422
|
+
this.session.abortCompaction();
|
|
423
|
+
this.session.abortBranchSummary();
|
|
424
|
+
void this.session.abort();
|
|
425
|
+
}
|
|
426
|
+
else if (this.keybindings.matches(data, "app.exit") && this.editor.getText().length === 0) {
|
|
427
|
+
this.handleCtrlD();
|
|
428
|
+
}
|
|
429
|
+
},
|
|
430
|
+
getScrollWheelStep: () => this.settingsManager.getScrollWheelStep(),
|
|
431
|
+
allowViewportKeys: (data) => this.ui.isComponentFocused(this.editor) ||
|
|
432
|
+
(!matchesKey(data, "space") &&
|
|
433
|
+
!Object.keys(TUI_KEYBINDINGS).some((action) => !action.startsWith("tui.viewport.") && this.keybindings.matches(data, action))),
|
|
434
|
+
keepReadingOnInput: () => {
|
|
435
|
+
const ownsFocus = (component) => this.ui.isComponentFocused(component) ||
|
|
436
|
+
(component instanceof Container && component.children.some(ownsFocus));
|
|
437
|
+
return this.editorContainer.children.some(ownsFocus);
|
|
438
|
+
},
|
|
439
|
+
handlePresentationInput: (data) => {
|
|
440
|
+
if (!this.ui.isComponentFocused(this.editor))
|
|
441
|
+
return false;
|
|
442
|
+
if (!this.keybindings.matches(data, "app.tools.expand") &&
|
|
443
|
+
!this.keybindings.matches(data, "app.thinking.toggle"))
|
|
444
|
+
return false;
|
|
445
|
+
this.editor.handleInput(data);
|
|
446
|
+
return true;
|
|
447
|
+
},
|
|
448
|
+
});
|
|
449
|
+
// SCRAMJET-DIVERGENCE: completion needs a painted slot even when its hardware cursor is hidden.
|
|
450
|
+
this.ui.addInputListener((data) => {
|
|
451
|
+
if (!this.ui.isComponentFocused(this.editor) ||
|
|
452
|
+
!this.editor.isShowingAutocomplete?.() ||
|
|
453
|
+
isKeyRelease(data) ||
|
|
454
|
+
/^\x1b\[\d+;\d+;\d+t$/.test(data) ||
|
|
455
|
+
this.keybindings.matches(data, "app.tools.expand") ||
|
|
456
|
+
this.keybindings.matches(data, "app.thinking.toggle"))
|
|
457
|
+
return undefined;
|
|
458
|
+
const visible = this.ui.isComponentVisible(this.editorContainer);
|
|
459
|
+
if (!visible && !this.ui.hasOverlay())
|
|
460
|
+
this.ui.revealComponent(this.editorContainer);
|
|
461
|
+
if ((this.keybindings.matches(data, "tui.input.tab") || this.keybindings.matches(data, "tui.select.confirm")) &&
|
|
462
|
+
(!visible || !this.ui.isViewportFrameFlushed()))
|
|
463
|
+
return { consume: true };
|
|
464
|
+
return undefined;
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
// SCRAMJET-DIVERGENCE: finalized transcript components are promoted atomically into append-only history (#389).
|
|
468
|
+
promoteFinalizedChatPrefix() {
|
|
469
|
+
let promoted = false;
|
|
470
|
+
while (this.chatContainer.children.length > 0) {
|
|
471
|
+
const child = this.chatContainer.children[0];
|
|
472
|
+
if (this.mutableChatComponents.has(child))
|
|
473
|
+
break;
|
|
474
|
+
this.chatContainer.removeChild(child);
|
|
475
|
+
if (child instanceof ToolExecutionComponent)
|
|
476
|
+
child.markCommitted();
|
|
477
|
+
this.committedChatContainer.addChild(child);
|
|
478
|
+
promoted = true;
|
|
479
|
+
}
|
|
480
|
+
if (promoted) {
|
|
481
|
+
if (this.ui.getViewportState())
|
|
482
|
+
this.ui.requestRender();
|
|
483
|
+
else
|
|
484
|
+
this.ui.commit();
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
setChatComponentMutable(component, mutable) {
|
|
488
|
+
if (mutable)
|
|
489
|
+
this.mutableChatComponents.add(component);
|
|
490
|
+
else
|
|
491
|
+
this.mutableChatComponents.delete(component);
|
|
492
|
+
}
|
|
493
|
+
sealStatus() {
|
|
494
|
+
if (this.lastStatusSpacer)
|
|
495
|
+
this.mutableChatComponents.delete(this.lastStatusSpacer);
|
|
496
|
+
if (this.lastStatusText)
|
|
497
|
+
this.mutableChatComponents.delete(this.lastStatusText);
|
|
498
|
+
this.lastStatusSpacer = undefined;
|
|
499
|
+
this.lastStatusText = undefined;
|
|
500
|
+
this.promoteFinalizedChatPrefix();
|
|
501
|
+
}
|
|
502
|
+
clearTranscript() {
|
|
503
|
+
this.clipboardPasteGeneration++;
|
|
504
|
+
if (this.ui.getViewportState())
|
|
505
|
+
this.ui.resetViewport();
|
|
506
|
+
this.pendingToolFinalizations = new Set();
|
|
507
|
+
const tools = new Set([
|
|
508
|
+
...this.pendingTools.values(),
|
|
509
|
+
...this.committedChatContainer.children.filter((component) => component instanceof ToolExecutionComponent),
|
|
510
|
+
...this.chatContainer.children.filter((component) => component instanceof ToolExecutionComponent),
|
|
511
|
+
]);
|
|
512
|
+
for (const tool of tools)
|
|
513
|
+
tool.detach();
|
|
514
|
+
this.committedChatContainer.clear();
|
|
515
|
+
this.chatContainer.clear();
|
|
516
|
+
this.mutableChatComponents.clear();
|
|
517
|
+
this.lastStatusSpacer = undefined;
|
|
518
|
+
this.lastStatusText = undefined;
|
|
519
|
+
this.ui.rebuild();
|
|
520
|
+
}
|
|
521
|
+
updateToolImagePresentation(update) {
|
|
522
|
+
for (const container of [this.committedChatContainer, this.chatContainer]) {
|
|
523
|
+
for (const child of container.children) {
|
|
524
|
+
if (child instanceof ToolExecutionComponent)
|
|
525
|
+
update(child);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
this.ui.rebuild();
|
|
529
|
+
}
|
|
530
|
+
commitFinalizedChatOutput() {
|
|
531
|
+
this.sealStatus();
|
|
532
|
+
this.ui.requestRender();
|
|
533
|
+
}
|
|
534
|
+
bindThemeChangeHandler() {
|
|
535
|
+
onThemeChange(() => {
|
|
536
|
+
this.ui.invalidate();
|
|
537
|
+
this.updateEditorBorderColor();
|
|
538
|
+
this.ui.rebuild();
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
// SCRAMJET-DIVERGENCE: re-resolve the theme after extension themes register (or after a reload),
|
|
542
|
+
// so scramjet-dark can take effect once it becomes available. An explicit user setting still wins
|
|
543
|
+
// via the resolver; the captured classification (env-seeded + OSC-updated) drives auto-detection.
|
|
544
|
+
// Skips the apply when the resolved name already matches the active theme, unless `force` is set:
|
|
545
|
+
// the reload path forces a re-read from disk (registered extension themes have no file watcher, so
|
|
546
|
+
// /reload is their only refresh path even when the active theme's name is unchanged).
|
|
547
|
+
reapplyResolvedTheme(force = false) {
|
|
548
|
+
const name = resolveThemeName({
|
|
549
|
+
explicitSetting: this.settingsManager.getTheme(),
|
|
550
|
+
detectedClassification: this.detectedClassification,
|
|
551
|
+
hasScramjetDark: getAvailableThemes().includes("scramjet-dark"),
|
|
552
|
+
});
|
|
553
|
+
if (!force && name === getCurrentThemeName()) {
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
const result = setTheme(name, true);
|
|
557
|
+
if (!result.success) {
|
|
558
|
+
this.showError(`Failed to load theme "${name}": ${result.error}\nFell back to pi-dark theme.`);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
241
561
|
getAutocompleteSourceTag(sourceInfo) {
|
|
242
562
|
if (!sourceInfo) {
|
|
243
563
|
return undefined;
|
|
@@ -376,6 +696,7 @@ export class InteractiveMode {
|
|
|
376
696
|
this.chatContainer.addChild(new Spacer(1));
|
|
377
697
|
}
|
|
378
698
|
this.chatContainer.addChild(new DynamicBorder());
|
|
699
|
+
this.promoteFinalizedChatPrefix();
|
|
379
700
|
}
|
|
380
701
|
async init() {
|
|
381
702
|
if (this.isInitialized)
|
|
@@ -385,27 +706,58 @@ export class InteractiveMode {
|
|
|
385
706
|
this.changelogMarkdown = this.getChangelogForDisplay();
|
|
386
707
|
// Ensure fd and rg are available (downloads if missing, adds to PATH via getBinDir)
|
|
387
708
|
// Both are needed: fd for autocomplete, rg for grep tool and bash commands
|
|
388
|
-
const
|
|
709
|
+
const startupDiagnostics = [];
|
|
710
|
+
const report = (message) => {
|
|
711
|
+
startupDiagnostics.push(message);
|
|
712
|
+
};
|
|
713
|
+
const [fdPath] = await Promise.all([ensureTool("fd", false, report), ensureTool("rg", false, report)]);
|
|
389
714
|
this.fdPath = fdPath;
|
|
390
|
-
|
|
391
|
-
const modelList = this.session.scopedModels
|
|
392
|
-
.map((sm) => {
|
|
393
|
-
const thinkingStr = sm.thinkingLevel ? `:${sm.thinkingLevel}` : "";
|
|
394
|
-
return `${sm.model.id}${thinkingStr}`;
|
|
395
|
-
})
|
|
396
|
-
.join(", ");
|
|
397
|
-
const cycleKeys = this.keybindings.getKeys("app.model.cycleForward");
|
|
398
|
-
const cycleHint = cycleKeys.length > 0
|
|
399
|
-
? theme.fg("muted", ` (${formatKeyText(cycleKeys.join("/"), { capitalize: true })} to cycle)`)
|
|
400
|
-
: "";
|
|
401
|
-
console.log(theme.fg("dim", `Model scope: ${modelList}${cycleHint}`));
|
|
402
|
-
}
|
|
403
|
-
// Add header container as first child
|
|
715
|
+
// Add header container as first child (content built after theme detection below)
|
|
404
716
|
this.ui.addChild(this.headerContainer);
|
|
405
|
-
|
|
717
|
+
this.ui.addChild(this.committedChatContainer);
|
|
718
|
+
this.ui.addChild(this.chatContainer);
|
|
719
|
+
if (this.tuiMode === "retained")
|
|
720
|
+
this.configureRetainedViewport();
|
|
721
|
+
else
|
|
722
|
+
this.ui.setLiveRegionStart(this.chatContainer);
|
|
723
|
+
this.ui.addChild(this.pendingMessagesContainer);
|
|
724
|
+
this.ui.addChild(this.statusContainer);
|
|
725
|
+
this.renderWidgets(); // Initialize with default spacer
|
|
726
|
+
this.ui.addChild(this.widgetContainerAbove);
|
|
727
|
+
this.ui.addChild(this.editorContainer);
|
|
728
|
+
this.ui.addChild(this.widgetContainerBelow);
|
|
729
|
+
this.ui.addChild(this.footer);
|
|
730
|
+
this.ui.setFocus(this.editor);
|
|
731
|
+
this.setupKeyHandlers();
|
|
732
|
+
this.setupEditorSubmitHandler();
|
|
733
|
+
// Start the UI before initializing extensions so session_start handlers can use interactive dialogs
|
|
734
|
+
this.ui.start();
|
|
735
|
+
this.isInitialized = true;
|
|
736
|
+
// Detect terminal background via OSC 11 if no stronger signal resolved synchronously.
|
|
737
|
+
// The constructor already initialized theme from explicit setting, COLORFGBG, or Apple Terminal
|
|
738
|
+
// heuristic (via getDefaultTheme). This step only fires when no explicit theme is set and
|
|
739
|
+
// detectThemeFromEnvironment() returned undefined (no env signal available).
|
|
740
|
+
if (!this.settingsManager.getTheme() && !detectThemeFromEnvironment()) {
|
|
741
|
+
const rgb = await this.ui.queryTerminalBackgroundColor({ timeoutMs: 100 });
|
|
742
|
+
if (rgb) {
|
|
743
|
+
// SCRAMJET-DIVERGENCE: capture the OSC 11 classification and resolve through the shared
|
|
744
|
+
// resolver so a dark terminal maps to scramjet-dark (when available) and a light one to
|
|
745
|
+
// pi-light. Skips the apply when the resolved name is already active.
|
|
746
|
+
this.detectedClassification = classifyBackgroundColor(rgb);
|
|
747
|
+
this.reapplyResolvedTheme();
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
// Build themed header content (after theme detection so colors are correct)
|
|
406
751
|
if (this.options.verbose || !this.settingsManager.getQuietStartup()) {
|
|
752
|
+
if (this.session.scopedModels.length > 0) {
|
|
753
|
+
const modelList = this.session.scopedModels
|
|
754
|
+
.map(({ model, thinkingLevel }) => `${model.id}${thinkingLevel ? `:${thinkingLevel}` : ""}`)
|
|
755
|
+
.join(", ");
|
|
756
|
+
const cycleKeys = this.keybindings.getKeys("app.model.cycleForward");
|
|
757
|
+
const cycleHint = cycleKeys.length > 0 ? ` (${formatKeyText(cycleKeys.join("/"), { capitalize: true })} to cycle)` : "";
|
|
758
|
+
this.headerContainer.addChild(new Text(theme.fg("dim", `Model scope: ${modelList}${cycleHint}`), 1, 0));
|
|
759
|
+
}
|
|
407
760
|
const logo = theme.bold(theme.fg("accent", APP_NAME)) + theme.fg("dim", ` v${this.version}`);
|
|
408
|
-
// Build startup instructions using keybinding hint helpers
|
|
409
761
|
const hint = (keybinding, description) => keyHint(keybinding, description);
|
|
410
762
|
const expandedInstructions = [
|
|
411
763
|
hint("app.interrupt", "to interrupt"),
|
|
@@ -440,40 +792,22 @@ export class InteractiveMode {
|
|
|
440
792
|
// SCRAMJET-DIVERGENCE: rebrand Pi → APP_NAME.
|
|
441
793
|
`${APP_NAME} can explain its own features and look up its docs. Ask it how to use or extend ${APP_NAME}.`);
|
|
442
794
|
this.builtInHeader = new ExpandableText(() => `${logo}\n${compactInstructions}\n${compactOnboarding}\n\n${onboarding}`, () => `${logo}\n${expandedInstructions}\n\n${onboarding}`, this.getStartupExpansionState(), 1, 0);
|
|
443
|
-
// Setup UI layout
|
|
444
795
|
this.headerContainer.addChild(new Spacer(1));
|
|
445
796
|
this.headerContainer.addChild(this.builtInHeader);
|
|
446
797
|
this.headerContainer.addChild(new Spacer(1));
|
|
447
798
|
}
|
|
448
799
|
else {
|
|
449
|
-
// Minimal header when silenced
|
|
450
800
|
this.builtInHeader = new Text("", 0, 0);
|
|
451
801
|
this.headerContainer.addChild(this.builtInHeader);
|
|
452
802
|
}
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
this.ui.addChild(this.statusContainer);
|
|
456
|
-
this.renderWidgets(); // Initialize with default spacer
|
|
457
|
-
this.ui.addChild(this.widgetContainerAbove);
|
|
458
|
-
this.ui.addChild(this.editorContainer);
|
|
459
|
-
this.ui.addChild(this.widgetContainerBelow);
|
|
460
|
-
this.ui.addChild(this.footer);
|
|
461
|
-
this.ui.setFocus(this.editor);
|
|
462
|
-
this.setupKeyHandlers();
|
|
463
|
-
this.setupEditorSubmitHandler();
|
|
464
|
-
// Start the UI before initializing extensions so session_start handlers can use interactive dialogs
|
|
465
|
-
this.ui.start();
|
|
466
|
-
this.isInitialized = true;
|
|
803
|
+
for (const diagnostic of startupDiagnostics)
|
|
804
|
+
this.headerContainer.addChild(new Text(diagnostic, 1, 0));
|
|
467
805
|
// Initialize extensions first so resources are shown before messages
|
|
468
806
|
await this.rebindCurrentSession();
|
|
469
807
|
// Render initial messages AFTER showing loaded resources
|
|
470
808
|
this.renderInitialMessages();
|
|
471
809
|
// Set up theme file watcher
|
|
472
|
-
|
|
473
|
-
this.ui.invalidate();
|
|
474
|
-
this.updateEditorBorderColor();
|
|
475
|
-
this.ui.requestRender();
|
|
476
|
-
});
|
|
810
|
+
this.bindThemeChangeHandler();
|
|
477
811
|
// Set up git branch watcher (uses provider instead of footer)
|
|
478
812
|
this.footerDataProvider.onBranchChange(() => {
|
|
479
813
|
this.ui.requestRender();
|
|
@@ -485,6 +819,20 @@ export class InteractiveMode {
|
|
|
485
819
|
* Update terminal title with session name and cwd.
|
|
486
820
|
*/
|
|
487
821
|
updateTerminalTitle() {
|
|
822
|
+
// SCRAMJET-DIVERGENCE: delegate to title provider when set.
|
|
823
|
+
if (this.titleProvider) {
|
|
824
|
+
let title;
|
|
825
|
+
try {
|
|
826
|
+
title = this.titleProvider();
|
|
827
|
+
}
|
|
828
|
+
catch {
|
|
829
|
+
// Provider failed; fall through to default title
|
|
830
|
+
}
|
|
831
|
+
if (title !== undefined) {
|
|
832
|
+
this.ui.terminal.setTitle(title);
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
}
|
|
488
836
|
const cwdBasename = path.basename(this.sessionManager.getCwd());
|
|
489
837
|
const sessionName = this.sessionManager.getSessionName();
|
|
490
838
|
if (sessionName) {
|
|
@@ -1112,7 +1460,7 @@ export class InteractiveMode {
|
|
|
1112
1460
|
if (result.cancelled) {
|
|
1113
1461
|
return { cancelled: true };
|
|
1114
1462
|
}
|
|
1115
|
-
this.
|
|
1463
|
+
this.clearTranscript();
|
|
1116
1464
|
this.renderInitialMessages();
|
|
1117
1465
|
if (result.editorText && !this.editor.getText().trim()) {
|
|
1118
1466
|
this.editor.setText(result.editorText);
|
|
@@ -1139,11 +1487,15 @@ export class InteractiveMode {
|
|
|
1139
1487
|
},
|
|
1140
1488
|
});
|
|
1141
1489
|
setRegisteredThemes(this.session.resourceLoader.getThemes().themes);
|
|
1490
|
+
// SCRAMJET-DIVERGENCE: extension themes (e.g. scramjet-dark) register after the constructor's
|
|
1491
|
+
// initTheme, so re-resolve now that they are available.
|
|
1492
|
+
this.reapplyResolvedTheme();
|
|
1142
1493
|
this.setupAutocompleteProvider();
|
|
1143
1494
|
const extensionRunner = this.session.extensionRunner;
|
|
1144
1495
|
this.setupExtensionShortcuts(extensionRunner);
|
|
1145
1496
|
this.showLoadedResources({ force: false, showDiagnosticsWhenQuiet: true });
|
|
1146
1497
|
this.showStartupNoticesIfNeeded();
|
|
1498
|
+
this.promoteFinalizedChatPrefix();
|
|
1147
1499
|
}
|
|
1148
1500
|
applyRuntimeSettings() {
|
|
1149
1501
|
this.footer.setSession(this.session);
|
|
@@ -1151,7 +1503,6 @@ export class InteractiveMode {
|
|
|
1151
1503
|
this.footerDataProvider.setCwd(this.sessionManager.getCwd());
|
|
1152
1504
|
this.hideThinkingBlock = this.settingsManager.getHideThinkingBlock();
|
|
1153
1505
|
this.ui.setShowHardwareCursor(this.settingsManager.getShowHardwareCursor());
|
|
1154
|
-
this.ui.setClearOnShrink(this.settingsManager.getClearOnShrink());
|
|
1155
1506
|
const editorPaddingX = this.settingsManager.getEditorPaddingX();
|
|
1156
1507
|
const autocompleteMaxVisible = this.settingsManager.getAutocompleteMaxVisible();
|
|
1157
1508
|
this.defaultEditor.setPaddingX(editorPaddingX);
|
|
@@ -1196,14 +1547,13 @@ export class InteractiveMode {
|
|
|
1196
1547
|
}
|
|
1197
1548
|
}
|
|
1198
1549
|
async handleFatalRuntimeError(prefix, error) {
|
|
1199
|
-
|
|
1200
|
-
this.showError(`${prefix}: ${message}`);
|
|
1550
|
+
this.showError(`${prefix}: ${describeRuntimeError(error)}`);
|
|
1201
1551
|
stopThemeWatcher();
|
|
1202
1552
|
this.stop();
|
|
1203
1553
|
process.exit(1);
|
|
1204
1554
|
}
|
|
1205
1555
|
renderCurrentSessionState() {
|
|
1206
|
-
this.
|
|
1556
|
+
this.clearTranscript();
|
|
1207
1557
|
this.pendingMessagesContainer.clear();
|
|
1208
1558
|
this.compactionQueuedMessages = [];
|
|
1209
1559
|
this.streamingComponent = undefined;
|
|
@@ -1232,6 +1582,7 @@ export class InteractiveMode {
|
|
|
1232
1582
|
sessionManager: this.sessionManager,
|
|
1233
1583
|
modelRegistry: this.session.modelRegistry,
|
|
1234
1584
|
model: this.session.model,
|
|
1585
|
+
scopedModels: this.session.scopedModels,
|
|
1235
1586
|
isIdle: () => !this.session.isStreaming,
|
|
1236
1587
|
signal: this.session.agent.signal,
|
|
1237
1588
|
abort: () => this.session.abort(),
|
|
@@ -1312,7 +1663,7 @@ export class InteractiveMode {
|
|
|
1312
1663
|
}
|
|
1313
1664
|
setHiddenThinkingLabel(label) {
|
|
1314
1665
|
this.hiddenThinkingLabel = label ?? this.defaultHiddenThinkingLabel;
|
|
1315
|
-
for (const child of this.chatContainer.children) {
|
|
1666
|
+
for (const child of [...this.committedChatContainer.children, ...this.chatContainer.children]) {
|
|
1316
1667
|
if (child instanceof AssistantMessageComponent) {
|
|
1317
1668
|
child.setHiddenThinkingLabel(this.hiddenThinkingLabel);
|
|
1318
1669
|
}
|
|
@@ -1320,7 +1671,7 @@ export class InteractiveMode {
|
|
|
1320
1671
|
if (this.streamingComponent) {
|
|
1321
1672
|
this.streamingComponent.setHiddenThinkingLabel(this.hiddenThinkingLabel);
|
|
1322
1673
|
}
|
|
1323
|
-
this.ui.
|
|
1674
|
+
this.ui.rebuild();
|
|
1324
1675
|
}
|
|
1325
1676
|
/**
|
|
1326
1677
|
* Set an extension widget (string array or custom component).
|
|
@@ -1370,7 +1721,19 @@ export class InteractiveMode {
|
|
|
1370
1721
|
this.extensionWidgetsBelow.clear();
|
|
1371
1722
|
this.renderWidgets();
|
|
1372
1723
|
}
|
|
1724
|
+
// SCRAMJET-DIVERGENCE: height delivery belongs to one opted-in custom invocation, never an inferred component capability.
|
|
1725
|
+
releaseCustomHeight() {
|
|
1726
|
+
const allocation = this.customHeightAllocation;
|
|
1727
|
+
this.customHeightAllocation = undefined;
|
|
1728
|
+
try {
|
|
1729
|
+
allocation?.notify(undefined);
|
|
1730
|
+
}
|
|
1731
|
+
catch (error) {
|
|
1732
|
+
this.showError(`Custom input height release failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1373
1735
|
resetExtensionUI() {
|
|
1736
|
+
this.releaseCustomHeight();
|
|
1374
1737
|
if (this.extensionSelector) {
|
|
1375
1738
|
this.hideExtensionSelector();
|
|
1376
1739
|
}
|
|
@@ -1391,6 +1754,7 @@ export class InteractiveMode {
|
|
|
1391
1754
|
this.setCustomEditorComponent(undefined);
|
|
1392
1755
|
this.setupAutocompleteProvider();
|
|
1393
1756
|
this.defaultEditor.onExtensionShortcut = undefined;
|
|
1757
|
+
this.titleProvider = undefined;
|
|
1394
1758
|
this.updateTerminalTitle();
|
|
1395
1759
|
this.workingMessage = undefined;
|
|
1396
1760
|
this.workingVisible = true;
|
|
@@ -1493,7 +1857,7 @@ export class InteractiveMode {
|
|
|
1493
1857
|
this.headerContainer.children[index] = this.builtInHeader;
|
|
1494
1858
|
}
|
|
1495
1859
|
}
|
|
1496
|
-
this.ui.
|
|
1860
|
+
this.ui.rebuild();
|
|
1497
1861
|
}
|
|
1498
1862
|
addExtensionTerminalInputListener(handler) {
|
|
1499
1863
|
const unsubscribe = this.ui.addInputListener(handler);
|
|
@@ -1533,6 +1897,10 @@ export class InteractiveMode {
|
|
|
1533
1897
|
setFooter: (factory) => this.setExtensionFooter(factory),
|
|
1534
1898
|
setHeader: (factory) => this.setExtensionHeader(factory),
|
|
1535
1899
|
setTitle: (title) => this.ui.terminal.setTitle(title),
|
|
1900
|
+
setTitleProvider: (provider) => {
|
|
1901
|
+
this.titleProvider = provider;
|
|
1902
|
+
this.updateTerminalTitle();
|
|
1903
|
+
},
|
|
1536
1904
|
custom: (factory, options) => this.showExtensionCustom(factory, options),
|
|
1537
1905
|
pasteToEditor: (text) => this.editor.handleInput(`\x1b[200~${text}\x1b[201~`),
|
|
1538
1906
|
setEditorText: (text) => this.editor.setText(text),
|
|
@@ -1737,6 +2105,7 @@ export class InteractiveMode {
|
|
|
1737
2105
|
}
|
|
1738
2106
|
}
|
|
1739
2107
|
this.editor = newEditor;
|
|
2108
|
+
this.configureEditorHeight(this.editor);
|
|
1740
2109
|
}
|
|
1741
2110
|
else {
|
|
1742
2111
|
// Restore default editor with text from custom editor
|
|
@@ -1763,26 +2132,77 @@ export class InteractiveMode {
|
|
|
1763
2132
|
}
|
|
1764
2133
|
/** Show a custom component with keyboard focus. Overlay mode renders on top of existing content. */
|
|
1765
2134
|
async showExtensionCustom(factory, options) {
|
|
2135
|
+
if (options?.onAvailableHeight && (options.overlay || options.toolAttachedContext))
|
|
2136
|
+
throw new Error("Available height is supported only for ordinary custom input controls");
|
|
1766
2137
|
const savedText = this.editor.getText();
|
|
1767
2138
|
const isOverlay = options?.overlay ?? false;
|
|
1768
|
-
const restoreEditor = () => {
|
|
2139
|
+
const restoreEditor = (restoreFocus = true) => {
|
|
1769
2140
|
this.editorContainer.clear();
|
|
1770
2141
|
this.editorContainer.addChild(this.editor);
|
|
1771
2142
|
this.editor.setText(savedText);
|
|
1772
|
-
|
|
2143
|
+
if (restoreFocus)
|
|
2144
|
+
this.ui.setFocus(this.editor);
|
|
1773
2145
|
this.ui.requestRender();
|
|
1774
2146
|
};
|
|
1775
2147
|
return new Promise((resolve, reject) => {
|
|
1776
2148
|
let component;
|
|
2149
|
+
let attachedTool;
|
|
2150
|
+
let committedContext;
|
|
2151
|
+
let removeInputGuard;
|
|
2152
|
+
let releaseHeight = () => { };
|
|
2153
|
+
let revealingControls = false;
|
|
2154
|
+
let needsReveal = false;
|
|
2155
|
+
const retained = this.ui.getViewportState() !== undefined;
|
|
2156
|
+
const pendingFocus = { render: () => [], invalidate() { } };
|
|
2157
|
+
const restoreAttachedFocus = (current) => {
|
|
2158
|
+
if (!retained)
|
|
2159
|
+
return;
|
|
2160
|
+
this.ui.replaceFocus(pendingFocus, current ? this.editor : null);
|
|
2161
|
+
if (attachedTool)
|
|
2162
|
+
this.ui.replaceFocus(attachedTool, current ? this.editor : null);
|
|
2163
|
+
};
|
|
1777
2164
|
let closed = false;
|
|
2165
|
+
const attachmentCurrent = () => attachedTool &&
|
|
2166
|
+
committedContext &&
|
|
2167
|
+
this.pendingTools.get(options.toolAttachedContext.toolCallId) === attachedTool &&
|
|
2168
|
+
this.chatContainer.children.includes(attachedTool) &&
|
|
2169
|
+
this.committedChatContainer.children.includes(committedContext);
|
|
2170
|
+
const fail = (error) => {
|
|
2171
|
+
if (closed)
|
|
2172
|
+
return;
|
|
2173
|
+
closed = true;
|
|
2174
|
+
removeInputGuard?.();
|
|
2175
|
+
releaseHeight();
|
|
2176
|
+
const current = !attachedTool || this.chatContainer.children.includes(attachedTool);
|
|
2177
|
+
attachedTool?.cancelCommittedContext();
|
|
2178
|
+
if (committedContext) {
|
|
2179
|
+
this.committedChatContainer.removeChild(committedContext);
|
|
2180
|
+
this.ui.rebuild();
|
|
2181
|
+
}
|
|
2182
|
+
try {
|
|
2183
|
+
component?.dispose?.();
|
|
2184
|
+
}
|
|
2185
|
+
catch { }
|
|
2186
|
+
if (attachedTool)
|
|
2187
|
+
restoreAttachedFocus(current);
|
|
2188
|
+
if (!isOverlay && current)
|
|
2189
|
+
restoreEditor(!attachedTool || !retained);
|
|
2190
|
+
reject(error);
|
|
2191
|
+
};
|
|
1778
2192
|
const close = (result) => {
|
|
1779
2193
|
if (closed)
|
|
1780
2194
|
return;
|
|
1781
2195
|
closed = true;
|
|
2196
|
+
removeInputGuard?.();
|
|
2197
|
+
releaseHeight();
|
|
2198
|
+
attachedTool?.detachCommittedContext();
|
|
2199
|
+
const current = !attachedTool || this.chatContainer.children.includes(attachedTool);
|
|
2200
|
+
if (attachedTool)
|
|
2201
|
+
restoreAttachedFocus(current);
|
|
1782
2202
|
if (isOverlay)
|
|
1783
2203
|
this.ui.hideOverlay();
|
|
1784
|
-
else
|
|
1785
|
-
restoreEditor();
|
|
2204
|
+
else if (current)
|
|
2205
|
+
restoreEditor(!attachedTool || !retained);
|
|
1786
2206
|
// Note: both branches above already call requestRender
|
|
1787
2207
|
resolve(result);
|
|
1788
2208
|
try {
|
|
@@ -1793,10 +2213,90 @@ export class InteractiveMode {
|
|
|
1793
2213
|
}
|
|
1794
2214
|
};
|
|
1795
2215
|
Promise.resolve(factory(this.ui, theme, this.keybindings, close))
|
|
1796
|
-
.then((c) => {
|
|
2216
|
+
.then(async (c) => {
|
|
1797
2217
|
if (closed)
|
|
1798
2218
|
return;
|
|
1799
2219
|
component = c;
|
|
2220
|
+
const attachment = options?.toolAttachedContext;
|
|
2221
|
+
if (attachment) {
|
|
2222
|
+
if (isOverlay)
|
|
2223
|
+
throw new Error("Tool-attached context cannot be shown as an overlay");
|
|
2224
|
+
const tool = this.pendingTools.get(attachment.toolCallId);
|
|
2225
|
+
const orderingBarrier = this.chatContainer.children.find((child) => this.mutableChatComponents.has(child));
|
|
2226
|
+
if (!tool || !this.chatContainer.children.includes(tool) || orderingBarrier !== tool) {
|
|
2227
|
+
throw new Error("Tool-attached context requires the current pending tool row");
|
|
2228
|
+
}
|
|
2229
|
+
attachedTool = tool;
|
|
2230
|
+
committedContext = attachment.render(this.ui, theme);
|
|
2231
|
+
this.committedChatContainer.addChild(committedContext);
|
|
2232
|
+
tool.attachCommittedContext(component);
|
|
2233
|
+
if (retained)
|
|
2234
|
+
this.ui.replaceFocus(this.editor, pendingFocus);
|
|
2235
|
+
else
|
|
2236
|
+
this.ui.setFocus(null);
|
|
2237
|
+
this.editorContainer.clear();
|
|
2238
|
+
const reveal = async () => {
|
|
2239
|
+
revealingControls = true;
|
|
2240
|
+
this.ui.replaceFocus(tool, pendingFocus);
|
|
2241
|
+
if (retained) {
|
|
2242
|
+
this.ui.revealComponent(tool);
|
|
2243
|
+
await this.ui.renderNow({ requireFlush: true });
|
|
2244
|
+
}
|
|
2245
|
+
else
|
|
2246
|
+
await this.ui.commitNow({ requireFlush: true });
|
|
2247
|
+
if (closed)
|
|
2248
|
+
return;
|
|
2249
|
+
if (!attachmentCurrent())
|
|
2250
|
+
throw new Error("Tool-attached context was replaced before flush settled");
|
|
2251
|
+
if (retained && !this.ui.isViewportFrameFlushed()) {
|
|
2252
|
+
needsReveal = true;
|
|
2253
|
+
revealingControls = false;
|
|
2254
|
+
return;
|
|
2255
|
+
}
|
|
2256
|
+
if (retained &&
|
|
2257
|
+
(!this.ui.isComponentRenderComplete(committedContext) ||
|
|
2258
|
+
!this.ui.isComponentRenderComplete(tool)))
|
|
2259
|
+
throw new Error("Approval context or controls could not be rendered completely");
|
|
2260
|
+
const visibility = retained ? this.ui.getComponentVisibility(tool) : "visible";
|
|
2261
|
+
if (visibility === "outside")
|
|
2262
|
+
throw new Error("Approval controls do not fit in the visible viewport");
|
|
2263
|
+
needsReveal = visibility === "occluded";
|
|
2264
|
+
revealingControls = false;
|
|
2265
|
+
if (retained)
|
|
2266
|
+
this.ui.replaceFocus(pendingFocus, tool);
|
|
2267
|
+
else
|
|
2268
|
+
this.ui.setFocus(tool);
|
|
2269
|
+
};
|
|
2270
|
+
if (retained) {
|
|
2271
|
+
// SCRAMJET-DIVERGENCE: occluded controls require a fresh reveal/flush, never overlay focus theft.
|
|
2272
|
+
removeInputGuard = this.ui.addInputListener((data) => {
|
|
2273
|
+
if (isKeyRelease(data) || /^\x1b\[\d+;\d+;\d+t$/.test(data))
|
|
2274
|
+
return undefined;
|
|
2275
|
+
if (!this.ui.isComponentFocused(tool) && !this.ui.isComponentFocused(pendingFocus))
|
|
2276
|
+
return undefined;
|
|
2277
|
+
if (!attachmentCurrent()) {
|
|
2278
|
+
fail(new Error("Tool-attached context is no longer current"));
|
|
2279
|
+
return { consume: true };
|
|
2280
|
+
}
|
|
2281
|
+
if (revealingControls)
|
|
2282
|
+
return { consume: true };
|
|
2283
|
+
if (!needsReveal &&
|
|
2284
|
+
this.ui.isViewportFrameFlushed() &&
|
|
2285
|
+
this.ui.isComponentVisible(tool) &&
|
|
2286
|
+
this.ui.isComponentRenderComplete(committedContext) &&
|
|
2287
|
+
this.ui.isComponentRenderComplete(tool))
|
|
2288
|
+
return undefined;
|
|
2289
|
+
if (!this.ui.hasOverlay())
|
|
2290
|
+
void reveal().catch(fail);
|
|
2291
|
+
return { consume: true };
|
|
2292
|
+
});
|
|
2293
|
+
}
|
|
2294
|
+
await reveal();
|
|
2295
|
+
if (closed)
|
|
2296
|
+
return;
|
|
2297
|
+
this.ui.requestRender();
|
|
2298
|
+
return;
|
|
2299
|
+
}
|
|
1800
2300
|
if (isOverlay) {
|
|
1801
2301
|
// Resolve overlay options - can be static or dynamic function
|
|
1802
2302
|
const resolveOptions = () => {
|
|
@@ -1815,19 +2315,23 @@ export class InteractiveMode {
|
|
|
1815
2315
|
options?.onHandle?.(handle);
|
|
1816
2316
|
}
|
|
1817
2317
|
else {
|
|
2318
|
+
this.releaseCustomHeight();
|
|
2319
|
+
if (options?.onAvailableHeight) {
|
|
2320
|
+
const allocation = { component, notify: options.onAvailableHeight, fail };
|
|
2321
|
+
this.customHeightAllocation = allocation;
|
|
2322
|
+
releaseHeight = () => {
|
|
2323
|
+
if (this.customHeightAllocation === allocation)
|
|
2324
|
+
this.releaseCustomHeight();
|
|
2325
|
+
};
|
|
2326
|
+
allocation.notify(undefined);
|
|
2327
|
+
}
|
|
1818
2328
|
this.editorContainer.clear();
|
|
1819
2329
|
this.editorContainer.addChild(component);
|
|
1820
2330
|
this.ui.setFocus(component);
|
|
1821
2331
|
this.ui.requestRender();
|
|
1822
2332
|
}
|
|
1823
2333
|
})
|
|
1824
|
-
.catch(
|
|
1825
|
-
if (closed)
|
|
1826
|
-
return;
|
|
1827
|
-
if (!isOverlay)
|
|
1828
|
-
restoreEditor();
|
|
1829
|
-
reject(err);
|
|
1830
|
-
});
|
|
2334
|
+
.catch(fail);
|
|
1831
2335
|
});
|
|
1832
2336
|
}
|
|
1833
2337
|
/**
|
|
@@ -1848,7 +2352,7 @@ export class InteractiveMode {
|
|
|
1848
2352
|
this.chatContainer.addChild(new Text(stackLines, 1, 0));
|
|
1849
2353
|
}
|
|
1850
2354
|
}
|
|
1851
|
-
this.
|
|
2355
|
+
this.commitFinalizedChatOutput();
|
|
1852
2356
|
}
|
|
1853
2357
|
// =========================================================================
|
|
1854
2358
|
// Key Handlers
|
|
@@ -1857,6 +2361,11 @@ export class InteractiveMode {
|
|
|
1857
2361
|
// Set up handlers on defaultEditor - they use this.editor for text access
|
|
1858
2362
|
// so they work correctly regardless of which editor is active
|
|
1859
2363
|
this.defaultEditor.onEscape = () => {
|
|
2364
|
+
if (this.pendingSelectorOpenGeneration !== undefined) {
|
|
2365
|
+
this.selectorOpenGeneration++;
|
|
2366
|
+
this.pendingSelectorOpenGeneration = undefined;
|
|
2367
|
+
return;
|
|
2368
|
+
}
|
|
1860
2369
|
if (this.session.isStreaming) {
|
|
1861
2370
|
this.restoreQueuedMessagesToEditor({ abort: true });
|
|
1862
2371
|
}
|
|
@@ -1908,6 +2417,7 @@ export class InteractiveMode {
|
|
|
1908
2417
|
this.defaultEditor.onAction("app.session.fork", () => this.showUserMessageSelector());
|
|
1909
2418
|
this.defaultEditor.onAction("app.session.resume", () => this.showSessionSelector());
|
|
1910
2419
|
this.defaultEditor.onChange = (text) => {
|
|
2420
|
+
this.clipboardPasteGeneration++;
|
|
1911
2421
|
const wasBashMode = this.isBashMode;
|
|
1912
2422
|
this.isBashMode = text.trimStart().startsWith("!");
|
|
1913
2423
|
if (wasBashMode !== this.isBashMode) {
|
|
@@ -1944,6 +2454,8 @@ export class InteractiveMode {
|
|
|
1944
2454
|
text = text.trim();
|
|
1945
2455
|
if (!text)
|
|
1946
2456
|
return;
|
|
2457
|
+
// SCRAMJET-DIVERGENCE: submission intent precedes command routing and queued-message delivery.
|
|
2458
|
+
this.ui.followViewport();
|
|
1947
2459
|
// Handle commands
|
|
1948
2460
|
if (text === "/settings") {
|
|
1949
2461
|
this.showSettingsSelector();
|
|
@@ -2122,7 +2634,9 @@ export class InteractiveMode {
|
|
|
2122
2634
|
this.footer.invalidate();
|
|
2123
2635
|
switch (event.type) {
|
|
2124
2636
|
case "agent_start":
|
|
2637
|
+
this.agentRunGeneration += 1;
|
|
2125
2638
|
this.pendingTools.clear();
|
|
2639
|
+
this.pendingToolFinalizations = new Set();
|
|
2126
2640
|
if (this.settingsManager.getShowTerminalProgress()) {
|
|
2127
2641
|
this.ui.terminal.setProgress(true);
|
|
2128
2642
|
}
|
|
@@ -2161,19 +2675,22 @@ export class InteractiveMode {
|
|
|
2161
2675
|
this.updateEditorBorderColor();
|
|
2162
2676
|
break;
|
|
2163
2677
|
case "message_start":
|
|
2678
|
+
this.sealStatus();
|
|
2164
2679
|
if (event.message.role === "custom") {
|
|
2165
2680
|
this.addMessageToChat(event.message);
|
|
2166
2681
|
this.ui.requestRender();
|
|
2167
2682
|
}
|
|
2168
2683
|
else if (event.message.role === "user") {
|
|
2169
2684
|
this.addMessageToChat(event.message);
|
|
2685
|
+
this.ui.followViewport();
|
|
2170
2686
|
this.updatePendingMessagesDisplay();
|
|
2171
2687
|
this.ui.requestRender();
|
|
2172
2688
|
}
|
|
2173
2689
|
else if (event.message.role === "assistant") {
|
|
2174
|
-
this.streamingComponent = new AssistantMessageComponent(undefined, this.hideThinkingBlock, this.getMarkdownThemeWithSettings(), this.hiddenThinkingLabel);
|
|
2690
|
+
this.streamingComponent = new AssistantMessageComponent(undefined, this.hideThinkingBlock, this.getMarkdownThemeWithSettings(), this.hiddenThinkingLabel, false);
|
|
2175
2691
|
this.streamingMessage = event.message;
|
|
2176
2692
|
this.chatContainer.addChild(this.streamingComponent);
|
|
2693
|
+
this.setChatComponentMutable(this.streamingComponent, true);
|
|
2177
2694
|
this.streamingComponent.updateContent(this.streamingMessage);
|
|
2178
2695
|
this.ui.requestRender();
|
|
2179
2696
|
}
|
|
@@ -2191,6 +2708,7 @@ export class InteractiveMode {
|
|
|
2191
2708
|
}, this.getRegisteredToolDefinition(content.name), this.ui, this.sessionManager.getCwd());
|
|
2192
2709
|
component.setExpanded(this.toolOutputExpanded);
|
|
2193
2710
|
this.chatContainer.addChild(component);
|
|
2711
|
+
this.setChatComponentMutable(component, true);
|
|
2194
2712
|
this.pendingTools.set(content.id, component);
|
|
2195
2713
|
}
|
|
2196
2714
|
else {
|
|
@@ -2219,6 +2737,8 @@ export class InteractiveMode {
|
|
|
2219
2737
|
this.streamingMessage.errorMessage = errorMessage;
|
|
2220
2738
|
}
|
|
2221
2739
|
this.streamingComponent.updateContent(this.streamingMessage);
|
|
2740
|
+
this.streamingComponent.setFinalized(true);
|
|
2741
|
+
this.setChatComponentMutable(this.streamingComponent, false);
|
|
2222
2742
|
if (this.streamingMessage.stopReason === "aborted" || this.streamingMessage.stopReason === "error") {
|
|
2223
2743
|
if (!errorMessage) {
|
|
2224
2744
|
errorMessage = this.streamingMessage.errorMessage || "Error";
|
|
@@ -2228,6 +2748,8 @@ export class InteractiveMode {
|
|
|
2228
2748
|
content: [{ type: "text", text: errorMessage }],
|
|
2229
2749
|
isError: true,
|
|
2230
2750
|
});
|
|
2751
|
+
component.seal();
|
|
2752
|
+
this.setChatComponentMutable(component, false);
|
|
2231
2753
|
}
|
|
2232
2754
|
this.pendingTools.clear();
|
|
2233
2755
|
}
|
|
@@ -2237,6 +2759,7 @@ export class InteractiveMode {
|
|
|
2237
2759
|
component.setArgsComplete();
|
|
2238
2760
|
}
|
|
2239
2761
|
}
|
|
2762
|
+
this.promoteFinalizedChatPrefix();
|
|
2240
2763
|
this.streamingComponent = undefined;
|
|
2241
2764
|
this.streamingMessage = undefined;
|
|
2242
2765
|
this.footer.invalidate();
|
|
@@ -2244,6 +2767,7 @@ export class InteractiveMode {
|
|
|
2244
2767
|
this.ui.requestRender();
|
|
2245
2768
|
break;
|
|
2246
2769
|
case "tool_execution_start": {
|
|
2770
|
+
this.sealStatus();
|
|
2247
2771
|
let component = this.pendingTools.get(event.toolCallId);
|
|
2248
2772
|
if (!component) {
|
|
2249
2773
|
component = new ToolExecutionComponent(event.toolName, event.toolCallId, event.args, {
|
|
@@ -2252,6 +2776,7 @@ export class InteractiveMode {
|
|
|
2252
2776
|
}, this.getRegisteredToolDefinition(event.toolName), this.ui, this.sessionManager.getCwd());
|
|
2253
2777
|
component.setExpanded(this.toolOutputExpanded);
|
|
2254
2778
|
this.chatContainer.addChild(component);
|
|
2779
|
+
this.setChatComponentMutable(component, true);
|
|
2255
2780
|
this.pendingTools.set(event.toolCallId, component);
|
|
2256
2781
|
}
|
|
2257
2782
|
component.markExecutionStarted();
|
|
@@ -2270,12 +2795,34 @@ export class InteractiveMode {
|
|
|
2270
2795
|
const component = this.pendingTools.get(event.toolCallId);
|
|
2271
2796
|
if (component) {
|
|
2272
2797
|
component.updateResult({ ...event.result, isError: event.isError });
|
|
2273
|
-
this.
|
|
2274
|
-
|
|
2798
|
+
const pendingToolFinalizations = this.pendingToolFinalizations;
|
|
2799
|
+
const finalization = component.waitForImageConversions().then(() => {
|
|
2800
|
+
component.seal();
|
|
2801
|
+
if (this.pendingTools.get(event.toolCallId) === component) {
|
|
2802
|
+
this.pendingTools.delete(event.toolCallId);
|
|
2803
|
+
}
|
|
2804
|
+
if (this.mutableChatComponents.has(component) && this.chatContainer.children.includes(component)) {
|
|
2805
|
+
this.setChatComponentMutable(component, false);
|
|
2806
|
+
this.promoteFinalizedChatPrefix();
|
|
2807
|
+
this.ui.requestRender();
|
|
2808
|
+
}
|
|
2809
|
+
});
|
|
2810
|
+
pendingToolFinalizations.add(finalization);
|
|
2811
|
+
try {
|
|
2812
|
+
await finalization;
|
|
2813
|
+
}
|
|
2814
|
+
finally {
|
|
2815
|
+
pendingToolFinalizations.delete(finalization);
|
|
2816
|
+
}
|
|
2275
2817
|
}
|
|
2276
2818
|
break;
|
|
2277
2819
|
}
|
|
2278
|
-
case "agent_end":
|
|
2820
|
+
case "agent_end": {
|
|
2821
|
+
const runGeneration = this.agentRunGeneration;
|
|
2822
|
+
const pendingToolFinalizations = this.pendingToolFinalizations;
|
|
2823
|
+
await Promise.all(pendingToolFinalizations);
|
|
2824
|
+
if (runGeneration !== this.agentRunGeneration || pendingToolFinalizations !== this.pendingToolFinalizations)
|
|
2825
|
+
break;
|
|
2279
2826
|
if (this.settingsManager.getShowTerminalProgress()) {
|
|
2280
2827
|
this.ui.terminal.setProgress(false);
|
|
2281
2828
|
}
|
|
@@ -2286,13 +2833,20 @@ export class InteractiveMode {
|
|
|
2286
2833
|
}
|
|
2287
2834
|
if (this.streamingComponent) {
|
|
2288
2835
|
this.chatContainer.removeChild(this.streamingComponent);
|
|
2836
|
+
this.setChatComponentMutable(this.streamingComponent, false);
|
|
2289
2837
|
this.streamingComponent = undefined;
|
|
2290
2838
|
this.streamingMessage = undefined;
|
|
2291
2839
|
}
|
|
2840
|
+
for (const component of this.pendingTools.values()) {
|
|
2841
|
+
component.seal();
|
|
2842
|
+
this.setChatComponentMutable(component, false);
|
|
2843
|
+
}
|
|
2292
2844
|
this.pendingTools.clear();
|
|
2845
|
+
this.promoteFinalizedChatPrefix();
|
|
2293
2846
|
await this.checkShutdownRequested();
|
|
2294
2847
|
this.ui.requestRender();
|
|
2295
2848
|
break;
|
|
2849
|
+
}
|
|
2296
2850
|
case "compaction_start": {
|
|
2297
2851
|
if (this.settingsManager.getShowTerminalProgress()) {
|
|
2298
2852
|
this.ui.terminal.setProgress(true);
|
|
@@ -2334,7 +2888,6 @@ export class InteractiveMode {
|
|
|
2334
2888
|
}
|
|
2335
2889
|
}
|
|
2336
2890
|
else if (event.result) {
|
|
2337
|
-
this.chatContainer.clear();
|
|
2338
2891
|
this.rebuildChatFromMessages();
|
|
2339
2892
|
this.addMessageToChat(createCompactionSummaryMessage(event.result.summary, event.result.tokensBefore, new Date().toISOString()));
|
|
2340
2893
|
this.footer.invalidate();
|
|
@@ -2346,6 +2899,7 @@ export class InteractiveMode {
|
|
|
2346
2899
|
else {
|
|
2347
2900
|
this.chatContainer.addChild(new Spacer(1));
|
|
2348
2901
|
this.chatContainer.addChild(new Text(theme.fg("error", event.errorMessage), 1, 0));
|
|
2902
|
+
this.commitFinalizedChatOutput();
|
|
2349
2903
|
}
|
|
2350
2904
|
}
|
|
2351
2905
|
void this.flushCompactionQueue({ willRetry: event.willRetry });
|
|
@@ -2361,7 +2915,13 @@ export class InteractiveMode {
|
|
|
2361
2915
|
// Show retry indicator
|
|
2362
2916
|
this.statusContainer.clear();
|
|
2363
2917
|
this.retryCountdown?.dispose();
|
|
2364
|
-
const retryMessage = (seconds) =>
|
|
2918
|
+
const retryMessage = (seconds) => {
|
|
2919
|
+
const base = `Retrying (${event.attempt}/${event.maxAttempts}) in ${seconds}s...`;
|
|
2920
|
+
const cumulative = event.cumulativeErrors && event.cumulativeErrors > 1
|
|
2921
|
+
? ` [${event.cumulativeErrors} total failures this prompt]`
|
|
2922
|
+
: "";
|
|
2923
|
+
return `${base}${cumulative} (${keyText("app.interrupt")} to cancel)`;
|
|
2924
|
+
};
|
|
2365
2925
|
this.retryLoader = new Loader(this.ui, (spinner) => theme.fg("warning", spinner), (text) => theme.fg("muted", text), retryMessage(Math.ceil(event.delayMs / 1000)));
|
|
2366
2926
|
this.retryCountdown = new CountdownTimer(event.delayMs, this.ui, (seconds) => {
|
|
2367
2927
|
this.retryLoader?.setMessage(retryMessage(seconds));
|
|
@@ -2425,11 +2985,14 @@ export class InteractiveMode {
|
|
|
2425
2985
|
const text = new Text(theme.fg("dim", message), 1, 0);
|
|
2426
2986
|
this.chatContainer.addChild(spacer);
|
|
2427
2987
|
this.chatContainer.addChild(text);
|
|
2988
|
+
this.setChatComponentMutable(spacer, true);
|
|
2989
|
+
this.setChatComponentMutable(text, true);
|
|
2428
2990
|
this.lastStatusSpacer = spacer;
|
|
2429
2991
|
this.lastStatusText = text;
|
|
2430
2992
|
this.ui.requestRender();
|
|
2431
2993
|
}
|
|
2432
2994
|
addMessageToChat(message, options) {
|
|
2995
|
+
this.sealStatus();
|
|
2433
2996
|
switch (message.role) {
|
|
2434
2997
|
case "bashExecution": {
|
|
2435
2998
|
const component = new BashExecutionComponent(message.command, this.ui, message.excludeFromContext);
|
|
@@ -2480,9 +3043,10 @@ export class InteractiveMode {
|
|
|
2480
3043
|
this.chatContainer.addChild(userComponent);
|
|
2481
3044
|
}
|
|
2482
3045
|
if (options?.populateHistory) {
|
|
2483
|
-
const compactForm =
|
|
2484
|
-
|
|
2485
|
-
|
|
3046
|
+
const compactForm = options.historyText ??
|
|
3047
|
+
(scramjetBlock.userContext
|
|
3048
|
+
? `/${scramjetBlock.name} ${scramjetBlock.userContext}`
|
|
3049
|
+
: `/${scramjetBlock.name}`);
|
|
2486
3050
|
this.editor.addToHistory?.(compactForm);
|
|
2487
3051
|
}
|
|
2488
3052
|
}
|
|
@@ -2521,6 +3085,7 @@ export class InteractiveMode {
|
|
|
2521
3085
|
const _exhaustive = message;
|
|
2522
3086
|
}
|
|
2523
3087
|
}
|
|
3088
|
+
this.promoteFinalizedChatPrefix();
|
|
2524
3089
|
}
|
|
2525
3090
|
/**
|
|
2526
3091
|
* Render session context to chat. Used for initial load and rebuild after compaction.
|
|
@@ -2531,6 +3096,11 @@ export class InteractiveMode {
|
|
|
2531
3096
|
renderSessionContext(sessionContext, options = {}) {
|
|
2532
3097
|
this.pendingTools.clear();
|
|
2533
3098
|
const renderedPendingTools = new Map();
|
|
3099
|
+
const entries = options.populateHistory ? this.sessionManager.getEntries() : [];
|
|
3100
|
+
const commandStarts = indexScramjetCommandStarts(entries);
|
|
3101
|
+
const entriesByMessage = new Map(entries
|
|
3102
|
+
.filter((entry) => entry.type === "message")
|
|
3103
|
+
.map((entry) => [entry.message, entry]));
|
|
2534
3104
|
if (options.updateFooter) {
|
|
2535
3105
|
this.footer.invalidate();
|
|
2536
3106
|
this.updateEditorBorderColor();
|
|
@@ -2548,6 +3118,7 @@ export class InteractiveMode {
|
|
|
2548
3118
|
}, this.getRegisteredToolDefinition(content.name), this.ui, this.sessionManager.getCwd());
|
|
2549
3119
|
component.setExpanded(this.toolOutputExpanded);
|
|
2550
3120
|
this.chatContainer.addChild(component);
|
|
3121
|
+
this.setChatComponentMutable(component, true);
|
|
2551
3122
|
if (message.stopReason === "aborted" || message.stopReason === "error") {
|
|
2552
3123
|
let errorMessage;
|
|
2553
3124
|
if (message.stopReason === "aborted") {
|
|
@@ -2561,6 +3132,8 @@ export class InteractiveMode {
|
|
|
2561
3132
|
errorMessage = message.errorMessage || "Error";
|
|
2562
3133
|
}
|
|
2563
3134
|
component.updateResult({ content: [{ type: "text", text: errorMessage }], isError: true });
|
|
3135
|
+
component.seal();
|
|
3136
|
+
this.setChatComponentMutable(component, false);
|
|
2564
3137
|
}
|
|
2565
3138
|
else {
|
|
2566
3139
|
renderedPendingTools.set(content.id, component);
|
|
@@ -2572,18 +3145,45 @@ export class InteractiveMode {
|
|
|
2572
3145
|
// Match tool results to pending tool components
|
|
2573
3146
|
const component = renderedPendingTools.get(message.toolCallId);
|
|
2574
3147
|
if (component) {
|
|
3148
|
+
component.markExecutionStarted();
|
|
3149
|
+
component.setArgsComplete();
|
|
2575
3150
|
component.updateResult(message);
|
|
2576
3151
|
renderedPendingTools.delete(message.toolCallId);
|
|
3152
|
+
if (component.hasPendingImageConversions()) {
|
|
3153
|
+
const pendingToolFinalizations = this.pendingToolFinalizations;
|
|
3154
|
+
const finalization = component.waitForImageConversions().then(() => {
|
|
3155
|
+
if (!this.mutableChatComponents.has(component) ||
|
|
3156
|
+
!this.chatContainer.children.includes(component)) {
|
|
3157
|
+
component.seal();
|
|
3158
|
+
return;
|
|
3159
|
+
}
|
|
3160
|
+
component.seal();
|
|
3161
|
+
this.setChatComponentMutable(component, false);
|
|
3162
|
+
this.promoteFinalizedChatPrefix();
|
|
3163
|
+
this.ui.requestRender();
|
|
3164
|
+
});
|
|
3165
|
+
pendingToolFinalizations.add(finalization);
|
|
3166
|
+
void finalization.finally(() => pendingToolFinalizations.delete(finalization));
|
|
3167
|
+
}
|
|
3168
|
+
else {
|
|
3169
|
+
component.seal();
|
|
3170
|
+
this.setChatComponentMutable(component, false);
|
|
3171
|
+
}
|
|
2577
3172
|
}
|
|
2578
3173
|
}
|
|
2579
3174
|
else {
|
|
2580
3175
|
// All other messages use standard rendering
|
|
2581
|
-
|
|
3176
|
+
const entry = entriesByMessage.get(message);
|
|
3177
|
+
this.addMessageToChat(message, {
|
|
3178
|
+
...options,
|
|
3179
|
+
historyText: entry ? restoreScramjetCommandInvocation(entry, commandStarts) : undefined,
|
|
3180
|
+
});
|
|
2582
3181
|
}
|
|
2583
3182
|
}
|
|
2584
3183
|
for (const [toolCallId, component] of renderedPendingTools) {
|
|
2585
3184
|
this.pendingTools.set(toolCallId, component);
|
|
2586
3185
|
}
|
|
3186
|
+
this.promoteFinalizedChatPrefix();
|
|
2587
3187
|
this.ui.requestRender();
|
|
2588
3188
|
}
|
|
2589
3189
|
renderInitialMessages() {
|
|
@@ -2610,7 +3210,7 @@ export class InteractiveMode {
|
|
|
2610
3210
|
});
|
|
2611
3211
|
}
|
|
2612
3212
|
rebuildChatFromMessages() {
|
|
2613
|
-
this.
|
|
3213
|
+
this.clearTranscript();
|
|
2614
3214
|
const context = this.sessionManager.buildSessionContext();
|
|
2615
3215
|
this.renderSessionContext(context);
|
|
2616
3216
|
}
|
|
@@ -2645,9 +3245,18 @@ export class InteractiveMode {
|
|
|
2645
3245
|
// Drain any in-flight Kitty key release events before stopping.
|
|
2646
3246
|
// This prevents escape sequences from leaking to the parent shell over slow SSH.
|
|
2647
3247
|
await this.ui.terminal.drainInput(1000);
|
|
2648
|
-
this.
|
|
3248
|
+
await this.settingsManager.flush();
|
|
3249
|
+
let exitCode = 0;
|
|
3250
|
+
try {
|
|
3251
|
+
this.stop({ retainContent: this.settingsManager.getRetainTranscriptOnExit() });
|
|
3252
|
+
}
|
|
3253
|
+
catch (error) {
|
|
3254
|
+
exitCode = 1;
|
|
3255
|
+
console.error(`Could not retain terminal history: ${error instanceof Error ? error.message : String(error)}`);
|
|
3256
|
+
}
|
|
3257
|
+
await this.ui.terminal.flush?.();
|
|
2649
3258
|
await this.runtimeHost.dispose();
|
|
2650
|
-
process.exit(
|
|
3259
|
+
process.exit(exitCode);
|
|
2651
3260
|
}
|
|
2652
3261
|
emergencyTerminalExit() {
|
|
2653
3262
|
this.isShuttingDown = true;
|
|
@@ -2737,7 +3346,7 @@ export class InteractiveMode {
|
|
|
2737
3346
|
}
|
|
2738
3347
|
this.signalCleanupHandlers = [];
|
|
2739
3348
|
}
|
|
2740
|
-
handleCtrlZ() {
|
|
3349
|
+
async handleCtrlZ() {
|
|
2741
3350
|
if (process.platform === "win32") {
|
|
2742
3351
|
this.showStatus("Suspend to background is not supported on Windows");
|
|
2743
3352
|
return;
|
|
@@ -2755,10 +3364,11 @@ export class InteractiveMode {
|
|
|
2755
3364
|
clearInterval(suspendKeepAlive);
|
|
2756
3365
|
process.removeListener("SIGINT", ignoreSigint);
|
|
2757
3366
|
this.ui.start();
|
|
2758
|
-
this.ui.
|
|
3367
|
+
this.ui.rebuild();
|
|
2759
3368
|
});
|
|
2760
3369
|
try {
|
|
2761
|
-
//
|
|
3370
|
+
// SCRAMJET-DIVERGENCE: drain releases before handing keyboard ownership back to the shell.
|
|
3371
|
+
await this.ui.terminal.drainInput();
|
|
2762
3372
|
this.ui.stop();
|
|
2763
3373
|
// Send SIGTSTP to process group (pid=0 means all processes in group)
|
|
2764
3374
|
process.kill(0, "SIGTSTP");
|
|
@@ -2773,6 +3383,7 @@ export class InteractiveMode {
|
|
|
2773
3383
|
const text = (this.editor.getExpandedText?.() ?? this.editor.getText()).trim();
|
|
2774
3384
|
if (!text)
|
|
2775
3385
|
return;
|
|
3386
|
+
this.ui.followViewport();
|
|
2776
3387
|
// Queue input during compaction (extension commands execute immediately)
|
|
2777
3388
|
if (this.session.isCompacting) {
|
|
2778
3389
|
if (this.isExtensionCommand(text)) {
|
|
@@ -2858,28 +3469,28 @@ export class InteractiveMode {
|
|
|
2858
3469
|
if (isExpandable(activeHeader)) {
|
|
2859
3470
|
activeHeader.setExpanded(expanded);
|
|
2860
3471
|
}
|
|
2861
|
-
for (const child of this.chatContainer.children) {
|
|
3472
|
+
for (const child of [...this.committedChatContainer.children, ...this.chatContainer.children]) {
|
|
2862
3473
|
if (isExpandable(child)) {
|
|
2863
3474
|
child.setExpanded(expanded);
|
|
2864
3475
|
}
|
|
2865
3476
|
}
|
|
2866
|
-
this.ui.
|
|
3477
|
+
this.ui.rebuild();
|
|
2867
3478
|
}
|
|
2868
3479
|
toggleThinkingBlockVisibility() {
|
|
2869
|
-
this.
|
|
2870
|
-
this.settingsManager.setHideThinkingBlock(this.hideThinkingBlock);
|
|
2871
|
-
// Rebuild chat from session messages
|
|
2872
|
-
this.chatContainer.clear();
|
|
2873
|
-
this.rebuildChatFromMessages();
|
|
2874
|
-
// If streaming, re-add the streaming component with updated visibility and re-render
|
|
2875
|
-
if (this.streamingComponent && this.streamingMessage) {
|
|
2876
|
-
this.streamingComponent.setHideThinkingBlock(this.hideThinkingBlock);
|
|
2877
|
-
this.streamingComponent.updateContent(this.streamingMessage);
|
|
2878
|
-
this.chatContainer.addChild(this.streamingComponent);
|
|
2879
|
-
}
|
|
3480
|
+
this.setThinkingBlockVisibility(!this.hideThinkingBlock);
|
|
2880
3481
|
this.showStatus(`Thinking blocks: ${this.hideThinkingBlock ? "hidden" : "visible"}`);
|
|
2881
3482
|
}
|
|
2882
|
-
|
|
3483
|
+
// SCRAMJET-DIVERGENCE: presentation changes retain live tools and browsing identities.
|
|
3484
|
+
setThinkingBlockVisibility(hidden) {
|
|
3485
|
+
this.hideThinkingBlock = hidden;
|
|
3486
|
+
this.settingsManager.setHideThinkingBlock(hidden);
|
|
3487
|
+
for (const child of [...this.committedChatContainer.children, ...this.chatContainer.children]) {
|
|
3488
|
+
if (child instanceof AssistantMessageComponent)
|
|
3489
|
+
child.setHideThinkingBlock(hidden);
|
|
3490
|
+
}
|
|
3491
|
+
this.ui.rebuild();
|
|
3492
|
+
}
|
|
3493
|
+
async openExternalEditor() {
|
|
2883
3494
|
// Determine editor (respect $VISUAL, then $EDITOR)
|
|
2884
3495
|
const editorCmd = process.env.VISUAL || process.env.EDITOR;
|
|
2885
3496
|
if (!editorCmd) {
|
|
@@ -2891,7 +3502,8 @@ export class InteractiveMode {
|
|
|
2891
3502
|
try {
|
|
2892
3503
|
// Write current content to temp file
|
|
2893
3504
|
fs.writeFileSync(tmpFile, currentText, "utf-8");
|
|
2894
|
-
//
|
|
3505
|
+
// SCRAMJET-DIVERGENCE: the editor must not inherit the key release that opened it.
|
|
3506
|
+
await this.ui.terminal.drainInput();
|
|
2895
3507
|
this.ui.stop();
|
|
2896
3508
|
// Split by space to support editor arguments (e.g., "code --wait")
|
|
2897
3509
|
const [editor, ...editorArgs] = editorCmd.split(" ");
|
|
@@ -2917,8 +3529,7 @@ export class InteractiveMode {
|
|
|
2917
3529
|
}
|
|
2918
3530
|
// Restart TUI
|
|
2919
3531
|
this.ui.start();
|
|
2920
|
-
|
|
2921
|
-
this.ui.requestRender(true);
|
|
3532
|
+
this.ui.rebuild();
|
|
2922
3533
|
}
|
|
2923
3534
|
}
|
|
2924
3535
|
// =========================================================================
|
|
@@ -2929,14 +3540,18 @@ export class InteractiveMode {
|
|
|
2929
3540
|
this.ui.requestRender();
|
|
2930
3541
|
}
|
|
2931
3542
|
showError(errorMessage) {
|
|
3543
|
+
this.sealStatus();
|
|
2932
3544
|
this.chatContainer.addChild(new Spacer(1));
|
|
2933
3545
|
this.chatContainer.addChild(new Text(theme.fg("error", `Error: ${errorMessage}`), 1, 0));
|
|
2934
3546
|
this.chatContainer.addChild(new Spacer(1));
|
|
3547
|
+
this.promoteFinalizedChatPrefix();
|
|
2935
3548
|
this.ui.requestRender();
|
|
2936
3549
|
}
|
|
2937
3550
|
showWarning(warningMessage) {
|
|
3551
|
+
this.sealStatus();
|
|
2938
3552
|
this.chatContainer.addChild(new Spacer(1));
|
|
2939
3553
|
this.chatContainer.addChild(new Text(theme.fg("warning", `Warning: ${warningMessage}`), 1, 0));
|
|
3554
|
+
this.promoteFinalizedChatPrefix();
|
|
2940
3555
|
this.ui.requestRender();
|
|
2941
3556
|
}
|
|
2942
3557
|
// SCRAMJET-DIVERGENCE: showNewVersionNotification removed (version check gutted).
|
|
@@ -2948,7 +3563,7 @@ export class InteractiveMode {
|
|
|
2948
3563
|
this.chatContainer.addChild(new DynamicBorder((text) => theme.fg("warning", text)));
|
|
2949
3564
|
this.chatContainer.addChild(new Text(`${theme.bold(theme.fg("warning", "Package Updates Available"))}\n${updateInstruction}\n${theme.fg("muted", "Packages:")}\n${packageLines}`, 1, 0));
|
|
2950
3565
|
this.chatContainer.addChild(new DynamicBorder((text) => theme.fg("warning", text)));
|
|
2951
|
-
this.
|
|
3566
|
+
this.commitFinalizedChatOutput();
|
|
2952
3567
|
}
|
|
2953
3568
|
/**
|
|
2954
3569
|
* Get all queued messages (read-only).
|
|
@@ -3108,11 +3723,14 @@ export class InteractiveMode {
|
|
|
3108
3723
|
}
|
|
3109
3724
|
/** Move pending bash components from pending area to chat */
|
|
3110
3725
|
flushPendingBashComponents() {
|
|
3726
|
+
if (this.pendingBashComponents.length > 0)
|
|
3727
|
+
this.sealStatus();
|
|
3111
3728
|
for (const component of this.pendingBashComponents) {
|
|
3112
3729
|
this.pendingMessagesContainer.removeChild(component);
|
|
3113
3730
|
this.chatContainer.addChild(component);
|
|
3114
3731
|
}
|
|
3115
3732
|
this.pendingBashComponents = [];
|
|
3733
|
+
this.promoteFinalizedChatPrefix();
|
|
3116
3734
|
}
|
|
3117
3735
|
// =========================================================================
|
|
3118
3736
|
// Selectors
|
|
@@ -3122,7 +3740,10 @@ export class InteractiveMode {
|
|
|
3122
3740
|
* @param create Factory that receives a `done` callback and returns the component and focus target
|
|
3123
3741
|
*/
|
|
3124
3742
|
showSelector(create) {
|
|
3743
|
+
this.selectorOpenGeneration++;
|
|
3744
|
+
this.pendingSelectorOpenGeneration = undefined;
|
|
3125
3745
|
const done = () => {
|
|
3746
|
+
this.selectorOpenGeneration++;
|
|
3126
3747
|
this.editorContainer.clear();
|
|
3127
3748
|
this.editorContainer.addChild(this.editor);
|
|
3128
3749
|
this.ui.setFocus(this.editor);
|
|
@@ -3133,6 +3754,23 @@ export class InteractiveMode {
|
|
|
3133
3754
|
this.ui.setFocus(focus);
|
|
3134
3755
|
this.ui.requestRender();
|
|
3135
3756
|
}
|
|
3757
|
+
async settleLayoutSettings(selector) {
|
|
3758
|
+
const generation = ++this.layoutSettingsSettlementGeneration;
|
|
3759
|
+
const settings = this.settingsManager;
|
|
3760
|
+
await settings.flush();
|
|
3761
|
+
// SCRAMJET-DIVERGENCE: only the latest settlement may drain a shared batch of save errors.
|
|
3762
|
+
if (generation !== this.layoutSettingsSettlementGeneration)
|
|
3763
|
+
return;
|
|
3764
|
+
const errors = settings.drainErrors();
|
|
3765
|
+
selector.setSaveError(errors.length ? "Changes not saved; see warning" : undefined);
|
|
3766
|
+
if (errors.length) {
|
|
3767
|
+
const details = stripVTControlCharacters(errors.map(({ scope, error }) => `${scope}: ${error.message}`).join("; "))
|
|
3768
|
+
.replace(/[\r\n\t]/g, " ")
|
|
3769
|
+
.slice(0, 200);
|
|
3770
|
+
this.showWarning(`Display settings not saved (${details}). Changes may be lost on restart; project overrides still apply.`);
|
|
3771
|
+
}
|
|
3772
|
+
this.ui.requestRender();
|
|
3773
|
+
}
|
|
3136
3774
|
showSettingsSelector() {
|
|
3137
3775
|
this.showSelector((done) => {
|
|
3138
3776
|
const selector = new SettingsSelectorComponent({
|
|
@@ -3147,7 +3785,8 @@ export class InteractiveMode {
|
|
|
3147
3785
|
transport: this.settingsManager.getTransport(),
|
|
3148
3786
|
thinkingLevel: this.session.thinkingLevel,
|
|
3149
3787
|
availableThinkingLevels: this.session.getAvailableThinkingLevels(),
|
|
3150
|
-
|
|
3788
|
+
// SCRAMJET-DIVERGENCE: display fallback routed to the renamed builtin.
|
|
3789
|
+
currentTheme: this.settingsManager.getTheme() || "pi-dark",
|
|
3151
3790
|
availableThemes: getAvailableThemes(),
|
|
3152
3791
|
hideThinkingBlock: this.hideThinkingBlock,
|
|
3153
3792
|
collapseChangelog: this.settingsManager.getCollapseChangelog(),
|
|
@@ -3158,9 +3797,14 @@ export class InteractiveMode {
|
|
|
3158
3797
|
editorPaddingX: this.settingsManager.getEditorPaddingX(),
|
|
3159
3798
|
autocompleteMaxVisible: this.settingsManager.getAutocompleteMaxVisible(),
|
|
3160
3799
|
quietStartup: this.settingsManager.getQuietStartup(),
|
|
3161
|
-
clearOnShrink: this.settingsManager.getClearOnShrink(),
|
|
3162
3800
|
showTerminalProgress: this.settingsManager.getShowTerminalProgress(),
|
|
3163
3801
|
warnings: this.settingsManager.getWarnings(),
|
|
3802
|
+
tuiMode: this.tuiMode,
|
|
3803
|
+
dockEditor: this.settingsManager.getDockEditor(),
|
|
3804
|
+
retainTranscriptOnExit: this.settingsManager.getRetainTranscriptOnExit(),
|
|
3805
|
+
editorMaxHeightPercent: this.settingsManager.getEditorMaxHeightPercent(),
|
|
3806
|
+
scrollWheelStep: this.settingsManager.getScrollWheelStep(),
|
|
3807
|
+
viewportProjectOverrides: Object.keys(this.settingsManager.getProjectSettings()),
|
|
3164
3808
|
}, {
|
|
3165
3809
|
onAutoCompactChange: (enabled) => {
|
|
3166
3810
|
this.session.setAutoCompactionEnabled(enabled);
|
|
@@ -3168,19 +3812,11 @@ export class InteractiveMode {
|
|
|
3168
3812
|
},
|
|
3169
3813
|
onShowImagesChange: (enabled) => {
|
|
3170
3814
|
this.settingsManager.setShowImages(enabled);
|
|
3171
|
-
|
|
3172
|
-
if (child instanceof ToolExecutionComponent) {
|
|
3173
|
-
child.setShowImages(enabled);
|
|
3174
|
-
}
|
|
3175
|
-
}
|
|
3815
|
+
this.updateToolImagePresentation((component) => component.setShowImages(enabled));
|
|
3176
3816
|
},
|
|
3177
3817
|
onImageWidthCellsChange: (width) => {
|
|
3178
3818
|
this.settingsManager.setImageWidthCells(width);
|
|
3179
|
-
|
|
3180
|
-
if (child instanceof ToolExecutionComponent) {
|
|
3181
|
-
child.setImageWidthCells(width);
|
|
3182
|
-
}
|
|
3183
|
-
}
|
|
3819
|
+
this.updateToolImagePresentation((component) => component.setImageWidthCells(width));
|
|
3184
3820
|
},
|
|
3185
3821
|
onAutoResizeImagesChange: (enabled) => {
|
|
3186
3822
|
this.settingsManager.setImageAutoResize(enabled);
|
|
@@ -3212,7 +3848,8 @@ export class InteractiveMode {
|
|
|
3212
3848
|
this.settingsManager.setTheme(themeName);
|
|
3213
3849
|
this.ui.invalidate();
|
|
3214
3850
|
if (!result.success) {
|
|
3215
|
-
|
|
3851
|
+
// SCRAMJET-DIVERGENCE: fallback message names the renamed builtin.
|
|
3852
|
+
this.showError(`Failed to load theme "${themeName}": ${result.error}\nFell back to pi-dark theme.`);
|
|
3216
3853
|
}
|
|
3217
3854
|
},
|
|
3218
3855
|
onThemePreview: (themeName) => {
|
|
@@ -3222,17 +3859,7 @@ export class InteractiveMode {
|
|
|
3222
3859
|
this.ui.requestRender();
|
|
3223
3860
|
}
|
|
3224
3861
|
},
|
|
3225
|
-
onHideThinkingBlockChange: (hidden) =>
|
|
3226
|
-
this.hideThinkingBlock = hidden;
|
|
3227
|
-
this.settingsManager.setHideThinkingBlock(hidden);
|
|
3228
|
-
for (const child of this.chatContainer.children) {
|
|
3229
|
-
if (child instanceof AssistantMessageComponent) {
|
|
3230
|
-
child.setHideThinkingBlock(hidden);
|
|
3231
|
-
}
|
|
3232
|
-
}
|
|
3233
|
-
this.chatContainer.clear();
|
|
3234
|
-
this.rebuildChatFromMessages();
|
|
3235
|
-
},
|
|
3862
|
+
onHideThinkingBlockChange: (hidden) => this.setThinkingBlockVisibility(hidden),
|
|
3236
3863
|
onCollapseChangelogChange: (collapsed) => {
|
|
3237
3864
|
this.settingsManager.setCollapseChangelog(collapsed);
|
|
3238
3865
|
},
|
|
@@ -3266,21 +3893,42 @@ export class InteractiveMode {
|
|
|
3266
3893
|
this.editor.setAutocompleteMaxVisible(maxVisible);
|
|
3267
3894
|
}
|
|
3268
3895
|
},
|
|
3269
|
-
onClearOnShrinkChange: (enabled) => {
|
|
3270
|
-
this.settingsManager.setClearOnShrink(enabled);
|
|
3271
|
-
this.ui.setClearOnShrink(enabled);
|
|
3272
|
-
},
|
|
3273
3896
|
onShowTerminalProgressChange: (enabled) => {
|
|
3274
3897
|
this.settingsManager.setShowTerminalProgress(enabled);
|
|
3275
3898
|
},
|
|
3276
3899
|
onWarningsChange: (warnings) => {
|
|
3277
3900
|
this.settingsManager.setWarnings(warnings);
|
|
3278
3901
|
},
|
|
3902
|
+
onRetainTranscriptOnExitChange: (enabled) => {
|
|
3903
|
+
this.settingsManager.setRetainTranscriptOnExit(enabled);
|
|
3904
|
+
void this.settleLayoutSettings(selector);
|
|
3905
|
+
return this.settingsManager.getRetainTranscriptOnExit();
|
|
3906
|
+
},
|
|
3907
|
+
onDockEditorChange: (enabled) => {
|
|
3908
|
+
this.settingsManager.setDockEditor(enabled);
|
|
3909
|
+
void this.settleLayoutSettings(selector);
|
|
3910
|
+
this.ui.refreshViewportLayout();
|
|
3911
|
+
if (!this.settingsManager.getDockEditor())
|
|
3912
|
+
this.ui.revealComponent(this.editorContainer);
|
|
3913
|
+
return this.settingsManager.getDockEditor();
|
|
3914
|
+
},
|
|
3915
|
+
onEditorMaxHeightPercentChange: (percent) => {
|
|
3916
|
+
this.settingsManager.setEditorMaxHeightPercent(percent);
|
|
3917
|
+
void this.settleLayoutSettings(selector);
|
|
3918
|
+
this.ui.refreshViewportLayout();
|
|
3919
|
+
return this.settingsManager.getEditorMaxHeightPercent();
|
|
3920
|
+
},
|
|
3921
|
+
onScrollWheelStepChange: (step) => {
|
|
3922
|
+
this.settingsManager.setScrollWheelStep(step);
|
|
3923
|
+
void this.settleLayoutSettings(selector);
|
|
3924
|
+
this.ui.requestRender();
|
|
3925
|
+
return this.settingsManager.getScrollWheelStep();
|
|
3926
|
+
},
|
|
3279
3927
|
onCancel: () => {
|
|
3280
3928
|
done();
|
|
3281
3929
|
this.ui.requestRender();
|
|
3282
3930
|
},
|
|
3283
|
-
});
|
|
3931
|
+
}, () => Math.max(1, Math.min(this.editorAvailableRows ?? Infinity, this.ui.terminal.rows - 4)));
|
|
3284
3932
|
return { component: selector, focus: selector.getSettingsList() };
|
|
3285
3933
|
});
|
|
3286
3934
|
}
|
|
@@ -3356,7 +4004,22 @@ export class InteractiveMode {
|
|
|
3356
4004
|
// Ignore auth lookup failures for warning-only checks.
|
|
3357
4005
|
}
|
|
3358
4006
|
}
|
|
3359
|
-
|
|
4007
|
+
selectorPreparationIsCurrent(session, generation) {
|
|
4008
|
+
const current = this.isInitialized && this.session === session && this.selectorOpenGeneration === generation;
|
|
4009
|
+
if (!current && this.pendingSelectorOpenGeneration === generation)
|
|
4010
|
+
this.pendingSelectorOpenGeneration = undefined;
|
|
4011
|
+
return current;
|
|
4012
|
+
}
|
|
4013
|
+
// SCRAMJET-DIVERGENCE: Refresh profile-scoped output-rate history before opening model selectors (issue 476).
|
|
4014
|
+
async showModelSelector(initialSearchInput) {
|
|
4015
|
+
const session = this.session;
|
|
4016
|
+
const generation = ++this.selectorOpenGeneration;
|
|
4017
|
+
this.pendingSelectorOpenGeneration = generation;
|
|
4018
|
+
await session.refreshOutputThroughputHistory().catch(() => undefined);
|
|
4019
|
+
if (!this.selectorPreparationIsCurrent(session, generation))
|
|
4020
|
+
return;
|
|
4021
|
+
this.pendingSelectorOpenGeneration = undefined;
|
|
4022
|
+
const outputThroughputHistory = session.outputThroughputHistory;
|
|
3360
4023
|
this.showSelector((done) => {
|
|
3361
4024
|
const selector = new ModelSelectorComponent(this.ui, this.session.model, this.settingsManager, this.session.modelRegistry, this.session.scopedModels, async (model) => {
|
|
3362
4025
|
try {
|
|
@@ -3375,20 +4038,28 @@ export class InteractiveMode {
|
|
|
3375
4038
|
}, () => {
|
|
3376
4039
|
done();
|
|
3377
4040
|
this.ui.requestRender();
|
|
3378
|
-
}, initialSearchInput);
|
|
4041
|
+
}, initialSearchInput, outputThroughputHistory);
|
|
3379
4042
|
return { component: selector, focus: selector };
|
|
3380
4043
|
});
|
|
3381
4044
|
}
|
|
3382
4045
|
async showModelsSelector() {
|
|
4046
|
+
const session = this.session;
|
|
4047
|
+
const generation = ++this.selectorOpenGeneration;
|
|
4048
|
+
this.pendingSelectorOpenGeneration = generation;
|
|
4049
|
+
await session.refreshOutputThroughputHistory().catch(() => undefined);
|
|
4050
|
+
if (!this.selectorPreparationIsCurrent(session, generation))
|
|
4051
|
+
return;
|
|
4052
|
+
const outputThroughputHistory = session.outputThroughputHistory;
|
|
3383
4053
|
// Get all available models
|
|
3384
|
-
|
|
3385
|
-
const allModels =
|
|
4054
|
+
session.modelRegistry.refresh();
|
|
4055
|
+
const allModels = session.modelRegistry.getAvailable();
|
|
3386
4056
|
if (allModels.length === 0) {
|
|
4057
|
+
this.pendingSelectorOpenGeneration = undefined;
|
|
3387
4058
|
this.showStatus("No models available");
|
|
3388
4059
|
return;
|
|
3389
4060
|
}
|
|
3390
4061
|
// Check if session has scoped models (from previous session-only changes or CLI --models)
|
|
3391
|
-
const sessionScopedModels =
|
|
4062
|
+
const sessionScopedModels = session.scopedModels;
|
|
3392
4063
|
const hasSessionScope = sessionScopedModels.length > 0;
|
|
3393
4064
|
// Build enabled model IDs from session state or settings
|
|
3394
4065
|
let currentEnabledIds = null;
|
|
@@ -3400,10 +4071,13 @@ export class InteractiveMode {
|
|
|
3400
4071
|
// Fall back to settings
|
|
3401
4072
|
const patterns = this.settingsManager.getEnabledModels();
|
|
3402
4073
|
if (patterns !== undefined && patterns.length > 0) {
|
|
3403
|
-
const scopedModels = await resolveModelScope(patterns,
|
|
4074
|
+
const scopedModels = await resolveModelScope(patterns, session.modelRegistry);
|
|
3404
4075
|
currentEnabledIds = scopedModels.map((scoped) => `${scoped.model.provider}/${scoped.model.id}`);
|
|
3405
4076
|
}
|
|
3406
4077
|
}
|
|
4078
|
+
if (!this.selectorPreparationIsCurrent(session, generation))
|
|
4079
|
+
return;
|
|
4080
|
+
this.pendingSelectorOpenGeneration = undefined;
|
|
3407
4081
|
// Helper to update session's scoped models (session-only, no persist)
|
|
3408
4082
|
const updateSessionModels = async (enabledIds) => {
|
|
3409
4083
|
currentEnabledIds = enabledIds === null ? null : [...enabledIds];
|
|
@@ -3441,7 +4115,7 @@ export class InteractiveMode {
|
|
|
3441
4115
|
done();
|
|
3442
4116
|
this.ui.requestRender();
|
|
3443
4117
|
},
|
|
3444
|
-
});
|
|
4118
|
+
}, outputThroughputHistory);
|
|
3445
4119
|
return { component: selector, focus: selector };
|
|
3446
4120
|
});
|
|
3447
4121
|
}
|
|
@@ -3468,7 +4142,7 @@ export class InteractiveMode {
|
|
|
3468
4142
|
}
|
|
3469
4143
|
catch (error) {
|
|
3470
4144
|
done();
|
|
3471
|
-
this.showError(
|
|
4145
|
+
this.showError(describeRuntimeError(error));
|
|
3472
4146
|
}
|
|
3473
4147
|
}, () => {
|
|
3474
4148
|
done();
|
|
@@ -3494,7 +4168,7 @@ export class InteractiveMode {
|
|
|
3494
4168
|
this.showStatus("Cloned to new session");
|
|
3495
4169
|
}
|
|
3496
4170
|
catch (error) {
|
|
3497
|
-
this.showError(
|
|
4171
|
+
this.showError(describeRuntimeError(error));
|
|
3498
4172
|
}
|
|
3499
4173
|
}
|
|
3500
4174
|
showTreeSelector(initialSelectedId) {
|
|
@@ -3571,7 +4245,7 @@ export class InteractiveMode {
|
|
|
3571
4245
|
return;
|
|
3572
4246
|
}
|
|
3573
4247
|
// Update UI
|
|
3574
|
-
this.
|
|
4248
|
+
this.clearTranscript();
|
|
3575
4249
|
this.renderInitialMessages();
|
|
3576
4250
|
if (result.editorText && !this.editor.getText().trim()) {
|
|
3577
4251
|
this.editor.setText(result.editorText);
|
|
@@ -4006,7 +4680,7 @@ export class InteractiveMode {
|
|
|
4006
4680
|
this.editorContainer.clear();
|
|
4007
4681
|
this.editorContainer.addChild(reloadBox);
|
|
4008
4682
|
this.ui.setFocus(reloadBox);
|
|
4009
|
-
this.ui.
|
|
4683
|
+
this.ui.rebuild();
|
|
4010
4684
|
await new Promise((resolve) => process.nextTick(resolve));
|
|
4011
4685
|
const dismissReloadBox = (editor) => {
|
|
4012
4686
|
this.editorContainer.clear();
|
|
@@ -4023,11 +4697,10 @@ export class InteractiveMode {
|
|
|
4023
4697
|
}
|
|
4024
4698
|
setRegisteredThemes(this.session.resourceLoader.getThemes().themes);
|
|
4025
4699
|
this.hideThinkingBlock = this.settingsManager.getHideThinkingBlock();
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
}
|
|
4700
|
+
// SCRAMJET-DIVERGENCE: re-resolve through the resolver after reload so newly registered
|
|
4701
|
+
// themes (and the captured classification) are honored; force a re-read since reload's purpose
|
|
4702
|
+
// is to pick up on-disk theme edits even when the active theme's name is unchanged.
|
|
4703
|
+
this.reapplyResolvedTheme(true);
|
|
4031
4704
|
const editorPaddingX = this.settingsManager.getEditorPaddingX();
|
|
4032
4705
|
const autocompleteMaxVisible = this.settingsManager.getAutocompleteMaxVisible();
|
|
4033
4706
|
this.defaultEditor.setPaddingX(editorPaddingX);
|
|
@@ -4037,7 +4710,6 @@ export class InteractiveMode {
|
|
|
4037
4710
|
this.editor.setAutocompleteMaxVisible?.(autocompleteMaxVisible);
|
|
4038
4711
|
}
|
|
4039
4712
|
this.ui.setShowHardwareCursor(this.settingsManager.getShowHardwareCursor());
|
|
4040
|
-
this.ui.setClearOnShrink(this.settingsManager.getClearOnShrink());
|
|
4041
4713
|
this.setupAutocompleteProvider();
|
|
4042
4714
|
const runner = this.session.extensionRunner;
|
|
4043
4715
|
this.setupExtensionShortcuts(runner);
|
|
@@ -4055,7 +4727,7 @@ export class InteractiveMode {
|
|
|
4055
4727
|
}
|
|
4056
4728
|
catch (error) {
|
|
4057
4729
|
dismissReloadBox(previousEditor);
|
|
4058
|
-
this.showError(`Reload failed: ${
|
|
4730
|
+
this.showError(`Reload failed: ${describeRuntimeError(error)}`);
|
|
4059
4731
|
}
|
|
4060
4732
|
}
|
|
4061
4733
|
async handleExportCommand(text) {
|
|
@@ -4173,13 +4845,13 @@ export class InteractiveMode {
|
|
|
4173
4845
|
else {
|
|
4174
4846
|
this.showWarning("Usage: /name <name>");
|
|
4175
4847
|
}
|
|
4176
|
-
this.
|
|
4848
|
+
this.commitFinalizedChatOutput();
|
|
4177
4849
|
return;
|
|
4178
4850
|
}
|
|
4179
4851
|
this.session.setSessionName(name);
|
|
4180
4852
|
this.chatContainer.addChild(new Spacer(1));
|
|
4181
4853
|
this.chatContainer.addChild(new Text(theme.fg("dim", `Session name set: ${name}`), 1, 0));
|
|
4182
|
-
this.
|
|
4854
|
+
this.commitFinalizedChatOutput();
|
|
4183
4855
|
}
|
|
4184
4856
|
handleSessionCommand() {
|
|
4185
4857
|
const stats = this.session.getSessionStats();
|
|
@@ -4212,7 +4884,7 @@ export class InteractiveMode {
|
|
|
4212
4884
|
}
|
|
4213
4885
|
this.chatContainer.addChild(new Spacer(1));
|
|
4214
4886
|
this.chatContainer.addChild(new Text(info, 1, 0));
|
|
4215
|
-
this.
|
|
4887
|
+
this.commitFinalizedChatOutput();
|
|
4216
4888
|
}
|
|
4217
4889
|
handleChangelogCommand() {
|
|
4218
4890
|
const changelogPath = getChangelogPath();
|
|
@@ -4229,7 +4901,7 @@ export class InteractiveMode {
|
|
|
4229
4901
|
this.chatContainer.addChild(new Spacer(1));
|
|
4230
4902
|
this.chatContainer.addChild(new Markdown(changelogMarkdown, 1, 1, this.getMarkdownThemeWithSettings()));
|
|
4231
4903
|
this.chatContainer.addChild(new DynamicBorder());
|
|
4232
|
-
this.
|
|
4904
|
+
this.commitFinalizedChatOutput();
|
|
4233
4905
|
}
|
|
4234
4906
|
/**
|
|
4235
4907
|
* Get capitalized display string for an app keybinding action.
|
|
@@ -4350,7 +5022,7 @@ export class InteractiveMode {
|
|
|
4350
5022
|
this.chatContainer.addChild(new Spacer(1));
|
|
4351
5023
|
this.chatContainer.addChild(new Markdown(hotkeys.trim(), 1, 1, this.getMarkdownThemeWithSettings()));
|
|
4352
5024
|
this.chatContainer.addChild(new DynamicBorder());
|
|
4353
|
-
this.
|
|
5025
|
+
this.commitFinalizedChatOutput();
|
|
4354
5026
|
}
|
|
4355
5027
|
async handleClearCommand() {
|
|
4356
5028
|
if (this.loadingAnimation) {
|
|
@@ -4366,7 +5038,7 @@ export class InteractiveMode {
|
|
|
4366
5038
|
this.renderCurrentSessionState();
|
|
4367
5039
|
this.chatContainer.addChild(new Spacer(1));
|
|
4368
5040
|
this.chatContainer.addChild(new Text(`${theme.fg("accent", "✓ New session started")}`, 1, 1));
|
|
4369
|
-
this.
|
|
5041
|
+
this.commitFinalizedChatOutput();
|
|
4370
5042
|
}
|
|
4371
5043
|
catch (error) {
|
|
4372
5044
|
await this.handleFatalRuntimeError("Failed to create session", error);
|
|
@@ -4397,22 +5069,38 @@ export class InteractiveMode {
|
|
|
4397
5069
|
fs.writeFileSync(debugLogPath, debugData);
|
|
4398
5070
|
this.chatContainer.addChild(new Spacer(1));
|
|
4399
5071
|
this.chatContainer.addChild(new Text(`${theme.fg("accent", "✓ Debug log written")}\n${theme.fg("muted", debugLogPath)}`, 1, 1));
|
|
4400
|
-
this.
|
|
5072
|
+
this.commitFinalizedChatOutput();
|
|
4401
5073
|
}
|
|
4402
5074
|
handleArminSaysHi() {
|
|
5075
|
+
const component = new ArminComponent(this.ui, () => {
|
|
5076
|
+
if (!this.mutableChatComponents.has(component) || !this.chatContainer.children.includes(component))
|
|
5077
|
+
return;
|
|
5078
|
+
this.setChatComponentMutable(component, false);
|
|
5079
|
+
this.promoteFinalizedChatPrefix();
|
|
5080
|
+
this.ui.requestRender();
|
|
5081
|
+
});
|
|
4403
5082
|
this.chatContainer.addChild(new Spacer(1));
|
|
4404
|
-
this.chatContainer.addChild(
|
|
4405
|
-
this.
|
|
5083
|
+
this.chatContainer.addChild(component);
|
|
5084
|
+
this.setChatComponentMutable(component, true);
|
|
5085
|
+
this.commitFinalizedChatOutput();
|
|
4406
5086
|
}
|
|
4407
5087
|
handleDementedDelves() {
|
|
4408
5088
|
this.chatContainer.addChild(new Spacer(1));
|
|
4409
5089
|
this.chatContainer.addChild(new EarendilAnnouncementComponent());
|
|
4410
|
-
this.
|
|
5090
|
+
this.commitFinalizedChatOutput();
|
|
4411
5091
|
}
|
|
4412
5092
|
handleDaxnuts() {
|
|
5093
|
+
const component = new DaxnutsComponent(this.ui, () => {
|
|
5094
|
+
if (!this.mutableChatComponents.has(component) || !this.chatContainer.children.includes(component))
|
|
5095
|
+
return;
|
|
5096
|
+
this.setChatComponentMutable(component, false);
|
|
5097
|
+
this.promoteFinalizedChatPrefix();
|
|
5098
|
+
this.ui.requestRender();
|
|
5099
|
+
});
|
|
4413
5100
|
this.chatContainer.addChild(new Spacer(1));
|
|
4414
|
-
this.chatContainer.addChild(
|
|
4415
|
-
this.
|
|
5101
|
+
this.chatContainer.addChild(component);
|
|
5102
|
+
this.setChatComponentMutable(component, true);
|
|
5103
|
+
this.commitFinalizedChatOutput();
|
|
4416
5104
|
}
|
|
4417
5105
|
checkDaxnutsEasterEgg(model) {
|
|
4418
5106
|
if (model.provider === "opencode" && model.id.toLowerCase().includes("kimi-k2.5")) {
|
|
@@ -4438,6 +5126,7 @@ export class InteractiveMode {
|
|
|
4438
5126
|
this.pendingBashComponents.push(this.bashComponent);
|
|
4439
5127
|
}
|
|
4440
5128
|
else {
|
|
5129
|
+
this.sealStatus();
|
|
4441
5130
|
this.chatContainer.addChild(this.bashComponent);
|
|
4442
5131
|
}
|
|
4443
5132
|
// Show output and complete
|
|
@@ -4447,6 +5136,7 @@ export class InteractiveMode {
|
|
|
4447
5136
|
this.bashComponent.setComplete(result.exitCode, result.cancelled, result.truncated ? { truncated: true, content: result.output } : undefined, result.fullOutputPath);
|
|
4448
5137
|
// Record the result in session
|
|
4449
5138
|
this.session.recordBashResult(command, result, { excludeFromContext });
|
|
5139
|
+
this.promoteFinalizedChatPrefix();
|
|
4450
5140
|
this.bashComponent = undefined;
|
|
4451
5141
|
this.ui.requestRender();
|
|
4452
5142
|
return;
|
|
@@ -4454,6 +5144,7 @@ export class InteractiveMode {
|
|
|
4454
5144
|
// Normal execution path (possibly with custom operations)
|
|
4455
5145
|
const isDeferred = this.session.isStreaming;
|
|
4456
5146
|
this.bashComponent = new BashExecutionComponent(command, this.ui, excludeFromContext);
|
|
5147
|
+
this.setChatComponentMutable(this.bashComponent, true);
|
|
4457
5148
|
if (isDeferred) {
|
|
4458
5149
|
// Show in pending area when agent is streaming
|
|
4459
5150
|
this.pendingMessagesContainer.addChild(this.bashComponent);
|
|
@@ -4461,6 +5152,7 @@ export class InteractiveMode {
|
|
|
4461
5152
|
}
|
|
4462
5153
|
else {
|
|
4463
5154
|
// Show in chat immediately when agent is idle
|
|
5155
|
+
this.sealStatus();
|
|
4464
5156
|
this.chatContainer.addChild(this.bashComponent);
|
|
4465
5157
|
}
|
|
4466
5158
|
this.ui.requestRender();
|
|
@@ -4481,6 +5173,9 @@ export class InteractiveMode {
|
|
|
4481
5173
|
}
|
|
4482
5174
|
this.showError(`Bash command failed: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
4483
5175
|
}
|
|
5176
|
+
if (this.bashComponent)
|
|
5177
|
+
this.setChatComponentMutable(this.bashComponent, false);
|
|
5178
|
+
this.promoteFinalizedChatPrefix();
|
|
4484
5179
|
this.bashComponent = undefined;
|
|
4485
5180
|
this.ui.requestRender();
|
|
4486
5181
|
}
|
|
@@ -4503,24 +5198,30 @@ export class InteractiveMode {
|
|
|
4503
5198
|
// Ignore, will be emitted as an event
|
|
4504
5199
|
}
|
|
4505
5200
|
}
|
|
4506
|
-
stop() {
|
|
4507
|
-
|
|
4508
|
-
|
|
4509
|
-
|
|
4510
|
-
|
|
4511
|
-
|
|
4512
|
-
this.loadingAnimation.stop();
|
|
4513
|
-
this.loadingAnimation = undefined;
|
|
4514
|
-
}
|
|
4515
|
-
this.clearExtensionTerminalInputListeners();
|
|
4516
|
-
this.footer.dispose();
|
|
4517
|
-
this.footerDataProvider.dispose();
|
|
4518
|
-
if (this.unsubscribe) {
|
|
4519
|
-
this.unsubscribe();
|
|
5201
|
+
stop(options) {
|
|
5202
|
+
try {
|
|
5203
|
+
if (this.isInitialized) {
|
|
5204
|
+
this.isInitialized = false;
|
|
5205
|
+
this.ui.stop(options);
|
|
5206
|
+
}
|
|
4520
5207
|
}
|
|
4521
|
-
|
|
4522
|
-
this.
|
|
4523
|
-
this.
|
|
5208
|
+
finally {
|
|
5209
|
+
this.selectorOpenGeneration++;
|
|
5210
|
+
this.pendingSelectorOpenGeneration = undefined;
|
|
5211
|
+
this.unregisterSignalHandlers();
|
|
5212
|
+
if (this.settingsManager.getShowTerminalProgress()) {
|
|
5213
|
+
this.ui.terminal.setProgress(false);
|
|
5214
|
+
}
|
|
5215
|
+
if (this.loadingAnimation) {
|
|
5216
|
+
this.loadingAnimation.stop();
|
|
5217
|
+
this.loadingAnimation = undefined;
|
|
5218
|
+
}
|
|
5219
|
+
this.clearExtensionTerminalInputListeners();
|
|
5220
|
+
this.footer.dispose();
|
|
5221
|
+
this.footerDataProvider.dispose();
|
|
5222
|
+
if (this.unsubscribe) {
|
|
5223
|
+
this.unsubscribe();
|
|
5224
|
+
}
|
|
4524
5225
|
}
|
|
4525
5226
|
}
|
|
4526
5227
|
}
|