feat: add deeplink controls and Raycast extension - #1838
Conversation
| fn parse_recording_mode(value: String) -> Result<RecordingMode, ActionParseFromUrlError> { | ||
| serde_json::from_str::<RecordingMode>(&format!("\"{value}\"")) | ||
| .map_err(|e| ActionParseFromUrlError::ParseFailed(e.to_string())) | ||
| } |
There was a problem hiding this comment.
parse_recording_mode embeds the URL-decoded value directly into a JSON string literal with format!("\"{value}\"") without escaping. A value containing a backslash (e.g., mode=studio%5C) produces "studio\" — an unterminated JSON string — causing a misleading ParseFailed error rather than a clear "invalid mode" message. Use serde_json::to_string to produce a properly escaped JSON string.
| fn parse_recording_mode(value: String) -> Result<RecordingMode, ActionParseFromUrlError> { | |
| serde_json::from_str::<RecordingMode>(&format!("\"{value}\"")) | |
| .map_err(|e| ActionParseFromUrlError::ParseFailed(e.to_string())) | |
| } | |
| fn parse_recording_mode(value: String) -> Result<RecordingMode, ActionParseFromUrlError> { | |
| let json = serde_json::to_string(&value) | |
| .map_err(|e| ActionParseFromUrlError::ParseFailed(e.to_string()))?; | |
| serde_json::from_str::<RecordingMode>(&json) | |
| .map_err(|e| ActionParseFromUrlError::ParseFailed(e.to_string())) | |
| } |
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src-tauri/src/deeplink_actions.rs
Line: 212-215
Comment:
`parse_recording_mode` embeds the URL-decoded value directly into a JSON string literal with `format!("\"{value}\"")` without escaping. A value containing a backslash (e.g., `mode=studio%5C`) produces `"studio\"` — an unterminated JSON string — causing a misleading `ParseFailed` error rather than a clear "invalid mode" message. Use `serde_json::to_string` to produce a properly escaped JSON string.
```suggestion
fn parse_recording_mode(value: String) -> Result<RecordingMode, ActionParseFromUrlError> {
let json = serde_json::to_string(&value)
.map_err(|e| ActionParseFromUrlError::ParseFailed(e.to_string()))?;
serde_json::from_str::<RecordingMode>(&json)
.map_err(|e| ActionParseFromUrlError::ParseFailed(e.to_string()))
}
```
How can I resolve this? If you propose a fix, please make it concise.| CameraSelector::Label(label) => cameras | ||
| .iter() | ||
| .find(|camera| camera.display_name == *label) | ||
| .ok_or_else(|| format!("No camera with label \"{label}\""))? | ||
| .device_or_model_id(), |
There was a problem hiding this comment.
Label matching is case-sensitive here while
DeviceId and ModelId both use eq_ignore_ascii_case. A deeplink like cap-desktop://device/camera?label=facetime+hd+camera would fail to match a camera named FaceTime HD Camera, even though the same user intent succeeds with the other two selectors. Applying consistent case-folding avoids this surprise.
| CameraSelector::Label(label) => cameras | |
| .iter() | |
| .find(|camera| camera.display_name == *label) | |
| .ok_or_else(|| format!("No camera with label \"{label}\""))? | |
| .device_or_model_id(), | |
| CameraSelector::Label(label) => cameras | |
| .iter() | |
| .find(|camera| camera.display_name.eq_ignore_ascii_case(label)) | |
| .ok_or_else(|| format!("No camera with label \"{label}\""))? | |
| .device_or_model_id(), |
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src-tauri/src/deeplink_actions.rs
Line: 288-292
Comment:
Label matching is case-sensitive here while `DeviceId` and `ModelId` both use `eq_ignore_ascii_case`. A deeplink like `cap-desktop://device/camera?label=facetime+hd+camera` would fail to match a camera named `FaceTime HD Camera`, even though the same user intent succeeds with the other two selectors. Applying consistent case-folding avoids this surprise.
```suggestion
CameraSelector::Label(label) => cameras
.iter()
.find(|camera| camera.display_name.eq_ignore_ascii_case(label))
.ok_or_else(|| format!("No camera with label \"{label}\""))?
.device_or_model_id(),
```
How can I resolve this? If you propose a fix, please make it concise.| for (const value of Object.values(object)) { | ||
| if (typeof value !== "string") continue; | ||
| const directMatch = value.match(/\b[0-9a-fA-F]{4}:[0-9a-fA-F]{4}\b/); | ||
| if (directMatch) return directMatch[0].toLowerCase(); | ||
| } |
There was a problem hiding this comment.
Unanchored value scan may produce false-positive model IDs
readModelId scans every string value in the object for /\b[0-9a-fA-F]{4}:[0-9a-fA-F]{4}\b/ before consulting key names. Any string field that incidentally contains a xxxx:xxxx hex-like substring (e.g., a UUID fragment, a serial number, or a firmware version tag) would be treated as a model ID and generate a wrong deeplink. The key-scoped readHexId path that follows already applies key filtering; applying similar key filtering to the direct-match scan (e.g. checking that the key contains model, vendor, product, or usb) would prevent false matches.
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/raycast/src/lib/devices.ts
Line: 207-211
Comment:
**Unanchored value scan may produce false-positive model IDs**
`readModelId` scans every string value in the object for `/\b[0-9a-fA-F]{4}:[0-9a-fA-F]{4}\b/` before consulting key names. Any string field that incidentally contains a `xxxx:xxxx` hex-like substring (e.g., a UUID fragment, a serial number, or a firmware version tag) would be treated as a model ID and generate a wrong deeplink. The key-scoped `readHexId` path that follows already applies key filtering; applying similar key filtering to the direct-match scan (e.g. checking that the key contains `model`, `vendor`, `product`, or `usb`) would prevent false matches.
How can I resolve this? If you propose a fix, please make it concise.- use serde_json::to_string in parse_recording_mode to avoid unescaped JSON - apply eq_ignore_ascii_case to CameraSelector::Label for consistency - scope readModelId hex scan to model/vendor/product/usb keys to prevent false positives
Rebased onto CapSoftware/Cap main (181 commits ahead). Lockfile conflict resolved by taking upstream resolution strings; the raycast workspace packages don't affect the root lockfile. All other files merged automatically without conflicts.
Replace 'npx @raycast/api@latest lint' with 'ray lint', which uses the locally installed Raycast CLI from the pinned @raycast/api@1.104.17 dependency rather than fetching the unpinned latest at runtime. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Caught up on the bot feedback and did a few things: Greptile P2 fixes (already committed in the previous push, confirming they're still intact after rebasing):
Superagent LOW: Pinned the Raycast lint script to the locally-installed Superagent MEDIUM (external deeplinks controlling recording without confirmation): Noted. The deeplink surface is the intended UX — Raycast actions and system shortcuts are user-initiated. If the team wants an explicit trust gate (e.g. a one-time prompt on first deeplink use, or an opt-in setting in Preferences → Integrations), happy to add that before merge. Let me know the preferred direction. Merge conflicts: Rebased onto the latest upstream main and resolved the single |
|
We are closing the deeplinks and Raycast bounty (#1540) without awarding it, and closing the pull requests opened against it. The full reasoning is on that issue: the remaining actions it asked for are deliberately limited to debug builds because deeplinks are a remote control surface any web page can invoke, and widening that in production is a security decision we do not want to take through a bounty. This is not a reflection on your work. Leaving it unreviewed for this long is our failure, and we are sorry for the wasted effort. If you want to contribute again, the open bug reports are the most useful place and we will review promptly. |
Summary
cap-desktop://action?value=...andcap-desktop://signin?...behaviorapps/desktop/src-tauri/DEEPLINKS.mdplus a newapps/raycastextension withcap-controlandswitch-devicecommandsDeeplink URLs
cap-desktop://record/startcap-desktop://record/start?mode=studiocap-desktop://record/start?mode=instantcap-desktop://record/stopcap-desktop://record/pausecap-desktop://record/resumecap-desktop://record/toggle-pausecap-desktop://device/microphone?label=<device-label>cap-desktop://device/microphone?off=truecap-desktop://device/camera?model_id=<vid:pid>cap-desktop://device/camera?device_id=<unique-id>cap-desktop://device/camera?id=<unique-id>cap-desktop://device/camera?label=<display-name>cap-desktop://device/camera?off=trueValidation
cargo fmt --allcargo check -p cap-desktoppango,gdk-3.0,cairo,gdk-pixbuf-2.0./node_modules/.bin/tsc -p apps/raycast/tsconfig.json --noEmit --pretty falsenpx @raycast/api@latest lintauthorfield becauseCapSoftwareis not a valid Raycast usernameManual Testing
open "cap-desktop://..."flows or verify in-app macOS recording UI sync manually.Demo
Notes
system_profileroutput and prefers cameramodel_id, thendevice_id, then label fallback.Greptile Summary
This PR adds semantic
cap-desktop://record/*andcap-desktop://device/*deeplink routes to the Tauri desktop backend, alongside a newapps/raycastextension withcap-controlandswitch-devicecommands. Existingactionandsignindeeplinks are preserved unchanged.deeplink_actions.rs,lib.rs): new URL parser dispatches to recording and device-switch handlers, routing through the same pause/resume/stop paths already used by the in-app UI;start_recording_from_saved_settingsis extracted from the existingRequestStartRecordinglistener to share logic.devices.ts,deeplinks.ts,cap-control.tsx,switch-device.tsx): enumerates macOS audio and camera devices viasystem_profiler -json, builds deeplinks preferringmodel_id→device_id→label, and opens them viacap-desktop://scheme.#[cfg(test)]module indeeplink_actions.rspins URL shape, all new record/device parse paths, and error cases.Confidence Score: 4/5
The new deeplink routes reuse well-tested recording and device-switch paths; the refactor of start_recording_from_saved_settings is behaviour-preserving.
Three small correctness concerns exist: parse_recording_mode builds a JSON string via raw format! without escaping, camera label matching is case-sensitive while the two ID selectors use eq_ignore_ascii_case, and readModelId scans all string values for a hex pattern that could match non-ID fields. None block correct operation under normal inputs but are worth addressing.
apps/desktop/src-tauri/src/deeplink_actions.rs for mode-parsing and label-matching issues; apps/raycast/src/lib/devices.ts for the model-ID heuristic.
Important Files Changed
Prompt To Fix All With AI
Reviews (1): Last reviewed commit: "chore: fix raycast icon permissions" | Re-trigger Greptile