feat: add deeplink controls and Raycast extension#1838
Conversation
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
| 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
| @@ -144,9 +352,35 @@ impl DeepLinkAction { | |||
| .await | |||
There was a problem hiding this comment.
[MEDIUM] External deeplinks can control recording and devices without confirmation
Custom deeplinks directly start/stop recordings and switch devices without confirmation.
Fix: Gate recording/device deeplinks behind user opt-in or confirmation, or use authenticated local IPC.
| ], | ||
| "scripts": { | ||
| "typecheck": "tsc --noEmit", | ||
| "lint": "npx @raycast/api@latest lint" |
There was a problem hiding this comment.
[LOW] NPM script executes latest Raycast CLI outside the lockfile
lint runs npx @raycast/api@latest, bypassing the pinned lockfile version.
Fix: Use the locked local Raycast CLI or pin npx to the reviewed version.
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