Plan: Milestone 2 - Graph Appears.
Done (2026-09-18)
All seven steps are implemented. The index (wiki-links + tree-sitter Rust symbols) builds on folder open and refreshes on save; the Graph panel draws it with the wgpu renderer wasm module — confirmed rendering in Firefox by Daniel (WebGL/WebGPU are absent in the headless test browser, so the automated suite verifies everything up to the GPU and a legible failure beyond it); the Links panel shows backlinks/outgoing/unresolved for the active document; a
.sqlitefile opens as a database source whose tables open in a read-only table editor with a SQL box. Five Playwright suites pass (packages/web/tests/e2e/), 9 integration + 17 unit tests, clippy/fmt clean. Desktop confirmed by Daniel (2026-09-18): the app runs and the graph renders inside the WebKitGTK webview — ADR-0011 plan A works on Linux without the native-overlay fallback.
Steps as executed
| # | step | outcome | notes |
|---|---|---|---|
| 1 | core: Query::{Neighbours, All, Text}, Value, Table, TextDialect, Source::refresh | ✅ 8 unit tests | folder source answers Neighbours as children, refuses All/Text |
| 2 | moonkale-index | ✅ 4 integration + 2 unit tests | index.md |
| 3 | platforms: open_folder → [folder, index]; server refresh_source; refresh after save; graph_epoch | ✅ all targets | web build gets the index as a second RemoteSource |
| 4 | moonkale-graph-render wasm module (wgpu 30, force layout, camera, labels overlay, pointer events) | ✅ builds, 4 native unit tests, 2.4 MB module | graph-render.md |
| 5 | moonkale-editor-graph panel (host) | ✅ E2E packages/web/tests/e2e/graph.mjs | graph.md |
| 6 | moonkale-editor-markdown Links panel | ✅ E2E links-sqlite.mjs | markdown.md |
| 7 | moonkale-sources-sql SQLite + moonkale-editor-table | ✅ 3 tests + E2E | sources-sql.md, table.md |
Deviations from the plan
- Index stats live in the descriptor’s display name (“index: 5 files · 5 links · 5 symbols”) so the status bar shows them without a new API. Cheap; to be replaced by a proper stats query.
- Symbol keys carry the line number (
src/lib.rs#fn:new@3) so twofn newin differentimpls stay distinct. Stable until code moves — good enough until the index persists. - Graph nodes are indexed by position in the JSON sent to the renderer (
edges: [{a, b}]), not by id — halves the payload. - No text on the GPU: labels are a 2D-canvas overlay, capped at ~400 and hidden when zoomed out.
- Tables open as
Workspace::views, not documents:open_noderoutesNodeKind::Tableinto a separate list the table extension claims. Same mechanism will serve graph views and rows. .sqlitefiles are opened by clicking them in the Explorer (path = folder root + relative key, so the same click works on web through the server’sMOONKALE_ROOTcheck); no separate “connect to database” dialog yet.- Columns are graph nodes (
NodeKind::Column,Databaseadded to the open enum) so a database schema is a graph; the Graph panel does not show non-index sources yet.
Problems hit (→ Problem Log)
- P-046 Random ids don’t survive hydration. The panel’s canvas ids were derived from the (random)
WindowId; the server rendered one id, hydration kept it, the client script looked for another and returned silently. Same root cause as P-034. Rule: element ids used fromevalmust be deterministic. - P-047 Eval messages are lost before the host awaits / after the script returns. A message sent by JS before the Rust task has polled
recv()was dropped, and a script thatreturns right afterdioxus.sendloses that message too. Pattern now: JS waits for aninithandshake first, and a failing script parks onrecv()until the host sendsdestroy. Also: don’t mount an eval from ause_effectthat writes a signal it reads — mount from the element’sonmountedhandler. - P-048
dx servekeeps missing non-rsx Rust changes (third occurrence); every change to the eval script constant needed a restart. Costly: ~3 min per cycle. - P-049 Headless Firefox has no GL on this machine (
FEATURE_FAILURE_WEBGL_EXHAUSTED_DRIVERS, even with software-rendering prefs and Mesa env), andwgpuhangs rather than fails when a surface can’t get a context. The panel now probes for WebGL2/WebGPU before callingcreate()and shows the reason. - P-051 Graph view jumped back after drags (auto-fit on every settle; dragged nodes unpinned) — fixed after Daniel’s test.
- P-052 A new side panel stole the active tab: panels attached to a tile become active; the default layout now lists
["explorer", "links"]explicitly. - P-053
dragendnot delivered after a drop on the workbench’s drop zone (Firefox): the session drag now also ends on anydropin the source window. - P-050 wgpu 30 API drift vs. what I remembered (
InstanceDescriptor::new_without_display_handle,apply_limit_buckets,color_space,Option<&BindGroupLayout>,CurrentSurfaceTexture,queue.present). Read the registry source, don’t guess.
Decisions worth keeping
- Renderer as a standalone wasm module loaded like
codemirror.js— the same artefact works in the browser and inside the desktop webview (ADR-0011 plan A). Built bypackages/editors/graph-render/build.shwith dx’swasm-bindgenintoeditors/graph/assets/; committed. wasm-releaseprofile (opt-level = "s", fat LTO,panic = "abort", strip): 4.3 MB → 2.4 MB.wasm-opt -Ozwould help further; not installed here.require_git(false)-style honesty for the index:.git,target,node_modulesare skipped even when not ignored; ≤ 5000 files, ≤ 512 KB per text file.- Backlinks = incoming
Linksedges fromNeighbours{depth 1, In}— the parent directory’sContainsedge is incoming too, so consumers filter by edge kind.
Verified by hand
Web (Firefox) and desktop (Linux, WebKitGTK): the graph renders, pan/zoom/drag work after P-051.
What to look at in a real browser / on desktop
- Open a folder with markdown and Rust files → status bar shows
index: …. - Graph tab → nodes and edges should draw; the info line shows the backend (
browserwebgpuorgl). If it shows an error instead, that text is the diagnosis. - Hover → popup; double-click a file → opens in the editor; drag nodes; wheel zooms; Local follows the active document.