From 2ceac319b8d0a0c5ccbe171b04859dcb9a09c640 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Tue, 3 Mar 2026 11:54:27 -0500 Subject: [PATCH] fix(site): eagerly fetch API key for Open in Cursor/VS Code buttons (#22554) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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) --- site/src/modules/apps/apps.ts | 2 +- site/src/pages/AgentsPage/AgentDetail.tsx | 61 +++++++++-------------- 2 files changed, 24 insertions(+), 39 deletions(-) diff --git a/site/src/modules/apps/apps.ts b/site/src/modules/apps/apps.ts index f1e001334b..deabbc2753 100644 --- a/site/src/modules/apps/apps.ts +++ b/site/src/modules/apps/apps.ts @@ -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({ diff --git a/site/src/pages/AgentsPage/AgentDetail.tsx b/site/src/pages/AgentsPage/AgentDetail.tsx index f1ffc58ab0..c05f73ab1f 100644 --- a/site/src/pages/AgentsPage/AgentDetail.tsx +++ b/site/src/pages/AgentsPage/AgentDetail.tsx @@ -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,