@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.
Files changed (57) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/README.md +21 -11
  3. package/dist/core/agent-session.d.ts +7 -0
  4. package/dist/core/agent-session.d.ts.map +1 -1
  5. package/dist/core/agent-session.js +22 -0
  6. package/dist/core/agent-session.js.map +1 -1
  7. package/dist/core/buddy/buddy-controller.d.ts.map +1 -1
  8. package/dist/core/buddy/buddy-controller.js +5 -23
  9. package/dist/core/buddy/buddy-controller.js.map +1 -1
  10. package/dist/core/context-buffer.d.ts +49 -0
  11. package/dist/core/context-buffer.d.ts.map +1 -0
  12. package/dist/core/context-buffer.js +84 -0
  13. package/dist/core/context-buffer.js.map +1 -0
  14. package/dist/core/sdk.d.ts.map +1 -1
  15. package/dist/core/sdk.js +3 -1
  16. package/dist/core/sdk.js.map +1 -1
  17. package/dist/core/settings-manager.d.ts +13 -0
  18. package/dist/core/settings-manager.d.ts.map +1 -1
  19. package/dist/core/settings-manager.js +40 -0
  20. package/dist/core/settings-manager.js.map +1 -1
  21. package/dist/core/system-prompt.d.ts +5 -0
  22. package/dist/core/system-prompt.d.ts.map +1 -1
  23. package/dist/core/system-prompt.js +6 -0
  24. package/dist/core/system-prompt.js.map +1 -1
  25. package/dist/core/thinking.d.ts +10 -1
  26. package/dist/core/thinking.d.ts.map +1 -1
  27. package/dist/core/thinking.js +14 -0
  28. package/dist/core/thinking.js.map +1 -1
  29. package/dist/core/tools/subagent.d.ts.map +1 -1
  30. package/dist/core/tools/subagent.js +1 -0
  31. package/dist/core/tools/subagent.js.map +1 -1
  32. package/dist/modes/interactive/components/settings-selector.d.ts +5 -0
  33. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  34. package/dist/modes/interactive/components/settings-selector.js +16 -0
  35. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  36. package/dist/modes/interactive/components/tool-execution.d.ts +7 -0
  37. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  38. package/dist/modes/interactive/components/tool-execution.js +16 -0
  39. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  40. package/dist/modes/interactive/interactive-mode.d.ts +33 -0
  41. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  42. package/dist/modes/interactive/interactive-mode.js +210 -87
  43. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  44. package/dist/modes/interactive/tab-title.d.ts +21 -3
  45. package/dist/modes/interactive/tab-title.d.ts.map +1 -1
  46. package/dist/modes/interactive/tab-title.js +47 -25
  47. package/dist/modes/interactive/tab-title.js.map +1 -1
  48. package/docs/agent-models.md +10 -0
  49. package/docs/development.md +1 -1
  50. package/docs/providers.md +7 -0
  51. package/docs/settings.md +29 -2
  52. package/docs/tui.md +42 -0
  53. package/examples/extensions/custom-provider-anthropic/package.json +3 -0
  54. package/examples/extensions/custom-provider-gitlab-duo/package.json +3 -0
  55. package/examples/extensions/custom-provider-qwen-cli/package.json +3 -0
  56. package/examples/extensions/with-deps/package.json +3 -0
  57. 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
- this.ui.requestRender();
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
- // Render any messages added via setup, or show empty session
1004
- this.renderInitialMessages();
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.chatContainer.clear();
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.chatContainer.clear();
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
- this.ui.requestRender();
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.ui.setFocus(this.editor);
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.ui.setFocus(this.editor);
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.ui.setFocus(this.editor);
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.ui.setFocus(this.editor);
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
- // Show retry status
2216
- this.statusContainer.clear();
2217
- if (this.loadingAnimation) {
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
- // Show retry status
2243
- this.statusContainer.clear();
2244
- if (this.loadingAnimation) {
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
- this.ui.requestRender(true);
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
- this.ui.requestRender();
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
- // Rebuild chat from session messages
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
- // Force full re-render since external editor uses alternate screen
2724
- this.ui.requestRender(true);
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.editorContainer.clear();
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
- for (const child of this.chatContainer.children) {
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.chatContainer.clear();
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.chatContainer.clear();
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.chatContainer.clear();
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.editorContainer.clear();
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.editorContainer.clear();
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.chatContainer.clear();
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.requestRender();
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);