Skip to content

Conversation

@njg7194
Copy link

@njg7194 njg7194 commented Feb 1, 2026

Deeplinks Support + Raycast Extension

Closes #1540

Summary

This PR adds comprehensive deeplink support for Cap and a complete Raycast extension to control Cap directly from Raycast.

Changes

1. Extended Deeplink Actions (apps/desktop/src-tauri/src/deeplink_actions.rs)

Added the following new deeplink actions:

Action Description
pause_recording Pause the current recording
resume_recording Resume a paused recording
toggle_pause Toggle pause state of current recording
set_microphone Switch microphone input
set_camera Switch camera input
take_screenshot Take a screenshot of primary display
show_main_window Show the main Cap window
list_displays List available displays
list_windows List available windows
list_microphones List available microphones
list_cameras List available cameras
get_recording_status Get current recording status

2. Raycast Extension (extensions/raycast-cap/)

A complete Raycast extension with the following commands:

  • Start Recording - Start a new screen recording
  • Stop Recording - Stop the current recording
  • Pause Recording - Pause the current recording
  • Resume Recording - Resume a paused recording
  • Toggle Pause - Toggle pause state
  • Take Screenshot - Capture the current screen
  • Open Cap - Open the Cap application
  • Open Settings - Open Cap settings
  • Recording Controls - Quick access list view with all actions

Deeplink Format

cap-desktop://action?value=<URL_ENCODED_JSON>

Examples

Stop Recording:

cap-desktop://action?value="stop_recording"

Pause Recording:

cap-desktop://action?value="pause_recording"

Toggle Pause:

cap-desktop://action?value="toggle_pause"

Start Recording:

cap-desktop://action?value={"start_recording":{"capture_mode":{"screen":"Main Display"},"camera":null,"mic_label":null,"capture_system_audio":false,"mode":"instant"}}

Testing

Test Deeplinks (macOS Terminal)

# Stop recording
open "cap-desktop://action?value=\"stop_recording\""

# Pause recording
open "cap-desktop://action?value=\"pause_recording\""

# Resume recording
open "cap-desktop://action?value=\"resume_recording\""

# Toggle pause
open "cap-desktop://action?value=\"toggle_pause\""

# Take screenshot
open "cap-desktop://action?value=\"take_screenshot\""

# Open settings
open "cap-desktop://action?value={\"open_settings\":{\"page\":null}}"

Test Raycast Extension

cd extensions/raycast-cap
npm install
npm run dev

Screenshots

(Add screenshots of Raycast extension here)

Checklist

  • Deeplinks for pause/resume/toggle recording
  • Deeplinks for switching microphone/camera
  • Deeplink for taking screenshots
  • Deeplinks for listing devices/displays/windows
  • Deeplink for getting recording status
  • Raycast extension with all commands
  • Raycast extension includes Recording Controls list view
  • README for Raycast extension

Greptile Overview

Greptile Summary

This PR adds new Cap deeplink actions on the desktop side (pause/resume/toggle, input switching, screenshots, window/display/device listing, and recording status) and introduces a new Raycast extension that triggers these actions by opening cap-desktop:// URLs.

The Raycast extension is straightforward (each command calls a shared deeplink helper), and the desktop handler wires the new actions into existing recording/window APIs.

Main issue to address: the new “list/query” deeplink actions (list_displays, list_windows, list_microphones, list_cameras, get_recording_status) only emit trace! logs and return Ok(()), so consumers like Raycast cannot actually receive the requested data. There are also smaller correctness/maintainability nits (e.g., choosing the first display as “primary”, Debug-formatted IDs, and a couple of unused/redundant TS bits).

Confidence Score: 3/5

  • This PR is reasonably safe to merge, but some newly added deeplink actions are effectively non-functional for consumers due to missing a response mechanism.
  • Most changes are additive and wire into existing APIs; the main risk is product-level correctness (query actions don’t return results; screenshot display selection may surprise users) rather than crashes or security issues.
  • apps/desktop/src-tauri/src/deeplink_actions.rs

Important Files Changed

Filename Overview
apps/desktop/src-tauri/src/deeplink_actions.rs Adds many new deeplink actions (pause/resume/toggle, device switching, screenshot, list/query actions); list/status actions currently only log via tracing and don’t return data.
extensions/raycast-cap/README.md Adds README documenting Raycast extension features and deeplink format.
extensions/raycast-cap/assets/cap-icon.png Adds Raycast extension icon asset.
extensions/raycast-cap/package.json Adds Raycast extension manifest (commands, deps, scripts).
extensions/raycast-cap/src/open-cap.tsx Adds Raycast command to open Cap via bundle id.
extensions/raycast-cap/src/open-settings.tsx Adds Raycast command to open Cap settings via deeplink.
extensions/raycast-cap/src/pause-recording.tsx Adds Raycast command to pause recording via deeplink.
extensions/raycast-cap/src/recording-controls.tsx Adds list-view command aggregating multiple recording actions; has unused imports.
extensions/raycast-cap/src/resume-recording.tsx Adds Raycast command to resume recording via deeplink.
extensions/raycast-cap/src/start-recording.tsx Adds Raycast command to start recording via deeplink with default params.
extensions/raycast-cap/src/stop-recording.tsx Adds Raycast command to stop recording via deeplink.
extensions/raycast-cap/src/take-screenshot.tsx Adds Raycast command to take screenshot via deeplink; depends on desktop picking intended display.
extensions/raycast-cap/src/toggle-pause.tsx Adds Raycast command to toggle pause via deeplink.
extensions/raycast-cap/src/utils/deeplink.ts Adds shared deeplink URL builder/executor and Cap install/open helpers; contains a redundant conditional in URL builder.
extensions/raycast-cap/tsconfig.json Adds TypeScript config for Raycast extension.

Sequence Diagram

sequenceDiagram
  participant Raycast as Raycast Extension
  participant OS as macOS URL Handler
  participant Cap as Cap Desktop (Tauri)
  participant DL as deeplink_actions::handle
  participant Rec as recording subsystem

  Raycast->>Raycast: buildDeeplinkUrl(action)
  Raycast->>OS: open(cap-desktop://action?value=...)
  OS->>Cap: deliver URL event
  Cap->>DL: handle(app_handle, urls)
  DL->>DL: DeepLinkAction::try_from(url)
  alt start_recording
    DL->>Rec: set_camera_input / set_mic_input
    DL->>Rec: start_recording(inputs)
  else stop/pause/resume/toggle
    DL->>Rec: stop_recording / pause_recording / resume_recording / toggle_pause_recording
  else take_screenshot
    DL->>Rec: list_displays() & take_screenshot(target)
  else list/query actions
    DL->>DL: list_* / get_recording_status
    DL-->>DL: trace!(...) (no return to Raycast)
  end
  DL-->>Cap: Result logged on error
Loading

(3/5) Reply to the agent's comments like "Can you suggest a fix for this @greptileai?" or ask follow-up questions!

Context used:

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

This PR adds comprehensive deeplink support for Cap and a Raycast extension
to control Cap from Raycast.

## Deeplinks Added (deeplink_actions.rs)

New deeplink actions:
- `pause_recording` - Pause current recording
- `resume_recording` - Resume paused recording
- `toggle_pause` - Toggle pause state
- `set_microphone` - Switch microphone input
- `set_camera` - Switch camera input
- `take_screenshot` - Take a screenshot of primary display
- `show_main_window` - Show main Cap window
- `list_displays` - List available displays
- `list_windows` - List available windows
- `list_microphones` - List available microphones
- `list_cameras` - List available cameras
- `get_recording_status` - Get current recording status

## Raycast Extension (extensions/raycast-cap/)

Commands included:
- Start Recording - Start a new screen recording
- Stop Recording - Stop current recording
- Pause Recording - Pause current recording
- Resume Recording - Resume paused recording
- Toggle Pause - Toggle pause state
- Take Screenshot - Capture screen
- Open Cap - Open the Cap application
- Open Settings - Open Cap settings
- Recording Controls - Quick access list view

Deeplink format: `cap-desktop://action?value=<JSON_ENCODED_ACTION>`

Closes CapSoftware#1540
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.

15 files reviewed, 5 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +18 to +25
/// Response types for deeplink queries
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct RecordingStatusResponse {
pub is_recording: bool,
pub is_paused: bool,
pub mode: Option<RecordingMode>,
}
Copy link
Contributor

Choose a reason for hiding this comment

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

[P1] RecordingStatusResponse/DisplayInfo/etc are never used and (more importantly) the deep link “query” actions (List*, GetRecordingStatus) don’t return anything to the caller. Right now they only trace! and Ok(()), which makes them effectively no-ops from the deeplink consumer’s perspective (Raycast can’t read stdout/stderr/tracing). This also means these new structs add dead code unless a return mechanism is introduced.

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:25

Comment:
[P1] `RecordingStatusResponse`/`DisplayInfo`/etc are never used and (more importantly) the deep link “query” actions (`List*`, `GetRecordingStatus`) don’t return anything to the caller. Right now they only `trace!` and `Ok(())`, which makes them effectively no-ops from the deeplink consumer’s perspective (Raycast can’t read stdout/stderr/tracing). This also means these new structs add dead code unless a return mechanism is introduced.

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

Comment on lines +256 to +266
DeepLinkAction::ListDisplays => {
let displays: Vec<DisplayInfo> = cap_recording::screen_capture::list_displays()
.into_iter()
.map(|(d, _)| DisplayInfo {
name: d.name.clone(),
id: format!("{:?}", d.id),
})
.collect();
// Log for debugging; in practice this could be returned via a different mechanism
trace!("Available displays: {:?}", displays);
Ok(())
Copy link
Contributor

Choose a reason for hiding this comment

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

[P2] ListDisplays formats d.id with format!("{:?}", d.id) instead of a stable identifier. If DisplayId’s Debug impl changes across versions/platforms, the value becomes non-portable for consumers (e.g., a Raycast command that wants to persist an ID). Consider exposing the underlying id type/string in a stable way.

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

Comment:
[P2] `ListDisplays` formats `d.id` with `format!("{:?}", d.id)` instead of a stable identifier. If `DisplayId`’s `Debug` impl changes across versions/platforms, the value becomes non-portable for consumers (e.g., a Raycast command that wants to persist an ID). Consider exposing the underlying id type/string in a stable way.

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

Comment on lines +235 to +245
DeepLinkAction::TakeScreenshot => {
// Take a screenshot of the primary display
let displays = cap_recording::screen_capture::list_displays();
if let Some((display, _)) = displays.into_iter().next() {
let target = ScreenCaptureTarget::Display { id: display.id };
crate::recording::take_screenshot(app.clone(), target)
.await
.map(|_| ())
} else {
Err("No display found for screenshot".to_string())
}
Copy link
Contributor

Choose a reason for hiding this comment

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

[P2] TakeScreenshot picks list_displays().into_iter().next() as “primary display”. list_displays() ordering isn’t guaranteed, so this can capture a non-primary monitor depending on platform/ordering. If “primary” matters, it should be selected explicitly (or allow a display id/name parameter).

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

Comment:
[P2] `TakeScreenshot` picks `list_displays().into_iter().next()` as “primary display”. `list_displays()` ordering isn’t guaranteed, so this can capture a non-primary monitor depending on platform/ordering. If “primary” matters, it should be selected explicitly (or allow a display id/name parameter).

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

Comment on lines +56 to +61
export function buildDeeplinkUrl(action: DeepLinkAction): string {
const actionValue =
typeof action === "string" ? JSON.stringify(action) : JSON.stringify(action);
const encodedValue = encodeURIComponent(actionValue);
return `${CAP_DEEPLINK_SCHEME}://action?value=${encodedValue}`;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

[P3] buildDeeplinkUrl has a redundant conditional: typeof action === "string" ? JSON.stringify(action) : JSON.stringify(action). It always does the same thing, so the typeof check can be removed to reduce noise.

Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/raycast-cap/src/utils/deeplink.ts
Line: 56:61

Comment:
[P3] `buildDeeplinkUrl` has a redundant conditional: `typeof action === "string" ? JSON.stringify(action) : JSON.stringify(action)`. It always does the same thing, so the `typeof` check can be removed to reduce noise.

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

Comment on lines +1 to +3
import { ActionPanel, Action, List, Icon, Color, showToast, Toast } from "@raycast/api";
import { executeCapAction, isCapInstalled, openCap } from "./utils/deeplink";
import { useEffect, useState } from "react";
Copy link
Contributor

Choose a reason for hiding this comment

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

[P3] Unused imports: showToast and Toast are imported but never used. This will trip linters (ray lint) and adds noise.

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

Comment:
[P3] Unused imports: `showToast` and `Toast` are imported but never used. This will trip linters (`ray lint`) and adds noise.

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

const [isInstalled, setIsInstalled] = useState<boolean | null>(null);

useEffect(() => {
isCapInstalled().then(setIsInstalled);
Copy link

Choose a reason for hiding this comment

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

Consider handling isCapInstalled() errors + avoiding state updates after unmount; also makes showToast/Toast imports meaningful.

Suggested change
isCapInstalled().then(setIsInstalled);
useEffect(() => {
let active = true;
void (async () => {
try {
const installed = await isCapInstalled();
if (active) setIsInstalled(installed);
} catch (error) {
if (!active) return;
setIsInstalled(false);
await showToast({
style: Toast.Style.Failure,
title: "Failed to detect Cap",
message: error instanceof Error ? error.message : "Unknown error",
});
}
})();
return () => {
active = false;
};
}, []);

await executeCapAction(
{
start_recording: {
capture_mode: { screen: "Main Display" },
Copy link

Choose a reason for hiding this comment

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

Heads up: capture_mode.screen needs to exactly match Cap's display names (from cap_recording::screen_capture::list_displays()); hardcoding "Main Display" may not resolve on many systems. Might be worth supporting it as an alias in the deeplink handler or switching to ID-based selection.

*/
export function buildDeeplinkUrl(action: DeepLinkAction): string {
const actionValue =
typeof action === "string" ? JSON.stringify(action) : JSON.stringify(action);
Copy link

Choose a reason for hiding this comment

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

Minor: this branch is redundant (both sides do JSON.stringify).

Suggested change
typeof action === "string" ? JSON.stringify(action) : JSON.stringify(action);
const actionValue = JSON.stringify(action);

}

try {
await open("", CAP_BUNDLE_ID);
Copy link

Choose a reason for hiding this comment

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

Launching via a deeplink is usually more reliable than open("", bundleId) (it also gets you into a known UI state).

Suggested change
await open("", CAP_BUNDLE_ID);
await open(buildDeeplinkUrl("show_main_window"));

Window(String),
}

/// Response types for deeplink queries
Copy link

Choose a reason for hiding this comment

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

Repo style seems to avoid code comments; these new ////// comments in the deeplink handler might be worth removing for consistency.

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