@apholdings/jensen-code 0.0.3 → 0.0.4

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 (116) hide show
  1. package/dist/core/agent-session.d.ts +1 -0
  2. package/dist/core/agent-session.d.ts.map +1 -1
  3. package/dist/core/agent-session.js +15 -0
  4. package/dist/core/agent-session.js.map +1 -1
  5. package/dist/core/extensions/loader.d.ts.map +1 -1
  6. package/dist/core/extensions/loader.js +1 -1
  7. package/dist/core/extensions/loader.js.map +1 -1
  8. package/dist/core/footer-data-provider.d.ts +4 -1
  9. package/dist/core/footer-data-provider.d.ts.map +1 -1
  10. package/dist/core/footer-data-provider.js +25 -11
  11. package/dist/core/footer-data-provider.js.map +1 -1
  12. package/dist/modes/interactive/components/assistant-message.d.ts +6 -1
  13. package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  14. package/dist/modes/interactive/components/assistant-message.js +40 -10
  15. package/dist/modes/interactive/components/assistant-message.js.map +1 -1
  16. package/dist/modes/interactive/components/footer.d.ts +0 -2
  17. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  18. package/dist/modes/interactive/components/footer.js +8 -146
  19. package/dist/modes/interactive/components/footer.js.map +1 -1
  20. package/dist/modes/interactive/components/header.d.ts +9 -3
  21. package/dist/modes/interactive/components/header.d.ts.map +1 -1
  22. package/dist/modes/interactive/components/header.js +125 -196
  23. package/dist/modes/interactive/components/header.js.map +1 -1
  24. package/dist/modes/interactive/components/top-bar.d.ts.map +1 -1
  25. package/dist/modes/interactive/components/top-bar.js +1 -1
  26. package/dist/modes/interactive/components/top-bar.js.map +1 -1
  27. package/dist/modes/interactive/components/user-message.d.ts.map +1 -1
  28. package/dist/modes/interactive/components/user-message.js +1 -1
  29. package/dist/modes/interactive/components/user-message.js.map +1 -1
  30. package/dist/modes/interactive/interactive-mode.d.ts +17 -1
  31. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  32. package/dist/modes/interactive/interactive-mode.js +507 -211
  33. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  34. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  35. package/dist/modes/interactive/theme/theme.js +2 -0
  36. package/dist/modes/interactive/theme/theme.js.map +1 -1
  37. package/examples/extensions/antigravity-image-gen.ts +0 -1
  38. package/examples/extensions/auto-commit-on-exit.ts +0 -1
  39. package/examples/extensions/bash-spawn-hook.ts +0 -1
  40. package/examples/extensions/bookmark.ts +0 -1
  41. package/examples/extensions/built-in-tool-renderer.ts +0 -1
  42. package/examples/extensions/claude-rules.ts +0 -1
  43. package/examples/extensions/commands.ts +0 -1
  44. package/examples/extensions/confirm-destructive.ts +0 -1
  45. package/examples/extensions/custom-compaction.ts +0 -1
  46. package/examples/extensions/custom-footer.ts +0 -1
  47. package/examples/extensions/custom-header.ts +0 -1
  48. package/examples/extensions/custom-provider-anthropic/index.ts +0 -1
  49. package/examples/extensions/custom-provider-gitlab-duo/index.ts +0 -1
  50. package/examples/extensions/custom-provider-qwen-cli/index.ts +0 -1
  51. package/examples/extensions/dirty-repo-guard.ts +0 -1
  52. package/examples/extensions/doom-overlay/index.ts +0 -1
  53. package/examples/extensions/dynamic-resources/index.ts +0 -1
  54. package/examples/extensions/dynamic-tools.ts +0 -1
  55. package/examples/extensions/event-bus.ts +0 -1
  56. package/examples/extensions/file-trigger.ts +0 -1
  57. package/examples/extensions/git-checkpoint.ts +0 -1
  58. package/examples/extensions/handoff.ts +0 -1
  59. package/examples/extensions/hello.ts +0 -1
  60. package/examples/extensions/inline-bash.ts +0 -1
  61. package/examples/extensions/input-transform.ts +0 -1
  62. package/examples/extensions/interactive-shell.ts +0 -1
  63. package/examples/extensions/mac-system-theme.ts +0 -1
  64. package/examples/extensions/message-renderer.ts +0 -1
  65. package/examples/extensions/minimal-mode.ts +0 -1
  66. package/examples/extensions/modal-editor.ts +0 -1
  67. package/examples/extensions/model-status.ts +0 -1
  68. package/examples/extensions/notify.ts +0 -1
  69. package/examples/extensions/overlay-qa-tests.ts +0 -1
  70. package/examples/extensions/overlay-test.ts +0 -1
  71. package/examples/extensions/permission-gate.ts +0 -1
  72. package/examples/extensions/pirate.ts +0 -1
  73. package/examples/extensions/plan-mode/index.ts +0 -1
  74. package/examples/extensions/preset.ts +0 -1
  75. package/examples/extensions/protected-paths.ts +0 -1
  76. package/examples/extensions/provider-payload.ts +0 -1
  77. package/examples/extensions/qna.ts +0 -1
  78. package/examples/extensions/question.ts +0 -1
  79. package/examples/extensions/questionnaire.ts +0 -1
  80. package/examples/extensions/rainbow-editor.ts +0 -1
  81. package/examples/extensions/reload-runtime.ts +0 -1
  82. package/examples/extensions/rpc-demo.ts +0 -1
  83. package/examples/extensions/sandbox/index.ts +0 -1
  84. package/examples/extensions/send-user-message.ts +0 -1
  85. package/examples/extensions/session-name.ts +0 -1
  86. package/examples/extensions/shutdown-command.ts +0 -1
  87. package/examples/extensions/snake.ts +0 -1
  88. package/examples/extensions/space-invaders.ts +0 -1
  89. package/examples/extensions/ssh.ts +0 -1
  90. package/examples/extensions/status-line.ts +0 -1
  91. package/examples/extensions/subagent/agents.ts +0 -1
  92. package/examples/extensions/subagent/index.ts +0 -1
  93. package/examples/extensions/summarize.ts +0 -1
  94. package/examples/extensions/system-prompt-header.ts +0 -1
  95. package/examples/extensions/timed-confirm.ts +0 -1
  96. package/examples/extensions/titlebar-spinner.ts +0 -1
  97. package/examples/extensions/todo.ts +0 -1
  98. package/examples/extensions/tool-override.ts +0 -1
  99. package/examples/extensions/tools.ts +0 -1
  100. package/examples/extensions/trigger-compact.ts +0 -1
  101. package/examples/extensions/truncated-tool.ts +0 -1
  102. package/examples/extensions/widget-placement.ts +0 -1
  103. package/examples/extensions/with-deps/index.ts +0 -1
  104. package/examples/sdk/01-minimal.ts +0 -1
  105. package/examples/sdk/02-custom-model.ts +0 -1
  106. package/examples/sdk/03-custom-prompt.ts +0 -1
  107. package/examples/sdk/04-skills.ts +0 -1
  108. package/examples/sdk/05-tools.ts +0 -1
  109. package/examples/sdk/06-extensions.ts +0 -1
  110. package/examples/sdk/07-context-files.ts +0 -1
  111. package/examples/sdk/08-prompt-templates.ts +0 -1
  112. package/examples/sdk/09-api-keys-and-oauth.ts +0 -1
  113. package/examples/sdk/10-settings.ts +0 -1
  114. package/examples/sdk/11-sessions.ts +0 -1
  115. package/examples/sdk/12-full-control.ts +0 -1
  116. package/package.json +4 -3
@@ -67,21 +67,27 @@ export class InteractiveMode {
67
67
  header;
68
68
  /** Container that normally hosts the prompt/editor, but may be replaced by selectors or extension UI. */
69
69
  editorContainer;
70
+ promptAreaComponent;
71
+ promptWidgetsVisible = true;
70
72
  footer;
71
73
  footerDataProvider;
72
74
  keybindings;
73
75
  version;
74
76
  isInitialized = false;
77
+ initializing = null;
75
78
  onInputCallback;
76
79
  loadingAnimation = undefined;
77
80
  pendingWorkingMessage = undefined;
78
81
  defaultWorkingMessage = "Working...";
82
+ startupUiGateActive = true;
83
+ deferredStartupNotices = [];
79
84
  lastSigintTime = 0;
80
85
  lastEscapeTime = 0;
81
86
  changelogMarkdown = undefined;
82
87
  // Status line tracking (for mutating immediately-sequential status updates)
83
88
  lastStatusSpacer = undefined;
84
89
  lastStatusText = undefined;
90
+ statusComponent = undefined;
85
91
  // Streaming message tracking
86
92
  streamingComponent = undefined;
87
93
  streamingMessage = undefined;
@@ -113,6 +119,9 @@ export class InteractiveMode {
113
119
  compactionQueuedMessages = [];
114
120
  // Shutdown state
115
121
  shutdownRequested = false;
122
+ // Epoch hardening
123
+ uiEpoch = 0;
124
+ sessionEpoch = 0;
116
125
  // Extension UI state
117
126
  extensionSelector = undefined;
118
127
  extensionInput = undefined;
@@ -163,7 +172,8 @@ export class InteractiveMode {
163
172
  getThinkingLevel: () => this.session.thinkingLevel || "off",
164
173
  keybindings: this.keybindings,
165
174
  });
166
- this.header = new Header();
175
+ this.footerDataProvider = new FooterDataProvider();
176
+ this.header = new Header(this.session, this.footerDataProvider);
167
177
  const editorPaddingX = this.settingsManager.getEditorPaddingX();
168
178
  const autocompleteMaxVisible = this.settingsManager.getAutocompleteMaxVisible();
169
179
  this.defaultEditor = new CustomEditor(this.ui, getEditorTheme(), this.keybindings, {
@@ -177,7 +187,7 @@ export class InteractiveMode {
177
187
  this.editor = this.defaultEditor;
178
188
  this.editorContainer = new Container();
179
189
  this.editorContainer.addChild(this.editor);
180
- this.footerDataProvider = new FooterDataProvider();
190
+ this.promptAreaComponent = this.editor;
181
191
  this.footer = new FooterComponent(session, this.footerDataProvider);
182
192
  this.footer.setAutoCompactEnabled(session.autoCompactionEnabled);
183
193
  // Load hide thinking block setting
@@ -343,10 +353,43 @@ export class InteractiveMode {
343
353
  this.chatContainer.addChild(helpContainer);
344
354
  this.ui.requestRender();
345
355
  }
346
- resetSessionUiState(renderInitialMessages = false) {
356
+ resetInteractiveSessionUI(renderInitialMessages = false) {
357
+ // Invalidate all stale async callbacks
358
+ this.uiEpoch++;
359
+ this.sessionEpoch++;
360
+ // Stop all loaders
361
+ if (this.loadingAnimation) {
362
+ this.loadingAnimation.dispose();
363
+ this.loadingAnimation = undefined;
364
+ }
365
+ if (this.autoCompactionLoader) {
366
+ this.autoCompactionLoader.dispose();
367
+ this.autoCompactionLoader = undefined;
368
+ }
369
+ if (this.retryLoader) {
370
+ this.retryLoader.dispose();
371
+ this.retryLoader = undefined;
372
+ }
373
+ // Clean up any transient prompt UI
374
+ if (this.extensionSelector) {
375
+ this.extensionSelector.dispose();
376
+ this.extensionSelector = undefined;
377
+ }
378
+ if (this.extensionInput) {
379
+ this.extensionInput.dispose();
380
+ this.extensionInput = undefined;
381
+ }
382
+ if (this.extensionEditor) {
383
+ this.extensionEditor = undefined;
384
+ }
385
+ this.ui.hideOverlay();
347
386
  this.clearChatContainer();
348
387
  this.pendingMessagesContainer.clear();
388
+ this.clearStatusOwner({ requestRender: false });
389
+ // Force empty the status container directly in case there are other elements left behind
349
390
  this.statusContainer.clear();
391
+ this.statusComponent = undefined;
392
+ this.restoreCanonicalEditor({ requestRender: false, text: "" });
350
393
  this.compactionQueuedMessages = [];
351
394
  this.streamingComponent = undefined;
352
395
  this.streamingMessage = undefined;
@@ -355,7 +398,8 @@ export class InteractiveMode {
355
398
  this.bashComponent = undefined;
356
399
  this.pendingWorkingMessage = undefined;
357
400
  this.isBashMode = false;
358
- this.editor.setText("");
401
+ // Ensure clean widget state
402
+ this.renderWidgets(false);
359
403
  this.updatePromptChrome();
360
404
  if (renderInitialMessages) {
361
405
  this.renderInitialMessages();
@@ -365,96 +409,108 @@ export class InteractiveMode {
365
409
  async init() {
366
410
  if (this.isInitialized)
367
411
  return;
368
- // Load changelog (only show new entries, skip for resumed sessions)
369
- this.changelogMarkdown = this.getChangelogForDisplay();
370
- // Ensure fd and rg are available (downloads if missing, adds to PATH via getBinDir)
371
- // Both are needed: fd for autocomplete, rg for grep tool and bash commands
372
- const [fdPath] = await Promise.all([ensureTool("fd"), ensureTool("rg")]);
373
- this.fdPath = fdPath;
374
- // Add header container as first child
375
- this.ui.addChild(this.headerContainer);
376
- // Always show the branded header/logo, even when quietStartup is enabled
377
- this.headerContainer.addChild(this.header);
378
- this.headerContainer.addChild(new Spacer(1));
379
- // Add startup instructions only when not silenced
380
- if (this.options.verbose || !this.settingsManager.getQuietStartup()) {
381
- const instructions = this.buildStartupInstructionsText();
382
- this.builtInHeader = new Text(instructions, 1, 0);
383
- this.headerContainer.addChild(this.builtInHeader);
384
- this.headerContainer.addChild(new Spacer(1));
385
- // Add changelog if provided
386
- if (this.changelogMarkdown) {
387
- this.headerContainer.addChild(new DynamicBorder());
388
- if (this.settingsManager.getCollapseChangelog()) {
389
- const versionMatch = this.changelogMarkdown.match(/##\s+\[?(\d+\.\d+\.\d+)\]?/);
390
- const latestVersion = versionMatch ? versionMatch[1] : this.version;
391
- const condensedText = `Updated to v${latestVersion}. Use ${theme.bold("/changelog")} to view full changelog.`;
392
- this.headerContainer.addChild(new Text(condensedText, 1, 0));
412
+ if (this.initializing)
413
+ return this.initializing;
414
+ this.initializing = (async () => {
415
+ try {
416
+ if (this.isInitialized)
417
+ return;
418
+ // Load changelog (only show new entries, skip for resumed sessions)
419
+ this.changelogMarkdown = this.getChangelogForDisplay();
420
+ // Ensure fd and rg are available (downloads if missing, adds to PATH via getBinDir)
421
+ // Both are needed: fd for autocomplete, rg for grep tool and bash commands
422
+ const [fdPath] = await Promise.all([ensureTool("fd"), ensureTool("rg")]);
423
+ this.fdPath = fdPath;
424
+ // Add header container as first child
425
+ this.ui.addChild(this.headerContainer);
426
+ // Always show the branded header/logo, even when quietStartup is enabled
427
+ this.headerContainer.addChild(this.header);
428
+ this.headerContainer.addChild(new Spacer(1));
429
+ // Add startup instructions only when not silenced
430
+ if (this.options.verbose || !this.settingsManager.getQuietStartup()) {
431
+ const instructions = this.buildStartupInstructionsText();
432
+ this.builtInHeader = new Text(instructions, 1, 0);
433
+ this.headerContainer.addChild(this.builtInHeader);
434
+ this.headerContainer.addChild(new Spacer(1));
435
+ // Add changelog if provided
436
+ if (this.changelogMarkdown) {
437
+ this.headerContainer.addChild(new DynamicBorder());
438
+ if (this.settingsManager.getCollapseChangelog()) {
439
+ const versionMatch = this.changelogMarkdown.match(/##\s+\[?(\d+\.\d+\.\d+)\]?/);
440
+ const latestVersion = versionMatch ? versionMatch[1] : this.version;
441
+ const condensedText = `Updated to v${latestVersion}. Use ${theme.bold("/changelog")} to view full changelog.`;
442
+ this.headerContainer.addChild(new Text(condensedText, 1, 0));
443
+ }
444
+ else {
445
+ this.headerContainer.addChild(new Text(theme.bold(theme.fg("accent", "What's New")), 1, 0));
446
+ this.headerContainer.addChild(new Spacer(1));
447
+ this.headerContainer.addChild(new Markdown(this.changelogMarkdown.trim(), 1, 0, this.getMarkdownThemeWithSettings()));
448
+ this.headerContainer.addChild(new Spacer(1));
449
+ }
450
+ this.headerContainer.addChild(new DynamicBorder());
451
+ }
393
452
  }
394
453
  else {
395
- this.headerContainer.addChild(new Text(theme.bold(theme.fg("accent", "What's New")), 1, 0));
396
- this.headerContainer.addChild(new Spacer(1));
397
- this.headerContainer.addChild(new Markdown(this.changelogMarkdown.trim(), 1, 0, this.getMarkdownThemeWithSettings()));
398
- this.headerContainer.addChild(new Spacer(1));
454
+ // Quiet startup: keep the logo visible, but suppress instruction text
455
+ this.builtInHeader = new Text("", 0, 0);
456
+ this.headerContainer.addChild(this.builtInHeader);
457
+ if (this.changelogMarkdown) {
458
+ this.headerContainer.addChild(new Spacer(1));
459
+ const versionMatch = this.changelogMarkdown.match(/##\s+\[?(\d+\.\d+\.\d+)\]?/);
460
+ const latestVersion = versionMatch ? versionMatch[1] : this.version;
461
+ const condensedText = `Updated to v${latestVersion}. Use ${theme.bold("/changelog")} to view full changelog.`;
462
+ this.headerContainer.addChild(new Text(condensedText, 1, 0));
463
+ }
399
464
  }
400
- this.headerContainer.addChild(new DynamicBorder());
465
+ this.ui.addChild(this.chatContainer);
466
+ this.ui.addChild(this.pendingMessagesContainer);
467
+ this.ui.addChild(this.statusContainer);
468
+ this.renderWidgets(); // Initialize with default spacer
469
+ // The prompt area consists of an optional widget strip, the editor, and a bottom widget strip.
470
+ // Extensions should use ctx.ui.setWidget(..., { placement: "aboveEditor" }) for the top strip.
471
+ this.ui.addChild(this.widgetContainerAbove);
472
+ this.ui.addChild(this.editorContainer);
473
+ this.ui.addChild(this.widgetContainerBelow);
474
+ this.ui.addChild(this.footer);
475
+ this.ui.setFocus(this.editor);
476
+ this.updatePromptChrome();
477
+ this.setupKeyHandlers();
478
+ this.setupEditorSubmitHandler();
479
+ // Initialize extensions first so resources are shown before messages
480
+ await this.initExtensions();
481
+ // Render initial messages AFTER showing loaded resources
482
+ this.renderInitialMessages();
483
+ // Start the UI
484
+ this.ui.start();
485
+ this.isInitialized = true;
486
+ // Set terminal title
487
+ this.updateTerminalTitle();
488
+ // Subscribe to agent events
489
+ this.subscribeToAgent();
490
+ this.windowFocusChangeUnsubscribe = this.ui.onWindowFocusChange(() => {
491
+ this.updatePromptChrome();
492
+ });
493
+ this.focusTargetChangeUnsubscribe = this.ui.onFocusTargetChange(() => {
494
+ this.updatePromptChrome();
495
+ });
496
+ // Set up theme file watcher
497
+ onThemeChange(() => {
498
+ this.ui.invalidate();
499
+ this.updatePromptChrome();
500
+ this.ui.requestRender();
501
+ });
502
+ // Set up git branch watcher (uses provider instead of footer)
503
+ this.footerDataProvider.onBranchChange(() => {
504
+ this.ui.requestRender();
505
+ });
506
+ // Initialize available provider count for footer display
507
+ await this.updateAvailableProviderCount();
401
508
  }
402
- }
403
- else {
404
- // Quiet startup: keep the logo visible, but suppress instruction text
405
- this.builtInHeader = new Text("", 0, 0);
406
- this.headerContainer.addChild(this.builtInHeader);
407
- if (this.changelogMarkdown) {
408
- this.headerContainer.addChild(new Spacer(1));
409
- const versionMatch = this.changelogMarkdown.match(/##\s+\[?(\d+\.\d+\.\d+)\]?/);
410
- const latestVersion = versionMatch ? versionMatch[1] : this.version;
411
- const condensedText = `Updated to v${latestVersion}. Use ${theme.bold("/changelog")} to view full changelog.`;
412
- this.headerContainer.addChild(new Text(condensedText, 1, 0));
413
- }
414
- }
415
- this.ui.addChild(this.chatContainer);
416
- this.ui.addChild(this.pendingMessagesContainer);
417
- this.ui.addChild(this.statusContainer);
418
- this.renderWidgets(); // Initialize with default spacer
419
- // The prompt area consists of an optional widget strip, the editor, and a bottom widget strip.
420
- // Extensions should use ctx.ui.setWidget(..., { placement: "aboveEditor" }) for the top strip.
421
- this.ui.addChild(this.widgetContainerAbove);
422
- this.ui.addChild(this.editorContainer);
423
- this.ui.addChild(this.widgetContainerBelow);
424
- this.ui.addChild(this.footer);
425
- this.ui.setFocus(this.editor);
426
- this.updatePromptChrome();
427
- this.setupKeyHandlers();
428
- this.setupEditorSubmitHandler();
429
- // Initialize extensions first so resources are shown before messages
430
- await this.initExtensions();
431
- // Render initial messages AFTER showing loaded resources
432
- this.renderInitialMessages();
433
- // Start the UI
434
- this.ui.start();
435
- this.isInitialized = true;
436
- // Set terminal title
437
- this.updateTerminalTitle();
438
- // Subscribe to agent events
439
- this.subscribeToAgent();
440
- this.windowFocusChangeUnsubscribe = this.ui.onWindowFocusChange(() => {
441
- this.updatePromptChrome();
442
- });
443
- this.focusTargetChangeUnsubscribe = this.ui.onFocusTargetChange(() => {
444
- this.updatePromptChrome();
445
- });
446
- // Set up theme file watcher
447
- onThemeChange(() => {
448
- this.ui.invalidate();
449
- this.updatePromptChrome();
450
- this.ui.requestRender();
451
- });
452
- // Set up git branch watcher (uses provider instead of footer)
453
- this.footerDataProvider.onBranchChange(() => {
454
- this.ui.requestRender();
455
- });
456
- // Initialize available provider count for footer display
457
- await this.updateAvailableProviderCount();
509
+ finally {
510
+ this.initializing = null;
511
+ }
512
+ })();
513
+ return this.initializing;
458
514
  }
459
515
  /**
460
516
  * Update terminal title with session name and cwd.
@@ -929,22 +985,23 @@ export class InteractiveMode {
929
985
  waitForIdle: () => this.session.agent.waitForIdle(),
930
986
  newSession: async (options) => {
931
987
  if (this.loadingAnimation) {
932
- this.loadingAnimation.stop();
988
+ this.loadingAnimation.dispose();
933
989
  this.loadingAnimation = undefined;
934
990
  }
935
- this.statusContainer.clear();
991
+ this.clearStatusOwner({ requestRender: false });
936
992
  // Delegate to AgentSession (handles setup + agent state sync)
937
993
  const success = await this.session.newSession(options);
938
994
  if (!success) {
939
995
  return { cancelled: true };
940
996
  }
941
- this.resetSessionUiState(true);
997
+ this.resetInteractiveSessionUI(true);
942
998
  this.ui.requestRender();
943
999
  return { cancelled: false };
944
1000
  },
945
1001
  fork: async (entryId) => {
1002
+ const capturedEpoch = this.sessionEpoch;
946
1003
  const result = await this.session.fork(entryId);
947
- if (result.cancelled) {
1004
+ if (result.cancelled || this.sessionEpoch !== capturedEpoch) {
948
1005
  return { cancelled: true };
949
1006
  }
950
1007
  this.clearChatContainer();
@@ -954,13 +1011,14 @@ export class InteractiveMode {
954
1011
  return { cancelled: false };
955
1012
  },
956
1013
  navigateTree: async (targetId, options) => {
1014
+ const capturedEpoch = this.sessionEpoch;
957
1015
  const result = await this.session.navigateTree(targetId, {
958
1016
  summarize: options?.summarize,
959
1017
  customInstructions: options?.customInstructions,
960
1018
  replaceInstructions: options?.replaceInstructions,
961
1019
  label: options?.label,
962
1020
  });
963
- if (result.cancelled) {
1021
+ if (result.cancelled || this.sessionEpoch !== capturedEpoch) {
964
1022
  return { cancelled: true };
965
1023
  }
966
1024
  this.clearChatContainer();
@@ -1140,17 +1198,133 @@ export class InteractiveMode {
1140
1198
  this.loadingAnimation.setMessage(`${this.defaultWorkingMessage} (${appKey(this.keybindings, "interrupt")} to interrupt)`);
1141
1199
  }
1142
1200
  }
1201
+ setPromptWidgetsVisible(visible) {
1202
+ if (this.promptWidgetsVisible === visible) {
1203
+ return;
1204
+ }
1205
+ this.promptWidgetsVisible = visible;
1206
+ if (visible) {
1207
+ this.renderWidgets(false);
1208
+ return;
1209
+ }
1210
+ this.widgetContainerAbove.clear();
1211
+ this.widgetContainerBelow.clear();
1212
+ }
1213
+ mountPromptOwner(component, options) {
1214
+ const focus = options?.focus ?? component;
1215
+ const showWidgets = options?.showWidgets ?? false;
1216
+ const requestRender = options?.requestRender ?? true;
1217
+ const isMounted = this.promptAreaComponent === component &&
1218
+ this.editorContainer.children.length === 1 &&
1219
+ this.editorContainer.children[0] === component;
1220
+ if (!isMounted) {
1221
+ this.editorContainer.clear();
1222
+ this.editorContainer.addChild(component);
1223
+ this.promptAreaComponent = component;
1224
+ }
1225
+ if (process.env.NODE_ENV !== "production") {
1226
+ if (this.editorContainer.children.length > 1) {
1227
+ throw new Error("Invariant violation: Multiple prompt owners mounted");
1228
+ }
1229
+ }
1230
+ this.setPromptWidgetsVisible(showWidgets);
1231
+ if (this.ui.getFocusedComponent() !== focus) {
1232
+ this.ui.setFocus(focus);
1233
+ }
1234
+ if (requestRender) {
1235
+ this.ui.requestRender();
1236
+ }
1237
+ }
1238
+ restoreCanonicalEditor(options) {
1239
+ if (options?.text !== undefined) {
1240
+ this.editor.setText(options.text);
1241
+ }
1242
+ const editorComponent = this.editor;
1243
+ const isMounted = this.promptAreaComponent === editorComponent &&
1244
+ this.editorContainer.children.length === 1 &&
1245
+ this.editorContainer.children[0] === editorComponent;
1246
+ if (!isMounted) {
1247
+ this.editorContainer.clear();
1248
+ this.editorContainer.addChild(editorComponent);
1249
+ this.promptAreaComponent = editorComponent;
1250
+ }
1251
+ if (process.env.NODE_ENV !== "production") {
1252
+ if (this.editorContainer.children.length > 1) {
1253
+ throw new Error("Invariant violation: Multiple prompt owners mounted");
1254
+ }
1255
+ if (this.promptAreaComponent !== editorComponent) {
1256
+ throw new Error("Invariant violation: restoreCanonicalEditor mounted non-canonical editor");
1257
+ }
1258
+ }
1259
+ this.setPromptWidgetsVisible(true);
1260
+ if (options?.focus ?? true) {
1261
+ this.ui.setFocus(editorComponent);
1262
+ }
1263
+ if (options?.requestRender ?? true) {
1264
+ this.ui.requestRender();
1265
+ }
1266
+ }
1267
+ mountStatusOwner(component, options) {
1268
+ const requestRender = options?.requestRender ?? true;
1269
+ const isMounted = this.statusComponent === component &&
1270
+ this.statusContainer.children.length === 1 &&
1271
+ this.statusContainer.children[0] === component;
1272
+ if (!isMounted) {
1273
+ this.statusContainer.clear();
1274
+ this.statusContainer.addChild(component);
1275
+ this.statusComponent = component;
1276
+ }
1277
+ if (process.env.NODE_ENV !== "production") {
1278
+ if (this.statusContainer.children.length > 1) {
1279
+ throw new Error("Invariant violation: Multiple status owners mounted");
1280
+ }
1281
+ }
1282
+ if (requestRender) {
1283
+ this.ui.requestRender();
1284
+ }
1285
+ }
1286
+ mountStatusLoader(loader, options) {
1287
+ const row = new Container();
1288
+ row.addChild(new Spacer(1));
1289
+ row.addChild(loader);
1290
+ this.mountStatusOwner(row, options);
1291
+ }
1292
+ clearStatusOwner(options) {
1293
+ const requestRender = options?.requestRender ?? true;
1294
+ if (this.statusComponent !== undefined || this.statusContainer.children.length > 0) {
1295
+ this.statusContainer.clear();
1296
+ this.statusComponent = undefined;
1297
+ }
1298
+ if (process.env.NODE_ENV !== "production") {
1299
+ if (this.statusContainer.children.length > 0) {
1300
+ throw new Error("Invariant violation: Status container not empty after clear");
1301
+ }
1302
+ }
1303
+ if (requestRender) {
1304
+ this.ui.requestRender();
1305
+ }
1306
+ }
1143
1307
  // Maximum total widget lines to prevent viewport overflow
1144
1308
  static MAX_WIDGET_LINES = 10;
1145
1309
  /**
1146
1310
  * Render all extension widgets to the widget container.
1147
1311
  */
1148
- renderWidgets() {
1312
+ renderWidgets(requestRender = true) {
1149
1313
  if (!this.widgetContainerAbove || !this.widgetContainerBelow)
1150
1314
  return;
1315
+ if (!this.promptWidgetsVisible) {
1316
+ this.widgetContainerAbove.clear();
1317
+ this.widgetContainerBelow.clear();
1318
+ if (requestRender) {
1319
+ this.ui.requestRender();
1320
+ }
1321
+ return;
1322
+ }
1151
1323
  this.renderWidgetContainer(this.widgetContainerAbove, this.extensionWidgetsAbove, true, true, this.topBar);
1152
1324
  this.renderWidgetContainer(this.widgetContainerBelow, this.extensionWidgetsBelow, false, false);
1153
- this.ui.requestRender();
1325
+ if (requestRender) {
1326
+ this.ui.requestRender();
1327
+ }
1154
1328
  }
1155
1329
  renderWidgetContainer(container, widgets, spacerWhenEmpty, leadingSpacer, builtInComponent) {
1156
1330
  container.clear();
@@ -1311,29 +1485,39 @@ export class InteractiveMode {
1311
1485
  * Show a selector for extensions.
1312
1486
  */
1313
1487
  showExtensionSelector(title, options, opts) {
1488
+ const capturedEpoch = this.uiEpoch;
1314
1489
  return new Promise((resolve) => {
1315
1490
  if (opts?.signal?.aborted) {
1316
1491
  resolve(undefined);
1317
1492
  return;
1318
1493
  }
1319
1494
  const onAbort = () => {
1495
+ if (this.uiEpoch !== capturedEpoch) {
1496
+ resolve(undefined);
1497
+ return;
1498
+ }
1320
1499
  this.hideExtensionSelector();
1321
1500
  resolve(undefined);
1322
1501
  };
1323
1502
  opts?.signal?.addEventListener("abort", onAbort, { once: true });
1324
1503
  this.extensionSelector = new ExtensionSelectorComponent(title, options, (option) => {
1325
1504
  opts?.signal?.removeEventListener("abort", onAbort);
1505
+ if (this.uiEpoch !== capturedEpoch) {
1506
+ resolve(undefined);
1507
+ return;
1508
+ }
1326
1509
  this.hideExtensionSelector();
1327
1510
  resolve(option);
1328
1511
  }, () => {
1329
1512
  opts?.signal?.removeEventListener("abort", onAbort);
1513
+ if (this.uiEpoch !== capturedEpoch) {
1514
+ resolve(undefined);
1515
+ return;
1516
+ }
1330
1517
  this.hideExtensionSelector();
1331
1518
  resolve(undefined);
1332
1519
  }, { tui: this.ui, timeout: opts?.timeout });
1333
- this.editorContainer.clear();
1334
- this.editorContainer.addChild(this.extensionSelector);
1335
- this.ui.setFocus(this.extensionSelector);
1336
- this.ui.requestRender();
1520
+ this.mountPromptOwner(this.extensionSelector);
1337
1521
  });
1338
1522
  }
1339
1523
  /**
@@ -1341,11 +1525,8 @@ export class InteractiveMode {
1341
1525
  */
1342
1526
  hideExtensionSelector() {
1343
1527
  this.extensionSelector?.dispose();
1344
- this.editorContainer.clear();
1345
- this.editorContainer.addChild(this.editor);
1346
1528
  this.extensionSelector = undefined;
1347
- this.ui.setFocus(this.editor);
1348
- this.ui.requestRender();
1529
+ this.restoreCanonicalEditor();
1349
1530
  }
1350
1531
  /**
1351
1532
  * Show a confirmation dialog for extensions.
@@ -1358,29 +1539,39 @@ export class InteractiveMode {
1358
1539
  * Show a text input for extensions.
1359
1540
  */
1360
1541
  showExtensionInput(title, placeholder, opts) {
1542
+ const capturedEpoch = this.uiEpoch;
1361
1543
  return new Promise((resolve) => {
1362
1544
  if (opts?.signal?.aborted) {
1363
1545
  resolve(undefined);
1364
1546
  return;
1365
1547
  }
1366
1548
  const onAbort = () => {
1549
+ if (this.uiEpoch !== capturedEpoch) {
1550
+ resolve(undefined);
1551
+ return;
1552
+ }
1367
1553
  this.hideExtensionInput();
1368
1554
  resolve(undefined);
1369
1555
  };
1370
1556
  opts?.signal?.addEventListener("abort", onAbort, { once: true });
1371
1557
  this.extensionInput = new ExtensionInputComponent(title, placeholder, (value) => {
1372
1558
  opts?.signal?.removeEventListener("abort", onAbort);
1559
+ if (this.uiEpoch !== capturedEpoch) {
1560
+ resolve(undefined);
1561
+ return;
1562
+ }
1373
1563
  this.hideExtensionInput();
1374
1564
  resolve(value);
1375
1565
  }, () => {
1376
1566
  opts?.signal?.removeEventListener("abort", onAbort);
1567
+ if (this.uiEpoch !== capturedEpoch) {
1568
+ resolve(undefined);
1569
+ return;
1570
+ }
1377
1571
  this.hideExtensionInput();
1378
1572
  resolve(undefined);
1379
1573
  }, { tui: this.ui, timeout: opts?.timeout });
1380
- this.editorContainer.clear();
1381
- this.editorContainer.addChild(this.extensionInput);
1382
- this.ui.setFocus(this.extensionInput);
1383
- this.ui.requestRender();
1574
+ this.mountPromptOwner(this.extensionInput);
1384
1575
  });
1385
1576
  }
1386
1577
  /**
@@ -1388,39 +1579,39 @@ export class InteractiveMode {
1388
1579
  */
1389
1580
  hideExtensionInput() {
1390
1581
  this.extensionInput?.dispose();
1391
- this.editorContainer.clear();
1392
- this.editorContainer.addChild(this.editor);
1393
1582
  this.extensionInput = undefined;
1394
- this.ui.setFocus(this.editor);
1395
- this.ui.requestRender();
1583
+ this.restoreCanonicalEditor();
1396
1584
  }
1397
1585
  /**
1398
1586
  * Show a multi-line editor for extensions (with Ctrl+G support).
1399
1587
  */
1400
1588
  showExtensionEditor(title, prefill) {
1589
+ const capturedEpoch = this.uiEpoch;
1401
1590
  return new Promise((resolve) => {
1402
1591
  this.extensionEditor = new ExtensionEditorComponent(this.ui, this.keybindings, title, prefill, (value) => {
1592
+ if (this.uiEpoch !== capturedEpoch) {
1593
+ resolve(undefined);
1594
+ return;
1595
+ }
1403
1596
  this.hideExtensionEditor();
1404
1597
  resolve(value);
1405
1598
  }, () => {
1599
+ if (this.uiEpoch !== capturedEpoch) {
1600
+ resolve(undefined);
1601
+ return;
1602
+ }
1406
1603
  this.hideExtensionEditor();
1407
1604
  resolve(undefined);
1408
1605
  });
1409
- this.editorContainer.clear();
1410
- this.editorContainer.addChild(this.extensionEditor);
1411
- this.ui.setFocus(this.extensionEditor);
1412
- this.ui.requestRender();
1606
+ this.mountPromptOwner(this.extensionEditor);
1413
1607
  });
1414
1608
  }
1415
1609
  /**
1416
1610
  * Hide the extension editor.
1417
1611
  */
1418
1612
  hideExtensionEditor() {
1419
- this.editorContainer.clear();
1420
- this.editorContainer.addChild(this.editor);
1421
1613
  this.extensionEditor = undefined;
1422
- this.ui.setFocus(this.editor);
1423
- this.ui.requestRender();
1614
+ this.restoreCanonicalEditor();
1424
1615
  }
1425
1616
  /**
1426
1617
  * Set a custom editor component from an extension.
@@ -1429,7 +1620,6 @@ export class InteractiveMode {
1429
1620
  setCustomEditorComponent(factory) {
1430
1621
  // Save text from current editor before switching
1431
1622
  const currentText = this.editor.getText();
1432
- this.editorContainer.clear();
1433
1623
  if (factory) {
1434
1624
  // Create the custom editor with tui, theme, and keybindings
1435
1625
  const newEditor = factory(this.ui, getEditorTheme(), this.keybindings);
@@ -1473,8 +1663,7 @@ export class InteractiveMode {
1473
1663
  this.defaultEditor.setText(currentText);
1474
1664
  this.editor = this.defaultEditor;
1475
1665
  }
1476
- this.editorContainer.addChild(this.editor);
1477
- this.ui.setFocus(this.editor);
1666
+ this.restoreCanonicalEditor({ requestRender: false });
1478
1667
  this.updatePromptChrome();
1479
1668
  }
1480
1669
  /**
@@ -1493,14 +1682,11 @@ export class InteractiveMode {
1493
1682
  }
1494
1683
  /** Show a custom component with keyboard focus. Overlay mode renders on top of existing content. */
1495
1684
  async showExtensionCustom(factory, options) {
1685
+ const capturedEpoch = this.uiEpoch;
1496
1686
  const savedText = this.editor.getText();
1497
1687
  const isOverlay = options?.overlay ?? false;
1498
1688
  const restoreEditor = () => {
1499
- this.editorContainer.clear();
1500
- this.editorContainer.addChild(this.editor);
1501
- this.editor.setText(savedText);
1502
- this.ui.setFocus(this.editor);
1503
- this.ui.requestRender();
1689
+ this.restoreCanonicalEditor({ text: savedText });
1504
1690
  };
1505
1691
  return new Promise((resolve, reject) => {
1506
1692
  let component;
@@ -1509,6 +1695,11 @@ export class InteractiveMode {
1509
1695
  if (closed)
1510
1696
  return;
1511
1697
  closed = true;
1698
+ if (this.uiEpoch !== capturedEpoch) {
1699
+ // We're stale, just resolve the promise to avoid leaks, but don't mutate UI
1700
+ resolve(result);
1701
+ return;
1702
+ }
1512
1703
  if (isOverlay)
1513
1704
  this.ui.hideOverlay();
1514
1705
  else
@@ -1526,6 +1717,14 @@ export class InteractiveMode {
1526
1717
  .then((c) => {
1527
1718
  if (closed)
1528
1719
  return;
1720
+ if (this.uiEpoch !== capturedEpoch) {
1721
+ closed = true;
1722
+ try {
1723
+ c?.dispose?.();
1724
+ }
1725
+ catch { }
1726
+ return; // Do not mount if epoch changed
1727
+ }
1529
1728
  component = c;
1530
1729
  if (isOverlay) {
1531
1730
  // Resolve overlay options - can be static or dynamic function
@@ -1545,10 +1744,7 @@ export class InteractiveMode {
1545
1744
  options?.onHandle?.(handle);
1546
1745
  }
1547
1746
  else {
1548
- this.editorContainer.clear();
1549
- this.editorContainer.addChild(component);
1550
- this.ui.setFocus(component);
1551
- this.ui.requestRender();
1747
+ this.mountPromptOwner(component);
1552
1748
  }
1553
1749
  })
1554
1750
  .catch((err) => {
@@ -1841,9 +2037,13 @@ export class InteractiveMode {
1841
2037
  });
1842
2038
  }
1843
2039
  async handleEvent(event) {
2040
+ const capturedEpoch = this.sessionEpoch;
1844
2041
  if (!this.isInitialized) {
1845
2042
  await this.init();
1846
2043
  }
2044
+ if (this.sessionEpoch !== capturedEpoch) {
2045
+ return; // Stale event from previous session/epoch
2046
+ }
1847
2047
  this.footer.invalidate();
1848
2048
  switch (event.type) {
1849
2049
  case "agent_start":
@@ -1854,15 +2054,15 @@ export class InteractiveMode {
1854
2054
  this.retryEscapeHandler = undefined;
1855
2055
  }
1856
2056
  if (this.retryLoader) {
1857
- this.retryLoader.stop();
2057
+ this.retryLoader.dispose();
1858
2058
  this.retryLoader = undefined;
1859
2059
  }
1860
2060
  if (this.loadingAnimation) {
1861
- this.loadingAnimation.stop();
2061
+ this.loadingAnimation.dispose();
1862
2062
  }
1863
- this.statusContainer.clear();
2063
+ this.clearStatusOwner({ requestRender: false });
1864
2064
  this.loadingAnimation = new Loader(this.ui, (spinner) => theme.fg("accent", spinner), (text) => theme.fg("muted", text), this.defaultWorkingMessage);
1865
- this.statusContainer.addChild(this.loadingAnimation);
2065
+ this.mountStatusLoader(this.loadingAnimation, { requestRender: false });
1866
2066
  // Apply any pending working message queued before loader existed
1867
2067
  if (this.pendingWorkingMessage !== undefined) {
1868
2068
  if (this.pendingWorkingMessage) {
@@ -1875,15 +2075,21 @@ export class InteractiveMode {
1875
2075
  case "message_start":
1876
2076
  if (event.message.role === "custom") {
1877
2077
  this.addMessageToChat(event.message);
2078
+ if (this.loadingAnimation || this.statusContainer.children.length > 0) {
2079
+ this.ui.invalidate();
2080
+ }
1878
2081
  this.ui.requestRender();
1879
2082
  }
1880
2083
  else if (event.message.role === "user") {
1881
2084
  this.addMessageToChat(event.message);
1882
2085
  this.updatePendingMessagesDisplay();
2086
+ if (this.loadingAnimation || this.statusContainer.children.length > 0) {
2087
+ this.ui.invalidate();
2088
+ }
1883
2089
  this.ui.requestRender();
1884
2090
  }
1885
2091
  else if (event.message.role === "assistant") {
1886
- this.streamingComponent = new AssistantMessageComponent(undefined, this.hideThinkingBlock, this.getMarkdownThemeWithSettings());
2092
+ this.streamingComponent = new AssistantMessageComponent(undefined, this.hideThinkingBlock, this.getMarkdownThemeWithSettings(), 0, 1);
1887
2093
  this.streamingMessage = event.message;
1888
2094
  this.chatContainer.addChild(this.streamingComponent);
1889
2095
  this.streamingComponent.updateContent(this.streamingMessage);
@@ -1985,9 +2191,9 @@ export class InteractiveMode {
1985
2191
  }
1986
2192
  case "agent_end":
1987
2193
  if (this.loadingAnimation) {
1988
- this.loadingAnimation.stop();
2194
+ this.loadingAnimation.dispose();
1989
2195
  this.loadingAnimation = undefined;
1990
- this.statusContainer.clear();
2196
+ this.clearStatusOwner({ requestRender: false });
1991
2197
  }
1992
2198
  if (this.streamingComponent) {
1993
2199
  this.chatContainer.removeChild(this.streamingComponent);
@@ -1996,7 +2202,13 @@ export class InteractiveMode {
1996
2202
  }
1997
2203
  this.pendingTools.clear();
1998
2204
  await this.checkShutdownRequested();
1999
- this.ui.requestRender();
2205
+ if (this.startupUiGateActive) {
2206
+ this.startupUiGateActive = false;
2207
+ this.flushDeferredStartupNotices();
2208
+ }
2209
+ else {
2210
+ this.ui.requestRender();
2211
+ }
2000
2212
  break;
2001
2213
  case "auto_compaction_start": {
2002
2214
  // Keep editor active; submissions are queued during compaction.
@@ -2006,10 +2218,10 @@ export class InteractiveMode {
2006
2218
  this.session.abortCompaction();
2007
2219
  };
2008
2220
  // Show compacting indicator with reason
2009
- this.statusContainer.clear();
2221
+ this.clearStatusOwner({ requestRender: false });
2010
2222
  const reasonText = event.reason === "overflow" ? "Context overflow detected, " : "";
2011
2223
  this.autoCompactionLoader = new Loader(this.ui, (spinner) => theme.fg("accent", spinner), (text) => theme.fg("muted", text), `${reasonText}Auto-compacting... (${appKey(this.keybindings, "interrupt")} to cancel)`);
2012
- this.statusContainer.addChild(this.autoCompactionLoader);
2224
+ this.mountStatusLoader(this.autoCompactionLoader, { requestRender: false });
2013
2225
  this.ui.requestRender();
2014
2226
  break;
2015
2227
  }
@@ -2021,9 +2233,9 @@ export class InteractiveMode {
2021
2233
  }
2022
2234
  // Stop loader
2023
2235
  if (this.autoCompactionLoader) {
2024
- this.autoCompactionLoader.stop();
2236
+ this.autoCompactionLoader.dispose();
2025
2237
  this.autoCompactionLoader = undefined;
2026
- this.statusContainer.clear();
2238
+ this.clearStatusOwner({ requestRender: false });
2027
2239
  }
2028
2240
  // Handle result
2029
2241
  if (event.aborted) {
@@ -2058,10 +2270,10 @@ export class InteractiveMode {
2058
2270
  this.session.abortRetry();
2059
2271
  };
2060
2272
  // Show retry indicator
2061
- this.statusContainer.clear();
2273
+ this.clearStatusOwner({ requestRender: false });
2062
2274
  const delaySeconds = Math.round(event.delayMs / 1000);
2063
2275
  this.retryLoader = new Loader(this.ui, (spinner) => theme.fg("warning", spinner), (text) => theme.fg("muted", text), `Retrying (${event.attempt}/${event.maxAttempts}) in ${delaySeconds}s... (${appKey(this.keybindings, "interrupt")} to cancel)`);
2064
- this.statusContainer.addChild(this.retryLoader);
2276
+ this.mountStatusLoader(this.retryLoader, { requestRender: false });
2065
2277
  this.ui.requestRender();
2066
2278
  break;
2067
2279
  }
@@ -2073,9 +2285,9 @@ export class InteractiveMode {
2073
2285
  }
2074
2286
  // Stop loader
2075
2287
  if (this.retryLoader) {
2076
- this.retryLoader.stop();
2288
+ this.retryLoader.dispose();
2077
2289
  this.retryLoader = undefined;
2078
- this.statusContainer.clear();
2290
+ this.clearStatusOwner({ requestRender: false });
2079
2291
  }
2080
2292
  // Show error only on final failure (success shows normal response)
2081
2293
  if (!event.success) {
@@ -2179,7 +2391,7 @@ export class InteractiveMode {
2179
2391
  break;
2180
2392
  }
2181
2393
  case "assistant": {
2182
- const assistantComponent = new AssistantMessageComponent(message, this.hideThinkingBlock, this.getMarkdownThemeWithSettings());
2394
+ const assistantComponent = new AssistantMessageComponent(message, this.hideThinkingBlock, this.getMarkdownThemeWithSettings(), 0, 1);
2183
2395
  this.chatContainer.addChild(assistantComponent);
2184
2396
  break;
2185
2397
  }
@@ -2354,6 +2566,7 @@ export class InteractiveMode {
2354
2566
  const text = (this.editor.getExpandedText?.() ?? this.editor.getText()).trim();
2355
2567
  if (!text)
2356
2568
  return;
2569
+ const capturedEpoch = this.sessionEpoch;
2357
2570
  // Queue input during compaction (extension commands execute immediately)
2358
2571
  if (this.session.isCompacting) {
2359
2572
  if (this.isExtensionCommand(text)) {
@@ -2372,6 +2585,8 @@ export class InteractiveMode {
2372
2585
  this.editor.addToHistory?.(text);
2373
2586
  this.editor.setText("");
2374
2587
  await this.session.prompt(text, { streamingBehavior: "followUp" });
2588
+ if (this.sessionEpoch !== capturedEpoch)
2589
+ return;
2375
2590
  this.updatePendingMessagesDisplay();
2376
2591
  this.ui.requestRender();
2377
2592
  }
@@ -2435,8 +2650,11 @@ export class InteractiveMode {
2435
2650
  }
2436
2651
  }
2437
2652
  async cycleModel(direction) {
2653
+ const capturedEpoch = this.sessionEpoch;
2438
2654
  try {
2439
2655
  const result = await this.session.cycleModel(direction);
2656
+ if (this.sessionEpoch !== capturedEpoch)
2657
+ return;
2440
2658
  if (result === undefined) {
2441
2659
  const msg = this.session.scopedModels.length > 0 ? "Only one model in scope" : "Only one model available";
2442
2660
  this.showStatus(msg);
@@ -2449,6 +2667,8 @@ export class InteractiveMode {
2449
2667
  }
2450
2668
  }
2451
2669
  catch (error) {
2670
+ if (this.sessionEpoch !== capturedEpoch)
2671
+ return;
2452
2672
  this.showError(error instanceof Error ? error.message : String(error));
2453
2673
  }
2454
2674
  }
@@ -2532,21 +2752,43 @@ export class InteractiveMode {
2532
2752
  this.chatContainer.addChild(new Text(theme.fg("error", `Error: ${errorMessage}`), 1, 0));
2533
2753
  this.ui.requestRender();
2534
2754
  }
2535
- showWarning(warningMessage) {
2536
- this.chatContainer.addChild(new Spacer(1));
2537
- this.chatContainer.addChild(new Text(theme.fg("warning", `Warning: ${warningMessage}`), 1, 0));
2755
+ enqueueStartupNotice(notice) {
2756
+ if (this.startupUiGateActive) {
2757
+ this.deferredStartupNotices.push(notice);
2758
+ return;
2759
+ }
2760
+ notice();
2761
+ this.ui.requestRender();
2762
+ }
2763
+ flushDeferredStartupNotices() {
2764
+ if (this.deferredStartupNotices.length === 0) {
2765
+ return;
2766
+ }
2767
+ const notices = [...this.deferredStartupNotices];
2768
+ this.deferredStartupNotices = [];
2769
+ for (const notice of notices) {
2770
+ notice();
2771
+ }
2772
+ this.ui.invalidate();
2538
2773
  this.ui.requestRender();
2539
2774
  }
2775
+ showWarning(warningMessage) {
2776
+ this.enqueueStartupNotice(() => {
2777
+ this.chatContainer.addChild(new Spacer(1));
2778
+ this.chatContainer.addChild(new Text(theme.fg("warning", `Warning: ${warningMessage}`), 1, 0));
2779
+ });
2780
+ }
2540
2781
  showNewVersionNotification(newVersion) {
2541
2782
  const action = theme.fg("accent", getUpdateInstruction(PACKAGE_NAME));
2542
2783
  const updateInstruction = theme.fg("muted", `New version ${newVersion} is available. `) + action;
2543
2784
  const changelogUrl = theme.fg("accent", "https://github.com/apholdings/jensen-code/blob/main/packages/coding-agent/CHANGELOG.md");
2544
2785
  const changelogLine = theme.fg("muted", "Changelog: ") + changelogUrl;
2545
- this.chatContainer.addChild(new Spacer(1));
2546
- this.chatContainer.addChild(new DynamicBorder((text) => theme.fg("warning", text)));
2547
- this.chatContainer.addChild(new Text(`${theme.bold(theme.fg("warning", "Update Available"))}\n${updateInstruction}\n${changelogLine}`, 1, 0));
2548
- this.chatContainer.addChild(new DynamicBorder((text) => theme.fg("warning", text)));
2549
- this.ui.requestRender();
2786
+ this.enqueueStartupNotice(() => {
2787
+ this.chatContainer.addChild(new Spacer(1));
2788
+ this.chatContainer.addChild(new DynamicBorder((text) => theme.fg("warning", text)));
2789
+ this.chatContainer.addChild(new Text(`${theme.bold(theme.fg("warning", "Update Available"))}\n${updateInstruction}\n${changelogLine}`, 1, 0));
2790
+ this.chatContainer.addChild(new DynamicBorder((text) => theme.fg("warning", text)));
2791
+ });
2550
2792
  }
2551
2793
  /**
2552
2794
  * Get all queued messages (read-only).
@@ -2641,10 +2883,13 @@ export class InteractiveMode {
2641
2883
  if (this.compactionQueuedMessages.length === 0) {
2642
2884
  return;
2643
2885
  }
2886
+ const capturedEpoch = this.sessionEpoch;
2644
2887
  const queuedMessages = [...this.compactionQueuedMessages];
2645
2888
  this.compactionQueuedMessages = [];
2646
2889
  this.updatePendingMessagesDisplay();
2647
2890
  const restoreQueue = (error) => {
2891
+ if (this.sessionEpoch !== capturedEpoch)
2892
+ return;
2648
2893
  this.session.clearQueue();
2649
2894
  this.compactionQueuedMessages = queuedMessages;
2650
2895
  this.updatePendingMessagesDisplay();
@@ -2654,6 +2899,8 @@ export class InteractiveMode {
2654
2899
  if (options?.willRetry) {
2655
2900
  // When retry is pending, queue messages for the retry turn
2656
2901
  for (const message of queuedMessages) {
2902
+ if (this.sessionEpoch !== capturedEpoch)
2903
+ return;
2657
2904
  if (this.isExtensionCommand(message.text)) {
2658
2905
  await this.session.prompt(message.text);
2659
2906
  }
@@ -2664,7 +2911,9 @@ export class InteractiveMode {
2664
2911
  await this.session.steer(message.text);
2665
2912
  }
2666
2913
  }
2667
- this.updatePendingMessagesDisplay();
2914
+ if (this.sessionEpoch === capturedEpoch) {
2915
+ this.updatePendingMessagesDisplay();
2916
+ }
2668
2917
  return;
2669
2918
  }
2670
2919
  // Find first non-extension-command message to use as prompt
@@ -2672,6 +2921,8 @@ export class InteractiveMode {
2672
2921
  if (firstPromptIndex === -1) {
2673
2922
  // All extension commands - execute them all
2674
2923
  for (const message of queuedMessages) {
2924
+ if (this.sessionEpoch !== capturedEpoch)
2925
+ return;
2675
2926
  await this.session.prompt(message.text);
2676
2927
  }
2677
2928
  return;
@@ -2681,14 +2932,20 @@ export class InteractiveMode {
2681
2932
  const firstPrompt = queuedMessages[firstPromptIndex];
2682
2933
  const rest = queuedMessages.slice(firstPromptIndex + 1);
2683
2934
  for (const message of preCommands) {
2935
+ if (this.sessionEpoch !== capturedEpoch)
2936
+ return;
2684
2937
  await this.session.prompt(message.text);
2685
2938
  }
2939
+ if (this.sessionEpoch !== capturedEpoch)
2940
+ return;
2686
2941
  // Send first prompt (starts streaming)
2687
2942
  const promptPromise = this.session.prompt(firstPrompt.text).catch((error) => {
2688
2943
  restoreQueue(error);
2689
2944
  });
2690
2945
  // Queue remaining messages
2691
2946
  for (const message of rest) {
2947
+ if (this.sessionEpoch !== capturedEpoch)
2948
+ return;
2692
2949
  if (this.isExtensionCommand(message.text)) {
2693
2950
  await this.session.prompt(message.text);
2694
2951
  }
@@ -2699,7 +2956,9 @@ export class InteractiveMode {
2699
2956
  await this.session.steer(message.text);
2700
2957
  }
2701
2958
  }
2702
- this.updatePendingMessagesDisplay();
2959
+ if (this.sessionEpoch === capturedEpoch) {
2960
+ this.updatePendingMessagesDisplay();
2961
+ }
2703
2962
  void promptPromise;
2704
2963
  }
2705
2964
  catch (error) {
@@ -2722,16 +2981,14 @@ export class InteractiveMode {
2722
2981
  * @param create Factory that receives a `done` callback and returns the component and focus target
2723
2982
  */
2724
2983
  showSelector(create) {
2984
+ const capturedEpoch = this.uiEpoch;
2725
2985
  const done = () => {
2726
- this.editorContainer.clear();
2727
- this.editorContainer.addChild(this.editor);
2728
- this.ui.setFocus(this.editor);
2986
+ if (this.uiEpoch !== capturedEpoch)
2987
+ return;
2988
+ this.restoreCanonicalEditor({ requestRender: false });
2729
2989
  };
2730
2990
  const { component, focus } = create(done);
2731
- this.editorContainer.clear();
2732
- this.editorContainer.addChild(component);
2733
- this.ui.setFocus(focus);
2734
- this.ui.requestRender();
2991
+ this.mountPromptOwner(component, { focus });
2735
2992
  }
2736
2993
  showSettingsSelector() {
2737
2994
  this.showSelector((done) => {
@@ -2870,16 +3127,23 @@ export class InteractiveMode {
2870
3127
  this.showModelSelector();
2871
3128
  return;
2872
3129
  }
3130
+ const capturedEpoch = this.sessionEpoch;
2873
3131
  const model = await this.findExactModelMatch(searchTerm);
3132
+ if (this.sessionEpoch !== capturedEpoch)
3133
+ return;
2874
3134
  if (model) {
2875
3135
  try {
2876
3136
  await this.session.setModel(model);
3137
+ if (this.sessionEpoch !== capturedEpoch)
3138
+ return;
2877
3139
  this.footer.invalidate();
2878
3140
  this.updatePromptChrome();
2879
3141
  // this.showStatus(`Model: ${model.id}`);
2880
3142
  this.checkDaxnutsEasterEgg(model);
2881
3143
  }
2882
3144
  catch (error) {
3145
+ if (this.sessionEpoch !== capturedEpoch)
3146
+ return;
2883
3147
  this.showError(error instanceof Error ? error.message : String(error));
2884
3148
  }
2885
3149
  return;
@@ -3140,7 +3404,7 @@ export class InteractiveMode {
3140
3404
  };
3141
3405
  this.chatContainer.addChild(new Spacer(1));
3142
3406
  summaryLoader = new Loader(this.ui, (spinner) => theme.fg("accent", spinner), (text) => theme.fg("muted", text), `Summarizing branch... (${appKey(this.keybindings, "interrupt")} to cancel)`);
3143
- this.statusContainer.addChild(summaryLoader);
3407
+ this.mountStatusLoader(summaryLoader, { requestRender: false });
3144
3408
  this.ui.requestRender();
3145
3409
  }
3146
3410
  try {
@@ -3171,8 +3435,8 @@ export class InteractiveMode {
3171
3435
  }
3172
3436
  finally {
3173
3437
  if (summaryLoader) {
3174
- summaryLoader.stop();
3175
- this.statusContainer.clear();
3438
+ summaryLoader.dispose();
3439
+ this.clearStatusOwner({ requestRender: false });
3176
3440
  }
3177
3441
  this.defaultEditor.onEscape = originalOnEscape;
3178
3442
  }
@@ -3213,16 +3477,16 @@ export class InteractiveMode {
3213
3477
  async handleResumeSession(sessionPath) {
3214
3478
  // Stop loading animation
3215
3479
  if (this.loadingAnimation) {
3216
- this.loadingAnimation.stop();
3480
+ this.loadingAnimation.dispose();
3217
3481
  this.loadingAnimation = undefined;
3218
3482
  }
3219
- this.statusContainer.clear();
3483
+ this.clearStatusOwner({ requestRender: false });
3220
3484
  // Clear UI state
3221
3485
  this.pendingMessagesContainer.clear();
3222
3486
  this.compactionQueuedMessages = [];
3223
3487
  // Switch session via AgentSession (emits extension session events)
3224
3488
  await this.session.switchSession(sessionPath);
3225
- this.resetSessionUiState(true);
3489
+ this.resetInteractiveSessionUI(true);
3226
3490
  this.showStatus("Resumed session");
3227
3491
  }
3228
3492
  async showOAuthSelector(mode) {
@@ -3273,10 +3537,7 @@ export class InteractiveMode {
3273
3537
  // Completion handled below
3274
3538
  });
3275
3539
  // Show dialog in editor container
3276
- this.editorContainer.clear();
3277
- this.editorContainer.addChild(dialog);
3278
- this.ui.setFocus(dialog);
3279
- this.ui.requestRender();
3540
+ this.mountPromptOwner(dialog);
3280
3541
  // Promise for manual code input (racing with callback server)
3281
3542
  let manualCodeResolve;
3282
3543
  let manualCodeReject;
@@ -3285,11 +3546,11 @@ export class InteractiveMode {
3285
3546
  manualCodeReject = reject;
3286
3547
  });
3287
3548
  // Restore editor helper
3549
+ const capturedEpoch = this.uiEpoch;
3288
3550
  const restoreEditor = () => {
3289
- this.editorContainer.clear();
3290
- this.editorContainer.addChild(this.editor);
3291
- this.ui.setFocus(this.editor);
3292
- this.ui.requestRender();
3551
+ if (this.uiEpoch !== capturedEpoch)
3552
+ return;
3553
+ this.restoreCanonicalEditor();
3293
3554
  };
3294
3555
  try {
3295
3556
  await this.session.modelRegistry.authStorage.login(providerId, {
@@ -3331,10 +3592,14 @@ export class InteractiveMode {
3331
3592
  restoreEditor();
3332
3593
  this.session.modelRegistry.refresh();
3333
3594
  await this.updateAvailableProviderCount();
3595
+ if (this.uiEpoch !== capturedEpoch)
3596
+ return;
3334
3597
  this.showStatus(`Logged in to ${providerName}. Credentials saved to ${getAuthPath()}`);
3335
3598
  }
3336
3599
  catch (error) {
3337
3600
  restoreEditor();
3601
+ if (this.uiEpoch !== capturedEpoch)
3602
+ return;
3338
3603
  const errorMsg = error instanceof Error ? error.message : String(error);
3339
3604
  if (errorMsg !== "Login cancelled") {
3340
3605
  this.showError(`Failed to login to ${providerName}: ${errorMsg}`);
@@ -3358,19 +3623,20 @@ export class InteractiveMode {
3358
3623
  cancellable: false,
3359
3624
  });
3360
3625
  const previousEditor = this.editor;
3361
- this.editorContainer.clear();
3362
- this.editorContainer.addChild(loader);
3363
- this.ui.setFocus(loader);
3364
- this.ui.requestRender();
3365
- const dismissLoader = (editor) => {
3626
+ this.mountPromptOwner(loader);
3627
+ const capturedEpoch = this.uiEpoch;
3628
+ const dismissLoader = (_editor) => {
3366
3629
  loader.dispose();
3367
- this.editorContainer.clear();
3368
- this.editorContainer.addChild(editor);
3369
- this.ui.setFocus(editor);
3370
- this.ui.requestRender();
3630
+ if (this.uiEpoch !== capturedEpoch)
3631
+ return;
3632
+ this.restoreCanonicalEditor();
3371
3633
  };
3372
3634
  try {
3373
3635
  await this.session.reload();
3636
+ if (this.uiEpoch !== capturedEpoch) {
3637
+ loader.dispose();
3638
+ return;
3639
+ }
3374
3640
  setRegisteredThemes(this.session.resourceLoader.getThemes().themes);
3375
3641
  this.hideThinkingBlock = this.settingsManager.getHideThinkingBlock();
3376
3642
  const themeName = this.settingsManager.getTheme();
@@ -3408,6 +3674,10 @@ export class InteractiveMode {
3408
3674
  this.showStatus("Reloaded extensions, skills, prompts, themes");
3409
3675
  }
3410
3676
  catch (error) {
3677
+ if (this.uiEpoch !== capturedEpoch) {
3678
+ loader.dispose();
3679
+ return;
3680
+ }
3411
3681
  dismissLoader(previousEditor);
3412
3682
  this.showError(`Reload failed: ${error instanceof Error ? error.message : String(error)}`);
3413
3683
  }
@@ -3415,11 +3685,16 @@ export class InteractiveMode {
3415
3685
  async handleExportCommand(text) {
3416
3686
  const parts = text.split(/\s+/);
3417
3687
  const outputPath = parts.length > 1 ? parts[1] : undefined;
3688
+ const capturedEpoch = this.uiEpoch;
3418
3689
  try {
3419
3690
  const filePath = await this.session.exportToHtml(outputPath);
3691
+ if (this.uiEpoch !== capturedEpoch)
3692
+ return;
3420
3693
  this.showStatus(`Session exported to: ${filePath}`);
3421
3694
  }
3422
3695
  catch (error) {
3696
+ if (this.uiEpoch !== capturedEpoch)
3697
+ return;
3423
3698
  this.showError(`Failed to export session: ${error instanceof Error ? error.message : "Unknown error"}`);
3424
3699
  }
3425
3700
  }
@@ -3436,26 +3711,28 @@ export class InteractiveMode {
3436
3711
  this.showError("GitHub CLI (gh) is not installed. Install it from https://cli.github.com/");
3437
3712
  return;
3438
3713
  }
3714
+ const capturedEpoch = this.uiEpoch;
3439
3715
  // Export to a temp file
3440
3716
  const tmpFile = path.join(os.tmpdir(), "session.html");
3441
3717
  try {
3442
3718
  await this.session.exportToHtml(tmpFile);
3719
+ if (this.uiEpoch !== capturedEpoch)
3720
+ return;
3443
3721
  }
3444
3722
  catch (error) {
3723
+ if (this.uiEpoch !== capturedEpoch)
3724
+ return;
3445
3725
  this.showError(`Failed to export session: ${error instanceof Error ? error.message : "Unknown error"}`);
3446
3726
  return;
3447
3727
  }
3448
3728
  // Show cancellable loader, replacing the editor
3449
3729
  const loader = new BorderedLoader(this.ui, theme, "Creating gist...");
3450
- this.editorContainer.clear();
3451
- this.editorContainer.addChild(loader);
3452
- this.ui.setFocus(loader);
3453
- this.ui.requestRender();
3730
+ this.mountPromptOwner(loader);
3454
3731
  const restoreEditor = () => {
3455
3732
  loader.dispose();
3456
- this.editorContainer.clear();
3457
- this.editorContainer.addChild(this.editor);
3458
- this.ui.setFocus(this.editor);
3733
+ if (this.uiEpoch !== capturedEpoch)
3734
+ return;
3735
+ this.restoreCanonicalEditor({ requestRender: false });
3459
3736
  try {
3460
3737
  fs.unlinkSync(tmpFile);
3461
3738
  }
@@ -3468,6 +3745,8 @@ export class InteractiveMode {
3468
3745
  loader.onAbort = () => {
3469
3746
  proc?.kill();
3470
3747
  restoreEditor();
3748
+ if (this.uiEpoch !== capturedEpoch)
3749
+ return;
3471
3750
  this.showStatus("Share cancelled");
3472
3751
  };
3473
3752
  try {
@@ -3485,6 +3764,8 @@ export class InteractiveMode {
3485
3764
  });
3486
3765
  if (loader.signal.aborted)
3487
3766
  return;
3767
+ if (this.uiEpoch !== capturedEpoch)
3768
+ return;
3488
3769
  restoreEditor();
3489
3770
  if (result.code !== 0) {
3490
3771
  const errorMsg = result.stderr?.trim() || "Unknown error";
@@ -3506,6 +3787,8 @@ export class InteractiveMode {
3506
3787
  catch (error) {
3507
3788
  if (!loader.signal.aborted) {
3508
3789
  restoreEditor();
3790
+ if (this.uiEpoch !== capturedEpoch)
3791
+ return;
3509
3792
  this.showError(`Failed to create gist: ${error instanceof Error ? error.message : "Unknown error"}`);
3510
3793
  }
3511
3794
  }
@@ -3724,14 +4007,9 @@ export class InteractiveMode {
3724
4007
  this.ui.requestRender();
3725
4008
  }
3726
4009
  async handleClearCommand() {
3727
- // Stop loading animation
3728
- if (this.loadingAnimation) {
3729
- this.loadingAnimation.stop();
3730
- this.loadingAnimation = undefined;
3731
- }
3732
4010
  // New session via session (emits extension session events)
3733
4011
  await this.session.newSession();
3734
- this.resetSessionUiState();
4012
+ this.resetInteractiveSessionUI(true);
3735
4013
  this.ui.requestRender();
3736
4014
  }
3737
4015
  handleDebugCommand() {
@@ -3777,6 +4055,7 @@ export class InteractiveMode {
3777
4055
  }
3778
4056
  }
3779
4057
  async handleBashCommand(command, excludeFromContext = false) {
4058
+ const capturedEpoch = this.sessionEpoch;
3780
4059
  const extensionRunner = this.session.extensionRunner;
3781
4060
  // Emit user_bash event to let extensions intercept
3782
4061
  const eventResult = extensionRunner
@@ -3787,6 +4066,8 @@ export class InteractiveMode {
3787
4066
  cwd: process.cwd(),
3788
4067
  })
3789
4068
  : undefined;
4069
+ if (this.sessionEpoch !== capturedEpoch)
4070
+ return;
3790
4071
  // If extension returned a full result, use it directly
3791
4072
  if (eventResult?.result) {
3792
4073
  const result = eventResult.result;
@@ -3825,16 +4106,22 @@ export class InteractiveMode {
3825
4106
  this.ui.requestRender();
3826
4107
  try {
3827
4108
  const result = await this.session.executeBash(command, (chunk) => {
4109
+ if (this.sessionEpoch !== capturedEpoch)
4110
+ return;
3828
4111
  if (this.bashComponent) {
3829
4112
  this.bashComponent.appendOutput(chunk);
3830
4113
  this.ui.requestRender();
3831
4114
  }
3832
4115
  }, { excludeFromContext, operations: eventResult?.operations });
4116
+ if (this.sessionEpoch !== capturedEpoch)
4117
+ return;
3833
4118
  if (this.bashComponent) {
3834
4119
  this.bashComponent.setComplete(result.exitCode, result.cancelled, result.truncated ? { truncated: true, content: result.output } : undefined, result.fullOutputPath);
3835
4120
  }
3836
4121
  }
3837
4122
  catch (error) {
4123
+ if (this.sessionEpoch !== capturedEpoch)
4124
+ return;
3838
4125
  if (this.bashComponent) {
3839
4126
  this.bashComponent.setComplete(undefined, false);
3840
4127
  }
@@ -3853,12 +4140,13 @@ export class InteractiveMode {
3853
4140
  await this.executeCompaction(customInstructions, false);
3854
4141
  }
3855
4142
  async executeCompaction(customInstructions, isAuto = false) {
4143
+ const capturedEpoch = this.sessionEpoch;
3856
4144
  // Stop loading animation
3857
4145
  if (this.loadingAnimation) {
3858
- this.loadingAnimation.stop();
4146
+ this.loadingAnimation.dispose();
3859
4147
  this.loadingAnimation = undefined;
3860
4148
  }
3861
- this.statusContainer.clear();
4149
+ this.clearStatusOwner({ requestRender: false });
3862
4150
  // Set up escape handler during compaction
3863
4151
  const originalOnEscape = this.defaultEditor.onEscape;
3864
4152
  this.defaultEditor.onEscape = () => {
@@ -3869,11 +4157,13 @@ export class InteractiveMode {
3869
4157
  const cancelHint = `(${appKey(this.keybindings, "interrupt")} to cancel)`;
3870
4158
  const label = isAuto ? `Auto-compacting context... ${cancelHint}` : `Compacting context... ${cancelHint}`;
3871
4159
  const compactingLoader = new Loader(this.ui, (spinner) => theme.fg("accent", spinner), (text) => theme.fg("muted", text), label);
3872
- this.statusContainer.addChild(compactingLoader);
4160
+ this.mountStatusLoader(compactingLoader, { requestRender: false });
3873
4161
  this.ui.requestRender();
3874
4162
  let result;
3875
4163
  try {
3876
4164
  result = await this.session.compact(customInstructions);
4165
+ if (this.sessionEpoch !== capturedEpoch)
4166
+ return undefined;
3877
4167
  // Rebuild UI
3878
4168
  this.rebuildChatFromMessages();
3879
4169
  // Add compaction component at bottom so user sees it without scrolling
@@ -3882,6 +4172,8 @@ export class InteractiveMode {
3882
4172
  this.footer.invalidate();
3883
4173
  }
3884
4174
  catch (error) {
4175
+ if (this.sessionEpoch !== capturedEpoch)
4176
+ return undefined;
3885
4177
  const message = error instanceof Error ? error.message : String(error);
3886
4178
  if (message === "Compaction cancelled" || (error instanceof Error && error.name === "AbortError")) {
3887
4179
  this.showError("Compaction cancelled");
@@ -3891,16 +4183,20 @@ export class InteractiveMode {
3891
4183
  }
3892
4184
  }
3893
4185
  finally {
3894
- compactingLoader.stop();
3895
- this.statusContainer.clear();
3896
- this.defaultEditor.onEscape = originalOnEscape;
4186
+ if (this.sessionEpoch === capturedEpoch) {
4187
+ compactingLoader.dispose();
4188
+ this.clearStatusOwner({ requestRender: false });
4189
+ this.defaultEditor.onEscape = originalOnEscape;
4190
+ }
4191
+ }
4192
+ if (this.sessionEpoch === capturedEpoch) {
4193
+ void this.flushCompactionQueue({ willRetry: false });
3897
4194
  }
3898
- void this.flushCompactionQueue({ willRetry: false });
3899
4195
  return result;
3900
4196
  }
3901
4197
  stop() {
3902
4198
  if (this.loadingAnimation) {
3903
- this.loadingAnimation.stop();
4199
+ this.loadingAnimation.dispose();
3904
4200
  this.loadingAnimation = undefined;
3905
4201
  }
3906
4202
  this.clearExtensionTerminalInputListeners();