Skip to content

Cap Desktop -> GPUI - #2133

Open
richiemcilroy wants to merge 121 commits into
mainfrom
gpui
Open

Cap Desktop -> GPUI#2133
richiemcilroy wants to merge 121 commits into
mainfrom
gpui

Conversation

@richiemcilroy

@richiemcilroy richiemcilroy commented Aug 18, 2026

Copy link
Copy Markdown
Member

Adds a native Cap GPUI Version (apps/desktop-gpui) alongside the existing Tauri desktop. Same Cap media stack, no webview. The UI is drawn by GPUI.

There was already an Electron experiment in #2119. The plan is to ship Cap GPUI Version as an optional toggle in the existing Cap desktop so people can trial it without replacing Tauri.

On the same machine and protocol as that Electron comparison, Cap GPUI Version is substantially faster and lighter than both Tauri and Electron:

  • Playback CPU 17.5% vs 76.6% Tauri and 53.2% Electron
  • Playback memory 440 MiB vs 3,724 MiB Tauri and 1,035 MiB Electron
  • Recording CPU 20.3% vs 37.6% Tauri, recording memory 256 MiB vs 1,983 MiB Tauri
  • Packaged app 162 MiB vs 204 MiB Tauri and 410 MiB Electron
  • Playback holds 60 fps with far less CPU and memory

GPUI vs Tauri and Electron performance

Test plan

  • cd apps/desktop-gpui && cargo run opens the native recording window
  • Record, play back, and export a short studio capture
  • Confirm Tauri apps/desktop is unchanged and still the default shipping app

Greptile Summary

The PR adds a native GPUI desktop application alongside the existing Tauri app and extends the shared recording, playback, rendering, encoding, and export stack.

  • Adds GPUI-native recording, editing, exporting, uploading, settings, and supporting UI/assets.
  • Adds media-pipeline performance work, benchmarks, packaging scripts, and documentation.
  • Attempts to make thumbnail upload failures retryable without creating duplicate server videos.

Confidence Score: 4/5

The PR should not merge until the outstanding upload retry path reliably persists the completed video ID or stops before returning a retryable thumbnail error.

A video can finish uploading, the sharing-metadata save can fail and be ignored, and a subsequent thumbnail failure can return control without a durable video ID; the next export then selects initial-upload mode and creates another server video.

Files Needing Attention: apps/desktop-gpui/src/upload.rs, apps/desktop-gpui/src/editor_export.rs

Important Files Changed

Filename Overview
apps/desktop-gpui/src/upload.rs Adds multipart video and thumbnail upload state handling; the attempted persistence ordering fix still leaves the previously reported duplicate-video path outstanding when saving sharing metadata fails.
apps/desktop-gpui/src/editor_export.rs Integrates export-to-link uploads and selects reupload mode from persisted sharing metadata.
apps/desktop-gpui/src/main.rs Establishes the native GPUI desktop application entry point and application wiring.
crates/editor/src/playback.rs Updates shared editor playback behavior and performance infrastructure used by the new desktop client.
crates/export/src/mp4.rs Updates the shared MP4 export pipeline consumed by desktop export flows.
crates/rendering/src/frame_pipeline.rs Updates shared frame rendering and conversion paths used for preview and export.

Reviews (6): Last reviewed commit: "fix(desktop-gpui): persist sharing meta ..." | Re-trigger Greptile

Context used:

Phase 0 of the gpui rewrite: a standalone crate that opens the main
recording window shell. No tauri, no webview.

Kept out of the root cargo workspace on purpose. The gpui tree pulls its
own git revisions of the wgpu/font stacks, and the root workspace carries
`[patch.crates-io]` entries (vendored wgpu-hal, tao) that exist only for
the Tauri app. A nested workspace leaves the root Cargo.lock, the
`cargo hakari` workspace-hack and CI untouched.

gpui is pinned to the wingleeio/zed fork at 5d1f83d, which carries the
Porter-Duff OVER fix for destination alpha on transparent windows that the
rounded main-window shell depends on. Device enumeration reuses cap-camera,
scap-targets and the same cpal fork the recorder uses, so device identities
stay byte-identical to what cap-recording expects.

rust-toolchain.toml pins stable here rather than the root's 1.88.0: gpui's
tree needs 1.89+ (smol_str 0.3.6, cosmic-text 0.19). The nearest toolchain
file wins, so the Tauri app is unaffected.

Dev profile builds dependencies at opt-level 2 and the app crate at 0,
matching zed/comet -- gpui is unusable with unoptimized deps.
gpui resolves `.font_family("Geist")` against the platform text system, so
the faces have to be added before the first window renders or every label
silently falls back to the system UI font.

Only four files are embedded, so `include_bytes!` covers it -- zed reaches
for `rust_embed` because it walks icons/themes/sounds/prompts as well. The
`AssetSource` impl is still worth having: gpui's `svg()` element and image
loader resolve their paths through it.
Cameras, microphones, displays and windows, through the same crates the
recorder uses. The identities handed back are the ones cap-recording
already expects -- camera device_id, microphone name, DisplayId, WindowId
-- rather than a new identity space that would need translating later.

Microphone listing mirrors MicrophoneFeed::list_with_settings: default
input device first so it heads the list, then the rest, deduped by name.

Window listing mirrors the picker path of screen_capture::list_windows
(include_accessory_panels: false), which collapses to a non-empty title,
not owned by the Window Server, and window level 0. The level check is the
one that matters -- a raw Window::list() on this machine returns 33 entries,
19 of which are menu-bar extras (Control Centre alone contributes seven
`Item-0` windows), leaving 14 real application windows. It is duplicated
rather than imported because cap-recording drags in ffmpeg and the entire
encode stack, which this app has no other reason to build.

Enumeration blocks on AVFoundation and the window server, so it is
documented as background-executor work, never to be called from render.
The whole window drawn in gpui: custom header with traffic lights, logo,
mode pill, the four capture-target tiles and the three device rows.

Metrics are transcribed from the Tauri implementation rather than eyeballed,
and the Tailwind class each one comes from is quoted next to it, so `pl-3`
and `gap-2.5` stay checkable against the original. Colours are the resolved
Radix values with the dark-mode overrides from theme.css applied -- six of
the dark grays and gray-11 deviate from stock Radix, so regenerating the
palette from a Radix crate would silently change it.

Two things could not be drawn the obvious way. gpui rasterises an SVG and
keeps only its alpha, tinting the result with one colour, so the logo mark's
three concentric circles would have collapsed into a solid block: the mark is
rebuilt from divs and only the genuinely single-colour wordmark goes through
`svg()`, which also makes it swap between the app's light and dark logo
variants for free. And the window drag region is scoped to the header spacer
alone, not the header root -- a mouse-down handler on the root starts a
window drag for every press in the header, which eats the button clicks
before they are delivered.

The expanded state is the real 600x660 layout, not a scaled compact one:
section headings appear, the target tiles turn horizontal and gain
descriptions, and the control rhythm goes 8px -> 10px. Verified at both
sizes, in light and dark.

Device enumeration runs on the background executor and reports 3 cameras,
3 microphones, 1 display and 14 windows on this machine; the rows render
the result but selecting a device is not wired up yet.
Clicking the camera or microphone row now opens a picker, and picking a
device updates the row and its pill. Like the Tauri app, this is not a popup:
the panel takes over the whole window body and offers a Back button.

Rows carry real subtitles rather than placeholder text. Cameras report their
highest-resolution advertised format via `CameraInfo::formats()`, ordered by
pixel count then frame rate to match the web UI's `bestFormat`; microphones
report the config they would actually be opened with, from cpal's
`default_input_config`. On this machine that reads 1920x1080 @ 30fps for the
Camo camera and 44.1kHz Mono for the built-in microphone. Enumerating all of
it still takes 0.27s, so it stays on the background executor.

Selection uses `--blue-500`, which is a Cap-specific token and a different
colour from `blue-9` used by the pills -- worth keeping straight, since both
are "the blue one".

The search field that sits beside Back in the Tauri panel is not here: it
needs real text input, which is its own piece of gpui plumbing.

System audio has no device to choose, so its row is a plain toggle.
Groups 1-5 and 7 of the punch list, plus two window-lifecycle bugs the
screenshots turned up. They land together because generalising the panel
state underpins the mode-info panel, the target pickers and the search
field, and splitting that refactor from its three callers would have meant
three commits that do not build.

Fidelity:
- The logo is the real `logo-full.svg` again, through `img()` rather than
  `svg()`. The two take different paths in gpui -- `svg()` keeps only the
  alpha and tints it one colour, which flattened the badge, the blue rings
  and the wordmark into a silhouette, so it had been rebuilt out of divs.
  `img()` rasterises through resvg and keeps the colour, at 2x, so a 103px
  source covers the 184 device pixels a 92px lockup needs. Dark mode picks
  `logo-full-dark.svg`, as the app does.
- The plan badge ("Personal") sits next to it. Only the free variant is
  drawn: which of Pro/Commercial applies comes from the license query, and
  claiming a plan with no license plumbing would be worse than showing none.
- The help button is lucide `circle-help`, not the chat bubble.
- The mode pill gains its info dot, hanging off the top-left corner.

Panels. The Display and Window chevrons now open two-column card grids of
real targets: displays with their refresh rate, windows with their owner and
`1920x1080 @ 60 Hz` from real bounds. The mode info panel carries
`ModeInfoPanel`'s copy, which is deliberately not `MODE_BUTTONS`' copy -- the
hover cards and the panel describe the modes differently and the app ships
both. All three share one panel chrome with the device pickers.

Search is hand-rolled; gpui has no stock text input. Focus is tracked so the
panel keeps receiving keys, `key_char` supplies the typed character so dead
keys and option-layouts work, and the caret is a 1px div. There is no
selection, no cursor movement and no blink -- enough for a filter and
nothing more. Escape clears, then closes.

Recents appears when expanded, header and empty state only; thumbnails need
the recordings library. Its copy is the real "Your latest captures will
appear here.", not the shorter string in the brief.
How to run it, what is real, and a table of every deviation from the Tauri
app with the reason. Several of those reasons are non-obvious enough to be
worth writing down -- notably that `WindowKind::Floating` is not the way to
get always-on-top, because it allocates an NSPanel and a panel hides itself
when the application deactivates.

The roadmap lists all fourteen app windows with their real sizes from
windows.rs, so the size of what is left is legible: the main window is one
of fourteen, and the editor alone is larger than everything done so far.

Also records the gpui trap that cost the most time today -- touching the
window from inside `open_window`'s builder fails silently, twice over -- and
the per-window `screencapture -l` recipe, since a plain full-screen capture
returns only the desktop without Screen Recording permission.
180ms with an ease-out cubic, matching `resizeMainWindow`. The task is held
on the view so that assigning a new one drops the old: a second toggle
mid-animation cancels the first, rather than both driving `resize` and
leaving the window at some interpolated size.

Verified by sampling the window's bounds while it runs -- it interpolates and
lands exactly on 600x660. Smoothness could not be verified here: while the
app is inactive its foreground run loop is barely pumped, so the 8ms timer
between steps takes closer to a second and the resize arrives as two or three
jumps. That is the same starvation that stops an inactive window repainting
at all, not something in the animation.

Does not re-clamp into the monitor work area afterwards, which the Tauri
version does; noted in the README.
`logo-wordmark.svg` was cut out of the logo for the div-rebuilt mark, which
`img()` replaced; `support.svg` was the chat bubble standing in for the help
button before lucide `circle-help`. Both were still embedded.

The existing test only checked that every icon referenced is embedded. It now
checks the reverse as well, since a dead asset is invisible -- it costs binary
size and nothing else notices.
cap-recording as a path dep (studio + instant actors, kameo feeds), the
root's vendored wgpu-hal patch mirrored, ort pinned to the root lockfile's
rc.12 (rc.13 renames CoreMLExecutionProvider out from under camera-effects).
Projects land in the same recordings library as the Tauri app and are
finalized the way the CLI finalizes: remux for studio, output.mp4 + meta
pair for instant.
Idle/Starting/Recording/Stopping on the entity, driven over gpui_tokio.
The recording takeover matches index.tsx 3510-3529 (gray-1/80 wash, red
stop button); a TEMP blue start footer stands in for the target-select
overlay that owns starting in the real app. CAP_GPUI_AUTO_RECORD drives
the same code paths for the end-to-end check.
…s bar

The platform module reaches the NSWindow behind a gpui window through
raw-window-handle and applies what gpui does not expose: window level,
Spaces collection behavior, shadow. The main window gets its level-100
all-Spaces treatment from windows.rs; the 320x150 controls bar is a
non-activating WindowKind::PopUp panel at the level the Tauri bar
actually runs at (CGWindowLevelForKey(10) = 8 -- the constant windows.rs
names kCGMaximumWindowLevelKey is really the modal-panel key, reproduced
verbatim for parity).

The recording lifecycle moves off MainWindow into a RecordingSession
entity both windows observe. Starting opens the bar first and hides the
main window, passing the bar's window number to the actors as a capture
exclusion; stop, delete, restart, and failed starts tear it back down.
Pause/resume produce real segment boundaries. A microphone that
enumerates but fails to open degrades to a no-mic recording instead of
failing the start.
occlusionState reports visible windows with an undocumented 0x2000 bit on
macOS 26 and no longer sets NSWindowOcclusionStateVisible (0x2), which is the
only bit gpui's display-link gate checks -- so no window ever received frame
callbacks after its first paint. The bar timer sat frozen on Starting through
whole recordings and every prior refresh-tick workaround was painting nothing.

Shim occlusionState on gpui's own window classes (GPUIWindow/GPUIPanel) to OR
the documented bit back in whenever AppKit reports any visibility, and re-fire
the occlusion handler from apply_panel_behavior so the link starts. Drop when
the gpui pin understands the macOS 26 bit.
Selecting a camera spins up an app-scoped CameraFeed and opens the preview
bubble immediately, the Tauri model; the mic feed is app-scoped too, feeding
live levels to the picker rows and the recording bar, whose mute button now
flips the recording-scoped payload-zeroing mute in instant mode. Recordings
lock the running feeds instead of spawning their own (per-recording spawn
kept as fallback), and the bubble is excluded from studio captures but
included in instant ones, mirroring filter_for_instant_mode.

Frames go VideoToolbox 420v->BGRA into gpui RenderImages -- the surface()
element hard-asserts 420f on this rev and ignores rounded clipping, and the
default bubble is a circle. Chrome state (size/shape/blur) persists to
gpui-state.json; round/square/full shapes, S/L presets, hover toolbar,
corner resize, and drag-to-move all match CameraPreviewChrome.tsx; mirroring
is present-but-disabled (no flip transform in this gpui rev).
One transparent non-activating panel per display at the Tauri-verbatim
level 7 (CGWindowLevelForKey(10) - 1), translated from
target-select-overlay.tsx: the display variant's hover wash and monitor
art, the window variant's cursor-following highlight with click-to-pin
and app icons, the area variant's draw/move/resize crop with eight
handles and min-size validation, and the camera-only variant. The
overlays own Start Recording: starting closes them, opens the bar,
hides the main window, and excludes the overlay windows from capture.

An app-scoped TargetSelect entity polls the cursor's display and
topmost window on the background executor (the Tauri app's 50ms tokio
loop, at 80ms) and repaints every overlay explicitly, since none of
them is the active window.

Two platform findings shipped alongside: gpui windows all carry
NSTitledWindowMask, so AppKit's constrainFrameRect: pushes a
display-covering window below the menu bar - a shim on GPUIWindow and
GPUIPanel returns the rect unchanged, the same override tao gives the
Tauri app; and setFrame:/orderFrontRegardless synchronously re-enter
gpui's window callbacks, so placement runs from a spawned task instead
of inside a window update (inside one it logs 'RefCell already
borrowed' and drops the callback).

Verified: all four variants screenshotted; overlay probed at CG layer 7
covering 0,0 1512x982; a seeded 800x500 area crop recorded a 1600x1000
display track (ffprobe); CAP_GPUI_AUTO_OVERLAY / CAP_GPUI_AUTO_AREA
document the harness.
…ct overlays

Target-select row done (capture-area marked superseded - nothing in the
Tauri frontend opens the standalone window), the TEMP start footer and
'Area does not arm' deviations removed, overlay deviations added
(focused-key Escape, start-only cluster, bubble over inline preview),
and two new gpui traps: AppKit window mutations inside a gpui update,
and the titled-window menu-bar constraint.
The recording start/stop flows called orderOut:/makeKeyAndOrderFront:
inside window updates, which re-enters gpui's window callbacks and
logged 'RefCell already borrowed' three times per stop, dropping the
callbacks. Same fix shape as the overlay placement: grab the retained
NSWindow inside the update, do the AppKit call from a spawned task.
A full record-stop-reshow cycle now logs zero borrow errors.
…window

The main window now sits on the same native material the Tauri app
gives its chrome windows via applyMacOSWindowMaterial("panel"): an
NSGlassEffectView on macOS 26+ (runtime class lookup, style regular,
radius 16, the SystemManaged main-window path -- no always-active SPI
pinning), an NSVisualEffectView windowBackground/BehindWindow fallback
below that, and on both paths the content view's layer clipped to a
16px continuous-corner squircle. gpui's contentView is a plain AppKit
container with the Metal view as a subview, so the material drops in
NSWindowBelow it exactly as the Tauri recipe assumes. Installed from a
spawned task on the retained NSWindow -- subview insertion re-enters
gpui's window callbacks, so it cannot run inside an update.

What the shell paints over the material is transcribed from theme.css's
--macos-settings-* tokens into theme::MaterialTokens (glass tint
rgba(255,255,255,0.55) light / rgba(17,17,17,0.88) dark, control fills,
selection, text), with the CSS rule quoted next to each value. The
body remaps are liquid-glass-only, which is the CSS's own asymmetry:
under vibrancy only the shell and header change.

No occlusion SPI, no CGS, no setState:/setActive: probing -- the
shipping app's WindowServer wedge is the recorded reason.

CAP_GPUI_AUTO_EXPAND=1 drives the expand animation for verification.

Verified: NSGlassEffectView path confirmed by log on macOS 26;
translucency screenshotted light+dark, compact+expanded (glass tracks
the resize via the autoresizing mask); full record cycle finalizes with
zero borrow errors; 12 tests green.
A real second chrome window: 782x775, resizable to the 780x560 minimum
(gpui's window_min_size covers setContentMinSize:, and TitlebarOptions
appears_transparent + traffic_light_position covers the (22,22) lights
-- no new AppKit needed), on the settings material at radius 26 with
the settings-surface tokens (sidebar/content/card/fill/hover/muted)
added to theme::MaterialTokens from their --macos-settings-* rules.

The sidebar carries all twelve pages from settings.tsx's settingsItems
(ungated there, ungated here); General is built in full -- theme tiles,
app toggles, the Pro rows, quality, recording behaviours, countdown,
storage path, project-name template, excluded windows, update channel,
self-host, telemetry -- each row writing its real store key.

Writes go through store::set_store_setting: a read-modify-write on the
raw JSON of the tauri-plugin-store file ('store', no extension) that
replaces exactly one key and preserves everything else -- auth,
presets, migration flags, the settings no page renders. A corrupt
store is refused, never replaced; writes are temp-file + rename.
Typed reads are field-by-field with per-field defaults so one unknown
value cannot blank the page. recordings_dir now reads through the same
module, so CAP_GPUI_TAURI_STORE redirects everything at once.

The main window's header gear opens settings and hides the main window
(the showWindow({Settings}) + hide() pair from new-main); closing
settings brings the main window back.

Verified: material log LiquidGlass radius=26; window screenshot at
782x775 with sidebar + full General page; single-key write against a
byte-copy of the real store diffs to exactly one line with all 12
sections and 40 general_settings keys preserved (md5-proven the real
store was never touched); record cycle finalizes with zero borrow
errors; 23 tests green.
Mode select: the real 580x340 mode-select route -- three fixed cards,
opaque bg-gray-1 (it is an opaque slab in the shipping app too), native
traffic lights at AppKit's default position, selection through the same
set_mode path the main window's pill uses, main window hidden while it
is up. One faithfulness call went the other way from the implementing
agent's draft: shipping new-main passes onInfoClick into Mode.tsx, so
the mode dot opens the in-body info panel there -- and now here too;
nothing in the shipping frontend ever reaches showWindow("ModeSelect"),
so the standalone window is harness-only (CAP_GPUI_AUTO_MODE_SELECT).

Teleprompter: 560x320 resizable to 420x220, native level 101
(TELEPROMPTER_PANEL_LEVEL = MAIN_PANEL_LEVEL + 1) on all Spaces, the
teleprompter material at radius 22, traffic lights at (14,14). Typed
script with the centered layout, fade vignette and cue markers;
word-count-driven auto-scroll porting calculate_playback_speed /
advance_playback_position from teleprompter-utils.ts with their tests;
wpm 60-350, window alpha 45-100 through NSWindow alphaValue, font
22-52; settings popover with cue markers and (inert, no flip transform
in this gpui rev) mirror. State persists under the store's teleprompter
key through set_store_setting with a 250ms debounce. While a recording
runs the window is excluded from capture and content-protected
(sharingType none), restored after -- the recording-gated behavior
apply_content_protection ships.

Deviations documented: append-only script editor (no selection or
click-to-position), wash-not-mask vignette, inert mirror.

Verified: level=101 probed, material LiquidGlass radius=22, alpha 0.92
applied, capture exclusion + sharing=0 logged during a finalized record
cycle with zero borrow errors; script persistence round-trip against a
store copy diffs to the one teleprompter key with the real store
md5-untouched; 31 tests green.
…ridge

library.rs transcribes list_recordings / list_screenshots faithfully:
every known recordings folder (active, default, previousRecordingsPaths,
deduped by canonical path), recording-meta.json through cap-project's
own loader, the shipping asymmetries kept (screenshots filter on .cap
and sort by their PNG's ctime; recordings do neither), both lists
capped at 9 before and after the merge. Thumbnails are the pre-baked
bundle files (screenshots/display.jpg, or the screenshot's PNG),
decoded and downscaled on the background executor into RenderImages
with the replaced frame evicted from the atlas.

cap-recording never writes the thumbnail, so a recording made here
showed the icon fallback forever -- including in the shipping app. The
first-frame JPEG generator (create_screenshot, lib.rs:2582) is ported;
post-finalize now writes it for studio and instant recordings both.
The display path is re-read from disk after the remux: until
remux_if_needed runs the meta points at the fragmented segment
directory, after it at display.mp4 (the same ordering that makes the
Tauri app use updated_studio_meta).

The camera bubble's blur toggle now bridges into the finished studio
project's camera.backgroundBlur.mode (recording.rs:3889 in the Tauri
app), read at finalize for the live-state semantics
handle_recording_finish has. Blur is never baked into the track by
either app -- this field is what makes the project open blurred in the
editor. The write is the store discipline applied to project-config:
raw-JSON read-modify-write of exactly one nested key, parse failure
refused, temp + rename.

Verified end-to-end in one run: record -> finalize -> thumbnail on
disk -> blur bridged with all 16 config keys and the timeline intact ->
library rescan -> the new card renders its real thumbnail; zero borrow
errors; 38 tests green.
…eadlessly

cap-rendering, cap-editor and cap-export join the workspace as path
deps. The three feared blockers from the editor recon all dissolved
under evidence:

- wgpu skew: none exists and none could. cap-rendering (wgpu 25.0.2 on
  the vendored wgpu-hal) was already in this graph via cap-recording,
  and gpui_macos is Metal-direct -- the wgpu 29 in the lock is reached
  only through gpui's linux/web backends and resolves to nothing on the
  host (cargo tree -i prints empty). No duplicate wgpu or naga.
- image: ^0.25.1 (gpui) and ^0.25.2 (cap crates) unify at 0.25.10 on
  one node, so gpui frames and cap frames stay the same type; all
  camera/library image paths unchanged.
- workspace-hack: no new route to tauri-utils; tauri proper still
  absent from the lock.

tests/editor_frame0.rs proves the stack end to end: EditorInstance
(clean of tauri types -- the websocket glue lives in the Tauri app's
editor_window.rs) opened on a copy of a real studio .cap with a
headless audio output, one instruction pushed onto preview_tx (seek_to
renders nothing -- the digest trap, now confirmed in source), frame 0
taken off the frame callback, un-padded from the 256-byte row stride
and written as a 1080x702 PNG with size/layout/content asserts. A
bundle this app recorded itself round-trips through the editor stack
at the same dimensions.

Opening a project is not read-only (EditorInstance::new writes
project-config.json back in two places) -- the smoke works on a copy,
and the editor window unit inherits that fact. Also inherited:
ProjectRecordingsMeta::new panics rather than errors on a corrupt
display track, so the window needs a pre-flight probe.

cargo check +17.8s, target 18G -> 19G, 38+1 tests green, record cycle
unaffected.
…ames

editor_window.rs: 1275x800 (min the same), native traffic lights at
(20,32), one window per .cap path with re-focus on a second open
(EditorWindowIds, as a registry Vec). Opaque, no material, faithfully:
/editor is a sibling of (window-chrome), applyMacOSWindowMaterial has
exactly two call sites and neither is it, and CapWindowId::Editor is
absent from is_transparent().

Project load runs EditorInstance on the background executor behind a
preflight that reproduces every Err EditorInstance::new can return and
then runs the panicking ProjectRecordingsMeta::new under catch_unwind
-- both arms proven with deliberately corrupted bundle copies, the app
alive and showing the in-window error state each time. Opening a
project writes project-config.json back, exactly as the shipping app
does. The main window hides on open and restores when the last editor
closes (windows.rs:1930 + new-main:2925 + lib.rs:5788).

Frames: frame_cb -> bounded flume channel -> background un-pad of the
256-byte stride + R/B swap -> RenderImage, previous frame evicted from
the atlas per replacement; the first picture arrives by pushing the
playhead onto preview_tx (seek_to renders nothing). Letterboxing is
Player.tsx:566-601 verbatim, painted via canvas bounds into the fitted
rect only.

Shell regions pixel-measured against the capture: header 56, sidebar
rail 64, sidebar column 416 with the 8px gutter, resize handle 16,
timeline 260, clip track 52 -- every one the transcribed metric.
Recents cards now open the editor; CAP_GPUI_AUTO_EDITOR drives it for
the harness. Record -> edit round trip proven on a bundle this app
recorded.

Header/transport/sidebar controls render inert where their features do
not exist yet (documented); the full 1:1 build-out continues in E2+.

49 unit + 1 integration tests green; record cycle zero borrow errors.
…, measured 60fps

The on_state_change seam is real now: playhead positions land in an
AtomicU32 poked through a one-slot channel from the cap-playback OS
thread, drained on the main thread with burst coalescing. Transport is
desired-state -- {playing, seek, seek_gen} behind a mutex with a single
driver task applying stop/seek/start in the source's order, which is
what makes seek-while-playing coalesce exactly as the Tauri frontend
does (Timeline/index.tsx:829-909).

Behaviors transcribed with evidence, including the surprises: at end
of media the UI stops 0.1s before the engine's own end and parks the
playhead (Player.tsx:156-210); prev/next are jump-to-start/end, not
frame steps (Player.tsx:370-405); container presses seek on mouseup to
the mousedown position while only the ruler surface scrub-drags
(TL/index.tsx:1155-1169 vs 873-957); Space toggles; playback starts at
(60, 1248x702). Ruler scrubbing includes the 10px snap-to-zero.

Performance, measured on a 15.9s recording: 59.9fps sustained, 949
frames, 0 dropped, engine skips 0, convert 0.93ms/frame (5.6% of the
60fps budget) -- the zero-copy upstream change is not needed for
playback. The one real finding: at opt-level 0 the same convert is
30.1ms and the pump drops 45% of frames, so the dev profile now
compiles the app crate at opt-level 2 (+7s build) to show the editor's
true speed.

Reliability: 45-cycle play/pause/seek torture, 45/45 clean, one
dropped frame total, RSS plateaus at 1.1GB (engine frame cache, not a
leak; paused editor sits at 345MB); close mid-playback leaves no
cap-playback thread 1.2s later and a reopened editor plays at full
rate; verified with real CGEvent input (Accessibility is granted on
this machine) alongside the env hooks.

Also corrected E1's README claim about lazy audio: EditorInstance
prewarms the cpal stream at load (editor_instance.rs:305).

56 unit + 1 integration tests green; record cycle zero borrow errors.
src/ui/, 13 modules: Button/IconButton (8 variants x 4 sizes), Toggle,
Slider (one maths module replacing both formula families it found --
proven equivalent at 10,001 points per range), SegmentedControl (the
superset of all four Tauri idioms), Menu with the full keyboard
contract (open-at-pointer, arrow-walk with wrap, Enter commits, Escape
and click-away dismiss), Select, TextField, Card/Popover/SettingRow/
Section surfaces incl. liquid-glass, Collapsible with real measured-
height animation, TabRail, CircularProgress (one, replacing three),
KbdChip, Tooltip. Theming is named constructors per surface, mirroring
exactly the three attribute-selector remaps theme.css actually has.

Settings, teleprompter, main and editor windows retrofitted; mode
select is genuinely bespoke and stays local. Net -882/+446 across the
window files.

The regression bar: before/after captures for every window pair are
pixel-identical (0 differing pixels; the two apparent diffs traced to
a parked-cursor hover and store ordering, not code). Menu and slider
contracts proven with real CGEvent input, including the store write
landing (doNothing -> reopenRecordingWindow; defaultZoomAmount 3.9).
Every harness flow re-run clean; playback still 59.9fps / 0 dropped /
867us convert.

One intentional behavior delta, documented: settings text fields now
treat modifier+Backspace/Escape as edits, matching the main window's
field. 93 unit + 1 integration tests green (+37).
…zoom and pan

editor_timeline.rs: the whole timeline model and rendering at fidelity.
The transform is the Tauri model exactly (zoom = visible seconds,
position = left-edge second, clamp order matters on short projects,
originPercentage capped not floored, the once-only 80px on-mount fit),
and every zoom gesture anchors where the source anchors -- playbackTime
for keys/glyphs/slider, previewTime ?? playbackTime for ctrl+wheel
(context.ts:1389-1487, Player.tsx:256-465, TL/index.tsx:1190-1205).
gpui's scroll delta is the DOM's opposite sign; pinch maps through
Chromium's ctrl+wheel synthesis. CGEvent-verified to four decimals.

All nine track types render from real data and were verified visually
on a fixture carrying every one: clip (with in-clip markings, hold
bands, and REAL waveforms -- the peak extraction from src-tauri
audio.rs:42-73 transcribed and unit-tested, the createWaveformPath
curve through PathBuilder), captions, keyboard, text, mask, audio with
fade envelopes, zoom segments with the hover ghost, 3D, and scene.
Colors carry their theme.css quotes; the hover ghost playhead shows
previewTime in the clock without rendering a frame, as shipped.

Also fixed an E2 measurement error: the timeline content column is
1111px at 1275 (the scroll body carries pr-1), not 1115.

Full 11-row timeline costs 0.3 fps: 59.6 vs 59.9 on the same media,
zero dropped, segments outside the viewport never built. 45-cycle
torture clean. Two shipping findings recorded: border-green-7 and
border-sky-7 are dead classes (caption/keyboard selection changes
nothing on screen in the Tauri app), and the Tauri editor has no
noninteractive route to open a project, so comparison was against the
TSX metrics.

120 unit + 1 integration tests green (+27).
@superagent-security

Copy link
Copy Markdown

🚨 Contributor flagged. Click here for more info: Superagent Dashboard

@socket-security

socket-security Bot commented Aug 18, 2026

Copy link
Copy Markdown

@socket-security

socket-security Bot commented Aug 18, 2026

Copy link
Copy Markdown

Warning

Review the following alerts detected in dependencies.

According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.

Action Severity Alert  (click "▶" to expand/collapse)
Warn High
Obfuscated code: cargo cbindgen is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: ?cargo/cbindgen@0.28.0

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore cargo/cbindgen@0.28.0. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
Obfuscated code: cargo hyper-util is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: ?cargo/reqwest@0.12.28cargo/axum@0.7.9cargo/hyper-util@0.1.20

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore cargo/hyper-util@0.1.20. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
Obfuscated code: cargo openssl is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: ?cargo/reqwest@0.12.28cargo/ort@2.0.0-rc.12cargo/sentry@0.42.0cargo/openssl@0.10.81

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore cargo/openssl@0.10.81. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
Obfuscated code: cargo tokio is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: apps/desktop-gpui/Cargo.lockcargo/tokio@1.53.1

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore cargo/tokio@1.53.1. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
Obfuscated code: cargo zune-jpeg is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: ?cargo/image@0.25.10cargo/zune-jpeg@0.5.15

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore cargo/zune-jpeg@0.5.15. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

View full report

@richiemcilroy richiemcilroy changed the title Add GPUI desktop as an optional trial toggle Add Cap GPUI Version as an optional trial toggle Aug 18, 2026
@richiemcilroy

Copy link
Copy Markdown
Member Author

hey @greptile-apps please review

Comment thread apps/desktop-gpui/src/upload.rs Outdated
@richiemcilroy richiemcilroy changed the title Add Cap GPUI Version as an optional trial toggle Migrate Cap Desktop to GPUI Aug 18, 2026
@richiemcilroy richiemcilroy changed the title Migrate Cap Desktop to GPUI Cap Desktop -> GPUI Aug 18, 2026
@richiemcilroy

Copy link
Copy Markdown
Member Author

hey @greptileai, please re-review the PR

Comment thread apps/desktop-gpui/src/upload.rs Outdated
@richiemcilroy

Copy link
Copy Markdown
Member Author

hey @greptileai, please re-review the PR

Comment thread apps/desktop-gpui/src/upload.rs Outdated
@richiemcilroy

Copy link
Copy Markdown
Member Author

hey @greptileai, please re-review the PR

Comment thread apps/desktop-gpui/src/upload.rs
@richiemcilroy

Copy link
Copy Markdown
Member Author

hey @greptileai, please re-review the PR

Comment thread apps/desktop-gpui/src/upload.rs
@richiemcilroy

Copy link
Copy Markdown
Member Author

hey @greptileai, please re-review the PR

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant