Deep-link control for pause/resume/switch + Raycast extension#1829
Deep-link control for pause/resume/switch + Raycast extension#1829js360000 wants to merge 3 commits into
Conversation
…aycast extension (CapSoftware#1540) Extends the existing `cap-desktop://action` deep-link surface in `deeplink_actions.rs` with the four operations the bounty body calls for that weren't already wired through it: - `pause_recording` → forwards to `recording::pause_recording` - `resume_recording` → forwards to `recording::resume_recording` - `switch_camera` → forwards to `set_camera_input(...)` with `Option<DeviceOrModelID>` exactly as `start_recording` already accepts it - `switch_microphone` → forwards to `set_mic_input(...)` with `Option<String>` `start_recording`, `stop_recording`, `open_editor`, and `open_settings` are unchanged. Unit tests cover every new variant plus the existing error paths (`Invalid`, `NotAction`, `ParseFailed`) using `Url::parse_with_params` so the test cases exercise the same query-pair decode path that the real `tauri-plugin-deep-link` callback uses. The wire format is documented in a new `apps/desktop/src-tauri/DEEPLINKS.md` so future deep-link work (including the OS-level shortcut wiring for Windows) doesn't have to re-derive it from source. A minimal Raycast extension lives at `apps/raycast/` and is wired into the existing pnpm workspace (`apps/*` glob in `pnpm-workspace.yaml`). Five `no-view` commands: - Stop Cap Recording - Pause Cap Recording - Resume Cap Recording - Switch Cap Microphone (text argument: label, blank to clear) - Switch Cap Camera (text argument: ModelID or DeviceID, hex/dash pattern routes to `DeviceID`, anything else to `ModelID`, blank to clear) Each command builds a `cap-desktop://action?value=<json>` URL via a shared `fireAction` helper that uses `@raycast/api`'s `open()` and shows a HUD on success or a failure toast if the OS handler rejects the open (Cap not installed / not running). Start-recording is intentionally not wired into Raycast in this PR — picking a capture target needs a multi-step picker that we can layer on once the deep-link side has merged. Local gates run: `cargo fmt -p cap-desktop`, `biome check apps/raycast`. Full `cargo clippy -p cap-desktop --all-targets -- -D warnings` could not run locally because `ring` fails to build on this Windows ARM64 box (C-toolchain permissions issue unrelated to this change); CI will exercise it. Closes CapSoftware#1540
| const value = | ||
| typeof payload === "string" ? `"${payload}"` : JSON.stringify(payload); |
There was a problem hiding this comment.
buildActionUrl manually wraps string payloads in quotes with a template literal instead of delegating to JSON.stringify. For the three current string literals this is safe, but if a string variant were ever added that contains a backslash or a double-quote, the output would be invalid JSON. Using JSON.stringify uniformly avoids the inconsistency.
| const value = | |
| typeof payload === "string" ? `"${payload}"` : JSON.stringify(payload); | |
| const value = JSON.stringify(payload); |
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/raycast/src/deeplink.ts
Line: 15-16
Comment:
`buildActionUrl` manually wraps string payloads in quotes with a template literal instead of delegating to `JSON.stringify`. For the three current string literals this is safe, but if a string variant were ever added that contains a backslash or a double-quote, the output would be invalid JSON. Using `JSON.stringify` uniformly avoids the inconsistency.
```suggestion
const value = JSON.stringify(payload);
```
How can I resolve this? If you propose a fix, please make it concise.| identifier?: string; | ||
| } | ||
|
|
||
| const DEVICE_ID_PATTERN = /^[0-9a-fA-F-]{8,}$/; |
There was a problem hiding this comment.
The
DEVICE_ID_PATTERN regex /^[0-9a-fA-F-]{8,}$/ allows strings like -------- (8 hyphens) or aaaaaaaa (8 repeated chars) to be classified as DeviceIDs. A camera ModelID that starts with 8 hex-compatible characters could be misrouted. A pattern that requires at least one hex digit segment between hyphens — closer to a UUID — would be more precise.
| const DEVICE_ID_PATTERN = /^[0-9a-fA-F-]{8,}$/; | |
| const DEVICE_ID_PATTERN = /^[0-9a-fA-F]{8}(-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}$/i; |
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/raycast/src/switch-camera.ts
Line: 8
Comment:
The `DEVICE_ID_PATTERN` regex `/^[0-9a-fA-F-]{8,}$/` allows strings like `--------` (8 hyphens) or `aaaaaaaa` (8 repeated chars) to be classified as `DeviceID`s. A camera ModelID that starts with 8 hex-compatible characters could be misrouted. A pattern that requires at least one hex digit segment between hyphens — closer to a UUID — would be more precise.
```suggestion
const DEVICE_ID_PATTERN = /^[0-9a-fA-F]{8}(-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}$/i;
```
How can I resolve this? If you propose a fix, please make it concise.deeplink.ts: drop the special-case template-literal wrapper around
string payloads. `JSON.stringify` already produces the correct quoted
JSON for the three string variants and is robust to backslash/quote
characters if a new string variant is ever added.
switch-camera.ts: tighten `DEVICE_ID_PATTERN` from
`/^[0-9a-fA-F-]{8,}$/` (which matched degenerate inputs like
`--------` or `aaaaaaaa`) to the canonical UUID shape
`/^[0-9a-fA-F]{8}(-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}$/i`. Camera
ModelIDs that happen to start with 8 hex-compatible characters can
no longer be misrouted to DeviceID.
Biome clean on apps/raycast/src/.
|
Thanks @greptile-apps — addressed both P2 comments in
Biome clean on |
|
Both P2 fixes are confirmed clean in match url.domain() {
Some(v) if v != "action" => Err(ActionParseFromUrlError::NotAction),
_ => Err(ActionParseFromUrlError::Invalid),
}?;The Once that's in, all nine unit tests should pass and the four new deep-link actions will actually dispatch at runtime. Happy to approve once this is addressed. Tip: You can customize Greptile's behavior for this repo with |
`DeepLinkAction::try_from` was returning `Err` in both arms of the
`url.domain()` match, so the `?` propagator always early-returned and
the function never reached `query_pairs()`. Action URLs of the form
`cap-desktop://action?value=…` matched the wildcard arm
(`_ => Err(Invalid)`) and the whole action-dispatch surface was dead
code at runtime.
The fix matches `Some("action") => Ok(())` first, falls through to
`Some(_) => Err(NotAction)` for unknown domains, and keeps the
`_ => Err(Invalid)` for missing domains. After this, the existing
StartRecording / StopRecording / OpenEditor / OpenSettings flow and
the four new variants in this PR (Pause/Resume/SwitchCamera/
SwitchMicrophone) all dispatch correctly. The nine unit tests in
`#[cfg(test)] mod tests` also start passing once this is in place.
Spotted by greptile-apps during PR CapSoftware#1829 review.
|
Good catch @greptile-apps — fixed in match url.domain() {
Some("action") => Ok(()),
Some(_) => Err(ActionParseFromUrlError::NotAction),
_ => Err(ActionParseFromUrlError::Invalid),
}?;The guard now has the missing
|
|
Fix looks correct — the three-arm match is exactly right:
This unblocks all nine new tests and the pre-existing dispatch paths simultaneously. Once CI confirms clippy passes, this issue is resolved. |
Summary
feat(desktop, raycast): deep-link control for pause/resume/switch + Raycast extension (#1540)
Extends the existing
cap-desktop://actiondeep-link surface indeeplink_actions.rswith the four operations the bounty body callsfor that weren't already wired through it:
pause_recording→ forwards torecording::pause_recordingresume_recording→ forwards torecording::resume_recordingswitch_camera→ forwards toset_camera_input(...)withOption<DeviceOrModelID>exactly asstart_recordingalready accepts itswitch_microphone→ forwards toset_mic_input(...)withOption<String>start_recording,stop_recording,open_editor, andopen_settingsare unchanged.Unit tests cover every new variant plus the existing error paths
(
Invalid,NotAction,ParseFailed) usingUrl::parse_with_paramsso the test cases exercise the same query-pair decode path that the
real
tauri-plugin-deep-linkcallback uses.The wire format is documented in a new
apps/desktop/src-tauri/DEEPLINKS.mdso future deep-link work(including the OS-level shortcut wiring for Windows) doesn't have to
re-derive it from source.
A minimal Raycast extension lives at
apps/raycast/and is wired intothe existing pnpm workspace (
apps/*glob inpnpm-workspace.yaml). Fiveno-viewcommands:pattern routes to
DeviceID, anything else toModelID, blank toclear)
Each command builds a
cap-desktop://action?value=<json>URL via ashared
fireActionhelper that uses@raycast/api'sopen()andshows a HUD on success or a failure toast if the OS handler rejects
the open (Cap not installed / not running). Start-recording is
intentionally not wired into Raycast in this PR — picking a capture
target needs a multi-step picker that we can layer on once the
deep-link side has merged.
Local gates run:
cargo fmt -p cap-desktop,biome check apps/raycast. Fullcargo clippy -p cap-desktop --all-targets -- -D warningscould not run locally becauseringfails to build on thisWindows ARM64 box (C-toolchain permissions issue unrelated to this
change); CI will exercise it.
Closes #1540
Diff
Claim
/claim #1540
Greptile Summary
This PR extends the
cap-desktop://actiondeep-link surface with four new actions (pause/resume recording, switch camera/microphone) and adds a Raycast extension that wraps them asno-viewcommands. The Rust dispatch logic and the TypeScript command layer are both well-structured.deeplink_actions.rs: New enum variants and theirexecutearms look correct, but all nine new unit tests will fail at theparse(\u2026).expect(\"parses\")call because the pre-existingtry_fromguard (_ => Err(Invalid)) always short-circuits before reaching the JSON deserialization. The guard needs aSome(\"action\") => Ok(())arm.apps/raycast/: Fiveno-viewcommands correctly map to deep-link payloads via a sharedfireActionhelper; theDEVICE_ID_PATTERNregex for camera routing is broader than a UUID pattern and could misclassify short hex-looking ModelIDs.Confidence Score: 3/5
The newly added actions won’t dispatch and the new tests won’t pass until the try_from guard is corrected; the Raycast extension itself is safe to merge independently.
The pre-existing match guard in try_from makes all URL parsing fail before the JSON deserializer is reached, meaning none of the four new deep-link actions would actually fire at runtime, and every one of the nine new tests would panic. Fixing the guard is a one-liner but it must happen before any of the Rust-side changes are functional.
apps/desktop/src-tauri/src/deeplink_actions.rs — specifically the try_from match guard at the top of the URL parsing path.
Important Files Changed
Comments Outside Diff (1)
apps/desktop/src-tauri/src/deeplink_actions.rs, line 99-113 (link)try_fromguard always short-circuits — all new unit tests would failBoth arms of the match return
Err(...), so the?always returns early before the query-pair parsing is reached. Whenurl.domain()isSome("action"), the first arm's guard (v != "action") isfalse, so it falls through to_ => Err(Invalid). The function never reaches thequery_pairs()/serde_json::from_strcode, meaning everycap-desktop://action?value=…URL is silently rejected asInvalid. Every new test in this PR that callsparse(…).expect("parses")would panic.The guard logic is inverted — a
Some("action") => Ok(())arm is missing. This is pre-existing code, but the nine new unit tests added here are the first to exercise it, so CI would surface the failures on this PR.Prompt To Fix With AI
Prompt To Fix All With AI
Reviews (1): Last reviewed commit: "feat(desktop, raycast): deep-link contro..." | Re-trigger Greptile