@leanandmean/coding-agent 0.74.1-scramjet.7 → 0.74.1-scramjet.70
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 +23 -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 +78 -15
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +1059 -203
- 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/runner.d.ts +9 -5
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +160 -3
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/types.d.ts +85 -8
- 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 +19 -6
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +131 -17
- 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 +24 -20
- 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 +901 -176
- 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 +21 -3
- package/docs/custom-provider.md +33 -4
- package/docs/extensions.md +150 -24
- package/docs/json.md +2 -0
- 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 +31 -3
- package/docs/session-format.md +5 -1
- package/docs/settings.md +19 -5
- 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);
|
|
@@ -2466,7 +3029,31 @@ export class InteractiveMode {
|
|
|
2466
3029
|
case "user": {
|
|
2467
3030
|
const textContent = this.getUserMessageText(message);
|
|
2468
3031
|
if (textContent) {
|
|
2469
|
-
|
|
3032
|
+
// SCRAMJET-DIVERGENCE: separate user rows from visible committed results without doubling gaps (#579).
|
|
3033
|
+
let committedTailRow;
|
|
3034
|
+
if (this.chatContainer.children.length === 0) {
|
|
3035
|
+
for (let i = this.committedChatContainer.children.length - 1; i >= 0; i--) {
|
|
3036
|
+
const lines = this.committedChatContainer.children[i].render(this.ui.terminal.columns);
|
|
3037
|
+
if (lines.length > 0) {
|
|
3038
|
+
committedTailRow = lines[lines.length - 1];
|
|
3039
|
+
break;
|
|
3040
|
+
}
|
|
3041
|
+
}
|
|
3042
|
+
}
|
|
3043
|
+
const tailNeedsGap = committedTailRow !== undefined &&
|
|
3044
|
+
(stripVTControlCharacters(committedTailRow).trim() !== "" ||
|
|
3045
|
+
[...committedTailRow.matchAll(/\x1b\[([\d;:]*)m/g)].some(([, params]) => {
|
|
3046
|
+
const codes = params.split(";");
|
|
3047
|
+
for (let i = 0; i < codes.length; i++) {
|
|
3048
|
+
const code = Number(codes[i].split(":")[0]);
|
|
3049
|
+
if (code === 48 || (code >= 40 && code <= 47) || (code >= 100 && code <= 107))
|
|
3050
|
+
return true;
|
|
3051
|
+
if (code === 38)
|
|
3052
|
+
i += codes[i + 1] === "2" ? 4 : codes[i + 1] === "5" ? 2 : 0;
|
|
3053
|
+
}
|
|
3054
|
+
return false;
|
|
3055
|
+
}));
|
|
3056
|
+
if (this.chatContainer.children.length > 0 || tailNeedsGap) {
|
|
2470
3057
|
this.chatContainer.addChild(new Spacer(1));
|
|
2471
3058
|
}
|
|
2472
3059
|
// SCRAMJET-DIVERGENCE: scramjet-command block rendering (issue 82)
|
|
@@ -2480,9 +3067,10 @@ export class InteractiveMode {
|
|
|
2480
3067
|
this.chatContainer.addChild(userComponent);
|
|
2481
3068
|
}
|
|
2482
3069
|
if (options?.populateHistory) {
|
|
2483
|
-
const compactForm =
|
|
2484
|
-
|
|
2485
|
-
|
|
3070
|
+
const compactForm = options.historyText ??
|
|
3071
|
+
(scramjetBlock.userContext
|
|
3072
|
+
? `/${scramjetBlock.name} ${scramjetBlock.userContext}`
|
|
3073
|
+
: `/${scramjetBlock.name}`);
|
|
2486
3074
|
this.editor.addToHistory?.(compactForm);
|
|
2487
3075
|
}
|
|
2488
3076
|
}
|
|
@@ -2521,6 +3109,7 @@ export class InteractiveMode {
|
|
|
2521
3109
|
const _exhaustive = message;
|
|
2522
3110
|
}
|
|
2523
3111
|
}
|
|
3112
|
+
this.promoteFinalizedChatPrefix();
|
|
2524
3113
|
}
|
|
2525
3114
|
/**
|
|
2526
3115
|
* Render session context to chat. Used for initial load and rebuild after compaction.
|
|
@@ -2531,6 +3120,11 @@ export class InteractiveMode {
|
|
|
2531
3120
|
renderSessionContext(sessionContext, options = {}) {
|
|
2532
3121
|
this.pendingTools.clear();
|
|
2533
3122
|
const renderedPendingTools = new Map();
|
|
3123
|
+
const entries = options.populateHistory ? this.sessionManager.getEntries() : [];
|
|
3124
|
+
const commandStarts = indexScramjetCommandStarts(entries);
|
|
3125
|
+
const entriesByMessage = new Map(entries
|
|
3126
|
+
.filter((entry) => entry.type === "message")
|
|
3127
|
+
.map((entry) => [entry.message, entry]));
|
|
2534
3128
|
if (options.updateFooter) {
|
|
2535
3129
|
this.footer.invalidate();
|
|
2536
3130
|
this.updateEditorBorderColor();
|
|
@@ -2548,6 +3142,7 @@ export class InteractiveMode {
|
|
|
2548
3142
|
}, this.getRegisteredToolDefinition(content.name), this.ui, this.sessionManager.getCwd());
|
|
2549
3143
|
component.setExpanded(this.toolOutputExpanded);
|
|
2550
3144
|
this.chatContainer.addChild(component);
|
|
3145
|
+
this.setChatComponentMutable(component, true);
|
|
2551
3146
|
if (message.stopReason === "aborted" || message.stopReason === "error") {
|
|
2552
3147
|
let errorMessage;
|
|
2553
3148
|
if (message.stopReason === "aborted") {
|
|
@@ -2561,6 +3156,8 @@ export class InteractiveMode {
|
|
|
2561
3156
|
errorMessage = message.errorMessage || "Error";
|
|
2562
3157
|
}
|
|
2563
3158
|
component.updateResult({ content: [{ type: "text", text: errorMessage }], isError: true });
|
|
3159
|
+
component.seal();
|
|
3160
|
+
this.setChatComponentMutable(component, false);
|
|
2564
3161
|
}
|
|
2565
3162
|
else {
|
|
2566
3163
|
renderedPendingTools.set(content.id, component);
|
|
@@ -2572,18 +3169,45 @@ export class InteractiveMode {
|
|
|
2572
3169
|
// Match tool results to pending tool components
|
|
2573
3170
|
const component = renderedPendingTools.get(message.toolCallId);
|
|
2574
3171
|
if (component) {
|
|
3172
|
+
component.markExecutionStarted();
|
|
3173
|
+
component.setArgsComplete();
|
|
2575
3174
|
component.updateResult(message);
|
|
2576
3175
|
renderedPendingTools.delete(message.toolCallId);
|
|
3176
|
+
if (component.hasPendingImageConversions()) {
|
|
3177
|
+
const pendingToolFinalizations = this.pendingToolFinalizations;
|
|
3178
|
+
const finalization = component.waitForImageConversions().then(() => {
|
|
3179
|
+
if (!this.mutableChatComponents.has(component) ||
|
|
3180
|
+
!this.chatContainer.children.includes(component)) {
|
|
3181
|
+
component.seal();
|
|
3182
|
+
return;
|
|
3183
|
+
}
|
|
3184
|
+
component.seal();
|
|
3185
|
+
this.setChatComponentMutable(component, false);
|
|
3186
|
+
this.promoteFinalizedChatPrefix();
|
|
3187
|
+
this.ui.requestRender();
|
|
3188
|
+
});
|
|
3189
|
+
pendingToolFinalizations.add(finalization);
|
|
3190
|
+
void finalization.finally(() => pendingToolFinalizations.delete(finalization));
|
|
3191
|
+
}
|
|
3192
|
+
else {
|
|
3193
|
+
component.seal();
|
|
3194
|
+
this.setChatComponentMutable(component, false);
|
|
3195
|
+
}
|
|
2577
3196
|
}
|
|
2578
3197
|
}
|
|
2579
3198
|
else {
|
|
2580
3199
|
// All other messages use standard rendering
|
|
2581
|
-
|
|
3200
|
+
const entry = entriesByMessage.get(message);
|
|
3201
|
+
this.addMessageToChat(message, {
|
|
3202
|
+
...options,
|
|
3203
|
+
historyText: entry ? restoreScramjetCommandInvocation(entry, commandStarts) : undefined,
|
|
3204
|
+
});
|
|
2582
3205
|
}
|
|
2583
3206
|
}
|
|
2584
3207
|
for (const [toolCallId, component] of renderedPendingTools) {
|
|
2585
3208
|
this.pendingTools.set(toolCallId, component);
|
|
2586
3209
|
}
|
|
3210
|
+
this.promoteFinalizedChatPrefix();
|
|
2587
3211
|
this.ui.requestRender();
|
|
2588
3212
|
}
|
|
2589
3213
|
renderInitialMessages() {
|
|
@@ -2610,7 +3234,7 @@ export class InteractiveMode {
|
|
|
2610
3234
|
});
|
|
2611
3235
|
}
|
|
2612
3236
|
rebuildChatFromMessages() {
|
|
2613
|
-
this.
|
|
3237
|
+
this.clearTranscript();
|
|
2614
3238
|
const context = this.sessionManager.buildSessionContext();
|
|
2615
3239
|
this.renderSessionContext(context);
|
|
2616
3240
|
}
|
|
@@ -2645,9 +3269,18 @@ export class InteractiveMode {
|
|
|
2645
3269
|
// Drain any in-flight Kitty key release events before stopping.
|
|
2646
3270
|
// This prevents escape sequences from leaking to the parent shell over slow SSH.
|
|
2647
3271
|
await this.ui.terminal.drainInput(1000);
|
|
2648
|
-
this.
|
|
3272
|
+
await this.settingsManager.flush();
|
|
3273
|
+
let exitCode = 0;
|
|
3274
|
+
try {
|
|
3275
|
+
this.stop({ retainContent: this.settingsManager.getRetainTranscriptOnExit() });
|
|
3276
|
+
}
|
|
3277
|
+
catch (error) {
|
|
3278
|
+
exitCode = 1;
|
|
3279
|
+
console.error(`Could not retain terminal history: ${error instanceof Error ? error.message : String(error)}`);
|
|
3280
|
+
}
|
|
3281
|
+
await this.ui.terminal.flush?.();
|
|
2649
3282
|
await this.runtimeHost.dispose();
|
|
2650
|
-
process.exit(
|
|
3283
|
+
process.exit(exitCode);
|
|
2651
3284
|
}
|
|
2652
3285
|
emergencyTerminalExit() {
|
|
2653
3286
|
this.isShuttingDown = true;
|
|
@@ -2737,7 +3370,7 @@ export class InteractiveMode {
|
|
|
2737
3370
|
}
|
|
2738
3371
|
this.signalCleanupHandlers = [];
|
|
2739
3372
|
}
|
|
2740
|
-
handleCtrlZ() {
|
|
3373
|
+
async handleCtrlZ() {
|
|
2741
3374
|
if (process.platform === "win32") {
|
|
2742
3375
|
this.showStatus("Suspend to background is not supported on Windows");
|
|
2743
3376
|
return;
|
|
@@ -2755,10 +3388,11 @@ export class InteractiveMode {
|
|
|
2755
3388
|
clearInterval(suspendKeepAlive);
|
|
2756
3389
|
process.removeListener("SIGINT", ignoreSigint);
|
|
2757
3390
|
this.ui.start();
|
|
2758
|
-
this.ui.
|
|
3391
|
+
this.ui.rebuild();
|
|
2759
3392
|
});
|
|
2760
3393
|
try {
|
|
2761
|
-
//
|
|
3394
|
+
// SCRAMJET-DIVERGENCE: drain releases before handing keyboard ownership back to the shell.
|
|
3395
|
+
await this.ui.terminal.drainInput();
|
|
2762
3396
|
this.ui.stop();
|
|
2763
3397
|
// Send SIGTSTP to process group (pid=0 means all processes in group)
|
|
2764
3398
|
process.kill(0, "SIGTSTP");
|
|
@@ -2773,6 +3407,7 @@ export class InteractiveMode {
|
|
|
2773
3407
|
const text = (this.editor.getExpandedText?.() ?? this.editor.getText()).trim();
|
|
2774
3408
|
if (!text)
|
|
2775
3409
|
return;
|
|
3410
|
+
this.ui.followViewport();
|
|
2776
3411
|
// Queue input during compaction (extension commands execute immediately)
|
|
2777
3412
|
if (this.session.isCompacting) {
|
|
2778
3413
|
if (this.isExtensionCommand(text)) {
|
|
@@ -2858,28 +3493,28 @@ export class InteractiveMode {
|
|
|
2858
3493
|
if (isExpandable(activeHeader)) {
|
|
2859
3494
|
activeHeader.setExpanded(expanded);
|
|
2860
3495
|
}
|
|
2861
|
-
for (const child of this.chatContainer.children) {
|
|
3496
|
+
for (const child of [...this.committedChatContainer.children, ...this.chatContainer.children]) {
|
|
2862
3497
|
if (isExpandable(child)) {
|
|
2863
3498
|
child.setExpanded(expanded);
|
|
2864
3499
|
}
|
|
2865
3500
|
}
|
|
2866
|
-
this.ui.
|
|
3501
|
+
this.ui.rebuild();
|
|
2867
3502
|
}
|
|
2868
3503
|
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
|
-
}
|
|
3504
|
+
this.setThinkingBlockVisibility(!this.hideThinkingBlock);
|
|
2880
3505
|
this.showStatus(`Thinking blocks: ${this.hideThinkingBlock ? "hidden" : "visible"}`);
|
|
2881
3506
|
}
|
|
2882
|
-
|
|
3507
|
+
// SCRAMJET-DIVERGENCE: presentation changes retain live tools and browsing identities.
|
|
3508
|
+
setThinkingBlockVisibility(hidden) {
|
|
3509
|
+
this.hideThinkingBlock = hidden;
|
|
3510
|
+
this.settingsManager.setHideThinkingBlock(hidden);
|
|
3511
|
+
for (const child of [...this.committedChatContainer.children, ...this.chatContainer.children]) {
|
|
3512
|
+
if (child instanceof AssistantMessageComponent)
|
|
3513
|
+
child.setHideThinkingBlock(hidden);
|
|
3514
|
+
}
|
|
3515
|
+
this.ui.rebuild();
|
|
3516
|
+
}
|
|
3517
|
+
async openExternalEditor() {
|
|
2883
3518
|
// Determine editor (respect $VISUAL, then $EDITOR)
|
|
2884
3519
|
const editorCmd = process.env.VISUAL || process.env.EDITOR;
|
|
2885
3520
|
if (!editorCmd) {
|
|
@@ -2891,7 +3526,8 @@ export class InteractiveMode {
|
|
|
2891
3526
|
try {
|
|
2892
3527
|
// Write current content to temp file
|
|
2893
3528
|
fs.writeFileSync(tmpFile, currentText, "utf-8");
|
|
2894
|
-
//
|
|
3529
|
+
// SCRAMJET-DIVERGENCE: the editor must not inherit the key release that opened it.
|
|
3530
|
+
await this.ui.terminal.drainInput();
|
|
2895
3531
|
this.ui.stop();
|
|
2896
3532
|
// Split by space to support editor arguments (e.g., "code --wait")
|
|
2897
3533
|
const [editor, ...editorArgs] = editorCmd.split(" ");
|
|
@@ -2917,8 +3553,7 @@ export class InteractiveMode {
|
|
|
2917
3553
|
}
|
|
2918
3554
|
// Restart TUI
|
|
2919
3555
|
this.ui.start();
|
|
2920
|
-
|
|
2921
|
-
this.ui.requestRender(true);
|
|
3556
|
+
this.ui.rebuild();
|
|
2922
3557
|
}
|
|
2923
3558
|
}
|
|
2924
3559
|
// =========================================================================
|
|
@@ -2929,14 +3564,18 @@ export class InteractiveMode {
|
|
|
2929
3564
|
this.ui.requestRender();
|
|
2930
3565
|
}
|
|
2931
3566
|
showError(errorMessage) {
|
|
3567
|
+
this.sealStatus();
|
|
2932
3568
|
this.chatContainer.addChild(new Spacer(1));
|
|
2933
3569
|
this.chatContainer.addChild(new Text(theme.fg("error", `Error: ${errorMessage}`), 1, 0));
|
|
2934
3570
|
this.chatContainer.addChild(new Spacer(1));
|
|
3571
|
+
this.promoteFinalizedChatPrefix();
|
|
2935
3572
|
this.ui.requestRender();
|
|
2936
3573
|
}
|
|
2937
3574
|
showWarning(warningMessage) {
|
|
3575
|
+
this.sealStatus();
|
|
2938
3576
|
this.chatContainer.addChild(new Spacer(1));
|
|
2939
3577
|
this.chatContainer.addChild(new Text(theme.fg("warning", `Warning: ${warningMessage}`), 1, 0));
|
|
3578
|
+
this.promoteFinalizedChatPrefix();
|
|
2940
3579
|
this.ui.requestRender();
|
|
2941
3580
|
}
|
|
2942
3581
|
// SCRAMJET-DIVERGENCE: showNewVersionNotification removed (version check gutted).
|
|
@@ -2948,7 +3587,7 @@ export class InteractiveMode {
|
|
|
2948
3587
|
this.chatContainer.addChild(new DynamicBorder((text) => theme.fg("warning", text)));
|
|
2949
3588
|
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
3589
|
this.chatContainer.addChild(new DynamicBorder((text) => theme.fg("warning", text)));
|
|
2951
|
-
this.
|
|
3590
|
+
this.commitFinalizedChatOutput();
|
|
2952
3591
|
}
|
|
2953
3592
|
/**
|
|
2954
3593
|
* Get all queued messages (read-only).
|
|
@@ -3108,11 +3747,14 @@ export class InteractiveMode {
|
|
|
3108
3747
|
}
|
|
3109
3748
|
/** Move pending bash components from pending area to chat */
|
|
3110
3749
|
flushPendingBashComponents() {
|
|
3750
|
+
if (this.pendingBashComponents.length > 0)
|
|
3751
|
+
this.sealStatus();
|
|
3111
3752
|
for (const component of this.pendingBashComponents) {
|
|
3112
3753
|
this.pendingMessagesContainer.removeChild(component);
|
|
3113
3754
|
this.chatContainer.addChild(component);
|
|
3114
3755
|
}
|
|
3115
3756
|
this.pendingBashComponents = [];
|
|
3757
|
+
this.promoteFinalizedChatPrefix();
|
|
3116
3758
|
}
|
|
3117
3759
|
// =========================================================================
|
|
3118
3760
|
// Selectors
|
|
@@ -3122,7 +3764,10 @@ export class InteractiveMode {
|
|
|
3122
3764
|
* @param create Factory that receives a `done` callback and returns the component and focus target
|
|
3123
3765
|
*/
|
|
3124
3766
|
showSelector(create) {
|
|
3767
|
+
this.selectorOpenGeneration++;
|
|
3768
|
+
this.pendingSelectorOpenGeneration = undefined;
|
|
3125
3769
|
const done = () => {
|
|
3770
|
+
this.selectorOpenGeneration++;
|
|
3126
3771
|
this.editorContainer.clear();
|
|
3127
3772
|
this.editorContainer.addChild(this.editor);
|
|
3128
3773
|
this.ui.setFocus(this.editor);
|
|
@@ -3133,6 +3778,23 @@ export class InteractiveMode {
|
|
|
3133
3778
|
this.ui.setFocus(focus);
|
|
3134
3779
|
this.ui.requestRender();
|
|
3135
3780
|
}
|
|
3781
|
+
async settleLayoutSettings(selector) {
|
|
3782
|
+
const generation = ++this.layoutSettingsSettlementGeneration;
|
|
3783
|
+
const settings = this.settingsManager;
|
|
3784
|
+
await settings.flush();
|
|
3785
|
+
// SCRAMJET-DIVERGENCE: only the latest settlement may drain a shared batch of save errors.
|
|
3786
|
+
if (generation !== this.layoutSettingsSettlementGeneration)
|
|
3787
|
+
return;
|
|
3788
|
+
const errors = settings.drainErrors();
|
|
3789
|
+
selector.setSaveError(errors.length ? "Changes not saved; see warning" : undefined);
|
|
3790
|
+
if (errors.length) {
|
|
3791
|
+
const details = stripVTControlCharacters(errors.map(({ scope, error }) => `${scope}: ${error.message}`).join("; "))
|
|
3792
|
+
.replace(/[\r\n\t]/g, " ")
|
|
3793
|
+
.slice(0, 200);
|
|
3794
|
+
this.showWarning(`Display settings not saved (${details}). Changes may be lost on restart; project overrides still apply.`);
|
|
3795
|
+
}
|
|
3796
|
+
this.ui.requestRender();
|
|
3797
|
+
}
|
|
3136
3798
|
showSettingsSelector() {
|
|
3137
3799
|
this.showSelector((done) => {
|
|
3138
3800
|
const selector = new SettingsSelectorComponent({
|
|
@@ -3147,7 +3809,8 @@ export class InteractiveMode {
|
|
|
3147
3809
|
transport: this.settingsManager.getTransport(),
|
|
3148
3810
|
thinkingLevel: this.session.thinkingLevel,
|
|
3149
3811
|
availableThinkingLevels: this.session.getAvailableThinkingLevels(),
|
|
3150
|
-
|
|
3812
|
+
// SCRAMJET-DIVERGENCE: display fallback routed to the renamed builtin.
|
|
3813
|
+
currentTheme: this.settingsManager.getTheme() || "pi-dark",
|
|
3151
3814
|
availableThemes: getAvailableThemes(),
|
|
3152
3815
|
hideThinkingBlock: this.hideThinkingBlock,
|
|
3153
3816
|
collapseChangelog: this.settingsManager.getCollapseChangelog(),
|
|
@@ -3158,9 +3821,14 @@ export class InteractiveMode {
|
|
|
3158
3821
|
editorPaddingX: this.settingsManager.getEditorPaddingX(),
|
|
3159
3822
|
autocompleteMaxVisible: this.settingsManager.getAutocompleteMaxVisible(),
|
|
3160
3823
|
quietStartup: this.settingsManager.getQuietStartup(),
|
|
3161
|
-
clearOnShrink: this.settingsManager.getClearOnShrink(),
|
|
3162
3824
|
showTerminalProgress: this.settingsManager.getShowTerminalProgress(),
|
|
3163
3825
|
warnings: this.settingsManager.getWarnings(),
|
|
3826
|
+
tuiMode: this.tuiMode,
|
|
3827
|
+
dockEditor: this.settingsManager.getDockEditor(),
|
|
3828
|
+
retainTranscriptOnExit: this.settingsManager.getRetainTranscriptOnExit(),
|
|
3829
|
+
editorMaxHeightPercent: this.settingsManager.getEditorMaxHeightPercent(),
|
|
3830
|
+
scrollWheelStep: this.settingsManager.getScrollWheelStep(),
|
|
3831
|
+
viewportProjectOverrides: Object.keys(this.settingsManager.getProjectSettings()),
|
|
3164
3832
|
}, {
|
|
3165
3833
|
onAutoCompactChange: (enabled) => {
|
|
3166
3834
|
this.session.setAutoCompactionEnabled(enabled);
|
|
@@ -3168,19 +3836,11 @@ export class InteractiveMode {
|
|
|
3168
3836
|
},
|
|
3169
3837
|
onShowImagesChange: (enabled) => {
|
|
3170
3838
|
this.settingsManager.setShowImages(enabled);
|
|
3171
|
-
|
|
3172
|
-
if (child instanceof ToolExecutionComponent) {
|
|
3173
|
-
child.setShowImages(enabled);
|
|
3174
|
-
}
|
|
3175
|
-
}
|
|
3839
|
+
this.updateToolImagePresentation((component) => component.setShowImages(enabled));
|
|
3176
3840
|
},
|
|
3177
3841
|
onImageWidthCellsChange: (width) => {
|
|
3178
3842
|
this.settingsManager.setImageWidthCells(width);
|
|
3179
|
-
|
|
3180
|
-
if (child instanceof ToolExecutionComponent) {
|
|
3181
|
-
child.setImageWidthCells(width);
|
|
3182
|
-
}
|
|
3183
|
-
}
|
|
3843
|
+
this.updateToolImagePresentation((component) => component.setImageWidthCells(width));
|
|
3184
3844
|
},
|
|
3185
3845
|
onAutoResizeImagesChange: (enabled) => {
|
|
3186
3846
|
this.settingsManager.setImageAutoResize(enabled);
|
|
@@ -3212,7 +3872,8 @@ export class InteractiveMode {
|
|
|
3212
3872
|
this.settingsManager.setTheme(themeName);
|
|
3213
3873
|
this.ui.invalidate();
|
|
3214
3874
|
if (!result.success) {
|
|
3215
|
-
|
|
3875
|
+
// SCRAMJET-DIVERGENCE: fallback message names the renamed builtin.
|
|
3876
|
+
this.showError(`Failed to load theme "${themeName}": ${result.error}\nFell back to pi-dark theme.`);
|
|
3216
3877
|
}
|
|
3217
3878
|
},
|
|
3218
3879
|
onThemePreview: (themeName) => {
|
|
@@ -3222,17 +3883,7 @@ export class InteractiveMode {
|
|
|
3222
3883
|
this.ui.requestRender();
|
|
3223
3884
|
}
|
|
3224
3885
|
},
|
|
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
|
-
},
|
|
3886
|
+
onHideThinkingBlockChange: (hidden) => this.setThinkingBlockVisibility(hidden),
|
|
3236
3887
|
onCollapseChangelogChange: (collapsed) => {
|
|
3237
3888
|
this.settingsManager.setCollapseChangelog(collapsed);
|
|
3238
3889
|
},
|
|
@@ -3266,21 +3917,42 @@ export class InteractiveMode {
|
|
|
3266
3917
|
this.editor.setAutocompleteMaxVisible(maxVisible);
|
|
3267
3918
|
}
|
|
3268
3919
|
},
|
|
3269
|
-
onClearOnShrinkChange: (enabled) => {
|
|
3270
|
-
this.settingsManager.setClearOnShrink(enabled);
|
|
3271
|
-
this.ui.setClearOnShrink(enabled);
|
|
3272
|
-
},
|
|
3273
3920
|
onShowTerminalProgressChange: (enabled) => {
|
|
3274
3921
|
this.settingsManager.setShowTerminalProgress(enabled);
|
|
3275
3922
|
},
|
|
3276
3923
|
onWarningsChange: (warnings) => {
|
|
3277
3924
|
this.settingsManager.setWarnings(warnings);
|
|
3278
3925
|
},
|
|
3926
|
+
onRetainTranscriptOnExitChange: (enabled) => {
|
|
3927
|
+
this.settingsManager.setRetainTranscriptOnExit(enabled);
|
|
3928
|
+
void this.settleLayoutSettings(selector);
|
|
3929
|
+
return this.settingsManager.getRetainTranscriptOnExit();
|
|
3930
|
+
},
|
|
3931
|
+
onDockEditorChange: (enabled) => {
|
|
3932
|
+
this.settingsManager.setDockEditor(enabled);
|
|
3933
|
+
void this.settleLayoutSettings(selector);
|
|
3934
|
+
this.ui.refreshViewportLayout();
|
|
3935
|
+
if (!this.settingsManager.getDockEditor())
|
|
3936
|
+
this.ui.revealComponent(this.editorContainer);
|
|
3937
|
+
return this.settingsManager.getDockEditor();
|
|
3938
|
+
},
|
|
3939
|
+
onEditorMaxHeightPercentChange: (percent) => {
|
|
3940
|
+
this.settingsManager.setEditorMaxHeightPercent(percent);
|
|
3941
|
+
void this.settleLayoutSettings(selector);
|
|
3942
|
+
this.ui.refreshViewportLayout();
|
|
3943
|
+
return this.settingsManager.getEditorMaxHeightPercent();
|
|
3944
|
+
},
|
|
3945
|
+
onScrollWheelStepChange: (step) => {
|
|
3946
|
+
this.settingsManager.setScrollWheelStep(step);
|
|
3947
|
+
void this.settleLayoutSettings(selector);
|
|
3948
|
+
this.ui.requestRender();
|
|
3949
|
+
return this.settingsManager.getScrollWheelStep();
|
|
3950
|
+
},
|
|
3279
3951
|
onCancel: () => {
|
|
3280
3952
|
done();
|
|
3281
3953
|
this.ui.requestRender();
|
|
3282
3954
|
},
|
|
3283
|
-
});
|
|
3955
|
+
}, () => Math.max(1, Math.min(this.editorAvailableRows ?? Infinity, this.ui.terminal.rows - 4)));
|
|
3284
3956
|
return { component: selector, focus: selector.getSettingsList() };
|
|
3285
3957
|
});
|
|
3286
3958
|
}
|
|
@@ -3356,7 +4028,22 @@ export class InteractiveMode {
|
|
|
3356
4028
|
// Ignore auth lookup failures for warning-only checks.
|
|
3357
4029
|
}
|
|
3358
4030
|
}
|
|
3359
|
-
|
|
4031
|
+
selectorPreparationIsCurrent(session, generation) {
|
|
4032
|
+
const current = this.isInitialized && this.session === session && this.selectorOpenGeneration === generation;
|
|
4033
|
+
if (!current && this.pendingSelectorOpenGeneration === generation)
|
|
4034
|
+
this.pendingSelectorOpenGeneration = undefined;
|
|
4035
|
+
return current;
|
|
4036
|
+
}
|
|
4037
|
+
// SCRAMJET-DIVERGENCE: Refresh profile-scoped output-rate history before opening model selectors (issue 476).
|
|
4038
|
+
async showModelSelector(initialSearchInput) {
|
|
4039
|
+
const session = this.session;
|
|
4040
|
+
const generation = ++this.selectorOpenGeneration;
|
|
4041
|
+
this.pendingSelectorOpenGeneration = generation;
|
|
4042
|
+
await session.refreshOutputThroughputHistory().catch(() => undefined);
|
|
4043
|
+
if (!this.selectorPreparationIsCurrent(session, generation))
|
|
4044
|
+
return;
|
|
4045
|
+
this.pendingSelectorOpenGeneration = undefined;
|
|
4046
|
+
const outputThroughputHistory = session.outputThroughputHistory;
|
|
3360
4047
|
this.showSelector((done) => {
|
|
3361
4048
|
const selector = new ModelSelectorComponent(this.ui, this.session.model, this.settingsManager, this.session.modelRegistry, this.session.scopedModels, async (model) => {
|
|
3362
4049
|
try {
|
|
@@ -3375,20 +4062,28 @@ export class InteractiveMode {
|
|
|
3375
4062
|
}, () => {
|
|
3376
4063
|
done();
|
|
3377
4064
|
this.ui.requestRender();
|
|
3378
|
-
}, initialSearchInput);
|
|
4065
|
+
}, initialSearchInput, outputThroughputHistory);
|
|
3379
4066
|
return { component: selector, focus: selector };
|
|
3380
4067
|
});
|
|
3381
4068
|
}
|
|
3382
4069
|
async showModelsSelector() {
|
|
4070
|
+
const session = this.session;
|
|
4071
|
+
const generation = ++this.selectorOpenGeneration;
|
|
4072
|
+
this.pendingSelectorOpenGeneration = generation;
|
|
4073
|
+
await session.refreshOutputThroughputHistory().catch(() => undefined);
|
|
4074
|
+
if (!this.selectorPreparationIsCurrent(session, generation))
|
|
4075
|
+
return;
|
|
4076
|
+
const outputThroughputHistory = session.outputThroughputHistory;
|
|
3383
4077
|
// Get all available models
|
|
3384
|
-
|
|
3385
|
-
const allModels =
|
|
4078
|
+
session.modelRegistry.refresh();
|
|
4079
|
+
const allModels = session.modelRegistry.getAvailable();
|
|
3386
4080
|
if (allModels.length === 0) {
|
|
4081
|
+
this.pendingSelectorOpenGeneration = undefined;
|
|
3387
4082
|
this.showStatus("No models available");
|
|
3388
4083
|
return;
|
|
3389
4084
|
}
|
|
3390
4085
|
// Check if session has scoped models (from previous session-only changes or CLI --models)
|
|
3391
|
-
const sessionScopedModels =
|
|
4086
|
+
const sessionScopedModels = session.scopedModels;
|
|
3392
4087
|
const hasSessionScope = sessionScopedModels.length > 0;
|
|
3393
4088
|
// Build enabled model IDs from session state or settings
|
|
3394
4089
|
let currentEnabledIds = null;
|
|
@@ -3400,10 +4095,13 @@ export class InteractiveMode {
|
|
|
3400
4095
|
// Fall back to settings
|
|
3401
4096
|
const patterns = this.settingsManager.getEnabledModels();
|
|
3402
4097
|
if (patterns !== undefined && patterns.length > 0) {
|
|
3403
|
-
const scopedModels = await resolveModelScope(patterns,
|
|
4098
|
+
const scopedModels = await resolveModelScope(patterns, session.modelRegistry);
|
|
3404
4099
|
currentEnabledIds = scopedModels.map((scoped) => `${scoped.model.provider}/${scoped.model.id}`);
|
|
3405
4100
|
}
|
|
3406
4101
|
}
|
|
4102
|
+
if (!this.selectorPreparationIsCurrent(session, generation))
|
|
4103
|
+
return;
|
|
4104
|
+
this.pendingSelectorOpenGeneration = undefined;
|
|
3407
4105
|
// Helper to update session's scoped models (session-only, no persist)
|
|
3408
4106
|
const updateSessionModels = async (enabledIds) => {
|
|
3409
4107
|
currentEnabledIds = enabledIds === null ? null : [...enabledIds];
|
|
@@ -3441,7 +4139,7 @@ export class InteractiveMode {
|
|
|
3441
4139
|
done();
|
|
3442
4140
|
this.ui.requestRender();
|
|
3443
4141
|
},
|
|
3444
|
-
});
|
|
4142
|
+
}, outputThroughputHistory);
|
|
3445
4143
|
return { component: selector, focus: selector };
|
|
3446
4144
|
});
|
|
3447
4145
|
}
|
|
@@ -3468,7 +4166,7 @@ export class InteractiveMode {
|
|
|
3468
4166
|
}
|
|
3469
4167
|
catch (error) {
|
|
3470
4168
|
done();
|
|
3471
|
-
this.showError(
|
|
4169
|
+
this.showError(describeRuntimeError(error));
|
|
3472
4170
|
}
|
|
3473
4171
|
}, () => {
|
|
3474
4172
|
done();
|
|
@@ -3494,7 +4192,7 @@ export class InteractiveMode {
|
|
|
3494
4192
|
this.showStatus("Cloned to new session");
|
|
3495
4193
|
}
|
|
3496
4194
|
catch (error) {
|
|
3497
|
-
this.showError(
|
|
4195
|
+
this.showError(describeRuntimeError(error));
|
|
3498
4196
|
}
|
|
3499
4197
|
}
|
|
3500
4198
|
showTreeSelector(initialSelectedId) {
|
|
@@ -3571,7 +4269,7 @@ export class InteractiveMode {
|
|
|
3571
4269
|
return;
|
|
3572
4270
|
}
|
|
3573
4271
|
// Update UI
|
|
3574
|
-
this.
|
|
4272
|
+
this.clearTranscript();
|
|
3575
4273
|
this.renderInitialMessages();
|
|
3576
4274
|
if (result.editorText && !this.editor.getText().trim()) {
|
|
3577
4275
|
this.editor.setText(result.editorText);
|
|
@@ -4006,7 +4704,7 @@ export class InteractiveMode {
|
|
|
4006
4704
|
this.editorContainer.clear();
|
|
4007
4705
|
this.editorContainer.addChild(reloadBox);
|
|
4008
4706
|
this.ui.setFocus(reloadBox);
|
|
4009
|
-
this.ui.
|
|
4707
|
+
this.ui.rebuild();
|
|
4010
4708
|
await new Promise((resolve) => process.nextTick(resolve));
|
|
4011
4709
|
const dismissReloadBox = (editor) => {
|
|
4012
4710
|
this.editorContainer.clear();
|
|
@@ -4023,11 +4721,10 @@ export class InteractiveMode {
|
|
|
4023
4721
|
}
|
|
4024
4722
|
setRegisteredThemes(this.session.resourceLoader.getThemes().themes);
|
|
4025
4723
|
this.hideThinkingBlock = this.settingsManager.getHideThinkingBlock();
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
}
|
|
4724
|
+
// SCRAMJET-DIVERGENCE: re-resolve through the resolver after reload so newly registered
|
|
4725
|
+
// themes (and the captured classification) are honored; force a re-read since reload's purpose
|
|
4726
|
+
// is to pick up on-disk theme edits even when the active theme's name is unchanged.
|
|
4727
|
+
this.reapplyResolvedTheme(true);
|
|
4031
4728
|
const editorPaddingX = this.settingsManager.getEditorPaddingX();
|
|
4032
4729
|
const autocompleteMaxVisible = this.settingsManager.getAutocompleteMaxVisible();
|
|
4033
4730
|
this.defaultEditor.setPaddingX(editorPaddingX);
|
|
@@ -4037,7 +4734,6 @@ export class InteractiveMode {
|
|
|
4037
4734
|
this.editor.setAutocompleteMaxVisible?.(autocompleteMaxVisible);
|
|
4038
4735
|
}
|
|
4039
4736
|
this.ui.setShowHardwareCursor(this.settingsManager.getShowHardwareCursor());
|
|
4040
|
-
this.ui.setClearOnShrink(this.settingsManager.getClearOnShrink());
|
|
4041
4737
|
this.setupAutocompleteProvider();
|
|
4042
4738
|
const runner = this.session.extensionRunner;
|
|
4043
4739
|
this.setupExtensionShortcuts(runner);
|
|
@@ -4055,7 +4751,7 @@ export class InteractiveMode {
|
|
|
4055
4751
|
}
|
|
4056
4752
|
catch (error) {
|
|
4057
4753
|
dismissReloadBox(previousEditor);
|
|
4058
|
-
this.showError(`Reload failed: ${
|
|
4754
|
+
this.showError(`Reload failed: ${describeRuntimeError(error)}`);
|
|
4059
4755
|
}
|
|
4060
4756
|
}
|
|
4061
4757
|
async handleExportCommand(text) {
|
|
@@ -4173,13 +4869,13 @@ export class InteractiveMode {
|
|
|
4173
4869
|
else {
|
|
4174
4870
|
this.showWarning("Usage: /name <name>");
|
|
4175
4871
|
}
|
|
4176
|
-
this.
|
|
4872
|
+
this.commitFinalizedChatOutput();
|
|
4177
4873
|
return;
|
|
4178
4874
|
}
|
|
4179
4875
|
this.session.setSessionName(name);
|
|
4180
4876
|
this.chatContainer.addChild(new Spacer(1));
|
|
4181
4877
|
this.chatContainer.addChild(new Text(theme.fg("dim", `Session name set: ${name}`), 1, 0));
|
|
4182
|
-
this.
|
|
4878
|
+
this.commitFinalizedChatOutput();
|
|
4183
4879
|
}
|
|
4184
4880
|
handleSessionCommand() {
|
|
4185
4881
|
const stats = this.session.getSessionStats();
|
|
@@ -4212,7 +4908,7 @@ export class InteractiveMode {
|
|
|
4212
4908
|
}
|
|
4213
4909
|
this.chatContainer.addChild(new Spacer(1));
|
|
4214
4910
|
this.chatContainer.addChild(new Text(info, 1, 0));
|
|
4215
|
-
this.
|
|
4911
|
+
this.commitFinalizedChatOutput();
|
|
4216
4912
|
}
|
|
4217
4913
|
handleChangelogCommand() {
|
|
4218
4914
|
const changelogPath = getChangelogPath();
|
|
@@ -4229,7 +4925,7 @@ export class InteractiveMode {
|
|
|
4229
4925
|
this.chatContainer.addChild(new Spacer(1));
|
|
4230
4926
|
this.chatContainer.addChild(new Markdown(changelogMarkdown, 1, 1, this.getMarkdownThemeWithSettings()));
|
|
4231
4927
|
this.chatContainer.addChild(new DynamicBorder());
|
|
4232
|
-
this.
|
|
4928
|
+
this.commitFinalizedChatOutput();
|
|
4233
4929
|
}
|
|
4234
4930
|
/**
|
|
4235
4931
|
* Get capitalized display string for an app keybinding action.
|
|
@@ -4350,7 +5046,7 @@ export class InteractiveMode {
|
|
|
4350
5046
|
this.chatContainer.addChild(new Spacer(1));
|
|
4351
5047
|
this.chatContainer.addChild(new Markdown(hotkeys.trim(), 1, 1, this.getMarkdownThemeWithSettings()));
|
|
4352
5048
|
this.chatContainer.addChild(new DynamicBorder());
|
|
4353
|
-
this.
|
|
5049
|
+
this.commitFinalizedChatOutput();
|
|
4354
5050
|
}
|
|
4355
5051
|
async handleClearCommand() {
|
|
4356
5052
|
if (this.loadingAnimation) {
|
|
@@ -4366,7 +5062,7 @@ export class InteractiveMode {
|
|
|
4366
5062
|
this.renderCurrentSessionState();
|
|
4367
5063
|
this.chatContainer.addChild(new Spacer(1));
|
|
4368
5064
|
this.chatContainer.addChild(new Text(`${theme.fg("accent", "✓ New session started")}`, 1, 1));
|
|
4369
|
-
this.
|
|
5065
|
+
this.commitFinalizedChatOutput();
|
|
4370
5066
|
}
|
|
4371
5067
|
catch (error) {
|
|
4372
5068
|
await this.handleFatalRuntimeError("Failed to create session", error);
|
|
@@ -4397,22 +5093,38 @@ export class InteractiveMode {
|
|
|
4397
5093
|
fs.writeFileSync(debugLogPath, debugData);
|
|
4398
5094
|
this.chatContainer.addChild(new Spacer(1));
|
|
4399
5095
|
this.chatContainer.addChild(new Text(`${theme.fg("accent", "✓ Debug log written")}\n${theme.fg("muted", debugLogPath)}`, 1, 1));
|
|
4400
|
-
this.
|
|
5096
|
+
this.commitFinalizedChatOutput();
|
|
4401
5097
|
}
|
|
4402
5098
|
handleArminSaysHi() {
|
|
5099
|
+
const component = new ArminComponent(this.ui, () => {
|
|
5100
|
+
if (!this.mutableChatComponents.has(component) || !this.chatContainer.children.includes(component))
|
|
5101
|
+
return;
|
|
5102
|
+
this.setChatComponentMutable(component, false);
|
|
5103
|
+
this.promoteFinalizedChatPrefix();
|
|
5104
|
+
this.ui.requestRender();
|
|
5105
|
+
});
|
|
4403
5106
|
this.chatContainer.addChild(new Spacer(1));
|
|
4404
|
-
this.chatContainer.addChild(
|
|
4405
|
-
this.
|
|
5107
|
+
this.chatContainer.addChild(component);
|
|
5108
|
+
this.setChatComponentMutable(component, true);
|
|
5109
|
+
this.commitFinalizedChatOutput();
|
|
4406
5110
|
}
|
|
4407
5111
|
handleDementedDelves() {
|
|
4408
5112
|
this.chatContainer.addChild(new Spacer(1));
|
|
4409
5113
|
this.chatContainer.addChild(new EarendilAnnouncementComponent());
|
|
4410
|
-
this.
|
|
5114
|
+
this.commitFinalizedChatOutput();
|
|
4411
5115
|
}
|
|
4412
5116
|
handleDaxnuts() {
|
|
5117
|
+
const component = new DaxnutsComponent(this.ui, () => {
|
|
5118
|
+
if (!this.mutableChatComponents.has(component) || !this.chatContainer.children.includes(component))
|
|
5119
|
+
return;
|
|
5120
|
+
this.setChatComponentMutable(component, false);
|
|
5121
|
+
this.promoteFinalizedChatPrefix();
|
|
5122
|
+
this.ui.requestRender();
|
|
5123
|
+
});
|
|
4413
5124
|
this.chatContainer.addChild(new Spacer(1));
|
|
4414
|
-
this.chatContainer.addChild(
|
|
4415
|
-
this.
|
|
5125
|
+
this.chatContainer.addChild(component);
|
|
5126
|
+
this.setChatComponentMutable(component, true);
|
|
5127
|
+
this.commitFinalizedChatOutput();
|
|
4416
5128
|
}
|
|
4417
5129
|
checkDaxnutsEasterEgg(model) {
|
|
4418
5130
|
if (model.provider === "opencode" && model.id.toLowerCase().includes("kimi-k2.5")) {
|
|
@@ -4438,6 +5150,7 @@ export class InteractiveMode {
|
|
|
4438
5150
|
this.pendingBashComponents.push(this.bashComponent);
|
|
4439
5151
|
}
|
|
4440
5152
|
else {
|
|
5153
|
+
this.sealStatus();
|
|
4441
5154
|
this.chatContainer.addChild(this.bashComponent);
|
|
4442
5155
|
}
|
|
4443
5156
|
// Show output and complete
|
|
@@ -4447,6 +5160,7 @@ export class InteractiveMode {
|
|
|
4447
5160
|
this.bashComponent.setComplete(result.exitCode, result.cancelled, result.truncated ? { truncated: true, content: result.output } : undefined, result.fullOutputPath);
|
|
4448
5161
|
// Record the result in session
|
|
4449
5162
|
this.session.recordBashResult(command, result, { excludeFromContext });
|
|
5163
|
+
this.promoteFinalizedChatPrefix();
|
|
4450
5164
|
this.bashComponent = undefined;
|
|
4451
5165
|
this.ui.requestRender();
|
|
4452
5166
|
return;
|
|
@@ -4454,6 +5168,7 @@ export class InteractiveMode {
|
|
|
4454
5168
|
// Normal execution path (possibly with custom operations)
|
|
4455
5169
|
const isDeferred = this.session.isStreaming;
|
|
4456
5170
|
this.bashComponent = new BashExecutionComponent(command, this.ui, excludeFromContext);
|
|
5171
|
+
this.setChatComponentMutable(this.bashComponent, true);
|
|
4457
5172
|
if (isDeferred) {
|
|
4458
5173
|
// Show in pending area when agent is streaming
|
|
4459
5174
|
this.pendingMessagesContainer.addChild(this.bashComponent);
|
|
@@ -4461,6 +5176,7 @@ export class InteractiveMode {
|
|
|
4461
5176
|
}
|
|
4462
5177
|
else {
|
|
4463
5178
|
// Show in chat immediately when agent is idle
|
|
5179
|
+
this.sealStatus();
|
|
4464
5180
|
this.chatContainer.addChild(this.bashComponent);
|
|
4465
5181
|
}
|
|
4466
5182
|
this.ui.requestRender();
|
|
@@ -4481,6 +5197,9 @@ export class InteractiveMode {
|
|
|
4481
5197
|
}
|
|
4482
5198
|
this.showError(`Bash command failed: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
4483
5199
|
}
|
|
5200
|
+
if (this.bashComponent)
|
|
5201
|
+
this.setChatComponentMutable(this.bashComponent, false);
|
|
5202
|
+
this.promoteFinalizedChatPrefix();
|
|
4484
5203
|
this.bashComponent = undefined;
|
|
4485
5204
|
this.ui.requestRender();
|
|
4486
5205
|
}
|
|
@@ -4503,24 +5222,30 @@ export class InteractiveMode {
|
|
|
4503
5222
|
// Ignore, will be emitted as an event
|
|
4504
5223
|
}
|
|
4505
5224
|
}
|
|
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();
|
|
5225
|
+
stop(options) {
|
|
5226
|
+
try {
|
|
5227
|
+
if (this.isInitialized) {
|
|
5228
|
+
this.isInitialized = false;
|
|
5229
|
+
this.ui.stop(options);
|
|
5230
|
+
}
|
|
4520
5231
|
}
|
|
4521
|
-
|
|
4522
|
-
this.
|
|
4523
|
-
this.
|
|
5232
|
+
finally {
|
|
5233
|
+
this.selectorOpenGeneration++;
|
|
5234
|
+
this.pendingSelectorOpenGeneration = undefined;
|
|
5235
|
+
this.unregisterSignalHandlers();
|
|
5236
|
+
if (this.settingsManager.getShowTerminalProgress()) {
|
|
5237
|
+
this.ui.terminal.setProgress(false);
|
|
5238
|
+
}
|
|
5239
|
+
if (this.loadingAnimation) {
|
|
5240
|
+
this.loadingAnimation.stop();
|
|
5241
|
+
this.loadingAnimation = undefined;
|
|
5242
|
+
}
|
|
5243
|
+
this.clearExtensionTerminalInputListeners();
|
|
5244
|
+
this.footer.dispose();
|
|
5245
|
+
this.footerDataProvider.dispose();
|
|
5246
|
+
if (this.unsubscribe) {
|
|
5247
|
+
this.unsubscribe();
|
|
5248
|
+
}
|
|
4524
5249
|
}
|
|
4525
5250
|
}
|
|
4526
5251
|
}
|