Compare commits

...
Author SHA1 Message Date
Arafatkatze 7190d0fe10 example of common data 2025-11-03 23:34:26 -08:00
abeatrix b5d0f6faf9 update AuthServiceMock 2025-11-03 16:34:58 -08:00
abeatrix 7c3909d8e8 feat: add loading screen to onboarding flow
- Add loading state (step 2) during authentication process
- Simplify welcomeViewCompleted logic by removing auth service check
- Move welcomeViewCompleted state update to auth success handler
- Display loading spinner during sign-in to improve UX
- Ensure auth status update occurs in finally block for reliability

The loading screen provides visual feedback while authentication completes, preventing users from seeing incomplete UI states during the sign-in process.
2025-11-03 15:43:48 -08:00
abeatrix 48abfd4536 Fix search box reset state 2025-11-03 13:03:24 -08:00
abeatrix 9c4a18b170 clean up 2025-11-03 12:38:05 -08:00
abeatrix e99e5b990e remove unused description field 2025-11-03 12:37:25 -08:00
abeatrix 9ed7ef54dd Update search box placeholder text 2025-11-03 12:31:33 -08:00
abeatrix 59bcb08466 style(onboarding): adjust max-width constraint placement
Move max-width constraint from parent container to content wrapper div to improve responsive layout behavior. This ensures the full width is utilized at the top level while constraining only the scrollable content area.

Changes:
- Added w-full to root container for proper width handling
- Removed max-w-lg from middle container
- Applied max-w-lg to content wrapper instead
2025-11-03 12:30:11 -08:00
abeatrix 72d8d53d1f update badge 2025-10-31 15:06:16 -07:00
abeatrix 309e9546f1 update e2e test 2025-10-31 15:05:03 -07:00
abeatrix dd3fda8a3b search result info text 2025-10-31 13:28:23 -07:00
abeatrix 51cfd464cd spacing 2025-10-31 13:15:38 -07:00
abeatrix 7ced24fd13 Update langauge 2025-10-31 13:09:34 -07:00
abeatrix 21dae24b0a async onboarding 2025-10-31 13:04:05 -07:00
abeatrix 92eb399c33 clean up 2025-10-31 12:48:37 -07:00
abeatrix b4590f2a21 improve model selection with names and improved search
- Add name field to ModelInfo interface for better model identification
- Populate model name from OpenRouter API response
- Improve model search filtering to exclude embedding models
- Add case-insensitive search for better UX
- Enhance UI with badges for model capabilities and pricing
- Update styling for better visual hierarchy and selected state
- Display model names in search results for clarity

This improves the onboarding experience by making model selection more informative and user-friendly with better search capabilities and visual feedback.
2025-10-31 12:27:29 -07:00
abeatrix ce72bb6b3a Merge branch 'main' into bee/onboarding 2025-10-31 08:54:39 -07:00
abeatrix d6529a81e8 Add search box 2025-10-29 16:33:54 -07:00
abeatrix 52e621bfa1 Update model list 2025-10-29 16:08:12 -07:00
abeatrix e110259167 Debug buttons 2025-10-29 13:21:01 -07:00
abeatrix 8ef3a3b735 feat(auth): new onboarding UI
- Add optional `strict` parameter to `createAuthRequest()` to prevent opening new auth windows when already authenticated
- Update onboarding flow with new UI text and button labels ("Login to Cline", "I have my own key", "Ready")
- Add new onboarding data models and step configuration for improved user experience
- Update all e2e tests to reflect new button labels and authentication flow
- Remove obsolete `closeBanners` utility function
- Refactor authentication logic to support strict mode for better control over auth window behavior

This change improves the authentication UX by preventing duplicate auth windows and provides a more streamlined onboarding experience with clearer call-to-action buttons.
2025-10-29 12:45:40 -07:00
28 changed files with 1117 additions and 82 deletions
+1
View File
@@ -94,6 +94,7 @@ message OpenRouterModelInfo {
optional ThinkingConfig thinking_config = 10;
optional bool supports_global_endpoint = 11;
repeated ModelTier tiers = 12;
optional string name = 13;
}
// Shared response message for model information
+4 -4
View File
@@ -855,9 +855,9 @@ export class Controller {
const vscodeTerminalExecutionMode = this.stateManager.getGlobalStateKey("vscodeTerminalExecutionMode")
const defaultTerminalProfile = this.stateManager.getGlobalSettingsKey("defaultTerminalProfile")
const isNewUser = this.stateManager.getGlobalStateKey("isNewUser")
const welcomeViewCompleted = Boolean(
this.stateManager.getGlobalStateKey("welcomeViewCompleted") || this.authService.getInfo()?.user?.uid,
)
// Can be undefined but is set to either true or false by the migration that runs on extension launch in extension.ts
const welcomeViewCompleted = !!this.stateManager.getGlobalStateKey("welcomeViewCompleted")
const customPrompt = this.stateManager.getGlobalSettingsKey("customPrompt")
const mcpResponsesCollapsed = this.stateManager.getGlobalStateKey("mcpResponsesCollapsed")
const terminalOutputLineLimit = this.stateManager.getGlobalSettingsKey("terminalOutputLineLimit")
@@ -934,7 +934,7 @@ export class Controller {
vscodeTerminalExecutionMode: vscodeTerminalExecutionMode,
defaultTerminalProfile,
isNewUser,
welcomeViewCompleted: welcomeViewCompleted as boolean, // Can be undefined but is set to either true or false by the migration that runs on extension launch in extension.ts
welcomeViewCompleted,
mcpResponsesCollapsed,
terminalOutputLineLimit,
maxConsecutiveMistakes,
@@ -88,6 +88,7 @@ export async function refreshOpenRouterModels(controller: Controller): Promise<R
for (const rawModel of rawModels as OpenRouterRawModelInfo[]) {
const supportThinking = rawModel.supported_parameters?.some((p) => p === "include_reasoning")
const modelInfo: ModelInfo = {
name: rawModel.name,
maxTokens: rawModel.top_provider?.max_completion_tokens ?? 0,
contextWindow: rawModel.context_length ?? 0,
supportsImages: rawModel.architecture?.modality?.includes("image") ?? false,
+7 -3
View File
@@ -3,6 +3,7 @@ import { type EmptyRequest, String } from "@shared/proto/cline/common"
import { ClineEnv } from "@/config"
import { Controller } from "@/core/controller"
import { getRequestRegistry, type StreamingResponseHandler } from "@/core/controller/grpc-handler"
import { setWelcomeViewCompleted } from "@/core/controller/state/setWelcomeViewCompleted"
import { HostProvider } from "@/hosts/host-provider"
import { telemetryService } from "@/services/telemetry"
import { openExternal } from "@/utils/env"
@@ -229,8 +230,9 @@ export class AuthService {
})
}
async createAuthRequest(): Promise<String> {
if (this._authenticated) {
async createAuthRequest(strict = false): Promise<String> {
// In strict mode, we do not open a new auth window if already authenticated
if (strict && this._authenticated) {
this.sendAuthStatusUpdate()
return String.create({ value: "Already authenticated" })
}
@@ -279,11 +281,13 @@ export class AuthService {
this._authenticated = this._clineAuthInfo?.idToken !== undefined
telemetryService.captureAuthSucceeded(this._provider.name)
await this.sendAuthStatusUpdate()
await setWelcomeViewCompleted(this._controller, { value: true })
} catch (error) {
console.error("Error signing in with custom token:", error)
telemetryService.captureAuthFailed(this._provider.name)
throw error
} finally {
await this.sendAuthStatusUpdate()
}
}
+2
View File
@@ -1,6 +1,7 @@
import { String } from "@shared/proto/cline/common"
import { ClineEnv } from "@/config"
import { Controller } from "@/core/controller"
import { setWelcomeViewCompleted } from "@/core/controller/state/setWelcomeViewCompleted"
import { WebviewProvider } from "@/core/webview"
import { CLINE_API_ENDPOINT } from "@/shared/cline/api"
import { AuthService } from "./AuthService"
@@ -121,6 +122,7 @@ export class AuthServiceMock extends AuthService {
override async handleAuthCallback(_token: string, _provider: string): Promise<void> {
try {
this._authenticated = true
await setWelcomeViewCompleted(this._controller, { value: true })
await this.sendAuthStatusUpdate()
} catch (error) {
console.error("Error signing in with custom token:", error)
+1
View File
@@ -209,6 +209,7 @@ interface PriceTier {
}
export interface ModelInfo {
name?: string
maxTokens?: number
contextWindow?: number
supportsImages?: boolean
+7 -7
View File
@@ -5,11 +5,12 @@ import { e2e } from "./utils/helpers"
e2e("Views - can set up API keys and navigate to Settings from Chat", async ({ sidebar }) => {
// Use the page object to interact with editor outside the sidebar
// Verify initial state
await expect(sidebar.getByRole("button", { name: "Get Started for Free" })).toBeVisible()
await expect(sidebar.getByRole("button", { name: "Use your own API key" })).toBeVisible()
await expect(sidebar.getByRole("button", { name: "Login to Cline" })).toBeVisible()
await expect(sidebar.getByText("Bring my own API key")).toBeVisible()
// Navigate to API key setup
await sidebar.getByRole("button", { name: "Use your own API key" }).click()
await sidebar.getByText("Bring my own API key").click()
await sidebar.getByRole("button", { name: "Continue" }).click()
const providerSelectorInput = sidebar.getByTestId("provider-selector-input")
@@ -33,10 +34,9 @@ e2e("Views - can set up API keys and navigate to Settings from Chat", async ({ s
await apiKeyInput.fill("test-api-key")
await expect(apiKeyInput).toHaveValue("test-api-key")
await apiKeyInput.click({ delay: 100 })
const submitButton = sidebar.getByRole("button", { name: "Let's go!" })
await expect(submitButton).toBeEnabled()
await submitButton.click({ delay: 100 })
await expect(sidebar.getByRole("button", { name: "Get Started for Free" })).not.toBeVisible()
await sidebar.getByRole("button", { name: "Continue" }).click()
await expect(sidebar.getByRole("button", { name: "Login to Cline" })).not.toBeVisible()
// Verify start up page is no longer visible
await expect(apiKeyInput).not.toBeVisible()
+1 -1
View File
@@ -7,7 +7,7 @@ e2e.describe("Diff Editor", () => {
e2e.extend({
workspaceType,
})(title, async ({ page, sidebar }) => {
await sidebar.getByRole("button", { name: "Get Started for Free" }).click({ delay: 100 })
await sidebar.getByRole("button", { name: "Login to Cline" }).click({ delay: 100 })
// Submit a message
await cleanChatView(page)
+1 -1
View File
@@ -7,7 +7,7 @@ e2e.describe("Code Actions and Editor Panel", () => {
e2e.extend({
workspaceType,
})(title, async ({ page, sidebar }) => {
await sidebar.getByRole("button", { name: "Get Started for Free" }).click({ delay: 100 })
await sidebar.getByRole("button", { name: "Login to Cline" }).click({ delay: 100 })
// Sidebar - input should start empty
const sidebarInput = sidebar.getByTestId("chat-input")
await sidebarInput.click()
+1 -9
View File
@@ -35,16 +35,8 @@ export const toggleNotifications = async (_page: Page) => {
return _page
}
export const closeBanners = async (sidebar: Page) => {
const banners = ["Get Started for Free", "Close banner and enable"]
for (const banner of banners) {
await sidebar.getByRole("button", { name: banner }).click({ delay: 100 })
}
}
export async function cleanChatView(sidebar: Page): Promise<Page> {
const signUpBtn = sidebar.getByRole("button", { name: "Get Started for Free" })
const signUpBtn = sidebar.getByRole("button", { name: "Login to Cline" })
if (await signUpBtn.isVisible()) {
await signUpBtn.click({ delay: 50 })
}
+2 -15
View File
@@ -118,23 +118,10 @@ export class E2ETestHelper {
}
public async signin(webview: Frame): Promise<void> {
const byokButton = webview.getByRole("button", {
name: "Use your own API key",
})
await expect(byokButton).toBeVisible()
await byokButton.click()
// Complete setup with OpenRouter
const apiKeyInput = webview.getByRole("textbox", {
name: "OpenRouter API Key",
})
await apiKeyInput.fill("test-api-key")
await webview.getByRole("button", { name: "Let's go!" }).click()
await webview.getByRole("button", { name: "Login to Cline" }).click({ delay: 100 })
// Verify start up page is no longer visible
await expect(webview.locator("#api-provider div").first()).not.toBeVisible()
await expect(byokButton).not.toBeVisible()
await expect(webview.getByRole("button", { name: "Login to Cline" })).not.toBeVisible()
}
public static async openClineSidebar(page: Page): Promise<void> {
+24
View File
@@ -15,6 +15,7 @@
"@radix-ui/react-hover-card": "^1.1.15",
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-progress": "^1.1.7",
"@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-tooltip": "^1.2.8",
"@vscode/webview-ui-toolkit": "^1.4.0",
@@ -4090,6 +4091,29 @@
}
}
},
"node_modules/@radix-ui/react-separator": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz",
"integrity": "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-primitive": "2.1.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-slot": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+1
View File
@@ -23,6 +23,7 @@
"@radix-ui/react-hover-card": "^1.1.15",
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-progress": "^1.1.7",
"@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-tooltip": "^1.2.8",
"@vscode/webview-ui-toolkit": "^1.4.0",
+2 -2
View File
@@ -4,8 +4,8 @@ import AccountView from "./components/account/AccountView"
import ChatView from "./components/chat/ChatView"
import HistoryView from "./components/history/HistoryView"
import McpView from "./components/mcp/configuration/McpConfigurationView"
import OnboardingView from "./components/onboarding/OnboardingView"
import SettingsView from "./components/settings/SettingsView"
import WelcomeView from "./components/welcome/WelcomeView"
import { useClineAuth } from "./context/ClineAuthContext"
import { useExtensionState } from "./context/ExtensionStateContext"
import { Providers } from "./Providers"
@@ -54,7 +54,7 @@ const AppContent = () => {
}
if (showWelcome) {
return <WelcomeView />
return <OnboardingView />
}
return (
@@ -0,0 +1,351 @@
import type { ModelInfo } from "@shared/api"
import { AlertCircleIcon, CircleCheckIcon, CircleIcon, ListIcon, LoaderCircleIcon, StarIcon, ZapIcon } from "lucide-react"
import { useCallback, useEffect, useMemo, useState } from "react"
import ClineLogoWhite from "@/assets/ClineLogoWhite"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Item, ItemContent, ItemDescription, ItemHeader, ItemMedia, ItemTitle } from "@/components/ui/item"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { cn } from "@/lib/utils"
import { AccountServiceClient, StateServiceClient } from "@/services/grpc-client"
import ApiConfigurationSection from "../settings/sections/ApiConfigurationSection"
import { useApiConfigurationHandlers } from "../settings/utils/useApiConfigurationHandlers"
import {
getCapabilities,
getOverviewLabel,
getPriceRange,
ONBOARDING_MODEL_SELECTIONS,
type OnboardingModelOption,
} from "./data-models"
import { NEW_USER_TYPE, STEP_CONFIG, USER_TYPE_SELECTIONS } from "./data-steps"
type ModelSelectionProps = {
userType: NEW_USER_TYPE.FREE | NEW_USER_TYPE.POWER
selectedModelId: string
onSelectModel: (modelId: string) => void
models?: Record<string, ModelInfo>
searchTerm: string
setSearchTerm: (term: string) => void
}
const ModelSelection = ({ userType, selectedModelId, onSelectModel, models, searchTerm, setSearchTerm }: ModelSelectionProps) => {
const modelGroups = ONBOARDING_MODEL_SELECTIONS[userType === NEW_USER_TYPE.FREE ? "free" : "power"]
const searchedModels = useMemo(() => {
if (!models || !searchTerm) {
return []
}
const flattenedModels = modelGroups.flatMap((g) => g.models.map((m) => m.id))
// Filter out embedding models and already listed models
const filtered = Object.entries(models).filter(
([id, _info]) => !id.includes("embedding") && !flattenedModels.includes(id) && id.includes(searchTerm.toLowerCase()),
)
return filtered.slice(0, 5) // Return the first 5 models
}, [models, modelGroups, searchTerm])
// Model Item Component
const ModelItem = ({ id, model, isSelected }: { id: string; model: OnboardingModelOption; isSelected: boolean }) => {
return (
<Item
className={cn("cursor-pointer hover:cursor-pointer", {
"bg-input-background/80 border border-button-background": isSelected,
})}
key={id}
onClick={() => onSelectModel(id)}
variant="outline">
<ItemHeader className="flex flex-col w-full align-baseline">
<ItemTitle className="flex w-full justify-between">
<span className="font-semibold">{model.name || id}</span>
{model.badge ? <Badge variant="info">{model.badge}</Badge> : <Badge>{getPriceRange(model)}</Badge>}
</ItemTitle>
{isSelected && (
<ItemDescription>
<span className="text-foreground/70 text-sm">Support: </span>
<span className="text-foreground text-sm">{getCapabilities(model).join(", ")}</span>
</ItemDescription>
)}
</ItemHeader>
{model.badge && isSelected && (
<ItemContent className="w-full border-t border-muted-foreground pt-5 text-ellipsis overflow-hidden">
<div className="flex flex-col gap-3">
{model.score && (
<div className="inline-flex gap-1 [&_svg]:stroke-warning [&_svg]:size-3 items-center text-sm">
<StarIcon />
<span>Model Overview: </span>
<span className="text-foreground/70">{model.score}%</span>
<span className="text-foreground/70 hidden xs:block">{getOverviewLabel(model.score)}</span>
</div>
)}
<div className="inline-flex gap-1 [&_svg]:stroke-success [&_svg]:size-3 items-center text-sm">
<ZapIcon />
<span>Speed: </span>
<span className="text-foreground/70">{model.speed}</span>
</div>
<div className="flex w-full justify-between">
<div className="inline-flex gap-1 [&_svg]:stroke-foreground [&_svg]:size-3 items-center text-sm">
<ListIcon />
<span>Context: </span>
<span className="text-foreground/70">{(model?.contextWindow || 0) / 1000}k</span>
</div>
<Badge>{getPriceRange(model)}</Badge>
</div>
</div>
</ItemContent>
)}
</Item>
)
}
return (
<div className="flex flex-col w-full items-center px-2">
<div className="flex w-full max-w-lg flex-col gap-6 my-4">
{modelGroups.map((group) => (
<div className="flex flex-col gap-3" key={group.group}>
<h4 className="text-sm font-bold text-foreground/70 uppercase mb-2">{group.group}</h4>
{group.models.map((model) => (
<ModelItem id={model.id} isSelected={selectedModelId === model.id} key={model.id} model={model} />
))}
</div>
))}
</div>
{/* SEARCH MODEL */}
<div className="flex w-full max-w-lg flex-col gap-6 my-4 border-t border-muted-foreground">
<div className="flex flex-col gap-3 mt-6" key="search-results">
<h4 className="text-sm font-bold text-foreground/70 uppercase mb-2">other options</h4>
<Input
autoFocus={false}
className="focus-visible:border-button-background"
onChange={(e) => {
if (!e.target?.value) {
onSelectModel("")
}
setSearchTerm(e.target.value)
}}
onClick={() => onSelectModel("")}
placeholder="Search model..."
type="search"
value={searchTerm}
/>
<div className="w-full flex flex-col gap-3">
{searchTerm &&
searchedModels.map(([id, info]) => {
const isSelected = selectedModelId === id
return (
<ModelItem
id={id}
isSelected={isSelected}
key={id}
model={{ id, name: info.name, ...info }}
/>
)
})}
{searchTerm.length > 0 && searchedModels.length === 0 && (
<p className="px-1 mt-1 text-sm text-foreground/70">No result found for "{searchTerm}"</p>
)}
</div>
</div>
</div>
</div>
)
}
type UserTypeSelectionProps = {
userType: NEW_USER_TYPE | undefined
onSelectUserType: (type: NEW_USER_TYPE) => void
}
const UserTypeSelectionStep = ({ userType, onSelectUserType }: UserTypeSelectionProps) => (
<div className="flex flex-col w-full items-center">
<div className="flex w-full max-w-lg flex-col gap-6 my-4">
<h3 className="text-base text-left self-start font-semibold">LETS GET STARTED</h3>
{USER_TYPE_SELECTIONS.map((option) => {
const isSelected = userType === option.type
return (
<Item
className={cn("cursor-pointer hover:cursor-pointer w-full", {
"bg-input-background/50 border border-input-foreground/30": isSelected,
})}
key={option.type}
onClick={() => onSelectUserType(option.type)}>
<ItemMedia className="[&_svg]:stroke-button-background" variant="icon">
{isSelected ? <CircleCheckIcon className="stroke-1.5" /> : <CircleIcon className="stroke-1" />}
</ItemMedia>
<ItemContent className="w-full">
<ItemTitle>{option.title}</ItemTitle>
<ItemDescription>{option.description}</ItemDescription>
</ItemContent>
</Item>
)
})}
</div>
</div>
)
type OnboardingStepContentProps = {
step: number
userType: NEW_USER_TYPE | undefined
selectedModelId: string
onSelectUserType: (type: NEW_USER_TYPE) => void
onSelectModel: (modelId: string) => void
searchTerm: string
setSearchTerm: (term: string) => void
models?: Record<string, ModelInfo>
}
const OnboardingStepContent = ({
step,
userType,
selectedModelId,
onSelectUserType,
onSelectModel,
searchTerm,
setSearchTerm,
models,
}: OnboardingStepContentProps) => {
if (step === 0) {
return <UserTypeSelectionStep onSelectUserType={onSelectUserType} userType={userType} />
}
if (step === 2) {
return null
}
if (userType === NEW_USER_TYPE.FREE || userType === NEW_USER_TYPE.POWER) {
return (
<ModelSelection
models={models}
onSelectModel={onSelectModel}
searchTerm={searchTerm}
selectedModelId={selectedModelId}
setSearchTerm={setSearchTerm}
userType={userType}
/>
)
}
// userType === NEW_USER_TYPE.BYOK
return <ApiConfigurationSection />
}
const OnboardingView = () => {
const { handleFieldsChange } = useApiConfigurationHandlers()
const { openRouterModels, hideSettings, hideAccount, setShowWelcome } = useExtensionState()
const [stepNumber, setStepNumber] = useState(0)
const [userType, setUserType] = useState<NEW_USER_TYPE>(NEW_USER_TYPE.FREE)
const [selectedModelId, setSelectedModelId] = useState("")
const [searchTerm, setSearchTerm] = useState("")
useEffect(() => {
setSearchTerm("")
const userGroup = userType === NEW_USER_TYPE.POWER ? NEW_USER_TYPE.POWER : NEW_USER_TYPE.FREE
const modelGroup = ONBOARDING_MODEL_SELECTIONS[userGroup][0]
const userGroupInitModel = modelGroup.models[0]
setSelectedModelId(userGroupInitModel.id)
}, [userType])
const finishOnboarding = useCallback(
async (updateModelId: boolean) => {
if (updateModelId && selectedModelId) {
await handleFieldsChange({
planModeOpenRouterModelId: selectedModelId,
actModeOpenRouterModelId: selectedModelId,
planModeOpenRouterModelInfo: openRouterModels[selectedModelId],
actModeOpenRouterModelInfo: openRouterModels[selectedModelId],
planModeApiProvider: "cline",
actModeApiProvider: "cline",
})
}
hideAccount()
hideSettings()
},
[hideAccount, hideSettings, handleFieldsChange, selectedModelId, openRouterModels],
)
const handleFooterAction = useCallback(
async (action: "signin" | "next" | "back" | "done" | "signup") => {
switch (action) {
case "signup":
setStepNumber(stepNumber + 1)
await AccountServiceClient.accountLoginClicked({}).catch(() => {})
await finishOnboarding(true)
break
case "signin":
await AccountServiceClient.accountLoginClicked({}).catch(() => {})
await finishOnboarding(true)
break
case "next":
setStepNumber(stepNumber + 1)
break
case "back":
setStepNumber(stepNumber - 1)
break
case "done":
await StateServiceClient.setWelcomeViewCompleted({ value: true }).catch(() => {})
setShowWelcome(false)
await finishOnboarding(false)
break
}
},
[stepNumber, finishOnboarding, setShowWelcome],
)
const stepDisplayInfo = useMemo(() => {
const step = stepNumber === 0 || stepNumber === 2 ? STEP_CONFIG[stepNumber] : null
const title = step ? step.title : userType ? STEP_CONFIG[userType].title : STEP_CONFIG[0].title
const description = step ? step.description : null
const buttons = step ? step.buttons : userType ? STEP_CONFIG[userType].buttons : STEP_CONFIG[0].buttons
return { title, description, buttons }
}, [stepNumber, userType])
return (
<div className="fixed inset-0 p-0 flex flex-col w-full">
<div className="h-full px-5 xs:mx-10 overflow-auto flex flex-col gap-7 items-center justify-center mt-10">
<ClineLogoWhite className="size-16" />
<h2 className="text-lg font-semibold p-0">{stepDisplayInfo.title}</h2>
{stepNumber === 2 && (
<div className="flex w-full max-w-lg flex-col gap-6 my-4 items-center ">
<LoaderCircleIcon className="animate-spin" />
</div>
)}
{stepDisplayInfo.description && (
<p className="text-foreground text-sm text-center m-0 p-0">{stepDisplayInfo.description}</p>
)}
<div className="flex-1 w-full flex max-w-lg overflow-y-scroll">
<OnboardingStepContent
models={openRouterModels}
onSelectModel={setSelectedModelId}
onSelectUserType={setUserType}
searchTerm={searchTerm}
selectedModelId={selectedModelId}
setSearchTerm={setSearchTerm}
step={stepNumber}
userType={userType}
/>
</div>
<footer className="flex w-full max-w-lg flex-col gap-3 my-2 px-2 overflow-hidden">
{stepDisplayInfo.buttons.map((btn) => (
<Button
className="w-full rounded-xs"
key={btn.text}
onClick={() => handleFooterAction(btn.action)}
variant={btn.variant}>
{btn.text}
</Button>
))}
{stepNumber !== 2 && (
<div className="items-center justify-center flex text-sm text-foreground gap-2 mb-3 text-pretty">
<AlertCircleIcon className="shrink-0 size-2" /> You can change this later in settings
</div>
)}
</footer>
</div>
</div>
)
}
export default OnboardingView
@@ -0,0 +1,150 @@
# Onboarding Models Pattern
## Overview
The onboarding model selection uses a **Single Source of Truth** pattern to avoid duplicating model definitions between `src/shared/api.ts` and the webview.
## Architecture
### Files Involved
1. **`src/shared/api.ts`** - Contains complete model definitions with capabilities
2. **`data-models.ts`** - Contains only UI-specific metadata (score, speed, badge)
### How It Works
```typescript
// 1. Define only UI-specific metadata
const ONBOARDING_MODEL_METADATA = {
power: {
"anthropic/claude-sonnet-4.5": {
id: "anthropic/claude-sonnet-4.5",
name: "Anthropic: Claude Sonnet 4.5",
badge: "Best",
score: 97,
speed: "Fast",
}
}
}
// 2. Reference the source model from api.ts
const MODEL_SOURCE_MAP: Record<string, ModelInfo> = {
"anthropic/claude-sonnet-4.5": openRouterDefaultModelInfo,
}
// 3. Merge them together
const model = createOnboardingModel(
ONBOARDING_MODEL_METADATA.power["anthropic/claude-sonnet-4.5"],
MODEL_SOURCE_MAP["anthropic/claude-sonnet-4.5"]
)
```
## Benefits
**No Duplication** - Model capabilities (contextWindow, prices, etc.) are only defined once in `api.ts`
**Automatic Updates** - Changes to model specs in `api.ts` automatically propagate to onboarding
**Clear Separation** - UI metadata (score, badge) is separate from technical specs
**Type Safety** - TypeScript ensures consistency between definitions
## Adding a New Model
To add a new model to the onboarding flow:
### Step 1: Add the model metadata
```typescript
const ONBOARDING_MODEL_METADATA = {
power: {
"new-provider/new-model": {
id: "new-provider/new-model",
name: "Provider: Model Name",
badge: "New", // Optional: "Best", "Trending", "Free"
score: 85, // Performance score 0-100
speed: "Fast", // "Fast", "Average", or "Slow"
}
}
}
```
### Step 2: Map to source model
If the model exists in `api.ts`:
```typescript
const MODEL_SOURCE_MAP: Record<string, ModelInfo> = {
"new-provider/new-model": providerModels["model-id"],
}
```
If the model doesn't exist in `api.ts` yet:
```typescript
const MODEL_SOURCE_MAP: Record<string, ModelInfo> = {
"new-provider/new-model": {
maxTokens: 8192,
contextWindow: 128000,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 1.0,
outputPrice: 5.0,
},
}
```
### Step 3: Add to selection list
```typescript
export const ONBOARDING_MODEL_SELECTIONS = {
power: [
{
group: "frontier",
models: [
createOnboardingModel(
ONBOARDING_MODEL_METADATA.power["new-provider/new-model"],
MODEL_SOURCE_MAP["new-provider/new-model"],
),
]
}
]
}
```
## Important Notes
- **Never duplicate** `maxTokens`, `contextWindow`, `inputPrice`, `outputPrice`, etc. in onboarding metadata
- **Always reference** the source model from `api.ts` when available
- **Only add** UI-specific properties: `name`, `badge`, `score`, `speed`
- **Keep in sync** - When a model is added to `api.ts`, update the source map reference
## Migration from Old Pattern
**Before (duplicated):**
```typescript
{
id: "anthropic/claude-sonnet-4.5",
name: "Anthropic: Claude Sonnet 4.5",
badge: "Best",
score: 97,
speed: "Fast",
contextWindow: 200000, // ❌ Duplicated
supportsImages: true, // ❌ Duplicated
supportsPromptCache: true, // ❌ Duplicated
inputPrice: 3.0, // ❌ Duplicated
outputPrice: 15.0, // ❌ Duplicated
}
```
**After (referenced):**
```typescript
// Metadata only
const metadata = {
id: "anthropic/claude-sonnet-4.5",
name: "Anthropic: Claude Sonnet 4.5",
badge: "Best",
score: 97,
speed: "Fast",
}
// Source from api.ts
const source = openRouterDefaultModelInfo
// Merged automatically
const model = createOnboardingModel(metadata, source)
@@ -0,0 +1,235 @@
import type { ModelInfo } from "@shared/api"
import { cerebrasModels, openAiNativeModels, openRouterDefaultModelInfo } from "@shared/api"
import { NEW_USER_TYPE } from "./data-steps"
export interface OnboardingModelOption extends ModelInfo {
id: string
name?: string
badge?: string
supported_parameters?: string[]
score?: number
speed?: string
}
type ModelGroup = {
group: string
models: OnboardingModelOption[]
}
/**
* Onboarding-specific metadata for models
* Contains only UI-specific properties (score, speed, badge, display name)
* Model capabilities (contextWindow, prices, etc.) are pulled from api.ts
*/
interface OnboardingModelMetadata {
/** Model ID used in OpenRouter or provider-specific format */
id: string
/** Display name for the onboarding UI */
name: string
/** Badge to display (e.g., "Best", "Trending", "Free") */
badge?: string
/** Performance score (0-100) */
score: number
/** Speed indicator ("Fast", "Average", "Slow") */
speed: "Fast" | "Average" | "Slow"
}
/**
* Creates an OnboardingModelOption by merging source model data with metadata
*/
function createOnboardingModel(metadata: OnboardingModelMetadata, sourceModel: ModelInfo): OnboardingModelOption {
return {
...sourceModel,
...metadata,
}
}
/**
* Model metadata definitions - only contains onboarding-specific fields
* Actual model capabilities come from the source models in api.ts
*/
const ONBOARDING_MODEL_METADATA = {
free: {
"x-ai/grok-code-fast-1": {
id: "x-ai/grok-code-fast-1",
name: "xAI: Grok Code Fast 1",
badge: "Best",
score: 90,
speed: "Fast" as const,
},
"minimax/minimax-m1": {
id: "minimax/minimax-m1",
name: "MiniMax: MiniMax M1",
badge: "Trending",
score: 90,
speed: "Fast" as const,
},
},
power: {
"anthropic/claude-sonnet-4.5": {
id: "anthropic/claude-sonnet-4.5",
name: "Anthropic: Claude Sonnet 4.5",
badge: "Best",
score: 97,
speed: "Fast" as const,
},
"openai/gpt-5-codex": {
id: "openai/gpt-5-codex",
name: "OpenAI: GPT-5 Codex",
badge: "Best",
score: 97,
speed: "Slow" as const,
},
"z-ai/glm-4.6:exacto": {
id: "z-ai/glm-4.6:exacto",
name: "Z.AI: GLM 4.6 (exacto)",
badge: "Trending",
score: 90,
speed: "Average" as const,
},
"moonshotai/kimi-dev-72b:free": {
id: "moonshotai/kimi-dev-72b:free",
name: "MoonshotAI: Kimi Dev 72B (free)",
badge: "Free",
score: 90,
speed: "Fast" as const,
},
},
} as const
/**
* Maps model IDs to their source definitions in api.ts
* This creates the single source of truth for model capabilities
*/
const MODEL_SOURCE_MAP: Record<string, ModelInfo> = {
// Free tier models
"x-ai/grok-code-fast-1": {
// Placeholder - this model doesn't exist in api.ts yet
// Using xAI grok-4-fast-reasoning as reference
maxTokens: 30000,
contextWindow: 2000000,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
},
"minimax/minimax-m1": {
// Placeholder - this model doesn't exist in api.ts yet
// Using MiniMax-M2 as reference
maxTokens: 128000,
contextWindow: 1000000,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
},
// Power tier models - reference actual models from api.ts
"anthropic/claude-sonnet-4.5": openRouterDefaultModelInfo,
"openai/gpt-5-codex": {
// Using GPT-5 from openAiNativeModels as base
...openAiNativeModels["gpt-5-2025-08-07"],
// Override with codex-specific values if different
contextWindow: 400000,
},
"z-ai/glm-4.6:exacto": cerebrasModels["zai-glm-4.6"],
"moonshotai/kimi-dev-72b:free": {
// Placeholder - using estimated values
maxTokens: 16384,
contextWindow: 131072,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
},
}
export const ONBOARDING_MODEL_SELECTIONS: Record<"free" | "power", ModelGroup[]> = {
[NEW_USER_TYPE.FREE]: [
{
group: "free",
models: [
createOnboardingModel(
ONBOARDING_MODEL_METADATA.free["x-ai/grok-code-fast-1"],
MODEL_SOURCE_MAP["x-ai/grok-code-fast-1"],
),
createOnboardingModel(
ONBOARDING_MODEL_METADATA.free["minimax/minimax-m1"],
MODEL_SOURCE_MAP["minimax/minimax-m1"],
),
],
},
],
[NEW_USER_TYPE.POWER]: [
{
group: "frontier",
models: [
createOnboardingModel(
ONBOARDING_MODEL_METADATA.power["anthropic/claude-sonnet-4.5"],
MODEL_SOURCE_MAP["anthropic/claude-sonnet-4.5"],
),
createOnboardingModel(
ONBOARDING_MODEL_METADATA.power["openai/gpt-5-codex"],
MODEL_SOURCE_MAP["openai/gpt-5-codex"],
),
],
},
{
group: "open source",
models: [
createOnboardingModel(
ONBOARDING_MODEL_METADATA.power["z-ai/glm-4.6:exacto"],
MODEL_SOURCE_MAP["z-ai/glm-4.6:exacto"],
),
createOnboardingModel(
ONBOARDING_MODEL_METADATA.power["moonshotai/kimi-dev-72b:free"],
MODEL_SOURCE_MAP["moonshotai/kimi-dev-72b:free"],
),
],
},
],
}
export function getPriceRange(modelInfo: ModelInfo): string {
const prompt = Number(modelInfo.inputPrice ?? 0)
const completion = Number(modelInfo.outputPrice ?? 0)
const cost = prompt + completion
if (cost === 0) {
return "Free"
}
if (cost < 10) {
return "$"
}
if (cost > 50) {
return "$$$"
}
return "$$"
}
export function getOverviewLabel(overview: number): string {
if (overview >= 95) {
return "Top Performer"
}
if (overview >= 80) {
return "Great"
}
if (overview >= 60) {
return "Good"
}
if (overview >= 50) {
return "Average"
}
return "Below Average"
}
export function getCapabilities(modelInfo: ModelInfo): string[] {
const capabilities = new Set<string>()
if (modelInfo.supportsImages) {
capabilities.add("Images")
}
if (modelInfo.supportsPromptCache) {
capabilities.add("Prompt Cache")
}
capabilities.add("Tools")
return Array.from(capabilities)
}
@@ -0,0 +1,54 @@
export enum NEW_USER_TYPE {
FREE = "free",
POWER = "power",
BYOK = "byok",
}
type UserTypeSelection = {
title: string
description: string
type: NEW_USER_TYPE
}
export const STEP_CONFIG = {
0: {
title: "How will you use Cline?",
description: "Select an option below to get started.",
buttons: [
{ text: "Continue", action: "next", variant: "default" },
{ text: "Login to Cline", action: "signin", variant: "secondary" },
],
},
[NEW_USER_TYPE.FREE]: {
title: "Select a free model",
buttons: [
{ text: "Create my Account", action: "signup", variant: "default" },
{ text: "Back", action: "back", variant: "secondary" },
],
},
[NEW_USER_TYPE.POWER]: {
title: "Select your model",
buttons: [
{ text: "Create my Account", action: "signup", variant: "default" },
{ text: "Back", action: "back", variant: "secondary" },
],
},
[NEW_USER_TYPE.BYOK]: {
title: "Configure your provider",
buttons: [
{ text: "Continue", action: "done", variant: "default" },
{ text: "Back", action: "back", variant: "secondary" },
],
},
2: {
title: "Almost there!",
description: "Complete account creation in your browser. Then come back here to finish up.",
buttons: [{ text: "Back", action: "back", variant: "secondary" }],
},
} as const
export const USER_TYPE_SELECTIONS: UserTypeSelection[] = [
{ title: "Absolutely Free", description: "Get started at no cost", type: NEW_USER_TYPE.FREE },
{ title: "Frontier Model", description: "Claude 4.5, GPT-5 Codex, etc", type: NEW_USER_TYPE.POWER },
{ title: "Bring my own API key", description: "Use Cline with your provider of choice", type: NEW_USER_TYPE.BYOK },
]
@@ -1,4 +1,4 @@
import { ExtensionMessage } from "@shared/ExtensionMessage"
import type { ExtensionMessage } from "@shared/ExtensionMessage"
import { ResetStateRequest } from "@shared/proto/cline/state"
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import debounce from "debounce"
@@ -6,7 +6,7 @@ import {
CheckCheck,
FlaskConical,
Info,
LucideIcon,
type LucideIcon,
SlidersHorizontal,
SquareMousePointer,
SquareTerminal,
@@ -78,15 +78,6 @@ export const SETTINGS_TABS: SettingsTab[] = [
headerText: "Terminal Settings",
icon: SquareTerminal,
},
// Only show in dev mode
{
id: "debug",
name: "Debug",
tooltipText: "Debug Tools",
headerText: "Debug",
icon: FlaskConical,
hidden: !IS_DEV,
},
{
id: "general",
name: "General",
@@ -101,6 +92,15 @@ export const SETTINGS_TABS: SettingsTab[] = [
headerText: "About",
icon: Info,
},
// Only show in dev mode
{
id: "debug",
name: "Debug",
tooltipText: "Debug Tools",
headerText: "Debug",
icon: FlaskConical,
hidden: !IS_DEV,
},
]
type SettingsViewProps = {
@@ -11,7 +11,7 @@ import { syncModeConfigurations } from "../utils/providerUtils"
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
interface ApiConfigurationSectionProps {
renderSectionHeader: (tabId: string) => JSX.Element | null
renderSectionHeader?: (tabId: string) => JSX.Element | null
}
const ApiConfigurationSection = ({ renderSectionHeader }: ApiConfigurationSectionProps) => {
@@ -20,7 +20,7 @@ const ApiConfigurationSection = ({ renderSectionHeader }: ApiConfigurationSectio
const { handleFieldsChange } = useApiConfigurationHandlers()
return (
<div>
{renderSectionHeader("api-config")}
{renderSectionHeader?.("api-config")}
<Section>
{/* Tabs container */}
{planActSeparateModelsSetting ? (
@@ -1,4 +1,6 @@
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import { Button } from "@/components/ui/button"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { StateServiceClient } from "@/services/grpc-client"
import Section from "../Section"
interface DebugSectionProps {
@@ -7,26 +9,32 @@ interface DebugSectionProps {
}
const DebugSection = ({ onResetState, renderSectionHeader }: DebugSectionProps) => {
const { setShowWelcome } = useExtensionState()
return (
<div>
{renderSectionHeader("debug")}
<Section>
<VSCodeButton
className="mt-[5px] w-auto"
onClick={() => onResetState()}
style={{ backgroundColor: "var(--vscode-errorForeground)", color: "black" }}>
<Button onClick={() => onResetState()} variant="danger">
Reset Workspace State
</VSCodeButton>
<VSCodeButton
className="mt-[5px] w-auto"
onClick={() => onResetState(true)}
style={{ backgroundColor: "var(--vscode-errorForeground)", color: "black" }}>
</Button>
<Button onClick={() => onResetState(true)} variant="danger">
Reset Global State
</VSCodeButton>
</Button>
<p className="text-xs mt-[5px] text-(--vscode-descriptionForeground)">
This will reset all global state and secret storage in the extension.
</p>
</Section>
<Section>
<Button
onClick={async () =>
await StateServiceClient.setWelcomeViewCompleted({ value: false })
.catch(() => {})
.finally(() => setShowWelcome(true))
}
variant="secondary">
Reset Onboarding State
</Button>
</Section>
</div>
)
}
+34
View File
@@ -0,0 +1,34 @@
import { cva, type VariantProps } from "class-variance-authority"
import type * as React from "react"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"inline-flex items-center justify-center border text-xs transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 [&_svg]:size-2",
{
variants: {
variant: {
default: "border-transparent bg-badge-background text-badge-foreground shadow hover:bg-badge-background/80",
info: "border-transparent bg-button-background/80 text-button-foreground hover:bg-button-hover",
danger: "border-transparent bg-error text-error-foreground shadow hover:bg-error/80",
outline: "text-foreground",
},
type: {
default: "rounded-md px-1 font-normal",
round: "rounded-full h-5 w-auto",
},
},
defaultVariants: {
variant: "default",
type: "default",
},
},
)
export interface BadgeProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, type, ...props }: BadgeProps) {
return <div className={cn(badgeVariants({ variant, type }), className)} {...props} />
}
export { Badge, badgeVariants }
+13 -12
View File
@@ -4,25 +4,26 @@ import * as React from "react"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 cursor-pointer [&_svg]:size-2",
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-xs transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 cursor-pointer [&_svg]:size-2",
{
variants: {
variant: {
default: "bg-button-background text-primary-foreground shadow hover:bg-button-background-hover",
destructive: "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
outline: "text-foreground p-0 m-0",
default:
"bg-button-background text-primary-foreground hover:bg-button-hover shadow-sm shadow-button-background/50",
secondary:
"bg-button-secondary-background text-button-secondary-foreground shadow-sm hover:bg-button-secondary-background-hover",
ghost: "bg-transparent border border-foreground/20 shadow-sm hover:bg-accent/10",
link: "text-link underline-offset-4 hover:underline",
"bg-button-secondary-background text-button-secondary-foreground hover:bg-button-secondary-background-hover shadow-sm shadow-button-secondary-background/50",
danger: "bg-error text-background hover:bg-error/90 shadow-sm shadow-error/50",
outline: "hover:bg-accent/10 border border-accent/20 shadow-sm shadow-accent/50",
ghost: "hover:bg-accent/10",
link: "text-link underline-offset-4 hover:underline p-0 m-0",
text: "text-foreground",
icon: "bg-transparent hover:opacity-80 p-0 h-auto m-0 border-0 cursor-pointer hover:bg-transparent hover:shadow-none focus:ring-0 focus:ring-offset-0",
icon: "hover:opacity-80 p-0 m-0 border-0 cursor-pointer hover:shadow-none focus:ring-0 focus:ring-offset-0",
},
size: {
default: "h-5 p-4 [&_svg]:size-3",
sm: "h-3 rounded-md px-3 text-sm [&_svg]:size-2",
xs: "h-1 rounded-xs px-1 text-xs [&_svg]:size-2",
lg: "h-8 rounded-md px-8 [&_svg]:size-3",
default: "py-1.5 px-4 [&_svg]:size-3",
sm: "py-1 px-3 text-sm [&_svg]:size-2",
xs: "p-1 text-xs [&_svg]:size-2",
lg: "py-4 px-8 [&_svg]:size-4 font-medium",
icon: "px-0.5 m-0 [&_svg]:size-2",
},
},
+20
View File
@@ -0,0 +1,20 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(({ className, type, ...props }, ref) => {
return (
<input
className={cn(
"flex w-full rounded-sm border border-input-foreground/20 bg-input-background px-3 py-2 text-base text-input-foreground shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-input-placeholder focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-input-border disabled:cursor-not-allowed disabled:opacity-50 md:text-sm text-pretty text-ellipsis",
className,
)}
ref={ref}
type={type}
{...props}
/>
)
})
Input.displayName = "Input"
export { Input }
+137
View File
@@ -0,0 +1,137 @@
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import type * as React from "react"
import { Separator } from "@/components/ui/separator"
import { cn } from "@/lib/utils"
function ItemGroup({ className, ...props }: React.ComponentProps<"div">) {
return <div className={cn("group/item-group flex flex-col", className)} data-slot="item-group" role="list" {...props} />
}
function ItemSeparator({ className, ...props }: React.ComponentProps<typeof Separator>) {
return <Separator className={cn("my-0", className)} data-slot="item-separator" orientation="horizontal" {...props} />
}
const itemVariants = cva(
"group/item [a]:hover:bg-accent/50 focus-visible:border-ring focus-visible:ring-ring/50 [a]:transition-colors flex flex-wrap items-center rounded-sm border border-transparent text-sm outline-none transition-colors duration-100 focus-visible:ring-[3px]",
{
variants: {
variant: {
default: "bg-transparent",
outline: "border-input-foreground/30",
select: "bg-input-background/50 hover:bg-input-background/70 border border-input-foreground/10",
muted: "bg-muted/50",
},
size: {
default: "gap-4 p-4 ",
sm: "gap-2.5 px-4 py-3",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
)
function Item({
className,
variant = "default",
size = "default",
asChild = false,
...props
}: React.ComponentProps<"div"> & VariantProps<typeof itemVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "div"
return (
<Comp
className={cn(itemVariants({ variant, size, className }))}
data-size={size}
data-slot="item"
data-variant={variant}
{...props}
/>
)
}
const itemMediaVariants = cva(
"flex shrink-0 items-center justify-center gap-2 group-has-[[data-slot=item-description]]/item:translate-y-0.5 group-has-[[data-slot=item-description]]/item:self-start [&_svg]:pointer-events-none",
{
variants: {
variant: {
default: "bg-transparent",
icon: "bg-transparent size-8 rounded-sm [&_svg:not([class*='size-'])]:size-4",
image: "size-10 overflow-hidden rounded-sm [&_img]:size-full [&_img]:object-cover",
},
},
defaultVariants: {
variant: "default",
},
},
)
function ItemMedia({
className,
variant = "default",
selected = false,
...props
}: React.ComponentProps<"div"> & VariantProps<typeof itemMediaVariants> & { selected?: boolean }) {
return (
<div className={cn(itemMediaVariants({ variant, className }))} data-slot="item-media" data-variant={variant} {...props} />
)
}
function ItemContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
className={cn("w-full flex flex-1 flex-col gap-1 [&+[data-slot=item-content]]:flex-none", className)}
data-slot="item-content"
{...props}
/>
)
}
function ItemTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
className={cn("w-full flex items-center gap-2 text-sm font-medium leading-snug", className)}
data-slot="item-title"
{...props}
/>
)
}
function ItemDescription({ className, ...props }: React.ComponentProps<"p">) {
return (
<p
className={cn(
"w-full text-muted-foreground line-clamp-2 text-pretty text-sm font-normal leading-normal p-0 m-0",
"[&>a:hover]:text-foreground [&>a]:underline [&>a]:underline-offset-4",
className,
)}
data-slot="item-description"
{...props}
/>
)
}
function ItemActions({ className, ...props }: React.ComponentProps<"div">) {
return <div className={cn("flex items-center gap-2", className)} data-slot="item-actions" {...props} />
}
function ItemHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
className={cn("w-full flex basis-full items-center justify-between gap-2", className)}
data-slot="item-header"
{...props}
/>
)
}
function ItemFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div className={cn("flex basis-full items-center justify-between gap-2", className)} data-slot="item-footer" {...props} />
)
}
export { Item, ItemMedia, ItemContent, ItemActions, ItemGroup, ItemSeparator, ItemTitle, ItemDescription, ItemHeader, ItemFooter }
@@ -0,0 +1,26 @@
import * as SeparatorPrimitive from "@radix-ui/react-separator"
import * as React from "react"
import { cn } from "@/lib/utils"
function Separator({
className,
orientation = "horizontal",
decorative = true,
...props
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
return (
<SeparatorPrimitive.Root
className={cn(
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
className,
)}
data-slot="separator"
decorative={decorative}
orientation={orientation}
{...props}
/>
)
}
export { Separator }
@@ -80,6 +80,7 @@ export interface ExtensionStateContextType extends ExtensionState {
setMcpMarketplaceCatalog: (value: McpMarketplaceCatalog) => void
setTotalTasksSize: (value: number | null) => void
setExpandTaskHeader: (value: boolean) => void
setShowWelcome: (value: boolean) => void
// Refresh functions
refreshOpenRouterModels: () => void
@@ -316,8 +317,12 @@ export const ExtensionStateContextProvider: React.FC<{
: prevState.autoApprovalSettings,
}
// Update welcome screen state based on API configuration
setShowWelcome(!newState.welcomeViewCompleted)
// Update welcome screen state based on API configuration if welcome view not in progress
if (!newState.welcomeViewCompleted && !showWelcome) {
setShowWelcome(true)
} else if (newState.welcomeViewCompleted) {
setShowWelcome(false)
}
setDidHydrateState(true)
console.log("[DEBUG] returning new state in ESC")
@@ -677,6 +682,7 @@ export const ExtensionStateContextProvider: React.FC<{
hideAnnouncement,
setShowAnnouncement,
hideChatModelSelector,
setShowWelcome,
setShowChatModelSelector,
setShouldShowAnnouncement: (value) =>
setState((prevState) => ({
+2 -2
View File
@@ -26,7 +26,7 @@
--color-button-secondary-background: var(--vscode-button-secondaryBackground);
--color-button-secondary-background-hover: var(--vscode-button-secondaryHoverBackground);
--color-button-secondary-foreground: var(--vscode-button-secondaryForeground);
--color-muted: var(--vscode-editor-foldBackground);
--color-muted: color-mix(in srgb, var(--vscode-toolbar-hoverBackground) 65%, transparent);
--color-muted-foreground: var(--vscode-editor-foldPlaceholderForeground);
--color-menu: var(--vscode-menu-background);
--color-menu-foreground: var(--vscode-menu-foreground);
@@ -74,7 +74,7 @@
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted: color-mix(in srgb, var(--vscode-toolbar-hoverBackground) 65%, transparent);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);