Plan: Milestone 6 - Scale and Extend.
Done (2026-09-19)
All seven steps are implemented and verified on the web build; desktop and mobile compile and share every code path except the wasm runtime’s placement. Extensions are a managed catalog: every built-in declares
core/optional/opt_inand the permissions it wants; Settings → Extensions toggles them and grants permissions; the shell drops disabled extensions live (no panels, no libraries, no tools). The Flow editor (opt-in) opens*.flow.jsonon a dioxus-flow canvas with a palette from enabledFlowLibrarycontributions, typed ports (tensor-shape unification), per-block parameters, and a Generate button. The Lux.jl library (opt-in) contributes Input / Dense / Conv / MaxPool / Flatten / Dropout / BatchNorm / Loss / Optimiser blocks and writesmodel.jl(a LuxChain+ training scaffold) next to the flow — verified withjulia’s parser. wasm extensions v1: core wasm modules with a JSON ABI (manifest/run, hostlog/call), loaded by wasmtime from~/.config/moonkale/extensions/*.wasmand<folder>/.moonkale/extensions/, permission-checked at every host call, listed in Settings (off by default), their commands offered to the agent as tools;extensions/wordcountis the example. Scale: Barnes–Hut repulsion (θ = 0.8) from 1 500 nodes, iteration caps and label LOD — 100k nodes lay out at 169 ms/step natively and 189 ms/step in Firefox’s wasm (O(n²) extrapolates to ~12 s/step). Phone-sized shell: below 700 px the workbench collapses to one tile with a bottom bar (Files / Search / Editor / Graph / Terminal / Agent / Settings), verified at 420 px. Three new E2E suites (flow,wasm-ext,phone) plus all fourteen earlier ones pass; 64 native tests; clippy/fmt clean.
Steps as executed
| # | step | outcome | notes |
|---|---|---|---|
| 1 | Manifest::{core, optional, opt_in} + with_permissions; ExtensionsFile/ExtensionsSettings in settings (enabled/disabled/permissions, user ← workspace overlay per id); shell filters; Settings → Extensions | ✅ 1 unit test, E2E (flow.mjs toggles the flow editor on) | ext-api.md, ui.md |
| 2 | ext-api::flow (PortType/unify, Param, BlockKind, FlowLibrary, Flow JSON, validate, topological); Extension::flow_libraries(); editors/flow on dioxus-flow 0.1.2; File → New Flow…; Explorer opens .flow.json | ✅ 2 unit tests, E2E flow.mjs | flow.md |
| 3 | extensions/lux: block library, shape unification through the ports, Julia codegen with a lazy flat_features, Generate writes model.jl via the folder source | ✅ 3 unit tests incl. julia -e Meta.parseall | lux.md |
| 4 | ext-host: JSON ABI v1 (abi.rs), wasmtime 48 runtime with permission-checked host calls (runtime.rs), discover; desktop in-process, web through server functions; commands → agent tools; extensions/wordcount example + build.sh | ✅ integration test builds and runs the module; E2E wasm-ext.mjs | ext-host.md, wordcount.md |
| 5 | Barnes–Hut quadtree, iteration caps, label LOD, bench_layout export, GRAPH_LIMIT 100 000, native bench_layout example | ✅ numbers below | graph-render.md |
| 6 | phone shell: narrow from matchMedia, single-tile controlled layout, bottom bar, no persistence while narrow; mobile crate builds | ✅ E2E phone.mjs at 420 px | ui.md |
| 7 | verify, log, vault | ✅ | this note |
Measurements (step 5)
Force layout, ms per step, synthetic graph (n nodes, ~1.5 n edges), this machine (Ryzen 7 7800X3D, single thread):
| nodes | native (cargo run --release --example bench_layout) | wasm in Firefox (bench.mjs) | O(n²) extrapolated from 1k |
|---|---|---|---|
| 1 000 | 1.2 | 1.8 | 1.2 |
| 10 000 | 12.9 | 15.3 | 120 |
| 50 000 | 79.4 | 85.7 | 3 000 |
| 100 000 | 169.3 | 189.3 | 12 000 |
Barnes–Hut turns the step from quadratic to n·log n; wasm costs ~10 %. Drawing 100k nodes is not the problem (two instanced draws); the layout is, and at 120 iterations (the cap above 50k) a 100k graph settles in ~20 s in the browser, ~17 s natively. WebGPU compute is therefore not needed for this milestone (plan: only if Barnes–Hut fell short) and stays deferred. Labels switch off above 20 000 nodes; the panel asks sources for at most 100 000 nodes.
What the user sees
- Settings → Extensions lists every built-in with a toggle and its permissions; the Flow editor and the Lux library are off until switched on (per machine or per folder —
.moonkale/settings.jsoncarriesextensions.enabled/disabled/permissions). wasm modules found on disk are listed underneath, off by default, with permission checkboxes. - With the flow editor on: File → New Flow… creates
untitled.flow.json; the palette (left) shows the enabled libraries’ blocks; click a block to place it, drag from an output handle to an input handle to wire (mismatched shapes are rejected), edit parameters in the block, Generate writesmodel.jlnext to the flow (Lux), Layout auto-arranges, Save (Ctrl+S) stores the JSON. - Install the example wasm extension:
packages/extensions/wordcount/build.sh(needsrustup target add wasm32-unknown-unknown) copieswordcount.wasmto~/.config/moonkale/extensions/; enable it in Settings → Extensions, grant read-sources, then ask the agent to “count the words in README.md” — thewordcount.counttool appears with an approval card (third-party tools are treated as mutating). - On a phone-sized window (< 700 px wide) the rail disappears and a bottom bar switches between the panels; widening the window brings the saved multi-tile layout back.
Deviations from the plan
- Extension enablement stayed in
ext-api(settings::ExtensionsSettings), not inext-host: the shell and the Settings panel already readSettings;ext-hostnow contains only the wasm runtime and ABI, behind thewasmtimefeature (desktop and server), so the web wasm build never compiles wasmtime. - The flow editor keeps one wire per input port and validates at connect time with
PortType::unify(rank-polymorphic tensors:Tensor(Any)unifies with any rank); dioxus-flow’s ownis_valid_connectionhook was enough, so no plan-B SVG canvas. - Flows are files (
*.flow.json,version: 1); the code editor skips them (CodeEditorExtension::skipping(is_flow)) the way it skips markdown, so the flow tab uses the sharededitor:<uuid>panel id and closing/saving/dirty work unchanged. Generateis a toolbar button, not a command palette entry (there is no palette yet);Runis a hint in the status line (julia model.jl) rather than an auto-opened terminal — Lux is not installed here, so the terminal would only show the install message.- wasm ABI v1 is core modules + JSON (
alloc,manifest,runexports;moonkale.log/moonkale.callimports; packed(ptr << 32) | lenreturns). No WIT, no component model, no browser runtime: documented as the next step in Extension System. Permissions are checked per host call in the host, never trusted from the module. - wasm on web runs on the server (
list_wasm_extensions,run_wasm_commandserver functions, granted permissions passed per call from the client’s settings); the browser never loads wasmtime. Discovery uses the server’s config dir (MOONKALE_CONFIG_DIR) and the open folder. - Third-party tools are
Mutatingby policy: an unknown tool name is never classified read-only, so a wasm command always asks unless Allow mutating tools is on. The plan said “commands become tools”; it did not say they would be trusted. - The phone shell is a mode of the same
Shell, not a second component tree: twoPanelWorkspacebranches, each with its own controlled signal (the workbench needs one signal per lifetime),home_tile()maps every contribution tomainwhile narrow, andon_layout_changeis ignored so the phone arrangement never overwrites the saved desktop layout. Switching modes remounts the editors (acceptable: it happens on a resize, not while typing). - No Android build (no SDK here):
cargo check -p mobile --features mobilepasses and the shell is the same; the APK recipe stays inpackages/mobile/README.md.
Problems hit (→ Problem Log)
- P-073 dioxus-flow handle geometry lags the auto-layout animation: after Layout,
Handlepositions used for connection validation are stale for a few frames, so a drag started right after a layout can miss. The E2E wires before laying out; a real fix needs a settled-layout callback from dioxus-flow (not yet requested upstream). Related: the third palette column sat under the tile splitter at 1500 px, so blocks are placed on a 165 px grid. - P-074 The workbench renders a reconciled copy of a controlled layout: newly attached panels (and the auto-activated document tab) exist in the rendered tree before they are written to the application’s signal, so reading “which tab is in front” from the signal was stale. The phone bar reconciles the signal with the current placements before reading or activating, exactly as
ShowPaneldoes. - P-059 (fixture drift) is closed:
packages/web/tests/e2e/fixture.shbuilds the fixture folder deterministically (users table, smallpeople.lbugviaseed_people … small), and the regression script resetsHome.md,.moonkale/and generated files between suites. The scratchpad this session used was wiped at the day change, which is what forced the script into existence. - P-047 again (desktop): the narrow-shell watcher (
matchMediaeval) was created in a hook and never reported on desktop; started from the shell’sonmountednow, and the desktop window’s minimum width is 360 px so the phone layout can be tried there. - P-070 again: assets edited without a Rust change are served stale by
dx serve— touch a.rsin the crate that owns the asset and let dx rebuild (the hash is computed at compile time). mistral-code-latestwas not needed this milestone; all suites run on the mock provider (MOONKALE_LLM=mock).
Decisions worth keeping
- Enablement is data:
default_enabledin the manifest, overridden byextensions.enabled/disabledper scope; the shell filters every render, so a toggle applies live without a restart. - Libraries are contributions: the flow editor never knows Lux exists; a second library (MTK, a data-pipeline set) is another
FlowLibraryfrom another extension. - The ABI is versioned and boring (
abi: 1, JSON strings both ways) so it can be replaced by WIT without changing the manifest format. - Permissions are enforced at the host boundary, and third-party commands are policy-
Mutatinguntil proven otherwise. - Barnes–Hut before compute shaders: works on WebGL2 and on the desktop’s GL path, and it removed the wall.
- Narrow mode is not a layout to save.
Verified
- Web (Firefox, Playwright, mock provider, port 8090, fresh fixture per suite):
milestone1,menubar,session,graph,links-sqlite,terminal,typst,lsp,ladybug,agent,search-trace,settings,rich,agent-writes,flow,wasm-ext,phone— all PASS. - Native:
cargo test --workspace --features moonkale-sources-graph/ladybug,moonkale-ext-host/wasmtime→ 64 passed, 0 failed (3 ignored: live services);cargo clippy --workspace --all-targetsclean;cargo fmtclean;cargo build -p desktop --features desktopandcargo check -p mobile --features mobilepass. - Julia: the generated
model.jlfor a CNN parses (Meta.parseall); running it needsLux,Optimisers,Zygote.
Confirmed on desktop by Daniel (2026-09-19)
The shell switches to the phone layout once the window goes below the width threshold, and the agent runs the wasm
wordcountcommand after the extension was enabled and granted read-sources. (First attempt did nothing: the media-query watcher was created from a hook, which is lost on desktop — P-047 — and the window’s minimum width was 640 px; both fixed.)
What to look at on desktop
Ctrl+,→ Extensions: switch Flow editor and Lux.jl blocks on; File → New Flow…; place Input → Conv → MaxPool → Flatten → Dense → Loss, wire them, Generate, openmodel.jl.packages/extensions/wordcount/build.sh, restart the app, Settings → Extensions → enable Word count (wasm example) and tick read-sources; ask the agent for a word count.- Open a
.lbugwith many rows, or a big folder: the graph settles instead of freezing; labels appear once you zoom in. - Resize the window below 700 px: the bottom bar appears; widen it again.
Deferred to Milestone 7
WIT/component extensions and the browser runtime (R-28), an MTK/data-pipeline flow library, execute-in-place Julia (run the generated model in a terminal with output back into the flow), GPU compute layouts (not needed at 100k), 3D graph (R-27), Postgres/Turso (R-19), TypeDB/Helix (R-26), an Android build on a machine with the SDK, OS keychain, a cancel for running tools (P-069), settled-layout callback in dioxus-flow (P-073).