mirror of
https://github.com/cline/cline.git
synced 2026-09-11 16:42:40 +08:00
Compare commits
21
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7190d0fe10 | ||
|
|
b5d0f6faf9 | ||
|
|
7c3909d8e8 | ||
|
|
48abfd4536 | ||
|
|
9c4a18b170 | ||
|
|
e99e5b990e | ||
|
|
9ed7ef54dd | ||
|
|
59bcb08466 | ||
|
|
72d8d53d1f | ||
|
|
309e9546f1 | ||
|
|
dd3fda8a3b | ||
|
|
51cfd464cd | ||
|
|
7ced24fd13 | ||
|
|
21dae24b0a | ||
|
|
92eb399c33 | ||
|
|
b4590f2a21 | ||
|
|
ce72bb6b3a | ||
|
|
d6529a81e8 | ||
|
|
52e621bfa1 | ||
|
|
e110259167 | ||
|
|
8ef3a3b735 |
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -209,6 +209,7 @@ interface PriceTier {
|
||||
}
|
||||
|
||||
export interface ModelInfo {
|
||||
name?: string
|
||||
maxTokens?: number
|
||||
contextWindow?: number
|
||||
supportsImages?: boolean
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
|
||||
@@ -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> {
|
||||
|
||||
Generated
+24
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
@@ -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",
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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 }
|
||||
@@ -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) => ({
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user