Skip to content

Add per-node attribution to q2-debug AST view#122

Draft
shikokuchuo wants to merge 11 commits intomainfrom
feat/node-attribution
Draft

Add per-node attribution to q2-debug AST view#122
shikokuchuo wants to merge 11 commits intomainfrom
feat/node-attribution

Conversation

@shikokuchuo
Copy link
Copy Markdown
Member

@shikokuchuo shikokuchuo commented Apr 15, 2026

Closes #115.

Summary

Add per-node authorship to the q2-debug AST view. When enabled via the Settings sidebar toggle, each AST node is colored by the author who last edited it, with a hover badge showing their name and timestamp. Off by default — no impact on normal editing.

How it works

Replays Automerge history diffs to build a per-character { actor, timestamp } map, then resolves each AST node's source location to find its author.

The full build (cold start) processes history in chunks of 50 entries via requestIdleCallback to stay off the main thread. After the initial build, edits trigger debounced incremental updates that only process new history entries. If history gets compacted under us, we detect it and fall back to a full rebuild.

Source locations in the AST are byte offsets into the QMD text, but JS strings are UTF-16 — so we build a byte-to-char index map that handles multi-byte UTF-8 and surrogate pairs. Node attribution results are cached per render cycle.

The attribution data flowing through AttributionContext and into getNodeAttribution() is now source-agnostic — just CharAttribution[] (per-character actor + timestamp). The Automerge-specific incremental update state (processedHeads, processedHistoryIndex) stays internal to the useAttribution hook. This makes it straightforward to swap in a different attribution source (e.g. git blame) by writing a new hook that produces the same { entries: CharAttribution[], byteToCharMap: number[] } shape — no changes needed in the context, renderer, or query logic.

Key pieces:

  • services/attribution.ts — builds the per-character attribution map from Automerge patches (splice/del/put); query function takes source-agnostic CharAttribution[]
  • hooks/useAttribution.ts — React hook managing the async build lifecycle, debounced incremental updates, and cancellation on file switch or unmount; exposes only entries: CharAttribution[] (Automerge bookkeeping stays internal)
  • ReactAstDebugRenderer.tsx — consumes attribution via context, resolves each node's sourceInfoPool entry to a byte range, queries the map, and renders with the author's cursor color + hover badge
  • Editor.tsx — wires up the context provider when the Settings sidebar "Authorship" toggle is enabled (q2-debug format only)

The authorship toggle is a user preference (persisted in localStorage), not a document property — no YAML metadata changes needed.

Also includes minor type fixes in ts-packages/annotated-qmd (unused imports, explicit return types) to compile cleanly under hub-client's strict tsconfig.

Replays Automerge history to build a per-character attribution map,
then colors each AST node by author with hover tooltips showing name
and relative time. Supports chunked async builds, incremental updates,
and graceful degradation when offline or without history.
The hub-client tsconfig enables erasableSyntaxOnly, verbatimModuleSyntax,
and noUnusedLocals which surface errors in annotated-qmd sources resolved
through the types field. Convert parameter properties to explicit field
declarations, split type-only imports, and remove unused variables.
Real Automerge diffs use splice/put/del actions (not insert/del from
the old Text API). Handle splice with value:string, put for field-level
init, and add @automerge/automerge as a direct hub-client dependency.
Also fix relative time display for second-precision timestamps.
The useAttribution hook now only runs when the document has
format: q2-debug AND attribution: true in its YAML frontmatter.
Without the flag, no Automerge history traversal occurs.
…ke test

Remove console.log/warn/error statements used during development,
drop unused _identities parameter from useAttribution() (identities
flow through AttributionContext), and delete the stale spike test
that documented the old Automerge Text API patch format.
Show a colored badge (author dot + name + relative time) on hover
instead of the browser's plain title tooltip. Badge uses a solid
white background with the author's color on border and text, and
appears below the hovered node. CSS is injected once from AstRenderer
rather than per-Node.
Cache node attribution lookups in a Map keyed by sourceInfoId so
re-renders that don't change attribution data make zero WASM calls.
Use byteToCharMap from AttributionContext instead of recomputing it
in the renderer. Replace per-node hidden AttributionBadge elements
with a single floating badge shown on hover via event delegation.
Attribution was previously opt-in via `attribution: true` in the qmd
YAML frontmatter. This is a viewing concern, not a document property,
so move it to a persistent UI toggle in the Settings sidebar instead.

The toggle appears only when the preview format is q2-debug and is
labeled "Authorship" for clarity. The preference persists across
sessions via localStorage.
Consumer-facing interfaces now expose CharAttribution[] entries
instead of the full AttributionMap (which carries Automerge-specific
processedHeads/processedHistoryIndex bookkeeping). This enables
swapping the attribution data source (e.g. to git blame) without
changing any consumer code.
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.

Replay UI: show attribution of highlighted text

1 participant