diff --git a/packages/cloud/src/CloudService.ts b/packages/cloud/src/CloudService.ts index 8c8320cba7..1ff433c140 100644 --- a/packages/cloud/src/CloudService.ts +++ b/packages/cloud/src/CloudService.ts @@ -216,6 +216,20 @@ export class CloudService extends EventEmitter implements Di return this.authService!.handleCallback(code, state, organizationId) } + public async handleManualToken(token: string): Promise { + this.ensureInitialized() + if (!this.authService) { + throw new Error("Auth service not available") + } + // For WebAuthService, we need to add a method to handle manual tokens + // Type guard to check if the auth service is WebAuthService + if (this.authService instanceof WebAuthService) { + return this.authService.handleManualToken(token) + } else { + throw new Error("Manual token authentication not supported with current auth service") + } + } + // SettingsService public getAllowList(): OrganizationAllowList { diff --git a/packages/cloud/src/WebAuthService.ts b/packages/cloud/src/WebAuthService.ts index 934ca90b71..cede4bd68a 100644 --- a/packages/cloud/src/WebAuthService.ts +++ b/packages/cloud/src/WebAuthService.ts @@ -330,6 +330,42 @@ export class WebAuthService extends EventEmitter implements A } } + /** + * Handle manual token input + * + * This method allows users to manually paste a token from the authentication page + * when automatic redirect doesn't work (e.g., in Firebase Studio) + * + * @param token The authentication token from the URL + */ + public async handleManualToken(token: string): Promise { + if (!token || !token.trim()) { + throw new Error("Invalid token provided") + } + + try { + // The token is the ticket that we need to exchange for credentials + const credentials = await this.clerkSignIn(token.trim()) + + // For manual token, we don't have organization context from the URL + // Set it to null (personal account) by default + credentials.organizationId = null + + await this.storeCredentials(credentials) + + const vscode = await importVscode() + + if (vscode) { + vscode.window.showInformationMessage("Successfully authenticated with Roo Code Cloud via manual token") + } + + this.log("[auth] Successfully authenticated with Roo Code Cloud via manual token") + } catch (error) { + this.log(`[auth] Error handling manual token: ${error}`) + throw new Error(`Failed to authenticate with manual token: ${error}`) + } + } + /** * Log out * diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 080fbbcd94..ac7de213b5 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -2251,6 +2251,35 @@ export const webviewMessageHandler = async ( break } + case "rooCloudManualToken": { + if (message.token) { + try { + // Extract the actual token from the URL if a full URL is pasted + let token = message.token.trim() + const tokenMatch = token.match(/[?&]token=([^&]+)/) + if (tokenMatch) { + token = tokenMatch[1] + } + + // Call the manual token authentication method + await CloudService.instance.handleManualToken(token) + await provider.postStateToWebview() + provider.postMessageToWebview({ + type: "authenticatedUser", + userInfo: CloudService.instance.getUserInfo() || undefined, + }) + } catch (error) { + provider.log( + `Manual token authentication failed: ${error instanceof Error ? error.message : String(error)}`, + ) + vscode.window.showErrorMessage( + t("common:errors.manual_token_auth_failed") || + "Manual token authentication failed. Please check the token and try again.", + ) + } + } + break + } case "saveCodeIndexSettingsAtomic": { if (!message.codeIndexSettings) { diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index 565712bfbf..259b4a2340 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -180,6 +180,7 @@ export interface WebviewMessage { | "cloudButtonClicked" | "rooCloudSignIn" | "rooCloudSignOut" + | "rooCloudManualToken" | "condenseTaskContextRequest" | "requestIndexingStatus" | "startIndexing" @@ -222,6 +223,7 @@ export interface WebviewMessage { | "removeQueuedMessage" | "editQueuedMessage" text?: string + token?: string editedMessageContent?: string tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "cloud" disabled?: boolean diff --git a/webview-ui/src/components/cloud/CloudView.tsx b/webview-ui/src/components/cloud/CloudView.tsx index 63733ef7d2..72f86ebc2c 100644 --- a/webview-ui/src/components/cloud/CloudView.tsx +++ b/webview-ui/src/components/cloud/CloudView.tsx @@ -1,5 +1,5 @@ -import { useEffect, useRef } from "react" -import { VSCodeButton } from "@vscode/webview-ui-toolkit/react" +import { useEffect, useRef, useState } from "react" +import { VSCodeButton, VSCodeTextField } from "@vscode/webview-ui-toolkit/react" import { type CloudUserInfo, TelemetryEventName } from "@roo-code/types" @@ -9,7 +9,7 @@ import { vscode } from "@src/utils/vscode" import { telemetryClient } from "@src/utils/TelemetryClient" import { ToggleSwitch } from "@/components/ui/toggle-switch" -import { History, PiggyBank, SquareArrowOutUpRightIcon } from "lucide-react" +import { History, PiggyBank, SquareArrowOutUpRightIcon, Key } from "lucide-react" // Define the production URL constant locally to avoid importing from cloud package in tests const PRODUCTION_ROO_CODE_API_URL = "https://app.roocode.com" @@ -25,6 +25,9 @@ export const CloudView = ({ userInfo, isAuthenticated, cloudApiUrl, onDone }: Cl const { t } = useAppTranslation() const { remoteControlEnabled, setRemoteControlEnabled } = useExtensionState() const wasAuthenticatedRef = useRef(false) + const [showManualToken, setShowManualToken] = useState(false) + const [manualToken, setManualToken] = useState("") + const [tokenError, setTokenError] = useState("") const rooLogoUri = (window as any).IMAGES_BASE_URI + "/roo-logo.svg" @@ -75,6 +78,26 @@ export const CloudView = ({ userInfo, isAuthenticated, cloudApiUrl, onDone }: Cl vscode.postMessage({ type: "remoteControlEnabled", bool: newValue }) } + const handleManualTokenSubmit = () => { + if (!manualToken.trim()) { + setTokenError(t("cloud:manualTokenError") || "Please enter a valid token") + return + } + setTokenError("") + // Extract the token from the URL if a full URL is pasted + let token = manualToken.trim() + const tokenMatch = token.match(/[?&]token=([^&]+)/) + if (tokenMatch) { + token = tokenMatch[1] + } + + // Send the manual token to the backend + vscode.postMessage({ type: "rooCloudManualToken", token }) + // Clear the token field for security + setManualToken("") + setShowManualToken(false) + } + return (
@@ -192,6 +215,45 @@ export const CloudView = ({ userInfo, isAuthenticated, cloudApiUrl, onDone }: Cl {t("cloud:connect")} + + {/* Manual token input section */} +
+ + + {showManualToken && ( +
+

+ {t("cloud:manualTokenDescription") || + "If you're using Firebase Studio or another IDE that doesn't support automatic authentication, you can paste the token from the authentication page here."} +

+
+ { + setManualToken(e.target.value) + setTokenError("") + }} + placeholder={t("cloud:manualTokenPlaceholder") || "Paste token or URL here"} + className="flex-1" + /> + + {t("cloud:submitToken") || "Submit"} + +
+ {tokenError && ( +

{tokenError}

+ )} +
+ )} +
)} diff --git a/webview-ui/src/i18n/locales/en/cloud.json b/webview-ui/src/i18n/locales/en/cloud.json index 88948c9153..f0686ec010 100644 --- a/webview-ui/src/i18n/locales/en/cloud.json +++ b/webview-ui/src/i18n/locales/en/cloud.json @@ -13,5 +13,10 @@ "visitCloudWebsite": "Visit Roo Code Cloud", "remoteControl": "Roomote Control", "remoteControlDescription": "Enable following and interacting with tasks in this workspace with Roo Code Cloud", - "cloudUrlPillLabel": "Roo Code Cloud URL" + "cloudUrlPillLabel": "Roo Code Cloud URL", + "manualTokenLink": "Having trouble? Enter token manually", + "manualTokenDescription": "If you're using Firebase Studio or another IDE that doesn't support automatic authentication, you can paste the token from the authentication page here.", + "manualTokenPlaceholder": "Paste token or URL here", + "manualTokenError": "Please enter a valid token", + "submitToken": "Submit" }