Skip to content

feat: add recording control deeplinks and Raycast extension#1815

Open
camirian wants to merge 1 commit into
CapSoftware:mainfrom
camirian:feat/deeplinks-raycast
Open

feat: add recording control deeplinks and Raycast extension#1815
camirian wants to merge 1 commit into
CapSoftware:mainfrom
camirian:feat/deeplinks-raycast

Conversation

@camirian
Copy link
Copy Markdown

@camirian camirian commented May 14, 2026

This PR implements the requested deeplink actions (pause, resume, toggle, switch device) and adds a corresponding Raycast extension. Fixes #1540.

Greptile Summary

This PR wires up five new deeplink actions (PauseRecording, ResumeRecording, TogglePauseRecording, SwitchCamera, SwitchMic) in the Rust backend and ships a new Raycast extension that invokes them over the cap-desktop://action URL scheme.

  • Backend (deeplink_actions.rs): New enum variants and execute() arms delegate cleanly to the existing recording functions — no issues here.
  • Raycast utils.ts: triggerAction catches errors internally without re-throwing, so every caller unconditionally shows a success HUD even when the deeplink failed to open. The failure HUD is immediately overwritten by the success message.
  • start-recording.ts: capture_mode is hardcoded to { screen: "Main" }, which silently fails on any machine where the primary display is not named exactly "Main" (backend returns an error that is only logged to stderr).

Confidence Score: 3/5

The Rust backend additions are solid, but two defects in the Raycast layer mean that failures show a success message to the user and the start-recording command will silently do nothing on most machines.

The error-swallowing in triggerAction causes the success HUD to overwrite the failure HUD on every command — users get no actionable feedback when a deeplink fails. The hardcoded "Main" display name in start-recording.ts will silently break recording on any system whose primary display has a different name, with no user-visible error. Both are present defects on the current code path.

packages/raycast/src/utils.ts and packages/raycast/src/start-recording.ts need the most attention before this is published to the Raycast store.

Important Files Changed

Filename Overview
apps/desktop/src-tauri/src/deeplink_actions.rs Adds PauseRecording, ResumeRecording, TogglePauseRecording, SwitchCamera, and SwitchMic enum variants with matching execute() branches that delegate to existing recording functions — clean and consistent with the existing pattern.
packages/raycast/src/utils.ts triggerAction swallows errors internally without re-throwing, so all callers unconditionally display a success HUD even when the open() call failed — the failure HUD is immediately overwritten by the success HUD.
packages/raycast/src/start-recording.ts Hardcodes capture_mode to { screen: "Main" }, which fails silently on any system where the primary display isn't named exactly "Main".
packages/raycast/src/switch-camera.ts Always sends camera as device_id, ignoring model_id — the placeholder text says "Camera Name/ID" but model_id is silently unsupported; this is documented in a code comment.
packages/raycast/package.json New Raycast extension manifest; commands, arguments, and dependencies look correct.
packages/raycast/src/pause-recording.ts Correct deeplink key; affected by the utils.ts error-swallowing issue shared by all command files.
packages/raycast/src/resume-recording.ts Correct deeplink key; affected by the utils.ts error-swallowing issue shared by all command files.
packages/raycast/src/stop-recording.ts Correct deeplink key; affected by the utils.ts error-swallowing issue shared by all command files.
packages/raycast/src/toggle-pause.ts Correct deeplink key; affected by the utils.ts error-swallowing issue shared by all command files.
packages/raycast/src/switch-mic.ts Correctly passes mic_label through the deeplink URL.
Prompt To Fix All With AI
Fix the following 5 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 5
packages/raycast/src/utils.ts:3-12
Error is caught and swallowed here without re-throwing, so every caller unconditionally runs its success `showHUD` immediately after. When `open(url)` fails the user sees the failure HUD replaced by the success HUD — the failure is completely masked.

```suggestion
export async function triggerAction(action: any): Promise<boolean> {
  try {
    const json = JSON.stringify(action);
    const url = `cap-desktop://action?value=${encodeURIComponent(json)}`;
    await open(url);
    return true;
  } catch (error) {
    await showHUD("Failed to trigger Cap action");
    console.error(error);
    return false;
  }
}
```

### Issue 2 of 5
packages/raycast/src/pause-recording.ts:4-7
Because `triggerAction` silently swallows errors, `showHUD` here always runs — even when the deeplink failed to open. The same pattern appears in `resume-recording.ts`, `stop-recording.ts`, `toggle-pause.ts`, `switch-camera.ts`, and `switch-mic.ts`. Guard the success HUD on the return value of `triggerAction`.

```suggestion
export default async function Command() {
  const ok = await triggerAction("pause_recording");
  if (ok) await showHUD("Pausing Cap Recording");
}
```

### Issue 3 of 5
packages/raycast/src/start-recording.ts:6-14
The screen name `"Main"` is hardcoded, but the backend matches it with an exact string comparison against `list_displays()`. On systems where the primary display is named anything other than `"Main"` the action silently fails with no user-visible feedback — the deeplink error is only logged to stderr in the desktop app.

### Issue 4 of 5
packages/raycast/src/switch-camera.ts:10-11
**Placeholder misleads users about model ID support.** The `package.json` argument placeholder says `"Camera Name/ID"`, implying a human-readable name or model ID is accepted, but this command always wraps the value as `{ device_id: camera }`. Passing a model ID or display name will send it to the backend as a device ID and silently fail to match any camera. Either restrict the placeholder to `"Device ID"` or branch on the input to support both `device_id` and `model_id`.

### Issue 5 of 5
packages/raycast/src/utils.ts:3
**Loose `any` type removes compile-time safety.** Using `any` for `action` means malformed payloads (e.g., `switch_mic` with a typo in the key) pass TypeScript checking and only fail silently at runtime. A typed union matching the backend's `DeepLinkAction` variants would catch these mistakes before shipping.

Reviews (1): Last reviewed commit: "feat: add recording control deeplinks an..." | Re-trigger Greptile

Greptile also left 5 inline comments on this PR.

@superagent-security superagent-security Bot added contributor:verified Contributor passed trust analysis. pr:flagged PR flagged for review by security analysis. labels May 14, 2026
Copy link
Copy Markdown

@superagent-security superagent-security Bot left a comment

Choose a reason for hiding this comment

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

Superagent found 1 security concern(s).

"dev": "ray dev",
"fix-lint": "ray lint --fix",
"lint": "ray lint",
"publish": "npx @raycast/api@latest publish"
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[LOW] Publish script executes unpinned package via npx @latest

Publish script runs npx @raycast/api@latest, fetching mutable code during release.

Fix: Use the local lockfile-resolved CLI or pin an exact @raycast/api version instead of @latest.

Comment on lines +3 to +12
export async function triggerAction(action: any) {
try {
const json = JSON.stringify(action);
const url = `cap-desktop://action?value=${encodeURIComponent(json)}`;
await open(url);
} catch (error) {
await showHUD("Failed to trigger Cap action");
console.error(error);
}
}
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 Error is caught and swallowed here without re-throwing, so every caller unconditionally runs its success showHUD immediately after. When open(url) fails the user sees the failure HUD replaced by the success HUD — the failure is completely masked.

Suggested change
export async function triggerAction(action: any) {
try {
const json = JSON.stringify(action);
const url = `cap-desktop://action?value=${encodeURIComponent(json)}`;
await open(url);
} catch (error) {
await showHUD("Failed to trigger Cap action");
console.error(error);
}
}
export async function triggerAction(action: any): Promise<boolean> {
try {
const json = JSON.stringify(action);
const url = `cap-desktop://action?value=${encodeURIComponent(json)}`;
await open(url);
return true;
} catch (error) {
await showHUD("Failed to trigger Cap action");
console.error(error);
return false;
}
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/raycast/src/utils.ts
Line: 3-12

Comment:
Error is caught and swallowed here without re-throwing, so every caller unconditionally runs its success `showHUD` immediately after. When `open(url)` fails the user sees the failure HUD replaced by the success HUD — the failure is completely masked.

```suggestion
export async function triggerAction(action: any): Promise<boolean> {
  try {
    const json = JSON.stringify(action);
    const url = `cap-desktop://action?value=${encodeURIComponent(json)}`;
    await open(url);
    return true;
  } catch (error) {
    await showHUD("Failed to trigger Cap action");
    console.error(error);
    return false;
  }
}
```

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

Comment on lines +4 to +7
export default async function Command() {
await triggerAction("pause_recording");
await showHUD("Pausing Cap Recording");
}
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 Because triggerAction silently swallows errors, showHUD here always runs — even when the deeplink failed to open. The same pattern appears in resume-recording.ts, stop-recording.ts, toggle-pause.ts, switch-camera.ts, and switch-mic.ts. Guard the success HUD on the return value of triggerAction.

Suggested change
export default async function Command() {
await triggerAction("pause_recording");
await showHUD("Pausing Cap Recording");
}
export default async function Command() {
const ok = await triggerAction("pause_recording");
if (ok) await showHUD("Pausing Cap Recording");
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/raycast/src/pause-recording.ts
Line: 4-7

Comment:
Because `triggerAction` silently swallows errors, `showHUD` here always runs — even when the deeplink failed to open. The same pattern appears in `resume-recording.ts`, `stop-recording.ts`, `toggle-pause.ts`, `switch-camera.ts`, and `switch-mic.ts`. Guard the success HUD on the return value of `triggerAction`.

```suggestion
export default async function Command() {
  const ok = await triggerAction("pause_recording");
  if (ok) await showHUD("Pausing Cap Recording");
}
```

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

Comment on lines +6 to +14
await triggerAction({
start_recording: {
capture_mode: { screen: "Main" },
camera: null,
mic_label: null,
capture_system_audio: true,
mode: "instant"
}
});
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 The screen name "Main" is hardcoded, but the backend matches it with an exact string comparison against list_displays(). On systems where the primary display is named anything other than "Main" the action silently fails with no user-visible feedback — the deeplink error is only logged to stderr in the desktop app.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/raycast/src/start-recording.ts
Line: 6-14

Comment:
The screen name `"Main"` is hardcoded, but the backend matches it with an exact string comparison against `list_displays()`. On systems where the primary display is named anything other than `"Main"` the action silently fails with no user-visible feedback — the deeplink error is only logged to stderr in the desktop app.

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

Comment on lines +10 to +11
// We assume it's a DeviceID for simplicity in this Raycast command
await triggerAction({ switch_camera: { camera: { device_id: camera } } });
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 Placeholder misleads users about model ID support. The package.json argument placeholder says "Camera Name/ID", implying a human-readable name or model ID is accepted, but this command always wraps the value as { device_id: camera }. Passing a model ID or display name will send it to the backend as a device ID and silently fail to match any camera. Either restrict the placeholder to "Device ID" or branch on the input to support both device_id and model_id.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/raycast/src/switch-camera.ts
Line: 10-11

Comment:
**Placeholder misleads users about model ID support.** The `package.json` argument placeholder says `"Camera Name/ID"`, implying a human-readable name or model ID is accepted, but this command always wraps the value as `{ device_id: camera }`. Passing a model ID or display name will send it to the backend as a device ID and silently fail to match any camera. Either restrict the placeholder to `"Device ID"` or branch on the input to support both `device_id` and `model_id`.

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

@@ -0,0 +1,12 @@
import { open, showHUD } from "@raycast/api";

export async function triggerAction(action: any) {
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 Loose any type removes compile-time safety. Using any for action means malformed payloads (e.g., switch_mic with a typo in the key) pass TypeScript checking and only fail silently at runtime. A typed union matching the backend's DeepLinkAction variants would catch these mistakes before shipping.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/raycast/src/utils.ts
Line: 3

Comment:
**Loose `any` type removes compile-time safety.** Using `any` for `action` means malformed payloads (e.g., `switch_mic` with a typo in the key) pass TypeScript checking and only fail silently at runtime. A typed union matching the backend's `DeepLinkAction` variants would catch these mistakes before shipping.

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

@camirian
Copy link
Copy Markdown
Author

/attempt #1540

@camirian
Copy link
Copy Markdown
Author

/attempt

@camirian
Copy link
Copy Markdown
Author

/claim #1540

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

Labels

contributor:verified Contributor passed trust analysis. pr:flagged PR flagged for review by security analysis.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bounty: Deeplinks support + Raycast Extension

1 participant