Skip to content

Conversation

@OpenClaw3827
Copy link

@OpenClaw3827 OpenClaw3827 commented Feb 3, 2026

Summary\n- Added deeplink actions for pause/resume, switch microphone, and switch camera\n- Added Raycast extension (apps/raycast) to trigger deeplinks\n- Updated README with deeplink format and examples\n\n## Manual verification\n- See README Deeplinks section for encoded examples\n- Run in apps/raycast and verify each command triggers Cap Desktop\n\n## Notes\nRaycast CLI lint not run here due to missing registry access; manual verification provided.

Greptile Overview

Greptile Summary

This PR adds deeplink support for controlling Cap Desktop recording actions (pause/resume, switch microphone/camera) and includes a Raycast extension to trigger these deeplinks.

Key Changes:

  • Extended deeplink_actions.rs with new action variants: PauseRecording, ResumeRecording, SetMicrophone, SetCamera
  • Created Raycast extension (apps/raycast) with 7 commands that trigger Cap deeplinks
  • Added comprehensive deeplink documentation to README with format and examples

Issues Found:

  • Critical: start-recording-window.ts has empty window name that will always fail at runtime
  • High: start-recording-screen.ts hardcodes "Built-in Display" which fails on external monitors or Windows
  • Medium: deeplink_actions.rs:94 uses .unwrap() that can panic on malformed file URLs (macOS only)

Recommendations:

  • Fix window recording command to either select a window or use a valid default
  • Add UI to select displays/windows or use primary display as default
  • Replace .unwrap() with proper error handling

Confidence Score: 2/5

  • This PR has critical logic errors that will cause runtime failures
  • Score reflects multiple blocking issues: window recording command will always fail with empty window name, screen recording will fail for non-default displays, and potential panic in Rust code. The implementation is well-structured but the hardcoded values make key features non-functional.
  • Critical attention needed for apps/raycast/src/start-recording-window.ts and apps/raycast/src/start-recording-screen.ts which have logic errors that will cause runtime failures. Also review apps/desktop/src-tauri/src/deeplink_actions.rs for panic-prone unwrap.

Important Files Changed

Filename Overview
apps/raycast/src/start-recording-window.ts Hardcoded empty window name will cause recordings to fail
apps/raycast/src/start-recording-screen.ts Hardcoded screen name may fail on non-Mac systems or external displays
apps/desktop/src-tauri/src/deeplink_actions.rs Deeplink parsing and execution with potential unwrap panic on macOS
apps/raycast/src/utils.ts Simple deeplink URL builder with proper encoding
apps/raycast/src/switch-camera.ts Camera switching form with basic validation and correct payload structure

Sequence Diagram

sequenceDiagram
    participant User
    participant Raycast
    participant DeepLink as cap-desktop:// URL
    participant Tauri
    participant Handler as deeplink_actions::handle
    participant Action as DeepLinkAction
    participant Recording as recording module
    participant Desktop as Cap Desktop App

    User->>Raycast: Trigger command (e.g., Pause Recording)
    Raycast->>Raycast: Build JSON payload
    Raycast->>Raycast: Encode and construct URL
    Raycast->>DeepLink: Open cap-desktop://action?value={...}
    DeepLink->>Tauri: OS deeplink handler
    Tauri->>Handler: handle(app_handle, urls)
    Handler->>Handler: Filter and parse URLs
    Handler->>Action: TryFrom<&Url>
    Action->>Action: Parse query param "value"
    Action->>Action: Deserialize JSON to enum variant
    Action-->>Handler: DeepLinkAction instance
    Handler->>Action: spawn async task
    Action->>Action: execute(app_handle)
    
    alt Pause/Resume/Stop Recording
        Action->>Recording: pause_recording/resume_recording/stop_recording
        Recording->>Desktop: Update recording state
        Desktop-->>Recording: Result
        Recording-->>Action: Result
    else Start Recording
        Action->>Recording: set_camera_input
        Action->>Recording: set_mic_input
        Action->>Recording: start_recording with inputs
        Recording->>Desktop: Begin capture
        Desktop-->>Recording: Result
        Recording-->>Action: Result
    else Set Microphone/Camera
        Action->>Recording: set_mic_input or set_camera_input
        Recording->>Desktop: Update input device
        Desktop-->>Recording: Result
        Recording-->>Action: Result
    else Open Settings/Editor
        Action->>Desktop: show_window or open_project_from_path
        Desktop-->>Action: Result
    end
    
    Action-->>Handler: Result
    Handler-->>User: Action completed (or error logged)
Loading

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

Copilot AI review requested due to automatic review settings February 3, 2026 00:22
@OpenClaw3827
Copy link
Author

Verification\nManual verification (Raycast CLI unavailable in this environment):\n\n- Deeplinks (macOS)\n - Start (screen):\n open "cap-desktop://action?value=%7B%22start_recording%22%3A%7B%22capture_mode%22%3A%7B%22screen%22%3A%22Built-in%20Display%22%7D%2C%22camera%22%3Anull%2C%22mic_label%22%3Anull%2C%22capture_system_audio%22%3Atrue%2C%22mode%22%3A%22instant%22%7D%7D"\n - Stop: open "cap-desktop://action?value=%7B%22stop_recording%22%3A%7B%7D%7D"\n - Pause: open "cap-desktop://action?value=%7B%22pause_recording%22%3A%7B%7D%7D"\n - Resume: open "cap-desktop://action?value=%7B%22resume_recording%22%3A%7B%7D%7D"\n - Switch mic: open "cap-desktop://action?value=%7B%22set_microphone%22%3A%7B%22mic_label%22%3A%22MacBook%20Pro%20Microphone%22%7D%7D"\n - Switch camera: open "cap-desktop://action?value=%7B%22set_camera%22%3A%7B%22camera%22%3A%7B%22DeviceID%22%3A%22YOUR_DEVICE_ID%22%7D%7D%7D"\n\n- Raycast extension\n - cd apps/raycast\n - raycast dev\n - Run 7 commands and confirm Cap Desktop responds.\n\nNotes: Raycast CLI lint could not be executed here due to missing registry access; manual verification steps provided.

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.

5 files reviewed, 3 comments

Edit Code Review Agent Settings | Greptile

export default async function Command() {
await openCapDeepLink({
start_recording: {
capture_mode: { window: "" },
Copy link
Contributor

Choose a reason for hiding this comment

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

Empty window name will fail to match any window. The Rust code at deeplink_actions.rs:140 uses .find() which will return None and error with "No window with name """. Either provide a valid default window name or add a UI form to select the window (like switch-microphone.ts and switch-camera.ts).

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

Comment:
Empty window name will fail to match any window. The Rust code at `deeplink_actions.rs:140` uses `.find()` which will return `None` and error with "No window with name """. Either provide a valid default window name or add a UI form to select the window (like `switch-microphone.ts` and `switch-camera.ts`).

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

export default async function Command() {
await openCapDeepLink({
start_recording: {
capture_mode: { screen: "Built-in Display" },
Copy link
Contributor

Choose a reason for hiding this comment

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

Hardcoded "Built-in Display" will fail if user has external monitors or uses Windows. The Rust code at deeplink_actions.rs:134 uses .find() which will return None and error with "No screen with name "Built-in Display"". Consider adding a UI form to select available displays or use the primary display.

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

Comment:
Hardcoded `"Built-in Display"` will fail if user has external monitors or uses Windows. The Rust code at `deeplink_actions.rs:134` uses `.find()` which will return `None` and error with "No screen with name \"Built-in Display\"". Consider adding a UI form to select available displays or use the primary display.

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 (1)

apps/desktop/src-tauri/src/deeplink_actions.rs
.unwrap() will panic if the file URL cannot be converted to a path (e.g., network paths, malformed URLs). Use .map_err() to return an error instead.

                project_path: url.to_file_path().map_err(|_| ActionParseFromUrlError::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: 94:94

Comment:
`.unwrap()` will panic if the file URL cannot be converted to a path (e.g., network paths, malformed URLs). Use `.map_err()` to return an error instead.

```suggestion
                project_path: url.to_file_path().map_err(|_| ActionParseFromUrlError::Invalid)?,
```

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

@OpenClaw3827
Copy link
Author

Applied review feedback: replaced unwrap on file URL to_file_path() with map_err -> ActionParseFromUrlError::Invalid to avoid panic on malformed URLs.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This pull request adds deeplink support for pause/resume recording and switching microphone/camera inputs, along with a Raycast extension to trigger these actions via keyboard shortcuts.

Changes:

  • Added four new deeplink actions to the backend: PauseRecording, ResumeRecording, SetMicrophone, and SetCamera
  • Created a new Raycast extension (apps/raycast) with 7 commands to control Cap Desktop via deeplinks
  • Updated README with comprehensive deeplink documentation and examples

Reviewed changes

Copilot reviewed 12 out of 14 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
apps/desktop/src-tauri/src/deeplink_actions.rs Added new enum variants and execution handlers for pause, resume, set microphone, and set camera actions
apps/raycast/package.json Package manifest for the Raycast extension with @raycast/api dependency
apps/raycast/package-lock.json Lock file for the Raycast extension dependencies
apps/raycast/raycast.json Raycast extension configuration defining 7 commands
apps/raycast/icon.png Extension icon for Raycast
apps/raycast/src/utils.ts Utility function to construct and open deeplinks
apps/raycast/src/pause-recording.ts Command to pause current recording
apps/raycast/src/resume-recording.ts Command to resume paused recording
apps/raycast/src/stop-recording.ts Command to stop current recording
apps/raycast/src/start-recording-screen.ts Command to start screen recording
apps/raycast/src/start-recording-window.ts Command to start window recording
apps/raycast/src/switch-microphone.ts Form-based command to switch microphone by label
apps/raycast/src/switch-camera.ts Form-based command to switch camera by device/model ID
README.md Added comprehensive deeplink documentation with format description, examples, and Raycast extension overview
Files not reviewed (1)
  • apps/raycast/package-lock.json: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

export default async function Command() {
await openCapDeepLink({
start_recording: {
capture_mode: { window: "" },
Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

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

The window capture mode is set to an empty string, but based on the backend implementation in deeplink_actions.rs lines 136-140, this will fail to find a window with an empty name. The deeplink requires a specific window name to match. Consider either prompting the user for a window name (similar to switch-microphone) or using a more appropriate default value that indicates "current window" or removing this command if it cannot function without user input.

Suggested change
capture_mode: { window: "" },
capture_mode: null,

Copilot uses AI. Check for mistakes.
export default async function Command() {
await openCapDeepLink({
start_recording: {
capture_mode: { screen: "Built-in Display" },
Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

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

The hardcoded screen name "Built-in Display" may not exist on all systems. The backend implementation at deeplink_actions.rs lines 131-135 will fail if no display matches this name exactly. Consider either prompting the user to select a display, using the primary/default display, or documenting that this command only works on MacBooks with built-in displays.

Suggested change
capture_mode: { screen: "Built-in Display" },
capture_mode: {},

Copilot uses AI. Check for mistakes.
"lint": "raycast lint"
},
"dependencies": {
"@raycast/api": "^1.74.0",
Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

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

The package.json specifies @raycast/api": "^1.74.0" but the package-lock.json has version 1.104.4 installed. While the caret (^) allows this, there's a significant version jump (1.74 to 1.104) which could indicate the dependency specification should be updated to reflect the actual minimum required version, or the lock file was generated with a newer version than intended. Consider verifying the minimum required API version for the features used in this extension.

Suggested change
"@raycast/api": "^1.74.0",
"@raycast/api": "^1.104.4",

Copilot uses AI. Check for mistakes.
Comment on lines +12 to +17
const micLabel = (values.micLabel as string)?.trim();
if (!micLabel) {
await showToast({ style: Toast.Style.Failure, title: "Mic label is required" });
return;
}
await openCapDeepLink({ set_microphone: { mic_label: micLabel } });
Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

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

The microphone label validation should allow empty strings to be valid since the backend set_mic_input function accepts None as a valid value to remove the microphone input. The current validation rejects empty strings after trimming, but users should be able to clear/remove the microphone by submitting an empty value. Consider allowing empty strings or providing a separate "Clear Microphone" command.

Suggested change
const micLabel = (values.micLabel as string)?.trim();
if (!micLabel) {
await showToast({ style: Toast.Style.Failure, title: "Mic label is required" });
return;
}
await openCapDeepLink({ set_microphone: { mic_label: micLabel } });
const rawMicLabel = values.micLabel as string | undefined;
const micLabel = rawMicLabel?.trim();
// If the field is unexpectedly missing, keep existing "required" behavior.
if (micLabel === undefined) {
await showToast({ style: Toast.Style.Failure, title: "Mic label is required" });
return;
}
// Allow an empty string to clear the microphone by sending `null` to the backend.
const payloadMicLabel = micLabel === "" ? null : micLabel;
await openCapDeepLink({ set_microphone: { mic_label: payloadMicLabel } });

Copilot uses AI. Check for mistakes.
const deviceId = (values.deviceId as string)?.trim();
const modelId = (values.modelId as string)?.trim();
if (!deviceId && !modelId) {
await showToast({ style: Toast.Style.Failure, title: "Provide Device ID or Model ID" });
Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

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

The camera validation logic prevents users from clearing the camera input. Since the backend set_camera_input function accepts None to remove the camera, users should be able to submit with both fields empty. Consider allowing empty submissions to clear the camera, or provide a separate "Clear Camera" command. This aligns with the pattern used in the set_microphone deeplink action which accepts null for mic_label.

Suggested change
await showToast({ style: Toast.Style.Failure, title: "Provide Device ID or Model ID" });
// Both fields empty: clear the current camera
await openCapDeepLink({ set_camera: { camera: null } });
await showToast({ style: Toast.Style.Success, title: "Camera cleared" });

Copilot uses AI. Check for mistakes.
await showToast({ style: Toast.Style.Failure, title: "Provide Device ID or Model ID" });
return;
}
const camera = deviceId ? { DeviceID: deviceId } : { ModelID: modelId };
Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

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

If the user provides both deviceId and modelId, only deviceId is used and modelId is silently ignored. Consider either validating that only one is provided, or documenting this priority order in the form placeholder or title. This would prevent user confusion when both fields are filled.

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +7
import { open } from "@raycast/api";

const SCHEME = "cap-desktop://action?value=";

export function openCapDeepLink(payload: unknown) {
const encoded = encodeURIComponent(JSON.stringify(payload));
return open(`${SCHEME}${encoded}`);
Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

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

The openCapDeepLink function does not handle errors from the open call. If Cap Desktop is not installed or the deeplink fails to open, users will not receive feedback. Consider adding error handling to show a toast notification when the deeplink fails to open, similar to the validation error handling in switch-microphone and switch-camera commands.

Suggested change
import { open } from "@raycast/api";
const SCHEME = "cap-desktop://action?value=";
export function openCapDeepLink(payload: unknown) {
const encoded = encodeURIComponent(JSON.stringify(payload));
return open(`${SCHEME}${encoded}`);
import { open, showToast, Toast } from "@raycast/api";
const SCHEME = "cap-desktop://action?value=";
export function openCapDeepLink(payload: unknown) {
const encoded = encodeURIComponent(JSON.stringify(payload));
return open(`${SCHEME}${encoded}`).catch((error) => {
const message =
typeof error === "object" && error !== null && "message" in error
? String((error as { message?: unknown }).message ?? "Unknown error")
: String(error ?? "Unknown error");
showToast(Toast.Style.Failure, "Failed to open Cap Desktop", message);
});

Copilot uses AI. Check for mistakes.

export default async function Command() {
await openCapDeepLink({ stop_recording: {} });
}
Copy link

Choose a reason for hiding this comment

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

I think DeepLinkAction::StopRecording is a unit variant, so this likely needs to be a JSON string ("stop_recording") rather than { stop_recording: {} } (same for pause/resume).

Suggested change
}
await openCapDeepLink("stop_recording");

await openCapDeepLink({
start_recording: {
capture_mode: { window: "" },
camera: null,
Copy link

Choose a reason for hiding this comment

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

With window: "" this will consistently fail to match anything. Consider making this command take user input (Raycast argument or Form) instead of hard-coding an empty window name.

Stop recording:
```
cap-desktop://action?value={"stop_recording":{}}
```
Copy link

Choose a reason for hiding this comment

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

This example should URL-encode the JSON value, and for unit actions serde typically expects a JSON string (e.g. "stop_recording") rather than { "stop_recording": {} }.

Suggested change
```
cap-desktop://action?value=%22stop_recording%22

@OpenClaw3827
Copy link
Author

Vercel check failure appears to be an auth/config issue (needs Vercel access for the repo/org). Code changes shouldn't affect this; please re-run or authorize Vercel for this repo if needed.

@OpenClaw3827
Copy link
Author

Good catch. Updated README examples for unit actions to use URL-encoded JSON string values (e.g., cap-desktop://action?value=%22stop_recording%22). Pushed fix.

@OpenClaw3827
Copy link
Author

Updated Raycast start commands to avoid hardcoded screen/window names. They now prompt for input (Form) before firing the deeplink. This prevents hardcoded defaults and matches expected behavior.

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.

1 participant