feat: Add deeplinks for recording controls + Raycast extension#1632
feat: Add deeplinks for recording controls + Raycast extension#1632DebuggingMax wants to merge 6 commits intoCapSoftware:mainfrom
Conversation
Adds new deeplink actions for recording control: - pause_recording - Pause the current recording - resume_recording - Resume a paused recording - toggle_pause_recording - Toggle pause/resume state - restart_recording - Restart the current recording - set_microphone - Switch microphone input - set_camera - Switch camera input Also includes a complete Raycast extension with commands for: - Start Recording - Stop Recording - Pause Recording - Resume Recording - Toggle Pause - Restart Recording - Open Settings Closes CapSoftware#1540
| DeepLinkAction::SetMicrophone { label } => { | ||
| let state = app.state::<ArcLock<App>>(); | ||
| crate::set_mic_input(state, label).await | ||
| } | ||
| DeepLinkAction::SetCamera { id } => { | ||
| let state = app.state::<ArcLock<App>>(); | ||
| crate::set_camera_input(app.clone(), state, id, None).await | ||
| } |
There was a problem hiding this comment.
set_mic_input and set_camera_input are not pub in lib.rs, but are called here with crate:: prefix. This will fail compilation.
| DeepLinkAction::SetMicrophone { label } => { | |
| let state = app.state::<ArcLock<App>>(); | |
| crate::set_mic_input(state, label).await | |
| } | |
| DeepLinkAction::SetCamera { id } => { | |
| let state = app.state::<ArcLock<App>>(); | |
| crate::set_camera_input(app.clone(), state, id, None).await | |
| } | |
| DeepLinkAction::SetMicrophone { label } => { | |
| let state = app.state::<ArcLock<App>>(); | |
| crate::commands::set_mic_input(state, label).await | |
| } | |
| DeepLinkAction::SetCamera { id } => { | |
| let state = app.state::<ArcLock<App>>(); | |
| crate::commands::set_camera_input(app.clone(), state, id, None).await | |
| } |
Or make set_mic_input and set_camera_input public in lib.rs by adding pub before async fn.
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src-tauri/src/deeplink_actions.rs
Line: 173-180
Comment:
`set_mic_input` and `set_camera_input` are not `pub` in `lib.rs`, but are called here with `crate::` prefix. This will fail compilation.
```suggestion
DeepLinkAction::SetMicrophone { label } => {
let state = app.state::<ArcLock<App>>();
crate::commands::set_mic_input(state, label).await
}
DeepLinkAction::SetCamera { id } => {
let state = app.state::<ArcLock<App>>();
crate::commands::set_camera_input(app.clone(), state, id, None).await
}
```
Or make `set_mic_input` and `set_camera_input` public in `lib.rs` by adding `pub` before `async fn`.
How can I resolve this? If you propose a fix, please make it concise.| type DeepLinkAction = | ||
| | { stop_recording: Record<string, never> } | ||
| | { pause_recording: Record<string, never> } | ||
| | { resume_recording: Record<string, never> } | ||
| | { toggle_pause_recording: Record<string, never> } | ||
| | { restart_recording: Record<string, never> } | ||
| | { set_microphone: { label: string | null } } | ||
| | { set_camera: { id: string | null } } | ||
| | { open_settings: { page: string | null } }; |
There was a problem hiding this comment.
Missing start_recording variant in the DeepLinkAction type definition. While start-recording.ts defines its own interface, this type should include all possible actions for consistency and type safety.
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/raycast/src/utils/deeplink.ts
Line: 5-13
Comment:
Missing `start_recording` variant in the `DeepLinkAction` type definition. While `start-recording.ts` defines its own interface, this type should include all possible actions for consistency and type safety.
How can I resolve this? If you propose a fix, please make it concise.
extensions/raycast/README.md
Outdated
| ## Requirements | ||
|
|
||
| - [Cap](https://cap.so) must be installed and running | ||
| - macOS only (Cap deeplinks use the `cap-desktop://` scheme) |
There was a problem hiding this comment.
DEEPLINKS.md states that Cap deeplinks work on both macOS and Windows, but this README says "macOS only". Update to clarify platform support or remove if Windows is supported.
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/raycast/README.md
Line: 18
Comment:
DEEPLINKS.md states that Cap deeplinks work on both macOS and Windows, but this README says "macOS only". Update to clarify platform support or remove if Windows is supported.
How can I resolve this? If you propose a fix, please make it concise.| export default async function Command() { | ||
| const action: StartRecordingAction = { | ||
| start_recording: { | ||
| capture_mode: { screen: "Main Display" }, |
There was a problem hiding this comment.
Hardcoded "Main Display" may not match the actual display name on all systems. If the display name doesn't match, the deeplink will fail with "No screen with name" error (see deeplink_actions.rs:137). Consider adding configuration or detecting the primary display name.
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/raycast/src/start-recording.ts
Line: 18
Comment:
Hardcoded "Main Display" may not match the actual display name on all systems. If the display name doesn't match, the deeplink will fail with "No screen with name" error (see `deeplink_actions.rs:137`). Consider adding configuration or detecting the primary display name.
How can I resolve this? If you propose a fix, please make it concise.| const DEEPLINK_SCHEME = "cap-desktop://action"; | ||
|
|
||
| type DeepLinkAction = | ||
| | { stop_recording: Record<string, never> } |
There was a problem hiding this comment.
I think there’s a serialization mismatch here: DeepLinkAction in Rust has unit variants for stop/pause/resume/toggle/restart, so serde_json expects those as JSON strings (e.g. "stop_recording"). This extension is currently sending objects with empty payloads (e.g. { "stop_recording": {} }), which likely won’t deserialize and will make these commands no-op.
Options:
- Raycast side: send a JSON string for unit actions (keep object form for
start_recording/set_*/open_settings). - Rust side: change unit variants to empty struct variants so
{ "stop_recording": {} }works consistently.
| export default async function Command() { | ||
| const action: StartRecordingAction = { | ||
| start_recording: { | ||
| capture_mode: { screen: "Main Display" }, |
There was a problem hiding this comment.
Hardcoding capture_mode: { screen: "Main Display" } seems brittle since the desktop handler matches on the exact display/window name it enumerates. Consider making the target configurable via Raycast preferences (or prompting), otherwise this will fail for users whose primary display name doesn’t match this string.
|
|
||
| ## How It Works | ||
|
|
||
| This extension uses Cap's deeplink API to control recordings. Each command sends a URL like: |
There was a problem hiding this comment.
The docs here show raw JSON embedded in the URL (and as an object). In practice value needs to be URL-encoded JSON, and unit actions likely need to be a JSON string to match the Rust enum.
| This extension uses Cap's deeplink API to control recordings. Each command sends a URL like: | |
| This extension uses Cap's deeplink API to control recordings. Each command sends a URL like (the `value` param must be URL-encoded JSON): | |
cap-desktop://action?value=%22stop_recording%22
|
|
||
| ### Recording Controls | ||
|
|
||
| #### Stop Recording |
There was a problem hiding this comment.
If the desktop enum keeps StopRecording/PauseRecording/etc as unit variants, these examples should be JSON strings (not { "stop_recording": {} }). Also the URL examples should encode the quotes.
| #### Stop Recording | |
| #### Stop Recording | |
| ```json | |
| "stop_recording" |
Pause Recording
"pause_recording"Resume Recording
"resume_recording"Toggle Pause/Resume
"toggle_pause_recording"Restart Recording
Stops and immediately restarts with the same settings:
"restart_recording"|
|
||
| This extension uses Cap's deeplink API to control recordings. Each command sends a URL like: | ||
|
|
||
| ``` |
There was a problem hiding this comment.
Formatting nit: GitHub suggestions don’t handle nested triple-backtick fences well. Here’s the same change without fenced-code nesting.
| ``` | |
| This extension uses Cap's deeplink API to control recordings. Each command sends a URL like (the `value` param must be URL-encoded JSON): | |
| cap-desktop://action?value=%22stop_recording%22 |
| ### Recording Controls | ||
|
|
||
| #### Stop Recording | ||
| ```json |
There was a problem hiding this comment.
Same note about suggestion formatting: avoiding backtick fences makes this apply cleanly.
| ```json | |
| #### Stop Recording | |
| ~~~json | |
| "stop_recording" | |
| ~~~ | |
| #### Pause Recording | |
| ~~~json | |
| "pause_recording" | |
| ~~~ | |
| #### Resume Recording | |
| ~~~json | |
| "resume_recording" | |
| ~~~ | |
| #### Toggle Pause/Resume | |
| ~~~json | |
| "toggle_pause_recording" | |
| ~~~ | |
| #### Restart Recording | |
| Stops and immediately restarts with the same settings: | |
| ~~~json | |
| "restart_recording" | |
| ~~~ |
…ink_actions Address review comment: these functions need pub visibility to be callable from deeplink_actions.rs
Address review comment: serde serializes unit enum variants as JSON
strings like "stop_recording", not objects like {"stop_recording": {}}.
Added start_recording to DeepLinkAction type.
Address review comment: don't hardcode "Main Display". Added preferences for: - Display name (configurable, defaults to Main Display) - Recording mode (instant/studio) - Capture system audio (checkbox)
Address review comments: - Changed "macOS only" to "macOS and Windows supported" - Fixed examples to show URL-encoded JSON - Fixed unit variant format: strings like "stop_recording", not objects - Added troubleshooting note about unit vs struct variants
| @@ -470,7 +470,7 @@ impl App { | |||
| #[tauri::command] | |||
There was a problem hiding this comment.
pub(crate) should be enough here (the deeplink handler is in the same crate) and keeps these from becoming part of the public surface.
| #[tauri::command] | |
| pub(crate) async fn set_mic_input(state: MutableState<'_, App>, label: Option<String>) -> Result<(), String> { |
| @@ -573,7 +573,7 @@ fn get_system_diagnostics() -> cap_recording::diagnostics::SystemDiagnostics { | |||
| #[specta::specta] | |||
There was a problem hiding this comment.
Same idea here: crate-visible is sufficient for deeplink_actions.rs and avoids widening the API.
| #[specta::specta] | |
| pub(crate) async fn set_camera_input( |
| const preferences = getPreferenceValues<Preferences>(); | ||
|
|
||
| // Use configured display name or fall back to empty string | ||
| // Empty string will let Cap use the primary/default display | ||
| const displayName = preferences.displayName?.trim() || "Main Display"; |
There was a problem hiding this comment.
Minor: repo convention is to avoid inline comments; also these two lines describe a behavior that isn’t implemented (the fallback is still "Main Display").
| const preferences = getPreferenceValues<Preferences>(); | |
| // Use configured display name or fall back to empty string | |
| // Empty string will let Cap use the primary/default display | |
| const displayName = preferences.displayName?.trim() || "Main Display"; | |
| const preferences = getPreferenceValues<Preferences>(); | |
| const displayName = preferences.displayName?.trim() || "Main Display"; |
| const DEEPLINK_SCHEME = "cap-desktop://action"; | ||
|
|
||
| // Unit variants serialize as strings in serde | ||
| type UnitAction = | ||
| | "stop_recording" | ||
| | "pause_recording" | ||
| | "resume_recording" | ||
| | "toggle_pause_recording" | ||
| | "restart_recording"; | ||
|
|
||
| // Struct variants serialize as objects | ||
| type StructAction = |
There was a problem hiding this comment.
Same note about inline comments; the types are already self-explanatory.
| const DEEPLINK_SCHEME = "cap-desktop://action"; | |
| // Unit variants serialize as strings in serde | |
| type UnitAction = | |
| | "stop_recording" | |
| | "pause_recording" | |
| | "resume_recording" | |
| | "toggle_pause_recording" | |
| | "restart_recording"; | |
| // Struct variants serialize as objects | |
| type StructAction = | |
| const DEEPLINK_SCHEME = "cap-desktop://action"; | |
| type UnitAction = | |
| | "stop_recording" | |
| | "pause_recording" | |
| | "resume_recording" | |
| | "toggle_pause_recording" | |
| | "restart_recording"; | |
| type StructAction = |
|
|
||
| The extension supports the following preferences (configurable in Raycast): | ||
|
|
||
| - **Display Name** - Name of the display to record (leave empty for primary display) |
There was a problem hiding this comment.
This currently doesn’t actually “leave empty for primary display” (the command falls back to "Main Display"). Might be worth rewording so users don’t assume empty will always work.
| - **Display Name** - Name of the display to record (leave empty for primary display) | |
| - **Display Name** - Name of the display to record (defaults to "Main Display"; set this if it doesn’t match your system) |
extensions/raycast/package.json
Outdated
| { | ||
| "name": "displayName", | ||
| "title": "Display Name", | ||
| "description": "Name of the display to record. Leave empty to record the primary/main display.", |
There was a problem hiding this comment.
Same mismatch as the README: the implementation falls back to "Main Display" rather than selecting the primary display. Suggest aligning the preference description with actual behavior.
| "description": "Name of the display to record. Leave empty to record the primary/main display.", | |
| "description": "Name of the display to record. If empty, defaults to \"Main Display\".", |
- Change set_mic_input and set_camera_input to pub(crate) in lib.rs - Remove explanatory type comments in deeplink.ts (types are self-explanatory) - Fix display preference description to accurately reflect default behavior
Summary
This PR implements deeplinks support for recording controls and a complete Raycast extension, as requested in #1540.
New Deeplink Actions
The following deeplink actions have been added to
deeplink_actions.rs:pause_recordingresume_recordingtoggle_pause_recordingrestart_recordingset_microphoneset_cameraDeeplink URL Format
Examples:
cap-desktop://action?value=%7B%22stop_recording%22%3A%7B%7D%7Dcap-desktop://action?value=%7B%22toggle_pause_recording%22%3A%7B%7D%7DRaycast Extension
Located in
extensions/raycast/, includes:Documentation
Full deeplink documentation is included in
extensions/raycast/DEEPLINKS.mdwith examples for:Testing
The Raycast extension can be tested by:
cd extensions/raycast && npm install && npm run devCloses #1540
Bounty: $200
Greptile Summary
Added deeplink support for recording controls (pause, resume, toggle, restart, set microphone/camera) and a complete Raycast extension with 7 commands. The implementation extends the existing deeplink system in
deeplink_actions.rsto support new recording control actions, and provides a TypeScript-based Raycast extension that constructs and triggers these deeplinks.Key Changes:
DeepLinkActionenum with 6 new variants for recording controls and device configurationCritical Issues:
set_mic_inputandset_camera_inputfunctions are not public but called withcrate::prefix, causing build failureMinor Issues:
start_recordingvariant for consistencyConfidence Score: 1/5
deeplink_actions.rscallscrate::set_mic_inputandcrate::set_camera_input, but these functions are not public inlib.rs. This will cause build failure. Additionally, the hardcoded "Main Display" in start recording will cause runtime failures on systems with different display names. The Raycast extension code itself is well-structured, but the underlying Rust integration has blocking issues.apps/desktop/src-tauri/src/deeplink_actions.rsrequires immediate attention for compilation errors, andextensions/raycast/src/start-recording.tsneeds fixes for cross-system compatibilityImportant Files Changed
Sequence Diagram
sequenceDiagram participant User participant Raycast participant DeeplinkHandler as Cap Deeplink Handler participant DeeplinkActions as deeplink_actions.rs participant Recording as recording.rs participant App as Cap App State User->>Raycast: Trigger command (e.g., "Stop Recording") Raycast->>Raycast: Build JSON action payload Raycast->>Raycast: URL encode payload Raycast->>DeeplinkHandler: Open cap-desktop://action?value={...} DeeplinkHandler->>DeeplinkActions: Parse URL and deserialize JSON alt Stop Recording DeeplinkActions->>Recording: stop_recording(app, state) Recording->>App: Update recording state Recording-->>DeeplinkActions: Result else Pause/Resume/Toggle DeeplinkActions->>Recording: pause/resume/toggle_pause_recording() Recording->>App: Update pause state Recording-->>DeeplinkActions: Result else Set Microphone/Camera DeeplinkActions->>App: set_mic_input/set_camera_input() App->>App: Update device configuration App-->>DeeplinkActions: Result end DeeplinkActions-->>DeeplinkHandler: Success/Error DeeplinkHandler-->>Raycast: Execution result Raycast->>User: Show HUD notificationLast reviewed commit: f03b16d
(2/5) Greptile learns from your feedback when you react with thumbs up/down!