neuron-explorer
Qwen3.5‑4B 전용 조향형 뉴런 탐색기, 모든 MLP 뉴런을 밀어 결과를 보관·탐색
빠르고 네이티브 수준 웹 앱용 선언형 UI 툴킷 타입드 레이아웃·DOM 없이 메인 스레드 외에서 동작
A declarative UI toolkit for fast, native-grade web apps. Typed layout, no DOM, and the whole interface off the main thread.
개발 도구TypeScript · VueGitHub @kevinpbaker온라인운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기A retained-mode UI framework that draws your app on a canvas, lays it out like Chrome, and never lets the main thread touch it.
저장소 README에서 가져옴
README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
Native-grade apps, built on the web.
Gesso is a declarative UI toolkit in the spirit of SwiftUI and Jetpack Compose: components that run once, a real layout engine with typed props, and a theme instead of CSS. It draws the whole interface to a from a worker, so the app stays fast however hard your code is working.
Try it live · A logic simulator built on it · A spreadsheet built on it
gesso (JESS-oh): the primer coat that makes a raw canvas take paint.README 전문 보기 ↗Gesso is a declarative UI toolkit in the spirit of SwiftUI and Jetpack Compose: components that run once, a real layout engine with typed props, and a theme instead of CSS.It draws the whole interface to a <canvas> from a worker, so the app stays fast however hard your code is working.
Try it live · A logic simulator built on it · A spreadsheet built on it
Thirty seconds of code · What you get · Built for agents · Is it for you? · How it works · Proof · Get started
gesso (JESS-oh): the primer coat that makes a raw canvas take paint.
The logic simulator is the framework's own load-bearing example: an 8-bit CPU made of 8,559 gates, playing Pong, that you can open up and rewire while it plays without the game or the editor dropping a frame.
The web has a document toolkit. Gesso is an application toolkit: the kind of thing SwiftUI is on the Mac and Compose is on Android, written in TypeScript and running in any modern browser.
You describe a screen as a tree of components. Stacks are row and column, sizes and alignment are typed props, and colour comes from the theme. There is no DOM underneath, no stylesheet and no virtual DOM. The engine lays the tree out with flex and grid semantics checked against Chrome, sets text in every script, and paints through Canvas2D or WebGPU.
Every web framework you have used shares one thread between your work and your interface. Parse a file, diff a document, simulate a system, sort a hundred thousand rows, and the person using the app watches the two take turns. The pointer sticks. The list stutters mid-scroll. The keystroke lands a beat late.
The usual fix is to move the work into a worker and post the results back. By hand. Per feature. Forever.
Gesso moves the interface instead, and moves it once. Layout is not the DOM's, so it can run anywhere; paint is a canvas, so it can be an OffscreenCanvas; input is raw pointer events, so the main thread's only job is to forward them. Your application gets its own worker and talks to the interface over a MessageChannel that never touches main. The result is a browser app whose frame rate does not depend on how busy your code is.
Two copies of the same component, one in a render worker and one on the main thread. Each ticks every 40 ms and remembers the longest gap it has ever seen. Block the main thread for three seconds:
| Worst gap between two ticks | |
|---|---|
| Main thread | 3,010 ms |
| Gesso | 41 ms |
The worker copy did not miss a tick. The demo, and the input-latency version of the same measurement, runs on Why Gesso in the docs. Press the button and watch which one cares.
pnpm create gesso-app my-app
A component is a function that runs once. State is a cell; a cell bound to a prop updates that prop, and nothing else re-runs:
function Counter(inputs: Inputs<{ label?: string }>) {
const label = input(inputs.label, 'Count');
const count = internalState(0);
const caption = computed(() => `${label.value}: ${count.value}`);
return (
<row gap={12} x="center" y="center">
<text text={caption} textStyle="title" />
<Button label="Add one" onClick={() => count.value++} />
</row>
);
}
Notice what isn't there. No re-render pass, so nothing to memoise and no dependency array to keep honest. No virtual DOM, because component identity is node identity. No useEffect. No CSS: gap and x are typed props on a layout engine, and y: 'middle' is a compile error. No colour, because Button reads the theme. And the same function mounts in a worker, on the main thread, or in a vitest test with no browser at all.
maxLines and ellipsis. Ten disagreements, each pinned.engine.explain(node) says why a box is the size it is, in sentences, in the order the rules applied. A hover inspector paints the boxes and a heatmap of what the last frame re-measured.gesso-testing mounts a tree on a manual clock and queries it through the same semantics tree a screen reader gets. A failing box assertion prints explain's answer under it. 4,640 tests run in under thirty seconds with no browser.A Gesso app already declares what it holds and what it can be asked to do, as channels of plain data and typed commands, and already describes its screen as a semantics tree for assistive technology. Those are an AI agent's tools, so every Gesso app is an MCP server with nothing more written.
/** The notes the person has written. */
export const Notes = defineChannel('notes', {
view: { rows: [] as readonly NoteRow[] },
commands: {} as {
/** Opens a note in the editor. @param id The note's id. */
open(id: string): void;
/** Deletes a note for good. @destructive @confirm */
remove(id: string): void;
}
});
From that, an agent gets notes_view, notes_open and notes_remove, described from the JSDoc and checked against the types, and remove is put to the person before it is sent. For anything no channel covers, ui_snapshot, ui_press, ui_type and ui_key read and operate the screen the way a screen reader does. And to design with what the project already has, library_overview, library_search and library_get list its own components with every input they take, tokens_view gives the theme's names, and render_frame draws a module at any width as a PNG the agent can look at.
pnpm dev serves MCP at /__gesso/mcp and prints the lines to connect Claude Code, Codex or Gemini CLI; any MCP client connects to the same address.createApp({ webmcp: true }) registers the same tools with WebMCP.mcpHandler from gesso-framework/agent serves them over HTTP.Gesso is easy for a coding agent to write, too: every new project carries an AGENTS.md, the documentation is published as llms.txt, and most wrong guesses are type errors. Built for AI agents has the whole story.
Gesso is a runtime for applications: editors, dashboards, tools, anything with a hundred thousand rows or a computation the interface must not wait on. It is the wrong tool for anything whose value is being read by a crawler.
| Reach for it when | Don't when |
|---|---|
| Your app does real work and the interface must not feel it | Your screen is a document, an article or a marketing page |
| A scroll through 100,000 rows has to stay at 60 fps | Crawlers, view-source or browser extensions need to read it |
| You want layout you can reason about and ask questions of | You need native form controls: autofill, password managers, mobile keyboards |
| You want tests that run in node and query what a screen reader sees | You need the React ecosystem more than you need the thread |
The long form, with a framework-by-framework comparison and the list of what has not been proven, is Why Gesso.
| Thread | Owns |
|---|---|
| Shell (main) | the <canvas>, input forwarding, the editing proxy for IME, ResizeObserver, and the APIs that exist only here: history and the address bar, localStorage, clipboard, IPC |
| App worker | api → storage → domain → view models. Plain RxJS |
| Render worker | components, the retained graph, layout, input dispatch, hit-testing, rasterization, and the runtime services |
Application work has to cost something somewhere, and the arrangement decides what. State in the render worker costs dropped frames. State on the shell costs input latency. State in the app worker costs neither, which is what its thread buys.
An application is three files across three threads, joined by one shared module that holds names and shapes and no implementation:
// notes.contract.ts: the only module both workers import
export interface NotesView {
readonly rows: readonly NoteRow[];
readonly open: OpenNote | null;
}
export interface NotesCommands {
open(id: string): void;
create(): void;
setBody(body: string): void;
}
export const Notes = channel<NotesView, NotesCommands>('notes', { rows: [], open: null });
// notes.app.worker.ts: your application. Plain classes, plain RxJS.
const domain = new NotesDomain(new OpfsNotesRepository(SEED_NOTES));
const view = new NotesViewModel(domain);
serveChannels([
{
token: Notes,
source: {
view: { rows: view.rows, open: view.open },
commands: { open: id => domain.select(id), create: () => domain.create(), setBody: body => domain.setBody(body) }
}
}
]);
// notes.render.worker.ts: everything the user sees
function NotesApp(props, ctx) {
const notes = ctx.channel(Notes);
return (
<column gap={12} padding={24}>
{notes.view.rows.pipe(map(rows => rows.map(row => createComponent(NoteRow, { row }, row.id))))}
<Button label="New note" onClick={() => notes.send.create()} />
</column>
);
}
renderRoot(NotesApp).useChannel(Notes);
// main.ts: the main thread's entire job
createApp({
renderWorker: () => new Worker(new URL('./notes.render.worker.ts', import.meta.url), { type: 'module' }),
appLogicWorker: () => new Worker(new URL('./notes.app.worker.ts', import.meta.url), { type: 'module' })
}).mount('#app');
Above serveChannels there is no framework import in the app worker's dependency graph. No decorator, no base class, no runtime, so every layer beneath it is testable with bare vitest, in node. The shell spawns both workers, creates one MessageChannel between them, hands each an end, and then holds neither. It cannot be in a patch's way even by accident.
Only plain data crosses. provide() checks each key's first emission in development and throws naming the path when it finds a Date, a Map, a Set or a class instance, because the structural differ would otherwise report "changed" forever. The barrier makes that a startup error rather than a silent performance bug.
Single-thread mode exists too, createSyncApp(NotesApp).useChannel(Notes, { source }).mountSync('#app'), for tests, headless rendering, and environments without OffscreenCanvas. Channels resolve in-process there, so the same contract runs with no ports. The full model is in Channels and the barrier and Workers.
┌──────────────────────────────────────────────────────────────────────────────┐
│ shell (main) forwards input · owns the <canvas> · spawns both workers │
└───────┬───────────────────────────────────────────────────┬──────────────────┘
│ input, resize ↓ ↑ metrics, errors │ platform events ↓
┌───────▼───────────────────────────────────────────┐ ┌────▼──────────────────┐
│ render worker │ │ app worker │
│ │ │ │
│ Components ── body runs once ──┐ │ │ api │
│ Runtime services ── observables ┤ │ │ ↓ │
│ ▼ │ │ storage (OPFS) │
│ Retained UiGraph ◄── UiGraphBuilder reconciles │ │ ↓ │
│ │ keyed children, props, │ │ domain │
│ ▼ bindings, handlers │ │ ↓ │
│ LayoutEngine measure → place → scroll, │ │ view models │
│ │ from relayout boundaries │ │ ↓ │
│ ▼ │ │ provide(token) │
│ UiRenderer Canvas2D │ WebGPU → Offscreen │ │ │
│ Input hit-test · pointer · keyboard │ │ │
│ │ │ │
│ ctx.channel(token) ◄──────── patches ───────────┼──┼── MessageChannel │
│ send.command() ─────────────────────────┼──┼─► (never touches │
│ │ │ the main thread) │
└───────────────────────────────────────────────────┘ └───────────────────────┘
A frame runs as guarded, timed phases:
ticks → patches → environment → virtualize → layout → semantics → render
Animations advance, channel patches land, theme and environment values propagate, lazy windows advance, dirty subtrees re-lay out from their boundaries, the semantics tree is diffed, and the renderer paints. Each phase is timed and each is guarded, so a throw is reported to the shell instead of vanishing inside a worker. FrameMetrics come out the other side, including inputLatencyMs measured from the event's own timeStamp to the end of the frame that answers it.
Every figure above is produced by a script in the repository, and the script is the claim.
| Claim | Evidence | Run it |
|---|---|---|
| Layout matches Chrome | 260 conformance cases rendered by headless Chrome, every box within 0.1 px. Known divergences are pinned as it.fails so a fix reports itself instead of passing silently | pnpm test:run · regenerate with pnpm fixtures:layout |
| Text breaks where Chrome breaks | 140 paragraphs in seven Noto faces (Latin, CJK, Arabic, Hebrew, Devanagari, Thai, colour emoji), laid out by Chrome and by layoutParagraph over Chrome's own run widths, agree on every line's offsets, ink and baseline. The ten that differ are pinned by name | pnpm fixtures:text:check |
| Cost follows the change | Budgets: full layout once; a deep text edit measures < 20 nodes from one relayout root; a scroll frame measures 0; a row gaining a child re-lays out the row, not the root | LayoutEngine.budget.spec.ts |
| Renderers agree | A draw-for-draw parity spec, plus a headless-Chrome pixel diff of the compare route that fails above 0.05 % | pnpm parity:webgpu |
| A blocked shell costs frames nothing | Busy-loop the main thread for five seconds. Input latency climbs, because the person is waiting on the shell, while the render worker's worst frame gap does not move | #framework |
| The patch stream bypasses main | With the shell busy-looped, a channel fed from the app worker keeps delivering patches throughout, and the render worker's frame gap is unchanged | #framework |
| The main thread stays small | The shell bundle is held under a byte budget and grepped for Canvas2D calls, because the number alone would pass a build that kept the rasterizer and got lucky | pnpm check:bundle |
| Application state outlives the renderer | Switching renderer replaces the render worker (the frame count restarts) while a channel keeps counting across the swap | #framework, renderer toggle |
| Data survives a reload | The notes example persists through FileSystemSyncAccessHandle in the app worker: type a marker, reload, and it is still there | #example-notes |
| The published packages work | Every package is packed to a tarball, installed with npm into a fresh Vite project, typechecked against the rolled-up declarations with , built, and clicked in Chrome |
One thing is deliberately not claimed: no screen reader has been run against any of this. pnpm check:a11y proves the information reaches the platform's accessibility API and that presses and focus come back, which is the strongest evidence a Linux machine can produce. VoiceOver and NVDA remain unrun.
Everything in that table runs on every push and pull request (.github/workflows/ci.yml), except the screenshot gate, which is advisory there: canvas text is rasterised with the system's fonts, so baselines captured on one machine are not comparable with a runner's output.
Every claim of doneness in this project has an exit criterion observable in a browser, not just a test runner, because WebGPU cannot run in vitest at all and a passing suite is not evidence a route renders. The input-latency measurement is the cautionary tale: the first cut stamped the event where the shell forwards it, which reads ~0 ms however blocked the shell is, because the listener only runs once the thread is free. The vitest suite was perfectly happy with it. The browser check is what found it.
The documentation site under apps/docs is the reference. This is the map.
| Area | The short version | Read more |
|---|---|---|
| Layout | Flexbox in full: stretch by default, automatic minimum size, CSS §9.7 freeze-and-redistribute, wrap, reverse, RTL, margin: auto, aspectRatio. CSS Grid with typed tracks (fr(), minmax(), repeat()). absolute, relative and sticky positioning, anchored overlays that flip to stay on screen, overflow on any container. Lengths are tagged values, never strings: columns: '1fr 1fr' throws and names the property. | Flex · Grid · Positioning · Scrolling |
| Text | One paragraph algorithm shared by the engine and both renderers. Word and character wrapping, maxLines, ellipsis, hanging spaces, fit-content width, real baselines. Line breaks checked against Chrome in seven scripts, including CJK, Arabic, Hebrew, Devanagari and Thai. | Text · Rich text · Fonts |
| Virtualization | LazyColumn({ count: 100_000, estimatedExtent: 28 }, i => Row(...)) mounts the visible rows plus an overscan band. Anchoring keeps the row under your eye still when estimates get corrected above it. | Virtualization |
pnpm create gesso-app my-app # a browser project, from the packages on npm
pnpm create gesso-app my-app --template electrobun # the same app in a native window
Or work in this repository:
pnpm install
pnpm dev # the playground; open the URL Vite prints
pnpm docs:dev # the documentation site
pnpm create:app ../my-app --local # scaffold a project against this working tree
--local packs the packages out of this checkout and points the new project at the tarballs instead, which is what you want while you are changing them. Installation covers setting a project up by hand.
One shell, switched by hash:
| Route | What you'll see |
|---|---|
#workbench | Every component in its states, to check by hand |
#examples | Small complete apps, below |
#renderers/dom | One layout scene as DOM boxes: hover a node, read why it's that size |
#renderers/canvas | The same scene in Canvas2D across three threads: main, data worker, render worker |
#renderers/webgpu | The same scene through the WebGPU renderer |
#renderers/compare | Canvas2D and WebGPU side by side with a live pixel diff |
#benchmark | WebGPU throughput. Not in the nav; it is a tool to run, not a page to browse |
The four renderer views are one page with a switch in the header. The routes they replaced (#debug, #canvas, #webgpu, #compare, #framework-sync, #modifiers) still work and redirect to where each now lives.
| Example | What it demonstrates |
|---|---|
#example-signin | A passcode sign-in, written in JSX |
#example-notes | Text editing and IME over a channel, persisted to OPFS from the app worker |
#example-theme | Theming through the environment |
#example-live | A live feed bound straight to the canvas |
#example-router | Nested routes, typed params and a guard, walked by the browser's Back button |
#example-animation | A board that moves, and an idle app that does not |
#example-input | Gestures and shortcuts |
#example-layout | Layout an application can shape at runtime |
#example-paint | Painting |
#example-transitions | Shared elements morphing between routes, with video decoded by WebCodecs and audio on the shell |
#example-modifiers | Behaviour attached to an element without a wrapper component |
#framework | One app on a render worker; stall the main thread and watch the heartbeat keep time |
#framework/main-thread | The same app on the main thread, so the contrast is unmissable |
Each example is meant to be read as much as run. The card on the examples page names the source file.
| Package | Directory | What it is |
|---|---|---|
gesso-core | packages/core | The retained graph, layout, text, the two renderers, input, theming. Knows nothing about components. |
gesso-framework | packages/framework | Components, cells, the frame runtime, channels, routing, the worker barrier. |
gesso-components | packages/components | The component library: five tiers, one contract. |
gesso-testing | packages/testing | renderTest: mount a component with no browser and query it as a screen reader would. |
gesso-devtools | packages/devtools | The error overlay, node inspector, frame profiler and action log. |
gesso-vite-plugin | packages/vite-plugin | Finds an application's worker entries and writes the constructions and hot-replacement wiring. Optional. |
gesso-electrobun | packages/electrobun | Runs an application in an Electrobun window, with its stores in the main process. |
create-gesso-app | packages/create-gesso-app | The scaffold. Two templates: a browser project and a native window. |
gesso-core and gesso-framework each carry a second entry, ./testing, holding the doubles their own suites use; nothing an application builds against reaches through it. Cross-package imports go through a package's root entry, never a deep path, so the committed API reports review the whole surface.
| App | Directory | What it is |
|---|---|---|
gesso-playground | apps/playground | The demo harness: the routes above, and one shell. |
gesso-docs | apps/docs | The documentation site, VitePress, with live examples that are also specs. |
gesso-devtools-extension | apps/devtools-extension | The Gesso panel in Chrome devtools. |
pnpm dev # Vite dev server with the playground
pnpm docs:dev # the documentation site
pnpm build # tsdown for the packages, then vite for the playground and the extension
pnpm test # vitest, watch mode
pnpm test:run # vitest, once
pnpm lint # oxlint
pnpm format # oxfmt
pnpm typecheck # tsc --noEmit
pnpm check # format, lint, types, tests, build, API reports and the docs gates, in CI's order
pnpm api:check # each package's public surface against its committed report
pnpm api:update # rewrite those reports after an intended change
pnpm fixtures:layout # re-render layout conformance cases in headless Chrome
pnpm fixtures:text # re-render text conformance paragraphs in real fonts
pnpm parity:webgpu # Canvas2D vs WebGPU pixel diff in headless Chrome (needs a WebGPU adapter)
pnpm screenshots # each covered route against its committed baseline
pnpm check:a11y # Chrome's computed accessibility tree against the committed reports
pnpm check:install # pack, install and run the packages in a fresh Vite project
pnpm check:scaffold # run the scaffold and drive the result in Chrome
Every Chrome-driven script runs with no browser-automation dependency: --dump-dom for fixtures, the DevTools protocol over Node's built-in WebSocket for the rest. Set CHROME_BIN to choose the binary. The full list is in package.json.
Gesso is an active research project: a serious attempt at building a small, deterministic UI runtime from first principles. The runtime is functional and heavily tested, on the evidence in the table above rather than on assertion.
It is also published. All eight packages are on npm at 0.3.0: gesso-core, gesso-framework, gesso-components, gesso-testing, gesso-devtools, gesso-vite-plugin, gesso-electrobun and create-gesso-app. pnpm create gesso-app my-app scaffolds a working project from them right now.
Pre-1.0 means the API still moves. What that buys you is that it moves in the open: every package's public surface is committed as an API report, so an addition or a removal shows up as a diff in review, and every release is a changeset that says in the package's CHANGELOG.md what changed and why.
The source is public at github.com/kevinpbaker/gesso and the documentation site is at gesso-docs.vercel.app.
Licensed MIT, root and every package.
skipLibCheck: falsepnpm check:install |
| The scaffold produces a working app | create-gesso-app runs into a temporary directory; the result is installed, typechecked, built, and its dev server is driven in headless Chrome until the counter counts. Both templates | pnpm check:scaffold · pnpm check:scaffold:electrobun |
| The public surface is reviewed | Each package's exported declarations are committed as packages/*/api/*.api.d.ts; an added export fails the check as an added line | pnpm api:check |
| Every control works from the keyboard | One form holding every control in the library, tabbed through from nothing, each control operated by its role's key with the result read from the semantics tree | Keyboard.spec.ts in gesso-components |
| A screen reader has something to read | Chrome's own computed accessibility tree for the example routes, written to committed reports in apps/playground/accessibility/: every node's role, name, states and value, and the order the Tab key reaches them in | pnpm check:a11y |
| A route cannot change silently | Covered playground routes are captured in headless Chrome and diffed against committed baselines | pnpm screenshots |
| It stays this way | 4,640 tests in 431 files, every push. The pinned divergences run as expected failures, so there are exactly as many of those as the tables above say | pnpm test:run |
| The docs do not lie | Every live example on the site has a worker and a spec behind it, every quoted code region exists, and every page carries a description | pnpm docs:check |
| Explainability | engine.explain(node) returns constraints received, sizes measured, and per-axis decidedBy in human sentences. A change is laid out from the nearest relayout boundary, not the root, and the inspector shows which. | Asking the engine why · Inspecting a node |
| Typed authoring | Every factory is typed from the property registry, so a prop's type is by construction the type layout and paint read. Row({ y: 'middle' }) is a compile error. Components are classes or functions; JSX is optional and compiles to the same calls with the same types. | Your first component · Components run once · Cells and bindings |
| Components | Five tiers, one contract: controlled by default with an optional defaultX, themed through UiTheme tokens with no colour props, keyboard operable from a keymap that is data, and emitting semantics from the day it was written. Inputs, overlays, structure, data and media. | Components · Forms |
| Semantics | Nodes carry role, label, value and states, or inherit them from the component that built them. The runtime diffs a semantics tree per frame and mirrors it to the platform's accessibility API. An unknown role fails the build. | Semantics · The mirror · Keyboard |
| Testing | gesso-testing mounts a tree on a manual clock over a canvas double and queries it through the same semantics tree a screen reader gets. A toHaveBox that misses prints explain's answer under it. | Testing |
| Routing | Routes declare full paths and a parent, so params are typed from the path and a misspelled one is a compile error. One outlet renders the matched chain; guards run outermost first; the only thing on the wire is a url. | Routing |
| Modifiers | Extend what an element does (hover, press, focus rings, tooltips, drag, measurement) without wrapping it in a component. The analogue is Compose's Modifier.Node or Svelte's use:action. | Modifiers · Writing a modifier |
| Animation & transitions | animate and spring on cells, declarative transition on elements, FLIP between boxes on reorder, enter and exit, and shared elements that morph across routes. An idle app reports ticks 0.00. Reduced motion arrives from the shell and everything but spinners snaps under it. | Motion · Enter and exit · Shared elements |
| Editing, selection, find | EditableText has a caret, selection, undo, IME composition and clipboard, with the buffer owned by the render worker. Selection spans text runs. Find is a controller over the retained graph. | Text editing and IME · Selection · Find |
| Input | Pointer, wheel, keyboard, focus, gestures and hit-testing run in the render worker against the painted boxes. Event props are declarative and reconciled with the node, so a handler can never outlive its element. | Pointer and keyboard · Gestures · Drag and drop |
| Two renderers | Canvas2DRenderer and WebGPURenderer implement one UiRenderer interface. The WebGPU backend builds a single ordered command list in Canvas2D's exact sequence. The framework picks at runtime; nothing above the renderer knows which one it got. | Canvas2D and WebGPU |
| Themes | UiEnvironment carries typed, scoped, reactive values down the tree, and a theme change dirties exactly the nodes that read it. A motion vocabulary sits beside the theme rather than inside it. | Themes and the environment · Light and dark |
| Devtools | A canvas app that throws leaves its last good frame on screen, looking exactly like one that works. gesso-devtools draws the error over the app with source-mapped stacks, and a Chrome devtools panel shows the tree, a node's report, the workers' consoles, the frame profiler and the action log. | Devtools · Errors and the overlay · Frames and phases |
| Desktop | gesso-electrobun runs an application in a native window with its stores in the main process. The scaffold has a template for it. | Gesso on Electrobun · Desktop windows |