Skip to content

feat(desktop-gpui): native GPUI desktop client - #209

Draft
yordis wants to merge 58 commits into
mainfrom
yordis/feat-gpui-desktop
Draft

yordis wants to merge 58 commits into
mainfrom
yordis/feat-gpui-desktop

Conversation

@yordis

@yordis yordis commented Nov 5, 2026

Copy link
Copy Markdown
Member

Electron ships Node and Chromium with every desktop install. This adds apps/desktop-gpui, a native Rust client built on GPUI that is meant to replace apps/desktop over time without embedding either.

What it does so far:

  • Starts the T3 server as a child process with the same fd 3 bootstrap Electron uses, exchanges the bootstrap token for a session, and opens the RPC WebSocket with ping and reconnect. It can also attach to an already running server with a one-time pairing token (vp run dev:desktop-gpui).
  • Wire types are generated, not hand-written: packages/contracts/schema/rpc.schema.json is generated from the Effect schemas, and src/wire/generated.rs is generated from it with typify. A test fails when the committed file is stale.
  • Main screen skeleton from the live orchestration.subscribeShell stream: a sidebar with the web's sections and sort order (Working, Snoozed and Settled shelves), so rows no longer reorder on every status change.
  • Empty-state project picker that follows the web: grouping by repository, pinning, environment badges, "start without a project", tooltips and the combobox popup sizing.

Known gaps, tracked for follow-up: no openScratch RPC yet, no keybindings, the composer is static, and CI does not build the crate yet (GPUI needs a macOS runner or Linux system libraries). Reading the Electron app's saved credentials is a stopgap marked TODO(gpui-credentials) and must be replaced before this ships.

yordis added 12 commits November 4, 2026 22:49
… the server

A native client lets us replace Electron without shipping Chromium or a
Node runtime: the app launches the server with the user's Node, the same
bootstrap handshake Electron uses, so no pairing is needed.

Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
Hand-porting 212 RPC methods to Rust would drift from the TypeScript
contracts. Export a JSON Schema from WsRpcGroup, check it for drift in CI,
and generate the Rust types from it so the native client decodes the
shell subscription with the same shapes the server sends.

Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
…g token

Spawning a second server on a home that another server already owns would race it on the same database. Attaching with a pairing token lets the native client show a running server's data, and it is the same path remote and tunnel servers will need.

Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
A status line can't show whether the generated types hold up against real data. gpui-component supplies native inputs, buttons and theming, so the sidebar, empty state and composer render without a webview. That requires the crates.io gpui-pre build it targets.

Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
Rows were a fixed 60px holding three lines, so the project name was cut off, and status tags pushed past the sidebar edge on long titles. The header also floated to the middle of the main area.

Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
The heading named a fixed project with no way to change it. Match the web empty state: the project name opens a picker, and the start-without-a-project link shows its shortcut.

Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
The hero picker lists projects the way the web does: pinned first, then by recent activity, with the active project leading, colored initials, and a search filter.

Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
…eady

Hover a picker row to pin or unpin the project through projects.mutate, gated on the server's projectOrganization capability. The picker keeps its order while open and the default project no longer follows thread activity, so rows and the heading stop jumping. The project name is plain heading text that underlines on hover, and the composer gains the attach button.

Catalog icons beyond gpui-component's defaults rendered blank, so the app now embeds the extra icons it uses.

Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
…ronments

The picker listed every project on its own and never showed which other machines had a project, so it disagreed with the web.

Projects now group by repository the way the web's default mode does, with the same label and icon rules, and pinning a group pins every member. Saved environments from the Electron app's connection catalog are read, so a project that also exists elsewhere gets the "Also on" badge. Reading Electron's catalog and DPoP key is a stopgap marked TODO(gpui-credentials) until this app has its own pairing.

Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
Rows were sorted by updatedAt, which the server bumps on each status change, so the list jumped while agents worked.

The list now follows the web sidebar's default sections and anchors: pinned and active threads first, then collapsible Working, Snoozed and Settled shelves, each sorted the way the web sorts it.

Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
Tooltips used gpui-component's default text-sm and a key chip, and the "or start without a project" link stayed muted on hover.

Tooltips now use the web TooltipPopup sizing with the shortcut as plain text, and the link is medium weight and turns foreground with an underline on hover, like the web's muted InlineButton.

Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
@yordis yordis added the ignore label Nov 5, 2026
@github-actions github-actions Bot added size:XXL vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. labels Nov 5, 2026
yordis added 15 commits November 5, 2026 03:00
Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
…er strip match the web

The desktop shell should feel identical to the web app, so the sidebar, composer and context strip follow the web's layout, breakpoints and data.

Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
Clicking a thread card did nothing but highlight it. It now opens the thread in the main area as a read-only transcript.

The window subscribes to orchestration.subscribeThread for the selected thread, interrupting the previous stream when the selection changes and resubscribing after a reconnect. A small reducer turns the snapshot and turn-item updates into user and assistant messages plus one muted line per tool item, drawn in a variable-height list that follows the tail. The breadcrumb shows the thread title, and back/forward now walk project and thread routes.
…debar

The server sends repository identity refreshes as shell snapshots with no threads and only the changed projects. The client treated every snapshot as authoritative, so the first refresh emptied the sidebar until threads trickled back through updates.

Snapshots that name resolved repository roots now only update repository identities on known projects, mirroring mergeShellSnapshotProjects. Authoritative snapshots keep an identity the client already knew.
The server sent a 20 MB message and tungstenite rejected it against its 16 MiB frame default, so the client looped on reconnect. Browsers cap neither frames nor messages, so the client now lifts both limits.
@github-actions

github-actions Bot commented Nov 7, 2026 •

Copy link
Copy Markdown

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ℹ️ The exact PR base did not have a successful artifact. Baseline uses the latest successful main measurement shown below.

Provider Metric Main baseline This PR Impact PR ceiling
Codex Total thread wire 5.0 KiB 5.0 KiB 0 B (0.0%) 6.8 KiB ✅
Codex Thread snapshot wire 3.8 KiB 3.8 KiB 0 B (0.0%) 4.9 KiB ✅
Codex Live turn WebSocket wire 1.2 KiB 1.2 KiB 0 B (0.0%) 2.0 KiB ✅
Codex Live turn WebSocket decoded 21.5 KiB 21.5 KiB 0 B (0.0%) 24.4 KiB ✅
Codex Live turn messages 1 1 0 (0.0%) 8 ✅
Claude Total thread wire 5.0 KiB 5.0 KiB 0 B (0.0%) 6.8 KiB ✅
Claude Thread snapshot wire 3.8 KiB 3.8 KiB 0 B (0.0%) 4.9 KiB ✅
Claude Live turn WebSocket wire 1.2 KiB 1.2 KiB 0 B (0.0%) 2.0 KiB ✅
Claude Live turn WebSocket decoded 21.9 KiB 21.9 KiB 0 B (0.0%) 24.4 KiB ✅
Claude Live turn messages 1 1 0 (0.0%) 8 ✅

Baseline: 886aa9f · PR result: 465bdb3 · Source CI: failure

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 108.5 KiB
  • Claude decoded thread snapshot: 108.8 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

yordis added 14 commits November 7, 2026 20:41
The app saves the bearer session a pairing token buys (one Keychain item per
environment id), reconnects with it on later launches, deletes it when the
server answers 401, and forgets it on --unpair. The Electron catalog and DPoP
key reading are gone, along with the crates only they used.
A fresh draft showed a dead Commit row and a Changes row labelled with its
base ref, and split rows filled as one block on hover.

Git actions now hide on a quiet draft and in compact density unless an action
is running, the Changes row shows only its totals, each half of a split row
stacks its own hover fill with the open menu keeping it, and the editor menu
matches the row width and picks the favorite instead of opening it.
Rows no longer show the model name as a tooltip, the speed segments no longer invent a description, effort levels show their description, the favorite star hovers with the accent fill, and unavailable rail items dim to 50% and read like describeUnavailableInstance (Disabled in settings, Unavailable, Limited, Not ready, plus the provider message).
The paperclip opened nothing. Files now stage and upload at attach time like the web: attachments.createUploadUrl, a signed POST of the bytes with progress, three uploads at a time, delete on remove, and a send that waits for uploads and carries the attachment context records.
The web inserts a [name](t3-context://v1/file/<id>) reference at the cursor for each attached file and renders it as a chip; the GPUI composer only listed files under the strip. Attached files now insert atomic inline tokens with the web's kind icons, accents, hover underline and selection highlight, images get chips only when the prompt already has text, and files referenced inline leave the strip.
The sidebar Usage button opened nothing. It and cmd-u now open the web's Usage modal: window and metric tabs with saved preferences, refresh, a scrolling body with provider totals, the daily chart, totals, tokens by type and the model/day breakdown, fed by server.getUsageSummary.
Limits is the web's default usage view, and the GPUI modal opened on a placeholder there. It now ports UsageLimits and the pooled view: pool sections, window bars with pace and refill marks, the hover popover with reset credits behind a confirm, notices, ChatGPT sharing and Cursor keychain rows, and a throttled refresh on open.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ignore size:XXL vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant