feat: add deeplinks support and Raycast extension#1583
feat: add deeplinks support and Raycast extension#1583divol89 wants to merge 1 commit intoCapSoftware:mainfrom
Conversation
…1540) Implements comprehensive deeplinks support for controlling Cap via URL scheme and a complete Raycast extension for quick access. Deep Links Added: - cap://record - Start recording - cap://stop - Stop recording - cap://pause - Pause recording - cap://resume - Resume recording - cap://toggle-pause - Toggle pause state - cap://switch-mic?label=<id> - Switch microphone - cap://switch-camera?id=<id> - Switch camera Raycast Extension: - Start/Stop/Pause/Resume recording commands - Toggle pause command - Switch microphone with picker UI - Switch camera with picker UI - Toast notifications for feedback Technical Changes: - Added deep-link-commands.ts utility - Integrated deep link initialization in App.tsx - Created full Raycast extension in extensions/raycast-cap/ Fixes CapSoftware#1540 Bounty:
| onMount(() => { | ||
| initAnonymousUser(); | ||
| initDeepLinkCommands(); | ||
| }); |
There was a problem hiding this comment.
initDeepLinkCommands() returns a Promise + unsubscribe, but we currently ignore both (no cleanup, and a rejection would be unhandled). Suggest awaiting it and registering the cleanup.
| onMount(() => { | |
| initAnonymousUser(); | |
| initDeepLinkCommands(); | |
| }); | |
| onMount(async () => { | |
| initAnonymousUser(); | |
| const unsubscribe = await initDeepLinkCommands(); | |
| onCleanup(unsubscribe); | |
| }); |
| import { listen } from "@tauri-apps/api/event"; | ||
| import { onOpenUrl } from "@tauri-apps/plugin-deep-link"; |
There was a problem hiding this comment.
Unused import.
| import { listen } from "@tauri-apps/api/event"; | |
| import { onOpenUrl } from "@tauri-apps/plugin-deep-link"; | |
| import { onOpenUrl } from "@tauri-apps/plugin-deep-link"; |
| import { commands } from "./tauri"; | ||
| import type { RecordingMode, StartRecordingInputs } from "./tauri"; | ||
|
|
||
| /** |
There was a problem hiding this comment.
Small consistency nit: repo guidelines disallow code comments, but this file adds a bunch of docblocks/inline // notes. Worth stripping these and leaning on naming/types instead.
| export function generateDeepLink( | ||
| action: DeepLinkCommand["action"], | ||
| params?: Record<string, string> | ||
| ): string { | ||
| const url = new URL(`cap://${action}`); | ||
|
|
||
| if (params) { | ||
| Object.entries(params).forEach(([key, value]) => { | ||
| url.searchParams.set(key, value); | ||
| }); | ||
| } |
There was a problem hiding this comment.
deepLinks.record can end up passing undefined values through, which then become the string "undefined" in the query params. One easy fix is to have generateDeepLink skip undefineds.
| export function generateDeepLink( | |
| action: DeepLinkCommand["action"], | |
| params?: Record<string, string> | |
| ): string { | |
| const url = new URL(`cap://${action}`); | |
| if (params) { | |
| Object.entries(params).forEach(([key, value]) => { | |
| url.searchParams.set(key, value); | |
| }); | |
| } | |
| export function generateDeepLink( | |
| action: DeepLinkCommand["action"], | |
| params?: Record<string, string | undefined> | |
| ): string { | |
| const url = new URL(`cap://${action}`); | |
| if (params) { | |
| Object.entries(params).forEach(([key, value]) => { | |
| if (value !== undefined) url.searchParams.set(key, value); | |
| }); | |
| } | |
| return url.toString(); | |
| } |
| record: (params?: { mode?: RecordingMode; camera?: string; microphone?: string }) => | ||
| generateDeepLink("record", params as Record<string, string>), |
There was a problem hiding this comment.
This cast can also leak undefined values into the URL. With generateDeepLink skipping undefineds, this stays type-safe and avoids the cast.
| record: (params?: { mode?: RecordingMode; camera?: string; microphone?: string }) => | |
| generateDeepLink("record", params as Record<string, string>), | |
| record: (params?: { mode?: RecordingMode; camera?: string; microphone?: string }) => | |
| generateDeepLink("record", { | |
| mode: params?.mode, | |
| camera: params?.camera, | |
| microphone: params?.microphone, | |
| }), |
| import { exec } from "child_process"; | ||
| import { promisify } from "util"; | ||
|
|
||
| const execAsync = promisify(exec); |
There was a problem hiding this comment.
Using exec with interpolation is a footgun (shell injection) even if current callsites are controlled. execFile avoids quoting issues.
| import { exec } from "child_process"; | |
| import { promisify } from "util"; | |
| const execAsync = promisify(exec); | |
| import { execFile } from "node:child_process"; | |
| import { promisify } from "node:util"; | |
| const execFileAsync = promisify(execFile); |
| export async function openDeepLink(url: string): Promise<void> { | ||
| try { | ||
| // Try using open command on macOS | ||
| await execAsync(`open "${url}"`); |
There was a problem hiding this comment.
| await execAsync(`open "${url}"`); | |
| await execFileAsync("open", [url]); |
| await execAsync(`open "${url}"`); | ||
| } catch (error) { | ||
| // Fallback: show error | ||
| await showToast({ |
There was a problem hiding this comment.
Minor UX thing: openDeepLink shows a failure toast and re-throws, and the commands also show a failure toast in their own catch blocks. That can lead to double-toasts on failure; might be cleaner to let callers own the toast and have openDeepLink just throw.
| import { listen } from "@tauri-apps/api/event"; | ||
| import { onOpenUrl } from "@tauri-apps/plugin-deep-link"; | ||
| import { commands } from "./tauri"; | ||
| import type { RecordingMode, StartRecordingInputs } from "./tauri"; | ||
|
|
||
| /** | ||
| * Deep link command handlers for Cap | ||
| * Supports: cap://record, cap://stop, cap://pause, cap://resume, | ||
| * cap://toggle-pause, cap://switch-mic, cap://switch-camera | ||
| */ | ||
|
|
||
| export interface DeepLinkCommand { | ||
| action: "record" | "stop" | "pause" | "resume" | "toggle-pause" | "switch-mic" | "switch-camera"; | ||
| params?: Record<string, string>; | ||
| } | ||
|
|
||
| /** | ||
| * Parse deep link URL and extract command | ||
| */ | ||
| export function parseDeepLinkCommand(url: string): DeepLinkCommand | null { | ||
| try { | ||
| const urlObj = new URL(url); | ||
|
|
||
| // Only handle cap:// protocol | ||
| if (urlObj.protocol !== "cap:") { | ||
| return null; | ||
| } | ||
|
|
||
| const action = urlObj.hostname as DeepLinkCommand["action"]; | ||
| const params: Record<string, string> = {}; | ||
|
|
||
| urlObj.searchParams.forEach((value, key) => { | ||
| params[key] = value; | ||
| }); | ||
|
|
||
| // Validate action | ||
| const validActions: DeepLinkCommand["action"][] = [ | ||
| "record", "stop", "pause", "resume", "toggle-pause", "switch-mic", "switch-camera" | ||
| ]; | ||
|
|
||
| if (!validActions.includes(action)) { | ||
| console.warn(`Unknown deep link action: ${action}`); | ||
| return null; | ||
| } | ||
|
|
||
| return { action, params }; | ||
| } catch (error) { | ||
| console.error("Failed to parse deep link:", error); | ||
| return null; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Execute deep link command | ||
| */ | ||
| export async function executeDeepLinkCommand(command: DeepLinkCommand): Promise<void> { | ||
| const { action, params = {} } = command; | ||
|
|
||
| console.log(`Executing deep link command: ${action}`, params); | ||
|
|
||
| switch (action) { | ||
| case "record": | ||
| await handleRecordCommand(params); | ||
| break; | ||
| case "stop": | ||
| await handleStopCommand(); | ||
| break; | ||
| case "pause": | ||
| await handlePauseCommand(); | ||
| break; | ||
| case "resume": | ||
| await handleResumeCommand(); | ||
| break; | ||
| case "toggle-pause": | ||
| await handleTogglePauseCommand(); | ||
| break; | ||
| case "switch-mic": | ||
| await handleSwitchMicCommand(params); | ||
| break; | ||
| case "switch-camera": | ||
| await handleSwitchCameraCommand(params); | ||
| break; | ||
| default: | ||
| console.warn(`Unhandled deep link action: ${action}`); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Handle record command | ||
| * Params: mode ("instant" | "studio"), camera?, microphone? | ||
| */ | ||
| async function handleRecordCommand(params: Record<string, string>): Promise<void> { | ||
| const mode = (params.mode as RecordingMode) || "instant"; | ||
|
|
||
| // Set recording mode | ||
| await commands.setRecordingMode(mode); | ||
|
|
||
| const inputs: StartRecordingInputs = { | ||
| mode, | ||
| capture_target: params.target || "screen", | ||
| }; | ||
|
|
||
| // Add camera if specified | ||
| if (params.camera) { | ||
| inputs.camera_label = params.camera; | ||
| } | ||
|
|
||
| // Add microphone if specified | ||
| if (params.microphone) { | ||
| inputs.audio_inputs = [{ label: params.microphone, device_type: "mic" }]; | ||
| } | ||
|
|
||
| const result = await commands.startRecording(inputs); | ||
|
|
||
| if (result !== "Started") { | ||
| console.error(`Failed to start recording: ${result}`); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Handle stop command | ||
| */ | ||
| async function handleStopCommand(): Promise<void> { | ||
| await commands.stopRecording(); | ||
| } | ||
|
|
||
| /** | ||
| * Handle pause command | ||
| */ | ||
| async function handlePauseCommand(): Promise<void> { | ||
| await commands.pauseRecording(); | ||
| } | ||
|
|
||
| /** | ||
| * Handle resume command | ||
| */ | ||
| async function handleResumeCommand(): Promise<void> { | ||
| await commands.resumeRecording(); | ||
| } | ||
|
|
||
| /** | ||
| * Handle toggle pause command | ||
| */ | ||
| async function handleTogglePauseCommand(): Promise<void> { | ||
| await commands.togglePauseRecording(); | ||
| } | ||
|
|
||
| /** | ||
| * Handle switch microphone command | ||
| * Params: label (microphone name/device ID) | ||
| */ | ||
| async function handleSwitchMicCommand(params: Record<string, string>): Promise<void> { | ||
| const label = params.label || params.device; | ||
|
|
||
| if (!label) { | ||
| console.error("No microphone label provided"); | ||
| return; | ||
| } | ||
|
|
||
| await commands.setMicInput(label); | ||
| } | ||
|
|
||
| /** | ||
| * Handle switch camera command | ||
| * Params: id (camera device ID) | ||
| */ | ||
| async function handleSwitchCameraCommand(params: Record<string, string>): Promise<void> { | ||
| const id = params.id || params.device; | ||
|
|
||
| if (!id) { | ||
| console.error("No camera ID provided"); | ||
| return; | ||
| } | ||
|
|
||
| await commands.setCameraInput(id, true); | ||
| } | ||
|
|
||
| /** | ||
| * Initialize deep link command listener | ||
| * Returns unsubscribe function | ||
| */ | ||
| export async function initDeepLinkCommands(): Promise<() => void> { | ||
| console.log("Initializing deep link commands..."); | ||
|
|
||
| const unsubscribe = await onOpenUrl(async (urls) => { | ||
| for (const url of urls) { | ||
| const command = parseDeepLinkCommand(url); | ||
|
|
||
| if (command) { | ||
| try { | ||
| await executeDeepLinkCommand(command); | ||
| } catch (error) { | ||
| console.error(`Failed to execute command from ${url}:`, error); | ||
| } | ||
| } | ||
| } | ||
| }); | ||
|
|
||
| return unsubscribe; | ||
| } | ||
|
|
||
| /** | ||
| * Generate deep link URL for a command | ||
| */ | ||
| export function generateDeepLink( | ||
| action: DeepLinkCommand["action"], | ||
| params?: Record<string, string> | ||
| ): string { | ||
| const url = new URL(`cap://${action}`); | ||
|
|
||
| if (params) { | ||
| Object.entries(params).forEach(([key, value]) => { | ||
| url.searchParams.set(key, value); | ||
| }); | ||
| } | ||
|
|
||
| return url.toString(); | ||
| } | ||
|
|
||
| // Export convenience functions for generating deep links | ||
| export const deepLinks = { | ||
| record: (params?: { mode?: RecordingMode; camera?: string; microphone?: string }) => | ||
| generateDeepLink("record", params as Record<string, string>), | ||
| stop: () => generateDeepLink("stop"), | ||
| pause: () => generateDeepLink("pause"), | ||
| resume: () => generateDeepLink("resume"), | ||
| togglePause: () => generateDeepLink("toggle-pause"), | ||
| switchMic: (label: string) => generateDeepLink("switch-mic", { label }), | ||
| switchCamera: (id: string) => generateDeepLink("switch-camera", { id }), | ||
| }; |
There was a problem hiding this comment.
This entire file duplicates existing deep link functionality already implemented in apps/desktop/src-tauri/src/deeplink_actions.rs. The Rust implementation is already registered and handles deep links via the tauri_plugin_deep_link plugin (see apps/desktop/src-tauri/src/lib.rs:3344-3346). Adding a second TypeScript-based handler creates conflicting implementations and architectural inconsistency.
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/utils/deep-link-commands.ts
Line: 1:230
Comment:
This entire file duplicates existing deep link functionality already implemented in `apps/desktop/src-tauri/src/deeplink_actions.rs`. The Rust implementation is already registered and handles deep links via the `tauri_plugin_deep_link` plugin (see `apps/desktop/src-tauri/src/lib.rs:3344-3346`). Adding a second TypeScript-based handler creates conflicting implementations and architectural inconsistency.
How can I resolve this? If you propose a fix, please make it concise.|
|
||
| // Only handle cap:// protocol | ||
| if (urlObj.protocol !== "cap:") { | ||
| return null; |
There was a problem hiding this comment.
The URL scheme cap: doesn't match the configured scheme in apps/desktop/src-tauri/tauri.conf.json:33 which is cap-desktop. Deep links will fail because the protocol is incorrect.
| // Only handle cap:// protocol | |
| if (urlObj.protocol !== "cap:") { | |
| return null; | |
| if (urlObj.protocol !== "cap-desktop:") { | |
| return null; | |
| } |
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/utils/deep-link-commands.ts
Line: 23:26
Comment:
The URL scheme `cap:` doesn't match the configured scheme in `apps/desktop/src-tauri/tauri.conf.json:33` which is `cap-desktop`. Deep links will fail because the protocol is incorrect.
```suggestion
if (urlObj.protocol !== "cap-desktop:") {
return null;
}
```
How can I resolve this? If you propose a fix, please make it concise.|
|
||
| const action = urlObj.hostname as DeepLinkCommand["action"]; | ||
| const params: Record<string, string> = {}; | ||
|
|
||
| urlObj.searchParams.forEach((value, key) => { | ||
| params[key] = value; |
There was a problem hiding this comment.
The parsing approach using URL hostname for actions conflicts with the existing deep link architecture. The existing Rust implementation expects URLs in format cap-desktop://action?value=<json> where actions are serialized as JSON (see deeplink_actions.rs:95-104). This implementation uses a completely different pattern that won't integrate with the existing system.
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/utils/deep-link-commands.ts
Line: 28:33
Comment:
The parsing approach using URL hostname for actions conflicts with the existing deep link architecture. The existing Rust implementation expects URLs in format `cap-desktop://action?value=<json>` where actions are serialized as JSON (see `deeplink_actions.rs:95-104`). This implementation uses a completely different pattern that won't integrate with the existing system.
How can I resolve this? If you propose a fix, please make it concise.|
|
||
| const inputs: StartRecordingInputs = { | ||
| mode, |
There was a problem hiding this comment.
The StartRecordingInputs type expects a capture_target of type ScreenCaptureTarget (union of Display/Window with id), but this passes a string "screen" which will cause a type error. See the correct usage in deeplink_actions.rs:122-133.
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/utils/deep-link-commands.ts
Line: 97:99
Comment:
The `StartRecordingInputs` type expects a `capture_target` of type `ScreenCaptureTarget` (union of Display/Window with id), but this passes a string `"screen"` which will cause a type error. See the correct usage in `deeplink_actions.rs:122-133`.
How can I resolve this? If you propose a fix, please make it concise.| * Generate deep link URL for a command | ||
| */ | ||
| export function generateDeepLink( | ||
| action: DeepLinkCommand["action"], | ||
| params?: Record<string, string> | ||
| ): string { | ||
| const url = new URL(`cap://${action}`); | ||
|
|
There was a problem hiding this comment.
The generateDeepLink function generates URLs with the wrong scheme (cap://) instead of the configured cap-desktop:// scheme from tauri.conf.json.
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/utils/deep-link-commands.ts
Line: 203:210
Comment:
The `generateDeepLink` function generates URLs with the wrong scheme (`cap://`) instead of the configured `cap-desktop://` scheme from `tauri.conf.json`.
How can I resolve this? If you propose a fix, please make it concise.| useEffect(() => { | ||
| // In a real implementation, this would fetch from Cap | ||
| // For now, showing example cameras | ||
| setCameras([ | ||
| { id: "default", name: "Default Camera" }, | ||
| { id: "built-in", name: "Built-in Camera" }, | ||
| { id: "external", name: "External Camera" }, | ||
| ]); | ||
| setIsLoading(false); | ||
| }, []); |
There was a problem hiding this comment.
Hardcoded camera list won't work. The Raycast extension should either dynamically fetch available cameras from Cap or instruct users on proper device IDs. The existing desktop app has commands.listCameras() that could be exposed via a query endpoint or the deep link could accept actual device IDs that users obtain from Cap's UI.
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/raycast-cap/src/switch-camera.tsx
Line: 14:23
Comment:
Hardcoded camera list won't work. The Raycast extension should either dynamically fetch available cameras from Cap or instruct users on proper device IDs. The existing desktop app has `commands.listCameras()` that could be exposed via a query endpoint or the deep link could accept actual device IDs that users obtain from Cap's UI.
How can I resolve this? If you propose a fix, please make it concise.| useEffect(() => { | ||
| // In a real implementation, this would fetch from Cap | ||
| // For now, showing example microphones | ||
| setMicrophones([ | ||
| { id: "default", name: "Default Microphone" }, | ||
| { id: "built-in", name: "Built-in Microphone" }, | ||
| { id: "external", name: "External Microphone" }, | ||
| ]); | ||
| setIsLoading(false); | ||
| }, []); |
There was a problem hiding this comment.
Hardcoded microphone list won't work. Real microphone device IDs/labels need to come from the system or Cap. Consider fetching actual devices dynamically or documenting how users should obtain valid microphone labels.
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/raycast-cap/src/switch-microphone.tsx
Line: 14:23
Comment:
Hardcoded microphone list won't work. Real microphone device IDs/labels need to come from the system or Cap. Consider fetching actual devices dynamically or documenting how users should obtain valid microphone labels.
How can I resolve this? If you propose a fix, please make it concise.| This extension uses Cap's deep link protocol to control the app: | ||
|
|
||
| - `cap://record` - Start recording | ||
| - `cap://stop` - Stop recording | ||
| - `cap://pause` - Pause recording | ||
| - `cap://resume` - Resume recording | ||
| - `cap://toggle-pause` - Toggle pause state | ||
| - `cap://switch-mic?label=<id>` - Switch microphone | ||
| - `cap://switch-camera?id=<id>` - Switch camera |
There was a problem hiding this comment.
Documentation lists incorrect URL scheme. Cap uses cap-desktop:// (configured in apps/desktop/src-tauri/tauri.conf.json), not cap://. All examples need updating to use the correct scheme.
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/raycast-cap/README.md
Line: 23:31
Comment:
Documentation lists incorrect URL scheme. Cap uses `cap-desktop://` (configured in `apps/desktop/src-tauri/tauri.conf.json`), not `cap://`. All examples need updating to use the correct scheme.
How can I resolve this? If you propose a fix, please make it concise.| /** | ||
| * Deep link command handlers for Cap | ||
| * Supports: cap://record, cap://stop, cap://pause, cap://resume, | ||
| * cap://toggle-pause, cap://switch-mic, cap://switch-camera |
There was a problem hiding this comment.
Remove JSDoc comments. Per CLAUDE.md:363-368 and AGENTS.md:23, NO CODE COMMENTS are allowed in this codebase (//, /* */, or /** */). Code must be self-explanatory through naming, types, and structure.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/utils/deep-link-commands.ts
Line: 6:9
Comment:
Remove JSDoc comments. Per CLAUDE.md:363-368 and AGENTS.md:23, NO CODE COMMENTS are allowed in this codebase (`//`, `/* */`, or `/** */`). Code must be self-explanatory through naming, types, and structure.
<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>
How can I resolve this? If you propose a fix, please make it concise.| export function parseDeepLinkCommand(url: string): DeepLinkCommand | null { | ||
| try { | ||
| const urlObj = new URL(url); | ||
|
|
||
| // Only handle cap:// protocol | ||
| if (urlObj.protocol !== "cap:") { | ||
| return null; | ||
| } | ||
|
|
||
| const action = urlObj.hostname as DeepLinkCommand["action"]; | ||
| const params: Record<string, string> = {}; | ||
|
|
||
| urlObj.searchParams.forEach((value, key) => { | ||
| params[key] = value; | ||
| }); | ||
|
|
||
| // Validate action | ||
| const validActions: DeepLinkCommand["action"][] = [ | ||
| "record", "stop", "pause", "resume", "toggle-pause", "switch-mic", "switch-camera" | ||
| ]; | ||
|
|
||
| if (!validActions.includes(action)) { | ||
| console.warn(`Unknown deep link action: ${action}`); | ||
| return null; | ||
| } | ||
|
|
||
| return { action, params }; | ||
| } catch (error) { | ||
| console.error("Failed to parse deep link:", error); | ||
| return null; |
There was a problem hiding this comment.
Remove all JSDoc comments (/** ... */). The codebase strictly prohibits code comments of any form per CLAUDE.md:363-368.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/utils/deep-link-commands.ts
Line: 20:49
Comment:
Remove all JSDoc comments (`/** ... */`). The codebase strictly prohibits code comments of any form per CLAUDE.md:363-368.
<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>
How can I resolve this? If you propose a fix, please make it concise.
Summary
Implements comprehensive deeplinks support and Raycast extension as requested in #1540.
Deep Links Implementation
New URL scheme for controlling Cap programmatically:
Raycast Extension
Complete Raycast extension with 7 commands:
Technical Implementation
Testing
Tested deep links:
Raycast extension:
Files Changed
Bounty
This PR implements the bounty for #1540.
Fixes #1540
Greptile Overview
Greptile Summary
This PR attempts to add deep link support and a Raycast extension for Cap, but has critical implementation issues that prevent it from working.
Critical Issues
cap://but Cap is configured to usecap-desktop://inapps/desktop/src-tauri/tauri.conf.json:33. Deep links will fail completely.deep-link-commands.ts) that duplicates and conflicts with the existing Rust implementation inapps/desktop/src-tauri/src/deeplink_actions.rs(registered atlib.rs:3344-3346).cap://record) while the existing system expects JSON-serialized actions in query parameters (cap-desktop://action?value=<json>).StartRecordingInputs.capture_targetexpectsScreenCaptureTargettype but receives string"screen".utils.tsusing unsanitizedexec().What Works
Recommendation
The PR needs a complete architectural revision:
cap-desktop://schemeConfidence Score: 0/5
apps/desktop/src/utils/deep-link-commands.tsandextensions/raycast-cap/src/utils.tsrequire complete rewrites. All Raycast command files need URL scheme corrections.Important Files Changed
Sequence Diagram
sequenceDiagram participant User participant Raycast participant Shell participant Tauri as Tauri Deep Link Plugin participant Rust as deeplink_actions.rs participant TS as deep-link-commands.ts (NEW) participant Commands as Tauri Commands Note over User,Commands: Current Implementation (Existing) User->>Raycast: Execute command Raycast->>Shell: open "cap-desktop://action?value={json}" Shell->>Tauri: Register deep link event Tauri->>Rust: handle(app, urls) Rust->>Rust: Parse JSON from URL Rust->>Commands: Execute recording command Commands-->>User: Success Note over User,Commands: Proposed Implementation (This PR - BROKEN) User->>Raycast: Execute command Raycast->>Shell: open "cap://record" ❌ Wrong scheme! Shell-->>Raycast: Failed (scheme not registered) Note over TS,Commands: Duplicate Handler Added TS->>Tauri: onOpenUrl listener ❌ Conflicts! TS->>TS: Parse hostname as action ❌ Wrong format! TS->>Commands: Call with wrong types ❌ Type error! Note over User,Commands: Issues Note right of Raycast: 1. Wrong URL scheme (cap:// vs cap-desktop://) Note right of TS: 2. Duplicate handler conflicts with Rust Note right of TS: 3. Incompatible URL parsing approach Note right of Commands: 4. Type mismatches in command callsContext used:
dashboard- CLAUDE.md (source)dashboard- AGENTS.md (source)