fix(palette): restore composer remount for never-activated pet widgets - #4605
fix(palette): restore composer remount for never-activated pet widgets#4605Yeachan-Heo wants to merge 1 commit into
Conversation
#4591 gated GajaePetWidget.remountComposer() behind #canMutateSharedUi(), 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
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
Root cause
Pre-#4591, Validation (exact PR head c194e79)
CI on this head (run 31932247745 + 31932247752)Green: affected-path plan, native-build, Red: — |
Summary
Fixes #4604. Restores the palette close/cancel contract broken by #4591: a
CommandPaletteComponent(with itsDynamicBorder+CommandPaletteListchildren) leaked ineditorContainerwhenever a palette overlay closed, because the composer was never re-mounted.Root cause
InteractiveMode.restoreComposer()routes throughpetWidget.remountComposer()whenever a pet widget exists — which is always afterinit(), even withpet.mode: "off".remountComposer()behind#canMutateSharedUi():#canMutateSharedUi()admits only (a) the current overlay-emitter owner, or (b) a previously-activated widget whose ownership epoch still matches.#ownedOverlayEpoch === 0, never registered inpetOverlayEmitterOwners) satisfies neither clause →remountComposer()becomes a silent no-op → the palette modal stays mounted.remountComposer()mounted unconditionally; for a never-activated widget#mountEditor(false)is byte-identical to the no-pet fallback (editorContainer.clear(); addChild(editor)).The guard's real purpose — preventing a predecessor retired by a successor from clobbering the successor's composer — only ever applies to widgets that once claimed the overlay slot. Composer-mount authority and overlay ownership are separate concerns; the fix separates them:
This keeps #4591's successor-takeover isolation intact (
remountComposerremains a no-op for a widget retired by a successor, covered by the existing "ignores stale off and remount calls after successor takeover" test) and additionally makes the active-pet case remount the framed editor directly rather than relying on the epoch clause alone.Validation
bun test packages/coding-agent/test/command-palette-interactive-host.test.tsbun test packages/coding-agent/test/gajae-pet-widget.test.tsbun test packages/tui/test/gajae-pet.test.tsbun run --cwd packages/tui checkbun run --cwd packages/coding-agent checkNew regressions pin both ownership states:
editorContainer→remountComposer()restores[editor]remountComposer()restores[framedEditor]Non-goals honored
#canMutateSharedUi()gating indispose()/#applyMode()is untouched.Closes #4604