From 43a9bda369002747b9cc21d95569b49f1b2c724b Mon Sep 17 00:00:00 2001 From: Yeachan Heo Date: Sun, 16 Aug 2026 06:46:29 +0000 Subject: [PATCH 1/3] fix(palette): restore composer remount for never-activated pet widgets which only admits the current overlay owner or a widget whose epoch still matches. A widget that never activated (pet.mode "off" at startup) claims neither, so InteractiveMode.restoreComposer() -- which always routes through petWidget.remountComposer() once init() creates the widget -- silently no-opped. Palette cancel/close paths then left their CommandPaletteComponent mounted in editorContainer, leaking the modal (issue #4604, three command-palette-interactive-host failures). Composer-mount authority and overlay ownership are separate concerns: a never-activated widget still owns its host's composer mount and must remount the plain editor exactly like the no-pet fallback, while only disposal or a live successor widget revokes the mount. The active-owner case now also remounts the framed editor instead of relying on the epoch clause alone. Lore-id: 4604-palette-pet-remount Constraint: must not weaken #4591 successor-takeover isolation Tested: bun test command-palette-interactive-host.test.ts (8/8) Tested: bun test gajae-pet-widget.test.ts (62 pass incl. 2 new regressions) Tested: bun test packages/tui/test/gajae-pet.test.ts (19 pass) Not-tested: live iTerm2 pet drag interaction (requires real terminal) Confidence: high Scope-risk: narrow Reversibility: trivial --- .../test/gajae-pet-widget.test.ts | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/packages/coding-agent/test/gajae-pet-widget.test.ts b/packages/coding-agent/test/gajae-pet-widget.test.ts index 631a686962..01833e262f 100644 --- a/packages/coding-agent/test/gajae-pet-widget.test.ts +++ b/packages/coding-agent/test/gajae-pet-widget.test.ts @@ -1,4 +1,5 @@ import { afterEach, describe, expect, it, vi } from "bun:test"; +import type { Component } from "@gajae-code/tui"; import * as tui from "@gajae-code/tui"; import { __animationSchedulerTestHooks, @@ -646,6 +647,60 @@ describe("GajaePetWidget", () => { expect(stubs.written).toHaveLength(0); second.dispose(); }); + it("remounts the plain editor while never activated so palette close paths cannot leak a modal", () => { + const stubs = makeStubs(); + const widget = new GajaePetWidget({ + ui: stubs.ui, + editor: stubs.editor, + editorContainer: stubs.editorContainer, + floorContainer: stubs.floorContainer, + isWorking: () => false, + getComposerBottomOffset: () => stubs.floorContainer.render(80).length, + syncManagedItermCursor: async () => true, + forcePixelProtocol: "sixel", + autoFlexGapMs: null, + }); + try { + // Simulate the palette overlay: the host replaces the composer with a + // foreign component (pet never activated, so no overlay claim). + const overlay = { render: () => [] } as unknown as Component; + stubs.editorContainer.clear(); + stubs.editorContainer.addChild(overlay); + widget.remountComposer(); + + expect(stubs.editorContainer.children).toEqual([stubs.editor]); + expect(stubs.editorContainer.children[0]).not.toBe(overlay); + expect(widget.mode).toBe("off"); + } finally { + widget.dispose(); + } + }); + it("remounts the framed editor while active after a palette overlay replaces it", () => { + const stubs = makeStubs(); + const widget = new GajaePetWidget({ + ui: stubs.ui, + editor: stubs.editor, + editorContainer: stubs.editorContainer, + floorContainer: stubs.floorContainer, + isWorking: () => false, + getComposerBottomOffset: () => stubs.floorContainer.render(80).length, + syncManagedItermCursor: async () => true, + forcePixelProtocol: "sixel", + autoFlexGapMs: null, + }); + try { + widget.setMode("red"); + const framed = stubs.editorContainer.children[0]; + const overlay = { render: () => [] } as unknown as Component; + stubs.editorContainer.clear(); + stubs.editorContainer.addChild(overlay); + widget.remountComposer(); + + expect(stubs.editorContainer.children).toEqual([framed]); + } finally { + widget.dispose(); + } + }); it("retains emitted predecessor cleanup across an unavailable terminal takeover", () => { const stubs = makeStubs(); const make = () => From 15ddac8b2cdc7418d066607b2bfe7d783d227e32 Mon Sep 17 00:00:00 2001 From: Yeachan Heo Date: Mon, 17 Aug 2026 19:02:33 +0000 Subject: [PATCH 2/3] fix(palette): stop disposing the reusable composer on overlay open/restore Container.clear() disposes children terminally, and Editor.dispose() tears down the tab-width change listener. Opening a selector cleared editorContainer with the live composer attached, so the first palette round-trip silently killed the editor's listener; every later restore re-mounted a dead editor and runtime tab-width changes stopped re-deriving composer layout. Overlay open paths now detach the reusable editor first (Container.detachChild, the tui detach-then-readd reuse contract), and the pet-aware restores (#mountEditor, restoreComposer, queued-message restore) detach both reusable mounts before clearing. Successor ownership isolation in remountComposer is unchanged. Regression coverage: a real-CustomEditor lifecycle test in the widget suite (disposal observably stops tab-width invalidations) and an end-to-end host test that fails on the pre-fix open path and passes with the fix (verified red/green by reverting the source change). Lore-id: pr4605-composer-lifecycle Constraint: preserve #4591 successor-takeover isolation (remountComposer untouched) Tested: red/green on command-palette-interactive-host (8+1 pass with fix; new test fails without) Tested: 264 pass across 12 adjacent suites; gajae-pet-widget 63 pass Tested: tui + coding-agent check clean; coding-agent build clean Confidence: high Scope-risk: moderate Reversibility: trivial --- packages/coding-agent/CHANGELOG.md | 1 + .../src/modes/components/gajae-pet-widget.ts | 6 + .../src/modes/controllers/input-controller.ts | 6 + .../modes/controllers/selector-controller.ts | 4 + .../src/modes/interactive-mode.ts | 3 + .../command-palette-interactive-host.test.ts | 41 +++++- .../test/gajae-pet-widget.test.ts | 131 +++++++++++++----- .../test/input-controller-keybindings.test.ts | 4 + .../model-selector-controller-batch.test.ts | 2 +- .../test/model-selector-profiles.test.ts | 2 +- ...elector-controller-command-palette.test.ts | 6 +- 11 files changed, 162 insertions(+), 44 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 5bc0c1b15e..f5d7f228a5 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -4,6 +4,7 @@ - Fixed resume listing scaling its read-syscall count with total transcript bytes. The trailing `header_patch` scan walks back to BOF whenever `cwd`/`title` stay unresolved (#3633), which is the common case because only `/rename` and workspace moves ever emit a patch; because the scan borrowed the caller's 4 KiB prefix buffer, that walk cost one `read` per 4 KiB of every candidate transcript on each `--resume`, `--continue`, and picker open. The scan now owns a 64 KiB buffer, so the same bytes are covered in ~16x fewer syscalls. Measured on a real 31-session workspace holding 105 MB of transcripts (largest 41 MB): 25,715 reads / 61.9 s before, 1,652 reads / 0.5 s after, with all 24 recovered titles unchanged. Buried-title recovery, the bytes examined, the `header_patch` marker prefilter, and listing results are unchanged. - `gjc team` worker auto-checkpoints no longer commit and merge root-level worker runtime state (`.gjc/state/**`, e.g. SDK broker endpoints like `.gjc/state/sdk/.json` and settings migration markers) into the leader repo's default branch. The checkpoint classifier's protected prefixes now cover both GJC runtime roots — `.gjc/_session-*/` and `.gjc/state/` — while user-owned `.gjc/` content (config, agents, skills) stays eligible as reviewable worker work. The worker-runtime-state e2e guard now asserts absence at the actual leader merge-target path instead of an unrelated session-scoped path, and matcher boundary cases (`.gjc/state` bare entry vs `.gjc/state-*` siblings) are pinned (#4603). - Discovered oMLX models now keep thinking metadata (`reasoning: true`, `supportsReasoningEffort`, `thinkingFormat: qwen-chat-template`) so `macos-omlx-*` role suffixes (`:low`/`:medium`/`:high`) survive clamp and reach oMLX as `chat_template_kwargs.reasoning_effort`. +- Command palette and selector overlays no longer dispose the reusable composer editor. Opening a selector cleared `editorContainer` with the live editor attached, and `Container.clear()` disposes children terminally — `Editor.dispose()` tears down the tab-width change listener — so every palette/model-selector/queued-message close re-mounted an editor whose listener was permanently gone (tab-width changes stopped re-deriving composer layout after the first palette round-trip). Overlay open paths now detach the reusable editor first (`Container.detachChild`) so `clear()` disposes only the transient overlay, and pet-aware restores (`GajaePetWidget.#mountEditor`, `InteractiveMode.restoreComposer`, `InputController` queued-message restore) detach both reusable mounts before clearing (#4604, follow-up to the `52dad458` remount fix). - Added built-in `MACOS LOCAL (OMLX)` model profiles (`macos-omlx-fast`, `macos-omlx-balanced`, `macos-omlx-quality`, `macos-omlx-abliterated-fast`, `macos-omlx-abliterated-balanced`) for oMLX local inference on Apple Silicon Macs with native full context support and single-LLM thinking effort role mappings to eliminate model swap latency. - Fixed an HTTP 400 that killed every deep-interview session on the `google-antigravity` provider before the first assistant turn. The Round-0 topology `ask` schema pinned `round` with `z.literal(0)`, which zod serializes as `const: 0` and the Cloud Code Assist normalizer rewrites to a numeric `enum: [0]` — a shape CCA rejects (`TYPE_STRING`). `round` is now pinned with an integer range `[0, 0]` instead, so the wire schema carries `type: integer` with the bounds spilled into the description (the same treatment `ambiguity` already gets) and no numeric enum remains. Runtime contract unchanged: only `0` validates (#4606). - The terminal-app integration docs now cite the upstream work that backs each support rating: Gajae Code is proposed for Paseo's in-app ACP provider catalog ([getpaseo/paseo#3471](https://github.com/getpaseo/paseo/pull/3471)) and for Orca's built-in agent registry ([stablyai/orca#15025](https://github.com/stablyai/orca/pull/15025)), while T3 Code has no GJC harness and the integration shape is under discussion upstream ([pingdotgg/t3code#7290](https://github.com/pingdotgg/t3code/discussions/7290)). diff --git a/packages/coding-agent/src/modes/components/gajae-pet-widget.ts b/packages/coding-agent/src/modes/components/gajae-pet-widget.ts index 73ac17c64f..2c6f9055b0 100644 --- a/packages/coding-agent/src/modes/components/gajae-pet-widget.ts +++ b/packages/coding-agent/src/modes/components/gajae-pet-widget.ts @@ -443,6 +443,12 @@ export class GajaePetWidget { } #mountEditor(framed: boolean): void { + // The composer editor is reusable across overlays; disposal is terminal + // (Editor.dispose tears down the tab-width listener), so detach the + // reusable mounts before clearing. Only transient overlay children + // (palette, selectors) are disposed by the clear. + this.#editorContainer.detachChild(this.#editor); + this.#editorContainer.detachChild(this.#framedEditor); this.#editorContainer.clear(); this.#editorContainer.addChild(framed ? this.#framedEditor : this.#editor); } diff --git a/packages/coding-agent/src/modes/controllers/input-controller.ts b/packages/coding-agent/src/modes/controllers/input-controller.ts index 840576b46d..80dfa39a0b 100644 --- a/packages/coding-agent/src/modes/controllers/input-controller.ts +++ b/packages/coding-agent/src/modes/controllers/input-controller.ts @@ -1247,6 +1247,9 @@ export class InputController { } #restoreEditorFocus(): void { + // The composer is reusable across overlays: detach (never dispose) + // before clearing so only the transient overlay is torn down. + this.ctx.editorContainer.detachChild(this.ctx.editor); this.ctx.editorContainer.clear(); this.ctx.editorContainer.addChild(this.ctx.editor); this.ctx.ui.setFocus(this.ctx.editor); @@ -1308,6 +1311,9 @@ export class InputController { ), }, ); + // Detach the reusable composer before clearing so the terminal clear() + // disposes only the selector overlay, not the editor. + this.ctx.editorContainer.detachChild(this.ctx.editor); this.ctx.editorContainer.clear(); this.ctx.editorContainer.addChild(selector); this.ctx.ui.setFocus(selector); diff --git a/packages/coding-agent/src/modes/controllers/selector-controller.ts b/packages/coding-agent/src/modes/controllers/selector-controller.ts index 4ad93c4ce9..b79fffac28 100644 --- a/packages/coding-agent/src/modes/controllers/selector-controller.ts +++ b/packages/coding-agent/src/modes/controllers/selector-controller.ts @@ -1338,6 +1338,10 @@ export class SelectorController { } }; const { component, focus } = create(done); + // The composer is reusable across overlays; detach it before clearing so + // clear() disposes only the transient overlay, not the editor's + // tab-width listener / paste state (disposal is terminal). + this.ctx.editorContainer.detachChild(this.ctx.editor); this.ctx.editorContainer.clear(); this.ctx.editorContainer.addChild(component); this.ctx.ui.setFocus(focus); diff --git a/packages/coding-agent/src/modes/interactive-mode.ts b/packages/coding-agent/src/modes/interactive-mode.ts index ea6b30b4d6..9236f344d2 100644 --- a/packages/coding-agent/src/modes/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive-mode.ts @@ -1401,6 +1401,9 @@ export class InteractiveMode implements InteractiveModeContext { if (this.petWidget) { this.petWidget.remountComposer(); } else { + // The composer is reusable across overlays: detach (never dispose) + // before clearing so only the transient overlay is torn down. + this.editorContainer.detachChild(this.editor); this.editorContainer.clear(); this.editorContainer.addChild(this.editor); } diff --git a/packages/coding-agent/test/command-palette-interactive-host.test.ts b/packages/coding-agent/test/command-palette-interactive-host.test.ts index 43e620ded8..61ae497b37 100644 --- a/packages/coding-agent/test/command-palette-interactive-host.test.ts +++ b/packages/coding-agent/test/command-palette-interactive-host.test.ts @@ -19,7 +19,7 @@ import { HistoryStorage } from "@gajae-code/coding-agent/session/history-storage import { SessionManager } from "@gajae-code/coding-agent/session/session-manager"; import * as titleGenerator from "@gajae-code/coding-agent/utils/title-generator"; import { setKeybindings } from "@gajae-code/tui"; -import { TempDir } from "@gajae-code/utils"; +import { getDefaultTabWidth, setDefaultTabWidth, TempDir } from "@gajae-code/utils"; import { ModelRegistry } from "../src/config/model-registry"; interface InteractivePaletteHost { @@ -425,6 +425,45 @@ describe("command palette InteractiveMode host", () => { expect(host.mode.editorContainer.children).toEqual([host.mode.editor]); } }); + it("keeps the composer's tab-width listener alive across repeated palette open/close cycles", async () => { + // Regression for the editor-lifecycle half of #4604: opening the + // palette used to clear() the container with the live editor attached, + // and Container.clear() disposes children terminally — Editor.dispose() + // tears down the tab-width change listener. Every restore after that + // re-mounted a dead editor whose listener was permanently gone. + const host = await createHost(); + const editor = host.mode.editor; + const defaultWidth = getDefaultTabWidth(); + const otherWidth = defaultWidth === 3 ? 4 : 3; + const invalidations = { count: 0 }; + const originalInvalidate = editor.invalidate.bind(editor); + editor.invalidate = () => { + invalidations.count += 1; + originalInvalidate(); + }; + try { + editor.setText(""); + await Promise.resolve(); + let previousCount = 0; + for (let index = 0; index < 3; index += 1) { + const palette = await openPalette(host); + palette.handleInput("\u001b"); + await waitFor(() => host.mode.editorContainer.children[0] === editor, "the composer to be restored"); + setDefaultTabWidth(otherWidth); + setDefaultTabWidth(defaultWidth); + // Each cycle's tab-width toggles reached a live listener. After + // the old clear()-dispose bug, cycle 2+ contributed nothing. + expect(invalidations.count).toBeGreaterThan(previousCount + 1); + previousCount = invalidations.count; + } + + // The restored composer still accepts input. + editor.handleInput("x"); + expect(editor.getText()).toBe("x"); + } finally { + setDefaultTabWidth(defaultWidth); + } + }); it("blocks the palette while a draft or palette command is active without leaking a modal", async () => { const host = await createHost(); diff --git a/packages/coding-agent/test/gajae-pet-widget.test.ts b/packages/coding-agent/test/gajae-pet-widget.test.ts index 01833e262f..7c37f99b4a 100644 --- a/packages/coding-agent/test/gajae-pet-widget.test.ts +++ b/packages/coding-agent/test/gajae-pet-widget.test.ts @@ -1,5 +1,4 @@ import { afterEach, describe, expect, it, vi } from "bun:test"; -import type { Component } from "@gajae-code/tui"; import * as tui from "@gajae-code/tui"; import { __animationSchedulerTestHooks, @@ -11,7 +10,9 @@ import { type TUI, wrapITerm2RecordForTmux, } from "@gajae-code/tui"; -import type { CustomEditor } from "../src/modes/components/custom-editor"; +import { getDefaultTabWidth, setDefaultTabWidth } from "@gajae-code/utils"; +import { defaultEditorTheme } from "../../tui/test/test-themes"; +import { CustomEditor } from "../src/modes/components/custom-editor"; import { GajaePetWidget, PetFramedEditor } from "../src/modes/components/gajae-pet-widget"; import { setVerifiedItermPetAvailability } from "../src/modes/components/pet-capability"; @@ -193,9 +194,28 @@ function makeWidget( isWorking?: () => boolean; autoFlexGapMs?: [number, number] | null; protocol?: "sixel" | "kitty" | null; + /** Mount a real CustomEditor in a real Container so disposal semantics match production. */ + editor?: "real"; } = {}, ) { const stubs = makeStubs(columns, rows); + if (options.editor === "real") { + const editor = new CustomEditor(defaultEditorTheme); + const editorContainer = new Container(); + editorContainer.addChild(editor); + const widget = new GajaePetWidget({ + ui: stubs.ui, + editor, + editorContainer, + floorContainer: stubs.floorContainer, + isWorking: options.isWorking ?? (() => false), + getComposerBottomOffset: () => stubs.floorContainer.render(columns).length + (options.bottomOffset ?? 0), + syncManagedItermCursor: async () => true, + forcePixelProtocol: options.protocol === null ? undefined : (options.protocol ?? "sixel"), + autoFlexGapMs: options.autoFlexGapMs !== undefined ? options.autoFlexGapMs : null, + }); + return { ...stubs, editor, editorContainer, widget }; + } const widget = new GajaePetWidget({ ui: stubs.ui, editor: stubs.editor, @@ -648,56 +668,91 @@ describe("GajaePetWidget", () => { second.dispose(); }); it("remounts the plain editor while never activated so palette close paths cannot leak a modal", () => { - const stubs = makeStubs(); - const widget = new GajaePetWidget({ - ui: stubs.ui, - editor: stubs.editor, - editorContainer: stubs.editorContainer, - floorContainer: stubs.floorContainer, - isWorking: () => false, - getComposerBottomOffset: () => stubs.floorContainer.render(80).length, - syncManagedItermCursor: async () => true, - forcePixelProtocol: "sixel", - autoFlexGapMs: null, - }); + const { widget, editor, editorContainer } = makeWidget(80, 30, { editor: "real" }); try { - // Simulate the palette overlay: the host replaces the composer with a - // foreign component (pet never activated, so no overlay claim). - const overlay = { render: () => [] } as unknown as Component; - stubs.editorContainer.clear(); - stubs.editorContainer.addChild(overlay); + // Palette open: host detaches the reusable composer and mounts the + // transient overlay (pet never activated, so no overlay claim). + const overlay = new Container(); + editorContainer.detachChild(editor); + editorContainer.clear(); + editorContainer.addChild(overlay); widget.remountComposer(); - expect(stubs.editorContainer.children).toEqual([stubs.editor]); - expect(stubs.editorContainer.children[0]).not.toBe(overlay); + expect(editorContainer.children).toEqual([editor]); + expect(editorContainer.children[0]).not.toBe(overlay); expect(widget.mode).toBe("off"); } finally { widget.dispose(); } }); it("remounts the framed editor while active after a palette overlay replaces it", () => { - const stubs = makeStubs(); - const widget = new GajaePetWidget({ - ui: stubs.ui, - editor: stubs.editor, - editorContainer: stubs.editorContainer, - floorContainer: stubs.floorContainer, - isWorking: () => false, - getComposerBottomOffset: () => stubs.floorContainer.render(80).length, - syncManagedItermCursor: async () => true, - forcePixelProtocol: "sixel", - autoFlexGapMs: null, - }); + const { widget, editor, editorContainer } = makeWidget(80, 30, { editor: "real" }); try { widget.setMode("red"); - const framed = stubs.editorContainer.children[0]; - const overlay = { render: () => [] } as unknown as Component; - stubs.editorContainer.clear(); - stubs.editorContainer.addChild(overlay); + const framed = editorContainer.children[0]; + const overlay = new Container(); + editorContainer.clear(); + editorContainer.addChild(overlay); widget.remountComposer(); - expect(stubs.editorContainer.children).toEqual([framed]); + expect(editorContainer.children).toEqual([framed]); + } finally { + widget.dispose(); + } + }); + it("keeps the composer usable across repeated palette close/remount cycles without disposing it", () => { + // Real editor so the disposal contract is observable: Editor.dispose() + // tears down the tab-width change listener, and that listener fires + // editor.invalidate() on a runtime tab-width change. Counting + // invalidations therefore proves the reusable editor was never + // disposed across overlay open/close cycles. + const { widget, editor, editorContainer } = makeWidget(80, 30, { editor: "real" }); + const defaultWidth = getDefaultTabWidth(); + const otherWidth = defaultWidth === 3 ? 4 : 3; + const invalidations = { count: 0 }; + const originalInvalidate = editor.invalidate.bind(editor); + editor.invalidate = () => { + invalidations.count += 1; + originalInvalidate(); + }; + try { + editor.setText("draft text"); + const rendersBefore = editor.render(80).length; + + for (let cycle = 0; cycle < 4; cycle += 1) { + // Production-shaped open (SelectorController.showSelector): + // detach the reusable composer, then clear() disposes only the + // transient overlay, then mount it. + const overlay = new Container(); + editorContainer.detachChild(editor); + editorContainer.clear(); + editorContainer.addChild(overlay); + // Production-shaped close: pet-aware composer restore. + widget.remountComposer(); + // Tab-width toggle: exactly one invalidate per change while the + // editor's listener is live. + setDefaultTabWidth(otherWidth); + setDefaultTabWidth(defaultWidth); + } + + expect(editorContainer.children).toEqual([editor]); + expect(editor.getText()).toBe("draft text"); + // All 4 cycles' tab-width toggles reached a live listener. + expect(invalidations.count).toBe(8); + expect(editor.render(80).length).toBe(rendersBefore); + editor.handleInput("x"); + expect(editor.getText()).toBe("draft textx"); + + // Red control for the probe itself: a genuinely disposed editor's + // listener no longer fires, so invalidations stop accruing. + const disposedCount = invalidations.count; + editorContainer.clear(); + editorContainer.addChild(editor); + setDefaultTabWidth(otherWidth); + setDefaultTabWidth(defaultWidth); + expect(invalidations.count).toBe(disposedCount); } finally { + setDefaultTabWidth(defaultWidth); widget.dispose(); } }); diff --git a/packages/coding-agent/test/input-controller-keybindings.test.ts b/packages/coding-agent/test/input-controller-keybindings.test.ts index 23d2438a54..39ee167919 100644 --- a/packages/coding-agent/test/input-controller-keybindings.test.ts +++ b/packages/coding-agent/test/input-controller-keybindings.test.ts @@ -198,6 +198,10 @@ async function createContext(options?: { clear: vi.fn(() => { editorContainerChildren.length = 0; }), + detachChild: vi.fn((child: unknown) => { + const index = editorContainerChildren.indexOf(child); + if (index !== -1) editorContainerChildren.splice(index, 1); + }), addChild: vi.fn((child: unknown) => { editorContainerChildren.push(child); }), diff --git a/packages/coding-agent/test/model-selector-controller-batch.test.ts b/packages/coding-agent/test/model-selector-controller-batch.test.ts index 7f808c10b4..4bb4b380f0 100644 --- a/packages/coding-agent/test/model-selector-controller-batch.test.ts +++ b/packages/coding-agent/test/model-selector-controller-batch.test.ts @@ -124,7 +124,7 @@ function createControllerContext() { }; const ctx = { ui: { setFocus: vi.fn(), requestRender: vi.fn() }, - editorContainer: { clear: vi.fn(), addChild: vi.fn() }, + editorContainer: { clear: vi.fn(), detachChild: vi.fn(), addChild: vi.fn() }, editor: {}, settings, session, diff --git a/packages/coding-agent/test/model-selector-profiles.test.ts b/packages/coding-agent/test/model-selector-profiles.test.ts index 698189ce71..373baff22f 100644 --- a/packages/coding-agent/test/model-selector-profiles.test.ts +++ b/packages/coding-agent/test/model-selector-profiles.test.ts @@ -141,7 +141,7 @@ function createControllerContext(options: { missingCredentials?: boolean } = {}) }; const ctx = { ui: { setFocus: vi.fn(), requestRender: vi.fn() }, - editorContainer: { clear: vi.fn(), addChild: vi.fn() }, + editorContainer: { clear: vi.fn(), detachChild: vi.fn(), addChild: vi.fn() }, editor: {}, settings, session, diff --git a/packages/coding-agent/test/modes/controllers/selector-controller-command-palette.test.ts b/packages/coding-agent/test/modes/controllers/selector-controller-command-palette.test.ts index e8dd56fb52..ed731dfc27 100644 --- a/packages/coding-agent/test/modes/controllers/selector-controller-command-palette.test.ts +++ b/packages/coding-agent/test/modes/controllers/selector-controller-command-palette.test.ts @@ -6,7 +6,7 @@ import type { SlashCommand } from "@gajae-code/tui"; describe("SelectorController command palette", () => { it("surfaces rejected handlers without an unhandled rejection", async () => { - const component = { clear: vi.fn(), addChild: vi.fn() }; + const component = { clear: vi.fn(), detachChild: vi.fn(), addChild: vi.fn() }; const errorShown = Promise.withResolvers(); const showError = vi.fn(() => errorShown.resolve()); const ctx = { @@ -37,7 +37,7 @@ describe("SelectorController command palette", () => { } }); it("surfaces rejected action handlers", async () => { - const component = { clear: vi.fn(), addChild: vi.fn() }; + const component = { clear: vi.fn(), detachChild: vi.fn(), addChild: vi.fn() }; const errorShown = Promise.withResolvers(); const showError = vi.fn(() => errorShown.resolve()); const ctx = { @@ -70,7 +70,7 @@ describe("SelectorController command palette", () => { expect(showError).toHaveBeenCalledWith("external editor failed"); }); it("uses effective display strings and omits unbound action shortcuts", () => { - const component = { clear: vi.fn(), addChild: vi.fn() }; + const component = { clear: vi.fn(), detachChild: vi.fn(), addChild: vi.fn() }; const keybindings = { getDisplayString(action: string) { return ( From eef82ef5dc5a620f847312057d3ea1eab7bbe55d Mon Sep 17 00:00:00 2001 From: Yeachan Heo Date: Mon, 17 Aug 2026 21:55:41 +0000 Subject: [PATCH 3/3] test(pet): remove unused lifecycle fixture binding The framed-remount regression test constructs a real editor to exercise container disposal semantics, but does not otherwise need the local binding. Removing it keeps the affected-package check warning-free. Lore-id: pr4605-lint-cleanup Tested: bun test packages/coding-agent/test/gajae-pet-widget.test.ts (63 pass) Tested: bun run --cwd=packages/coding-agent check Confidence: high Scope-risk: trivial Reversibility: trivial --- packages/coding-agent/test/gajae-pet-widget.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/coding-agent/test/gajae-pet-widget.test.ts b/packages/coding-agent/test/gajae-pet-widget.test.ts index 7c37f99b4a..47bfbeec10 100644 --- a/packages/coding-agent/test/gajae-pet-widget.test.ts +++ b/packages/coding-agent/test/gajae-pet-widget.test.ts @@ -686,7 +686,7 @@ describe("GajaePetWidget", () => { } }); it("remounts the framed editor while active after a palette overlay replaces it", () => { - const { widget, editor, editorContainer } = makeWidget(80, 30, { editor: "real" }); + const { widget, editorContainer } = makeWidget(80, 30, { editor: "real" }); try { widget.setMode("red"); const framed = editorContainer.children[0];