feat: chain onto previously installed editor factory (#3935)#18
feat: chain onto previously installed editor factory (#3935)#18kylesnowschwartz wants to merge 1 commit into
Conversation
Convert ModalEditor into a class-mixin factory `createModalEditor(Base)` so pi-vim can wrap a custom editor installed by another extension instead of replacing it. The session_start handler now reads `ctx.ui.getEditorComponent()` (Pi 0.71+) and, when present, builds the modal editor as a subclass of that extension's class rather than the default `CustomEditor`. This resolves the silent collision described in earendil-works/pi#3935. With no prior factory installed, behavior is unchanged: the canonical `ModalEditor` is used so existing `instanceof ModalEditor` checks still pass. API surface: - `createModalEditor(Base)` is now exported for extensions composing on top of pi-vim. It returns a `class extends Base` mixin. - `ModalEditor` remains exported as both a value and a type, defined as `createModalEditor(CustomEditor)`. - Constructor signature is now a passthrough `(...args)` to support the mixin chain. `labelColorizers` moved from a constructor argument to a `setColorizers(colorizers)` method, called post-construction by the session_start handler. Test code that previously passed colorizers to the constructor now uses the setter. Adds a modal-editor test that asserts the composed editor is an instance of the previously installed factory's class, that the probe runs once during install, and that standalone use still produces an `instanceof ModalEditor` instance. Bumps the @mariozechner/pi-coding-agent devDependency to ^0.71.0 so the typecheck sees the new `ExtensionUIContext.getEditorComponent` member. The peerDependencies range stays at `*` so older pi versions remain supported at runtime via the optional-chaining call (`?.`). Bumps pack-check size thresholds modestly to accommodate the README section and createModalEditor wrapper. Adds node_modules/ to .gitignore since it was missing.
|
Thanks very much for the PR and the upstream API fix. I heard about the ModalEditor issue but didn't act on it as I was not facing it myself. Looks good at first glance, will consider merging with my next batch very soon. |
There was a problem hiding this comment.
[DRAFT COMMENT]
I found one blocking issue in the composition contract.
ctx.ui.getEditorComponent() returns an EditorFactory, but index.ts uses it only to create a probe instance, reads probe.constructor, discards the probe, then constructs a new subclass with the standard (tui, theme, kb) arguments.
That loses valid factory-level behavior from the previous editor factory:
- extra constructor arguments supplied from the factory closure
- instance mutations after construction
- listeners/resources registered by the factory
- cleanup references stored by the factory
It also runs probe side effects on an instance that is never mounted.
Concrete repro shape:
class PreviousEditor extends CustomEditor {
constructor(tui, theme, kb, required) {
if (!required) throw new Error("missing required factory arg");
super(tui, theme, kb);
}
}
ctx.ui.getEditorComponent = () =>
(tui, theme, kb) => new PreviousEditor(tui, theme, kb, { ok: true });With the current implementation, pi-vim probes that factory successfully, then mounts new Composed(tui, theme, kb), which calls PreviousEditor without the required factory-provided arg.
The same issue applies when pi-vim is the inner editor for a later extension: pi-vim's per-instance configuration is currently factory-level (setColorizers, clipboard policy, quit handler, notify handler). A later extension that probes pi-vim and subclasses only ModalEditor would drop that configuration on the mounted editor.
Please either preserve the previous factory semantics, or document/test a class-only composition contract. If this keeps the class-only approach, add coverage where the previous factory supplies required closure state or mutates the returned instance; the current test only covers a zero-arg class with constructor-initialized state.
Verification I ran on this branch:
npm cinpm run check— 500 tests passednpm run pack:check— passed
|
Quick context for anyone following this PR. #21 hardens the delegation layer this PR introduced — same architecture, plus the review findings (malformed-previous-editor rejection, scoped Separately, building and reviewing this work made us want to articulate a longer-term alternative for pi-vim: a full editor-replacement shape, rather than a wrapper sitting on top of an unknown previous editor. The composition direction pushes every editor extension to learn every other one's surface, and existing modal-editing implementations in similar agents (Codex CLI's composer Vim mode, Claude Code's prompt-input Vim mode) take the other route — the app owns the prompt editor with Vim as an internal sub-engine, and slash commands / paste / history / autocomplete are integrated inside the owned editor rather than negotiated across a chain. To be clear: that direction is exploratory and contingent on Pi's own evolution and on whether enough editor-owning extensions exist to make replacement a worthwhile peer architecture. pi-vim is not committing to executing on it. We're shipping the composition bridge here and keeping it healthy in the meantime. |
|
Thanks for the update @lajarre I came to the conclusion that the extension interface was a bit unstable too. I considered your feedback and came to no definitive conclusion on best approach. So for now I'll maintain a local fork until a major release. |
|
Closing the loop on this. TL;DR: please install in order (i) pi-vim, then (ii) pi-image-attachments. This should work today. Happy to continue the discussion and please reach out if you have any issue. More details below (clankertext). where we landedAfter working through #21 (which hardened the wrap-of-previous-editor approach this PR introduced), I pivoted on the architecture. The complexity cost of "pi-vim wraps any previous editor as an INSERT delegate" wasn't paying back — ~3.4k LOC in The shape I settled on is in #23 ( This will close #18 and #21 in favor of #23. Thanks for forcing the conversation — most of the framing that ended up in #23's README and the retrospective fell out of working through your code. what landed on pi-image-attachments' sidejordyvandomselaar/pi-image-attachments#4 merged 2026-05-05 — a different contributor shipped composition from the other side. Your #3 was closed in favor of that one. what this means for your setupYou don't need the local fork:
bigger pictureThe retrospective + the architectural decisions are written up here: It also flags a UX risk (silent clobbering when multiple editor extensions install) and a strategic gap that would surface for any future full-replacement editor (e.g. a Neovim bridge). Not asking pi-mono for anything actively; flagging for the maintainer's awareness. Thanks again for the PR. |
This opts pi-vim into the new
ctx.ui.getEditorComponent()API that landed in@mariozechner/pi-coding-agent0.71 (pi-mono#3935). With this change, pi-vim can be loaded alongside another extension that also installs a custom editor (the concrete case I had:@jordyvd/pi-image-attachments) and both behaviors compose, instead of last-session_start-wins clobbering one of them.The structural change is converting
class ModalEditor extends CustomEditorinto a class-mixin factory:ModalEditorremains exported as both a value and a type, so external consumers (and the existing test suite) don't have to change anything. The session_start handler readsgetEditorComponent()and, when a previous factory is present, probes it once to grab its class and uses that asBaseinstead ofCustomEditor. With no previous factory, the canonicalModalEditoris reused soinstanceof ModalEditorchecks keep working for the standalone case.A few small follow-on changes were necessary to make the mixin pattern clean:
(...args)passthrough.labelColorizerswas the one extra arg the constructor used to take, and that doesn't survive a chain — a wrapping subclass would have to know how to thread it through. I moved colorizers to asetColorizers(colorizers: ModeLabelColorizers | null)method called post-construction by the session_start handler. The one test that constructed aModalEditorwith colorizers directly was updated to call the setter instead.@mariozechner/pi-coding-agentdev dependency was bumped from0.55.xto^0.71.0sotscsees the newExtensionUIContext.getEditorComponentmember. ThepeerDependenciesrange stays at*and the runtime call uses optional chaining (ctx.ui.getEditorComponent?.()), so older pi versions still work — they just don't compose, which is the existing behavior.node_modules/was added to.gitignore(was missing). I noticed this when I accidentally tracked it on the first push and had to force-push to clean up.script/pack-check.tssize thresholds bumped by ~3% to accommodate the README section and the wrapper. Comment updated.Tested by:
npm run check(lint + typecheck + 500 tests) all green. Added two new tests intest/modal-editor.test.tsunder acomposable editor factory (#3935)describe block: one installs a fake previous factory and verifies the resulting editor isinstanceof PreviousEditorand the probe runs exactly once; the other confirms standalone use still produces aninstanceof ModalEditorinstance.pi 0.71.0with@jordyvd/pi-image-attachmentslisted beforepi-viminsettings.json.packages. Both extensions install cleanly with no shadowing inpi --verbose's[Extension issues]section, the footer renders the vim mode label, and image-path pastes still produce attachment placeholders inside the modal editor.A note on extension authors composing on top of pi-vim:
createModalEditor(Base)is now part of the public API. If you want to layer something on top of vim modal editing, you can call it with your own base class and you'll get a subclass back. README documents this.Companion PR on
jordyvandomselaar/pi-image-attachmentsdoing the same opt-in from the other side: jordyvandomselaar/pi-image-attachments#3