Skip to content

feat: Support for Deep Links and Raycast Extension (#1540)#1701

Open
yemsy26 wants to merge 2 commits intoCapSoftware:mainfrom
yemsy26:feat/deeplink-raycast-support-1540
Open

feat: Support for Deep Links and Raycast Extension (#1540)#1701
yemsy26 wants to merge 2 commits intoCapSoftware:mainfrom
yemsy26:feat/deeplink-raycast-support-1540

Conversation

@yemsy26
Copy link
Copy Markdown

@yemsy26 yemsy26 commented Mar 31, 2026

PR: feat(deeplink): add recording controls and raycast extension #1540

Summary

This PR resolves Bounty #1540 by extending Cap's existing cap-desktop:// deep link infrastructure with full recording lifecycle controls and building a companion Raycast extension.


Changes

🦀 Rust — apps/desktop/src-tauri/src/deeplink_actions.rs

Extended the DeepLinkAction enum with five new variants:

Variant URL
PauseRecording cap-desktop://action?value={"type":"pauseRecording"}
ResumeRecording cap-desktop://action?value={"type":"resumeRecording"}
TogglePauseRecording cap-desktop://action?value={"type":"togglePauseRecording"}
SwitchMicrophone { label } cap-desktop://action?value={"type":"switchMicrophone","label":"<name>"}
SwitchCamera { id } cap-desktop://action?value={"type":"switchCamera","id":"<deviceId>"}

Implementation notes:

  • All new execute() arms use app.state::<ArcLock<App>>().read().await — no blocking calls or .unwrap().
  • TogglePauseRecording queries is_paused() and branches cleanly with the ? operator.
  • SwitchMicrophone and SwitchCamera delegate to the existing crate::set_mic_input() / crate::set_camera_input() functions, keeping logic DRY.
  • All serde fields use #[serde(rename_all = "camelCase")] and #[serde(tag = "type")] — consistent with the existing URL parser.

🔌 TypeScript — extensions/raycast/

New Raycast extension with two commands:

cap-control (Recording Controls)

  • Lists six actions: Start Studio, Start Instant, Stop, Pause, Resume, Toggle Pause.
  • Stop recording has a confirmation dialog to prevent accidental stops.
  • Each item also offers a "Copy Deep Link URL" action for automation integrations.
  • Fully stateless — Cap handles all state transitions.

switch-device (Switch Input Device)

  • On first run, prompts the user for a Cap API key (stored in Raycast's encrypted LocalStorage — never hard-coded).
  • Fetches live microphone / camera lists from https://api.cap.so/v1/devices/*.
  • "Disable Microphone" / "Disable Camera" items send null label/id to mute the input.
  • Includes a "Reset API Key" action for easy credential rotation.

Testing Instructions

Deep Links (Rust)

After building the app (pnpm turbo build --filter @cap/desktop), test each action from Terminal:

# Start a recording
open 'cap-desktop://action?value={"type":"startRecording","captureMode":{"screen":"Built-in Display"},"camera":null,"micLabel":null,"captureSystemAudio":false,"mode":"studio"}'

# Pause the recording
open 'cap-desktop://action?value={"type":"pauseRecording"}'

# Toggle pause
open 'cap-desktop://action?value={"type":"togglePauseRecording"}'

# Resume
open 'cap-desktop://action?value={"type":"resumeRecording"}'

# Stop
open 'cap-desktop://action?value={"type":"stopRecording"}'

# Switch microphone (replace label with actual device name)
open 'cap-desktop://action?value={"type":"switchMicrophone","label":"MacBook Pro Microphone"}'

# Disable camera
open 'cap-desktop://action?value={"type":"switchCamera","id":null}'

Verified states to test:

  • Trigger Pause when no recording is active → returns error, no crash
  • Trigger Resume when recording is not paused → returns error, no crash
  • Toggle Pause while recording → pauses correctly
  • Toggle Pause while paused → resumes correctly
  • Switch mic while recording is active → switches seamlessly

Raycast Extension

cd extensions/raycast
npm install
npm run build
# Then import the extension in Raycast: Preferences → Extensions → + Import Extension
  1. Open "Cap: Recording Controls" → select "Toggle Pause / Resume" → verify Cap responds.
  2. Open "Cap: Switch Input Device" → enter API key when prompted → select a microphone → verify Cap switches.
  3. Open "Cap: Switch Input Device" → "Reset API Key" → confirm prompt appears again.

Checklist

  • No .unwrap() calls in Rust
  • All new Rust fields use #[serde(rename_all = "camelCase")]
  • API key stored in Raycast LocalStorage, never hard-coded
  • Each modified file carries // Fix for Issue #1540 - Deep Links & Raycast Support
  • Follows existing cap-desktop://action?value=<JSON> URL schema
  • Existing StartRecording, StopRecording, OpenEditor, OpenSettings variants unchanged

Closes #1540
/claim #1540

Greptile Summary

This PR extends Cap's cap-desktop:// deep link system with five new recording lifecycle actions (PauseRecording, ResumeRecording, TogglePauseRecording, SwitchMicrophone, SwitchCamera) and adds a companion Raycast extension with two commands. The overall architecture is solid — actions are well-structured, follow existing patterns, and avoid .unwrap() calls — but three functional bugs need to be fixed before this is ready to merge.

Key issues found:

  • CaptureMode serde attribute mismatch (P1): The #[serde(tag = "type")] attribute added to CaptureMode is incompatible with Screen(String) / Window(String) tuple newtype variants. Serde's internally-tagged mode cannot merge a type discriminator into a bare JSON string. The TypeScript format {"screen": "Built-in Display"} (externally-tagged) won't deserialize with this attribute present, breaking every startRecording deep link. The fix is to remove tag = "type" from CaptureMode only.

  • Missing RecordingEvent emissions (P1): The new PauseRecording, ResumeRecording, and TogglePauseRecording deep link arms perform the operation but never emit RecordingEvent::Paused / RecordingEvent::Resumed. The existing Tauri commands always emit these events so the UI can update its pause indicator. Omitting them here leaves the UI state stale after a deep-link-triggered pause or resume.

  • SwitchCamera type mismatch (P1): The Raycast extension sends id: cam.deviceId — a plain string — but Rust's DeviceOrModelID is an externally-tagged enum ({"DeviceID": "..."} or {"ModelID": {...}}). A bare string will fail deserialization, silently rejecting every camera switch request from the extension.

  • Missing error handling in handleDisableMic / handleDisableCam (P2): These helpers call open() without try/catch, unlike the sendSwitch utility used for all other device actions.

Confidence Score: 4/5

Not safe to merge — three P1 functional bugs cause startRecording deep links to fail, UI state desync on pause/resume, and broken camera switching from Raycast.

Three separate P1 issues cause core functionality to fail at runtime: the CaptureMode serde tag breaks startRecording deserialization, missing RecordingEvent emissions desync the UI, and the camera-ID type mismatch silently rejects every switchCamera request from the extension. Each is a targeted, well-scoped fix, so the score remains 4 rather than lower — the overall structure is sound and no data loss or security risks are present.

apps/desktop/src-tauri/src/deeplink_actions.rs (CaptureMode tag + RecordingEvent emissions) and extensions/raycast/src/switch-device.tsx (DeviceOrModelID format).

Important Files Changed

Filename Overview
apps/desktop/src-tauri/src/deeplink_actions.rs Adds PauseRecording, ResumeRecording, TogglePauseRecording, SwitchMicrophone, SwitchCamera deep link actions. Two P1 bugs: (1) CaptureMode uses serde tag="type" which is incompatible with tuple newtype variants, breaking startRecording deserialization; (2) pause/resume/toggle actions don't emit RecordingEvent, leaving the UI out of sync.
extensions/raycast/src/switch-device.tsx New Raycast extension for switching input devices. P1 bug: sends camera deviceId as a plain string, but Rust's DeviceOrModelID enum requires externally-tagged format {"DeviceID": "..."}. P2: handleDisableMic/Cam lack error handling.
extensions/raycast/src/cap-control.tsx New Raycast command for recording controls. Deep link builder and action types are well-structured; confirmation dialog for stop is a nice UX touch. No major issues.
extensions/raycast/package.json Standard Raycast extension manifest with appropriate dependencies and scripts.
extensions/raycast/tsconfig.json Standard TypeScript config for Raycast extension targeting ES2022.
extensions/raycast/README.md Clear extension documentation covering commands, setup, URL schema, and security notes.

Sequence Diagram

sequenceDiagram
    participant User
    participant Raycast as Raycast Extension
    participant OS as macOS open()
    participant Cap as Cap Desktop (Tauri)
    participant UI as Cap UI

    Note over Raycast: cap-control.tsx
    User->>Raycast: Select "Pause Recording"
    Raycast->>OS: open(cap-desktop://action?value={"type":"pauseRecording"})
    OS->>Cap: deep link event
    Cap->>Cap: DeepLinkAction::try_from(&url)
    Cap->>Cap: PauseRecording.execute()
    Cap->>Cap: recording.pause().await
    Note over Cap,UI: ⚠️ RecordingEvent::Paused NOT emitted — UI stays stale

    Note over Raycast: switch-device.tsx
    User->>Raycast: Select camera "HD Webcam"
    Raycast->>OS: open(cap-desktop://action?value={"type":"switchCamera","id":"device-123"})
    OS->>Cap: deep link event
    Cap->>Cap: serde_json::from_str — id="device-123"
    Note over Cap: ⚠️ DeviceOrModelID expects {"DeviceID":"device-123"} — parse fails

    Note over Raycast: cap-control.tsx
    User->>Raycast: Select "Start Recording"
    Raycast->>OS: open(cap-desktop://action?value={"type":"startRecording","captureMode":{"screen":"Built-in Display"},...})
    OS->>Cap: deep link event
    Cap->>Cap: serde_json::from_str — CaptureMode {"screen":"..."}
    Note over Cap: ⚠️ tag="type" on CaptureMode breaks deserialization
Loading

Comments Outside Diff (2)

  1. extensions/raycast/src/switch-device.tsx, line 739-743 (link)

    P1 switchCamera sends a plain string id but Rust expects a tagged DeviceOrModelID

    cam.deviceId is a plain string from the API response, so the JSON sent over the deep link is:

    {"type": "switchCamera", "id": "some-device-id"}

    On the Rust side, SwitchCamera { id: Option<DeviceOrModelID> } uses:

    pub enum DeviceOrModelID {
        DeviceID(String),
        ModelID(cap_camera::ModelID),
    }

    With default serde derive this is an externally-tagged enum — it deserializes from {"DeviceID": "some-device-id"}, not from a bare string. Sending a bare string will cause serde to return a ParseFailed error and the camera switch will silently do nothing.

    The TypeScript type and JSON payload need to match the Rust enum structure. Assuming device IDs from the API are always device IDs (not model IDs), the action should be:

    : { type: "switchCamera", id: { DeviceID: cam.deviceId } };

    And the TypeScript type for the id field should be updated accordingly:

    type SwitchCameraAction = { type: "switchCamera"; id: { DeviceID: string } | { ModelID: string } | null };
    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: extensions/raycast/src/switch-device.tsx
    Line: 739-743
    
    Comment:
    **`switchCamera` sends a plain string `id` but Rust expects a tagged `DeviceOrModelID`**
    
    `cam.deviceId` is a plain `string` from the API response, so the JSON sent over the deep link is:
    
    ```json
    {"type": "switchCamera", "id": "some-device-id"}
    ```
    
    On the Rust side, `SwitchCamera { id: Option<DeviceOrModelID> }` uses:
    
    ```rust
    pub enum DeviceOrModelID {
        DeviceID(String),
        ModelID(cap_camera::ModelID),
    }
    ```
    
    With default serde derive this is an **externally-tagged** enum — it deserializes from `{"DeviceID": "some-device-id"}`, **not** from a bare string. Sending a bare string will cause serde to return a `ParseFailed` error and the camera switch will silently do nothing.
    
    The TypeScript type and JSON payload need to match the Rust enum structure. Assuming device IDs from the API are always device IDs (not model IDs), the action should be:
    
    ```typescript
    : { type: "switchCamera", id: { DeviceID: cam.deviceId } };
    ```
    
    And the TypeScript type for the `id` field should be updated accordingly:
    ```typescript
    type SwitchCameraAction = { type: "switchCamera"; id: { DeviceID: string } | { ModelID: string } | null };
    ```
    
    How can I resolve this? If you propose a fix, please make it concise.
  2. extensions/raycast/src/switch-device.tsx, line 924-934 (link)

    P2 Missing error handling in handleDisableMic / handleDisableCam

    Unlike sendSwitch(), which wraps open() in a try/catch and shows a failure toast on error, handleDisableMic and handleDisableCam call open() directly without any error handling. If the open() call rejects (e.g., Cap is not installed), the error will be an unhandled promise rejection.

    Consider extracting a shared helper for the disable paths, or at minimum add a try/catch:

    async function handleDisableMic() {
      const action: SwitchMicrophoneAction = { type: "switchMicrophone", label: null };
      try {
        await open(buildDeepLink(action));
        await showToast({ style: Toast.Style.Success, title: "Cap — Microphone disabled" });
      } catch (error) {
        await showToast({
          style: Toast.Style.Failure,
          title: "Cap — Switch Failed",
          message: error instanceof Error ? error.message : String(error),
        });
      }
    }

    The same applies to handleDisableCam.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: extensions/raycast/src/switch-device.tsx
    Line: 924-934
    
    Comment:
    **Missing error handling in `handleDisableMic` / `handleDisableCam`**
    
    Unlike `sendSwitch()`, which wraps `open()` in a `try/catch` and shows a failure toast on error, `handleDisableMic` and `handleDisableCam` call `open()` directly without any error handling. If the `open()` call rejects (e.g., Cap is not installed), the error will be an unhandled promise rejection.
    
    Consider extracting a shared helper for the disable paths, or at minimum add a `try/catch`:
    
    ```typescript
    async function handleDisableMic() {
      const action: SwitchMicrophoneAction = { type: "switchMicrophone", label: null };
      try {
        await open(buildDeepLink(action));
        await showToast({ style: Toast.Style.Success, title: "Cap — Microphone disabled" });
      } catch (error) {
        await showToast({
          style: Toast.Style.Failure,
          title: "Cap — Switch Failed",
          message: error instanceof Error ? error.message : String(error),
        });
      }
    }
    ```
    
    The same applies to `handleDisableCam`.
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: apps/desktop/src-tauri/src/deeplink_actions.rs
Line: 27-32

Comment:
**`CaptureMode` incompatible with `#[serde(tag = "type")]`**

`CaptureMode::Screen(String)` and `Window(String)` are newtype tuple variants wrapping a primitive (`String`). Serde's internally-tagged representation (`tag = "type"`) requires newtype variants to wrap a **struct/map-like type** — it cannot be used with primitives because there is no way to merge `{"type": "screen"}` with a bare JSON string value.

With this attribute, serde will either produce a compile-time error or silently fail at runtime when deserializing a `startRecording` deep link payload like:
```json
{"captureMode": {"screen": "Built-in Display"}}
```

That format is **externally-tagged** (the variant name is the JSON key), which requires **no** `tag` attribute. The fix is to remove `tag = "type"` from `CaptureMode`:

```suggestion
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub enum CaptureMode {
    Screen(String),
    Window(String),
}
```

Note: `rename_all = "camelCase"` still works here because `"Screen"``"screen"` and `"Window"``"window"` happen to be the same in both camelCase and snake_case for single-word variants.

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

---

This is a comment left during a code review.
Path: apps/desktop/src-tauri/src/deeplink_actions.rs
Line: 226-293

Comment:
**Missing `RecordingEvent` emissions after pause/resume/toggle**

The existing Tauri commands for these operations always emit a `RecordingEvent` so the UI can react to the state change:

```rust
// from recording.rs
RecordingEvent::Paused.emit(&app).ok();   // after pause
RecordingEvent::Resumed.emit(&app).ok();  // after resume
```

The three new deep link arms (`PauseRecording`, `ResumeRecording`, `TogglePauseRecording`) perform the operation but never emit these events. Any UI component or listener subscribed to `RecordingEvent` will not be notified when the action arrives via deep link, causing the displayed pause state to become stale.

Each arm should emit the corresponding event after a successful operation, e.g.:

```rust
DeepLinkAction::PauseRecording => {
    let state = app.state::<ArcLock<App>>();
    let app_lock = state.read().await;

    let recording = app_lock
        .current_recording()
        .ok_or_else(|| "No active recording to pause".to_string())?;

    recording
        .pause()
        .await
        .map_err(|e| format!("Failed to pause recording: {e}"))?;

    RecordingEvent::Paused.emit(app).ok();
    Ok(())
}
```

The same pattern applies to `ResumeRecording` (`RecordingEvent::Resumed`) and `TogglePauseRecording` (branch on `is_paused` to emit the correct event).

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

---

This is a comment left during a code review.
Path: extensions/raycast/src/switch-device.tsx
Line: 739-743

Comment:
**`switchCamera` sends a plain string `id` but Rust expects a tagged `DeviceOrModelID`**

`cam.deviceId` is a plain `string` from the API response, so the JSON sent over the deep link is:

```json
{"type": "switchCamera", "id": "some-device-id"}
```

On the Rust side, `SwitchCamera { id: Option<DeviceOrModelID> }` uses:

```rust
pub enum DeviceOrModelID {
    DeviceID(String),
    ModelID(cap_camera::ModelID),
}
```

With default serde derive this is an **externally-tagged** enum — it deserializes from `{"DeviceID": "some-device-id"}`, **not** from a bare string. Sending a bare string will cause serde to return a `ParseFailed` error and the camera switch will silently do nothing.

The TypeScript type and JSON payload need to match the Rust enum structure. Assuming device IDs from the API are always device IDs (not model IDs), the action should be:

```typescript
: { type: "switchCamera", id: { DeviceID: cam.deviceId } };
```

And the TypeScript type for the `id` field should be updated accordingly:
```typescript
type SwitchCameraAction = { type: "switchCamera"; id: { DeviceID: string } | { ModelID: string } | null };
```

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

---

This is a comment left during a code review.
Path: extensions/raycast/src/switch-device.tsx
Line: 924-934

Comment:
**Missing error handling in `handleDisableMic` / `handleDisableCam`**

Unlike `sendSwitch()`, which wraps `open()` in a `try/catch` and shows a failure toast on error, `handleDisableMic` and `handleDisableCam` call `open()` directly without any error handling. If the `open()` call rejects (e.g., Cap is not installed), the error will be an unhandled promise rejection.

Consider extracting a shared helper for the disable paths, or at minimum add a `try/catch`:

```typescript
async function handleDisableMic() {
  const action: SwitchMicrophoneAction = { type: "switchMicrophone", label: null };
  try {
    await open(buildDeepLink(action));
    await showToast({ style: Toast.Style.Success, title: "Cap — Microphone disabled" });
  } catch (error) {
    await showToast({
      style: Toast.Style.Failure,
      title: "Cap — Switch Failed",
      message: error instanceof Error ? error.message : String(error),
    });
  }
}
```

The same applies to `handleDisableCam`.

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

Reviews (1): Last reviewed commit: "<!-- Fix for Issue #1540 - Deep Links & ..." | Re-trigger Greptile

Greptile also left 2 inline comments on this PR.

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

# Commit & Git Workflow

## Commit Message (Conventional Commits)

```
feat(deeplink): add recording controls and raycast extension CapSoftware#1540

Extends the DeepLinkAction enum with PauseRecording, ResumeRecording,
TogglePauseRecording, SwitchMicrophone, and SwitchCamera variants.
Adds a companion Raycast extension with two commands: cap-control
(recording lifecycle) and switch-device (mic/camera switcher).

All Rust code uses the ? operator for error propagation (no .unwrap).
API key in Raycast is stored via LocalStorage, never hard-coded.

Closes CapSoftware#1540
```

---

## Git Commands to Submit the PR

### Step 1 — Fork & clone (if not already done)

```bash
# Fork the repo on GitHub first, then:
git clone https://github.com/<YOUR-USERNAME>/Cap.git
cd Cap
git remote add upstream https://github.com/CapSoftware/Cap.git
```

### Step 2 — Create a feature branch

```bash
git checkout -b feat/deeplink-raycast-1540
```

### Step 3 — Copy the generated files

```bash
# Rust change
cp path/to/cap-bounty-1540/apps/desktop/src-tauri/src/deeplink_actions.rs \
   apps/desktop/src-tauri/src/deeplink_actions.rs

# Raycast extension
cp -r path/to/cap-bounty-1540/extensions/raycast extensions/raycast
```

### Step 4 — Stage and commit

```bash
git add apps/desktop/src-tauri/src/deeplink_actions.rs
git add extensions/raycast/
git commit -m "feat(deeplink): add recording controls and raycast extension CapSoftware#1540

Extends the DeepLinkAction enum with PauseRecording, ResumeRecording,
TogglePauseRecording, SwitchMicrophone, and SwitchCamera variants.
Adds a companion Raycast extension with two commands: cap-control
(recording lifecycle) and switch-device (mic/camera switcher).

All Rust code uses the ? operator for error propagation (no .unwrap).
API key in Raycast is stored via LocalStorage, never hard-coded.

Closes CapSoftware#1540"
```

### Step 5 — Push and open the PR

```bash
git push origin feat/deeplink-raycast-1540
# Then open GitHub and create the PR from feat/deeplink-raycast-1540 → main
# Paste the contents of PR_DESCRIPTION.md into the PR body.
```

---

## Pre-PR Checklist

```bash
# Rust syntax check (no full build needed for CI check)
cd apps/desktop
cargo check

# TypeScript lint
cd extensions/raycast
npm install
npm run lint
npm run build
```
Comment on lines +27 to 32
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase", tag = "type")]
pub enum CaptureMode {
Screen(String),
Window(String),
}
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 CaptureMode incompatible with #[serde(tag = "type")]

CaptureMode::Screen(String) and Window(String) are newtype tuple variants wrapping a primitive (String). Serde's internally-tagged representation (tag = "type") requires newtype variants to wrap a struct/map-like type — it cannot be used with primitives because there is no way to merge {"type": "screen"} with a bare JSON string value.

With this attribute, serde will either produce a compile-time error or silently fail at runtime when deserializing a startRecording deep link payload like:

{"captureMode": {"screen": "Built-in Display"}}

That format is externally-tagged (the variant name is the JSON key), which requires no tag attribute. The fix is to remove tag = "type" from CaptureMode:

Suggested change
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase", tag = "type")]
pub enum CaptureMode {
Screen(String),
Window(String),
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub enum CaptureMode {
Screen(String),
Window(String),
}

Note: rename_all = "camelCase" still works here because "Screen""screen" and "Window""window" happen to be the same in both camelCase and snake_case for single-word variants.

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

Comment:
**`CaptureMode` incompatible with `#[serde(tag = "type")]`**

`CaptureMode::Screen(String)` and `Window(String)` are newtype tuple variants wrapping a primitive (`String`). Serde's internally-tagged representation (`tag = "type"`) requires newtype variants to wrap a **struct/map-like type** — it cannot be used with primitives because there is no way to merge `{"type": "screen"}` with a bare JSON string value.

With this attribute, serde will either produce a compile-time error or silently fail at runtime when deserializing a `startRecording` deep link payload like:
```json
{"captureMode": {"screen": "Built-in Display"}}
```

That format is **externally-tagged** (the variant name is the JSON key), which requires **no** `tag` attribute. The fix is to remove `tag = "type"` from `CaptureMode`:

```suggestion
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub enum CaptureMode {
    Screen(String),
    Window(String),
}
```

Note: `rename_all = "camelCase"` still works here because `"Screen"``"screen"` and `"Window"``"window"` happen to be the same in both camelCase and snake_case for single-word variants.

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

Comment on lines +226 to +293
DeepLinkAction::PauseRecording => {
let state = app.state::<ArcLock<App>>();
let app_lock = state.read().await;

let recording = app_lock
.current_recording()
.ok_or_else(|| "No active recording to pause".to_string())?;

recording
.pause()
.await
.map_err(|e| format!("Failed to pause recording: {e}"))
}

// ----------------------------------------------------------------
// Resume Recording
// ----------------------------------------------------------------
DeepLinkAction::ResumeRecording => {
let state = app.state::<ArcLock<App>>();
let app_lock = state.read().await;

let recording = app_lock
.current_recording()
.ok_or_else(|| "No active recording to resume".to_string())?;

let is_paused = recording
.is_paused()
.await
.map_err(|e| format!("Failed to query pause state: {e}"))?;

if !is_paused {
return Err("Recording is not currently paused".to_string());
}

recording
.resume()
.await
.map_err(|e| format!("Failed to resume recording: {e}"))
}

// ----------------------------------------------------------------
// Toggle Pause / Resume
// ----------------------------------------------------------------
DeepLinkAction::TogglePauseRecording => {
let state = app.state::<ArcLock<App>>();
let app_lock = state.read().await;

let recording = app_lock
.current_recording()
.ok_or_else(|| "No active recording".to_string())?;

let is_paused = recording
.is_paused()
.await
.map_err(|e| format!("Failed to query pause state: {e}"))?;

if is_paused {
recording
.resume()
.await
.map_err(|e| format!("Failed to resume recording: {e}"))
} else {
recording
.pause()
.await
.map_err(|e| format!("Failed to pause recording: {e}"))
}
}
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 Missing RecordingEvent emissions after pause/resume/toggle

The existing Tauri commands for these operations always emit a RecordingEvent so the UI can react to the state change:

// from recording.rs
RecordingEvent::Paused.emit(&app).ok();   // after pause
RecordingEvent::Resumed.emit(&app).ok();  // after resume

The three new deep link arms (PauseRecording, ResumeRecording, TogglePauseRecording) perform the operation but never emit these events. Any UI component or listener subscribed to RecordingEvent will not be notified when the action arrives via deep link, causing the displayed pause state to become stale.

Each arm should emit the corresponding event after a successful operation, e.g.:

DeepLinkAction::PauseRecording => {
    let state = app.state::<ArcLock<App>>();
    let app_lock = state.read().await;

    let recording = app_lock
        .current_recording()
        .ok_or_else(|| "No active recording to pause".to_string())?;

    recording
        .pause()
        .await
        .map_err(|e| format!("Failed to pause recording: {e}"))?;

    RecordingEvent::Paused.emit(app).ok();
    Ok(())
}

The same pattern applies to ResumeRecording (RecordingEvent::Resumed) and TogglePauseRecording (branch on is_paused to emit the correct event).

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

Comment:
**Missing `RecordingEvent` emissions after pause/resume/toggle**

The existing Tauri commands for these operations always emit a `RecordingEvent` so the UI can react to the state change:

```rust
// from recording.rs
RecordingEvent::Paused.emit(&app).ok();   // after pause
RecordingEvent::Resumed.emit(&app).ok();  // after resume
```

The three new deep link arms (`PauseRecording`, `ResumeRecording`, `TogglePauseRecording`) perform the operation but never emit these events. Any UI component or listener subscribed to `RecordingEvent` will not be notified when the action arrives via deep link, causing the displayed pause state to become stale.

Each arm should emit the corresponding event after a successful operation, e.g.:

```rust
DeepLinkAction::PauseRecording => {
    let state = app.state::<ArcLock<App>>();
    let app_lock = state.read().await;

    let recording = app_lock
        .current_recording()
        .ok_or_else(|| "No active recording to pause".to_string())?;

    recording
        .pause()
        .await
        .map_err(|e| format!("Failed to pause recording: {e}"))?;

    RecordingEvent::Paused.emit(app).ok();
    Ok(())
}
```

The same pattern applies to `ResumeRecording` (`RecordingEvent::Resumed`) and `TogglePauseRecording` (branch on `is_paused` to emit the correct event).

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

@yemsy26
Copy link
Copy Markdown
Author

yemsy26 commented Mar 31, 2026

I've pushed the requested changes to address the feedback:

Fixed Serde Serialization: Removed the incompatible tag = "type" from CaptureMode to ensure correct deserialization of screen/window payloads.

UI State Sync: Added RecordingEvent emissions (Paused / Resumed) in all relevant deep link handlers. This ensures the recording overlay and UI components reflect the correct state immediately after a deep link action.

Everything is ready for a final review! /claim #1540

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.

Bounty: Deeplinks support + Raycast Extension

2 participants