feat: add deeplink support and Raycast extension (#1540)#1728
feat: add deeplink support and Raycast extension (#1540)#1728bcornish1797 wants to merge 2 commits intoCapSoftware:mainfrom
Conversation
apps/raycast/src/deeplink.ts
Outdated
| if (!url.startsWith("cap-desktop://")) { | ||
| throw new Error("Cap deeplink must use cap-desktop://"); | ||
| } |
There was a problem hiding this comment.
buildCapUrl always constructs the URL with the cap-desktop:// prefix via the template literal, so url.startsWith("cap-desktop://") is unconditionally true and the branch can never be taken. The guard can be removed without changing behavior.
| if (!url.startsWith("cap-desktop://")) { | |
| throw new Error("Cap deeplink must use cap-desktop://"); | |
| } | |
| const url = buildCapUrl(path, params); | |
| try { |
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/raycast/src/deeplink.ts
Line: 41-43
Comment:
**Dead safety guard**
`buildCapUrl` always constructs the URL with the `cap-desktop://` prefix via the template literal, so `url.startsWith("cap-desktop://")` is unconditionally `true` and the branch can never be taken. The guard can be removed without changing behavior.
```suggestion
const url = buildCapUrl(path, params);
try {
```
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
已处理:这个 dead guard 已在后续提交里移除。
| - `capture_type`: `screen` or `window` (required) | ||
| - `target`: screen/window name exactly as shown in Cap (required) | ||
| - `capture_system_audio`: `true` / `false` (optional) | ||
| - `mic_label`: microphone label exactly as shown in Cap (optional) | ||
| - omitting `mic_label`, `device_id`, `model_id`, and `off` keeps the current Cap inputs unchanged | ||
| - camera: | ||
| - `device_id=<id>` or `model_id=<VID:PID>` | ||
| - `off=true` to disable camera |
There was a problem hiding this comment.
mic_off parameter not documented
The Rust parser for record/start calls parse_microphone_from_query(&query, "mic_label", "mic_off"), meaning mic_off=true is a valid way to explicitly disable the microphone when starting a recording. This parameter is never mentioned in the docs — users relying on the docs will not know it exists.
Consider adding a bullet like:
- `mic_off=true` to disable microphone input (cannot be combined with `mic_label`)
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src-tauri/DEEPLINKS.md
Line: 15-22
Comment:
**`mic_off` parameter not documented**
The Rust parser for `record/start` calls `parse_microphone_from_query(&query, "mic_label", "mic_off")`, meaning `mic_off=true` is a valid way to explicitly disable the microphone when starting a recording. This parameter is never mentioned in the docs — users relying on the docs will not know it exists.
Consider adding a bullet like:
```
- `mic_off=true` to disable microphone input (cannot be combined with `mic_label`)
```
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
已处理:文档里已补充 mic_off=true,且注明不能与 mic_label 同用。
|
Hi, it looks like the CI workflow is waiting for approval to run (first-time contributor from a fork). Could you please approve the workflow run? Thanks! |
|
@greptile-apps both review suggestions (dead safety guard in apps/raycast/src/deeplink.ts and mic_off parameter documentation in apps/desktop/src-tauri/DEEPLINKS.md) are already addressed in commit 1d8f6cf. Please re-review. |
|
Bumping this: greptile suggestions already addressed in commit 1d8f6cf (dead safety guard removed, mic_off documented). CI is green aside from the Vercel 'Authorization required to deploy' gate that's a fork-workflow permission issue, not code. Would appreciate a re-review @greptile-apps and a look from maintainers when time allows. Happy to add anything else if blocking. |
| let (mic_label, _) = parse_microphone_from_query(&query, "label", "off")?; | ||
| Ok(Self::SwitchMicrophone { mic_label }) |
There was a problem hiding this comment.
No-params call silently disables microphone
parse_microphone_from_query returns (None, false) when neither label nor off is present — the false signals "no change requested." By discarding it with _, this arm creates SwitchMicrophone { mic_label: None }, which calls set_mic_input(state, None). Confirmed from lib.rs:565-572: set_mic_input(None) compares against app.selected_mic_label and, when the mic is currently active, proceeds to disable it. A bare cap-desktop://device/microphone (no query params) will therefore silently disable an active microphone instead of being a no-op or returning an error.
Either require at least one of label or off and return a ParseFailed error when both are absent, or mirror the StartRecordingPath pattern and only call set_mic_input when apply_mic is true:
("device", "microphone") => {
let (mic_label, apply_mic) = parse_microphone_from_query(&query, "label", "off")?;
if !apply_mic {
return Err(ActionParseFromUrlError::ParseFailed(
"device/microphone requires 'label' or 'off=true'".to_string(),
));
}
Ok(Self::SwitchMicrophone { mic_label })
}Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src-tauri/src/deeplink_actions.rs
Line: 281-282
Comment:
**No-params call silently disables microphone**
`parse_microphone_from_query` returns `(None, false)` when neither `label` nor `off` is present — the `false` signals "no change requested." By discarding it with `_`, this arm creates `SwitchMicrophone { mic_label: None }`, which calls `set_mic_input(state, None)`. Confirmed from `lib.rs:565-572`: `set_mic_input(None)` compares against `app.selected_mic_label` and, when the mic is currently active, proceeds to disable it. A bare `cap-desktop://device/microphone` (no query params) will therefore silently disable an active microphone instead of being a no-op or returning an error.
Either require at least one of `label` or `off` and return a `ParseFailed` error when both are absent, or mirror the `StartRecordingPath` pattern and only call `set_mic_input` when `apply_mic` is `true`:
```rust
("device", "microphone") => {
let (mic_label, apply_mic) = parse_microphone_from_query(&query, "label", "off")?;
if !apply_mic {
return Err(ActionParseFromUrlError::ParseFailed(
"device/microphone requires 'label' or 'off=true'".to_string(),
));
}
Ok(Self::SwitchMicrophone { mic_label })
}
```
How can I resolve this? If you propose a fix, please make it concise.|
Hi, could a maintainer please approve the CI for this fork PR? All checks are ready to run. Thank you! |
|
Hi team — quick follow-up on this one. Looking at the CI state, the only remaining blocker is the Vercel deployment check, which shows Would any team member with Vercel team access mind approving the deployment when you get a chance? That's the single step between this PR and mergeability. Thanks! |
Summary
cap-desktop://deeplinks for start/stop/pause/resume/toggle/restart plus microphone, camera, and settings actionscap-desktop://action?value=...JSON deeplinksDeviceOrModelIDhandlingapps/raycast/demo/cap-raycast-demo.mp4/claim #1540
Validation
corepack pnpm --dir apps/raycast run typecheckNotes
apps/raycast/demo/cap-raycast-demo.mp4is included in this branch as a lightweight walkthrough videoray lintstill hits Raycast package validation / standalone lint wiring issues in this monorepo context, but the extension source typechecks cleanlyGreptile Summary
This PR adds path-based
cap-desktop://deeplinks (record/*,device/*,settings/*) alongside a new Raycast extension with 9 commands, while preserving backward compatibility with the legacycap-desktop://action?value=...JSON format. The implementation is well-structured with good input validation and unit tests — one P1 logic bug was found in the Rust deeplink parser for thedevice/microphoneroute.Confidence Score: 4/5
Safe to merge after fixing the SwitchMicrophone no-params bug — the Raycast extension itself never triggers it, but the Rust handler has a confirmed incorrect behavior for that URL shape.
One confirmed P1 defect: discarding apply_mic=false in the device/microphone route causes a bare cap-desktop://device/microphone URL to silently disable an active microphone rather than error or no-op. Verified via set_mic_input implementation in lib.rs. All other findings are P2 style/refactoring suggestions. The Raycast extension and main recording controls are clean.
apps/desktop/src-tauri/src/deeplink_actions.rs — specifically the ("device", "microphone") arm at line 281
Important Files Changed
Flowchart
%%{init: {'theme': 'neutral'}}%% flowchart TD A[cap-desktop:// URL received] --> B{path_action resolves?} B -- yes --> C{domain/action} B -- no --> D{scheme == cap-desktop?} D -- no --> E[NotAction] D -- yes --> F{domain == action?} F -- no --> G[NotAction / Invalid] F -- yes --> H[Parse JSON value param] H --> I[Legacy DeepLinkAction] C --> J["record/start → StartRecordingPath"] C --> K["record/stop|pause|resume|toggle-pause|restart"] C --> L["device/microphone → SwitchMicrophone"] C --> M["device/camera → SwitchCamera"] C --> N["settings/open → OpenSettings"] C --> O["unknown → NotAction"] J --> P[parse_camera_from_query\nparse_microphone_from_query\nparse_mode / parse_bool] L --> Q["parse_microphone_from_query\n⚠️ apply_mic flag discarded"] Q --> R["SwitchMicrophone { mic_label: None }\nif no params → silently disables mic"] K --> S[Execute recording command] P --> T[StartRecordingPath.execute\nconditional camera+mic apply] M --> U[SwitchCamera.execute] N --> V[show_window Settings] I --> W[Legacy StartRecording.execute\nunconditional camera+mic apply]Prompt To Fix All With AI
Reviews (3): Last reviewed commit: "fix: address Raycast review nits" | Re-trigger Greptile