Skip to content

feat(desktop): add cap:// deep links and minimal Raycast controls#1830

Open
JYZ-LESLIE wants to merge 3 commits into
CapSoftware:mainfrom
JYZ-LESLIE:codex/algora-28-raycast-controls
Open

feat(desktop): add cap:// deep links and minimal Raycast controls#1830
JYZ-LESLIE wants to merge 3 commits into
CapSoftware:mainfrom
JYZ-LESLIE:codex/algora-28-raycast-controls

Conversation

@JYZ-LESLIE
Copy link
Copy Markdown

@JYZ-LESLIE JYZ-LESLIE commented May 16, 2026

/claim #28

Summary

This delivers a focused Raycast-control baseline for Cap Desktop (v0.3 architecture) with minimal surface area:

  • Added lightweight deep links on desktop:
    • cap://record -> starts recording using the user’s saved defaults
    • cap://stop -> stops active recording
  • Registered cap custom scheme in Tauri deep-link config.
  • Added a minimal Raycast extension at extensions/raycast with three no-view commands:
    • Start Recording
    • Stop Recording
    • Open Dashboard

Why this scope

Issue #28 asks for fast start/stop flows from Raycast with defaults. Existing cap-desktop://action start flow requires explicit capture payloads; this PR adds a simpler protocol path for keyboard-first invocation.

Default-setting behavior

cap://record reuses the app’s existing recording preferences from RecordingSettingsStore:

  • target (or primary display fallback)
  • selected mic/camera
  • recording mode
  • system audio

Validation notes

  • Added parser unit tests for cap://record, cap://stop, and existing cap-desktop://action compatibility.
  • Rust toolchain is unavailable in this runner, so I could not execute cargo test locally here.

Greptile Summary

This PR adds two new cap:// deep-link routes (record and stop) to the Tauri desktop app and ships a companion Raycast extension with three no-view commands (Start Recording, Stop Recording, Open Dashboard). The cap:// URL parsing is wired correctly next to the existing cap-desktop:// handler and the scheme is registered in tauri.conf.json.

  • The parses_existing_action_format unit test added for backward-compat verification will fail: the match url.domain() guard block returns Err(...) on both arms (including the Some(\"action\") case), causing the ? to short-circuit before the JSON parsing code is reached.
  • StartDefaultRecording silently drops errors from set_mic_input/set_camera_input with let _ =, while the existing StartRecording path propagates them; a broken device will be skipped without any feedback.
  • The Raycast commands display a success HUD immediately after dispatching the deep link, with no way to confirm Cap Desktop received or acted on it.

Confidence Score: 3/5

The new cap:// routing and Raycast extension work correctly end-to-end, but the broken unit test and silent device-error path should be resolved before merge.

The parses_existing_action_format test will panic when cargo test is run because the cap-desktop://action URL parsing code always short-circuits before reaching the JSON deserialization step. Additionally, mic and camera setup failures are silently ignored in the StartDefaultRecording path, meaning a recording can start without the user's saved audio/video devices with no indication that something went wrong.

apps/desktop/src-tauri/src/deeplink_actions.rs needs the most attention — both the broken test and the silent error handling are in that file.

Important Files Changed

Filename Overview
apps/desktop/src-tauri/src/deeplink_actions.rs Adds cap://record and cap://stop scheme handling plus a StartDefaultRecording action; URL parsing for the new scheme is correct, but the added parses_existing_action_format test will fail and mic/camera errors are silently swallowed.
apps/desktop/src-tauri/tauri.conf.json Registers cap as an additional deep-link scheme alongside cap-desktop; minimal and correct change.
extensions/raycast/src/start-recording.ts Implements the Start Recording Raycast command via cap://record; HUD fires optimistically without confirmation from Cap Desktop.
extensions/raycast/src/stop-recording.ts Implements the Stop Recording Raycast command via cap://stop; same optimistic HUD pattern as start-recording.
extensions/raycast/src/open-dashboard.ts Opens the Cap dashboard in the default browser; straightforward and correct.
extensions/raycast/package.json Standard Raycast extension manifest with three no-view commands and correct dependency declarations.
extensions/raycast/tsconfig.json Standard TypeScript config for a Raycast extension; no issues.
Prompt To Fix All With AI
Fix the following 3 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 3
apps/desktop/src-tauri/src/deeplink_actions.rs:217-224
**`parses_existing_action_format` test will always fail**

The `match url.domain()` block that precedes the JSON-parsing code has no arm that returns `Ok` — both arms always return `Err(...)`, so the `?` at the end short-circuits and the function exits with `Err(Invalid)` before it ever reaches `query_pairs()` or `serde_json::from_str`. For `cap-desktop://action?value=...`, `url.domain()` returns `Some("action")`, which falls through to the `_ => Err(ActionParseFromUrlError::Invalid)` arm. The test therefore panics at `.expect("action deep link should parse")`. Since Rust toolchain was unavailable when authoring this PR, this was not caught.

### Issue 2 of 3
apps/desktop/src-tauri/src/deeplink_actions.rs:131-133
**Silent error discard for mic/camera setup differs from `StartRecording` path**

`set_mic_input` and `set_camera_input` errors are swallowed with `let _ =`, so a recording can silently start without the user's saved microphone or camera. The `StartRecording` arm immediately below uses `?` to propagate the same calls as hard errors — the inconsistency means default-recording failures are invisible while explicit-payload failures surface correctly.

```suggestion
                crate::set_mic_input(state.clone(), settings.mic_name).await?;
                crate::set_camera_input(app.clone(), state.clone(), settings.camera_id, None)
                    .await?;
```

### Issue 3 of 3
extensions/raycast/src/start-recording.ts:7-8
**`showHUD` fires before recording is confirmed**

`open("cap://record")` dispatches the URL to the OS but returns as soon as the handoff completes — it does not wait for Cap Desktop to accept and act on the link. `showHUD("Cap recording started")` therefore always runs, even if Cap is not installed, is not yet launched, or encounters an internal error starting the recording. The same pattern applies in `stop-recording.ts`. Consider omitting the HUD (or replacing it with a neutral message like "Sent to Cap") since there is no acknowledgement channel from the app over this protocol.

Reviews (1): Last reviewed commit: "fix(desktop): harden raycast deep-link s..." | Re-trigger Greptile

Greptile also left 3 inline comments on this PR.

@superagent-security superagent-security Bot added the contributor:verified Contributor passed trust analysis. label May 16, 2026
@superagent-security superagent-security Bot added the pr:flagged PR flagged for review by security analysis. label May 16, 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 2 security concern(s).

@JYZ-LESLIE
Copy link
Copy Markdown
Author

Addressed the security findings in commit e13e954:

  1. Deep-link start hardening
  • Added a native confirmation dialog before executing cap://record start action.
  • If user cancels, no recording starts.
  1. Publish script hardening
  • Replaced npx @raycast/api@latest publish with ray publish to avoid executing an unpinned latest package.

Validation note: Rust toolchain is unavailable in this runner, so I could not run cargo test locally here.

@superagent-security superagent-security Bot added pr:verified PR passed security analysis. and removed pr:flagged PR flagged for review by security analysis. labels May 16, 2026
Comment on lines +217 to +224
fn parses_cap_record_as_start_default_recording() {
let url = Url::parse("cap://record").expect("valid cap record url");
let action = DeepLinkAction::try_from(&url).expect("cap record should parse");
assert!(matches!(action, DeepLinkAction::StartDefaultRecording));
}

#[test]
fn parses_cap_stop_as_stop_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 parses_existing_action_format test will always fail

The match url.domain() block that precedes the JSON-parsing code has no arm that returns Ok — both arms always return Err(...), so the ? at the end short-circuits and the function exits with Err(Invalid) before it ever reaches query_pairs() or serde_json::from_str. For cap-desktop://action?value=..., url.domain() returns Some("action"), which falls through to the _ => Err(ActionParseFromUrlError::Invalid) arm. The test therefore panics at .expect("action deep link should parse"). Since Rust toolchain was unavailable when authoring this PR, this was not caught.

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

Comment:
**`parses_existing_action_format` test will always fail**

The `match url.domain()` block that precedes the JSON-parsing code has no arm that returns `Ok` — both arms always return `Err(...)`, so the `?` at the end short-circuits and the function exits with `Err(Invalid)` before it ever reaches `query_pairs()` or `serde_json::from_str`. For `cap-desktop://action?value=...`, `url.domain()` returns `Some("action")`, which falls through to the `_ => Err(ActionParseFromUrlError::Invalid)` arm. The test therefore panics at `.expect("action deep link should parse")`. Since Rust toolchain was unavailable when authoring this PR, this was not caught.

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

Comment on lines +131 to +133
.buttons(MessageDialogButtons::OkCancel)
.blocking_show();

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 Silent error discard for mic/camera setup differs from StartRecording path

set_mic_input and set_camera_input errors are swallowed with let _ =, so a recording can silently start without the user's saved microphone or camera. The StartRecording arm immediately below uses ? to propagate the same calls as hard errors — the inconsistency means default-recording failures are invisible while explicit-payload failures surface correctly.

Suggested change
.buttons(MessageDialogButtons::OkCancel)
.blocking_show();
crate::set_mic_input(state.clone(), settings.mic_name).await?;
crate::set_camera_input(app.clone(), state.clone(), settings.camera_id, None)
.await?;
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src-tauri/src/deeplink_actions.rs
Line: 131-133

Comment:
**Silent error discard for mic/camera setup differs from `StartRecording` path**

`set_mic_input` and `set_camera_input` errors are swallowed with `let _ =`, so a recording can silently start without the user's saved microphone or camera. The `StartRecording` arm immediately below uses `?` to propagate the same calls as hard errors — the inconsistency means default-recording failures are invisible while explicit-payload failures surface correctly.

```suggestion
                crate::set_mic_input(state.clone(), settings.mic_name).await?;
                crate::set_camera_input(app.clone(), state.clone(), settings.camera_id, None)
                    .await?;
```

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

Comment on lines +7 to +8
await showHUD("Cap recording started");
} catch (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.

P2 showHUD fires before recording is confirmed

open("cap://record") dispatches the URL to the OS but returns as soon as the handoff completes — it does not wait for Cap Desktop to accept and act on the link. showHUD("Cap recording started") therefore always runs, even if Cap is not installed, is not yet launched, or encounters an internal error starting the recording. The same pattern applies in stop-recording.ts. Consider omitting the HUD (or replacing it with a neutral message like "Sent to Cap") since there is no acknowledgement channel from the app over this protocol.

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

Comment:
**`showHUD` fires before recording is confirmed**

`open("cap://record")` dispatches the URL to the OS but returns as soon as the handoff completes — it does not wait for Cap Desktop to accept and act on the link. `showHUD("Cap recording started")` therefore always runs, even if Cap is not installed, is not yet launched, or encounters an internal error starting the recording. The same pattern applies in `stop-recording.ts`. Consider omitting the HUD (or replacing it with a neutral message like "Sent to Cap") since there is no acknowledgement channel from the app over this protocol.

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

@JYZ-LESLIE
Copy link
Copy Markdown
Author

Addressed the new Greptile review points in commit 6416043:

  1. Fixed action deep-link parsing for existing format
  • Switched the gate from url.domain() to url.host_str() and accept Some("action"), so cap-desktop://action?value=... now reaches JSON parsing instead of short-circuiting with Invalid.
  1. Propagated default recording setup errors
  • In StartDefaultRecording, set_mic_input and set_camera_input now use ? (no silent discard), matching StartRecording behavior.
  1. Made Raycast HUD messaging neutral
  • Updated HUD text to Sent start request to Cap / Sent stop request to Cap to avoid implying app-level acknowledgement.

Validation note: Rust toolchain is unavailable in this runner, so I could not execute cargo tests locally here.

@socket-security
Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedeslint@​8.57.18910010050100
Added@​raycast/​eslint-config@​1.0.11821005987100
Added@​types/​node@​20.8.101001008296100
Added@​raycast/​api@​1.104.179610084100100
Addedprettier@​3.8.3901009793100

View full report

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

Labels

🙋 Bounty claim contributor:verified Contributor passed trust analysis. pr:verified PR passed security analysis.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant