fix(board): sheet @-mentions + subpages on local boards - #265
Conversation
The sheet editor's PageProvider still called the REST board API (listBoardContents / getNote / addNotes), but boards went offline-first — those routes have no local-board counterpart, so apiFetch threw. Result: @ autocomplete returned nothing, existing @ chips couldn't resolve, and /subpage created nothing. (The sidebar migrated to the on-device store in #190/#191; the PageProvider was missed.) Migrate all three methods to the local replica: - list -> listLocalBoardContents (sheet-kind nodes) - get -> live store, else the whole-board persistence replica - create -> build a real sheet (createDefaultNote + noteToNode, sets styleType so it lists) and write sync-correctly: same layer as the view -> the store; a subpage's child layer -> the S7 headless intake (record + submitLocalBatch scene:false), so the user's view never moves. No call-site changes (both providers just pass boardId/parentNoteId/onNavigate).
…o dangling chip) Address code-review findings on the on-device page provider: - create() awaits persistence.flush() before invalidating, so an off-scene subpage (no store 'change' to flush-chain) is durable before the contents index reloads — otherwise the picker/sidebar stayed stale until an unrelated edit. - writeSheetNode returns a boolean; create() throws when there's no live board to write into, so the editor never inserts a chip for a non-existent page. - get() filters to sheet-kind on both the live-store and replica paths (a page is a sheet), matching list(). - list()/get() share a short (1.5s) per-instance whole-board cache so typeahead and hover cards don't replay the snapshot+oplog on every call. Tests: off-scene create is durable + listable without a manual flush; get() on a non-sheet returns null; create() throws with no live board.
Review response (code-review high)All 5 findings were valid; fixed in 48e4f98:
Added tests: off-scene create is durable + listable without a manual flush; |
The contents index keyed surface detection purely on the display `styleType`, which only the convert layer (createDefaultNote -> noteToNode) sets. Agent surfaces built through the mutator carry the canonical `node.type` but no `styleType`, so an agent-created sheet/folder never appeared in the sidebar tree or the @ page picker (though it rendered fine on the canvas). Fall back to `node.type` when `styleType` is absent, in both surface readers: - listLocalBoardContents (sidebar tree + page picker source) - affectsSurfaceTree (live sidebar refresh on create/remove) styleType stays a NoteNodeData (display) field, not canonical DimNodeData, so the fix is a tolerant reader, not stamping an off-model field on agent writes. Tests: an agent-style sheet (node.type only) is indexed by listLocalBoardContents + the provider list; affectsSurfaceTree fires for it.
Added: agent-authored surfaces now index correctly (dcf4c3d)Folded in the Problem: the contents index (sidebar tree + Fix: fall back to Tests added; full board sweep (248) + check-all green. The PR now covers: |
- get() live-store lookup now runs inside the try/catch, so a malformed id can't reject the promise — honors the 'return null on missing' contract. - extract titleFromData(); nodeToPage + list() share it (no duplicated RichText-vs-string decoding). - affectsSurfaceTree: node.update that patches the node-level `type` (a kind change) now counts as tree-affecting, symmetric with the add/remove node.type fallback. Skipped by design: re-adding the ['boardContents'] invalidation (dead cache — useBoardContents has no callers; the sidebar reads ['localBoardContents']), and the REST get/list fallback (offline-first narrowing; self-heals on materialize, and re-adding it would break local boards).
Second review response (code-review high, post-styleType patch)Ran a second pass since
Skipped, by design (verified):
check-all + provider/sidebar suites green. |
Completes local-board sub-pages: creating one worked (#265), but OPENING it showed 'This sheet no longer exists'. The surface host resolved a note only via the live store (current layer) or REST — a /subpage lives off-scene in a sub-layer, so it was in neither (not on the canvas, not on the server for a local board), and even a successful load would have lost edits (save was store-or-REST too). Add useOffSceneNote (+ testable openOffSceneNoteStore core): seed the note's layer from the whole-board replica into a throwaway store and forward its edits to the sync intake (record + submitLocalBatch scene:false) — the surface-host analog of the agent's HeadlessMutator. sheet-panel now resolves the note as live-store -> off-scene replica -> REST, and points `store` at whichever holds it, so the existing store.updateNode save path works off-scene unchanged. REST is gated to fire only after the off-scene load settles (synced-not-materialized). Save callbacks read prevData from the live store node so multi-edit merges don't clobber a prior off-scene edit. Tests: off-scene sub-page loads into an editable store; an edit records + enters the sync intake scene:false and lands in the oplog.
…ar, caches) - Off-scene node is now LIVE: it re-reads on the off-scene store's changes, so a sub-page rename / icon edit shows in the panel instead of reverting to the load-time snapshot (edits were saved but looked reverted). - Surface-relevant off-scene edits invalidate the sidebar's localBoardContents cache (the sidebar sync only watches the live store, so it never saw them). - Optimistic rename/icon patch now targets localBoardContents (what the sidebar reads) instead of the dead boardContents key. - openOffSceneNoteStore returns a null store when the note isn't in the replica, so a synced-not-materialized note falls back to REST without building a doomed store (no wasted whole-board replay + subscription). - Page provider get() trusts the live store only when it IS this provider's board (guards a sub-graph id collision). Skipped: the 1500ms @-picker loadBoard cache staleness — an accepted trade-off against per-keystroke oplog replay. Tests: not-in-replica returns a null store; existing off-scene load + edit-sync tests updated for the boardId param.
Third review response (code-review high, off-scene surface)Findings on the surface-host commit, addressed in 1510a93:
Skipped, noted: #6 — the 1500ms Tests updated (+ not-in-replica returns a null store). check-all + 290-test board sweep green. |
The bug
In sheet notes, the
@-mention (page reference) syntax and the/subpagesyntax stopped working on local boards — no autocomplete, existing@chips wouldn't resolve, and creating a subpage did nothing.Root cause
createBoardPageProvider(which backs both syntaxes) was still calling the REST board API —listBoardContents(/boards/{id}/contents),getNote(/boards/{id}/notes/{id}),addNotes. When boards became offline-first, those routes have no local-board counterpart, soapiFetchthrows (src/api.ts:236). Each method fails:list(autocomplete) throws → no suggestions,get(chip resolution, via page-cache) → caught →null→ chips can't resolve,create(/subpage, "Create new") throws → nothing created.Pre-existing, not from recent work. The provider was last touched in #100 (page references); the on-device contents path arrived in #190/#191 (the offline-first sidebar migration), which moved the sidebar to
useLocalBoardContentsbut left the PageProvider on the dead REST path.list-board-contents.tseven notes "no component consumes this anymore." Our recent PRs never touch this file.The fix
Migrate all three methods to the on-device replica (no call-site changes — both providers just pass
boardId/parentNoteId/onNavigate):list→listLocalBoardContents(boardId), filtered to sheet-kind nodes.get→ the live store first (current layer), else the whole-board persistence replica; maps the harness node →Page.create→ builds a real sheet viacreateDefaultNote({nodeType:"sheet"}) → noteToNode(which setsstyleType, so it shows up inlist), then writes sync-correctly: same layer as the view →store.addNode(renders + syncs); a subpage's child layer (always off-scene) → the S7 headless intake (record+submitLocalBatch(scene:false)), so the user's view never moves. Invalidates["localBoardContents", boardId].Test plan
noteToPageunit tests unchanged (still green).listreturns sheets and excludes non-sheets;list(query)filters by title;getresolves title + snippet from the whole-board replica (and null for missing);createadds a top-level sheet in-scene with an empty body and lists it;createwith a parent writes the subpage off-scene (not in the visible store) under the right parent.npm run check-all+ a board sweep (240 tests) pass.Follow-up (out of scope)
Agent-created sheets (via the mutator) set
node.typebut notdata.styleType, so they don't yet appear inlistLocalBoardContents(neither the@-page picker nor the sidebar). Worth a separate small fix so agent-authored sheets are referenceable.