fix(site): eagerly fetch API key for Open in Cursor/VS Code buttons (#22554)

## Problem

The **Open in Cursor** and **Open in VS Code** buttons on the agent
detail page were broken. Clicking them did nothing.

### Root Cause

The `handleOpenInEditor` handler in `AgentDetail.tsx` called
`window.location.assign()` with a custom protocol URI (`vscode://` or
`cursor://`) **after** an `await API.getApiKey()` call. This creates an
async boundary that breaks the browser's user gesture chain, causing
custom protocol navigations (`vscode://`, `cursor://`) to be silently
blocked by the browser.

The handler was invoked from a Radix `DropdownMenuItem.onSelect`, which
adds another layer of event indirection that makes the gesture chain
more fragile.

In contrast, the workspace page's `VSCodeDesktopButton` works because it
uses a direct `onClick` handler on a button element.

## Fix

- **Eagerly fetch and cache the API key** via `useQuery` when workspace
and agent data is available
- **Make `handleOpenInEditor` synchronous** — it reads the cached key
instead of awaiting a network call, keeping `window.location.assign()`
within the original user gesture context
- **Disable buttons** while the API key is still loading
(`canOpenEditors` now gates on key availability)
- **Simplify** the `onOpenInEditor` callback (remove `void` async
wrapper)
This commit is contained in:
Kyle Carberry
2026-03-03 16:54:27 +00:00
committed by GitHub
parent bca638a498
commit 2ceac319b8
2 changed files with 24 additions and 39 deletions
+1 -1
View File
@@ -37,7 +37,7 @@ type GetVSCodeHrefParams = {
};
export const getVSCodeHref = (
app: "vscode" | "vscode-insiders",
app: "vscode" | "vscode-insiders" | "cursor",
{ owner, workspace, token, agent, folder }: GetVSCodeHrefParams,
) => {
const query = new URLSearchParams({
+23 -38
View File
@@ -21,7 +21,6 @@ import {
getTerminalHref,
getVSCodeHref,
openAppInNewWindow,
SESSION_TOKEN_PLACEHOLDER,
} from "modules/apps/apps";
import {
type FC,
@@ -839,45 +838,33 @@ const AgentDetail: FC = () => {
: undefined;
const shouldShowDiffPanel = hasDiffStatus && showDiffPanel;
const handleOpenInEditor = async (editor: "cursor" | "vscode") => {
const generateKeyMutation = useMutation({
mutationFn: () => API.getApiKey(),
});
const handleOpenInEditor = (editor: "cursor" | "vscode") => {
if (!workspace || !workspaceAgent) {
return;
}
try {
const { key } = await API.getApiKey();
const vscodeHref = getVSCodeHref("vscode", {
owner: workspace.owner_name,
workspace: workspace.name,
token: key,
agent: workspaceAgent.name,
folder: workspaceAgent.expanded_directory,
});
if (editor === "cursor") {
const cursorApp = workspaceAgent.apps.find((app) => {
const name = (app.display_name ?? app.slug).toLowerCase();
return app.slug.toLowerCase() === "cursor" || name === "cursor";
generateKeyMutation.mutate(undefined, {
onSuccess: ({ key }) => {
location.href = getVSCodeHref(editor, {
owner: workspace.owner_name,
workspace: workspace.name,
token: key,
agent: workspaceAgent.name,
folder: workspaceAgent.expanded_directory,
});
if (cursorApp?.external && cursorApp.url) {
const href = cursorApp.url.includes(SESSION_TOKEN_PLACEHOLDER)
? cursorApp.url.replaceAll(SESSION_TOKEN_PLACEHOLDER, key)
: cursorApp.url;
window.location.assign(href);
return;
}
window.location.assign(vscodeHref.replace(/^vscode:/, "cursor:"));
return;
}
window.location.assign(vscodeHref);
} catch {
toast.error(
editor === "cursor"
? "Failed to open in Cursor."
: "Failed to open in VS Code.",
);
}
},
onError: () => {
toast.error(
editor === "cursor"
? "Failed to open in Cursor."
: "Failed to open in VS Code.",
);
},
});
};
const handleViewWorkspace = () => {
@@ -1035,9 +1022,7 @@ const AgentDetail: FC = () => {
workspace={{
canOpenEditors,
canOpenWorkspace,
onOpenInEditor: (editor) => {
void handleOpenInEditor(editor);
},
onOpenInEditor: handleOpenInEditor,
onViewWorkspace: handleViewWorkspace,
onOpenTerminal: handleOpenTerminal,
sshCommand,