feat: add deeplink actions for recording control and Raycast extension#1814
feat: add deeplink actions for recording control and Raycast extension#1814fansifei wants to merge 2 commits into
Conversation
Add new DeepLinkAction variants for pause, resume, toggle-pause, restart, screenshot, switch camera/microphone, and device cache refresh. Fix URL parsing bug: url.domain() returns None for custom schemes like cap-desktop:// — use url.host_str() instead. Create Raycast extension at apps/raycast/ with 10 commands: start/stop/pause/resume/toggle/restart recording, screenshot, device listing, switch camera, switch microphone. Closes CapSoftware#1540 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
| export async function sendAction(action: string) { | ||
| const url = `${DEEP_LINK_BASE}?value=${encodeURIComponent(action)}`; | ||
| await open(url); | ||
| } |
There was a problem hiding this comment.
sendAction passes a bare string to the URL instead of a JSON-encoded string. The Rust backend calls serde_json::from_str(json_value) on the decoded value, so it requires valid JSON — a plain string like pause_recording is not valid JSON and will fail to parse. Every command using sendAction (pause, resume, stop, restart, take-screenshot, toggle-pause) will silently fail. Note that list-devices.ts already correctly wraps the value in JSON.stringify("refresh_raycast_device_cache").
| export async function sendAction(action: string) { | |
| const url = `${DEEP_LINK_BASE}?value=${encodeURIComponent(action)}`; | |
| await open(url); | |
| } | |
| export async function sendAction(action: string) { | |
| const url = `${DEEP_LINK_BASE}?value=${encodeURIComponent(JSON.stringify(action))}`; | |
| await open(url); | |
| } |
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/raycast/src/utils.ts
Line: 5-8
Comment:
`sendAction` passes a bare string to the URL instead of a JSON-encoded string. The Rust backend calls `serde_json::from_str(json_value)` on the decoded value, so it requires valid JSON — a plain string like `pause_recording` is not valid JSON and will fail to parse. Every command using `sendAction` (pause, resume, stop, restart, take-screenshot, toggle-pause) will silently fail. Note that `list-devices.ts` already correctly wraps the value in `JSON.stringify("refresh_raycast_device_cache")`.
```suggestion
export async function sendAction(action: string) {
const url = `${DEEP_LINK_BASE}?value=${encodeURIComponent(JSON.stringify(action))}`;
await open(url);
}
```
How can I resolve this? If you propose a fix, please make it concise.| const captureName = props.arguments.captureName || ""; | ||
|
|
||
| await sendActionWithPayload("start_recording", { | ||
| capture_mode: { [captureMode]: captureName }, |
There was a problem hiding this comment.
Empty
captureName causes start recording to fail silently
When captureName is not supplied, it defaults to "". The Rust handler then calls list_displays().into_iter().find(|(s, _)| s.name == name) with an empty string, which will never match any real display name and returns Err("No screen with name \"\""). The user sees the "Cap: Recording started" HUD but no recording is started. Either default to the first available display (matching what TakeScreenshot does), or require the argument to be provided in package.json.
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/raycast/src/start-recording.ts
Line: 14-17
Comment:
**Empty `captureName` causes start recording to fail silently**
When `captureName` is not supplied, it defaults to `""`. The Rust handler then calls `list_displays().into_iter().find(|(s, _)| s.name == name)` with an empty string, which will never match any real display name and returns `Err("No screen with name \"\"")`. The user sees the "Cap: Recording started" HUD but no recording is started. Either default to the first available display (matching what `TakeScreenshot` does), or require the argument to be provided in `package.json`.
How can I resolve this? If you propose a fix, please make it concise.| const cameras = [ | ||
| { name: "Built-in Camera", id: "built-in" }, | ||
| { name: "External Camera", id: "external" }, | ||
| ]; |
There was a problem hiding this comment.
Hardcoded device list never reflects actual hardware
Both switch-camera.ts and switch-microphone.ts define a fixed array ("Built-in Camera", "External Camera", etc.) that is never populated with real system devices. A user with a USB webcam or external audio interface will not see it listed, and selecting a hardcoded entry will try to switch to a device ID/name that likely doesn't exist in the Cap backend. The same issue affects list-devices.ts, where the comment says "actual list comes from the Cap app" but there is no IPC channel for Cap to push device data back to Raycast — the list is always the two hardcoded entries.
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/raycast/src/switch-camera.ts
Line: 8-11
Comment:
**Hardcoded device list never reflects actual hardware**
Both `switch-camera.ts` and `switch-microphone.ts` define a fixed array (`"Built-in Camera"`, `"External Camera"`, etc.) that is never populated with real system devices. A user with a USB webcam or external audio interface will not see it listed, and selecting a hardcoded entry will try to switch to a device ID/name that likely doesn't exist in the Cap backend. The same issue affects `list-devices.ts`, where the comment says "actual list comes from the Cap app" but there is no IPC channel for Cap to push device data back to Raycast — the list is always the two hardcoded entries.
How can I resolve this? If you propose a fix, please make it concise.| DeepLinkAction::RefreshRaycastDeviceCache => { | ||
| // Refresh by re-initializing the mic feed | ||
| let state: tauri::State<'_, ArcLock<App>> = app.state(); | ||
| let mut app_state = state.write().await; | ||
| app_state.restart_mic_feed().await.map_err(|e| e.to_string())?; | ||
| Ok(()) | ||
| } |
There was a problem hiding this comment.
RefreshRaycastDeviceCache only refreshes microphone feed, not cameras
The handler calls restart_mic_feed() only. Camera device enumeration is left untouched, so Raycast's "List Devices" / "Switch Camera" flows won't benefit from this refresh for cameras. If the intent is a full device-list refresh, the camera feed should also be restarted here.
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src-tauri/src/deeplink_actions.rs
Line: 193-199
Comment:
**`RefreshRaycastDeviceCache` only refreshes microphone feed, not cameras**
The handler calls `restart_mic_feed()` only. Camera device enumeration is left untouched, so Raycast's "List Devices" / "Switch Camera" flows won't benefit from this refresh for cameras. If the intent is a full device-list refresh, the camera feed should also be restarted here.
How can I resolve this? If you propose a fix, please make it concise.| useEffect(() => { | ||
| // Populate with known device types — actual list comes from the Cap app | ||
| setDevices([ | ||
| { name: "Built-in Camera", type: "camera" }, | ||
| { name: "Built-in Microphone", type: "microphone" }, | ||
| ]); | ||
| setIsLoading(false); | ||
| }, []); |
There was a problem hiding this comment.
Second
useEffect immediately overrides loading state with hardcoded data
The second useEffect runs synchronously on mount and calls setIsLoading(false) before the async deeplink in the first effect resolves. As a result, isLoading flips to false almost immediately regardless of whether Cap has responded, and the shown devices are always the two hardcoded entries — the refresh deeplink result has no path to update this list. If the goal is to show a spinner while Cap refreshes, the second effect should not set isLoading(false).
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/raycast/src/list-devices.ts
Line: 25-32
Comment:
**Second `useEffect` immediately overrides loading state with hardcoded data**
The second `useEffect` runs synchronously on mount and calls `setIsLoading(false)` before the async deeplink in the first effect resolves. As a result, `isLoading` flips to `false` almost immediately regardless of whether Cap has responded, and the shown devices are always the two hardcoded entries — the refresh deeplink result has no path to update this list. If the goal is to show a spinner while Cap refreshes, the second effect should not set `isLoading(false)`.
How can I resolve this? If you propose a fix, please make it concise.- Fix sendAction(): wrap action string in JSON.stringify() so serde_json::from_str() on the Rust side can parse it correctly - Fix start-recording: use undefined instead of empty string for captureName to avoid matching failure on display lookup - Fix list-devices: merge two useEffects into one to prevent loading state from flipping before deeplink resolves
| DeepLinkAction::RestartRecording => { | ||
| crate::recording::restart_recording(app.clone(), app.state()) | ||
| .await | ||
| .map(|_| ()) |
There was a problem hiding this comment.
[MEDIUM] Unauthenticated deeplink action can capture screenshots
New cap-desktop deeplink action directly invokes take_screenshot without caller validation.
Fix: Gate screenshot deeplinks with confirmation, nonce/auth, or trusted IPC before capturing the screen.
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
Closes #1540
Greptile Summary
This PR adds a Raycast extension for controlling Cap screen recording via
cap-desktop://deeplinks, and extends the Rust deeplink handler with new actions (pause, resume, restart, screenshot, switch device). Theurl.domain()→url.host_str()fix is correct and necessary for custom URL schemes.sendActioninutils.tspasses a bare string (e.g.pause_recording) instead of a JSON-encoded string (e.g.\"pause_recording\");serde_json::from_strrejects it, so every simple command — pause, resume, stop, restart, take-screenshot, toggle-pause — silently fails.captureNamedefaults to\"\", which never matches any display name in the Rust handler, causing an error that the HUD does not surface to the user.switch-camera.ts,switch-microphone.ts, andlist-devices.tsall show fixed, made-up device names with no mechanism to receive real device data back from Cap, so switching devices will target non-existent hardware.Confidence Score: 2/5
Not ready to merge — the core deeplink encoding bug means the majority of Raycast commands will silently do nothing when invoked.
The
sendActionutility is the shared foundation for six of the ten commands, and its missingJSON.stringifycall means every one of those commands sends a URL the Rust backend cannot parse. Additionally, Start Recording will fail with any user who omits a screen name (the common case), and both switch-device commands operate on a hardcoded fixture list rather than real hardware.apps/raycast/src/utils.tsis the highest-priority fix;apps/raycast/src/start-recording.ts,switch-camera.ts,switch-microphone.ts, andlist-devices.tsall need follow-up before the extension is usable.Important Files Changed
Prompt To Fix All With AI
Reviews (1): Last reviewed commit: "feat: add deeplink actions for recording..." | Re-trigger Greptile