Skip to content

feat: extend deeplinks with pause/resume + Raycast extension#1687

Open
TateLyman wants to merge 2 commits intoCapSoftware:mainfrom
TateLyman:feat/deeplinks-raycast
Open

feat: extend deeplinks with pause/resume + Raycast extension#1687
TateLyman wants to merge 2 commits intoCapSoftware:mainfrom
TateLyman:feat/deeplinks-raycast

Conversation

@TateLyman
Copy link
Copy Markdown

@TateLyman TateLyman commented Mar 27, 2026

Summary

  • Adds PauseRecording, ResumeRecording, TogglePauseRecording deeplink actions to the existing deeplink system
  • Creates a Raycast extension with 4 commands: Start Recording, Stop Recording, Toggle Pause, Open Settings

Changes

Deeplink Actions (deeplink_actions.rs)

  • Added 3 new variants to DeepLinkAction enum
  • Wired them to existing pause_recording, resume_recording, toggle_pause_recording functions

Raycast Extension (extensions/raycast/)

  • start-recording — Starts screen recording via cap://action deeplink
  • stop-recording — Stops current recording
  • toggle-pause — Pauses or resumes recording
  • open-settings — Opens Cap settings window

Test plan

  • Test deeplinks via open cap://action?value=... in terminal
  • Test Raycast extension commands in Raycast
  • Verify pause/resume works during active recording

Closes #1540

Greptile Summary

This PR extends Cap's deeplink system with three new pause/resume actions and introduces a Raycast extension providing four no-view commands (Start, Stop, Toggle Pause, Open Settings). The Rust side is clean and correct; the primary concerns are all in the new Raycast extension.

Key changes:

  • deeplink_actions.rs: PauseRecording, ResumeRecording, and TogglePauseRecording variants added to DeepLinkAction and correctly wired to existing recording:: functions — no issues.
  • extensions/raycast/src/utils.ts: sendDeepLink is typed as action: object, which excludes string primitives. stop-recording.ts and toggle-pause.ts both pass string literals, producing TypeScript compile errors that will prevent the extension from building.
  • extensions/raycast/src/start-recording.ts: The display name "Main Display" is hard-coded. The Rust handler performs an exact string match against OS display names and returns an error (printed to stderr) when no match is found — the user sees the success HUD regardless.
  • extensions/raycast/package.json: References command-icon.png which is absent from the PR; the extension also lacks a tsconfig.json and a lock file, making it non-buildable out of the box.

Issues found:

  • P0 — TypeScript compile error: sendDeepLink(action: object) called with string in two files; fix by widening to string | Record<string, unknown>.
  • P1 — command-icon.png missing; extension cannot be built or submitted to the Raycast store without it.
  • P1 — Hard-coded "Main Display" will silently fail on non-English macOS or any machine with a differently-named primary display.
  • P2 — No error handling in sendDeepLink when Cap is not running; success HUD is shown unconditionally.
  • P2 — Missing tsconfig.json and lock file for reproducible builds.

Confidence Score: 2/5

Not safe to merge — the Raycast extension has a TypeScript compile error and is missing required assets, so it cannot be built in its current state.

The Rust deeplink additions are correct and complete, but the TypeScript extension has a blocking compile error (string passed where object is expected), a missing required icon asset, and no tsconfig/lock file. These issues make the extension non-functional out of the box and would fail Raycast's store validation. The hard-coded display name also creates a silent failure path for most users. At least the type error and missing icon need to be resolved before this is ready to ship.

extensions/raycast/src/utils.ts (type error), extensions/raycast/package.json (missing icon + config files), extensions/raycast/src/start-recording.ts (hard-coded display name)

Important Files Changed

Filename Overview
apps/desktop/src-tauri/src/deeplink_actions.rs Adds PauseRecording, ResumeRecording, and TogglePauseRecording enum variants and routes them to existing recording functions — clean, correct Rust; no issues found.
extensions/raycast/src/utils.ts sendDeepLink typed as action: object, which excludes string primitives — callers in stop-recording.ts and toggle-pause.ts pass strings, causing a TypeScript compile error; also missing error handling.
extensions/raycast/src/start-recording.ts Hard-codes the display name "Main Display" which will silently fail on non-English macOS locales or any system where the primary display has a different name.
extensions/raycast/package.json References command-icon.png which is missing from the PR; also lacks tsconfig.json and a lock file needed for a complete, reproducible Raycast extension.
extensions/raycast/src/stop-recording.ts Correct deeplink payload ("stop_recording"); only issue is the type mismatch at the call site due to utils.ts typing.
extensions/raycast/src/toggle-pause.ts Correct deeplink payload ("toggle_pause_recording"); only issue is the type mismatch at the call site due to utils.ts typing.
extensions/raycast/src/open-settings.ts Correct use of open_settings deeplink with null page — no issues found.

Sequence Diagram

sequenceDiagram
    participant User
    participant Raycast
    participant macOS as macOS URL Handler
    participant Cap as Cap (Tauri)
    participant Recording as recording.rs

    User->>Raycast: Invoke command (e.g. Toggle Pause)
    Raycast->>Raycast: sendDeepLink("toggle_pause_recording")
    Note over Raycast: JSON.stringify → encodeURIComponent
    Raycast->>macOS: open("cap://action?value=%22toggle_pause_recording%22")
    macOS->>Cap: URL scheme handler
    Cap->>Cap: DeepLinkAction::try_from(url)
    Note over Cap: serde_json::from_str → TogglePauseRecording
    Cap->>Recording: toggle_pause_recording(app, state)
    Recording-->>Cap: Ok(()) / Err(String)
    Cap-->>macOS: (fire-and-forget)
    Raycast->>User: showHUD("Recording pause toggled")
    Note over Raycast,User: HUD shown before action completes
Loading
Prompt To Fix All With AI
This is a comment left during a code review.
Path: extensions/raycast/src/utils.ts
Line: 3

Comment:
**`object` type excludes strings — TypeScript compile error**

`stop-recording.ts` and `toggle-pause.ts` both call `sendDeepLink("stop_recording")` and `sendDeepLink("toggle_pause_recording")` passing string literals. TypeScript's built-in `object` type explicitly excludes all primitive types (strings, numbers, booleans), so both call sites are type errors that will prevent the extension from building.

The fix is to widen the parameter type to accept primitives:

```suggestion
export async function sendDeepLink(action: string | Record<string, unknown>) {
```

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/start-recording.ts
Line: 7

Comment:
**Hard-coded display name will silently fail on most systems**

`"Main Display"` is matched by name against `cap_recording::screen_capture::list_displays()` in Rust. On non-English macOS locales and on any machine whose primary display doesn't carry that exact name, `find` returns `None`, which produces an `Err(...)` that is only printed to stderr. The user will still see the `"Recording started"` HUD but nothing will actually record.

Consider either documenting this limitation clearly or falling back to the first available display in the Rust handler. At minimum, a comment here explaining that this name must match the OS display name would help future contributors.

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/package.json
Line: 7

Comment:
**`command-icon.png` is referenced but not included in the PR**

`package.json` declares `"icon": "command-icon.png"`, but no such file appears in `extensions/raycast/`. Raycast validates the icon at build and submission time — the extension will fail to build or be rejected from the store without it. The icon must be a `512×512` PNG placed at the root of the extension directory.

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/utils.ts
Line: 4-6

Comment:
**No error handling when Cap is not running**

`open(url)` will silently succeed even when Cap is not installed or not running — the OS URL handler simply won't find a target and does nothing. The user will see the success HUD (e.g., "Recording started") with no indication that the action was never delivered.

Wrapping the call in a try/catch and showing an error HUD would significantly improve discoverability:

```ts
export async function sendDeepLink(action: string | Record<string, unknown>) {
  const value = encodeURIComponent(JSON.stringify(action));
  const url = `cap://action?value=${value}`;
  try {
    await open(url);
  } catch (e) {
    await showHUD("Failed to communicate with Cap — is it running?");
    throw e;
  }
}
```

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/package.json
Line: 1-23

Comment:
**Missing `tsconfig.json` and lock file**

A standard Raycast TypeScript extension requires both a `tsconfig.json` (Raycast's scaffolder ships one that sets `"moduleResolution": "bundler"` and `"jsx": "react-jsx"`) and a lock file (`package-lock.json` or `yarn.lock`) so that CI and reviewers can reproduce the exact dependency tree. Neither is present in this PR.

Without `tsconfig.json` the TypeScript compiler will use loose defaults, which can mask type errors. Without a lock file, `@raycast/api` will float to whatever `^1.50.0` resolves to at install time, potentially breaking the build later.

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

Reviews (1): Last reviewed commit: "feat: extend deeplinks with pause/resume..." | Re-trigger Greptile

Greptile also left 5 inline comments on this PR.

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

- Add PauseRecording, ResumeRecording, TogglePauseRecording deeplink actions
- Create Raycast extension with start/stop/toggle-pause/settings commands
- Extension uses cap:// deeplink protocol to control recording
@@ -0,0 +1,7 @@
import { open } from "@raycast/api";

export async function sendDeepLink(action: object) {
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.

P0 object type excludes strings — TypeScript compile error

stop-recording.ts and toggle-pause.ts both call sendDeepLink("stop_recording") and sendDeepLink("toggle_pause_recording") passing string literals. TypeScript's built-in object type explicitly excludes all primitive types (strings, numbers, booleans), so both call sites are type errors that will prevent the extension from building.

The fix is to widen the parameter type to accept primitives:

Suggested change
export async function sendDeepLink(action: object) {
export async function sendDeepLink(action: string | Record<string, unknown>) {
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/raycast/src/utils.ts
Line: 3

Comment:
**`object` type excludes strings — TypeScript compile error**

`stop-recording.ts` and `toggle-pause.ts` both call `sendDeepLink("stop_recording")` and `sendDeepLink("toggle_pause_recording")` passing string literals. TypeScript's built-in `object` type explicitly excludes all primitive types (strings, numbers, booleans), so both call sites are type errors that will prevent the extension from building.

The fix is to widen the parameter type to accept primitives:

```suggestion
export async function sendDeepLink(action: string | Record<string, unknown>) {
```

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

export default async function Command() {
await sendDeepLink({
start_recording: {
capture_mode: { screen: "Main Display" },
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 Hard-coded display name will silently fail on most systems

"Main Display" is matched by name against cap_recording::screen_capture::list_displays() in Rust. On non-English macOS locales and on any machine whose primary display doesn't carry that exact name, find returns None, which produces an Err(...) that is only printed to stderr. The user will still see the "Recording started" HUD but nothing will actually record.

Consider either documenting this limitation clearly or falling back to the first available display in the Rust handler. At minimum, a comment here explaining that this name must match the OS display name would help future contributors.

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

Comment:
**Hard-coded display name will silently fail on most systems**

`"Main Display"` is matched by name against `cap_recording::screen_capture::list_displays()` in Rust. On non-English macOS locales and on any machine whose primary display doesn't carry that exact name, `find` returns `None`, which produces an `Err(...)` that is only printed to stderr. The user will still see the `"Recording started"` HUD but nothing will actually record.

Consider either documenting this limitation clearly or falling back to the first available display in the Rust handler. At minimum, a comment here explaining that this name must match the OS display name would help future contributors.

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

"title": "Cap",
"description": "Control Cap screen recording from Raycast",
"icon": "command-icon.png",
"author": "TateLyman",
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 command-icon.png is referenced but not included in the PR

package.json declares "icon": "command-icon.png", but no such file appears in extensions/raycast/. Raycast validates the icon at build and submission time — the extension will fail to build or be rejected from the store without it. The icon must be a 512×512 PNG placed at the root of the extension directory.

Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/raycast/package.json
Line: 7

Comment:
**`command-icon.png` is referenced but not included in the PR**

`package.json` declares `"icon": "command-icon.png"`, but no such file appears in `extensions/raycast/`. Raycast validates the icon at build and submission time — the extension will fail to build or be rejected from the store without it. The icon must be a `512×512` PNG placed at the root of the extension directory.

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

Comment on lines +4 to +6
const value = encodeURIComponent(JSON.stringify(action));
const url = `cap://action?value=${value}`;
await open(url);
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 No error handling when Cap is not running

open(url) will silently succeed even when Cap is not installed or not running — the OS URL handler simply won't find a target and does nothing. The user will see the success HUD (e.g., "Recording started") with no indication that the action was never delivered.

Wrapping the call in a try/catch and showing an error HUD would significantly improve discoverability:

export async function sendDeepLink(action: string | Record<string, unknown>) {
  const value = encodeURIComponent(JSON.stringify(action));
  const url = `cap://action?value=${value}`;
  try {
    await open(url);
  } catch (e) {
    await showHUD("Failed to communicate with Cap — is it running?");
    throw e;
  }
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/raycast/src/utils.ts
Line: 4-6

Comment:
**No error handling when Cap is not running**

`open(url)` will silently succeed even when Cap is not installed or not running — the OS URL handler simply won't find a target and does nothing. The user will see the success HUD (e.g., "Recording started") with no indication that the action was never delivered.

Wrapping the call in a try/catch and showing an error HUD would significantly improve discoverability:

```ts
export async function sendDeepLink(action: string | Record<string, unknown>) {
  const value = encodeURIComponent(JSON.stringify(action));
  const url = `cap://action?value=${value}`;
  try {
    await open(url);
  } catch (e) {
    await showHUD("Failed to communicate with Cap — is it running?");
    throw e;
  }
}
```

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

Comment on lines +1 to +23
{
"$schema": "https://www.raycast.com/schemas/extension.json",
"name": "cap",
"title": "Cap",
"description": "Control Cap screen recording from Raycast",
"icon": "command-icon.png",
"author": "TateLyman",
"categories": ["Productivity"],
"license": "MIT",
"commands": [
{ "name": "start-recording", "title": "Start Recording", "description": "Start a new screen recording", "mode": "no-view" },
{ "name": "stop-recording", "title": "Stop Recording", "description": "Stop the current recording", "mode": "no-view" },
{ "name": "toggle-pause", "title": "Toggle Pause", "description": "Pause or resume the current recording", "mode": "no-view" },
{ "name": "open-settings", "title": "Open Settings", "description": "Open Cap settings", "mode": "no-view" }
],
"dependencies": {
"@raycast/api": "^1.50.0"
},
"devDependencies": {
"@raycast/eslint-config": "^1.0.6",
"typescript": "^5.0.0"
}
}
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 Missing tsconfig.json and lock file

A standard Raycast TypeScript extension requires both a tsconfig.json (Raycast's scaffolder ships one that sets "moduleResolution": "bundler" and "jsx": "react-jsx") and a lock file (package-lock.json or yarn.lock) so that CI and reviewers can reproduce the exact dependency tree. Neither is present in this PR.

Without tsconfig.json the TypeScript compiler will use loose defaults, which can mask type errors. Without a lock file, @raycast/api will float to whatever ^1.50.0 resolves to at install time, potentially breaking the build later.

Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/raycast/package.json
Line: 1-23

Comment:
**Missing `tsconfig.json` and lock file**

A standard Raycast TypeScript extension requires both a `tsconfig.json` (Raycast's scaffolder ships one that sets `"moduleResolution": "bundler"` and `"jsx": "react-jsx"`) and a lock file (`package-lock.json` or `yarn.lock`) so that CI and reviewers can reproduce the exact dependency tree. Neither is present in this PR.

Without `tsconfig.json` the TypeScript compiler will use loose defaults, which can mask type errors. Without a lock file, `@raycast/api` will float to whatever `^1.50.0` resolves to at install time, potentially breaking the build later.

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

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