From cacfef488663deccc179ba28f246cff3c4fc706c Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 26 Jul 2026 18:22:26 +0200 Subject: [PATCH] fix(coding-agent): preserve dialogs during editor updates --- .../src/modes/interactive/interactive-mode.ts | 15 +- .../interactive-mode-editor-component.test.ts | 133 ++++++++++++++++++ 2 files changed, 142 insertions(+), 6 deletions(-) create mode 100644 packages/coding-agent/test/interactive-mode-editor-component.test.ts diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index c42f1fcee..c3202eb6e 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -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(); } diff --git a/packages/coding-agent/test/interactive-mode-editor-component.test.ts b/packages/coding-agent/test/interactive-mode-editor-component.test.ts new file mode 100644 index 000000000..d2dc6a226 --- /dev/null +++ b/packages/coding-agent/test/interactive-mode-editor-component.test.ts @@ -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); + }); +});