DocumentationDecision recordsADR-0020

UI architecture selection

ADR-0004 deferred every UI and editor decision until measured evidence existed. The UI architecture gate in TODO.md required benchmarking editor candidates,…

accepted

Context

ADR-0004 deferred every UI and editor decision until measured evidence existed. The UI architecture gate in TODO.md required benchmarking editor candidates, cached switching, tree virtualization, renderer store isolation, persistent editor hosting, and desktop bridge overhead before selecting a stack. All six measurement items are complete, with evidence recorded under docs/benchmarks/:

  • 2026-07-21-editor-candidates-initial.md, 2026-07-21-editor-retained.md, 2026-07-21-editor-bounded.md, 2026-07-21-editor-native-presentation.md, 2026-07-21-editor-product-contract.md, 2026-07-21-editor-structured-window.md
  • 2026-07-21-renderer-store-selectors.md

The tree-virtualization and desktop-bridge measurements also informed this decision. Both conclusions were carried into the product (bounded row pool, zero-IPC navigation) and are covered by product tests and the later reference-production measurement, so their disposable harnesses and benchmark records were retired after the decision landed.

The product decision that unblocks a simpler default: notes are expected to stay short. Multi-thousand-block documents are an edge case, not the primary workload.

Decision

Editor: ProseMirror

Direct ProseMirror is selected. Lexical is rejected: it showed no product capability advantage and consistently used more DOM, memory, and interaction time in the retained and bounded measurements.

The product editor uses one persistent ProseMirror instance that is never remounted during navigation, with the representative Markdown schema, keymaps, list commands, input rules, and slash-query state from spikes/ui-architecture/src/editors/prosemirror-product.ts.

Editor rendering path: whole-document by default, bounded window as fallback

The default path renders the whole canonical document in the persistent editor. Measurements show this meets the cached-swap budget at 50 blocks and is marginal at 500; since notes are expected to stay short, the simple path ships first and keeps navigation free of projection bookkeeping.

The bounded 192-block canonical window (structured JSON preservation, compact per-note history, window movement, selection and scroll restoration) is retained as the validated fallback for large documents: 3.845 ms switching P95 and 225 DOM elements at 2,000 blocks. It is enabled per document above a block-count threshold chosen during implementation, and is not part of the MVP navigation path.

Renderer store: dependency-free external store with useSyncExternalStore

React is selected as the view layer. Application state lives in a dependency-free external store with narrow selector subscriptions, ported from spikes/renderer-store. Measured isolation guarantees carry over as invariants: editor keystrokes must not render the application shell, and note selection must render selected-note consumers only. Render-count assertions from the spike become product tests.

Build tool: Vite

Vite builds the renderer. All routes and the editor load in the startup bundle; no post-startup chunk loading exists on navigation or editing paths, per the performance contract.

Desktop shell: Tauri 2

Tauri 2 hosts the renderer over the existing Rust workspace. The measured bridge contract holds: zero IPC during navigation, optimistic FIFO-acknowledged writes through the serialized runtime, and sub-millisecond command overhead at product payload sizes.

Undo history policy (by fiat)

Edits within a 500 ms burst group into one undo entry. Per-note history is capped at 200 entries; the oldest entries are dropped beyond the cap. This is standard editor practice and requires no further measurement.

Deferred evidence

Fixed-runner (reference hardware) presentation evidence is deferred until the MVP UI exists; measuring disposable spikes on fixed hardware would not survive into product evidence. The 100-cached-switch zero-dropped-frame proof runs once against the real application after the MVP UI is assembled.

Performance contract check

Every selected option was measured within the provisional budgets of docs/performance-contract.md in its spike: cached editor swap, tree interaction, store transitions, and bridge overhead all held P95 below 8 ms with zero observed dropped frames on the development machine. No candidate that failed the contract was selected; the unbounded retained editor pool and naive full-DOM swap at 2,000 blocks were rejected on that basis.

Consequences

  • Product UI construction starts now; spike code is ported, not re-derived.
  • Whole-note find/copy, IME completion, and accessible traversal are implemented in the product against the canonical document, not proven in further spikes.
  • The bounded-window fallback keeps large documents viable without complicating the default navigation path.
  • Fixed-runner evidence is a post-MVP verification step, not a construction gate.