fix(coding-agent): preserve dialogs during editor updates

This commit is contained in:
Armin Ronacher 2026-07-26 18:22:26 +02:00
parent 6f6f5683e1
commit cacfef4886
2 changed files with 142 additions and 6 deletions

View file

@ -2380,10 +2380,10 @@ export class InteractiveMode {
private setCustomEditorComponent(factory: EditorFactory | undefined): void {
this.editorComponentFactory = factory;
// Save text from current editor before switching
const currentText = this.editor.getText();
this.editorContainer.clear();
const previousEditor = this.editor;
const editorIsDisplayed =
this.editorContainer.children.length === 1 && this.editorContainer.children[0] === previousEditor;
const currentText = previousEditor.getText();
if (factory) {
// Create the custom editor with tui, theme, and keybindings
@ -2438,8 +2438,11 @@ export class InteractiveMode {
this.editor = this.defaultEditor;
}
this.editorContainer.addChild(this.editor as Component);
this.ui.setFocus(this.editor as Component);
if (editorIsDisplayed) {
this.editorContainer.clear();
this.editorContainer.addChild(this.editor as Component);
this.ui.setFocus(this.editor as Component);
}
this.ui.requestRender();
}

View file

@ -0,0 +1,133 @@
import type { Component, EditorComponent } from "@earendil-works/pi-tui";
import { Container } from "@earendil-works/pi-tui";
import { beforeAll, describe, expect, it, vi } from "vitest";
import type { EditorFactory } from "../src/core/extensions/types.ts";
import { InteractiveMode } from "../src/modes/interactive/interactive-mode.ts";
import { initTheme } from "../src/modes/interactive/theme/theme.ts";
class TestEditor implements EditorComponent {
onSubmit?: (text: string) => void;
onChange?: (text: string) => void;
borderColor?: (text: string) => string;
private text = "";
readonly label: string;
constructor(label: string) {
this.label = label;
}
getText(): string {
return this.text;
}
setText(text: string): void {
this.text = text;
}
handleInput(): void {}
invalidate(): void {}
render(): string[] {
return [this.label];
}
}
class TestDefaultEditor extends TestEditor {
getPaddingX(): number {
return 1;
}
}
class TestDialog implements Component {
readonly dispose = vi.fn();
invalidate(): void {}
render(): string[] {
return ["dialog"];
}
}
type EditorContext = {
editorComponentFactory: EditorFactory | undefined;
defaultEditor: TestDefaultEditor;
editor: EditorComponent;
editorContainer: Container;
ui: {
setFocus: (component: Component) => void;
requestRender: () => void;
};
keybindings: object;
autocompleteProvider: undefined;
extensionSelector?: TestDialog;
};
type InteractiveModePrivate = {
setCustomEditorComponent(this: EditorContext, factory: EditorFactory | undefined): void;
hideExtensionSelector(this: EditorContext): void;
};
const interactiveModePrototype = InteractiveMode.prototype as unknown as InteractiveModePrivate;
function createContext(editorContainer: Container, defaultEditor: TestDefaultEditor): EditorContext {
return {
editorComponentFactory: undefined,
defaultEditor,
editor: defaultEditor,
editorContainer,
ui: {
setFocus: vi.fn(),
requestRender: vi.fn(),
},
keybindings: {},
autocompleteProvider: undefined,
};
}
describe("InteractiveMode custom editor replacement", () => {
beforeAll(() => {
initTheme("dark");
});
it("keeps an active selector visible and restores the delayed custom editor when it closes", () => {
const defaultEditor = new TestDefaultEditor("default editor");
defaultEditor.setText("draft");
const selector = new TestDialog();
const editorContainer = new Container();
editorContainer.addChild(selector);
const context = createContext(editorContainer, defaultEditor);
context.extensionSelector = selector;
const customEditor = new TestEditor("custom editor");
const factory: EditorFactory = () => customEditor;
interactiveModePrototype.setCustomEditorComponent.call(context, factory);
expect(context.editor).toBe(customEditor);
expect(customEditor.getText()).toBe("draft");
expect(editorContainer.children).toEqual([selector]);
expect(context.ui.setFocus).not.toHaveBeenCalled();
interactiveModePrototype.hideExtensionSelector.call(context);
expect(selector.dispose).toHaveBeenCalledOnce();
expect(editorContainer.children).toEqual([customEditor]);
expect(context.ui.setFocus).toHaveBeenCalledWith(customEditor);
});
it("replaces and focuses the editor immediately when the old editor is displayed", () => {
const defaultEditor = new TestDefaultEditor("default editor");
defaultEditor.setText("draft");
const editorContainer = new Container();
editorContainer.addChild(defaultEditor);
const context = createContext(editorContainer, defaultEditor);
const customEditor = new TestEditor("custom editor");
interactiveModePrototype.setCustomEditorComponent.call(context, () => customEditor);
expect(context.editor).toBe(customEditor);
expect(customEditor.getText()).toBe("draft");
expect(editorContainer.children).toEqual([customEditor]);
expect(context.ui.setFocus).toHaveBeenCalledWith(customEditor);
});
});