Skip to content

feat: add Raycast extension and new deeplink actions#1599

Open
omair445 wants to merge 3 commits intoCapSoftware:mainfrom
omair445:feat/deeplinks-raycast-extension
Open

feat: add Raycast extension and new deeplink actions#1599
omair445 wants to merge 3 commits intoCapSoftware:mainfrom
omair445:feat/deeplinks-raycast-extension

Conversation

@omair445
Copy link

@omair445 omair445 commented Feb 14, 2026

hey! this adds a Raycast extension for Cap and extends the deeplink system with new actions.

new deeplink actions (Rust):

  • PauseRecording, ResumeRecording, TogglePause
  • SetCamera, SetMicrophone
  • all wired to existing recording/device functions

Raycast extension (apps/raycast-extension/):

  • Start Recording — kicks off a new capture
  • Stop Recording — ends current recording
  • Toggle Pause — pause/resume mid-recording
  • Open Settings — jumps to Cap preferences
  • Recent Recordings — browse your latest captures

everything uses cap-desktop:// deeplinks to communicate with the running app so there's no need for a separate API server or IPC setup.

resolves #1540

Greptile Overview

Greptile Summary

Added Raycast extension for controlling Cap recordings and extended the desktop app's deeplink system with pause/resume/toggle actions and device switching capabilities. The Raycast extension provides 5 commands (start/stop/toggle-pause recording, open settings, list recent recordings) that communicate with the Cap desktop app through cap-desktop:// deeplinks, eliminating the need for a separate API server or IPC mechanism.

Key changes:

  • Extended DeepLinkAction enum with PauseRecording, ResumeRecording, TogglePause, SetCamera, and SetMicrophone variants
  • All new actions properly wired to existing recording functions in apps/desktop/src-tauri/src/recording.rs
  • New apps/raycast-extension/ directory with TypeScript implementation using @raycast/api
  • Deeplink builder properly encodes JSON action payloads and handles errors with user-friendly toasts

Issues found:

  • README example uses invalid "mode":"normal" (should be "studio", "instant", or "screenshot")
  • Start recording command hardcodes "Main Display" which will fail if user's display has a different name
  • Recent recordings feature uses macOS-specific path (~/Library/Application Support/so.cap.desktop/recordings) preventing Windows compatibility

Confidence Score: 4/5

  • Safe to merge with minor issues that affect specific use cases
  • Core implementation is solid with proper type safety and error handling, but has platform/configuration-specific issues (hardcoded display name, macOS-only path, README typo) that limit robustness
  • Pay attention to apps/raycast-extension/src/start-recording.ts and apps/raycast-extension/src/list-recent-recordings.tsx for cross-platform and configuration concerns

Important Files Changed

Filename Overview
apps/desktop/src-tauri/src/deeplink_actions.rs Added new deeplink actions for pause/resume/toggle recording and device switching, all properly wired to existing recording functions
apps/raycast-extension/README.md Documents extension usage and deeplink format; contains one typo in example (uses 'normal' instead of valid mode)
apps/raycast-extension/src/deeplink.ts Core deeplink builder and opener with proper TypeScript types matching Rust enums and good error handling
apps/raycast-extension/src/start-recording.ts Starts recording with hardcoded 'Main Display' and studio mode; may fail if display name differs
apps/raycast-extension/src/list-recent-recordings.tsx Lists recordings from hardcoded macOS path; will not work on Windows and path may vary across users

Sequence Diagram

sequenceDiagram
    participant User
    participant Raycast
    participant DeeplinkBuilder
    participant OS
    participant CapApp
    participant DeeplinkHandler
    participant RecordingModule

    User->>Raycast: Execute command (e.g., Start Recording)
    Raycast->>DeeplinkBuilder: buildDeeplink(action)
    DeeplinkBuilder->>DeeplinkBuilder: JSON.stringify(action)
    DeeplinkBuilder->>DeeplinkBuilder: encodeURIComponent(json)
    DeeplinkBuilder-->>Raycast: cap-desktop://action?value=...
    Raycast->>OS: open(url)
    OS->>CapApp: Route deeplink to Cap
    CapApp->>DeeplinkHandler: handle(url)
    DeeplinkHandler->>DeeplinkHandler: Parse URL and extract action
    DeeplinkHandler->>DeeplinkHandler: Deserialize JSON to DeepLinkAction
    DeeplinkHandler->>RecordingModule: execute(action)
    alt StartRecording
        RecordingModule->>RecordingModule: Set camera/mic inputs
        RecordingModule->>RecordingModule: Resolve capture target
        RecordingModule->>RecordingModule: start_recording()
    else PauseRecording
        RecordingModule->>RecordingModule: pause_recording()
    else TogglePause
        RecordingModule->>RecordingModule: toggle_pause_recording()
    else SetCamera/SetMicrophone
        RecordingModule->>RecordingModule: set_camera_input() / set_mic_input()
    end
    RecordingModule-->>CapApp: Result
    CapApp-->>Raycast: Success/Failure (via toast)
Loading

Last reviewed commit: 7bc798f

Extends the existing deeplink system with new actions:
- pause_recording, resume_recording, toggle_pause
- set_camera, set_microphone

Adds a Raycast extension (apps/raycast-extension) with commands:
- Start/Stop Recording
- Toggle Pause
- Open Settings
- Recent Recordings browser

Closes CapSoftware#1540, CapSoftware#28
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.

11 files reviewed, 3 comments

Edit Code Review Agent Settings | Greptile


Actions with parameters:
```
cap-desktop://action?value={"start_recording":{"capture_mode":{"screen":"Main Display"},"camera":null,"mic_label":null,"capture_system_audio":false,"mode":"normal"}}
Copy link
Contributor

Choose a reason for hiding this comment

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

"mode":"normal" is invalid - use "studio", "instant", or "screenshot" to match RecordingMode enum

Suggested change
cap-desktop://action?value={"start_recording":{"capture_mode":{"screen":"Main Display"},"camera":null,"mic_label":null,"capture_system_audio":false,"mode":"normal"}}
cap-desktop://action?value={"start_recording":{"capture_mode":{"screen":"Main Display"},"camera":null,"mic_label":null,"capture_system_audio":false,"mode":"studio"}}
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/raycast-extension/README.md
Line: 46:46

Comment:
`"mode":"normal"` is invalid - use `"studio"`, `"instant"`, or `"screenshot"` to match `RecordingMode` enum

```suggestion
cap-desktop://action?value={"start_recording":{"capture_mode":{"screen":"Main Display"},"camera":null,"mic_label":null,"capture_system_audio":false,"mode":"studio"}}
```

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

await openDeeplink(
{
start_recording: {
capture_mode: { screen: "Main 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 "Main Display" will fail if user's primary display has different name

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

Comment:
hardcoded `"Main Display"` will fail if user's primary display has different name

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

Comment on lines +10 to +16
return join(
homedir(),
"Library",
"Application Support",
"so.cap.desktop",
"recordings",
);
Copy link
Contributor

Choose a reason for hiding this comment

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

hardcoded macOS path will prevent extension from working on Windows

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/raycast-extension/src/list-recent-recordings.tsx
Line: 10:16

Comment:
hardcoded macOS path will prevent extension from working on Windows

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

@@ -0,0 +1,78 @@
import { Action, ActionPanel, List, getApplications } from "@raycast/api";
Copy link

Choose a reason for hiding this comment

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

getApplications is unused here; worth dropping to keep the extension lint-clean.

Suggested change
import { Action, ActionPanel, List, getApplications } from "@raycast/api";
import { Action, ActionPanel, List } from "@raycast/api";

Copy link
Author

Choose a reason for hiding this comment

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

good catch, removed it 👍

Comment on lines 8 to 17
function getRecordingsDir(): string {
// Cap stores recordings in its app data directory
return join(
homedir(),
"Library",
"Application Support",
"so.cap.desktop",
"recordings",
);
}
Copy link

Choose a reason for hiding this comment

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

Minor style thing: repo has a no-comments rule, so I'd remove the inline comment here.

Suggested change
function getRecordingsDir(): string {
// Cap stores recordings in its app data directory
return join(
homedir(),
"Library",
"Application Support",
"so.cap.desktop",
"recordings",
);
}
function getRecordingsDir(): string {
return join(
homedir(),
"Library",
"Application Support",
"so.cap.desktop",
"recordings",
);
}

Copy link
Author

Choose a reason for hiding this comment

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

done, stripped out the inline comments

Comment on lines 64 to 68
<Action
title="Open in Editor"
onAction={() =>
openDeeplink({ open_editor: { project_path: rec.path } })
}
Copy link

Choose a reason for hiding this comment

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

Action handlers can be async; making this async ensures the openDeeplink promise is handled.

Suggested change
<Action
title="Open in Editor"
onAction={() =>
openDeeplink({ open_editor: { project_path: rec.path } })
}
onAction={async () =>
openDeeplink({ open_editor: { project_path: rec.path } })
}

Copy link
Author

Choose a reason for hiding this comment

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

makes sense, added async to the handler

Comment on lines 24 to 39
/**
* Builds a Cap deeplink URL for the given action.
* Format: cap-desktop://action?value=<json-encoded-action>
*/
function buildDeeplink(action: DeepLinkAction): string {
const json = JSON.stringify(action);
return `${SCHEME}://action?value=${encodeURIComponent(json)}`;
}

/**
* Opens a Cap deeplink. Shows a toast on failure.
*/
export async function openDeeplink(
action: DeepLinkAction,
successMessage?: string,
): Promise<void> {
Copy link

Choose a reason for hiding this comment

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

This repo avoids code comments; I’d remove these docblocks and let the names/types carry the intent.

Suggested change
/**
* Builds a Cap deeplink URL for the given action.
* Format: cap-desktop://action?value=<json-encoded-action>
*/
function buildDeeplink(action: DeepLinkAction): string {
const json = JSON.stringify(action);
return `${SCHEME}://action?value=${encodeURIComponent(json)}`;
}
/**
* Opens a Cap deeplink. Shows a toast on failure.
*/
export async function openDeeplink(
action: DeepLinkAction,
successMessage?: string,
): Promise<void> {
function buildDeeplink(action: DeepLinkAction): string {
const json = JSON.stringify(action);
return `${SCHEME}://action?value=${encodeURIComponent(json)}`;
}
export async function openDeeplink(
action: DeepLinkAction,
successMessage?: string,
): Promise<void> {

Copy link
Author

Choose a reason for hiding this comment

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

fair point, removed the docblocks — the types are self-documenting enough

Comment on lines 33 to 47
### URL Format

```
cap-desktop://action?value=<url-encoded-json>
```

Unit actions (no parameters):
```
cap-desktop://action?value="stop_recording"
```

Actions with parameters:
```
cap-desktop://action?value={"start_recording":{"capture_mode":{"screen":"Main Display"},"camera":null,"mic_label":null,"capture_system_audio":false,"mode":"normal"}}
```
Copy link

Choose a reason for hiding this comment

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

The examples look slightly off: value needs to be URL-encoded JSON (including quotes for unit actions), and mode should match the actual enum values (studio|instant|screenshot).

Suggested change
### URL Format
```
cap-desktop://action?value=<url-encoded-json>
```
Unit actions (no parameters):
```
cap-desktop://action?value="stop_recording"
```
Actions with parameters:
```
cap-desktop://action?value={"start_recording":{"capture_mode":{"screen":"Main Display"},"camera":null,"mic_label":null,"capture_system_audio":false,"mode":"normal"}}
```
### URL Format

cap-desktop://action?value=


Unit actions (no parameters):

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


Actions with parameters (URL-encode the JSON):
```json
{"start_recording":{"capture_mode":{"screen":"Main Display"},"camera":null,"mic_label":null,"capture_system_audio":false,"mode":"studio"}}

Copy link
Author

Choose a reason for hiding this comment

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

fixed the examples to use proper URL encoding and corrected the mode values, thanks for catching that

@omair445
Copy link
Author

hey thanks for the review! just pushed a commit addressing all your points - removed the unused import, dropped the inline comment and docblocks, made the handler async, and fixed the deeplink examples in the README. good catches all around 👍

if (successMessage) {
await showToast({ style: Toast.Style.Success, title: successMessage });
}
} catch {
Copy link

Choose a reason for hiding this comment

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

Catching without the error makes it hard to debug failures (scheme not registered vs app not running). Consider capturing and surfacing the message.

Suggested change
} catch {
} catch (error) {
await showToast({
style: Toast.Style.Failure,
title: "Failed to communicate with Cap",
message: error instanceof Error ? error.message : String(error),
});
}

Copy link
Author

Choose a reason for hiding this comment

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

yep, updated to capture and surface the error message now


```bash
cd apps/raycast-extension
npm install
Copy link

Choose a reason for hiding this comment

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

Minor consistency: repo uses pnpm, and package.json already has a dev script.

Suggested change
npm install
pnpm install
pnpm dev

Copy link
Author

Choose a reason for hiding this comment

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

switched to pnpm, good call

- Capture error in catch for better debugging
- Fix README to use pnpm and URL-encoded examples
function listRecordings(): Recording[] {
const dir = getRecordingsDir();
try {
return readdirSync(dir)
Copy link

Choose a reason for hiding this comment

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

If the recordings folder grows, doing a statSync per entry can get sluggish. Small win: use withFileTypes to skip non-files and cap the list.

Suggested change
return readdirSync(dir)
return readdirSync(dir, { withFileTypes: true })
.filter((d) => d.isFile() && d.name.endsWith(".cap"))
.map((d) => {
const fullPath = join(dir, d.name);
const stat = statSync(fullPath);
return {
name: d.name.replace(/\.cap$/, ""),
path: fullPath,
modifiedAt: stat.mtime,
};
})
.sort((a, b) => b.modifiedAt.getTime() - a.modifiedAt.getTime())
.slice(0, 200);

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

Comments