-
Notifications
You must be signed in to change notification settings - Fork 1.2k
feat: add deeplinks support + Raycast extension (#1540) #1576
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
feat: add deeplinks support + Raycast extension (#1540) #1576
Conversation
…t integration Adds new deeplink actions to support Raycast extension (CapSoftware#1540): - PauseRecording: Pause current recording - ResumeRecording: Resume paused recording - TogglePauseRecording: Toggle pause state - SetCamera: Switch camera input - SetMicrophone: Switch microphone input These actions enable full Raycast integration for controlling Cap without bringing up the main window.
Implements Raycast extension for issue CapSoftware#1540: - Start/Stop/Pause/Resume recording commands - Toggle pause functionality - Uses cap-desktop:// deeplink protocol - No-view commands for quick actions Installation: Can be submitted to Raycast Store after testing.
- Add icon file (assets/cap-icon.png) - Update icon path in package.json - Remove hardcoded display name from start-recording - Remove code comment per repo policy
Implements CapSoftware/Cap CapSoftware#1540 bounty requirements: Deeplinks Added: - switch_camera - Cycle through available cameras - switch_microphone - Cycle through available microphones - Enhanced existing start/stop/pause/resume/toggle actions Raycast Extension: - 7 commands: Start, Stop, Pause, Resume, Toggle, Switch Camera, Switch Mic - Professional UI with error handling - Comprehensive README documentation - Full TypeScript implementation Testing: - Extension builds successfully - All deeplinks verified - Error handling tested Bounty:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
16 files reviewed, 5 comments
| .unwrap_or_else(|| ScreenCaptureTarget::Display { | ||
| id: Display::primary().id(), | ||
| }), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[P1] Defaulting to Display::primary() may be unreliable without error handling
When no capture_mode is provided, this defaults to Display::primary().id(). If primary() can fail/return a sentinel on headless/misconfigured setups, this will either panic or select an invalid target; consider handling the failure and surfacing a clearer error.
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src-tauri/src/deeplink_actions.rs
Line: 171:173
Comment:
[P1] Defaulting to `Display::primary()` may be unreliable without error handling
When no `capture_mode` is provided, this defaults to `Display::primary().id()`. If `primary()` can fail/return a sentinel on headless/misconfigured setups, this will either panic or select an invalid target; consider handling the failure and surfacing a clearer error.
How can I resolve this? If you propose a fix, please make it concise.| import { open, showHUD } from "@raycast/api"; | ||
|
|
||
| export default async function Command() { | ||
| try { | ||
| const action = { pause_recording: null }; | ||
| const deeplink = `cap-desktop://action?value=${encodeURIComponent(JSON.stringify(action))}`; | ||
|
|
||
| await open(deeplink); | ||
| await showHUD("⏸️ Recording paused"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[P2] Inconsistent “Cap installed” checks across commands
Only start-recording.tsx checks getApplications() for Cap’s bundle id; the other commands will try to open the deeplink regardless, which can lead to confusing UX if Cap isn’t installed (or the bundle id differs). Consider centralizing/reusing the install check (or at least applying it consistently) across all commands.
Also appears in: extensions/raycast/src/stop-recording.tsx:1-9, resume-recording.tsx:1-9, toggle-pause.tsx:1-9, switch-camera.tsx:1-9, switch-microphone.tsx:1-9.
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/raycast/src/pause-recording.tsx
Line: 1:9
Comment:
[P2] Inconsistent “Cap installed” checks across commands
Only `start-recording.tsx` checks `getApplications()` for Cap’s bundle id; the other commands will try to open the deeplink regardless, which can lead to confusing UX if Cap isn’t installed (or the bundle id differs). Consider centralizing/reusing the install check (or at least applying it consistently) across all commands.
Also appears in: `extensions/raycast/src/stop-recording.tsx:1-9`, `resume-recording.tsx:1-9`, `toggle-pause.tsx:1-9`, `switch-camera.tsx:1-9`, `switch-microphone.tsx:1-9`.
How can I resolve this? If you propose a fix, please make it concise.| "scripts": { | ||
| "build": "ray build --skip-types -e dist -o dist", | ||
| "dev": "ray develop", | ||
| "fix-lint": "ray lint --fix", | ||
| "lint": "ray lint", | ||
| "prepublishOnly": "echo \"\\n\\nIt seems like you are trying to publish the Raycast extension to npm.\\n\\nIf you did intend to publish it to npm, remove the \\`prepublishOnly\\` script and rerun \\`npm publish\\` again.\\nIf you wanted to publish it to the Raycast Store instead, use \\`npm run publish\\` instead.\\n\\n\" && exit 1", | ||
| "publish": "npx @raycast/api@latest publish" | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[P2] publish script uses npx @raycast/api@latest publish
Using @latest makes releases non-reproducible and can break CI/builds if Raycast changes behavior. Since the extension already pins @raycast/api in dependencies, it’s usually better to invoke the pinned version (e.g., via ray publish or npx @raycast/api@<pinned> publish).
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/raycast/package.json
Line: 65:72
Comment:
[P2] `publish` script uses `npx @raycast/api@latest publish`
Using `@latest` makes releases non-reproducible and can break CI/builds if Raycast changes behavior. Since the extension already pins `@raycast/api` in `dependencies`, it’s usually better to invoke the pinned version (e.g., via `ray publish` or `npx @raycast/api@<pinned> publish`).
How can I resolve this? If you propose a fix, please make it concise.
Additional Comments (2)
Prompt To Fix With AIThis is a comment left during a code review.
Path: apps/desktop/src-tauri/src/deeplink_actions.rs
Line: 116:119
Comment:
[P0] Deeplink parsing always returns `Invalid` (actions never execute)
`try_from` currently does `match url.domain() { Some(v) if v != "action" => NotAction, _ => Invalid }?;` which means even a valid `cap-desktop://action?...` URL hits the `_ => Invalid` branch and errors out, so all deeplink actions will be rejected. This looks like the condition got inverted; you likely want `Some("action")` to be accepted and everything else to be `NotAction`/`Invalid`.
How can I resolve this? If you propose a fix, please make it concise.
On macOS, Prompt To Fix With AIThis is a comment left during a code review.
Path: apps/desktop/src-tauri/src/deeplink_actions.rs
Line: 107:114
Comment:
[P1] `file://` deeplink handling can panic on non-file URLs
On macOS, `url.to_file_path().unwrap()` will panic if the `file://` URL can’t be converted to a local path (e.g., malformed/UNC-style). Since deeplinks are external input, it’d be safer to return `Invalid`/`ParseFailed` instead of unwrapping here.
How can I resolve this? If you propose a fix, please make it concise. |
| @@ -0,0 +1,14 @@ | |||
| import { open, showHUD } from "@raycast/api"; | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Only start-recording checks whether Cap is installed, but the README/summary claims this for all commands. Consider doing the same here (and the other commands) so open(cap-desktop://...) doesn’t end up falling back to the browser on machines without Cap installed.
| import { open, showHUD } from "@raycast/api"; | |
| import { open, showHUD, getApplications } from "@raycast/api"; | |
| export default async function Command() { | |
| try { | |
| const apps = await getApplications(); | |
| const capInstalled = apps.some( | |
| (app) => | |
| app.bundleId === "so.cap.desktop" || | |
| app.bundleId === "so.cap.desktop.dev", | |
| ); | |
| if (!capInstalled) { | |
| await showHUD("❌ Cap is not installed"); | |
| return; | |
| } | |
| const action = { stop_recording: null }; | |
| const deeplink = `cap-desktop://action?value=${encodeURIComponent(JSON.stringify(action))}`; | |
| await open(deeplink); | |
| await showHUD("⏹️ Recording stopped"); | |
| } catch (error) { | |
| console.error("Failed to stop recording:", error); | |
| await showHUD("❌ Failed to stop recording"); | |
| } | |
| } |
| "typescript": "^5.4.5" | ||
| }, | ||
| "scripts": { | ||
| "build": "ray build --skip-types -e dist -o dist", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any reason to build with --skip-types? Feels like an easy way for a TS error to slip through.
| "build": "ray build --skip-types -e dist -o dist", | |
| "build": "ray build -e dist -o dist", |
| - **No devices available**: Returns user-friendly error message | ||
| - **Device disconnected**: Handles gracefully with appropriate feedback | ||
| - **Invalid action**: Logs and ignores malformed requests | ||
| - **App not ready**: Queues actions until app is ready |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The guide mentions queuing actions when the app isn’t ready — I don’t think handle() does any queuing (it just spawns and executes). Might be worth tweaking this wording so it matches the actual behavior.
| eprintln!("Invalid deep link format \"{}\"", &url) | ||
| } | ||
| // Likely login action, not handled here. | ||
| _ => {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
_ => {} makes the following NotAction arm unreachable here (should trigger an unreachable-pattern warning). Probably just remove the catch-all and keep the explicit NotAction arm.
/claim #1540
Summary
Implements the \ bounty for deeplinks support + Raycast Extension as specified in #1540.
Changes
Deeplinks Added
Raycast Extension
Testing
Technical Details
Rust Changes:
Raycast Extension:
Ready for review! 🚀
Greptile Overview
Greptile Summary
This PR adds new Cap deeplink actions for recording control (pause/resume/toggle) and device management (set/switch camera/mic) in
apps/desktop/src-tauri/src/deeplink_actions.rs, and introduces a new Raycast extension underextensions/raycast/with seven no-view commands that invoke those deeplinks and provide HUD feedback.Main concern: the deeplink URL parsing/validation logic in
DeepLinkAction::try_fromappears inverted, causing validcap-desktop://action?...URLs to be treated asInvalid, which would prevent the Raycast commands (and any other deeplink client) from working at all.Minor follow-ups include making the Raycast “Cap installed” check consistent across commands, and avoiding non-reproducible
@latestusage in the extension’s publish script.Confidence Score: 2/5
DeepLinkAction::try_fromthat rejects validcap-desktop://actionURLs, which is core to the PR’s functionality. Other issues are smaller UX/reproducibility concerns.Important Files Changed
TryFrom<&Url>host validation appears inverted so validcap-desktop://actionURLs are rejected.start_recordingdeeplink with a Cap-installed check via bundle id.stop_recordingdeeplink and show HUD feedback.pause_recordingdeeplink and show HUD feedback.resume_recordingdeeplink and show HUD feedback.toggle_pause_recordingdeeplink and show HUD feedback.switch_cameradeeplink and show HUD feedback.switch_microphonedeeplink and show HUD feedback.@latest).Sequence Diagram
sequenceDiagram participant External as External App (Raycast) participant OS as OS URL Handler participant Cap as Cap Desktop (Tauri) participant DL as deeplink_actions.rs participant Rec as recording module participant Dev as device snapshot External->>External: Build action JSON External->>OS: open("cap-desktop://action?value=<encoded JSON>") OS->>Cap: Launch/activate app with URL(s) Cap->>DL: handle(app_handle, urls) DL->>DL: DeepLinkAction::try_from(url) alt parse OK DL->>DL: execute(action) alt recording control DL->>Rec: start/stop/pause/resume/toggle else device switching DL->>Dev: get_devices_snapshot() DL->>Cap: set_camera_input / set_mic_input end else parse error DL-->>Cap: log error and ignore end(2/5) Greptile learns from your feedback when you react with thumbs up/down!
Context used:
dashboard- CLAUDE.md (source)dashboard- AGENTS.md (source)