@leanandmean/coding-agent 0.74.1-scramjet.24 → 0.74.1-scramjet.25

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 (41) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/dist/core/extensions/types.d.ts +1 -1
  3. package/dist/core/extensions/types.d.ts.map +1 -1
  4. package/dist/core/extensions/types.js.map +1 -1
  5. package/dist/core/settings-manager.d.ts +0 -3
  6. package/dist/core/settings-manager.d.ts.map +1 -1
  7. package/dist/core/settings-manager.js +0 -15
  8. package/dist/core/settings-manager.js.map +1 -1
  9. package/dist/main.d.ts.map +1 -1
  10. package/dist/main.js +1 -1
  11. package/dist/main.js.map +1 -1
  12. package/dist/modes/interactive/components/armin.d.ts +4 -2
  13. package/dist/modes/interactive/components/armin.d.ts.map +1 -1
  14. package/dist/modes/interactive/components/armin.js +14 -8
  15. package/dist/modes/interactive/components/armin.js.map +1 -1
  16. package/dist/modes/interactive/components/assistant-message.d.ts +3 -1
  17. package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  18. package/dist/modes/interactive/components/assistant-message.js +8 -2
  19. package/dist/modes/interactive/components/assistant-message.js.map +1 -1
  20. package/dist/modes/interactive/components/daxnuts.d.ts +4 -1
  21. package/dist/modes/interactive/components/daxnuts.d.ts.map +1 -1
  22. package/dist/modes/interactive/components/daxnuts.js +14 -6
  23. package/dist/modes/interactive/components/daxnuts.js.map +1 -1
  24. package/dist/modes/interactive/components/extension-editor.d.ts.map +1 -1
  25. package/dist/modes/interactive/components/extension-editor.js +2 -2
  26. package/dist/modes/interactive/components/extension-editor.js.map +1 -1
  27. package/dist/modes/interactive/components/settings-selector.d.ts +0 -2
  28. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  29. package/dist/modes/interactive/components/settings-selector.js +1 -13
  30. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  31. package/dist/modes/interactive/components/tool-execution.d.ts +11 -0
  32. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  33. package/dist/modes/interactive/components/tool-execution.js +69 -18
  34. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  35. package/dist/modes/interactive/interactive-mode.d.ts +13 -0
  36. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  37. package/dist/modes/interactive/interactive-mode.js +213 -59
  38. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  39. package/docs/settings.md +0 -1
  40. package/docs/tui.md +12 -0
  41. package/package.json +2 -2
@@ -108,7 +108,9 @@ export class InteractiveMode {
108
108
  options;
109
109
  runtimeHost;
110
110
  ui;
111
+ committedChatContainer;
111
112
  chatContainer;
113
+ mutableChatComponents = new Set();
112
114
  pendingMessagesContainer;
113
115
  statusContainer;
114
116
  defaultEditor;
@@ -150,6 +152,8 @@ export class InteractiveMode {
150
152
  streamingMessage = undefined;
151
153
  // Tool execution tracking: toolCallId -> component
152
154
  pendingTools = new Map();
155
+ pendingToolFinalizations = new Set();
156
+ agentRunGeneration = 0;
153
157
  // Tool output expansion state
154
158
  toolOutputExpanded = false;
155
159
  // Thinking block visibility state
@@ -217,9 +221,10 @@ export class InteractiveMode {
217
221
  await this.rebindCurrentSession();
218
222
  });
219
223
  this.version = VERSION;
220
- this.ui = new TUI(new ProcessTerminal(), this.settingsManager.getShowHardwareCursor());
221
- this.ui.setClearOnShrink(this.settingsManager.getClearOnShrink());
224
+ // SCRAMJET-DIVERGENCE: mandatory committed/live rendering and injectable terminals (#389).
225
+ this.ui = new TUI(options.terminal ?? new ProcessTerminal(), this.settingsManager.getShowHardwareCursor());
222
226
  this.headerContainer = new Container();
227
+ this.committedChatContainer = new Container();
223
228
  this.chatContainer = new Container();
224
229
  this.pendingMessagesContainer = new Container();
225
230
  this.statusContainer = new Container();
@@ -250,6 +255,73 @@ export class InteractiveMode {
250
255
  this.detectedClassification = detectThemeFromEnvironment()?.theme;
251
256
  initTheme(this.settingsManager.getTheme(), true);
252
257
  }
258
+ // SCRAMJET-DIVERGENCE: finalized transcript components are promoted atomically into append-only history (#389).
259
+ promoteFinalizedChatPrefix() {
260
+ let promoted = false;
261
+ while (this.chatContainer.children.length > 0) {
262
+ const child = this.chatContainer.children[0];
263
+ if (this.mutableChatComponents.has(child))
264
+ break;
265
+ this.chatContainer.removeChild(child);
266
+ if (child instanceof ToolExecutionComponent)
267
+ child.markCommitted();
268
+ this.committedChatContainer.addChild(child);
269
+ promoted = true;
270
+ }
271
+ if (promoted)
272
+ this.ui.commit();
273
+ }
274
+ setChatComponentMutable(component, mutable) {
275
+ if (mutable)
276
+ this.mutableChatComponents.add(component);
277
+ else
278
+ this.mutableChatComponents.delete(component);
279
+ }
280
+ sealStatus() {
281
+ if (this.lastStatusSpacer)
282
+ this.mutableChatComponents.delete(this.lastStatusSpacer);
283
+ if (this.lastStatusText)
284
+ this.mutableChatComponents.delete(this.lastStatusText);
285
+ this.lastStatusSpacer = undefined;
286
+ this.lastStatusText = undefined;
287
+ this.promoteFinalizedChatPrefix();
288
+ }
289
+ clearTranscript() {
290
+ this.pendingToolFinalizations = new Set();
291
+ const tools = new Set([
292
+ ...this.pendingTools.values(),
293
+ ...this.committedChatContainer.children.filter((component) => component instanceof ToolExecutionComponent),
294
+ ...this.chatContainer.children.filter((component) => component instanceof ToolExecutionComponent),
295
+ ]);
296
+ for (const tool of tools)
297
+ tool.detach();
298
+ this.committedChatContainer.clear();
299
+ this.chatContainer.clear();
300
+ this.mutableChatComponents.clear();
301
+ this.lastStatusSpacer = undefined;
302
+ this.lastStatusText = undefined;
303
+ this.ui.rebuild();
304
+ }
305
+ updateToolImagePresentation(update) {
306
+ for (const container of [this.committedChatContainer, this.chatContainer]) {
307
+ for (const child of container.children) {
308
+ if (child instanceof ToolExecutionComponent)
309
+ update(child);
310
+ }
311
+ }
312
+ this.ui.rebuild();
313
+ }
314
+ commitFinalizedChatOutput() {
315
+ this.sealStatus();
316
+ this.ui.requestRender();
317
+ }
318
+ bindThemeChangeHandler() {
319
+ onThemeChange(() => {
320
+ this.ui.invalidate();
321
+ this.updateEditorBorderColor();
322
+ this.ui.rebuild();
323
+ });
324
+ }
253
325
  // SCRAMJET-DIVERGENCE: re-resolve the theme after extension themes register (or after a reload),
254
326
  // so scramjet-dark can take effect once it becomes available. An explicit user setting still wins
255
327
  // via the resolver; the captured classification (env-seeded + OSC-updated) drives auto-detection.
@@ -408,6 +480,7 @@ export class InteractiveMode {
408
480
  this.chatContainer.addChild(new Spacer(1));
409
481
  }
410
482
  this.chatContainer.addChild(new DynamicBorder());
483
+ this.promoteFinalizedChatPrefix();
411
484
  }
412
485
  async init() {
413
486
  if (this.isInitialized)
@@ -434,7 +507,9 @@ export class InteractiveMode {
434
507
  }
435
508
  // Add header container as first child (content built after theme detection below)
436
509
  this.ui.addChild(this.headerContainer);
510
+ this.ui.addChild(this.committedChatContainer);
437
511
  this.ui.addChild(this.chatContainer);
512
+ this.ui.setLiveRegionStart(this.chatContainer);
438
513
  this.ui.addChild(this.pendingMessagesContainer);
439
514
  this.ui.addChild(this.statusContainer);
440
515
  this.renderWidgets(); // Initialize with default spacer
@@ -512,11 +587,7 @@ export class InteractiveMode {
512
587
  // Render initial messages AFTER showing loaded resources
513
588
  this.renderInitialMessages();
514
589
  // Set up theme file watcher
515
- onThemeChange(() => {
516
- this.ui.invalidate();
517
- this.updateEditorBorderColor();
518
- this.ui.requestRender();
519
- });
590
+ this.bindThemeChangeHandler();
520
591
  // Set up git branch watcher (uses provider instead of footer)
521
592
  this.footerDataProvider.onBranchChange(() => {
522
593
  this.ui.requestRender();
@@ -1169,7 +1240,7 @@ export class InteractiveMode {
1169
1240
  if (result.cancelled) {
1170
1241
  return { cancelled: true };
1171
1242
  }
1172
- this.chatContainer.clear();
1243
+ this.clearTranscript();
1173
1244
  this.renderInitialMessages();
1174
1245
  if (result.editorText && !this.editor.getText().trim()) {
1175
1246
  this.editor.setText(result.editorText);
@@ -1204,6 +1275,7 @@ export class InteractiveMode {
1204
1275
  this.setupExtensionShortcuts(extensionRunner);
1205
1276
  this.showLoadedResources({ force: false, showDiagnosticsWhenQuiet: true });
1206
1277
  this.showStartupNoticesIfNeeded();
1278
+ this.promoteFinalizedChatPrefix();
1207
1279
  }
1208
1280
  applyRuntimeSettings() {
1209
1281
  this.footer.setSession(this.session);
@@ -1211,7 +1283,6 @@ export class InteractiveMode {
1211
1283
  this.footerDataProvider.setCwd(this.sessionManager.getCwd());
1212
1284
  this.hideThinkingBlock = this.settingsManager.getHideThinkingBlock();
1213
1285
  this.ui.setShowHardwareCursor(this.settingsManager.getShowHardwareCursor());
1214
- this.ui.setClearOnShrink(this.settingsManager.getClearOnShrink());
1215
1286
  const editorPaddingX = this.settingsManager.getEditorPaddingX();
1216
1287
  const autocompleteMaxVisible = this.settingsManager.getAutocompleteMaxVisible();
1217
1288
  this.defaultEditor.setPaddingX(editorPaddingX);
@@ -1262,7 +1333,7 @@ export class InteractiveMode {
1262
1333
  process.exit(1);
1263
1334
  }
1264
1335
  renderCurrentSessionState() {
1265
- this.chatContainer.clear();
1336
+ this.clearTranscript();
1266
1337
  this.pendingMessagesContainer.clear();
1267
1338
  this.compactionQueuedMessages = [];
1268
1339
  this.streamingComponent = undefined;
@@ -1372,7 +1443,7 @@ export class InteractiveMode {
1372
1443
  }
1373
1444
  setHiddenThinkingLabel(label) {
1374
1445
  this.hiddenThinkingLabel = label ?? this.defaultHiddenThinkingLabel;
1375
- for (const child of this.chatContainer.children) {
1446
+ for (const child of [...this.committedChatContainer.children, ...this.chatContainer.children]) {
1376
1447
  if (child instanceof AssistantMessageComponent) {
1377
1448
  child.setHiddenThinkingLabel(this.hiddenThinkingLabel);
1378
1449
  }
@@ -1380,7 +1451,7 @@ export class InteractiveMode {
1380
1451
  if (this.streamingComponent) {
1381
1452
  this.streamingComponent.setHiddenThinkingLabel(this.hiddenThinkingLabel);
1382
1453
  }
1383
- this.ui.requestRender();
1454
+ this.ui.rebuild();
1384
1455
  }
1385
1456
  /**
1386
1457
  * Set an extension widget (string array or custom component).
@@ -1554,7 +1625,7 @@ export class InteractiveMode {
1554
1625
  this.headerContainer.children[index] = this.builtInHeader;
1555
1626
  }
1556
1627
  }
1557
- this.ui.requestRender();
1628
+ this.ui.rebuild();
1558
1629
  }
1559
1630
  addExtensionTerminalInputListener(handler) {
1560
1631
  const unsubscribe = this.ui.addInputListener(handler);
@@ -1913,7 +1984,7 @@ export class InteractiveMode {
1913
1984
  this.chatContainer.addChild(new Text(stackLines, 1, 0));
1914
1985
  }
1915
1986
  }
1916
- this.ui.requestRender();
1987
+ this.commitFinalizedChatOutput();
1917
1988
  }
1918
1989
  // =========================================================================
1919
1990
  // Key Handlers
@@ -2187,7 +2258,9 @@ export class InteractiveMode {
2187
2258
  this.footer.invalidate();
2188
2259
  switch (event.type) {
2189
2260
  case "agent_start":
2261
+ this.agentRunGeneration += 1;
2190
2262
  this.pendingTools.clear();
2263
+ this.pendingToolFinalizations = new Set();
2191
2264
  if (this.settingsManager.getShowTerminalProgress()) {
2192
2265
  this.ui.terminal.setProgress(true);
2193
2266
  }
@@ -2226,6 +2299,7 @@ export class InteractiveMode {
2226
2299
  this.updateEditorBorderColor();
2227
2300
  break;
2228
2301
  case "message_start":
2302
+ this.sealStatus();
2229
2303
  if (event.message.role === "custom") {
2230
2304
  this.addMessageToChat(event.message);
2231
2305
  this.ui.requestRender();
@@ -2236,9 +2310,10 @@ export class InteractiveMode {
2236
2310
  this.ui.requestRender();
2237
2311
  }
2238
2312
  else if (event.message.role === "assistant") {
2239
- this.streamingComponent = new AssistantMessageComponent(undefined, this.hideThinkingBlock, this.getMarkdownThemeWithSettings(), this.hiddenThinkingLabel);
2313
+ this.streamingComponent = new AssistantMessageComponent(undefined, this.hideThinkingBlock, this.getMarkdownThemeWithSettings(), this.hiddenThinkingLabel, false);
2240
2314
  this.streamingMessage = event.message;
2241
2315
  this.chatContainer.addChild(this.streamingComponent);
2316
+ this.setChatComponentMutable(this.streamingComponent, true);
2242
2317
  this.streamingComponent.updateContent(this.streamingMessage);
2243
2318
  this.ui.requestRender();
2244
2319
  }
@@ -2256,6 +2331,7 @@ export class InteractiveMode {
2256
2331
  }, this.getRegisteredToolDefinition(content.name), this.ui, this.sessionManager.getCwd());
2257
2332
  component.setExpanded(this.toolOutputExpanded);
2258
2333
  this.chatContainer.addChild(component);
2334
+ this.setChatComponentMutable(component, true);
2259
2335
  this.pendingTools.set(content.id, component);
2260
2336
  }
2261
2337
  else {
@@ -2284,6 +2360,8 @@ export class InteractiveMode {
2284
2360
  this.streamingMessage.errorMessage = errorMessage;
2285
2361
  }
2286
2362
  this.streamingComponent.updateContent(this.streamingMessage);
2363
+ this.streamingComponent.setFinalized(true);
2364
+ this.setChatComponentMutable(this.streamingComponent, false);
2287
2365
  if (this.streamingMessage.stopReason === "aborted" || this.streamingMessage.stopReason === "error") {
2288
2366
  if (!errorMessage) {
2289
2367
  errorMessage = this.streamingMessage.errorMessage || "Error";
@@ -2293,6 +2371,8 @@ export class InteractiveMode {
2293
2371
  content: [{ type: "text", text: errorMessage }],
2294
2372
  isError: true,
2295
2373
  });
2374
+ component.seal();
2375
+ this.setChatComponentMutable(component, false);
2296
2376
  }
2297
2377
  this.pendingTools.clear();
2298
2378
  }
@@ -2302,6 +2382,7 @@ export class InteractiveMode {
2302
2382
  component.setArgsComplete();
2303
2383
  }
2304
2384
  }
2385
+ this.promoteFinalizedChatPrefix();
2305
2386
  this.streamingComponent = undefined;
2306
2387
  this.streamingMessage = undefined;
2307
2388
  this.footer.invalidate();
@@ -2309,6 +2390,7 @@ export class InteractiveMode {
2309
2390
  this.ui.requestRender();
2310
2391
  break;
2311
2392
  case "tool_execution_start": {
2393
+ this.sealStatus();
2312
2394
  let component = this.pendingTools.get(event.toolCallId);
2313
2395
  if (!component) {
2314
2396
  component = new ToolExecutionComponent(event.toolName, event.toolCallId, event.args, {
@@ -2317,6 +2399,7 @@ export class InteractiveMode {
2317
2399
  }, this.getRegisteredToolDefinition(event.toolName), this.ui, this.sessionManager.getCwd());
2318
2400
  component.setExpanded(this.toolOutputExpanded);
2319
2401
  this.chatContainer.addChild(component);
2402
+ this.setChatComponentMutable(component, true);
2320
2403
  this.pendingTools.set(event.toolCallId, component);
2321
2404
  }
2322
2405
  component.markExecutionStarted();
@@ -2335,12 +2418,34 @@ export class InteractiveMode {
2335
2418
  const component = this.pendingTools.get(event.toolCallId);
2336
2419
  if (component) {
2337
2420
  component.updateResult({ ...event.result, isError: event.isError });
2338
- this.pendingTools.delete(event.toolCallId);
2339
- this.ui.requestRender();
2421
+ const pendingToolFinalizations = this.pendingToolFinalizations;
2422
+ const finalization = component.waitForImageConversions().then(() => {
2423
+ component.seal();
2424
+ if (this.pendingTools.get(event.toolCallId) === component) {
2425
+ this.pendingTools.delete(event.toolCallId);
2426
+ }
2427
+ if (this.mutableChatComponents.has(component) && this.chatContainer.children.includes(component)) {
2428
+ this.setChatComponentMutable(component, false);
2429
+ this.promoteFinalizedChatPrefix();
2430
+ this.ui.requestRender();
2431
+ }
2432
+ });
2433
+ pendingToolFinalizations.add(finalization);
2434
+ try {
2435
+ await finalization;
2436
+ }
2437
+ finally {
2438
+ pendingToolFinalizations.delete(finalization);
2439
+ }
2340
2440
  }
2341
2441
  break;
2342
2442
  }
2343
- case "agent_end":
2443
+ case "agent_end": {
2444
+ const runGeneration = this.agentRunGeneration;
2445
+ const pendingToolFinalizations = this.pendingToolFinalizations;
2446
+ await Promise.all(pendingToolFinalizations);
2447
+ if (runGeneration !== this.agentRunGeneration || pendingToolFinalizations !== this.pendingToolFinalizations)
2448
+ break;
2344
2449
  if (this.settingsManager.getShowTerminalProgress()) {
2345
2450
  this.ui.terminal.setProgress(false);
2346
2451
  }
@@ -2351,13 +2456,20 @@ export class InteractiveMode {
2351
2456
  }
2352
2457
  if (this.streamingComponent) {
2353
2458
  this.chatContainer.removeChild(this.streamingComponent);
2459
+ this.setChatComponentMutable(this.streamingComponent, false);
2354
2460
  this.streamingComponent = undefined;
2355
2461
  this.streamingMessage = undefined;
2356
2462
  }
2463
+ for (const component of this.pendingTools.values()) {
2464
+ component.seal();
2465
+ this.setChatComponentMutable(component, false);
2466
+ }
2357
2467
  this.pendingTools.clear();
2468
+ this.promoteFinalizedChatPrefix();
2358
2469
  await this.checkShutdownRequested();
2359
2470
  this.ui.requestRender();
2360
2471
  break;
2472
+ }
2361
2473
  case "compaction_start": {
2362
2474
  if (this.settingsManager.getShowTerminalProgress()) {
2363
2475
  this.ui.terminal.setProgress(true);
@@ -2399,7 +2511,6 @@ export class InteractiveMode {
2399
2511
  }
2400
2512
  }
2401
2513
  else if (event.result) {
2402
- this.chatContainer.clear();
2403
2514
  this.rebuildChatFromMessages();
2404
2515
  this.addMessageToChat(createCompactionSummaryMessage(event.result.summary, event.result.tokensBefore, new Date().toISOString()));
2405
2516
  this.footer.invalidate();
@@ -2411,6 +2522,7 @@ export class InteractiveMode {
2411
2522
  else {
2412
2523
  this.chatContainer.addChild(new Spacer(1));
2413
2524
  this.chatContainer.addChild(new Text(theme.fg("error", event.errorMessage), 1, 0));
2525
+ this.commitFinalizedChatOutput();
2414
2526
  }
2415
2527
  }
2416
2528
  void this.flushCompactionQueue({ willRetry: event.willRetry });
@@ -2496,11 +2608,14 @@ export class InteractiveMode {
2496
2608
  const text = new Text(theme.fg("dim", message), 1, 0);
2497
2609
  this.chatContainer.addChild(spacer);
2498
2610
  this.chatContainer.addChild(text);
2611
+ this.setChatComponentMutable(spacer, true);
2612
+ this.setChatComponentMutable(text, true);
2499
2613
  this.lastStatusSpacer = spacer;
2500
2614
  this.lastStatusText = text;
2501
2615
  this.ui.requestRender();
2502
2616
  }
2503
2617
  addMessageToChat(message, options) {
2618
+ this.sealStatus();
2504
2619
  switch (message.role) {
2505
2620
  case "bashExecution": {
2506
2621
  const component = new BashExecutionComponent(message.command, this.ui, message.excludeFromContext);
@@ -2592,6 +2707,7 @@ export class InteractiveMode {
2592
2707
  const _exhaustive = message;
2593
2708
  }
2594
2709
  }
2710
+ this.promoteFinalizedChatPrefix();
2595
2711
  }
2596
2712
  /**
2597
2713
  * Render session context to chat. Used for initial load and rebuild after compaction.
@@ -2619,6 +2735,7 @@ export class InteractiveMode {
2619
2735
  }, this.getRegisteredToolDefinition(content.name), this.ui, this.sessionManager.getCwd());
2620
2736
  component.setExpanded(this.toolOutputExpanded);
2621
2737
  this.chatContainer.addChild(component);
2738
+ this.setChatComponentMutable(component, true);
2622
2739
  if (message.stopReason === "aborted" || message.stopReason === "error") {
2623
2740
  let errorMessage;
2624
2741
  if (message.stopReason === "aborted") {
@@ -2632,6 +2749,8 @@ export class InteractiveMode {
2632
2749
  errorMessage = message.errorMessage || "Error";
2633
2750
  }
2634
2751
  component.updateResult({ content: [{ type: "text", text: errorMessage }], isError: true });
2752
+ component.seal();
2753
+ this.setChatComponentMutable(component, false);
2635
2754
  }
2636
2755
  else {
2637
2756
  renderedPendingTools.set(content.id, component);
@@ -2647,6 +2766,26 @@ export class InteractiveMode {
2647
2766
  component.setArgsComplete();
2648
2767
  component.updateResult(message);
2649
2768
  renderedPendingTools.delete(message.toolCallId);
2769
+ if (component.hasPendingImageConversions()) {
2770
+ const pendingToolFinalizations = this.pendingToolFinalizations;
2771
+ const finalization = component.waitForImageConversions().then(() => {
2772
+ if (!this.mutableChatComponents.has(component) ||
2773
+ !this.chatContainer.children.includes(component)) {
2774
+ component.seal();
2775
+ return;
2776
+ }
2777
+ component.seal();
2778
+ this.setChatComponentMutable(component, false);
2779
+ this.promoteFinalizedChatPrefix();
2780
+ this.ui.requestRender();
2781
+ });
2782
+ pendingToolFinalizations.add(finalization);
2783
+ void finalization.finally(() => pendingToolFinalizations.delete(finalization));
2784
+ }
2785
+ else {
2786
+ component.seal();
2787
+ this.setChatComponentMutable(component, false);
2788
+ }
2650
2789
  }
2651
2790
  }
2652
2791
  else {
@@ -2657,6 +2796,7 @@ export class InteractiveMode {
2657
2796
  for (const [toolCallId, component] of renderedPendingTools) {
2658
2797
  this.pendingTools.set(toolCallId, component);
2659
2798
  }
2799
+ this.promoteFinalizedChatPrefix();
2660
2800
  this.ui.requestRender();
2661
2801
  }
2662
2802
  renderInitialMessages() {
@@ -2683,7 +2823,7 @@ export class InteractiveMode {
2683
2823
  });
2684
2824
  }
2685
2825
  rebuildChatFromMessages() {
2686
- this.chatContainer.clear();
2826
+ this.clearTranscript();
2687
2827
  const context = this.sessionManager.buildSessionContext();
2688
2828
  this.renderSessionContext(context);
2689
2829
  }
@@ -2828,7 +2968,7 @@ export class InteractiveMode {
2828
2968
  clearInterval(suspendKeepAlive);
2829
2969
  process.removeListener("SIGINT", ignoreSigint);
2830
2970
  this.ui.start();
2831
- this.ui.requestRender(true);
2971
+ this.ui.rebuild();
2832
2972
  });
2833
2973
  try {
2834
2974
  // Stop the TUI (restore terminal to normal mode)
@@ -2931,24 +3071,24 @@ export class InteractiveMode {
2931
3071
  if (isExpandable(activeHeader)) {
2932
3072
  activeHeader.setExpanded(expanded);
2933
3073
  }
2934
- for (const child of this.chatContainer.children) {
3074
+ for (const child of [...this.committedChatContainer.children, ...this.chatContainer.children]) {
2935
3075
  if (isExpandable(child)) {
2936
3076
  child.setExpanded(expanded);
2937
3077
  }
2938
3078
  }
2939
- this.ui.requestRender();
3079
+ this.ui.rebuild();
2940
3080
  }
2941
3081
  toggleThinkingBlockVisibility() {
2942
3082
  this.hideThinkingBlock = !this.hideThinkingBlock;
2943
3083
  this.settingsManager.setHideThinkingBlock(this.hideThinkingBlock);
2944
3084
  // Rebuild chat from session messages
2945
- this.chatContainer.clear();
2946
3085
  this.rebuildChatFromMessages();
2947
3086
  // If streaming, re-add the streaming component with updated visibility and re-render
2948
3087
  if (this.streamingComponent && this.streamingMessage) {
2949
3088
  this.streamingComponent.setHideThinkingBlock(this.hideThinkingBlock);
2950
3089
  this.streamingComponent.updateContent(this.streamingMessage);
2951
3090
  this.chatContainer.addChild(this.streamingComponent);
3091
+ this.setChatComponentMutable(this.streamingComponent, true);
2952
3092
  }
2953
3093
  this.showStatus(`Thinking blocks: ${this.hideThinkingBlock ? "hidden" : "visible"}`);
2954
3094
  }
@@ -2990,8 +3130,7 @@ export class InteractiveMode {
2990
3130
  }
2991
3131
  // Restart TUI
2992
3132
  this.ui.start();
2993
- // Force full re-render since external editor uses alternate screen
2994
- this.ui.requestRender(true);
3133
+ this.ui.rebuild();
2995
3134
  }
2996
3135
  }
2997
3136
  // =========================================================================
@@ -3002,14 +3141,18 @@ export class InteractiveMode {
3002
3141
  this.ui.requestRender();
3003
3142
  }
3004
3143
  showError(errorMessage) {
3144
+ this.sealStatus();
3005
3145
  this.chatContainer.addChild(new Spacer(1));
3006
3146
  this.chatContainer.addChild(new Text(theme.fg("error", `Error: ${errorMessage}`), 1, 0));
3007
3147
  this.chatContainer.addChild(new Spacer(1));
3148
+ this.promoteFinalizedChatPrefix();
3008
3149
  this.ui.requestRender();
3009
3150
  }
3010
3151
  showWarning(warningMessage) {
3152
+ this.sealStatus();
3011
3153
  this.chatContainer.addChild(new Spacer(1));
3012
3154
  this.chatContainer.addChild(new Text(theme.fg("warning", `Warning: ${warningMessage}`), 1, 0));
3155
+ this.promoteFinalizedChatPrefix();
3013
3156
  this.ui.requestRender();
3014
3157
  }
3015
3158
  // SCRAMJET-DIVERGENCE: showNewVersionNotification removed (version check gutted).
@@ -3021,7 +3164,7 @@ export class InteractiveMode {
3021
3164
  this.chatContainer.addChild(new DynamicBorder((text) => theme.fg("warning", text)));
3022
3165
  this.chatContainer.addChild(new Text(`${theme.bold(theme.fg("warning", "Package Updates Available"))}\n${updateInstruction}\n${theme.fg("muted", "Packages:")}\n${packageLines}`, 1, 0));
3023
3166
  this.chatContainer.addChild(new DynamicBorder((text) => theme.fg("warning", text)));
3024
- this.ui.requestRender();
3167
+ this.commitFinalizedChatOutput();
3025
3168
  }
3026
3169
  /**
3027
3170
  * Get all queued messages (read-only).
@@ -3181,11 +3324,14 @@ export class InteractiveMode {
3181
3324
  }
3182
3325
  /** Move pending bash components from pending area to chat */
3183
3326
  flushPendingBashComponents() {
3327
+ if (this.pendingBashComponents.length > 0)
3328
+ this.sealStatus();
3184
3329
  for (const component of this.pendingBashComponents) {
3185
3330
  this.pendingMessagesContainer.removeChild(component);
3186
3331
  this.chatContainer.addChild(component);
3187
3332
  }
3188
3333
  this.pendingBashComponents = [];
3334
+ this.promoteFinalizedChatPrefix();
3189
3335
  }
3190
3336
  // =========================================================================
3191
3337
  // Selectors
@@ -3232,7 +3378,6 @@ export class InteractiveMode {
3232
3378
  editorPaddingX: this.settingsManager.getEditorPaddingX(),
3233
3379
  autocompleteMaxVisible: this.settingsManager.getAutocompleteMaxVisible(),
3234
3380
  quietStartup: this.settingsManager.getQuietStartup(),
3235
- clearOnShrink: this.settingsManager.getClearOnShrink(),
3236
3381
  showTerminalProgress: this.settingsManager.getShowTerminalProgress(),
3237
3382
  warnings: this.settingsManager.getWarnings(),
3238
3383
  }, {
@@ -3242,19 +3387,11 @@ export class InteractiveMode {
3242
3387
  },
3243
3388
  onShowImagesChange: (enabled) => {
3244
3389
  this.settingsManager.setShowImages(enabled);
3245
- for (const child of this.chatContainer.children) {
3246
- if (child instanceof ToolExecutionComponent) {
3247
- child.setShowImages(enabled);
3248
- }
3249
- }
3390
+ this.updateToolImagePresentation((component) => component.setShowImages(enabled));
3250
3391
  },
3251
3392
  onImageWidthCellsChange: (width) => {
3252
3393
  this.settingsManager.setImageWidthCells(width);
3253
- for (const child of this.chatContainer.children) {
3254
- if (child instanceof ToolExecutionComponent) {
3255
- child.setImageWidthCells(width);
3256
- }
3257
- }
3394
+ this.updateToolImagePresentation((component) => component.setImageWidthCells(width));
3258
3395
  },
3259
3396
  onAutoResizeImagesChange: (enabled) => {
3260
3397
  this.settingsManager.setImageAutoResize(enabled);
@@ -3300,12 +3437,11 @@ export class InteractiveMode {
3300
3437
  onHideThinkingBlockChange: (hidden) => {
3301
3438
  this.hideThinkingBlock = hidden;
3302
3439
  this.settingsManager.setHideThinkingBlock(hidden);
3303
- for (const child of this.chatContainer.children) {
3440
+ for (const child of [...this.committedChatContainer.children, ...this.chatContainer.children]) {
3304
3441
  if (child instanceof AssistantMessageComponent) {
3305
3442
  child.setHideThinkingBlock(hidden);
3306
3443
  }
3307
3444
  }
3308
- this.chatContainer.clear();
3309
3445
  this.rebuildChatFromMessages();
3310
3446
  },
3311
3447
  onCollapseChangelogChange: (collapsed) => {
@@ -3341,10 +3477,6 @@ export class InteractiveMode {
3341
3477
  this.editor.setAutocompleteMaxVisible(maxVisible);
3342
3478
  }
3343
3479
  },
3344
- onClearOnShrinkChange: (enabled) => {
3345
- this.settingsManager.setClearOnShrink(enabled);
3346
- this.ui.setClearOnShrink(enabled);
3347
- },
3348
3480
  onShowTerminalProgressChange: (enabled) => {
3349
3481
  this.settingsManager.setShowTerminalProgress(enabled);
3350
3482
  },
@@ -3646,7 +3778,7 @@ export class InteractiveMode {
3646
3778
  return;
3647
3779
  }
3648
3780
  // Update UI
3649
- this.chatContainer.clear();
3781
+ this.clearTranscript();
3650
3782
  this.renderInitialMessages();
3651
3783
  if (result.editorText && !this.editor.getText().trim()) {
3652
3784
  this.editor.setText(result.editorText);
@@ -4081,7 +4213,7 @@ export class InteractiveMode {
4081
4213
  this.editorContainer.clear();
4082
4214
  this.editorContainer.addChild(reloadBox);
4083
4215
  this.ui.setFocus(reloadBox);
4084
- this.ui.requestRender(true);
4216
+ this.ui.rebuild();
4085
4217
  await new Promise((resolve) => process.nextTick(resolve));
4086
4218
  const dismissReloadBox = (editor) => {
4087
4219
  this.editorContainer.clear();
@@ -4111,7 +4243,6 @@ export class InteractiveMode {
4111
4243
  this.editor.setAutocompleteMaxVisible?.(autocompleteMaxVisible);
4112
4244
  }
4113
4245
  this.ui.setShowHardwareCursor(this.settingsManager.getShowHardwareCursor());
4114
- this.ui.setClearOnShrink(this.settingsManager.getClearOnShrink());
4115
4246
  this.setupAutocompleteProvider();
4116
4247
  const runner = this.session.extensionRunner;
4117
4248
  this.setupExtensionShortcuts(runner);
@@ -4247,13 +4378,13 @@ export class InteractiveMode {
4247
4378
  else {
4248
4379
  this.showWarning("Usage: /name <name>");
4249
4380
  }
4250
- this.ui.requestRender();
4381
+ this.commitFinalizedChatOutput();
4251
4382
  return;
4252
4383
  }
4253
4384
  this.session.setSessionName(name);
4254
4385
  this.chatContainer.addChild(new Spacer(1));
4255
4386
  this.chatContainer.addChild(new Text(theme.fg("dim", `Session name set: ${name}`), 1, 0));
4256
- this.ui.requestRender();
4387
+ this.commitFinalizedChatOutput();
4257
4388
  }
4258
4389
  handleSessionCommand() {
4259
4390
  const stats = this.session.getSessionStats();
@@ -4286,7 +4417,7 @@ export class InteractiveMode {
4286
4417
  }
4287
4418
  this.chatContainer.addChild(new Spacer(1));
4288
4419
  this.chatContainer.addChild(new Text(info, 1, 0));
4289
- this.ui.requestRender();
4420
+ this.commitFinalizedChatOutput();
4290
4421
  }
4291
4422
  handleChangelogCommand() {
4292
4423
  const changelogPath = getChangelogPath();
@@ -4303,7 +4434,7 @@ export class InteractiveMode {
4303
4434
  this.chatContainer.addChild(new Spacer(1));
4304
4435
  this.chatContainer.addChild(new Markdown(changelogMarkdown, 1, 1, this.getMarkdownThemeWithSettings()));
4305
4436
  this.chatContainer.addChild(new DynamicBorder());
4306
- this.ui.requestRender();
4437
+ this.commitFinalizedChatOutput();
4307
4438
  }
4308
4439
  /**
4309
4440
  * Get capitalized display string for an app keybinding action.
@@ -4424,7 +4555,7 @@ export class InteractiveMode {
4424
4555
  this.chatContainer.addChild(new Spacer(1));
4425
4556
  this.chatContainer.addChild(new Markdown(hotkeys.trim(), 1, 1, this.getMarkdownThemeWithSettings()));
4426
4557
  this.chatContainer.addChild(new DynamicBorder());
4427
- this.ui.requestRender();
4558
+ this.commitFinalizedChatOutput();
4428
4559
  }
4429
4560
  async handleClearCommand() {
4430
4561
  if (this.loadingAnimation) {
@@ -4440,7 +4571,7 @@ export class InteractiveMode {
4440
4571
  this.renderCurrentSessionState();
4441
4572
  this.chatContainer.addChild(new Spacer(1));
4442
4573
  this.chatContainer.addChild(new Text(`${theme.fg("accent", "✓ New session started")}`, 1, 1));
4443
- this.ui.requestRender();
4574
+ this.commitFinalizedChatOutput();
4444
4575
  }
4445
4576
  catch (error) {
4446
4577
  await this.handleFatalRuntimeError("Failed to create session", error);
@@ -4471,22 +4602,38 @@ export class InteractiveMode {
4471
4602
  fs.writeFileSync(debugLogPath, debugData);
4472
4603
  this.chatContainer.addChild(new Spacer(1));
4473
4604
  this.chatContainer.addChild(new Text(`${theme.fg("accent", "✓ Debug log written")}\n${theme.fg("muted", debugLogPath)}`, 1, 1));
4474
- this.ui.requestRender();
4605
+ this.commitFinalizedChatOutput();
4475
4606
  }
4476
4607
  handleArminSaysHi() {
4608
+ const component = new ArminComponent(this.ui, () => {
4609
+ if (!this.mutableChatComponents.has(component) || !this.chatContainer.children.includes(component))
4610
+ return;
4611
+ this.setChatComponentMutable(component, false);
4612
+ this.promoteFinalizedChatPrefix();
4613
+ this.ui.requestRender();
4614
+ });
4477
4615
  this.chatContainer.addChild(new Spacer(1));
4478
- this.chatContainer.addChild(new ArminComponent(this.ui));
4479
- this.ui.requestRender();
4616
+ this.chatContainer.addChild(component);
4617
+ this.setChatComponentMutable(component, true);
4618
+ this.commitFinalizedChatOutput();
4480
4619
  }
4481
4620
  handleDementedDelves() {
4482
4621
  this.chatContainer.addChild(new Spacer(1));
4483
4622
  this.chatContainer.addChild(new EarendilAnnouncementComponent());
4484
- this.ui.requestRender();
4623
+ this.commitFinalizedChatOutput();
4485
4624
  }
4486
4625
  handleDaxnuts() {
4626
+ const component = new DaxnutsComponent(this.ui, () => {
4627
+ if (!this.mutableChatComponents.has(component) || !this.chatContainer.children.includes(component))
4628
+ return;
4629
+ this.setChatComponentMutable(component, false);
4630
+ this.promoteFinalizedChatPrefix();
4631
+ this.ui.requestRender();
4632
+ });
4487
4633
  this.chatContainer.addChild(new Spacer(1));
4488
- this.chatContainer.addChild(new DaxnutsComponent(this.ui));
4489
- this.ui.requestRender();
4634
+ this.chatContainer.addChild(component);
4635
+ this.setChatComponentMutable(component, true);
4636
+ this.commitFinalizedChatOutput();
4490
4637
  }
4491
4638
  checkDaxnutsEasterEgg(model) {
4492
4639
  if (model.provider === "opencode" && model.id.toLowerCase().includes("kimi-k2.5")) {
@@ -4512,6 +4659,7 @@ export class InteractiveMode {
4512
4659
  this.pendingBashComponents.push(this.bashComponent);
4513
4660
  }
4514
4661
  else {
4662
+ this.sealStatus();
4515
4663
  this.chatContainer.addChild(this.bashComponent);
4516
4664
  }
4517
4665
  // Show output and complete
@@ -4521,6 +4669,7 @@ export class InteractiveMode {
4521
4669
  this.bashComponent.setComplete(result.exitCode, result.cancelled, result.truncated ? { truncated: true, content: result.output } : undefined, result.fullOutputPath);
4522
4670
  // Record the result in session
4523
4671
  this.session.recordBashResult(command, result, { excludeFromContext });
4672
+ this.promoteFinalizedChatPrefix();
4524
4673
  this.bashComponent = undefined;
4525
4674
  this.ui.requestRender();
4526
4675
  return;
@@ -4528,6 +4677,7 @@ export class InteractiveMode {
4528
4677
  // Normal execution path (possibly with custom operations)
4529
4678
  const isDeferred = this.session.isStreaming;
4530
4679
  this.bashComponent = new BashExecutionComponent(command, this.ui, excludeFromContext);
4680
+ this.setChatComponentMutable(this.bashComponent, true);
4531
4681
  if (isDeferred) {
4532
4682
  // Show in pending area when agent is streaming
4533
4683
  this.pendingMessagesContainer.addChild(this.bashComponent);
@@ -4535,6 +4685,7 @@ export class InteractiveMode {
4535
4685
  }
4536
4686
  else {
4537
4687
  // Show in chat immediately when agent is idle
4688
+ this.sealStatus();
4538
4689
  this.chatContainer.addChild(this.bashComponent);
4539
4690
  }
4540
4691
  this.ui.requestRender();
@@ -4555,6 +4706,9 @@ export class InteractiveMode {
4555
4706
  }
4556
4707
  this.showError(`Bash command failed: ${error instanceof Error ? error.message : "Unknown error"}`);
4557
4708
  }
4709
+ if (this.bashComponent)
4710
+ this.setChatComponentMutable(this.bashComponent, false);
4711
+ this.promoteFinalizedChatPrefix();
4558
4712
  this.bashComponent = undefined;
4559
4713
  this.ui.requestRender();
4560
4714
  }