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