diff --git a/src/assets/images/hf.png b/src/assets/images/hf.png new file mode 100644 index 0000000000..e60b60f8b0 Binary files /dev/null and b/src/assets/images/hf.png differ diff --git a/webview-ui/src/components/welcome/WelcomeView.tsx b/webview-ui/src/components/welcome/WelcomeView.tsx index 12ef319f92..8ce7a19361 100644 --- a/webview-ui/src/components/welcome/WelcomeView.tsx +++ b/webview-ui/src/components/welcome/WelcomeView.tsx @@ -2,6 +2,7 @@ import { useCallback, useState } from "react" import knuthShuffle from "knuth-shuffle-seeded" import { Trans } from "react-i18next" import { VSCodeButton, VSCodeLink } from "@vscode/webview-ui-toolkit/react" +import pkceChallenge from "pkce-challenge" import type { ProviderSettings } from "@roo-code/types" @@ -9,7 +10,7 @@ import { useExtensionState } from "@src/context/ExtensionStateContext" import { validateApiConfiguration } from "@src/utils/validate" import { vscode } from "@src/utils/vscode" import { useAppTranslation } from "@src/i18n/TranslationContext" -import { getRequestyAuthUrl, getOpenRouterAuthUrl } from "@src/oauth/urls" +import { getRequestyAuthUrl, getOpenRouterAuthUrl, getHuggingFaceAuthUrl } from "@src/oauth/urls" import ApiOptions from "../settings/ApiOptions" import { Tab, TabContent } from "../common/Tab" @@ -47,23 +48,57 @@ const WelcomeView = () => { return w.IMAGES_BASE_URI || "" }) + // Handle Hugging Face OAuth with PKCE + const handleHuggingFaceOAuth = useCallback(async () => { + try { + // Generate PKCE challenge using the library + const pkce = await pkceChallenge() + const state = crypto.randomUUID() // Use built-in UUID for state + + // Store verifier/state in extension (secrets) + vscode.postMessage({ + type: "storeHuggingFacePkce", + values: { verifier: pkce.code_verifier, state }, + }) + + const authUrl = getHuggingFaceAuthUrl(uriScheme, pkce.code_challenge, state) + + // Open externally via extension + vscode.postMessage({ type: "openExternal", url: authUrl }) + } catch (e) { + console.error("Failed to start Hugging Face OAuth:", e) + } + }, [uriScheme]) + + // Handle provider click + const handleProviderClick = useCallback( + (provider: any) => { + if (provider.slug === "hf") { + // Hugging Face needs special handling for PKCE + handleHuggingFaceOAuth() + } else { + // Other providers can use direct links + vscode.postMessage({ type: "openExternal", url: provider.authUrl }) + } + }, + [handleHuggingFaceOAuth], + ) + return ( - + -

{t("welcome:greeting")}

+

{t("welcome:greeting")}

-
+

-

+

+   +

-
- -
-

{t("welcome:startRouter")}

{/* Define the providers */} @@ -83,6 +118,12 @@ const WelcomeView = () => { description: t("welcome:routers.openrouter.description"), authUrl: getOpenRouterAuthUrl(uriScheme), }, + { + slug: "hf", + name: "Hugging Face", + description: t("welcome:routers.huggingface.description"), + authUrl: getHuggingFaceAuthUrl(uriScheme), + }, ] // Shuffle providers based on machine ID (will be consistent for the same machine) @@ -91,12 +132,26 @@ const WelcomeView = () => { // Render the provider cards return orderedProviders.map((provider, index) => ( - + onClick={(e) => { + e.preventDefault() + handleProviderClick(provider) + }} + className="relative flex-1 border border-vscode-panel-border hover:bg-secondary rounded-md py-3 px-4 mb-2 flex flex-row gap-3 cursor-pointer transition-all no-underline text-inherit" + role="button" + tabIndex={0} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault() + handleProviderClick(provider) + } + }}> + {provider.incentive && ( +
+ {provider.incentive} +
+ )}
)) })()}
-

{t("welcome:startCustom")}

+

{t("welcome:startCustom")}