Skip to content

feat: extend deeplinks with recording controls + Raycast extension#1660

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

feat: extend deeplinks with recording controls + Raycast extension#1660
qiridigital wants to merge 1 commit intoCapSoftware:mainfrom
qiridigital:feat/deeplinks-raycast

Conversation

@qiridigital
Copy link

@qiridigital qiridigital commented Mar 16, 2026

Summary

Extends Cap's existing deeplink system with recording control actions and adds a complete Raycast extension.

Deeplink Actions Added

Action URL Example
Pause Recording cap-desktop://action?value=%22pause_recording%22
Resume Recording cap-desktop://action?value=%22resume_recording%22
Toggle Pause cap-desktop://action?value=%22toggle_pause_recording%22
Set Camera cap-desktop://action?value={%22set_camera%22:{%22camera%22:{%22ModelID%22:%22vid:pid%22}}}
Set Microphone cap-desktop://action?value={%22set_microphone%22:{%22mic_label%22:%22Mic%20Name%22}}

Implementation

All new variants are added to the existing DeepLinkAction Rust enum with serde deserialization, reusing the established cap-desktop://action?value={json} URL scheme. Each action delegates to existing public functions:

  • pause_recording() / resume_recording() / toggle_pause_recording() from recording.rs
  • set_camera_input() / set_mic_input() from lib.rs

Zero frontend changes required — the existing TryFrom<&Url> parser automatically deserializes the new variants via serde.

Raycast Extension (apps/raycast/)

8 commands built with @raycast/api:

Command Mode Description
Start Recording Form Configure capture mode, camera, mic, system audio
Stop Recording Instant One-click stop
Pause Recording Instant Pause current recording
Resume Recording Instant Resume paused recording
Toggle Pause Instant Toggle pause/resume
Set Camera Argument Switch camera by model name
Set Microphone Argument Switch mic by label
Open Settings Instant Open Cap settings

Files Changed

  • apps/desktop/src-tauri/src/deeplink_actions.rs — 5 new enum variants + execute handlers
  • apps/raycast/ — Complete Raycast extension (8 commands, README, TypeScript types)

Closes #1540

/claim #1540

Greptile Summary

Extends Cap's deeplink system with 5 new DeepLinkAction enum variants (pause, resume, toggle-pause, set-camera, set-microphone) and adds a complete Raycast extension with 8 commands that invoke these deeplinks. The Rust-side changes are clean and follow existing patterns well.

  • Critical: ModelID format mismatch causes panic — The Raycast set-camera and start-recording commands pass human-readable camera names (e.g., "FaceTime HD Camera") as ModelID, but the Rust ModelID deserializer expects a "vid:pid" format string and calls split_once(":").unwrap(), which will panic if the input lacks a colon. This affects set-camera.tsx, start-recording.tsx, and the README examples.
  • Code comments in utils.ts — The repo convention (CLAUDE.md) prohibits all code comments. utils.ts contains a JSDoc block and a single-line comment that should be removed.
  • Simple deeplink commands look good — The pause, resume, toggle-pause, stop, open-settings, and set-microphone commands all correctly serialize their actions and match the Rust-side expectations.

Confidence Score: 2/5

  • The camera-related commands will crash the desktop app at runtime due to a ModelID deserialization panic; must be fixed before merging.
  • The Rust deeplink additions are well-structured and safe. However, the Raycast extension has a critical bug: camera name strings are passed as ModelID values, but ModelID::deserialize calls unwrap() on split_once(":") — any camera name without a colon will panic and crash the app. This affects two commands (set-camera and start-recording) and the README documentation.
  • apps/raycast/src/set-camera.tsx and apps/raycast/src/start-recording.tsx — both pass camera names where vid:pid format is required, causing a runtime panic

Important Files Changed

Filename Overview
apps/desktop/src-tauri/src/deeplink_actions.rs Adds 5 new DeepLinkAction enum variants (PauseRecording, ResumeRecording, TogglePauseRecording, SetCamera, SetMicrophone) with execute handlers that correctly delegate to existing crate functions. Clean, consistent with existing patterns.
apps/raycast/src/utils.ts Core deeplink URL builder with TypeScript types mirroring the Rust DeepLinkAction enum. Contains code comments that violate repo conventions. The CameraId type definition is correct but the consuming commands misuse it.
apps/raycast/src/set-camera.tsx Passes user-provided camera name directly as ModelID, but Rust ModelID expects "vid:pid" format — will cause a panic (unwrap on None) when the input lacks a colon separator.
apps/raycast/src/start-recording.tsx Start recording form with capture mode, camera, mic, and system audio options. Same ModelID format mismatch issue as set-camera — camera name input will panic the Rust deserializer.
apps/raycast/package.json Raycast extension manifest with 8 commands. Dependencies and configuration look standard for a Raycast extension.
apps/raycast/README.md Documentation with deeplink reference. Contains misleading ModelID examples that show camera names instead of the required "vid:pid" format.

Sequence Diagram

sequenceDiagram
    participant R as Raycast Extension
    participant OS as macOS URL Handler
    participant T as Tauri Deep Link Handler
    participant DL as DeepLinkAction
    participant Rec as recording.rs
    participant Lib as lib.rs

    R->>OS: open cap-desktop://action?value=json
    OS->>T: handle(urls)
    T->>T: parse URL and deserialize JSON
    T->>DL: action.execute(app)

    alt PauseRecording / ResumeRecording / TogglePauseRecording
        DL->>Rec: pause/resume/toggle recording
    end

    alt SetCamera
        DL->>Lib: set_camera_input(app, state, camera, None)
    end

    alt SetMicrophone
        DL->>Lib: set_mic_input(state, mic_label)
    end

    alt StartRecording
        DL->>Lib: set_camera_input + set_mic_input
        DL->>Rec: start_recording(app, state, inputs)
    end
Loading
Prompt To Fix All With AI
This is a comment left during a code review.
Path: apps/raycast/src/set-camera.tsx
Line: 13

Comment:
**`ModelID` expects `vid:pid` format, not a camera name**

The Rust `ModelID` deserializer at `crates/camera/src/lib.rs:144` splits on `:` with an `unwrap()`:

```rust
let (vid, pid) = s.split_once(":").unwrap();
```

If a user enters a human-readable name like "FaceTime HD Camera" (which the placeholder text `"Camera name or model"` suggests), the deeplink will produce `{"ModelID": "FaceTime HD Camera"}`. Since there's no `:` in that string, `split_once(":").unwrap()` will **panic** and crash the desktop app.

The `ModelID` type requires a vendor-ID:product-ID string (e.g., `"0x1234:0x5678"`), not a friendly camera name. Either the Raycast command should explain this format requirement to the user, or it should use `DeviceID` instead if that accepts friendly names.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: apps/raycast/src/start-recording.tsx
Line: 44

Comment:
**Same `ModelID` panic risk as set-camera**

This line has the same issue as `set-camera.tsx`: the form placeholder says "Camera name or model" but wraps the input in `{ ModelID: values.camera }`. The Rust `ModelID` deserializer expects a `"vid:pid"` format string and will **panic** (`unwrap()` on `split_once(":")`) if the user enters a human-readable camera name like "FaceTime HD Camera".

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: apps/raycast/src/utils.ts
Line: 1-14

Comment:
**Code comments violate repo convention**

Per the project's CLAUDE.md: "CRITICAL: NO CODE COMMENTS: Never add any form of comments to code." This file contains a JSDoc block (lines 1-8) and a single-line comment on line 14 (`// Unit variants serialize as plain strings`). These should be removed — the types and function names are already self-explanatory.

**Rule Used:** CLAUDE.md ([source](https://app.greptile.com/review/custom-context?memory=9a906542-f1fe-42c1-89a2-9f252d96d9f0))

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: apps/raycast/README.md
Line: 45

Comment:
**Misleading `ModelID` example in docs**

The example shows `"ModelID": "FaceTime HD Camera"`, but in the Rust codebase `ModelID` actually deserializes from a `"vid:pid"` format string (e.g., `"0x1234:0x5678"`). This applies to both the "Start recording" and "Switch camera" examples (lines 45 and 56). Using a friendly camera name here would crash the desktop app due to an `unwrap()` panic in the `ModelID` deserializer.

How can I resolve this? If you propose a fix, please make it concise.

Last reviewed commit: ac714dc

Greptile also left 4 inline comments on this PR.

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

Context used:

  • Rule used - CLAUDE.md (source)

Add new deeplink actions for recording control:
- PauseRecording: pause the current recording
- ResumeRecording: resume a paused recording
- TogglePauseRecording: toggle pause/resume state
- SetCamera: switch camera input by model/device ID
- SetMicrophone: switch microphone input by label

All new actions reuse existing recording control functions
(pause_recording, resume_recording, toggle_pause_recording,
set_camera_input, set_mic_input) and follow the established
cap-desktop://action?value={json} URL scheme with serde
deserialization, requiring zero frontend changes.

Add Raycast extension (apps/raycast/) with 8 commands:
- Start/Stop/Pause/Resume/Toggle recording
- Set Camera, Set Microphone
- Open Settings

Closes CapSoftware#1540
@@ -0,0 +1,60 @@
/**
Copy link

Choose a reason for hiding this comment

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

Repo-wide guideline is no code comments; can we drop the docblock + inline comment here?

Suggested change
/**
export function buildDeeplinkUrl(action: DeepLinkAction): string {
const json = JSON.stringify(action);
return `cap-desktop://action?value=${encodeURIComponent(json)}`;
}
export type DeepLinkAction =
| "stop_recording"
| "pause_recording"
| "resume_recording"
| "toggle_pause_recording"
| StartRecordingAction
| SetCameraAction
| SetMicrophoneAction
| OpenEditorAction
| OpenSettingsAction;

}

async function handleSubmit(values: FormValues) {
const captureMode =
Copy link

Choose a reason for hiding this comment

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

Worth validating/normalizing inputs before firing the deeplink; empty captureName or whitespace camera/mic become hard-to-debug errors on the desktop side.

Suggested change
const captureMode =
async function handleSubmit(values: FormValues) {
const captureName = values.captureName.trim();
if (!captureName) {
await showHUD("Please enter a screen/window name");
return;
}
const captureMode = values.captureType === "screen" ? { screen: captureName } : { window: captureName };
const camera = values.camera.trim() ? { ModelID: values.camera.trim() } : null;
const micLabel = values.mic.trim() || null;
const url = buildDeeplinkUrl({
start_recording: {
capture_mode: captureMode,
camera,
mic_label: micLabel,
capture_system_audio: values.systemAudio,
mode: values.mode as "studio" | "instant",
},
});
await open(url);
await showHUD("Starting Cap recording");
}

import { LaunchProps, open, showHUD } from "@raycast/api";
import { buildDeeplinkUrl } from "./utils";

interface Arguments {
Copy link

Choose a reason for hiding this comment

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

package.json marks this argument as required: false, so the type should allow it to be missing.

Suggested change
interface Arguments {
interface Arguments {
camera?: string;
}

import { LaunchProps, open, showHUD } from "@raycast/api";
import { buildDeeplinkUrl } from "./utils";

interface Arguments {
Copy link

Choose a reason for hiding this comment

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

Same as set-camera: argument is optional in package.json, so the type should reflect that.

Suggested change
interface Arguments {
interface Arguments {
microphone?: string;
}

| Command | Description | Mode |
|---------|-------------|------|
| **Start Recording** | Start a screen/window recording with configurable options | Form |
| **Stop Recording** | Stop the current recording | Instant |
Copy link

Choose a reason for hiding this comment

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

Minor docs mismatch: Set Camera / Set Microphone are mode: "view" in apps/raycast/package.json (argument entry), but the table says Instant.

Suggested change
| **Stop Recording** | Stop the current recording | Instant |
| **Set Camera** | Switch camera input (pass name as argument) | Form |
| **Set Microphone** | Switch microphone input (pass label as argument) | Form |


const url = buildDeeplinkUrl({
set_camera: {
camera: camera ? { ModelID: camera } : null,
Copy link
Contributor

Choose a reason for hiding this comment

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

ModelID expects vid:pid format, not a camera name

The Rust ModelID deserializer at crates/camera/src/lib.rs:144 splits on : with an unwrap():

let (vid, pid) = s.split_once(":").unwrap();

If a user enters a human-readable name like "FaceTime HD Camera" (which the placeholder text "Camera name or model" suggests), the deeplink will produce {"ModelID": "FaceTime HD Camera"}. Since there's no : in that string, split_once(":").unwrap() will panic and crash the desktop app.

The ModelID type requires a vendor-ID:product-ID string (e.g., "0x1234:0x5678"), not a friendly camera name. Either the Raycast command should explain this format requirement to the user, or it should use DeviceID instead if that accepts friendly names.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/raycast/src/set-camera.tsx
Line: 13

Comment:
**`ModelID` expects `vid:pid` format, not a camera name**

The Rust `ModelID` deserializer at `crates/camera/src/lib.rs:144` splits on `:` with an `unwrap()`:

```rust
let (vid, pid) = s.split_once(":").unwrap();
```

If a user enters a human-readable name like "FaceTime HD Camera" (which the placeholder text `"Camera name or model"` suggests), the deeplink will produce `{"ModelID": "FaceTime HD Camera"}`. Since there's no `:` in that string, `split_once(":").unwrap()` will **panic** and crash the desktop app.

The `ModelID` type requires a vendor-ID:product-ID string (e.g., `"0x1234:0x5678"`), not a friendly camera name. Either the Raycast command should explain this format requirement to the user, or it should use `DeviceID` instead if that accepts friendly names.

How can I resolve this? If you propose a fix, please make it concise.

? { screen: values.captureName }
: { window: values.captureName };

const camera = values.camera ? { ModelID: values.camera } : null;
Copy link
Contributor

Choose a reason for hiding this comment

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

Same ModelID panic risk as set-camera

This line has the same issue as set-camera.tsx: the form placeholder says "Camera name or model" but wraps the input in { ModelID: values.camera }. The Rust ModelID deserializer expects a "vid:pid" format string and will panic (unwrap() on split_once(":")) if the user enters a human-readable camera name like "FaceTime HD Camera".

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/raycast/src/start-recording.tsx
Line: 44

Comment:
**Same `ModelID` panic risk as set-camera**

This line has the same issue as `set-camera.tsx`: the form placeholder says "Camera name or model" but wraps the input in `{ ModelID: values.camera }`. The Rust `ModelID` deserializer expects a `"vid:pid"` format string and will **panic** (`unwrap()` on `split_once(":")`) if the user enters a human-readable camera name like "FaceTime HD Camera".

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +1 to +14
/**
* Builds a Cap desktop deeplink URL.
*
* Cap desktop uses the URL scheme: cap-desktop://action?value={json_encoded_action}
* The JSON value follows the Rust serde enum serialization format:
* - Unit variants: "variant_name"
* - Struct variants: { "variant_name": { field: value } }
*/
export function buildDeeplinkUrl(action: DeepLinkAction): string {
const json = JSON.stringify(action);
return `cap-desktop://action?value=${encodeURIComponent(json)}`;
}

// Unit variants serialize as plain strings
Copy link
Contributor

Choose a reason for hiding this comment

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

Code comments violate repo convention

Per the project's CLAUDE.md: "CRITICAL: NO CODE COMMENTS: Never add any form of comments to code." This file contains a JSDoc block (lines 1-8) and a single-line comment on line 14 (// Unit variants serialize as plain strings). These should be removed — the types and function names are already self-explanatory.

Rule Used: CLAUDE.md (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/raycast/src/utils.ts
Line: 1-14

Comment:
**Code comments violate repo convention**

Per the project's CLAUDE.md: "CRITICAL: NO CODE COMMENTS: Never add any form of comments to code." This file contains a JSDoc block (lines 1-8) and a single-line comment on line 14 (`// Unit variants serialize as plain strings`). These should be removed — the types and function names are already self-explanatory.

**Rule Used:** CLAUDE.md ([source](https://app.greptile.com/review/custom-context?memory=9a906542-f1fe-42c1-89a2-9f252d96d9f0))

How can I resolve this? If you propose a fix, please make it concise.

{
"start_recording": {
"capture_mode": { "screen": "Built-in Retina Display" },
"camera": { "ModelID": "FaceTime HD Camera" },
Copy link
Contributor

Choose a reason for hiding this comment

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

Misleading ModelID example in docs

The example shows "ModelID": "FaceTime HD Camera", but in the Rust codebase ModelID actually deserializes from a "vid:pid" format string (e.g., "0x1234:0x5678"). This applies to both the "Start recording" and "Switch camera" examples (lines 45 and 56). Using a friendly camera name here would crash the desktop app due to an unwrap() panic in the ModelID deserializer.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/raycast/README.md
Line: 45

Comment:
**Misleading `ModelID` example in docs**

The example shows `"ModelID": "FaceTime HD Camera"`, but in the Rust codebase `ModelID` actually deserializes from a `"vid:pid"` format string (e.g., `"0x1234:0x5678"`). This applies to both the "Start recording" and "Switch camera" examples (lines 45 and 56). Using a friendly camera name here would crash the desktop app due to an `unwrap()` panic in the `ModelID` deserializer.

How can I resolve this? If you propose a fix, please make it concise.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bounty: Deeplinks support + Raycast Extension

1 participant