All skills
lassejlv avatar

/build-gpui-apps

@52f4cfd
by Lasselassejlv/skills50 stars
1

Build, scaffold, refactor, debug, review, and validate native Rust desktop applications with GPUI. Recommend GPUI Kit and ask before adopting it; use gpui_kit imports after agreement, or preserve the chosen upstream GPUI stack. Includes the merged GPUI Kit component and design skills, full application/Base/Component/Shell documentation, coding and design guides, state, actions, async, input, accessibility, motion, themes, native integration, packaging, auto updates, testing, and production delivery. Use paper-to-gpui when the primary task is faithfully translating a selected Paper.design frame into an existing view.

Use this Skill: https://skilld.dev/gh/lassejlv/skills/build-gpui-apps

This session only. Nothing lands on disk.

referencesgpui-kitupstreamshell.md

≈5.5k tokens on demand. Your agent reads this file only when SKILL.md points to it.

GPUI Shell

gpui-shell exists to make a Rust GPUI application extensible in JavaScript.

The primary goal is plugin extension. A host application compiles and ships once. After that, a new panel, a side tool or a piece of business logic arrives as a script loaded into the same process — no rebuild, no binary to redistribute, and no fork for a contributor who only wants to add a panel.

The secondary goal is writing a whole application in JavaScript. The CLI runs an application directory on its own, which is a usable path in itself and also how a plugin is developed: get the script running standalone, then mount it in a host.

It is not an Electron or a Tauri. There is no WebView, no DOM, no HTML or CSS, no browser engine, and no Node.js. A script View describes an interface when invalidated; GPUI can reuse that description on later frames without rerunning its script render. Those frames use the same element model and renderer as a Rust application on gpui-base. This does not mean an idle window continuously draws, or that all frame work is free of JavaScript: virtual-list item and dock chrome callbacks are exceptions. Taking the whole runtime costs +13.5 MiB of binary in the measured build.

Both goals rest on the same split. gpui-shell is built directly on gpui-base, with QuickJS running on the host's own thread. The host builds the runtime and grants what a script may reach; the script draws real interface inside the same process. Rust keeps rendering, layout, text editing, virtualization, focus, overlays and every system capability; the script owns composition, presentation and business logic.

import { View } from "gpui-kit";
import { v_flex, Button } from "gpui-base";

export default class Counter extends View {
  init() {
    this.count = 0;
  }

  render(cx) {
    return v_flex()
      .size_full()
      .items_center()
      .justify_center()
      .gap(20)
      .bg(cx.theme().colors.background)
      .child(
        div()
          .text_3xl()
          .text_color(cx.theme().colors.foreground)
          .child(`${this.count}`),
      )
      .child(
        Button.new("increment")
          .h(32)
          .px(14)
          .items_center()
          .justify_center()
          .bg(cx.theme().colors.primary)
          .text_color(cx.theme().colors.primary_foreground)
          .rounded(6)
          .on_click((_event, cx) => {
            this.count += 1;
            cx.notify();
          })
          .child("Increment"),
      );
  }
}

Why plugins come first

crates/base/src/dock already holds half of what a plugin system needs: a layout that is pure data, a PanelRegistry that rebuilds a panel from a name in a persisted file, and a per-panel serde_json::Value that rides along with it. The missing half is that a panel's implementation has to be compiled into the host binary — nobody can contribute one without forking it. gpui-shell supplies that half.

Plugins-first is not a positioning statement. It is the reason behind decisions that would each have gone another way for a runtime aimed only at standalone scripts:

Decision Why it follows from plugins
Capabilities::default() is the empty set, and the host grants A plugin is code someone else wrote; the grant has to be the host's, not a self-declaration in the plugin's own manifest
A separate Policy per plugin, and unload cancels every task carrying it Several plugins share one runtime, so grants must not bleed between them
A script fault is a recoverable exception, and the host process survives One broken plugin should not take the application with it
A repaint replays a Snapshot and never enters the VM The host answers for the frame budget, so a plugin's JavaScript cannot sit on it
HostModule lends the host's own Rust to a script Only meaningful when the script runs inside a host — a standalone application has no host to borrow from
Dock panels keep their place and state across an uninstall Plugins get installed and removed; a panel comes back where it was, with what it had
The foundation ships no presentation, so the script owns all of it A plugin has to look like part of its host, which takes control of every pixel

A standalone script application uses few of these. What it gains is the iteration speed — hot reload, check, and a generated gpui-kit.d.ts — which is why it sits second: it is where a plugin is developed and proven, rather than the point of the runtime.

Text editing, syntax highlighting, LSP, virtualization and motion sampling stay in Rust. That line is a division of responsibility rather than a limit on the script: the host owns everything that has to sit close to the GPU and the system, so a plugin never becomes a variable in the application's performance or stability.

::: warning Plugins are the goal, not yet the whole interface The machinery below a plugin is built and tested — manifest parsing and discovery, load and unload, a per-plugin policy and data directory. A script now contributes panels and draws a dock's chrome: DockArea, dock_area(...) and DockArea.register_panel are public, and a layout with a script's panels in it survives a restart. What is still missing is the rest of the contribution registry (gpui.command, gpui.keymap), the authorization UI, and a CLI that uses PluginManager. What runs end to end today is the standalone path, docks included. See Dock and Panels. :::

What defines it

Architecture: the script describes, the host renders

A script never holds a GPUI element. It records a description of one — every call in a builder chain writes an operation into an arena, and Rust replays those operations into real elements when a frame needs them. Layout, painting, hit testing, scrolling, IME and text editing stay in Rust and never call back into the script. How a script becomes an interface traces one pass of that.

The engine is a parameter of the design rather than a part of it. QuickJS is the only one today, but everything above the seam — the arena, the materializer, the call scope, the style table, the theme, the capability model, the overlay host, hot-reload — names no VM anywhere in its source. See The engine seam.

Capability: a whole application layer, not a widget set

A script gets what a Rust application built on gpui-base gets: elements and layout, links and controls, a fluent style surface over semantic theme tokens, View state through init / render / cx.notify(), retained host state such as a text input's rope and selection, dialogs, a sheet and toasts, asynchronous tasks, native transitions and springs, and gated filesystem, storage, clipboard, process, HTTP, TCP and WebSocket surfaces.

Around that: --watch hot-reloads on save, gpui-shell.json declares identity and least-privilege capabilities before code runs, a generated gpui-kit.d.ts describes the whole API to an editor or a model, and check reports mistakes before the application runs.

::: tip gpui-kit.d.ts can go in .gitignore — it is generated. :::

Performance: the script is not in the frame

render does not run once per frame. It describes a script View into a Snapshot when that View is invalidated. On a later requested frame, GPUI can use the Snapshot without running that View's render. Pointer hover, cursor blinking, scrolling and native animation need not invalidate the script View; frame-path callbacks such as virtual-list item renderers are separate. The display's refresh rate does not determine how many frames an idle window requests.

The runtime counts the two events separately, and the gallery's Shell story (cargo run -- shell) puts both counters on screen:

<img class="architecture-light" src="https://gpui-kit.com/shell-render-frequency-light.svg" alt="Illustrative one-second timeline with 60 requested frames. If script data does not change, the View's JavaScript render track stays empty. If prices change every 50 milliseconds, the View is rebuilt about 20 times while other frames reuse its Snapshot. This does not describe idle frame cadence."> <img class="architecture-dark" src="https://gpui-kit.com/shell-render-frequency-dark.svg" alt="Illustrative one-second timeline with 60 requested frames. If script data does not change, the View's JavaScript render track stays empty. If prices change every 50 milliseconds, the View is rebuilt about 20 times while other frames reuse its Snapshot. This does not describe idle frame cadence.">
What the interface is doing Frames a second JavaScript runs a second
Repainting, with nothing JavaScript reads changed 60 0
Prices moving every 50 ms 60 19

This is an illustrative interval in which 60 frames were requested, not an idle behavior or a guaranteed frame rate. The number of frames actually produced depends on scheduling and workload; the script render count depends on invalidations and coalescing. In the second row, 41 frames use a description that already exists.

The script description cost is paid when that View is invalidated, rather than on every frame. In the measured 443-node benchmark, running render and recording the interface into a Snapshot took 1.1 ms; a cached frame took 1.3 ms for GPUI work, with no script render. These are timings for that workload and machine, not a sustained FPS guarantee for a complex application.

Cost per requested benchmark frame
Without a Snapshot 1.1 ms (JS render) + 1.3 ms (Rust render) = 2.4 ms/frame render
With a Snapshot 1.3 ms

Growing the panel does not change the fact that a cached frame does not rerun the script View. The benchmark covers sizes up to 8,403 nodes; its cached-frame test asserts that the View's script render does not run. Its timings still grow substantially with panel size.

Size: a script runtime for +13.5 MiB

A host that runs a real script application ships a 26.1 MiB binary and holds 81 MiB resident, QuickJS and the whole Standard Runtime included. Taking the dependency costs +13.5 MiB of binary and +14 MiB of memory over the same application without it.

That figure is a constant, not a proportion: the component gallery — five times the size — adds the same 13.5 MiB. What linking it costs gives the pair it was measured on, and where the megabytes go.

All figures here were taken on a MacBook Pro (M3, 8 cores, 24 GB): the frame and run counts from the Shell story, the milliseconds from a release build of the benchmark, the binary and memory figures from release builds of examples/hello_world and the gpui-shell CLI.

Security: nothing by default, and a language trimmed to match

Capabilities::default() is the empty set — no file access, no storage, no clipboard, no process execution, no network. The host decides the grant before loading a View, which then keeps that grant for its lifetime; every path in the fs surface goes through one resolver that refuses anything landing outside a granted root.

Below the grants, the sandbox trims the language itself, because one VM will eventually host several plugins: eval and all four function compilers are gone, the built-in prototypes are frozen so one plugin cannot change Object.prototype for another, module resolution is confined to the application directory, and the heap (256 MiB), interpreter stack (1 MiB) and time in a single call (50 ms in render) are capped. That time limit is an interrupt a catch block cannot swallow, which is measured by a test. See Capabilities.

How a script becomes an interface

<img class="architecture-light" src="https://gpui-kit.com/shell-architecture-light.svg" alt="How a script becomes an interface: the script describes elements, Rust materializes them, GPUI paints"> <img class="architecture-dark" src="https://gpui-kit.com/shell-architecture-dark.svg" alt="How a script becomes an interface: the script describes elements, Rust materializes them, GPUI paints">

The diagram traces one frame, and the shape of it explains most of this documentation.

GPUI elements are values that are consumed when used: RenderOnce::render takes self by value, .child() takes its child by value, and a View rebuilds its whole element tree on every redraw. A JavaScript object can therefore never be a GPUI element — there is nothing for it to hold onto.

So the script does not build elements. It describes them. Every call in a builder chain records one operation into an arena of element descriptions; the object the script holds carries nothing but an integer index into that arena. When GPUI asks the View to render, Rust replays the recorded operations into real elements, hands them to GPUI, and clears the arena. Layout, painting, hit testing, scrolling and IME never return to the script.

Three consequences follow directly, and each has a page below:

  • Elements are single-use. The description is gone at the end of the pass, so a stored element throws on its next use rather than drawing something unexpected. See Elements.
  • The cx handed to a call belongs to that call. It carries a generation number, checked against the live call stack, so a cx kept across an await reports a clear error instead of touching a dead stack frame. See State and Views.
  • Callbacks belong to the render that registered them. They are replaced wholesale by the next render, which is what keeps script closures from accumulating in the host. See Elements.

All three fall out of binding a script to an element model that consumes its values.

Presentation belongs to the script

Most scripting layers hand a script a set of finished widgets and let it arrange them. This one has none to hand over, because the layer underneath it has none either.

gpui-base controls carry no visual style at all. Button::new("save") in Rust has no padding, no background, no radius and no size, and that is the contract. The JavaScript bindings preserve it exactly: Button.new("save") with no styling draws nothing but its children.

The consequence is the point. Because the foundation ships no presentation, the script owns all of it — every colour, every pixel of spacing, every hover state, every corner radius. That is the same trade a Rust application makes when it builds on gpui-base instead of gpui-component; the difference is that here the trade is made in a file you can save and see the result of immediately, with no cargo build in between.

What the script gains in exchange for the extra typing is the whole application layer. Changing a button's radius does not mean going back to Rust.

Where it fits

  • Adding plugin support to an existing GPUI application — the primary case. Plugins run inside the host process under capabilities the host grants one at a time, starting from none. Extending the product stops meaning a fork or a new release: interface and business logic ship as script and change without recompiling or redistributing a binary, and a failing plugin surfaces as a recoverable error rather than taking the host down.
  • Writing a complete application in JavaScript on gpui-shell — the secondary case. The whole application layer — elements, styling, View state, overlays and system APIs — while rendering, text editing, virtualization and every animation frame stay in Rust. It is also where a plugin is written and proven before it is mounted in a host.

Where it sits

  JavaScript application       main.js · Views · styles · business logic
            │  import { … } from "gpui-kit"
            ▼
  gpui-shell                   engine seam · element descriptions · call scope
                               style table · theme tokens · capabilities
                               ShellRoot (dialogs, sheet, toasts) · scheduler
            │
            ▼
  gpui-base                    behavior · state · infrastructure (no style)
            │
            ▼
  gpui                         elements · styling · rendering · GPU · platform

gpui-shell sits beside gpui-component rather than beneath it: both are consumers of gpui-base, and both supply a presentation layer that Base does not. gpui-component supplies one in Rust, finished and coherent. gpui-shell supplies the machinery for a script to supply its own.

Read next

Page What it covers
Getting started Running the example, the smallest application, check and types
Examples The two applications in the repository, and what to copy from them
Elements Constructors, child / children / when, and why an element is single-use
Styling The fluent style surface, lengths, colour tokens and state styles
State and Views init / render, cx.notify(), retained state, async
Overlays Dialogs, the sheet, toasts, and the phase rule
Capabilities gpui-shell.json, default deny, filesystem, storage, process and network APIs
Dependencies Shell packages: what makes one, how a manifest names and pins it, and the types an editor gets
Hosting The Rust side in full: mounting, refreshing, metrics, exit, hot-reload
HostModule Lending the host's own Rust to a script, and the plain-data boundary
Dock and Panels A script View as a dockable panel, the chrome you draw for it, and what survives a restart
Performance What a script costs: invalidation against description size, the View as the boundary, and the counters
The engine seam QuickJS, why the seam exists, and the measurements that tell script cost from frame cost

Status

The crate is at milestone M0: a feasibility baseline, not a stable interface. It is not published to crates.io, and the script API is expected to change. What is documented here exists and works; what is missing is called out on the page where you would go looking for it.

The design is specified in the GPUI Shell design document, and the crate lives at crates/shell.

Documentation license: original prose and illustrations for which GPUI Kit holds licensing rights are also offered under CC BY 4.0. When copying or adapting, credit GPUI Kit, link the source (https://gpui-kit.com/shell) and https://creativecommons.org/licenses/by/4.0/, and indicate changes. Code examples and software source use Apache-2.0; third-party material retains its terms; existing Apache-2.0 permissions remain.

Bundled from GPUI Kit. Documentation prose: CC BY 4.0; code examples: Apache-2.0. Changes: documentation links localized, asset URLs made absolute, and this attribution added.

Source: SKILL.md on GitHub

No alertstoday3 checks · Risk SAFE
  • Gen Agent Trust Hubtoday

    The skill is a comprehensive development toolkit for building native Rust desktop applications with the GPUI framework. It provides a reference application, extensive documentation, and maintenance scripts. No malicious patterns were identified. The skill has a low-risk profile typical of developer tools that process and execute external codebases.

  • Sockettoday

    No alerts

  • Snyktoday

    Risk: LOW · No issues

Signed by skilld at 52f4cfd. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub yesterday.

Activeupdated 4 days ago

README badge

README badge for lassejlv/skills/build-gpui-apps