Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/desktop/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import "./styles/theme.css";
import { CapErrorBoundary } from "./components/CapErrorBoundary";
import { generalSettingsStore } from "./store";
import { initAnonymousUser } from "./utils/analytics";
import { initDeepLinkCommands } from "./utils/deep-link-commands";
Copy link
Contributor

Choose a reason for hiding this comment

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

Importing a module that duplicates existing Rust-based deep link handling and uses incorrect URL schemes.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/App.tsx
Line: 18:18

Comment:
Importing a module that duplicates existing Rust-based deep link handling and uses incorrect URL schemes.

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

import { type AppTheme, commands } from "./utils/tauri";
import titlebar from "./utils/titlebar-state";

Expand Down Expand Up @@ -102,6 +103,7 @@ function Inner() {

onMount(() => {
initAnonymousUser();
initDeepLinkCommands();
Copy link
Contributor

Choose a reason for hiding this comment

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

This initialization conflicts with the existing deep link handler already registered in apps/desktop/src-tauri/src/lib.rs:3344-3346. The Rust handler via tauri_plugin_deep_link is the correct approach and this TypeScript handler should be removed.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/App.tsx
Line: 106:106

Comment:
This initialization conflicts with the existing deep link handler already registered in `apps/desktop/src-tauri/src/lib.rs:3344-3346`. The Rust handler via `tauri_plugin_deep_link` is the correct approach and this TypeScript handler should be removed.

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

});
Comment on lines 104 to 107
Copy link

Choose a reason for hiding this comment

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

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.

Suggested change
onMount(() => {
initAnonymousUser();
initDeepLinkCommands();
});
onMount(async () => {
initAnonymousUser();
const unsubscribe = await initDeepLinkCommands();
onCleanup(unsubscribe);
});


return (
Expand Down
230 changes: 230 additions & 0 deletions apps/desktop/src/utils/deep-link-commands.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
import { listen } from "@tauri-apps/api/event";
import { onOpenUrl } from "@tauri-apps/plugin-deep-link";
Comment on lines +1 to +2
Copy link

Choose a reason for hiding this comment

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

Unused import.

Suggested change
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";

/**
Copy link

Choose a reason for hiding this comment

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

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.

* Deep link command handlers for Cap
* Supports: cap://record, cap://stop, cap://pause, cap://resume,
* cap://toggle-pause, cap://switch-mic, cap://switch-camera
Comment on lines +6 to +9
Copy link
Contributor

Choose a reason for hiding this comment

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

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 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;
Comment on lines +23 to +26
Copy link
Contributor

Choose a reason for hiding this comment

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

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.

Suggested change
// 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;
Comment on lines +28 to +33
Copy link
Contributor

Choose a reason for hiding this comment

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

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.

});

// 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;
Comment on lines +20 to +49
Copy link
Contributor

Choose a reason for hiding this comment

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

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.

}
}

/**
* 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,
Comment on lines +97 to +99
Copy link
Contributor

Choose a reason for hiding this comment

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

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.

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}`);

Comment on lines +203 to +210
Copy link
Contributor

Choose a reason for hiding this comment

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

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.

if (params) {
Object.entries(params).forEach(([key, value]) => {
url.searchParams.set(key, value);
});
}
Comment on lines +205 to +215
Copy link

Choose a reason for hiding this comment

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

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.

Suggested change
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();
}


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>),
Comment on lines +222 to +223
Copy link

Choose a reason for hiding this comment

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

This cast can also leak undefined values into the URL. With generateDeepLink skipping undefineds, this stays type-safe and avoids the cast.

Suggested change
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,
}),

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 }),
};
Comment on lines +1 to +230
Copy link
Contributor

Choose a reason for hiding this comment

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

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.

49 changes: 49 additions & 0 deletions extensions/raycast-cap/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Cap Raycast Extension

Control Cap screen recording from Raycast.

## Features

- **Start Recording** - Start a new screen recording instantly
- **Stop Recording** - Stop the current recording
- **Pause Recording** - Pause the recording
- **Resume Recording** - Resume a paused recording
- **Toggle Pause** - Toggle between pause and resume
- **Switch Microphone** - Change to a different microphone input
- **Switch Camera** - Change to a different camera input

## Installation

1. Make sure you have [Raycast](https://raycast.com/) installed
2. Install the Cap extension from the Raycast Store
3. Ensure [Cap](https://cap.so) is installed on your Mac

## Deep Link Protocol

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
Comment on lines +23 to +31
Copy link
Contributor

Choose a reason for hiding this comment

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

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.


## Requirements

- macOS 11.0 or later
- Cap app installed
- Raycast v1.50.0 or later

## Development

```bash
cd extensions/raycast-cap
npm install
npm run dev
```

## License

MIT
67 changes: 67 additions & 0 deletions extensions/raycast-cap/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
{
"name": "cap",
"title": "Cap",
"description": "Control Cap screen recording from Raycast",
"icon": "cap-icon.png",
"author": "divol89",
"categories": ["Productivity", "Media"],
"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": "pause-recording",
"title": "Pause Recording",
"description": "Pause the current recording",
"mode": "no-view"
},
{
"name": "resume-recording",
"title": "Resume Recording",
"description": "Resume the paused recording",
"mode": "no-view"
},
{
"name": "toggle-pause",
"title": "Toggle Pause",
"description": "Toggle pause/resume recording",
"mode": "no-view"
},
{
"name": "switch-microphone",
"title": "Switch Microphone",
"description": "Switch to a different microphone",
"mode": "view"
},
{
"name": "switch-camera",
"title": "Switch Camera",
"description": "Switch to a different camera",
"mode": "view"
}
],
"dependencies": {
"@raycast/api": "^1.64.0",
"@raycast/utils": "^1.10.0"
},
"devDependencies": {
"@types/node": "^20.8.10",
"typescript": "^5.2.2"
},
"scripts": {
"build": "ray build -e dist",
"dev": "ray develop",
"fix-lint": "ray fix-lint",
"lint": "ray lint"
}
}
19 changes: 19 additions & 0 deletions extensions/raycast-cap/src/pause-recording.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { showToast, Toast } from "@raycast/api";
import { openDeepLink, generateDeepLink } from "./utils";

export default async function Command() {
try {
await openDeepLink(generateDeepLink("pause"));

await showToast({
style: Toast.Style.Success,
title: "Paused Recording",
});
} catch (error) {
await showToast({
style: Toast.Style.Failure,
title: "Failed to Pause Recording",
message: String(error),
});
}
}
Loading