feat: add Raycast extension and new deeplink actions#1599
feat: add Raycast extension and new deeplink actions#1599omair445 wants to merge 3 commits intoCapSoftware:mainfrom
Conversation
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
apps/raycast-extension/README.md
Outdated
|
|
||
| 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"}} |
There was a problem hiding this comment.
"mode":"normal" is invalid - use "studio", "instant", or "screenshot" to match RecordingMode enum
| 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" }, |
There was a problem hiding this comment.
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.| return join( | ||
| homedir(), | ||
| "Library", | ||
| "Application Support", | ||
| "so.cap.desktop", | ||
| "recordings", | ||
| ); |
There was a problem hiding this comment.
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"; | |||
There was a problem hiding this comment.
getApplications is unused here; worth dropping to keep the extension lint-clean.
| import { Action, ActionPanel, List, getApplications } from "@raycast/api"; | |
| import { Action, ActionPanel, List } from "@raycast/api"; |
| function getRecordingsDir(): string { | ||
| // Cap stores recordings in its app data directory | ||
| return join( | ||
| homedir(), | ||
| "Library", | ||
| "Application Support", | ||
| "so.cap.desktop", | ||
| "recordings", | ||
| ); | ||
| } |
There was a problem hiding this comment.
Minor style thing: repo has a no-comments rule, so I'd remove the inline comment here.
| 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", | |
| ); | |
| } |
There was a problem hiding this comment.
done, stripped out the inline comments
| <Action | ||
| title="Open in Editor" | ||
| onAction={() => | ||
| openDeeplink({ open_editor: { project_path: rec.path } }) | ||
| } |
There was a problem hiding this comment.
Action handlers can be async; making this async ensures the openDeeplink promise is handled.
| <Action | |
| title="Open in Editor" | |
| onAction={() => | |
| openDeeplink({ open_editor: { project_path: rec.path } }) | |
| } | |
| onAction={async () => | |
| openDeeplink({ open_editor: { project_path: rec.path } }) | |
| } |
There was a problem hiding this comment.
makes sense, added async to the handler
| /** | ||
| * 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> { |
There was a problem hiding this comment.
This repo avoids code comments; I’d remove these docblocks and let the names/types carry the intent.
| /** | |
| * 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> { |
There was a problem hiding this comment.
fair point, removed the docblocks — the types are self-documenting enough
| ### 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"}} | ||
| ``` |
There was a problem hiding this comment.
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).
| ### 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"}}
There was a problem hiding this comment.
fixed the examples to use proper URL encoding and corrected the mode values, thanks for catching that
…e comments, fix deeplink examples
|
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 { |
There was a problem hiding this comment.
Catching without the error makes it hard to debug failures (scheme not registered vs app not running). Consider capturing and surfacing the message.
| } catch { | |
| } catch (error) { | |
| await showToast({ | |
| style: Toast.Style.Failure, | |
| title: "Failed to communicate with Cap", | |
| message: error instanceof Error ? error.message : String(error), | |
| }); | |
| } |
There was a problem hiding this comment.
yep, updated to capture and surface the error message now
apps/raycast-extension/README.md
Outdated
|
|
||
| ```bash | ||
| cd apps/raycast-extension | ||
| npm install |
There was a problem hiding this comment.
Minor consistency: repo uses pnpm, and package.json already has a dev script.
| npm install | |
| pnpm install | |
| pnpm dev |
- 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) |
There was a problem hiding this comment.
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.
| 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); |
hey! this adds a Raycast extension for Cap and extends the deeplink system with new actions.
new deeplink actions (Rust):
PauseRecording,ResumeRecording,TogglePauseSetCamera,SetMicrophoneRaycast extension (
apps/raycast-extension/):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:
DeepLinkActionenum withPauseRecording,ResumeRecording,TogglePause,SetCamera, andSetMicrophonevariantsapps/desktop/src-tauri/src/recording.rsapps/raycast-extension/directory with TypeScript implementation using@raycast/apiIssues found:
"mode":"normal"(should be"studio","instant", or"screenshot")"Main Display"which will fail if user's display has a different name~/Library/Application Support/so.cap.desktop/recordings) preventing Windows compatibilityConfidence Score: 4/5
apps/raycast-extension/src/start-recording.tsandapps/raycast-extension/src/list-recent-recordings.tsxfor cross-platform and configuration concernsImportant Files Changed
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)Last reviewed commit: 7bc798f