Skip to content

feat(desktop): add deeplink control actions#1804

Closed
Iineman2 wants to merge 2 commits into
CapSoftware:mainfrom
Iineman2:codex/cap-deeplink-controls-1540
Closed

feat(desktop): add deeplink control actions#1804
Iineman2 wants to merge 2 commits into
CapSoftware:mainfrom
Iineman2:codex/cap-deeplink-controls-1540

Conversation

@Iineman2
Copy link
Copy Markdown

@Iineman2 Iineman2 commented May 12, 2026

/claim #1540

This is a focused desktop-side quality pass for the Cap deeplink/Raycast surface.

What changed:

  • fixes cap-desktop://action?... routing so action deeplinks reach the JSON payload parser instead of being rejected as invalid
  • adds recording control actions for pause, resume, toggle pause, restart, screenshot, microphone selection, camera selection, and Raycast device-cache refresh
  • reuses the existing desktop recording/input functions rather than duplicating state changes
  • writes the Raycast device cache asynchronously to the app data directory so an extension can read displays, windows, microphones, and cameras without blocking the handler
  • adds parser coverage for action-host links, nullable microphone/camera payloads, screenshot/start-recording payloads, and non-action hosts such as cap-desktop://signin

Why this is useful:

  • the existing parser path made the documented action host unusable
  • nullable mic/camera payloads are important for clearing selected devices from Raycast, not only setting them
  • the device-cache action gives Raycast a stable desktop-owned source for selectable targets

Validation:

  • cargo fmt --all -- --check passes
  • git diff --check passes
  • attempted cargo test -p cap-desktop deeplink_actions --lib --no-run, but this Windows environment resolves link.exe to uutils.coreutils instead of the MSVC linker, so Rust dependency build scripts fail before compiling project code

Demo note:

  • I cannot record a real Raycast/macOS desktop demo from this Windows runner. The included parser tests cover the concrete payload surface this PR changes, and the actions delegate into existing Cap desktop commands.

Greptile Summary

This PR fixes a long-standing routing bug where cap-desktop://action?... deeplinks were silently rejected because url.domain() returns None for custom URL schemes — only url.host_str() correctly returns \"action\". It also adds eight new recording-control actions (pause, resume, toggle-pause, restart, screenshot, microphone selection, camera selection, and Raycast device-cache refresh) that delegate directly into existing Cap desktop commands.

  • Routing fix: TryFrom<&Url> now uses host_str() instead of domain(), making the action host correctly routable for all non-special URL schemes.
  • New actions: Pause/resume/toggle/restart reuse the existing recording.rs commands verbatim; SetMicrophone/SetCamera correctly pass Option to support deselection; TakeScreenshot extracts the shared capture_target_from_mode helper from StartRecording.
  • Raycast device cache: refresh_raycast_device_cache enumerates displays, windows, microphones, and cameras and writes a JSON file asynchronously to the app data directory for Raycast to read.

Confidence Score: 4/5

The routing fix is correct and well-tested; the new actions delegate cleanly into existing, proven commands with no data-loss or correctness risk.

The url.domain() → url.host_str() fix is verified by the new parser tests. New actions reuse existing code paths without introducing new state. The device-cache refresh runs several synchronous OS APIs directly on a Tokio task thread; on a machine with slow camera or audio subsystems this could momentarily stall the runtime, but it is not a data-loss or correctness issue.

The refresh_raycast_device_cache function in deeplink_actions.rs deserves a second look for the blocking enumeration calls.

Important Files Changed

Filename Overview
apps/desktop/src-tauri/src/deeplink_actions.rs Fixes the core url.domain() → url.host_str() routing bug, adds 8 new control actions, extracts capture_target_from_mode helper, and writes the Raycast device cache asynchronously. Blocking OS enumeration calls inside the async refresh function are a minor concern.
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
apps/desktop/src-tauri/src/deeplink_actions.rs:224-240
**Blocking OS APIs on async task thread**

`list_displays()`, `list_windows()`, `MicrophoneFeed::list()`, and `cap_camera::list_cameras()` are all synchronous, potentially slow OS API calls executed directly on a Tokio async task thread. Camera and audio enumeration in particular can stall the entire Tauri runtime while they run. Since this action is designed to be called frequently from Raycast, the risk of stalls is higher than it is for `StartRecording`. Consider wrapping the enumeration block in `tokio::task::spawn_blocking(|| { ... }).await` to keep the runtime free.

### Issue 2 of 2
apps/desktop/src-tauri/src/deeplink_actions.rs:186-191
**`TakeScreenshot` silently drops the output path**

`take_screenshot` returns `Result<PathBuf, String>` and this arm discards the `PathBuf` with `.map(|_| ())`. That's fine for the deeplink contract, but there's no signal back to the Raycast caller whether the capture succeeded or where the file landed. Worth confirming whether Raycast needs the resulting path or whether fire-and-forget is intentional here.

Reviews (1): Last reviewed commit: "Add desktop deeplink control actions" | Re-trigger Greptile

Greptile also left 2 inline comments on this PR.

@superagent-security superagent-security Bot added contributor:verified Contributor passed trust analysis. pr:verified PR passed security analysis. labels May 12, 2026
Comment on lines +224 to +240
async fn refresh_raycast_device_cache(app: &AppHandle) -> Result<(), String> {
let displays = cap_recording::screen_capture::list_displays()
.into_iter()
.map(|(display, _)| display.name)
.collect();
let windows = cap_recording::screen_capture::list_windows()
.into_iter()
.map(|(window, _)| window.name)
.collect();
let microphones = MicrophoneFeed::list().keys().cloned().collect();
let cameras = cap_camera::list_cameras()
.map(|camera| RaycastCamera {
name: camera.display_name().to_string(),
camera: DeviceOrModelID::from_info(&camera),
})
.collect();
let cache = RaycastDeviceCache {
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 Blocking OS APIs on async task thread

list_displays(), list_windows(), MicrophoneFeed::list(), and cap_camera::list_cameras() are all synchronous, potentially slow OS API calls executed directly on a Tokio async task thread. Camera and audio enumeration in particular can stall the entire Tauri runtime while they run. Since this action is designed to be called frequently from Raycast, the risk of stalls is higher than it is for StartRecording. Consider wrapping the enumeration block in tokio::task::spawn_blocking(|| { ... }).await to keep the runtime free.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src-tauri/src/deeplink_actions.rs
Line: 224-240

Comment:
**Blocking OS APIs on async task thread**

`list_displays()`, `list_windows()`, `MicrophoneFeed::list()`, and `cap_camera::list_cameras()` are all synchronous, potentially slow OS API calls executed directly on a Tokio async task thread. Camera and audio enumeration in particular can stall the entire Tauri runtime while they run. Since this action is designed to be called frequently from Raycast, the risk of stalls is higher than it is for `StartRecording`. Consider wrapping the enumeration block in `tokio::task::spawn_blocking(|| { ... }).await` to keep the runtime free.

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Addressed in 7f1c1bc: the synchronous display/window/microphone/camera enumeration now runs inside tokio::task::spawn_blocking, then the device cache write happens asynchronously after the blocking result returns.

Comment on lines +186 to +191
DeepLinkAction::TakeScreenshot { capture_mode } => {
let capture_target = capture_target_from_mode(capture_mode)?;
crate::recording::take_screenshot(app.clone(), capture_target)
.await
.map(|_| ())
}
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 TakeScreenshot silently drops the output path

take_screenshot returns Result<PathBuf, String> and this arm discards the PathBuf with .map(|_| ()). That's fine for the deeplink contract, but there's no signal back to the Raycast caller whether the capture succeeded or where the file landed. Worth confirming whether Raycast needs the resulting path or whether fire-and-forget is intentional here. Is fire-and-forget acceptable for TakeScreenshot from Raycast, or does the extension need the output path?

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src-tauri/src/deeplink_actions.rs
Line: 186-191

Comment:
**`TakeScreenshot` silently drops the output path**

`take_screenshot` returns `Result<PathBuf, String>` and this arm discards the `PathBuf` with `.map(|_| ())`. That's fine for the deeplink contract, but there's no signal back to the Raycast caller whether the capture succeeded or where the file landed. Worth confirming whether Raycast needs the resulting path or whether fire-and-forget is intentional here. Is fire-and-forget acceptable for TakeScreenshot from Raycast, or does the extension need the output path?

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Addressed in 7f1c1bc: TakeScreenshot now records the returned screenshot path to raycast-last-screenshot.json in the desktop app data directory, using the same JSON writer pattern as the Raycast device cache.

@Iineman2
Copy link
Copy Markdown
Author

Addressed the Greptile P2 feedback in 7f1c1bc:

  • RefreshRaycastDeviceCache now runs the synchronous display/window/microphone/camera enumeration inside tokio::task::spawn_blocking, then writes the JSON cache asynchronously.
  • TakeScreenshot now persists the resulting path to raycast-last-screenshot.json in the app data directory, so a Raycast command has a desktop-written result file instead of losing the screenshot location.
  • Shared the app-data JSON writer with the existing raycast-device-cache.json path.

Verification after the update:

  • cargo fmt --all -- --check
  • git diff --check

I retried cargo test -p cap-desktop deeplink_actions --lib --no-run; this Windows environment still resolves link.exe to uutils.coreutils, so dependency build scripts fail before project code compiles. The Vercel failure also appears to be the usual fork deployment authorization gate.

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

Labels

🙋 Bounty claim contributor:verified Contributor passed trust analysis. pr:verified PR passed security analysis.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants