Plugin API v1.2: unlock #70 / #71 / #72 / #73 as plugins (closes #112, #113)#141
Merged
Conversation
Extends the curated VNode renderer with the seven new shapes required by every later v1.2 PR: button, input, list, link, radio, svg, and box. Lays down the shared event-handler record (`VNodeEvent`) and the wire envelope (`host:vnodeEvent`) so PR B (fullscreen) and the capability PRs can ship their adapters without churning the protocol again. Security: every plugin-supplied string lands in a React children slot — the renderer never reaches React's raw-HTML escape hatch (a static-source guard pins the rule). Numeric props are coerced via Number() and rejected on NaN / Infinity. Link hrefs use a discriminated union (note id or anchor fragment); plugins cannot produce a raw href string, so javascript: and data: are structurally impossible. The named `isSafePluginHref` guard covers the belt-and-braces case. SVG ships only the five primitives in the plan (line / circle / rect / text / path); unknown svg child tags drop. List / box recursion shares one depth budget (`MAX_LIST_DEPTH = 8`). Tests: 32 unit tests under `src/plugins/__tests__/PluginVNode.test.tsx` cover the rendered DOM for every new shape, the sanitiser path, javascript: link rejection, and the event-shape round-trip through the dispatcher. Reference plugin: `public/plugins/noteser-vnode-demo/` exercises every new shape inside a sidebar panel for manual smoke. The plugin loads via the existing vault-scan / URL install paths. Impl notes: `docs/plugins-v1.2-impl-notes.md` documents the deviations from the plan (link discriminated union over raw-href guard, fullscreen source variant pre-shipped in protocol.ts for PR B, list / box sharing one depth budget). References docs/plugins-v1.2-plan.md section 2. Co-authored-by: noteser-agent <claude-agent@noteser.local> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Lands the first v1.2 plugin capability: read every note's body + frontmatter from the worker. Required by backlinks, unlinked-mentions, graph derivation, and AI-RAG plugins (issues #70, #71, #72). Adds three SDK methods under `ctx.vault.read`: - getAllNotes() returns a snapshot of every non-deleted note. Host caches by a cheap rolling SHA so a second call within the same snapshot is free; rejects with "Vault too large; use stream()." above the 4 MiB projected payload guard. - getNote(id) returns one note or null. - stream({ chunkSize? }) paginates over the vault, defaulting to 100 notes per chunk (max 500 to stay under MAX_ENVELOPE_BYTES). Yields to the main thread between chunks; the perf budget caps a 5000-note walk well under the 50ms-per-task target from #79. Wire-protocol additions in src/plugins/protocol.ts: - worker:requestVaultRead carries a 'all' / 'one' / 'stream' mode. - host:vaultReadResult answers the first two; host:vaultStreamChunk answers stream mode, with notes:[] + non-empty error signalling mid-flight permission revocation. Manifest validator accepts the new vault.read.all permission and the install-preview modal renders an informational bullet describing it (non-destructive — vault.write's red bullet is PR D's scope). Settings -> Plugins gains a per-installed-plugin permissions block that lets the user revoke vault.read.all at runtime. Revocation is persisted on the install record and re-applied on every boot; the PluginHost rejects subsequent calls with "Permission ... was revoked." immediately, including chunks of an already-running stream. Reference plugin at public/plugins/noteser-vault-read-demo/ exercises both getAllNotes() and stream({ chunkSize: 50 }) from palette commands, notifying the count on success or the rejection on revoke. Tests under src/plugins/__tests__/vaultRead.test.ts cover the manifest acceptance, the host snapshot + cache, the chunked stream's slice counts at chunkSize 1 / 100 / above-cap, mid-flight revocation, and the PluginHost wire-protocol gate (declared / revoked / restore). Spec: docs/plugins-v1.2-plan.md sections 4.1, 5, 6, 7, 8. Per-PR deviations recorded in the new docs/plugins-v1.2-impl-notes.md (PR A will reconcile its own section against the TODO stub at the top). Co-authored-by: noteser-agent <claude-agent@noteser.local> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Adds the `vault.events` capability per docs/plugins-v1.2-plan.md section 4.4. Plugins now subscribe to `onVaultChange`, `onNoteSaved(noteId)`, and `onActiveNoteChange(noteId)` and receive a debounced (250 ms) event stream the host fans out from the noteStore + workspaceStore + folderStore subscriptions. Required for the graph view / backlinks plugin (#71) and AI cache-invalidation plugins (#70). Wire protocol gains worker:subscribeVault / worker:unsubscribeVault and host:vaultChanged / host:noteSaved / host:activeNoteIdChanged envelopes; every host event carries the worker-minted subscriptionId so the in-worker handler dispatcher pairs correctly. PluginHost auto-clears every subscription on unload (disable, uninstall, page unload), with an explicit leak test that mounts + unmounts a plugin 10 times and asserts the subscriber count returns to zero each iteration. Settings → Plugins adds a per-permission toggle. Revoked permissions re-check at every dispatch, so toggling off mid-stream silences delivery without crashing existing subscribers — they keep their Unsubscribe thunk and start receiving again on re-grant. Reference plugin at public/plugins/noteser-event-demo subscribes to all three events and toasts on each fire, useful for verifying the 250 ms debounce manually. Co-authored-by: noteser-agent <noteser-agent@local> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Adds the v1.2 fs.open-directory permission so plugins can pop the
native directory picker, walk the chosen folder, and read each file
lazily via a Blob handle. Required for Obsidian / Logseq importers
(see plugins-v1.2-plan.md section 4.3).
- Wire envelopes worker:requestDirectoryOpen / host:directoryOpenResult
added to src/plugins/protocol.ts with a 50,000-entry cap constant.
- ctx.fs.openDirectory({ extensions? }) on the SDK ctx, mirrored in
the published @noteser/plugin-sdk package.
- Host handler prefers showDirectoryPicker (Chromium) and falls back
to <input type=file webkitdirectory> (Safari / Firefox). Walker
lives in src/plugins/directoryPickerHelpers.ts so it unit-tests
without dragging in the singleton wiring.
- Settings → Plugins gains a per-permission revocation checkbox
backed by InstalledPluginRecord.revokedPermissions; the directory
handler honours it on every call.
- Reference plugin public/plugins/noteser-folder-demo counts .md /
.markdown files in a picked folder, exercising both code paths.
- Manifest-preview modal line "Open folders to read files into the
plugin" surfaces automatically via PERMISSION_DESCRIPTIONS.
- Unit tests cover manifest acceptance, permission gate, cap, filter,
the webkitdirectory cancel path, and the new revocation store
action.
Co-authored-by: noteser-agent <noteser-agent@claude>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Adds the `surfaces.fullscreenViews` manifest field, the host modal at `src/components/plugins/PluginFullscreenView.tsx`, the wire envelopes for open/close/setContent + lifecycle, and the `ctx.openFullscreen` / `ctx.closeFullscreen` / `ctx.setFullscreenContent` SDK methods. Follows `docs/plugins-v1.2-plan.md` section 3.1. Lifecycle choices documented in `docs/plugins-v1.2-impl-notes.md`: - Reject (do not replace) on a second openFullscreen call. - Modal persists across active-note changes; plugin stays in control. - Open response and mount notification split into two envelopes so a plugin's `await openFullscreen` resolves before the mount handler starts pushing content. - Esc bound at document-level capture phase so a plugin handler cannot trap it. - Focus trap + body scroll lock + focus snapshot/restore mirror the existing Modal.tsx contract from #104. Unit tests under `src/plugins/__tests__/PluginFullscreenView.test.tsx` cover mount/unmount, Esc + X close, focus-trap wrap, body scroll lock, content updates, and the singleton dismiss helper. Manifest validator tests gain a `surfaces.fullscreenViews` block. PluginHost tests gain a PR-B describe block covering the open / close / setContent fan-out. Reference plugin `noteser-vnode-demo` extended (v0.2.0) to add a "VNode demo: show fullscreen" command that opens a modal with a callout + button + SVG so a human can confirm the lifecycle. Co-authored-by: noteser-agent <noteser-agent@claude> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Lands the v1.2 vault.write capability per docs/plugins-v1.2-plan.md section 4.2, plus the destructive-permission UI flow (section 8) and a persistent audit log of every plugin-initiated vault mutation. Wire protocol additions (src/plugins/protocol.ts): - WorkerRequestVaultWrite carries a discriminated `op` union over create / update / delete / createFolder. - HostVaultWriteResult carries `ok`, `requestSeq`, `id` + `conflictResolved` on successful create, `error` on failure. SDK (src/plugins/sdk.ts + packages/noteser-plugin-sdk/src/sdk.ts): - ctx.vault.write.createNote / updateNote / deleteNote / createFolder. - Sits alongside PR C's `ctx.vault.read` namespace under the same `ctx.vault` root. Host implementation (src/plugins/pluginHostSingleton.ts): - handleVaultWriteRequest writes through useNoteStore.addNote / updateNote / deleteNote and useFolderStore.ensureFolderPath. Same paths the user-driven flows use — sync, indexing, and undo behave identically. - Title-collision resolver appends " (imported)" (chains to "(imported 2)" on repeat). Host returns the conflictResolved flag so the importer plugin (#73) plumbs it straight into its progress log. - Per plan §4.2: title 1-200 chars, body <= 1 MiB, folderPath regex. Hard-delete intentionally absent — recovery stays in the Trash UI. Permission infrastructure (src/plugins/PluginHost.ts): - vault.write joins file-save / file-open in the per-message gate inside handleWorkerMessage. Two-layer check: declared in manifest AND not currently revoked. Reuses PR C's revokePermission / hasPermission / revokedPermissions Set on InstalledPlugin. - New respondVaultWrite() carries the host's reply envelope. - File-save / file-open paths now also honour the revocation Set (previously only the declared-permissions list). Manifest (src/plugins/manifest.ts): - PERMISSIONS gains `vault.write` (after PR C's `vault.read.all`). - New DESTRUCTIVE_PERMISSIONS set + isDestructivePermission helper. Modal UI uses this to render a red-bordered section with a per- permission opt-in checkbox; Install button stays disabled until every destructive entry is acknowledged. Settings UI (src/components/modals/PluginsSettingsPanel.tsx): - Persistent red "Destructive" badge on plugin rows whose manifest declares any destructive permission — survives revocation so the user always sees a historical trace. - Permission rows for destructive perms render in red and reuse PR C's setPluginPermissionRevoked toggle. - New PluginAuditPanel renders the last 50 audit entries. Audit trail (src/utils/pluginAudit.ts — NEW): - 500-entry in-memory ring buffer, debounced flush to localStorage every 250 ms. Survives reloads, doesn't depend on the noteser Zustand stores being hydrated. Entries: {ts, pluginId, op, target, ok, error?, conflictResolved?}. Every accepted op and every validation rejection records. Reference plugin (public/plugins/noteser-write-demo/): - One command "Plugin demo note" that calls ctx.vault.write.createNote. Running it twice exercises the collision suffix path. Tests: - src/__tests__/plugins/vaultWrite.test.ts — 15 tests: manifest acceptance, isDestructivePermission flag, permission gate (undeclared + revoked), respondVaultWrite envelope, end-to-end round-trip per op, chained title collision (Twin + (imported) + (imported 2)), validation failure surfaces ok=false + audit entry. - src/__tests__/plugins/pluginAudit.test.ts — 6 tests: ordering, MAX_ENTRIES rollover, per-plugin filtering, error capture, and the localStorage flush. Spec: docs/plugins-v1.2-plan.md §4.2, §5, §6, §7, §8. Impl notes: docs/plugins-v1.2-impl-notes.md "PR D" section. Co-authored-by: noteser-agent <noteser-agent@claude> Co-authored-by: noteser-agent <claude-agent@noteser.local> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This was referenced Jun 7, 2026
thetechjon
added a commit
that referenced
this pull request
Jun 7, 2026
…142) 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: noteser-agent <noteser-agent@claude> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
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.
Merges the v1.2 work that landed across PRs #116, #117, #118, #119, #132, #139 onto a long-running branch, into
dev.Closes #112 (graph plugin gaps) and #113 (importer plugin gaps) since the API now supports them.
What v1.2 ships
Reference plugins added
Verification
Post-merge follow-ups