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

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 (65) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/dist/core/agent-session-runtime.d.ts.map +1 -1
  3. package/dist/core/agent-session-runtime.js +2 -10
  4. package/dist/core/agent-session-runtime.js.map +1 -1
  5. package/dist/core/agent-session.d.ts +1 -1
  6. package/dist/core/agent-session.d.ts.map +1 -1
  7. package/dist/core/agent-session.js +58 -24
  8. package/dist/core/agent-session.js.map +1 -1
  9. package/dist/core/extensions/index.d.ts +1 -1
  10. package/dist/core/extensions/index.d.ts.map +1 -1
  11. package/dist/core/extensions/index.js.map +1 -1
  12. package/dist/core/extensions/runner.d.ts.map +1 -1
  13. package/dist/core/extensions/runner.js +30 -4
  14. package/dist/core/extensions/runner.js.map +1 -1
  15. package/dist/core/extensions/types.d.ts +13 -1
  16. package/dist/core/extensions/types.d.ts.map +1 -1
  17. package/dist/core/extensions/types.js.map +1 -1
  18. package/dist/core/scramjet-command-parser.d.ts +4 -0
  19. package/dist/core/scramjet-command-parser.d.ts.map +1 -1
  20. package/dist/core/scramjet-command-parser.js +72 -1
  21. package/dist/core/scramjet-command-parser.js.map +1 -1
  22. package/dist/core/session-manager.d.ts +10 -3
  23. package/dist/core/session-manager.d.ts.map +1 -1
  24. package/dist/core/session-manager.js +36 -11
  25. package/dist/core/session-manager.js.map +1 -1
  26. package/dist/core/settings-manager.d.ts +0 -3
  27. package/dist/core/settings-manager.d.ts.map +1 -1
  28. package/dist/core/settings-manager.js +0 -15
  29. package/dist/core/settings-manager.js.map +1 -1
  30. package/dist/index.d.ts +1 -1
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js.map +1 -1
  33. package/dist/main.d.ts.map +1 -1
  34. package/dist/main.js +1 -1
  35. package/dist/main.js.map +1 -1
  36. package/dist/modes/interactive/components/armin.d.ts +4 -2
  37. package/dist/modes/interactive/components/armin.d.ts.map +1 -1
  38. package/dist/modes/interactive/components/armin.js +14 -8
  39. package/dist/modes/interactive/components/armin.js.map +1 -1
  40. package/dist/modes/interactive/components/assistant-message.d.ts +3 -1
  41. package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  42. package/dist/modes/interactive/components/assistant-message.js +8 -2
  43. package/dist/modes/interactive/components/assistant-message.js.map +1 -1
  44. package/dist/modes/interactive/components/daxnuts.d.ts +4 -1
  45. package/dist/modes/interactive/components/daxnuts.d.ts.map +1 -1
  46. package/dist/modes/interactive/components/daxnuts.js +14 -6
  47. package/dist/modes/interactive/components/daxnuts.js.map +1 -1
  48. package/dist/modes/interactive/components/extension-editor.d.ts.map +1 -1
  49. package/dist/modes/interactive/components/extension-editor.js +2 -2
  50. package/dist/modes/interactive/components/extension-editor.js.map +1 -1
  51. package/dist/modes/interactive/components/settings-selector.d.ts +0 -2
  52. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  53. package/dist/modes/interactive/components/settings-selector.js +1 -13
  54. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  55. package/dist/modes/interactive/components/tool-execution.d.ts +11 -0
  56. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  57. package/dist/modes/interactive/components/tool-execution.js +69 -18
  58. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  59. package/dist/modes/interactive/interactive-mode.d.ts +13 -0
  60. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  61. package/dist/modes/interactive/interactive-mode.js +228 -64
  62. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  63. package/docs/settings.md +0 -1
  64. package/docs/tui.md +12 -0
  65. package/package.json +2 -2
@@ -19,7 +19,7 @@ import { defaultModelPerProvider, findExactModelReferenceMatch, resolveModelScop
19
19
  import { DefaultPackageManager } from "../../core/package-manager.js";
20
20
  import { BUILT_IN_PROVIDER_DISPLAY_NAMES } from "../../core/provider-display-names.js";
21
21
  import { describeRuntimeError } from "../../core/resource-loader.js";
22
- import { parseScramjetCommandBlock } from "../../core/scramjet-command-parser.js"; // SCRAMJET-DIVERGENCE: scramjet-command block parsing (issue 82)
22
+ import { indexScramjetCommandStarts, parseScramjetCommandBlock, restoreScramjetCommandInvocation, } from "../../core/scramjet-command-parser.js"; // SCRAMJET-DIVERGENCE: scramjet-command restoration (issues 82, 414)
23
23
  import { formatMissingSessionCwdPrompt, MissingSessionCwdError } from "../../core/session-cwd.js";
24
24
  import { SessionManager } from "../../core/session-manager.js";
25
25
  import { BUILTIN_SLASH_COMMANDS } from "../../core/slash-commands.js";
@@ -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);
@@ -2551,9 +2666,10 @@ export class InteractiveMode {
2551
2666
  this.chatContainer.addChild(userComponent);
2552
2667
  }
2553
2668
  if (options?.populateHistory) {
2554
- const compactForm = scramjetBlock.userContext
2555
- ? `/${scramjetBlock.name} ${scramjetBlock.userContext}`
2556
- : `/${scramjetBlock.name}`;
2669
+ const compactForm = options.historyText ??
2670
+ (scramjetBlock.userContext
2671
+ ? `/${scramjetBlock.name} ${scramjetBlock.userContext}`
2672
+ : `/${scramjetBlock.name}`);
2557
2673
  this.editor.addToHistory?.(compactForm);
2558
2674
  }
2559
2675
  }
@@ -2592,6 +2708,7 @@ export class InteractiveMode {
2592
2708
  const _exhaustive = message;
2593
2709
  }
2594
2710
  }
2711
+ this.promoteFinalizedChatPrefix();
2595
2712
  }
2596
2713
  /**
2597
2714
  * Render session context to chat. Used for initial load and rebuild after compaction.
@@ -2602,6 +2719,11 @@ export class InteractiveMode {
2602
2719
  renderSessionContext(sessionContext, options = {}) {
2603
2720
  this.pendingTools.clear();
2604
2721
  const renderedPendingTools = new Map();
2722
+ const entries = options.populateHistory ? this.sessionManager.getEntries() : [];
2723
+ const commandStarts = indexScramjetCommandStarts(entries);
2724
+ const entriesByMessage = new Map(entries
2725
+ .filter((entry) => entry.type === "message")
2726
+ .map((entry) => [entry.message, entry]));
2605
2727
  if (options.updateFooter) {
2606
2728
  this.footer.invalidate();
2607
2729
  this.updateEditorBorderColor();
@@ -2619,6 +2741,7 @@ export class InteractiveMode {
2619
2741
  }, this.getRegisteredToolDefinition(content.name), this.ui, this.sessionManager.getCwd());
2620
2742
  component.setExpanded(this.toolOutputExpanded);
2621
2743
  this.chatContainer.addChild(component);
2744
+ this.setChatComponentMutable(component, true);
2622
2745
  if (message.stopReason === "aborted" || message.stopReason === "error") {
2623
2746
  let errorMessage;
2624
2747
  if (message.stopReason === "aborted") {
@@ -2632,6 +2755,8 @@ export class InteractiveMode {
2632
2755
  errorMessage = message.errorMessage || "Error";
2633
2756
  }
2634
2757
  component.updateResult({ content: [{ type: "text", text: errorMessage }], isError: true });
2758
+ component.seal();
2759
+ this.setChatComponentMutable(component, false);
2635
2760
  }
2636
2761
  else {
2637
2762
  renderedPendingTools.set(content.id, component);
@@ -2647,16 +2772,41 @@ export class InteractiveMode {
2647
2772
  component.setArgsComplete();
2648
2773
  component.updateResult(message);
2649
2774
  renderedPendingTools.delete(message.toolCallId);
2775
+ if (component.hasPendingImageConversions()) {
2776
+ const pendingToolFinalizations = this.pendingToolFinalizations;
2777
+ const finalization = component.waitForImageConversions().then(() => {
2778
+ if (!this.mutableChatComponents.has(component) ||
2779
+ !this.chatContainer.children.includes(component)) {
2780
+ component.seal();
2781
+ return;
2782
+ }
2783
+ component.seal();
2784
+ this.setChatComponentMutable(component, false);
2785
+ this.promoteFinalizedChatPrefix();
2786
+ this.ui.requestRender();
2787
+ });
2788
+ pendingToolFinalizations.add(finalization);
2789
+ void finalization.finally(() => pendingToolFinalizations.delete(finalization));
2790
+ }
2791
+ else {
2792
+ component.seal();
2793
+ this.setChatComponentMutable(component, false);
2794
+ }
2650
2795
  }
2651
2796
  }
2652
2797
  else {
2653
2798
  // All other messages use standard rendering
2654
- this.addMessageToChat(message, options);
2799
+ const entry = entriesByMessage.get(message);
2800
+ this.addMessageToChat(message, {
2801
+ ...options,
2802
+ historyText: entry ? restoreScramjetCommandInvocation(entry, commandStarts) : undefined,
2803
+ });
2655
2804
  }
2656
2805
  }
2657
2806
  for (const [toolCallId, component] of renderedPendingTools) {
2658
2807
  this.pendingTools.set(toolCallId, component);
2659
2808
  }
2809
+ this.promoteFinalizedChatPrefix();
2660
2810
  this.ui.requestRender();
2661
2811
  }
2662
2812
  renderInitialMessages() {
@@ -2683,7 +2833,7 @@ export class InteractiveMode {
2683
2833
  });
2684
2834
  }
2685
2835
  rebuildChatFromMessages() {
2686
- this.chatContainer.clear();
2836
+ this.clearTranscript();
2687
2837
  const context = this.sessionManager.buildSessionContext();
2688
2838
  this.renderSessionContext(context);
2689
2839
  }
@@ -2828,7 +2978,7 @@ export class InteractiveMode {
2828
2978
  clearInterval(suspendKeepAlive);
2829
2979
  process.removeListener("SIGINT", ignoreSigint);
2830
2980
  this.ui.start();
2831
- this.ui.requestRender(true);
2981
+ this.ui.rebuild();
2832
2982
  });
2833
2983
  try {
2834
2984
  // Stop the TUI (restore terminal to normal mode)
@@ -2931,24 +3081,24 @@ export class InteractiveMode {
2931
3081
  if (isExpandable(activeHeader)) {
2932
3082
  activeHeader.setExpanded(expanded);
2933
3083
  }
2934
- for (const child of this.chatContainer.children) {
3084
+ for (const child of [...this.committedChatContainer.children, ...this.chatContainer.children]) {
2935
3085
  if (isExpandable(child)) {
2936
3086
  child.setExpanded(expanded);
2937
3087
  }
2938
3088
  }
2939
- this.ui.requestRender();
3089
+ this.ui.rebuild();
2940
3090
  }
2941
3091
  toggleThinkingBlockVisibility() {
2942
3092
  this.hideThinkingBlock = !this.hideThinkingBlock;
2943
3093
  this.settingsManager.setHideThinkingBlock(this.hideThinkingBlock);
2944
3094
  // Rebuild chat from session messages
2945
- this.chatContainer.clear();
2946
3095
  this.rebuildChatFromMessages();
2947
3096
  // If streaming, re-add the streaming component with updated visibility and re-render
2948
3097
  if (this.streamingComponent && this.streamingMessage) {
2949
3098
  this.streamingComponent.setHideThinkingBlock(this.hideThinkingBlock);
2950
3099
  this.streamingComponent.updateContent(this.streamingMessage);
2951
3100
  this.chatContainer.addChild(this.streamingComponent);
3101
+ this.setChatComponentMutable(this.streamingComponent, true);
2952
3102
  }
2953
3103
  this.showStatus(`Thinking blocks: ${this.hideThinkingBlock ? "hidden" : "visible"}`);
2954
3104
  }
@@ -2990,8 +3140,7 @@ export class InteractiveMode {
2990
3140
  }
2991
3141
  // Restart TUI
2992
3142
  this.ui.start();
2993
- // Force full re-render since external editor uses alternate screen
2994
- this.ui.requestRender(true);
3143
+ this.ui.rebuild();
2995
3144
  }
2996
3145
  }
2997
3146
  // =========================================================================
@@ -3002,14 +3151,18 @@ export class InteractiveMode {
3002
3151
  this.ui.requestRender();
3003
3152
  }
3004
3153
  showError(errorMessage) {
3154
+ this.sealStatus();
3005
3155
  this.chatContainer.addChild(new Spacer(1));
3006
3156
  this.chatContainer.addChild(new Text(theme.fg("error", `Error: ${errorMessage}`), 1, 0));
3007
3157
  this.chatContainer.addChild(new Spacer(1));
3158
+ this.promoteFinalizedChatPrefix();
3008
3159
  this.ui.requestRender();
3009
3160
  }
3010
3161
  showWarning(warningMessage) {
3162
+ this.sealStatus();
3011
3163
  this.chatContainer.addChild(new Spacer(1));
3012
3164
  this.chatContainer.addChild(new Text(theme.fg("warning", `Warning: ${warningMessage}`), 1, 0));
3165
+ this.promoteFinalizedChatPrefix();
3013
3166
  this.ui.requestRender();
3014
3167
  }
3015
3168
  // SCRAMJET-DIVERGENCE: showNewVersionNotification removed (version check gutted).
@@ -3021,7 +3174,7 @@ export class InteractiveMode {
3021
3174
  this.chatContainer.addChild(new DynamicBorder((text) => theme.fg("warning", text)));
3022
3175
  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
3176
  this.chatContainer.addChild(new DynamicBorder((text) => theme.fg("warning", text)));
3024
- this.ui.requestRender();
3177
+ this.commitFinalizedChatOutput();
3025
3178
  }
3026
3179
  /**
3027
3180
  * Get all queued messages (read-only).
@@ -3181,11 +3334,14 @@ export class InteractiveMode {
3181
3334
  }
3182
3335
  /** Move pending bash components from pending area to chat */
3183
3336
  flushPendingBashComponents() {
3337
+ if (this.pendingBashComponents.length > 0)
3338
+ this.sealStatus();
3184
3339
  for (const component of this.pendingBashComponents) {
3185
3340
  this.pendingMessagesContainer.removeChild(component);
3186
3341
  this.chatContainer.addChild(component);
3187
3342
  }
3188
3343
  this.pendingBashComponents = [];
3344
+ this.promoteFinalizedChatPrefix();
3189
3345
  }
3190
3346
  // =========================================================================
3191
3347
  // Selectors
@@ -3232,7 +3388,6 @@ export class InteractiveMode {
3232
3388
  editorPaddingX: this.settingsManager.getEditorPaddingX(),
3233
3389
  autocompleteMaxVisible: this.settingsManager.getAutocompleteMaxVisible(),
3234
3390
  quietStartup: this.settingsManager.getQuietStartup(),
3235
- clearOnShrink: this.settingsManager.getClearOnShrink(),
3236
3391
  showTerminalProgress: this.settingsManager.getShowTerminalProgress(),
3237
3392
  warnings: this.settingsManager.getWarnings(),
3238
3393
  }, {
@@ -3242,19 +3397,11 @@ export class InteractiveMode {
3242
3397
  },
3243
3398
  onShowImagesChange: (enabled) => {
3244
3399
  this.settingsManager.setShowImages(enabled);
3245
- for (const child of this.chatContainer.children) {
3246
- if (child instanceof ToolExecutionComponent) {
3247
- child.setShowImages(enabled);
3248
- }
3249
- }
3400
+ this.updateToolImagePresentation((component) => component.setShowImages(enabled));
3250
3401
  },
3251
3402
  onImageWidthCellsChange: (width) => {
3252
3403
  this.settingsManager.setImageWidthCells(width);
3253
- for (const child of this.chatContainer.children) {
3254
- if (child instanceof ToolExecutionComponent) {
3255
- child.setImageWidthCells(width);
3256
- }
3257
- }
3404
+ this.updateToolImagePresentation((component) => component.setImageWidthCells(width));
3258
3405
  },
3259
3406
  onAutoResizeImagesChange: (enabled) => {
3260
3407
  this.settingsManager.setImageAutoResize(enabled);
@@ -3300,12 +3447,11 @@ export class InteractiveMode {
3300
3447
  onHideThinkingBlockChange: (hidden) => {
3301
3448
  this.hideThinkingBlock = hidden;
3302
3449
  this.settingsManager.setHideThinkingBlock(hidden);
3303
- for (const child of this.chatContainer.children) {
3450
+ for (const child of [...this.committedChatContainer.children, ...this.chatContainer.children]) {
3304
3451
  if (child instanceof AssistantMessageComponent) {
3305
3452
  child.setHideThinkingBlock(hidden);
3306
3453
  }
3307
3454
  }
3308
- this.chatContainer.clear();
3309
3455
  this.rebuildChatFromMessages();
3310
3456
  },
3311
3457
  onCollapseChangelogChange: (collapsed) => {
@@ -3341,10 +3487,6 @@ export class InteractiveMode {
3341
3487
  this.editor.setAutocompleteMaxVisible(maxVisible);
3342
3488
  }
3343
3489
  },
3344
- onClearOnShrinkChange: (enabled) => {
3345
- this.settingsManager.setClearOnShrink(enabled);
3346
- this.ui.setClearOnShrink(enabled);
3347
- },
3348
3490
  onShowTerminalProgressChange: (enabled) => {
3349
3491
  this.settingsManager.setShowTerminalProgress(enabled);
3350
3492
  },
@@ -3646,7 +3788,7 @@ export class InteractiveMode {
3646
3788
  return;
3647
3789
  }
3648
3790
  // Update UI
3649
- this.chatContainer.clear();
3791
+ this.clearTranscript();
3650
3792
  this.renderInitialMessages();
3651
3793
  if (result.editorText && !this.editor.getText().trim()) {
3652
3794
  this.editor.setText(result.editorText);
@@ -4081,7 +4223,7 @@ export class InteractiveMode {
4081
4223
  this.editorContainer.clear();
4082
4224
  this.editorContainer.addChild(reloadBox);
4083
4225
  this.ui.setFocus(reloadBox);
4084
- this.ui.requestRender(true);
4226
+ this.ui.rebuild();
4085
4227
  await new Promise((resolve) => process.nextTick(resolve));
4086
4228
  const dismissReloadBox = (editor) => {
4087
4229
  this.editorContainer.clear();
@@ -4111,7 +4253,6 @@ export class InteractiveMode {
4111
4253
  this.editor.setAutocompleteMaxVisible?.(autocompleteMaxVisible);
4112
4254
  }
4113
4255
  this.ui.setShowHardwareCursor(this.settingsManager.getShowHardwareCursor());
4114
- this.ui.setClearOnShrink(this.settingsManager.getClearOnShrink());
4115
4256
  this.setupAutocompleteProvider();
4116
4257
  const runner = this.session.extensionRunner;
4117
4258
  this.setupExtensionShortcuts(runner);
@@ -4247,13 +4388,13 @@ export class InteractiveMode {
4247
4388
  else {
4248
4389
  this.showWarning("Usage: /name <name>");
4249
4390
  }
4250
- this.ui.requestRender();
4391
+ this.commitFinalizedChatOutput();
4251
4392
  return;
4252
4393
  }
4253
4394
  this.session.setSessionName(name);
4254
4395
  this.chatContainer.addChild(new Spacer(1));
4255
4396
  this.chatContainer.addChild(new Text(theme.fg("dim", `Session name set: ${name}`), 1, 0));
4256
- this.ui.requestRender();
4397
+ this.commitFinalizedChatOutput();
4257
4398
  }
4258
4399
  handleSessionCommand() {
4259
4400
  const stats = this.session.getSessionStats();
@@ -4286,7 +4427,7 @@ export class InteractiveMode {
4286
4427
  }
4287
4428
  this.chatContainer.addChild(new Spacer(1));
4288
4429
  this.chatContainer.addChild(new Text(info, 1, 0));
4289
- this.ui.requestRender();
4430
+ this.commitFinalizedChatOutput();
4290
4431
  }
4291
4432
  handleChangelogCommand() {
4292
4433
  const changelogPath = getChangelogPath();
@@ -4303,7 +4444,7 @@ export class InteractiveMode {
4303
4444
  this.chatContainer.addChild(new Spacer(1));
4304
4445
  this.chatContainer.addChild(new Markdown(changelogMarkdown, 1, 1, this.getMarkdownThemeWithSettings()));
4305
4446
  this.chatContainer.addChild(new DynamicBorder());
4306
- this.ui.requestRender();
4447
+ this.commitFinalizedChatOutput();
4307
4448
  }
4308
4449
  /**
4309
4450
  * Get capitalized display string for an app keybinding action.
@@ -4424,7 +4565,7 @@ export class InteractiveMode {
4424
4565
  this.chatContainer.addChild(new Spacer(1));
4425
4566
  this.chatContainer.addChild(new Markdown(hotkeys.trim(), 1, 1, this.getMarkdownThemeWithSettings()));
4426
4567
  this.chatContainer.addChild(new DynamicBorder());
4427
- this.ui.requestRender();
4568
+ this.commitFinalizedChatOutput();
4428
4569
  }
4429
4570
  async handleClearCommand() {
4430
4571
  if (this.loadingAnimation) {
@@ -4440,7 +4581,7 @@ export class InteractiveMode {
4440
4581
  this.renderCurrentSessionState();
4441
4582
  this.chatContainer.addChild(new Spacer(1));
4442
4583
  this.chatContainer.addChild(new Text(`${theme.fg("accent", "✓ New session started")}`, 1, 1));
4443
- this.ui.requestRender();
4584
+ this.commitFinalizedChatOutput();
4444
4585
  }
4445
4586
  catch (error) {
4446
4587
  await this.handleFatalRuntimeError("Failed to create session", error);
@@ -4471,22 +4612,38 @@ export class InteractiveMode {
4471
4612
  fs.writeFileSync(debugLogPath, debugData);
4472
4613
  this.chatContainer.addChild(new Spacer(1));
4473
4614
  this.chatContainer.addChild(new Text(`${theme.fg("accent", "✓ Debug log written")}\n${theme.fg("muted", debugLogPath)}`, 1, 1));
4474
- this.ui.requestRender();
4615
+ this.commitFinalizedChatOutput();
4475
4616
  }
4476
4617
  handleArminSaysHi() {
4618
+ const component = new ArminComponent(this.ui, () => {
4619
+ if (!this.mutableChatComponents.has(component) || !this.chatContainer.children.includes(component))
4620
+ return;
4621
+ this.setChatComponentMutable(component, false);
4622
+ this.promoteFinalizedChatPrefix();
4623
+ this.ui.requestRender();
4624
+ });
4477
4625
  this.chatContainer.addChild(new Spacer(1));
4478
- this.chatContainer.addChild(new ArminComponent(this.ui));
4479
- this.ui.requestRender();
4626
+ this.chatContainer.addChild(component);
4627
+ this.setChatComponentMutable(component, true);
4628
+ this.commitFinalizedChatOutput();
4480
4629
  }
4481
4630
  handleDementedDelves() {
4482
4631
  this.chatContainer.addChild(new Spacer(1));
4483
4632
  this.chatContainer.addChild(new EarendilAnnouncementComponent());
4484
- this.ui.requestRender();
4633
+ this.commitFinalizedChatOutput();
4485
4634
  }
4486
4635
  handleDaxnuts() {
4636
+ const component = new DaxnutsComponent(this.ui, () => {
4637
+ if (!this.mutableChatComponents.has(component) || !this.chatContainer.children.includes(component))
4638
+ return;
4639
+ this.setChatComponentMutable(component, false);
4640
+ this.promoteFinalizedChatPrefix();
4641
+ this.ui.requestRender();
4642
+ });
4487
4643
  this.chatContainer.addChild(new Spacer(1));
4488
- this.chatContainer.addChild(new DaxnutsComponent(this.ui));
4489
- this.ui.requestRender();
4644
+ this.chatContainer.addChild(component);
4645
+ this.setChatComponentMutable(component, true);
4646
+ this.commitFinalizedChatOutput();
4490
4647
  }
4491
4648
  checkDaxnutsEasterEgg(model) {
4492
4649
  if (model.provider === "opencode" && model.id.toLowerCase().includes("kimi-k2.5")) {
@@ -4512,6 +4669,7 @@ export class InteractiveMode {
4512
4669
  this.pendingBashComponents.push(this.bashComponent);
4513
4670
  }
4514
4671
  else {
4672
+ this.sealStatus();
4515
4673
  this.chatContainer.addChild(this.bashComponent);
4516
4674
  }
4517
4675
  // Show output and complete
@@ -4521,6 +4679,7 @@ export class InteractiveMode {
4521
4679
  this.bashComponent.setComplete(result.exitCode, result.cancelled, result.truncated ? { truncated: true, content: result.output } : undefined, result.fullOutputPath);
4522
4680
  // Record the result in session
4523
4681
  this.session.recordBashResult(command, result, { excludeFromContext });
4682
+ this.promoteFinalizedChatPrefix();
4524
4683
  this.bashComponent = undefined;
4525
4684
  this.ui.requestRender();
4526
4685
  return;
@@ -4528,6 +4687,7 @@ export class InteractiveMode {
4528
4687
  // Normal execution path (possibly with custom operations)
4529
4688
  const isDeferred = this.session.isStreaming;
4530
4689
  this.bashComponent = new BashExecutionComponent(command, this.ui, excludeFromContext);
4690
+ this.setChatComponentMutable(this.bashComponent, true);
4531
4691
  if (isDeferred) {
4532
4692
  // Show in pending area when agent is streaming
4533
4693
  this.pendingMessagesContainer.addChild(this.bashComponent);
@@ -4535,6 +4695,7 @@ export class InteractiveMode {
4535
4695
  }
4536
4696
  else {
4537
4697
  // Show in chat immediately when agent is idle
4698
+ this.sealStatus();
4538
4699
  this.chatContainer.addChild(this.bashComponent);
4539
4700
  }
4540
4701
  this.ui.requestRender();
@@ -4555,6 +4716,9 @@ export class InteractiveMode {
4555
4716
  }
4556
4717
  this.showError(`Bash command failed: ${error instanceof Error ? error.message : "Unknown error"}`);
4557
4718
  }
4719
+ if (this.bashComponent)
4720
+ this.setChatComponentMutable(this.bashComponent, false);
4721
+ this.promoteFinalizedChatPrefix();
4558
4722
  this.bashComponent = undefined;
4559
4723
  this.ui.requestRender();
4560
4724
  }