Kroma — native GPU UI framework

  • Area: Engines / SDK (engines/)
  • Path: engines/sdk/kroma
  • Kind: Rust library + verification bins — the Stack's native GPU UI framework
  • RFCs: stack-RFC-019 (UI render strategy, accepted) · stack-RFC-021 (Kroma ↔ compositor/DE contract, accepted)
  • Status: in development — the render substrate + a complete terminal are built and verified; the broader app-framework layer is ahead

Role in the stack

Kroma is the Stack's long-term UI convergence target: the Verge design system as a Rust widgetscene layer rendered by *ne wgpuVello backend to native GPU (VulkanMetalDX12 on desktop+mobile, WebGPU on the web). One codebase, Verge pixel-identical everywhere, Koder owning the whole UI stack — the bet that removes Flutter AND GTK at convergence*for the graphical surfaces.

It does not cover every variant: app/cli + app/tui are text (Go/Bubble Tea) and landing/ is HTML — Kroma is for GPU-rendered app UI, not text or content surfaces. Retiring Flutter is the goal but is a long, per-app, evidence-gated migration (each app migrates when there's evidence it should — e.g. kterm#003 is gated_by: evidence-soak).

The first consumer / proof-slice is the native terminal (kterm#007, gated). Building that terminal end-to-end is how the RFC-019 §4.2 gate (a11y + headless-testability + measured feel) was cleared substantively GO.

Architecture

PTY → vte (VT parser) → VtScreen (Cell grid + terminal semantics) → Grid widget → painter (Vello scene) → Backend (wgpu present). Backend-agnostic seams:

  • widgetWidget trait (layout/paintsync_focus over Vello) under

    min..max LayoutCtx constraints, Verge tokens, TextContainerLinear (flex weights + CrossAlign incl. Stretch)FillBoxClickableFocusableButtonTextField Scrollable (interactive rendered widgets — Verge states, caret editing, clipped scroll viewport, flex/stretch layout), and the terminal Grid.

  • event — input events (pointerscrollkey) + the retained-tree dispatch with single-

    pointer capture + keyboard focus (DispatcherEventCtx): hit-testpress→release gesture (Clickable), id-based key→Focusable routing with Tab traversal, wheel scroll.

  • view — the declarative app spine (App<State> + pure view(&State) -> Box<dyn View<State>>

    + recursive keyed reconcile + a message loop): the view tree is diffed so stateful widget state + keyboard focus survive a rebuild at any depth, stateless nodes re-render, and a Button's action runs the loop (event → action → state → reconcile) with reused widgets config-updated. View is a trait — open for extension (a consumer ships a custom widget + node); built-ins textfieldbuttoncolumnscrollable (the last a stateful container, scroll offset preserved while its child reconciles) — done/062066. (Flutter/Elm model, minimises Flutter→Kroma porting.) Framework layer 5 complete; constraint layout next.

  • terminal_paneTerminalPane: a live terminal (TermSession + VT + GlyphCache) as a

    Widget / Tile leaf — layout sizes the grid, paint renders the VT Grid, event feeds keys. Cross-target (S4b): spawn/spawn_in (local PTY) are desktop-gated; from_session wraps an any-transport session for the web build. The terminal-in-a-pane half of kterm-on-Kroma (pending/078 S2/S4b).

  • tilingTileTree/Tile: a binary split-tree of panes (any widget is a pane), the

    Rust successor to the Flutter koder_tiling; splitcloseequalize + pane focus/event routing. Foundation of the kterm-on-Kroma rewrite (pending/078 S1).

  • tiling_terminalTilingTerminal + run_tiling_terminal: the live *ulti-pane tiling

    terminal (a TileTree of TerminalPanes) — split spawns a fresh PTY, close reaps it, focus cycles/clicks; the winit runner wakes a redraw on any pane's output and intercepts Ctrl+Shift chords (EO split, W close, X equalize, →← focus) + font zoom Ctrl++-0 (set_font_pxzoom_fontreset_font_px, clamp 6–72px — S6). Controller is headless-testable; the window isn't. The interactive heart of kterm-on-Kroma (pending/078 S2b). Desktop-only. The windowed app is kroma-kterm*(src/kterm_bin.rs): autosave + --restore (reopen the last workspace) + --version--help (standard-flags R1R2).

  • workspace — workspace session restore (kterm-RFC-002 / pending/078 S3): the *ersioned

    snapshot schema (WorkspaceSnapshot/LayoutNode — a split tree + per-pane cwd + restore-handler spec, serde JSON, schema_version always written/checked) + atomic crash-safe persistence (temp+ rename latest.json + rotated history). TilingTerminal::snapshot()/from_layout() capture + rebuild the live tiling layout (each pane respawns at its captured cwd) + an optional WindowGeometry (xywhmaximized/monitor, restored on window-create — faithful X11, best-effort Wayland). Schema cross-target (web restore reuses it); file I/O desktop. S3 complete*(schemaautosavehandlers/geometry); --restore CLI + .desktop favorite are kterm-product wiring on the SDK blocks.

  • autosave — the autosave engine (kterm-RFC-002 §3 / pending/078 S3b): Autosave decides

    when to persist a workspace snapshot — debounce on layout change (TilingTerminal::layout_revision) + periodic heartbeat (captures cwd drift). Decision is pure + clock-injected (headless-testable); maybe_save ties it to an atomic write with dedup (no rewrite of an unchanged snapshot). Wired into the windowed runner via run_tiling_terminal_opts(…, AutosaveConfig).

  • restore_handler — pane-restore-handler registry (kterm-RFC-002 §2 / pending/078 S3c–e): the

    extension point RestoreHandler/HandlerRegistry classifies a live pane → {handler,args} and plans a spec → RestoreAction (Shell / Run / Dormant). Allowlist/deny-by-default (§2.1): only a safe handler auto-runs; a captured generic command comes back pre-typed-not-executed. shell fallback + a /proc process-tree reader (foreground_of/descendants_of). Built-in handlers: claude resolves a session id (argv --resume, or the owned .jsonl via /proc/fd) → k.claude --resume <id> (resume re-attaches, no tool re-exec); ssh reconnects (ssh <target>, options skipped); editor reopens the same files (vimnvimnano/… by basename) — all safe_replay. with_defaults() = claude → ssh → editor → shell (the complete RFC §2 v1 table).

  • app_runnerrun_app(app, title): the general on-screen window runner for a declarative

    App<State> (the non-terminal counterpart of winit_backend::run_terminal) — winit window + wgpuVello surface, paints the widget tree per frame, routes pointerwheel/keyboard → kroma Events into the app (the message loop runs live). Turns a view(state) app into a runnable desktop binary (done/070; kroma-form-window proven live under Xvfb).

  • painterlayout_and_paint(root) -> Scene + headless render-to-PNG. The seam

    between "app builds a frame" and "host presents it".

  • backend (RFC-021) — Backend trait + HeadlessBackend (offscreen → PNG, the test

    path); winit_backend::WinitBackend is the on-screen window (winit + Vello surface).

  • vtVtScreen over vte: full CSI (cursorerasescroll/SGR), alt-screen, DECTCEM,

    256-colour + truecolour, pending-wrap, scrollback, mouse-mode tracking, OSC (titlecolour hyperlinksclipboard), DADSRDECRQMXTVERSION query replies, bracketed-paste + focus-event modes.

  • sessionTermSession: a VT terminal driven over a Transport trait (kroma#078 S4a) —

    PtyTransport (desktop: local PTY+shell) or a socket transport (web, S4b); bidirectional I/O, writes query replies back. EchoTransport is the cross-target PTY-less proof. The VT/grid core is shared across desktop + web (the seam that lets the tiling terminal run in the browser over the kterm server).

  • ws_protocol — the kterm PTY-over-WebSocket wire codec (kroma#078 S4b-2): encode_resize/

    parse_resize (the {"type":"resize",cols,rows} control frame) + classify_inbound (binary→PTY output, text→control). Pure + cross-target (headless-tested); owns the wire contract with the kterm Go server. ws_transport (wasm-only) — WsTransport impl Transport over a web-sys WebSocket speaking it, so the web tiling terminal drives the same VT/grid core over a socket.

  • a11y — AccessKit tree (Window>Terminal>row Labels + caret) and the AT-SPI publisher.
  • contrast — WCAG 2.1 (matches design-gen) + the minimum-contrast a11y filter.
  • scroll / mouse / selection / search — the headless cores of

    scrollback navigation, mouse encoding, text selection+copy, and incremental find (with case-sensitivity + whole-word modes via SearchOptions + a 1-based match counter — pending/079).

  • metrics — PII-free perf counters (frame-time percentiles, VT throughput) exposed

    for a consumer to forward; Kroma is a lib, so it doesn't emit to a telemetry backend.

Capabilities (built + verified, backlog done/001071)

Ships as a .deb: the example app packages into an installable Linux desktop binary (packaging/form-example/ — naming.kmd layout: /opt/koder/<slug>/, /usr/local/bin symlink, dev.koder.<slug>.desktop, hicolor icon; run_app's app_id = X11 WM_CLASS matches StartupWMClass). Verified install→launch on dev-linux-kdb (done/071). The tiling terminal ships the same way: bin kroma-kterm (src/kterm_bin.rs — windowed tiling terminal + workspace autosave) + packaging/terminal/ (.deb verified built+installed; pending/078 S5a). Slug kroma-kterm is the migration preview — the kterm product cutover (retire Flutter kterm, repackage as kterm) is owner-gated (S5b).

Web surface — INTERACTIVE & PROVEN (4th backend): the same view(state) app runs in a browser via WebGPU. web_backend::run_app_web (offscreen render → 2D canvas + DOM pointer/ keyboard → app.dispatch) — confirmed on a real GPU: clicking a field focuses it, the message loop runs (done/074/075). (Offscreen, not surface-present: a Dawn canvas-swapchain quirk #076.) vello 0.4 / wgpu 23; bundle packaging/web-example/. Web module packaging / shared runtime: rfcs/stack-RFC-023-kroma-web-module-packaging.kmd. The offscreen-render path is factored into pub(crate) helpers (WebGpuweb_gpu_initsubmit_scene/readback_and_blit) reused by web_tiling_terminal::run_tiling_terminal_web (pending/078 S4c) — the web tiling terminal: a TileTree of TerminalPanes over WsTransport (the kterm PTY-over-WebSocket server), repainting on input + a requestAnimationFrame loop that drains pane output. Web-only. Wasm entrypoints are named exports (RFC-023 P4; web_entry.rs) — kroma_form(canvas) + kroma_terminal(canvas, ws_url) — no auto-start; bundles packaging/web-{example,terminal}/ each call one. S4 COMPLETE (S4a–d): the Kroma tiling terminal runs in the browser over the kterm server — same VTgridtiling core, desktop PTY + web socket (RFC-019 convergence).

First example app: kroma-form — a declarative "Sign in" desktop form (title + Name/Email fields + accent Sign-in button + a reactive status line), Verge-styled, rendered through the real Vello pipeline (done/069). On-screen live window pending a general run_app runner.

  • Render: wgpu scaffold → headless Vello render (Verge surface, cold-start ~270ms vs

    Flutter ~1-2s) → text (skrifa+Vello glyph runs) → widget tree → Linear layout → terminal Grid. On-screen WinitBackend runs a live window, building each frame from a persistent GlyphCache (one font parse + memoised char→glyph, no per-frame clone — 1.15× frame build); a user-event loop wakes on PTY output via a session waker (prompt redraws, no busy-poll) + a blink animation clock.

  • Terminal: live PTY shell + resize with reflow (window → PTY SIGWINCH + VT grid; a

    width change rewraps logical lines across scrollback + live); VT-completeness (CSI set) — runs real full-screen TUIs (less verified end-to-end); SGR completeness (256truecolour + attrs incl. singledoublecurlydotted/dashed underline, overline, visible blink, ;/: forms); cursor + block caret; glyph batching; scrollback (+ on-screen Shift+PgUp/wheel navigation, mouse text selection + Ctrl+Shift+C copy (soft-wrap-aware: a wrapped line copies without a break), Ctrl+Shift+F incremental search with match nav/highlight); mouse reporting (DEC 100010021006, SGR+X11 encoding); OSC window title; queryresponse (DADSR written back to the PTY); OSC palette queries (light/dark detection); OSC 8 hyperlinks (Ctrl+click opens, hover cue, a11y Link nodes); DECRQM mode-state + XTVERSION replies; bracketed-paste (?2004) + focus-events (?1004); OSC 52 clipboard (set behind operator opt-in, Ctrl+Shift+V paste via arboard).

  • a11y (RFC-019 §4.2 leg — DONE): AccessKit tree + AT-SPI bus publisher (a pyatspi client

    reads the terminal headlessly, like Orca) + on-screen accesskit_winit adapter + caret offset (exact cursor column via TextRun/TextSelection) + operable Link nodes (an AT activating a hyperlink — Action::ClickLinkActionsxdg-open).

  • AA contrast (gate leg — DONE): Verge tokens bound to generated verge_tokens.rs

    (design-gen .preset.verge seed→map output, design-gen#208 — derived SURFACE #0E0E11 / ON_SURFACE #F5F3F7 / ACCENT #3584E4, ΔE≤2.0 parity-gated); kroma-contrast audits chrome ≥ AA + the min-contrast filter.

  • Measured feel (gate leg — DONE): CPU bench (VT 50 MB/s, frame-build 83 µs ≈ 12k fps CPU

    ceiling) + real-GPU bench on RTX 4070 under heavy-work-isolation §R9 (cold first-frame 41 ms, warm GPU render 831 µs ≈ 1200 fps — full frame ~0.9 ms, ~18-20× under the 60fps budget).

RFC-019 §4.2 gate status

GO substantively: a11y DONE · AA contrast DONE · measured-feel DONE (raw frame-time evidence complete on real hardware). Remaining formal piece: request→painted vs Tilix (the stack-RFC-012 corpus, a KKIT-095-class XTEST harness) — the GO verdict already holds.

Primary couplings

  • Verge (specs/themes/verge.kmd) — the token system Kroma renders. widget::verge is

    bound to the generated verge_tokens.rs (design-gen emits the .preset.verge product palette as Rust — design-gen#208; Kroma is the first Rust consumer, regenerate via tokens-regen), not a hand snapshot.

  • kompose (infra/linux/x) + Kolide (infra/linux/kolide) — the Koder DE (GNOME-Shell

    parity, floating WM). Kroma runs on kompose as a normal Wayland client via winit — the separate "deep KoderWaylandBackend" (S4f) was dropped (done/046): the DE has no compositor tiling, kompose exposes standard wlroots protocols winit already speaks, and a bespoke backend would drop Kroma's portability. a11y already ships via accesskit_unix.

  • kterm (products/dev/kterm) — the first consumer (kterm#007, evidence-gated): consumes

    the lib to retire kterm's Flutter terminal guts.

  • koder_tiling — the Flutter in-app splittile SDK. Its RustKroma successor is a widget

    (a SplitTile layout), opened demand-driven — NOT a Wayland backend (`done046`).

Verification

Headless-first throughout: every capability has a kroma-* verification bin (or cargo test) run off-laptop on dev-linux-kdb per heavy-work-isolation. Pixel checks render over the Verge surface base; a11y is verified by a real pyatspi AT-SPI client; the real-GPU bench ran on the owner laptop's RTX under the §R9 carve-out (registries/perf-baseline.md, hardware-readiness.md).

Naming

Renamed Kanvas → Kroma (2026-06-24) to free the kanvas slug for the A2UI product (products/horizontal/kanvas, paired with the services/ai/canvas engine). "Kroma" (chroma) fits the colourpixel render output. See `registriescomponent-names.md`.