Skip to content

feat(raycast): add official Cap Raycast extension with saved user defaults (#28) - #4149

Open
cocvn157 wants to merge 1 commit into
CapSoftware:mainfrom
cocvn157:feat/raycast-extension-28
Open

cocvn157 wants to merge 1 commit into
CapSoftware:mainfrom
cocvn157:feat/raycast-extension-28

Conversation

@cocvn157

@cocvn157 cocvn157 commented Nov 13, 2026 •

Copy link
Copy Markdown

Closes #28
/claim #28

Summary

Adds the official Raycast extension for Cap (extensions/raycast) along with native GPUI deep-link support (apps/desktop-gpui/src/deeplink.rs and apps/desktop-gpui/src/hotkeys.rs) that preserves the user's saved recording defaults.

Why StartDefaultRecording is needed in apps/desktop-gpui

Issue #28 specifies:

  • Start recording (w/ default settings set by user)
  • Stop recording (Copies the video URL by default at the end of the recording)
  • Opening the web dashboard

In apps/desktop-gpui/src/deeplink.rs, the existing DeepLinkAction::StartRecording arm explicitly overwrites Feeds::global(cx) (feeds.set_camera(selection, cx); feeds.set_microphone(mic_label.clone(), cx)). Sending camera: null and mic_label: null from an external launcher clears the user's selected camera and microphone in Cap and forces a hardcoded recording mode and display.

This PR adds:

  1. DeepLinkAction::StartDefaultRecording { mode: Option<RecordingMode> } (apps/desktop-gpui/src/deeplink.rs & hotkeys.rs):
    • Reuses hotkeys::start_default_recording(mode, cx) (start_from_hotkey), which preserves the user's selected camera, microphone, and system audio in MainWindow / Feeds::global(cx), honors the confirmBeforeRecordingWithoutMicrophone safety check, and resolves the capture target in priority order:
      1. Armed target in the main window (view.armed_target())
      2. Last recorded target from recording_settings.target (verified live via target_is_live)
      3. Display containing the cursor (scap_targets::Display::get_containing_cursor()), falling back to scap_targets::Display::primary()
    • Uses the user's saved recording mode (Mode::from_store()) when mode is omitted, while allowing an optional "instant" or "studio" override from Raycast command arguments.
    • Also adds "default", "primary", and "cursor" display aliases to CaptureMode::Screen in DeepLinkAction::StartRecording for direct screen-targeted deep links.
  2. Official Raycast Extension (extensions/raycast):
    • start-recording: Starts recording using the user's saved Cap defaults via {"start_default_recording":{}} (with an optional dropdown argument to override Instant or Studio mode).
    • stop-recording: Stops the active Cap recording via "stop_recording" (in Instant mode, Cap automatically copies the shareable video link to the clipboard).
    • open-dashboard: Reads general_settings.serverUrl from Cap's shared so.cap.desktop/store file (with an optional Raycast extension preference override and https://cap.so/dashboard fallback) so both Cap Cloud and self-hosted Cap instances open the right dashboard.
    • open-settings: Opens the Cap desktop settings window via {"open_settings":{"page":null}}.

Verification

  • Added unit tests parses_start_default_recording_action_urls and parses_start_recording_action_url_default_and_cursor_screen in apps/desktop-gpui/src/deeplink.rs.
  • Formatted all TypeScript and JSON files with Biome (tab indentation, double quotes, sorted imports) in accordance with AGENTS.md.

RetriggerView in GreptileConfidence Score: 4/5

Fix pending-start cancellation before merging so Stop cannot be followed by an unexpected recording.

Findings

  1. P1 Recording starts after Stop ▶
  2. P2 Dashboard URL keeps pasted paths ▶
  3. P2 Tests miss display selection ▶
Fix with agent prompt
### Issue 1
apps/desktop-gpui/src/deeplink.rs:474
`StartDefaultRecording` waits for target lookup and microphone checks before creating a `RecordingStartPermit`. If `StopRecording` arrives during that wait, there is no permit to cancel and the session stays idle. The waiting task can then finish and start recording after the user asked to stop.

Create a cancellable start request before the first wait, and check it before showing prompts or starting capture.

### Issue 2
extensions/raycast/src/utils.ts:93-96
`resolveDashboardUrl` appends `/dashboard` to the raw preference, including any query or fragment. For example, `https://host/dashboard?x=1` becomes `https://host/dashboard?x=1/dashboard`. Pasted paths can also send users to the wrong page.

Desktop settings already reduce pasted URLs to their HTTP or HTTPS origin. Parse the preference the same way before adding the dashboard path.

### Issue 3
apps/desktop-gpui/src/deeplink.rs:903-906
`parses_start_recording_action_url_default_and_cursor_screen` only checks that alias strings parse. `CaptureMode::Screen(String)` already accepts arbitrary strings, so this test would pass without the new alias-selection code. It cannot catch a change that records the wrong display.

Extract display selection into a testable helper and check primary selection, cursor selection, and the cursor fallback.

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!

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

Adds four Raycast commands and a desktop action that starts recording with the user's saved inputs.

  • Raycast starts recordings with Cap’s saved settings.
  • Direct recording links can name the primary or cursor display.
  • Raycast stops the active Cap recording.
  • Raycast opens the dashboard for the chosen Cap server.
  • Raycast opens Cap’s settings window.

Reviews (1) · Last reviewed commit: "feat(raycast): add official Cap Raycast ..." · Reviewed by Greptile

@vercel

vercel Bot commented Nov 13, 2026

Copy link
Copy Markdown
Contributor

@cocvn157 is attempting to deploy a commit to the Cap Software Inc Team on Vercel.

A member of the Team first needs to authorize it.

}
None => None,
};
crate::hotkeys::start_default_recording(mode, cx);

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 starts after Stop

StartDefaultRecording waits for target lookup and microphone checks before creating a RecordingStartPermit. If StopRecording arrives during that wait, there is no permit to cancel and the session stays idle. The waiting task can then finish and start recording after the user asked to stop.

Create a cancellable start request before the first wait, and check it before showing prompts or starting capture.

Knowledge Base Used:

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop-gpui/src/deeplink.rs
Line: 474

Comment:
**Recording starts after Stop**

`StartDefaultRecording` waits for target lookup and microphone checks before creating a `RecordingStartPermit`. If `StopRecording` arrives during that wait, there is no permit to cancel and the session stays idle. The waiting task can then finish and start recording after the user asked to stop.

Create a cancellable start request before the first wait, and check it before showing prompts or starting capture.

**Knowledge Base Used:**
- [GPUI desktop client](https://app.greptile.com/-/switch-tenant?tenant=cap&next=%2Fcustom-context%2Fknowledge-base%2Fcapsoftware%2Fcap%2F-%2Fdocs%2Fgpui-desktop-client.md)
- [Desktop application and recording experience](https://app.greptile.com/-/switch-tenant?tenant=cap&next=%2Fcustom-context%2Fknowledge-base%2Fcapsoftware%2Fcap%2F-%2Fdocs%2Fdesktop-application.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Comment on lines +93 to +96
const normalized = candidate.replace(/\/+$/, "");
return normalized.endsWith("/dashboard")
? normalized
: `${normalized}/dashboard`;

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 Dashboard URL keeps pasted paths

resolveDashboardUrl appends /dashboard to the raw preference, including any query or fragment. For example, https://host/dashboard?x=1 becomes https://host/dashboard?x=1/dashboard. Pasted paths can also send users to the wrong page.

Desktop settings already reduce pasted URLs to their HTTP or HTTPS origin. Parse the preference the same way before adding the dashboard path.

Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/raycast/src/utils.ts
Line: 93-96

Comment:
**Dashboard URL keeps pasted paths**

`resolveDashboardUrl` appends `/dashboard` to the raw preference, including any query or fragment. For example, `https://host/dashboard?x=1` becomes `https://host/dashboard?x=1/dashboard`. Pasted paths can also send users to the wrong page.

Desktop settings already reduce pasted URLs to their HTTP or HTTPS origin. Parse the preference the same way before adding the dashboard path.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Comment on lines +903 to +906
assert_eq!(
DeepLinkAction::try_from(&url),
Ok(DeepLinkAction::StartRecording {
capture_mode: CaptureMode::Screen(screen_alias.to_string()),

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 Tests miss display selection

parses_start_recording_action_url_default_and_cursor_screen only checks that alias strings parse. CaptureMode::Screen(String) already accepts arbitrary strings, so this test would pass without the new alias-selection code. It cannot catch a change that records the wrong display.

Extract display selection into a testable helper and check primary selection, cursor selection, and the cursor fallback.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop-gpui/src/deeplink.rs
Line: 903-906

Comment:
**Tests miss display selection**

`parses_start_recording_action_url_default_and_cursor_screen` only checks that alias strings parse. `CaptureMode::Screen(String)` already accepts arbitrary strings, so this test would pass without the new alias-selection code. It cannot catch a change that records the wrong display.

Extract display selection into a testable helper and check primary selection, cursor selection, and the cursor fallback.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

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!

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cap.so Raycast extension

1 participant