Skip to content

ENG-1249 Implement Discourse context overlay feature in Obsidian - #1400

Open
trangdoan982 wants to merge 8 commits into
mainfrom
eng-1249-implement-discourse-context-overlay-feature-in-obsidian
Open

ENG-1249 Implement Discourse context overlay feature in Obsidian#1400
trangdoan982 wants to merge 8 commits into
mainfrom
eng-1249-implement-discourse-context-overlay-feature-in-obsidian

Conversation

@trangdoan982

@trangdoan982 trangdoan982 commented Sep 5, 2026

Copy link
Copy Markdown
Member

Reviewer brief

Result: Links to a discourse node carry an inline badge with that node's relation count, in both Live Preview and Reading view. Selecting one opens a popover built on RelationshipSection — the same component the Discourse Context panel renders — so the two cannot disagree. A node with no relations still badges, reading 0, and its popover says "No discourse relation found". Toggled by Show discourse context overlay in General settings, applied without a reload.

Review focus:

  • Two render surfaces, one badge. Live Preview goes through a CM6 ViewPlugin, following the existing wikilink drag handle. Reading view needs a markdown post processor, which this plugin had no precedent for. They share createDiscourseContextBadge so they cannot drift visually — worth checking that the two paths stay in step.
  • RelationsIndex is the load-bearing piece. loadRelations costs a full vault file read plus a JSON parse per call, which is fine once per panel open but not once per link on screen. The index keeps a parsed snapshot so a render path can ask synchronously. Its concurrency rules are subtle and commented: a generation counter (relations.json being written mid-read is the normal case, since saving a relation causes it), the snapshot kept rather than dropped on invalidation, and reads deliberately not scheduling loads.
  • What the badge counts. It must match what the panel will actually display, not what the store holds. Relations awaiting acceptance after an import are excluded, and so are relations whose relation type was deleted — deleting a type leaves its relations behind in relations.json and the panel drops them. See countDisplayableRelations.

Risk or follow-up:

  • buildGroupedRelations also drops relations whose other endpoint no longer resolves to a file. The badge does not model that, so a relation pointing at a deleted note will still be counted. Not fixed here because it needs a file lookup per relation on a render path; worth a follow-up ticket if orphaned endpoints turn out to be common.
  • Badges are emitted into Export to PDF/HTML output, since post processors run there too.
  • Live Preview skips a link that straddles a viewport-range boundary, and markdown links without a literal .md before the closing paren.

Verification

  • pnpm install --frozen-lockfile + pnpm ci:validate from the repo root: 8/8 check-types, 4/4 test:unit.
  • No unit tests: apps/obsidian has no test setup, and adding one was deliberately left out of this PR. The counting, grouping and link-parsing rules are isolated in pure modules (relationsEndpointIndex, discourseLinkFrontmatter, internalLinkParsing) so they can be covered later without restructuring.
  • Driven against a real vault over CDP: 16 assertions covering both surfaces, badge count matching the popover's rows, popover open/scroll/dismiss, the empty state, and the setting adding and removing badges live. Stable across repeated runs.

Three bugs were found by that pass rather than by tests, and are fixed here: the badge over-counting orphaned relations, ignoreEvent() inverted so clicks never reached the badge, and scrolling inside the popover dismissing it — which made "Add a new relation" unreachable.

Scope check

  • Ran $scope-check against the ENG ticket and final diff.
  • Scope beyond Done When: None. The ticket's three criteria — a General setting, the overlay showing the same relations as the panel, and documentation — are each covered.

Local delegated full review

  • Ran a comprehensive review of the entire final diff in a subagent with a fresh context.

Its high-severity findings are fixed in c1a7b49f2: Reading view badges never refreshing, the index flashing every badge to 0 on save, an inFlight leak that wedged the snapshot permanently, a read-triggered refresh loop that left Reading view blank, the popover outliving plugin unload with an Escape handler that swallowed the key, and positioning that measured before React had committed so it never flipped up near the window bottom. Remaining findings are listed under Risk or follow-up above.

Loom video

Not recorded. Screenshots of both surfaces and the empty state are attached to the Linear ticket discussion instead.

trangdoan982 and others added 7 commits September 5, 2026 17:53
Reading relations straight from disk costs a full vault file read plus a
JSON parse per call. That is fine for the Discourse Context panel, which
asks once per file open, but the upcoming discourse context overlay asks
once per discourse-node link on screen.

RelationsIndex keeps a parsed snapshot of relations.json grouped by
endpoint id, so a lookup on a render path is a Map hit and can answer
synchronously. It rebuilds from the vault's own modify/create/delete
events, which covers writes made through saveRelations as well as edits
arriving over sync, so relationsStore does not need to know it exists.

Also wires apps/obsidian into the root test:unit task, which it was not
previously exposed to.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The overlay has to decide, per link and per viewport update, whether a
link points at a discourse node and how many relations it has. Every read
here hits an in-memory cache so the answer is available without awaiting.

Notably this avoids getNodeTypeIdForFile/getNodeInstanceIdForFile, which
poll for up to 500ms waiting on frontmatter for a just-created file. That
is correct for relation bookkeeping and wrong on a render path, so an
uncached file yields no badge and is picked up on the next redraw.

Relations awaiting acceptance are excluded from the count: the Discourse
Context panel lists those separately, so including them would show a
number the panel never repeats back.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The badge is plain DOM rather than React so the Live Preview widget and
the Reading view post processor can share one implementation: neither has
a React root, and mounting one per link would be far too heavy.

Toggling the setting applies immediately to both surfaces. Live Preview
needs an empty CM6 transaction to make ViewPlugins re-evaluate; Reading
view has no equivalent, so already-rendered content has to be thrown away
and rebuilt. Both helpers move to markdownViewRefresh, which also takes
over the copy previously inlined in onload.

Neither surface renders a badge yet — that follows.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Live Preview goes through a CM6 ViewPlugin, following the existing
wikilink drag handle. Reading view needs a markdown post processor, which
this plugin had no precedent for; because Obsidian reuses rendered
sections and runs post processors over them again — as it also does for
hover previews and exports — that path checks each link for an existing
badge rather than assuming it runs once.

Selecting a badge opens a popover built on RelationshipSection, the same
component the Discourse Context panel renders, so the two cannot disagree
about a node's relations. InfoTooltip moves out of DiscourseContextView
so using it does not pull in the whole ItemView.

Two fixes found while verifying against a real vault:

- The badge counted relations the panel hides. Deleting a relation type
  leaves its relations behind in relations.json, and the panel drops
  those, so a node with 4 stored relations showed a badge reading 4 over
  a panel listing 2. Counting now mirrors what the panel will display.
- Scrolling inside the popover dismissed it, because the scroll listener
  that follows the badge away could not tell outside scrolling from the
  popover's own — which made "Add a new relation" unreachable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Matches Roam: a node with no relations still gets a badge, reading 0,
whose popover says "No discourse relation found" above the option to add
one. CurrentRelationships renders nothing at all in that case, so without
the message the popover would open on an unexplained button.

Also drops the node title from the top of the popover — the badge sits
directly after the link whose title it was repeating.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Extends the existing discourse context page with the overlay as a fourth
way in, and adds the setting to General settings, rather than adding a
new page for a feature that is another entry point to something already
documented.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
From a delegated review of the full diff. The substantive ones:

Reading view badges never updated. Only the CM6 extension watched the
relations index, and the post processor skipped links that already had a
badge, so a Reading view badge kept its first number for the life of the
view — including the 0 it showed when the index was still loading as the
note first rendered. Both surfaces now redraw from one debounced handler,
and the post processor replaces a stale badge instead of skipping it.

Invalidating the index dropped the snapshot outright, so every badge read
0 until the reload landed — on the very action that triggered it, since
saving a relation writes relations.json. The old snapshot is now kept
until the new one is ready.

ensureLoaded left inFlight set when a load was superseded mid-read, which
made it hand out a settled promise forever: the snapshot stayed stale and
every read re-requested a load that never ran. Reading from a render path
also scheduled loads, so notify -> re-render -> read looped and left
Reading view permanently blank.

The popover outlived the plugin, keeping an Escape handler that swallowed
the key for the rest of the session, and it measured itself before React
had committed, so it never flipped up near the bottom of the window. It
now closes on unload and derives geometry from the window its anchor is
in, which also fixes popout windows.

Also: badge no longer moves the caret on mousedown, widget eq() accounts
for the node type, link parsing moves to a pure module so it can be
tested, and unused accessors are gone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@linear-code

linear-code Bot commented Sep 5, 2026

Copy link
Copy Markdown

ENG-1249

@vercel

vercel Bot commented Sep 5, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated
discourse-graph Skipped Skipped Sep 6, 2026 7:26pm UTC

Request Review

@supabase

supabase Bot commented Sep 5, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project zytfjzqyijgagqxrzbmz because there are no changes detected in packages/database/supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@graphite-app

graphite-app Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

PR size/scope check

This PR is over our review-size guideline.

  • Recommended: ~200 lines changed
  • Acceptable limit: up to 400 lines when well-scoped/self-contained
  • Preferred file count: fewer than 5 files

Please split this into smaller PRs unless there is a clear reason the changes need to land together.

If keeping it as one PR, please add a brief justification covering:

  • What single problem this PR solves
  • Why the files/changes are coupled

Drops the three test files, vitest.config.mts, and the test:unit script
and vitest devDependency, so apps/obsidian is no longer part of the root
test:unit task and the lockfile returns to matching main.

The pure helper modules stay: their separation from Obsidian and
CodeMirror still keeps the counting, grouping and link-parsing rules
readable on their own. Their doc comments no longer cite testability as
the reason.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant