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:
widget—Widgettrait (layout/paintsync_focusover Vello) undermin..max
LayoutCtxconstraints, Verge tokens,TextContainerLinear(flex weights +CrossAlignincl. Stretch)FillBoxClickableFocusableButtonTextFieldScrollable(interactive rendered widgets — Verge states, caret editing, clipped scroll viewport, flex/stretch layout), and the terminalGrid.event— input events (pointerscrollkey) + the retained-tree dispatch with single-pointer capture + keyboard focus (
DispatcherEventCtx): hit-testpress→release gesture (Clickable), id-based key→Focusablerouting with Tab traversal, wheel scroll.view— the declarative app spine (App<State>+ pureview(&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.Viewis a trait — open for extension (a consumer ships a custom widget + node); built-instextfieldbuttoncolumnscrollable(the last a stateful container, scroll offset preserved while its child reconciles) —done/062–066. (Flutter/Elm model, minimises Flutter→Kroma porting.) Framework layer 5 complete; constraint layout next.terminal_pane—TerminalPane: a live terminal (TermSession+ VT +GlyphCache) as aWidget/Tileleaf —layoutsizes the grid,paintrenders the VTGrid,eventfeeds keys. Cross-target (S4b):spawn/spawn_in(local PTY) are desktop-gated;from_sessionwraps an any-transport session for the web build. The terminal-in-a-pane half of kterm-on-Kroma (pending/078S2/S4b).tiling—TileTree/Tile: a binary split-tree of panes (any widget is a pane), theRust successor to the Flutter
koder_tiling;splitcloseequalize+ pane focus/event routing. Foundation of the kterm-on-Kroma rewrite (pending/078S1).tiling_terminal—TilingTerminal+run_tiling_terminal: the live *ulti-pane tilingterminal (a
TileTreeofTerminalPanes) — 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/078S2b). Desktop-only. The windowed app iskroma-kterm*(src/kterm_bin.rs): autosave +--restore(reopen the last workspace) +--version--help(standard-flags R1R2).workspace— workspace session restore (kterm-RFC-002 /pending/078S3): the *ersionedsnapshot schema (
WorkspaceSnapshot/LayoutNode— a split tree + per-pane cwd + restore-handler spec, serde JSON,schema_versionalways written/checked) + atomic crash-safe persistence (temp+ renamelatest.json+ rotated history).TilingTerminal::snapshot()/from_layout()capture + rebuild the live tiling layout (each pane respawns at its captured cwd) + an optionalWindowGeometry(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);--restoreCLI +.desktopfavorite are kterm-product wiring on the SDK blocks.autosave— the autosave engine (kterm-RFC-002 §3 /pending/078S3b):Autosavedecideswhen 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_saveties it to an atomic write with dedup (no rewrite of an unchanged snapshot). Wired into the windowed runner viarun_tiling_terminal_opts(…, AutosaveConfig).restore_handler— pane-restore-handler registry (kterm-RFC-002 §2 /pending/078S3c–e): theextension point
RestoreHandler/HandlerRegistryclassifies 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.shellfallback + a/procprocess-tree reader (foreground_of/descendants_of). Built-in handlers:clauderesolves a session id (argv--resume, or the owned.jsonlvia/proc/fd) →k.claude --resume <id>(resume re-attaches, no tool re-exec);sshreconnects (ssh <target>, options skipped);editorreopens the same files (vimnvimnano/… by basename) — all safe_replay.with_defaults()= claude → ssh → editor → shell (the complete RFC §2 v1 table).app_runner—run_app(app, title): the general on-screen window runner for a declarativeApp<State>(the non-terminal counterpart ofwinit_backend::run_terminal) — winit window + wgpuVello surface, paints the widget tree per frame, routes pointerwheel/keyboard → kromaEvents into the app (the message loop runs live). Turns aview(state)app into a runnable desktop binary (done/070;kroma-form-windowproven live under Xvfb).painter—layout_and_paint(root) -> Scene+ headless render-to-PNG. The seambetween "app builds a frame" and "host presents it".
backend(RFC-021) —Backendtrait +HeadlessBackend(offscreen → PNG, the testpath);
winit_backend::WinitBackendis the on-screen window (winit + Vello surface).vt—VtScreenovervte: 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.
session—TermSession: a VT terminal driven over aTransporttrait (kroma#078 S4a) —PtyTransport(desktop: local PTY+shell) or a socket transport (web, S4b); bidirectional I/O, writes query replies back.EchoTransportis 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) —WsTransportimplTransportover a web-sysWebSocketspeaking 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 ofscrollback 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) exposedfor a consumer to forward; Kroma is a lib, so it doesn't emit to a telemetry backend.
Capabilities (built + verified, backlog done/001–071)
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/binsymlink,dev.koder.<slug>.desktop, hicolor icon;run_app'sapp_id= X11 WM_CLASS matchesStartupWMClass). Verified install→launch on dev-linux-kdb (done/071). The tiling terminal ships the same way: binkroma-kterm(src/kterm_bin.rs— windowed tiling terminal + workspace autosave) +packaging/terminal/(.debverified built+installed;pending/078S5a). Slugkroma-ktermis the migration preview — thektermproduct cutover (retire Flutter kterm, repackage askterm) 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; bundlepackaging/web-example/. Web module packaging / shared runtime:rfcs/stack-RFC-023-kroma-web-module-packaging.kmd. The offscreen-render path is factored intopub(crate)helpers (WebGpuweb_gpu_initsubmit_scene/readback_and_blit) reused byweb_tiling_terminal::run_tiling_terminal_web(pending/078S4c) — the web tiling terminal: aTileTreeofTerminalPanes overWsTransport(the kterm PTY-over-WebSocket server), repainting on input + arequestAnimationFrameloop 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; bundlespackaging/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 generalrun_apprunner.
- Render: wgpu scaffold → headless Vello render (Verge surface, cold-start ~270ms vs
Flutter ~1-2s) → text (skrifa+Vello glyph runs) → widget tree →
Linearlayout → terminalGrid. On-screenWinitBackendruns a live window, building each frame from a persistentGlyphCache(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 (
lessverified 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_winitadapter + caret offset (exact cursor column via TextRun/TextSelection) + operable Link nodes (an AT activating a hyperlink —Action::Click→LinkActions→xdg-open). - AA contrast (gate leg — DONE): Verge tokens bound to generated
verge_tokens.rs(design-gen
.preset.vergeseed→map output, design-gen#208 — derived SURFACE #0E0E11 / ON_SURFACE #F5F3F7 / ACCENT #3584E4, ΔE≤2.0 parity-gated);kroma-contrastaudits 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::vergeisbound to the generated
verge_tokens.rs(design-gen emits the.preset.vergeproduct palette as Rust — design-gen#208; Kroma is the first Rust consumer, regenerate viatokens-regen), not a hand snapshot. - kompose (
infra/linux/x) + Kolide (infra/linux/kolide) — the Koder DE (GNOME-Shellparity, 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): consumesthe lib to retire kterm's Flutter terminal guts.
- koder_tiling — the Flutter in-app splittile SDK. Its RustKroma successor is a widget
(a
SplitTilelayout), 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`.