Compare commits

...
Author SHA1 Message Date
celestial-vault 36dfc7de11 refactor out apiconfig section 2025-06-27 10:04:57 -07:00
ToshiiandStvLz 656e3276c6 lite llm provider ui (#4517)
* base 2

Co-authored-by: StvLz <lizarazo.steven@gmail.com>

* base 3

* base

---------

Co-authored-by: StvLz <lizarazo.steven@gmail.com>
2025-06-26 22:51:12 -07:00
ToshiiandStvLz 0e29f05e28 refactor nebius and remove unused logic (#4515)
* base 2

Co-authored-by: StvLz <lizarazo.steven@gmail.com>

* base 3

---------

Co-authored-by: StvLz <lizarazo.steven@gmail.com>
2025-06-26 22:39:56 -07:00
celestial-vault 985cb51c39 refactor lmstudio (#4514)
* refactor lmstudio

* merge conflict deleted stuff
2025-06-26 22:34:14 -07:00
celestial-vault abccde0e2a refactor out vscode lm (#4513) 2025-06-26 21:55:37 -07:00
celestial-vault ca984609ca refactor out cline (#4512) 2025-06-26 21:41:35 -07:00
celestial-vault 6690d392cd refactor out bedrock (#4510) 2025-06-26 21:29:27 -07:00
Toshii 40244f09fe base (#4511) 2025-06-26 21:23:43 -07:00
Toshii 9563a71c8a base (#4509) 2025-06-26 21:14:00 -07:00
celestial-vault c13e749eed refactor out sap ai core settings (#4508) 2025-06-26 21:05:29 -07:00
Toshii 8c3fd8ba55 refactor cerebras provider menu ui (#4507)
* xai

* base
2025-06-26 20:53:58 -07:00
celestial-vault 8bfa7daa28 refactor ollama settings to different component (#4506) 2025-06-26 20:45:30 -07:00
Toshii 7cd4be7a68 xai (#4504) 2025-06-26 20:34:34 -07:00
Toshii ab9f1a0785 refactor request and fireworks provider ui (#4499)
* requesty

* fireworks
2025-06-26 20:11:15 -07:00
canvrno 68b84f3df4 Fix for delete all bug, refactored deleteAll logic (#4497) 2025-06-26 17:53:44 -07:00
kevinneung 16da0f1e06 Fix telemetry source code link path (#4476)
Update the source code link in telemetry documentation to point to the correct file location: src/services/posthog/telemetry/TelemetryService.ts
2025-06-26 16:54:28 -07:00
Daniel Steigman e898bd8825 Updated best model to be gemini (#4500) 2025-06-26 16:47:34 -07:00
canvrno b7b0e96cc7 Add env host bridge service, migrate clipboard read/write calls (#4496)
* host bridge migration - clipboard

* changeset

* removed dev logging

* switched to empty return on clipboard write

* Moved new hostServiceNameMap entry to new proto configs
2025-06-26 16:18:29 -07:00
Andrei EternalandAndrei Eternal f00c5f4ecc experimental vscode impls & build-proto cleanup (#4493)
* experimental vscode impls

* weird errors

* add workspace host config

* format

---------

Co-authored-by: Andrei Eternal <eternal@cline.bot>
2025-06-26 16:07:21 -07:00
Tomás Barreiro 2709ccefcd fix: ENAMETOOLONG on Claude Code - write to stdin instead of including the messages in the prompt (#4494)
* Write to stdin instead of including the messages in the prompt

* Add changeset
2025-06-27 04:13:58 +05:30
Toshii c8f0324536 remove gemini free text for 2.5 models (#4492)
* gemini text

* remove
2025-06-26 15:43:24 -07:00
ToshiiandStvLz 937cebc7de refactor vertex and doubao provider ui (#4474)
* doubao

* vertex

Co-authored-by: StvLz <lizarazo.steven@gmail.com>

* ui

---------

Co-authored-by: StvLz <lizarazo.steven@gmail.com>
2025-06-26 15:01:31 -07:00
canvrno d4d5a49e67 Refactor autoApprove out of task class (#4233)
* Refactor auto approve out of task class

* Rebase fixes

* cleanup

* reorg
2025-06-26 10:40:37 -07:00
SmileFisherandToshii 01d3afe0c5 Consider adding a pronunciation guide, since many Chinese developers tend to pronounce it as "C line". (#3635)
* Update README.md

add pronunciation

* Update README.md

---------

Co-authored-by: Toshii <94262432+0xToshii@users.noreply.github.com>
2025-06-25 18:57:45 -07:00
github-actions[bot] 7bdab5613b v3.18.0 Release Notes
-   Optimized Cline to work with the Claude 4 family of models, resulting in improved performance, reliability, and new capabilities
-   Added a new Gemini CLI provider that allows you to use your local Gemini CLI authentication to access Gemini models for free (Thanks @google-gemini!)
-   Optimized Cline to work with the Gemini 2.5 family of models
-   Updated the default and recommended model to Claude 4 Sonnet for the best performance
-   Fix race condition in Plan/Act mode switching
-   Improve robustness of search and replace parsing
2025-06-25 17:39:46 -07:00
AraandCline Evaluation d79722bec0 Fix: Solving race condition in the Plan/Act mode switching functionality (#4421)
* type fix

* markdown fix

* markdown fix

* Fix settings on switch

* Fix settings on switch

---------

Co-authored-by: Cline Evaluation <cline@example.com>
2025-06-25 16:58:48 -07:00
Saoud Rizwan 06af720d85 Show placeholder while first response loads (#4475) 2025-06-25 16:55:55 -07:00
AraandCline Evaluation b80ef25a33 fix: Solving Context window counting issues in gemini model family in openrouter and cline provider (#4314)
Co-authored-by: Cline Evaluation <cline@example.com>
2025-06-25 16:54:30 -07:00
71 changed files with 3937 additions and 2057 deletions
-12
View File
@@ -1,12 +0,0 @@
---
"claude-dev": patch
---
Adding Gemini CLI Provider
* This implementation provides access to Google's Gemini models through OAuth authentication,
* leveraging the same authentication mechanism as the official Gemini CLI tool.
*
* Attribution: This implementation is inspired by and uses concepts from the Google Gemini CLI,
* which is licensed under the Apache License 2.0.
* Original project: https://github.com/google-gemini/gemini-cli
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Fix ENAMETOOLONG when calling Claude Code
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
Optimizing cline to work for gemini 2.5 family of models
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Modified diff regex to account for trailing > in search & replace blocks
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
telemtrySetting protobus migration
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
Optimized Cline for Claude 4
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
clearAllTaskHistory protobus migration
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Removed deleteNonFavoritedTasks, moved popup to extension, cleaned up deletion logic
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
Updating default and recommended model to claude 4 sonnet
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
host bridge migration - clipboard
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Refactor task class, moving auto approve
+9
View File
@@ -1,5 +1,14 @@
# Changelog
## [3.18.0]
- Optimized Cline to work with the Claude 4 family of models, resulting in improved performance, reliability, and new capabilities
- Added a new Gemini CLI provider that allows you to use your local Gemini CLI authentication to access Gemini models for free (Thanks @google-gemini!)
- Optimized Cline to work with the Gemini 2.5 family of models
- Updated the default and recommended model to Claude 4 Sonnet for the best performance
- Fix race condition in Plan/Act mode switching
- Improve robustness of search and replace parsing
## [3.17.16]
- Fix Claude Code provider error handling for incomplete messages during long-running tasks (Thanks @BarreiroT!)
+1 -1
View File
@@ -30,7 +30,7 @@ English | <a href="https://github.com/cline/cline/blob/main/locales/es/README.md
</table>
</div>
Meet Cline, an AI assistant that can use your **CLI** a**N**d **E**ditor.
Meet Cline (pronounced /klaɪn/, like "Klein"), an AI assistant that can use your **CLI** a**N**d **E**ditor.
Thanks to [Claude 3.7 Sonnet's agentic coding capabilities](https://www.anthropic.com/claude/sonnet), Cline can handle complex software development tasks step-by-step. With tools that let him create & edit files, explore large projects, use the browser, and execute terminal commands (after you grant permission), he can assist you in ways that go beyond code completion or tech support. Cline can even use the Model Context Protocol (MCP) to create new tools and extend his own capabilities. While autonomous AI scripts traditionally run in sandboxed environments, this extension provides a human-in-the-loop GUI to approve every file change and terminal command, providing a safe and accessible way to explore the potential of agentic AI.
+2 -2
View File
@@ -6,7 +6,7 @@ title: "Telemetry"
To help make Cline better for everyone, we collect anonymous usage data that helps us understand how developers are using our open-source AI coding agent. This feedback loop is crucial for improving Cline's capabilities and user experience.
We use PostHog, an open-source analytics platform, for data collection and analysis. Our telemetry implementation is fully transparent - you can review the [source code](https://github.com/cline/cline/blob/main/src/services/telemetry/TelemetryService.ts) to see exactly what we track.
We use PostHog, an open-source analytics platform, for data collection and analysis. Our telemetry implementation is fully transparent - you can review the [source code](https://github.com/cline/cline/blob/main/src/services/posthog/telemetry/TelemetryService.ts) to see exactly what we track.
### Tracking Policy
@@ -22,7 +22,7 @@ We collect basic anonymous usage data including:
**System Context:** OS type and VS Code environment details\
**UI Activity:** Navigation patterns and feature usage
For complete transparency, you can inspect our [telemetry implementation](https://github.com/cline/cline/blob/main/src/services/telemetry/TelemetryService.ts) to see the exact events we track.
For complete transparency, you can inspect our [telemetry implementation](https://github.com/cline/cline/blob/main/src/services/posthog/telemetry/TelemetryService.ts) to see the exact events we track.
### How to Opt Out
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "claude-dev",
"version": "3.17.16",
"version": "3.18.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "claude-dev",
"version": "3.17.16",
"version": "3.18.0",
"license": "Apache-2.0",
"dependencies": {
"@anthropic-ai/sdk": "^0.37.0",
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "claude-dev",
"displayName": "Cline",
"description": "Autonomous coding agent right in your IDE, capable of creating/editing files, running commands, using the browser, and more with your permission every step of the way.",
"version": "3.17.16",
"version": "3.18.0",
"icon": "assets/icons/icon.png",
"engines": {
"vscode": "^1.84.0"
+30
View File
@@ -0,0 +1,30 @@
// Configuration file for protocol buffer build scripts
// Contains service name mappings used by both build-proto.js and build-go-proto.js
// List of gRPC services
// To add a new service, simply add it to this map and run the build scripts
// The service handler will be automatically discovered and used by grpc-handler.ts
export const serviceNameMap = {
account: "cline.AccountService",
browser: "cline.BrowserService",
checkpoints: "cline.CheckpointsService",
file: "cline.FileService",
mcp: "cline.McpService",
state: "cline.StateService",
task: "cline.TaskService",
web: "cline.WebService",
models: "cline.ModelsService",
slash: "cline.SlashService",
ui: "cline.UiService",
// Add new services here - no other code changes needed!
}
// List of host gRPC services (IDE API bridge)
// These services are implemented in the IDE extension and called by the standalone Cline Core
export const hostServiceNameMap = {
uri: "host.UriService",
watch: "host.WatchService",
workspace: "host.WorkspaceService",
env: "host.EnvService",
// Add new host services here
}
+18 -45
View File
@@ -9,16 +9,18 @@ import chalk from "chalk"
import os from "os"
import { createRequire } from "module"
import { serviceNameMap, hostServiceNameMap } from "./build-proto-config.js"
const require = createRequire(import.meta.url)
const PROTOC = path.join(require.resolve("grpc-tools"), "../bin/protoc")
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url))
const ROOT_DIR = path.resolve(SCRIPT_DIR, "..")
const TS_OUT_DIR = path.join(ROOT_DIR, "src/shared/proto")
const GRPC_JS_OUT_DIR = path.join(ROOT_DIR, "src/generated/grpc-js")
const NICE_JS_OUT_DIR = path.join(ROOT_DIR, "src/generated/nice-grpc")
const DESCRIPTOR_OUT_DIR = path.join(ROOT_DIR, "dist-standalone/proto")
const TS_OUT_DIR = path.join(ROOT_DIR, "src", "shared", "proto")
const GRPC_JS_OUT_DIR = path.join(ROOT_DIR, "src", "generated", "grpc-js")
const NICE_JS_OUT_DIR = path.join(ROOT_DIR, "src", "generated", "nice-grpc")
const DESCRIPTOR_OUT_DIR = path.join(ROOT_DIR, "dist-standalone", "proto")
const isWindows = process.platform === "win32"
const TS_PROTO_PLUGIN = isWindows
@@ -34,34 +36,13 @@ const TS_PROTO_OPTIONS = [
"useDate=false", // Timestamp fields will not be automatically converted to Date.
]
// List of gRPC services
// To add a new service, simply add it to this map and run this script
// The service handler will be automatically discovered and used by grpc-handler.ts
const serviceNameMap = {
account: "cline.AccountService",
browser: "cline.BrowserService",
checkpoints: "cline.CheckpointsService",
file: "cline.FileService",
mcp: "cline.McpService",
state: "cline.StateService",
task: "cline.TaskService",
web: "cline.WebService",
models: "cline.ModelsService",
slash: "cline.SlashService",
ui: "cline.UiService",
// Add new services here - no other code changes needed!
}
const serviceDirs = Object.keys(serviceNameMap).map((serviceKey) => path.join(ROOT_DIR, "src/core/controller", serviceKey))
// Service directories derived from imported serviceNameMap
const serviceDirs = Object.keys(serviceNameMap).map((serviceKey) => path.join(ROOT_DIR, "src", "core", "controller", serviceKey))
// List of host gRPC services (IDE API bridge)
// These services are implemented in the IDE extension and called by the standalone Cline Core
const hostServiceNameMap = {
uri: "host.UriService",
watch: "host.WatchService",
workspace: "host.WorkspaceService",
// Add new host services here
}
const hostServiceDirs = Object.keys(hostServiceNameMap).map((serviceKey) => path.join(ROOT_DIR, "src/hosts/vscode", serviceKey))
// Host service directories derived from imported hostServiceNameMap
const hostServiceDirs = Object.keys(hostServiceNameMap).map((serviceKey) =>
path.join(ROOT_DIR, "src", "hosts", "vscode", serviceKey),
)
async function main() {
console.log(chalk.bold.blue("Starting Protocol Buffer code generation..."))
@@ -177,7 +158,7 @@ export {
${serviceExports.join(",\n\t")}
}`
const filePath = path.join(ROOT_DIR, "webview-ui/src/services/grpc-client.ts")
const filePath = path.join(ROOT_DIR, "webview-ui", "src", "services", "grpc-client.ts")
await writeFileWithMkdirs(filePath, content)
log_verbose(chalk.green(`Generated gRPC client at ${filePath}`))
}
@@ -386,7 +367,7 @@ export interface ServiceHandlerConfig {
export const serviceHandlers: Record<string, ServiceHandlerConfig> = {${serviceConfigs.join(",")}
};`
const configPath = path.join(ROOT_DIR, "src/core/controller/grpc-service-config.ts")
const configPath = path.join(ROOT_DIR, "src", "core", "controller", "grpc-service-config.ts")
await writeFileWithMkdirs(configPath, content)
log_verbose(chalk.green(`Generated service configuration at ${configPath}`))
}
@@ -602,13 +583,12 @@ async function cleanup() {
for (const file of existingFiles) {
await fs.unlink(path.join(TS_OUT_DIR, file))
}
await rmdir(path.join(ROOT_DIR, "src/generated"))
await rmdir(path.join(ROOT_DIR, "src", "generated"))
// Clean up generated files that were moved.
await fs.rm(path.join(ROOT_DIR, "src/standalone/services/host-grpc-client.ts"), { force: true })
await rmdir(path.join(ROOT_DIR, "src/standalone/services"))
await fs.rm(path.join(ROOT_DIR, "hosts/vscode"), { force: true, recursive: true })
await fs.rm(path.join(ROOT_DIR, "src", "standalone", "services", "host-grpc-client.ts"), { force: true })
await rmdir(path.join(ROOT_DIR, "src", "standalone", "services"))
await fs.rm(path.join(ROOT_DIR, "hosts", "vscode"), { force: true, recursive: true })
await rmdir(path.join(ROOT_DIR, "hosts"))
await fs.rm(path.join(ROOT_DIR, "src/standalone/server-setup.ts"), { force: true })
@@ -636,13 +616,6 @@ async function rmdir(path) {
}
}
function serviceNameWithoutPackage(fullServiceName) {
return fullServiceName.replace(/.*\./, "")
}
function lowercaseFirstChar(str) {
return str.charAt(0).toLowerCase() + str.slice(1)
}
// Check for Apple Silicon compatibility
function checkAppleSiliconCompatibility() {
// Only run check on macOS
+16
View File
@@ -0,0 +1,16 @@
syntax = "proto3";
package host;
option java_package = "bot.cline.host.proto";
option java_multiple_files = true;
import "common.proto";
// Provides methods for working with the user's environment.
service EnvService {
// Writes text to the system clipboard.
rpc clipboardWriteText(cline.StringRequest) returns (cline.Empty);
// Reads text from the system clipboard.
rpc clipboardReadText(cline.EmptyRequest) returns (cline.String);
}
+1 -9
View File
@@ -23,8 +23,6 @@ service TaskService {
rpc exportTaskWithId(StringRequest) returns (Empty);
// Toggles the favorite status of a task
rpc toggleTaskFavorite(TaskFavoriteRequest) returns (Empty);
// Deletes all non-favorited tasks
rpc deleteNonFavoritedTasks(EmptyRequest) returns (DeleteNonFavoritedTasksResults);
// Gets filtered task history
rpc getTaskHistory(GetTaskHistoryRequest) returns (TaskHistoryArray);
// Sends a response to a previous ask operation
@@ -36,7 +34,7 @@ service TaskService {
// Executes a quick win task with command and title
rpc executeQuickWin(ExecuteQuickWinRequest) returns (Empty);
// Deletes all task history
rpc deleteAllTaskHistory(BooleanRequest) returns (DeleteAllTaskHistoryCount);
rpc deleteAllTaskHistory(EmptyRequest) returns (DeleteAllTaskHistoryCount);
}
// Request message for creating a new task
@@ -68,12 +66,6 @@ message TaskResponse {
int32 cache_reads = 10;
}
// Results returned when deleting non-favorited tasks
message DeleteNonFavoritedTasksResults {
int32 tasks_preserved = 1;
int32 tasks_deleted = 2;
}
// Request for getting task history with filtering
message GetTaskHistoryRequest {
Metadata metadata = 1;
+41 -15
View File
@@ -98,14 +98,26 @@ export class ClineHandler implements ApiHandler {
totalCost = 0
}
yield {
type: "usage",
cacheWriteTokens: 0,
cacheReadTokens: chunk.usage.prompt_tokens_details?.cached_tokens || 0,
inputTokens: chunk.usage.prompt_tokens || 0,
outputTokens: chunk.usage.completion_tokens || 0,
// @ts-ignore-next-line
totalCost,
if (modelId.includes("gemini")) {
yield {
type: "usage",
cacheWriteTokens: 0,
cacheReadTokens: chunk.usage.prompt_tokens_details?.cached_tokens || 0,
inputTokens: (chunk.usage.prompt_tokens || 0) - (chunk.usage.prompt_tokens_details?.cached_tokens || 0),
outputTokens: chunk.usage.completion_tokens || 0,
// @ts-ignore-next-line
totalCost,
}
} else {
yield {
type: "usage",
cacheWriteTokens: 0,
cacheReadTokens: chunk.usage.prompt_tokens_details?.cached_tokens || 0,
inputTokens: chunk.usage.prompt_tokens || 0,
outputTokens: chunk.usage.completion_tokens || 0,
// @ts-ignore-next-line
totalCost,
}
}
didOutputUsage = true
}
@@ -131,13 +143,27 @@ export class ClineHandler implements ApiHandler {
})
const generation = response.data
return {
type: "usage",
cacheWriteTokens: 0,
cacheReadTokens: generation?.native_tokens_cached || 0,
inputTokens: generation?.native_tokens_prompt || 0,
outputTokens: generation?.native_tokens_completion || 0,
totalCost: generation?.total_cost || 0,
let modelId = this.options.openRouterModelId
if (modelId && modelId.includes("gemini")) {
return {
type: "usage",
cacheWriteTokens: 0,
cacheReadTokens: generation?.native_tokens_cached || 0,
// openrouter generation endpoint fails often
inputTokens: (generation?.native_tokens_prompt || 0) - (generation?.native_tokens_cached || 0),
outputTokens: generation?.native_tokens_completion || 0,
totalCost: generation?.total_cost || 0,
}
} else {
return {
type: "usage",
cacheWriteTokens: 0,
cacheReadTokens: generation?.native_tokens_cached || 0,
// openrouter generation endpoint fails often
inputTokens: generation?.native_tokens_prompt || 0,
outputTokens: generation?.native_tokens_completion || 0,
totalCost: generation?.total_cost || 0,
}
}
} catch (error) {
// ignore if fails
+42 -16
View File
@@ -98,14 +98,27 @@ export class OpenRouterHandler implements ApiHandler {
}
if (!didOutputUsage && chunk.usage) {
yield {
type: "usage",
cacheWriteTokens: 0,
cacheReadTokens: chunk.usage.prompt_tokens_details?.cached_tokens || 0,
inputTokens: chunk.usage.prompt_tokens || 0,
outputTokens: chunk.usage.completion_tokens || 0,
// @ts-ignore-next-line
totalCost: (chunk.usage.cost || 0) + (chunk.usage.cost_details?.upstream_inference_cost || 0),
let modelId = this.options.openRouterModelId
if (modelId && modelId.includes("gemini")) {
yield {
type: "usage",
cacheWriteTokens: 0,
cacheReadTokens: chunk.usage.prompt_tokens_details?.cached_tokens || 0,
inputTokens: (chunk.usage.prompt_tokens || 0) - (chunk.usage.prompt_tokens_details?.cached_tokens || 0),
outputTokens: chunk.usage.completion_tokens || 0,
// @ts-ignore-next-line
totalCost: (chunk.usage.cost || 0) + (chunk.usage.cost_details?.upstream_inference_cost || 0),
}
} else {
yield {
type: "usage",
cacheWriteTokens: 0,
cacheReadTokens: chunk.usage.prompt_tokens_details?.cached_tokens || 0,
inputTokens: chunk.usage.prompt_tokens || 0,
outputTokens: chunk.usage.completion_tokens || 0,
// @ts-ignore-next-line
totalCost: (chunk.usage.cost || 0) + (chunk.usage.cost_details?.upstream_inference_cost || 0),
}
}
didOutputUsage = true
}
@@ -127,14 +140,27 @@ export class OpenRouterHandler implements ApiHandler {
const generationIterator = this.fetchGenerationDetails(this.lastGenerationId)
const generation = (await generationIterator.next()).value
// console.log("OpenRouter generation details:", generation)
return {
type: "usage",
cacheWriteTokens: 0,
cacheReadTokens: generation?.native_tokens_cached || 0,
// openrouter generation endpoint fails often
inputTokens: generation?.native_tokens_prompt || 0,
outputTokens: generation?.native_tokens_completion || 0,
totalCost: generation?.total_cost || 0,
let modelId = this.options.openRouterModelId
if (modelId && modelId.includes("gemini")) {
return {
type: "usage",
cacheWriteTokens: 0,
cacheReadTokens: generation?.native_tokens_cached || 0,
// openrouter generation endpoint fails often
inputTokens: (generation?.native_tokens_prompt || 0) - (generation?.native_tokens_cached || 0),
outputTokens: generation?.native_tokens_completion || 0,
totalCost: generation?.total_cost || 0,
}
} else {
return {
type: "usage",
cacheWriteTokens: 0,
cacheReadTokens: generation?.native_tokens_cached || 0,
// openrouter generation endpoint fails often
inputTokens: generation?.native_tokens_prompt || 0,
outputTokens: generation?.native_tokens_completion || 0,
totalCost: generation?.total_cost || 0,
}
}
} catch (error) {
// ignore if fails
+2 -1
View File
@@ -1,6 +1,7 @@
import * as vscode from "vscode"
import { Controller } from ".."
import { Empty, StringRequest } from "../../../shared/proto/common"
import { writeTextToClipboard } from "@/utils/env"
/**
* Copies text to the system clipboard
@@ -11,7 +12,7 @@ import { Empty, StringRequest } from "../../../shared/proto/common"
export async function copyToClipboard(controller: Controller, request: StringRequest): Promise<Empty> {
try {
if (request.value) {
await vscode.env.clipboard.writeText(request.value)
await writeTextToClipboard(request.value)
}
} catch (error) {
console.error("Error copying to clipboard:", error)
+1 -1
View File
@@ -63,7 +63,7 @@ export class Controller {
workspaceTracker: WorkspaceTracker
mcpHub: McpHub
accountService: ClineAccountService
latestAnnouncementId = "may-22-2025_16:11:00" // update to some unique identifier when we add a new announcement
latestAnnouncementId = "june-25-2025_16:11:00" // update to some unique identifier when we add a new announcement
constructor(
readonly context: vscode.ExtensionContext,
@@ -22,7 +22,7 @@ export async function updateAutoApprovalSettings(controller: Controller, request
await updateGlobalState(controller.context, "autoApprovalSettings", settings)
if (controller.task) {
controller.task.autoApprovalSettings = settings
controller.task.updateAutoApprovalSettings(settings)
}
await controller.postStateToWebview()
@@ -2,7 +2,6 @@ import path from "path"
import fs from "fs/promises"
import { Controller } from ".."
import { DeleteAllTaskHistoryCount } from "../../../shared/proto/task"
import { BooleanRequest } from "../../../shared/proto/common"
import { getGlobalState, updateGlobalState } from "../../storage/state"
import { fileExistsAtPath } from "../../../utils/fs"
import vscode from "vscode"
@@ -13,7 +12,7 @@ import vscode from "vscode"
* @param request Request with option to preserve favorites
* @returns Results with count of deleted tasks
*/
export async function deleteAllTaskHistory(controller: Controller, request: BooleanRequest): Promise<DeleteAllTaskHistoryCount> {
export async function deleteAllTaskHistory(controller: Controller): Promise<DeleteAllTaskHistoryCount> {
try {
// Clear current task first
await controller.clearTask()
@@ -22,8 +21,22 @@ export async function deleteAllTaskHistory(controller: Controller, request: Bool
const taskHistory = ((await getGlobalState(controller.context, "taskHistory")) as any[]) || []
const totalTasks = taskHistory.length
const userChoice = await vscode.window.showWarningMessage(
"What would you like to delete?",
{ modal: true },
"Delete All Except Favorites",
"Delete Everything",
)
// Default VS Code Cancel button returns `undefined` - don't delete anything
if (userChoice === undefined) {
return DeleteAllTaskHistoryCount.create({
tasksDeleted: 0,
})
}
// If preserving favorites, filter out non-favorites
if (request.value) {
if (userChoice === "Delete All Except Favorites") {
const favoritedTasks = taskHistory.filter((task) => task.isFavorited === true)
// If there are favorited tasks, update state
@@ -45,9 +58,20 @@ export async function deleteAllTaskHistory(controller: Controller, request: Bool
tasksDeleted: totalTasks - favoritedTasks.length,
})
} else {
return DeleteAllTaskHistoryCount.create({
tasksDeleted: 0,
})
// No favorited tasks found - show warning and ask user what to do
const answer = await vscode.window.showWarningMessage(
"No favorited tasks found. Would you like to delete all tasks anyway?",
{ modal: true },
"Delete All Tasks",
)
// User cancelled - don't delete anything
if (answer === undefined) {
return DeleteAllTaskHistoryCount.create({
tasksDeleted: 0,
})
}
// If user chose "Delete All Tasks", fall through to the `delete everything` section below
}
}
@@ -1,88 +0,0 @@
import path from "path"
import fs from "fs/promises"
import { Controller } from ".."
import { EmptyRequest } from "../../../shared/proto/common"
import { DeleteNonFavoritedTasksResults } from "../../../shared/proto/task"
import { getGlobalState, updateGlobalState } from "../../storage/state"
import { fileExistsAtPath } from "../../../utils/fs"
/**
* Deletes all non-favorited tasks, preserving only favorited ones
* @param controller The controller instance
* @param request Empty request
* @returns DeleteNonFavoritedTasksResults with counts of preserved and deleted tasks
*/
export async function deleteNonFavoritedTasks(
controller: Controller,
_request: EmptyRequest,
): Promise<DeleteNonFavoritedTasksResults> {
try {
// Clear current task first
await controller.clearTask()
// Get existing task history
const taskHistory = ((await getGlobalState(controller.context, "taskHistory")) as any[]) || []
// Filter out non-favorited tasks
const favoritedTasks = taskHistory.filter((task) => task.isFavorited === true)
const deletedCount = taskHistory.length - favoritedTasks.length
console.log(`[deleteNonFavoritedTasks] Found ${favoritedTasks.length} favorited tasks to preserve`)
// Update global state
if (favoritedTasks.length > 0) {
await updateGlobalState(controller.context, "taskHistory", favoritedTasks)
} else {
await updateGlobalState(controller.context, "taskHistory", undefined)
}
// Handle file system cleanup for deleted tasks
const preserveTaskIds = favoritedTasks.map((task) => task.id)
await cleanupTaskFiles(controller, preserveTaskIds)
// Update webview
try {
await controller.postStateToWebview()
} catch (webviewErr) {
console.error("Error posting to webview:", webviewErr)
}
return DeleteNonFavoritedTasksResults.create({
tasksPreserved: favoritedTasks.length,
tasksDeleted: deletedCount,
})
} catch (error) {
console.error("Error in deleteNonFavoritedTasks:", error)
throw error
}
}
/**
* Helper function to cleanup task files while preserving specified tasks
*/
async function cleanupTaskFiles(controller: Controller, preserveTaskIds: string[]) {
const taskDirPath = path.join(controller.context.globalStorageUri.fsPath, "tasks")
try {
if (await fileExistsAtPath(taskDirPath)) {
if (preserveTaskIds.length > 0) {
const taskDirs = await fs.readdir(taskDirPath)
console.debug(`[cleanupTaskFiles] Found ${taskDirs.length} task directories`)
// Delete only non-preserved task directories
for (const dir of taskDirs) {
if (!preserveTaskIds.includes(dir)) {
await fs.rm(path.join(taskDirPath, dir), { recursive: true, force: true })
}
}
} else {
// No tasks to preserve, delete everything
await fs.rm(taskDirPath, { recursive: true, force: true })
}
}
} catch (error) {
console.error("Error cleaning up task files:", error)
}
return true
}
+22 -3
View File
@@ -11,6 +11,7 @@ import { ClineIgnoreController } from "@core/ignore/ClineIgnoreController"
import { AutoApprovalSettings } from "@shared/AutoApprovalSettings"
import { ChatSettings } from "@shared/ChatSettings"
import { ToolParamName, ToolUse, ToolUseName } from "../assistant-message"
import { AutoApprove } from "./tools/autoApprove"
import {
BrowserAction,
BrowserActionResult,
@@ -57,6 +58,17 @@ import { setTimeout as setTimeoutPromise } from "node:timers/promises"
import { ChangeLocation, StreamingJsonReplacer } from "../assistant-message/diff-json"
export class ToolExecutor {
private autoApprover: AutoApprove
// Auto-approval methods using the AutoApprove class
private shouldAutoApproveTool(toolName: ToolUseName): boolean | [boolean, boolean] {
return this.autoApprover.shouldAutoApproveTool(toolName)
}
private shouldAutoApproveToolWithPath(blockname: ToolUseName, autoApproveActionpath: string | undefined): boolean {
return this.autoApprover.shouldAutoApproveToolWithPath(blockname, autoApproveActionpath)
}
constructor(
// Core Services & Managers
private context: vscode.ExtensionContext,
@@ -96,13 +108,20 @@ export class ToolExecutor {
private saveCheckpoint: (isAttemptCompletionMessage?: boolean) => Promise<void>,
private reinitExistingTaskFromId: (taskId: string) => Promise<void>,
private cancelTask: () => Promise<void>,
private shouldAutoApproveTool: (toolName: ToolUseName) => boolean | [boolean, boolean],
private shouldAutoApproveToolWithPath: (blockname: ToolUseName, autoApproveActionpath: string | undefined) => boolean,
private sayAndCreateMissingParamError: (toolName: ToolUseName, paramName: string, relPath?: string) => Promise<any>,
private removeLastPartialMessageIfExistsWithType: (type: "ask" | "say", askOrSay: ClineAsk | ClineSay) => Promise<void>,
private executeCommandTool: (command: string) => Promise<[boolean, any]>,
private doesLatestTaskCompletionHaveNewChanges: () => Promise<boolean>,
) {}
) {
this.autoApprover = new AutoApprove(autoApprovalSettings)
}
/**
* Updates the auto approval settings
*/
public updateAutoApprovalSettings(settings: AutoApprovalSettings): void {
this.autoApprover.updateSettings(settings)
}
private pushToolResult = (content: ToolResponse, block: ToolUse) => {
const isNextGenModel = isClaude4ModelFamily(this.api) || isGemini2dot5ModelFamily(this.api)
+7 -65
View File
@@ -314,8 +314,6 @@ export class Task {
this.saveCheckpoint.bind(this),
this.reinitExistingTaskFromId.bind(this),
this.cancelTask.bind(this),
this.shouldAutoApproveTool.bind(this),
this.shouldAutoApproveToolWithPath.bind(this),
this.sayAndCreateMissingParamError.bind(this),
this.removeLastPartialMessageIfExistsWithType.bind(this),
this.executeCommandTool.bind(this),
@@ -333,6 +331,13 @@ export class Task {
return context
}
/**
* Updates the auto approval settings for this task
*/
public updateAutoApprovalSettings(settings: AutoApprovalSettings): void {
this.toolExecutor.updateAutoApprovalSettings(settings)
}
async restoreCheckpoint(messageTs: number, restoreType: ClineCheckpointRestore, offset?: number) {
const clineMessages = this.messageStateHandler.getClineMessages()
const messageIndex = clineMessages.findIndex((m) => m.ts === messageTs) - (offset || 0)
@@ -1554,69 +1559,6 @@ export class Task {
}
}
// Check if the tool should be auto-approved based on the settings
// Returns bool for most tools, and tuple for tools with nested settings
shouldAutoApproveTool(toolName: ToolUseName): boolean | [boolean, boolean] {
if (this.autoApprovalSettings.enabled) {
switch (toolName) {
case "read_file":
case "list_files":
case "list_code_definition_names":
case "search_files":
return [
this.autoApprovalSettings.actions.readFiles,
this.autoApprovalSettings.actions.readFilesExternally ?? false,
]
case "new_rule":
case "write_to_file":
case "replace_in_file":
return [
this.autoApprovalSettings.actions.editFiles,
this.autoApprovalSettings.actions.editFilesExternally ?? false,
]
case "execute_command":
return [
this.autoApprovalSettings.actions.executeSafeCommands ?? false,
this.autoApprovalSettings.actions.executeAllCommands ?? false,
]
case "browser_action":
return this.autoApprovalSettings.actions.useBrowser
case "web_fetch":
return this.autoApprovalSettings.actions.useBrowser
case "access_mcp_resource":
case "use_mcp_tool":
return this.autoApprovalSettings.actions.useMcp
}
}
return false
}
// Check if the tool should be auto-approved based on the settings
// and the path of the action. Returns true if the tool should be auto-approved
// based on the user's settings and the path of the action.
shouldAutoApproveToolWithPath(blockname: ToolUseName, autoApproveActionpath: string | undefined): boolean {
let isLocalRead: boolean = false
if (autoApproveActionpath) {
const absolutePath = path.resolve(cwd, autoApproveActionpath)
isLocalRead = absolutePath.startsWith(cwd)
} else {
// If we do not get a path for some reason, default to a (safer) false return
isLocalRead = false
}
// Get auto-approve settings for local and external edits
const autoApproveResult = this.shouldAutoApproveTool(blockname)
const [autoApproveLocal, autoApproveExternal] = Array.isArray(autoApproveResult)
? autoApproveResult
: [autoApproveResult, false]
if ((isLocalRead && autoApproveLocal) || (!isLocalRead && autoApproveLocal && autoApproveExternal)) {
return true
} else {
return false
}
}
/**
* Migrates the disableBrowserTool setting from VSCode configuration to browserSettings
*/
+83
View File
@@ -0,0 +1,83 @@
import { AutoApprovalSettings } from "@shared/AutoApprovalSettings"
import { ToolUseName } from "@core/assistant-message"
import * as path from "path"
import * as vscode from "vscode"
import os from "os"
export const cwd =
vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) ?? path.join(os.homedir(), "Desktop")
export class AutoApprove {
autoApprovalSettings: AutoApprovalSettings
constructor(autoApprovalSettings: AutoApprovalSettings) {
this.autoApprovalSettings = autoApprovalSettings
}
// Check if the tool should be auto-approved based on the settings
// Returns bool for most tools, and tuple for tools with nested settings
shouldAutoApproveTool(toolName: ToolUseName): boolean | [boolean, boolean] {
if (this.autoApprovalSettings.enabled) {
switch (toolName) {
case "read_file":
case "list_files":
case "list_code_definition_names":
case "search_files":
return [
this.autoApprovalSettings.actions.readFiles,
this.autoApprovalSettings.actions.readFilesExternally ?? false,
]
case "new_rule":
case "write_to_file":
case "replace_in_file":
return [
this.autoApprovalSettings.actions.editFiles,
this.autoApprovalSettings.actions.editFilesExternally ?? false,
]
case "execute_command":
return [
this.autoApprovalSettings.actions.executeSafeCommands ?? false,
this.autoApprovalSettings.actions.executeAllCommands ?? false,
]
case "browser_action":
return this.autoApprovalSettings.actions.useBrowser
case "web_fetch":
return this.autoApprovalSettings.actions.useBrowser
case "access_mcp_resource":
case "use_mcp_tool":
return this.autoApprovalSettings.actions.useMcp
}
}
return false
}
// Check if the tool should be auto-approved based on the settings
// and the path of the action. Returns true if the tool should be auto-approved
// based on the user's settings and the path of the action.
shouldAutoApproveToolWithPath(blockname: ToolUseName, autoApproveActionpath: string | undefined): boolean {
let isLocalRead: boolean = false
if (autoApproveActionpath) {
const absolutePath = path.resolve(cwd, autoApproveActionpath)
isLocalRead = absolutePath.startsWith(cwd)
} else {
// If we do not get a path for some reason, default to a (safer) false return
isLocalRead = false
}
// Get auto-approve settings for local and external edits
const autoApproveResult = this.shouldAutoApproveTool(blockname)
const [autoApproveLocal, autoApproveExternal] = Array.isArray(autoApproveResult)
? autoApproveResult
: [autoApproveResult, false]
if ((isLocalRead && autoApproveLocal) || (!isLocalRead && autoApproveLocal && autoApproveExternal)) {
return true
} else {
return false
}
}
updateSettings(settings: AutoApprovalSettings): void {
this.autoApprovalSettings = settings
}
}
+5 -4
View File
@@ -34,6 +34,7 @@ import * as hostProviders from "@hosts/host-providers"
import { vscodeHostBridgeClient } from "@/hosts/vscode/client/host-grpc-client"
import { VscodeWebviewProvider } from "./core/webview/VscodeWebviewProvider"
import { ExtensionContext } from "vscode"
import { writeTextToClipboard, readTextFromClipboard } from "@/utils/env"
/*
Built using https://github.com/microsoft/vscode-webview-ui-toolkit
@@ -369,17 +370,17 @@ export async function activate(context: vscode.ExtensionContext) {
}
// Save current clipboard content
const tempCopyBuffer = await vscode.env.clipboard.readText()
const tempCopyBuffer = await readTextFromClipboard()
try {
// Copy the *existing* terminal selection (without selecting all)
await vscode.commands.executeCommand("workbench.action.terminal.copySelection")
// Get copied content
let terminalContents = (await vscode.env.clipboard.readText()).trim()
let terminalContents = (await readTextFromClipboard()).trim()
// Restore original clipboard content
await vscode.env.clipboard.writeText(tempCopyBuffer)
await writeTextToClipboard(tempCopyBuffer)
if (!terminalContents) {
// No terminal content was copied (either nothing selected or some error)
@@ -405,7 +406,7 @@ export async function activate(context: vscode.ExtensionContext) {
await visibleWebview?.controller.addSelectedTerminalOutputToChat(terminalContents, terminal.name)
} catch (error) {
// Ensure clipboard is restored even if an error occurs
await vscode.env.clipboard.writeText(tempCopyBuffer)
await writeTextToClipboard(tempCopyBuffer)
console.error("Error getting terminal contents:", error)
vscode.window.showErrorMessage("Failed to get terminal contents")
}
+2
View File
@@ -2,6 +2,7 @@ import {
UriServiceClientInterface,
WatchServiceClientInterface,
WorkspaceServiceClientInterface,
EnvServiceClientInterface,
} from "@generated/hosts/host-bridge-client-types"
/**
@@ -11,6 +12,7 @@ export interface HostBridgeClientProvider {
uriServiceClient: UriServiceClientInterface
watchServiceClient: WatchServiceClientInterface
workspaceClient: WorkspaceServiceClientInterface
envClient: EnvServiceClientInterface
}
/**
@@ -6,4 +6,5 @@ export const vscodeHostBridgeClient: HostBridgeClientProvider = {
uriServiceClient: createGrpcClient(host.UriServiceDefinition),
watchServiceClient: createGrpcClient(host.WatchServiceDefinition),
workspaceClient: createGrpcClient(host.WorkspaceServiceDefinition),
envClient: createGrpcClient(host.EnvServiceDefinition),
}
+7
View File
@@ -0,0 +1,7 @@
import { EmptyRequest, String } from "@/shared/proto/common"
import * as vscode from "vscode"
export async function clipboardReadText(_: EmptyRequest): Promise<String> {
const text = await vscode.env.clipboard.readText()
return String.create({ value: text })
}
+7
View File
@@ -0,0 +1,7 @@
import { StringRequest, Empty } from "@/shared/proto/common"
import * as vscode from "vscode"
export async function clipboardWriteText(request: StringRequest): Promise<Empty> {
await vscode.env.clipboard.writeText(request.value)
return Empty.create({})
}
+7 -3
View File
@@ -112,7 +112,6 @@ function runProcess({ systemPrompt, messages, path, modelId }: ClaudeCodeOptions
const args = [
"-p",
JSON.stringify(messages),
"--system-prompt",
systemPrompt,
"--verbose",
@@ -129,8 +128,8 @@ function runProcess({ systemPrompt, messages, path, modelId }: ClaudeCodeOptions
args.push("--model", modelId)
}
return execa(claudePath, args, {
stdin: "ignore",
const claudeCodeProcess = execa(claudePath, args, {
stdin: "pipe",
stdout: "pipe",
stderr: "pipe",
env: {
@@ -142,6 +141,11 @@ function runProcess({ systemPrompt, messages, path, modelId }: ClaudeCodeOptions
maxBuffer: 1024 * 1024 * 1000,
timeout: CLAUDE_CODE_TIMEOUT,
})
claudeCodeProcess.stdin.write(JSON.stringify(messages))
claudeCodeProcess.stdin.end()
return claudeCodeProcess
}
function parseChunk(data: string, processState: ProcessState) {
@@ -1,5 +1,6 @@
import * as vscode from "vscode"
import { getWorkingState } from "@utils/git"
import { writeTextToClipboard } from "@utils/env"
/**
* Formats the git diff into a prompt for the AI
@@ -57,7 +58,7 @@ export function extractCommitMessage(aiResponse: string): string {
* @param message The commit message to copy
*/
export async function copyCommitMessageToClipboard(message: string): Promise<void> {
await vscode.env.clipboard.writeText(message)
await writeTextToClipboard(message)
vscode.window.showInformationMessage("Commit message copied to clipboard")
}
@@ -1,4 +1,5 @@
import * as vscode from "vscode"
import { readTextFromClipboard, writeTextToClipboard } from "@utils/env"
/**
* Gets the contents of the active terminal
@@ -6,7 +7,7 @@ import * as vscode from "vscode"
*/
export async function getLatestTerminalOutput(): Promise<string> {
// Store original clipboard content to restore later
const originalClipboard = await vscode.env.clipboard.readText()
const originalClipboard = await readTextFromClipboard()
try {
// Select terminal content
@@ -19,7 +20,7 @@ export async function getLatestTerminalOutput(): Promise<string> {
await vscode.commands.executeCommand("workbench.action.terminal.clearSelection")
// Get terminal contents from clipboard
let terminalContents = (await vscode.env.clipboard.readText()).trim()
let terminalContents = (await readTextFromClipboard()).trim()
// Check if there's actually a terminal open
if (terminalContents === originalClipboard) {
@@ -40,6 +41,6 @@ export async function getLatestTerminalOutput(): Promise<string> {
return terminalContents
} finally {
// Restore original clipboard content
await vscode.env.clipboard.writeText(originalClipboard)
await writeTextToClipboard(originalClipboard)
}
}
@@ -3,11 +3,13 @@ import {
UriServiceClientImpl,
WatchServiceClientImpl,
WorkspaceServiceClientImpl,
EnvServiceClientImpl,
} from "@generated/standalone/host-bridge-clients"
import {
UriServiceClientInterface,
WatchServiceClientInterface,
WorkspaceServiceClientInterface,
EnvServiceClientInterface,
} from "@generated/hosts/host-bridge-client-types"
import { HostBridgeClientProvider } from "@/hosts/host-provider-types"
@@ -20,6 +22,7 @@ export class ExternalHostBridgeClientManager implements HostBridgeClientProvider
uriServiceClient: UriServiceClientInterface
watchServiceClient: WatchServiceClientInterface
workspaceClient: WorkspaceServiceClientInterface
envClient: EnvServiceClientInterface
constructor() {
const address = process.env.HOST_BRIDGE_ADDRESS || "localhost:50052"
@@ -28,6 +31,7 @@ export class ExternalHostBridgeClientManager implements HostBridgeClientProvider
this.uriServiceClient = new UriServiceClientImpl(this.channel)
this.watchServiceClient = new WatchServiceClientImpl(this.channel)
this.workspaceClient = new WorkspaceServiceClientImpl(this.channel)
this.envClient = new EnvServiceClientImpl(this.channel)
}
public close(): void {
+32
View File
@@ -0,0 +1,32 @@
import { getHostBridgeProvider } from "@/hosts/host-providers"
import { StringRequest, EmptyRequest } from "@/shared/proto/common"
/**
* Writes text to the system clipboard
* @param text The text to write to the clipboard
* @returns Promise that resolves when the operation is complete
* @throws Error if the operation fails
*/
export async function writeTextToClipboard(text: string): Promise<void> {
try {
await getHostBridgeProvider().envClient.clipboardWriteText(StringRequest.create({ value: text }))
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
throw new Error(`Failed to write to clipboard: ${errorMessage}`)
}
}
/**
* Reads text from the system clipboard
* @returns Promise that resolves to the clipboard text
* @throws Error if the operation fails
*/
export async function readTextFromClipboard(): Promise<string> {
try {
const response = await getHostBridgeProvider().envClient.clipboardReadText(EmptyRequest.create({}))
return response.value
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
throw new Error(`Failed to read from clipboard: ${errorMessage}`)
}
}
+3 -2
View File
@@ -17,6 +17,7 @@ import * as vscode from "vscode"
import * as cp from "child_process"
import * as os from "os"
import * as util from "util"
import { writeTextToClipboard } from "@/utils/env"
/**
* Creates a properly encoded GitHub issue URL.
@@ -81,7 +82,7 @@ export async function openUrlInBrowser(url: string): Promise<void> {
// Always copy to clipboard as a fallback
try {
await vscode.env.clipboard.writeText(url)
await writeTextToClipboard(url)
console.log("URL copied to clipboard as backup")
} catch (error) {
console.error(`Failed to copy URL to clipboard: ${error}`)
@@ -159,7 +160,7 @@ export async function openUrlInBrowser(url: string): Promise<void> {
)
.then((selection) => {
if (selection === "Copy URL Again") {
vscode.env.clipboard.writeText(url)
writeTextToClipboard(url)
}
})
}
@@ -0,0 +1,514 @@
const { spawn } = require("child_process")
const { EventEmitter } = require("events")
const path = require("path")
const os = require("os")
// Enhanced terminal management for standalone Cline
// This replaces VSCode's terminal integration with real subprocess management
class StandaloneTerminalProcess extends EventEmitter {
constructor() {
super()
this.waitForShellIntegration = false // We don't need to wait since we control the process
this.isListening = true
this.buffer = ""
this.fullOutput = ""
this.lastRetrievedIndex = 0
this.isHot = false
this.hotTimer = null
this.childProcess = null
this.exitCode = null
this.isCompleted = false
}
async run(terminal, command) {
console.log(`[StandaloneTerminal] Running command: ${command}`)
// Get shell and working directory from terminal
const shell = terminal._shellPath || this.getDefaultShell()
const cwd = terminal._cwd || process.cwd()
// Prepare command for execution
const shellArgs = this.getShellArgs(shell, command)
try {
// Spawn the process
this.childProcess = spawn(shell, shellArgs, {
cwd: cwd,
stdio: ["pipe", "pipe", "pipe"],
env: { ...process.env, TERM: "xterm-256color" },
})
// Track process state
let didEmitEmptyLine = false
// Handle stdout
this.childProcess.stdout.on("data", (data) => {
const output = data.toString()
this.handleOutput(output, didEmitEmptyLine)
if (!didEmitEmptyLine && output) {
this.emit("line", "") // Signal start of output
didEmitEmptyLine = true
}
})
// Handle stderr
this.childProcess.stderr.on("data", (data) => {
const output = data.toString()
this.handleOutput(output, didEmitEmptyLine)
if (!didEmitEmptyLine && output) {
this.emit("line", "")
didEmitEmptyLine = true
}
})
// Handle process completion
this.childProcess.on("close", (code, signal) => {
console.log(`[StandaloneTerminal] Process closed with code ${code}, signal ${signal}`)
this.exitCode = code
this.isCompleted = true
this.emitRemainingBuffer()
// Clear hot timer
if (this.hotTimer) {
clearTimeout(this.hotTimer)
this.isHot = false
}
this.emit("completed")
this.emit("continue")
})
// Handle process errors
this.childProcess.on("error", (error) => {
console.error(`[StandaloneTerminal] Process error:`, error)
this.emit("error", error)
})
// Update terminal's process reference
terminal._process = this.childProcess
terminal._processId = this.childProcess.pid
} catch (error) {
console.error(`[StandaloneTerminal] Failed to spawn process:`, error)
this.emit("error", error)
}
}
handleOutput(data, didEmitEmptyLine) {
// Set process as hot (actively outputting)
this.isHot = true
if (this.hotTimer) {
clearTimeout(this.hotTimer)
}
// Check for compilation markers to adjust hot timeout
const compilingMarkers = ["compiling", "building", "bundling", "transpiling", "generating", "starting"]
const markerNullifiers = [
"compiled",
"success",
"finish",
"complete",
"succeed",
"done",
"end",
"stop",
"exit",
"terminate",
"error",
"fail",
]
const isCompiling =
compilingMarkers.some((marker) => data.toLowerCase().includes(marker.toLowerCase())) &&
!markerNullifiers.some((nullifier) => data.toLowerCase().includes(nullifier.toLowerCase()))
const hotTimeout = isCompiling ? 15000 : 2000
this.hotTimer = setTimeout(() => {
this.isHot = false
}, hotTimeout)
// Store full output
this.fullOutput += data
if (this.isListening) {
this.emitLines(data)
this.lastRetrievedIndex = this.fullOutput.length - this.buffer.length
}
}
emitLines(chunk) {
this.buffer += chunk
let lineEndIndex
while ((lineEndIndex = this.buffer.indexOf("\n")) !== -1) {
let line = this.buffer.slice(0, lineEndIndex).trimEnd()
this.emit("line", line)
this.buffer = this.buffer.slice(lineEndIndex + 1)
}
}
emitRemainingBuffer() {
if (this.buffer && this.isListening) {
const remainingBuffer = this.removeLastLineArtifacts(this.buffer)
if (remainingBuffer) {
this.emit("line", remainingBuffer)
}
this.buffer = ""
this.lastRetrievedIndex = this.fullOutput.length
}
}
continue() {
this.emitRemainingBuffer()
this.isListening = false
this.removeAllListeners("line")
this.emit("continue")
}
getUnretrievedOutput() {
const unretrieved = this.fullOutput.slice(this.lastRetrievedIndex)
this.lastRetrievedIndex = this.fullOutput.length
return this.removeLastLineArtifacts(unretrieved)
}
removeLastLineArtifacts(output) {
const lines = output.trimEnd().split("\n")
if (lines.length > 0) {
const lastLine = lines[lines.length - 1]
lines[lines.length - 1] = lastLine.replace(/[%$#>]\s*$/, "")
}
return lines.join("\n").trimEnd()
}
getDefaultShell() {
if (process.platform === "win32") {
return process.env.COMSPEC || "cmd.exe"
} else {
return process.env.SHELL || "/bin/bash"
}
}
getShellArgs(shell, command) {
if (process.platform === "win32") {
if (shell.toLowerCase().includes("powershell") || shell.toLowerCase().includes("pwsh")) {
return ["-Command", command]
} else {
return ["/c", command]
}
} else {
return ["-c", command]
}
}
// Terminate the process if it's still running
terminate() {
if (this.childProcess && !this.isCompleted) {
console.log(`[StandaloneTerminal] Terminating process ${this.childProcess.pid}`)
this.childProcess.kill("SIGTERM")
// Force kill after timeout
setTimeout(() => {
if (!this.isCompleted) {
console.log(`[StandaloneTerminal] Force killing process ${this.childProcess.pid}`)
this.childProcess.kill("SIGKILL")
}
}, 5000)
}
}
}
class StandaloneTerminal {
constructor(options = {}) {
this.name = options.name || `Terminal ${Math.floor(Math.random() * 10000)}`
this.processId = Promise.resolve(Math.floor(Math.random() * 100000))
this.creationOptions = options
this.exitStatus = undefined
this.state = { isInteractedWith: false }
this._cwd = options.cwd || process.cwd()
this._shellPath = options.shellPath
this._process = null
this._processId = null
// Mock shell integration for compatibility
this.shellIntegration = {
cwd: { fsPath: this._cwd },
executeCommand: (command) => {
// Return a mock execution object that the TerminalProcess expects
return {
read: async function* () {
// This will be handled by our StandaloneTerminalProcess
yield ""
},
}
},
}
console.log(`[StandaloneTerminal] Created terminal: ${this.name} in ${this._cwd}`)
}
sendText(text, addNewLine = true) {
console.log(`[StandaloneTerminal] sendText: ${text}`)
// If we have an active process, send input to it
if (this._process && !this._process.killed) {
try {
this._process.stdin.write(text + (addNewLine ? "\n" : ""))
} catch (error) {
console.error(`[StandaloneTerminal] Error sending text to process:`, error)
}
} else {
// For compatibility with old behavior, we could spawn a new process
console.log(`[StandaloneTerminal] No active process to send text to`)
}
}
show() {
console.log(`[StandaloneTerminal] show: ${this.name}`)
this.state.isInteractedWith = true
}
hide() {
console.log(`[StandaloneTerminal] hide: ${this.name}`)
}
dispose() {
console.log(`[StandaloneTerminal] dispose: ${this.name}`)
if (this._process && !this._process.killed) {
this._process.kill("SIGTERM")
}
}
}
// Terminal registry for tracking terminals
class StandaloneTerminalRegistry {
constructor() {
this.terminals = new Map()
this.nextId = 1
}
createTerminal(options = {}) {
const terminal = new StandaloneTerminal(options)
const id = this.nextId++
const terminalInfo = {
id: id,
terminal: terminal,
busy: false,
lastCommand: "",
shellPath: options.shellPath,
lastActive: Date.now(),
pendingCwdChange: undefined,
cwdResolved: undefined,
}
this.terminals.set(id, terminalInfo)
console.log(`[StandaloneTerminalRegistry] Created terminal ${id}`)
return terminalInfo
}
getTerminal(id) {
return this.terminals.get(id)
}
getAllTerminals() {
return Array.from(this.terminals.values())
}
removeTerminal(id) {
const terminalInfo = this.terminals.get(id)
if (terminalInfo) {
terminalInfo.terminal.dispose()
this.terminals.delete(id)
console.log(`[StandaloneTerminalRegistry] Removed terminal ${id}`)
}
}
updateTerminal(id, updates) {
const terminalInfo = this.terminals.get(id)
if (terminalInfo) {
Object.assign(terminalInfo, updates)
}
}
}
// Enhanced terminal manager
class StandaloneTerminalManager {
constructor() {
this.registry = new StandaloneTerminalRegistry()
this.processes = new Map()
this.terminalIds = new Set()
this.shellIntegrationTimeout = 4000
this.terminalReuseEnabled = true
this.terminalOutputLineLimit = 500
this.defaultTerminalProfile = "default"
}
runCommand(terminalInfo, command) {
console.log(`[StandaloneTerminalManager] Running command on terminal ${terminalInfo.id}: ${command}`)
terminalInfo.busy = true
terminalInfo.lastCommand = command
const process = new StandaloneTerminalProcess()
this.processes.set(terminalInfo.id, process)
process.once("completed", () => {
terminalInfo.busy = false
console.log(`[StandaloneTerminalManager] Command completed on terminal ${terminalInfo.id}`)
})
process.once("error", (error) => {
terminalInfo.busy = false
console.error(`[StandaloneTerminalManager] Command error on terminal ${terminalInfo.id}:`, error)
})
// Create promise for the process
const promise = new Promise((resolve, reject) => {
process.once("continue", () => resolve())
process.once("error", (error) => reject(error))
})
// Run the command immediately (no shell integration wait needed)
process.run(terminalInfo.terminal, command)
// Return merged promise/process object
return this.mergePromise(process, promise)
}
async getOrCreateTerminal(cwd) {
const terminals = this.registry.getAllTerminals()
// Find available terminal with matching CWD
const matchingTerminal = terminals.find((t) => {
if (t.busy) return false
return t.terminal._cwd === cwd
})
if (matchingTerminal) {
this.terminalIds.add(matchingTerminal.id)
console.log(`[StandaloneTerminalManager] Reusing terminal ${matchingTerminal.id}`)
return matchingTerminal
}
// Find any available terminal if reuse is enabled
if (this.terminalReuseEnabled) {
const availableTerminal = terminals.find((t) => !t.busy)
if (availableTerminal) {
// Change directory
await this.runCommand(availableTerminal, `cd "${cwd}"`)
availableTerminal.terminal._cwd = cwd
availableTerminal.terminal.shellIntegration.cwd.fsPath = cwd
this.terminalIds.add(availableTerminal.id)
console.log(`[StandaloneTerminalManager] Reused terminal ${availableTerminal.id} with cd`)
return availableTerminal
}
}
// Create new terminal
const newTerminalInfo = this.registry.createTerminal({
cwd: cwd,
name: `Cline Terminal ${this.registry.nextId}`,
})
this.terminalIds.add(newTerminalInfo.id)
console.log(`[StandaloneTerminalManager] Created new terminal ${newTerminalInfo.id}`)
return newTerminalInfo
}
getTerminals(busy) {
return Array.from(this.terminalIds)
.map((id) => this.registry.getTerminal(id))
.filter((t) => t && t.busy === busy)
.map((t) => ({ id: t.id, lastCommand: t.lastCommand }))
}
getUnretrievedOutput(terminalId) {
if (!this.terminalIds.has(terminalId)) {
return ""
}
const process = this.processes.get(terminalId)
return process ? process.getUnretrievedOutput() : ""
}
isProcessHot(terminalId) {
const process = this.processes.get(terminalId)
return process ? process.isHot : false
}
processOutput(outputLines) {
if (outputLines.length > this.terminalOutputLineLimit) {
const halfLimit = Math.floor(this.terminalOutputLineLimit / 2)
const start = outputLines.slice(0, halfLimit)
const end = outputLines.slice(outputLines.length - halfLimit)
return `${start.join("\n")}\n... (output truncated) ...\n${end.join("\n")}`.trim()
}
return outputLines.join("\n").trim()
}
disposeAll() {
// Terminate all processes
for (const [terminalId, process] of this.processes) {
if (process && process.terminate) {
process.terminate()
}
}
// Clear all tracking
this.terminalIds.clear()
this.processes.clear()
// Dispose all terminals
for (const terminalInfo of this.registry.getAllTerminals()) {
terminalInfo.terminal.dispose()
}
console.log(`[StandaloneTerminalManager] Disposed all terminals`)
}
// Set shell integration timeout (compatibility method)
setShellIntegrationTimeout(timeout) {
this.shellIntegrationTimeout = timeout
console.log(`[StandaloneTerminalManager] Set shell integration timeout to ${timeout}ms`)
}
// Set terminal reuse enabled (compatibility method)
setTerminalReuseEnabled(enabled) {
this.terminalReuseEnabled = enabled
console.log(`[StandaloneTerminalManager] Set terminal reuse enabled to ${enabled}`)
}
// Set terminal output line limit (compatibility method)
setTerminalOutputLineLimit(limit) {
this.terminalOutputLineLimit = limit
console.log(`[StandaloneTerminalManager] Set terminal output line limit to ${limit}`)
}
// Set default terminal profile (compatibility method)
setDefaultTerminalProfile(profile) {
this.defaultTerminalProfile = profile
console.log(`[StandaloneTerminalManager] Set default terminal profile to ${profile}`)
}
// Helper to merge process and promise (similar to execa)
mergePromise(process, promise) {
const nativePromisePrototype = (async () => {})().constructor.prototype
const descriptors = ["then", "catch", "finally"].map((property) => [
property,
Reflect.getOwnPropertyDescriptor(nativePromisePrototype, property),
])
for (const [property, descriptor] of descriptors) {
if (descriptor) {
const value = descriptor.value.bind(promise)
Reflect.defineProperty(process, property, { ...descriptor, value })
}
}
return process
}
}
module.exports = {
StandaloneTerminal,
StandaloneTerminalProcess,
StandaloneTerminalRegistry,
StandaloneTerminalManager,
}
+719 -24
View File
@@ -2,8 +2,19 @@ console.log("Loading stub impls...")
const { createStub } = require("./stub-utils")
const open = require("open").default
const fs = require("fs")
const path = require("path")
const { StandaloneTerminalManager } = require("./enhanced-terminal")
// Import the base vscode object from stubs
const vscode = require("./vscode-stubs.js")
// Create global terminal manager instance
const globalTerminalManager = new StandaloneTerminalManager()
// Extend the existing window object from stubs rather than overwriting it
vscode.window = {
...vscode.window, // Keep existing properties from stubs
showInformationMessage: (...args) => {
console.log("Stubbed showInformationMessage:", ...args)
return Promise.resolve(undefined)
@@ -28,9 +39,210 @@ vscode.window = {
console.log("Stubbed showSaveDialog:", options)
return undefined
},
showTextDocument: async (...args) => {
console.log("Stubbed showTextDocument:", ...args)
return {}
showTextDocument: async (uri, options) => {
console.log("Stubbed showTextDocument:", uri, options)
// Extract file path from URI
let filePath = uri.path || uri.fsPath || uri
if (typeof filePath !== "string") {
filePath = uri.toString()
}
// Remove file:// prefix if present
if (filePath.startsWith("file://")) {
filePath = filePath.substring(7)
}
// Create a function that always reads the current file content
const getCurrentFileContent = async () => {
try {
const content = await fs.promises.readFile(filePath, "utf8")
console.log(`getCurrentFileContent: Read file ${filePath} (${content.length} chars)`)
return content
} catch (error) {
console.log(`getCurrentFileContent: Could not read file ${filePath}:`, error.message)
return ""
}
}
// Try to read the initial file content
let fileContent = await getCurrentFileContent()
let lineCount = fileContent.split("\n").length
// Check if we already have an active editor for this file path
const existingEditor = vscode.window._documentEditors && vscode.window._documentEditors[filePath]
if (existingEditor) {
console.log(`showTextDocument: Updating existing editor for ${filePath}`)
// Update the existing editor's content
fileContent = await getCurrentFileContent()
lineCount = fileContent.split("\n").length
// Update the document's getText method to return current content
existingEditor.document.getText = (range) => {
// Always read fresh content for getText calls
const currentContent = require("fs").readFileSync(filePath, "utf8")
if (!range) {
return currentContent
}
// Handle range-based getText with current content
const lines = currentContent.split("\n")
const startLine = Math.max(0, range.start.line)
const endLine = Math.min(lines.length - 1, range.end.line)
if (startLine === endLine) {
// Single line
const line = lines[startLine] || ""
const startChar = Math.max(0, range.start.character)
const endChar = Math.min(line.length, range.end.character)
return line.substring(startChar, endChar)
} else {
// Multiple lines
const result = []
for (let i = startLine; i <= endLine; i++) {
const line = lines[i] || ""
if (i === startLine) {
result.push(line.substring(range.start.character))
} else if (i === endLine) {
result.push(line.substring(0, range.end.character))
} else {
result.push(line)
}
}
return result.join("\n")
}
}
// Update other properties
existingEditor.document.lineCount = lineCount
existingEditor.document.fileName = filePath
// Update the active text editor reference
vscode.window.activeTextEditor = existingEditor
return existingEditor
}
// Create a new mock text editor that always reads current file content
const mockEditor = {
document: {
uri: uri,
fileName: filePath,
isDirty: false,
lineCount: lineCount,
getText: (range) => {
// Always read fresh content for getText calls
try {
const currentContent = require("fs").readFileSync(filePath, "utf8")
console.log(`document.getText: Read fresh content (${currentContent.length} chars)`)
if (!range) {
return currentContent
}
// Handle range-based getText with current content
const lines = currentContent.split("\n")
const startLine = Math.max(0, range.start.line)
const endLine = Math.min(lines.length - 1, range.end.line)
if (startLine === endLine) {
// Single line
const line = lines[startLine] || ""
const startChar = Math.max(0, range.start.character)
const endChar = Math.min(line.length, range.end.character)
return line.substring(startChar, endChar)
} else {
// Multiple lines
const result = []
for (let i = startLine; i <= endLine; i++) {
const line = lines[i] || ""
if (i === startLine) {
result.push(line.substring(range.start.character))
} else if (i === endLine) {
result.push(line.substring(0, range.end.character))
} else {
result.push(line)
}
}
return result.join("\n")
}
} catch (error) {
console.error(`Error reading file in getText: ${error.message}`)
return ""
}
},
save: async () => {
console.log("Called mock textDocument.save")
return true
},
positionAt: (offset) => {
try {
const currentContent = require("fs").readFileSync(filePath, "utf8")
const lines = currentContent.split("\n")
let currentOffset = 0
for (let line = 0; line < lines.length; line++) {
const lineLength = lines[line].length + 1 // +1 for newline
if (currentOffset + lineLength > offset) {
return { line: line, character: offset - currentOffset }
}
currentOffset += lineLength
}
return { line: lines.length - 1, character: lines[lines.length - 1]?.length || 0 }
} catch (error) {
return { line: 0, character: 0 }
}
},
offsetAt: (position) => {
try {
const currentContent = require("fs").readFileSync(filePath, "utf8")
const lines = currentContent.split("\n")
let offset = 0
for (let i = 0; i < position.line && i < lines.length; i++) {
offset += lines[i].length + 1 // +1 for newline
}
offset += Math.min(position.character, lines[position.line]?.length || 0)
return offset
} catch (error) {
return 0
}
},
},
selection: { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } },
selections: [],
visibleRanges: [{ start: { line: 0, character: 0 }, end: { line: 0, character: 0 } }],
options: {},
viewColumn: 1,
edit: async (callback) => {
console.log("Called mock textEditor.edit")
return true
},
insertSnippet: async () => true,
setDecorations: () => {},
revealRange: () => {},
show: () => {},
hide: () => {},
}
// Store the editor by file path for future reference
if (!vscode.window._documentEditors) {
vscode.window._documentEditors = {}
}
vscode.window._documentEditors[filePath] = mockEditor
// Update the active text editor
vscode.window.activeTextEditor = mockEditor
// Trigger onDidChangeActiveTextEditor listeners
if (vscode.window._activeTextEditorListeners) {
setTimeout(() => {
vscode.window._activeTextEditorListeners.forEach((listener) => {
try {
listener(mockEditor)
} catch (error) {
console.error("Error calling onDidChangeActiveTextEditor listener:", error)
}
})
}, 10) // Small delay to simulate async behavior
}
return mockEditor
},
createOutputChannel: (name) => {
console.log("Stubbed createOutputChannel:", name)
@@ -41,20 +253,60 @@ vscode.window = {
}
},
createTerminal: (...args) => {
console.log("Stubbed createTerminal:", ...args)
return {
sendText: console.log,
show: () => {},
dispose: () => {},
console.log("Enhanced createTerminal:", ...args)
// Extract options from arguments
let options = {}
if (args.length > 0) {
if (typeof args[0] === "string") {
// Called with (name, shellPath, shellArgs)
options = {
name: args[0],
shellPath: args[1],
shellArgs: args[2],
}
} else if (typeof args[0] === "object") {
// Called with options object
options = args[0]
}
}
// Use our enhanced terminal manager to create a terminal
const terminalInfo = globalTerminalManager.registry.createTerminal({
name: options.name || `Terminal ${Date.now()}`,
cwd: options.cwd || process.cwd(),
shellPath: options.shellPath,
})
// Store reference for tracking
vscode.window.terminals.push(terminalInfo.terminal)
if (!vscode.window.activeTerminal) {
vscode.window.activeTerminal = terminalInfo.terminal
}
console.log(`Enhanced terminal created: ${terminalInfo.id}`)
return terminalInfo.terminal
},
activeTextEditor: undefined,
visibleTextEditors: [],
tabGroups: {
all: [],
close: async () => {},
onDidChangeTabs: createStub("vscode.env.tabGroups.onDidChangeTabs"),
activeTabGroup: { tabs: [] },
all: [
{
tabs: [],
isActive: true,
viewColumn: 1,
},
],
activeTabGroup: {
tabs: [],
isActive: true,
viewColumn: 1,
},
close: async (tab) => {
console.log("Stubbed tabGroups.close:", tab)
return true
},
onDidChangeTabs: createStub("vscode.window.tabGroups.onDidChangeTabs"),
},
withProgress: async (_options, task) => {
console.log("Stubbed withProgress")
@@ -62,14 +314,56 @@ vscode.window = {
},
registerUriHandler: () => ({ dispose: () => {} }),
registerWebviewViewProvider: () => ({ dispose: () => {} }),
onDidChangeActiveTextEditor: () => ({ dispose: () => {} }),
createTextEditorDecorationType: () => ({ dispose: () => {} }),
createWebviewPanel: (..._args) => {
throw new Error("WebviewPanel is not supported in standalone app.")
onDidChangeActiveTextEditor: (listener) => {
console.log("Called vscode.window.onDidChangeActiveTextEditor")
// Store the listener so we can call it when showTextDocument is called
vscode.window._activeTextEditorListeners = vscode.window._activeTextEditorListeners || []
vscode.window._activeTextEditorListeners.push(listener)
return {
dispose: () => {
console.log("Disposed onDidChangeActiveTextEditor listener")
const index = vscode.window._activeTextEditorListeners.indexOf(listener)
if (index > -1) {
vscode.window._activeTextEditorListeners.splice(index, 1)
}
},
}
},
createTextEditorDecorationType: () => ({ dispose: () => {} }),
createWebviewPanel: (...args) => {
console.log("Stubbed createWebviewPanel:", ...args)
return {
webview: {},
reveal: () => {},
dispose: () => {},
}
},
onDidChangeTerminalState: (listener) => {
console.log("Called vscode.window.onDidChangeTerminalState")
return {
dispose: () => {
console.log("Disposed onDidChangeTerminalState listener")
},
}
},
onDidChangeTextEditorVisibleRanges: (listener) => {
console.log("Called vscode.window.onDidChangeTextEditorVisibleRanges")
return {
dispose: () => {
console.log("Disposed onDidChangeTextEditorVisibleRanges listener")
},
}
},
terminals: [],
activeTerminal: null,
}
vscode.env = {
// Initialize env object if it doesn't exist, then extend it
if (!vscode.env) {
vscode.env = {}
}
Object.assign(vscode.env, {
uriScheme: "vscode",
appName: "Visual Studio Code",
appRoot: "/tmp/vscode/appRoot",
@@ -79,17 +373,26 @@ vscode.env = {
sessionId: "stub-session-id",
shell: "/bin/bash",
// Add the stub functions that were missing
clipboard: createStub("vscode.env.clipboard"),
openExternal: createStub("vscode.env.openExternal"),
getQueryParameter: createStub("vscode.env.getQueryParameter"),
onDidChangeTelemetryEnabled: createStub("vscode.env.onDidChangeTelemetryEnabled"),
isTelemetryEnabled: createStub("vscode.env.isTelemetryEnabled"),
telemetryConfiguration: createStub("vscode.env.telemetryConfiguration"),
onDidChangeTelemetryConfiguration: createStub("vscode.env.onDidChangeTelemetryConfiguration"),
createTelemetryLogger: createStub("vscode.env.createTelemetryLogger"),
})
// Override the openExternal function with actual implementation
vscode.env.openExternal = async (uri) => {
const url = typeof uri === "string" ? uri : (uri.toString?.() ?? "")
console.log("Opening browser:", url)
await open(url)
return true
}
vscode.Uri = {
// Extend Uri object with improved implementations
Object.assign(vscode.Uri, {
parse: (uriString) => {
const url = new URL(uriString)
return {
@@ -134,13 +437,405 @@ vscode.Uri = {
const joined = segments.map((s) => (typeof s === "string" ? s : s.path)).join("/")
return vscode.Uri.file("/" + joined.replace(/\/+/g, "/"))
},
})
// Extend workspace object with file system operations
Object.assign(vscode.workspace, {
fs: {
readFile: async function (uri) {
console.log(`Called vscode.workspace.fs.readFile with uri:`, uri)
try {
// Extract file path from URI
let filePath = uri.path || uri.fsPath || uri
if (typeof filePath !== "string") {
filePath = uri.toString()
}
// Remove file:// prefix if present
if (filePath.startsWith("file://")) {
filePath = filePath.substring(7)
}
console.log(`Reading file: ${filePath}`)
const content = await fs.promises.readFile(filePath, "utf8")
console.log(
`File content read (${content.length} chars):`,
content.substring(0, 100) + (content.length > 100 ? "..." : ""),
)
return new Uint8Array(Buffer.from(content, "utf8"))
} catch (error) {
console.error(`Error reading file:`, error)
throw error
}
},
writeFile: async function (uri, content) {
console.log(`Called vscode.workspace.fs.writeFile with uri:`, uri)
try {
// Extract file path from URI
let filePath = uri.path || uri.fsPath || uri
if (typeof filePath !== "string") {
filePath = uri.toString()
}
// Remove file:// prefix if present
if (filePath.startsWith("file://")) {
filePath = filePath.substring(7)
}
console.log(`Writing file: ${filePath}`)
// Ensure directory exists
const dir = path.dirname(filePath)
await fs.promises.mkdir(dir, { recursive: true })
// Write the file
await fs.promises.writeFile(filePath, content)
} catch (error) {
console.error(`Error writing file:`, error)
throw error
}
},
},
// Add workspace folder configuration
rootPath: process.cwd(),
workspaceFolders: [
{
uri: vscode.Uri.file(process.cwd()),
name: path.basename(process.cwd()),
index: 0,
},
],
name: path.basename(process.cwd()),
workspaceFile: vscode.Uri.file(path.join(process.cwd(), ".vscode", "workspace.json")),
// Add other workspace methods as stubs
getConfiguration: () => ({
get: () => undefined,
update: () => Promise.resolve(),
has: () => false,
}),
getWorkspaceFolder: (uri) => {
console.log("Called vscode.workspace.getWorkspaceFolder with:", uri)
// Return the first workspace folder for any URI in standalone mode
if (vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length > 0) {
return vscode.workspace.workspaceFolders[0]
}
return undefined
},
createFileSystemWatcher: () => ({
onDidChange: () => ({ dispose: () => {} }),
onDidCreate: () => ({ dispose: () => {} }),
onDidDelete: () => ({ dispose: () => {} }),
dispose: () => {},
}),
onDidChangeConfiguration: () => ({ dispose: () => {} }),
onDidChangeWorkspaceFolders: () => ({ dispose: () => {} }),
onDidCreateFiles: createStub("vscode.workspace.onDidCreateFiles"),
onDidDeleteFiles: createStub("vscode.workspace.onDidDeleteFiles"),
onDidRenameFiles: createStub("vscode.workspace.onDidRenameFiles"),
onWillCreateFiles: createStub("vscode.workspace.onWillCreateFiles"),
onWillDeleteFiles: createStub("vscode.workspace.onWillDeleteFiles"),
onWillRenameFiles: createStub("vscode.workspace.onWillRenameFiles"),
textDocuments: {
find: (predicate) => {
console.log("Called vscode.workspace.textDocuments.find")
// Return a mock text document that behaves like VSCode expects
return {
uri: { fsPath: "/tmp/mock-document" },
fileName: "/tmp/mock-document",
isDirty: false,
save: async () => {
console.log("Called mock textDocument.save")
return true
},
getText: () => "",
lineCount: 0,
}
},
forEach: (callback) => {
console.log("Called vscode.workspace.textDocuments.forEach")
// No documents to iterate over in standalone mode
},
length: 0,
[Symbol.iterator]: function* () {
// Empty iterator for standalone mode
},
},
// Add the crucial applyEdit method
applyEdit: async (workspaceEdit) => {
console.log("Called vscode.workspace.applyEdit", workspaceEdit)
// For standalone mode, we'll simulate applying the edit by actually writing to files
try {
// WorkspaceEdit can contain multiple types of edits
if (workspaceEdit._edits) {
for (const edit of workspaceEdit._edits) {
if (edit._type === 1) {
// TextEdit
const uri = edit._uri
const edits = edit._edits
let filePath = uri.path || uri.fsPath
if (filePath.startsWith("file://")) {
filePath = filePath.substring(7)
}
console.log(`Applying text edits to: ${filePath}`)
// Read current content if file exists
let currentContent = ""
try {
currentContent = await fs.promises.readFile(filePath, "utf8")
} catch (e) {
// File doesn't exist, start with empty content
console.log(`File ${filePath} doesn't exist, starting with empty content`)
}
// Apply edits in reverse order (from end to beginning) to maintain positions
const sortedEdits = edits.sort((a, b) => {
const aStart = a.range.start.line * 1000000 + a.range.start.character
const bStart = b.range.start.line * 1000000 + b.range.start.character
return bStart - aStart
})
let lines = currentContent.split("\n")
for (const edit of sortedEdits) {
const startLine = edit.range.start.line
const startChar = edit.range.start.character
const endLine = edit.range.end.line
const endChar = edit.range.end.character
const newText = edit.newText
console.log(`Applying edit: ${startLine}:${startChar} - ${endLine}:${endChar} -> "${newText}"`)
// Handle the edit
if (startLine === endLine) {
// Single line edit
const line = lines[startLine] || ""
lines[startLine] = line.substring(0, startChar) + newText + line.substring(endChar)
} else {
// Multi-line edit
const firstLine = lines[startLine] || ""
const lastLine = lines[endLine] || ""
const newFirstLine = firstLine.substring(0, startChar) + newText + lastLine.substring(endChar)
// Replace the range with the new content
lines.splice(startLine, endLine - startLine + 1, newFirstLine)
}
}
const newContent = lines.join("\n")
// Ensure directory exists
const dir = path.dirname(filePath)
await fs.promises.mkdir(dir, { recursive: true })
// Write the updated content
await fs.promises.writeFile(filePath, newContent, "utf8")
console.log(`Successfully applied edits to: ${filePath}`)
}
}
}
return true
} catch (error) {
console.error("Error applying workspace edit:", error)
return false
}
},
})
// Fix CodeActionKind to have static properties instead of being a class
vscode.CodeActionKind = {
Empty: "",
QuickFix: "quickfix",
Refactor: "refactor",
RefactorExtract: "refactor.extract",
RefactorInline: "refactor.inline",
RefactorRewrite: "refactor.rewrite",
Source: "source",
SourceOrganizeImports: "source.organizeImports",
SourceFixAll: "source.fixAll",
}
vscode.env.openExternal = async (uri) => {
const url = typeof uri === "string" ? uri : (uri.toString?.() ?? "")
console.log("Opening browser:", url)
await open(url)
return true
// Add missing commands implementation
if (!vscode.commands) {
vscode.commands = {}
}
Object.assign(vscode.commands, {
executeCommand: async (command, ...args) => {
console.log(`Called vscode.commands.executeCommand: ${command}`, args)
// Handle the vscode.diff command specifically
if (command === "vscode.diff") {
const [originalUri, modifiedUri, title, options] = args
console.log("Opening diff view:", { originalUri, modifiedUri, title })
// For standalone mode, just open the modified file directly
// since we can't show a proper diff view
const editor = await vscode.window.showTextDocument(modifiedUri, {
preserveFocus: options?.preserveFocus || false,
preview: false,
})
// Ensure the onDidChangeActiveTextEditor event fires with a slight delay
// This is crucial for DiffViewProvider.openDiffEditor() to work properly
setTimeout(() => {
if (vscode.window._activeTextEditorListeners) {
vscode.window._activeTextEditorListeners.forEach((listener) => {
try {
listener(editor)
} catch (error) {
console.error("Error calling onDidChangeActiveTextEditor listener in vscode.diff:", error)
}
})
}
}, 50) // Slightly longer delay to ensure proper event ordering
return editor
}
// For other commands, just return a resolved promise
return Promise.resolve()
},
registerCommand: (command, callback) => {
console.log(`Registered command: ${command}`)
return { dispose: () => {} }
},
getCommands: async () => {
return []
},
})
// Add missing TabInput classes
vscode.TabInputText = class TabInputText {
constructor(uri) {
this.uri = uri
}
}
vscode.TabInputTextDiff = class TabInputTextDiff {
constructor(original, modified) {
this.original = original
this.modified = modified
}
}
// Add missing WorkspaceEdit and related classes
vscode.WorkspaceEdit = class WorkspaceEdit {
constructor() {
this._edits = []
}
replace(uri, range, newText) {
console.log("WorkspaceEdit.replace:", uri, range, newText)
this._edits.push({
_type: 1, // TextEdit
_uri: uri,
_edits: [
{
range: range,
newText: newText,
},
],
})
}
insert(uri, position, newText) {
console.log("WorkspaceEdit.insert:", uri, position, newText)
this.replace(uri, new vscode.Range(position, position), newText)
}
delete(uri, range) {
console.log("WorkspaceEdit.delete:", uri, range)
this.replace(uri, range, "")
}
}
vscode.Range = class Range {
constructor(startLine, startCharacter, endLine, endCharacter) {
if (typeof startLine === "object") {
// Called with Position objects
this.start = startLine
this.end = startCharacter
} else {
// Called with line/character numbers
this.start = new vscode.Position(startLine, startCharacter)
this.end = new vscode.Position(endLine, endCharacter)
}
}
}
vscode.Position = class Position {
constructor(line, character) {
this.line = line
this.character = character
}
}
vscode.Selection = class Selection extends vscode.Range {
constructor(anchorLine, anchorCharacter, activeLine, activeCharacter) {
if (typeof anchorLine === "object") {
// Called with Position objects
super(anchorLine, anchorCharacter)
this.anchor = anchorLine
this.active = anchorCharacter
} else {
// Called with line/character numbers
super(anchorLine, anchorCharacter, activeLine, activeCharacter)
this.anchor = new vscode.Position(anchorLine, anchorCharacter)
this.active = new vscode.Position(activeLine, activeCharacter)
}
}
}
// Add TextEditorRevealType enum
vscode.TextEditorRevealType = {
Default: 0,
InCenter: 1,
InCenterIfOutsideViewport: 2,
AtTop: 3,
}
// Add missing languages API
if (!vscode.languages) {
vscode.languages = {}
}
Object.assign(vscode.languages, {
getDiagnostics: (uri) => {
console.log("Called vscode.languages.getDiagnostics")
// Return empty diagnostics for standalone mode
if (uri) {
return []
} else {
// Return all diagnostics as empty array
return []
}
},
registerCodeActionsProvider: () => ({ dispose: () => {} }),
createDiagnosticCollection: () => ({
set: () => {},
delete: () => {},
clear: () => {},
dispose: () => {},
}),
})
console.log("Finished loading stub impls...")
// Export the terminal manager globally for Cline core to use
global.standaloneTerminalManager = globalTerminalManager
// Override the TerminalManager to use our standalone implementation
if (typeof global !== "undefined") {
// Replace the TerminalManager class with our standalone implementation
global.StandaloneTerminalManagerClass = require("./enhanced-terminal").StandaloneTerminalManager
}
module.exports = vscode
+24 -28
View File
@@ -44,15 +44,23 @@ const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
</h3>
<ul style={ulStyle}>
<li>
<b>Claude 4 Models:</b> Now with support for Anthropic Claude Sonnet 4 and Claude Opus 4 in both Anthropic and
Vertex providers.
<b>Optimized for Claude 4:</b> Cline is now optimized to work with the Claude 4 family of models, resulting in
improved performance, reliability, and new capabilities.
</li>
<li>
<b>New Settings Page:</b> Redesigned settings, now split into tabs for easier navigation and a cleaner
experience.
<b>Gemini CLI Provider:</b> Added a new Gemini CLI provider that allows you to use your local Gemini CLI
authentication to access Gemini models for free.
</li>
<li>
<b>Nebius AI Studio:</b> Added Nebius AI Studio as a new provider. (Thanks @Aktsvigun!)
<b>WebFetch Tool:</b> Gemini 2.5 Pro and Claude 4 models now support the WebFetch tool, allowing Cline to
retrieve and summarize web content directly in conversations.
</li>
<li>
<b>Self Knowledge:</b> When using frontier models, Cline is self-aware about his capabilities and featureset.
</li>
<li>
<b>Improved Diff Editing:</b> Improved diff editing to achieve record lows in diff edit failures for frontier
models
</li>
</ul>
<Accordion isCompact className="pl-0">
@@ -67,6 +75,17 @@ const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
"text-[var(--vscode-foreground)] mb-0.5 -rotate-180 data-[open=true]:-rotate-90 rtl:rotate-0 rtl:data-[open=true]:-rotate-90",
}}>
<ul style={ulStyle}>
<li>
<b>Claude 4 Models:</b> Now with support for Anthropic Claude Sonnet 4 and Claude Opus 4 in both
Anthropic and Vertex providers.
</li>
<li>
<b>New Settings Page:</b> Redesigned settings, now split into tabs for easier navigation and a cleaner
experience.
</li>
<li>
<b>Nebius AI Studio:</b> Added Nebius AI Studio as a new provider. (Thanks @Aktsvigun!)
</li>
<li>
<b>Workflows:</b> Create and manage workflow files that can be injected into conversations via slash
commands, making it easy to automate repetitive tasks.
@@ -79,29 +98,6 @@ const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
<b>Global Endpoint for Vertex AI:</b> Improved availability and reduced rate limiting errors for
Vertex AI users.
</li>
<li>
<b>New User Experience:</b> Special components and guidance for new users to help them get started
with Cline.
</li>
<li>
<b>UI Improvements:</b> Fixed loading states and improved settings organization for a smoother
experience.
</li>
<li>
<b>Task Timeline:</b> See the history of your coding journey with a visual timeline of checkpoints.
</li>
<li>
<b>UX Improvements:</b> Type while Cline works, smarter auto-scrolling, and copy buttons for task
headers and messages.
</li>
<li>
<b>Gemini prompt caching:</b> Gemini and Vertex providers now support prompt caching and price
tracking.
</li>
<li>
<b>Global Cline Rules:</b> Store multiple rules files in Documents/Cline/Rules to share between
projects.
</li>
</ul>
</AccordionItem>
</Accordion>
@@ -74,6 +74,22 @@ const TaskTimeline: React.FC<TaskTimelineProps> = ({ messages, onBlockClick }) =
// Render a timeline block
const TimelineBlock = useCallback(
(index: number) => {
// Show placeholder block when no items exist
if (taskTimelinePropsMessages.length === 0 || index >= taskTimelinePropsMessages.length) {
return (
<div
style={{
width: BLOCK_WIDTH,
height: "100%",
backgroundColor: "#e5e5e5", // Light gray placeholder
flexShrink: 0,
marginRight: BLOCK_GAP,
opacity: 0.5,
}}
/>
)
}
const message = taskTimelinePropsMessages[index]
const originalMessageIndex = messageIndexMap[index]
@@ -112,10 +128,6 @@ const TaskTimeline: React.FC<TaskTimelineProps> = ({ messages, onBlockClick }) =
}
}, [taskTimelinePropsMessages])
if (taskTimelinePropsMessages.length === 0) {
return null
}
return (
<div
ref={containerRef}
@@ -146,7 +158,7 @@ const TaskTimeline: React.FC<TaskTimelineProps> = ({ messages, onBlockClick }) =
height: TIMELINE_HEIGHT,
width: "100%",
}}
totalCount={taskTimelinePropsMessages.length}
totalCount={Math.max(1, taskTimelinePropsMessages.length)}
itemContent={TimelineBlock}
horizontalDirection={true}
increaseViewportBy={12}
@@ -35,7 +35,7 @@ export function AlertDialogContent({ className, children, ...props }: React.HTML
className={`fixed top-[50%] left-[50%] grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] ${className}`}
onClick={(e) => e.stopPropagation()}
{...props}>
<div className="bg-[var(--vscode-editor-background)] rounded-sm gap-3 border border-[var(--vscode-panel-border)] p-4 shadow-lg sm:max-w-md">
<div className="bg-[var(--vscode-editor-background)] rounded-sm gap-3 border border-[var(--vscode-panel-border)] p-6 shadow-lg sm:max-w-lg">
{children}
</div>
</div>
@@ -47,7 +47,7 @@ export function AlertDialogHeader({ className, ...props }: React.HTMLAttributes<
}
export function AlertDialogFooter({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
return <div className={`flex flex-row justify-end gap-2 mt-4 ${className}`} {...props} />
return <div className={`flex flex-row justify-end gap-3 mt-6 ${className}`} {...props} />
}
export function AlertDialogTitle({ className, ...props }: React.HTMLAttributes<HTMLHeadingElement>) {
@@ -76,11 +76,23 @@ export function UnsavedChangesDialog({
onOpenChange,
onConfirm,
onCancel,
onSave,
title = "Unsaved Changes",
description = "You have unsaved changes. Are you sure you want to discard them?",
confirmText = "Discard Changes",
saveText = "Save & Continue",
showSaveOption = false,
}: {
open: boolean
onOpenChange: (open: boolean) => void
onConfirm: () => void
onCancel: () => void
onSave?: () => void
title?: string
description?: string
confirmText?: string
saveText?: string
showSaveOption?: boolean
}) {
return (
<AlertDialog open={open} onOpenChange={onOpenChange}>
@@ -88,15 +100,16 @@ export function UnsavedChangesDialog({
<AlertDialogHeader>
<AlertDialogTitle>
<AlertTriangle className="w-5 h-5 text-[var(--vscode-errorForeground)]" />
Unsaved Changes
{title}
</AlertDialogTitle>
<AlertDialogDescription>
You have unsaved changes. Are you sure you want to discard them?
</AlertDialogDescription>
<AlertDialogDescription>{description}</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel onClick={onCancel}>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={onConfirm}>Discard Changes</AlertDialogAction>
{showSaveOption && onSave && <AlertDialogAction onClick={onSave}>{saveText}</AlertDialogAction>}
<AlertDialogAction onClick={onConfirm} appearance={showSaveOption ? "secondary" : "primary"}>
{confirmText}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
@@ -15,25 +15,36 @@ import { StateServiceClient } from "@/services/grpc-client"
import { PlanActMode, TogglePlanActModeRequest } from "@shared/proto/state"
// Styled component for Act Mode text with more specific styling
const ActModeHighlight: React.FC = () => (
<span
onClick={() => {
StateServiceClient.togglePlanActMode(
TogglePlanActModeRequest.create({
chatSettings: {
mode: PlanActMode.ACT,
},
}),
)
}}
title="Click to toggle to Act Mode"
className="text-[var(--vscode-textLink-foreground)] hover:opacity-90 cursor-pointer inline-flex items-center gap-1">
<div className="p-1 rounded-[12px] bg-[var(--vscode-editor-background)] flex items-center justify-end w-4 border-[1px] border-[var(--vscode-input-border)]">
<div className="rounded-full bg-[var(--vscode-textLink-foreground)] w-2 h-2" />
</div>
Act Mode (A)
</span>
)
const ActModeHighlight: React.FC = () => {
const { chatSettings } = useExtensionState()
return (
<span
onClick={() => {
// Only toggle to Act mode if we're currently in Plan mode
if (chatSettings.mode === "plan") {
StateServiceClient.togglePlanActMode(
TogglePlanActModeRequest.create({
chatSettings: {
mode: PlanActMode.ACT,
preferredLanguage: chatSettings.preferredLanguage,
openAiReasoningEffort: chatSettings.openAIReasoningEffort,
},
}),
)
}
}}
title={chatSettings.mode === "plan" ? "Click to toggle to Act Mode" : "Already in Act Mode"}
className={`text-[var(--vscode-textLink-foreground)] inline-flex items-center gap-1 ${
chatSettings.mode === "plan" ? "hover:opacity-90 cursor-pointer" : "cursor-default opacity-60"
}`}>
<div className="p-1 rounded-[12px] bg-[var(--vscode-editor-background)] flex items-center justify-end w-4 border-[1px] border-[var(--vscode-input-border)]">
<div className="rounded-full bg-[var(--vscode-textLink-foreground)] w-2 h-2" />
</div>
Act Mode (A)
</span>
)
}
interface MarkdownBlockProps {
markdown?: string
@@ -727,17 +727,9 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
disabled={deleteAllDisabled || taskHistory.length === 0}
onClick={() => {
setDeleteAllDisabled(true)
const confirmDelete = window.confirm("Are you sure you want to delete all task history?")
if (confirmDelete) {
const preserveFavorites = window.confirm(
"Would you like to preserve favorited tasks?\n\nClick 'OK' to preserve favorites, or 'Cancel' to delete everything.",
)
TaskServiceClient.deleteAllTaskHistory(BooleanRequest.create({ value: preserveFavorites }))
.catch((error) => console.error("Error deleting task history:", error))
.finally(() => setDeleteAllDisabled(false))
} else {
setDeleteAllDisabled(false)
}
TaskServiceClient.deleteAllTaskHistory(BooleanRequest.create({}))
.catch((error) => console.error("Error deleting task history:", error))
.finally(() => setDeleteAllDisabled(false))
}}>
Delete All History{totalTasksSize !== null ? ` (${formatSize(totalTasksSize)})` : ""}
</DangerButton>
@@ -113,19 +113,21 @@ const McpConfigurationView = ({ onDone, initialTab }: McpViewProps) => {
)
}
const StyledTabButton = styled.button<{ isActive: boolean }>`
const StyledTabButton = styled.button<{ isActive: boolean; disabled?: boolean }>`
background: none;
border: none;
border-bottom: 2px solid ${(props) => (props.isActive ? "var(--vscode-foreground)" : "transparent")};
color: ${(props) => (props.isActive ? "var(--vscode-foreground)" : "var(--vscode-descriptionForeground)")};
padding: 8px 16px;
cursor: pointer;
cursor: ${(props) => (props.disabled ? "not-allowed" : "pointer")};
font-size: 13px;
margin-bottom: -1px;
font-family: inherit;
opacity: ${(props) => (props.disabled ? 0.6 : 1)};
pointer-events: ${(props) => (props.disabled ? "none" : "auto")};
&:hover {
color: var(--vscode-foreground);
color: ${(props) => (props.disabled ? "var(--vscode-descriptionForeground)" : "var(--vscode-foreground)")};
}
`
@@ -133,12 +135,16 @@ export const TabButton = ({
children,
isActive,
onClick,
disabled,
style,
}: {
children: React.ReactNode
isActive: boolean
onClick: () => void
disabled?: boolean
style?: React.CSSProperties
}) => (
<StyledTabButton isActive={isActive} onClick={onClick}>
<StyledTabButton isActive={isActive} onClick={onClick} disabled={disabled} style={style}>
{children}
</StyledTabButton>
)
File diff suppressed because it is too large Load Diff
@@ -10,7 +10,7 @@ import { useRemark } from "react-remark"
import { useMount } from "react-use"
import styled from "styled-components"
import { highlight } from "../history/HistoryView"
import { ModelInfoView } from "./ApiOptions"
import { ModelInfoView } from "./common/ModelInfoView"
import { normalizeApiConfiguration } from "./utils/providerUtils"
import FeaturedModelCard from "./FeaturedModelCard"
import ThinkingBudgetSlider from "./ThinkingBudgetSlider"
@@ -43,13 +43,13 @@ export interface OpenRouterModelPickerProps {
// Featured models for Cline provider
const featuredModels = [
{
id: "anthropic/claude-sonnet-4",
description: "Recommended for agentic coding in Cline",
id: "google/gemini-2.5-pro",
description: "Large 1M context window, great value",
label: "Best",
},
{
id: "google/gemini-2.5-pro",
description: "Large 1M context window, great value",
id: "anthropic/claude-sonnet-4",
description: "Recommended for agentic coding in Cline",
label: "Trending",
},
{
@@ -66,7 +66,6 @@ const OpenRouterModelPicker: React.FC<OpenRouterModelPickerProps> = ({ isPopup }
const [selectedIndex, setSelectedIndex] = useState(-1)
const dropdownRef = useRef<HTMLDivElement>(null)
const itemRefs = useRef<(HTMLDivElement | null)[]>([])
const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false)
const dropdownListRef = useRef<HTMLDivElement>(null)
const handleModelChange = (newModelId: string) => {
@@ -311,13 +310,7 @@ const OpenRouterModelPicker: React.FC<OpenRouterModelPickerProps> = ({ isPopup }
<ThinkingBudgetSlider apiConfiguration={apiConfiguration} setApiConfiguration={setApiConfiguration} />
)}
<ModelInfoView
selectedModelId={selectedModelId}
modelInfo={selectedModelInfo}
isDescriptionExpanded={isDescriptionExpanded}
setIsDescriptionExpanded={setIsDescriptionExpanded}
isPopup={isPopup}
/>
<ModelInfoView selectedModelId={selectedModelId} modelInfo={selectedModelInfo} isPopup={isPopup} />
</>
) : (
<p
@@ -334,8 +327,8 @@ const OpenRouterModelPicker: React.FC<OpenRouterModelPickerProps> = ({ isPopup }
If you're unsure which model to choose, Cline works best with{" "}
<VSCodeLink
style={{ display: "inline", fontSize: "inherit" }}
onClick={() => handleModelChange("anthropic/claude-sonnet-4")}>
anthropic/claude-sonnet-4.
onClick={() => handleModelChange("google/gemini-2.5-pro")}>
google/gemini-2.5-pro.
</VSCodeLink>
You can also try searching "free" for no-cost options currently available.
</>
@@ -10,7 +10,7 @@ import { useExtensionState } from "../../context/ExtensionStateContext"
import { ModelsServiceClient } from "../../services/grpc-client"
import { CODE_BLOCK_BG_COLOR } from "../common/CodeBlock"
import { highlight } from "../history/HistoryView"
import { ModelInfoView } from "./ApiOptions"
import { ModelInfoView } from "./common/ModelInfoView"
import { normalizeApiConfiguration } from "./utils/providerUtils"
import ThinkingBudgetSlider from "./ThinkingBudgetSlider"
@@ -25,7 +25,6 @@ const RequestyModelPicker: React.FC<RequestyModelPickerProps> = ({ isPopup }) =>
const [selectedIndex, setSelectedIndex] = useState(-1)
const dropdownRef = useRef<HTMLDivElement>(null)
const itemRefs = useRef<(HTMLDivElement | null)[]>([])
const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false)
const dropdownListRef = useRef<HTMLDivElement>(null)
const handleModelChange = (newModelId: string) => {
@@ -230,13 +229,7 @@ const RequestyModelPicker: React.FC<RequestyModelPickerProps> = ({ isPopup }) =>
{showBudgetSlider && (
<ThinkingBudgetSlider apiConfiguration={apiConfiguration} setApiConfiguration={setApiConfiguration} />
)}
<ModelInfoView
selectedModelId={selectedModelId}
modelInfo={selectedModelInfo}
isDescriptionExpanded={isDescriptionExpanded}
setIsDescriptionExpanded={setIsDescriptionExpanded}
isPopup={isPopup}
/>
<ModelInfoView selectedModelId={selectedModelId} modelInfo={selectedModelInfo} isPopup={isPopup} />
</>
) : (
<p
@@ -21,6 +21,7 @@ import PreferredLanguageSetting from "./PreferredLanguageSetting" // Added impor
import Section from "./Section"
import SectionHeader from "./SectionHeader"
import TerminalSettingsSection from "./TerminalSettingsSection"
import ApiConfigurationSection from "./sections/ApiConfigurationSection"
import { convertApiConfigurationToProtoApiConfiguration } from "@shared/proto-conversions/state/settings-conversion"
import { convertChatSettingsToProtoChatSettings } from "@shared/proto-conversions/state/chat-settings-conversion"
const IS_DEV = process.env.IS_DEV
@@ -112,6 +113,10 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
const [isUnsavedChangesDialogOpen, setIsUnsavedChangesDialogOpen] = useState(false)
// Store the action to perform after confirmation
const pendingAction = useRef<() => void>()
// Track if we're currently switching modes
const [isSwitchingMode, setIsSwitchingMode] = useState(false)
// Track pending mode switch when there are unsaved changes
const [pendingModeSwitch, setPendingModeSwitch] = useState<"plan" | "act" | null>(null)
const {
apiConfiguration,
version,
@@ -260,8 +265,32 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
setModelIdErrorMessage(undefined)
}, [apiConfiguration])
// Track the previous mode to detect mode switches
const previousMode = useRef(chatSettings.mode)
// Update original state when mode changes
useEffect(() => {
// Detect if the mode has changed
if (previousMode.current !== chatSettings.mode) {
// Mode has changed, update the original state immediately to reflect the new apiConfiguration and chatSettings
originalState.current = {
...originalState.current,
apiConfiguration: apiConfiguration,
chatSettings: chatSettings,
}
// Update the previous mode reference
previousMode.current = chatSettings.mode
}
}, [chatSettings.mode, apiConfiguration, chatSettings])
// Check for unsaved changes by comparing current state with original state
useEffect(() => {
// Don't check for changes while switching modes
if (isSwitchingMode) {
return
}
const hasChanges =
JSON.stringify(apiConfiguration) !== JSON.stringify(originalState.current.apiConfiguration) ||
telemetrySetting !== originalState.current.telemetrySetting ||
@@ -271,7 +300,6 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
mcpRichDisplayEnabled !== originalState.current.mcpRichDisplayEnabled ||
JSON.stringify(chatSettings) !== JSON.stringify(originalState.current.chatSettings) ||
mcpResponsesCollapsed !== originalState.current.mcpResponsesCollapsed ||
JSON.stringify(chatSettings) !== JSON.stringify(originalState.current.chatSettings) ||
shellIntegrationTimeout !== originalState.current.shellIntegrationTimeout ||
terminalOutputLineLimit !== originalState.current.terminalOutputLineLimit ||
terminalReuseEnabled !== originalState.current.terminalReuseEnabled ||
@@ -292,7 +320,7 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
terminalReuseEnabled,
terminalOutputLineLimit,
defaultTerminalProfile,
localBrowserSettings,
isSwitchingMode,
])
// Handle cancel button click
@@ -368,17 +396,134 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
])
// Handle confirmation dialog actions
const handleConfirmDiscard = useCallback(() => {
const handleConfirmDiscard = useCallback(async () => {
setIsUnsavedChangesDialogOpen(false)
if (pendingAction.current) {
// Check if this is for a mode switch
if (pendingModeSwitch) {
// Reset all state to original values (discard changes)
setTelemetrySetting(originalState.current.telemetrySetting)
setPlanActSeparateModelsSetting(originalState.current.planActSeparateModelsSetting)
setChatSettings(originalState.current.chatSettings)
if (typeof setApiConfiguration === "function") {
setApiConfiguration(originalState.current.apiConfiguration ?? {})
}
if (typeof setEnableCheckpointsSetting === "function") {
setEnableCheckpointsSetting(
typeof originalState.current.enableCheckpointsSetting === "boolean"
? originalState.current.enableCheckpointsSetting
: false,
)
}
if (typeof setMcpMarketplaceEnabled === "function") {
setMcpMarketplaceEnabled(
typeof originalState.current.mcpMarketplaceEnabled === "boolean"
? originalState.current.mcpMarketplaceEnabled
: false,
)
}
if (typeof setMcpRichDisplayEnabled === "function") {
setMcpRichDisplayEnabled(
typeof originalState.current.mcpRichDisplayEnabled === "boolean"
? originalState.current.mcpRichDisplayEnabled
: true,
)
}
// Reset terminal settings
if (typeof setShellIntegrationTimeout === "function") {
setShellIntegrationTimeout(originalState.current.shellIntegrationTimeout)
}
if (typeof setTerminalOutputLineLimit === "function") {
setTerminalOutputLineLimit(originalState.current.terminalOutputLineLimit)
}
if (typeof setTerminalReuseEnabled === "function") {
setTerminalReuseEnabled(originalState.current.terminalReuseEnabled ?? true)
}
if (typeof setDefaultTerminalProfile === "function") {
setDefaultTerminalProfile(originalState.current.defaultTerminalProfile ?? "default")
}
if (typeof setMcpResponsesCollapsed === "function") {
setMcpResponsesCollapsed(originalState.current.mcpResponsesCollapsed ?? false)
}
// Now perform the mode switch
const targetMode = pendingModeSwitch
setPendingModeSwitch(null)
setIsSwitchingMode(true)
try {
await StateServiceClient.togglePlanActMode(
TogglePlanActModeRequest.create({
chatSettings: {
mode: targetMode === "plan" ? PlanActMode.PLAN : PlanActMode.ACT,
preferredLanguage: chatSettings.preferredLanguage,
openAiReasoningEffort: chatSettings.openAIReasoningEffort,
},
}),
)
} catch (error) {
console.error("Failed to toggle Plan/Act mode:", error)
} finally {
setIsSwitchingMode(false)
}
} else if (pendingAction.current) {
// Regular cancel button flow
pendingAction.current()
pendingAction.current = undefined
}
}, [])
}, [
pendingModeSwitch,
setTelemetrySetting,
setPlanActSeparateModelsSetting,
setChatSettings,
setApiConfiguration,
setEnableCheckpointsSetting,
setMcpMarketplaceEnabled,
setMcpRichDisplayEnabled,
setShellIntegrationTimeout,
setTerminalOutputLineLimit,
setTerminalReuseEnabled,
setDefaultTerminalProfile,
setMcpResponsesCollapsed,
chatSettings.preferredLanguage,
chatSettings.openAIReasoningEffort,
])
// Handle save and switch for mode changes
const handleSaveAndSwitch = useCallback(async () => {
setIsUnsavedChangesDialogOpen(false)
if (pendingModeSwitch) {
// Save the current settings first
await handleSubmit(true)
// Now perform the mode switch
const targetMode = pendingModeSwitch
setPendingModeSwitch(null)
setIsSwitchingMode(true)
try {
await StateServiceClient.togglePlanActMode(
TogglePlanActModeRequest.create({
chatSettings: {
mode: targetMode === "plan" ? PlanActMode.PLAN : PlanActMode.ACT,
preferredLanguage: chatSettings.preferredLanguage,
openAiReasoningEffort: chatSettings.openAIReasoningEffort,
},
}),
)
} catch (error) {
console.error("Failed to toggle Plan/Act mode:", error)
} finally {
setIsSwitchingMode(false)
}
}
}, [pendingModeSwitch, handleSubmit, chatSettings.preferredLanguage, chatSettings.openAIReasoningEffort])
const handleCancelDiscard = useCallback(() => {
setIsUnsavedChangesDialogOpen(false)
pendingAction.current = undefined
setPendingModeSwitch(null)
}, [])
// validate as soon as the component is mounted
@@ -446,14 +591,25 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
}
const handlePlanActModeChange = async (tab: "plan" | "act") => {
if (tab === chatSettings.mode) {
// Prevent switching if already in that mode or if currently switching
if (tab === chatSettings.mode || isSwitchingMode) {
return
}
// Update settings first to ensure any changes to the current tab are saved
await handleSubmit(true)
// Check if there are unsaved changes
if (hasUnsavedChanges) {
// Store the pending mode switch
setPendingModeSwitch(tab)
// Show the unsaved changes dialog
setIsUnsavedChangesDialogOpen(true)
return
}
// No unsaved changes, proceed with the switch
setIsSwitchingMode(true)
try {
// Perform the mode switch
await StateServiceClient.togglePlanActMode(
TogglePlanActModeRequest.create({
chatSettings: {
@@ -465,6 +621,9 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
)
} catch (error) {
console.error("Failed to toggle Plan/Act mode:", error)
} finally {
// Always re-enable mode switching, even on error
setIsSwitchingMode(false)
}
}
@@ -603,62 +762,16 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
<TabContent className="flex-1 overflow-auto">
{/* API Configuration Tab */}
{activeTab === "api-config" && (
<div>
{renderSectionHeader("api-config")}
<Section>
{/* Tabs container */}
{planActSeparateModelsSetting ? (
<div className="rounded-md mb-5 bg-[var(--vscode-panel-background)]">
<div className="flex gap-[1px] mb-[10px] -mt-2 border-0 border-b border-solid border-[var(--vscode-panel-border)]">
<TabButton
isActive={chatSettings.mode === "plan"}
onClick={() => handlePlanActModeChange("plan")}>
Plan Mode
</TabButton>
<TabButton
isActive={chatSettings.mode === "act"}
onClick={() => handlePlanActModeChange("act")}>
Act Mode
</TabButton>
</div>
{/* Content container */}
<div className="-mb-3">
<ApiOptions
key={chatSettings.mode}
showModelOptions={true}
apiErrorMessage={apiErrorMessage}
modelIdErrorMessage={modelIdErrorMessage}
/>
</div>
</div>
) : (
<ApiOptions
key={"single"}
showModelOptions={true}
apiErrorMessage={apiErrorMessage}
modelIdErrorMessage={modelIdErrorMessage}
/>
)}
<div className="mb-[5px]">
<VSCodeCheckbox
className="mb-[5px]"
checked={planActSeparateModelsSetting}
onChange={(e: any) => {
const checked = e.target.checked === true
setPlanActSeparateModelsSetting(checked)
}}>
Use different models for Plan and Act modes
</VSCodeCheckbox>
<p className="text-xs mt-[5px] text-[var(--vscode-descriptionForeground)]">
Switching between Plan and Act mode will persist the API and model used in the
previous mode. This may be helpful e.g. when using a strong reasoning model to
architect a plan for a cheaper coding model to act on.
</p>
</div>
</Section>
</div>
<ApiConfigurationSection
planActSeparateModelsSetting={planActSeparateModelsSetting}
chatSettings={chatSettings}
isSwitchingMode={isSwitchingMode}
apiErrorMessage={apiErrorMessage}
modelIdErrorMessage={modelIdErrorMessage}
handlePlanActModeChange={handlePlanActModeChange}
setPlanActSeparateModelsSetting={setPlanActSeparateModelsSetting}
renderSectionHeader={renderSectionHeader}
/>
)}
{/* General Settings Tab */}
@@ -787,6 +900,16 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
onOpenChange={setIsUnsavedChangesDialogOpen}
onConfirm={handleConfirmDiscard}
onCancel={handleCancelDiscard}
onSave={pendingModeSwitch ? handleSaveAndSwitch : undefined}
title={pendingModeSwitch ? "Save Changes?" : "Unsaved Changes"}
description={
pendingModeSwitch
? `Do you want to save your changes to ${chatSettings.mode === "plan" ? "Plan" : "Act"} mode before switching to ${pendingModeSwitch === "plan" ? "Plan" : "Act"} mode?`
: "You have unsaved changes. Are you sure you want to discard them?"
}
confirmText={pendingModeSwitch ? "Switch Without Saving" : "Discard Changes"}
saveText="Save & Switch"
showSaveOption={!!pendingModeSwitch}
/>
</Tab>
)
@@ -196,9 +196,7 @@ export const ModelInfoView = ({ selectedModelId, modelInfo, isPopup }: ModelInfo
),
outputPriceElement, // Add the generated output price block
isGeminiProvider && (
<span key="geminiInfo" style={{ fontStyle: "italic" }}>
* Free up to {selectedModelId && selectedModelId.includes("flash") ? "15" : "2"} requests per minute. After that,
billing depends on prompt size.{" "}
<span key="geminiPricing" style={{ fontStyle: "italic" }}>
<VSCodeLink href="https://ai.google.dev/pricing" style={{ display: "inline", fontSize: "inherit" }}>
For more info, see pricing details.
</VSCodeLink>
@@ -0,0 +1,311 @@
import { ApiConfiguration, bedrockDefaultModelId, bedrockModels } from "@shared/api"
import {
VSCodeCheckbox,
VSCodeDropdown,
VSCodeOption,
VSCodeRadio,
VSCodeRadioGroup,
VSCodeTextField,
} from "@vscode/webview-ui-toolkit/react"
import { useState } from "react"
import { ModelInfoView } from "../common/ModelInfoView"
import { DropdownContainer } from "../common/ModelSelector"
import ThinkingBudgetSlider from "../ThinkingBudgetSlider"
import { normalizeApiConfiguration } from "../utils/providerUtils"
// Z-index constants for proper dropdown layering
const DROPDOWN_Z_INDEX = 1000
interface BedrockProviderProps {
apiConfiguration: ApiConfiguration
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
showModelOptions: boolean
isPopup?: boolean
setApiConfiguration: (config: ApiConfiguration) => void
}
export const BedrockProvider = ({
apiConfiguration,
handleInputChange,
showModelOptions,
isPopup,
setApiConfiguration,
}: BedrockProviderProps) => {
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration)
const [awsEndpointSelected, setAwsEndpointSelected] = useState(!!apiConfiguration?.awsBedrockEndpoint)
return (
<div
style={{
display: "flex",
flexDirection: "column",
gap: 5,
}}>
<VSCodeRadioGroup
value={apiConfiguration?.awsUseProfile ? "profile" : "credentials"}
onChange={(e) => {
const value = (e.target as HTMLInputElement)?.value
const useProfile = value === "profile"
setApiConfiguration({
...apiConfiguration,
awsUseProfile: useProfile,
})
}}>
<VSCodeRadio value="credentials">AWS Credentials</VSCodeRadio>
<VSCodeRadio value="profile">AWS Profile</VSCodeRadio>
</VSCodeRadioGroup>
{apiConfiguration?.awsUseProfile ? (
<VSCodeTextField
value={apiConfiguration?.awsProfile || ""}
style={{ width: "100%" }}
onInput={handleInputChange("awsProfile")}
placeholder="Enter profile name (default if empty)">
<span style={{ fontWeight: 500 }}>AWS Profile Name</span>
</VSCodeTextField>
) : (
<>
<VSCodeTextField
value={apiConfiguration?.awsAccessKey || ""}
style={{ width: "100%" }}
type="password"
onInput={handleInputChange("awsAccessKey")}
placeholder="Enter Access Key...">
<span style={{ fontWeight: 500 }}>AWS Access Key</span>
</VSCodeTextField>
<VSCodeTextField
value={apiConfiguration?.awsSecretKey || ""}
style={{ width: "100%" }}
type="password"
onInput={handleInputChange("awsSecretKey")}
placeholder="Enter Secret Key...">
<span style={{ fontWeight: 500 }}>AWS Secret Key</span>
</VSCodeTextField>
<VSCodeTextField
value={apiConfiguration?.awsSessionToken || ""}
style={{ width: "100%" }}
type="password"
onInput={handleInputChange("awsSessionToken")}
placeholder="Enter Session Token...">
<span style={{ fontWeight: 500 }}>AWS Session Token</span>
</VSCodeTextField>
</>
)}
<DropdownContainer zIndex={DROPDOWN_Z_INDEX - 1} className="dropdown-container">
<label htmlFor="aws-region-dropdown">
<span style={{ fontWeight: 500 }}>AWS Region</span>
</label>
<VSCodeDropdown
id="aws-region-dropdown"
value={apiConfiguration?.awsRegion || ""}
style={{ width: "100%" }}
onChange={handleInputChange("awsRegion")}>
<VSCodeOption value="">Select a region...</VSCodeOption>
{/* The user will have to choose a region that supports the model they use, but this shouldn't be a problem since they'd have to request access for it in that region in the first place. */}
<VSCodeOption value="us-east-1">us-east-1</VSCodeOption>
<VSCodeOption value="us-east-2">us-east-2</VSCodeOption>
{/* <VSCodeOption value="us-west-1">us-west-1</VSCodeOption> */}
<VSCodeOption value="us-west-2">us-west-2</VSCodeOption>
{/* <VSCodeOption value="af-south-1">af-south-1</VSCodeOption> */}
{/* <VSCodeOption value="ap-east-1">ap-east-1</VSCodeOption> */}
<VSCodeOption value="ap-south-1">ap-south-1</VSCodeOption>
<VSCodeOption value="ap-northeast-1">ap-northeast-1</VSCodeOption>
<VSCodeOption value="ap-northeast-2">ap-northeast-2</VSCodeOption>
<VSCodeOption value="ap-northeast-3">ap-northeast-3</VSCodeOption>
<VSCodeOption value="ap-southeast-1">ap-southeast-1</VSCodeOption>
<VSCodeOption value="ap-southeast-2">ap-southeast-2</VSCodeOption>
<VSCodeOption value="ca-central-1">ca-central-1</VSCodeOption>
<VSCodeOption value="eu-central-1">eu-central-1</VSCodeOption>
<VSCodeOption value="eu-central-2">eu-central-2</VSCodeOption>
<VSCodeOption value="eu-west-1">eu-west-1</VSCodeOption>
<VSCodeOption value="eu-west-2">eu-west-2</VSCodeOption>
<VSCodeOption value="eu-west-3">eu-west-3</VSCodeOption>
<VSCodeOption value="eu-north-1">eu-north-1</VSCodeOption>
<VSCodeOption value="eu-south-1">eu-south-1</VSCodeOption>
<VSCodeOption value="eu-south-2">eu-south-2</VSCodeOption>
{/* <VSCodeOption value="me-south-1">me-south-1</VSCodeOption> */}
<VSCodeOption value="sa-east-1">sa-east-1</VSCodeOption>
<VSCodeOption value="us-gov-east-1">us-gov-east-1</VSCodeOption>
<VSCodeOption value="us-gov-west-1">us-gov-west-1</VSCodeOption>
{/* <VSCodeOption value="us-gov-east-1">us-gov-east-1</VSCodeOption> */}
</VSCodeDropdown>
</DropdownContainer>
<div style={{ display: "flex", flexDirection: "column" }}>
<VSCodeCheckbox
checked={awsEndpointSelected}
onChange={(e: any) => {
const isChecked = e.target.checked === true
setAwsEndpointSelected(isChecked)
if (!isChecked) {
setApiConfiguration({
...apiConfiguration,
awsBedrockEndpoint: "",
})
}
}}>
Use custom VPC endpoint
</VSCodeCheckbox>
{awsEndpointSelected && (
<VSCodeTextField
value={apiConfiguration?.awsBedrockEndpoint || ""}
style={{ width: "100%", marginTop: 3, marginBottom: 5 }}
type="url"
onInput={handleInputChange("awsBedrockEndpoint")}
placeholder="Enter VPC Endpoint URL (optional)"
/>
)}
<VSCodeCheckbox
checked={apiConfiguration?.awsUseCrossRegionInference || false}
onChange={(e: any) => {
const isChecked = e.target.checked === true
setApiConfiguration({
...apiConfiguration,
awsUseCrossRegionInference: isChecked,
})
}}>
Use cross-region inference
</VSCodeCheckbox>
{selectedModelInfo.supportsPromptCache && (
<>
<VSCodeCheckbox
checked={apiConfiguration?.awsBedrockUsePromptCache || false}
onChange={(e: any) => {
const isChecked = e.target.checked === true
setApiConfiguration({
...apiConfiguration,
awsBedrockUsePromptCache: isChecked,
})
}}>
Use prompt caching
</VSCodeCheckbox>
</>
)}
</div>
<p
style={{
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}}>
{apiConfiguration?.awsUseProfile ? (
<>
Using AWS Profile credentials from ~/.aws/credentials. Leave profile name empty to use the default
profile. These credentials are only used locally to make API requests from this extension.
</>
) : (
<>
Authenticate by either providing the keys above or use the default AWS credential providers, i.e.
~/.aws/credentials or environment variables. These credentials are only used locally to make API requests
from this extension.
</>
)}
</p>
{showModelOptions && (
<>
<label htmlFor="bedrock-model-dropdown">
<span style={{ fontWeight: 500 }}>Model</span>
</label>
<DropdownContainer zIndex={DROPDOWN_Z_INDEX - 2} className="dropdown-container">
<VSCodeDropdown
id="bedrock-model-dropdown"
value={apiConfiguration?.awsBedrockCustomSelected ? "custom" : selectedModelId}
onChange={(e: any) => {
const isCustom = e.target.value === "custom"
setApiConfiguration({
...apiConfiguration,
apiModelId: isCustom ? "" : e.target.value,
awsBedrockCustomSelected: isCustom,
awsBedrockCustomModelBaseId: bedrockDefaultModelId,
})
}}
style={{ width: "100%" }}>
<VSCodeOption value="">Select a model...</VSCodeOption>
{Object.keys(bedrockModels).map((modelId) => (
<VSCodeOption
key={modelId}
value={modelId}
style={{
whiteSpace: "normal",
wordWrap: "break-word",
maxWidth: "100%",
}}>
{modelId}
</VSCodeOption>
))}
<VSCodeOption value="custom">Custom</VSCodeOption>
</VSCodeDropdown>
</DropdownContainer>
{apiConfiguration?.awsBedrockCustomSelected && (
<div>
<p
style={{
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}}>
Select "Custom" when using the Application Inference Profile in Bedrock. Enter the Application
Inference Profile ARN in the Model ID field.
</p>
<label htmlFor="bedrock-model-input">
<span style={{ fontWeight: 500 }}>Model ID</span>
</label>
<VSCodeTextField
id="bedrock-model-input"
value={apiConfiguration?.apiModelId || ""}
style={{ width: "100%", marginTop: 3 }}
onInput={handleInputChange("apiModelId")}
placeholder="Enter custom model ID..."
/>
<label htmlFor="bedrock-base-model-dropdown">
<span style={{ fontWeight: 500 }}>Base Inference Model</span>
</label>
<DropdownContainer zIndex={DROPDOWN_Z_INDEX - 3} className="dropdown-container">
<VSCodeDropdown
id="bedrock-base-model-dropdown"
value={apiConfiguration?.awsBedrockCustomModelBaseId || bedrockDefaultModelId}
onChange={handleInputChange("awsBedrockCustomModelBaseId")}
style={{ width: "100%" }}>
<VSCodeOption value="">Select a model...</VSCodeOption>
{Object.keys(bedrockModels).map((modelId) => (
<VSCodeOption
key={modelId}
value={modelId}
style={{
whiteSpace: "normal",
wordWrap: "break-word",
maxWidth: "100%",
}}>
{modelId}
</VSCodeOption>
))}
</VSCodeDropdown>
</DropdownContainer>
</div>
)}
{(selectedModelId === "anthropic.claude-3-7-sonnet-20250219-v1:0" ||
selectedModelId === "anthropic.claude-sonnet-4-20250514-v1:0" ||
selectedModelId === "anthropic.claude-opus-4-20250514-v1:0" ||
(apiConfiguration?.awsBedrockCustomSelected &&
apiConfiguration?.awsBedrockCustomModelBaseId === "anthropic.claude-3-7-sonnet-20250219-v1:0") ||
(apiConfiguration?.awsBedrockCustomSelected &&
apiConfiguration?.awsBedrockCustomModelBaseId === "anthropic.claude-sonnet-4-20250514-v1:0") ||
(apiConfiguration?.awsBedrockCustomSelected &&
apiConfiguration?.awsBedrockCustomModelBaseId === "anthropic.claude-opus-4-20250514-v1:0")) && (
<ThinkingBudgetSlider apiConfiguration={apiConfiguration} setApiConfiguration={setApiConfiguration} />
)}
<ModelInfoView selectedModelId={selectedModelId} modelInfo={selectedModelInfo} isPopup={isPopup} />
</>
)}
</div>
)
}
@@ -0,0 +1,47 @@
import { ApiConfiguration, cerebrasModels } from "@shared/api"
import { ApiKeyField } from "../common/ApiKeyField"
import { ModelSelector } from "../common/ModelSelector"
import { ModelInfoView } from "../common/ModelInfoView"
import { normalizeApiConfiguration } from "../utils/providerUtils"
/**
* Props for the CerebrasProvider component
*/
interface CerebrasProviderProps {
apiConfiguration: ApiConfiguration
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
showModelOptions: boolean
isPopup?: boolean
}
/**
* The Cerebras provider configuration component
*/
export const CerebrasProvider = ({ apiConfiguration, handleInputChange, showModelOptions, isPopup }: CerebrasProviderProps) => {
// Get the normalized configuration
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration)
return (
<div>
<ApiKeyField
value={apiConfiguration?.cerebrasApiKey || ""}
onChange={handleInputChange("cerebrasApiKey")}
providerName="Cerebras"
signupUrl="https://cloud.cerebras.ai/"
/>
{showModelOptions && (
<>
<ModelSelector
models={cerebrasModels}
selectedModelId={selectedModelId}
onChange={handleInputChange("apiModelId")}
label="Model"
/>
<ModelInfoView selectedModelId={selectedModelId} modelInfo={selectedModelInfo} isPopup={isPopup} />
</>
)}
</div>
)
}
@@ -0,0 +1,63 @@
import { ApiConfiguration, claudeCodeModels } from "@shared/api"
import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import { ModelSelector } from "../common/ModelSelector"
import { ModelInfoView } from "../common/ModelInfoView"
import { normalizeApiConfiguration } from "../utils/providerUtils"
/**
* Props for the ClaudeCodeProvider component
*/
interface ClaudeCodeProviderProps {
apiConfiguration: ApiConfiguration
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
showModelOptions: boolean
isPopup?: boolean
}
/**
* The Claude Code provider configuration component
*/
export const ClaudeCodeProvider = ({
apiConfiguration,
handleInputChange,
showModelOptions,
isPopup,
}: ClaudeCodeProviderProps) => {
// Get the normalized configuration
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration)
return (
<div>
<VSCodeTextField
value={apiConfiguration?.claudeCodePath || ""}
style={{ width: "100%", marginTop: 3 }}
type="text"
onInput={handleInputChange("claudeCodePath")}
placeholder="Default: claude">
<span style={{ fontWeight: 500 }}>Claude Code CLI Path</span>
</VSCodeTextField>
<p
style={{
fontSize: "12px",
marginTop: 3,
color: "var(--vscode-descriptionForeground)",
}}>
Path to the Claude Code CLI.
</p>
{showModelOptions && (
<>
<ModelSelector
models={claudeCodeModels}
selectedModelId={selectedModelId}
onChange={handleInputChange("apiModelId")}
label="Model"
/>
<ModelInfoView selectedModelId={selectedModelId} modelInfo={selectedModelInfo} isPopup={isPopup} />
</>
)}
</div>
)
}
@@ -0,0 +1,86 @@
import { ApiConfiguration } from "@shared/api"
import { VSCodeCheckbox, VSCodeDropdown, VSCodeOption } from "@vscode/webview-ui-toolkit/react"
import { useState } from "react"
import { ClineAccountInfoCard } from "../ClineAccountInfoCard"
import OpenRouterModelPicker, { OPENROUTER_MODEL_PICKER_Z_INDEX } from "../OpenRouterModelPicker"
import { DropdownContainer } from "../common/ModelSelector"
/**
* Props for the ClineProvider component
*/
interface ClineProviderProps {
apiConfiguration: ApiConfiguration
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
showModelOptions: boolean
isPopup?: boolean
}
/**
* The Cline provider configuration component
*/
export const ClineProvider = ({ apiConfiguration, handleInputChange, showModelOptions, isPopup }: ClineProviderProps) => {
const [providerSortingSelected, setProviderSortingSelected] = useState(!!apiConfiguration?.openRouterProviderSorting)
// Create a wrapper for handling field changes more directly
const handleFieldChange = (field: keyof ApiConfiguration) => (value: any) => {
handleInputChange(field)({ target: { value } })
}
return (
<div>
{/* Cline Account Info Card */}
<div style={{ marginBottom: 14, marginTop: 4 }}>
<ClineAccountInfoCard />
</div>
{showModelOptions && (
<>
{/* Provider Sorting Options */}
<VSCodeCheckbox
style={{ marginTop: -10 }}
checked={providerSortingSelected}
onChange={(e: any) => {
const isChecked = e.target.checked === true
setProviderSortingSelected(isChecked)
if (!isChecked) {
handleFieldChange("openRouterProviderSorting")("")
}
}}>
Sort underlying provider routing
</VSCodeCheckbox>
{providerSortingSelected && (
<div style={{ marginBottom: -6 }}>
<DropdownContainer className="dropdown-container" zIndex={OPENROUTER_MODEL_PICKER_Z_INDEX + 1}>
<VSCodeDropdown
style={{ width: "100%", marginTop: 3 }}
value={apiConfiguration?.openRouterProviderSorting}
onChange={(e: any) => {
handleFieldChange("openRouterProviderSorting")(e.target.value)
}}>
<VSCodeOption value="">Default</VSCodeOption>
<VSCodeOption value="price">Price</VSCodeOption>
<VSCodeOption value="throughput">Throughput</VSCodeOption>
<VSCodeOption value="latency">Latency</VSCodeOption>
</VSCodeDropdown>
</DropdownContainer>
<p style={{ fontSize: "12px", marginTop: 3, color: "var(--vscode-descriptionForeground)" }}>
{!apiConfiguration?.openRouterProviderSorting &&
"Default behavior is to load balance requests across providers (like AWS, Google Vertex, Anthropic), prioritizing price while considering provider uptime"}
{apiConfiguration?.openRouterProviderSorting === "price" &&
"Sort providers by price, prioritizing the lowest cost provider"}
{apiConfiguration?.openRouterProviderSorting === "throughput" &&
"Sort providers by throughput, prioritizing the provider with the highest throughput (may increase cost)"}
{apiConfiguration?.openRouterProviderSorting === "latency" &&
"Sort providers by response time, prioritizing the provider with the lowest latency"}
</p>
</div>
)}
{/* OpenRouter Model Picker */}
<OpenRouterModelPicker isPopup={isPopup} />
</>
)}
</div>
)
}
@@ -0,0 +1,47 @@
import { ApiConfiguration, doubaoModels } from "@shared/api"
import { ApiKeyField } from "../common/ApiKeyField"
import { ModelSelector } from "../common/ModelSelector"
import { ModelInfoView } from "../common/ModelInfoView"
import { normalizeApiConfiguration } from "../utils/providerUtils"
/**
* Props for the DoubaoProvider component
*/
interface DoubaoProviderProps {
apiConfiguration: ApiConfiguration
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
showModelOptions: boolean
isPopup?: boolean
}
/**
* The ByteDance Doubao provider configuration component
*/
export const DoubaoProvider = ({ apiConfiguration, handleInputChange, showModelOptions, isPopup }: DoubaoProviderProps) => {
// Get the normalized configuration
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration)
return (
<div>
<ApiKeyField
value={apiConfiguration?.doubaoApiKey || ""}
onChange={handleInputChange("doubaoApiKey")}
providerName="Doubao"
signupUrl="https://console.volcengine.com/home"
/>
{showModelOptions && (
<>
<ModelSelector
models={doubaoModels}
selectedModelId={selectedModelId}
onChange={handleInputChange("apiModelId")}
label="Model"
/>
<ModelInfoView selectedModelId={selectedModelId} modelInfo={selectedModelInfo} isPopup={isPopup} />
</>
)}
</div>
)
}
@@ -0,0 +1,83 @@
import { ApiConfiguration } from "@shared/api"
import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import { ApiKeyField } from "../common/ApiKeyField"
/**
* Props for the FireworksProvider component
*/
interface FireworksProviderProps {
apiConfiguration: ApiConfiguration
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
showModelOptions: boolean
isPopup?: boolean
}
/**
* The Fireworks provider configuration component
*/
export const FireworksProvider = ({ apiConfiguration, handleInputChange, showModelOptions, isPopup }: FireworksProviderProps) => {
// Handler for number input fields with validation
const handleNumberInputChange = (field: keyof ApiConfiguration) => (e: any) => {
const value = (e.target as HTMLInputElement).value
if (!value) {
return
}
const num = parseInt(value, 10)
if (isNaN(num)) {
return
}
handleInputChange(field)({
target: {
value: num,
},
})
}
return (
<div>
<ApiKeyField
value={apiConfiguration?.fireworksApiKey || ""}
onChange={handleInputChange("fireworksApiKey")}
providerName="Fireworks"
signupUrl="https://fireworks.ai/settings/users/api-keys"
/>
{showModelOptions && (
<>
<VSCodeTextField
value={apiConfiguration?.fireworksModelId || ""}
style={{ width: "100%" }}
onInput={handleInputChange("fireworksModelId")}
placeholder={"Enter Model ID..."}>
<span style={{ fontWeight: 500 }}>Model ID</span>
</VSCodeTextField>
<p
style={{
fontSize: "12px",
marginTop: 3,
color: "var(--vscode-descriptionForeground)",
}}>
<span style={{ color: "var(--vscode-errorForeground)" }}>
(<span style={{ fontWeight: 500 }}>Note:</span> Cline uses complex prompts and works best with Claude
models. Less capable models may not work as expected.)
</span>
</p>
<VSCodeTextField
value={apiConfiguration?.fireworksModelMaxCompletionTokens?.toString() || ""}
style={{ width: "100%", marginBottom: 8 }}
onInput={handleNumberInputChange("fireworksModelMaxCompletionTokens")}
placeholder={"2000"}>
<span style={{ fontWeight: 500 }}>Max Completion Tokens</span>
</VSCodeTextField>
<VSCodeTextField
value={apiConfiguration?.fireworksModelMaxTokens?.toString() || ""}
style={{ width: "100%", marginBottom: 8 }}
onInput={handleNumberInputChange("fireworksModelMaxTokens")}
placeholder={"4000"}>
<span style={{ fontWeight: 500 }}>Max Context Tokens</span>
</VSCodeTextField>
</>
)}
</div>
)
}
@@ -0,0 +1,109 @@
import { ApiConfiguration } from "@shared/api"
import { VSCodeTextField, VSCodeRadioGroup, VSCodeRadio, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import { useState, useCallback, useEffect } from "react"
import { useInterval } from "react-use"
import { ModelsServiceClient } from "@/services/grpc-client"
import { StringRequest } from "@shared/proto/common"
import { BaseUrlField } from "../common/BaseUrlField"
/**
* Props for the LMStudioProvider component
*/
interface LMStudioProviderProps {
apiConfiguration: ApiConfiguration
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
showModelOptions: boolean
isPopup?: boolean
}
/**
* The LM Studio provider configuration component
*/
export const LMStudioProvider = ({ apiConfiguration, handleInputChange, showModelOptions, isPopup }: LMStudioProviderProps) => {
const [lmStudioModels, setLmStudioModels] = useState<string[]>([])
// Poll LM Studio models
const requestLmStudioModels = useCallback(async () => {
try {
const response = await ModelsServiceClient.getLmStudioModels(
StringRequest.create({
value: apiConfiguration?.lmStudioBaseUrl || "",
}),
)
if (response && response.values) {
setLmStudioModels(response.values)
}
} catch (error) {
console.error("Failed to fetch LM Studio models:", error)
setLmStudioModels([])
}
}, [apiConfiguration?.lmStudioBaseUrl])
useEffect(() => {
requestLmStudioModels()
}, [requestLmStudioModels])
useInterval(requestLmStudioModels, 2000)
return (
<div>
<BaseUrlField
value={apiConfiguration?.lmStudioBaseUrl}
onChange={(value) => handleInputChange("lmStudioBaseUrl")({ target: { value } })}
placeholder="Default: http://localhost:1234"
label="Use custom base URL"
/>
<VSCodeTextField
value={apiConfiguration?.lmStudioModelId || ""}
style={{ width: "100%" }}
onInput={handleInputChange("lmStudioModelId")}
placeholder={"e.g. meta-llama-3.1-8b-instruct"}>
<span style={{ fontWeight: 500 }}>Model ID</span>
</VSCodeTextField>
{lmStudioModels.length > 0 && (
<VSCodeRadioGroup
value={
lmStudioModels.includes(apiConfiguration?.lmStudioModelId || "") ? apiConfiguration?.lmStudioModelId : ""
}
onChange={(e) => {
const value = (e.target as HTMLInputElement)?.value
// need to check value first since radio group returns empty string sometimes
if (value) {
handleInputChange("lmStudioModelId")({
target: { value },
})
}
}}>
{lmStudioModels.map((model) => (
<VSCodeRadio key={model} value={model} checked={apiConfiguration?.lmStudioModelId === model}>
{model}
</VSCodeRadio>
))}
</VSCodeRadioGroup>
)}
<p
style={{
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}}>
LM Studio allows you to run models locally on your computer. For instructions on how to get started, see their
<VSCodeLink href="https://lmstudio.ai/docs" style={{ display: "inline", fontSize: "inherit" }}>
quickstart guide.
</VSCodeLink>
You will also need to start LM Studio's{" "}
<VSCodeLink href="https://lmstudio.ai/docs/basics/server" style={{ display: "inline", fontSize: "inherit" }}>
local server
</VSCodeLink>{" "}
feature to use it with this extension.{" "}
<span style={{ color: "var(--vscode-errorForeground)" }}>
(<span style={{ fontWeight: 500 }}>Note:</span> Cline uses complex prompts and works best with Claude models.
Less capable models may not work as expected.)
</span>
</p>
</div>
)
}
@@ -0,0 +1,231 @@
import { useState } from "react"
import { ApiConfiguration, liteLlmModelInfoSaneDefaults } from "@shared/api"
import { VSCodeTextField, VSCodeCheckbox, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import { getAsVar, VSC_DESCRIPTION_FOREGROUND } from "@/utils/vscStyles"
import { normalizeApiConfiguration } from "../utils/providerUtils"
import { ModelInfoView } from "../common/ModelInfoView"
import ThinkingBudgetSlider from "../ThinkingBudgetSlider"
/**
* Props for the LiteLlmProvider component
*/
interface LiteLlmProviderProps {
apiConfiguration: ApiConfiguration
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
showModelOptions: boolean
isPopup?: boolean
setApiConfiguration: (config: ApiConfiguration) => void
}
/**
* The LiteLLM provider configuration component
*/
export const LiteLlmProvider = ({
apiConfiguration,
handleInputChange,
showModelOptions,
isPopup,
setApiConfiguration,
}: LiteLlmProviderProps) => {
// Get the normalized configuration
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration)
// Local state for collapsible model configuration section
const [modelConfigurationSelected, setModelConfigurationSelected] = useState(false)
return (
<div>
<VSCodeTextField
value={apiConfiguration?.liteLlmBaseUrl || ""}
style={{ width: "100%" }}
type="url"
onInput={handleInputChange("liteLlmBaseUrl")}
placeholder={"Default: http://localhost:4000"}>
<span style={{ fontWeight: 500 }}>Base URL (optional)</span>
</VSCodeTextField>
<VSCodeTextField
value={apiConfiguration?.liteLlmApiKey || ""}
style={{ width: "100%" }}
type="password"
onInput={handleInputChange("liteLlmApiKey")}
placeholder="Default: noop">
<span style={{ fontWeight: 500 }}>API Key</span>
</VSCodeTextField>
<VSCodeTextField
value={apiConfiguration?.liteLlmModelId || ""}
style={{ width: "100%" }}
onInput={handleInputChange("liteLlmModelId")}
placeholder={"e.g. anthropic/claude-sonnet-4-20250514"}>
<span style={{ fontWeight: 500 }}>Model ID</span>
</VSCodeTextField>
<div style={{ display: "flex", flexDirection: "column", marginTop: 10, marginBottom: 10 }}>
{selectedModelInfo.supportsPromptCache && (
<>
<VSCodeCheckbox
checked={apiConfiguration?.liteLlmUsePromptCache || false}
onChange={(e: any) => {
const isChecked = e.target.checked === true
setApiConfiguration({
...apiConfiguration,
liteLlmUsePromptCache: isChecked,
})
}}
style={{ fontWeight: 500, color: "var(--vscode-charts-green)" }}>
Use prompt caching (GA)
</VSCodeCheckbox>
<p style={{ fontSize: "12px", marginTop: 3, color: "var(--vscode-charts-green)" }}>
Prompt caching requires a supported provider and model
</p>
</>
)}
</div>
<>
<ThinkingBudgetSlider apiConfiguration={apiConfiguration} setApiConfiguration={setApiConfiguration} />
<p
style={{
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}}>
Extended thinking is available for models such as Sonnet-4, o3-mini, Deepseek R1, etc. More info on{" "}
<VSCodeLink
href="https://docs.litellm.ai/docs/reasoning_content"
style={{ display: "inline", fontSize: "inherit" }}>
thinking mode configuration
</VSCodeLink>
</p>
</>
<div
style={{
color: getAsVar(VSC_DESCRIPTION_FOREGROUND),
display: "flex",
margin: "10px 0",
cursor: "pointer",
alignItems: "center",
}}
onClick={() => setModelConfigurationSelected((val) => !val)}>
<span
className={`codicon ${modelConfigurationSelected ? "codicon-chevron-down" : "codicon-chevron-right"}`}
style={{
marginRight: "4px",
}}></span>
<span
style={{
fontWeight: 700,
textTransform: "uppercase",
}}>
Model Configuration
</span>
</div>
{modelConfigurationSelected && (
<>
<VSCodeCheckbox
checked={!!apiConfiguration?.liteLlmModelInfo?.supportsImages}
onChange={(e: any) => {
const isChecked = e.target.checked === true
const modelInfo = apiConfiguration?.liteLlmModelInfo
? apiConfiguration.liteLlmModelInfo
: { ...liteLlmModelInfoSaneDefaults }
modelInfo.supportsImages = isChecked
setApiConfiguration({
...apiConfiguration,
liteLlmModelInfo: modelInfo,
})
}}>
Supports Images
</VSCodeCheckbox>
<div style={{ display: "flex", gap: 10, marginTop: "5px" }}>
<VSCodeTextField
value={
apiConfiguration?.liteLlmModelInfo?.contextWindow
? apiConfiguration.liteLlmModelInfo.contextWindow.toString()
: liteLlmModelInfoSaneDefaults.contextWindow?.toString()
}
style={{ flex: 1 }}
onInput={(input: any) => {
const modelInfo = apiConfiguration?.liteLlmModelInfo
? apiConfiguration.liteLlmModelInfo
: { ...liteLlmModelInfoSaneDefaults }
modelInfo.contextWindow = Number(input.target.value)
setApiConfiguration({
...apiConfiguration,
liteLlmModelInfo: modelInfo,
})
}}>
<span style={{ fontWeight: 500 }}>Context Window Size</span>
</VSCodeTextField>
<VSCodeTextField
value={
apiConfiguration?.liteLlmModelInfo?.maxTokens
? apiConfiguration.liteLlmModelInfo.maxTokens.toString()
: liteLlmModelInfoSaneDefaults.maxTokens?.toString()
}
style={{ flex: 1 }}
onInput={(input: any) => {
const modelInfo = apiConfiguration?.liteLlmModelInfo
? apiConfiguration.liteLlmModelInfo
: { ...liteLlmModelInfoSaneDefaults }
modelInfo.maxTokens = input.target.value
setApiConfiguration({
...apiConfiguration,
liteLlmModelInfo: modelInfo,
})
}}>
<span style={{ fontWeight: 500 }}>Max Output Tokens</span>
</VSCodeTextField>
</div>
<div style={{ display: "flex", gap: 10, marginTop: "5px" }}>
<VSCodeTextField
value={
apiConfiguration?.liteLlmModelInfo?.temperature !== undefined
? apiConfiguration.liteLlmModelInfo.temperature.toString()
: liteLlmModelInfoSaneDefaults.temperature?.toString()
}
onInput={(input: any) => {
const modelInfo = apiConfiguration?.liteLlmModelInfo
? apiConfiguration.liteLlmModelInfo
: { ...liteLlmModelInfoSaneDefaults }
// Check if the input ends with a decimal point or has trailing zeros after decimal
const value = input.target.value
const shouldPreserveFormat = value.endsWith(".") || (value.includes(".") && value.endsWith("0"))
modelInfo.temperature =
value === ""
? liteLlmModelInfoSaneDefaults.temperature
: shouldPreserveFormat
? value // Keep as string to preserve decimal format
: parseFloat(value)
setApiConfiguration({
...apiConfiguration,
liteLlmModelInfo: modelInfo,
})
}}>
<span style={{ fontWeight: 500 }}>Temperature</span>
</VSCodeTextField>
</div>
</>
)}
<p
style={{
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}}>
LiteLLM provides a unified interface to access various LLM providers' models. See their{" "}
<VSCodeLink href="https://docs.litellm.ai/docs/" style={{ display: "inline", fontSize: "inherit" }}>
quickstart guide
</VSCodeLink>{" "}
for more information.
</p>
{showModelOptions && (
<ModelInfoView selectedModelId={selectedModelId} modelInfo={selectedModelInfo} isPopup={isPopup} />
)}
</div>
)
}
@@ -0,0 +1,48 @@
import { ApiConfiguration, nebiusModels } from "@shared/api"
import { ApiKeyField } from "../common/ApiKeyField"
import { ModelSelector } from "../common/ModelSelector"
import { ModelInfoView } from "../common/ModelInfoView"
import { normalizeApiConfiguration } from "../utils/providerUtils"
/**
* Props for the NebiusProvider component
*/
interface NebiusProviderProps {
apiConfiguration: ApiConfiguration
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
showModelOptions: boolean
isPopup?: boolean
}
/**
* The Nebius AI Studio provider configuration component
*/
export const NebiusProvider = ({ apiConfiguration, handleInputChange, showModelOptions, isPopup }: NebiusProviderProps) => {
// Get the normalized configuration
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration)
return (
<div>
<ApiKeyField
value={apiConfiguration?.nebiusApiKey || ""}
onChange={handleInputChange("nebiusApiKey")}
providerName="Nebius"
signupUrl="https://studio.nebius.com/settings/api-keys"
helpText="This key is stored locally and only used to make API requests from this extension. (Note: Cline uses complex prompts and works best with Claude models. Less capable models may not work as expected.)"
/>
{showModelOptions && (
<>
<ModelSelector
models={nebiusModels}
selectedModelId={selectedModelId}
onChange={handleInputChange("apiModelId")}
label="Model"
/>
<ModelInfoView selectedModelId={selectedModelId} modelInfo={selectedModelInfo} isPopup={isPopup} />
</>
)}
</div>
)
}
@@ -0,0 +1,147 @@
import { ApiConfiguration } from "@shared/api"
import { VSCodeTextField, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import { useState, useCallback, useEffect } from "react"
import { useInterval } from "react-use"
import { ModelsServiceClient } from "@/services/grpc-client"
import { StringRequest } from "@shared/proto/common"
import OllamaModelPicker from "../OllamaModelPicker"
import { BaseUrlField } from "../common/BaseUrlField"
/**
* Props for the OllamaProvider component
*/
interface OllamaProviderProps {
apiConfiguration: ApiConfiguration
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
showModelOptions: boolean
isPopup?: boolean
setApiConfiguration: (config: ApiConfiguration) => void
}
/**
* The Ollama provider configuration component
*/
export const OllamaProvider = ({
apiConfiguration,
handleInputChange,
showModelOptions,
isPopup,
setApiConfiguration,
}: OllamaProviderProps) => {
const [ollamaModels, setOllamaModels] = useState<string[]>([])
// Poll ollama models
const requestOllamaModels = useCallback(async () => {
try {
const response = await ModelsServiceClient.getOllamaModels(
StringRequest.create({
value: apiConfiguration?.ollamaBaseUrl || "",
}),
)
if (response && response.values) {
setOllamaModels(response.values)
}
} catch (error) {
console.error("Failed to fetch Ollama models:", error)
setOllamaModels([])
}
}, [apiConfiguration?.ollamaBaseUrl])
useEffect(() => {
requestOllamaModels()
}, [requestOllamaModels])
useInterval(requestOllamaModels, 2000)
return (
<div>
<BaseUrlField
value={apiConfiguration?.ollamaBaseUrl}
onChange={(value) => handleInputChange("ollamaBaseUrl")({ target: { value } })}
placeholder="Default: http://localhost:11434"
label="Use custom base URL"
/>
{/* Model selection - use filterable picker */}
<label htmlFor="ollama-model-selection">
<span style={{ fontWeight: 500 }}>Model</span>
</label>
<OllamaModelPicker
ollamaModels={ollamaModels}
selectedModelId={apiConfiguration?.ollamaModelId || ""}
onModelChange={(modelId) => {
setApiConfiguration({
...apiConfiguration,
ollamaModelId: modelId,
})
}}
placeholder={ollamaModels.length > 0 ? "Search and select a model..." : "e.g. llama3.1"}
/>
{/* Show status message based on model availability */}
{ollamaModels.length === 0 && (
<p
style={{
fontSize: "12px",
marginTop: "3px",
color: "var(--vscode-descriptionForeground)",
fontStyle: "italic",
}}>
Unable to fetch models from Ollama server. Please ensure Ollama is running and accessible, or enter the model
ID manually above.
</p>
)}
<VSCodeTextField
value={apiConfiguration?.ollamaApiOptionsCtxNum || "32768"}
style={{ width: "100%" }}
onInput={handleInputChange("ollamaApiOptionsCtxNum")}
placeholder={"e.g. 32768"}>
<span style={{ fontWeight: 500 }}>Model Context Window</span>
</VSCodeTextField>
{showModelOptions && (
<>
<VSCodeTextField
value={apiConfiguration?.requestTimeoutMs ? apiConfiguration.requestTimeoutMs.toString() : "30000"}
style={{ width: "100%" }}
onInput={(e: any) => {
const value = e.target.value
// Convert to number, with validation
const numValue = parseInt(value, 10)
if (!isNaN(numValue) && numValue > 0) {
setApiConfiguration({
...apiConfiguration,
requestTimeoutMs: numValue,
})
}
}}
placeholder="Default: 30000 (30 seconds)">
<span style={{ fontWeight: 500 }}>Request Timeout (ms)</span>
</VSCodeTextField>
<p style={{ fontSize: "12px", marginTop: 3, color: "var(--vscode-descriptionForeground)" }}>
Maximum time in milliseconds to wait for API responses before timing out.
</p>
</>
)}
<p
style={{
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}}>
Ollama allows you to run models locally on your computer. For instructions on how to get started, see their{" "}
<VSCodeLink
href="https://github.com/ollama/ollama/blob/main/README.md"
style={{ display: "inline", fontSize: "inherit" }}>
quickstart guide.
</VSCodeLink>{" "}
<span style={{ color: "var(--vscode-errorForeground)" }}>
(<span style={{ fontWeight: 500 }}>Note:</span> Cline uses complex prompts and works best with Claude models.
Less capable models may not work as expected.)
</span>
</p>
</div>
)
}
@@ -0,0 +1,108 @@
import { ApiConfiguration, internationalQwenModels, mainlandQwenModels } from "@shared/api"
import { VSCodeDropdown, VSCodeOption } from "@vscode/webview-ui-toolkit/react"
import { ApiKeyField } from "../common/ApiKeyField"
import { ModelSelector, DropdownContainer } from "../common/ModelSelector"
import { ModelInfoView } from "../common/ModelInfoView"
import { normalizeApiConfiguration } from "../utils/providerUtils"
import ThinkingBudgetSlider from "../ThinkingBudgetSlider"
import { DROPDOWN_Z_INDEX } from "../ApiOptions"
const SUPPORTED_THINKING_MODELS = [
"qwen3-235b-a22b",
"qwen3-32b",
"qwen3-30b-a3b",
"qwen3-14b",
"qwen3-8b",
"qwen3-4b",
"qwen3-1.7b",
"qwen3-0.6b",
"qwen-plus-latest",
"qwen-turbo-latest",
]
/**
* Props for the QwenProvider component
*/
interface QwenProviderProps {
apiConfiguration: ApiConfiguration
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
showModelOptions: boolean
isPopup?: boolean
setApiConfiguration: (config: ApiConfiguration) => void
}
/**
* The Alibaba Qwen provider configuration component
*/
export const QwenProvider = ({
apiConfiguration,
handleInputChange,
showModelOptions,
isPopup,
setApiConfiguration,
}: QwenProviderProps) => {
// Get the normalized configuration
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration)
// Determine which models to use based on API line selection
const qwenModels = apiConfiguration?.qwenApiLine === "china" ? mainlandQwenModels : internationalQwenModels
return (
<div>
<DropdownContainer className="dropdown-container" style={{ position: "inherit" }}>
<label htmlFor="qwen-line-provider">
<span style={{ fontWeight: 500, marginTop: 5 }}>Alibaba API Line</span>
</label>
<VSCodeDropdown
id="qwen-line-provider"
value={apiConfiguration?.qwenApiLine || "china"}
onChange={handleInputChange("qwenApiLine")}
style={{
minWidth: 130,
position: "relative",
}}>
<VSCodeOption value="china">China API</VSCodeOption>
<VSCodeOption value="international">International API</VSCodeOption>
</VSCodeDropdown>
</DropdownContainer>
<p
style={{
fontSize: "12px",
marginTop: 3,
color: "var(--vscode-descriptionForeground)",
}}>
Please select the appropriate API interface based on your location. If you are in China, choose the China API
interface. Otherwise, choose the International API interface.
</p>
<ApiKeyField
value={apiConfiguration?.qwenApiKey || ""}
onChange={handleInputChange("qwenApiKey")}
providerName="Qwen"
signupUrl="https://bailian.console.aliyun.com/"
/>
{showModelOptions && (
<>
<ModelSelector
models={qwenModels}
selectedModelId={selectedModelId}
onChange={handleInputChange("apiModelId")}
label="Model"
zIndex={DROPDOWN_Z_INDEX - 2}
/>
{SUPPORTED_THINKING_MODELS.includes(selectedModelId) && (
<ThinkingBudgetSlider
apiConfiguration={apiConfiguration}
setApiConfiguration={setApiConfiguration}
maxBudget={selectedModelInfo.thinkingConfig?.maxBudget}
/>
)}
<ModelInfoView selectedModelId={selectedModelId} modelInfo={selectedModelInfo} isPopup={isPopup} />
</>
)}
</div>
)
}
@@ -0,0 +1,31 @@
import { ApiConfiguration } from "@shared/api"
import { ApiKeyField } from "../common/ApiKeyField"
import RequestyModelPicker from "../RequestyModelPicker"
/**
* Props for the RequestyProvider component
*/
interface RequestyProviderProps {
apiConfiguration: ApiConfiguration
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
showModelOptions: boolean
isPopup?: boolean
}
/**
* The Requesty provider configuration component
*/
export const RequestyProvider = ({ apiConfiguration, handleInputChange, showModelOptions, isPopup }: RequestyProviderProps) => {
return (
<div>
<ApiKeyField
value={apiConfiguration?.requestyApiKey || ""}
onChange={handleInputChange("requestyApiKey")}
providerName="Requesty"
signupUrl="https://app.requesty.ai/manage-api"
/>
{showModelOptions && <RequestyModelPicker isPopup={isPopup} />}
</div>
)
}
@@ -0,0 +1,105 @@
import { ApiConfiguration, sapAiCoreModels } from "@shared/api"
import { VSCodeTextField, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import { ModelSelector } from "../common/ModelSelector"
import { ModelInfoView } from "../common/ModelInfoView"
import { normalizeApiConfiguration } from "../utils/providerUtils"
/**
* Props for the SapAiCoreProvider component
*/
interface SapAiCoreProviderProps {
apiConfiguration: ApiConfiguration
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
showModelOptions: boolean
isPopup?: boolean
}
/**
* The SAP AI Core provider configuration component
*/
export const SapAiCoreProvider = ({ apiConfiguration, handleInputChange, showModelOptions, isPopup }: SapAiCoreProviderProps) => {
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration)
return (
<div style={{ display: "flex", flexDirection: "column", gap: 5 }}>
<VSCodeTextField
value={apiConfiguration?.sapAiCoreClientId || ""}
style={{ width: "100%" }}
type="password"
onInput={handleInputChange("sapAiCoreClientId")}
placeholder="Enter AI Core Client Id...">
<span style={{ fontWeight: 500 }}>AI Core Client Id</span>
</VSCodeTextField>
{apiConfiguration?.sapAiCoreClientId && (
<p style={{ fontSize: "12px", color: "var(--vscode-descriptionForeground)" }}>
Client Id is set. To change it, please re-enter the value.
</p>
)}
<VSCodeTextField
value={apiConfiguration?.sapAiCoreClientSecret ? "********" : ""}
style={{ width: "100%" }}
type="password"
onInput={handleInputChange("sapAiCoreClientSecret")}
placeholder="Enter AI Core Client Secret...">
<span style={{ fontWeight: 500 }}>AI Core Client Secret</span>
</VSCodeTextField>
{apiConfiguration?.sapAiCoreClientSecret && (
<p style={{ fontSize: "12px", color: "var(--vscode-descriptionForeground)" }}>
Client Secret is set. To change it, please re-enter the value.
</p>
)}
<VSCodeTextField
value={apiConfiguration?.sapAiCoreBaseUrl || ""}
style={{ width: "100%" }}
onInput={handleInputChange("sapAiCoreBaseUrl")}
placeholder="Enter AI Core Base URL...">
<span style={{ fontWeight: 500 }}>AI Core Base URL</span>
</VSCodeTextField>
<VSCodeTextField
value={apiConfiguration?.sapAiCoreTokenUrl || ""}
style={{ width: "100%" }}
onInput={handleInputChange("sapAiCoreTokenUrl")}
placeholder="Enter AI Core Auth URL...">
<span style={{ fontWeight: 500 }}>AI Core Auth URL</span>
</VSCodeTextField>
<VSCodeTextField
value={apiConfiguration?.sapAiResourceGroup || ""}
style={{ width: "100%" }}
onInput={handleInputChange("sapAiResourceGroup")}
placeholder="Enter AI Core Resource Group...">
<span style={{ fontWeight: 500 }}>AI Core Resource Group</span>
</VSCodeTextField>
<p
style={{
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}}>
These credentials are stored locally and only used to make API requests from this extension.
<VSCodeLink
href="https://help.sap.com/docs/sap-ai-core/sap-ai-core-service-guide/access-sap-ai-core-via-api"
style={{ display: "inline" }}>
You can find more information about SAP AI Core API access here.
</VSCodeLink>
</p>
{showModelOptions && (
<>
<ModelSelector
models={sapAiCoreModels}
selectedModelId={selectedModelId}
onChange={handleInputChange("apiModelId")}
label="Model"
/>
<ModelInfoView selectedModelId={selectedModelId} modelInfo={selectedModelInfo} isPopup={isPopup} />
</>
)}
</div>
)
}
@@ -0,0 +1,96 @@
import { ApiConfiguration } from "@shared/api"
import { EmptyRequest } from "@shared/proto/common"
import { ModelsServiceClient } from "@/services/grpc-client"
import { VSCodeDropdown, VSCodeOption } from "@vscode/webview-ui-toolkit/react"
import { useState, useCallback, useEffect } from "react"
import { useInterval } from "react-use"
import * as vscodemodels from "vscode"
import { DropdownContainer, DROPDOWN_Z_INDEX } from "../ApiOptions"
interface VSCodeLmProviderProps {
apiConfiguration: ApiConfiguration
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
}
export const VSCodeLmProvider = ({ apiConfiguration, handleInputChange }: VSCodeLmProviderProps) => {
const [vsCodeLmModels, setVsCodeLmModels] = useState<vscodemodels.LanguageModelChatSelector[]>([])
// Poll VS Code LM models
const requestVsCodeLmModels = useCallback(async () => {
try {
const response = await ModelsServiceClient.getVsCodeLmModels(EmptyRequest.create({}))
if (response && response.models) {
setVsCodeLmModels(response.models)
}
} catch (error) {
console.error("Failed to fetch VS Code LM models:", error)
setVsCodeLmModels([])
}
}, [])
useEffect(() => {
requestVsCodeLmModels()
}, [requestVsCodeLmModels])
useInterval(requestVsCodeLmModels, 2000)
return (
<div>
<DropdownContainer zIndex={DROPDOWN_Z_INDEX - 2} className="dropdown-container">
<label htmlFor="vscode-lm-model">
<span style={{ fontWeight: 500 }}>Language Model</span>
</label>
{vsCodeLmModels.length > 0 ? (
<VSCodeDropdown
id="vscode-lm-model"
value={
apiConfiguration?.vsCodeLmModelSelector
? `${apiConfiguration.vsCodeLmModelSelector.vendor ?? ""}/${apiConfiguration.vsCodeLmModelSelector.family ?? ""}`
: ""
}
onChange={(e) => {
const value = (e.target as HTMLInputElement).value
if (!value) {
return
}
const [vendor, family] = value.split("/")
handleInputChange("vsCodeLmModelSelector")({
target: {
value: { vendor, family },
},
})
}}
style={{ width: "100%" }}>
<VSCodeOption value="">Select a model...</VSCodeOption>
{vsCodeLmModels.map((model) => (
<VSCodeOption key={`${model.vendor}/${model.family}`} value={`${model.vendor}/${model.family}`}>
{model.vendor} - {model.family}
</VSCodeOption>
))}
</VSCodeDropdown>
) : (
<p
style={{
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}}>
The VS Code Language Model API allows you to run models provided by other VS Code extensions (including
but not limited to GitHub Copilot). The easiest way to get started is to install the Copilot extension
from the VS Marketplace and enabling Claude 4 Sonnet.
</p>
)}
<p
style={{
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-errorForeground)",
fontWeight: 500,
}}>
Note: This is a very experimental integration and may not work as expected.
</p>
</DropdownContainer>
</div>
)
}
@@ -0,0 +1,121 @@
import { ApiConfiguration, vertexGlobalModels, vertexModels } from "@shared/api"
import { VSCodeTextField, VSCodeDropdown, VSCodeOption, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import { ModelSelector } from "../common/ModelSelector"
import { ModelInfoView } from "../common/ModelInfoView"
import { normalizeApiConfiguration } from "../utils/providerUtils"
import { DropdownContainer, DROPDOWN_Z_INDEX } from "../ApiOptions"
import ThinkingBudgetSlider from "../ThinkingBudgetSlider"
/**
* Props for the VertexProvider component
*/
interface VertexProviderProps {
apiConfiguration: ApiConfiguration
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
showModelOptions: boolean
isPopup?: boolean
setApiConfiguration: (config: ApiConfiguration) => void
}
// Vertex models that support thinking
const SUPPORTED_THINKING_MODELS = [
"claude-3-7-sonnet@20250219",
"claude-sonnet-4@20250514",
"claude-opus-4@20250514",
"gemini-2.5-flash",
"gemini-2.5-pro",
]
/**
* The GCP Vertex AI provider configuration component
*/
export const VertexProvider = ({
apiConfiguration,
handleInputChange,
showModelOptions,
isPopup,
setApiConfiguration,
}: VertexProviderProps) => {
// Get the normalized configuration
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration)
// Determine which models to use based on region
const modelsToUse = apiConfiguration?.vertexRegion === "global" ? vertexGlobalModels : vertexModels
return (
<div
style={{
display: "flex",
flexDirection: "column",
gap: 5,
}}>
<VSCodeTextField
value={apiConfiguration?.vertexProjectId || ""}
style={{ width: "100%" }}
onInput={handleInputChange("vertexProjectId")}
placeholder="Enter Project ID...">
<span style={{ fontWeight: 500 }}>Google Cloud Project ID</span>
</VSCodeTextField>
<DropdownContainer zIndex={DROPDOWN_Z_INDEX - 1} className="dropdown-container">
<label htmlFor="vertex-region-dropdown">
<span style={{ fontWeight: 500 }}>Google Cloud Region</span>
</label>
<VSCodeDropdown
id="vertex-region-dropdown"
value={apiConfiguration?.vertexRegion || ""}
style={{ width: "100%" }}
onChange={handleInputChange("vertexRegion")}>
<VSCodeOption value="">Select a region...</VSCodeOption>
<VSCodeOption value="us-east5">us-east5</VSCodeOption>
<VSCodeOption value="us-central1">us-central1</VSCodeOption>
<VSCodeOption value="europe-west1">europe-west1</VSCodeOption>
<VSCodeOption value="europe-west4">europe-west4</VSCodeOption>
<VSCodeOption value="asia-southeast1">asia-southeast1</VSCodeOption>
<VSCodeOption value="global">global</VSCodeOption>
</VSCodeDropdown>
</DropdownContainer>
<p
style={{
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}}>
To use Google Cloud Vertex AI, you need to
<VSCodeLink
href="https://cloud.google.com/vertex-ai/generative-ai/docs/partner-models/use-claude#before_you_begin"
style={{ display: "inline", fontSize: "inherit" }}>
{"1) create a Google Cloud account enable the Vertex AI API enable the desired Claude models,"}
</VSCodeLink>{" "}
<VSCodeLink
href="https://cloud.google.com/docs/authentication/provide-credentials-adc#google-idp"
style={{ display: "inline", fontSize: "inherit" }}>
{"2) install the Google Cloud CLI configure Application Default Credentials."}
</VSCodeLink>
</p>
{showModelOptions && (
<>
<ModelSelector
models={modelsToUse}
selectedModelId={selectedModelId}
onChange={handleInputChange("apiModelId")}
label="Model"
zIndex={DROPDOWN_Z_INDEX - 2}
/>
{SUPPORTED_THINKING_MODELS.includes(selectedModelId) && (
<ThinkingBudgetSlider
apiConfiguration={apiConfiguration}
setApiConfiguration={setApiConfiguration}
maxBudget={selectedModelInfo.thinkingConfig?.maxBudget}
/>
)}
<ModelInfoView selectedModelId={selectedModelId} modelInfo={selectedModelInfo} isPopup={isPopup} />
</>
)}
</div>
)
}
@@ -0,0 +1,125 @@
import { ApiConfiguration, xaiModels } from "@shared/api"
import { VSCodeCheckbox, VSCodeDropdown, VSCodeOption } from "@vscode/webview-ui-toolkit/react"
import { useState } from "react"
import { ApiKeyField } from "../common/ApiKeyField"
import { ModelSelector, DropdownContainer } from "../common/ModelSelector"
import { ModelInfoView } from "../common/ModelInfoView"
import { normalizeApiConfiguration } from "../utils/providerUtils"
import { DROPDOWN_Z_INDEX } from "../ApiOptions"
/**
* Props for the XaiProvider component
*/
interface XaiProviderProps {
apiConfiguration: ApiConfiguration
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
showModelOptions: boolean
isPopup?: boolean
setApiConfiguration: (config: ApiConfiguration) => void
}
/**
* The xAI provider configuration component
*/
export const XaiProvider = ({
apiConfiguration,
handleInputChange,
showModelOptions,
isPopup,
setApiConfiguration,
}: XaiProviderProps) => {
// Get the normalized configuration
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration)
// Local state for reasoning effort toggle
const [reasoningEffortSelected, setReasoningEffortSelected] = useState(!!apiConfiguration?.reasoningEffort)
return (
<div>
<div>
<ApiKeyField
value={apiConfiguration?.xaiApiKey || ""}
onChange={handleInputChange("xaiApiKey")}
providerName="X AI"
signupUrl="https://x.ai"
/>
<p
style={{
fontSize: "12px",
marginTop: -10,
color: "var(--vscode-descriptionForeground)",
}}>
<span style={{ color: "var(--vscode-errorForeground)" }}>
(<span style={{ fontWeight: 500 }}>Note:</span> Cline uses complex prompts and works best with Claude
models. Less capable models may not work as expected.)
</span>
</p>
</div>
{showModelOptions && (
<>
<ModelSelector
models={xaiModels}
selectedModelId={selectedModelId}
onChange={handleInputChange("apiModelId")}
label="Model"
/>
{selectedModelId && selectedModelId.includes("3-mini") && (
<>
<VSCodeCheckbox
style={{ marginTop: 0 }}
checked={reasoningEffortSelected}
onChange={(e: any) => {
const isChecked = e.target.checked === true
setReasoningEffortSelected(isChecked)
if (!isChecked) {
setApiConfiguration({
...apiConfiguration,
reasoningEffort: "",
})
}
}}>
Modify reasoning effort
</VSCodeCheckbox>
{reasoningEffortSelected && (
<div>
<label htmlFor="reasoning-effort-dropdown">
<span style={{}}>Reasoning Effort</span>
</label>
<DropdownContainer className="dropdown-container" zIndex={DROPDOWN_Z_INDEX - 100}>
<VSCodeDropdown
id="reasoning-effort-dropdown"
style={{ width: "100%", marginTop: 3 }}
value={apiConfiguration?.reasoningEffort || "high"}
onChange={(e: any) => {
setApiConfiguration({
...apiConfiguration,
reasoningEffort: e.target.value,
})
}}>
<VSCodeOption value="low">low</VSCodeOption>
<VSCodeOption value="high">high</VSCodeOption>
</VSCodeDropdown>
</DropdownContainer>
<p
style={{
fontSize: "12px",
marginTop: 3,
marginBottom: 0,
color: "var(--vscode-descriptionForeground)",
}}>
High effort may produce more thorough analysis but takes longer and uses more tokens.
</p>
</div>
)}
</>
)}
<ModelInfoView selectedModelId={selectedModelId} modelInfo={selectedModelInfo} isPopup={isPopup} />
</>
)}
</div>
)
}
@@ -0,0 +1,99 @@
import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
import { memo } from "react"
import { TabButton } from "../../mcp/configuration/McpConfigurationView"
import ApiOptions from "../ApiOptions"
import Section from "../Section"
import { ChatSettings } from "@shared/ChatSettings"
interface ApiConfigurationSectionProps {
planActSeparateModelsSetting: boolean
chatSettings: ChatSettings
isSwitchingMode: boolean
apiErrorMessage?: string
modelIdErrorMessage?: string
handlePlanActModeChange: (mode: "plan" | "act") => Promise<void>
setPlanActSeparateModelsSetting: (value: boolean) => void
renderSectionHeader: (tabId: string) => JSX.Element | null
}
const ApiConfigurationSection = ({
planActSeparateModelsSetting,
chatSettings,
isSwitchingMode,
apiErrorMessage,
modelIdErrorMessage,
handlePlanActModeChange,
setPlanActSeparateModelsSetting,
renderSectionHeader,
}: ApiConfigurationSectionProps) => {
return (
<div>
{renderSectionHeader("api-config")}
<Section>
{/* Tabs container */}
{planActSeparateModelsSetting ? (
<div className="rounded-md mb-5 bg-[var(--vscode-panel-background)]">
<div className="flex gap-[1px] mb-[10px] -mt-2 border-0 border-b border-solid border-[var(--vscode-panel-border)]">
<TabButton
isActive={chatSettings.mode === "plan"}
onClick={() => handlePlanActModeChange("plan")}
disabled={isSwitchingMode}
style={{
opacity: isSwitchingMode ? 0.6 : 1,
cursor: isSwitchingMode ? "not-allowed" : "pointer",
}}>
{isSwitchingMode && chatSettings.mode === "act" ? "Switching..." : "Plan Mode"}
</TabButton>
<TabButton
isActive={chatSettings.mode === "act"}
onClick={() => handlePlanActModeChange("act")}
disabled={isSwitchingMode}
style={{
opacity: isSwitchingMode ? 0.6 : 1,
cursor: isSwitchingMode ? "not-allowed" : "pointer",
}}>
{isSwitchingMode && chatSettings.mode === "plan" ? "Switching..." : "Act Mode"}
</TabButton>
</div>
{/* Content container */}
<div className="-mb-3">
<ApiOptions
key={chatSettings.mode}
showModelOptions={true}
apiErrorMessage={apiErrorMessage}
modelIdErrorMessage={modelIdErrorMessage}
/>
</div>
</div>
) : (
<ApiOptions
key={"single"}
showModelOptions={true}
apiErrorMessage={apiErrorMessage}
modelIdErrorMessage={modelIdErrorMessage}
/>
)}
<div className="mb-[5px]">
<VSCodeCheckbox
className="mb-[5px]"
checked={planActSeparateModelsSetting}
onChange={(e: any) => {
const checked = e.target.checked === true
setPlanActSeparateModelsSetting(checked)
}}>
Use different models for Plan and Act modes
</VSCodeCheckbox>
<p className="text-xs mt-[5px] text-[var(--vscode-descriptionForeground)]">
Switching between Plan and Act mode will persist the API and model used in the previous mode. This may be
helpful e.g. when using a strong reasoning model to architect a plan for a cheaper coding model to act on.
</p>
</div>
</Section>
</div>
)
}
export default memo(ApiConfigurationSection)