Skip to content

feat: Add deeplinks for recording controls + Raycast extension#1632

Open
DebuggingMax wants to merge 1 commit intoCapSoftware:mainfrom
DebuggingMax:feat/deeplinks-raycast
Open

feat: Add deeplinks for recording controls + Raycast extension#1632
DebuggingMax wants to merge 1 commit intoCapSoftware:mainfrom
DebuggingMax:feat/deeplinks-raycast

Conversation

@DebuggingMax
Copy link

@DebuggingMax DebuggingMax commented Feb 26, 2026

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:

Action Description
pause_recording Pause the current recording
resume_recording Resume a paused recording
toggle_pause_recording Toggle pause/resume state
restart_recording Stop and restart with same settings
set_microphone Switch microphone input by label
set_camera Switch camera input by device ID

Deeplink URL Format

cap-desktop://action?value=<URL-encoded JSON>

Examples:

  • Stop: cap-desktop://action?value=%7B%22stop_recording%22%3A%7B%7D%7D
  • Toggle Pause: cap-desktop://action?value=%7B%22toggle_pause_recording%22%3A%7B%7D%7D

Raycast Extension

Located in extensions/raycast/, includes:

  • Start Recording - Start a new screen recording
  • Stop Recording - Stop the current recording
  • Pause Recording - Pause the current recording
  • Resume Recording - Resume a paused recording
  • Toggle Pause - Toggle pause/resume on current recording
  • Restart Recording - Restart the current recording
  • Open Settings - Open Cap settings

Documentation

Full deeplink documentation is included in extensions/raycast/DEEPLINKS.md with examples for:

  • Shell scripts
  • AppleScript
  • JavaScript/Node.js
  • Python

Testing

The Raycast extension can be tested by:

  1. cd extensions/raycast && npm install && npm run dev
  2. Use Raycast to run the commands

Closes #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.rs to support new recording control actions, and provides a TypeScript-based Raycast extension that constructs and triggers these deeplinks.

Key Changes:

  • Extended DeepLinkAction enum with 6 new variants for recording controls and device configuration
  • Created Raycast extension with command wrappers that build and execute deeplinks
  • Added comprehensive documentation for deeplink API with multi-language examples

Critical Issues:

  • Compilation error: set_mic_input and set_camera_input functions are not public but called with crate:: prefix, causing build failure
  • Hardcoded "Main Display" in start recording may fail on systems with different display names

Minor Issues:

  • TypeScript type definition missing start_recording variant for consistency
  • Platform support documentation inconsistency between README and DEEPLINKS.md

Confidence Score: 1/5

  • This PR cannot be merged safely due to critical compilation errors in the Rust code
  • Score of 1 reflects a critical compilation error where deeplink_actions.rs calls crate::set_mic_input and crate::set_camera_input, but these functions are not public in lib.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.rs requires immediate attention for compilation errors, and extensions/raycast/src/start-recording.ts needs fixes for cross-system compatibility

Important Files Changed

Filename Overview
apps/desktop/src-tauri/src/deeplink_actions.rs Added 6 new deeplink actions (pause/resume/toggle/restart/set mic/set camera), but calls non-public functions causing compilation errors
extensions/raycast/src/utils/deeplink.ts Helper functions for executing deeplinks with proper error handling; missing start_recording type definition
extensions/raycast/src/start-recording.ts Implements start recording command with hardcoded "Main Display" that may fail on systems with different display names
extensions/raycast/README.md Documentation for Raycast extension with minor platform support inconsistency

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 notification
Loading

Last reviewed commit: f03b16d

(2/5) Greptile learns from your feedback when you react with thumbs up/down!

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
Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

14 files reviewed, 4 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +173 to +180
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
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

set_mic_input and set_camera_input are not pub in lib.rs, but are called here with crate:: prefix. This will fail compilation.

Suggested change
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.

Comment on lines +5 to +13
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 } };
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

## Requirements

- [Cap](https://cap.so) must be installed and running
- macOS only (Cap deeplinks use the `cap-desktop://` scheme)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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" },
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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> }
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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" },
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
#### 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:

```
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Formatting nit: GitHub suggestions don’t handle nested triple-backtick fences well. Here’s the same change without fenced-code nesting.

Suggested change
```
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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same note about suggestion formatting: avoiding backtick fences makes this apply cleanly.

Suggested change
```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"
~~~

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bounty: Deeplinks support + Raycast Extension

1 participant