Skip to content

Comments

Bounty: Deeplinks support + Raycast Extension#1628

Open
SolariSystems wants to merge 5 commits intoCapSoftware:mainfrom
SolariSystems:bounty/SBR_CapSoftwar_Cap_1540
Open

Bounty: Deeplinks support + Raycast Extension#1628
SolariSystems wants to merge 5 commits intoCapSoftware:mainfrom
SolariSystems:bounty/SBR_CapSoftwar_Cap_1540

Conversation

@SolariSystems
Copy link

@SolariSystems SolariSystems commented Feb 22, 2026

Summary

Bounty: Deeplinks support + Raycast Extension.

Changes

  • Modified apps/desktop/src-tauri/src/deeplink_actions.rs
  • Modified apps/desktop/src-tauri/tauri.conf.json

Testing

Note: Some tests may need attention. See CI results.

/claim #1540

Greptile Summary

Added deeplink support for controlling Cap recordings via URL schemes (cap:// and cap-desktop://) and implemented a new Raycast extension for remote control functionality. The implementation includes new deeplink actions for pause, resume, and switching audio/video devices.

  • Extended Rust deeplink_actions.rs with PauseRecording, ResumeRecording, SwitchMic, and SwitchCamera actions
  • Added simple path-based deeplink parsing (e.g., cap://pause-recording) alongside legacy JSON-based format for backward compatibility
  • Created complete Raycast extension with 6 commands (start, stop, pause, resume, switch mic, switch camera)
  • Configured Tauri to register cap and cap-desktop URL schemes in system

Issues Found:

  • Incorrect recording mode value in start-recording.tsx will cause runtime failures (instant_capture doesn't exist in Rust enum)
  • Unused register function in deeplink_actions.rs (deeplinks already configured in lib.rs)
  • Minor: inconsistent default recording mode between package.json description and actual default value

Confidence Score: 3/5

  • This PR cannot be merged safely due to a critical type mismatch that will cause runtime errors
  • Score reflects a critical bug in the Raycast extension where instant_capture is used instead of instant, causing the deeplink action to fail. The Rust deeplink implementation is solid and properly integrated, but the TypeScript code will produce invalid deeplink URLs that won't deserialize correctly on the Rust side.
  • Pay close attention to apps/raycast-extension/src/start-recording.tsx - the recording mode bug must be fixed before merge

Important Files Changed

Filename Overview
apps/desktop/src-tauri/src/deeplink_actions.rs Added new deeplink actions (pause, resume, switch mic/camera) with both simple path-based and legacy JSON-based parsing
apps/desktop/src-tauri/tauri.conf.json Configured deep-link plugin with cap and cap-desktop URL schemes
apps/raycast-extension/package.json New Raycast extension configuration with commands for recording control via deeplinks
apps/raycast-extension/src/start-recording.tsx Triggers recording via deeplink with preferences; contains incorrect default mode value

Sequence Diagram

sequenceDiagram
    participant Raycast as Raycast Extension
    participant OS as Operating System
    participant Tauri as Tauri Deep Link Plugin
    participant Handler as deeplink_actions::handle
    participant Action as DeepLinkAction
    participant Recording as recording module

    Raycast->>OS: open("cap://pause-recording")
    OS->>Tauri: URL scheme handler triggered
    Tauri->>Handler: on_open_url event with URL
    Handler->>Action: TryFrom<&Url> parse URL
    
    alt Simple path-based (new)
        Action-->>Action: Match domain "pause-recording"
        Action-->>Handler: DeepLinkAction::PauseRecording
    else Legacy JSON-based
        Action-->>Action: Parse ?value=<json>
        Action-->>Handler: Deserialized action
    end
    
    Handler->>Action: execute(&app_handle)
    Action->>Recording: pause_recording(app, state)
    Recording-->>Action: Result<(), String>
    Action-->>Handler: Result
    Handler-->>Tauri: Async task complete
Loading

Last reviewed commit: 0ec5e67

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.

12 files reviewed, 3 comments

Edit Code Review Agent Settings | Greptile

camera: null,
mic_label: null,
capture_system_audio: prefs.captureSystemAudio ?? false,
mode: prefs.recordingMode ?? "instant",
Copy link
Contributor

Choose a reason for hiding this comment

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

instant_capture doesn't match the Rust RecordingMode enum values (studio or instant)

Suggested change
mode: prefs.recordingMode ?? "instant",
mode: prefs.recordingMode ?? "instant",
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/raycast-extension/src/start-recording.tsx
Line: 25

Comment:
`instant_capture` doesn't match the Rust `RecordingMode` enum values (`studio` or `instant`)

```suggestion
      mode: prefs.recordingMode ?? "instant",
```

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

Comment on lines 45 to 52
pub fn register(app: &tauri::App) -> Result<(), Box<dyn std::error::Error>> {
use tauri_plugin_deep_link::DeepLinkExt;
let app_handle = app.handle().clone();
app.deep_link().on_open_urls(move |event| {
handle(&app_handle, event.urls().to_vec());
})?;
Ok(())
}
Copy link
Contributor

Choose a reason for hiding this comment

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

The register function is defined but never called. Deeplinks are already handled via app.deep_link().on_open_url() in lib.rs:3390.

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

Comment:
The `register` function is defined but never called. Deeplinks are already handled via `app.deep_link().on_open_url()` in `lib.rs:3390`.

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

"description": "Instant shares immediately; Studio lets you edit first",
"type": "dropdown",
"required": false,
"default": "instant",
Copy link
Contributor

Choose a reason for hiding this comment

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

Consider using studio as default to match the description on line 15 and provide consistency with the desktop app defaults

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/raycast-extension/package.json
Line: 53

Comment:
Consider using `studio` as default to match the description on line 15 and provide consistency with the desktop app defaults

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

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

.gitignore Outdated
analysis/plans/
.ralphy No newline at end of file
.ralphy
# Auto-added by Solari to prevent build artifact commits
Copy link

Choose a reason for hiding this comment

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

This looks like it accidentally includes contributor-specific ignores (and the attribution line). I’d keep the generic ones, but drop the Solari-specific patterns.

Suggested change
# Auto-added by Solari to prevent build artifact commits
.venv/
venv/
node_modules/
__pycache__/
*.pyc

.gitignore Outdated
Comment on lines 71 to 78
# Auto-added by Solari to prevent build artifact commits
.venv/
venv/
node_modules/
__pycache__/
*.pyc
.solari_*
.solari_deps_installed
Copy link

Choose a reason for hiding this comment

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

Edit: suggestion meant to cover the whole new block here (drop the attribution + .solari_* entries).

Suggested change
# Auto-added by Solari to prevent build artifact commits
.venv/
venv/
node_modules/
__pycache__/
*.pyc
.solari_*
.solari_deps_installed
.venv/
venv/
node_modules/
__pycache__/
*.pyc

@@ -0,0 +1,1455 @@
{
Copy link

Choose a reason for hiding this comment

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

This repo is pnpm-based; committing a root package-lock.json usually means someone ran npm install and it’ll fight pnpm-lock.yaml. I’d drop this file from the PR and stick with pnpm’s lockfile.

let raw = v.as_ref();
serde_json::from_str::<DeviceOrModelID>(raw)
.or_else(|_| {
serde_json::from_str::<DeviceOrModelID>(&format!(r#""{}""#, raw))
Copy link

Choose a reason for hiding this comment

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

The fallback format!(r#"\"{}\""#, raw) will break if raw contains quotes/backslashes. Using serde_json::Value::String avoids manual escaping.

Suggested change
serde_json::from_str::<DeviceOrModelID>(&format!(r#""{}""#, raw))
serde_json::from_str::<DeviceOrModelID>(raw)
.or_else(|_| {
serde_json::from_value::<DeviceOrModelID>(serde_json::Value::String(
raw.to_string(),
))
})
.ok()

Comment on lines 124 to 127
serde_json::from_str::<DeviceOrModelID>(raw)
.or_else(|_| {
serde_json::from_str::<DeviceOrModelID>(&format!(r#""{}""#, raw))
})
Copy link

Choose a reason for hiding this comment

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

Edit: suggestion intended to replace the whole parse block.

Suggested change
serde_json::from_str::<DeviceOrModelID>(raw)
.or_else(|_| {
serde_json::from_str::<DeviceOrModelID>(&format!(r#""{}""#, raw))
})
serde_json::from_str::<DeviceOrModelID>(raw)
.or_else(|_| {
serde_json::from_value::<DeviceOrModelID>(serde_json::Value::String(
raw.to_string(),
))
})
.ok()

}
}

{
Copy link

Choose a reason for hiding this comment

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

Looks like apps/raycast-extension content (extension schema / tsconfig / TS commands) got pasted into this Rust file after the impl DeepLinkAction block. I’d remove everything from here to EOF so deeplink_actions.rs stays valid Rust (the Raycast extension files are already added under apps/raycast-extension/).

{
"name": "captureName",
"title": "Screen or Window Name",
"description": "Exact name of the screen or window to capture (leave blank for first available)",
Copy link

Choose a reason for hiding this comment

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

Cap currently matches by exact name; leaving this blank won’t select “first available” (it’ll send an empty string). Either implement empty-as-default on the app side, or tweak this description so it doesn’t promise that behavior.

Suggested change
"description": "Exact name of the screen or window to capture (leave blank for first available)",
"description": "Exact name of the screen or window to capture",

Comment on lines 13 to 17
const captureName = prefs.captureName?.trim() || "";
const captureMode =
prefs.captureType === "window"
? { window: captureName }
: { screen: captureName };
Copy link

Choose a reason for hiding this comment

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

If captureName is empty, the desktop side will treat it as an exact match and error. Consider failing fast in the command so users get immediate feedback.

Suggested change
const captureName = prefs.captureName?.trim() || "";
const captureMode =
prefs.captureType === "window"
? { window: captureName }
: { screen: captureName };
const captureName = prefs.captureName?.trim();
if (!captureName) {
await showHUD("Set a screen/window name in Raycast preferences first");
return;
}
const captureMode =
prefs.captureType === "window"
? { window: captureName }
: { screen: captureName };

"deep-link": {
"desktop": {
"schemes": ["cap-desktop"]
"schemes": ["cap-desktop", "cap"]
Copy link

Choose a reason for hiding this comment

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

Adding the global cap:// scheme means any app/site can trigger these actions once Cap is installed (e.g. starting/stopping recordings). Might be worth gating “remote control” deeplinks behind an explicit user setting, or at least limiting which actions are allowed without user confirmation.

<sub>[Edit Code Review Agent Settings](https://app.greptile.com/review/github) | [Greptile](https://greptile; This looks like it accidentally includes contrib
Comment on lines 115 to 122
let id = params.get("id").and_then(|v| {
let raw = v.as_ref();
serde_json::from_str::<DeviceOrModelID>(raw)
.or_else(|_| {
serde_json::from_str::<DeviceOrModelID>(&format!(r#""{}""#, raw))
})
.ok()
});
Copy link

Choose a reason for hiding this comment

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

DeviceOrModelID is an externally-tagged enum (DeviceID/ModelID), so deserializing from a plain string will always fail. As-is, cap://switch-camera?id=<some id> likely resolves to id: None and disables the camera instead of switching.

Suggested change
let id = params.get("id").and_then(|v| {
let raw = v.as_ref();
serde_json::from_str::<DeviceOrModelID>(raw)
.or_else(|_| {
serde_json::from_str::<DeviceOrModelID>(&format!(r#""{}""#, raw))
})
.ok()
});
let id = params.get("id").and_then(|v| {
let raw = v.as_ref();
if raw.is_empty() {
return None;
}
serde_json::from_str::<DeviceOrModelID>(raw)
.ok()
.or_else(|| Some(DeviceOrModelID::DeviceID(raw.to_string())))
});

<sub>[Edit Code Review Agent Settings](https://app.greptile.com/review/github) | [Greptile](https://greptile; This looks like it accidentally includes contrib
<sub>[Edit Code Review Agent Settings](https://app.greptile.com/review/github) | [Greptile](https://greptile; This looks like it accidentally includes contrib
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