@dreb/coding-agent 2.25.3 → 2.27.2
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 +4 -0
- package/README.md +21 -11
- package/dist/core/agent-session.d.ts +7 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +22 -0
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/buddy/buddy-controller.d.ts.map +1 -1
- package/dist/core/buddy/buddy-controller.js +5 -23
- package/dist/core/buddy/buddy-controller.js.map +1 -1
- package/dist/core/context-buffer.d.ts +49 -0
- package/dist/core/context-buffer.d.ts.map +1 -0
- package/dist/core/context-buffer.js +84 -0
- package/dist/core/context-buffer.js.map +1 -0
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +3 -1
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/settings-manager.d.ts +13 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +40 -0
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/system-prompt.d.ts +5 -0
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +6 -0
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/thinking.d.ts +10 -1
- package/dist/core/thinking.d.ts.map +1 -1
- package/dist/core/thinking.js +14 -0
- package/dist/core/thinking.js.map +1 -1
- package/dist/core/tools/subagent.d.ts.map +1 -1
- package/dist/core/tools/subagent.js +1 -0
- package/dist/core/tools/subagent.js.map +1 -1
- package/dist/modes/interactive/components/settings-selector.d.ts +5 -0
- package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js +16 -0
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/interactive/components/tool-execution.d.ts +7 -0
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +16 -0
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +33 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +210 -87
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/tab-title.d.ts +21 -3
- package/dist/modes/interactive/tab-title.d.ts.map +1 -1
- package/dist/modes/interactive/tab-title.js +47 -25
- package/dist/modes/interactive/tab-title.js.map +1 -1
- package/docs/agent-models.md +10 -0
- package/docs/development.md +1 -1
- package/docs/providers.md +7 -0
- package/docs/settings.md +29 -2
- package/docs/tui.md +42 -0
- package/examples/extensions/custom-provider-anthropic/package.json +3 -0
- package/examples/extensions/custom-provider-gitlab-duo/package.json +3 -0
- package/examples/extensions/custom-provider-qwen-cli/package.json +3 -0
- package/examples/extensions/with-deps/package.json +3 -0
- package/package.json +2 -2
|
@@ -6,6 +6,7 @@ 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 { supportsAdaptiveThinking } from "@dreb/ai";
|
|
9
10
|
import { CombinedAutocompleteProvider, Container, fuzzyFilter, Loader, Markdown, matchesKey, ProcessTerminal, Spacer, setKeybindings, Text, TruncatedText, TUI, visibleWidth, } from "@dreb/tui";
|
|
10
11
|
import { spawn, spawnSync } from "child_process";
|
|
11
12
|
import { APP_NAME, getAgentDir, getAuthPath, getDebugLogPath, getUpdateInstruction, VERSION } from "../../config.js";
|
|
@@ -22,6 +23,7 @@ import { DefaultPackageManager } from "../../core/package-manager.js";
|
|
|
22
23
|
import { SessionManager } from "../../core/session-manager.js";
|
|
23
24
|
import { BUILTIN_SLASH_COMMANDS } from "../../core/slash-commands.js";
|
|
24
25
|
import { restoreStderr, takeOverStderr } from "../../core/stderr-guard.js";
|
|
26
|
+
import { resolveThinkingDisplay } from "../../core/thinking.js";
|
|
25
27
|
import { resolveToCwd } from "../../core/tools/path-utils.js";
|
|
26
28
|
import { abortBackgroundAgents, discoverAgentTypes, getRunningBackgroundAgents } from "../../core/tools/subagent.js";
|
|
27
29
|
import { getChangelogPath, getNewEntries, parseChangelog } from "../../utils/changelog.js";
|
|
@@ -68,6 +70,7 @@ export class InteractiveMode {
|
|
|
68
70
|
options;
|
|
69
71
|
session;
|
|
70
72
|
ui;
|
|
73
|
+
committedChatContainer;
|
|
71
74
|
chatContainer;
|
|
72
75
|
pendingMessagesContainer;
|
|
73
76
|
statusContainer;
|
|
@@ -98,6 +101,8 @@ export class InteractiveMode {
|
|
|
98
101
|
streamingMessage = undefined;
|
|
99
102
|
// Tool execution tracking: toolCallId -> component
|
|
100
103
|
pendingTools = new Map();
|
|
104
|
+
// Deferred commit: set to true when components finalize, commit runs after next render
|
|
105
|
+
commitNeeded = false;
|
|
101
106
|
// Tool output expansion state
|
|
102
107
|
toolOutputExpanded = false;
|
|
103
108
|
// Thinking block visibility state
|
|
@@ -188,6 +193,7 @@ export class InteractiveMode {
|
|
|
188
193
|
});
|
|
189
194
|
this.ui = new TUI(new ProcessTerminal(), this.settingsManager.getShowHardwareCursor());
|
|
190
195
|
this.headerContainer = new Container();
|
|
196
|
+
this.committedChatContainer = new Container();
|
|
191
197
|
this.chatContainer = new Container();
|
|
192
198
|
this.pendingMessagesContainer = new Container();
|
|
193
199
|
this.statusContainer = new Container();
|
|
@@ -413,6 +419,7 @@ export class InteractiveMode {
|
|
|
413
419
|
this.headerContainer.addChild(new Text(condensedText, 1, 0));
|
|
414
420
|
}
|
|
415
421
|
}
|
|
422
|
+
this.ui.addChild(this.committedChatContainer);
|
|
416
423
|
this.ui.addChild(this.chatContainer);
|
|
417
424
|
this.ui.addChild(this.pendingMessagesContainer);
|
|
418
425
|
this.ui.addChild(this.statusContainer);
|
|
@@ -423,6 +430,20 @@ export class InteractiveMode {
|
|
|
423
430
|
this.ui.addChild(this.widgetContainerBelow);
|
|
424
431
|
this.ui.addChild(this.footer);
|
|
425
432
|
this.ui.setFocus(this.editor);
|
|
433
|
+
// Mark header + committedChatContainer as committed to scrollback.
|
|
434
|
+
// The differential renderer will only manage children after this boundary.
|
|
435
|
+
this.ui.setCommittedChildCount(2);
|
|
436
|
+
// Wire up deferred commit: after each render cycle, commit any finalized
|
|
437
|
+
// components that were painted with their final state. This ensures
|
|
438
|
+
// components are committed to scrollback only AFTER their final content
|
|
439
|
+
// has been rendered to the terminal (not before, which would freeze
|
|
440
|
+
// stale/intermediate content in scrollback).
|
|
441
|
+
this.ui.onPostRender = () => {
|
|
442
|
+
if (this.commitNeeded) {
|
|
443
|
+
this.commitNeeded = false;
|
|
444
|
+
this.tryCommitPrefix();
|
|
445
|
+
}
|
|
446
|
+
};
|
|
426
447
|
this.setupKeyHandlers();
|
|
427
448
|
this.setupEditorSubmitHandler();
|
|
428
449
|
// Start the UI before initializing extensions so session_start handlers can use interactive dialogs
|
|
@@ -433,8 +454,14 @@ export class InteractiveMode {
|
|
|
433
454
|
this.activateStderrGuard();
|
|
434
455
|
// Initialize extensions first so resources are shown before messages
|
|
435
456
|
await this.initExtensions();
|
|
436
|
-
// Render initial messages AFTER showing loaded resources
|
|
457
|
+
// Render initial messages AFTER showing loaded resources.
|
|
458
|
+
// Do NOT call resetChatDisplay() here — initExtensions() already populated
|
|
459
|
+
// chatContainer with Context/Memory/Skills via showLoadedResources().
|
|
460
|
+
// Clearing would erase that content. Just append session messages, commit,
|
|
461
|
+
// and repaint.
|
|
437
462
|
this.renderInitialMessages();
|
|
463
|
+
this.tryCommitPrefix();
|
|
464
|
+
this.ui.recommitAll();
|
|
438
465
|
// Set terminal title
|
|
439
466
|
this.updateTerminalTitle();
|
|
440
467
|
// Initialize tab title auto-generation
|
|
@@ -447,7 +474,8 @@ export class InteractiveMode {
|
|
|
447
474
|
onThemeChange(() => {
|
|
448
475
|
this.ui.invalidate();
|
|
449
476
|
this.updateEditorBorderColor();
|
|
450
|
-
|
|
477
|
+
// Theme affects committed content — full re-commit to repaint
|
|
478
|
+
this.ui.recommitAll();
|
|
451
479
|
});
|
|
452
480
|
// Set up git branch watcher (uses provider instead of footer)
|
|
453
481
|
this.footerDataProvider.onBranchChange(() => {
|
|
@@ -480,11 +508,17 @@ export class InteractiveMode {
|
|
|
480
508
|
return;
|
|
481
509
|
this.tabTitleGenerator = new TabTitleGenerator(settings, {
|
|
482
510
|
setTitle: (title) => this.ui.terminal.setTitle(title),
|
|
511
|
+
setSessionName: (name) => {
|
|
512
|
+
this.sessionManager.appendSessionInfo(name);
|
|
513
|
+
},
|
|
483
514
|
getMessages: () => this.session.state.messages,
|
|
484
515
|
getModel: () => this.session.model,
|
|
485
516
|
getModelRegistry: () => this.session.modelRegistry,
|
|
486
517
|
getProvider: () => this.session.model?.provider,
|
|
487
518
|
getAgentModelsOverride: (name) => this.settingsManager.getAgentModelsForAgent(name),
|
|
519
|
+
getBranch: () => this.footerDataProvider.getGitBranch(),
|
|
520
|
+
getRepo: () => path.basename(process.cwd()),
|
|
521
|
+
getCwd: () => process.cwd(),
|
|
488
522
|
});
|
|
489
523
|
}
|
|
490
524
|
/**
|
|
@@ -994,15 +1028,13 @@ export class InteractiveMode {
|
|
|
994
1028
|
}
|
|
995
1029
|
// Clear UI state
|
|
996
1030
|
this.editor.setGhostText?.(null);
|
|
997
|
-
this.chatContainer.clear();
|
|
998
1031
|
this.pendingMessagesContainer.clear();
|
|
999
1032
|
this.compactionQueuedMessages = [];
|
|
1000
1033
|
this.streamingComponent = undefined;
|
|
1001
1034
|
this.streamingMessage = undefined;
|
|
1002
1035
|
this.pendingTools.clear();
|
|
1003
|
-
//
|
|
1004
|
-
this.
|
|
1005
|
-
this.ui.requestRender();
|
|
1036
|
+
// Re-render from new session state
|
|
1037
|
+
this.resetChatDisplay();
|
|
1006
1038
|
return { cancelled: false };
|
|
1007
1039
|
},
|
|
1008
1040
|
fork: async (entryId) => {
|
|
@@ -1010,8 +1042,7 @@ export class InteractiveMode {
|
|
|
1010
1042
|
if (result.cancelled) {
|
|
1011
1043
|
return { cancelled: true };
|
|
1012
1044
|
}
|
|
1013
|
-
this.
|
|
1014
|
-
this.renderInitialMessages();
|
|
1045
|
+
this.resetChatDisplay();
|
|
1015
1046
|
this.editor.setText(result.selectedText);
|
|
1016
1047
|
this.showStatus("Forked to new session");
|
|
1017
1048
|
return { cancelled: false };
|
|
@@ -1026,8 +1057,7 @@ export class InteractiveMode {
|
|
|
1026
1057
|
if (result.cancelled) {
|
|
1027
1058
|
return { cancelled: true };
|
|
1028
1059
|
}
|
|
1029
|
-
this.
|
|
1030
|
-
this.renderInitialMessages();
|
|
1060
|
+
this.resetChatDisplay();
|
|
1031
1061
|
if (result.editorText && !this.editor.getText().trim()) {
|
|
1032
1062
|
this.editor.setText(result.editorText);
|
|
1033
1063
|
}
|
|
@@ -1295,7 +1325,9 @@ export class InteractiveMode {
|
|
|
1295
1325
|
this.headerContainer.children[index] = this.builtInHeader;
|
|
1296
1326
|
}
|
|
1297
1327
|
}
|
|
1298
|
-
|
|
1328
|
+
// Header is committed content (child 0 of the TUI, before committedChildCount);
|
|
1329
|
+
// requestRender() only re-renders the live region and would silently drop this change.
|
|
1330
|
+
this.ui.recommitAll();
|
|
1299
1331
|
}
|
|
1300
1332
|
addExtensionTerminalInputListener(handler) {
|
|
1301
1333
|
const unsubscribe = this.ui.addInputListener(handler);
|
|
@@ -1404,11 +1436,8 @@ export class InteractiveMode {
|
|
|
1404
1436
|
*/
|
|
1405
1437
|
hideExtensionSelector() {
|
|
1406
1438
|
this.extensionSelector?.dispose();
|
|
1407
|
-
this.editorContainer.clear();
|
|
1408
|
-
this.editorContainer.addChild(this.editor);
|
|
1409
1439
|
this.extensionSelector = undefined;
|
|
1410
|
-
this.
|
|
1411
|
-
this.ui.requestRender();
|
|
1440
|
+
this.restoreEditorComponent();
|
|
1412
1441
|
}
|
|
1413
1442
|
/**
|
|
1414
1443
|
* Show a confirmation dialog for extensions.
|
|
@@ -1451,11 +1480,8 @@ export class InteractiveMode {
|
|
|
1451
1480
|
*/
|
|
1452
1481
|
hideExtensionInput() {
|
|
1453
1482
|
this.extensionInput?.dispose();
|
|
1454
|
-
this.editorContainer.clear();
|
|
1455
|
-
this.editorContainer.addChild(this.editor);
|
|
1456
1483
|
this.extensionInput = undefined;
|
|
1457
|
-
this.
|
|
1458
|
-
this.ui.requestRender();
|
|
1484
|
+
this.restoreEditorComponent();
|
|
1459
1485
|
}
|
|
1460
1486
|
/**
|
|
1461
1487
|
* Show a multi-line editor for extensions (with Ctrl+G support).
|
|
@@ -1479,11 +1505,8 @@ export class InteractiveMode {
|
|
|
1479
1505
|
* Hide the extension editor.
|
|
1480
1506
|
*/
|
|
1481
1507
|
hideExtensionEditor() {
|
|
1482
|
-
this.editorContainer.clear();
|
|
1483
|
-
this.editorContainer.addChild(this.editor);
|
|
1484
1508
|
this.extensionEditor = undefined;
|
|
1485
|
-
this.
|
|
1486
|
-
this.ui.requestRender();
|
|
1509
|
+
this.restoreEditorComponent();
|
|
1487
1510
|
}
|
|
1488
1511
|
/**
|
|
1489
1512
|
* Set a custom editor component from an extension.
|
|
@@ -1563,11 +1586,8 @@ export class InteractiveMode {
|
|
|
1563
1586
|
const savedText = this.editor.getText();
|
|
1564
1587
|
const isOverlay = options?.overlay ?? false;
|
|
1565
1588
|
const restoreEditor = () => {
|
|
1566
|
-
this.editorContainer.clear();
|
|
1567
|
-
this.editorContainer.addChild(this.editor);
|
|
1568
1589
|
this.editor.setText(savedText);
|
|
1569
|
-
this.
|
|
1570
|
-
this.ui.requestRender();
|
|
1590
|
+
this.restoreEditorComponent();
|
|
1571
1591
|
};
|
|
1572
1592
|
return new Promise((resolve, reject) => {
|
|
1573
1593
|
let component;
|
|
@@ -2055,6 +2075,10 @@ export class InteractiveMode {
|
|
|
2055
2075
|
}
|
|
2056
2076
|
// Capture assistant response for buddy context
|
|
2057
2077
|
this.buddyController.handleEvent(event);
|
|
2078
|
+
// Capture context for tab title generation
|
|
2079
|
+
this.tabTitleGenerator?.onMessageEnd(event.message);
|
|
2080
|
+
// Defer commit until after the next render paints the final state
|
|
2081
|
+
this.commitNeeded = true;
|
|
2058
2082
|
this.ui.requestRender();
|
|
2059
2083
|
break;
|
|
2060
2084
|
case "tool_execution_start": {
|
|
@@ -2084,12 +2108,19 @@ export class InteractiveMode {
|
|
|
2084
2108
|
if (component) {
|
|
2085
2109
|
component.updateResult({ ...event.result, isError: event.isError });
|
|
2086
2110
|
this.pendingTools.delete(event.toolCallId);
|
|
2111
|
+
// Wire up Kitty conversion callback for deferred commit
|
|
2112
|
+
component.onConversionComplete = () => {
|
|
2113
|
+
this.commitNeeded = true;
|
|
2114
|
+
this.ui.requestRender();
|
|
2115
|
+
};
|
|
2116
|
+
// Defer commit until after the next render paints the final state
|
|
2117
|
+
this.commitNeeded = true;
|
|
2087
2118
|
this.ui.requestRender();
|
|
2088
2119
|
}
|
|
2089
2120
|
// Buddy context + reaction for tool execution
|
|
2090
2121
|
this.buddyController.handleEvent(event);
|
|
2091
2122
|
// Tab title auto-generation
|
|
2092
|
-
this.tabTitleGenerator?.onToolEnd();
|
|
2123
|
+
this.tabTitleGenerator?.onToolEnd(event);
|
|
2093
2124
|
break;
|
|
2094
2125
|
}
|
|
2095
2126
|
case "agent_end":
|
|
@@ -2108,6 +2139,9 @@ export class InteractiveMode {
|
|
|
2108
2139
|
this.streamingMessage = undefined;
|
|
2109
2140
|
}
|
|
2110
2141
|
this.pendingTools.clear();
|
|
2142
|
+
// Defer final commit sweep — all remaining finalized components will
|
|
2143
|
+
// move to scrollback after the next render paints their final state
|
|
2144
|
+
this.commitNeeded = true;
|
|
2111
2145
|
await this.checkShutdownRequested();
|
|
2112
2146
|
// Buddy reaction: session wrap-up quip
|
|
2113
2147
|
this.buddyController.handleEvent(event);
|
|
@@ -2147,8 +2181,9 @@ export class InteractiveMode {
|
|
|
2147
2181
|
}
|
|
2148
2182
|
else if (event.result) {
|
|
2149
2183
|
// Rebuild chat to show compacted state
|
|
2150
|
-
this.chatContainer.clear();
|
|
2151
2184
|
this.rebuildChatFromMessages();
|
|
2185
|
+
this.tryCommitPrefix();
|
|
2186
|
+
this.ui.recommitAll();
|
|
2152
2187
|
// Add compaction component at bottom so user sees it without scrolling
|
|
2153
2188
|
this.addMessageToChat({
|
|
2154
2189
|
role: "compactionSummary",
|
|
@@ -2212,19 +2247,9 @@ export class InteractiveMode {
|
|
|
2212
2247
|
this.chatContainer.removeChild(component);
|
|
2213
2248
|
}
|
|
2214
2249
|
this.pendingTools.clear();
|
|
2215
|
-
//
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
this.loadingAnimation.stop();
|
|
2219
|
-
this.loadingAnimation = undefined;
|
|
2220
|
-
}
|
|
2221
|
-
if (this.retryLoader) {
|
|
2222
|
-
this.retryLoader.stop();
|
|
2223
|
-
this.retryLoader = undefined;
|
|
2224
|
-
}
|
|
2225
|
-
this.retryLoader = new Loader(this.ui, (spinner) => theme.fg("warning", spinner), (text) => theme.fg("muted", text), `Stream dropped, retrying (${event.attempt}/${event.maxAttempts})... (${keyText("app.interrupt")} to cancel)`);
|
|
2226
|
-
this.statusContainer.addChild(this.retryLoader);
|
|
2227
|
-
this.ui.requestRender();
|
|
2250
|
+
// Warn in the chat scrollback — keep the working spinner running so ESC
|
|
2251
|
+
// aborts via the normal loadingAnimation path (same AbortController).
|
|
2252
|
+
this.showWarning(`Stream dropped, retrying (${event.attempt}/${event.maxAttempts})…`);
|
|
2228
2253
|
break;
|
|
2229
2254
|
}
|
|
2230
2255
|
case "length_retry": {
|
|
@@ -2239,19 +2264,9 @@ export class InteractiveMode {
|
|
|
2239
2264
|
this.chatContainer.removeChild(component);
|
|
2240
2265
|
}
|
|
2241
2266
|
this.pendingTools.clear();
|
|
2242
|
-
//
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
this.loadingAnimation.stop();
|
|
2246
|
-
this.loadingAnimation = undefined;
|
|
2247
|
-
}
|
|
2248
|
-
if (this.retryLoader) {
|
|
2249
|
-
this.retryLoader.stop();
|
|
2250
|
-
this.retryLoader = undefined;
|
|
2251
|
-
}
|
|
2252
|
-
this.retryLoader = new Loader(this.ui, (spinner) => theme.fg("warning", spinner), (text) => theme.fg("muted", text), `Response truncated, retrying with larger token budget (${event.attempt}/${event.maxAttempts})... (${keyText("app.interrupt")} to cancel)`);
|
|
2253
|
-
this.statusContainer.addChild(this.retryLoader);
|
|
2254
|
-
this.ui.requestRender();
|
|
2267
|
+
// Warn in the chat scrollback — keep the working spinner running so ESC
|
|
2268
|
+
// aborts via the normal loadingAnimation path (same AbortController).
|
|
2269
|
+
this.showWarning(`Response truncated, retrying with larger token budget (${event.attempt}/${event.maxAttempts})…`);
|
|
2255
2270
|
break;
|
|
2256
2271
|
}
|
|
2257
2272
|
case "background_agent_start":
|
|
@@ -2487,6 +2502,10 @@ export class InteractiveMode {
|
|
|
2487
2502
|
});
|
|
2488
2503
|
}
|
|
2489
2504
|
rebuildChatFromMessages() {
|
|
2505
|
+
// Clear both containers (committed content will be re-committed after rebuild).
|
|
2506
|
+
// Callers that use this for global actions should also call
|
|
2507
|
+
// tryCommitPrefix() + ui.recommitAll() after.
|
|
2508
|
+
this.committedChatContainer.clear();
|
|
2490
2509
|
this.chatContainer.clear();
|
|
2491
2510
|
const context = this.sessionManager.buildSessionContext();
|
|
2492
2511
|
this.renderSessionContext(context);
|
|
@@ -2556,7 +2575,9 @@ export class InteractiveMode {
|
|
|
2556
2575
|
process.removeListener("SIGINT", ignoreSigint);
|
|
2557
2576
|
this.ui.start();
|
|
2558
2577
|
this.activateStderrGuard();
|
|
2559
|
-
|
|
2578
|
+
// stop() moved the cursor, so hardwareCursorRow is stale. recommitAll()
|
|
2579
|
+
// uses position-independent sequences (2J/H/3J) — safe after terminal takeover.
|
|
2580
|
+
this.ui.recommitAll();
|
|
2560
2581
|
});
|
|
2561
2582
|
try {
|
|
2562
2583
|
// Stop the TUI (restore terminal to normal mode)
|
|
@@ -2654,23 +2675,81 @@ export class InteractiveMode {
|
|
|
2654
2675
|
this.ui.requestRender();
|
|
2655
2676
|
this.showStatus(`Tasks panel: ${this.tasksPanel.visible ? "visible" : "hidden"}`);
|
|
2656
2677
|
}
|
|
2678
|
+
/**
|
|
2679
|
+
* Clear committed and live chat, re-render from session state, commit the
|
|
2680
|
+
* finalized prefix, and repaint everything. Used after session switches,
|
|
2681
|
+
* imports, resumes, and navigation — any operation that replaces the
|
|
2682
|
+
* displayed transcript with a different session's content.
|
|
2683
|
+
*/
|
|
2684
|
+
resetChatDisplay() {
|
|
2685
|
+
this.committedChatContainer.clear();
|
|
2686
|
+
this.chatContainer.clear();
|
|
2687
|
+
this.renderInitialMessages();
|
|
2688
|
+
this.tryCommitPrefix();
|
|
2689
|
+
this.ui.recommitAll();
|
|
2690
|
+
}
|
|
2691
|
+
/**
|
|
2692
|
+
* Scan the live chatContainer for the longest contiguous prefix of fully-finalized
|
|
2693
|
+
* components, move them to committedChatContainer, and advance the TUI's committed
|
|
2694
|
+
* boundary. This prevents the differential renderer from replaying history.
|
|
2695
|
+
*
|
|
2696
|
+
* A component is "finalized" if:
|
|
2697
|
+
* - AssistantMessageComponent: not the active streamingComponent
|
|
2698
|
+
* - ToolExecutionComponent: not in pendingTools AND no pending Kitty conversions
|
|
2699
|
+
* - Any other component (Spacer, Text, etc.): always finalized
|
|
2700
|
+
*/
|
|
2701
|
+
tryCommitPrefix() {
|
|
2702
|
+
let commitCount = 0;
|
|
2703
|
+
for (const child of this.chatContainer.children) {
|
|
2704
|
+
if (child instanceof AssistantMessageComponent) {
|
|
2705
|
+
if (child === this.streamingComponent)
|
|
2706
|
+
break; // still streaming
|
|
2707
|
+
commitCount++;
|
|
2708
|
+
}
|
|
2709
|
+
else if (child instanceof ToolExecutionComponent) {
|
|
2710
|
+
if (this.pendingTools.has(child.getToolCallId()))
|
|
2711
|
+
break; // still executing
|
|
2712
|
+
if (child.hasPendingConversions())
|
|
2713
|
+
break; // Kitty conversion pending
|
|
2714
|
+
commitCount++;
|
|
2715
|
+
}
|
|
2716
|
+
else {
|
|
2717
|
+
commitCount++; // Spacer, Text, etc. — always safe
|
|
2718
|
+
}
|
|
2719
|
+
}
|
|
2720
|
+
if (commitCount === 0)
|
|
2721
|
+
return;
|
|
2722
|
+
// Move the finalized prefix from chatContainer to committedChatContainer
|
|
2723
|
+
const toCommit = this.chatContainer.children.splice(0, commitCount);
|
|
2724
|
+
for (const c of toCommit) {
|
|
2725
|
+
this.committedChatContainer.addChild(c);
|
|
2726
|
+
}
|
|
2727
|
+
// Update the TUI's committed line tracking
|
|
2728
|
+
this.ui.commit();
|
|
2729
|
+
}
|
|
2657
2730
|
toggleToolOutputExpansion() {
|
|
2658
2731
|
this.setToolsExpanded(!this.toolOutputExpanded);
|
|
2659
2732
|
}
|
|
2660
2733
|
setToolsExpanded(expanded) {
|
|
2661
2734
|
this.toolOutputExpanded = expanded;
|
|
2735
|
+
// Expand/collapse in both committed and live containers
|
|
2736
|
+
for (const child of this.committedChatContainer.children) {
|
|
2737
|
+
if (isExpandable(child)) {
|
|
2738
|
+
child.setExpanded(expanded);
|
|
2739
|
+
}
|
|
2740
|
+
}
|
|
2662
2741
|
for (const child of this.chatContainer.children) {
|
|
2663
2742
|
if (isExpandable(child)) {
|
|
2664
2743
|
child.setExpanded(expanded);
|
|
2665
2744
|
}
|
|
2666
2745
|
}
|
|
2667
|
-
|
|
2746
|
+
// Committed content changed — full re-commit to repaint
|
|
2747
|
+
this.ui.recommitAll();
|
|
2668
2748
|
}
|
|
2669
2749
|
toggleThinkingBlockVisibility() {
|
|
2670
2750
|
this.hideThinkingBlock = !this.hideThinkingBlock;
|
|
2671
2751
|
this.settingsManager.setHideThinkingBlock(this.hideThinkingBlock);
|
|
2672
|
-
//
|
|
2673
|
-
this.chatContainer.clear();
|
|
2752
|
+
// Full rebuild: rebuildChatFromMessages() clears both containers internally
|
|
2674
2753
|
this.rebuildChatFromMessages();
|
|
2675
2754
|
// If streaming, re-add the streaming component with updated visibility and re-render
|
|
2676
2755
|
if (this.streamingComponent && this.streamingMessage) {
|
|
@@ -2678,6 +2757,9 @@ export class InteractiveMode {
|
|
|
2678
2757
|
this.streamingComponent.updateContent(this.streamingMessage);
|
|
2679
2758
|
this.chatContainer.addChild(this.streamingComponent);
|
|
2680
2759
|
}
|
|
2760
|
+
// Committed content rebuilt — full re-commit to repaint
|
|
2761
|
+
this.tryCommitPrefix();
|
|
2762
|
+
this.ui.recommitAll();
|
|
2681
2763
|
this.showStatus(`Thinking blocks: ${this.hideThinkingBlock ? "hidden" : "visible"}`);
|
|
2682
2764
|
}
|
|
2683
2765
|
openExternalEditor() {
|
|
@@ -2720,8 +2802,9 @@ export class InteractiveMode {
|
|
|
2720
2802
|
// Restart TUI and re-intercept stderr
|
|
2721
2803
|
this.ui.start();
|
|
2722
2804
|
this.activateStderrGuard();
|
|
2723
|
-
//
|
|
2724
|
-
|
|
2805
|
+
// stop() moved the cursor, so hardwareCursorRow is stale. recommitAll()
|
|
2806
|
+
// uses position-independent sequences (2J/H/3J) — safe after terminal takeover.
|
|
2807
|
+
this.ui.recommitAll();
|
|
2725
2808
|
}
|
|
2726
2809
|
}
|
|
2727
2810
|
// =========================================================================
|
|
@@ -2952,15 +3035,31 @@ export class InteractiveMode {
|
|
|
2952
3035
|
// =========================================================================
|
|
2953
3036
|
// Selectors
|
|
2954
3037
|
// =========================================================================
|
|
3038
|
+
/**
|
|
3039
|
+
* Restore an editor component into the editor container after an inline modal
|
|
3040
|
+
* (selector, dialog, input, loader) was shown in its place, and repaint.
|
|
3041
|
+
*
|
|
3042
|
+
* Uses recommitAll() rather than requestRender(): an inline modal can grow the
|
|
3043
|
+
* live region tall enough to push committed content up into terminal scrollback.
|
|
3044
|
+
* Closing it shrinks the live region, and a differential redraw can't pull that
|
|
3045
|
+
* committed content back down — leaving ghost whitespace below the prompt.
|
|
3046
|
+
* recommitAll() repaints the whole transcript with position-independent sequences.
|
|
3047
|
+
* Safe here: these modals are interactive and the user is at the bottom with no
|
|
3048
|
+
* agent streaming.
|
|
3049
|
+
*/
|
|
3050
|
+
restoreEditorComponent(editor = this.editor) {
|
|
3051
|
+
this.editorContainer.clear();
|
|
3052
|
+
this.editorContainer.addChild(editor);
|
|
3053
|
+
this.ui.setFocus(editor);
|
|
3054
|
+
this.ui.recommitAll();
|
|
3055
|
+
}
|
|
2955
3056
|
/**
|
|
2956
3057
|
* Shows a selector component in place of the editor.
|
|
2957
3058
|
* @param create Factory that receives a `done` callback and returns the component and focus target
|
|
2958
3059
|
*/
|
|
2959
3060
|
showSelector(create) {
|
|
2960
3061
|
const done = () => {
|
|
2961
|
-
this.
|
|
2962
|
-
this.editorContainer.addChild(this.editor);
|
|
2963
|
-
this.ui.setFocus(this.editor);
|
|
3062
|
+
this.restoreEditorComponent();
|
|
2964
3063
|
};
|
|
2965
3064
|
const { component, focus } = create(done);
|
|
2966
3065
|
this.editorContainer.clear();
|
|
@@ -2975,6 +3074,12 @@ export class InteractiveMode {
|
|
|
2975
3074
|
const agentNames = Array.from(agentTypes.keys()).sort();
|
|
2976
3075
|
const availableModels = this.session.modelRegistry.getAvailable();
|
|
2977
3076
|
const availableModelIds = availableModels.map((m) => `${m.provider}/${m.id}`);
|
|
3077
|
+
const currentModel = this.session.model;
|
|
3078
|
+
const thinkingDisplaySupported = currentModel ? supportsAdaptiveThinking(currentModel) : false;
|
|
3079
|
+
const thinkingDisplay = currentModel
|
|
3080
|
+
? (resolveThinkingDisplay(currentModel, this.settingsManager.getModelThinkingDisplay(currentModel.id)) ??
|
|
3081
|
+
"summarized")
|
|
3082
|
+
: "summarized";
|
|
2978
3083
|
const selector = new SettingsSelectorComponent({
|
|
2979
3084
|
autoCompact: this.session.autoCompactionEnabled,
|
|
2980
3085
|
showImages: this.settingsManager.getShowImages(),
|
|
@@ -2989,6 +3094,8 @@ export class InteractiveMode {
|
|
|
2989
3094
|
currentTheme: this.settingsManager.getTheme() || "dark",
|
|
2990
3095
|
availableThemes: getAvailableThemes(),
|
|
2991
3096
|
hideThinkingBlock: this.hideThinkingBlock,
|
|
3097
|
+
thinkingDisplaySupported,
|
|
3098
|
+
thinkingDisplay,
|
|
2992
3099
|
collapseChangelog: this.settingsManager.getCollapseChangelog(),
|
|
2993
3100
|
doubleEscapeAction: this.settingsManager.getDoubleEscapeAction(),
|
|
2994
3101
|
treeFilterMode: this.settingsManager.getTreeFilterMode(),
|
|
@@ -3006,11 +3113,18 @@ export class InteractiveMode {
|
|
|
3006
3113
|
},
|
|
3007
3114
|
onShowImagesChange: (enabled) => {
|
|
3008
3115
|
this.settingsManager.setShowImages(enabled);
|
|
3116
|
+
for (const child of this.committedChatContainer.children) {
|
|
3117
|
+
if (child instanceof ToolExecutionComponent) {
|
|
3118
|
+
child.setShowImages(enabled);
|
|
3119
|
+
}
|
|
3120
|
+
}
|
|
3009
3121
|
for (const child of this.chatContainer.children) {
|
|
3010
3122
|
if (child instanceof ToolExecutionComponent) {
|
|
3011
3123
|
child.setShowImages(enabled);
|
|
3012
3124
|
}
|
|
3013
3125
|
}
|
|
3126
|
+
// Committed content changed — full re-commit to repaint
|
|
3127
|
+
this.ui.recommitAll();
|
|
3014
3128
|
},
|
|
3015
3129
|
onAutoResizeImagesChange: (enabled) => {
|
|
3016
3130
|
this.settingsManager.setImageAutoResize(enabled);
|
|
@@ -3055,13 +3169,26 @@ export class InteractiveMode {
|
|
|
3055
3169
|
onHideThinkingBlockChange: (hidden) => {
|
|
3056
3170
|
this.hideThinkingBlock = hidden;
|
|
3057
3171
|
this.settingsManager.setHideThinkingBlock(hidden);
|
|
3058
|
-
|
|
3059
|
-
if (child instanceof AssistantMessageComponent) {
|
|
3060
|
-
child.setHideThinkingBlock(hidden);
|
|
3061
|
-
}
|
|
3062
|
-
}
|
|
3063
|
-
this.chatContainer.clear();
|
|
3172
|
+
// Full rebuild: rebuildChatFromMessages() clears both containers
|
|
3064
3173
|
this.rebuildChatFromMessages();
|
|
3174
|
+
this.tryCommitPrefix();
|
|
3175
|
+
this.ui.recommitAll();
|
|
3176
|
+
},
|
|
3177
|
+
onThinkingDisplayChange: (display) => {
|
|
3178
|
+
const model = this.session.model;
|
|
3179
|
+
if (!model)
|
|
3180
|
+
return;
|
|
3181
|
+
this.settingsManager.setModelThinkingDisplay(model.id, display);
|
|
3182
|
+
// Refresh the live agent so the change takes effect this session.
|
|
3183
|
+
const effective = resolveThinkingDisplay(model, this.settingsManager.getModelThinkingDisplay(model.id));
|
|
3184
|
+
this.session.agent.thinkingDisplay = effective;
|
|
3185
|
+
// The toggle writes to GLOBAL settings, but getModelThinkingDisplay
|
|
3186
|
+
// resolves project-over-global. If a project-level modelSettings
|
|
3187
|
+
// override exists in .dreb/settings.json, it shadows the toggle and
|
|
3188
|
+
// the change silently has no effect. Fail loudly instead.
|
|
3189
|
+
if (supportsAdaptiveThinking(model) && effective !== display) {
|
|
3190
|
+
this.showWarning(`Thinking display for "${model.id}" was not changed: a project-level "modelSettings" override in .dreb/settings.json takes precedence (effective: "${effective}"). Remove or edit that override to use this toggle.`);
|
|
3191
|
+
}
|
|
3065
3192
|
},
|
|
3066
3193
|
onCollapseChangelogChange: (collapsed) => {
|
|
3067
3194
|
this.settingsManager.setCollapseChangelog(collapsed);
|
|
@@ -3304,8 +3431,7 @@ export class InteractiveMode {
|
|
|
3304
3431
|
this.ui.requestRender();
|
|
3305
3432
|
return;
|
|
3306
3433
|
}
|
|
3307
|
-
this.
|
|
3308
|
-
this.renderInitialMessages();
|
|
3434
|
+
this.resetChatDisplay();
|
|
3309
3435
|
this.editor.setText(result.selectedText);
|
|
3310
3436
|
done();
|
|
3311
3437
|
this.showStatus("Branched to new session");
|
|
@@ -3390,8 +3516,7 @@ export class InteractiveMode {
|
|
|
3390
3516
|
return;
|
|
3391
3517
|
}
|
|
3392
3518
|
// Update UI
|
|
3393
|
-
this.
|
|
3394
|
-
this.renderInitialMessages();
|
|
3519
|
+
this.resetChatDisplay();
|
|
3395
3520
|
if (result.editorText && !this.editor.getText().trim()) {
|
|
3396
3521
|
this.editor.setText(result.editorText);
|
|
3397
3522
|
}
|
|
@@ -3459,8 +3584,7 @@ export class InteractiveMode {
|
|
|
3459
3584
|
await this.session.switchSession(sessionPath);
|
|
3460
3585
|
await this.footerDataProvider.refreshDailyCost();
|
|
3461
3586
|
// Clear and re-render the chat
|
|
3462
|
-
this.
|
|
3463
|
-
this.renderInitialMessages();
|
|
3587
|
+
this.resetChatDisplay();
|
|
3464
3588
|
this.showStatus("Resumed session");
|
|
3465
3589
|
}
|
|
3466
3590
|
async showOAuthSelector(mode) {
|
|
@@ -3524,10 +3648,7 @@ export class InteractiveMode {
|
|
|
3524
3648
|
});
|
|
3525
3649
|
// Restore editor helper
|
|
3526
3650
|
const restoreEditor = () => {
|
|
3527
|
-
this.
|
|
3528
|
-
this.editorContainer.addChild(this.editor);
|
|
3529
|
-
this.ui.setFocus(this.editor);
|
|
3530
|
-
this.ui.requestRender();
|
|
3651
|
+
this.restoreEditorComponent();
|
|
3531
3652
|
};
|
|
3532
3653
|
try {
|
|
3533
3654
|
await this.session.modelRegistry.authStorage.login(providerId, {
|
|
@@ -3602,10 +3723,7 @@ export class InteractiveMode {
|
|
|
3602
3723
|
this.ui.requestRender();
|
|
3603
3724
|
const dismissLoader = (editor) => {
|
|
3604
3725
|
loader.dispose();
|
|
3605
|
-
this.
|
|
3606
|
-
this.editorContainer.addChild(editor);
|
|
3607
|
-
this.ui.setFocus(editor);
|
|
3608
|
-
this.ui.requestRender();
|
|
3726
|
+
this.restoreEditorComponent(editor);
|
|
3609
3727
|
};
|
|
3610
3728
|
try {
|
|
3611
3729
|
await this.session.reload();
|
|
@@ -3632,6 +3750,9 @@ export class InteractiveMode {
|
|
|
3632
3750
|
this.setupExtensionShortcuts(runner);
|
|
3633
3751
|
}
|
|
3634
3752
|
this.rebuildChatFromMessages();
|
|
3753
|
+
this.tryCommitPrefix();
|
|
3754
|
+
// dismissLoader() restores the editor and calls recommitAll(), which
|
|
3755
|
+
// repaints the rebuilt committed content — no separate recommit needed.
|
|
3635
3756
|
dismissLoader(this.editor);
|
|
3636
3757
|
this.showLoadedResources({
|
|
3637
3758
|
force: false,
|
|
@@ -3696,8 +3817,7 @@ export class InteractiveMode {
|
|
|
3696
3817
|
return;
|
|
3697
3818
|
}
|
|
3698
3819
|
// Clear and re-render the chat
|
|
3699
|
-
this.
|
|
3700
|
-
this.renderInitialMessages();
|
|
3820
|
+
this.resetChatDisplay();
|
|
3701
3821
|
this.showStatus(`Session imported from: ${inputPath}`);
|
|
3702
3822
|
}
|
|
3703
3823
|
catch (error) {
|
|
@@ -3983,6 +4103,7 @@ ${cycleModelForward || cycleModelBackward ? `| \`${cycleModelForward}\` / \`${cy
|
|
|
3983
4103
|
await this.footerDataProvider.refreshDailyCost();
|
|
3984
4104
|
// Clear UI state
|
|
3985
4105
|
this.headerContainer.clear();
|
|
4106
|
+
this.committedChatContainer.clear();
|
|
3986
4107
|
this.chatContainer.clear();
|
|
3987
4108
|
this.pendingMessagesContainer.clear();
|
|
3988
4109
|
this.compactionQueuedMessages = [];
|
|
@@ -3991,7 +4112,7 @@ ${cycleModelForward || cycleModelBackward ? `| \`${cycleModelForward}\` / \`${cy
|
|
|
3991
4112
|
this.pendingTools.clear();
|
|
3992
4113
|
this.chatContainer.addChild(new Spacer(1));
|
|
3993
4114
|
this.chatContainer.addChild(new Text(`${theme.fg("accent", "✓ New session started")}`, 1, 1));
|
|
3994
|
-
this.ui.
|
|
4115
|
+
this.ui.recommitAll();
|
|
3995
4116
|
}
|
|
3996
4117
|
handleDebugCommand() {
|
|
3997
4118
|
const width = this.ui.terminal.columns;
|
|
@@ -4267,6 +4388,8 @@ ${cycleModelForward || cycleModelBackward ? `| \`${cycleModelForward}\` / \`${cy
|
|
|
4267
4388
|
result = await this.session.compact(customInstructions);
|
|
4268
4389
|
// Rebuild UI
|
|
4269
4390
|
this.rebuildChatFromMessages();
|
|
4391
|
+
this.tryCommitPrefix();
|
|
4392
|
+
this.ui.recommitAll();
|
|
4270
4393
|
// Add compaction component at bottom so user sees it without scrolling
|
|
4271
4394
|
const msg = createCompactionSummaryMessage(result.summary, result.tokensBefore, new Date().toISOString());
|
|
4272
4395
|
this.addMessageToChat(msg);
|