@apholdings/jensen-code 0.0.4 → 0.0.6

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 (161) hide show
  1. package/CHANGELOG.md +3061 -3061
  2. package/README.md +1 -1
  3. package/dist/cli/args.d.ts.map +1 -1
  4. package/dist/cli/args.js +6 -6
  5. package/dist/cli/args.js.map +1 -1
  6. package/dist/config.d.ts +17 -6
  7. package/dist/config.d.ts.map +1 -1
  8. package/dist/config.js +55 -28
  9. package/dist/config.js.map +1 -1
  10. package/dist/core/agent-session.d.ts.map +1 -1
  11. package/dist/core/agent-session.js +10 -0
  12. package/dist/core/agent-session.js.map +1 -1
  13. package/dist/index.d.ts +1 -1
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +1 -1
  16. package/dist/index.js.map +1 -1
  17. package/dist/modes/interactive/components/assistant-message.d.ts +1 -6
  18. package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  19. package/dist/modes/interactive/components/assistant-message.js +10 -40
  20. package/dist/modes/interactive/components/assistant-message.js.map +1 -1
  21. package/dist/modes/interactive/components/custom-editor.d.ts +1 -0
  22. package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
  23. package/dist/modes/interactive/components/custom-editor.js +5 -0
  24. package/dist/modes/interactive/components/custom-editor.js.map +1 -1
  25. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  26. package/dist/modes/interactive/components/tool-execution.js +1 -2
  27. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  28. package/dist/modes/interactive/components/top-bar.d.ts.map +1 -1
  29. package/dist/modes/interactive/components/top-bar.js +1 -1
  30. package/dist/modes/interactive/components/top-bar.js.map +1 -1
  31. package/dist/modes/interactive/components/user-message.d.ts.map +1 -1
  32. package/dist/modes/interactive/components/user-message.js +1 -1
  33. package/dist/modes/interactive/components/user-message.js.map +1 -1
  34. package/dist/modes/interactive/interactive-mode.d.ts +6 -3
  35. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  36. package/dist/modes/interactive/interactive-mode.js +204 -86
  37. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  38. package/dist/utils/frontmatter.d.ts.map +1 -1
  39. package/dist/utils/frontmatter.js +8 -4
  40. package/dist/utils/frontmatter.js.map +1 -1
  41. package/dist/utils/tools-manager.d.ts.map +1 -1
  42. package/dist/utils/tools-manager.js +2 -2
  43. package/dist/utils/tools-manager.js.map +1 -1
  44. package/docs/custom-provider.md +592 -592
  45. package/docs/session.md +412 -412
  46. package/examples/extensions/osgrep.ts +643 -0
  47. package/examples/extensions/subagent/agents.ts +150 -37
  48. package/examples/extensions/subagent/index.ts +634 -513
  49. package/package.json +3 -3
  50. package/examples/README.md +0 -25
  51. package/examples/extensions/README.md +0 -206
  52. package/examples/extensions/antigravity-image-gen.ts +0 -415
  53. package/examples/extensions/auto-commit-on-exit.ts +0 -49
  54. package/examples/extensions/bash-spawn-hook.ts +0 -30
  55. package/examples/extensions/bookmark.ts +0 -50
  56. package/examples/extensions/built-in-tool-renderer.ts +0 -246
  57. package/examples/extensions/claude-rules.ts +0 -86
  58. package/examples/extensions/commands.ts +0 -72
  59. package/examples/extensions/confirm-destructive.ts +0 -59
  60. package/examples/extensions/custom-compaction.ts +0 -114
  61. package/examples/extensions/custom-footer.ts +0 -64
  62. package/examples/extensions/custom-header.ts +0 -73
  63. package/examples/extensions/custom-provider-anthropic/index.ts +0 -604
  64. package/examples/extensions/custom-provider-anthropic/package-lock.json +0 -24
  65. package/examples/extensions/custom-provider-anthropic/package.json +0 -19
  66. package/examples/extensions/custom-provider-gitlab-duo/index.ts +0 -349
  67. package/examples/extensions/custom-provider-gitlab-duo/package.json +0 -16
  68. package/examples/extensions/custom-provider-gitlab-duo/test.ts +0 -82
  69. package/examples/extensions/custom-provider-qwen-cli/index.ts +0 -345
  70. package/examples/extensions/custom-provider-qwen-cli/package.json +0 -16
  71. package/examples/extensions/dirty-repo-guard.ts +0 -56
  72. package/examples/extensions/doom-overlay/README.md +0 -46
  73. package/examples/extensions/doom-overlay/doom/build/doom.js +0 -21
  74. package/examples/extensions/doom-overlay/doom/build/doom.wasm +0 -0
  75. package/examples/extensions/doom-overlay/doom/build.sh +0 -152
  76. package/examples/extensions/doom-overlay/doom/doomgeneric_pi.c +0 -72
  77. package/examples/extensions/doom-overlay/doom-component.ts +0 -132
  78. package/examples/extensions/doom-overlay/doom-engine.ts +0 -173
  79. package/examples/extensions/doom-overlay/doom-keys.ts +0 -104
  80. package/examples/extensions/doom-overlay/index.ts +0 -74
  81. package/examples/extensions/doom-overlay/wad-finder.ts +0 -51
  82. package/examples/extensions/dynamic-resources/SKILL.md +0 -8
  83. package/examples/extensions/dynamic-resources/dynamic.json +0 -79
  84. package/examples/extensions/dynamic-resources/dynamic.md +0 -5
  85. package/examples/extensions/dynamic-resources/index.ts +0 -15
  86. package/examples/extensions/dynamic-tools.ts +0 -74
  87. package/examples/extensions/event-bus.ts +0 -43
  88. package/examples/extensions/file-trigger.ts +0 -41
  89. package/examples/extensions/git-checkpoint.ts +0 -53
  90. package/examples/extensions/handoff.ts +0 -150
  91. package/examples/extensions/hello.ts +0 -25
  92. package/examples/extensions/inline-bash.ts +0 -94
  93. package/examples/extensions/input-transform.ts +0 -43
  94. package/examples/extensions/interactive-shell.ts +0 -196
  95. package/examples/extensions/mac-system-theme.ts +0 -47
  96. package/examples/extensions/message-renderer.ts +0 -59
  97. package/examples/extensions/minimal-mode.ts +0 -426
  98. package/examples/extensions/modal-editor.ts +0 -85
  99. package/examples/extensions/model-status.ts +0 -31
  100. package/examples/extensions/notify.ts +0 -55
  101. package/examples/extensions/overlay-qa-tests.ts +0 -1348
  102. package/examples/extensions/overlay-test.ts +0 -150
  103. package/examples/extensions/permission-gate.ts +0 -34
  104. package/examples/extensions/pirate.ts +0 -47
  105. package/examples/extensions/plan-mode/README.md +0 -65
  106. package/examples/extensions/plan-mode/index.ts +0 -340
  107. package/examples/extensions/plan-mode/utils.ts +0 -168
  108. package/examples/extensions/preset.ts +0 -398
  109. package/examples/extensions/protected-paths.ts +0 -30
  110. package/examples/extensions/provider-payload.ts +0 -14
  111. package/examples/extensions/qna.ts +0 -119
  112. package/examples/extensions/question.ts +0 -264
  113. package/examples/extensions/questionnaire.ts +0 -427
  114. package/examples/extensions/rainbow-editor.ts +0 -88
  115. package/examples/extensions/reload-runtime.ts +0 -37
  116. package/examples/extensions/rpc-demo.ts +0 -124
  117. package/examples/extensions/sandbox/index.ts +0 -318
  118. package/examples/extensions/sandbox/package-lock.json +0 -92
  119. package/examples/extensions/sandbox/package.json +0 -19
  120. package/examples/extensions/send-user-message.ts +0 -97
  121. package/examples/extensions/session-name.ts +0 -27
  122. package/examples/extensions/shutdown-command.ts +0 -63
  123. package/examples/extensions/snake.ts +0 -343
  124. package/examples/extensions/space-invaders.ts +0 -560
  125. package/examples/extensions/ssh.ts +0 -220
  126. package/examples/extensions/status-line.ts +0 -40
  127. package/examples/extensions/subagent/README.md +0 -172
  128. package/examples/extensions/subagent/agents/planner.md +0 -37
  129. package/examples/extensions/subagent/agents/reviewer.md +0 -35
  130. package/examples/extensions/subagent/agents/scout.md +0 -50
  131. package/examples/extensions/subagent/agents/worker.md +0 -24
  132. package/examples/extensions/subagent/prompts/implement-and-review.md +0 -10
  133. package/examples/extensions/subagent/prompts/implement.md +0 -10
  134. package/examples/extensions/subagent/prompts/scout-and-plan.md +0 -9
  135. package/examples/extensions/summarize.ts +0 -195
  136. package/examples/extensions/system-prompt-header.ts +0 -17
  137. package/examples/extensions/timed-confirm.ts +0 -70
  138. package/examples/extensions/titlebar-spinner.ts +0 -58
  139. package/examples/extensions/todo.ts +0 -299
  140. package/examples/extensions/tool-override.ts +0 -143
  141. package/examples/extensions/tools.ts +0 -146
  142. package/examples/extensions/trigger-compact.ts +0 -40
  143. package/examples/extensions/truncated-tool.ts +0 -192
  144. package/examples/extensions/widget-placement.ts +0 -17
  145. package/examples/extensions/with-deps/index.ts +0 -32
  146. package/examples/extensions/with-deps/package-lock.json +0 -31
  147. package/examples/extensions/with-deps/package.json +0 -22
  148. package/examples/rpc-extension-ui.ts +0 -632
  149. package/examples/sdk/01-minimal.ts +0 -22
  150. package/examples/sdk/02-custom-model.ts +0 -49
  151. package/examples/sdk/03-custom-prompt.ts +0 -55
  152. package/examples/sdk/04-skills.ts +0 -46
  153. package/examples/sdk/05-tools.ts +0 -56
  154. package/examples/sdk/06-extensions.ts +0 -88
  155. package/examples/sdk/07-context-files.ts +0 -40
  156. package/examples/sdk/08-prompt-templates.ts +0 -47
  157. package/examples/sdk/09-api-keys-and-oauth.ts +0 -48
  158. package/examples/sdk/10-settings.ts +0 -51
  159. package/examples/sdk/11-sessions.ts +0 -48
  160. package/examples/sdk/12-full-control.ts +0 -82
  161. package/examples/sdk/README.md +0 -145
@@ -122,6 +122,10 @@ export class InteractiveMode {
122
122
  // Epoch hardening
123
123
  uiEpoch = 0;
124
124
  sessionEpoch = 0;
125
+ isInitialMessagesRendered = false;
126
+ // Visual region ownership
127
+ statusOwner = undefined;
128
+ promptOwner = undefined;
125
129
  // Extension UI state
126
130
  extensionSelector = undefined;
127
131
  extensionInput = undefined;
@@ -357,6 +361,7 @@ export class InteractiveMode {
357
361
  // Invalidate all stale async callbacks
358
362
  this.uiEpoch++;
359
363
  this.sessionEpoch++;
364
+ this.isInitialMessagesRendered = false;
360
365
  // Stop all loaders
361
366
  if (this.loadingAnimation) {
362
367
  this.loadingAnimation.dispose();
@@ -421,57 +426,63 @@ export class InteractiveMode {
421
426
  // Both are needed: fd for autocomplete, rg for grep tool and bash commands
422
427
  const [fdPath] = await Promise.all([ensureTool("fd"), ensureTool("rg")]);
423
428
  this.fdPath = fdPath;
429
+ // Add containers only if they're not already children
430
+ const addIfMissing = (parent, child) => {
431
+ if (!parent.children.includes(child)) {
432
+ parent.addChild(child);
433
+ }
434
+ };
424
435
  // Add header container as first child
425
- this.ui.addChild(this.headerContainer);
436
+ addIfMissing(this.ui, this.headerContainer);
426
437
  // Always show the branded header/logo, even when quietStartup is enabled
427
- this.headerContainer.addChild(this.header);
428
- this.headerContainer.addChild(new Spacer(1));
438
+ addIfMissing(this.headerContainer, this.header);
439
+ addIfMissing(this.headerContainer, new Spacer(1));
429
440
  // Add startup instructions only when not silenced
430
441
  if (this.options.verbose || !this.settingsManager.getQuietStartup()) {
431
442
  const instructions = this.buildStartupInstructionsText();
432
443
  this.builtInHeader = new Text(instructions, 1, 0);
433
- this.headerContainer.addChild(this.builtInHeader);
434
- this.headerContainer.addChild(new Spacer(1));
444
+ addIfMissing(this.headerContainer, this.builtInHeader);
445
+ addIfMissing(this.headerContainer, new Spacer(1));
435
446
  // Add changelog if provided
436
447
  if (this.changelogMarkdown) {
437
- this.headerContainer.addChild(new DynamicBorder());
448
+ addIfMissing(this.headerContainer, new DynamicBorder());
438
449
  if (this.settingsManager.getCollapseChangelog()) {
439
450
  const versionMatch = this.changelogMarkdown.match(/##\s+\[?(\d+\.\d+\.\d+)\]?/);
440
451
  const latestVersion = versionMatch ? versionMatch[1] : this.version;
441
452
  const condensedText = `Updated to v${latestVersion}. Use ${theme.bold("/changelog")} to view full changelog.`;
442
- this.headerContainer.addChild(new Text(condensedText, 1, 0));
453
+ addIfMissing(this.headerContainer, new Text(condensedText, 1, 0));
443
454
  }
444
455
  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));
456
+ addIfMissing(this.headerContainer, new Text(theme.bold(theme.fg("accent", "What's New")), 1, 0));
457
+ addIfMissing(this.headerContainer, new Spacer(1));
458
+ addIfMissing(this.headerContainer, new Markdown(this.changelogMarkdown.trim(), 1, 0, this.getMarkdownThemeWithSettings()));
459
+ addIfMissing(this.headerContainer, new Spacer(1));
449
460
  }
450
- this.headerContainer.addChild(new DynamicBorder());
461
+ addIfMissing(this.headerContainer, new DynamicBorder());
451
462
  }
452
463
  }
453
464
  else {
454
465
  // Quiet startup: keep the logo visible, but suppress instruction text
455
466
  this.builtInHeader = new Text("", 0, 0);
456
- this.headerContainer.addChild(this.builtInHeader);
467
+ addIfMissing(this.headerContainer, this.builtInHeader);
457
468
  if (this.changelogMarkdown) {
458
- this.headerContainer.addChild(new Spacer(1));
469
+ addIfMissing(this.headerContainer, new Spacer(1));
459
470
  const versionMatch = this.changelogMarkdown.match(/##\s+\[?(\d+\.\d+\.\d+)\]?/);
460
471
  const latestVersion = versionMatch ? versionMatch[1] : this.version;
461
472
  const condensedText = `Updated to v${latestVersion}. Use ${theme.bold("/changelog")} to view full changelog.`;
462
- this.headerContainer.addChild(new Text(condensedText, 1, 0));
473
+ addIfMissing(this.headerContainer, new Text(condensedText, 1, 0));
463
474
  }
464
475
  }
465
- this.ui.addChild(this.chatContainer);
466
- this.ui.addChild(this.pendingMessagesContainer);
467
- this.ui.addChild(this.statusContainer);
476
+ addIfMissing(this.ui, this.chatContainer);
477
+ addIfMissing(this.ui, this.pendingMessagesContainer);
478
+ addIfMissing(this.ui, this.statusContainer);
468
479
  this.renderWidgets(); // Initialize with default spacer
469
480
  // The prompt area consists of an optional widget strip, the editor, and a bottom widget strip.
470
481
  // 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);
482
+ addIfMissing(this.ui, this.widgetContainerAbove);
483
+ addIfMissing(this.ui, this.editorContainer);
484
+ addIfMissing(this.ui, this.widgetContainerBelow);
485
+ addIfMissing(this.ui, this.footer);
475
486
  this.ui.setFocus(this.editor);
476
487
  this.updatePromptChrome();
477
488
  this.setupKeyHandlers();
@@ -1210,7 +1221,7 @@ export class InteractiveMode {
1210
1221
  this.widgetContainerAbove.clear();
1211
1222
  this.widgetContainerBelow.clear();
1212
1223
  }
1213
- mountPromptOwner(component, options) {
1224
+ mountPromptOwner(component, owner, options) {
1214
1225
  const focus = options?.focus ?? component;
1215
1226
  const showWidgets = options?.showWidgets ?? false;
1216
1227
  const requestRender = options?.requestRender ?? true;
@@ -1218,6 +1229,7 @@ export class InteractiveMode {
1218
1229
  this.editorContainer.children.length === 1 &&
1219
1230
  this.editorContainer.children[0] === component;
1220
1231
  if (!isMounted) {
1232
+ this.promptOwner = owner;
1221
1233
  this.editorContainer.clear();
1222
1234
  this.editorContainer.addChild(component);
1223
1235
  this.promptAreaComponent = component;
@@ -1236,6 +1248,9 @@ export class InteractiveMode {
1236
1248
  }
1237
1249
  }
1238
1250
  restoreCanonicalEditor(options) {
1251
+ if (options?.owner !== undefined && this.promptOwner !== options.owner) {
1252
+ return;
1253
+ }
1239
1254
  if (options?.text !== undefined) {
1240
1255
  this.editor.setText(options.text);
1241
1256
  }
@@ -1244,6 +1259,7 @@ export class InteractiveMode {
1244
1259
  this.editorContainer.children.length === 1 &&
1245
1260
  this.editorContainer.children[0] === editorComponent;
1246
1261
  if (!isMounted) {
1262
+ this.promptOwner = undefined;
1247
1263
  this.editorContainer.clear();
1248
1264
  this.editorContainer.addChild(editorComponent);
1249
1265
  this.promptAreaComponent = editorComponent;
@@ -1264,12 +1280,13 @@ export class InteractiveMode {
1264
1280
  this.ui.requestRender();
1265
1281
  }
1266
1282
  }
1267
- mountStatusOwner(component, options) {
1283
+ mountStatusOwner(component, owner, options) {
1268
1284
  const requestRender = options?.requestRender ?? true;
1269
1285
  const isMounted = this.statusComponent === component &&
1270
1286
  this.statusContainer.children.length === 1 &&
1271
1287
  this.statusContainer.children[0] === component;
1272
1288
  if (!isMounted) {
1289
+ this.statusOwner = owner;
1273
1290
  this.statusContainer.clear();
1274
1291
  this.statusContainer.addChild(component);
1275
1292
  this.statusComponent = component;
@@ -1287,11 +1304,15 @@ export class InteractiveMode {
1287
1304
  const row = new Container();
1288
1305
  row.addChild(new Spacer(1));
1289
1306
  row.addChild(loader);
1290
- this.mountStatusOwner(row, options);
1307
+ this.mountStatusOwner(row, loader, options);
1291
1308
  }
1292
1309
  clearStatusOwner(options) {
1310
+ if (options?.owner !== undefined && this.statusOwner !== options.owner) {
1311
+ return;
1312
+ }
1293
1313
  const requestRender = options?.requestRender ?? true;
1294
1314
  if (this.statusComponent !== undefined || this.statusContainer.children.length > 0) {
1315
+ this.statusOwner = undefined;
1295
1316
  this.statusContainer.clear();
1296
1317
  this.statusComponent = undefined;
1297
1318
  }
@@ -1337,7 +1358,7 @@ export class InteractiveMode {
1337
1358
  return;
1338
1359
  }
1339
1360
  if (leadingSpacer) {
1340
- container.addChild(new Spacer(0));
1361
+ container.addChild(new Spacer(1));
1341
1362
  }
1342
1363
  if (builtInComponent) {
1343
1364
  container.addChild(builtInComponent);
@@ -1426,14 +1447,44 @@ export class InteractiveMode {
1426
1447
  * Create the ExtensionUIContext for extensions.
1427
1448
  */
1428
1449
  createExtensionUIContext() {
1450
+ const capturedUiEpoch = this.uiEpoch;
1451
+ const capturedSessionEpoch = this.sessionEpoch;
1452
+ const guard = (fn) => {
1453
+ return ((...args) => {
1454
+ if (this.uiEpoch !== capturedUiEpoch || this.sessionEpoch !== capturedSessionEpoch) {
1455
+ return undefined;
1456
+ }
1457
+ return fn(...args);
1458
+ });
1459
+ };
1429
1460
  return {
1430
- select: (title, options, opts) => this.showExtensionSelector(title, options, opts),
1431
- confirm: (title, message, opts) => this.showExtensionConfirm(title, message, opts),
1432
- input: (title, placeholder, opts) => this.showExtensionInput(title, placeholder, opts),
1433
- notify: (message, type) => this.showExtensionNotify(message, type),
1434
- onTerminalInput: (handler) => this.addExtensionTerminalInputListener(handler),
1435
- setStatus: (key, text) => this.setExtensionStatus(key, text),
1436
- setWorkingMessage: (message) => {
1461
+ select: (title, options, opts) => {
1462
+ if (this.uiEpoch !== capturedUiEpoch || this.sessionEpoch !== capturedSessionEpoch) {
1463
+ return Promise.resolve(undefined);
1464
+ }
1465
+ return this.showExtensionSelector(title, options, opts);
1466
+ },
1467
+ confirm: (title, message, opts) => {
1468
+ if (this.uiEpoch !== capturedUiEpoch || this.sessionEpoch !== capturedSessionEpoch) {
1469
+ return Promise.resolve(false);
1470
+ }
1471
+ return this.showExtensionConfirm(title, message, opts);
1472
+ },
1473
+ input: (title, placeholder, opts) => {
1474
+ if (this.uiEpoch !== capturedUiEpoch || this.sessionEpoch !== capturedSessionEpoch) {
1475
+ return Promise.resolve(undefined);
1476
+ }
1477
+ return this.showExtensionInput(title, placeholder, opts);
1478
+ },
1479
+ notify: guard((message, type) => this.showExtensionNotify(message, type)),
1480
+ onTerminalInput: (handler) => {
1481
+ if (this.uiEpoch !== capturedUiEpoch || this.sessionEpoch !== capturedSessionEpoch) {
1482
+ return () => { };
1483
+ }
1484
+ return this.addExtensionTerminalInputListener(handler);
1485
+ },
1486
+ setStatus: guard((key, text) => this.setExtensionStatus(key, text)),
1487
+ setWorkingMessage: guard((message) => {
1437
1488
  if (this.loadingAnimation) {
1438
1489
  if (message) {
1439
1490
  this.loadingAnimation.setMessage(message);
@@ -1446,23 +1497,41 @@ export class InteractiveMode {
1446
1497
  // Queue message for when loadingAnimation is created (handles agent_start race)
1447
1498
  this.pendingWorkingMessage = message;
1448
1499
  }
1500
+ }),
1501
+ setWidget: guard((key, content, options) => this.setExtensionWidget(key, content, options)),
1502
+ setFooter: guard((factory) => this.setExtensionFooter(factory)),
1503
+ setHeader: guard((factory) => this.setExtensionHeader(factory)),
1504
+ setTitle: guard((title) => this.ui.terminal.setTitle(title)),
1505
+ custom: (factory, options) => {
1506
+ if (this.uiEpoch !== capturedUiEpoch || this.sessionEpoch !== capturedSessionEpoch) {
1507
+ return Promise.resolve(undefined);
1508
+ }
1509
+ return this.showExtensionCustom(factory, options);
1510
+ },
1511
+ pasteToEditor: guard((text) => this.editor.handleInput(`\x1b[200~${text}\x1b[201~`)),
1512
+ setEditorText: guard((text) => this.editor.setText(text)),
1513
+ getEditorText: () => {
1514
+ if (this.uiEpoch !== capturedUiEpoch || this.sessionEpoch !== capturedSessionEpoch) {
1515
+ return "";
1516
+ }
1517
+ return this.editor.getText();
1449
1518
  },
1450
- setWidget: (key, content, options) => this.setExtensionWidget(key, content, options),
1451
- setFooter: (factory) => this.setExtensionFooter(factory),
1452
- setHeader: (factory) => this.setExtensionHeader(factory),
1453
- setTitle: (title) => this.ui.terminal.setTitle(title),
1454
- custom: (factory, options) => this.showExtensionCustom(factory, options),
1455
- pasteToEditor: (text) => this.editor.handleInput(`\x1b[200~${text}\x1b[201~`),
1456
- setEditorText: (text) => this.editor.setText(text),
1457
- getEditorText: () => this.editor.getText(),
1458
- editor: (title, prefill) => this.showExtensionEditor(title, prefill),
1459
- setEditorComponent: (factory) => this.setCustomEditorComponent(factory),
1519
+ editor: (title, prefill) => {
1520
+ if (this.uiEpoch !== capturedUiEpoch || this.sessionEpoch !== capturedSessionEpoch) {
1521
+ return Promise.resolve(undefined);
1522
+ }
1523
+ return this.showExtensionEditor(title, prefill);
1524
+ },
1525
+ setEditorComponent: guard((factory) => this.setCustomEditorComponent(factory)),
1460
1526
  get theme() {
1461
1527
  return theme;
1462
1528
  },
1463
1529
  getAllThemes: () => getAvailableThemesWithPaths(),
1464
1530
  getTheme: (name) => getThemeByName(name),
1465
1531
  setTheme: (themeOrName) => {
1532
+ if (this.uiEpoch !== capturedUiEpoch || this.sessionEpoch !== capturedSessionEpoch) {
1533
+ return { success: false, error: "Stale context" };
1534
+ }
1466
1535
  if (themeOrName instanceof Theme) {
1467
1536
  setThemeInstance(themeOrName);
1468
1537
  this.ui.requestRender();
@@ -1478,7 +1547,7 @@ export class InteractiveMode {
1478
1547
  return result;
1479
1548
  },
1480
1549
  getToolsExpanded: () => this.toolOutputExpanded,
1481
- setToolsExpanded: (expanded) => this.setToolsExpanded(expanded),
1550
+ setToolsExpanded: guard((expanded) => this.setToolsExpanded(expanded)),
1482
1551
  };
1483
1552
  }
1484
1553
  /**
@@ -1506,7 +1575,7 @@ export class InteractiveMode {
1506
1575
  resolve(undefined);
1507
1576
  return;
1508
1577
  }
1509
- this.hideExtensionSelector();
1578
+ this.hideExtensionSelector(this.extensionSelector);
1510
1579
  resolve(option);
1511
1580
  }, () => {
1512
1581
  opts?.signal?.removeEventListener("abort", onAbort);
@@ -1514,19 +1583,19 @@ export class InteractiveMode {
1514
1583
  resolve(undefined);
1515
1584
  return;
1516
1585
  }
1517
- this.hideExtensionSelector();
1586
+ this.hideExtensionSelector(this.extensionSelector);
1518
1587
  resolve(undefined);
1519
1588
  }, { tui: this.ui, timeout: opts?.timeout });
1520
- this.mountPromptOwner(this.extensionSelector);
1589
+ this.mountPromptOwner(this.extensionSelector, this.extensionSelector);
1521
1590
  });
1522
1591
  }
1523
1592
  /**
1524
1593
  * Hide the extension selector.
1525
1594
  */
1526
- hideExtensionSelector() {
1595
+ hideExtensionSelector(owner) {
1527
1596
  this.extensionSelector?.dispose();
1528
1597
  this.extensionSelector = undefined;
1529
- this.restoreCanonicalEditor();
1598
+ this.restoreCanonicalEditor({ owner });
1530
1599
  }
1531
1600
  /**
1532
1601
  * Show a confirmation dialog for extensions.
@@ -1550,7 +1619,7 @@ export class InteractiveMode {
1550
1619
  resolve(undefined);
1551
1620
  return;
1552
1621
  }
1553
- this.hideExtensionInput();
1622
+ this.hideExtensionInput(this.extensionInput);
1554
1623
  resolve(undefined);
1555
1624
  };
1556
1625
  opts?.signal?.addEventListener("abort", onAbort, { once: true });
@@ -1560,7 +1629,7 @@ export class InteractiveMode {
1560
1629
  resolve(undefined);
1561
1630
  return;
1562
1631
  }
1563
- this.hideExtensionInput();
1632
+ this.hideExtensionInput(this.extensionInput);
1564
1633
  resolve(value);
1565
1634
  }, () => {
1566
1635
  opts?.signal?.removeEventListener("abort", onAbort);
@@ -1568,19 +1637,19 @@ export class InteractiveMode {
1568
1637
  resolve(undefined);
1569
1638
  return;
1570
1639
  }
1571
- this.hideExtensionInput();
1640
+ this.hideExtensionInput(this.extensionInput);
1572
1641
  resolve(undefined);
1573
1642
  }, { tui: this.ui, timeout: opts?.timeout });
1574
- this.mountPromptOwner(this.extensionInput);
1643
+ this.mountPromptOwner(this.extensionInput, this.extensionInput);
1575
1644
  });
1576
1645
  }
1577
1646
  /**
1578
1647
  * Hide the extension input.
1579
1648
  */
1580
- hideExtensionInput() {
1649
+ hideExtensionInput(owner) {
1581
1650
  this.extensionInput?.dispose();
1582
1651
  this.extensionInput = undefined;
1583
- this.restoreCanonicalEditor();
1652
+ this.restoreCanonicalEditor({ owner });
1584
1653
  }
1585
1654
  /**
1586
1655
  * Show a multi-line editor for extensions (with Ctrl+G support).
@@ -1593,25 +1662,25 @@ export class InteractiveMode {
1593
1662
  resolve(undefined);
1594
1663
  return;
1595
1664
  }
1596
- this.hideExtensionEditor();
1665
+ this.hideExtensionEditor(this.extensionEditor);
1597
1666
  resolve(value);
1598
1667
  }, () => {
1599
1668
  if (this.uiEpoch !== capturedEpoch) {
1600
1669
  resolve(undefined);
1601
1670
  return;
1602
1671
  }
1603
- this.hideExtensionEditor();
1672
+ this.hideExtensionEditor(this.extensionEditor);
1604
1673
  resolve(undefined);
1605
1674
  });
1606
- this.mountPromptOwner(this.extensionEditor);
1675
+ this.mountPromptOwner(this.extensionEditor, this.extensionEditor);
1607
1676
  });
1608
1677
  }
1609
1678
  /**
1610
1679
  * Hide the extension editor.
1611
1680
  */
1612
- hideExtensionEditor() {
1681
+ hideExtensionEditor(owner) {
1613
1682
  this.extensionEditor = undefined;
1614
- this.restoreCanonicalEditor();
1683
+ this.restoreCanonicalEditor({ owner });
1615
1684
  }
1616
1685
  /**
1617
1686
  * Set a custom editor component from an extension.
@@ -1685,11 +1754,11 @@ export class InteractiveMode {
1685
1754
  const capturedEpoch = this.uiEpoch;
1686
1755
  const savedText = this.editor.getText();
1687
1756
  const isOverlay = options?.overlay ?? false;
1757
+ let component;
1688
1758
  const restoreEditor = () => {
1689
- this.restoreCanonicalEditor({ text: savedText });
1759
+ this.restoreCanonicalEditor({ text: savedText, owner: component });
1690
1760
  };
1691
1761
  return new Promise((resolve, reject) => {
1692
- let component;
1693
1762
  let closed = false;
1694
1763
  const close = (result) => {
1695
1764
  if (closed)
@@ -1744,7 +1813,7 @@ export class InteractiveMode {
1744
1813
  options?.onHandle?.(handle);
1745
1814
  }
1746
1815
  else {
1747
- this.mountPromptOwner(component);
1816
+ this.mountPromptOwner(component, component);
1748
1817
  }
1749
1818
  })
1750
1819
  .catch((err) => {
@@ -2059,8 +2128,13 @@ export class InteractiveMode {
2059
2128
  }
2060
2129
  if (this.loadingAnimation) {
2061
2130
  this.loadingAnimation.dispose();
2131
+ const oldLoader = this.loadingAnimation;
2132
+ this.loadingAnimation = undefined;
2133
+ this.clearStatusOwner({ requestRender: false, owner: oldLoader });
2134
+ }
2135
+ else {
2136
+ this.clearStatusOwner({ requestRender: false });
2062
2137
  }
2063
- this.clearStatusOwner({ requestRender: false });
2064
2138
  this.loadingAnimation = new Loader(this.ui, (spinner) => theme.fg("accent", spinner), (text) => theme.fg("muted", text), this.defaultWorkingMessage);
2065
2139
  this.mountStatusLoader(this.loadingAnimation, { requestRender: false });
2066
2140
  // Apply any pending working message queued before loader existed
@@ -2089,7 +2163,16 @@ export class InteractiveMode {
2089
2163
  this.ui.requestRender();
2090
2164
  }
2091
2165
  else if (event.message.role === "assistant") {
2092
- this.streamingComponent = new AssistantMessageComponent(undefined, this.hideThinkingBlock, this.getMarkdownThemeWithSettings(), 0, 1);
2166
+ // Check if we already have a streaming component for this message to prevent duplicates
2167
+ if (this.streamingComponent && this.streamingMessage === event.message) {
2168
+ return;
2169
+ }
2170
+ // If we have a different streaming component, something is wrong, but let's at least not leak it
2171
+ if (this.streamingComponent) {
2172
+ this.chatContainer.removeChild(this.streamingComponent);
2173
+ this.streamingComponent = undefined;
2174
+ }
2175
+ this.streamingComponent = new AssistantMessageComponent(undefined, this.hideThinkingBlock, this.getMarkdownThemeWithSettings());
2093
2176
  this.streamingMessage = event.message;
2094
2177
  this.chatContainer.addChild(this.streamingComponent);
2095
2178
  this.streamingComponent.updateContent(this.streamingMessage);
@@ -2192,8 +2275,9 @@ export class InteractiveMode {
2192
2275
  case "agent_end":
2193
2276
  if (this.loadingAnimation) {
2194
2277
  this.loadingAnimation.dispose();
2278
+ const oldLoader = this.loadingAnimation;
2195
2279
  this.loadingAnimation = undefined;
2196
- this.clearStatusOwner({ requestRender: false });
2280
+ this.clearStatusOwner({ requestRender: false, owner: oldLoader });
2197
2281
  }
2198
2282
  if (this.streamingComponent) {
2199
2283
  this.chatContainer.removeChild(this.streamingComponent);
@@ -2217,8 +2301,13 @@ export class InteractiveMode {
2217
2301
  this.defaultEditor.onEscape = () => {
2218
2302
  this.session.abortCompaction();
2219
2303
  };
2220
- // Show compacting indicator with reason
2304
+ // Stop loading animation
2305
+ if (this.loadingAnimation) {
2306
+ this.loadingAnimation.dispose();
2307
+ this.loadingAnimation = undefined;
2308
+ }
2221
2309
  this.clearStatusOwner({ requestRender: false });
2310
+ // Show compacting indicator with reason
2222
2311
  const reasonText = event.reason === "overflow" ? "Context overflow detected, " : "";
2223
2312
  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)`);
2224
2313
  this.mountStatusLoader(this.autoCompactionLoader, { requestRender: false });
@@ -2234,8 +2323,9 @@ export class InteractiveMode {
2234
2323
  // Stop loader
2235
2324
  if (this.autoCompactionLoader) {
2236
2325
  this.autoCompactionLoader.dispose();
2326
+ const oldLoader = this.autoCompactionLoader;
2237
2327
  this.autoCompactionLoader = undefined;
2238
- this.clearStatusOwner({ requestRender: false });
2328
+ this.clearStatusOwner({ requestRender: false, owner: oldLoader });
2239
2329
  }
2240
2330
  // Handle result
2241
2331
  if (event.aborted) {
@@ -2269,8 +2359,13 @@ export class InteractiveMode {
2269
2359
  this.defaultEditor.onEscape = () => {
2270
2360
  this.session.abortRetry();
2271
2361
  };
2272
- // Show retry indicator
2362
+ // Stop loading animation
2363
+ if (this.loadingAnimation) {
2364
+ this.loadingAnimation.dispose();
2365
+ this.loadingAnimation = undefined;
2366
+ }
2273
2367
  this.clearStatusOwner({ requestRender: false });
2368
+ // Show retry indicator
2274
2369
  const delaySeconds = Math.round(event.delayMs / 1000);
2275
2370
  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)`);
2276
2371
  this.mountStatusLoader(this.retryLoader, { requestRender: false });
@@ -2286,8 +2381,9 @@ export class InteractiveMode {
2286
2381
  // Stop loader
2287
2382
  if (this.retryLoader) {
2288
2383
  this.retryLoader.dispose();
2384
+ const oldLoader = this.retryLoader;
2289
2385
  this.retryLoader = undefined;
2290
- this.clearStatusOwner({ requestRender: false });
2386
+ this.clearStatusOwner({ requestRender: false, owner: oldLoader });
2291
2387
  }
2292
2388
  // Show error only on final failure (success shows normal response)
2293
2389
  if (!event.success) {
@@ -2313,7 +2409,9 @@ export class InteractiveMode {
2313
2409
  * If multiple status messages are emitted back-to-back (without anything else being added to the chat),
2314
2410
  * we update the previous status line instead of appending new ones to avoid log spam.
2315
2411
  */
2316
- showStatus(message) {
2412
+ showStatus(message, ownerEpoch) {
2413
+ if (ownerEpoch !== undefined && this.sessionEpoch !== ownerEpoch)
2414
+ return;
2317
2415
  const children = this.chatContainer.children;
2318
2416
  const last = children.length > 0 ? children[children.length - 1] : undefined;
2319
2417
  const secondLast = children.length > 1 ? children[children.length - 2] : undefined;
@@ -2391,7 +2489,7 @@ export class InteractiveMode {
2391
2489
  break;
2392
2490
  }
2393
2491
  case "assistant": {
2394
- const assistantComponent = new AssistantMessageComponent(message, this.hideThinkingBlock, this.getMarkdownThemeWithSettings(), 0, 1);
2492
+ const assistantComponent = new AssistantMessageComponent(message, this.hideThinkingBlock, this.getMarkdownThemeWithSettings());
2395
2493
  this.chatContainer.addChild(assistantComponent);
2396
2494
  break;
2397
2495
  }
@@ -2463,6 +2561,9 @@ export class InteractiveMode {
2463
2561
  this.ui.requestRender();
2464
2562
  }
2465
2563
  renderInitialMessages() {
2564
+ if (this.isInitialMessagesRendered)
2565
+ return;
2566
+ this.isInitialMessagesRendered = true;
2466
2567
  // Get aligned messages and entries from session context
2467
2568
  const context = this.sessionManager.buildSessionContext();
2468
2569
  this.renderSessionContext(context, {
@@ -2747,7 +2848,9 @@ export class InteractiveMode {
2747
2848
  this.editor.setText("");
2748
2849
  this.ui.requestRender();
2749
2850
  }
2750
- showError(errorMessage) {
2851
+ showError(errorMessage, ownerEpoch) {
2852
+ if (ownerEpoch !== undefined && this.sessionEpoch !== ownerEpoch)
2853
+ return;
2751
2854
  this.chatContainer.addChild(new Spacer(1));
2752
2855
  this.chatContainer.addChild(new Text(theme.fg("error", `Error: ${errorMessage}`), 1, 0));
2753
2856
  this.ui.requestRender();
@@ -2772,13 +2875,17 @@ export class InteractiveMode {
2772
2875
  this.ui.invalidate();
2773
2876
  this.ui.requestRender();
2774
2877
  }
2775
- showWarning(warningMessage) {
2878
+ showWarning(warningMessage, ownerEpoch) {
2879
+ if (ownerEpoch !== undefined && this.sessionEpoch !== ownerEpoch)
2880
+ return;
2776
2881
  this.enqueueStartupNotice(() => {
2777
2882
  this.chatContainer.addChild(new Spacer(1));
2778
2883
  this.chatContainer.addChild(new Text(theme.fg("warning", `Warning: ${warningMessage}`), 1, 0));
2779
2884
  });
2780
2885
  }
2781
- showNewVersionNotification(newVersion) {
2886
+ showNewVersionNotification(newVersion, ownerEpoch) {
2887
+ if (ownerEpoch !== undefined && this.sessionEpoch !== ownerEpoch)
2888
+ return;
2782
2889
  const action = theme.fg("accent", getUpdateInstruction(PACKAGE_NAME));
2783
2890
  const updateInstruction = theme.fg("muted", `New version ${newVersion} is available. `) + action;
2784
2891
  const changelogUrl = theme.fg("accent", "https://github.com/apholdings/jensen-code/blob/main/packages/coding-agent/CHANGELOG.md");
@@ -2985,10 +3092,10 @@ export class InteractiveMode {
2985
3092
  const done = () => {
2986
3093
  if (this.uiEpoch !== capturedEpoch)
2987
3094
  return;
2988
- this.restoreCanonicalEditor({ requestRender: false });
3095
+ this.restoreCanonicalEditor({ requestRender: false, owner: component });
2989
3096
  };
2990
3097
  const { component, focus } = create(done);
2991
- this.mountPromptOwner(component, { focus });
3098
+ this.mountPromptOwner(component, component, { focus });
2992
3099
  }
2993
3100
  showSettingsSelector() {
2994
3101
  this.showSelector((done) => {
@@ -3436,7 +3543,7 @@ export class InteractiveMode {
3436
3543
  finally {
3437
3544
  if (summaryLoader) {
3438
3545
  summaryLoader.dispose();
3439
- this.clearStatusOwner({ requestRender: false });
3546
+ this.clearStatusOwner({ requestRender: false, owner: summaryLoader });
3440
3547
  }
3441
3548
  this.defaultEditor.onEscape = originalOnEscape;
3442
3549
  }
@@ -3537,7 +3644,7 @@ export class InteractiveMode {
3537
3644
  // Completion handled below
3538
3645
  });
3539
3646
  // Show dialog in editor container
3540
- this.mountPromptOwner(dialog);
3647
+ this.mountPromptOwner(dialog, dialog);
3541
3648
  // Promise for manual code input (racing with callback server)
3542
3649
  let manualCodeResolve;
3543
3650
  let manualCodeReject;
@@ -3550,7 +3657,7 @@ export class InteractiveMode {
3550
3657
  const restoreEditor = () => {
3551
3658
  if (this.uiEpoch !== capturedEpoch)
3552
3659
  return;
3553
- this.restoreCanonicalEditor();
3660
+ this.restoreCanonicalEditor({ owner: dialog });
3554
3661
  };
3555
3662
  try {
3556
3663
  await this.session.modelRegistry.authStorage.login(providerId, {
@@ -3623,13 +3730,13 @@ export class InteractiveMode {
3623
3730
  cancellable: false,
3624
3731
  });
3625
3732
  const previousEditor = this.editor;
3626
- this.mountPromptOwner(loader);
3733
+ this.mountPromptOwner(loader, loader);
3627
3734
  const capturedEpoch = this.uiEpoch;
3628
3735
  const dismissLoader = (_editor) => {
3629
3736
  loader.dispose();
3630
3737
  if (this.uiEpoch !== capturedEpoch)
3631
3738
  return;
3632
- this.restoreCanonicalEditor();
3739
+ this.restoreCanonicalEditor({ owner: loader });
3633
3740
  };
3634
3741
  try {
3635
3742
  await this.session.reload();
@@ -3727,12 +3834,12 @@ export class InteractiveMode {
3727
3834
  }
3728
3835
  // Show cancellable loader, replacing the editor
3729
3836
  const loader = new BorderedLoader(this.ui, theme, "Creating gist...");
3730
- this.mountPromptOwner(loader);
3837
+ this.mountPromptOwner(loader, loader);
3731
3838
  const restoreEditor = () => {
3732
3839
  loader.dispose();
3733
3840
  if (this.uiEpoch !== capturedEpoch)
3734
3841
  return;
3735
- this.restoreCanonicalEditor({ requestRender: false });
3842
+ this.restoreCanonicalEditor({ requestRender: false, owner: loader });
3736
3843
  try {
3737
3844
  fs.unlinkSync(tmpFile);
3738
3845
  }
@@ -4008,7 +4115,10 @@ export class InteractiveMode {
4008
4115
  }
4009
4116
  async handleClearCommand() {
4010
4117
  // New session via session (emits extension session events)
4011
- await this.session.newSession();
4118
+ const success = await this.session.newSession();
4119
+ if (!success) {
4120
+ return;
4121
+ }
4012
4122
  this.resetInteractiveSessionUI(true);
4013
4123
  this.ui.requestRender();
4014
4124
  }
@@ -4185,7 +4295,7 @@ export class InteractiveMode {
4185
4295
  finally {
4186
4296
  if (this.sessionEpoch === capturedEpoch) {
4187
4297
  compactingLoader.dispose();
4188
- this.clearStatusOwner({ requestRender: false });
4298
+ this.clearStatusOwner({ requestRender: false, owner: compactingLoader });
4189
4299
  this.defaultEditor.onEscape = originalOnEscape;
4190
4300
  }
4191
4301
  }
@@ -4199,6 +4309,14 @@ export class InteractiveMode {
4199
4309
  this.loadingAnimation.dispose();
4200
4310
  this.loadingAnimation = undefined;
4201
4311
  }
4312
+ if (this.autoCompactionLoader) {
4313
+ this.autoCompactionLoader.dispose();
4314
+ this.autoCompactionLoader = undefined;
4315
+ }
4316
+ if (this.retryLoader) {
4317
+ this.retryLoader.dispose();
4318
+ this.retryLoader = undefined;
4319
+ }
4202
4320
  this.clearExtensionTerminalInputListeners();
4203
4321
  this.footer.dispose();
4204
4322
  this.footerDataProvider.dispose();