Plugin: graph view + backlinks / unlinked mentions (#71)#146
Merged
Conversation
Ships a reference plugin at public/plugins/noteser-graph/ that closes issue #71 by delivering both the backlinks / unlinked-mentions sidebar panel and the force-directed global graph view, all on top of the Plugin API v1.2 capability surface that landed last week. Sidebar panel "Graph" (one of the v1.2 sidebarPanels surfaces): - Backlinks section: notes whose body contains a wikilink to the active note (case-insensitive title match, same shape as the core findBacklinks helper). - Unlinked mentions section: notes containing the active note's title as plain text, with whole-word matching plus exclusion of occurrences inside fenced code blocks, inline code spans, and existing wikilinks. - "Open global graph" button at the bottom. Fullscreen view "Graph" (v1.2 fullscreenViews surface, PR B): - Force-directed SVG of the entire vault. Nodes are notes, edges are wikilinks. Hand-rolled O(n^2) repulsion + spring attraction + center pull + damping, with adaptive iteration count so 1 k nodes lay out comfortably under the 500 ms budget the brief calls for. - Header buttons: Recompute, Reset view, Zoom in / out, Pan four directions. Clicking a circle stores the picked node id and re-renders with a `link` VNode (`{ kind: 'note', noteId }`) in the header so the host's wikilink:// intercept opens the note on the confirmation click. Permissions: vault.read.all (snapshot every note's body for the graph + mention scan) and vault.events (re-derive the panel + graph on save / active-note change; host already debounces at 250 ms). Tests at src/__tests__/noteserGraphPlugin.test.ts cover the pure derivation helpers (35 cases): unlinked-mention detector including code-block / wikilink exclusion and whole-word matching; graph derivation on a 5-note fixture covering self-link drop, parallel- edge de-dup, untitled-source handling, and degree counting; the snapshot-SHA cache key; and the force simulator's determinism + bounds. Perf on the worktree: - Panel re-derive on a 5 000-note vault: ~23 ms (target <50 ms). - Graph layout open on 1 000 nodes / 3 000 edges: ~400 ms (target <500 ms). The plugin co-exists with the core src/components/sidebar/BacklinksView.tsx; the README documents the swap plan (default-install the plugin, port alias support across, then retire the core view). The README also flags the v1.2 API gaps the brief surfaced: there is no `onWheel` / pointer event on SVG children and no `ctx.openNote(id)`, so wheel-zoom and drag-pan ship as button-based equivalents and clicking a circle is a two-click navigation through a `link` VNode. A v1.3 increment would close both. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Strips Unicode em dashes, ellipses, middle dots, and box-drawing characters from main.js and README.md so the source diffs render as text rather than binary in code review. Replaces them with ASCII equivalents that read the same in fixed-width fonts. Also corrects the README's force-simulator description: the iteration count is adaptive (220 for <=100 nodes down to 25 above 1 000), not a flat 220, so a 1 k-node layout completes in ~400 ms rather than the ~1.95 s the flat schedule produced. 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.
Why a plugin, not a core component
The brief asked for two complementary surfaces — a backlinks /
unlinked-mentions sidebar panel and a force-directed global graph
view — and Jon's standing rule is "build new feature surfaces as
plugins where the Plugin API can carry them." v1.2 (PRs A, B, C, F
plus the post-v1.2 VNode event delivery + wikilink intercept
follow-up in #142) added every capability this work needs:
vault.read.allfor body access across the whole vault (PR C).vault.eventsfor debounced re-derivation on save and onactive-note change (PR F).
fullscreenViewssurface for the global graph (PR B).svg,link,button,box,and
list(PR A).ctx.onVNodeEventplus thewikilink://click intercept (Wire VNode event delivery + wikilink intercept (post-v1.2 followup) #142),without which the SVG and the per-link backlinks list would be
inert.
Building this as core today would re-introduce the gap v1.2 was
supposed to close. Shipping it as the reference plugin for this
capability set instead validates the API surface end-to-end and
gives the next plugin author a working blueprint.
What this PR changes
Two new directories, four new files:
No core source changes. The existing
src/components/sidebar/BacklinksView.tsxandsrc/utils/backlinks.tsstay in place; the swap plan is documentedin
public/plugins/noteser-graph/README.mdunder "Co-existencewith core BacklinksView".
Surfaces shipped
Sidebar panel
Graph(active note):title (case-insensitive).
an existing
[[wikilink]], NOT inside a fenced code block, andNOT inside an inline code span.
Fullscreen view
Graph: hand-rolled force-directed SVG of thewhole vault. Adaptive iteration schedule keeps 1 k nodes under the
500 ms budget without Barnes-Hut. Header buttons cover Recompute,
Reset view, Zoom in / out, and four-direction Pan.
Performance numbers (measured on the worktree)
The plugin emits
console.loglines for both numbers so thebudget is verifiable on any real vault from the devtools console.
Deprecation plan for core BacklinksView
Documented in detail in the plugin's
README.md. Three steps:surfaces.
getAliasesForNote(note)fromsrc/utils/aliases.ts; theplugin currently matches title only.
NoteWithBody.frontmatteralready exposes parsed frontmatter, so the alias scanner only
needs to be re-implemented inside the plugin.
src/components/sidebar/BacklinksView.tsxand itsright-sidebar registry entry. The internal
src/utils/backlinks.tshelper stays for the sync layer'sbroken-link check.
v1.2 API gaps surfaced
Three pieces of the brief did NOT map cleanly to the v1.2 VNode
event set, so the plugin ships button-based equivalents and a
two-click open path. Both are called out in the plugin's README:
onWheelon VNodes. Shipped as Zoom in /out buttons.
onPointerDown/onMouseMoveon VNodes.Shipped as four-direction Pan buttons.
accept
onClickonly (nolink-wrapping in the SvgChild union),and
PluginCtxhas noctx.openNote(id)method. Shipped astwo-click: click circle to select, then click the
linkVNodethe plugin renders in the header to navigate (the host's
wikilink://intercept fires on that click).A v1.3 increment that adds
onWheel+ pointer events on SVG and actx.openNote(id)method would close all three without breakingthis plugin's UI. Not blocking the merge.
Test plan
npm run lintclean.npx tsc --noEmitzero errors.npm test -- --cigreen (208 suites, 2 599 tests pass; 1skipped suite is unrelated).
src/__tests__/noteserGraphPlugin.test.tscover the unlinked-mention detector (code-block exclusion,
inline-code exclusion, wikilink exclusion, whole-word
matching, multi-word titles, case-insensitivity), graph
derivation on a 5-note fixture (self-link drop, parallel-
edge de-dup, untitled-source handling, degree counting,
orphan handling), the snapshot-SHA cache key, and the
force simulator's determinism + bounds.
npm run dev, pastehttp://localhost:3001/plugins/noteser-graph/manifest.jsoninto Settings -> Plugins, grant
vault.read.all+vault.events, open a note with wikilinks, verify thepanel shows backlinks; click "Open global graph", verify
the SVG renders, the zoom / pan buttons work, and clicking
a node followed by clicking the resulting
linkrow opensthat note.
🤖 Generated with Claude Code