mirror of
https://github.com/cline/cline.git
synced 2026-09-07 22:16:30 +08:00
Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d1f26011aa | ||
|
|
efb941a45f | ||
|
|
76410b42b0 | ||
|
|
e4e347a8a4 | ||
|
|
59e7c2b7b9 | ||
|
|
5371377b21 | ||
|
|
5a444dc30a | ||
|
|
34c48264c0 | ||
|
|
daf14ef181 | ||
|
|
f1bf9b3f90 | ||
|
|
6e20047c3e | ||
|
|
1d4fe88bec | ||
|
|
74dbbb9d6b |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Added change to hide the context window usage message from env details when using next gen models and before the usage has reached an elevated state
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cline/cli/pkg/cli/global"
|
||||
"github.com/cline/cli/pkg/cli/task"
|
||||
@@ -75,6 +76,11 @@ func QuickSetupFromFlags(ctx context.Context, provider, apiKey, modelID, baseURL
|
||||
}
|
||||
}
|
||||
|
||||
// WORKAROUND: Wait for debounced state persistence to complete
|
||||
// Fixes `cline auth` issue when ran in docker environments
|
||||
// TODO: implement better solution w/ changes in StateManager
|
||||
time.Sleep(600 * time.Millisecond)
|
||||
|
||||
// Success message
|
||||
fmt.Printf("\n✓ Successfully configured %s provider\n", GetProviderDisplayName(providerEnum))
|
||||
fmt.Printf(" Model: %s\n", finalModelID)
|
||||
|
||||
@@ -96,6 +96,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
|
||||
@@ -312,7 +313,7 @@ message ApiConfiguration {
|
||||
// Request for updating API configuration (new - uses separate options and secrets)
|
||||
message UpdateApiConfigurationRequestNew {
|
||||
Metadata metadata = 1;
|
||||
ApiConfiguration api_configuration = 2;
|
||||
ApiConfiguration updates = 2;
|
||||
|
||||
// Required field mask specifying which fields to update.
|
||||
// Field paths use dot notation with camelCase field names:
|
||||
|
||||
@@ -23,6 +23,7 @@ service StateService {
|
||||
rpc updateSettingsCli(UpdateSettingsRequestCli) returns (Empty);
|
||||
rpc updateTaskSettings(UpdateTaskSettingsRequest) returns (Empty);
|
||||
rpc updateTelemetrySetting(TelemetrySettingRequest) returns (Empty);
|
||||
rpc captureOnboardingProgress(OnboardingProgressRequest) returns (Empty);
|
||||
rpc setWelcomeViewCompleted(BooleanRequest) returns (Empty);
|
||||
rpc updateInfoBannerVersion(Int64Request) returns (Empty);
|
||||
rpc updateModelBannerVersion(Int64Request) returns (Empty);
|
||||
@@ -379,3 +380,10 @@ message ProcessInfo {
|
||||
optional string version = 2;
|
||||
optional int64 uptime_ms = 3;
|
||||
}
|
||||
|
||||
message OnboardingProgressRequest {
|
||||
int32 step = 1;
|
||||
optional string action = 2;
|
||||
optional bool completed = 3;
|
||||
optional string model_selected = 4;
|
||||
}
|
||||
|
||||
+1
-1
@@ -62,7 +62,7 @@ class ClineEndpoint {
|
||||
environment: Environment.staging,
|
||||
appBaseUrl: "https://staging-app.cline.bot",
|
||||
apiBaseUrl: "https://core-api.staging.int.cline.bot",
|
||||
mcpBaseUrl: "https://api.cline.bot/v1/mcp",
|
||||
mcpBaseUrl: "https://core-api.staging.int.cline.bot/v1/mcp",
|
||||
firebase: {
|
||||
apiKey: "AIzaSyASSwkwX1kSO8vddjZkE5N19QU9cVQ0CIk",
|
||||
authDomain: "cline-staging.firebaseapp.com",
|
||||
|
||||
@@ -853,9 +853,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")
|
||||
@@ -932,7 +932,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,
|
||||
|
||||
@@ -4,7 +4,12 @@ import axios from "axios"
|
||||
import cloneDeep from "clone-deep"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import { CLAUDE_SONNET_1M_TIERS, openRouterClaudeSonnet41mModelId, openRouterClaudeSonnet451mModelId } from "@/shared/api"
|
||||
import {
|
||||
ANTHROPIC_MAX_THINKING_BUDGET,
|
||||
CLAUDE_SONNET_1M_TIERS,
|
||||
openRouterClaudeSonnet41mModelId,
|
||||
openRouterClaudeSonnet451mModelId,
|
||||
} from "@/shared/api"
|
||||
import type { Controller } from ".."
|
||||
|
||||
type OpenRouterSupportedParams =
|
||||
@@ -59,7 +64,6 @@ interface OpenRouterRawModelInfo {
|
||||
input_cache_read: string
|
||||
input_cache_write: string
|
||||
} | null
|
||||
thinking_config: Record<string, unknown> | null
|
||||
supports_global_endpoint: boolean | null
|
||||
tiers: any[] | null
|
||||
supported_parameters?: OpenRouterSupportedParams[] | null
|
||||
@@ -86,8 +90,10 @@ export async function refreshOpenRouterModels(controller: Controller): Promise<R
|
||||
return undefined
|
||||
}
|
||||
for (const rawModel of rawModels as OpenRouterRawModelInfo[]) {
|
||||
const supportThinking = rawModel.supported_parameters?.some((p) => p === "include_reasoning")
|
||||
const supportThinking = rawModel.supported_parameters?.some((p) => p === "include_reasoning" || p === "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,
|
||||
@@ -97,7 +103,9 @@ export async function refreshOpenRouterModels(controller: Controller): Promise<R
|
||||
cacheWritesPrice: parsePrice(rawModel.pricing?.input_cache_write),
|
||||
cacheReadsPrice: parsePrice(rawModel.pricing?.input_cache_read),
|
||||
description: rawModel.description ?? "",
|
||||
thinkingConfig: (supportThinking && rawModel.thinking_config) || undefined,
|
||||
// If thinking is supported, set maxBudget with a default value as a placeholder
|
||||
// to ensure it has a valid thinkingConfig that lets the application know thinking is supported.
|
||||
thinkingConfig: supportThinking ? { maxBudget: ANTHROPIC_MAX_THINKING_BUDGET } : undefined,
|
||||
supportsGlobalEndpoint: rawModel.supports_global_endpoint ?? undefined,
|
||||
tiers: rawModel.tiers ?? undefined,
|
||||
}
|
||||
|
||||
@@ -31,6 +31,20 @@ function parseFieldMask(updateMask: string[]): {
|
||||
return { options, secrets }
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the alternate mode field name (e.g., planModeX <-> actModeX)
|
||||
* @param fieldName The field name to get alternate for
|
||||
* @returns The alternate mode field name or null if not a mode-specific field
|
||||
*/
|
||||
function getAlternateModeField(fieldName: string): string | null {
|
||||
if (fieldName.startsWith("planMode")) {
|
||||
return fieldName.replace("planMode", "actMode")
|
||||
} else if (fieldName.startsWith("actMode")) {
|
||||
return fieldName.replace("actMode", "planMode")
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates API configuration using field mask
|
||||
* @param controller The controller instance
|
||||
@@ -39,9 +53,9 @@ function parseFieldMask(updateMask: string[]): {
|
||||
*/
|
||||
export async function updateApiConfiguration(controller: Controller, request: UpdateApiConfigurationRequestNew): Promise<Empty> {
|
||||
try {
|
||||
const { apiConfiguration, updateMask } = request
|
||||
const { updates, updateMask } = request
|
||||
|
||||
if (!apiConfiguration) {
|
||||
if (!updates) {
|
||||
throw new Error("API configuration is required")
|
||||
}
|
||||
|
||||
@@ -49,7 +63,7 @@ export async function updateApiConfiguration(controller: Controller, request: Up
|
||||
throw new Error("Update mask is required and must contain at least one path")
|
||||
}
|
||||
|
||||
const { options: protoOptions, secrets: protoSecrets } = apiConfiguration
|
||||
const { options: protoOptions, secrets: protoSecrets } = updates
|
||||
|
||||
// Parse the field mask to determine which fields to update
|
||||
const { options: maskOptionsFields, secrets: maskSecretsFields } = parseFieldMask(updateMask)
|
||||
@@ -81,6 +95,10 @@ export async function updateApiConfiguration(controller: Controller, request: Up
|
||||
throw new Error(`Field "${fieldName}" specified in mask but not found in options`)
|
||||
}
|
||||
}
|
||||
|
||||
// Check if mode-specific configurations should be kept separate
|
||||
const separateModeConfigs = controller.stateManager.getGlobalSettingsKey("planActSeparateModelsSetting")
|
||||
|
||||
// Process entries that are in the mask
|
||||
for (const [key, value] of Object.entries(protoOptions)) {
|
||||
if (maskOptionsFields.has(key)) {
|
||||
@@ -92,6 +110,20 @@ export async function updateApiConfiguration(controller: Controller, request: Up
|
||||
} else {
|
||||
options[key as keyof ApiHandlerOptions] = value
|
||||
}
|
||||
|
||||
// If mode configs should be synced, also update the alternate mode field
|
||||
if (!separateModeConfigs) {
|
||||
const alternateField = getAlternateModeField(key)
|
||||
if (alternateField) {
|
||||
if (alternateField === "planModeApiProvider") {
|
||||
options.planModeApiProvider = convertProtoToApiProvider(value)
|
||||
} else if (alternateField === "actModeApiProvider") {
|
||||
options.actModeApiProvider = convertProtoToApiProvider(value)
|
||||
} else {
|
||||
options[alternateField as keyof ApiHandlerOptions] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Empty } from "@shared/proto/cline/common"
|
||||
import { OnboardingProgressRequest } from "@shared/proto/cline/state"
|
||||
import { telemetryService } from "../../../services/telemetry"
|
||||
import type { Controller } from "../index"
|
||||
|
||||
/**
|
||||
* Captures the onboarding progress step
|
||||
* @param controller The controller instance
|
||||
* @param request The request containing the step number
|
||||
* @returns Empty response
|
||||
*/
|
||||
export async function captureOnboardingProgress(_controller: Controller, request: OnboardingProgressRequest): Promise<Empty> {
|
||||
try {
|
||||
telemetryService.captureOnboardingProgress({
|
||||
step: Number(request.step),
|
||||
model: request.modelSelected,
|
||||
action: request.action,
|
||||
completed: !!request.completed,
|
||||
})
|
||||
return Empty.create({})
|
||||
} catch (error) {
|
||||
console.error("Failed to set welcome view completed:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
+20
-4
@@ -67,7 +67,7 @@ import { CLINE_MCP_TOOL_IDENTIFIER } from "@shared/mcp"
|
||||
import { convertClineMessageToProto } from "@shared/proto-conversions/cline-message"
|
||||
import { ClineDefaultTool } from "@shared/tools"
|
||||
import { ClineAskResponse } from "@shared/WebviewMessage"
|
||||
import { isLocalModel, isNextGenModelFamily } from "@utils/model-utils"
|
||||
import { isClaude4PlusModelFamily, isGPT5ModelFamily, isLocalModel, isNextGenModelFamily } from "@utils/model-utils"
|
||||
import { arePathsEqual, getDesktopDir } from "@utils/path"
|
||||
import { filterExistingFiles } from "@utils/tabFiltering"
|
||||
import cloneDeep from "clone-deep"
|
||||
@@ -3450,7 +3450,7 @@ export class Task {
|
||||
}
|
||||
}
|
||||
|
||||
// Add context window usage information
|
||||
// Add context window usage information (conditionally for some models)
|
||||
const { contextWindow } = getContextWindowInfo(this.api)
|
||||
|
||||
// Get the token count from the most recent API request to accurately reflect context management
|
||||
@@ -3478,8 +3478,24 @@ export class Task {
|
||||
const lastApiReqTotalTokens = lastApiReqMessage ? getTotalTokensFromApiReqMessage(lastApiReqMessage) : 0
|
||||
const usagePercentage = Math.round((lastApiReqTotalTokens / contextWindow) * 100)
|
||||
|
||||
details += "\n\n# Context Window Usage"
|
||||
details += `\n${lastApiReqTotalTokens.toLocaleString()} / ${(contextWindow / 1000).toLocaleString()}K tokens used (${usagePercentage}%)`
|
||||
// Determine if context window info should be displayed
|
||||
const currentModelId = this.api.getModel().id
|
||||
const isNextGenModel = isClaude4PlusModelFamily(currentModelId) || isGPT5ModelFamily(currentModelId)
|
||||
|
||||
let shouldShowContextWindow = true
|
||||
// For next-gen models, only show context window usage if it exceeds a certain threshold
|
||||
if (isNextGenModel) {
|
||||
const autoCondenseThreshold =
|
||||
(this.stateManager.getGlobalSettingsKey("autoCondenseThreshold") as number | undefined) ?? 0.75
|
||||
const displayThreshold = autoCondenseThreshold - 0.15
|
||||
const currentUsageRatio = lastApiReqTotalTokens / contextWindow
|
||||
shouldShowContextWindow = currentUsageRatio >= displayThreshold
|
||||
}
|
||||
|
||||
if (shouldShowContextWindow) {
|
||||
details += "\n\n# Context Window Usage"
|
||||
details += `\n${lastApiReqTotalTokens.toLocaleString()} / ${(contextWindow / 1000).toLocaleString()}K tokens used (${usagePercentage}%)`
|
||||
}
|
||||
|
||||
details += "\n\n# Current Mode"
|
||||
const mode = this.stateManager.getGlobalSettingsKey("mode")
|
||||
|
||||
@@ -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 { fetch } from "@/shared/net"
|
||||
@@ -122,6 +123,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)
|
||||
|
||||
+170
-10
@@ -24,6 +24,7 @@ import {
|
||||
} from "@shared/mcp"
|
||||
import { convertMcpServersToProtoMcpServers } from "@shared/proto-conversions/mcp/mcp-server-conversion"
|
||||
import { fileExistsAtPath } from "@utils/fs"
|
||||
import { getCwd } from "@utils/path"
|
||||
import { secondsToMs } from "@utils/time"
|
||||
import chokidar, { FSWatcher } from "chokidar"
|
||||
import deepEqual from "fast-deep-equal"
|
||||
@@ -45,6 +46,7 @@ export class McpHub {
|
||||
private telemetryService: TelemetryService
|
||||
|
||||
private settingsWatcher?: FSWatcher
|
||||
private projectSettingsWatcher?: FSWatcher
|
||||
private fileWatchers: Map<string, FSWatcher> = new Map()
|
||||
connections: McpConnection[] = []
|
||||
isConnecting: boolean = false
|
||||
@@ -76,6 +78,7 @@ export class McpHub {
|
||||
this.clientVersion = clientVersion
|
||||
this.telemetryService = telemetryService
|
||||
this.watchMcpSettingsFile()
|
||||
this.watchProjectMcpSettingsFile()
|
||||
this.initializeMcpServers()
|
||||
}
|
||||
|
||||
@@ -127,26 +130,82 @@ export class McpHub {
|
||||
return mcpSettingsFilePath
|
||||
}
|
||||
|
||||
/**
|
||||
* Get project MCP settings file path for current workspace
|
||||
* Returns undefined if no workspace is open
|
||||
*/
|
||||
private async getProjectMcpSettingsFilePath(): Promise<string | undefined> {
|
||||
const workspacePath = await getCwd()
|
||||
if (!workspacePath) {
|
||||
return undefined
|
||||
}
|
||||
return path.join(workspacePath, ".cline", "mcp_settings.json")
|
||||
}
|
||||
|
||||
/**
|
||||
* Read project MCP settings if they exist
|
||||
* Returns empty object if file doesn't exist or is invalid
|
||||
*/
|
||||
private async readProjectMcpSettings(): Promise<Record<string, McpServerConfig>> {
|
||||
try {
|
||||
const projectPath = await this.getProjectMcpSettingsFilePath()
|
||||
if (!projectPath) {
|
||||
return {}
|
||||
}
|
||||
|
||||
const exists = await fileExistsAtPath(projectPath)
|
||||
if (!exists) {
|
||||
return {}
|
||||
}
|
||||
|
||||
const content = await fs.readFile(projectPath, "utf-8")
|
||||
const config = JSON.parse(content)
|
||||
|
||||
// Validate
|
||||
const result = McpSettingsSchema.safeParse(config)
|
||||
if (!result.success) {
|
||||
console.warn("Invalid project MCP settings, using global only:", result.error)
|
||||
return {}
|
||||
}
|
||||
|
||||
return result.data.mcpServers || {}
|
||||
} catch (error) {
|
||||
console.warn("Failed to read project MCP settings:", error)
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
private async readAndValidateMcpSettingsFile(): Promise<z.infer<typeof McpSettingsSchema> | undefined> {
|
||||
try {
|
||||
const settingsPath = await this.getMcpSettingsFilePath()
|
||||
const content = await fs.readFile(settingsPath, "utf-8")
|
||||
// Read global settings
|
||||
const globalSettingsPath = await this.getMcpSettingsFilePath()
|
||||
const globalContent = await fs.readFile(globalSettingsPath, "utf-8")
|
||||
let globalConfig: any
|
||||
|
||||
let config: any
|
||||
|
||||
// Parse JSON file content
|
||||
// Parse global JSON file content
|
||||
try {
|
||||
config = JSON.parse(content)
|
||||
globalConfig = JSON.parse(globalContent)
|
||||
} catch (_error) {
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: "Invalid MCP settings format. Please ensure your settings follow the correct JSON format.",
|
||||
message: "Invalid global MCP settings format.",
|
||||
})
|
||||
return undefined
|
||||
}
|
||||
|
||||
// Validate against schema
|
||||
const result = McpSettingsSchema.safeParse(config)
|
||||
// Read project settings (returns {} if not found/invalid)
|
||||
const projectServers = await this.readProjectMcpSettings()
|
||||
|
||||
// Merge: project overrides global
|
||||
const mergedConfig = {
|
||||
mcpServers: {
|
||||
...(globalConfig.mcpServers || {}),
|
||||
...projectServers,
|
||||
},
|
||||
}
|
||||
|
||||
// Validate merged config
|
||||
const result = McpSettingsSchema.safeParse(mergedConfig)
|
||||
if (!result.success) {
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
@@ -192,6 +251,79 @@ export class McpHub {
|
||||
})
|
||||
}
|
||||
|
||||
private async watchProjectMcpSettingsFile(): Promise<void> {
|
||||
const projectPath = await this.getProjectMcpSettingsFilePath()
|
||||
if (!projectPath) {
|
||||
// No workspace open, skip project watcher
|
||||
return
|
||||
}
|
||||
|
||||
// Watch both the file and .cline directory (to detect file creation)
|
||||
const clineDir = path.dirname(projectPath)
|
||||
const watchPaths = [projectPath]
|
||||
|
||||
// Also watch directory if it exists
|
||||
if (await fileExistsAtPath(clineDir)) {
|
||||
watchPaths.push(clineDir)
|
||||
}
|
||||
|
||||
this.projectSettingsWatcher = chokidar.watch(watchPaths, {
|
||||
persistent: true,
|
||||
ignoreInitial: true,
|
||||
awaitWriteFinish: {
|
||||
stabilityThreshold: 100,
|
||||
pollInterval: 100,
|
||||
},
|
||||
atomic: true,
|
||||
})
|
||||
|
||||
this.projectSettingsWatcher.on("change", async (path) => {
|
||||
// Only react to changes to mcp_settings.json
|
||||
if (path.endsWith("mcp_settings.json")) {
|
||||
const settings = await this.readAndValidateMcpSettingsFile()
|
||||
if (settings) {
|
||||
try {
|
||||
await this.updateServerConnections(settings.mcpServers)
|
||||
} catch (error) {
|
||||
console.error("Failed to process project MCP settings change:", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
this.projectSettingsWatcher.on("add", async (path) => {
|
||||
// Detect when mcp_settings.json is created
|
||||
if (path.endsWith("mcp_settings.json")) {
|
||||
const settings = await this.readAndValidateMcpSettingsFile()
|
||||
if (settings) {
|
||||
try {
|
||||
await this.updateServerConnections(settings.mcpServers)
|
||||
} catch (error) {
|
||||
console.error("Failed to process new project MCP settings:", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
this.projectSettingsWatcher.on("unlink", async (path) => {
|
||||
// Detect when mcp_settings.json is deleted
|
||||
if (path.endsWith("mcp_settings.json")) {
|
||||
const settings = await this.readAndValidateMcpSettingsFile()
|
||||
if (settings) {
|
||||
try {
|
||||
await this.updateServerConnections(settings.mcpServers)
|
||||
} catch (error) {
|
||||
console.error("Failed to process project MCP settings deletion:", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
this.projectSettingsWatcher.on("error", (error) => {
|
||||
console.error("Error watching project MCP settings file:", error)
|
||||
})
|
||||
}
|
||||
|
||||
private async initializeMcpServers(): Promise<void> {
|
||||
const settings = await this.readAndValidateMcpSettingsFile()
|
||||
if (settings) {
|
||||
@@ -203,6 +335,28 @@ export class McpHub {
|
||||
return this.connections.find((conn) => conn.server.name === name)
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves variables in the cwd path
|
||||
* Currently supports: ${workspaceFolder}
|
||||
*/
|
||||
private async resolveCwd(cwd: string | undefined): Promise<string | undefined> {
|
||||
if (!cwd) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
// Replace ${workspaceFolder} with the actual workspace path
|
||||
if (cwd.includes("${workspaceFolder}")) {
|
||||
const workspacePath = await getCwd()
|
||||
if (!workspacePath) {
|
||||
console.warn("Cannot resolve ${workspaceFolder}: no workspace folder open")
|
||||
return cwd
|
||||
}
|
||||
return cwd.replace(/\$\{workspaceFolder\}/g, workspacePath)
|
||||
}
|
||||
|
||||
return cwd
|
||||
}
|
||||
|
||||
private async connectToServer(
|
||||
name: string,
|
||||
config: z.infer<typeof ServerConfigSchema>,
|
||||
@@ -244,10 +398,13 @@ export class McpHub {
|
||||
|
||||
switch (config.type) {
|
||||
case "stdio": {
|
||||
// Resolve cwd variables like ${workspaceFolder}
|
||||
const resolvedCwd = await this.resolveCwd(config.cwd)
|
||||
|
||||
transport = new StdioClientTransport({
|
||||
command: config.command,
|
||||
args: config.args,
|
||||
cwd: config.cwd,
|
||||
cwd: resolvedCwd,
|
||||
env: {
|
||||
// ...(config.env ? await injectEnv(config.env) : {}), // Commented out as injectEnv is not found
|
||||
...getDefaultEnvironment(),
|
||||
@@ -1188,5 +1345,8 @@ export class McpHub {
|
||||
if (this.settingsWatcher) {
|
||||
await this.settingsWatcher.close()
|
||||
}
|
||||
if (this.projectSettingsWatcher) {
|
||||
await this.projectSettingsWatcher.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,6 +93,7 @@ export class TelemetryService {
|
||||
AUTH_SUCCEEDED: "user.auth_succeeded",
|
||||
AUTH_FAILED: "user.auth_failed",
|
||||
AUTH_LOGGED_OUT: "user.auth_logged_out",
|
||||
ONBOARDING_PROGRESS: "user.onboarding_progress",
|
||||
},
|
||||
DICTATION: {
|
||||
// Tracks when voice recording is started
|
||||
@@ -1634,6 +1635,15 @@ export class TelemetryService {
|
||||
})
|
||||
}
|
||||
|
||||
public captureOnboardingProgress(args: { step: number; action?: string; model?: string; completed?: boolean }) {
|
||||
this.capture({
|
||||
event: TelemetryService.EVENTS.USER.ONBOARDING_PROGRESS,
|
||||
properties: {
|
||||
...args,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up resources when the service is disposed
|
||||
*/
|
||||
|
||||
+1
-10
@@ -223,6 +223,7 @@ interface PriceTier {
|
||||
}
|
||||
|
||||
export interface ModelInfo {
|
||||
name?: string
|
||||
maxTokens?: number
|
||||
contextWindow?: number
|
||||
supportsImages?: boolean
|
||||
@@ -3766,16 +3767,6 @@ export const fireworksModels = {
|
||||
description:
|
||||
"Kimi K2 model gets a new version update: Agentic coding: more accurate, better generalization across scaffolds. Frontend coding: improved aesthetics and functionalities on web, 3d, and other tasks. Context length: extended from 128k to 256k, providing better long-horizon support.",
|
||||
},
|
||||
"accounts/fireworks/models/kimi-k2-instruct": {
|
||||
maxTokens: 16384,
|
||||
contextWindow: 128000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.6,
|
||||
outputPrice: 2.5,
|
||||
description:
|
||||
"Kimi K2 is a state-of-the-art mixture-of-experts (MoE) language model with 32 billion activated parameters and 1 trillion total parameters. Trained with the Muon optimizer, Kimi K2 achieves exceptional performance across frontier knowledge, reasoning, and coding tasks while being meticulously optimized for agentic capabilities.",
|
||||
},
|
||||
"accounts/fireworks/models/qwen3-235b-a22b-instruct-2507": {
|
||||
maxTokens: 32768,
|
||||
contextWindow: 256000,
|
||||
|
||||
@@ -87,6 +87,17 @@ export const VertexSettingsSchema = z.object({
|
||||
vertexRegion: z.string().optional(),
|
||||
})
|
||||
|
||||
export const LiteLLMModelSchema = z.object({
|
||||
id: z.string(),
|
||||
thinkingBudgetTokens: z.number().optional(),
|
||||
promptCachingEnabled: z.boolean().optional(),
|
||||
})
|
||||
|
||||
export const LiteLLMSchema = z.object({
|
||||
models: z.array(LiteLLMModelSchema).optional(),
|
||||
baseUrl: z.string().optional(),
|
||||
})
|
||||
|
||||
// Provider settings schema
|
||||
// Each provider becomes an optional field
|
||||
const ProviderSettingsSchema = z.object({
|
||||
@@ -94,6 +105,7 @@ const ProviderSettingsSchema = z.object({
|
||||
AwsBedrock: AwsBedrockSettingsSchema.optional(),
|
||||
Cline: ClineSettingsSchema.optional(),
|
||||
Vertex: VertexSettingsSchema.optional(),
|
||||
LiteLLM: LiteLLMSchema.optional(),
|
||||
})
|
||||
|
||||
export const AllowedMCPServerSchema = z.object({
|
||||
@@ -101,6 +113,13 @@ export const AllowedMCPServerSchema = z.object({
|
||||
id: z.string(),
|
||||
})
|
||||
|
||||
export const RemoteMCPServerSchema = z.object({
|
||||
// The name of the MCP server
|
||||
name: z.string(),
|
||||
// The URL of the MCP server
|
||||
url: z.string(),
|
||||
})
|
||||
|
||||
// Settings for a global cline rules or workflow file.
|
||||
export const GlobalInstructionsFileSchema = z.object({
|
||||
// When this is enabled, the user cannot turn off this rule or workflow.
|
||||
@@ -125,6 +144,7 @@ export const RemoteConfigSchema = z.object({
|
||||
// MCP settings
|
||||
mcpMarketplaceEnabled: z.boolean().optional(),
|
||||
allowedMCPServers: z.array(AllowedMCPServerSchema).optional(),
|
||||
remoteMCPServers: z.array(RemoteMCPServerSchema).optional(),
|
||||
|
||||
// If the user is allowed to enable YOLO mode. Note this is different from the extension setting
|
||||
// yoloModeEnabled, because we do not want to force YOLO enabled for the user.
|
||||
@@ -153,12 +173,22 @@ export const RemoteConfigSchema = z.object({
|
||||
})
|
||||
|
||||
// Type inference from schemas
|
||||
export type RemoteConfig = z.infer<typeof RemoteConfigSchema>
|
||||
export type MCPServer = z.infer<typeof AllowedMCPServerSchema>
|
||||
export type OpenAiCompatibleModel = z.infer<typeof OpenAiCompatibleModelSchema>
|
||||
export type RemoteMCPServer = z.infer<typeof RemoteMCPServerSchema>
|
||||
export type GlobalInstructionsFile = z.infer<typeof GlobalInstructionsFileSchema>
|
||||
|
||||
export type ProviderSettings = z.infer<typeof ProviderSettingsSchema>
|
||||
|
||||
export type OpenAiCompatible = z.infer<typeof OpenAiCompatibleSchema>
|
||||
export type OpenAiCompatibleModel = z.infer<typeof OpenAiCompatibleModelSchema>
|
||||
|
||||
export type AwsBedrockSettings = z.infer<typeof AwsBedrockSettingsSchema>
|
||||
export type AwsBedrockModel = z.infer<typeof AwsBedrockModelSchema>
|
||||
export type AwsBedrockCustomModel = z.infer<typeof AwsBedrockCustomModelSchema>
|
||||
export type AwsBedrockSettings = z.infer<typeof AwsBedrockSettingsSchema>
|
||||
export type ProviderSettings = z.infer<typeof ProviderSettingsSchema>
|
||||
export type RemoteConfig = z.infer<typeof RemoteConfigSchema>
|
||||
export type GlobalInstructionsFile = z.infer<typeof GlobalInstructionsFileSchema>
|
||||
|
||||
export type VertexSettings = z.infer<typeof VertexSettingsSchema>
|
||||
export type VertexModel = z.infer<typeof VertexModelSchema>
|
||||
|
||||
export type LiteLLMSettings = z.infer<typeof LiteLLMSchema>
|
||||
export type LiteLLMModel = z.infer<typeof LiteLLMModelSchema>
|
||||
|
||||
@@ -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,375 @@
|
||||
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,
|
||||
getSpeedLabel,
|
||||
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">{getSpeedLabel(model.latency)}</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 onUserTypeClick = useCallback((userType: NEW_USER_TYPE) => {
|
||||
setUserType(userType)
|
||||
const action =
|
||||
userType === NEW_USER_TYPE.POWER
|
||||
? "power_user_selected"
|
||||
: userType === NEW_USER_TYPE.FREE
|
||||
? "free_user_selected"
|
||||
: "byok_user_selected"
|
||||
// User selection is available in step 0 only
|
||||
StateServiceClient.captureOnboardingProgress({ step: 0, action })
|
||||
}, [])
|
||||
|
||||
const onModelClick = useCallback((modelSelected: string) => {
|
||||
setSelectedModelId(modelSelected)
|
||||
// User selection is available in step 1 only
|
||||
StateServiceClient.captureOnboardingProgress({ step: 1, modelSelected, action: "model_selected" })
|
||||
}, [])
|
||||
|
||||
const finishOnboarding = useCallback(
|
||||
async (updateModelId: boolean, step: number) => {
|
||||
const modelSelected = (updateModelId && selectedModelId) || undefined
|
||||
if (modelSelected) {
|
||||
await handleFieldsChange({
|
||||
planModeOpenRouterModelId: selectedModelId,
|
||||
actModeOpenRouterModelId: selectedModelId,
|
||||
planModeOpenRouterModelInfo: openRouterModels[selectedModelId],
|
||||
actModeOpenRouterModelInfo: openRouterModels[selectedModelId],
|
||||
planModeApiProvider: "cline",
|
||||
actModeApiProvider: "cline",
|
||||
})
|
||||
}
|
||||
hideAccount()
|
||||
hideSettings()
|
||||
const action = "onboarding_completed"
|
||||
StateServiceClient.captureOnboardingProgress({ step, modelSelected, action, completed: true })
|
||||
},
|
||||
[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, stepNumber + 1)
|
||||
break
|
||||
case "signin":
|
||||
await AccountServiceClient.accountLoginClicked({}).catch(() => {})
|
||||
await finishOnboarding(true, stepNumber + 1)
|
||||
break
|
||||
case "next":
|
||||
StateServiceClient.captureOnboardingProgress({ step: stepNumber + 1 })
|
||||
setStepNumber(stepNumber + 1)
|
||||
break
|
||||
case "back":
|
||||
StateServiceClient.captureOnboardingProgress({ step: stepNumber - 1 })
|
||||
setStepNumber(stepNumber - 1)
|
||||
break
|
||||
case "done":
|
||||
await StateServiceClient.setWelcomeViewCompleted({ value: true }).catch(() => {})
|
||||
setShowWelcome(false)
|
||||
await finishOnboarding(false, stepNumber)
|
||||
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={onModelClick}
|
||||
onSelectUserType={onUserTypeClick}
|
||||
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,159 @@
|
||||
import type { ModelInfo } 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
|
||||
latency?: number
|
||||
}
|
||||
|
||||
type ModelGroup = {
|
||||
group: string
|
||||
models: OnboardingModelOption[]
|
||||
}
|
||||
|
||||
export const ONBOARDING_MODEL_SELECTIONS: Record<"free" | "power", ModelGroup[]> = {
|
||||
[NEW_USER_TYPE.FREE]: [
|
||||
{
|
||||
group: "free",
|
||||
models: [
|
||||
{
|
||||
id: "x-ai/grok-code-fast-1",
|
||||
name: "xAI: Grok Code Fast 1",
|
||||
score: 90,
|
||||
latency: 1,
|
||||
badge: "Best",
|
||||
contextWindow: 256_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
[NEW_USER_TYPE.POWER]: [
|
||||
{
|
||||
group: "frontier",
|
||||
models: [
|
||||
{
|
||||
id: "anthropic/claude-sonnet-4.5",
|
||||
name: "Anthropic: Claude Sonnet 4.5",
|
||||
badge: "Best",
|
||||
score: 97,
|
||||
latency: 3,
|
||||
contextWindow: 200_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 3.0,
|
||||
outputPrice: 15.0,
|
||||
},
|
||||
{
|
||||
id: "openai/gpt-5-codex",
|
||||
name: "OpenAI: GPT-5 Codex",
|
||||
badge: "Best",
|
||||
score: 97,
|
||||
latency: 7,
|
||||
contextWindow: 400_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 1.25,
|
||||
outputPrice: 10.0,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
group: "open source",
|
||||
models: [
|
||||
{
|
||||
id: "z-ai/glm-4.6:exacto",
|
||||
name: "Z.AI: GLM 4.6 (exacto)",
|
||||
badge: "Trending",
|
||||
score: 90,
|
||||
latency: 2,
|
||||
contextWindow: 202_752,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.6,
|
||||
outputPrice: 2.5,
|
||||
},
|
||||
{
|
||||
id: "moonshotai/kimi-dev-72b:free",
|
||||
name: "MoonshotAI: Kimi Dev 72B (free)",
|
||||
badge: "Free",
|
||||
score: 90,
|
||||
latency: 1,
|
||||
contextWindow: 131_072,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
export function getSpeedLabel(latency?: number): string {
|
||||
if (!latency) {
|
||||
return "Average"
|
||||
}
|
||||
if (latency < 1) {
|
||||
return "Instant"
|
||||
}
|
||||
if (latency < 2) {
|
||||
return "Fast"
|
||||
}
|
||||
if (latency > 5) {
|
||||
return "Slow"
|
||||
}
|
||||
|
||||
return "Average"
|
||||
}
|
||||
@@ -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 { memo, useEffect } from "react"
|
||||
import { memo, useEffect, useRef, useState } from "react"
|
||||
import { useRemark } from "react-remark"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { cn } from "@/lib/utils"
|
||||
@@ -6,51 +6,57 @@ import { cn } from "@/lib/utils"
|
||||
interface ModelDescriptionMarkdownProps {
|
||||
markdown?: string
|
||||
key: string
|
||||
isExpanded: boolean
|
||||
setIsExpanded: (isExpanded: boolean) => void
|
||||
isPopup?: boolean
|
||||
}
|
||||
|
||||
export const ModelDescriptionMarkdown = memo(
|
||||
({ markdown, key, isExpanded, setIsExpanded, isPopup }: ModelDescriptionMarkdownProps) => {
|
||||
// Update the markdown content when the prop changes
|
||||
const [reactContent, setMarkdown] = useRemark()
|
||||
export const ModelDescriptionMarkdown = memo(({ markdown, key, isPopup }: ModelDescriptionMarkdownProps) => {
|
||||
// Update the markdown content when the prop changes
|
||||
const [reactContent, setMarkdown] = useRemark()
|
||||
const contentRef = useRef<HTMLDivElement>(null)
|
||||
const [isTruncated, setIsTruncated] = useState(false)
|
||||
const [isExpanded, setIsExpanded] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (markdown) {
|
||||
setMarkdown(markdown)
|
||||
}
|
||||
}, [markdown, setMarkdown])
|
||||
useEffect(() => {
|
||||
if (markdown) {
|
||||
setIsExpanded(false)
|
||||
setMarkdown(markdown)
|
||||
}
|
||||
}, [markdown, setMarkdown])
|
||||
|
||||
return (
|
||||
<div className="inline-block mb-0 description line-clamp-3" key={key}>
|
||||
useEffect(() => {
|
||||
if (contentRef.current && !isExpanded) {
|
||||
const element = contentRef.current
|
||||
// Check if content is truncated by comparing scrollHeight with clientHeight
|
||||
setIsTruncated(element.scrollHeight > element.clientHeight)
|
||||
}
|
||||
}, [reactContent, isExpanded])
|
||||
|
||||
return (
|
||||
<div className="inline-block mb-2 description line-clamp-3" key={key}>
|
||||
<div className="relative wrap-anywhere overflow-y-hidden">
|
||||
<div
|
||||
className={cn("relative wrap-anywhere overflow-y-hidden", {
|
||||
"overflow-y-auto": isExpanded,
|
||||
})}>
|
||||
<div
|
||||
className={cn("overflow-hidden text-sm line-clamp-3", {
|
||||
"line-clamp-none": isExpanded,
|
||||
"h-20": !isExpanded,
|
||||
})}>
|
||||
{reactContent}
|
||||
</div>
|
||||
{!isExpanded && (
|
||||
<div className="absolute bottom-0 right-0 flex items-center">
|
||||
<div className="w-10 h-5 bg-linear-to-r from-transparent to-sidebar-background" />
|
||||
<Button
|
||||
className={cn("bg-sidebar-background p-0 m-0 text-sm", {
|
||||
"bg-code-block-background": isPopup,
|
||||
})}
|
||||
onClick={() => setIsExpanded(true)}
|
||||
variant="link">
|
||||
See more
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
className={cn("overflow-hidden text-sm line-clamp-3", {
|
||||
"line-clamp-none": isExpanded,
|
||||
"max-h-19": !isExpanded,
|
||||
})}
|
||||
ref={contentRef}>
|
||||
{reactContent}
|
||||
</div>
|
||||
{isTruncated && (
|
||||
<div className="absolute bottom-0 right-0 flex items-center">
|
||||
<div className="w-10 h-5 bg-linear-to-r from-transparent to-sidebar-background" />
|
||||
<Button
|
||||
className={cn("bg-sidebar-background p-0 m-0 text-sm", {
|
||||
"bg-code-block-background": isPopup,
|
||||
})}
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
variant="link">
|
||||
{isExpanded ? "See less" : "See more"}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
)
|
||||
</div>
|
||||
)
|
||||
})
|
||||
ModelDescriptionMarkdown.displayName = "ModelDescriptionMarkdown"
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
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"
|
||||
import {
|
||||
CheckCheck,
|
||||
FlaskConical,
|
||||
Info,
|
||||
LucideIcon,
|
||||
type LucideIcon,
|
||||
SlidersHorizontal,
|
||||
SquareMousePointer,
|
||||
SquareTerminal,
|
||||
Wrench,
|
||||
} from "lucide-react"
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
||||
import { useCallback, useEffect, useMemo, useState } from "react"
|
||||
import { useEvent } from "react-use"
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { StateServiceClient } from "@/services/grpc-client"
|
||||
import { getEnvironmentColor } from "@/utils/environmentColors"
|
||||
import { Tab, TabContent, TabHeader, TabList, TabTrigger } from "../common/Tab"
|
||||
@@ -30,15 +30,6 @@ import TerminalSettingsSection from "./sections/TerminalSettingsSection"
|
||||
|
||||
const IS_DEV = process.env.IS_DEV
|
||||
|
||||
// Styles for the tab system
|
||||
const settingsTabsContainer = "flex flex-1 overflow-hidden [&.narrow_.tab-label]:hidden"
|
||||
const settingsTabList =
|
||||
"w-48 data-[compact=true]:w-12 shrink-0 flex flex-col overflow-y-auto overflow-x-hidden border-r border-(--vscode-sideBar-background)"
|
||||
const settingsTabTrigger =
|
||||
"whitespace-nowrap overflow-hidden min-w-0 h-12 px-4 py-3 box-border flex items-center border-l-2 border-transparent text-(--vscode-foreground) opacity-70 bg-transparent hover:bg-(--vscode-list-hoverBackground) data-[compact=true]:w-12 data-[compact=true]:p-4 cursor-pointer"
|
||||
const settingsTabTriggerActive =
|
||||
"opacity-100 border-l-2 border-l-(--vscode-focusBorder) border-t-0 border-r-0 border-b-0 bg-(--vscode-list-activeSelectionBackground)"
|
||||
|
||||
// Tab definitions
|
||||
interface SettingsTab {
|
||||
id: string
|
||||
@@ -78,15 +69,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 +83,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 = {
|
||||
@@ -142,12 +133,7 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
|
||||
|
||||
const { version, environment } = useExtensionState()
|
||||
|
||||
// Initialize active tab with memoized calculation
|
||||
const initialTab = useMemo(() => targetSection || SETTINGS_TABS[0].id, [targetSection])
|
||||
|
||||
const [activeTab, setActiveTab] = useState<string>(initialTab)
|
||||
const [isCompactMode, setIsCompactMode] = useState(true)
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const [activeTab, setActiveTab] = useState<string>(targetSection || SETTINGS_TABS[0].id)
|
||||
|
||||
// Optimized message handler with early returns
|
||||
const handleMessage = useCallback((event: MessageEvent) => {
|
||||
@@ -207,78 +193,31 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
|
||||
}
|
||||
}, [targetSection])
|
||||
|
||||
// Simplified tab change handler without debugging
|
||||
const handleTabChange = useCallback((tabId: string) => {
|
||||
setActiveTab(tabId)
|
||||
}, [])
|
||||
|
||||
// Optimized resize observer with debouncing
|
||||
useEffect(() => {
|
||||
const container = containerRef.current
|
||||
if (!container) {
|
||||
return
|
||||
}
|
||||
|
||||
const checkCompactMode = debounce((width: number) => {
|
||||
setIsCompactMode(width < 500)
|
||||
}, 100)
|
||||
|
||||
const observer = new ResizeObserver((entries) => {
|
||||
const entry = entries[0]
|
||||
if (entry) {
|
||||
checkCompactMode(entry.contentRect.width)
|
||||
}
|
||||
})
|
||||
|
||||
observer.observe(container)
|
||||
return () => observer.disconnect()
|
||||
}, [])
|
||||
|
||||
// Memoized tab item renderer
|
||||
const renderTabItem = useCallback(
|
||||
(tab: (typeof SETTINGS_TABS)[0]) => {
|
||||
const isActive = activeTab === tab.id
|
||||
const tabClassName = `${isActive ? `${settingsTabTrigger} ${settingsTabTriggerActive}` : settingsTabTrigger} focus:ring-0`
|
||||
const iconContainerClassName = `flex items-center gap-2 ${isCompactMode ? "justify-center" : ""}`
|
||||
|
||||
const TabIcon = tab.icon
|
||||
const tabContent = (
|
||||
<div className={iconContainerClassName}>
|
||||
<TabIcon className="w-4 h-4" />
|
||||
<span className="tab-label">{tab.name}</span>
|
||||
</div>
|
||||
)
|
||||
|
||||
if (isCompactMode) {
|
||||
return (
|
||||
return (
|
||||
<TabTrigger className="flex justify-baseline" data-testid={`tab-${tab.id}`} key={tab.id} value={tab.id}>
|
||||
<Tooltip key={tab.id}>
|
||||
<TooltipTrigger>
|
||||
<div
|
||||
className={tabClassName}
|
||||
data-compact={isCompactMode}
|
||||
data-testid={`tab-${tab.id}`}
|
||||
data-value={tab.id}
|
||||
onClick={() => handleTabChange(tab.id)}>
|
||||
{tabContent}
|
||||
className={cn(
|
||||
"whitespace-nowrap overflow-hidden h-12 sm:py-3 box-border flex items-center border-l-2 border-transparent text-foreground opacity-70 bg-transparent hover:bg-list-hover p-4 cursor-pointer gap-2",
|
||||
{
|
||||
"opacity-100 border-l-2 border-l-foreground border-t-0 border-r-0 border-b-0 bg-selection":
|
||||
activeTab === tab.id,
|
||||
},
|
||||
)}>
|
||||
<tab.icon className="w-4 h-4" />
|
||||
<span className="hidden sm:block">{tab.name}</span>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">{tab.tooltipText}</TooltipContent>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<TabTrigger
|
||||
className={tabClassName}
|
||||
data-compact={isCompactMode}
|
||||
data-testid={`tab-${tab.id}`}
|
||||
key={tab.id}
|
||||
value={tab.id}>
|
||||
{tabContent}
|
||||
</TabTrigger>
|
||||
)
|
||||
},
|
||||
[activeTab, isCompactMode, handleTabChange],
|
||||
[activeTab],
|
||||
)
|
||||
|
||||
// Memoized active content component
|
||||
@@ -314,11 +253,10 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
|
||||
</div>
|
||||
</TabHeader>
|
||||
|
||||
<div className={`${settingsTabsContainer} ${isCompactMode ? "narrow" : ""}`} ref={containerRef}>
|
||||
<div className="flex flex-1 overflow-hidden">
|
||||
<TabList
|
||||
className={settingsTabList}
|
||||
data-compact={isCompactMode}
|
||||
onValueChange={handleTabChange}
|
||||
className="shrink-0 flex flex-col overflow-y-auto border-r border-sidebar-background"
|
||||
onValueChange={setActiveTab}
|
||||
value={activeTab}>
|
||||
{SETTINGS_TABS.filter((tab) => !tab.hidden).map(renderTabItem)}
|
||||
</TabList>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { geminiModels, ModelInfo } from "@shared/api"
|
||||
import { Fragment, useState } from "react"
|
||||
import { Fragment } from "react"
|
||||
import { ModelDescriptionMarkdown } from "../ModelDescriptionMarkdown"
|
||||
import {
|
||||
formatPrice,
|
||||
@@ -97,9 +97,6 @@ interface ModelInfoViewProps {
|
||||
* This component manages its own description expansion state
|
||||
*/
|
||||
export const ModelInfoView = ({ selectedModelId, modelInfo, isPopup }: ModelInfoViewProps) => {
|
||||
// Internal state management for description expansion
|
||||
const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false)
|
||||
|
||||
const isGemini = Object.keys(geminiModels).includes(selectedModelId)
|
||||
const hasThinkingConfig = hasThinkingBudget(modelInfo)
|
||||
const hasTiers = !!modelInfo.tiers && modelInfo.tiers.length > 0
|
||||
@@ -151,13 +148,7 @@ export const ModelInfoView = ({ selectedModelId, modelInfo, isPopup }: ModelInfo
|
||||
|
||||
const infoItems = [
|
||||
modelInfo.description && (
|
||||
<ModelDescriptionMarkdown
|
||||
isExpanded={isDescriptionExpanded}
|
||||
isPopup={isPopup}
|
||||
key="description"
|
||||
markdown={modelInfo.description}
|
||||
setIsExpanded={setIsDescriptionExpanded}
|
||||
/>
|
||||
<ModelDescriptionMarkdown isPopup={isPopup} key="description" markdown={modelInfo.description} />
|
||||
),
|
||||
<ModelInfoSupportsItem
|
||||
doesNotSupportLabel="Does not support images"
|
||||
|
||||
@@ -42,7 +42,7 @@ export const LiteLlmProvider = ({ showModelOptions, isPopup, currentMode }: Lite
|
||||
onChange={async (value) => {
|
||||
await ModelsServiceClient.updateApiConfiguration(
|
||||
UpdateApiConfigurationRequestNew.create({
|
||||
apiConfiguration: {
|
||||
updates: {
|
||||
options: {
|
||||
liteLlmBaseUrl: value,
|
||||
},
|
||||
@@ -61,7 +61,7 @@ export const LiteLlmProvider = ({ showModelOptions, isPopup, currentMode }: Lite
|
||||
onChange={async (value) => {
|
||||
await ModelsServiceClient.updateApiConfiguration(
|
||||
UpdateApiConfigurationRequestNew.create({
|
||||
apiConfiguration: {
|
||||
updates: {
|
||||
secrets: {
|
||||
liteLlmApiKey: value,
|
||||
},
|
||||
@@ -79,14 +79,17 @@ export const LiteLlmProvider = ({ showModelOptions, isPopup, currentMode }: Lite
|
||||
initialValue={liteLlmModelId || ""}
|
||||
onChange={async (value) => {
|
||||
await ModelsServiceClient.updateApiConfiguration(
|
||||
UpdateApiConfigurationRequestNew.create({
|
||||
apiConfiguration: {
|
||||
options:
|
||||
currentMode === "plan" ? { planModeLiteLlmModelId: value } : { actModeLiteLlmModelId: value },
|
||||
},
|
||||
updateMask:
|
||||
currentMode === "plan" ? ["options.planModeLiteLlmModelId"] : ["options.actModeLiteLlmModelId"],
|
||||
}),
|
||||
UpdateApiConfigurationRequestNew.create(
|
||||
currentMode === "plan"
|
||||
? {
|
||||
updates: { options: { planModeLiteLlmModelId: value } },
|
||||
updateMask: ["options.planModeLiteLlmModelId"],
|
||||
}
|
||||
: {
|
||||
updates: { options: { actModeLiteLlmModelId: value } },
|
||||
updateMask: ["options.actModeLiteLlmModelId"],
|
||||
},
|
||||
),
|
||||
)
|
||||
}}
|
||||
placeholder={"e.g. anthropic/claude-sonnet-4-20250514"}
|
||||
@@ -104,7 +107,7 @@ export const LiteLlmProvider = ({ showModelOptions, isPopup, currentMode }: Lite
|
||||
|
||||
await ModelsServiceClient.updateApiConfiguration(
|
||||
UpdateApiConfigurationRequestNew.create({
|
||||
apiConfiguration: {
|
||||
updates: {
|
||||
options: {
|
||||
liteLlmUsePromptCache: isChecked,
|
||||
},
|
||||
@@ -170,18 +173,17 @@ export const LiteLlmProvider = ({ showModelOptions, isPopup, currentMode }: Lite
|
||||
modelInfo.supportsImages = isChecked
|
||||
|
||||
await ModelsServiceClient.updateApiConfiguration(
|
||||
UpdateApiConfigurationRequestNew.create({
|
||||
apiConfiguration: {
|
||||
options:
|
||||
currentMode === "plan"
|
||||
? { planModeLiteLlmModelInfo: modelInfo }
|
||||
: { actModeLiteLlmModelInfo: modelInfo },
|
||||
},
|
||||
updateMask:
|
||||
currentMode === "plan"
|
||||
? ["options.planModeLiteLlmModelInfo"]
|
||||
: ["options.actModeLiteLlmModelInfo"],
|
||||
}),
|
||||
UpdateApiConfigurationRequestNew.create(
|
||||
currentMode === "plan"
|
||||
? {
|
||||
updates: { options: { planModeLiteLlmModelInfo: modelInfo } },
|
||||
updateMask: ["options.planModeLiteLlmModelInfo"],
|
||||
}
|
||||
: {
|
||||
updates: { options: { actModeLiteLlmModelInfo: modelInfo } },
|
||||
updateMask: ["options.actModeLiteLlmModelInfo"],
|
||||
},
|
||||
),
|
||||
)
|
||||
}}>
|
||||
Supports Images
|
||||
@@ -198,18 +200,17 @@ export const LiteLlmProvider = ({ showModelOptions, isPopup, currentMode }: Lite
|
||||
modelInfo.contextWindow = Number(value)
|
||||
|
||||
await ModelsServiceClient.updateApiConfiguration(
|
||||
UpdateApiConfigurationRequestNew.create({
|
||||
apiConfiguration: {
|
||||
options:
|
||||
currentMode === "plan"
|
||||
? { planModeLiteLlmModelInfo: modelInfo }
|
||||
: { actModeLiteLlmModelInfo: modelInfo },
|
||||
},
|
||||
updateMask:
|
||||
currentMode === "plan"
|
||||
? ["options.planModeLiteLlmModelInfo"]
|
||||
: ["options.actModeLiteLlmModelInfo"],
|
||||
}),
|
||||
UpdateApiConfigurationRequestNew.create(
|
||||
currentMode === "plan"
|
||||
? {
|
||||
updates: { options: { planModeLiteLlmModelInfo: modelInfo } },
|
||||
updateMask: ["options.planModeLiteLlmModelInfo"],
|
||||
}
|
||||
: {
|
||||
updates: { options: { actModeLiteLlmModelInfo: modelInfo } },
|
||||
updateMask: ["options.actModeLiteLlmModelInfo"],
|
||||
},
|
||||
),
|
||||
)
|
||||
}}
|
||||
style={{ flex: 1 }}>
|
||||
@@ -226,18 +227,17 @@ export const LiteLlmProvider = ({ showModelOptions, isPopup, currentMode }: Lite
|
||||
modelInfo.maxTokens = Number(value)
|
||||
|
||||
await ModelsServiceClient.updateApiConfiguration(
|
||||
UpdateApiConfigurationRequestNew.create({
|
||||
apiConfiguration: {
|
||||
options:
|
||||
currentMode === "plan"
|
||||
? { planModeLiteLlmModelInfo: modelInfo }
|
||||
: { actModeLiteLlmModelInfo: modelInfo },
|
||||
},
|
||||
updateMask:
|
||||
currentMode === "plan"
|
||||
? ["options.planModeLiteLlmModelInfo"]
|
||||
: ["options.actModeLiteLlmModelInfo"],
|
||||
}),
|
||||
UpdateApiConfigurationRequestNew.create(
|
||||
currentMode === "plan"
|
||||
? {
|
||||
updates: { options: { planModeLiteLlmModelInfo: modelInfo } },
|
||||
updateMask: ["options.planModeLiteLlmModelInfo"],
|
||||
}
|
||||
: {
|
||||
updates: { options: { actModeLiteLlmModelInfo: modelInfo } },
|
||||
updateMask: ["options.actModeLiteLlmModelInfo"],
|
||||
},
|
||||
),
|
||||
)
|
||||
}}
|
||||
style={{ flex: 1 }}>
|
||||
@@ -261,18 +261,17 @@ export const LiteLlmProvider = ({ showModelOptions, isPopup, currentMode }: Lite
|
||||
value === "" ? liteLlmModelInfoSaneDefaults.temperature : parseFloat(value)
|
||||
|
||||
await ModelsServiceClient.updateApiConfiguration(
|
||||
UpdateApiConfigurationRequestNew.create({
|
||||
apiConfiguration: {
|
||||
options:
|
||||
currentMode === "plan"
|
||||
? { planModeLiteLlmModelInfo: modelInfo }
|
||||
: { actModeLiteLlmModelInfo: modelInfo },
|
||||
},
|
||||
updateMask:
|
||||
currentMode === "plan"
|
||||
? ["options.planModeLiteLlmModelInfo"]
|
||||
: ["options.actModeLiteLlmModelInfo"],
|
||||
}),
|
||||
UpdateApiConfigurationRequestNew.create(
|
||||
currentMode === "plan"
|
||||
? {
|
||||
updates: { options: { planModeLiteLlmModelInfo: modelInfo } },
|
||||
updateMask: ["options.planModeLiteLlmModelInfo"],
|
||||
}
|
||||
: {
|
||||
updates: { options: { actModeLiteLlmModelInfo: modelInfo } },
|
||||
updateMask: ["options.actModeLiteLlmModelInfo"],
|
||||
},
|
||||
),
|
||||
)
|
||||
}}>
|
||||
<span style={{ fontWeight: 500 }}>Temperature</span>
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { moonshotModels } from "@shared/api"
|
||||
import { UpdateApiConfigurationRequestNew } from "@shared/proto/index.cline"
|
||||
import { Mode } from "@shared/storage/types"
|
||||
import { VSCodeDropdown, VSCodeOption } from "@vscode/webview-ui-toolkit/react"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { ModelsServiceClient } from "@/services/grpc-client"
|
||||
import { ApiKeyField } from "../common/ApiKeyField"
|
||||
import { ModelInfoView } from "../common/ModelInfoView"
|
||||
import { DropdownContainer, ModelSelector } from "../common/ModelSelector"
|
||||
import { normalizeApiConfiguration } from "../utils/providerUtils"
|
||||
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
|
||||
|
||||
/**
|
||||
* Props for the MoonshotProvider component
|
||||
@@ -22,7 +23,6 @@ interface MoonshotProviderProps {
|
||||
*/
|
||||
export const MoonshotProvider = ({ showModelOptions, isPopup, currentMode }: MoonshotProviderProps) => {
|
||||
const { apiConfiguration } = useExtensionState()
|
||||
const { handleFieldChange, handleModeFieldChange } = useApiConfigurationHandlers()
|
||||
|
||||
// Get the normalized configuration
|
||||
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration, currentMode)
|
||||
@@ -35,7 +35,19 @@ export const MoonshotProvider = ({ showModelOptions, isPopup, currentMode }: Moo
|
||||
</label>
|
||||
<VSCodeDropdown
|
||||
id="moonshot-entrypoint"
|
||||
onChange={(e) => handleFieldChange("moonshotApiLine", (e.target as any).value)}
|
||||
onChange={async (e) => {
|
||||
const value = (e.target as any).value
|
||||
await ModelsServiceClient.updateApiConfiguration(
|
||||
UpdateApiConfigurationRequestNew.create({
|
||||
updates: {
|
||||
options: {
|
||||
moonshotApiLine: value,
|
||||
},
|
||||
},
|
||||
updateMask: ["options.moonshotApiLine"],
|
||||
}),
|
||||
)
|
||||
}}
|
||||
style={{
|
||||
minWidth: 130,
|
||||
position: "relative",
|
||||
@@ -48,7 +60,18 @@ export const MoonshotProvider = ({ showModelOptions, isPopup, currentMode }: Moo
|
||||
<ApiKeyField
|
||||
helpText="This key is stored locally and only used to make API requests from this extension."
|
||||
initialValue={apiConfiguration?.moonshotApiKey || ""}
|
||||
onChange={(value) => handleFieldChange("moonshotApiKey", value)}
|
||||
onChange={async (value) => {
|
||||
await ModelsServiceClient.updateApiConfiguration(
|
||||
UpdateApiConfigurationRequestNew.create({
|
||||
updates: {
|
||||
secrets: {
|
||||
moonshotApiKey: value,
|
||||
},
|
||||
},
|
||||
updateMask: ["secrets.moonshotApiKey"],
|
||||
}),
|
||||
)
|
||||
}}
|
||||
providerName="Moonshot"
|
||||
signupUrl={
|
||||
apiConfiguration?.moonshotApiLine === "china"
|
||||
@@ -62,13 +85,23 @@ export const MoonshotProvider = ({ showModelOptions, isPopup, currentMode }: Moo
|
||||
<ModelSelector
|
||||
label="Model"
|
||||
models={moonshotModels}
|
||||
onChange={(e: any) =>
|
||||
handleModeFieldChange(
|
||||
{ plan: "planModeApiModelId", act: "actModeApiModelId" },
|
||||
e.target.value,
|
||||
currentMode,
|
||||
onChange={async (e: any) => {
|
||||
const value = e.target.value
|
||||
|
||||
await ModelsServiceClient.updateApiConfiguration(
|
||||
UpdateApiConfigurationRequestNew.create(
|
||||
currentMode === "plan"
|
||||
? {
|
||||
updates: { options: { planModeApiModelId: value } },
|
||||
updateMask: ["options.planModeApiModelId"],
|
||||
}
|
||||
: {
|
||||
updates: { options: { actModeApiModelId: value } },
|
||||
updateMask: ["options.actModeApiModelId"],
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
}}
|
||||
selectedModelId={selectedModelId}
|
||||
/>
|
||||
|
||||
|
||||
@@ -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-xs 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 }
|
||||
@@ -77,6 +77,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
|
||||
@@ -313,8 +314,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")
|
||||
@@ -685,6 +690,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);
|
||||
@@ -58,6 +58,7 @@
|
||||
--text-xs: calc(0.85 * var(--vscode-font-size));
|
||||
--text-xxs: calc(0.5 * var(--vscode-font-size));
|
||||
--breakpoint-xs: 400px;
|
||||
--radius-xs: 4px;
|
||||
--breakpoint-xxs: 180px;
|
||||
}
|
||||
|
||||
@@ -74,7 +75,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