Skip to content

feat: Add deep link actions for recording control and Raycast extension#1744

Open
BossChaos wants to merge 1 commit intoCapSoftware:mainfrom
BossChaos:deeplinks-raycast-extension
Open

feat: Add deep link actions for recording control and Raycast extension#1744
BossChaos wants to merge 1 commit intoCapSoftware:mainfrom
BossChaos:deeplinks-raycast-extension

Conversation

@BossChaos
Copy link
Copy Markdown

@BossChaos BossChaos commented Apr 20, 2026

Summary

This PR implements the bounty requirements for #1540:

1. Extended Deep Link Support

Added new deep link actions for complete recording control:

  • PauseRecording - Pause an active recording
  • ResumeRecording - Resume a paused recording
  • TogglePauseRecording - Toggle pause state
  • SwitchMicrophone - Switch to a different microphone
  • SwitchCamera - Switch to a different camera

2. Raycast Extension

Created a complete Raycast extension (apps/raycast-extension/) with 8 commands:

  • Start Recording
  • Stop Recording
  • Pause Recording
  • Resume Recording
  • Toggle Pause
  • Switch Microphone
  • Switch Camera
  • Open Settings

The extension uses the cap-desktop://action deep link protocol to communicate with the desktop app.

Testing

  • Deep link actions follow the existing pattern in deeplink_actions.rs
  • Raycast extension uses standard @raycast/api patterns
  • All commands provide user feedback via Toast notifications

Files Changed

  • apps/desktop/src-tauri/src/deeplink_actions.rs - Extended enum and execute logic
  • apps/raycast-extension/* - New Raycast extension (11 files)

Fixes #1540

Greptile Summary

This PR adds 5 new deep link actions to the Rust backend (PauseRecording, ResumeRecording, TogglePauseRecording, SwitchMicrophone, SwitchCamera) and introduces a new Raycast extension with 8 commands that communicate via the cap-desktop://action deep link protocol. There are three P1 issues that prevent the extension from working end-to-end:

  • Serde tag mismatch: DeepLinkAction is missing #[serde(tag = \"action\")]. The extension generates internally-tagged JSON ({\"action\":\"pause_recording\"}), but serde's default external tagging expects \"pause_recording\" — every deep link action will silently fail with a ParseFailed error.
  • Broken import path: All command files import from \"../deeplink\", but deeplink.ts is in the same src/ directory — this is a compilation error.
  • Wrong Raycast command mode: The 5 fire-and-forget commands return a bare <ActionPanel> but are declared as mode: \"view\" in package.json, which will render blank in Raycast.

Confidence Score: 2/5

Not safe to merge — three P1 issues collectively prevent the extension from compiling and all deep link actions from executing.

All three P1 issues are on the critical path: broken imports prevent building, wrong serde representation means zero actions reach the Rust handler, and wrong view mode means no Raycast UI renders. None of these are speculative.

apps/desktop/src-tauri/src/deeplink_actions.rs (serde tag), apps/raycast-extension/src/deeplink.ts (import paths, hardcoded stubs), apps/raycast-extension/package.json (command modes), and all 5 fire-and-forget command files.

Important Files Changed

Filename Overview
apps/desktop/src-tauri/src/deeplink_actions.rs Adds 5 new DeepLinkAction variants (Pause/Resume/Toggle/SwitchMic/SwitchCamera); missing #[serde(tag = "action")] means all deep links from the Raycast extension will fail to deserialize.
apps/raycast-extension/src/deeplink.ts Core deep link utility; generates internally-tagged JSON ({"action":...}) incompatible with the Rust enum's default serde representation; device-list helpers are hardcoded stubs.
apps/raycast-extension/src/stop-recording.tsx Returns bare <ActionPanel> as root component but is declared mode: "view" — will render blank in Raycast; same issue affects start/pause/resume/toggle-pause commands.
apps/raycast-extension/src/switch-microphone.tsx Imports getAvailableMicrophones but uses a hardcoded device list; import path "../deeplink" is incorrect (should be "./deeplink").
apps/raycast-extension/src/switch-camera.tsx Correctly structured List view but uses a hardcoded camera list; import path "../deeplink" is incorrect.
apps/raycast-extension/src/toggle-pause.tsx Returns bare <ActionPanel> (same view-mode bug); uses semantically wrong Icon.SwitchCamera icon.
apps/raycast-extension/package.json All 8 commands declared as mode: "view"; the 5 fire-and-forget commands should use mode: "no-view" to match their implementations.
apps/raycast-extension/src/open-settings.tsx Well-structured List view with sections for settings pages; import path issue aside, the logic is sound.
apps/raycast-extension/tsconfig.json Standard Raycast TypeScript config; no issues.

Sequence Diagram

sequenceDiagram
    participant User
    participant Raycast
    participant deeplink.ts
    participant OS
    participant CapDesktop as Cap Desktop (Tauri)
    participant Rust as deeplink_actions.rs

    User->>Raycast: Invoke command (e.g. Pause Recording)
    Raycast->>deeplink.ts: sendDeepLink("pause_recording")
    deeplink.ts->>deeplink.ts: JSON.stringify({action: "pause_recording"})
    deeplink.ts->>OS: exec(`open "cap-desktop://action?value="`)
    OS->>CapDesktop: Deep link event
    CapDesktop->>Rust: DeepLinkAction::try_from(&url)
    Rust->>Rust: serde_json::from_str(json_value)
    Note over Rust: Expects "pause_recording" string<br/>Got {"action":"pause_recording"}<br/>ParseFailed error
    Rust-->>CapDesktop: Err(ParseFailed)
    CapDesktop-->>User: No action taken (silent failure)
Loading

Comments Outside Diff (1)

  1. apps/desktop/src-tauri/src/deeplink_actions.rs, line 18-20 (link)

    P1 Missing #[serde(tag = "action")] causes all deep link actions to fail

    The DeepLinkAction enum uses serde's default externally-tagged representation. This means serde expects JSON like "pause_recording" (a bare string for unit variants) or {"switch_microphone":{"mic_label":"..."}} (wrapped struct). However, the Raycast extension (deeplink.ts) generates {"action":"pause_recording"} and {"action":"switch_microphone","mic_label":"..."} — which is the internally-tagged (#[serde(tag = "action")]) format. As-is, serde_json::from_str will return a ParseFailed error for every action sent from the extension.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: apps/desktop/src-tauri/src/deeplink_actions.rs
    Line: 18-20
    
    Comment:
    **Missing `#[serde(tag = "action")]` causes all deep link actions to fail**
    
    The `DeepLinkAction` enum uses serde's default **externally-tagged** representation. This means serde expects JSON like `"pause_recording"` (a bare string for unit variants) or `{"switch_microphone":{"mic_label":"..."}}` (wrapped struct). However, the Raycast extension (`deeplink.ts`) generates `{"action":"pause_recording"}` and `{"action":"switch_microphone","mic_label":"..."}` — which is the **internally-tagged** (`#[serde(tag = "action")]`) format. As-is, `serde_json::from_str` will return a `ParseFailed` error for every action sent from the extension.
    
    
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: apps/desktop/src-tauri/src/deeplink_actions.rs
Line: 18-20

Comment:
**Missing `#[serde(tag = "action")]` causes all deep link actions to fail**

The `DeepLinkAction` enum uses serde's default **externally-tagged** representation. This means serde expects JSON like `"pause_recording"` (a bare string for unit variants) or `{"switch_microphone":{"mic_label":"..."}}` (wrapped struct). However, the Raycast extension (`deeplink.ts`) generates `{"action":"pause_recording"}` and `{"action":"switch_microphone","mic_label":"..."}` — which is the **internally-tagged** (`#[serde(tag = "action")]`) format. As-is, `serde_json::from_str` will return a `ParseFailed` error for every action sent from the extension.

```suggestion
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "snake_case", tag = "action")]
pub enum DeepLinkAction {
```

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-extension/src/switch-microphone.tsx
Line: 2

Comment:
**Broken import path — compilation error across all commands**

All command files import `from "../deeplink"`, but `deeplink.ts` lives in the same `src/` directory. With `moduleResolution: "node"` (per `tsconfig.json`), `"../deeplink"` resolves to `apps/raycast-extension/deeplink.ts` — a file that does not exist. The correct relative import is `"./deeplink"`. The same broken path appears in every command file (`stop-recording.tsx`, `start-recording.tsx`, `pause-recording.tsx`, `resume-recording.tsx`, `toggle-pause.tsx`, `switch-camera.tsx`).

```suggestion
import { sendDeepLink, getAvailableMicrophones } from "./deeplink";
```

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-extension/src/stop-recording.tsx
Line: 27-31

Comment:
**`mode: "view"` commands cannot return a bare `<ActionPanel>`**

In Raycast, a command declared with `"mode": "view"` must return a top-level view component (`<Detail>`, `<List>`, `<Form>`, etc.) as its root — `<ActionPanel>` is only valid as a child of those views. Returning `<ActionPanel>` directly will cause Raycast to display a blank or broken screen. The same issue exists in `start-recording.tsx`, `pause-recording.tsx`, `resume-recording.tsx`, and `toggle-pause.tsx`.

For fire-and-forget commands that don't need a UI, change the `"mode"` to `"no-view"` in `package.json` and call `showHUD(...)` instead of `showToast(...)` (toasts require an active view).

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-extension/src/switch-microphone.tsx
Line: 8

Comment:
**Hardcoded device list — `getAvailableMicrophones` imported but unused**

The component iterates a hardcoded array `["Default", "MacBook Pro Microphone", "External Microphone"]` instead of calling the already-imported `getAvailableMicrophones()`. The same pattern exists in `switch-camera.tsx` (hardcoded list; `getAvailableCameras` isn't even imported). Additionally, both `getAvailableMicrophones` and `getAvailableCameras` in `deeplink.ts` are themselves hardcoded stubs rather than fetching real device info, so the underlying data needs to come from the desktop app via an IPC/query mechanism.

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-extension/src/toggle-pause.tsx
Line: 29

Comment:
**Wrong icon for toggle-pause action**

`Icon.SwitchCamera` is semantically incorrect for a pause-toggle action. A more appropriate icon would be `Icon.Pause`, `Icon.Play`, or `Icon.ArrowClockwise`.

```suggestion
      <Action title="Toggle Pause" icon={Icon.Pause} onAction={handleToggle} />
```

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

Reviews (1): Last reviewed commit: "feat: Add deep link actions for recordin..." | Re-trigger Greptile

Greptile also left 4 inline comments on this PR.

- Added PauseRecording, ResumeRecording, TogglePauseRecording actions
- Added SwitchMicrophone and SwitchCamera actions
- Created Raycast extension with 8 commands for full Cap control
- Extension uses cap-desktop:// deep link protocol
- Commands: start/stop/pause/resume/toggle recording, switch mic/camera, open settings
@@ -0,0 +1,47 @@
import { Action, ActionPanel, Icon, List, showToast, Toast } from "@raycast/api";
import { sendDeepLink, getAvailableMicrophones } from "../deeplink";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Broken import path — compilation error across all commands

All command files import from "../deeplink", but deeplink.ts lives in the same src/ directory. With moduleResolution: "node" (per tsconfig.json), "../deeplink" resolves to apps/raycast-extension/deeplink.ts — a file that does not exist. The correct relative import is "./deeplink". The same broken path appears in every command file (stop-recording.tsx, start-recording.tsx, pause-recording.tsx, resume-recording.tsx, toggle-pause.tsx, switch-camera.tsx).

Suggested change
import { sendDeepLink, getAvailableMicrophones } from "../deeplink";
import { sendDeepLink, getAvailableMicrophones } from "./deeplink";
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/raycast-extension/src/switch-microphone.tsx
Line: 2

Comment:
**Broken import path — compilation error across all commands**

All command files import `from "../deeplink"`, but `deeplink.ts` lives in the same `src/` directory. With `moduleResolution: "node"` (per `tsconfig.json`), `"../deeplink"` resolves to `apps/raycast-extension/deeplink.ts` — a file that does not exist. The correct relative import is `"./deeplink"`. The same broken path appears in every command file (`stop-recording.tsx`, `start-recording.tsx`, `pause-recording.tsx`, `resume-recording.tsx`, `toggle-pause.tsx`, `switch-camera.tsx`).

```suggestion
import { sendDeepLink, getAvailableMicrophones } from "./deeplink";
```

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

Comment on lines +27 to +31
return (
<ActionPanel>
<Action title="Stop Recording" icon={Icon.Stop} onAction={handleStop} />
</ActionPanel>
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 mode: "view" commands cannot return a bare <ActionPanel>

In Raycast, a command declared with "mode": "view" must return a top-level view component (<Detail>, <List>, <Form>, etc.) as its root — <ActionPanel> is only valid as a child of those views. Returning <ActionPanel> directly will cause Raycast to display a blank or broken screen. The same issue exists in start-recording.tsx, pause-recording.tsx, resume-recording.tsx, and toggle-pause.tsx.

For fire-and-forget commands that don't need a UI, change the "mode" to "no-view" in package.json and call showHUD(...) instead of showToast(...) (toasts require an active view).

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

Comment:
**`mode: "view"` commands cannot return a bare `<ActionPanel>`**

In Raycast, a command declared with `"mode": "view"` must return a top-level view component (`<Detail>`, `<List>`, `<Form>`, etc.) as its root — `<ActionPanel>` is only valid as a child of those views. Returning `<ActionPanel>` directly will cause Raycast to display a blank or broken screen. The same issue exists in `start-recording.tsx`, `pause-recording.tsx`, `resume-recording.tsx`, and `toggle-pause.tsx`.

For fire-and-forget commands that don't need a UI, change the `"mode"` to `"no-view"` in `package.json` and call `showHUD(...)` instead of `showToast(...)` (toasts require an active view).

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

return (
<List>
<List.Section title="Select Microphone">
{["Default", "MacBook Pro Microphone", "External Microphone"].map((label) => (
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Hardcoded device list — getAvailableMicrophones imported but unused

The component iterates a hardcoded array ["Default", "MacBook Pro Microphone", "External Microphone"] instead of calling the already-imported getAvailableMicrophones(). The same pattern exists in switch-camera.tsx (hardcoded list; getAvailableCameras isn't even imported). Additionally, both getAvailableMicrophones and getAvailableCameras in deeplink.ts are themselves hardcoded stubs rather than fetching real device info, so the underlying data needs to come from the desktop app via an IPC/query mechanism.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/raycast-extension/src/switch-microphone.tsx
Line: 8

Comment:
**Hardcoded device list — `getAvailableMicrophones` imported but unused**

The component iterates a hardcoded array `["Default", "MacBook Pro Microphone", "External Microphone"]` instead of calling the already-imported `getAvailableMicrophones()`. The same pattern exists in `switch-camera.tsx` (hardcoded list; `getAvailableCameras` isn't even imported). Additionally, both `getAvailableMicrophones` and `getAvailableCameras` in `deeplink.ts` are themselves hardcoded stubs rather than fetching real device info, so the underlying data needs to come from the desktop app via an IPC/query mechanism.

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


return (
<ActionPanel>
<Action title="Toggle Pause" icon={Icon.SwitchCamera} onAction={handleToggle} />
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Wrong icon for toggle-pause action

Icon.SwitchCamera is semantically incorrect for a pause-toggle action. A more appropriate icon would be Icon.Pause, Icon.Play, or Icon.ArrowClockwise.

Suggested change
<Action title="Toggle Pause" icon={Icon.SwitchCamera} onAction={handleToggle} />
<Action title="Toggle Pause" icon={Icon.Pause} onAction={handleToggle} />
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/raycast-extension/src/toggle-pause.tsx
Line: 29

Comment:
**Wrong icon for toggle-pause action**

`Icon.SwitchCamera` is semantically incorrect for a pause-toggle action. A more appropriate icon would be `Icon.Pause`, `Icon.Play`, or `Icon.ArrowClockwise`.

```suggestion
      <Action title="Toggle Pause" icon={Icon.Pause} onAction={handleToggle} />
```

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

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

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