Skip to content

Add Cap recording deeplinks and Raycast controls#1842

Open
partyplatter08-lab wants to merge 2 commits into
CapSoftware:mainfrom
partyplatter08-lab:cap-1540-deeplinks-raycast
Open

Add Cap recording deeplinks and Raycast controls#1842
partyplatter08-lab wants to merge 2 commits into
CapSoftware:mainfrom
partyplatter08-lab:cap-1540-deeplinks-raycast

Conversation

@partyplatter08-lab
Copy link
Copy Markdown

@partyplatter08-lab partyplatter08-lab commented May 19, 2026

/claim #1540

Summary

  • add semantic cap-desktop://record/* deeplinks for starting, stopping, pausing, resuming, and toggling pause
  • add cap-desktop://device/* deeplinks for microphone and camera switching, including camera model ID, device ID, label, and off selectors
  • add a Raycast extension package with Cap recording controls, device switching, deeplink copy actions, docs, and a local parser smoke

Validation

  • pnpm exec biome check apps/raycast/package.json apps/raycast/tsconfig.json apps/raycast/README.md apps/raycast/src apps/raycast/scripts apps/desktop/src-tauri/DEEPLINKS.md
  • pnpm --dir apps/raycast exec tsc --noEmit --pretty false
  • pnpm --dir apps/raycast run smoke:devices
  • pnpm --dir apps/raycast run lint

Greptile Summary

This PR adds semantic cap-desktop:// deeplinks for recording control (start, stop, pause, resume, toggle-pause) and device switching (microphone by label, camera by model ID / device ID / label), plus a new Raycast extension that wraps those deeplinks into two commands. The Rust deeplink parser is substantially rewritten from a single legacy action handler into a routed URL parser with comprehensive unit tests.

  • deeplink_actions.rs is refactored into a proper URL router (record/*, device/*) with new action variants, case-insensitive camera resolution, and a CameraSelector enum; the existing action?value= legacy path is preserved unchanged.
  • lib.rs extracts the start_recording_from_saved_settings helper so it can be shared between the RequestStartRecording event handler and the new deeplink StartSavedRecording action.
  • apps/raycast/ introduces a self-contained Raycast extension with a recording-control list command, a device-switching list command backed by a heuristic system_profiler parser, and a smoke test script.

Confidence Score: 3/5

The recording-control and Raycast extension changes are generally well-structured, but a device-switch failure over a deeplink can leave an active recording stuck in a paused state with no recovery path.

The pause_for_input_change helper pauses a live recording before switching camera or microphone, but there is no matching resume on the error paths that follow it. An external caller who sends an unknown camera device_id or model_id will find their recording silently paused and never auto-resumed.

apps/desktop/src-tauri/src/deeplink_actions.rs — the SetCamera and SetMicrophone execute arms need an error-path resume guard.

Important Files Changed

Filename Overview
apps/desktop/src-tauri/src/deeplink_actions.rs Major rewrite of the deeplink parser — adds record/device URL routing, new actions (pause, resume, toggle-pause, SetMicrophone, SetCamera), a camera resolution helper, and comprehensive tests. Has a real issue: if a camera/mic switch fails after pause_for_input_change has already paused the recording, the recording remains stuck paused.
apps/desktop/src-tauri/src/lib.rs Extracts inline start_recording_from_saved_settings logic into a named pub(crate) function, reused by both RequestStartRecording and the new deeplink StartSavedRecording action. Refactor is semantically equivalent to the original.
apps/raycast/src/lib/devices.ts Heuristic system_profiler parser for enumerating microphones and cameras into deeplink URLs; well-guarded against false positives via hasChildObjects, name-match filters, and URL-based deduplication for cameras.
apps/raycast/src/switch-device.tsx Device-switching Raycast command with correct cancel/unmount guard on state updates, but showToast in the error path fires outside the cancelled check, potentially showing a stale toast after the command is dismissed.
apps/raycast/src/lib/deeplinks.ts Static list of Cap recording control deeplinks and an openCapDeeplink helper with error handling via Raycast Toast — straightforward and correct.
apps/raycast/src/cap-control.tsx Recording control Raycast command rendering the static action list; clean and straightforward.
apps/raycast/scripts/device-parser-smoke.ts Node.js smoke test for buildDeviceItemsFromSystemProfiler covering microphone, model-ID camera, device-ID camera, label-only camera, false-positive filtering, and off entries — good coverage of the heuristic parser.
apps/desktop/src-tauri/DEEPLINKS.md New documentation file covering all deeplink schemes, including the id alias for device_id, error handling notes, and how the routes map to existing Tauri commands.
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
apps/desktop/src-tauri/src/deeplink_actions.rs:372-383
**Recording left paused after failed device switch**

`pause_for_input_change` pauses an active recording before the switch, but there is no corresponding resume if the subsequent operation fails. If `resolve_camera_selector` returns an error (e.g. unknown device ID passed by an external caller), or if `set_camera_input` / `set_mic_input` fails, the recording silently remains paused with no way for the deeplink caller to recover it. The same issue exists in `SetMicrophone` (line 373). Consider wrapping the device-switch call in a guard that resumes the recording on any error path.

### Issue 2 of 2
apps/raycast/src/switch-device.tsx:21-32
`showToast` is called outside the `if (!cancelled)` guard, so it can fire after the Raycast command has already been dismissed. The state updates above it are correctly guarded — the toast should be too, to stay consistent and avoid surfacing stale error notifications.

```suggestion
			} catch (loadError) {
				const message =
					loadError instanceof Error ? loadError.message : String(loadError);
				if (!cancelled) {
					setItems([]);
					setError(message);
					await showToast({
						style: Toast.Style.Failure,
						title: "Failed to enumerate devices",
						message,
					});
				}
```

Reviews (1): Last reviewed commit: "feat: add Cap deeplink controls and Rayc..." | Re-trigger Greptile

Greptile also left 2 inline comments on this PR.

@superagent-security superagent-security Bot added the contributor:verified Contributor passed trust analysis. label May 19, 2026
@socket-security
Copy link
Copy Markdown

socket-security Bot commented May 19, 2026

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Added@​raycast/​api@​1.104.179610084100100

View full report

@superagent-security superagent-security Bot added the pr:flagged PR flagged for review by security analysis. label May 19, 2026
Copy link
Copy Markdown

@superagent-security superagent-security Bot left a comment

Choose a reason for hiding this comment

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

Superagent found 2 security concern(s).

Comment on lines +372 to +383
DeepLinkAction::SetMicrophone { label } => {
pause_for_input_change(app).await?;
crate::set_mic_input(app.state(), label).await
}
DeepLinkAction::SetCamera { selector } => {
pause_for_input_change(app).await?;
let camera_id = selector
.as_ref()
.map(|selector| resolve_camera_selector(selector, &CameraIdentity::current()))
.transpose()?;
crate::set_camera_input(app.clone(), app.state(), camera_id, None).await
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Recording left paused after failed device switch

pause_for_input_change pauses an active recording before the switch, but there is no corresponding resume if the subsequent operation fails. If resolve_camera_selector returns an error (e.g. unknown device ID passed by an external caller), or if set_camera_input / set_mic_input fails, the recording silently remains paused with no way for the deeplink caller to recover it. The same issue exists in SetMicrophone (line 373). Consider wrapping the device-switch call in a guard that resumes the recording on any error path.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src-tauri/src/deeplink_actions.rs
Line: 372-383

Comment:
**Recording left paused after failed device switch**

`pause_for_input_change` pauses an active recording before the switch, but there is no corresponding resume if the subsequent operation fails. If `resolve_camera_selector` returns an error (e.g. unknown device ID passed by an external caller), or if `set_camera_input` / `set_mic_input` fails, the recording silently remains paused with no way for the deeplink caller to recover it. The same issue exists in `SetMicrophone` (line 373). Consider wrapping the device-switch call in a guard that resumes the recording on any error path.

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

Comment on lines +21 to +32
} catch (loadError) {
const message =
loadError instanceof Error ? loadError.message : String(loadError);
if (!cancelled) {
setItems([]);
setError(message);
}
await showToast({
style: Toast.Style.Failure,
title: "Failed to enumerate devices",
message,
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 showToast is called outside the if (!cancelled) guard, so it can fire after the Raycast command has already been dismissed. The state updates above it are correctly guarded — the toast should be too, to stay consistent and avoid surfacing stale error notifications.

Suggested change
} catch (loadError) {
const message =
loadError instanceof Error ? loadError.message : String(loadError);
if (!cancelled) {
setItems([]);
setError(message);
}
await showToast({
style: Toast.Style.Failure,
title: "Failed to enumerate devices",
message,
});
} catch (loadError) {
const message =
loadError instanceof Error ? loadError.message : String(loadError);
if (!cancelled) {
setItems([]);
setError(message);
await showToast({
style: Toast.Style.Failure,
title: "Failed to enumerate devices",
message,
});
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/raycast/src/switch-device.tsx
Line: 21-32

Comment:
`showToast` is called outside the `if (!cancelled)` guard, so it can fire after the Raycast command has already been dismissed. The state updates above it are correctly guarded — the toast should be too, to stay consistent and avoid surfacing stale error notifications.

```suggestion
			} catch (loadError) {
				const message =
					loadError instanceof Error ? loadError.message : String(loadError);
				if (!cancelled) {
					setItems([]);
					setError(message);
					await showToast({
						style: Toast.Style.Failure,
						title: "Failed to enumerate devices",
						message,
					});
				}
```

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

@superagent-security superagent-security Bot removed the pr:flagged PR flagged for review by security analysis. label May 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🙋 Bounty claim contributor:verified Contributor passed trust analysis.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant