-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Add deeplink controls and Raycast extension #1574
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?
Conversation
Verification\nManual verification (Raycast CLI unavailable in this environment):\n\n- Deeplinks (macOS)\n - Start (screen):\n open "cap-desktop://action?value=%7B%22start_recording%22%3A%7B%22capture_mode%22%3A%7B%22screen%22%3A%22Built-in%20Display%22%7D%2C%22camera%22%3Anull%2C%22mic_label%22%3Anull%2C%22capture_system_audio%22%3Atrue%2C%22mode%22%3A%22instant%22%7D%7D"\n - Stop: open "cap-desktop://action?value=%7B%22stop_recording%22%3A%7B%7D%7D"\n - Pause: open "cap-desktop://action?value=%7B%22pause_recording%22%3A%7B%7D%7D"\n - Resume: open "cap-desktop://action?value=%7B%22resume_recording%22%3A%7B%7D%7D"\n - Switch mic: open "cap-desktop://action?value=%7B%22set_microphone%22%3A%7B%22mic_label%22%3A%22MacBook%20Pro%20Microphone%22%7D%7D"\n - Switch camera: open "cap-desktop://action?value=%7B%22set_camera%22%3A%7B%22camera%22%3A%7B%22DeviceID%22%3A%22YOUR_DEVICE_ID%22%7D%7D%7D"\n\n- Raycast extension\n - cd apps/raycast\n - raycast dev\n - Run 7 commands and confirm Cap Desktop responds.\n\nNotes: Raycast CLI lint could not be executed here due to missing registry access; manual verification steps provided. |
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.
5 files reviewed, 3 comments
| export default async function Command() { | ||
| await openCapDeepLink({ | ||
| start_recording: { | ||
| capture_mode: { window: "" }, |
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.
Empty window name will fail to match any window. The Rust code at deeplink_actions.rs:140 uses .find() which will return None and error with "No window with name """. Either provide a valid default window name or add a UI form to select the window (like switch-microphone.ts and switch-camera.ts).
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/raycast/src/start-recording-window.ts
Line: 6:6
Comment:
Empty window name will fail to match any window. The Rust code at `deeplink_actions.rs:140` uses `.find()` which will return `None` and error with "No window with name """. Either provide a valid default window name or add a UI form to select the window (like `switch-microphone.ts` and `switch-camera.ts`).
How can I resolve this? If you propose a fix, please make it concise.| export default async function Command() { | ||
| await openCapDeepLink({ | ||
| start_recording: { | ||
| capture_mode: { screen: "Built-in Display" }, |
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.
Hardcoded "Built-in Display" will fail if user has external monitors or uses Windows. The Rust code at deeplink_actions.rs:134 uses .find() which will return None and error with "No screen with name "Built-in Display"". Consider adding a UI form to select available displays or use the primary display.
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/raycast/src/start-recording-screen.ts
Line: 6:6
Comment:
Hardcoded `"Built-in Display"` will fail if user has external monitors or uses Windows. The Rust code at `deeplink_actions.rs:134` uses `.find()` which will return `None` and error with "No screen with name \"Built-in Display\"". Consider adding a UI form to select available displays or use the primary display.
How can I resolve this? If you propose a fix, please make it concise.
Additional Comments (1)
Prompt To Fix With AIThis is a comment left during a code review.
Path: apps/desktop/src-tauri/src/deeplink_actions.rs
Line: 94:94
Comment:
`.unwrap()` will panic if the file URL cannot be converted to a path (e.g., network paths, malformed URLs). Use `.map_err()` to return an error instead.
```suggestion
project_path: url.to_file_path().map_err(|_| ActionParseFromUrlError::Invalid)?,
```
How can I resolve this? If you propose a fix, please make it concise. |
|
Applied review feedback: replaced unwrap on file URL to_file_path() with map_err -> ActionParseFromUrlError::Invalid to avoid panic on malformed URLs. |
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.
Pull request overview
This pull request adds deeplink support for pause/resume recording and switching microphone/camera inputs, along with a Raycast extension to trigger these actions via keyboard shortcuts.
Changes:
- Added four new deeplink actions to the backend:
PauseRecording,ResumeRecording,SetMicrophone, andSetCamera - Created a new Raycast extension (
apps/raycast) with 7 commands to control Cap Desktop via deeplinks - Updated README with comprehensive deeplink documentation and examples
Reviewed changes
Copilot reviewed 12 out of 14 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| apps/desktop/src-tauri/src/deeplink_actions.rs | Added new enum variants and execution handlers for pause, resume, set microphone, and set camera actions |
| apps/raycast/package.json | Package manifest for the Raycast extension with @raycast/api dependency |
| apps/raycast/package-lock.json | Lock file for the Raycast extension dependencies |
| apps/raycast/raycast.json | Raycast extension configuration defining 7 commands |
| apps/raycast/icon.png | Extension icon for Raycast |
| apps/raycast/src/utils.ts | Utility function to construct and open deeplinks |
| apps/raycast/src/pause-recording.ts | Command to pause current recording |
| apps/raycast/src/resume-recording.ts | Command to resume paused recording |
| apps/raycast/src/stop-recording.ts | Command to stop current recording |
| apps/raycast/src/start-recording-screen.ts | Command to start screen recording |
| apps/raycast/src/start-recording-window.ts | Command to start window recording |
| apps/raycast/src/switch-microphone.ts | Form-based command to switch microphone by label |
| apps/raycast/src/switch-camera.ts | Form-based command to switch camera by device/model ID |
| README.md | Added comprehensive deeplink documentation with format description, examples, and Raycast extension overview |
Files not reviewed (1)
- apps/raycast/package-lock.json: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| export default async function Command() { | ||
| await openCapDeepLink({ | ||
| start_recording: { | ||
| capture_mode: { window: "" }, |
Copilot
AI
Feb 3, 2026
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 window capture mode is set to an empty string, but based on the backend implementation in deeplink_actions.rs lines 136-140, this will fail to find a window with an empty name. The deeplink requires a specific window name to match. Consider either prompting the user for a window name (similar to switch-microphone) or using a more appropriate default value that indicates "current window" or removing this command if it cannot function without user input.
| capture_mode: { window: "" }, | |
| capture_mode: null, |
| export default async function Command() { | ||
| await openCapDeepLink({ | ||
| start_recording: { | ||
| capture_mode: { screen: "Built-in Display" }, |
Copilot
AI
Feb 3, 2026
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 hardcoded screen name "Built-in Display" may not exist on all systems. The backend implementation at deeplink_actions.rs lines 131-135 will fail if no display matches this name exactly. Consider either prompting the user to select a display, using the primary/default display, or documenting that this command only works on MacBooks with built-in displays.
| capture_mode: { screen: "Built-in Display" }, | |
| capture_mode: {}, |
| "lint": "raycast lint" | ||
| }, | ||
| "dependencies": { | ||
| "@raycast/api": "^1.74.0", |
Copilot
AI
Feb 3, 2026
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 package.json specifies @raycast/api": "^1.74.0" but the package-lock.json has version 1.104.4 installed. While the caret (^) allows this, there's a significant version jump (1.74 to 1.104) which could indicate the dependency specification should be updated to reflect the actual minimum required version, or the lock file was generated with a newer version than intended. Consider verifying the minimum required API version for the features used in this extension.
| "@raycast/api": "^1.74.0", | |
| "@raycast/api": "^1.104.4", |
| const micLabel = (values.micLabel as string)?.trim(); | ||
| if (!micLabel) { | ||
| await showToast({ style: Toast.Style.Failure, title: "Mic label is required" }); | ||
| return; | ||
| } | ||
| await openCapDeepLink({ set_microphone: { mic_label: micLabel } }); |
Copilot
AI
Feb 3, 2026
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 microphone label validation should allow empty strings to be valid since the backend set_mic_input function accepts None as a valid value to remove the microphone input. The current validation rejects empty strings after trimming, but users should be able to clear/remove the microphone by submitting an empty value. Consider allowing empty strings or providing a separate "Clear Microphone" command.
| const micLabel = (values.micLabel as string)?.trim(); | |
| if (!micLabel) { | |
| await showToast({ style: Toast.Style.Failure, title: "Mic label is required" }); | |
| return; | |
| } | |
| await openCapDeepLink({ set_microphone: { mic_label: micLabel } }); | |
| const rawMicLabel = values.micLabel as string | undefined; | |
| const micLabel = rawMicLabel?.trim(); | |
| // If the field is unexpectedly missing, keep existing "required" behavior. | |
| if (micLabel === undefined) { | |
| await showToast({ style: Toast.Style.Failure, title: "Mic label is required" }); | |
| return; | |
| } | |
| // Allow an empty string to clear the microphone by sending `null` to the backend. | |
| const payloadMicLabel = micLabel === "" ? null : micLabel; | |
| await openCapDeepLink({ set_microphone: { mic_label: payloadMicLabel } }); |
| const deviceId = (values.deviceId as string)?.trim(); | ||
| const modelId = (values.modelId as string)?.trim(); | ||
| if (!deviceId && !modelId) { | ||
| await showToast({ style: Toast.Style.Failure, title: "Provide Device ID or Model ID" }); |
Copilot
AI
Feb 3, 2026
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 camera validation logic prevents users from clearing the camera input. Since the backend set_camera_input function accepts None to remove the camera, users should be able to submit with both fields empty. Consider allowing empty submissions to clear the camera, or provide a separate "Clear Camera" command. This aligns with the pattern used in the set_microphone deeplink action which accepts null for mic_label.
| await showToast({ style: Toast.Style.Failure, title: "Provide Device ID or Model ID" }); | |
| // Both fields empty: clear the current camera | |
| await openCapDeepLink({ set_camera: { camera: null } }); | |
| await showToast({ style: Toast.Style.Success, title: "Camera cleared" }); |
| await showToast({ style: Toast.Style.Failure, title: "Provide Device ID or Model ID" }); | ||
| return; | ||
| } | ||
| const camera = deviceId ? { DeviceID: deviceId } : { ModelID: modelId }; |
Copilot
AI
Feb 3, 2026
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.
If the user provides both deviceId and modelId, only deviceId is used and modelId is silently ignored. Consider either validating that only one is provided, or documenting this priority order in the form placeholder or title. This would prevent user confusion when both fields are filled.
| import { open } from "@raycast/api"; | ||
|
|
||
| const SCHEME = "cap-desktop://action?value="; | ||
|
|
||
| export function openCapDeepLink(payload: unknown) { | ||
| const encoded = encodeURIComponent(JSON.stringify(payload)); | ||
| return open(`${SCHEME}${encoded}`); |
Copilot
AI
Feb 3, 2026
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 openCapDeepLink function does not handle errors from the open call. If Cap Desktop is not installed or the deeplink fails to open, users will not receive feedback. Consider adding error handling to show a toast notification when the deeplink fails to open, similar to the validation error handling in switch-microphone and switch-camera commands.
| import { open } from "@raycast/api"; | |
| const SCHEME = "cap-desktop://action?value="; | |
| export function openCapDeepLink(payload: unknown) { | |
| const encoded = encodeURIComponent(JSON.stringify(payload)); | |
| return open(`${SCHEME}${encoded}`); | |
| import { open, showToast, Toast } from "@raycast/api"; | |
| const SCHEME = "cap-desktop://action?value="; | |
| export function openCapDeepLink(payload: unknown) { | |
| const encoded = encodeURIComponent(JSON.stringify(payload)); | |
| return open(`${SCHEME}${encoded}`).catch((error) => { | |
| const message = | |
| typeof error === "object" && error !== null && "message" in error | |
| ? String((error as { message?: unknown }).message ?? "Unknown error") | |
| : String(error ?? "Unknown error"); | |
| showToast(Toast.Style.Failure, "Failed to open Cap Desktop", message); | |
| }); |
|
|
||
| export default async function Command() { | ||
| await openCapDeepLink({ stop_recording: {} }); | ||
| } |
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.
I think DeepLinkAction::StopRecording is a unit variant, so this likely needs to be a JSON string ("stop_recording") rather than { stop_recording: {} } (same for pause/resume).
| } | |
| await openCapDeepLink("stop_recording"); |
| await openCapDeepLink({ | ||
| start_recording: { | ||
| capture_mode: { window: "" }, | ||
| camera: null, |
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.
With window: "" this will consistently fail to match anything. Consider making this command take user input (Raycast argument or Form) instead of hard-coding an empty window name.
| Stop recording: | ||
| ``` | ||
| cap-desktop://action?value={"stop_recording":{}} | ||
| ``` |
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.
This example should URL-encode the JSON value, and for unit actions serde typically expects a JSON string (e.g. "stop_recording") rather than { "stop_recording": {} }.
| ``` | |
| cap-desktop://action?value=%22stop_recording%22 |
|
Vercel check failure appears to be an auth/config issue (needs Vercel access for the repo/org). Code changes shouldn't affect this; please re-run or authorize Vercel for this repo if needed. |
|
Good catch. Updated README examples for unit actions to use URL-encoded JSON string values (e.g., cap-desktop://action?value=%22stop_recording%22). Pushed fix. |
|
Updated Raycast start commands to avoid hardcoded screen/window names. They now prompt for input (Form) before firing the deeplink. This prevents hardcoded defaults and matches expected behavior. |
Summary\n- Added deeplink actions for pause/resume, switch microphone, and switch camera\n- Added Raycast extension (apps/raycast) to trigger deeplinks\n- Updated README with deeplink format and examples\n\n## Manual verification\n- See README Deeplinks section for encoded examples\n- Run in apps/raycast and verify each command triggers Cap Desktop\n\n## Notes\nRaycast CLI lint not run here due to missing registry access; manual verification provided.
Greptile Overview
Greptile Summary
This PR adds deeplink support for controlling Cap Desktop recording actions (pause/resume, switch microphone/camera) and includes a Raycast extension to trigger these deeplinks.
Key Changes:
deeplink_actions.rswith new action variants:PauseRecording,ResumeRecording,SetMicrophone,SetCameraapps/raycast) with 7 commands that trigger Cap deeplinksIssues Found:
start-recording-window.tshas empty window name that will always fail at runtimestart-recording-screen.tshardcodes "Built-in Display" which fails on external monitors or Windowsdeeplink_actions.rs:94uses.unwrap()that can panic on malformed file URLs (macOS only)Recommendations:
.unwrap()with proper error handlingConfidence Score: 2/5
apps/raycast/src/start-recording-window.tsandapps/raycast/src/start-recording-screen.tswhich have logic errors that will cause runtime failures. Also reviewapps/desktop/src-tauri/src/deeplink_actions.rsfor panic-prone unwrap.Important Files Changed
Sequence Diagram
sequenceDiagram participant User participant Raycast participant DeepLink as cap-desktop:// URL participant Tauri participant Handler as deeplink_actions::handle participant Action as DeepLinkAction participant Recording as recording module participant Desktop as Cap Desktop App User->>Raycast: Trigger command (e.g., Pause Recording) Raycast->>Raycast: Build JSON payload Raycast->>Raycast: Encode and construct URL Raycast->>DeepLink: Open cap-desktop://action?value={...} DeepLink->>Tauri: OS deeplink handler Tauri->>Handler: handle(app_handle, urls) Handler->>Handler: Filter and parse URLs Handler->>Action: TryFrom<&Url> Action->>Action: Parse query param "value" Action->>Action: Deserialize JSON to enum variant Action-->>Handler: DeepLinkAction instance Handler->>Action: spawn async task Action->>Action: execute(app_handle) alt Pause/Resume/Stop Recording Action->>Recording: pause_recording/resume_recording/stop_recording Recording->>Desktop: Update recording state Desktop-->>Recording: Result Recording-->>Action: Result else Start Recording Action->>Recording: set_camera_input Action->>Recording: set_mic_input Action->>Recording: start_recording with inputs Recording->>Desktop: Begin capture Desktop-->>Recording: Result Recording-->>Action: Result else Set Microphone/Camera Action->>Recording: set_mic_input or set_camera_input Recording->>Desktop: Update input device Desktop-->>Recording: Result Recording-->>Action: Result else Open Settings/Editor Action->>Desktop: show_window or open_project_from_path Desktop-->>Action: Result end Action-->>Handler: Result Handler-->>User: Action completed (or error logged)(2/5) Greptile learns from your feedback when you react with thumbs up/down!