fix(coding-agent): keep open extension dialog visible when the editor component is replaced (#11466)

`ctx.ui.setEditorComponent()` always cleared the editor slot and put the
new editor there with focus. If an extension dialog
(`confirm`/`select`/`input`/`editor`, or an inline `custom()` component)
was open at that moment, the editor replaced it on screen. The dialog
was still on the dialog stack and its promise still pending, but nobody
could see or answer it. It hung until its timeout, or forever without
one.

Now `setCustomEditorComponent` leaves the editor slot and focus alone
while a dialog is open. The new editor is still created and wired up,
and `showTopExtensionDialog` shows it when the last dialog closes.
Without an open dialog nothing changes.

Testing:
- Added a regression test to
`test/interactive-mode-extension-dialogs.test.ts`. It opens a confirm,
replaces the editor, then checks that the confirm stays visible and
answerable and that the new editor is focused afterwards. It fails
without the fix and passes with it.
- `npm run check` passes.
- Ran the reproduction from the issue in tmux (`pi -e ./a.ts -e ./b.ts`,
where a.ts opens a non-blocking `confirm` in `session_start` and b.ts
calls `setEditorComponent`).
  - Before: only the editor shows, and the confirm times out.
  - After: the confirm shows, and Enter gives `answer: true`.

Fixes #11431

Requested by @badlogic

Co-authored-by: triagent <triagent@localhost>
This commit is contained in:
robopipi[bot]andtriagent authored and GitHub committed 2026-10-27 09:11:18 +01:00
1 parent fecc19e4e9
commit 7853186d14
2 files changed
+31 -4

No files matched your search

@@ -2839,8 +2839,12 @@ export class InteractiveMode {
// Save text from current editor before switching
const currentText = this.editor.getText();
this.disposeActiveSelector();
this.editorContainer.clear();
// An open extension dialog keeps the editor slot; the new editor is shown when the last dialog closes.
const dialogOpen = this.extensionDialogs.length > 0;
if (!dialogOpen) {
this.disposeActiveSelector();
this.editorContainer.clear();
}
if (factory) {
// Create the custom editor with tui, theme, and keybindings
@@ -2898,7 +2902,7 @@ export class InteractiveMode {
this.editor = this.defaultEditor;
}
this.editorContainer.addChild(this.editor as Component);
if (!dialogOpen) this.editorContainer.addChild(this.editor as Component);
if (this.activeStatusIndicator) {
this.statusContainer.clear();
this.activeWorkingIndicatorEmbedded = this.setEditorWorkingStatusIndicator(this.activeStatusIndicator);
@@ -2906,7 +2910,7 @@ export class InteractiveMode {
this.statusContainer.addChild(this.activeStatusIndicator);
}
}
this.ui.setFocus(this.editor as Component);
if (!dialogOpen) this.ui.setFocus(this.editor as Component);
this.ui.requestRender();
}
@@ -19,6 +19,9 @@ type DialogMethods = {
showExtensionCustom<T>(
factory: (tui: unknown, theme: unknown, keybindings: unknown, done: (result: T) => void) => Component,
): Promise<T>;
setCustomEditorComponent(
factory: ((tui: unknown, theme: unknown, keybindings: unknown) => Component) | undefined,
): void;
};
function createContext(): DialogContext & DialogMethods {
@@ -149,4 +152,24 @@ describe("InteractiveMode extension dialogs", () => {
await expect(custom).resolves.toBe("kept");
expect(context.focused).toBe(context.editor);
});
// Regression for #11431: replacing the editor while a dialog was open hid the dialog and left it unanswerable.
it("keeps an open dialog visible when the editor component is replaced", async () => {
const context = createContext();
(context as unknown as { defaultEditor: unknown }).defaultEditor = context.editor;
const confirmed = context.showExtensionConfirm("Title", "Continue?");
const customEditor: Component & { getText(): string; setText(text: string): void } = {
render: () => ["custom editor"],
invalidate: () => {},
getText: () => "",
setText: () => {},
};
context.setCustomEditorComponent(() => customEditor);
expect(visible(context)).toContain("Title");
context.focused?.handleInput?.("\r");
await expect(confirmed).resolves.toBe(true);
expect(context.focused).toBe(customEditor);
expect(context.editorContainer.children).toEqual([customEditor]);
});
});