Wire VNode event delivery + wikilink intercept (post-v1.2 followup)#142
Merged
Conversation
Plugin API v1.2 (PRs A-F + integration #141) shipped the VNode event envelope and the VNode shapes that emit events, but the dispatch + routing pipeline was never wired. Four feature plugins (#70 / #71 / #72 / #73) hit the same wall trying to use the API. Two gaps closed here: Gap 1 — VNode event delivery. Added PluginHost.sendVNodeEvent which constructs the host:vnodeEvent envelope and posts to the worker, rate-limited per plugin at MAX_VNODE_EVENTS_PER_SECOND (16/s/plugin). workerEntry handles host:vnodeEvent by fanning out to handlers registered through the new ctx.onVNodeEvent SDK method. PluginsPanel, PluginFullscreenView, and PluginCodeBlock now wrap PluginNode's onEvent with the right source descriptor. Gap 2 — wikilink:// click intercept. The renderer's renderLink now attaches an onClick that calls workspaceStore.openNote(noteId) when the href starts with wikilink://, taking the noteId off the typed VNodeLink.href shape (not URL parsing). Modifier-clicks and fragment anchors pass through to native browser behaviour. Reference plugin noteser-vnode-demo bumped to 0.3.0 — subscribes to every VNode event via the new API and renders a wikilink to the active note so the intercept can be smoke-tested manually. Backwards compatible: plugins that never call ctx.onVNodeEvent see no behaviour change. Docs appended to docs/plugins-v1.2-impl-notes.md under "Post-v1.2: VNode event delivery + wikilink intercept" with the rate-limit rationale and a deviation note on the (event, cb) vs (handler) SDK shape. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
4 tasks
thetechjon
added a commit
that referenced
this pull request
Jun 7, 2026
Adds `noteser-properties` under public/plugins/ — closes issue #72 by implementing both halves of the Obsidian Bases-parity ask as a plugin rather than core code. PR #142 just landed the VNode event delivery that makes interactive plugin UIs work end-to-end; this is the first feature plugin that exercises the full v1.2 stack. Surfaces: - Sidebar `Properties+` panel. Renders the active note's frontmatter as a key/value list with one `input` VNode per property. "+ Add property" opens a key + value draft row. Each commit calls `ctx.vault.write.updateNote(id, { frontmatter })`. Subscribes to `ctx.vault.events.onActiveNoteChange` and `onNoteSaved` so the panel re-pulls the host-parsed frontmatter on every relevant change. - Fullscreen `Vault tables` view. `ctx.vault.read.getAllNotes()` (falls back to `stream()` for vault-too-large), columns inferred as the union of every note's frontmatter keys, every row rendered as a `box` with a `link` VNode for the title (clicking dispatches the PR #142 wikilink intercept). Top input filters across title, folder path, every stringified frontmatter value. Column headers are `button` VNodes — click to sort, click again to flip direction. "Save current view as note" calls `ctx.vault.write.createNote` with the rendered markdown table. Frontmatter handling: - Source: `NoteWithBody.frontmatter` straight from the host's parser. The plugin never re-parses YAML; js-yaml is not a dependency (the plan's check confirmed it's not in package.json). - Type inference per column: `string` / `number` / `date` (ISO date) / `tag-array` / `boolean`. Heterogeneous columns fall back to `string` so sort stays sane. Test plan: - 44 unit tests cover inference (string/number/date/tag-array/ boolean/empty/heterogeneous), filter (title, folder, tags, status, numbers, case-insensitivity, empty filter), sort (every column type, asc + desc, empties last, _title / _folder special keys, immutability), value coercion (back to numbers / arrays / booleans on edit), and the markdown table serialiser (header + separator, pipe escaping). Mirror lives at src/plugins/propertiesPluginLogic.ts; production logic at public/plugins/noteser-properties/main.js (must move in lock step). Coexistence: the existing core `PropertiesPanel` is unchanged — the plugin adds a second, richer surface alongside it. `npm run lint`, `npx tsc --noEmit`, `npm test -- --ci` all green. Closes #72. Co-authored-by: noteser-agent <noteser-agent@claude>
5 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Plugin API v1.2 (PRs A–F + integration #141) shipped the VNode event ENVELOPE shape and the VNode shapes (
button/input/radio/ clickablesvg) that emit events, but the dispatch + routing pipeline was never wired. Four feature plugins (#70 / #71 / #72 / #73) hit the same wall trying to use the API today. This PR closes two distinct gaps in a single piece of plumbing.Gap 1 — VNode events did not reach the plugin
PluginFullscreenView.handleEventwas a documented no-op.PluginsPanel's<PluginNode>mount had noonEventprop.PluginHosthad nosendVNodeEventmethod.workerEntry.tshad no inbound handler forhost:vnodeEvent.PluginCtxhad noonVNodeEventregistration API.The wire envelope (
HostVNodeEventinsrc/plugins/protocol.ts) was already correct with thesource.kind: 'panel' | 'codeBlock' | 'fullscreen'discriminator — PR A pre-shipped the shape. This PR wires both ends.Gap 2 —
wikilink://clicks on plugin-rendered note links did nothingA plugin emits
{ tag: 'link', href: { kind: 'note', noteId } }; the renderer produces<a href="wikilink://<encodedId>">. Browser navigation to an unrecognised scheme is a no-op, so the click was inert.What landed
Dispatch + routing
PluginHost.sendVNodeEvent(pluginId, source, event, payload)constructs the envelope and posts to the worker.workerEntry.tshandleshost:vnodeEventby fanning out to every handler registered viactx.onVNodeEvent. Handlers live in a worker-localSet;unload()callsworker.terminate(), which drops the entire module and every handler with it.PluginsPanel,PluginFullscreenView, andPluginCodeBlocknow pass anonEventcallback to<PluginNode>with the rightsourcedescriptor ({ kind: 'panel', panelId }/{ kind: 'fullscreen', viewId }/{ kind: 'codeBlock', blockId }).SDK addition
ctx.onVNodeEvent(handler): Unsubscribeships in BOTHsrc/plugins/sdk.tsandpackages/noteser-plugin-sdk/src/sdk.ts.Handler signature:
ONE handler shape with the
sourcediscriminator rather than the plan-section-6(event, cb)pair so plugins that reuse an event name across surfaces can disambiguate without renaming.Wikilink intercept
The renderer's
renderLinknow attaches anonClick:hrefstarts withwikilink://AND the click is unmodified (no meta / ctrl / shift / alt, primary button), calle.preventDefault()anduseWorkspaceStore.getState().openNote(noteId). The noteId comes from the typedVNodeLink.href.kind === 'note'branch — NOT URL parsing — so the parsing surface stays at one chokepoint.#fragment) links are untouched (native fragment-scroll owns that case).Rate-limit choice
MAX_VNODE_EVENTS_PER_SECOND = 16events / sec / plugin, sliding 1-second window. Tighter than the generalMAX_MESSAGES_PER_SECONDceiling of 60.vnodeEventRateLimitedPluginHostEvent(one per window) so a runaway loop surfaces in the dev console without spamming the toast layer.The event family has no acknowledged use case for high-rate streaming — the high-rate paths are
setPanelContent+vault.events, both already capped elsewhere.Backwards compatibility
Plugins that never call
ctx.onVNodeEventsee no behaviour change. Their rendered controls fire events into an empty handler set in the worker, which silently drops them — exactly the pre-PR behaviour.The
PluginVNode.tsxrenderer adds anonClickto every<a>it emits, but the handler is a pass-through for any non-wikilink://href, so existing tests of the link shape still pass.Reference plugin
public/plugins/noteser-vnode-demobumps to0.3.0:ctx.onVNodeEventfromonActivate— toasts every event with the source kind, andconsole.logs every event for the manual smoke check.onActiveNoteChangeand renders alinkVNode to it, so the wikilink intercept can be smoke-tested by clicking the link in the panel.Test plan
npm run lintis clean.npx tsc --noEmitreports zero errors.npm test -- --ciis green (207 suites pass, 2564 tests).noteser-vnode-demoplugin, open the demo panel, click the button — devtools shows the[noteser-vnode-demo] vnodeEventlog line withsource.kind: 'panel'.source.kind: 'fullscreen'.🤖 Generated with Claude Code