Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion webview-ui/src/components/settings/ApiOptions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,11 @@ const ApiOptions = ({
options.unshift(rooOption)
}
} else {
// Filter out roo from the welcome view
const filteredOptions = options.filter((opt) => opt.value !== "roo")
options.length = 0
options.push(...filteredOptions)

const openRouterIndex = options.findIndex((opt) => opt.value === "openrouter")
if (openRouterIndex > 0) {
const [openRouterOption] = options.splice(openRouterIndex, 1)
Expand All @@ -472,7 +477,7 @@ const ApiOptions = ({
) : (
docs && (
<VSCodeLink href={docs.url} target="_blank" className="flex gap-2">
{docs.name}
{t("settings:providers.apiProviderDocs")}
<BookOpenText className="size-4 inline ml-2" />
</VSCodeLink>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -643,7 +643,7 @@ describe("ApiOptions", () => {
useExtensionStateMock.mockRestore()
})

it("does not pin roo provider to the top on welcome screen", () => {
it("filters out roo provider on welcome screen", () => {
// Mock useExtensionState to ensure no filtering
const useExtensionStateMock = vi.spyOn(ExtensionStateContext, "useExtensionState")
useExtensionStateMock.mockReturnValue({
Expand All @@ -663,13 +663,9 @@ describe("ApiOptions", () => {
// Filter out the placeholder option (empty value)
const providerOptions = options.filter((opt) => opt.value !== "")

// Check that roo is in the list
// Check that roo is NOT in the list when on welcome screen
const rooOption = providerOptions.find((opt) => opt.value === "roo")

if (rooOption) {
// If roo exists, verify it's NOT at the top (should be in alphabetical order)
expect(providerOptions[0].value).not.toBe("roo")
}
expect(rooOption).toBeUndefined()

useExtensionStateMock.mockRestore()
})
Expand Down
12 changes: 10 additions & 2 deletions webview-ui/src/components/welcome/RooHero.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const RooHero = () => {
})

return (
<div className="pb-4 forced-color-adjust-none group">
<div className="mb-4 relative forced-color-adjust-none group flex flex-col items-center w-30 pt-4 overflow-clip">
<div
style={{
backgroundColor: "var(--vscode-foreground)",
Expand All @@ -18,9 +18,17 @@ const RooHero = () => {
maskRepeat: "no-repeat",
maskSize: "contain",
}}
className="mx-auto group-hover:animate-bounce translate-y-0 transition-transform duration-500">
className="z-5 mr-auto group-hover:animate-bounce translate-y-0 transition-transform duration-500">
<img src={imagesBaseUri + "/roo-logo.svg"} alt="Roo logo" className="h-8 opacity-0" />
</div>
<div
className="w-[200%] -mt-0.25 h-0.5 overflow-hidden opacity-0 group-hover:opacity-70 transition-opacity duration-300"
data-testid="roo-hero-ground">
<div className="w-full border-b-1 group-hover:border-b-1 border-dashed border-vscode-foreground animate-ground-slide" />
</div>
<div className="z-4 bg-gradient-to-r from-transparent to-vscode-sideBar-background absolute top-0 right-0 bottom-0 w-10 opacity-100" />
<div className="z-3 bg-gradient-to-l from-transparent to-vscode-sideBar-background absolute top-0 left-0 bottom-0 w-10 opacity-100" />
<div className="bg-vscode-foreground/10 rounded-full size-10 z-1 absolute -bottom-4 animate-sun opacity-0 group-hover:opacity-100 transition-opacity duration-300 blur-[2px]" />
</div>
)
}
Expand Down
182 changes: 130 additions & 52 deletions webview-ui/src/components/welcome/WelcomeViewProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,20 @@ import { Tab, TabContent } from "../common/Tab"

import RooHero from "./RooHero"
import { Trans } from "react-i18next"
import { ArrowLeft, ArrowRight, BadgeInfo } from "lucide-react"
import { ArrowLeft, ArrowRight, BadgeInfo, Brain, TriangleAlert } from "lucide-react"
import { buildDocLink } from "@/utils/docLinks"

type ProviderOption = "roo" | "custom"
type AuthOrigin = "landing" | "providerSelection"

const WelcomeViewProvider = () => {
const { apiConfiguration, currentApiConfigName, setApiConfiguration, uriScheme, cloudIsAuthenticated } =
useExtensionState()
const { t } = useAppTranslation()
const [errorMessage, setErrorMessage] = useState<string | undefined>(undefined)
const [selectedProvider, setSelectedProvider] = useState<ProviderOption>("roo")
const [selectedProvider, setSelectedProvider] = useState<ProviderOption | null>(null)
const [authInProgress, setAuthInProgress] = useState(false)
const [authOrigin, setAuthOrigin] = useState<AuthOrigin | null>(null)
const [showManualEntry, setShowManualEntry] = useState(false)
const [manualUrl, setManualUrl] = useState("")
const [manualErrorMessage, setManualErrorMessage] = useState<boolean | undefined>(undefined)
Expand Down Expand Up @@ -72,17 +75,32 @@ const WelcomeViewProvider = () => {
)

const handleGetStarted = useCallback(() => {
if (selectedProvider === "roo") {
// Trigger cloud sign-in with provider signup flow
// NOTE: We intentionally do NOT save the API configuration yet.
// The configuration will be saved by the extension after auth completes.
// This keeps showWelcome true so we can show the waiting state.
// Landing screen - always trigger auth with Roo
if (selectedProvider === null) {
setAuthOrigin("landing")
vscode.postMessage({ type: "rooCloudSignIn", useProviderSignup: true })

// Show the waiting state
setAuthInProgress(true)
}
// Provider Selection screen
else if (selectedProvider === "roo") {
if (cloudIsAuthenticated) {
// Already authenticated - save config and finish
const rooConfig: ProviderSettings = {
apiProvider: "roo",
}
vscode.postMessage({
type: "upsertApiConfiguration",
text: currentApiConfigName,
apiConfiguration: rooConfig,
})
} else {
// Need to authenticate
setAuthOrigin("providerSelection")
vscode.postMessage({ type: "rooCloudSignIn", useProviderSignup: true })
setAuthInProgress(true)
}
} else {
// Use custom provider - validate first
// Custom provider - validate first
const error = apiConfiguration ? validateApiConfiguration(apiConfiguration) : undefined

if (error) {
Expand All @@ -93,14 +111,34 @@ const WelcomeViewProvider = () => {
setErrorMessage(undefined)
vscode.postMessage({ type: "upsertApiConfiguration", text: currentApiConfigName, apiConfiguration })
}
}, [selectedProvider, apiConfiguration, currentApiConfigName])
}, [selectedProvider, cloudIsAuthenticated, apiConfiguration, currentApiConfigName])

const handleNoAccount = useCallback(() => {
// Navigate to Provider Selection, defaulting to Roo option
setSelectedProvider("roo")
}, [])

const handleBackToLanding = useCallback(() => {
// Return to the landing screen
setSelectedProvider(null)
setErrorMessage(undefined)
}, [])

const handleGoBack = useCallback(() => {
setAuthInProgress(false)
setShowManualEntry(false)
setManualUrl("")
setManualErrorMessage(false)
}, [])

// Return to the appropriate screen based on origin
if (authOrigin === "providerSelection") {
// Keep selectedProvider as-is, user returns to Provider Selection
} else {
// Return to Landing
setSelectedProvider(null)
}
setAuthOrigin(null)
}, [authOrigin])

const handleManualUrlChange = (e: any) => {
const url = e.target.value
Expand All @@ -126,17 +164,17 @@ const WelcomeViewProvider = () => {
}, [manualUrl])

const handleOpenSignupUrl = () => {
vscode.postMessage({ type: "rooCloudSignIn", useProviderSignup: true })
vscode.postMessage({ type: "rooCloudSignIn", useProviderSignup: false })
}

// Render the waiting for cloud state
if (authInProgress) {
return (
<Tab>
<TabContent className="flex flex-col gap-4 p-6">
<TabContent className="flex flex-col gap-4 p-6 justify-center">
<div className="flex flex-col items-start gap-4 pt-8">
<VSCodeProgressRing className="size-6" />
<h2 className="mt-0 mb-0 text-lg font-semibold">{t("welcome:waitingForCloud.heading")}</h2>
<h2 className="my-0 text-xl font-semibold">{t("welcome:waitingForCloud.heading")}</h2>
<p className="text-vscode-descriptionForeground mt-0">
{t("welcome:waitingForCloud.description")}
</p>
Expand All @@ -159,25 +197,25 @@ const WelcomeViewProvider = () => {
</div>

<div className="flex gap-2 items-start pr-4 text-vscode-descriptionForeground">
<ArrowRight className="size-4 inline shrink-0" />
<TriangleAlert className="size-4 inline shrink-0" />
<div>
<p className="m-0">
<Trans
i18nKey="welcome:waitingForCloud.havingTrouble"
components={{
clickHere: (
<button
onClick={() => setShowManualEntry(true)}
className="text-vscode-textLink-foreground hover:text-vscode-textLink-activeForeground underline cursor-pointer bg-transparent border-none p-0 "
/>
),
}}
/>
</p>

{showManualEntry && (
{!showManualEntry ? (
<p className="m-0">
<Trans
i18nKey="welcome:waitingForCloud.havingTrouble"
components={{
clickHere: (
<button
onClick={() => setShowManualEntry(true)}
className="text-vscode-textLink-foreground hover:text-vscode-textLink-activeForeground underline cursor-pointer bg-transparent border-none p-0 "
/>
),
}}
/>
</p>
) : (
<div className="w-full max-w-sm">
<p className="text-vscode-descriptionForeground">
<p className="text-vscode-descriptionForeground mt-0">
{t("welcome:waitingForCloud.pasteUrl")}
</p>
<div className="flex gap-2 items-center">
Expand All @@ -195,6 +233,22 @@ const WelcomeViewProvider = () => {
<ArrowRight className="size-4" />
</Button>
</div>
<p className="mt-2">
<Trans
i18nKey="welcome:waitingForCloud.docsLink"
components={{
DocsLink: (
<a
href={buildDocLink("roo-code-cloud/login", "setup")}
target="_blank"
rel="noopener noreferrer"
className="text-vscode-textLink-foreground hover:underline">
{t("common:docsLink.label")}
</a>
),
}}
/>
</p>
{manualUrl && manualErrorMessage && (
<p className="text-vscode-errorForeground mt-2">
{t("welcome:waitingForCloud.invalidURL")}
Expand All @@ -217,24 +271,48 @@ const WelcomeViewProvider = () => {
)
}

// Landing screen - shown when selectedProvider === null
if (selectedProvider === null) {
return (
<Tab>
<TabContent className="flex flex-col gap-4 p-6 justify-center">
<RooHero />
<h2 className="mt-0 mb-0 text-xl">{t("welcome:landing.greeting")}</h2>

<div className="space-y-4 leading-normal">
<p className="text-base text-vscode-foreground">
<Trans i18nKey="welcome:landing.introduction" />
</p>
<p className="mb-0 font-semibold">
<Trans i18nKey="welcome:landing.accountMention" />
</p>
</div>

<div className="mt-2 flex gap-2 items-center">
<Button onClick={handleGetStarted} variant="primary">
{t("welcome:landing.getStarted")}
</Button>
<VSCodeLink onClick={handleNoAccount} className="cursor-pointer">
{t("welcome:landing.noAccount")}
</VSCodeLink>
</div>
</TabContent>
</Tab>
)
}

// Provider Selection screen - shown when selectedProvider is "roo" or "custom"
return (
<Tab>
<TabContent className="flex flex-col gap-4 p-6 justify-center">
<RooHero />
<h2 className="mt-0 mb-0 text-xl">{t("welcome:greeting")}</h2>
<Brain className="size-8" strokeWidth={1.5} />
<h2 className="mt-0 mb-0 text-xl">{t("welcome:providerSignup.heading")}</h2>

<div className="text-base text-vscode-foreground space-y-3">
{selectedProvider === "roo" && (
<p>
<Trans i18nKey="welcome:introduction" />
</p>
)}
<p>
<Trans i18nKey="welcome:chooseProvider" />
</p>
</div>
<p className="text-base text-vscode-foreground">
<Trans i18nKey="welcome:providerSignup.chooseProvider" />
</p>

<div className="mb-4">
<div>
<VSCodeRadioGroup
value={selectedProvider}
onChange={(e: Event | React.FormEvent<HTMLElement>) => {
Expand All @@ -249,13 +327,12 @@ const WelcomeViewProvider = () => {
{t("welcome:providerSignup.rooCloudProvider")}
</p>
<p className="text-base text-vscode-descriptionForeground mt-0">
{t("welcome:providerSignup.rooCloudDescription")} (
{t("welcome:providerSignup.rooCloudDescription")}{" "}
<VSCodeLink
href="https://roocode.com/provider/pricing?utm_source=extension&utm_medium=welcome-screen&utm_campaign=provider-signup&utm_content=learn-more"
className="cursor-pointer">
{t("welcome:providerSignup.learnMore")}
</VSCodeLink>
).
</p>
</div>
</VSCodeRadio>
Expand All @@ -277,9 +354,6 @@ const WelcomeViewProvider = () => {
<div className="mb-8 border-l-2 border-vscode-panel-border pl-6 ml-[7px]">
<div
className={`overflow-clip transition-[max-height] ease-in-out duration-300 ${selectedProvider === "custom" ? "max-h-[600px]" : "max-h-0"}`}>
<p className="text-base text-vscode-descriptionForeground mt-0">
{t("welcome:providerSignup.noApiKeys")}
</p>
<ApiOptions
fromWelcomeView
apiConfiguration={apiConfiguration || {}}
Expand All @@ -292,9 +366,13 @@ const WelcomeViewProvider = () => {
</div>
</div>

<div className="-mt-8">
<div className="-mt-4 flex gap-2">
<Button onClick={handleBackToLanding} variant="secondary">
<ArrowLeft className="size-4" />
{t("welcome:providerSignup.goBack")}
</Button>
<Button onClick={handleGetStarted} variant="primary">
{t("welcome:providerSignup.getStarted")} →
{t("welcome:providerSignup.finish")} →
</Button>
</div>
</TabContent>
Expand Down
Loading
Loading