Skip to content

feat: deeplinks support + Raycast extension#1734

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

feat: deeplinks support + Raycast extension#1734
zhuanxqq wants to merge 1 commit intoCapSoftware:mainfrom
zhuanxqq:feat/deeplinks-raycast

Conversation

@zhuanxqq
Copy link
Copy Markdown

@zhuanxqq zhuanxqq commented Apr 14, 2026

Closes #1540

This PR adds new deeplink actions for recording controls and includes a Raycast extension.

Changes:

  • Added /PauseRecording/ deeplink action
  • Added /ResumeRecording/ deeplink action
  • Added /SwitchMicrophone { mic_label }/ deeplink action
  • Added /SwitchCamera { camera }/ deeplink action
  • Created /extensions/raycast/ with 6 commands (start/stop/pause/resume/switch-mic/switch-camera)

/claim #1540

Greptile Summary

This PR extends the Tauri deeplink system with pause/resume recording and microphone/camera switching actions, and ships a Raycast extension that triggers them. The Rust side is correct, but all six Raycast commands will silently fail at runtime due to serde serialisation mismatches.

  • Every command uses PascalCase variant names (e.g. StopRecording) but DeepLinkAction has #[serde(rename_all = "snake_case")], so JSON keys must be snake_case (stop_recording, pause_recording, etc.).
  • start-recording.tsx additionally mismatches CaptureMode (Screenscreen) and RecordingMode (Studiostudio), and passes an empty display name that will never match a real screen.
  • switch-camera.tsx passes the camera as a plain string, but the Rust field type is DeviceOrModelID, an externally-tagged enum that expects { "DeviceID": "..." } or { "ModelID": ... }.

Confidence Score: 3/5

  • All six Raycast commands are broken due to serde rename mismatches and one type error; the deeplink additions on the Rust side are safe.
  • Every Raycast command has at least one P1 defect that prevents it from functioning. The serde rename_all mismatch means no deeplink will deserialise correctly, and the switch-camera command has an additional type mismatch. These must be fixed before the extension is usable.
  • All files under extensions/raycast/src/ — particularly start-recording.tsx and switch-camera.tsx which have multiple layered issues.

Important Files Changed

Filename Overview
apps/desktop/src-tauri/src/deeplink_actions.rs Adds PauseRecording, ResumeRecording, SwitchMicrophone, and SwitchCamera deeplink actions; Rust side looks correct and delegates cleanly to existing recording functions.
extensions/raycast/src/start-recording.tsx Three serde rename mismatches (StartRecording→start_recording, Screen→screen, Studio→studio) and hardcoded empty display name will cause this command to always fail at parse time.
extensions/raycast/src/stop-recording.tsx Uses PascalCase variant name StopRecording; must be stop_recording to match DeepLinkAction's rename_all = "snake_case".
extensions/raycast/src/pause-recording.tsx Uses PascalCase variant name PauseRecording; must be pause_recording.
extensions/raycast/src/resume-recording.tsx Uses PascalCase variant name ResumeRecording; must be resume_recording.
extensions/raycast/src/switch-microphone.tsx Uses PascalCase variant name SwitchMicrophone; must be switch_microphone.
extensions/raycast/src/switch-camera.tsx Wrong variant name (SwitchCamera→switch_camera) and camera passed as a bare string instead of DeviceOrModelID tagged object; both cause deserialization failure.
extensions/raycast/package.json Standard Raycast extension manifest declaring six commands with correct modes and argument definitions.
biome.json Adds a formatter override for extensions/raycast using 2-space indentation; appropriate for the Raycast extension.

Sequence Diagram

sequenceDiagram
    participant User
    participant Raycast
    participant macOS
    participant TauriApp as Cap (Tauri)
    participant Rust as deeplink_actions.rs
    participant Recording as recording.rs

    User->>Raycast: Trigger command (e.g. pause-recording)
    Raycast->>Raycast: "JSON.stringify({ pause_recording: {} })"
    Raycast->>macOS: "open("cap://action?value=...")"
    macOS->>TauriApp: Deep link URL
    TauriApp->>Rust: handle(urls)
    Rust->>Rust: DeepLinkAction::try_from(url)
    Rust->>Rust: serde_json::from_str(json_value)
    Rust->>Recording: pause_recording / resume_recording / set_mic_input / set_camera_input
    Recording-->>TauriApp: Ok(()) or Err(String)
Loading
Prompt To Fix All With AI
This is a comment left during a code review.
Path: extensions/raycast/src/stop-recording.tsx
Line: 4

Comment:
**Wrong serde variant names across all Raycast commands**

`DeepLinkAction` is declared with `#[serde(rename_all = "snake_case")]`, so every variant name is serialised as `snake_case` in JSON. All six Raycast commands use the original `PascalCase` names, which serde will never match, so every deeplink invocation silently fails with a parse error.

Same problem in `pause-recording.tsx` (`PauseRecording`), `resume-recording.tsx` (`ResumeRecording`), `switch-microphone.tsx` (`SwitchMicrophone`), `switch-camera.tsx` (`SwitchCamera`), and `start-recording.tsx` (`StartRecording`).

```suggestion
  const value = JSON.stringify({ stop_recording: {} });
```

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

---

This is a comment left during a code review.
Path: extensions/raycast/src/switch-camera.tsx
Line: 10-15

Comment:
**`camera` must be a `DeviceOrModelID` tagged object, not a bare string**

The Rust field type is `DeviceOrModelID`, an enum serialised with serde's default external-tag format. Passing a plain string like `"My Camera"` will fail to deserialise. The correct payload for a device ID is `{ DeviceID: "..." }` (or `{ ModelID: "..." }` for model IDs).

```suggestion
  const value = JSON.stringify({
    switch_camera: {
      camera: { DeviceID: props.arguments.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: extensions/raycast/src/start-recording.tsx
Line: 3-13

Comment:
**Nested serde renames also use the wrong case**

Beyond the outer variant name (`StartRecording``start_recording`), two inner types have their own `rename_all` rules:

- `CaptureMode` has `rename_all = "snake_case"``Screen` must be `screen`
- `RecordingMode` has `rename_all = "camelCase"``Studio` must be `studio`

All three mismatches together mean this command will always fail to parse.

```suggestion
export default async function Command() {
  const value = JSON.stringify({
    start_recording: {
      capture_mode: { screen: "" },
      camera: null,
      mic_label: null,
      capture_system_audio: false,
      mode: "studio",
    },
  });
  await open(`cap://action?value=${encodeURIComponent(value)}`);
}
```

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

---

This is a comment left during a code review.
Path: extensions/raycast/src/start-recording.tsx
Line: 5-8

Comment:
**Hardcoded screen/mode with no user control**

`capture_mode` is hard-coded to `Screen` with an empty display name (`""`), and `mode` is hard-coded to `"Studio"`. An empty display name causes the Rust handler to search `list_displays()` for a display whose `name == ""`, which will not match any real display and returns an error. The `start-recording` command should either accept arguments for these fields or use a sensible default like the primary display name.

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

Reviews (1): Last reviewed commit: "feat: add pause/resume/switch deeplinks ..." | Re-trigger Greptile

Greptile also left 4 inline comments on this PR.

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

import { open } from "@raycast/api";

export default async function Command() {
const value = JSON.stringify({ StopRecording: {} });
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 Wrong serde variant names across all Raycast commands

DeepLinkAction is declared with #[serde(rename_all = "snake_case")], so every variant name is serialised as snake_case in JSON. All six Raycast commands use the original PascalCase names, which serde will never match, so every deeplink invocation silently fails with a parse error.

Same problem in pause-recording.tsx (PauseRecording), resume-recording.tsx (ResumeRecording), switch-microphone.tsx (SwitchMicrophone), switch-camera.tsx (SwitchCamera), and start-recording.tsx (StartRecording).

Suggested change
const value = JSON.stringify({ StopRecording: {} });
const value = JSON.stringify({ stop_recording: {} });
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/raycast/src/stop-recording.tsx
Line: 4

Comment:
**Wrong serde variant names across all Raycast commands**

`DeepLinkAction` is declared with `#[serde(rename_all = "snake_case")]`, so every variant name is serialised as `snake_case` in JSON. All six Raycast commands use the original `PascalCase` names, which serde will never match, so every deeplink invocation silently fails with a parse error.

Same problem in `pause-recording.tsx` (`PauseRecording`), `resume-recording.tsx` (`ResumeRecording`), `switch-microphone.tsx` (`SwitchMicrophone`), `switch-camera.tsx` (`SwitchCamera`), and `start-recording.tsx` (`StartRecording`).

```suggestion
  const value = JSON.stringify({ stop_recording: {} });
```

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

Comment on lines +10 to +15
const value = JSON.stringify({
SwitchCamera: {
camera: props.arguments.camera,
},
});
await open(`cap://action?value=${encodeURIComponent(value)}`);
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 camera must be a DeviceOrModelID tagged object, not a bare string

The Rust field type is DeviceOrModelID, an enum serialised with serde's default external-tag format. Passing a plain string like "My Camera" will fail to deserialise. The correct payload for a device ID is { DeviceID: "..." } (or { ModelID: "..." } for model IDs).

Suggested change
const value = JSON.stringify({
SwitchCamera: {
camera: props.arguments.camera,
},
});
await open(`cap://action?value=${encodeURIComponent(value)}`);
const value = JSON.stringify({
switch_camera: {
camera: { DeviceID: props.arguments.camera },
},
});
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/raycast/src/switch-camera.tsx
Line: 10-15

Comment:
**`camera` must be a `DeviceOrModelID` tagged object, not a bare string**

The Rust field type is `DeviceOrModelID`, an enum serialised with serde's default external-tag format. Passing a plain string like `"My Camera"` will fail to deserialise. The correct payload for a device ID is `{ DeviceID: "..." }` (or `{ ModelID: "..." }` for model IDs).

```suggestion
  const value = JSON.stringify({
    switch_camera: {
      camera: { DeviceID: props.arguments.camera },
    },
  });
```

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

Comment on lines +3 to +13
export default async function Command() {
const value = JSON.stringify({
StartRecording: {
capture_mode: { Screen: "" },
camera: null,
mic_label: null,
capture_system_audio: false,
mode: "Studio",
},
});
await open(`cap://action?value=${encodeURIComponent(value)}`);
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 Nested serde renames also use the wrong case

Beyond the outer variant name (StartRecordingstart_recording), two inner types have their own rename_all rules:

  • CaptureMode has rename_all = "snake_case"Screen must be screen
  • RecordingMode has rename_all = "camelCase"Studio must be studio

All three mismatches together mean this command will always fail to parse.

Suggested change
export default async function Command() {
const value = JSON.stringify({
StartRecording: {
capture_mode: { Screen: "" },
camera: null,
mic_label: null,
capture_system_audio: false,
mode: "Studio",
},
});
await open(`cap://action?value=${encodeURIComponent(value)}`);
export default async function Command() {
const value = JSON.stringify({
start_recording: {
capture_mode: { screen: "" },
camera: null,
mic_label: null,
capture_system_audio: false,
mode: "studio",
},
});
await open(`cap://action?value=${encodeURIComponent(value)}`);
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/raycast/src/start-recording.tsx
Line: 3-13

Comment:
**Nested serde renames also use the wrong case**

Beyond the outer variant name (`StartRecording``start_recording`), two inner types have their own `rename_all` rules:

- `CaptureMode` has `rename_all = "snake_case"``Screen` must be `screen`
- `RecordingMode` has `rename_all = "camelCase"``Studio` must be `studio`

All three mismatches together mean this command will always fail to parse.

```suggestion
export default async function Command() {
  const value = JSON.stringify({
    start_recording: {
      capture_mode: { screen: "" },
      camera: null,
      mic_label: null,
      capture_system_audio: false,
      mode: "studio",
    },
  });
  await open(`cap://action?value=${encodeURIComponent(value)}`);
}
```

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

Comment on lines +5 to +8
StartRecording: {
capture_mode: { Screen: "" },
camera: null,
mic_label: null,
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 Hardcoded screen/mode with no user control

capture_mode is hard-coded to Screen with an empty display name (""), and mode is hard-coded to "Studio". An empty display name causes the Rust handler to search list_displays() for a display whose name == "", which will not match any real display and returns an error. The start-recording command should either accept arguments for these fields or use a sensible default like the primary display name.

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

Comment:
**Hardcoded screen/mode with no user control**

`capture_mode` is hard-coded to `Screen` with an empty display name (`""`), and `mode` is hard-coded to `"Studio"`. An empty display name causes the Rust handler to search `list_displays()` for a display whose `name == ""`, which will not match any real display and returns an error. The `start-recording` command should either accept arguments for these fields or use a sensible default like the primary display name.

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