@apholdings/jensen-code 0.0.3 → 0.0.5
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.
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +6 -6
- package/dist/cli/args.js.map +1 -1
- package/dist/config.d.ts +6 -5
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +32 -25
- package/dist/config.js.map +1 -1
- package/dist/core/agent-session.d.ts +1 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +25 -0
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +1 -1
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/footer-data-provider.d.ts +4 -1
- package/dist/core/footer-data-provider.d.ts.map +1 -1
- package/dist/core/footer-data-provider.js +25 -11
- package/dist/core/footer-data-provider.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/modes/interactive/components/custom-editor.d.ts +1 -0
- package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
- package/dist/modes/interactive/components/custom-editor.js +5 -0
- package/dist/modes/interactive/components/custom-editor.js.map +1 -1
- package/dist/modes/interactive/components/footer.d.ts +0 -2
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +8 -146
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/components/header.d.ts +9 -3
- package/dist/modes/interactive/components/header.d.ts.map +1 -1
- package/dist/modes/interactive/components/header.js +125 -196
- package/dist/modes/interactive/components/header.js.map +1 -1
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +1 -2
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +23 -4
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +657 -243
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
- package/dist/modes/interactive/theme/theme.js +2 -0
- package/dist/modes/interactive/theme/theme.js.map +1 -1
- package/dist/utils/frontmatter.d.ts.map +1 -1
- package/dist/utils/frontmatter.js +8 -4
- package/dist/utils/frontmatter.js.map +1 -1
- package/dist/utils/tools-manager.d.ts.map +1 -1
- package/dist/utils/tools-manager.js +2 -2
- package/dist/utils/tools-manager.js.map +1 -1
- package/examples/extensions/osgrep.ts +643 -0
- package/examples/extensions/subagent/agents.ts +150 -38
- package/examples/extensions/subagent/index.ts +634 -514
- package/package.json +4 -3
- package/examples/README.md +0 -25
- package/examples/extensions/README.md +0 -206
- package/examples/extensions/antigravity-image-gen.ts +0 -416
- package/examples/extensions/auto-commit-on-exit.ts +0 -50
- package/examples/extensions/bash-spawn-hook.ts +0 -31
- package/examples/extensions/bookmark.ts +0 -51
- package/examples/extensions/built-in-tool-renderer.ts +0 -247
- package/examples/extensions/claude-rules.ts +0 -87
- package/examples/extensions/commands.ts +0 -73
- package/examples/extensions/confirm-destructive.ts +0 -60
- package/examples/extensions/custom-compaction.ts +0 -115
- package/examples/extensions/custom-footer.ts +0 -65
- package/examples/extensions/custom-header.ts +0 -74
- package/examples/extensions/custom-provider-anthropic/index.ts +0 -605
- package/examples/extensions/custom-provider-anthropic/package-lock.json +0 -24
- package/examples/extensions/custom-provider-anthropic/package.json +0 -19
- package/examples/extensions/custom-provider-gitlab-duo/index.ts +0 -350
- package/examples/extensions/custom-provider-gitlab-duo/package.json +0 -16
- package/examples/extensions/custom-provider-gitlab-duo/test.ts +0 -82
- package/examples/extensions/custom-provider-qwen-cli/index.ts +0 -346
- package/examples/extensions/custom-provider-qwen-cli/package.json +0 -16
- package/examples/extensions/dirty-repo-guard.ts +0 -57
- package/examples/extensions/doom-overlay/README.md +0 -46
- package/examples/extensions/doom-overlay/doom/build/doom.js +0 -21
- package/examples/extensions/doom-overlay/doom/build/doom.wasm +0 -0
- package/examples/extensions/doom-overlay/doom/build.sh +0 -152
- package/examples/extensions/doom-overlay/doom/doomgeneric_pi.c +0 -72
- package/examples/extensions/doom-overlay/doom-component.ts +0 -132
- package/examples/extensions/doom-overlay/doom-engine.ts +0 -173
- package/examples/extensions/doom-overlay/doom-keys.ts +0 -104
- package/examples/extensions/doom-overlay/index.ts +0 -75
- package/examples/extensions/doom-overlay/wad-finder.ts +0 -51
- package/examples/extensions/dynamic-resources/SKILL.md +0 -8
- package/examples/extensions/dynamic-resources/dynamic.json +0 -79
- package/examples/extensions/dynamic-resources/dynamic.md +0 -5
- package/examples/extensions/dynamic-resources/index.ts +0 -16
- package/examples/extensions/dynamic-tools.ts +0 -75
- package/examples/extensions/event-bus.ts +0 -44
- package/examples/extensions/file-trigger.ts +0 -42
- package/examples/extensions/git-checkpoint.ts +0 -54
- package/examples/extensions/handoff.ts +0 -151
- package/examples/extensions/hello.ts +0 -26
- package/examples/extensions/inline-bash.ts +0 -95
- package/examples/extensions/input-transform.ts +0 -44
- package/examples/extensions/interactive-shell.ts +0 -197
- package/examples/extensions/mac-system-theme.ts +0 -48
- package/examples/extensions/message-renderer.ts +0 -60
- package/examples/extensions/minimal-mode.ts +0 -427
- package/examples/extensions/modal-editor.ts +0 -86
- package/examples/extensions/model-status.ts +0 -32
- package/examples/extensions/notify.ts +0 -56
- package/examples/extensions/overlay-qa-tests.ts +0 -1349
- package/examples/extensions/overlay-test.ts +0 -151
- package/examples/extensions/permission-gate.ts +0 -35
- package/examples/extensions/pirate.ts +0 -48
- package/examples/extensions/plan-mode/README.md +0 -65
- package/examples/extensions/plan-mode/index.ts +0 -341
- package/examples/extensions/plan-mode/utils.ts +0 -168
- package/examples/extensions/preset.ts +0 -399
- package/examples/extensions/protected-paths.ts +0 -31
- package/examples/extensions/provider-payload.ts +0 -15
- package/examples/extensions/qna.ts +0 -120
- package/examples/extensions/question.ts +0 -265
- package/examples/extensions/questionnaire.ts +0 -428
- package/examples/extensions/rainbow-editor.ts +0 -89
- package/examples/extensions/reload-runtime.ts +0 -38
- package/examples/extensions/rpc-demo.ts +0 -125
- package/examples/extensions/sandbox/index.ts +0 -319
- package/examples/extensions/sandbox/package-lock.json +0 -92
- package/examples/extensions/sandbox/package.json +0 -19
- package/examples/extensions/send-user-message.ts +0 -98
- package/examples/extensions/session-name.ts +0 -28
- package/examples/extensions/shutdown-command.ts +0 -64
- package/examples/extensions/snake.ts +0 -344
- package/examples/extensions/space-invaders.ts +0 -561
- package/examples/extensions/ssh.ts +0 -221
- package/examples/extensions/status-line.ts +0 -41
- package/examples/extensions/subagent/README.md +0 -172
- package/examples/extensions/subagent/agents/planner.md +0 -37
- package/examples/extensions/subagent/agents/reviewer.md +0 -35
- package/examples/extensions/subagent/agents/scout.md +0 -50
- package/examples/extensions/subagent/agents/worker.md +0 -24
- package/examples/extensions/subagent/prompts/implement-and-review.md +0 -10
- package/examples/extensions/subagent/prompts/implement.md +0 -10
- package/examples/extensions/subagent/prompts/scout-and-plan.md +0 -9
- package/examples/extensions/summarize.ts +0 -196
- package/examples/extensions/system-prompt-header.ts +0 -18
- package/examples/extensions/timed-confirm.ts +0 -71
- package/examples/extensions/titlebar-spinner.ts +0 -59
- package/examples/extensions/todo.ts +0 -300
- package/examples/extensions/tool-override.ts +0 -144
- package/examples/extensions/tools.ts +0 -147
- package/examples/extensions/trigger-compact.ts +0 -41
- package/examples/extensions/truncated-tool.ts +0 -193
- package/examples/extensions/widget-placement.ts +0 -18
- package/examples/extensions/with-deps/index.ts +0 -33
- package/examples/extensions/with-deps/package-lock.json +0 -31
- package/examples/extensions/with-deps/package.json +0 -22
- package/examples/rpc-extension-ui.ts +0 -632
- package/examples/sdk/01-minimal.ts +0 -23
- package/examples/sdk/02-custom-model.ts +0 -50
- package/examples/sdk/03-custom-prompt.ts +0 -56
- package/examples/sdk/04-skills.ts +0 -47
- package/examples/sdk/05-tools.ts +0 -57
- package/examples/sdk/06-extensions.ts +0 -89
- package/examples/sdk/07-context-files.ts +0 -41
- package/examples/sdk/08-prompt-templates.ts +0 -48
- package/examples/sdk/09-api-keys-and-oauth.ts +0 -49
- package/examples/sdk/10-settings.ts +0 -52
- package/examples/sdk/11-sessions.ts +0 -49
- package/examples/sdk/12-full-control.ts +0 -83
- package/examples/sdk/README.md +0 -145
|
@@ -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,13 @@ export class InteractiveMode {
|
|
|
113
119
|
compactionQueuedMessages = [];
|
|
114
120
|
// Shutdown state
|
|
115
121
|
shutdownRequested = false;
|
|
122
|
+
// Epoch hardening
|
|
123
|
+
uiEpoch = 0;
|
|
124
|
+
sessionEpoch = 0;
|
|
125
|
+
isInitialMessagesRendered = false;
|
|
126
|
+
// Visual region ownership
|
|
127
|
+
statusOwner = undefined;
|
|
128
|
+
promptOwner = undefined;
|
|
116
129
|
// Extension UI state
|
|
117
130
|
extensionSelector = undefined;
|
|
118
131
|
extensionInput = undefined;
|
|
@@ -163,7 +176,8 @@ export class InteractiveMode {
|
|
|
163
176
|
getThinkingLevel: () => this.session.thinkingLevel || "off",
|
|
164
177
|
keybindings: this.keybindings,
|
|
165
178
|
});
|
|
166
|
-
this.
|
|
179
|
+
this.footerDataProvider = new FooterDataProvider();
|
|
180
|
+
this.header = new Header(this.session, this.footerDataProvider);
|
|
167
181
|
const editorPaddingX = this.settingsManager.getEditorPaddingX();
|
|
168
182
|
const autocompleteMaxVisible = this.settingsManager.getAutocompleteMaxVisible();
|
|
169
183
|
this.defaultEditor = new CustomEditor(this.ui, getEditorTheme(), this.keybindings, {
|
|
@@ -177,7 +191,7 @@ export class InteractiveMode {
|
|
|
177
191
|
this.editor = this.defaultEditor;
|
|
178
192
|
this.editorContainer = new Container();
|
|
179
193
|
this.editorContainer.addChild(this.editor);
|
|
180
|
-
this.
|
|
194
|
+
this.promptAreaComponent = this.editor;
|
|
181
195
|
this.footer = new FooterComponent(session, this.footerDataProvider);
|
|
182
196
|
this.footer.setAutoCompactEnabled(session.autoCompactionEnabled);
|
|
183
197
|
// Load hide thinking block setting
|
|
@@ -343,10 +357,44 @@ export class InteractiveMode {
|
|
|
343
357
|
this.chatContainer.addChild(helpContainer);
|
|
344
358
|
this.ui.requestRender();
|
|
345
359
|
}
|
|
346
|
-
|
|
360
|
+
resetInteractiveSessionUI(renderInitialMessages = false) {
|
|
361
|
+
// Invalidate all stale async callbacks
|
|
362
|
+
this.uiEpoch++;
|
|
363
|
+
this.sessionEpoch++;
|
|
364
|
+
this.isInitialMessagesRendered = false;
|
|
365
|
+
// Stop all loaders
|
|
366
|
+
if (this.loadingAnimation) {
|
|
367
|
+
this.loadingAnimation.dispose();
|
|
368
|
+
this.loadingAnimation = undefined;
|
|
369
|
+
}
|
|
370
|
+
if (this.autoCompactionLoader) {
|
|
371
|
+
this.autoCompactionLoader.dispose();
|
|
372
|
+
this.autoCompactionLoader = undefined;
|
|
373
|
+
}
|
|
374
|
+
if (this.retryLoader) {
|
|
375
|
+
this.retryLoader.dispose();
|
|
376
|
+
this.retryLoader = undefined;
|
|
377
|
+
}
|
|
378
|
+
// Clean up any transient prompt UI
|
|
379
|
+
if (this.extensionSelector) {
|
|
380
|
+
this.extensionSelector.dispose();
|
|
381
|
+
this.extensionSelector = undefined;
|
|
382
|
+
}
|
|
383
|
+
if (this.extensionInput) {
|
|
384
|
+
this.extensionInput.dispose();
|
|
385
|
+
this.extensionInput = undefined;
|
|
386
|
+
}
|
|
387
|
+
if (this.extensionEditor) {
|
|
388
|
+
this.extensionEditor = undefined;
|
|
389
|
+
}
|
|
390
|
+
this.ui.hideOverlay();
|
|
347
391
|
this.clearChatContainer();
|
|
348
392
|
this.pendingMessagesContainer.clear();
|
|
393
|
+
this.clearStatusOwner({ requestRender: false });
|
|
394
|
+
// Force empty the status container directly in case there are other elements left behind
|
|
349
395
|
this.statusContainer.clear();
|
|
396
|
+
this.statusComponent = undefined;
|
|
397
|
+
this.restoreCanonicalEditor({ requestRender: false, text: "" });
|
|
350
398
|
this.compactionQueuedMessages = [];
|
|
351
399
|
this.streamingComponent = undefined;
|
|
352
400
|
this.streamingMessage = undefined;
|
|
@@ -355,7 +403,8 @@ export class InteractiveMode {
|
|
|
355
403
|
this.bashComponent = undefined;
|
|
356
404
|
this.pendingWorkingMessage = undefined;
|
|
357
405
|
this.isBashMode = false;
|
|
358
|
-
|
|
406
|
+
// Ensure clean widget state
|
|
407
|
+
this.renderWidgets(false);
|
|
359
408
|
this.updatePromptChrome();
|
|
360
409
|
if (renderInitialMessages) {
|
|
361
410
|
this.renderInitialMessages();
|
|
@@ -365,96 +414,114 @@ export class InteractiveMode {
|
|
|
365
414
|
async init() {
|
|
366
415
|
if (this.isInitialized)
|
|
367
416
|
return;
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
this.headerContainer
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
417
|
+
if (this.initializing)
|
|
418
|
+
return this.initializing;
|
|
419
|
+
this.initializing = (async () => {
|
|
420
|
+
try {
|
|
421
|
+
if (this.isInitialized)
|
|
422
|
+
return;
|
|
423
|
+
// Load changelog (only show new entries, skip for resumed sessions)
|
|
424
|
+
this.changelogMarkdown = this.getChangelogForDisplay();
|
|
425
|
+
// Ensure fd and rg are available (downloads if missing, adds to PATH via getBinDir)
|
|
426
|
+
// Both are needed: fd for autocomplete, rg for grep tool and bash commands
|
|
427
|
+
const [fdPath] = await Promise.all([ensureTool("fd"), ensureTool("rg")]);
|
|
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
|
+
};
|
|
435
|
+
// Add header container as first child
|
|
436
|
+
addIfMissing(this.ui, this.headerContainer);
|
|
437
|
+
// Always show the branded header/logo, even when quietStartup is enabled
|
|
438
|
+
addIfMissing(this.headerContainer, this.header);
|
|
439
|
+
addIfMissing(this.headerContainer, new Spacer(1));
|
|
440
|
+
// Add startup instructions only when not silenced
|
|
441
|
+
if (this.options.verbose || !this.settingsManager.getQuietStartup()) {
|
|
442
|
+
const instructions = this.buildStartupInstructionsText();
|
|
443
|
+
this.builtInHeader = new Text(instructions, 1, 0);
|
|
444
|
+
addIfMissing(this.headerContainer, this.builtInHeader);
|
|
445
|
+
addIfMissing(this.headerContainer, new Spacer(1));
|
|
446
|
+
// Add changelog if provided
|
|
447
|
+
if (this.changelogMarkdown) {
|
|
448
|
+
addIfMissing(this.headerContainer, new DynamicBorder());
|
|
449
|
+
if (this.settingsManager.getCollapseChangelog()) {
|
|
450
|
+
const versionMatch = this.changelogMarkdown.match(/##\s+\[?(\d+\.\d+\.\d+)\]?/);
|
|
451
|
+
const latestVersion = versionMatch ? versionMatch[1] : this.version;
|
|
452
|
+
const condensedText = `Updated to v${latestVersion}. Use ${theme.bold("/changelog")} to view full changelog.`;
|
|
453
|
+
addIfMissing(this.headerContainer, new Text(condensedText, 1, 0));
|
|
454
|
+
}
|
|
455
|
+
else {
|
|
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));
|
|
460
|
+
}
|
|
461
|
+
addIfMissing(this.headerContainer, new DynamicBorder());
|
|
462
|
+
}
|
|
393
463
|
}
|
|
394
464
|
else {
|
|
395
|
-
|
|
396
|
-
this.
|
|
397
|
-
this.headerContainer
|
|
398
|
-
this.
|
|
465
|
+
// Quiet startup: keep the logo visible, but suppress instruction text
|
|
466
|
+
this.builtInHeader = new Text("", 0, 0);
|
|
467
|
+
addIfMissing(this.headerContainer, this.builtInHeader);
|
|
468
|
+
if (this.changelogMarkdown) {
|
|
469
|
+
addIfMissing(this.headerContainer, new Spacer(1));
|
|
470
|
+
const versionMatch = this.changelogMarkdown.match(/##\s+\[?(\d+\.\d+\.\d+)\]?/);
|
|
471
|
+
const latestVersion = versionMatch ? versionMatch[1] : this.version;
|
|
472
|
+
const condensedText = `Updated to v${latestVersion}. Use ${theme.bold("/changelog")} to view full changelog.`;
|
|
473
|
+
addIfMissing(this.headerContainer, new Text(condensedText, 1, 0));
|
|
474
|
+
}
|
|
399
475
|
}
|
|
400
|
-
this.
|
|
476
|
+
addIfMissing(this.ui, this.chatContainer);
|
|
477
|
+
addIfMissing(this.ui, this.pendingMessagesContainer);
|
|
478
|
+
addIfMissing(this.ui, this.statusContainer);
|
|
479
|
+
this.renderWidgets(); // Initialize with default spacer
|
|
480
|
+
// The prompt area consists of an optional widget strip, the editor, and a bottom widget strip.
|
|
481
|
+
// Extensions should use ctx.ui.setWidget(..., { placement: "aboveEditor" }) for the top strip.
|
|
482
|
+
addIfMissing(this.ui, this.widgetContainerAbove);
|
|
483
|
+
addIfMissing(this.ui, this.editorContainer);
|
|
484
|
+
addIfMissing(this.ui, this.widgetContainerBelow);
|
|
485
|
+
addIfMissing(this.ui, this.footer);
|
|
486
|
+
this.ui.setFocus(this.editor);
|
|
487
|
+
this.updatePromptChrome();
|
|
488
|
+
this.setupKeyHandlers();
|
|
489
|
+
this.setupEditorSubmitHandler();
|
|
490
|
+
// Initialize extensions first so resources are shown before messages
|
|
491
|
+
await this.initExtensions();
|
|
492
|
+
// Render initial messages AFTER showing loaded resources
|
|
493
|
+
this.renderInitialMessages();
|
|
494
|
+
// Start the UI
|
|
495
|
+
this.ui.start();
|
|
496
|
+
this.isInitialized = true;
|
|
497
|
+
// Set terminal title
|
|
498
|
+
this.updateTerminalTitle();
|
|
499
|
+
// Subscribe to agent events
|
|
500
|
+
this.subscribeToAgent();
|
|
501
|
+
this.windowFocusChangeUnsubscribe = this.ui.onWindowFocusChange(() => {
|
|
502
|
+
this.updatePromptChrome();
|
|
503
|
+
});
|
|
504
|
+
this.focusTargetChangeUnsubscribe = this.ui.onFocusTargetChange(() => {
|
|
505
|
+
this.updatePromptChrome();
|
|
506
|
+
});
|
|
507
|
+
// Set up theme file watcher
|
|
508
|
+
onThemeChange(() => {
|
|
509
|
+
this.ui.invalidate();
|
|
510
|
+
this.updatePromptChrome();
|
|
511
|
+
this.ui.requestRender();
|
|
512
|
+
});
|
|
513
|
+
// Set up git branch watcher (uses provider instead of footer)
|
|
514
|
+
this.footerDataProvider.onBranchChange(() => {
|
|
515
|
+
this.ui.requestRender();
|
|
516
|
+
});
|
|
517
|
+
// Initialize available provider count for footer display
|
|
518
|
+
await this.updateAvailableProviderCount();
|
|
401
519
|
}
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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();
|
|
520
|
+
finally {
|
|
521
|
+
this.initializing = null;
|
|
522
|
+
}
|
|
523
|
+
})();
|
|
524
|
+
return this.initializing;
|
|
458
525
|
}
|
|
459
526
|
/**
|
|
460
527
|
* Update terminal title with session name and cwd.
|
|
@@ -929,22 +996,23 @@ export class InteractiveMode {
|
|
|
929
996
|
waitForIdle: () => this.session.agent.waitForIdle(),
|
|
930
997
|
newSession: async (options) => {
|
|
931
998
|
if (this.loadingAnimation) {
|
|
932
|
-
this.loadingAnimation.
|
|
999
|
+
this.loadingAnimation.dispose();
|
|
933
1000
|
this.loadingAnimation = undefined;
|
|
934
1001
|
}
|
|
935
|
-
this.
|
|
1002
|
+
this.clearStatusOwner({ requestRender: false });
|
|
936
1003
|
// Delegate to AgentSession (handles setup + agent state sync)
|
|
937
1004
|
const success = await this.session.newSession(options);
|
|
938
1005
|
if (!success) {
|
|
939
1006
|
return { cancelled: true };
|
|
940
1007
|
}
|
|
941
|
-
this.
|
|
1008
|
+
this.resetInteractiveSessionUI(true);
|
|
942
1009
|
this.ui.requestRender();
|
|
943
1010
|
return { cancelled: false };
|
|
944
1011
|
},
|
|
945
1012
|
fork: async (entryId) => {
|
|
1013
|
+
const capturedEpoch = this.sessionEpoch;
|
|
946
1014
|
const result = await this.session.fork(entryId);
|
|
947
|
-
if (result.cancelled) {
|
|
1015
|
+
if (result.cancelled || this.sessionEpoch !== capturedEpoch) {
|
|
948
1016
|
return { cancelled: true };
|
|
949
1017
|
}
|
|
950
1018
|
this.clearChatContainer();
|
|
@@ -954,13 +1022,14 @@ export class InteractiveMode {
|
|
|
954
1022
|
return { cancelled: false };
|
|
955
1023
|
},
|
|
956
1024
|
navigateTree: async (targetId, options) => {
|
|
1025
|
+
const capturedEpoch = this.sessionEpoch;
|
|
957
1026
|
const result = await this.session.navigateTree(targetId, {
|
|
958
1027
|
summarize: options?.summarize,
|
|
959
1028
|
customInstructions: options?.customInstructions,
|
|
960
1029
|
replaceInstructions: options?.replaceInstructions,
|
|
961
1030
|
label: options?.label,
|
|
962
1031
|
});
|
|
963
|
-
if (result.cancelled) {
|
|
1032
|
+
if (result.cancelled || this.sessionEpoch !== capturedEpoch) {
|
|
964
1033
|
return { cancelled: true };
|
|
965
1034
|
}
|
|
966
1035
|
this.clearChatContainer();
|
|
@@ -1140,17 +1209,143 @@ export class InteractiveMode {
|
|
|
1140
1209
|
this.loadingAnimation.setMessage(`${this.defaultWorkingMessage} (${appKey(this.keybindings, "interrupt")} to interrupt)`);
|
|
1141
1210
|
}
|
|
1142
1211
|
}
|
|
1212
|
+
setPromptWidgetsVisible(visible) {
|
|
1213
|
+
if (this.promptWidgetsVisible === visible) {
|
|
1214
|
+
return;
|
|
1215
|
+
}
|
|
1216
|
+
this.promptWidgetsVisible = visible;
|
|
1217
|
+
if (visible) {
|
|
1218
|
+
this.renderWidgets(false);
|
|
1219
|
+
return;
|
|
1220
|
+
}
|
|
1221
|
+
this.widgetContainerAbove.clear();
|
|
1222
|
+
this.widgetContainerBelow.clear();
|
|
1223
|
+
}
|
|
1224
|
+
mountPromptOwner(component, owner, options) {
|
|
1225
|
+
const focus = options?.focus ?? component;
|
|
1226
|
+
const showWidgets = options?.showWidgets ?? false;
|
|
1227
|
+
const requestRender = options?.requestRender ?? true;
|
|
1228
|
+
const isMounted = this.promptAreaComponent === component &&
|
|
1229
|
+
this.editorContainer.children.length === 1 &&
|
|
1230
|
+
this.editorContainer.children[0] === component;
|
|
1231
|
+
if (!isMounted) {
|
|
1232
|
+
this.promptOwner = owner;
|
|
1233
|
+
this.editorContainer.clear();
|
|
1234
|
+
this.editorContainer.addChild(component);
|
|
1235
|
+
this.promptAreaComponent = component;
|
|
1236
|
+
}
|
|
1237
|
+
if (process.env.NODE_ENV !== "production") {
|
|
1238
|
+
if (this.editorContainer.children.length > 1) {
|
|
1239
|
+
throw new Error("Invariant violation: Multiple prompt owners mounted");
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
this.setPromptWidgetsVisible(showWidgets);
|
|
1243
|
+
if (this.ui.getFocusedComponent() !== focus) {
|
|
1244
|
+
this.ui.setFocus(focus);
|
|
1245
|
+
}
|
|
1246
|
+
if (requestRender) {
|
|
1247
|
+
this.ui.requestRender();
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
restoreCanonicalEditor(options) {
|
|
1251
|
+
if (options?.owner !== undefined && this.promptOwner !== options.owner) {
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1254
|
+
if (options?.text !== undefined) {
|
|
1255
|
+
this.editor.setText(options.text);
|
|
1256
|
+
}
|
|
1257
|
+
const editorComponent = this.editor;
|
|
1258
|
+
const isMounted = this.promptAreaComponent === editorComponent &&
|
|
1259
|
+
this.editorContainer.children.length === 1 &&
|
|
1260
|
+
this.editorContainer.children[0] === editorComponent;
|
|
1261
|
+
if (!isMounted) {
|
|
1262
|
+
this.promptOwner = undefined;
|
|
1263
|
+
this.editorContainer.clear();
|
|
1264
|
+
this.editorContainer.addChild(editorComponent);
|
|
1265
|
+
this.promptAreaComponent = editorComponent;
|
|
1266
|
+
}
|
|
1267
|
+
if (process.env.NODE_ENV !== "production") {
|
|
1268
|
+
if (this.editorContainer.children.length > 1) {
|
|
1269
|
+
throw new Error("Invariant violation: Multiple prompt owners mounted");
|
|
1270
|
+
}
|
|
1271
|
+
if (this.promptAreaComponent !== editorComponent) {
|
|
1272
|
+
throw new Error("Invariant violation: restoreCanonicalEditor mounted non-canonical editor");
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
this.setPromptWidgetsVisible(true);
|
|
1276
|
+
if (options?.focus ?? true) {
|
|
1277
|
+
this.ui.setFocus(editorComponent);
|
|
1278
|
+
}
|
|
1279
|
+
if (options?.requestRender ?? true) {
|
|
1280
|
+
this.ui.requestRender();
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
mountStatusOwner(component, owner, options) {
|
|
1284
|
+
const requestRender = options?.requestRender ?? true;
|
|
1285
|
+
const isMounted = this.statusComponent === component &&
|
|
1286
|
+
this.statusContainer.children.length === 1 &&
|
|
1287
|
+
this.statusContainer.children[0] === component;
|
|
1288
|
+
if (!isMounted) {
|
|
1289
|
+
this.statusOwner = owner;
|
|
1290
|
+
this.statusContainer.clear();
|
|
1291
|
+
this.statusContainer.addChild(component);
|
|
1292
|
+
this.statusComponent = component;
|
|
1293
|
+
}
|
|
1294
|
+
if (process.env.NODE_ENV !== "production") {
|
|
1295
|
+
if (this.statusContainer.children.length > 1) {
|
|
1296
|
+
throw new Error("Invariant violation: Multiple status owners mounted");
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
if (requestRender) {
|
|
1300
|
+
this.ui.requestRender();
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
mountStatusLoader(loader, options) {
|
|
1304
|
+
const row = new Container();
|
|
1305
|
+
row.addChild(new Spacer(1));
|
|
1306
|
+
row.addChild(loader);
|
|
1307
|
+
this.mountStatusOwner(row, loader, options);
|
|
1308
|
+
}
|
|
1309
|
+
clearStatusOwner(options) {
|
|
1310
|
+
if (options?.owner !== undefined && this.statusOwner !== options.owner) {
|
|
1311
|
+
return;
|
|
1312
|
+
}
|
|
1313
|
+
const requestRender = options?.requestRender ?? true;
|
|
1314
|
+
if (this.statusComponent !== undefined || this.statusContainer.children.length > 0) {
|
|
1315
|
+
this.statusOwner = undefined;
|
|
1316
|
+
this.statusContainer.clear();
|
|
1317
|
+
this.statusComponent = undefined;
|
|
1318
|
+
}
|
|
1319
|
+
if (process.env.NODE_ENV !== "production") {
|
|
1320
|
+
if (this.statusContainer.children.length > 0) {
|
|
1321
|
+
throw new Error("Invariant violation: Status container not empty after clear");
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
if (requestRender) {
|
|
1325
|
+
this.ui.requestRender();
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1143
1328
|
// Maximum total widget lines to prevent viewport overflow
|
|
1144
1329
|
static MAX_WIDGET_LINES = 10;
|
|
1145
1330
|
/**
|
|
1146
1331
|
* Render all extension widgets to the widget container.
|
|
1147
1332
|
*/
|
|
1148
|
-
renderWidgets() {
|
|
1333
|
+
renderWidgets(requestRender = true) {
|
|
1149
1334
|
if (!this.widgetContainerAbove || !this.widgetContainerBelow)
|
|
1150
1335
|
return;
|
|
1336
|
+
if (!this.promptWidgetsVisible) {
|
|
1337
|
+
this.widgetContainerAbove.clear();
|
|
1338
|
+
this.widgetContainerBelow.clear();
|
|
1339
|
+
if (requestRender) {
|
|
1340
|
+
this.ui.requestRender();
|
|
1341
|
+
}
|
|
1342
|
+
return;
|
|
1343
|
+
}
|
|
1151
1344
|
this.renderWidgetContainer(this.widgetContainerAbove, this.extensionWidgetsAbove, true, true, this.topBar);
|
|
1152
1345
|
this.renderWidgetContainer(this.widgetContainerBelow, this.extensionWidgetsBelow, false, false);
|
|
1153
|
-
|
|
1346
|
+
if (requestRender) {
|
|
1347
|
+
this.ui.requestRender();
|
|
1348
|
+
}
|
|
1154
1349
|
}
|
|
1155
1350
|
renderWidgetContainer(container, widgets, spacerWhenEmpty, leadingSpacer, builtInComponent) {
|
|
1156
1351
|
container.clear();
|
|
@@ -1163,7 +1358,7 @@ export class InteractiveMode {
|
|
|
1163
1358
|
return;
|
|
1164
1359
|
}
|
|
1165
1360
|
if (leadingSpacer) {
|
|
1166
|
-
container.addChild(new Spacer(
|
|
1361
|
+
container.addChild(new Spacer(1));
|
|
1167
1362
|
}
|
|
1168
1363
|
if (builtInComponent) {
|
|
1169
1364
|
container.addChild(builtInComponent);
|
|
@@ -1252,14 +1447,44 @@ export class InteractiveMode {
|
|
|
1252
1447
|
* Create the ExtensionUIContext for extensions.
|
|
1253
1448
|
*/
|
|
1254
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
|
+
};
|
|
1255
1460
|
return {
|
|
1256
|
-
select: (title, options, opts) =>
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
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) => {
|
|
1263
1488
|
if (this.loadingAnimation) {
|
|
1264
1489
|
if (message) {
|
|
1265
1490
|
this.loadingAnimation.setMessage(message);
|
|
@@ -1272,23 +1497,41 @@ export class InteractiveMode {
|
|
|
1272
1497
|
// Queue message for when loadingAnimation is created (handles agent_start race)
|
|
1273
1498
|
this.pendingWorkingMessage = message;
|
|
1274
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();
|
|
1275
1518
|
},
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
getEditorText: () => this.editor.getText(),
|
|
1284
|
-
editor: (title, prefill) => this.showExtensionEditor(title, prefill),
|
|
1285
|
-
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)),
|
|
1286
1526
|
get theme() {
|
|
1287
1527
|
return theme;
|
|
1288
1528
|
},
|
|
1289
1529
|
getAllThemes: () => getAvailableThemesWithPaths(),
|
|
1290
1530
|
getTheme: (name) => getThemeByName(name),
|
|
1291
1531
|
setTheme: (themeOrName) => {
|
|
1532
|
+
if (this.uiEpoch !== capturedUiEpoch || this.sessionEpoch !== capturedSessionEpoch) {
|
|
1533
|
+
return { success: false, error: "Stale context" };
|
|
1534
|
+
}
|
|
1292
1535
|
if (themeOrName instanceof Theme) {
|
|
1293
1536
|
setThemeInstance(themeOrName);
|
|
1294
1537
|
this.ui.requestRender();
|
|
@@ -1304,48 +1547,55 @@ export class InteractiveMode {
|
|
|
1304
1547
|
return result;
|
|
1305
1548
|
},
|
|
1306
1549
|
getToolsExpanded: () => this.toolOutputExpanded,
|
|
1307
|
-
setToolsExpanded: (expanded) => this.setToolsExpanded(expanded),
|
|
1550
|
+
setToolsExpanded: guard((expanded) => this.setToolsExpanded(expanded)),
|
|
1308
1551
|
};
|
|
1309
1552
|
}
|
|
1310
1553
|
/**
|
|
1311
1554
|
* Show a selector for extensions.
|
|
1312
1555
|
*/
|
|
1313
1556
|
showExtensionSelector(title, options, opts) {
|
|
1557
|
+
const capturedEpoch = this.uiEpoch;
|
|
1314
1558
|
return new Promise((resolve) => {
|
|
1315
1559
|
if (opts?.signal?.aborted) {
|
|
1316
1560
|
resolve(undefined);
|
|
1317
1561
|
return;
|
|
1318
1562
|
}
|
|
1319
1563
|
const onAbort = () => {
|
|
1564
|
+
if (this.uiEpoch !== capturedEpoch) {
|
|
1565
|
+
resolve(undefined);
|
|
1566
|
+
return;
|
|
1567
|
+
}
|
|
1320
1568
|
this.hideExtensionSelector();
|
|
1321
1569
|
resolve(undefined);
|
|
1322
1570
|
};
|
|
1323
1571
|
opts?.signal?.addEventListener("abort", onAbort, { once: true });
|
|
1324
1572
|
this.extensionSelector = new ExtensionSelectorComponent(title, options, (option) => {
|
|
1325
1573
|
opts?.signal?.removeEventListener("abort", onAbort);
|
|
1326
|
-
this.
|
|
1574
|
+
if (this.uiEpoch !== capturedEpoch) {
|
|
1575
|
+
resolve(undefined);
|
|
1576
|
+
return;
|
|
1577
|
+
}
|
|
1578
|
+
this.hideExtensionSelector(this.extensionSelector);
|
|
1327
1579
|
resolve(option);
|
|
1328
1580
|
}, () => {
|
|
1329
1581
|
opts?.signal?.removeEventListener("abort", onAbort);
|
|
1330
|
-
this.
|
|
1582
|
+
if (this.uiEpoch !== capturedEpoch) {
|
|
1583
|
+
resolve(undefined);
|
|
1584
|
+
return;
|
|
1585
|
+
}
|
|
1586
|
+
this.hideExtensionSelector(this.extensionSelector);
|
|
1331
1587
|
resolve(undefined);
|
|
1332
1588
|
}, { tui: this.ui, timeout: opts?.timeout });
|
|
1333
|
-
this.
|
|
1334
|
-
this.editorContainer.addChild(this.extensionSelector);
|
|
1335
|
-
this.ui.setFocus(this.extensionSelector);
|
|
1336
|
-
this.ui.requestRender();
|
|
1589
|
+
this.mountPromptOwner(this.extensionSelector, this.extensionSelector);
|
|
1337
1590
|
});
|
|
1338
1591
|
}
|
|
1339
1592
|
/**
|
|
1340
1593
|
* Hide the extension selector.
|
|
1341
1594
|
*/
|
|
1342
|
-
hideExtensionSelector() {
|
|
1595
|
+
hideExtensionSelector(owner) {
|
|
1343
1596
|
this.extensionSelector?.dispose();
|
|
1344
|
-
this.editorContainer.clear();
|
|
1345
|
-
this.editorContainer.addChild(this.editor);
|
|
1346
1597
|
this.extensionSelector = undefined;
|
|
1347
|
-
this.
|
|
1348
|
-
this.ui.requestRender();
|
|
1598
|
+
this.restoreCanonicalEditor({ owner });
|
|
1349
1599
|
}
|
|
1350
1600
|
/**
|
|
1351
1601
|
* Show a confirmation dialog for extensions.
|
|
@@ -1358,69 +1608,79 @@ export class InteractiveMode {
|
|
|
1358
1608
|
* Show a text input for extensions.
|
|
1359
1609
|
*/
|
|
1360
1610
|
showExtensionInput(title, placeholder, opts) {
|
|
1611
|
+
const capturedEpoch = this.uiEpoch;
|
|
1361
1612
|
return new Promise((resolve) => {
|
|
1362
1613
|
if (opts?.signal?.aborted) {
|
|
1363
1614
|
resolve(undefined);
|
|
1364
1615
|
return;
|
|
1365
1616
|
}
|
|
1366
1617
|
const onAbort = () => {
|
|
1367
|
-
this.
|
|
1618
|
+
if (this.uiEpoch !== capturedEpoch) {
|
|
1619
|
+
resolve(undefined);
|
|
1620
|
+
return;
|
|
1621
|
+
}
|
|
1622
|
+
this.hideExtensionInput(this.extensionInput);
|
|
1368
1623
|
resolve(undefined);
|
|
1369
1624
|
};
|
|
1370
1625
|
opts?.signal?.addEventListener("abort", onAbort, { once: true });
|
|
1371
1626
|
this.extensionInput = new ExtensionInputComponent(title, placeholder, (value) => {
|
|
1372
1627
|
opts?.signal?.removeEventListener("abort", onAbort);
|
|
1373
|
-
this.
|
|
1628
|
+
if (this.uiEpoch !== capturedEpoch) {
|
|
1629
|
+
resolve(undefined);
|
|
1630
|
+
return;
|
|
1631
|
+
}
|
|
1632
|
+
this.hideExtensionInput(this.extensionInput);
|
|
1374
1633
|
resolve(value);
|
|
1375
1634
|
}, () => {
|
|
1376
1635
|
opts?.signal?.removeEventListener("abort", onAbort);
|
|
1377
|
-
this.
|
|
1636
|
+
if (this.uiEpoch !== capturedEpoch) {
|
|
1637
|
+
resolve(undefined);
|
|
1638
|
+
return;
|
|
1639
|
+
}
|
|
1640
|
+
this.hideExtensionInput(this.extensionInput);
|
|
1378
1641
|
resolve(undefined);
|
|
1379
1642
|
}, { tui: this.ui, timeout: opts?.timeout });
|
|
1380
|
-
this.
|
|
1381
|
-
this.editorContainer.addChild(this.extensionInput);
|
|
1382
|
-
this.ui.setFocus(this.extensionInput);
|
|
1383
|
-
this.ui.requestRender();
|
|
1643
|
+
this.mountPromptOwner(this.extensionInput, this.extensionInput);
|
|
1384
1644
|
});
|
|
1385
1645
|
}
|
|
1386
1646
|
/**
|
|
1387
1647
|
* Hide the extension input.
|
|
1388
1648
|
*/
|
|
1389
|
-
hideExtensionInput() {
|
|
1649
|
+
hideExtensionInput(owner) {
|
|
1390
1650
|
this.extensionInput?.dispose();
|
|
1391
|
-
this.editorContainer.clear();
|
|
1392
|
-
this.editorContainer.addChild(this.editor);
|
|
1393
1651
|
this.extensionInput = undefined;
|
|
1394
|
-
this.
|
|
1395
|
-
this.ui.requestRender();
|
|
1652
|
+
this.restoreCanonicalEditor({ owner });
|
|
1396
1653
|
}
|
|
1397
1654
|
/**
|
|
1398
1655
|
* Show a multi-line editor for extensions (with Ctrl+G support).
|
|
1399
1656
|
*/
|
|
1400
1657
|
showExtensionEditor(title, prefill) {
|
|
1658
|
+
const capturedEpoch = this.uiEpoch;
|
|
1401
1659
|
return new Promise((resolve) => {
|
|
1402
1660
|
this.extensionEditor = new ExtensionEditorComponent(this.ui, this.keybindings, title, prefill, (value) => {
|
|
1403
|
-
this.
|
|
1661
|
+
if (this.uiEpoch !== capturedEpoch) {
|
|
1662
|
+
resolve(undefined);
|
|
1663
|
+
return;
|
|
1664
|
+
}
|
|
1665
|
+
this.hideExtensionEditor(this.extensionEditor);
|
|
1404
1666
|
resolve(value);
|
|
1405
1667
|
}, () => {
|
|
1406
|
-
this.
|
|
1668
|
+
if (this.uiEpoch !== capturedEpoch) {
|
|
1669
|
+
resolve(undefined);
|
|
1670
|
+
return;
|
|
1671
|
+
}
|
|
1672
|
+
this.hideExtensionEditor(this.extensionEditor);
|
|
1407
1673
|
resolve(undefined);
|
|
1408
1674
|
});
|
|
1409
|
-
this.
|
|
1410
|
-
this.editorContainer.addChild(this.extensionEditor);
|
|
1411
|
-
this.ui.setFocus(this.extensionEditor);
|
|
1412
|
-
this.ui.requestRender();
|
|
1675
|
+
this.mountPromptOwner(this.extensionEditor, this.extensionEditor);
|
|
1413
1676
|
});
|
|
1414
1677
|
}
|
|
1415
1678
|
/**
|
|
1416
1679
|
* Hide the extension editor.
|
|
1417
1680
|
*/
|
|
1418
|
-
hideExtensionEditor() {
|
|
1419
|
-
this.editorContainer.clear();
|
|
1420
|
-
this.editorContainer.addChild(this.editor);
|
|
1681
|
+
hideExtensionEditor(owner) {
|
|
1421
1682
|
this.extensionEditor = undefined;
|
|
1422
|
-
this.
|
|
1423
|
-
this.ui.requestRender();
|
|
1683
|
+
this.restoreCanonicalEditor({ owner });
|
|
1424
1684
|
}
|
|
1425
1685
|
/**
|
|
1426
1686
|
* Set a custom editor component from an extension.
|
|
@@ -1429,7 +1689,6 @@ export class InteractiveMode {
|
|
|
1429
1689
|
setCustomEditorComponent(factory) {
|
|
1430
1690
|
// Save text from current editor before switching
|
|
1431
1691
|
const currentText = this.editor.getText();
|
|
1432
|
-
this.editorContainer.clear();
|
|
1433
1692
|
if (factory) {
|
|
1434
1693
|
// Create the custom editor with tui, theme, and keybindings
|
|
1435
1694
|
const newEditor = factory(this.ui, getEditorTheme(), this.keybindings);
|
|
@@ -1473,8 +1732,7 @@ export class InteractiveMode {
|
|
|
1473
1732
|
this.defaultEditor.setText(currentText);
|
|
1474
1733
|
this.editor = this.defaultEditor;
|
|
1475
1734
|
}
|
|
1476
|
-
this.
|
|
1477
|
-
this.ui.setFocus(this.editor);
|
|
1735
|
+
this.restoreCanonicalEditor({ requestRender: false });
|
|
1478
1736
|
this.updatePromptChrome();
|
|
1479
1737
|
}
|
|
1480
1738
|
/**
|
|
@@ -1493,22 +1751,24 @@ export class InteractiveMode {
|
|
|
1493
1751
|
}
|
|
1494
1752
|
/** Show a custom component with keyboard focus. Overlay mode renders on top of existing content. */
|
|
1495
1753
|
async showExtensionCustom(factory, options) {
|
|
1754
|
+
const capturedEpoch = this.uiEpoch;
|
|
1496
1755
|
const savedText = this.editor.getText();
|
|
1497
1756
|
const isOverlay = options?.overlay ?? false;
|
|
1757
|
+
let component;
|
|
1498
1758
|
const restoreEditor = () => {
|
|
1499
|
-
this.
|
|
1500
|
-
this.editorContainer.addChild(this.editor);
|
|
1501
|
-
this.editor.setText(savedText);
|
|
1502
|
-
this.ui.setFocus(this.editor);
|
|
1503
|
-
this.ui.requestRender();
|
|
1759
|
+
this.restoreCanonicalEditor({ text: savedText, owner: component });
|
|
1504
1760
|
};
|
|
1505
1761
|
return new Promise((resolve, reject) => {
|
|
1506
|
-
let component;
|
|
1507
1762
|
let closed = false;
|
|
1508
1763
|
const close = (result) => {
|
|
1509
1764
|
if (closed)
|
|
1510
1765
|
return;
|
|
1511
1766
|
closed = true;
|
|
1767
|
+
if (this.uiEpoch !== capturedEpoch) {
|
|
1768
|
+
// We're stale, just resolve the promise to avoid leaks, but don't mutate UI
|
|
1769
|
+
resolve(result);
|
|
1770
|
+
return;
|
|
1771
|
+
}
|
|
1512
1772
|
if (isOverlay)
|
|
1513
1773
|
this.ui.hideOverlay();
|
|
1514
1774
|
else
|
|
@@ -1526,6 +1786,14 @@ export class InteractiveMode {
|
|
|
1526
1786
|
.then((c) => {
|
|
1527
1787
|
if (closed)
|
|
1528
1788
|
return;
|
|
1789
|
+
if (this.uiEpoch !== capturedEpoch) {
|
|
1790
|
+
closed = true;
|
|
1791
|
+
try {
|
|
1792
|
+
c?.dispose?.();
|
|
1793
|
+
}
|
|
1794
|
+
catch { }
|
|
1795
|
+
return; // Do not mount if epoch changed
|
|
1796
|
+
}
|
|
1529
1797
|
component = c;
|
|
1530
1798
|
if (isOverlay) {
|
|
1531
1799
|
// Resolve overlay options - can be static or dynamic function
|
|
@@ -1545,10 +1813,7 @@ export class InteractiveMode {
|
|
|
1545
1813
|
options?.onHandle?.(handle);
|
|
1546
1814
|
}
|
|
1547
1815
|
else {
|
|
1548
|
-
this.
|
|
1549
|
-
this.editorContainer.addChild(component);
|
|
1550
|
-
this.ui.setFocus(component);
|
|
1551
|
-
this.ui.requestRender();
|
|
1816
|
+
this.mountPromptOwner(component, component);
|
|
1552
1817
|
}
|
|
1553
1818
|
})
|
|
1554
1819
|
.catch((err) => {
|
|
@@ -1841,9 +2106,13 @@ export class InteractiveMode {
|
|
|
1841
2106
|
});
|
|
1842
2107
|
}
|
|
1843
2108
|
async handleEvent(event) {
|
|
2109
|
+
const capturedEpoch = this.sessionEpoch;
|
|
1844
2110
|
if (!this.isInitialized) {
|
|
1845
2111
|
await this.init();
|
|
1846
2112
|
}
|
|
2113
|
+
if (this.sessionEpoch !== capturedEpoch) {
|
|
2114
|
+
return; // Stale event from previous session/epoch
|
|
2115
|
+
}
|
|
1847
2116
|
this.footer.invalidate();
|
|
1848
2117
|
switch (event.type) {
|
|
1849
2118
|
case "agent_start":
|
|
@@ -1854,15 +2123,20 @@ export class InteractiveMode {
|
|
|
1854
2123
|
this.retryEscapeHandler = undefined;
|
|
1855
2124
|
}
|
|
1856
2125
|
if (this.retryLoader) {
|
|
1857
|
-
this.retryLoader.
|
|
2126
|
+
this.retryLoader.dispose();
|
|
1858
2127
|
this.retryLoader = undefined;
|
|
1859
2128
|
}
|
|
1860
2129
|
if (this.loadingAnimation) {
|
|
1861
|
-
this.loadingAnimation.
|
|
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 });
|
|
1862
2137
|
}
|
|
1863
|
-
this.statusContainer.clear();
|
|
1864
2138
|
this.loadingAnimation = new Loader(this.ui, (spinner) => theme.fg("accent", spinner), (text) => theme.fg("muted", text), this.defaultWorkingMessage);
|
|
1865
|
-
this.
|
|
2139
|
+
this.mountStatusLoader(this.loadingAnimation, { requestRender: false });
|
|
1866
2140
|
// Apply any pending working message queued before loader existed
|
|
1867
2141
|
if (this.pendingWorkingMessage !== undefined) {
|
|
1868
2142
|
if (this.pendingWorkingMessage) {
|
|
@@ -1875,14 +2149,29 @@ export class InteractiveMode {
|
|
|
1875
2149
|
case "message_start":
|
|
1876
2150
|
if (event.message.role === "custom") {
|
|
1877
2151
|
this.addMessageToChat(event.message);
|
|
2152
|
+
if (this.loadingAnimation || this.statusContainer.children.length > 0) {
|
|
2153
|
+
this.ui.invalidate();
|
|
2154
|
+
}
|
|
1878
2155
|
this.ui.requestRender();
|
|
1879
2156
|
}
|
|
1880
2157
|
else if (event.message.role === "user") {
|
|
1881
2158
|
this.addMessageToChat(event.message);
|
|
1882
2159
|
this.updatePendingMessagesDisplay();
|
|
2160
|
+
if (this.loadingAnimation || this.statusContainer.children.length > 0) {
|
|
2161
|
+
this.ui.invalidate();
|
|
2162
|
+
}
|
|
1883
2163
|
this.ui.requestRender();
|
|
1884
2164
|
}
|
|
1885
2165
|
else if (event.message.role === "assistant") {
|
|
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
|
+
}
|
|
1886
2175
|
this.streamingComponent = new AssistantMessageComponent(undefined, this.hideThinkingBlock, this.getMarkdownThemeWithSettings());
|
|
1887
2176
|
this.streamingMessage = event.message;
|
|
1888
2177
|
this.chatContainer.addChild(this.streamingComponent);
|
|
@@ -1985,9 +2274,10 @@ export class InteractiveMode {
|
|
|
1985
2274
|
}
|
|
1986
2275
|
case "agent_end":
|
|
1987
2276
|
if (this.loadingAnimation) {
|
|
1988
|
-
this.loadingAnimation.
|
|
2277
|
+
this.loadingAnimation.dispose();
|
|
2278
|
+
const oldLoader = this.loadingAnimation;
|
|
1989
2279
|
this.loadingAnimation = undefined;
|
|
1990
|
-
this.
|
|
2280
|
+
this.clearStatusOwner({ requestRender: false, owner: oldLoader });
|
|
1991
2281
|
}
|
|
1992
2282
|
if (this.streamingComponent) {
|
|
1993
2283
|
this.chatContainer.removeChild(this.streamingComponent);
|
|
@@ -1996,7 +2286,13 @@ export class InteractiveMode {
|
|
|
1996
2286
|
}
|
|
1997
2287
|
this.pendingTools.clear();
|
|
1998
2288
|
await this.checkShutdownRequested();
|
|
1999
|
-
this.
|
|
2289
|
+
if (this.startupUiGateActive) {
|
|
2290
|
+
this.startupUiGateActive = false;
|
|
2291
|
+
this.flushDeferredStartupNotices();
|
|
2292
|
+
}
|
|
2293
|
+
else {
|
|
2294
|
+
this.ui.requestRender();
|
|
2295
|
+
}
|
|
2000
2296
|
break;
|
|
2001
2297
|
case "auto_compaction_start": {
|
|
2002
2298
|
// Keep editor active; submissions are queued during compaction.
|
|
@@ -2005,11 +2301,16 @@ export class InteractiveMode {
|
|
|
2005
2301
|
this.defaultEditor.onEscape = () => {
|
|
2006
2302
|
this.session.abortCompaction();
|
|
2007
2303
|
};
|
|
2304
|
+
// Stop loading animation
|
|
2305
|
+
if (this.loadingAnimation) {
|
|
2306
|
+
this.loadingAnimation.dispose();
|
|
2307
|
+
this.loadingAnimation = undefined;
|
|
2308
|
+
}
|
|
2309
|
+
this.clearStatusOwner({ requestRender: false });
|
|
2008
2310
|
// Show compacting indicator with reason
|
|
2009
|
-
this.statusContainer.clear();
|
|
2010
2311
|
const reasonText = event.reason === "overflow" ? "Context overflow detected, " : "";
|
|
2011
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)`);
|
|
2012
|
-
this.
|
|
2313
|
+
this.mountStatusLoader(this.autoCompactionLoader, { requestRender: false });
|
|
2013
2314
|
this.ui.requestRender();
|
|
2014
2315
|
break;
|
|
2015
2316
|
}
|
|
@@ -2021,9 +2322,10 @@ export class InteractiveMode {
|
|
|
2021
2322
|
}
|
|
2022
2323
|
// Stop loader
|
|
2023
2324
|
if (this.autoCompactionLoader) {
|
|
2024
|
-
this.autoCompactionLoader.
|
|
2325
|
+
this.autoCompactionLoader.dispose();
|
|
2326
|
+
const oldLoader = this.autoCompactionLoader;
|
|
2025
2327
|
this.autoCompactionLoader = undefined;
|
|
2026
|
-
this.
|
|
2328
|
+
this.clearStatusOwner({ requestRender: false, owner: oldLoader });
|
|
2027
2329
|
}
|
|
2028
2330
|
// Handle result
|
|
2029
2331
|
if (event.aborted) {
|
|
@@ -2057,11 +2359,16 @@ export class InteractiveMode {
|
|
|
2057
2359
|
this.defaultEditor.onEscape = () => {
|
|
2058
2360
|
this.session.abortRetry();
|
|
2059
2361
|
};
|
|
2362
|
+
// Stop loading animation
|
|
2363
|
+
if (this.loadingAnimation) {
|
|
2364
|
+
this.loadingAnimation.dispose();
|
|
2365
|
+
this.loadingAnimation = undefined;
|
|
2366
|
+
}
|
|
2367
|
+
this.clearStatusOwner({ requestRender: false });
|
|
2060
2368
|
// Show retry indicator
|
|
2061
|
-
this.statusContainer.clear();
|
|
2062
2369
|
const delaySeconds = Math.round(event.delayMs / 1000);
|
|
2063
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)`);
|
|
2064
|
-
this.
|
|
2371
|
+
this.mountStatusLoader(this.retryLoader, { requestRender: false });
|
|
2065
2372
|
this.ui.requestRender();
|
|
2066
2373
|
break;
|
|
2067
2374
|
}
|
|
@@ -2073,9 +2380,10 @@ export class InteractiveMode {
|
|
|
2073
2380
|
}
|
|
2074
2381
|
// Stop loader
|
|
2075
2382
|
if (this.retryLoader) {
|
|
2076
|
-
this.retryLoader.
|
|
2383
|
+
this.retryLoader.dispose();
|
|
2384
|
+
const oldLoader = this.retryLoader;
|
|
2077
2385
|
this.retryLoader = undefined;
|
|
2078
|
-
this.
|
|
2386
|
+
this.clearStatusOwner({ requestRender: false, owner: oldLoader });
|
|
2079
2387
|
}
|
|
2080
2388
|
// Show error only on final failure (success shows normal response)
|
|
2081
2389
|
if (!event.success) {
|
|
@@ -2101,7 +2409,9 @@ export class InteractiveMode {
|
|
|
2101
2409
|
* If multiple status messages are emitted back-to-back (without anything else being added to the chat),
|
|
2102
2410
|
* we update the previous status line instead of appending new ones to avoid log spam.
|
|
2103
2411
|
*/
|
|
2104
|
-
showStatus(message) {
|
|
2412
|
+
showStatus(message, ownerEpoch) {
|
|
2413
|
+
if (ownerEpoch !== undefined && this.sessionEpoch !== ownerEpoch)
|
|
2414
|
+
return;
|
|
2105
2415
|
const children = this.chatContainer.children;
|
|
2106
2416
|
const last = children.length > 0 ? children[children.length - 1] : undefined;
|
|
2107
2417
|
const secondLast = children.length > 1 ? children[children.length - 2] : undefined;
|
|
@@ -2251,6 +2561,9 @@ export class InteractiveMode {
|
|
|
2251
2561
|
this.ui.requestRender();
|
|
2252
2562
|
}
|
|
2253
2563
|
renderInitialMessages() {
|
|
2564
|
+
if (this.isInitialMessagesRendered)
|
|
2565
|
+
return;
|
|
2566
|
+
this.isInitialMessagesRendered = true;
|
|
2254
2567
|
// Get aligned messages and entries from session context
|
|
2255
2568
|
const context = this.sessionManager.buildSessionContext();
|
|
2256
2569
|
this.renderSessionContext(context, {
|
|
@@ -2354,6 +2667,7 @@ export class InteractiveMode {
|
|
|
2354
2667
|
const text = (this.editor.getExpandedText?.() ?? this.editor.getText()).trim();
|
|
2355
2668
|
if (!text)
|
|
2356
2669
|
return;
|
|
2670
|
+
const capturedEpoch = this.sessionEpoch;
|
|
2357
2671
|
// Queue input during compaction (extension commands execute immediately)
|
|
2358
2672
|
if (this.session.isCompacting) {
|
|
2359
2673
|
if (this.isExtensionCommand(text)) {
|
|
@@ -2372,6 +2686,8 @@ export class InteractiveMode {
|
|
|
2372
2686
|
this.editor.addToHistory?.(text);
|
|
2373
2687
|
this.editor.setText("");
|
|
2374
2688
|
await this.session.prompt(text, { streamingBehavior: "followUp" });
|
|
2689
|
+
if (this.sessionEpoch !== capturedEpoch)
|
|
2690
|
+
return;
|
|
2375
2691
|
this.updatePendingMessagesDisplay();
|
|
2376
2692
|
this.ui.requestRender();
|
|
2377
2693
|
}
|
|
@@ -2435,8 +2751,11 @@ export class InteractiveMode {
|
|
|
2435
2751
|
}
|
|
2436
2752
|
}
|
|
2437
2753
|
async cycleModel(direction) {
|
|
2754
|
+
const capturedEpoch = this.sessionEpoch;
|
|
2438
2755
|
try {
|
|
2439
2756
|
const result = await this.session.cycleModel(direction);
|
|
2757
|
+
if (this.sessionEpoch !== capturedEpoch)
|
|
2758
|
+
return;
|
|
2440
2759
|
if (result === undefined) {
|
|
2441
2760
|
const msg = this.session.scopedModels.length > 0 ? "Only one model in scope" : "Only one model available";
|
|
2442
2761
|
this.showStatus(msg);
|
|
@@ -2449,6 +2768,8 @@ export class InteractiveMode {
|
|
|
2449
2768
|
}
|
|
2450
2769
|
}
|
|
2451
2770
|
catch (error) {
|
|
2771
|
+
if (this.sessionEpoch !== capturedEpoch)
|
|
2772
|
+
return;
|
|
2452
2773
|
this.showError(error instanceof Error ? error.message : String(error));
|
|
2453
2774
|
}
|
|
2454
2775
|
}
|
|
@@ -2527,26 +2848,54 @@ export class InteractiveMode {
|
|
|
2527
2848
|
this.editor.setText("");
|
|
2528
2849
|
this.ui.requestRender();
|
|
2529
2850
|
}
|
|
2530
|
-
showError(errorMessage) {
|
|
2851
|
+
showError(errorMessage, ownerEpoch) {
|
|
2852
|
+
if (ownerEpoch !== undefined && this.sessionEpoch !== ownerEpoch)
|
|
2853
|
+
return;
|
|
2531
2854
|
this.chatContainer.addChild(new Spacer(1));
|
|
2532
2855
|
this.chatContainer.addChild(new Text(theme.fg("error", `Error: ${errorMessage}`), 1, 0));
|
|
2533
2856
|
this.ui.requestRender();
|
|
2534
2857
|
}
|
|
2535
|
-
|
|
2536
|
-
this.
|
|
2537
|
-
|
|
2858
|
+
enqueueStartupNotice(notice) {
|
|
2859
|
+
if (this.startupUiGateActive) {
|
|
2860
|
+
this.deferredStartupNotices.push(notice);
|
|
2861
|
+
return;
|
|
2862
|
+
}
|
|
2863
|
+
notice();
|
|
2864
|
+
this.ui.requestRender();
|
|
2865
|
+
}
|
|
2866
|
+
flushDeferredStartupNotices() {
|
|
2867
|
+
if (this.deferredStartupNotices.length === 0) {
|
|
2868
|
+
return;
|
|
2869
|
+
}
|
|
2870
|
+
const notices = [...this.deferredStartupNotices];
|
|
2871
|
+
this.deferredStartupNotices = [];
|
|
2872
|
+
for (const notice of notices) {
|
|
2873
|
+
notice();
|
|
2874
|
+
}
|
|
2875
|
+
this.ui.invalidate();
|
|
2538
2876
|
this.ui.requestRender();
|
|
2539
2877
|
}
|
|
2540
|
-
|
|
2878
|
+
showWarning(warningMessage, ownerEpoch) {
|
|
2879
|
+
if (ownerEpoch !== undefined && this.sessionEpoch !== ownerEpoch)
|
|
2880
|
+
return;
|
|
2881
|
+
this.enqueueStartupNotice(() => {
|
|
2882
|
+
this.chatContainer.addChild(new Spacer(1));
|
|
2883
|
+
this.chatContainer.addChild(new Text(theme.fg("warning", `Warning: ${warningMessage}`), 1, 0));
|
|
2884
|
+
});
|
|
2885
|
+
}
|
|
2886
|
+
showNewVersionNotification(newVersion, ownerEpoch) {
|
|
2887
|
+
if (ownerEpoch !== undefined && this.sessionEpoch !== ownerEpoch)
|
|
2888
|
+
return;
|
|
2541
2889
|
const action = theme.fg("accent", getUpdateInstruction(PACKAGE_NAME));
|
|
2542
2890
|
const updateInstruction = theme.fg("muted", `New version ${newVersion} is available. `) + action;
|
|
2543
2891
|
const changelogUrl = theme.fg("accent", "https://github.com/apholdings/jensen-code/blob/main/packages/coding-agent/CHANGELOG.md");
|
|
2544
2892
|
const changelogLine = theme.fg("muted", "Changelog: ") + changelogUrl;
|
|
2545
|
-
this.
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2893
|
+
this.enqueueStartupNotice(() => {
|
|
2894
|
+
this.chatContainer.addChild(new Spacer(1));
|
|
2895
|
+
this.chatContainer.addChild(new DynamicBorder((text) => theme.fg("warning", text)));
|
|
2896
|
+
this.chatContainer.addChild(new Text(`${theme.bold(theme.fg("warning", "Update Available"))}\n${updateInstruction}\n${changelogLine}`, 1, 0));
|
|
2897
|
+
this.chatContainer.addChild(new DynamicBorder((text) => theme.fg("warning", text)));
|
|
2898
|
+
});
|
|
2550
2899
|
}
|
|
2551
2900
|
/**
|
|
2552
2901
|
* Get all queued messages (read-only).
|
|
@@ -2641,10 +2990,13 @@ export class InteractiveMode {
|
|
|
2641
2990
|
if (this.compactionQueuedMessages.length === 0) {
|
|
2642
2991
|
return;
|
|
2643
2992
|
}
|
|
2993
|
+
const capturedEpoch = this.sessionEpoch;
|
|
2644
2994
|
const queuedMessages = [...this.compactionQueuedMessages];
|
|
2645
2995
|
this.compactionQueuedMessages = [];
|
|
2646
2996
|
this.updatePendingMessagesDisplay();
|
|
2647
2997
|
const restoreQueue = (error) => {
|
|
2998
|
+
if (this.sessionEpoch !== capturedEpoch)
|
|
2999
|
+
return;
|
|
2648
3000
|
this.session.clearQueue();
|
|
2649
3001
|
this.compactionQueuedMessages = queuedMessages;
|
|
2650
3002
|
this.updatePendingMessagesDisplay();
|
|
@@ -2654,6 +3006,8 @@ export class InteractiveMode {
|
|
|
2654
3006
|
if (options?.willRetry) {
|
|
2655
3007
|
// When retry is pending, queue messages for the retry turn
|
|
2656
3008
|
for (const message of queuedMessages) {
|
|
3009
|
+
if (this.sessionEpoch !== capturedEpoch)
|
|
3010
|
+
return;
|
|
2657
3011
|
if (this.isExtensionCommand(message.text)) {
|
|
2658
3012
|
await this.session.prompt(message.text);
|
|
2659
3013
|
}
|
|
@@ -2664,7 +3018,9 @@ export class InteractiveMode {
|
|
|
2664
3018
|
await this.session.steer(message.text);
|
|
2665
3019
|
}
|
|
2666
3020
|
}
|
|
2667
|
-
this.
|
|
3021
|
+
if (this.sessionEpoch === capturedEpoch) {
|
|
3022
|
+
this.updatePendingMessagesDisplay();
|
|
3023
|
+
}
|
|
2668
3024
|
return;
|
|
2669
3025
|
}
|
|
2670
3026
|
// Find first non-extension-command message to use as prompt
|
|
@@ -2672,6 +3028,8 @@ export class InteractiveMode {
|
|
|
2672
3028
|
if (firstPromptIndex === -1) {
|
|
2673
3029
|
// All extension commands - execute them all
|
|
2674
3030
|
for (const message of queuedMessages) {
|
|
3031
|
+
if (this.sessionEpoch !== capturedEpoch)
|
|
3032
|
+
return;
|
|
2675
3033
|
await this.session.prompt(message.text);
|
|
2676
3034
|
}
|
|
2677
3035
|
return;
|
|
@@ -2681,14 +3039,20 @@ export class InteractiveMode {
|
|
|
2681
3039
|
const firstPrompt = queuedMessages[firstPromptIndex];
|
|
2682
3040
|
const rest = queuedMessages.slice(firstPromptIndex + 1);
|
|
2683
3041
|
for (const message of preCommands) {
|
|
3042
|
+
if (this.sessionEpoch !== capturedEpoch)
|
|
3043
|
+
return;
|
|
2684
3044
|
await this.session.prompt(message.text);
|
|
2685
3045
|
}
|
|
3046
|
+
if (this.sessionEpoch !== capturedEpoch)
|
|
3047
|
+
return;
|
|
2686
3048
|
// Send first prompt (starts streaming)
|
|
2687
3049
|
const promptPromise = this.session.prompt(firstPrompt.text).catch((error) => {
|
|
2688
3050
|
restoreQueue(error);
|
|
2689
3051
|
});
|
|
2690
3052
|
// Queue remaining messages
|
|
2691
3053
|
for (const message of rest) {
|
|
3054
|
+
if (this.sessionEpoch !== capturedEpoch)
|
|
3055
|
+
return;
|
|
2692
3056
|
if (this.isExtensionCommand(message.text)) {
|
|
2693
3057
|
await this.session.prompt(message.text);
|
|
2694
3058
|
}
|
|
@@ -2699,7 +3063,9 @@ export class InteractiveMode {
|
|
|
2699
3063
|
await this.session.steer(message.text);
|
|
2700
3064
|
}
|
|
2701
3065
|
}
|
|
2702
|
-
this.
|
|
3066
|
+
if (this.sessionEpoch === capturedEpoch) {
|
|
3067
|
+
this.updatePendingMessagesDisplay();
|
|
3068
|
+
}
|
|
2703
3069
|
void promptPromise;
|
|
2704
3070
|
}
|
|
2705
3071
|
catch (error) {
|
|
@@ -2722,16 +3088,14 @@ export class InteractiveMode {
|
|
|
2722
3088
|
* @param create Factory that receives a `done` callback and returns the component and focus target
|
|
2723
3089
|
*/
|
|
2724
3090
|
showSelector(create) {
|
|
3091
|
+
const capturedEpoch = this.uiEpoch;
|
|
2725
3092
|
const done = () => {
|
|
2726
|
-
this.
|
|
2727
|
-
|
|
2728
|
-
this.
|
|
3093
|
+
if (this.uiEpoch !== capturedEpoch)
|
|
3094
|
+
return;
|
|
3095
|
+
this.restoreCanonicalEditor({ requestRender: false, owner: component });
|
|
2729
3096
|
};
|
|
2730
3097
|
const { component, focus } = create(done);
|
|
2731
|
-
this.
|
|
2732
|
-
this.editorContainer.addChild(component);
|
|
2733
|
-
this.ui.setFocus(focus);
|
|
2734
|
-
this.ui.requestRender();
|
|
3098
|
+
this.mountPromptOwner(component, component, { focus });
|
|
2735
3099
|
}
|
|
2736
3100
|
showSettingsSelector() {
|
|
2737
3101
|
this.showSelector((done) => {
|
|
@@ -2870,16 +3234,23 @@ export class InteractiveMode {
|
|
|
2870
3234
|
this.showModelSelector();
|
|
2871
3235
|
return;
|
|
2872
3236
|
}
|
|
3237
|
+
const capturedEpoch = this.sessionEpoch;
|
|
2873
3238
|
const model = await this.findExactModelMatch(searchTerm);
|
|
3239
|
+
if (this.sessionEpoch !== capturedEpoch)
|
|
3240
|
+
return;
|
|
2874
3241
|
if (model) {
|
|
2875
3242
|
try {
|
|
2876
3243
|
await this.session.setModel(model);
|
|
3244
|
+
if (this.sessionEpoch !== capturedEpoch)
|
|
3245
|
+
return;
|
|
2877
3246
|
this.footer.invalidate();
|
|
2878
3247
|
this.updatePromptChrome();
|
|
2879
3248
|
// this.showStatus(`Model: ${model.id}`);
|
|
2880
3249
|
this.checkDaxnutsEasterEgg(model);
|
|
2881
3250
|
}
|
|
2882
3251
|
catch (error) {
|
|
3252
|
+
if (this.sessionEpoch !== capturedEpoch)
|
|
3253
|
+
return;
|
|
2883
3254
|
this.showError(error instanceof Error ? error.message : String(error));
|
|
2884
3255
|
}
|
|
2885
3256
|
return;
|
|
@@ -3140,7 +3511,7 @@ export class InteractiveMode {
|
|
|
3140
3511
|
};
|
|
3141
3512
|
this.chatContainer.addChild(new Spacer(1));
|
|
3142
3513
|
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.
|
|
3514
|
+
this.mountStatusLoader(summaryLoader, { requestRender: false });
|
|
3144
3515
|
this.ui.requestRender();
|
|
3145
3516
|
}
|
|
3146
3517
|
try {
|
|
@@ -3171,8 +3542,8 @@ export class InteractiveMode {
|
|
|
3171
3542
|
}
|
|
3172
3543
|
finally {
|
|
3173
3544
|
if (summaryLoader) {
|
|
3174
|
-
summaryLoader.
|
|
3175
|
-
this.
|
|
3545
|
+
summaryLoader.dispose();
|
|
3546
|
+
this.clearStatusOwner({ requestRender: false, owner: summaryLoader });
|
|
3176
3547
|
}
|
|
3177
3548
|
this.defaultEditor.onEscape = originalOnEscape;
|
|
3178
3549
|
}
|
|
@@ -3213,16 +3584,16 @@ export class InteractiveMode {
|
|
|
3213
3584
|
async handleResumeSession(sessionPath) {
|
|
3214
3585
|
// Stop loading animation
|
|
3215
3586
|
if (this.loadingAnimation) {
|
|
3216
|
-
this.loadingAnimation.
|
|
3587
|
+
this.loadingAnimation.dispose();
|
|
3217
3588
|
this.loadingAnimation = undefined;
|
|
3218
3589
|
}
|
|
3219
|
-
this.
|
|
3590
|
+
this.clearStatusOwner({ requestRender: false });
|
|
3220
3591
|
// Clear UI state
|
|
3221
3592
|
this.pendingMessagesContainer.clear();
|
|
3222
3593
|
this.compactionQueuedMessages = [];
|
|
3223
3594
|
// Switch session via AgentSession (emits extension session events)
|
|
3224
3595
|
await this.session.switchSession(sessionPath);
|
|
3225
|
-
this.
|
|
3596
|
+
this.resetInteractiveSessionUI(true);
|
|
3226
3597
|
this.showStatus("Resumed session");
|
|
3227
3598
|
}
|
|
3228
3599
|
async showOAuthSelector(mode) {
|
|
@@ -3273,10 +3644,7 @@ export class InteractiveMode {
|
|
|
3273
3644
|
// Completion handled below
|
|
3274
3645
|
});
|
|
3275
3646
|
// Show dialog in editor container
|
|
3276
|
-
this.
|
|
3277
|
-
this.editorContainer.addChild(dialog);
|
|
3278
|
-
this.ui.setFocus(dialog);
|
|
3279
|
-
this.ui.requestRender();
|
|
3647
|
+
this.mountPromptOwner(dialog, dialog);
|
|
3280
3648
|
// Promise for manual code input (racing with callback server)
|
|
3281
3649
|
let manualCodeResolve;
|
|
3282
3650
|
let manualCodeReject;
|
|
@@ -3285,11 +3653,11 @@ export class InteractiveMode {
|
|
|
3285
3653
|
manualCodeReject = reject;
|
|
3286
3654
|
});
|
|
3287
3655
|
// Restore editor helper
|
|
3656
|
+
const capturedEpoch = this.uiEpoch;
|
|
3288
3657
|
const restoreEditor = () => {
|
|
3289
|
-
this.
|
|
3290
|
-
|
|
3291
|
-
this.
|
|
3292
|
-
this.ui.requestRender();
|
|
3658
|
+
if (this.uiEpoch !== capturedEpoch)
|
|
3659
|
+
return;
|
|
3660
|
+
this.restoreCanonicalEditor({ owner: dialog });
|
|
3293
3661
|
};
|
|
3294
3662
|
try {
|
|
3295
3663
|
await this.session.modelRegistry.authStorage.login(providerId, {
|
|
@@ -3331,10 +3699,14 @@ export class InteractiveMode {
|
|
|
3331
3699
|
restoreEditor();
|
|
3332
3700
|
this.session.modelRegistry.refresh();
|
|
3333
3701
|
await this.updateAvailableProviderCount();
|
|
3702
|
+
if (this.uiEpoch !== capturedEpoch)
|
|
3703
|
+
return;
|
|
3334
3704
|
this.showStatus(`Logged in to ${providerName}. Credentials saved to ${getAuthPath()}`);
|
|
3335
3705
|
}
|
|
3336
3706
|
catch (error) {
|
|
3337
3707
|
restoreEditor();
|
|
3708
|
+
if (this.uiEpoch !== capturedEpoch)
|
|
3709
|
+
return;
|
|
3338
3710
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
3339
3711
|
if (errorMsg !== "Login cancelled") {
|
|
3340
3712
|
this.showError(`Failed to login to ${providerName}: ${errorMsg}`);
|
|
@@ -3358,19 +3730,20 @@ export class InteractiveMode {
|
|
|
3358
3730
|
cancellable: false,
|
|
3359
3731
|
});
|
|
3360
3732
|
const previousEditor = this.editor;
|
|
3361
|
-
this.
|
|
3362
|
-
this.
|
|
3363
|
-
|
|
3364
|
-
this.ui.requestRender();
|
|
3365
|
-
const dismissLoader = (editor) => {
|
|
3733
|
+
this.mountPromptOwner(loader, loader);
|
|
3734
|
+
const capturedEpoch = this.uiEpoch;
|
|
3735
|
+
const dismissLoader = (_editor) => {
|
|
3366
3736
|
loader.dispose();
|
|
3367
|
-
this.
|
|
3368
|
-
|
|
3369
|
-
this.
|
|
3370
|
-
this.ui.requestRender();
|
|
3737
|
+
if (this.uiEpoch !== capturedEpoch)
|
|
3738
|
+
return;
|
|
3739
|
+
this.restoreCanonicalEditor({ owner: loader });
|
|
3371
3740
|
};
|
|
3372
3741
|
try {
|
|
3373
3742
|
await this.session.reload();
|
|
3743
|
+
if (this.uiEpoch !== capturedEpoch) {
|
|
3744
|
+
loader.dispose();
|
|
3745
|
+
return;
|
|
3746
|
+
}
|
|
3374
3747
|
setRegisteredThemes(this.session.resourceLoader.getThemes().themes);
|
|
3375
3748
|
this.hideThinkingBlock = this.settingsManager.getHideThinkingBlock();
|
|
3376
3749
|
const themeName = this.settingsManager.getTheme();
|
|
@@ -3408,6 +3781,10 @@ export class InteractiveMode {
|
|
|
3408
3781
|
this.showStatus("Reloaded extensions, skills, prompts, themes");
|
|
3409
3782
|
}
|
|
3410
3783
|
catch (error) {
|
|
3784
|
+
if (this.uiEpoch !== capturedEpoch) {
|
|
3785
|
+
loader.dispose();
|
|
3786
|
+
return;
|
|
3787
|
+
}
|
|
3411
3788
|
dismissLoader(previousEditor);
|
|
3412
3789
|
this.showError(`Reload failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
3413
3790
|
}
|
|
@@ -3415,11 +3792,16 @@ export class InteractiveMode {
|
|
|
3415
3792
|
async handleExportCommand(text) {
|
|
3416
3793
|
const parts = text.split(/\s+/);
|
|
3417
3794
|
const outputPath = parts.length > 1 ? parts[1] : undefined;
|
|
3795
|
+
const capturedEpoch = this.uiEpoch;
|
|
3418
3796
|
try {
|
|
3419
3797
|
const filePath = await this.session.exportToHtml(outputPath);
|
|
3798
|
+
if (this.uiEpoch !== capturedEpoch)
|
|
3799
|
+
return;
|
|
3420
3800
|
this.showStatus(`Session exported to: ${filePath}`);
|
|
3421
3801
|
}
|
|
3422
3802
|
catch (error) {
|
|
3803
|
+
if (this.uiEpoch !== capturedEpoch)
|
|
3804
|
+
return;
|
|
3423
3805
|
this.showError(`Failed to export session: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
3424
3806
|
}
|
|
3425
3807
|
}
|
|
@@ -3436,26 +3818,28 @@ export class InteractiveMode {
|
|
|
3436
3818
|
this.showError("GitHub CLI (gh) is not installed. Install it from https://cli.github.com/");
|
|
3437
3819
|
return;
|
|
3438
3820
|
}
|
|
3821
|
+
const capturedEpoch = this.uiEpoch;
|
|
3439
3822
|
// Export to a temp file
|
|
3440
3823
|
const tmpFile = path.join(os.tmpdir(), "session.html");
|
|
3441
3824
|
try {
|
|
3442
3825
|
await this.session.exportToHtml(tmpFile);
|
|
3826
|
+
if (this.uiEpoch !== capturedEpoch)
|
|
3827
|
+
return;
|
|
3443
3828
|
}
|
|
3444
3829
|
catch (error) {
|
|
3830
|
+
if (this.uiEpoch !== capturedEpoch)
|
|
3831
|
+
return;
|
|
3445
3832
|
this.showError(`Failed to export session: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
3446
3833
|
return;
|
|
3447
3834
|
}
|
|
3448
3835
|
// Show cancellable loader, replacing the editor
|
|
3449
3836
|
const loader = new BorderedLoader(this.ui, theme, "Creating gist...");
|
|
3450
|
-
this.
|
|
3451
|
-
this.editorContainer.addChild(loader);
|
|
3452
|
-
this.ui.setFocus(loader);
|
|
3453
|
-
this.ui.requestRender();
|
|
3837
|
+
this.mountPromptOwner(loader, loader);
|
|
3454
3838
|
const restoreEditor = () => {
|
|
3455
3839
|
loader.dispose();
|
|
3456
|
-
this.
|
|
3457
|
-
|
|
3458
|
-
this.
|
|
3840
|
+
if (this.uiEpoch !== capturedEpoch)
|
|
3841
|
+
return;
|
|
3842
|
+
this.restoreCanonicalEditor({ requestRender: false, owner: loader });
|
|
3459
3843
|
try {
|
|
3460
3844
|
fs.unlinkSync(tmpFile);
|
|
3461
3845
|
}
|
|
@@ -3468,6 +3852,8 @@ export class InteractiveMode {
|
|
|
3468
3852
|
loader.onAbort = () => {
|
|
3469
3853
|
proc?.kill();
|
|
3470
3854
|
restoreEditor();
|
|
3855
|
+
if (this.uiEpoch !== capturedEpoch)
|
|
3856
|
+
return;
|
|
3471
3857
|
this.showStatus("Share cancelled");
|
|
3472
3858
|
};
|
|
3473
3859
|
try {
|
|
@@ -3485,6 +3871,8 @@ export class InteractiveMode {
|
|
|
3485
3871
|
});
|
|
3486
3872
|
if (loader.signal.aborted)
|
|
3487
3873
|
return;
|
|
3874
|
+
if (this.uiEpoch !== capturedEpoch)
|
|
3875
|
+
return;
|
|
3488
3876
|
restoreEditor();
|
|
3489
3877
|
if (result.code !== 0) {
|
|
3490
3878
|
const errorMsg = result.stderr?.trim() || "Unknown error";
|
|
@@ -3506,6 +3894,8 @@ export class InteractiveMode {
|
|
|
3506
3894
|
catch (error) {
|
|
3507
3895
|
if (!loader.signal.aborted) {
|
|
3508
3896
|
restoreEditor();
|
|
3897
|
+
if (this.uiEpoch !== capturedEpoch)
|
|
3898
|
+
return;
|
|
3509
3899
|
this.showError(`Failed to create gist: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
3510
3900
|
}
|
|
3511
3901
|
}
|
|
@@ -3724,14 +4114,12 @@ export class InteractiveMode {
|
|
|
3724
4114
|
this.ui.requestRender();
|
|
3725
4115
|
}
|
|
3726
4116
|
async handleClearCommand() {
|
|
3727
|
-
// Stop loading animation
|
|
3728
|
-
if (this.loadingAnimation) {
|
|
3729
|
-
this.loadingAnimation.stop();
|
|
3730
|
-
this.loadingAnimation = undefined;
|
|
3731
|
-
}
|
|
3732
4117
|
// New session via session (emits extension session events)
|
|
3733
|
-
await this.session.newSession();
|
|
3734
|
-
|
|
4118
|
+
const success = await this.session.newSession();
|
|
4119
|
+
if (!success) {
|
|
4120
|
+
return;
|
|
4121
|
+
}
|
|
4122
|
+
this.resetInteractiveSessionUI(true);
|
|
3735
4123
|
this.ui.requestRender();
|
|
3736
4124
|
}
|
|
3737
4125
|
handleDebugCommand() {
|
|
@@ -3777,6 +4165,7 @@ export class InteractiveMode {
|
|
|
3777
4165
|
}
|
|
3778
4166
|
}
|
|
3779
4167
|
async handleBashCommand(command, excludeFromContext = false) {
|
|
4168
|
+
const capturedEpoch = this.sessionEpoch;
|
|
3780
4169
|
const extensionRunner = this.session.extensionRunner;
|
|
3781
4170
|
// Emit user_bash event to let extensions intercept
|
|
3782
4171
|
const eventResult = extensionRunner
|
|
@@ -3787,6 +4176,8 @@ export class InteractiveMode {
|
|
|
3787
4176
|
cwd: process.cwd(),
|
|
3788
4177
|
})
|
|
3789
4178
|
: undefined;
|
|
4179
|
+
if (this.sessionEpoch !== capturedEpoch)
|
|
4180
|
+
return;
|
|
3790
4181
|
// If extension returned a full result, use it directly
|
|
3791
4182
|
if (eventResult?.result) {
|
|
3792
4183
|
const result = eventResult.result;
|
|
@@ -3825,16 +4216,22 @@ export class InteractiveMode {
|
|
|
3825
4216
|
this.ui.requestRender();
|
|
3826
4217
|
try {
|
|
3827
4218
|
const result = await this.session.executeBash(command, (chunk) => {
|
|
4219
|
+
if (this.sessionEpoch !== capturedEpoch)
|
|
4220
|
+
return;
|
|
3828
4221
|
if (this.bashComponent) {
|
|
3829
4222
|
this.bashComponent.appendOutput(chunk);
|
|
3830
4223
|
this.ui.requestRender();
|
|
3831
4224
|
}
|
|
3832
4225
|
}, { excludeFromContext, operations: eventResult?.operations });
|
|
4226
|
+
if (this.sessionEpoch !== capturedEpoch)
|
|
4227
|
+
return;
|
|
3833
4228
|
if (this.bashComponent) {
|
|
3834
4229
|
this.bashComponent.setComplete(result.exitCode, result.cancelled, result.truncated ? { truncated: true, content: result.output } : undefined, result.fullOutputPath);
|
|
3835
4230
|
}
|
|
3836
4231
|
}
|
|
3837
4232
|
catch (error) {
|
|
4233
|
+
if (this.sessionEpoch !== capturedEpoch)
|
|
4234
|
+
return;
|
|
3838
4235
|
if (this.bashComponent) {
|
|
3839
4236
|
this.bashComponent.setComplete(undefined, false);
|
|
3840
4237
|
}
|
|
@@ -3853,12 +4250,13 @@ export class InteractiveMode {
|
|
|
3853
4250
|
await this.executeCompaction(customInstructions, false);
|
|
3854
4251
|
}
|
|
3855
4252
|
async executeCompaction(customInstructions, isAuto = false) {
|
|
4253
|
+
const capturedEpoch = this.sessionEpoch;
|
|
3856
4254
|
// Stop loading animation
|
|
3857
4255
|
if (this.loadingAnimation) {
|
|
3858
|
-
this.loadingAnimation.
|
|
4256
|
+
this.loadingAnimation.dispose();
|
|
3859
4257
|
this.loadingAnimation = undefined;
|
|
3860
4258
|
}
|
|
3861
|
-
this.
|
|
4259
|
+
this.clearStatusOwner({ requestRender: false });
|
|
3862
4260
|
// Set up escape handler during compaction
|
|
3863
4261
|
const originalOnEscape = this.defaultEditor.onEscape;
|
|
3864
4262
|
this.defaultEditor.onEscape = () => {
|
|
@@ -3869,11 +4267,13 @@ export class InteractiveMode {
|
|
|
3869
4267
|
const cancelHint = `(${appKey(this.keybindings, "interrupt")} to cancel)`;
|
|
3870
4268
|
const label = isAuto ? `Auto-compacting context... ${cancelHint}` : `Compacting context... ${cancelHint}`;
|
|
3871
4269
|
const compactingLoader = new Loader(this.ui, (spinner) => theme.fg("accent", spinner), (text) => theme.fg("muted", text), label);
|
|
3872
|
-
this.
|
|
4270
|
+
this.mountStatusLoader(compactingLoader, { requestRender: false });
|
|
3873
4271
|
this.ui.requestRender();
|
|
3874
4272
|
let result;
|
|
3875
4273
|
try {
|
|
3876
4274
|
result = await this.session.compact(customInstructions);
|
|
4275
|
+
if (this.sessionEpoch !== capturedEpoch)
|
|
4276
|
+
return undefined;
|
|
3877
4277
|
// Rebuild UI
|
|
3878
4278
|
this.rebuildChatFromMessages();
|
|
3879
4279
|
// Add compaction component at bottom so user sees it without scrolling
|
|
@@ -3882,6 +4282,8 @@ export class InteractiveMode {
|
|
|
3882
4282
|
this.footer.invalidate();
|
|
3883
4283
|
}
|
|
3884
4284
|
catch (error) {
|
|
4285
|
+
if (this.sessionEpoch !== capturedEpoch)
|
|
4286
|
+
return undefined;
|
|
3885
4287
|
const message = error instanceof Error ? error.message : String(error);
|
|
3886
4288
|
if (message === "Compaction cancelled" || (error instanceof Error && error.name === "AbortError")) {
|
|
3887
4289
|
this.showError("Compaction cancelled");
|
|
@@ -3891,18 +4293,30 @@ export class InteractiveMode {
|
|
|
3891
4293
|
}
|
|
3892
4294
|
}
|
|
3893
4295
|
finally {
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
4296
|
+
if (this.sessionEpoch === capturedEpoch) {
|
|
4297
|
+
compactingLoader.dispose();
|
|
4298
|
+
this.clearStatusOwner({ requestRender: false, owner: compactingLoader });
|
|
4299
|
+
this.defaultEditor.onEscape = originalOnEscape;
|
|
4300
|
+
}
|
|
4301
|
+
}
|
|
4302
|
+
if (this.sessionEpoch === capturedEpoch) {
|
|
4303
|
+
void this.flushCompactionQueue({ willRetry: false });
|
|
3897
4304
|
}
|
|
3898
|
-
void this.flushCompactionQueue({ willRetry: false });
|
|
3899
4305
|
return result;
|
|
3900
4306
|
}
|
|
3901
4307
|
stop() {
|
|
3902
4308
|
if (this.loadingAnimation) {
|
|
3903
|
-
this.loadingAnimation.
|
|
4309
|
+
this.loadingAnimation.dispose();
|
|
3904
4310
|
this.loadingAnimation = undefined;
|
|
3905
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
|
+
}
|
|
3906
4320
|
this.clearExtensionTerminalInputListeners();
|
|
3907
4321
|
this.footer.dispose();
|
|
3908
4322
|
this.footerDataProvider.dispose();
|