UI Improvement Roadmap

A living plan for Sysprose's user interface. It records what has shipped, what's next, and a backlog of ideas — grouped by theme. Each item is sized (S/M/L) and, where shipped, links to its commit + E2E coverage.

Working method (unchanged): every UI change goes through implement → independent Fable adversarial review → validate (tsc · vitest · build · E2E) → screenshot → commit, and refreshes docs/TEST-REPORT.md counts.

Status: the feature items below are implemented. Canvas drag-to-reparent (+ multi), per-element PageRank centrality, roving menu tab-order, and the shared toolbar↔canvas menu behavior all shipped in 9ba0b62 (each Fable-reviewed with all confirmed findings fixed). Still open: three measured performance items from the profiling round, P1 for a piece with nested boxes (the compound-layout option is now off for a flat piece, but a nested piece still pays for it), P2 (the solver clones its value map per probe) and P3 (the first "Apply text → model" click builds the parser on the page's thread), and Electron/Tauri (a hard environmental blocker: no Windows cross-build here). Community-detection per element stays deferred as low-value optional depth.


✅ Shipped

Explorer / navigation

Toolbar

Palette (per-diagram tools)

Canvas

Diagram layout

Keyboard

What-if

Diagram styling

Analysis

Theme

Platform / distribution

Accessibility / polish

Diagram editing

Analysis

Accessibility / polish (menus)


⚡ Performance — measured, and the Rust question answered

A profiling round (2026-09-08, six subsystems) asked what should be reimplemented in Rust/WebAssembly to improve the user's experience and memory use. The answer is: nothing yet. Four of six subsystems returned do-not-port, and the two largest user-visible costs are JavaScript defects a port would have inherited unchanged. Ordered by measured value.

P1 — elk.hierarchyHandling: 'INCLUDE_CHILDREN' is applied to every view — fixed for flat pieces, still open for nested ones

The single worst measured defect in the tool. The option makes ELK route edges across the node hierarchy, and it was set unconditionally for all fourteen view kinds. Measured with elkjs in this repo, one nested graph, everything else identical:

Graph With the option Without Ratio
300 leaf nodes in 30 parents, degree 2 13 781 ms 165 ms 83x
950 flat nodes, degree 1 (no nesting) 569 ms 342 ms 1.7x

The cost appears only when nodes actually have children — in the profile's reading, every structural view of a SysML model, because parts contain parts. A flat graph barely notices, which is why this had gone unseen. The profile's fix: set the option only when the projected graph has at least one nested node.

Shipped (214b3c5), the flat half. layoutOptionsFor(viewKind, nested) sets the option for a connected piece that contains nested boxes and SEPARATE_CHILDREN for every other piece (toElkTree computes nested, src/diagram/layout.ts). U diagram.layout2 asserts the option for a flat and for a nested call, not its timing. That is the 1.7× row, and it leaves a piece that nests exactly as it was.

Still open at 5943654: a piece that nests. toElkTree passes nested = true for any piece with one nested box, and that keeps INCLUDE_CHILDREN. Measured through layoutDiagram on the interconnection view, 30 parents of 10 children each and two edges per leaf (330 nodes): 11 113 ms as it stands (14 s on a loaded machine), 171 ms with SEPARATE_CHILDREN forced (65×); the raw ELK options for the profile's own graph give 12 003 ms against 164 ms. ELK runs in a Web Worker (src/ui/elk-worker.ts), so the page does not freeze, but the diagram appears no sooner. On the drone-swarm v9 model of mbse-workflow (42 584 elements, one run each) the option is not the dominant cost: the General view takes 5 035 ms against 4 469 ms forced, interconnection 550 against 486, and tree 44 184 against 42 411. Whether to drop the option for nested pieces (trading the layout of edges that cross a parent's border for time), or to bound the compound layout, is not decided; it is tracked in TODO.md (Decide what the layout does with a piece that nests) and awaits a user decision.

P2 — the solver clones its whole value map per probe (src/semantics/solver.ts: solveScalar, newtonStep, substitutedNewton)

const trial = new Map(values) sits inside the residual closure that every finite-difference probe calls (solver.ts:2520, :2706 and :2979 at 5943654), so a solve clones the entire map once per probe. The profiling round counted about 50 000 clones for one solve of the example shipped then. That count was not reproduced at 5943654. Counting new Map(<Map>) calls during a solve, solve copies a map 3 to 510 times on the six models in examples/ and solveFeasible 5 to 21 times, each in tens of milliseconds; on the drone-swarm v9 model of mbse-workflow (42 584 elements) solve makes 3 082 clones and solveFeasible 132, each in under 100 ms. So P2 is a code-level defect whose cost is not visible on any model measured since, not a measured cost. The fix is unchanged: set the one key, read the residual, restore it. No behaviour change; the map is not retained.

P3 — the first "Apply text → model" click blocks the main thread for ~1.28 s

Not parsing — parser construction. Chevrotain's ALL(*) lookahead analysis (performSelfAnalysis) costs 1 387 ms in the browser and 1 102 ms of it is one function, containsPath. Subsequent applies are 4–9 ms. A profiler tested whether configuration fixes it: maxLookahead: 3 fails the grammar outright with an ambiguity, and 5 and 8 do not finish in 150 s — so ALL(*) is the cheapest option Chevrotain offers this grammar, not a misconfiguration. Fix: build the parser in a Web Worker. Measured boundary cost to move a parse result across: 1.2 ms. Still open at 5943654: there is no parser worker (the dedicated workers are ELK's layout and the palette's checks), and in Node the first parse of a one-line model takes 1.2 s and the next 2 ms. Since branch load-health (2026-10-14) the checks worker builds the parser too, and parses each text a palette check's load health needs there; Apply's parse is still on the page.

What the profiling ruled OUT, with the arithmetic

One Rust candidate stays open, unmerged and unscheduled: a layered layout engine behind layoutDiagram, where the boundary cost is 0.05 % of the work. Its bar is ≥3x against a baseline without the nested-layout cost of P1 (the option off, or the compound layout fixed), not against the 11 to 14 s a nested piece takes today — a 3x win over a defect is not a win.

🔜 Next — diagram editing (near-term)

Item Size Notes
Multi-reparent M ✅ shipped — canvas drag-to-reparent works for a single node and for a whole multi-selection (subtree-root-reduced, one undo step). See Diagram editing above.

🟡 Medium — navigation & analysis intelligence

Item Size Notes
Richer keyboard shortcuts S ✅ shipped — Delete/Backspace, Ctrl+D duplicate, Ctrl+C/V copy-paste, digit view-switch hotkeys, / focus-search (on top of undo/redo/save).
Metrics overlay M ✅ where-used + degree/coupling + per-element PageRank centrality shipped in Properties. Remaining (optional): community-detection per element (whole-graph — already in the Analysis view).

🟢 Platform / distribution

Item Size Notes
Electron / Tauri desktop L A native .exe window; must be built on Windows (can't cross-compile in this Linux env). Documented as an environmental blocker.

🎨 Polish / accessibility

Item Size Notes
Keyboard navigation M ✅ focus-visible rings + arrow-key tree navigation + roving tab-order through the menus (useRovingMenu) shipped.
Context-menu polish S ✅ close-on-canvas-move + item icons + shared toolbar↔canvas menu behavior (useRovingMenu: roving/focus-lifecycle/ARIA) shipped; node/edge/pane menus share useContextMenuChrome. (Dismiss-logic dedup between the anchored dropdown and fixed canvas menus is a deliberate, low-value deferral.)

Known limitations (tracked, not yet scheduled)

This roadmap is maintained alongside the code — update it as items ship.