Skip to content

Cap Desktop -> GPUI - #2133

Open
richiemcilroy wants to merge 147 commits into
mainfrom
gpui
Open

Cap Desktop -> GPUI#2133
richiemcilroy wants to merge 147 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 implementation alongside the existing Tauri application and integrates an experimental launcher toggle.

  • Adds GPUI recording, editing, playback, export, upload, screenshot, settings, and native-window flows.
  • Extends shared media crates and adds packaging, development, benchmark, and integration support.
  • Adds the Tauri experimental setting used to launch the GPUI binary.

Confidence Score: 4/5

The PR is not yet safe to merge because failed persistence of a completed upload’s sharing ID can still make a thumbnail-error retry create a duplicate remote video.

The upload flow continues after failing to persist the completed video ID, but retries derive reupload mode solely from metadata reloaded from disk; when thumbnail upload then fails, the next attempt runs in initial 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 Implements video and screenshot upload flows, multipart transfer, metadata persistence, retries, and provider-specific behavior.
apps/desktop-gpui/src/main.rs Defines startup and application wiring for the new native GPUI desktop client.
apps/desktop-gpui/src/recording.rs Connects GPUI recording controls to the shared recording and capture stack.
apps/desktop-gpui/src/editor_export.rs Implements the GPUI editor export workflow and its transition into link upload.
apps/desktop/src-tauri/src/gpui_app.rs Adds Tauri-side discovery and launching of the optional GPUI application.
crates/editor/src/playback.rs Extends shared playback behavior used by the new native desktop implementation.
crates/recording/src/output_pipeline/ffmpeg.rs Updates the shared FFmpeg recording pipeline consumed by desktop recording flows.
crates/rendering/src/frame_pipeline.rs Updates frame processing used by GPUI preview and editor rendering.

Reviews (9): Last reviewed commit: "chore(desktop): dev-loop and hand-off up..." | Re-trigger Greptile

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).
The full screenshot-editor port in three layers: the window shell
(centred toolbar, aspect menu, five anchored popovers with the Tauri
side effects, checkerboard preview with Preview.tsx zoom/pan/pinch
math, undo history, toasts, keyboard map), the complete annotation
engine (all six tools, layout.ts/arrow.ts geometry verbatim, selection
handles, inline text editing, layers panel with drag reorder, config
bar, blur/pixelate mask overlays resampled off-thread), and the crop
dialog riding the editor_crop engine over original.png.

screenshot_export.rs is the renderScreenshotExportCanvas pass: masks
re-filtered from an unmodified copy at export scale, shapes via
tiny-skia, text via cosmic-text at the preview's Helvetica baseline,
the output expanded to the union of image and annotation boxes with
the source's white-fill rules, and per-destination encodings (PNG,
PNG-over-white for the clipboard, JPEG 0.9 for share).
Live mirroring through a hardware VTPixelRotationSession flip
(preview-only, matching the Tauri bubble), and live background blur
through a dedicated worker thread that owns a wgpu device and runs the
same cap-camera-effects BlurProcessor the recordings get, so the
preview shows exactly what will be baked. The blur-off path keeps the
existing zero-copy IOSurface paint untouched (p50 212us).

Also ported from camera.tsx: the always-dark chrome, the 24px shipped
radius, the bracket resize handles with diagonal cursors, the
camera-issue overlay, top-left-anchored resize clamped into the
monitor, and debounced position persistence under the shared store's
cameraWindowPosition keys so both apps remember one bubble position.
deeplink.rs ports deeplink_actions.rs verbatim: StartRecording (with
the name-to-target resolution and device selection), StopRecording,
OpenEditor with the status gates, OpenSettings, the debug-gated
pause/resume/camera actions, and the macOS file:// branch. Both
delivery paths work: the GURL AppleEvent while running (buffer grown
to 64KiB so JSON-bearing URLs are not truncated) and the launch argv
under new-instance-wins single instancing.

The host is matched whole against "action", no URL is ever built from
parts, and parse failures never log URL contents, so an auth callback
landing in an error arm cannot leak its token. The parser carries the
Tauri file's own tests plus hostile-input coverage.
The Clips sidebar's record modal is live end to end: the target
buttons open the real target-select overlays with the editor hidden
for the picker, studio mode is forced while an editor target is armed,
and the finished capture is appended to the open project through the
same segment-copy the import path uses, then the standalone recording
is deleted behind parent-dir and canonical-containment guards -- only
after a successful append, so a failed append leaves the capture in
the library. The armed target is consumed on every recording end,
clean or not, so it can never leak into the next session.
From a shortcut-by-shortcut audit against the Tauri app: one OS
registration per combo with every bound action dispatched (a second
action on a shared combo was silently dropped), hotkey-started
recordings honour the stored target instead of always the primary
display, and the mic-confirm dialog is ported with the same store
toggle and availability probe. The Shortcuts page no longer
re-registers mid-capture, and a captured code is stored only if it
parses -- one unparseable code makes the Tauri app drop every binding.

The "After a Studio recording" selector is gone from General: the
recordings overlay will not exist in this app, so the option is not
surfaced. A Tauri-written showOverlay value still reads fine and maps
to reshowing the main window.
app_windows gains the editor-recording lifecycle (hidden-for-picker,
finish routing, deep-link executor init), upload.rs the screenshot
share path (create-or-get with isScreenshot, presigned PUT, the exact
Tauri auth gates), the store the screenshot editor's persisted panel
and crop-snap flags, and Cargo the tiny-skia/cosmic-text/sha2 direct
deps the export compositor uses. README parity tables refreshed to the
delivered state.
The desktop dev script goes through scripts/dev-desktop.mjs (which
supervises the gpui/classic hand-off), the cinder indirection is
retired along with scripts/local-cargo.mjs, and the Tauri side's
hand-off plumbing and experimental toggle pick up the current
gpui-app behaviour.
@richiemcilroy

Copy link
Copy Markdown
Member Author

hey @greptileai, please re-review the PR

@richiemcilroy

Copy link
Copy Markdown
Member Author

hey @greptileai, please re-review the PR

@richiemcilroy

Copy link
Copy Markdown
Member Author

hey @greptile-apps please review

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