diff --git a/.changeset/fix-jetbrains-open-external-fallback.md b/.changeset/fix-jetbrains-open-external-fallback.md new file mode 100644 index 0000000000..2a5513e7f5 --- /dev/null +++ b/.changeset/fix-jetbrains-open-external-fallback.md @@ -0,0 +1,5 @@ +--- +"claude-dev": patch +--- + +Fix JetBrains sign-in regression by adding fallback for openExternal RPC diff --git a/.changeset/fix-vscode-web-auth-callback.md b/.changeset/fix-vscode-web-auth-callback.md new file mode 100644 index 0000000000..fce2e95b2b --- /dev/null +++ b/.changeset/fix-vscode-web-auth-callback.md @@ -0,0 +1,7 @@ +--- +"cline": patch +--- + +fix: use vscode.env.asExternalUri for auth callback URLs only in VS Code Web + +Fixes OAuth callback redirect in VS Code Web (`code serve-web`) environments by using `vscode.env.asExternalUri()` to resolve the callback URI. This is gated behind a `vscode.env.uiKind === UIKind.Web` check so regular desktop VS Code continues to use the `vscode://` URI directly, avoiding unintended transformations from `asExternalUri`. diff --git a/src/extension.ts b/src/extension.ts index b569cb4307..9142131084 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -352,7 +352,7 @@ export async function activate(context: vscode.ExtensionContext) { ) context.subscriptions.push( - vscode.commands.registerCommand(commands.FocusChatInput, async (preserveEditorFocus: boolean = false) => { + vscode.commands.registerCommand(commands.FocusChatInput, async (preserveEditorFocus = false) => { const webview = WebviewProvider.getInstance() as VscodeWebviewProvider // Show the webview @@ -581,7 +581,20 @@ function setupHostProvider(context: ExtensionContext) { const createCommentReview = () => getVscodeCommentReviewController() const createTerminalManager = () => new VscodeTerminalManager() - const getCallbackUrl = async () => `${vscode.env.uriScheme || "vscode"}://${context.extension.id}` + const getCallbackUrl = async () => { + if (vscode.env.uiKind === vscode.UIKind.Web) { + // In VS Code Web (code serve-web), vscode:// URIs redirect to the desktop app + // instead of staying in the browser. Use an HTTP-based callback server instead, + // which the browser can navigate to directly after auth completes. + const { AuthHandler } = await import("@/hosts/external/AuthHandler") + const authHandler = AuthHandler.getInstance() + authHandler.setEnabled(true) + return authHandler.getCallbackUrl() + } + // In regular desktop VS Code, use the vscode:// URI protocol handler directly. + const baseUri = vscode.Uri.parse(`${vscode.env.uriScheme || "vscode"}://${context.extension.id}`) + return baseUri.toString(true) + } HostProvider.initialize( createWebview, createDiffView, diff --git a/src/hosts/vscode/hostbridge/env/getIdeRedirectUri.ts b/src/hosts/vscode/hostbridge/env/getIdeRedirectUri.ts index 5404e1ab3d..3a573cb061 100644 --- a/src/hosts/vscode/hostbridge/env/getIdeRedirectUri.ts +++ b/src/hosts/vscode/hostbridge/env/getIdeRedirectUri.ts @@ -2,7 +2,12 @@ import { EmptyRequest, String } from "@shared/proto/cline/common" import * as vscode from "vscode" export async function getIdeRedirectUri(_: EmptyRequest): Promise { + if (vscode.env.uiKind === vscode.UIKind.Web) { + // In VS Code Web (code serve-web), the auth callback is handled by an HTTP server + // (AuthHandler). Returning empty here means the success page won't try to redirect + // to a vscode:// URI (which would open the desktop app instead of the web tab). + return { value: "" } + } const uriScheme = vscode.env.uriScheme || "vscode" - const url = `${uriScheme}://saoudrizwan.claude-dev` - return { value: url } + return { value: `${uriScheme}://saoudrizwan.claude-dev` } } diff --git a/src/utils/env.ts b/src/utils/env.ts index 682b77de48..bc79f6f168 100644 --- a/src/utils/env.ts +++ b/src/utils/env.ts @@ -1,4 +1,5 @@ import { EmptyRequest, StringRequest } from "@shared/proto/cline/common" +import { ShowMessageType } from "@shared/proto/host/window" import { HostProvider } from "@/hosts/host-provider" import { Logger } from "@/shared/services/Logger" @@ -33,13 +34,28 @@ export async function readTextFromClipboard(): Promise { } /** - * Opens an external URL in the default browser + * Opens an external URL in the default browser. + * Uses the host bridge RPC first (VS Code's openExternal which handles remote environments). + * Falls back to the `open` npm package if the host doesn't implement the RPC (e.g., JetBrains). * @param url The URL to open * @returns Promise that resolves when the operation is complete - * @throws Error if the operation fails */ export async function openExternal(url: string): Promise { Logger.log("Opening browser:", url) - // Use VS Code's openExternal which handles remote environments - await HostProvider.env.openExternal(StringRequest.create({ value: url })) + try { + await HostProvider.env.openExternal(StringRequest.create({ value: url })) + } catch (error) { + // Fallback for hosts that don't implement openExternal (e.g., JetBrains plugin) + Logger.warn(`Host openExternal RPC failed, falling back to 'open' package: ${error}`) + try { + const open = (await import("open")).default + await open(url) + } catch (fallbackError) { + Logger.error(`Fallback 'open' also failed: ${fallbackError}`) + HostProvider.window.showMessage({ + type: ShowMessageType.ERROR, + message: `Failed to open URL: ${url}`, + }) + } + } }