@dreb/coding-agent 2.25.4 → 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 +2 -0
- package/README.md +20 -10
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +7 -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/settings-manager.d.ts.map +1 -1
- 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/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/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 +184 -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 +2 -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
|
@@ -70,6 +70,7 @@ export class InteractiveMode {
|
|
|
70
70
|
options;
|
|
71
71
|
session;
|
|
72
72
|
ui;
|
|
73
|
+
committedChatContainer;
|
|
73
74
|
chatContainer;
|
|
74
75
|
pendingMessagesContainer;
|
|
75
76
|
statusContainer;
|
|
@@ -100,6 +101,8 @@ export class InteractiveMode {
|
|
|
100
101
|
streamingMessage = undefined;
|
|
101
102
|
// Tool execution tracking: toolCallId -> component
|
|
102
103
|
pendingTools = new Map();
|
|
104
|
+
// Deferred commit: set to true when components finalize, commit runs after next render
|
|
105
|
+
commitNeeded = false;
|
|
103
106
|
// Tool output expansion state
|
|
104
107
|
toolOutputExpanded = false;
|
|
105
108
|
// Thinking block visibility state
|
|
@@ -190,6 +193,7 @@ export class InteractiveMode {
|
|
|
190
193
|
});
|
|
191
194
|
this.ui = new TUI(new ProcessTerminal(), this.settingsManager.getShowHardwareCursor());
|
|
192
195
|
this.headerContainer = new Container();
|
|
196
|
+
this.committedChatContainer = new Container();
|
|
193
197
|
this.chatContainer = new Container();
|
|
194
198
|
this.pendingMessagesContainer = new Container();
|
|
195
199
|
this.statusContainer = new Container();
|
|
@@ -415,6 +419,7 @@ export class InteractiveMode {
|
|
|
415
419
|
this.headerContainer.addChild(new Text(condensedText, 1, 0));
|
|
416
420
|
}
|
|
417
421
|
}
|
|
422
|
+
this.ui.addChild(this.committedChatContainer);
|
|
418
423
|
this.ui.addChild(this.chatContainer);
|
|
419
424
|
this.ui.addChild(this.pendingMessagesContainer);
|
|
420
425
|
this.ui.addChild(this.statusContainer);
|
|
@@ -425,6 +430,20 @@ export class InteractiveMode {
|
|
|
425
430
|
this.ui.addChild(this.widgetContainerBelow);
|
|
426
431
|
this.ui.addChild(this.footer);
|
|
427
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
|
+
};
|
|
428
447
|
this.setupKeyHandlers();
|
|
429
448
|
this.setupEditorSubmitHandler();
|
|
430
449
|
// Start the UI before initializing extensions so session_start handlers can use interactive dialogs
|
|
@@ -435,8 +454,14 @@ export class InteractiveMode {
|
|
|
435
454
|
this.activateStderrGuard();
|
|
436
455
|
// Initialize extensions first so resources are shown before messages
|
|
437
456
|
await this.initExtensions();
|
|
438
|
-
// 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.
|
|
439
462
|
this.renderInitialMessages();
|
|
463
|
+
this.tryCommitPrefix();
|
|
464
|
+
this.ui.recommitAll();
|
|
440
465
|
// Set terminal title
|
|
441
466
|
this.updateTerminalTitle();
|
|
442
467
|
// Initialize tab title auto-generation
|
|
@@ -449,7 +474,8 @@ export class InteractiveMode {
|
|
|
449
474
|
onThemeChange(() => {
|
|
450
475
|
this.ui.invalidate();
|
|
451
476
|
this.updateEditorBorderColor();
|
|
452
|
-
|
|
477
|
+
// Theme affects committed content — full re-commit to repaint
|
|
478
|
+
this.ui.recommitAll();
|
|
453
479
|
});
|
|
454
480
|
// Set up git branch watcher (uses provider instead of footer)
|
|
455
481
|
this.footerDataProvider.onBranchChange(() => {
|
|
@@ -482,11 +508,17 @@ export class InteractiveMode {
|
|
|
482
508
|
return;
|
|
483
509
|
this.tabTitleGenerator = new TabTitleGenerator(settings, {
|
|
484
510
|
setTitle: (title) => this.ui.terminal.setTitle(title),
|
|
511
|
+
setSessionName: (name) => {
|
|
512
|
+
this.sessionManager.appendSessionInfo(name);
|
|
513
|
+
},
|
|
485
514
|
getMessages: () => this.session.state.messages,
|
|
486
515
|
getModel: () => this.session.model,
|
|
487
516
|
getModelRegistry: () => this.session.modelRegistry,
|
|
488
517
|
getProvider: () => this.session.model?.provider,
|
|
489
518
|
getAgentModelsOverride: (name) => this.settingsManager.getAgentModelsForAgent(name),
|
|
519
|
+
getBranch: () => this.footerDataProvider.getGitBranch(),
|
|
520
|
+
getRepo: () => path.basename(process.cwd()),
|
|
521
|
+
getCwd: () => process.cwd(),
|
|
490
522
|
});
|
|
491
523
|
}
|
|
492
524
|
/**
|
|
@@ -996,15 +1028,13 @@ export class InteractiveMode {
|
|
|
996
1028
|
}
|
|
997
1029
|
// Clear UI state
|
|
998
1030
|
this.editor.setGhostText?.(null);
|
|
999
|
-
this.chatContainer.clear();
|
|
1000
1031
|
this.pendingMessagesContainer.clear();
|
|
1001
1032
|
this.compactionQueuedMessages = [];
|
|
1002
1033
|
this.streamingComponent = undefined;
|
|
1003
1034
|
this.streamingMessage = undefined;
|
|
1004
1035
|
this.pendingTools.clear();
|
|
1005
|
-
//
|
|
1006
|
-
this.
|
|
1007
|
-
this.ui.requestRender();
|
|
1036
|
+
// Re-render from new session state
|
|
1037
|
+
this.resetChatDisplay();
|
|
1008
1038
|
return { cancelled: false };
|
|
1009
1039
|
},
|
|
1010
1040
|
fork: async (entryId) => {
|
|
@@ -1012,8 +1042,7 @@ export class InteractiveMode {
|
|
|
1012
1042
|
if (result.cancelled) {
|
|
1013
1043
|
return { cancelled: true };
|
|
1014
1044
|
}
|
|
1015
|
-
this.
|
|
1016
|
-
this.renderInitialMessages();
|
|
1045
|
+
this.resetChatDisplay();
|
|
1017
1046
|
this.editor.setText(result.selectedText);
|
|
1018
1047
|
this.showStatus("Forked to new session");
|
|
1019
1048
|
return { cancelled: false };
|
|
@@ -1028,8 +1057,7 @@ export class InteractiveMode {
|
|
|
1028
1057
|
if (result.cancelled) {
|
|
1029
1058
|
return { cancelled: true };
|
|
1030
1059
|
}
|
|
1031
|
-
this.
|
|
1032
|
-
this.renderInitialMessages();
|
|
1060
|
+
this.resetChatDisplay();
|
|
1033
1061
|
if (result.editorText && !this.editor.getText().trim()) {
|
|
1034
1062
|
this.editor.setText(result.editorText);
|
|
1035
1063
|
}
|
|
@@ -1297,7 +1325,9 @@ export class InteractiveMode {
|
|
|
1297
1325
|
this.headerContainer.children[index] = this.builtInHeader;
|
|
1298
1326
|
}
|
|
1299
1327
|
}
|
|
1300
|
-
|
|
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();
|
|
1301
1331
|
}
|
|
1302
1332
|
addExtensionTerminalInputListener(handler) {
|
|
1303
1333
|
const unsubscribe = this.ui.addInputListener(handler);
|
|
@@ -1406,11 +1436,8 @@ export class InteractiveMode {
|
|
|
1406
1436
|
*/
|
|
1407
1437
|
hideExtensionSelector() {
|
|
1408
1438
|
this.extensionSelector?.dispose();
|
|
1409
|
-
this.editorContainer.clear();
|
|
1410
|
-
this.editorContainer.addChild(this.editor);
|
|
1411
1439
|
this.extensionSelector = undefined;
|
|
1412
|
-
this.
|
|
1413
|
-
this.ui.requestRender();
|
|
1440
|
+
this.restoreEditorComponent();
|
|
1414
1441
|
}
|
|
1415
1442
|
/**
|
|
1416
1443
|
* Show a confirmation dialog for extensions.
|
|
@@ -1453,11 +1480,8 @@ export class InteractiveMode {
|
|
|
1453
1480
|
*/
|
|
1454
1481
|
hideExtensionInput() {
|
|
1455
1482
|
this.extensionInput?.dispose();
|
|
1456
|
-
this.editorContainer.clear();
|
|
1457
|
-
this.editorContainer.addChild(this.editor);
|
|
1458
1483
|
this.extensionInput = undefined;
|
|
1459
|
-
this.
|
|
1460
|
-
this.ui.requestRender();
|
|
1484
|
+
this.restoreEditorComponent();
|
|
1461
1485
|
}
|
|
1462
1486
|
/**
|
|
1463
1487
|
* Show a multi-line editor for extensions (with Ctrl+G support).
|
|
@@ -1481,11 +1505,8 @@ export class InteractiveMode {
|
|
|
1481
1505
|
* Hide the extension editor.
|
|
1482
1506
|
*/
|
|
1483
1507
|
hideExtensionEditor() {
|
|
1484
|
-
this.editorContainer.clear();
|
|
1485
|
-
this.editorContainer.addChild(this.editor);
|
|
1486
1508
|
this.extensionEditor = undefined;
|
|
1487
|
-
this.
|
|
1488
|
-
this.ui.requestRender();
|
|
1509
|
+
this.restoreEditorComponent();
|
|
1489
1510
|
}
|
|
1490
1511
|
/**
|
|
1491
1512
|
* Set a custom editor component from an extension.
|
|
@@ -1565,11 +1586,8 @@ export class InteractiveMode {
|
|
|
1565
1586
|
const savedText = this.editor.getText();
|
|
1566
1587
|
const isOverlay = options?.overlay ?? false;
|
|
1567
1588
|
const restoreEditor = () => {
|
|
1568
|
-
this.editorContainer.clear();
|
|
1569
|
-
this.editorContainer.addChild(this.editor);
|
|
1570
1589
|
this.editor.setText(savedText);
|
|
1571
|
-
this.
|
|
1572
|
-
this.ui.requestRender();
|
|
1590
|
+
this.restoreEditorComponent();
|
|
1573
1591
|
};
|
|
1574
1592
|
return new Promise((resolve, reject) => {
|
|
1575
1593
|
let component;
|
|
@@ -2057,6 +2075,10 @@ export class InteractiveMode {
|
|
|
2057
2075
|
}
|
|
2058
2076
|
// Capture assistant response for buddy context
|
|
2059
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;
|
|
2060
2082
|
this.ui.requestRender();
|
|
2061
2083
|
break;
|
|
2062
2084
|
case "tool_execution_start": {
|
|
@@ -2086,12 +2108,19 @@ export class InteractiveMode {
|
|
|
2086
2108
|
if (component) {
|
|
2087
2109
|
component.updateResult({ ...event.result, isError: event.isError });
|
|
2088
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;
|
|
2089
2118
|
this.ui.requestRender();
|
|
2090
2119
|
}
|
|
2091
2120
|
// Buddy context + reaction for tool execution
|
|
2092
2121
|
this.buddyController.handleEvent(event);
|
|
2093
2122
|
// Tab title auto-generation
|
|
2094
|
-
this.tabTitleGenerator?.onToolEnd();
|
|
2123
|
+
this.tabTitleGenerator?.onToolEnd(event);
|
|
2095
2124
|
break;
|
|
2096
2125
|
}
|
|
2097
2126
|
case "agent_end":
|
|
@@ -2110,6 +2139,9 @@ export class InteractiveMode {
|
|
|
2110
2139
|
this.streamingMessage = undefined;
|
|
2111
2140
|
}
|
|
2112
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;
|
|
2113
2145
|
await this.checkShutdownRequested();
|
|
2114
2146
|
// Buddy reaction: session wrap-up quip
|
|
2115
2147
|
this.buddyController.handleEvent(event);
|
|
@@ -2149,8 +2181,9 @@ export class InteractiveMode {
|
|
|
2149
2181
|
}
|
|
2150
2182
|
else if (event.result) {
|
|
2151
2183
|
// Rebuild chat to show compacted state
|
|
2152
|
-
this.chatContainer.clear();
|
|
2153
2184
|
this.rebuildChatFromMessages();
|
|
2185
|
+
this.tryCommitPrefix();
|
|
2186
|
+
this.ui.recommitAll();
|
|
2154
2187
|
// Add compaction component at bottom so user sees it without scrolling
|
|
2155
2188
|
this.addMessageToChat({
|
|
2156
2189
|
role: "compactionSummary",
|
|
@@ -2214,19 +2247,9 @@ export class InteractiveMode {
|
|
|
2214
2247
|
this.chatContainer.removeChild(component);
|
|
2215
2248
|
}
|
|
2216
2249
|
this.pendingTools.clear();
|
|
2217
|
-
//
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
this.loadingAnimation.stop();
|
|
2221
|
-
this.loadingAnimation = undefined;
|
|
2222
|
-
}
|
|
2223
|
-
if (this.retryLoader) {
|
|
2224
|
-
this.retryLoader.stop();
|
|
2225
|
-
this.retryLoader = undefined;
|
|
2226
|
-
}
|
|
2227
|
-
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)`);
|
|
2228
|
-
this.statusContainer.addChild(this.retryLoader);
|
|
2229
|
-
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})…`);
|
|
2230
2253
|
break;
|
|
2231
2254
|
}
|
|
2232
2255
|
case "length_retry": {
|
|
@@ -2241,19 +2264,9 @@ export class InteractiveMode {
|
|
|
2241
2264
|
this.chatContainer.removeChild(component);
|
|
2242
2265
|
}
|
|
2243
2266
|
this.pendingTools.clear();
|
|
2244
|
-
//
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
this.loadingAnimation.stop();
|
|
2248
|
-
this.loadingAnimation = undefined;
|
|
2249
|
-
}
|
|
2250
|
-
if (this.retryLoader) {
|
|
2251
|
-
this.retryLoader.stop();
|
|
2252
|
-
this.retryLoader = undefined;
|
|
2253
|
-
}
|
|
2254
|
-
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)`);
|
|
2255
|
-
this.statusContainer.addChild(this.retryLoader);
|
|
2256
|
-
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})…`);
|
|
2257
2270
|
break;
|
|
2258
2271
|
}
|
|
2259
2272
|
case "background_agent_start":
|
|
@@ -2489,6 +2502,10 @@ export class InteractiveMode {
|
|
|
2489
2502
|
});
|
|
2490
2503
|
}
|
|
2491
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();
|
|
2492
2509
|
this.chatContainer.clear();
|
|
2493
2510
|
const context = this.sessionManager.buildSessionContext();
|
|
2494
2511
|
this.renderSessionContext(context);
|
|
@@ -2558,7 +2575,9 @@ export class InteractiveMode {
|
|
|
2558
2575
|
process.removeListener("SIGINT", ignoreSigint);
|
|
2559
2576
|
this.ui.start();
|
|
2560
2577
|
this.activateStderrGuard();
|
|
2561
|
-
|
|
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();
|
|
2562
2581
|
});
|
|
2563
2582
|
try {
|
|
2564
2583
|
// Stop the TUI (restore terminal to normal mode)
|
|
@@ -2656,23 +2675,81 @@ export class InteractiveMode {
|
|
|
2656
2675
|
this.ui.requestRender();
|
|
2657
2676
|
this.showStatus(`Tasks panel: ${this.tasksPanel.visible ? "visible" : "hidden"}`);
|
|
2658
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
|
+
}
|
|
2659
2730
|
toggleToolOutputExpansion() {
|
|
2660
2731
|
this.setToolsExpanded(!this.toolOutputExpanded);
|
|
2661
2732
|
}
|
|
2662
2733
|
setToolsExpanded(expanded) {
|
|
2663
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
|
+
}
|
|
2664
2741
|
for (const child of this.chatContainer.children) {
|
|
2665
2742
|
if (isExpandable(child)) {
|
|
2666
2743
|
child.setExpanded(expanded);
|
|
2667
2744
|
}
|
|
2668
2745
|
}
|
|
2669
|
-
|
|
2746
|
+
// Committed content changed — full re-commit to repaint
|
|
2747
|
+
this.ui.recommitAll();
|
|
2670
2748
|
}
|
|
2671
2749
|
toggleThinkingBlockVisibility() {
|
|
2672
2750
|
this.hideThinkingBlock = !this.hideThinkingBlock;
|
|
2673
2751
|
this.settingsManager.setHideThinkingBlock(this.hideThinkingBlock);
|
|
2674
|
-
//
|
|
2675
|
-
this.chatContainer.clear();
|
|
2752
|
+
// Full rebuild: rebuildChatFromMessages() clears both containers internally
|
|
2676
2753
|
this.rebuildChatFromMessages();
|
|
2677
2754
|
// If streaming, re-add the streaming component with updated visibility and re-render
|
|
2678
2755
|
if (this.streamingComponent && this.streamingMessage) {
|
|
@@ -2680,6 +2757,9 @@ export class InteractiveMode {
|
|
|
2680
2757
|
this.streamingComponent.updateContent(this.streamingMessage);
|
|
2681
2758
|
this.chatContainer.addChild(this.streamingComponent);
|
|
2682
2759
|
}
|
|
2760
|
+
// Committed content rebuilt — full re-commit to repaint
|
|
2761
|
+
this.tryCommitPrefix();
|
|
2762
|
+
this.ui.recommitAll();
|
|
2683
2763
|
this.showStatus(`Thinking blocks: ${this.hideThinkingBlock ? "hidden" : "visible"}`);
|
|
2684
2764
|
}
|
|
2685
2765
|
openExternalEditor() {
|
|
@@ -2722,8 +2802,9 @@ export class InteractiveMode {
|
|
|
2722
2802
|
// Restart TUI and re-intercept stderr
|
|
2723
2803
|
this.ui.start();
|
|
2724
2804
|
this.activateStderrGuard();
|
|
2725
|
-
//
|
|
2726
|
-
|
|
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();
|
|
2727
2808
|
}
|
|
2728
2809
|
}
|
|
2729
2810
|
// =========================================================================
|
|
@@ -2954,15 +3035,31 @@ export class InteractiveMode {
|
|
|
2954
3035
|
// =========================================================================
|
|
2955
3036
|
// Selectors
|
|
2956
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
|
+
}
|
|
2957
3056
|
/**
|
|
2958
3057
|
* Shows a selector component in place of the editor.
|
|
2959
3058
|
* @param create Factory that receives a `done` callback and returns the component and focus target
|
|
2960
3059
|
*/
|
|
2961
3060
|
showSelector(create) {
|
|
2962
3061
|
const done = () => {
|
|
2963
|
-
this.
|
|
2964
|
-
this.editorContainer.addChild(this.editor);
|
|
2965
|
-
this.ui.setFocus(this.editor);
|
|
3062
|
+
this.restoreEditorComponent();
|
|
2966
3063
|
};
|
|
2967
3064
|
const { component, focus } = create(done);
|
|
2968
3065
|
this.editorContainer.clear();
|
|
@@ -3016,11 +3113,18 @@ export class InteractiveMode {
|
|
|
3016
3113
|
},
|
|
3017
3114
|
onShowImagesChange: (enabled) => {
|
|
3018
3115
|
this.settingsManager.setShowImages(enabled);
|
|
3116
|
+
for (const child of this.committedChatContainer.children) {
|
|
3117
|
+
if (child instanceof ToolExecutionComponent) {
|
|
3118
|
+
child.setShowImages(enabled);
|
|
3119
|
+
}
|
|
3120
|
+
}
|
|
3019
3121
|
for (const child of this.chatContainer.children) {
|
|
3020
3122
|
if (child instanceof ToolExecutionComponent) {
|
|
3021
3123
|
child.setShowImages(enabled);
|
|
3022
3124
|
}
|
|
3023
3125
|
}
|
|
3126
|
+
// Committed content changed — full re-commit to repaint
|
|
3127
|
+
this.ui.recommitAll();
|
|
3024
3128
|
},
|
|
3025
3129
|
onAutoResizeImagesChange: (enabled) => {
|
|
3026
3130
|
this.settingsManager.setImageAutoResize(enabled);
|
|
@@ -3065,13 +3169,10 @@ export class InteractiveMode {
|
|
|
3065
3169
|
onHideThinkingBlockChange: (hidden) => {
|
|
3066
3170
|
this.hideThinkingBlock = hidden;
|
|
3067
3171
|
this.settingsManager.setHideThinkingBlock(hidden);
|
|
3068
|
-
|
|
3069
|
-
if (child instanceof AssistantMessageComponent) {
|
|
3070
|
-
child.setHideThinkingBlock(hidden);
|
|
3071
|
-
}
|
|
3072
|
-
}
|
|
3073
|
-
this.chatContainer.clear();
|
|
3172
|
+
// Full rebuild: rebuildChatFromMessages() clears both containers
|
|
3074
3173
|
this.rebuildChatFromMessages();
|
|
3174
|
+
this.tryCommitPrefix();
|
|
3175
|
+
this.ui.recommitAll();
|
|
3075
3176
|
},
|
|
3076
3177
|
onThinkingDisplayChange: (display) => {
|
|
3077
3178
|
const model = this.session.model;
|
|
@@ -3330,8 +3431,7 @@ export class InteractiveMode {
|
|
|
3330
3431
|
this.ui.requestRender();
|
|
3331
3432
|
return;
|
|
3332
3433
|
}
|
|
3333
|
-
this.
|
|
3334
|
-
this.renderInitialMessages();
|
|
3434
|
+
this.resetChatDisplay();
|
|
3335
3435
|
this.editor.setText(result.selectedText);
|
|
3336
3436
|
done();
|
|
3337
3437
|
this.showStatus("Branched to new session");
|
|
@@ -3416,8 +3516,7 @@ export class InteractiveMode {
|
|
|
3416
3516
|
return;
|
|
3417
3517
|
}
|
|
3418
3518
|
// Update UI
|
|
3419
|
-
this.
|
|
3420
|
-
this.renderInitialMessages();
|
|
3519
|
+
this.resetChatDisplay();
|
|
3421
3520
|
if (result.editorText && !this.editor.getText().trim()) {
|
|
3422
3521
|
this.editor.setText(result.editorText);
|
|
3423
3522
|
}
|
|
@@ -3485,8 +3584,7 @@ export class InteractiveMode {
|
|
|
3485
3584
|
await this.session.switchSession(sessionPath);
|
|
3486
3585
|
await this.footerDataProvider.refreshDailyCost();
|
|
3487
3586
|
// Clear and re-render the chat
|
|
3488
|
-
this.
|
|
3489
|
-
this.renderInitialMessages();
|
|
3587
|
+
this.resetChatDisplay();
|
|
3490
3588
|
this.showStatus("Resumed session");
|
|
3491
3589
|
}
|
|
3492
3590
|
async showOAuthSelector(mode) {
|
|
@@ -3550,10 +3648,7 @@ export class InteractiveMode {
|
|
|
3550
3648
|
});
|
|
3551
3649
|
// Restore editor helper
|
|
3552
3650
|
const restoreEditor = () => {
|
|
3553
|
-
this.
|
|
3554
|
-
this.editorContainer.addChild(this.editor);
|
|
3555
|
-
this.ui.setFocus(this.editor);
|
|
3556
|
-
this.ui.requestRender();
|
|
3651
|
+
this.restoreEditorComponent();
|
|
3557
3652
|
};
|
|
3558
3653
|
try {
|
|
3559
3654
|
await this.session.modelRegistry.authStorage.login(providerId, {
|
|
@@ -3628,10 +3723,7 @@ export class InteractiveMode {
|
|
|
3628
3723
|
this.ui.requestRender();
|
|
3629
3724
|
const dismissLoader = (editor) => {
|
|
3630
3725
|
loader.dispose();
|
|
3631
|
-
this.
|
|
3632
|
-
this.editorContainer.addChild(editor);
|
|
3633
|
-
this.ui.setFocus(editor);
|
|
3634
|
-
this.ui.requestRender();
|
|
3726
|
+
this.restoreEditorComponent(editor);
|
|
3635
3727
|
};
|
|
3636
3728
|
try {
|
|
3637
3729
|
await this.session.reload();
|
|
@@ -3658,6 +3750,9 @@ export class InteractiveMode {
|
|
|
3658
3750
|
this.setupExtensionShortcuts(runner);
|
|
3659
3751
|
}
|
|
3660
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.
|
|
3661
3756
|
dismissLoader(this.editor);
|
|
3662
3757
|
this.showLoadedResources({
|
|
3663
3758
|
force: false,
|
|
@@ -3722,8 +3817,7 @@ export class InteractiveMode {
|
|
|
3722
3817
|
return;
|
|
3723
3818
|
}
|
|
3724
3819
|
// Clear and re-render the chat
|
|
3725
|
-
this.
|
|
3726
|
-
this.renderInitialMessages();
|
|
3820
|
+
this.resetChatDisplay();
|
|
3727
3821
|
this.showStatus(`Session imported from: ${inputPath}`);
|
|
3728
3822
|
}
|
|
3729
3823
|
catch (error) {
|
|
@@ -4009,6 +4103,7 @@ ${cycleModelForward || cycleModelBackward ? `| \`${cycleModelForward}\` / \`${cy
|
|
|
4009
4103
|
await this.footerDataProvider.refreshDailyCost();
|
|
4010
4104
|
// Clear UI state
|
|
4011
4105
|
this.headerContainer.clear();
|
|
4106
|
+
this.committedChatContainer.clear();
|
|
4012
4107
|
this.chatContainer.clear();
|
|
4013
4108
|
this.pendingMessagesContainer.clear();
|
|
4014
4109
|
this.compactionQueuedMessages = [];
|
|
@@ -4017,7 +4112,7 @@ ${cycleModelForward || cycleModelBackward ? `| \`${cycleModelForward}\` / \`${cy
|
|
|
4017
4112
|
this.pendingTools.clear();
|
|
4018
4113
|
this.chatContainer.addChild(new Spacer(1));
|
|
4019
4114
|
this.chatContainer.addChild(new Text(`${theme.fg("accent", "✓ New session started")}`, 1, 1));
|
|
4020
|
-
this.ui.
|
|
4115
|
+
this.ui.recommitAll();
|
|
4021
4116
|
}
|
|
4022
4117
|
handleDebugCommand() {
|
|
4023
4118
|
const width = this.ui.terminal.columns;
|
|
@@ -4293,6 +4388,8 @@ ${cycleModelForward || cycleModelBackward ? `| \`${cycleModelForward}\` / \`${cy
|
|
|
4293
4388
|
result = await this.session.compact(customInstructions);
|
|
4294
4389
|
// Rebuild UI
|
|
4295
4390
|
this.rebuildChatFromMessages();
|
|
4391
|
+
this.tryCommitPrefix();
|
|
4392
|
+
this.ui.recommitAll();
|
|
4296
4393
|
// Add compaction component at bottom so user sees it without scrolling
|
|
4297
4394
|
const msg = createCompactionSummaryMessage(result.summary, result.tokensBefore, new Date().toISOString());
|
|
4298
4395
|
this.addMessageToChat(msg);
|