Skip to content

feat(desktop-gpui): resize a picked window to a ratio or size, and focus it to record it alone - #3454

Merged
richiemcilroy merged 22 commits into
mainfrom
feat/gpui-window-arrange-focus
Oct 28, 2026
Merged

richiemcilroy merged 22 commits into
mainfrom
feat/gpui-window-arrange-focus

Conversation

@richiemcilroy

@richiemcilroy richiemcilroy commented Oct 27, 2026 •

Copy link
Copy Markdown
Member

Stacked on #3452.

Problem

In the window picker, the picked window's size was plain text, and the only way to change the shape of what gets recorded was to resize the window by hand. A window recording also captured a region of the screen. #3452 adds the option to record the window by itself, but nothing in the app could turn it on.

Change

  • Size chip. The size readout becomes a chip. It shows the size the window will record at (pixels, so 1440 × 900 points reads 2880 × 1800 on a Retina display) and its ratio when it matches one. It opens a menu:
    • Ratios: 21:9, 16:9, 16:10, 4:3, 1:1 and 9:16. A ratio keeps the window's area and centre, grows a tiny window to at least 160 pt on its shorter side, and keeps it at least 16 pt inside the display's usable area (menu bar, Dock and taskbar excluded).
    • Sizes: 720p, 1080p, 1440p and 4K in recorded pixels, listed only when they fit.
    • The current ratio or size is ticked.
  • Focus toggle. Focus centres the window in the usable area, brings it to the front, and records it by itself. Turning it off puts the window back where the picker found it.
    • Focus lives in the picker's shared state, so every display's overlay shows the choice the recording will use.
    • Picking a window while Focus is on centres and raises it, as turning Focus on does.
    • In Instant mode with a camera, macOS and Windows keep recording the screen region, because the camera bubble reaches the video through the screen capture (feat(recording): capture a window by itself, on request, on macOS and Windows #3452). The Focus tooltip says so.
  • Window capture style setting. Focus is the same stored choice as a new Settings › General › Window capture style row: Screen region (the default, today's behaviour) or Focused window. The last choice is remembered. Recording reads it as it starts.
  • Moving the window. foreign_window does the platform work:
    • macOS: the Accessibility API (AXPosition/AXSize, matched to the window through _AXUIElementGetWindow), which Cap already holds the permission for.
    • Windows: SetWindowPos, adding back DWM's invisible resize borders and restoring a maximized window first. The maximized window's WINDOWPLACEMENT is saved, so putting it back maximizes it again with its old restore size.
    • X11: _NET_MOVERESIZE_WINDOW and _NET_ACTIVE_WINDOW.
    • Wayland: cannot move other apps' windows, so the chip stays read-only with a tooltip saying so and Focus is hidden. The same happens on macOS without Accessibility.
  • Off the UI thread, in order. Moves and restores run on one worker thread, in the order they were asked for, because SetWindowPos waits on the target app. So a cancel's restore always lands after a move the user started first. The highlight then animates to wherever the app actually put the window, since apps clamp to their own minimum sizes or cell grids. A move that finishes after the user has picked another window, or closed the picker, leaves the new choice alone.
  • The menu holds its window. Opening the size menu pins the window it acts on, so passing over another app on the way to a row cannot change the target.
  • Highlight tracking. The pinned window's bounds are now re-read on every picker poll. Before, the highlight stayed where the window was first picked.
  • Restoring. Dismissing the picker puts back every window it moved. Starting a recording keeps them where they were shaped.

The new overlay strings are literals, like the rest of the overlay on this base. #3423 localizes the overlay, and they move into its catalogue when this rebases onto it. The settings row and options are localized in all seven catalogues.

Test

  • Unit tests (window_arrange, and foreign_window's ordering of queued jobs):

    • ratio keeps area and centre;
    • every ratio near every edge stays on screen;
    • tiny windows grow;
    • exact sizes fit or are refused on 1x and 2x displays;
    • centring;
    • ratio detection.
  • i18n: the i18n surface tests pin the new keys, and cap-i18n-build passes.

  • Real app: Picker smoke (real app) now drives a real window on macOS, Windows and Linux X11 through the overlay's own menu and toggle. It picks the window it opened (TextEdit, Notepad or xterm) outright, so the runner's cursor position doesn't matter:

    1. chooses 16:9, then checks the actual frame is within 4% of 16:9 and inside the display;
    2. turns Focus on, then checks the window is centred;
    3. dismisses the picker, then checks the window is back within 6 pt of where it started.

    Screenshots of each step are uploaded.

RetriggerView in GreptileConfidence Score: 5/5

The PR appears safe to merge, with a non-blocking gap in feedback when Start times out.

Findings

  1. P2 Start hides its retry message ▶
Fix with agent prompt
### Issue 1
apps/desktop-gpui/src/app_windows.rs:2482-2489
When a window move takes more than five seconds, Start stops and saves the retry message in `session.error`. The picker stays open, but only the hidden main window shows that error. The user sees neither a recording nor an explanation until they close the picker.

Show the error in the picker, or reveal the main window when Start times out.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

This PR adds window resizing and Focus controls to the picker, plus a saved window capture setting.

  • The picker lets people choose a window size or shape.
  • Focus centers the picked window and can record it by itself.
  • The picker moves picked windows and restores them when it closes.

Reviews (8) · Last reviewed commit: "fix(desktop-gpui): tie a waiting Start t..." · Reviewed by Greptile

@vercel

vercel Bot commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
cap-status Ready Ready Preview Oct 28, 2026 2:18am UTC

@richiemcilroy

Copy link
Copy Markdown
Member Author

hey @greptileai, please re-review the PR

Comment thread apps/desktop-gpui/src/target_overlay.rs
Comment thread apps/desktop-gpui/src/target_overlay.rs
Comment thread apps/desktop-gpui/src/target_overlay.rs
Comment thread apps/desktop-gpui/src/target_overlay.rs Outdated
Comment thread apps/desktop-gpui/src/foreign_window.rs
Comment thread apps/desktop-gpui/src/window_arrange.rs Outdated
# Conflicts:
#	apps/desktop-gpui/src/picker_probe.rs
… pin the size menu's window, ignore stale moves, restore maximized windows
# Conflicts:
#	apps/desktop-gpui/src/picker_probe.rs
@richiemcilroy

Copy link
Copy Markdown
Member Author

hey @greptileai, please re-review the PR

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

Copy link
Copy Markdown
Member Author

hey @greptileai, please re-review the PR

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

Copy link
Copy Markdown
Member Author

hey @greptileai, please re-review the PR

@capy-ai capy-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Capy found no issues.

Open in Capy

@capy-ai capy-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Capy found no issues.

Open in Capy

@capy-ai capy-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Capy found no issues.

Open in Capy

… put moved windows back on every non-recording exit

@capy-ai capy-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Capy found 1 potential issue (1 medium).

View 1 other finding in Capy.

Open in Capy

Comment thread apps/desktop-gpui/src/app_windows.rs Outdated
let limit = cx.background_executor().timer(ARRANGE_SETTLE_LIMIT);
futures_util::future::select(std::pin::pin!(settled), std::pin::pin!(limit)).await;
cx.update(|cx| {
if !cx.global::<AppWindows>().overlays.is_empty() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deferred overlay Start can fire against a re-armed picker

Medium Severity

With arranged windows pending, start_recording_from_overlay defers the start until in_order(|| ()) drains or ARRANGE_SETTLE_LIMIT (1500 ms) fires, and the overlay stays open and interactive the whole time: the Record click (target_overlay.rs:1252) only defers, and only start_recording_from_overlay_now closes it. If the user presses Escape (dismiss_target_overlays restores window A, closes the overlays) and re-picks window B within that window — likeliest when a hung target app forces the full 1500 ms — the deferred task's only revalidation is !cx.global::<AppWindows>().overlays.is_empty(), true again because of B's overlay. start_recording_from_overlay_now(target, cx) then runs with the click-time ScreenCaptureTarget for A: keep_arranged_windows drains B's arranged entry and discards its original, permanently leaving B's window moved, B's overlays are force-closed, and a recording of cancelled target A starts (A's own put_back is still queued behind the stuck move, so A's bounds may be read mid-restore). The gate must verify the same picker/target is still armed, or dismiss/arm must cancel a pending deferred start.

Open in Capy

@richiemcilroy

Copy link
Copy Markdown
Member Author

@greptileai

Comment thread apps/desktop-gpui/src/app_windows.rs Outdated
Comment on lines +2454 to +2456
cx.update(|cx| {
if !cx.global::<AppWindows>().overlays.is_empty() {
start_recording_from_overlay_now(target, cx);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Cancelled Start can resume

While a slow window move holds the queue, the user can press Start, dismiss the picker, and reopen it for another window. After the wait, this check accepts any open overlay and starts the old target, recording the window the user cancelled.

Tie the pending Start to the picker that requested it, and invalidate it on dismissal.

Knowledge Base Used: GPUI desktop client

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop-gpui/src/app_windows.rs
Line: 2454-2456

Comment:
**Cancelled Start can resume**

While a slow window move holds the queue, the user can press Start, dismiss the picker, and reopen it for another window. After the wait, this check accepts any open overlay and starts the old `target`, recording the window the user cancelled.

Tie the pending Start to the picker that requested it, and invalidate it on dismissal.

**Knowledge Base Used:** [GPUI desktop client](https://app.greptile.com/cap/-/custom-context/knowledge-base/capsoftware/cap/-/docs/gpui-desktop-client.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Comment thread apps/desktop-gpui/src/app_windows.rs Outdated
{
return;
}
keep_arranged_windows(cx);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Cancelled starts leave windows changed

keep_arranged_windows forgets the original frames before recording checks finish. For example, cancelling the low-storage prompt after pressing Start leaves every resized or focused window changed, even though no recording started. Users must put those windows back by hand.

Keep the saved frames through these checks and restore them on cancellation. Discard them only when capture commits to starting.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop-gpui/src/app_windows.rs
Line: 2472

Comment:
**Cancelled starts leave windows changed**

`keep_arranged_windows` forgets the original frames before recording checks finish. For example, cancelling the low-storage prompt after pressing Start leaves every resized or focused window changed, even though no recording started. Users must put those windows back by hand.

Keep the saved frames through these checks and restore them on cancellation. Discard them only when capture commits to starting.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

…ows still moving, keep moved windows until recording commits

@capy-ai capy-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Capy found 1 potential issue (1 medium).

View 2 other findings in Capy.

Open in Capy

);
cx.update(|cx| {
let current = overlay_handles(cx);
if current.is_empty() || current != overlays {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deferred Start records the click-time target after the pick changes

Medium Severity

The deferred Start captured target from the picker's selection at click time (target_overlay.rs start_recording), but the revalidation at app_windows.rs:2479 only compares overlay-window identity, and the new comment's claim that opening a new picker drops the Start does not hold for a same-mode re-open. While the settle wait runs (up to 5 s with the new limit), the picker stays interactive: a click on another window re-pins through the overlay's click-anywhere-to-select (pin_hovered), and a same-mode re-arm (tray item, hotkey) goes through open_overlays_core/reopens_overlays (app_windows.rs:1951), which keeps the identical overlay windows when the armed mode already matches, so current == overlays passes. The deferred task then calls start_recording_from_overlay_now with the stale target: it force-closes the picker that now presents window B and records window A; keep_arranged_windows at app_windows.rs:4015 then drains every arranged entry including B's saved original, so a B the user resized after the click is left moved with nothing to put it back. Bind the waiting Start to the picked target — compare the click-time target/pinned window against TargetSelect's current one and drop when it moved — not just to the overlay set.

Open in Capy

@richiemcilroy

Copy link
Copy Markdown
Member Author

@greptileai

Comment on lines +2482 to +2489
if !settled {
tracing::warn!("a picked window is still moving; recording not started");
RecordingSession::global(cx).update(cx, |session, cx| {
session.error =
Some("The window is still being resized. Try starting again.".into());
cx.notify();
});
return;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Start hides its retry message

When a window move takes more than five seconds, Start stops and saves the retry message in session.error. The picker stays open, but only the hidden main window shows that error. The user sees neither a recording nor an explanation until they close the picker.

Show the error in the picker, or reveal the main window when Start times out.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop-gpui/src/app_windows.rs
Line: 2482-2489

Comment:
**Start hides its retry message**

When a window move takes more than five seconds, Start stops and saves the retry message in `session.error`. The picker stays open, but only the hidden main window shows that error. The user sees neither a recording nor an explanation until they close the picker.

Show the error in the picker, or reveal the main window when Start times out.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

@superagent-security superagent-security Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Superagent found 1 security concern(s).

now=$(date +%s)
for client in "$app" /bin/bash /Users/runner/bin/Runner.Worker /Users/runner/bin/Runner.Listener; do
sudo sqlite3 "$db" "INSERT OR REPLACE INTO access (service,client,client_type,auth_value,auth_reason,auth_version,indirect_object_identifier,flags,last_modified) VALUES ('kTCCServiceScreenCapture','$client',1,2,4,1,'UNUSED',0,$now);"
for service in kTCCServiceScreenCapture kTCCServiceAccessibility; do

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: macOS picker smoke grants Accessibility TCC to runner processes used by fork-PR code

New loop grants Accessibility TCC to /bin/bash and runner worker processes used by untrusted fork-PR builds.

Grant Accessibility only to the built app, or prevent fork PRs from running this self-hosted smoke with the grant.

AI prompt
Check if this security scanner issue is valid. If so, understand the root cause and fix it. If appropriate, update or add tests. Keep the change focused and preserve intended behavior.

<file name=".github/workflows/picker-smoke.yml">
<violation number="1" location=".github/workflows/picker-smoke.yml:141">
<priority>P2</priority>
<title>macOS picker smoke grants Accessibility TCC to runner processes used by fork-PR code</title>
<evidence>The permission step previously inserted only `kTCCServiceScreenCapture` for the built app, `/bin/bash`, `Runner.Worker`, and `Runner.Listener`. The changed loop now inserts both `kTCCServiceScreenCapture` and `kTCCServiceAccessibility` for the same clients. This workflow is triggered by `pull_request` and, for fork PRs that touch `apps/desktop-gpui/src/**`, runs the PR-built binary and PR-controlled `scripts/setup.js`/`scripts/picker-smoke/run.sh` on Blacksmith macOS runners. Granting Accessibility to `/bin/bash` and the runner worker processes means any later shell step can use macOS UI automation, read other applications' accessibility trees, and synthesize input without an additional prompt. That is broader than the pre-existing screen-capture grant.</evidence>
<recommendation>Do not grant `kTCCServiceAccessibility` to `/bin/bash`, `Runner.Worker`, or `Runner.Listener`. Grant it only to the specific built binary, ideally only after the build has been reviewed, or restrict this workflow to same-repository PRs/trusted branches. If the smoke must run on fork PRs, use an ephemeral runner account that has no access to other applications or credentials and explicitly reset the TCC grant after the job.</recommendation>
</violation>
</file>

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This grant does not widen what PR code can do on that runner. The Blacksmith macOS runner is a single-use VM, and the same job already runs with passwordless sudo (this very step writes TCC.db with it), so code from a fork PR could grant itself any TCC service anyway. Accessibility is needed because the smoke moves another app's window through the AX API, and macOS checks that against the responsible process (the runner that launches cap-gpui), not the binary alone; granting it to the app only would make the window-arrangement check fail.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The developer’s explanation addresses the core concern: the Blacksmith runner is a single-use VM, so the TCC grant is not persistent, and the same job already runs with passwordless sudo—meaning untrusted code could already modify TCC.db directly. Additionally, the Accessibility grant to the runner processes is technically required because macOS applies the AX permission check to the responsible process launching the app, not the built binary alone, so restricting the grant to the app would break the smoke test. No additional exposure is introduced by this change.

@capy-ai capy-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Capy found no issues.

View 1 other finding in Capy.

Open in Capy

@richiemcilroy
richiemcilroy merged commit e1df511 into main Oct 28, 2026
61 checks passed

This branch was successfully deployed

2 active deployments
Preview – cap-web — be4fe9de Deployed Oct 28, 2026 by vercel[bot]
Preview – cap-status — be4fe9de Deployed Oct 28, 2026 by vercel[bot]
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