Skip to content

fix: stop waiting forever on a hung picker thumbnail capture - #2460

Open
AatmanAJ wants to merge 3 commits into
CapSoftware:mainfrom
AatmanAJ:fix/gpui-thumbnail-capture-timeout
Open

AatmanAJ wants to merge 3 commits into
CapSoftware:mainfrom
AatmanAJ:fix/gpui-thumbnail-capture-timeout

Conversation

@AatmanAJ

@AatmanAJ AatmanAJ commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

Refs #2339

When ScreenCaptureKit never answers a thumbnail request, the GPUI app waits on it forever. #2339 shows that this happens in practice: an SCScreenshotManager request hung right after launch and left the purple screen-recording indicator on.

In the GPUI app, that one hang also stopped thumbnails for the rest of the session:

  • The sweep's blocking thread parks for good.
  • Its channel never closes, so display_inflight (or window_inflight) is never cleared, and every later start_capture returns early.
  • Display thumbnails stop refreshing until Cap restarts.
  • The launch prewarm waits on the display sweep, so it never reaches windows.

Fix (target_thumbnails.rs)

  • Wrap the shareable-content read and each screenshot in a 5 s tokio::time::timeout. A healthy 320×180 capture takes milliseconds.
  • A timeout ends that sweep. Its sender drops, so the in-flight flag resets the normal way.
  • A sweep cut short by a hang isn't recorded as complete: its target-list signature is only committed when captures aren't paused. So the picker retries it once the pause lifts, even if the window list hasn't changed.
  • A timeout also pauses thumbnail captures for 5 minutes. start_capture won't start a sweep while paused, and a running sweep stops before its next target.
    • The pause exists because ScreenCaptureKit has no way to cancel a request. A hung one stays pending in replayd, so retrying on every 5 s picker poll would only stack more of them.
    • Cards keep their last thumbnail, or their app icon, in the meantime.
  • The sweep's current-thread runtime now enables the timer driver. Without it, tokio::time::timeout panics inside the sweep.

Not covered: this only stops Cap from waiting on, and piling up, hung requests. The request that is already stuck stays inside replayd. The report shows it outlives Cap quitting, so this can't clear a stuck indicator by itself. Doing that would mean moving thumbnails off SCScreenshotManager, for example to a one-frame SCStream, which can be stopped explicitly.

Testing

  • a_hung_capture_times_out_inside_a_sweep_and_pauses_captures runs a never-resolving future through the real run_capture path. It fails if .enable_time() is removed, and it fails if the timeout doesn't start the pause.
  • capture_pause_lasts_for_the_hang_pause checks the pause covers exactly 5 minutes.
  • cargo test --bin cap-gpui: 1077 passed. rustfmt is clean, and clippy reports nothing in the changed code.
  • A real ScreenCaptureKit hang can't be triggered on demand, so I haven't seen the timeout path fire in the app.
  • Hand-checked on macOS 27.0.1: the Display and Window picker thumbnails load as before.

RetriggerConfidence Score: 4/5

Fix the window retry path and satisfy the two repository rules before merging.

Findings

  1. P1 Window thumbnails never retry ▶
  2. P2 Test breaks time subtraction rule ▶
  3. P2 Comment repeats the code ▶
Fix with agent prompt
### Issue 1
apps/desktop-gpui/src/target_thumbnails.rs:647-648
A timed-out window sweep still gets marked as complete. It sends `WindowEvent::Listed` before this early return, and the receiver saves that list’s signature when the channel closes. With an unchanged window list, `windows_stale()` then stays false, so thumbnails do not retry after the five-minute pause or when the picker reopens.

Keep interrupted sweeps stale so they can retry after the pause.

### Issue 2
apps/desktop-gpui/src/target_thumbnails.rs:1574
The new assertion subtracts time directly. `AGENTS.md` requires `saturating_sub()` instead of `a - b` for `Duration` and `Instant`. Use `(now + CAPTURE_HANG_PAUSE).saturating_sub(Duration::from_millis(1))` here. This repository requirement must be satisfied before merging.

```suggestion
        assert!(pause.active_at((now + CAPTURE_HANG_PAUSE).saturating_sub(Duration::from_millis(1))));
```

### Issue 3
apps/desktop-gpui/src/target_thumbnails.rs:87-88
This new comment only restates what `captures_paused()` does. `AGENTS.md` bans comments that narrate code behavior and requires non-obvious context instead. Remove these two lines before merging to meet that requirement. Keep the nearby explanation of why hung requests cannot be cancelled.

```suggestion

```

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

---

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

Summary

This PR adds five-second thumbnail deadlines and pauses new captures for five minutes after a timeout.

  • Hung thumbnail requests no longer hold the picker’s capture sweeps open.

Acknowledged by AatmanAJ: A timed-out native request can remain stuck and leave the recording indicator on. The PR explicitly limits this fix to ending Cap’s wait and slowing retries; cancelling the native request is deferred.

Reviews (1) · Last reviewed commit: "fix(desktop-gpui): stop waiting forever ..." · Reviewed by Greptile

Picker thumbnails await ScreenCaptureKit's shareable content and each
SCScreenshotManager screenshot with no timeout. When one request never
answers, the sweep's blocking thread parks for good, the inflight flag is
never cleared, and display thumbnails stop refreshing for the rest of the
session (the launch prewarm never reaches windows either).

Bound each of those requests by 5s. A timeout ends the sweep, so its
channel closes and inflight resets, and pauses thumbnail captures for 5
minutes: a hung request cannot be cancelled and holds macOS's capture
session open in replayd, so retrying on every picker poll would only
stack more of them. The sweep's runtime now enables the timer driver
that tokio::time::timeout needs.

This does not end a request that is already stuck inside replayd, so it
cannot clear a stuck screen-recording indicator by itself.

Refs CapSoftware#2339
@AatmanAJ
AatmanAJ marked this pull request as ready for review October 11, 2026 09:03
Comment on lines +647 to +648
else {
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.

P1 Window thumbnails never retry

A timed-out window sweep still gets marked as complete. It sends WindowEvent::Listed before this early return, and the receiver saves that list’s signature when the channel closes. With an unchanged window list, windows_stale() then stays false, so thumbnails do not retry after the five-minute pause or when the picker reopens.

Keep interrupted sweeps stale so they can retry after the pause.

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

Comment:
**Window thumbnails never retry**

A timed-out window sweep still gets marked as complete. It sends `WindowEvent::Listed` before this early return, and the receiver saves that list’s signature when the channel closes. With an unchanged window list, `windows_stale()` then stays false, so thumbnails do not retry after the five-minute pause or when the picker reopens.

Keep interrupted sweeps stale so they can retry after the pause.

---

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 7d12f22. Both sweep receivers now save the list signature only when captures aren't paused, so a sweep cut short by a hang leaves the list stale and gets retried once the pause ends.

assert!(!pause.active_at(now));
pause.start_at(now);
assert!(pause.active_at(now));
assert!(pause.active_at(now + CAPTURE_HANG_PAUSE - Duration::from_millis(1)));

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 Test breaks time subtraction rule

The new assertion subtracts time directly. AGENTS.md requires saturating_sub() instead of a - b for Duration and Instant. Use (now + CAPTURE_HANG_PAUSE).saturating_sub(Duration::from_millis(1)) here. This repository requirement must be satisfied before merging.

Suggested change
assert!(pause.active_at(now + CAPTURE_HANG_PAUSE - Duration::from_millis(1)));
assert!(pause.active_at((now + CAPTURE_HANG_PAUSE).saturating_sub(Duration::from_millis(1))));

Context Used: AGENTS.md (source)

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

Comment:
**Test breaks time subtraction rule**

The new assertion subtracts time directly. `AGENTS.md` requires `saturating_sub()` instead of `a - b` for `Duration` and `Instant`. Use `(now + CAPTURE_HANG_PAUSE).saturating_sub(Duration::from_millis(1))` here. This repository requirement must be satisfied before merging.

```suggestion
        assert!(pause.active_at((now + CAPTURE_HANG_PAUSE).saturating_sub(Duration::from_millis(1))));
```

**Context Used:** AGENTS.md ([source](https://github.com/capsoftware/cap/blob/main/AGENTS.md))

---

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 7d12f22. The test now uses CAPTURE_HANG_PAUSE.saturating_sub(Duration::from_millis(1)). Instant has no saturating_sub(Duration), so the subtraction happens on the Duration.

Comment on lines +87 to +88
/// Whether a recent hang has paused thumbnail captures; sweeps are not started
/// while it is.

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 Comment repeats the code

This new comment only restates what captures_paused() does. AGENTS.md bans comments that narrate code behavior and requires non-obvious context instead. Remove these two lines before merging to meet that requirement. Keep the nearby explanation of why hung requests cannot be cancelled.

Suggested change
/// Whether a recent hang has paused thumbnail captures; sweeps are not started
/// while it is.

Context Used: AGENTS.md (source)

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

Comment:
**Comment repeats the code**

This new comment only restates what `captures_paused()` does. `AGENTS.md` bans comments that narrate code behavior and requires non-obvious context instead. Remove these two lines before merging to meet that requirement. Keep the nearby explanation of why hung requests cannot be cancelled.

```suggestion

```

**Context Used:** AGENTS.md ([source](https://github.com/capsoftware/cap/blob/main/AGENTS.md))

---

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

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Removed in 7fd38ae.

A sweep sends its target list before capturing, and the receiver commits
that list's signature when the channel closes. A sweep that a hang ended
early was therefore recorded as complete, and with an unchanged window
list windows_stale() stayed false, so window thumbnails never retried
after the pause. Skip the commit while captures are paused so the picker
retries the sweep once the pause lifts.

Also add the pause duration with Duration::saturating_sub in the test,
per the repository's time-subtraction rule.

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant