Skip to content
Closed
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
61 changes: 61 additions & 0 deletions apps/desktop/src/lib/deeplinks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { onOpenUrl } from "@tauri-apps/plugin-deep-link";
import { invoke } from "@tauri-apps/api/core";

export type DeeplinkAction =
| "start-recording"
| "stop-recording"
| "pause-recording"
| "resume-recording"
| "restart-recording"
| "switch-microphone"
| "switch-camera";

export function initDeeplinks() {
onOpenUrl((urls) => {
for (const url of urls) {
handleDeeplink(url);
Copy link

Choose a reason for hiding this comment

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

Dropping the promise here reads like an accident. Using void makes the intent explicit (and avoids any unhandled-promise linting).

Suggested change
handleDeeplink(url);
void handleDeeplink(url);

}
});
Comment on lines +13 to +18
Copy link
Contributor

Choose a reason for hiding this comment

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

initDeeplinks() is never called — handler is never registered

initDeeplinks is exported but there is no call site anywhere in the application's startup flow (checked all of apps/desktop/src). The onOpenUrl subscription is therefore never established, so no deeplink will ever be processed. This function must be called during app initialization (e.g., in the main entry point or root component).

Additionally, the project already has a Rust-level on_open_url handler registered in apps/desktop/src-tauri/src/lib.rs (via deeplink_actions.rs). Adding a second handler at the TypeScript layer means both will race to handle the same URL. The two systems need to be reconciled — either use the existing Rust handler and extend it, or remove the Rust handler and use this TypeScript one exclusively.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/lib/deeplinks.ts
Line: 13-18

Comment:
**`initDeeplinks()` is never called — handler is never registered**

`initDeeplinks` is exported but there is no call site anywhere in the application's startup flow (checked all of `apps/desktop/src`). The `onOpenUrl` subscription is therefore never established, so no deeplink will ever be processed. This function must be called during app initialization (e.g., in the main entry point or root component).

Additionally, the project already has a Rust-level `on_open_url` handler registered in `apps/desktop/src-tauri/src/lib.rs` (via `deeplink_actions.rs`). Adding a second handler at the TypeScript layer means both will race to handle the same URL. The two systems need to be reconciled — either use the existing Rust handler and extend it, or remove the Rust handler and use this TypeScript one exclusively.

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

}

export async function handleDeeplink(url: string) {
try {
const parsed = new URL(url);
Comment on lines +22 to +23
Copy link
Contributor

Choose a reason for hiding this comment

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

Wrong URL scheme — handler will never fire

tauri.conf.json registers the scheme as "cap-desktop", so the OS dispatches URLs like cap-desktop://start-recording. new URL("cap-desktop://start-recording").protocol returns "cap-desktop:", not "cap:". The guard on line 23 will always be true, silently swallowing every deeplink without performing any action.

Suggested change
try {
const parsed = new URL(url);
if (parsed.protocol !== "cap-desktop:") return;

This also means the Raycast commands must use cap-desktop://… URLs instead of cap://….

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/lib/deeplinks.ts
Line: 22-23

Comment:
**Wrong URL scheme — handler will never fire**

`tauri.conf.json` registers the scheme as `"cap-desktop"`, so the OS dispatches URLs like `cap-desktop://start-recording`. `new URL("cap-desktop://start-recording").protocol` returns `"cap-desktop:"`, not `"cap:"`. The guard on line 23 will always be `true`, silently swallowing every deeplink without performing any action.

```suggestion
    if (parsed.protocol !== "cap-desktop:") return;
```

This also means the Raycast commands must use `cap-desktop://…` URLs instead of `cap://…`.

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

if (parsed.protocol !== "cap:") return;

const action = parsed.hostname as DeeplinkAction;
Copy link

Choose a reason for hiding this comment

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

Minor robustness: cap://start-recording uses hostname, but cap:///start-recording would put the action in pathname instead. Might be worth supporting both so links work even if clients format the URL slightly differently.

const params = Object.fromEntries(parsed.searchParams.entries());

switch (action) {
case "start-recording":
await invoke("start_recording", params);
Copy link

Choose a reason for hiding this comment

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

start-recording is currently passing through all query params to start_recording. If the only supported query param is device-id (for the switch actions), I'd keep start_recording to {} so deeplinks can’t accidentally change behavior as you add params in the future.

Suggested change
await invoke("start_recording", params);
await invoke("start_recording", {});

break;
case "stop-recording":
Comment on lines +31 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.

start_recording receives untyped string params

The start_recording command expects a typed StartRecordingInputs struct on the Rust side (confirmed in apps/desktop/src-tauri/src/recording.rs). Passing params — a flat Record<string, string> built directly from URL query parameters — will not satisfy that schema and will cause a deserialization error at runtime.

The URL query params should be mapped to the expected struct fields explicitly, or this action should piggyback on the existing Rust deeplink system (deeplink_actions.rs) which already handles StartRecording with proper JSON-encoded payloads (?value={…}).

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/lib/deeplinks.ts
Line: 31-33

Comment:
**`start_recording` receives untyped string params**

The `start_recording` command expects a typed `StartRecordingInputs` struct on the Rust side (confirmed in `apps/desktop/src-tauri/src/recording.rs`). Passing `params` — a flat `Record<string, string>` built directly from URL query parameters — will not satisfy that schema and will cause a deserialization error at runtime.

The URL query params should be mapped to the expected struct fields explicitly, or this action should piggyback on the existing Rust deeplink system (`deeplink_actions.rs`) which already handles `StartRecording` with proper JSON-encoded payloads (`?value={…}`).

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

await invoke("stop_recording", {});
break;
case "pause-recording":
await invoke("pause_recording", {});
break;
case "resume-recording":
await invoke("resume_recording", {});
break;
case "restart-recording":
await invoke("restart_recording", {});
break;
case "switch-microphone": {
const deviceId = params["device-id"] ?? null;
await invoke("set_microphone", { deviceId });
break;
}
case "switch-camera": {
const deviceId = params["device-id"] ?? null;
await invoke("set_camera", { deviceId });
break;
Comment on lines +47 to +53
Copy link
Contributor

Choose a reason for hiding this comment

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

Wrong Tauri command names for microphone and camera

The registered Tauri commands (confirmed in apps/desktop/src-tauri/src/lib.rs, lines 3034–3035) are set_mic_input and set_camera_input, not set_microphone and set_camera. Invoking non-existent command names will throw a runtime error.

Suggested change
await invoke("set_microphone", { deviceId });
break;
}
case "switch-camera": {
const deviceId = params["device-id"] ?? null;
await invoke("set_camera", { deviceId });
break;
case "switch-microphone": {
const deviceId = params["device-id"] ?? null;
await invoke("set_mic_input", { label: deviceId });
break;
}
case "switch-camera": {
const deviceId = params["device-id"] ?? null;
await invoke("set_camera_input", { cameraId: deviceId });
break;
}

Note: also verify the parameter key names (label / cameraId) against the Rust function signatures for set_mic_input and set_camera_input.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/lib/deeplinks.ts
Line: 47-53

Comment:
**Wrong Tauri command names for microphone and camera**

The registered Tauri commands (confirmed in `apps/desktop/src-tauri/src/lib.rs`, lines 3034–3035) are `set_mic_input` and `set_camera_input`, not `set_microphone` and `set_camera`. Invoking non-existent command names will throw a runtime error.

```suggestion
      case "switch-microphone": {
        const deviceId = params["device-id"] ?? null;
        await invoke("set_mic_input", { label: deviceId });
        break;
      }
      case "switch-camera": {
        const deviceId = params["device-id"] ?? null;
        await invoke("set_camera_input", { cameraId: deviceId });
        break;
      }
```

Note: also verify the parameter key names (`label` / `cameraId`) against the Rust function signatures for `set_mic_input` and `set_camera_input`.

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

}
default:
console.warn("Unknown deeplink action:", action);
}
} catch (e) {
console.error("Failed to handle deeplink:", url, e);
}
}
60 changes: 60 additions & 0 deletions apps/raycast-extension/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
{
"$schema": "https://www.raycast.com/schemas/extension.json",
"name": "cap",
"title": "Cap",
"description": "Control Cap screen recordings from Raycast",
"icon": "cap-icon.png",
"author": "cap",
"categories": ["Applications", "Productivity"],
"license": "MIT",
"commands": [
{
"name": "start-recording",
"title": "Start Recording",
"description": "Start a new Cap screen recording",
"mode": "no-view"
},
{
"name": "stop-recording",
"title": "Stop Recording",
"description": "Stop the current Cap screen recording",
"mode": "no-view"
},
{
"name": "pause-recording",
"title": "Pause Recording",
"description": "Pause the current Cap screen recording",
"mode": "no-view"
},
{
"name": "resume-recording",
"title": "Resume Recording",
"description": "Resume the paused Cap screen recording",
"mode": "no-view"
},
{
"name": "restart-recording",
"title": "Restart Recording",
"description": "Restart the current Cap screen recording",
"mode": "no-view"
}
],
Copy link

Choose a reason for hiding this comment

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

PR description mentions Raycast commands for switch-microphone / switch-camera, but the extension manifest only includes start/stop/pause/resume/restart. If that’s intentional for this PR, all good; otherwise, adding those two commands (and matching src/*.ts files) would complete the set.

"dependencies": {
"@raycast/api": "^1.79.0"
},
"devDependencies": {
"@raycast/utils": "^1.17.0",
"@types/node": "20.8.10",
"@types/react": "18.3.3",
"eslint": "^8.57.0",
"prettier": "^3.3.3",
"typescript": "^5.4.5"
},
"scripts": {
"build": "ray build -e dist",
"dev": "ray develop",
"fix-lint": "ray lint --fix",
"lint": "ray lint",
"publish": "npx @raycast/api@latest publish"
}
}
Comment on lines +1 to +60
Copy link
Contributor

Choose a reason for hiding this comment

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

switch-microphone and switch-camera commands are missing

The PR description explicitly states that switch-microphone and switch-camera deeplinks accept an optional ?device-id= query param and are part of the feature. However, neither command has a corresponding source file in apps/raycast-extension/src/ nor an entry in the commands array here.

The two commands need to be added both to this commands array and as individual .ts source files (e.g., src/switch-microphone.ts and src/switch-camera.ts) that accept or prompt for a device-id.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/raycast-extension/package.json
Line: 1-60

Comment:
**`switch-microphone` and `switch-camera` commands are missing**

The PR description explicitly states that `switch-microphone` and `switch-camera` deeplinks accept an optional `?device-id=` query param and are part of the feature. However, neither command has a corresponding source file in `apps/raycast-extension/src/` nor an entry in the `commands` array here.

The two commands need to be added both to this `commands` array and as individual `.ts` source files (e.g., `src/switch-microphone.ts` and `src/switch-camera.ts`) that accept or prompt for a `device-id`.

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

6 changes: 6 additions & 0 deletions apps/raycast-extension/src/pause-recording.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { open, showHUD } from "@raycast/api";

export default async function Command() {
await open("cap://pause-recording");
await showHUD("⏸ Pausing Cap recording…");
}
6 changes: 6 additions & 0 deletions apps/raycast-extension/src/restart-recording.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { open, showHUD } from "@raycast/api";

export default async function Command() {
await open("cap://restart-recording");
await showHUD("🔄 Restarting Cap recording…");
}
6 changes: 6 additions & 0 deletions apps/raycast-extension/src/resume-recording.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { open, showHUD } from "@raycast/api";

export default async function Command() {
await open("cap://resume-recording");
await showHUD("▶ Resuming Cap recording…");
}
6 changes: 6 additions & 0 deletions apps/raycast-extension/src/start-recording.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { open, showHUD } from "@raycast/api";

export default async function Command() {
await open("cap://start-recording");
await showHUD("▶ Starting Cap recording…");
}
6 changes: 6 additions & 0 deletions apps/raycast-extension/src/stop-recording.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { open, showHUD } from "@raycast/api";

export default async function Command() {
await open("cap://stop-recording");
await showHUD("⏹ Stopping Cap recording…");
}
14 changes: 14 additions & 0 deletions apps/raycast-extension/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"$schema": "https://www.raycast.com/schemas/extension.json",
Copy link

Choose a reason for hiding this comment

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

This $schema looks like the Raycast extension manifest schema rather than the TSConfig schema. Swapping it makes editor validation/autocomplete work correctly.

Suggested change
"$schema": "https://www.raycast.com/schemas/extension.json",
"$schema": "https://json.schemastore.org/tsconfig",

Copy link
Contributor

Choose a reason for hiding this comment

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

Wrong $schema value for a TypeScript config file

The $schema field points to the Raycast extension schema (extension.json), but this is a tsconfig.json. That schema has no knowledge of TypeScript compiler options and will produce false validation errors in any editor that respects $schema.

Suggested change
"$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: apps/raycast-extension/tsconfig.json
Line: 2

Comment:
**Wrong `$schema` value for a TypeScript config file**

The `$schema` field points to the Raycast *extension* schema (`extension.json`), but this is a `tsconfig.json`. That schema has no knowledge of TypeScript compiler options and will produce false validation errors in any editor that respects `$schema`.

```suggestion
  "$schema": "https://json.schemastore.org/tsconfig",
```

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

"compilerOptions": {
"strict": true,
"module": "CommonJS",
"target": "ES2020",
"jsx": "react-jsx",
"jsxImportSource": "react",
"lib": ["ES2020"],
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"isolatedModules": true
}
}