@leanandmean/coding-agent 0.74.1-scramjet.58 → 0.74.1-scramjet.59
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/README.md +14 -2
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +2 -0
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/types.d.ts +3 -1
- 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/settings-manager.d.ts +15 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +74 -0
- package/dist/core/settings-manager.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/modes/interactive/components/assistant-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/assistant-message.js +3 -2
- package/dist/modes/interactive/components/assistant-message.js.map +1 -1
- package/dist/modes/interactive/components/extension-editor.d.ts.map +1 -1
- package/dist/modes/interactive/components/extension-editor.js +3 -1
- 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/settings-selector.d.ts +17 -1
- package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js +84 -2
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +6 -1
- 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 +18 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +480 -100
- package/dist/modes/interactive/interactive-mode.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/docs/extensions.md +27 -11
- package/docs/keybindings.md +23 -1
- package/docs/settings.md +11 -0
- package/docs/terminal-setup.md +79 -2
- package/docs/terminal-verification.md +71 -0
- package/docs/tui.md +91 -29
- package/examples/extensions/doom-overlay/doom-component.ts +3 -3
- 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/package.json +4 -4
|
@@ -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";
|
|
@@ -24,7 +25,7 @@ import { formatMissingSessionCwdPrompt, MissingSessionCwdError } from "../../cor
|
|
|
24
25
|
import { SessionManager } from "../../core/session-manager.js";
|
|
25
26
|
import { BUILTIN_SLASH_COMMANDS } from "../../core/slash-commands.js";
|
|
26
27
|
import { getChangelogPath, getNewEntries, parseChangelog } from "../../utils/changelog.js";
|
|
27
|
-
import { copyToClipboard } from "../../utils/clipboard.js";
|
|
28
|
+
import { copyToClipboard, createWslClipboardReader, readClipboardText } from "../../utils/clipboard.js";
|
|
28
29
|
import { extensionForImageMimeType, readClipboardImage } from "../../utils/clipboard-image.js";
|
|
29
30
|
import { parseGitUrl } from "../../utils/git.js";
|
|
30
31
|
import { getCwdRelativePath } from "../../utils/paths.js";
|
|
@@ -108,6 +109,7 @@ export class InteractiveMode {
|
|
|
108
109
|
options;
|
|
109
110
|
runtimeHost;
|
|
110
111
|
ui;
|
|
112
|
+
tuiMode;
|
|
111
113
|
committedChatContainer;
|
|
112
114
|
chatContainer;
|
|
113
115
|
mutableChatComponents = new Set();
|
|
@@ -115,6 +117,7 @@ export class InteractiveMode {
|
|
|
115
117
|
statusContainer;
|
|
116
118
|
defaultEditor;
|
|
117
119
|
editor;
|
|
120
|
+
editorAvailableRows;
|
|
118
121
|
editorComponentFactory;
|
|
119
122
|
autocompleteProvider;
|
|
120
123
|
autocompleteProviderWrappers = [];
|
|
@@ -156,6 +159,7 @@ export class InteractiveMode {
|
|
|
156
159
|
agentRunGeneration = 0;
|
|
157
160
|
selectorOpenGeneration = 0;
|
|
158
161
|
pendingSelectorOpenGeneration;
|
|
162
|
+
layoutSettingsSettlementGeneration = 0;
|
|
159
163
|
// Tool output expansion state
|
|
160
164
|
toolOutputExpanded = false;
|
|
161
165
|
// Thinking block visibility state
|
|
@@ -186,6 +190,7 @@ export class InteractiveMode {
|
|
|
186
190
|
extensionSelector = undefined;
|
|
187
191
|
extensionInput = undefined;
|
|
188
192
|
extensionEditor = undefined;
|
|
193
|
+
customHeightAllocation;
|
|
189
194
|
extensionTerminalInputUnsubscribers = new Set();
|
|
190
195
|
// Extension widgets (components rendered above/below the editor)
|
|
191
196
|
extensionWidgetsAbove = new Map();
|
|
@@ -223,8 +228,28 @@ export class InteractiveMode {
|
|
|
223
228
|
await this.rebindCurrentSession();
|
|
224
229
|
});
|
|
225
230
|
this.version = VERSION;
|
|
226
|
-
|
|
231
|
+
this.tuiMode = this.settingsManager.getTuiMode();
|
|
232
|
+
// SCRAMJET-DIVERGENCE: retained interactive rendering and injectable terminals.
|
|
227
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
|
+
});
|
|
228
253
|
this.headerContainer = new Container();
|
|
229
254
|
this.committedChatContainer = new Container();
|
|
230
255
|
this.chatContainer = new Container();
|
|
@@ -241,6 +266,7 @@ export class InteractiveMode {
|
|
|
241
266
|
autocompleteMaxVisible,
|
|
242
267
|
});
|
|
243
268
|
this.editor = this.defaultEditor;
|
|
269
|
+
this.configureEditorHeight(this.editor);
|
|
244
270
|
this.defaultEditor.setSpellcheckProvider(new NspellProvider());
|
|
245
271
|
this.editorContainer = new Container();
|
|
246
272
|
this.editorContainer.addChild(this.editor);
|
|
@@ -257,6 +283,187 @@ export class InteractiveMode {
|
|
|
257
283
|
this.detectedClassification = detectThemeFromEnvironment()?.theme;
|
|
258
284
|
initTheme(this.settingsManager.getTheme(), true);
|
|
259
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
|
+
}
|
|
260
467
|
// SCRAMJET-DIVERGENCE: finalized transcript components are promoted atomically into append-only history (#389).
|
|
261
468
|
promoteFinalizedChatPrefix() {
|
|
262
469
|
let promoted = false;
|
|
@@ -270,8 +477,12 @@ export class InteractiveMode {
|
|
|
270
477
|
this.committedChatContainer.addChild(child);
|
|
271
478
|
promoted = true;
|
|
272
479
|
}
|
|
273
|
-
if (promoted)
|
|
274
|
-
this.ui.
|
|
480
|
+
if (promoted) {
|
|
481
|
+
if (this.ui.getViewportState())
|
|
482
|
+
this.ui.requestRender();
|
|
483
|
+
else
|
|
484
|
+
this.ui.commit();
|
|
485
|
+
}
|
|
275
486
|
}
|
|
276
487
|
setChatComponentMutable(component, mutable) {
|
|
277
488
|
if (mutable)
|
|
@@ -289,6 +500,9 @@ export class InteractiveMode {
|
|
|
289
500
|
this.promoteFinalizedChatPrefix();
|
|
290
501
|
}
|
|
291
502
|
clearTranscript() {
|
|
503
|
+
this.clipboardPasteGeneration++;
|
|
504
|
+
if (this.ui.getViewportState())
|
|
505
|
+
this.ui.resetViewport();
|
|
292
506
|
this.pendingToolFinalizations = new Set();
|
|
293
507
|
const tools = new Set([
|
|
294
508
|
...this.pendingTools.values(),
|
|
@@ -492,26 +706,20 @@ export class InteractiveMode {
|
|
|
492
706
|
this.changelogMarkdown = this.getChangelogForDisplay();
|
|
493
707
|
// Ensure fd and rg are available (downloads if missing, adds to PATH via getBinDir)
|
|
494
708
|
// Both are needed: fd for autocomplete, rg for grep tool and bash commands
|
|
495
|
-
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)]);
|
|
496
714
|
this.fdPath = fdPath;
|
|
497
|
-
if (this.session.scopedModels.length > 0 && (this.options.verbose || !this.settingsManager.getQuietStartup())) {
|
|
498
|
-
const modelList = this.session.scopedModels
|
|
499
|
-
.map((sm) => {
|
|
500
|
-
const thinkingStr = sm.thinkingLevel ? `:${sm.thinkingLevel}` : "";
|
|
501
|
-
return `${sm.model.id}${thinkingStr}`;
|
|
502
|
-
})
|
|
503
|
-
.join(", ");
|
|
504
|
-
const cycleKeys = this.keybindings.getKeys("app.model.cycleForward");
|
|
505
|
-
const cycleHint = cycleKeys.length > 0
|
|
506
|
-
? theme.fg("muted", ` (${formatKeyText(cycleKeys.join("/"), { capitalize: true })} to cycle)`)
|
|
507
|
-
: "";
|
|
508
|
-
console.log(theme.fg("dim", `Model scope: ${modelList}${cycleHint}`));
|
|
509
|
-
}
|
|
510
715
|
// Add header container as first child (content built after theme detection below)
|
|
511
716
|
this.ui.addChild(this.headerContainer);
|
|
512
717
|
this.ui.addChild(this.committedChatContainer);
|
|
513
718
|
this.ui.addChild(this.chatContainer);
|
|
514
|
-
|
|
719
|
+
if (this.tuiMode === "retained")
|
|
720
|
+
this.configureRetainedViewport();
|
|
721
|
+
else
|
|
722
|
+
this.ui.setLiveRegionStart(this.chatContainer);
|
|
515
723
|
this.ui.addChild(this.pendingMessagesContainer);
|
|
516
724
|
this.ui.addChild(this.statusContainer);
|
|
517
725
|
this.renderWidgets(); // Initialize with default spacer
|
|
@@ -541,6 +749,14 @@ export class InteractiveMode {
|
|
|
541
749
|
}
|
|
542
750
|
// Build themed header content (after theme detection so colors are correct)
|
|
543
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
|
+
}
|
|
544
760
|
const logo = theme.bold(theme.fg("accent", APP_NAME)) + theme.fg("dim", ` v${this.version}`);
|
|
545
761
|
const hint = (keybinding, description) => keyHint(keybinding, description);
|
|
546
762
|
const expandedInstructions = [
|
|
@@ -584,6 +800,8 @@ export class InteractiveMode {
|
|
|
584
800
|
this.builtInHeader = new Text("", 0, 0);
|
|
585
801
|
this.headerContainer.addChild(this.builtInHeader);
|
|
586
802
|
}
|
|
803
|
+
for (const diagnostic of startupDiagnostics)
|
|
804
|
+
this.headerContainer.addChild(new Text(diagnostic, 1, 0));
|
|
587
805
|
// Initialize extensions first so resources are shown before messages
|
|
588
806
|
await this.rebindCurrentSession();
|
|
589
807
|
// Render initial messages AFTER showing loaded resources
|
|
@@ -1503,7 +1721,19 @@ export class InteractiveMode {
|
|
|
1503
1721
|
this.extensionWidgetsBelow.clear();
|
|
1504
1722
|
this.renderWidgets();
|
|
1505
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
|
+
}
|
|
1506
1735
|
resetExtensionUI() {
|
|
1736
|
+
this.releaseCustomHeight();
|
|
1507
1737
|
if (this.extensionSelector) {
|
|
1508
1738
|
this.hideExtensionSelector();
|
|
1509
1739
|
}
|
|
@@ -1875,6 +2105,7 @@ export class InteractiveMode {
|
|
|
1875
2105
|
}
|
|
1876
2106
|
}
|
|
1877
2107
|
this.editor = newEditor;
|
|
2108
|
+
this.configureEditorHeight(this.editor);
|
|
1878
2109
|
}
|
|
1879
2110
|
else {
|
|
1880
2111
|
// Restore default editor with text from custom editor
|
|
@@ -1901,29 +2132,77 @@ export class InteractiveMode {
|
|
|
1901
2132
|
}
|
|
1902
2133
|
/** Show a custom component with keyboard focus. Overlay mode renders on top of existing content. */
|
|
1903
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");
|
|
1904
2137
|
const savedText = this.editor.getText();
|
|
1905
2138
|
const isOverlay = options?.overlay ?? false;
|
|
1906
|
-
const restoreEditor = () => {
|
|
2139
|
+
const restoreEditor = (restoreFocus = true) => {
|
|
1907
2140
|
this.editorContainer.clear();
|
|
1908
2141
|
this.editorContainer.addChild(this.editor);
|
|
1909
2142
|
this.editor.setText(savedText);
|
|
1910
|
-
|
|
2143
|
+
if (restoreFocus)
|
|
2144
|
+
this.ui.setFocus(this.editor);
|
|
1911
2145
|
this.ui.requestRender();
|
|
1912
2146
|
};
|
|
1913
2147
|
return new Promise((resolve, reject) => {
|
|
1914
2148
|
let component;
|
|
1915
2149
|
let attachedTool;
|
|
1916
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
|
+
};
|
|
1917
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
|
+
};
|
|
1918
2192
|
const close = (result) => {
|
|
1919
2193
|
if (closed)
|
|
1920
2194
|
return;
|
|
1921
2195
|
closed = true;
|
|
2196
|
+
removeInputGuard?.();
|
|
2197
|
+
releaseHeight();
|
|
1922
2198
|
attachedTool?.detachCommittedContext();
|
|
2199
|
+
const current = !attachedTool || this.chatContainer.children.includes(attachedTool);
|
|
2200
|
+
if (attachedTool)
|
|
2201
|
+
restoreAttachedFocus(current);
|
|
1923
2202
|
if (isOverlay)
|
|
1924
2203
|
this.ui.hideOverlay();
|
|
1925
|
-
else
|
|
1926
|
-
restoreEditor();
|
|
2204
|
+
else if (current)
|
|
2205
|
+
restoreEditor(!attachedTool || !retained);
|
|
1927
2206
|
// Note: both branches above already call requestRender
|
|
1928
2207
|
resolve(result);
|
|
1929
2208
|
try {
|
|
@@ -1951,21 +2230,70 @@ export class InteractiveMode {
|
|
|
1951
2230
|
committedContext = attachment.render(this.ui, theme);
|
|
1952
2231
|
this.committedChatContainer.addChild(committedContext);
|
|
1953
2232
|
tool.attachCommittedContext(component);
|
|
1954
|
-
|
|
2233
|
+
if (retained)
|
|
2234
|
+
this.ui.replaceFocus(this.editor, pendingFocus);
|
|
2235
|
+
else
|
|
2236
|
+
this.ui.setFocus(null);
|
|
1955
2237
|
this.editorContainer.clear();
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
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
|
+
});
|
|
1965
2293
|
}
|
|
2294
|
+
await reveal();
|
|
1966
2295
|
if (closed)
|
|
1967
2296
|
return;
|
|
1968
|
-
this.ui.setFocus(tool);
|
|
1969
2297
|
this.ui.requestRender();
|
|
1970
2298
|
return;
|
|
1971
2299
|
}
|
|
@@ -1987,30 +2315,23 @@ export class InteractiveMode {
|
|
|
1987
2315
|
options?.onHandle?.(handle);
|
|
1988
2316
|
}
|
|
1989
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
|
+
}
|
|
1990
2328
|
this.editorContainer.clear();
|
|
1991
2329
|
this.editorContainer.addChild(component);
|
|
1992
2330
|
this.ui.setFocus(component);
|
|
1993
2331
|
this.ui.requestRender();
|
|
1994
2332
|
}
|
|
1995
2333
|
})
|
|
1996
|
-
.catch(
|
|
1997
|
-
if (closed)
|
|
1998
|
-
return;
|
|
1999
|
-
attachedTool?.cancelCommittedContext();
|
|
2000
|
-
if (committedContext) {
|
|
2001
|
-
this.committedChatContainer.removeChild(committedContext);
|
|
2002
|
-
this.ui.rebuild();
|
|
2003
|
-
}
|
|
2004
|
-
try {
|
|
2005
|
-
component?.dispose?.();
|
|
2006
|
-
}
|
|
2007
|
-
catch {
|
|
2008
|
-
/* ignore dispose errors */
|
|
2009
|
-
}
|
|
2010
|
-
if (!isOverlay)
|
|
2011
|
-
restoreEditor();
|
|
2012
|
-
reject(err);
|
|
2013
|
-
});
|
|
2334
|
+
.catch(fail);
|
|
2014
2335
|
});
|
|
2015
2336
|
}
|
|
2016
2337
|
/**
|
|
@@ -2096,6 +2417,7 @@ export class InteractiveMode {
|
|
|
2096
2417
|
this.defaultEditor.onAction("app.session.fork", () => this.showUserMessageSelector());
|
|
2097
2418
|
this.defaultEditor.onAction("app.session.resume", () => this.showSessionSelector());
|
|
2098
2419
|
this.defaultEditor.onChange = (text) => {
|
|
2420
|
+
this.clipboardPasteGeneration++;
|
|
2099
2421
|
const wasBashMode = this.isBashMode;
|
|
2100
2422
|
this.isBashMode = text.trimStart().startsWith("!");
|
|
2101
2423
|
if (wasBashMode !== this.isBashMode) {
|
|
@@ -2132,6 +2454,8 @@ export class InteractiveMode {
|
|
|
2132
2454
|
text = text.trim();
|
|
2133
2455
|
if (!text)
|
|
2134
2456
|
return;
|
|
2457
|
+
// SCRAMJET-DIVERGENCE: submission intent precedes command routing and queued-message delivery.
|
|
2458
|
+
this.ui.followViewport();
|
|
2135
2459
|
// Handle commands
|
|
2136
2460
|
if (text === "/settings") {
|
|
2137
2461
|
this.showSettingsSelector();
|
|
@@ -2358,6 +2682,7 @@ export class InteractiveMode {
|
|
|
2358
2682
|
}
|
|
2359
2683
|
else if (event.message.role === "user") {
|
|
2360
2684
|
this.addMessageToChat(event.message);
|
|
2685
|
+
this.ui.followViewport();
|
|
2361
2686
|
this.updatePendingMessagesDisplay();
|
|
2362
2687
|
this.ui.requestRender();
|
|
2363
2688
|
}
|
|
@@ -2920,9 +3245,18 @@ export class InteractiveMode {
|
|
|
2920
3245
|
// Drain any in-flight Kitty key release events before stopping.
|
|
2921
3246
|
// This prevents escape sequences from leaking to the parent shell over slow SSH.
|
|
2922
3247
|
await this.ui.terminal.drainInput(1000);
|
|
2923
|
-
this.
|
|
3248
|
+
await this.settingsManager.flush();
|
|
3249
|
+
let exitCode = 0;
|
|
3250
|
+
try {
|
|
3251
|
+
this.stop({ retainContent: this.settingsManager.getRetainTranscriptOnExit() });
|
|
3252
|
+
}
|
|
3253
|
+
catch (error) {
|
|
3254
|
+
exitCode = 1;
|
|
3255
|
+
console.error(`Could not retain terminal history: ${error instanceof Error ? error.message : String(error)}`);
|
|
3256
|
+
}
|
|
3257
|
+
await this.ui.terminal.flush?.();
|
|
2924
3258
|
await this.runtimeHost.dispose();
|
|
2925
|
-
process.exit(
|
|
3259
|
+
process.exit(exitCode);
|
|
2926
3260
|
}
|
|
2927
3261
|
emergencyTerminalExit() {
|
|
2928
3262
|
this.isShuttingDown = true;
|
|
@@ -3012,7 +3346,7 @@ export class InteractiveMode {
|
|
|
3012
3346
|
}
|
|
3013
3347
|
this.signalCleanupHandlers = [];
|
|
3014
3348
|
}
|
|
3015
|
-
handleCtrlZ() {
|
|
3349
|
+
async handleCtrlZ() {
|
|
3016
3350
|
if (process.platform === "win32") {
|
|
3017
3351
|
this.showStatus("Suspend to background is not supported on Windows");
|
|
3018
3352
|
return;
|
|
@@ -3033,7 +3367,8 @@ export class InteractiveMode {
|
|
|
3033
3367
|
this.ui.rebuild();
|
|
3034
3368
|
});
|
|
3035
3369
|
try {
|
|
3036
|
-
//
|
|
3370
|
+
// SCRAMJET-DIVERGENCE: drain releases before handing keyboard ownership back to the shell.
|
|
3371
|
+
await this.ui.terminal.drainInput();
|
|
3037
3372
|
this.ui.stop();
|
|
3038
3373
|
// Send SIGTSTP to process group (pid=0 means all processes in group)
|
|
3039
3374
|
process.kill(0, "SIGTSTP");
|
|
@@ -3048,6 +3383,7 @@ export class InteractiveMode {
|
|
|
3048
3383
|
const text = (this.editor.getExpandedText?.() ?? this.editor.getText()).trim();
|
|
3049
3384
|
if (!text)
|
|
3050
3385
|
return;
|
|
3386
|
+
this.ui.followViewport();
|
|
3051
3387
|
// Queue input during compaction (extension commands execute immediately)
|
|
3052
3388
|
if (this.session.isCompacting) {
|
|
3053
3389
|
if (this.isExtensionCommand(text)) {
|
|
@@ -3141,20 +3477,20 @@ export class InteractiveMode {
|
|
|
3141
3477
|
this.ui.rebuild();
|
|
3142
3478
|
}
|
|
3143
3479
|
toggleThinkingBlockVisibility() {
|
|
3144
|
-
this.
|
|
3145
|
-
this.settingsManager.setHideThinkingBlock(this.hideThinkingBlock);
|
|
3146
|
-
// Rebuild chat from session messages
|
|
3147
|
-
this.rebuildChatFromMessages();
|
|
3148
|
-
// If streaming, re-add the streaming component with updated visibility and re-render
|
|
3149
|
-
if (this.streamingComponent && this.streamingMessage) {
|
|
3150
|
-
this.streamingComponent.setHideThinkingBlock(this.hideThinkingBlock);
|
|
3151
|
-
this.streamingComponent.updateContent(this.streamingMessage);
|
|
3152
|
-
this.chatContainer.addChild(this.streamingComponent);
|
|
3153
|
-
this.setChatComponentMutable(this.streamingComponent, true);
|
|
3154
|
-
}
|
|
3480
|
+
this.setThinkingBlockVisibility(!this.hideThinkingBlock);
|
|
3155
3481
|
this.showStatus(`Thinking blocks: ${this.hideThinkingBlock ? "hidden" : "visible"}`);
|
|
3156
3482
|
}
|
|
3157
|
-
|
|
3483
|
+
// SCRAMJET-DIVERGENCE: presentation changes retain live tools and browsing identities.
|
|
3484
|
+
setThinkingBlockVisibility(hidden) {
|
|
3485
|
+
this.hideThinkingBlock = hidden;
|
|
3486
|
+
this.settingsManager.setHideThinkingBlock(hidden);
|
|
3487
|
+
for (const child of [...this.committedChatContainer.children, ...this.chatContainer.children]) {
|
|
3488
|
+
if (child instanceof AssistantMessageComponent)
|
|
3489
|
+
child.setHideThinkingBlock(hidden);
|
|
3490
|
+
}
|
|
3491
|
+
this.ui.rebuild();
|
|
3492
|
+
}
|
|
3493
|
+
async openExternalEditor() {
|
|
3158
3494
|
// Determine editor (respect $VISUAL, then $EDITOR)
|
|
3159
3495
|
const editorCmd = process.env.VISUAL || process.env.EDITOR;
|
|
3160
3496
|
if (!editorCmd) {
|
|
@@ -3166,7 +3502,8 @@ export class InteractiveMode {
|
|
|
3166
3502
|
try {
|
|
3167
3503
|
// Write current content to temp file
|
|
3168
3504
|
fs.writeFileSync(tmpFile, currentText, "utf-8");
|
|
3169
|
-
//
|
|
3505
|
+
// SCRAMJET-DIVERGENCE: the editor must not inherit the key release that opened it.
|
|
3506
|
+
await this.ui.terminal.drainInput();
|
|
3170
3507
|
this.ui.stop();
|
|
3171
3508
|
// Split by space to support editor arguments (e.g., "code --wait")
|
|
3172
3509
|
const [editor, ...editorArgs] = editorCmd.split(" ");
|
|
@@ -3417,6 +3754,23 @@ export class InteractiveMode {
|
|
|
3417
3754
|
this.ui.setFocus(focus);
|
|
3418
3755
|
this.ui.requestRender();
|
|
3419
3756
|
}
|
|
3757
|
+
async settleLayoutSettings(selector) {
|
|
3758
|
+
const generation = ++this.layoutSettingsSettlementGeneration;
|
|
3759
|
+
const settings = this.settingsManager;
|
|
3760
|
+
await settings.flush();
|
|
3761
|
+
// SCRAMJET-DIVERGENCE: only the latest settlement may drain a shared batch of save errors.
|
|
3762
|
+
if (generation !== this.layoutSettingsSettlementGeneration)
|
|
3763
|
+
return;
|
|
3764
|
+
const errors = settings.drainErrors();
|
|
3765
|
+
selector.setSaveError(errors.length ? "Changes not saved; see warning" : undefined);
|
|
3766
|
+
if (errors.length) {
|
|
3767
|
+
const details = stripVTControlCharacters(errors.map(({ scope, error }) => `${scope}: ${error.message}`).join("; "))
|
|
3768
|
+
.replace(/[\r\n\t]/g, " ")
|
|
3769
|
+
.slice(0, 200);
|
|
3770
|
+
this.showWarning(`Display settings not saved (${details}). Changes may be lost on restart; project overrides still apply.`);
|
|
3771
|
+
}
|
|
3772
|
+
this.ui.requestRender();
|
|
3773
|
+
}
|
|
3420
3774
|
showSettingsSelector() {
|
|
3421
3775
|
this.showSelector((done) => {
|
|
3422
3776
|
const selector = new SettingsSelectorComponent({
|
|
@@ -3445,6 +3799,12 @@ export class InteractiveMode {
|
|
|
3445
3799
|
quietStartup: this.settingsManager.getQuietStartup(),
|
|
3446
3800
|
showTerminalProgress: this.settingsManager.getShowTerminalProgress(),
|
|
3447
3801
|
warnings: this.settingsManager.getWarnings(),
|
|
3802
|
+
tuiMode: this.tuiMode,
|
|
3803
|
+
dockEditor: this.settingsManager.getDockEditor(),
|
|
3804
|
+
retainTranscriptOnExit: this.settingsManager.getRetainTranscriptOnExit(),
|
|
3805
|
+
editorMaxHeightPercent: this.settingsManager.getEditorMaxHeightPercent(),
|
|
3806
|
+
scrollWheelStep: this.settingsManager.getScrollWheelStep(),
|
|
3807
|
+
viewportProjectOverrides: Object.keys(this.settingsManager.getProjectSettings()),
|
|
3448
3808
|
}, {
|
|
3449
3809
|
onAutoCompactChange: (enabled) => {
|
|
3450
3810
|
this.session.setAutoCompactionEnabled(enabled);
|
|
@@ -3499,16 +3859,7 @@ export class InteractiveMode {
|
|
|
3499
3859
|
this.ui.requestRender();
|
|
3500
3860
|
}
|
|
3501
3861
|
},
|
|
3502
|
-
onHideThinkingBlockChange: (hidden) =>
|
|
3503
|
-
this.hideThinkingBlock = hidden;
|
|
3504
|
-
this.settingsManager.setHideThinkingBlock(hidden);
|
|
3505
|
-
for (const child of [...this.committedChatContainer.children, ...this.chatContainer.children]) {
|
|
3506
|
-
if (child instanceof AssistantMessageComponent) {
|
|
3507
|
-
child.setHideThinkingBlock(hidden);
|
|
3508
|
-
}
|
|
3509
|
-
}
|
|
3510
|
-
this.rebuildChatFromMessages();
|
|
3511
|
-
},
|
|
3862
|
+
onHideThinkingBlockChange: (hidden) => this.setThinkingBlockVisibility(hidden),
|
|
3512
3863
|
onCollapseChangelogChange: (collapsed) => {
|
|
3513
3864
|
this.settingsManager.setCollapseChangelog(collapsed);
|
|
3514
3865
|
},
|
|
@@ -3548,11 +3899,36 @@ export class InteractiveMode {
|
|
|
3548
3899
|
onWarningsChange: (warnings) => {
|
|
3549
3900
|
this.settingsManager.setWarnings(warnings);
|
|
3550
3901
|
},
|
|
3902
|
+
onRetainTranscriptOnExitChange: (enabled) => {
|
|
3903
|
+
this.settingsManager.setRetainTranscriptOnExit(enabled);
|
|
3904
|
+
void this.settleLayoutSettings(selector);
|
|
3905
|
+
return this.settingsManager.getRetainTranscriptOnExit();
|
|
3906
|
+
},
|
|
3907
|
+
onDockEditorChange: (enabled) => {
|
|
3908
|
+
this.settingsManager.setDockEditor(enabled);
|
|
3909
|
+
void this.settleLayoutSettings(selector);
|
|
3910
|
+
this.ui.refreshViewportLayout();
|
|
3911
|
+
if (!this.settingsManager.getDockEditor())
|
|
3912
|
+
this.ui.revealComponent(this.editorContainer);
|
|
3913
|
+
return this.settingsManager.getDockEditor();
|
|
3914
|
+
},
|
|
3915
|
+
onEditorMaxHeightPercentChange: (percent) => {
|
|
3916
|
+
this.settingsManager.setEditorMaxHeightPercent(percent);
|
|
3917
|
+
void this.settleLayoutSettings(selector);
|
|
3918
|
+
this.ui.refreshViewportLayout();
|
|
3919
|
+
return this.settingsManager.getEditorMaxHeightPercent();
|
|
3920
|
+
},
|
|
3921
|
+
onScrollWheelStepChange: (step) => {
|
|
3922
|
+
this.settingsManager.setScrollWheelStep(step);
|
|
3923
|
+
void this.settleLayoutSettings(selector);
|
|
3924
|
+
this.ui.requestRender();
|
|
3925
|
+
return this.settingsManager.getScrollWheelStep();
|
|
3926
|
+
},
|
|
3551
3927
|
onCancel: () => {
|
|
3552
3928
|
done();
|
|
3553
3929
|
this.ui.requestRender();
|
|
3554
3930
|
},
|
|
3555
|
-
});
|
|
3931
|
+
}, () => Math.max(1, Math.min(this.editorAvailableRows ?? Infinity, this.ui.terminal.rows - 4)));
|
|
3556
3932
|
return { component: selector, focus: selector.getSettingsList() };
|
|
3557
3933
|
});
|
|
3558
3934
|
}
|
|
@@ -4822,26 +5198,30 @@ export class InteractiveMode {
|
|
|
4822
5198
|
// Ignore, will be emitted as an event
|
|
4823
5199
|
}
|
|
4824
5200
|
}
|
|
4825
|
-
stop() {
|
|
4826
|
-
|
|
4827
|
-
|
|
4828
|
-
|
|
4829
|
-
|
|
4830
|
-
|
|
4831
|
-
}
|
|
4832
|
-
if (this.loadingAnimation) {
|
|
4833
|
-
this.loadingAnimation.stop();
|
|
4834
|
-
this.loadingAnimation = undefined;
|
|
4835
|
-
}
|
|
4836
|
-
this.clearExtensionTerminalInputListeners();
|
|
4837
|
-
this.footer.dispose();
|
|
4838
|
-
this.footerDataProvider.dispose();
|
|
4839
|
-
if (this.unsubscribe) {
|
|
4840
|
-
this.unsubscribe();
|
|
5201
|
+
stop(options) {
|
|
5202
|
+
try {
|
|
5203
|
+
if (this.isInitialized) {
|
|
5204
|
+
this.isInitialized = false;
|
|
5205
|
+
this.ui.stop(options);
|
|
5206
|
+
}
|
|
4841
5207
|
}
|
|
4842
|
-
|
|
4843
|
-
this.
|
|
4844
|
-
this.
|
|
5208
|
+
finally {
|
|
5209
|
+
this.selectorOpenGeneration++;
|
|
5210
|
+
this.pendingSelectorOpenGeneration = undefined;
|
|
5211
|
+
this.unregisterSignalHandlers();
|
|
5212
|
+
if (this.settingsManager.getShowTerminalProgress()) {
|
|
5213
|
+
this.ui.terminal.setProgress(false);
|
|
5214
|
+
}
|
|
5215
|
+
if (this.loadingAnimation) {
|
|
5216
|
+
this.loadingAnimation.stop();
|
|
5217
|
+
this.loadingAnimation = undefined;
|
|
5218
|
+
}
|
|
5219
|
+
this.clearExtensionTerminalInputListeners();
|
|
5220
|
+
this.footer.dispose();
|
|
5221
|
+
this.footerDataProvider.dispose();
|
|
5222
|
+
if (this.unsubscribe) {
|
|
5223
|
+
this.unsubscribe();
|
|
5224
|
+
}
|
|
4845
5225
|
}
|
|
4846
5226
|
}
|
|
4847
5227
|
}
|