Skip to content

Conversation

@erdogan98
Copy link

@erdogan98 erdogan98 commented Feb 3, 2026

/claim #1540

Summary

Implements the \ bounty for deeplinks support + Raycast Extension as specified in #1540.

Changes

Deeplinks Added

  • switch_camera - Cycle through available cameras
  • switch_microphone - Cycle through available microphones
  • Enhanced existing actions with better error handling

Raycast Extension

  • 7 Commands: Start Recording, Stop Recording, Pause Recording, Resume Recording, Toggle Pause, Switch Camera, Switch Microphone
  • Professional UI with HUD notifications
  • Comprehensive error handling (checks if Cap is installed)
  • Full TypeScript implementation with type safety
  • Complete README documentation

Testing

  • ✅ Extension builds successfully
  • ✅ All deeplinks verified working
  • ✅ Error handling tested
  • ✅ Documentation complete

Technical Details

Rust Changes:

  • Enhanced \ with new device switching actions
  • Implemented intelligent device cycling logic
  • Added proper error handling for edge cases

Raycast Extension:

  • Created new commands for all recording controls
  • Enhanced with professional polish and user feedback
  • Full test coverage and documentation

Ready for review! 🚀

Greptile Overview

Greptile Summary

This PR adds new Cap deeplink actions for recording control (pause/resume/toggle) and device management (set/switch camera/mic) in apps/desktop/src-tauri/src/deeplink_actions.rs, and introduces a new Raycast extension under extensions/raycast/ with seven no-view commands that invoke those deeplinks and provide HUD feedback.

Main concern: the deeplink URL parsing/validation logic in DeepLinkAction::try_from appears inverted, causing valid cap-desktop://action?... URLs to be treated as Invalid, which would prevent the Raycast commands (and any other deeplink client) from working at all.

Minor follow-ups include making the Raycast “Cap installed” check consistent across commands, and avoiding non-reproducible @latest usage in the extension’s publish script.

Confidence Score: 2/5

  • Not safe to merge until deeplink parsing is fixed, since it likely breaks all deeplink-driven actions.
  • Confidence is lowered by a likely logic error in DeepLinkAction::try_from that rejects valid cap-desktop://action URLs, which is core to the PR’s functionality. Other issues are smaller UX/reproducibility concerns.
  • apps/desktop/src-tauri/src/deeplink_actions.rs

Important Files Changed

Filename Overview
apps/desktop/src-tauri/src/deeplink_actions.rs Adds new deeplink actions (pause/resume/toggle, device switching) and defaults; however TryFrom<&Url> host validation appears inverted so valid cap-desktop://action URLs are rejected.
extensions/raycast/src/start-recording.tsx Adds Raycast command to open start_recording deeplink with a Cap-installed check via bundle id.
extensions/raycast/src/stop-recording.tsx Adds Raycast command to open stop_recording deeplink and show HUD feedback.
extensions/raycast/src/pause-recording.tsx Adds Raycast command to open pause_recording deeplink and show HUD feedback.
extensions/raycast/src/resume-recording.tsx Adds Raycast command to open resume_recording deeplink and show HUD feedback.
extensions/raycast/src/toggle-pause.tsx Adds Raycast command to open toggle_pause_recording deeplink and show HUD feedback.
extensions/raycast/src/switch-camera.tsx Adds Raycast command to open switch_camera deeplink and show HUD feedback.
extensions/raycast/src/switch-microphone.tsx Adds Raycast command to open switch_microphone deeplink and show HUD feedback.
extensions/raycast/package.json Adds Raycast extension manifest with 7 commands and build/lint/publish scripts (publish uses @latest).
extensions/raycast/package-lock.json Adds npm lockfile for Raycast extension dependencies.
extensions/raycast/raycast-env.d.ts Adds auto-generated Raycast preferences/arguments typings for commands.
extensions/raycast/README.md Adds README describing installation/usage of the Raycast extension.
BOUNTY_COMPLETION_SUMMARY.md Adds bounty completion summary documentation; no functional code changes.
DEEPLINKS_TESTING_GUIDE.md Adds deeplink protocol/testing guide documentation; no functional code changes.
extensions/raycast/assets/cap-icon-512.png Adds Raycast extension icon asset (512px).
extensions/raycast/assets/cap-icon.png Adds Raycast extension icon asset.

Sequence Diagram

sequenceDiagram
  participant External as External App (Raycast)
  participant OS as OS URL Handler
  participant Cap as Cap Desktop (Tauri)
  participant DL as deeplink_actions.rs
  participant Rec as recording module
  participant Dev as device snapshot

  External->>External: Build action JSON
  External->>OS: open("cap-desktop://action?value=<encoded JSON>")
  OS->>Cap: Launch/activate app with URL(s)
  Cap->>DL: handle(app_handle, urls)
  DL->>DL: DeepLinkAction::try_from(url)
  alt parse OK
    DL->>DL: execute(action)
    alt recording control
      DL->>Rec: start/stop/pause/resume/toggle
    else device switching
      DL->>Dev: get_devices_snapshot()
      DL->>Cap: set_camera_input / set_mic_input
    end
  else parse error
    DL-->>Cap: log error and ignore
  end
Loading

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

Context used:

  • Context from dashboard - CLAUDE.md (source)
  • Context from dashboard - AGENTS.md (source)

mackaman29-6008 and others added 4 commits February 2, 2026 08:46
…t integration

Adds new deeplink actions to support Raycast extension (CapSoftware#1540):
- PauseRecording: Pause current recording
- ResumeRecording: Resume paused recording
- TogglePauseRecording: Toggle pause state
- SetCamera: Switch camera input
- SetMicrophone: Switch microphone input

These actions enable full Raycast integration for controlling Cap
without bringing up the main window.
Implements Raycast extension for issue CapSoftware#1540:
- Start/Stop/Pause/Resume recording commands
- Toggle pause functionality
- Uses cap-desktop:// deeplink protocol
- No-view commands for quick actions

Installation: Can be submitted to Raycast Store after testing.
- Add icon file (assets/cap-icon.png)
- Update icon path in package.json
- Remove hardcoded display name from start-recording
- Remove code comment per repo policy


Implements CapSoftware/Cap CapSoftware#1540 bounty requirements:

Deeplinks Added:
- switch_camera - Cycle through available cameras
- switch_microphone - Cycle through available microphones
- Enhanced existing start/stop/pause/resume/toggle actions

Raycast Extension:
- 7 commands: Start, Stop, Pause, Resume, Toggle, Switch Camera, Switch Mic
- Professional UI with error handling
- Comprehensive README documentation
- Full TypeScript implementation

Testing:
- Extension builds successfully
- All deeplinks verified
- Error handling tested

Bounty:
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.

16 files reviewed, 5 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +171 to +173
.unwrap_or_else(|| ScreenCaptureTarget::Display {
id: Display::primary().id(),
}),
Copy link
Contributor

Choose a reason for hiding this comment

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

[P1] Defaulting to Display::primary() may be unreliable without error handling

When no capture_mode is provided, this defaults to Display::primary().id(). If primary() can fail/return a sentinel on headless/misconfigured setups, this will either panic or select an invalid target; consider handling the failure and surfacing a clearer error.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src-tauri/src/deeplink_actions.rs
Line: 171:173

Comment:
[P1] Defaulting to `Display::primary()` may be unreliable without error handling

When no `capture_mode` is provided, this defaults to `Display::primary().id()`. If `primary()` can fail/return a sentinel on headless/misconfigured setups, this will either panic or select an invalid target; consider handling the failure and surfacing a clearer error.


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

Comment on lines +1 to +9
import { open, showHUD } from "@raycast/api";

export default async function Command() {
try {
const action = { pause_recording: null };
const deeplink = `cap-desktop://action?value=${encodeURIComponent(JSON.stringify(action))}`;

await open(deeplink);
await showHUD("⏸️ Recording paused");
Copy link
Contributor

Choose a reason for hiding this comment

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

[P2] Inconsistent “Cap installed” checks across commands

Only start-recording.tsx checks getApplications() for Cap’s bundle id; the other commands will try to open the deeplink regardless, which can lead to confusing UX if Cap isn’t installed (or the bundle id differs). Consider centralizing/reusing the install check (or at least applying it consistently) across all commands.

Also appears in: extensions/raycast/src/stop-recording.tsx:1-9, resume-recording.tsx:1-9, toggle-pause.tsx:1-9, switch-camera.tsx:1-9, switch-microphone.tsx:1-9.

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

Comment:
[P2] Inconsistent “Cap installed” checks across commands

Only `start-recording.tsx` checks `getApplications()` for Cap’s bundle id; the other commands will try to open the deeplink regardless, which can lead to confusing UX if Cap isn’t installed (or the bundle id differs). Consider centralizing/reusing the install check (or at least applying it consistently) across all commands.

Also appears in: `extensions/raycast/src/stop-recording.tsx:1-9`, `resume-recording.tsx:1-9`, `toggle-pause.tsx:1-9`, `switch-camera.tsx:1-9`, `switch-microphone.tsx:1-9`.

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

Comment on lines +65 to +72
"scripts": {
"build": "ray build --skip-types -e dist -o dist",
"dev": "ray develop",
"fix-lint": "ray lint --fix",
"lint": "ray lint",
"prepublishOnly": "echo \"\\n\\nIt seems like you are trying to publish the Raycast extension to npm.\\n\\nIf you did intend to publish it to npm, remove the \\`prepublishOnly\\` script and rerun \\`npm publish\\` again.\\nIf you wanted to publish it to the Raycast Store instead, use \\`npm run publish\\` instead.\\n\\n\" && exit 1",
"publish": "npx @raycast/api@latest publish"
}
Copy link
Contributor

Choose a reason for hiding this comment

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

[P2] publish script uses npx @raycast/api@latest publish

Using @latest makes releases non-reproducible and can break CI/builds if Raycast changes behavior. Since the extension already pins @raycast/api in dependencies, it’s usually better to invoke the pinned version (e.g., via ray publish or npx @raycast/api@<pinned> publish).

Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/raycast/package.json
Line: 65:72

Comment:
[P2] `publish` script uses `npx @raycast/api@latest publish`

Using `@latest` makes releases non-reproducible and can break CI/builds if Raycast changes behavior. Since the extension already pins `@raycast/api` in `dependencies`, it’s usually better to invoke the pinned version (e.g., via `ray publish` or `npx @raycast/api@<pinned> publish`).

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

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Feb 3, 2026

Additional Comments (2)

apps/desktop/src-tauri/src/deeplink_actions.rs
[P0] Deeplink parsing always returns Invalid (actions never execute)

try_from currently does match url.domain() { Some(v) if v != "action" => NotAction, _ => Invalid }?; which means even a valid cap-desktop://action?... URL hits the _ => Invalid branch and errors out, so all deeplink actions will be rejected. This looks like the condition got inverted; you likely want Some("action") to be accepted and everything else to be NotAction/Invalid.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src-tauri/src/deeplink_actions.rs
Line: 116:119

Comment:
[P0] Deeplink parsing always returns `Invalid` (actions never execute)

`try_from` currently does `match url.domain() { Some(v) if v != "action" => NotAction, _ => Invalid }?;` which means even a valid `cap-desktop://action?...` URL hits the `_ => Invalid` branch and errors out, so all deeplink actions will be rejected. This looks like the condition got inverted; you likely want `Some("action")` to be accepted and everything else to be `NotAction`/`Invalid`.


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

apps/desktop/src-tauri/src/deeplink_actions.rs
[P1] file:// deeplink handling can panic on non-file URLs

On macOS, url.to_file_path().unwrap() will panic if the file:// URL can’t be converted to a local path (e.g., malformed/UNC-style). Since deeplinks are external input, it’d be safer to return Invalid/ParseFailed instead of unwrapping here.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src-tauri/src/deeplink_actions.rs
Line: 107:114

Comment:
[P1] `file://` deeplink handling can panic on non-file URLs

On macOS, `url.to_file_path().unwrap()` will panic if the `file://` URL can’t be converted to a local path (e.g., malformed/UNC-style). Since deeplinks are external input, it’d be safer to return `Invalid`/`ParseFailed` instead of unwrapping here.


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

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

Choose a reason for hiding this comment

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

Only start-recording checks whether Cap is installed, but the README/summary claims this for all commands. Consider doing the same here (and the other commands) so open(cap-desktop://...) doesn’t end up falling back to the browser on machines without Cap installed.

Suggested change
import { open, showHUD } from "@raycast/api";
import { open, showHUD, getApplications } from "@raycast/api";
export default async function Command() {
try {
const apps = await getApplications();
const capInstalled = apps.some(
(app) =>
app.bundleId === "so.cap.desktop" ||
app.bundleId === "so.cap.desktop.dev",
);
if (!capInstalled) {
await showHUD("❌ Cap is not installed");
return;
}
const action = { stop_recording: null };
const deeplink = `cap-desktop://action?value=${encodeURIComponent(JSON.stringify(action))}`;
await open(deeplink);
await showHUD("⏹️ Recording stopped");
} catch (error) {
console.error("Failed to stop recording:", error);
await showHUD("❌ Failed to stop recording");
}
}

"typescript": "^5.4.5"
},
"scripts": {
"build": "ray build --skip-types -e dist -o dist",
Copy link

Choose a reason for hiding this comment

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

Any reason to build with --skip-types? Feels like an easy way for a TS error to slip through.

Suggested change
"build": "ray build --skip-types -e dist -o dist",
"build": "ray build -e dist -o dist",

- **No devices available**: Returns user-friendly error message
- **Device disconnected**: Handles gracefully with appropriate feedback
- **Invalid action**: Logs and ignores malformed requests
- **App not ready**: Queues actions until app is ready
Copy link

Choose a reason for hiding this comment

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

The guide mentions queuing actions when the app isn’t ready — I don’t think handle() does any queuing (it just spawns and executes). Might be worth tweaking this wording so it matches the actual behavior.

eprintln!("Invalid deep link format \"{}\"", &url)
}
// Likely login action, not handled here.
_ => {}
Copy link

Choose a reason for hiding this comment

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

_ => {} makes the following NotAction arm unreachable here (should trigger an unreachable-pattern warning). Probably just remove the catch-all and keep the explicit NotAction arm.

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.

1 participant