feat: extend deeplinks + add Raycast extension#1690
feat: extend deeplinks + add Raycast extension#1690macakii327-prog wants to merge 2 commits intoCapSoftware:mainfrom
Conversation
Deeplinks (Rust): - Add PauseRecording, ResumeRecording, TogglePauseRecording actions - Add SwitchMicrophone and SwitchCamera actions - All new actions delegate to existing recording/input functions Raycast Extension: - Start/Stop/Toggle Pause recording via deeplinks - Open Cap app and settings - Browse recent recordings with search - All commands use cap-desktop:// URL scheme - No-view commands for instant execution Closes CapSoftware#1540
| export function getRecordingsDir(): string { | ||
| const home = process.env.HOME || "~"; | ||
| return `${home}/.cap/recordings`; | ||
| } |
There was a problem hiding this comment.
Incorrect recordings directory path
getRecordingsDir() returns $HOME/.cap/recordings, but Cap (built with Tauri) stores recordings under the Tauri app_data_dir(), which on macOS resolves to ~/Library/Application Support/<bundle-identifier>/recordings (e.g. ~/Library/Application Support/so.cap.desktop/recordings). The hardcoded ~/.cap/recordings path does not exist, so the Recent Recordings command will always return an empty list regardless of how many recordings exist.
The correct absolute path cannot be derived from the environment variable alone — it depends on the Tauri bundle identifier. One option is to expose the recordings directory via a dedicated deeplink/URL query so the extension can ask the app, or document the real path for users to override via a Raycast preference.
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/raycast/src/utils.ts
Line: 29-32
Comment:
**Incorrect recordings directory path**
`getRecordingsDir()` returns `$HOME/.cap/recordings`, but Cap (built with Tauri) stores recordings under the Tauri `app_data_dir()`, which on macOS resolves to `~/Library/Application Support/<bundle-identifier>/recordings` (e.g. `~/Library/Application Support/so.cap.desktop/recordings`). The hardcoded `~/.cap/recordings` path does not exist, so the **Recent Recordings** command will always return an empty list regardless of how many recordings exist.
The correct absolute path cannot be derived from the environment variable alone — it depends on the Tauri bundle identifier. One option is to expose the recordings directory via a dedicated deeplink/URL query so the extension can ask the app, or document the real path for users to override via a Raycast preference.
How can I resolve this? If you propose a fix, please make it concise.| onAction={async () => { | ||
| await open(`file://${rec.path}`); | ||
| await showHUD("Opening in Cap..."); |
There was a problem hiding this comment.
Unencoded spaces in
file:// URL will break deep link handler
rec.path is a raw filesystem path. On macOS the recordings directory lives under ~/Library/Application Support/…, which contains a space. Passing file:///Users/alice/Library/Application Support/… as-is creates an invalid URL — the Rust Url parser will reject it (or produce unexpected results), so OpenEditor will never be triggered.
The path must be percent-encoded before constructing the URL:
| onAction={async () => { | |
| await open(`file://${rec.path}`); | |
| await showHUD("Opening in Cap..."); | |
| await open(`file://${encodeURI(rec.path)}`); |
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/raycast/src/recent-recordings.tsx
Line: 59-61
Comment:
**Unencoded spaces in `file://` URL will break deep link handler**
`rec.path` is a raw filesystem path. On macOS the recordings directory lives under `~/Library/Application Support/…`, which contains a space. Passing `file:///Users/alice/Library/Application Support/…` as-is creates an invalid URL — the Rust `Url` parser will reject it (or produce unexpected results), so `OpenEditor` will never be triggered.
The path must be percent-encoded before constructing the URL:
```suggestion
await open(`file://${encodeURI(rec.path)}`);
```
How can I resolve this? If you propose a fix, please make it concise.| return readdirSync(dir) | ||
| .filter((name) => { | ||
| const fullPath = join(dir, name); | ||
| return statSync(fullPath).isDirectory(); | ||
| }) | ||
| .map((name) => { | ||
| const fullPath = join(dir, name); | ||
| const stat = statSync(fullPath); | ||
| return { name, path: fullPath, date: stat.mtime }; | ||
| }) | ||
| .sort((a, b) => b.date.getTime() - a.date.getTime()) | ||
| .slice(0, 50); |
There was a problem hiding this comment.
statSync called twice per directory entry
In the current implementation, every entry that passes the isDirectory() filter has statSync called again in the map step to retrieve mtime. This doubles the number of filesystem calls. A single map-then-filter pattern avoids the redundancy:
| return readdirSync(dir) | |
| .filter((name) => { | |
| const fullPath = join(dir, name); | |
| return statSync(fullPath).isDirectory(); | |
| }) | |
| .map((name) => { | |
| const fullPath = join(dir, name); | |
| const stat = statSync(fullPath); | |
| return { name, path: fullPath, date: stat.mtime }; | |
| }) | |
| .sort((a, b) => b.date.getTime() - a.date.getTime()) | |
| .slice(0, 50); | |
| return readdirSync(dir) | |
| .map((name) => { | |
| const fullPath = join(dir, name); | |
| const stat = statSync(fullPath); | |
| return { name, path: fullPath, stat }; | |
| }) | |
| .filter(({ stat }) => stat.isDirectory()) | |
| .map(({ name, path, stat }) => ({ name, path, date: stat.mtime })) | |
| .sort((a, b) => b.date.getTime() - a.date.getTime()) | |
| .slice(0, 50); |
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/raycast/src/recent-recordings.tsx
Line: 18-29
Comment:
**`statSync` called twice per directory entry**
In the current implementation, every entry that passes the `isDirectory()` filter has `statSync` called again in the `map` step to retrieve `mtime`. This doubles the number of filesystem calls. A single `map`-then-filter pattern avoids the redundancy:
```suggestion
return readdirSync(dir)
.map((name) => {
const fullPath = join(dir, name);
const stat = statSync(fullPath);
return { name, path: fullPath, stat };
})
.filter(({ stat }) => stat.isDirectory())
.map(({ name, path, stat }) => ({ name, path, date: stat.mtime }))
.sort((a, b) => b.date.getTime() - a.date.getTime())
.slice(0, 50);
```
How can I resolve this? If you propose a fix, please make it concise.| return readdirSync(dir) | ||
| .filter((name) => { | ||
| const fullPath = join(dir, name); | ||
| return statSync(fullPath).isDirectory(); | ||
| }) | ||
| .map((name) => { | ||
| const fullPath = join(dir, name); | ||
| const stat = statSync(fullPath); | ||
| return { name, path: fullPath, date: stat.mtime }; | ||
| }) | ||
| .sort((a, b) => b.date.getTime() - a.date.getTime()) | ||
| .slice(0, 50); |
There was a problem hiding this comment.
No filter for
.cap directory extension
getRecordings() lists all subdirectories in the recordings folder, including any non-recording artifacts. The Rust side treats only paths ending in .cap as valid recording projects (e.g. "my-recording.cap"). Adding a name filter keeps the list accurate:
| return readdirSync(dir) | |
| .filter((name) => { | |
| const fullPath = join(dir, name); | |
| return statSync(fullPath).isDirectory(); | |
| }) | |
| .map((name) => { | |
| const fullPath = join(dir, name); | |
| const stat = statSync(fullPath); | |
| return { name, path: fullPath, date: stat.mtime }; | |
| }) | |
| .sort((a, b) => b.date.getTime() - a.date.getTime()) | |
| .slice(0, 50); | |
| return readdirSync(dir) | |
| .filter((name) => { | |
| const fullPath = join(dir, name); | |
| return name.endsWith(".cap") && statSync(fullPath).isDirectory(); | |
| }) |
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/raycast/src/recent-recordings.tsx
Line: 18-29
Comment:
**No filter for `.cap` directory extension**
`getRecordings()` lists all subdirectories in the recordings folder, including any non-recording artifacts. The Rust side treats only paths ending in `.cap` as valid recording projects (e.g. `"my-recording.cap"`). Adding a name filter keeps the list accurate:
```suggestion
return readdirSync(dir)
.filter((name) => {
const fullPath = join(dir, name);
return name.endsWith(".cap") && statSync(fullPath).isDirectory();
})
```
How can I resolve this? If you propose a fix, please make it concise.
extensions/raycast/tsconfig.json
Outdated
| @@ -0,0 +1,16 @@ | |||
| { | |||
| "$schema": "https://www.raycast.com/schemas/extension.json", | |||
There was a problem hiding this comment.
Wrong
$schema for tsconfig.json
This file uses the Raycast extension JSON schema (https://www.raycast.com/schemas/extension.json), which is intended for package.json. The TypeScript configuration schema should point to "https://json.schemastore.org/tsconfig" (or be omitted). Using the wrong schema means IDEs will validate this file against Raycast's extension manifest rules, not TypeScript's compiler options, and will flag valid options as errors.
| "$schema": "https://www.raycast.com/schemas/extension.json", | |
| "$schema": "https://json.schemastore.org/tsconfig", |
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/raycast/tsconfig.json
Line: 2
Comment:
**Wrong `$schema` for `tsconfig.json`**
This file uses the Raycast _extension_ JSON schema (`https://www.raycast.com/schemas/extension.json`), which is intended for `package.json`. The TypeScript configuration schema should point to `"https://json.schemastore.org/tsconfig"` (or be omitted). Using the wrong schema means IDEs will validate this file against Raycast's extension manifest rules, not TypeScript's compiler options, and will flag valid options as errors.
```suggestion
"$schema": "https://json.schemastore.org/tsconfig",
```
How can I resolve this? If you propose a fix, please make it concise.- Fix recordings directory path to use Tauri app data dir (~/Library/Application Support/so.cap.desktop/recordings) - Percent-encode file:// URLs to handle spaces in paths - Optimize statSync to single call per entry (map-then-filter) - Filter recordings by .cap directory extension - Fix tsconfig.json schema URL
🎯 Bounty: #1540 — Deeplinks support + Raycast Extension
Deeplinks (Rust)
Extended
DeepLinkActionenum with new actions:PauseRecording— pause the current recordingResumeRecording— resume a paused recordingTogglePauseRecording— toggle pause/resumeSwitchMicrophone { label }— switch to a different micSwitchCamera { camera }— switch to a different cameraAll new actions delegate to existing functions (
pause_recording,resume_recording,toggle_pause_recording,set_mic_input,set_camera_input).Raycast Extension
Full Raycast extension at
extensions/raycast/:All commands use the
cap-desktop://action?value=<json>deeplink scheme with correct serde serialization format.Testing
TryFrom<&Url>parsercrate::recording::*)Closes #1540
Greptile Summary
This PR extends Cap's deeplink system with five new Rust actions (
PauseRecording,ResumeRecording,TogglePauseRecording,SwitchMicrophone,SwitchCamera) and ships a full Raycast extension that drives Cap via thecap-desktop://deep link scheme. The Rust side is clean and follows established patterns well. The Raycast extension commands for start, stop, toggle-pause, open-cap, and open-settings are all correctly implemented. However, the Recent Recordings feature has two bugs that make it non-functional:getRecordingsDir()returns$HOME/.cap/recordings, but Cap stores recordings under the Tauriapp_data_dir()(~/Library/Application Support/<bundle-id>/recordings), so the directory is never found.file://URLs without percent-encoding; paths containing spaces (e.g.Application Support) will produce malformed URLs that Cap's Rust URL parser will reject.Confidence Score: 4/5
Safe to merge for the Rust deeplink additions and most Raycast commands; the Recent Recordings feature will be silently broken until the path and URL-encoding issues are fixed.
Two P1 issues affect the same feature (Recent Recordings): wrong recordings directory path and unencoded file:// URLs. All other commands work correctly. Rust changes are solid with no issues found.
extensions/raycast/src/utils.ts (getRecordingsDir path) and extensions/raycast/src/recent-recordings.tsx (file:// URL encoding and directory filtering)
Important Files Changed
Sequence Diagram
sequenceDiagram participant User participant Raycast participant macOS participant Cap participant Recording User->>Raycast: Invoke command (e.g. Toggle Pause) Raycast->>Raycast: sendDeepLink("toggle_pause_recording") Raycast->>macOS: open cap-desktop://action?value=... macOS->>Cap: Deep link event Cap->>Cap: Parse URL into DeepLinkAction Cap->>Recording: toggle_pause_recording(app, state) Recording-->>Cap: Ok(()) Raycast-->>User: showHUD("Toggled pause")Prompt To Fix All With AI
Reviews (1): Last reviewed commit: "feat: extend deeplinks + add Raycast ext..." | Re-trigger Greptile
(2/5) Greptile learns from your feedback when you react with thumbs up/down!