mirror of
https://github.com/cline/cline.git
synced 2026-09-06 20:41:02 +08:00
Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d2979631d8 | |||
| 4dfc1358c5 | |||
| 6a96c183a3 | |||
| 9df023b9d0 | |||
| 19e4387b86 | |||
| ab01a518d1 | |||
| a66724e312 | |||
| cc56486814 | |||
| 277b20a1b2 | |||
| 55d12d7556 | |||
| a527acc56c | |||
| dc1d7f51cb | |||
| 4ff7e06044 | |||
| 2968c8d99c | |||
| c617d2550e | |||
| 7937530c74 | |||
| 3657e903f5 | |||
| 0fcab4d989 | |||
| afb64c896e | |||
| 65f1b05420 | |||
| 8f37543800 | |||
| abbe40ee9c | |||
| 5c082762c4 | |||
| 4a230ad878 | |||
| 94c432f3f3 | |||
| d88c07c932 | |||
| 5ee5577010 | |||
| 01877c1629 | |||
| f8a7b563aa | |||
| 915555f80f | |||
| 26eafd96dd | |||
| e504b9d414 | |||
| 312777ddc5 | |||
| c79acf5ffe | |||
| 7d5d347cdd | |||
| 95cc15a142 | |||
| f7d464a51d | |||
| a6c4c0c0ea | |||
| 976a8fa85e | |||
| 33413e91c6 | |||
| 801c59e75e | |||
| df9c8e2e80 | |||
| 4d480ea3fe |
@@ -209,7 +209,7 @@ class Task {
|
||||
switch (chunk.type) {
|
||||
case "text":
|
||||
// Parse into content blocks
|
||||
this.assistantMessageContent = parseAssistantMessage(chunk.text)
|
||||
this.assistantMessageContent = parseAssistantMessageV2(chunk.text)
|
||||
// Present blocks to user
|
||||
await this.presentAssistantMessage()
|
||||
break
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
blank_issues_enabled: false
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: ✨ Feature Request
|
||||
url: https://github.com/cline/cline/discussions/categories/feature-requests?discussions_q=is%3Aopen+category%3A%22Feature+Requests%22+sort%3Atop
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
out
|
||||
dist
|
||||
dist-standalone
|
||||
node_modules
|
||||
tmp
|
||||
.vscode-test/
|
||||
|
||||
Vendored
+22
@@ -16,6 +16,28 @@
|
||||
"IS_DEV": "true",
|
||||
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Run Extension (Fresh Install Mode)",
|
||||
"type": "extensionHost",
|
||||
"request": "launch",
|
||||
"runtimeExecutable": "${execPath}",
|
||||
"args": [
|
||||
"--profile-temp",
|
||||
"--sync",
|
||||
"off",
|
||||
"--disable-extensions",
|
||||
"--extensionDevelopmentPath=${workspaceFolder}",
|
||||
"${workspaceFolder}"
|
||||
],
|
||||
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
|
||||
"preLaunchTask": "clean-sandbox",
|
||||
"internalConsoleOptions": "openOnSessionStart",
|
||||
"postDebugTask": "stop",
|
||||
"env": {
|
||||
"IS_DEV": "true",
|
||||
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Vendored
+6
@@ -185,6 +185,12 @@
|
||||
"label": "stop",
|
||||
"command": "echo ${input:terminate}",
|
||||
"type": "shell"
|
||||
},
|
||||
{
|
||||
"label": "clean-sandbox",
|
||||
"type": "shell",
|
||||
"dependsOn": ["watch"],
|
||||
"command": "rm -rf .vscode-dev"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
|
||||
@@ -1,5 +1,34 @@
|
||||
# Changelog
|
||||
|
||||
## [3.15.5]
|
||||
|
||||
- Fix inefficient memory management in the task timeline
|
||||
- Fix Gemini rate limitation response not being handled properly (Thanks @BarreiroT!)
|
||||
|
||||
## [3.15.4]
|
||||
|
||||
- Add gemini model back to vertex provider
|
||||
- Add gemini telemetry
|
||||
- Add filtering for tasks tied to the current workspace
|
||||
|
||||
## [3.15.3]
|
||||
|
||||
- Add Fireworks API Provider
|
||||
- Fix minor visual issues with auto-approve menu
|
||||
- Fix one instance of terminal not getting output
|
||||
- Fix 'Chrome was launched but debug port is not responding' error
|
||||
|
||||
## [3.15.2]
|
||||
|
||||
- Added details to auto approve menu and more sensible default controls
|
||||
- Add detailed configuration options for LiteLLM provider
|
||||
- Add webview telemetry for users who have opted in to telemetry
|
||||
- Update Gemini in OpenRouter/Cline providers to use implicit caching
|
||||
- Fix freezing issues during rendering of large streaming text
|
||||
- Fix grey screen webview crashes by releasing memory after every diff edit
|
||||
- Fix breaking out of diff auto-scroll
|
||||
- Fix IME composition Enter auto‑sending edited message
|
||||
|
||||
## [3.15.1]
|
||||
|
||||
- Fix bug where PowerShell commands weren't given enough time before giving up and showing an error
|
||||
|
||||
@@ -26,9 +26,9 @@ For complete transparency, you can inspect our [telemetry implementation](https:
|
||||
|
||||
### How to Opt Out
|
||||
|
||||
Telemetry in Cline is entirely optional and requires your explicit consent:
|
||||
Telemetry in Cline is entirely optional:
|
||||
|
||||
- When you update or install our VS Code extension, you'll see a simple prompt: "Help Improve Cline" with Allow or Deny options
|
||||
- When you update or install our VS Code extension, you'll see a message about our anonymous telemetry
|
||||
- You can change your preference anytime in settings
|
||||
|
||||
Cline also respects VS Code's global telemetry settings. If you've disabled telemetry at the VS Code level, Cline's telemetry will automatically be disabled as well.
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "claude-dev",
|
||||
"version": "3.15.1",
|
||||
"version": "3.15.5",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "claude-dev",
|
||||
"version": "3.15.1",
|
||||
"version": "3.15.5",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/bedrock-sdk": "^0.12.4",
|
||||
|
||||
+3
-35
@@ -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.15.1",
|
||||
"version": "3.15.5",
|
||||
"icon": "assets/icons/icon.png",
|
||||
"engines": {
|
||||
"vscode": "^1.84.0"
|
||||
@@ -40,6 +40,8 @@
|
||||
"llama"
|
||||
],
|
||||
"activationEvents": [
|
||||
"onLanguage",
|
||||
"onStartupFinished",
|
||||
"workspaceContains:evals.env"
|
||||
],
|
||||
"main": "./dist/extension.js",
|
||||
@@ -235,45 +237,11 @@
|
||||
"configuration": {
|
||||
"title": "Cline",
|
||||
"properties": {
|
||||
"cline.vsCodeLmModelSelector": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"vendor": {
|
||||
"type": "string",
|
||||
"description": "The vendor of the language model (e.g. copilot)"
|
||||
},
|
||||
"family": {
|
||||
"type": "string",
|
||||
"description": "The family of the language model (e.g. gpt-4)"
|
||||
}
|
||||
},
|
||||
"description": "Settings for VSCode Language Model API"
|
||||
},
|
||||
"cline.enableCheckpoints": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Enables extension to save checkpoints of workspace throughout the task. Uses git under the hood which may not work well with large workspaces."
|
||||
},
|
||||
"cline.disableBrowserTool": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Disables extension from spawning browser session."
|
||||
},
|
||||
"cline.modelSettings.o3Mini.reasoningEffort": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"low",
|
||||
"medium",
|
||||
"high"
|
||||
],
|
||||
"default": "medium",
|
||||
"description": "Controls the reasoning effort when using an OpenAI reasoning model. Higher values may result in more thorough but slower responses."
|
||||
},
|
||||
"cline.chromeExecutablePath": {
|
||||
"type": "string",
|
||||
"default": null,
|
||||
"description": "Path to Chrome executable for browser use functionality. If not set, the extension will attempt to find or download it automatically."
|
||||
},
|
||||
"cline.preferredLanguage": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
|
||||
@@ -40,6 +40,8 @@ message BrowserSettings {
|
||||
Viewport viewport = 1;
|
||||
optional string remote_browser_host = 2;
|
||||
optional bool remote_browser_enabled = 3;
|
||||
optional string chrome_executable_path = 4;
|
||||
optional bool disable_tool_use = 5;
|
||||
}
|
||||
|
||||
message UpdateBrowserSettingsRequest {
|
||||
@@ -47,4 +49,6 @@ message UpdateBrowserSettingsRequest {
|
||||
Viewport viewport = 2;
|
||||
optional string remote_browser_host = 3;
|
||||
optional bool remote_browser_enabled = 4;
|
||||
optional string chrome_executable_path = 5;
|
||||
optional bool disable_tool_use = 6;
|
||||
}
|
||||
|
||||
+34
-21
@@ -29,6 +29,7 @@ const serviceNameMap = {
|
||||
task: "cline.TaskService",
|
||||
web: "cline.WebService",
|
||||
models: "cline.ModelsService",
|
||||
slash: "cline.SlashService",
|
||||
// Add new services here - no other code changes needed!
|
||||
}
|
||||
const serviceDirs = Object.keys(serviceNameMap).map((serviceKey) => path.join(ROOT_DIR, "src", "core", "controller", serviceKey))
|
||||
@@ -54,30 +55,42 @@ async function main() {
|
||||
|
||||
// Process all proto files
|
||||
console.log(chalk.cyan("Processing proto files from"), SCRIPT_DIR)
|
||||
const protoFiles = await globby("*.proto", { cwd: SCRIPT_DIR })
|
||||
const protoFiles = await globby("*.proto", { cwd: SCRIPT_DIR, absolute: true })
|
||||
|
||||
for (const protoFile of protoFiles) {
|
||||
console.log(chalk.cyan(`Generating TypeScript code for ${protoFile}...`))
|
||||
// Build the protoc command with proper path handling for cross-platform
|
||||
const tsProtocCommand = [
|
||||
protoc,
|
||||
`--proto_path="${SCRIPT_DIR}"`,
|
||||
`--plugin=protoc-gen-ts_proto="${tsProtoPlugin}"`,
|
||||
`--ts_proto_out="${TS_OUT_DIR}"`,
|
||||
"--ts_proto_opt=outputServices=generic-definitions,env=node,esModuleInterop=true,useDate=false,useOptionals=messages",
|
||||
...protoFiles,
|
||||
].join(" ")
|
||||
try {
|
||||
console.log(chalk.cyan(`Generating TypeScript code for:\n${protoFiles.join("\n")}...`))
|
||||
execSync(tsProtocCommand, { stdio: "inherit" })
|
||||
} catch (error) {
|
||||
console.error(chalk.red("Error generating TypeScript for proto files:"), error)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Build the protoc command with proper path handling for cross-platform
|
||||
const protocCommand = [
|
||||
protoc,
|
||||
`--plugin=protoc-gen-ts_proto="${tsProtoPlugin}"`,
|
||||
`--ts_proto_out="${TS_OUT_DIR}"`,
|
||||
"--ts_proto_opt=outputServices=generic-definitions,env=node,esModuleInterop=true,useDate=false,useOptionals=messages",
|
||||
`--proto_path="${SCRIPT_DIR}"`,
|
||||
`"${path.join(SCRIPT_DIR, protoFile)}"`,
|
||||
].join(" ")
|
||||
const descriptorOutDir = path.join(ROOT_DIR, "dist-standalone", "proto")
|
||||
await fs.mkdir(descriptorOutDir, { recursive: true })
|
||||
|
||||
try {
|
||||
const execOptions = {
|
||||
stdio: "inherit",
|
||||
}
|
||||
execSync(protocCommand, execOptions)
|
||||
} catch (error) {
|
||||
console.error(chalk.red(`Error generating TypeScript for ${protoFile}:`), error)
|
||||
process.exit(1)
|
||||
}
|
||||
const descriptorFile = path.join(descriptorOutDir, "descriptor_set.pb")
|
||||
const descriptorProtocCommand = [
|
||||
protoc,
|
||||
`--proto_path="${SCRIPT_DIR}"`,
|
||||
`--descriptor_set_out="${descriptorFile}"`,
|
||||
"--include_imports",
|
||||
...protoFiles,
|
||||
].join(" ")
|
||||
try {
|
||||
console.log(chalk.cyan("Generating descriptor set..."))
|
||||
execSync(descriptorProtocCommand, { stdio: "inherit" })
|
||||
} catch (error) {
|
||||
console.error(chalk.red("Error generating descriptor set for proto file:"), error)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log(chalk.green("Protocol Buffer code generation completed successfully."))
|
||||
|
||||
@@ -10,6 +10,7 @@ service McpService {
|
||||
rpc toggleMcpServer(ToggleMcpServerRequest) returns (McpServers);
|
||||
rpc updateMcpTimeout(UpdateMcpTimeoutRequest) returns (McpServers);
|
||||
rpc addRemoteMcpServer(AddRemoteMcpServerRequest) returns (McpServers);
|
||||
rpc downloadMcp(StringRequest) returns (Empty);
|
||||
}
|
||||
|
||||
message ToggleMcpServerRequest {
|
||||
|
||||
+6
-5
@@ -15,9 +15,11 @@ service ModelsService {
|
||||
// Fetches available models from VS Code LM API
|
||||
rpc getVsCodeLmModels(EmptyRequest) returns (VsCodeLmModelsArray);
|
||||
// Refreshes and returns OpenRouter models
|
||||
rpc refreshOpenRouterModels(EmptyRequest) returns (OpenRouterModels);
|
||||
rpc refreshOpenRouterModels(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
|
||||
// Refreshes and returns OpenAI models
|
||||
rpc refreshOpenAiModels(OpenAiModelsRequest) returns (StringArray);
|
||||
// Refreshes and returns Requesty models
|
||||
rpc refreshRequestyModels(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
|
||||
}
|
||||
|
||||
// List of VS Code LM models
|
||||
@@ -33,7 +35,7 @@ message VsCodeLmModel {
|
||||
string id = 4;
|
||||
}
|
||||
|
||||
// For ModelInfo structure in OpenRouterModels
|
||||
// For OpenRouterCompatibleModelInfo structure in OpenRouterModels
|
||||
message OpenRouterModelInfo {
|
||||
int32 max_tokens = 1;
|
||||
int32 context_window = 2;
|
||||
@@ -46,8 +48,8 @@ message OpenRouterModelInfo {
|
||||
string description = 9;
|
||||
}
|
||||
|
||||
// Response message for OpenRouter models
|
||||
message OpenRouterModels {
|
||||
// Shared response message for model information
|
||||
message OpenRouterCompatibleModelInfo {
|
||||
map<string, OpenRouterModelInfo> models = 1;
|
||||
}
|
||||
|
||||
@@ -57,4 +59,3 @@ message OpenAiModelsRequest {
|
||||
string baseUrl = 2;
|
||||
string apiKey = 3;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package cline;
|
||||
option java_package = "bot.cline.proto";
|
||||
option java_multiple_files = true;
|
||||
|
||||
import "common.proto";
|
||||
|
||||
// SlashService provides methods for managing slash
|
||||
service SlashService {
|
||||
// Sends button click message
|
||||
rpc reportBug(StringRequest) returns (Empty);
|
||||
rpc condense(StringRequest) returns (Empty);
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import "common.proto";
|
||||
service StateService {
|
||||
rpc getLatestState(EmptyRequest) returns (State);
|
||||
rpc subscribeToState(EmptyRequest) returns (stream State);
|
||||
rpc toggleFavoriteModel(StringRequest) returns (Empty);
|
||||
}
|
||||
|
||||
message State {
|
||||
|
||||
@@ -67,6 +67,7 @@ message GetTaskHistoryRequest {
|
||||
bool favorites_only = 2;
|
||||
string search_query = 3;
|
||||
string sort_by = 4;
|
||||
bool current_workspace_only = 5;
|
||||
}
|
||||
|
||||
// Response for task history
|
||||
|
||||
@@ -19,6 +19,7 @@ import { DoubaoHandler } from "./providers/doubao"
|
||||
import { VsCodeLmHandler } from "./providers/vscode-lm"
|
||||
import { ClineHandler } from "./providers/cline"
|
||||
import { LiteLlmHandler } from "./providers/litellm"
|
||||
import { FireworksHandler } from "./providers/fireworks"
|
||||
import { AskSageHandler } from "./providers/asksage"
|
||||
import { XAIHandler } from "./providers/xai"
|
||||
import { SambanovaHandler } from "./providers/sambanova"
|
||||
@@ -58,6 +59,8 @@ export function buildApiHandler(configuration: ApiConfiguration): ApiHandler {
|
||||
return new DeepSeekHandler(options)
|
||||
case "requesty":
|
||||
return new RequestyHandler(options)
|
||||
case "fireworks":
|
||||
return new FireworksHandler(options)
|
||||
case "together":
|
||||
return new TogetherHandler(options)
|
||||
case "qwen":
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import OpenAI from "openai"
|
||||
import { withRetry } from "../retry"
|
||||
import { ApiHandler } from ".."
|
||||
import {
|
||||
ApiHandlerOptions,
|
||||
DeepSeekModelId,
|
||||
ModelInfo,
|
||||
deepSeekDefaultModelId,
|
||||
deepSeekModels,
|
||||
openAiModelInfoSaneDefaults,
|
||||
} from "../../shared/api"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
|
||||
export class FireworksHandler implements ApiHandler {
|
||||
private options: ApiHandlerOptions
|
||||
private client: OpenAI
|
||||
|
||||
constructor(options: ApiHandlerOptions) {
|
||||
this.options = options
|
||||
this.client = new OpenAI({
|
||||
baseURL: "https://api.fireworks.ai/inference/v1",
|
||||
apiKey: this.options.fireworksApiKey,
|
||||
})
|
||||
}
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
const modelId = this.options.fireworksModelId ?? ""
|
||||
|
||||
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
||||
{ role: "system", content: systemPrompt },
|
||||
...convertToOpenAiMessages(messages),
|
||||
]
|
||||
|
||||
const stream = await this.client.chat.completions.create({
|
||||
model: modelId,
|
||||
...(this.options.fireworksModelMaxCompletionTokens
|
||||
? { max_completion_tokens: this.options.fireworksModelMaxCompletionTokens }
|
||||
: {}),
|
||||
...(this.options.fireworksModelMaxTokens ? { max_tokens: this.options.fireworksModelMaxTokens } : {}),
|
||||
messages: openAiMessages,
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
temperature: 0,
|
||||
})
|
||||
|
||||
let reasoning: string | null = null
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
if (reasoning || delta?.content?.includes("<think>")) {
|
||||
reasoning = (reasoning || "") + (delta.content ?? "")
|
||||
}
|
||||
|
||||
if (delta?.content && !reasoning) {
|
||||
yield {
|
||||
type: "text",
|
||||
text: delta.content,
|
||||
}
|
||||
}
|
||||
|
||||
if (reasoning || ("reasoning_content" in delta && delta.reasoning_content)) {
|
||||
yield {
|
||||
type: "reasoning",
|
||||
reasoning: delta.content || ((delta as any).reasoning_content as string | undefined) || "",
|
||||
}
|
||||
if (reasoning?.includes("</think>")) {
|
||||
// Reset so the next chunk is regular content
|
||||
reasoning = null
|
||||
}
|
||||
}
|
||||
|
||||
if (chunk.usage) {
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: chunk.usage.prompt_tokens || 0, // (deepseek reports total input AND cache reads/writes, see context caching: https://api-docs.deepseek.com/guides/kv_cache) where the input tokens is the sum of the cache hits/misses, while anthropic reports them as separate tokens. This is important to know for 1) context management truncation algorithm, and 2) cost calculation (NOTE: we report both input and cache stats but for now set input price to 0 since all the cost calculation will be done using cache hits/misses)
|
||||
outputTokens: chunk.usage.completion_tokens || 0,
|
||||
// @ts-ignore-next-line
|
||||
cacheReadTokens: chunk.usage.prompt_cache_hit_tokens || 0,
|
||||
// @ts-ignore-next-line
|
||||
cacheWriteTokens: chunk.usage.prompt_cache_miss_tokens || 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getModel(): { id: string; info: ModelInfo } {
|
||||
return {
|
||||
id: this.options.fireworksModelId ?? "",
|
||||
info: openAiModelInfoSaneDefaults,
|
||||
}
|
||||
}
|
||||
}
|
||||
+103
-44
@@ -1,11 +1,12 @@
|
||||
import type { Anthropic } from "@anthropic-ai/sdk"
|
||||
// Restore GenerateContentConfig import and add GenerateContentResponseUsageMetadata
|
||||
import { GoogleGenAI, type Content, type GenerateContentConfig, type GenerateContentResponseUsageMetadata } from "@google/genai"
|
||||
import { GoogleGenAI, type GenerateContentConfig, type GenerateContentResponseUsageMetadata } from "@google/genai"
|
||||
import { withRetry } from "../retry"
|
||||
import { ApiHandler } from "../"
|
||||
import { ApiHandlerOptions, geminiDefaultModelId, GeminiModelId, geminiModels, ModelInfo } from "@shared/api"
|
||||
import { convertAnthropicMessageToGemini } from "../transform/gemini-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
import { telemetryService } from "@services/posthog/telemetry/TelemetryService"
|
||||
|
||||
// Define a default TTL for the cache (e.g., 15 minutes in seconds)
|
||||
const DEFAULT_CACHE_TTL_SECONDS = 900
|
||||
@@ -74,7 +75,7 @@ export class GeminiHandler implements ApiHandler {
|
||||
*/
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
const { id: model, info } = this.getModel()
|
||||
const { id: modelId, info } = this.getModel()
|
||||
const contents = messages.map(convertAnthropicMessageToGemini)
|
||||
|
||||
// Configure thinking budget if supported
|
||||
@@ -98,52 +99,110 @@ export class GeminiHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
// Generate content using the configured parameters
|
||||
const result = await this.client.models.generateContentStream({
|
||||
model,
|
||||
contents: contents,
|
||||
config: {
|
||||
...requestConfig,
|
||||
},
|
||||
})
|
||||
|
||||
// Track usage metadata
|
||||
const sdkCallStartTime = Date.now()
|
||||
let sdkFirstChunkTime: number | undefined
|
||||
let ttftSdkMs: number | undefined
|
||||
let apiSuccess = false
|
||||
let apiError: string | undefined
|
||||
let promptTokens = 0
|
||||
let outputTokens = 0
|
||||
let cacheReadTokens = 0
|
||||
let lastUsageMetadata: GenerateContentResponseUsageMetadata | undefined
|
||||
|
||||
// Process the stream
|
||||
for await (const chunk of result) {
|
||||
if (chunk.text) {
|
||||
yield {
|
||||
type: "text",
|
||||
text: chunk.text,
|
||||
}
|
||||
}
|
||||
|
||||
if (chunk.usageMetadata) {
|
||||
lastUsageMetadata = chunk.usageMetadata
|
||||
}
|
||||
}
|
||||
|
||||
// Yield usage information at the end
|
||||
if (lastUsageMetadata) {
|
||||
const inputTokens = lastUsageMetadata.promptTokenCount ?? 0
|
||||
const outputTokens = lastUsageMetadata.candidatesTokenCount ?? 0
|
||||
const cacheReadTokens = lastUsageMetadata.cachedContentTokenCount
|
||||
|
||||
// Calculate immediate costs
|
||||
const totalCost = this.calculateCost({
|
||||
info,
|
||||
inputTokens,
|
||||
outputTokens,
|
||||
cacheReadTokens,
|
||||
try {
|
||||
const result = await this.client.models.generateContentStream({
|
||||
model: modelId,
|
||||
contents: contents,
|
||||
config: {
|
||||
...requestConfig,
|
||||
},
|
||||
})
|
||||
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens,
|
||||
outputTokens,
|
||||
cacheReadTokens,
|
||||
cacheWriteTokens: 0,
|
||||
totalCost,
|
||||
let isFirstSdkChunk = true
|
||||
for await (const chunk of result) {
|
||||
if (isFirstSdkChunk) {
|
||||
sdkFirstChunkTime = Date.now()
|
||||
ttftSdkMs = sdkFirstChunkTime - sdkCallStartTime
|
||||
isFirstSdkChunk = false
|
||||
}
|
||||
|
||||
if (chunk.text) {
|
||||
yield {
|
||||
type: "text",
|
||||
text: chunk.text,
|
||||
}
|
||||
}
|
||||
|
||||
if (chunk.usageMetadata) {
|
||||
lastUsageMetadata = chunk.usageMetadata
|
||||
promptTokens = lastUsageMetadata.promptTokenCount ?? promptTokens
|
||||
outputTokens = lastUsageMetadata.candidatesTokenCount ?? outputTokens
|
||||
cacheReadTokens = lastUsageMetadata.cachedContentTokenCount ?? cacheReadTokens
|
||||
}
|
||||
}
|
||||
apiSuccess = true
|
||||
|
||||
if (lastUsageMetadata) {
|
||||
const totalCost = this.calculateCost({
|
||||
info,
|
||||
inputTokens: promptTokens,
|
||||
outputTokens,
|
||||
cacheReadTokens,
|
||||
})
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: promptTokens,
|
||||
outputTokens,
|
||||
cacheReadTokens,
|
||||
cacheWriteTokens: 0,
|
||||
totalCost,
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
apiSuccess = false
|
||||
// Let the error propagate to be handled by withRetry or Task.ts
|
||||
// Telemetry will be sent in the finally block.
|
||||
if (error instanceof Error) {
|
||||
apiError = error.message
|
||||
|
||||
// Gemini doesn't include status codes in their errors
|
||||
// https://github.com/googleapis/js-genai/blob/61f7f27b866c74333ca6331883882489bcb708b9/src/_api_client.ts#L569
|
||||
if (error.name === "ClientError" && error.message.includes("got status: 429 Too Many Requests.")) {
|
||||
;(error as any).status = 429
|
||||
}
|
||||
} else {
|
||||
apiError = String(error)
|
||||
}
|
||||
|
||||
throw error
|
||||
} finally {
|
||||
const sdkCallEndTime = Date.now()
|
||||
const totalDurationSdkMs = sdkCallEndTime - sdkCallStartTime
|
||||
const cacheHit = cacheReadTokens > 0
|
||||
const cacheHitPercentage = promptTokens > 0 ? (cacheReadTokens / promptTokens) * 100 : undefined
|
||||
const throughputTokensPerSecSdk =
|
||||
totalDurationSdkMs > 0 && outputTokens > 0 ? outputTokens / (totalDurationSdkMs / 1000) : undefined
|
||||
|
||||
if (this.options.taskId) {
|
||||
telemetryService.captureGeminiApiPerformance(
|
||||
this.options.taskId,
|
||||
modelId,
|
||||
{
|
||||
ttftSec: ttftSdkMs !== undefined ? ttftSdkMs / 1000 : undefined,
|
||||
totalDurationSec: totalDurationSdkMs / 1000,
|
||||
promptTokens,
|
||||
outputTokens,
|
||||
cacheReadTokens,
|
||||
cacheHit,
|
||||
cacheHitPercentage,
|
||||
apiSuccess,
|
||||
apiError,
|
||||
throughputTokensPerSec: throughputTokensPerSecSdk,
|
||||
},
|
||||
true,
|
||||
)
|
||||
} else {
|
||||
console.warn("GeminiHandler: taskId not available for telemetry in createMessage.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ export class LiteLlmHandler implements ApiHandler {
|
||||
const reasoningOn = budgetTokens !== 0 ? true : false
|
||||
const thinkingConfig = reasoningOn ? { type: "enabled", budget_tokens: budgetTokens } : undefined
|
||||
|
||||
let temperature: number | undefined = 0
|
||||
let temperature: number | undefined = this.options.liteLlmModelInfo?.temperature ?? 0
|
||||
|
||||
if (isOminiModel && reasoningOn) {
|
||||
temperature = undefined // Thinking mode doesn't support temperature
|
||||
@@ -169,7 +169,7 @@ export class LiteLlmHandler implements ApiHandler {
|
||||
getModel() {
|
||||
return {
|
||||
id: this.options.liteLlmModelId || liteLlmDefaultModelId,
|
||||
info: liteLlmModelInfoSaneDefaults,
|
||||
info: this.options.liteLlmModelInfo || liteLlmModelInfoSaneDefaults,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,74 +75,6 @@ export async function createOpenRouterStream(
|
||||
break
|
||||
}
|
||||
|
||||
// handles gemini caching logic
|
||||
if (model.id.startsWith("google/") && model.info.supportsPromptCache) {
|
||||
// gemini only uses the last breakpoint for caching, so the others will be ignored
|
||||
openAiMessages[0] = {
|
||||
role: "system",
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: systemPrompt,
|
||||
// @ts-ignore-next-line
|
||||
cache_control: { type: "ephemeral" },
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
// for safety, but this should always be the case
|
||||
if (openAiMessages.length >= 2) {
|
||||
const msg = openAiMessages[1]
|
||||
|
||||
if (msg) {
|
||||
if (typeof msg.content === "string") {
|
||||
msg.content = [{ type: "text", text: msg.content }]
|
||||
}
|
||||
if (Array.isArray(msg.content)) {
|
||||
// NOTE: this is fine since env details will always be added at the end. but if it weren't there, and the user added a image_url type message, it would pop a text part before it and then move it after to the end.
|
||||
let lastTextPart = msg.content.filter((part) => part.type === "text").pop()
|
||||
|
||||
if (!lastTextPart) {
|
||||
lastTextPart = { type: "text", text: "..." }
|
||||
msg.content.push(lastTextPart)
|
||||
}
|
||||
// @ts-ignore-next-line
|
||||
lastTextPart["cache_control"] = { type: "ephemeral" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// it doesn't make sense to alter breakpoints at all with the gemini cache implementation at this time
|
||||
/*const GEMINI_CACHE_USER_MESSAGE_INTERVAL = 4 // add new breakpoint every 4 turns
|
||||
const userMessages = openAiMessages.filter((msg) => msg.role === "user")
|
||||
|
||||
const userMessageCount = userMessages.length
|
||||
const targetUserMessageNumber =
|
||||
Math.floor(userMessageCount / GEMINI_CACHE_USER_MESSAGE_INTERVAL) * GEMINI_CACHE_USER_MESSAGE_INTERVAL
|
||||
|
||||
if (targetUserMessageNumber > 0) {
|
||||
// otherwise dont need to add a breakpoint
|
||||
const msg = userMessages[targetUserMessageNumber - 1]
|
||||
|
||||
if (msg) {
|
||||
if (typeof msg.content === "string") {
|
||||
msg.content = [{ type: "text", text: msg.content }]
|
||||
}
|
||||
if (Array.isArray(msg.content)) {
|
||||
// NOTE: this is fine since env details will always be added at the end. but if it weren't there, and the user added a image_url type message, it would pop a text part before it and then move it after to the end.
|
||||
let lastTextPart = msg.content.filter((part) => part.type === "text").pop()
|
||||
|
||||
if (!lastTextPart) {
|
||||
lastTextPart = { type: "text", text: "..." }
|
||||
msg.content.push(lastTextPart)
|
||||
}
|
||||
// @ts-ignore-next-line
|
||||
lastTextPart["cache_control"] = { type: "ephemeral" }
|
||||
}
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
||||
// Not sure how openrouter defaults max tokens when no value is provided, but the anthropic api requires this value and since they offer both 4096 and 8192 variants, we should ensure 8192.
|
||||
// (models usually default to max tokens allowed)
|
||||
let maxTokens: number | undefined
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export type AssistantMessageContent = TextContent | ToolUse
|
||||
|
||||
export { parseAssistantMessage } from "./parse-assistant-message"
|
||||
export { parseAssistantMessageV1, parseAssistantMessageV2 } from "./parse-assistant-message"
|
||||
|
||||
export interface TextContent {
|
||||
type: "text"
|
||||
|
||||
@@ -1,6 +1,24 @@
|
||||
import { AssistantMessageContent, TextContent, ToolUse, ToolParamName, toolParamNames, toolUseNames, ToolUseName } from "."
|
||||
import { AssistantMessageContent, TextContent, ToolUse, ToolParamName, toolParamNames, toolUseNames, ToolUseName } from "." // Assuming types are defined in index.ts or a similar file
|
||||
|
||||
export function parseAssistantMessage(assistantMessage: string) {
|
||||
/**
|
||||
* @description **Version 1**
|
||||
* Parses an assistant message string potentially containing mixed text and tool usage blocks
|
||||
* marked with XML-like tags into an array of structured content objects.
|
||||
*
|
||||
* This version iterates through the message character by character, building an accumulator string.
|
||||
* It maintains state to track whether it's currently parsing text, a tool use block, or a specific tool parameter.
|
||||
* It detects the start and end of tool uses and parameters by checking if the accumulator ends with
|
||||
* the corresponding opening or closing tags.
|
||||
* Special handling is included for `write_to_file` and `new_rule` tool uses to correctly parse
|
||||
* the `content` parameter, which might contain the closing tag itself, by looking for the *last*
|
||||
* occurrence of the closing tag.
|
||||
* If the input string ends mid-tag or mid-content, the last block (text or tool use) is marked as partial.
|
||||
*
|
||||
* @param assistantMessage The raw string output from the assistant.
|
||||
* @returns An array of `AssistantMessageContent` objects, which can be `TextContent` or `ToolUse`.
|
||||
* Blocks that were not fully closed by the end of the input string will have their `partial` flag set to `true`.
|
||||
*/
|
||||
export function parseAssistantMessageV1(assistantMessage: string): AssistantMessageContent[] {
|
||||
const contentBlocks: AssistantMessageContent[] = []
|
||||
let currentTextContent: TextContent | undefined = undefined
|
||||
let currentTextContentStartIndex = 0
|
||||
@@ -14,46 +32,56 @@ export function parseAssistantMessage(assistantMessage: string) {
|
||||
const char = assistantMessage[i]
|
||||
accumulator += char
|
||||
|
||||
// --- State: Parsing a Tool Parameter ---
|
||||
// there should not be a param without a tool use
|
||||
if (currentToolUse && currentParamName) {
|
||||
const currentParamValue = accumulator.slice(currentParamValueStartIndex)
|
||||
const paramClosingTag = `</${currentParamName}>`
|
||||
if (currentParamValue.endsWith(paramClosingTag)) {
|
||||
// end of param value
|
||||
// End of param value found
|
||||
currentToolUse.params[currentParamName] = currentParamValue.slice(0, -paramClosingTag.length).trim()
|
||||
currentParamName = undefined
|
||||
continue
|
||||
currentParamName = undefined // Go back to parsing tool content or looking for next param
|
||||
continue // Move to next character
|
||||
} else {
|
||||
// partial param value is accumulating
|
||||
continue
|
||||
// Partial param value is accumulating
|
||||
continue // Move to next character
|
||||
}
|
||||
}
|
||||
|
||||
// --- State: Parsing a Tool Use (but not a specific parameter) ---
|
||||
// no currentParamName
|
||||
|
||||
if (currentToolUse) {
|
||||
const currentToolValue = accumulator.slice(currentToolUseStartIndex)
|
||||
const toolUseClosingTag = `</${currentToolUse.name}>`
|
||||
|
||||
if (currentToolValue.endsWith(toolUseClosingTag)) {
|
||||
// end of a tool use
|
||||
// End of a tool use found
|
||||
currentToolUse.partial = false
|
||||
contentBlocks.push(currentToolUse)
|
||||
currentToolUse = undefined
|
||||
continue
|
||||
currentToolUse = undefined // Go back to parsing text or looking for next tool
|
||||
// Reset text start index in case text follows immediately
|
||||
currentTextContentStartIndex = i + 1
|
||||
continue // Move to next character
|
||||
} else {
|
||||
// Check if starting a new parameter within the current tool use
|
||||
const possibleParamOpeningTags = toolParamNames.map((name) => `<${name}>`)
|
||||
let foundParamStart = false
|
||||
for (const paramOpeningTag of possibleParamOpeningTags) {
|
||||
if (accumulator.endsWith(paramOpeningTag)) {
|
||||
// start of a new parameter
|
||||
// Start of a new parameter found
|
||||
currentParamName = paramOpeningTag.slice(1, -1) as ToolParamName
|
||||
currentParamValueStartIndex = accumulator.length
|
||||
foundParamStart = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (foundParamStart) {
|
||||
continue // Move to next character
|
||||
}
|
||||
|
||||
// there's no current param, and not starting a new param
|
||||
|
||||
// special case for write_to_file where file contents could contain the closing tag, in which case the param would have closed and we end up with the rest of the file contents here. To work around this, we get the string between the starting content tag and the LAST content tag.
|
||||
// Special case for write_to_file/new_rule content param allowing nested tags
|
||||
// Check if a </content> tag appears, potentially indicating the end of the content param
|
||||
// even if the main tool closing tag hasn't been seen yet.
|
||||
const contentParamName: ToolParamName = "content"
|
||||
if (
|
||||
(currentToolUse.name === "write_to_file" || currentToolUse.name === "new_rule") &&
|
||||
@@ -63,73 +91,384 @@ export function parseAssistantMessage(assistantMessage: string) {
|
||||
const contentStartTag = `<${contentParamName}>`
|
||||
const contentEndTag = `</${contentParamName}>`
|
||||
const contentStartIndex = toolContent.indexOf(contentStartTag) + contentStartTag.length
|
||||
// Use lastIndexOf to handle cases where </content> might appear within the content itself
|
||||
const contentEndIndex = toolContent.lastIndexOf(contentEndTag)
|
||||
if (contentStartIndex !== -1 && contentEndIndex !== -1 && contentEndIndex > contentStartIndex) {
|
||||
currentToolUse.params[contentParamName] = toolContent.slice(contentStartIndex, contentEndIndex).trim()
|
||||
|
||||
// Ensure we found valid start/end tags and end is after start
|
||||
if (
|
||||
contentStartIndex !== -1 &&
|
||||
contentEndIndex !== -1 &&
|
||||
contentEndIndex > contentStartIndex - contentStartTag.length // Ensure end tag is after start tag begins
|
||||
) {
|
||||
// Check if this content param was already being parsed. If so, update it.
|
||||
// If not, and we just found the closing tag, assign it.
|
||||
// This handles cases where the </content> detection might fire before
|
||||
// the <content> tag detection logic, or if the content is very short.
|
||||
if (currentParamName === contentParamName) {
|
||||
// Already parsing content, now we found the end tag
|
||||
currentToolUse.params[contentParamName] = toolContent.slice(contentStartIndex, contentEndIndex).trim()
|
||||
currentParamName = undefined // Finished with this param
|
||||
} else if (currentParamName === undefined) {
|
||||
// Not parsing a param, but found </content>. Assume it closes the content block.
|
||||
currentToolUse.params[contentParamName] = toolContent.slice(contentStartIndex, contentEndIndex).trim()
|
||||
// We stay in the "parsing tool use" state, looking for more params or the tool end tag.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// partial tool value is accumulating
|
||||
continue
|
||||
// If none of the above, partial tool value is accumulating
|
||||
continue // Move to next character
|
||||
}
|
||||
}
|
||||
|
||||
// --- State: Parsing Text (or looking for start of a tool use) ---
|
||||
// no currentToolUse
|
||||
|
||||
let didStartToolUse = false
|
||||
const possibleToolUseOpeningTags = toolUseNames.map((name) => `<${name}>`)
|
||||
for (const toolUseOpeningTag of possibleToolUseOpeningTags) {
|
||||
if (accumulator.endsWith(toolUseOpeningTag)) {
|
||||
// start of a new tool use
|
||||
// Start of a new tool use found
|
||||
const toolName = toolUseOpeningTag.slice(1, -1) as ToolUseName
|
||||
currentToolUse = {
|
||||
type: "tool_use",
|
||||
name: toolUseOpeningTag.slice(1, -1) as ToolUseName,
|
||||
name: toolName,
|
||||
params: {},
|
||||
partial: true,
|
||||
}
|
||||
currentToolUseStartIndex = accumulator.length
|
||||
// this also indicates the end of the current text content
|
||||
|
||||
// This also indicates the end of the current text content block (if any)
|
||||
if (currentTextContent) {
|
||||
currentTextContent.partial = false
|
||||
// remove the partially accumulated tool use tag from the end of text (<tool)
|
||||
currentTextContent.content = currentTextContent.content
|
||||
.slice(0, -toolUseOpeningTag.slice(0, -1).length)
|
||||
.trim()
|
||||
contentBlocks.push(currentTextContent)
|
||||
// Extract text content, removing the part that formed the tool opening tag
|
||||
const textEndIndex = accumulator.length - toolUseOpeningTag.length
|
||||
currentTextContent.content = accumulator.slice(currentTextContentStartIndex, textEndIndex).trim()
|
||||
// Only add if there's actual content
|
||||
if (currentTextContent.content.length > 0) {
|
||||
contentBlocks.push(currentTextContent)
|
||||
}
|
||||
currentTextContent = undefined
|
||||
} else {
|
||||
// Check if there was text before this tool use started
|
||||
const textEndIndex = accumulator.length - toolUseOpeningTag.length
|
||||
const potentialText = accumulator.slice(currentTextContentStartIndex, textEndIndex).trim()
|
||||
if (potentialText.length > 0) {
|
||||
contentBlocks.push({
|
||||
type: "text",
|
||||
content: potentialText,
|
||||
partial: false, // Ended because tool use started
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
didStartToolUse = true
|
||||
break
|
||||
break // Found tool start, stop checking for others
|
||||
}
|
||||
}
|
||||
|
||||
if (!didStartToolUse) {
|
||||
// no tool use, so it must be text either at the beginning or between tools
|
||||
// No tool use started, so it must be text content accumulating
|
||||
// (or continuing after a closed tool use)
|
||||
if (currentTextContent === undefined) {
|
||||
currentTextContentStartIndex = i
|
||||
}
|
||||
currentTextContent = {
|
||||
type: "text",
|
||||
content: accumulator.slice(currentTextContentStartIndex).trim(),
|
||||
partial: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
// Start of a new text block
|
||||
currentTextContentStartIndex = i - (accumulator.length - currentTextContentStartIndex - 1) // Adjust start index based on how much we've accumulated since the last block ended or the beginning
|
||||
// If accumulator starts from 0, start index is i
|
||||
if (contentBlocks.length === 0 && currentToolUse === undefined) {
|
||||
currentTextContentStartIndex = accumulator.length - 1 // i
|
||||
} else {
|
||||
// Re-calculate based on the actual start of the current text segment
|
||||
// Find the end of the last block
|
||||
let lastBlockEndIndex = 0
|
||||
if (contentBlocks.length > 0) {
|
||||
const lastBlock = contentBlocks[contentBlocks.length - 1]
|
||||
// Approximation: find where the accumulator matches the end of the message string representation of the last block. This is complex.
|
||||
// Simpler: Assume text starts right after the last block ended implicitly at index i.
|
||||
lastBlockEndIndex = i // Where the loop *was* when the last block finished processing
|
||||
// Need a more robust way to track the end index of the *raw string* corresponding to the last block.
|
||||
// Let's stick to the accumulator slice approach for simplicity in this version.
|
||||
// The start index should be where the current *unmatched* text began.
|
||||
let lastProcessedIndex = -1
|
||||
if (contentBlocks.length > 0) {
|
||||
// This requires knowing the raw string length of the previous block, which V1 doesn't explicitly track easily.
|
||||
// We'll approximate based on the current accumulator and start index logic.
|
||||
// The issue arises if a tool tag was just closed. accumulator contains everything up to i.
|
||||
// lastBlockEndIndex should point to the character *after* the closing tag of the last block.
|
||||
}
|
||||
// Reset start index to the beginning of the *current* potential text block
|
||||
currentTextContentStartIndex = accumulator.length - 1 // Start accumulating from the current character `i`
|
||||
}
|
||||
|
||||
// If we just closed a tool, text starts *after* its closing tag
|
||||
// The logic needs refinement here for accurate start index after a tool closure.
|
||||
// Let's assume for now the start index logic inside the loop handles it via slicing.
|
||||
}
|
||||
|
||||
currentTextContent = {
|
||||
type: "text",
|
||||
content: "", // Content will be filled by slicing accumulator
|
||||
partial: true,
|
||||
}
|
||||
}
|
||||
// Update text content based on the accumulator from its start index
|
||||
currentTextContent.content = accumulator.slice(currentTextContentStartIndex).trimStart() // Trim start to avoid leading space if text follows tool
|
||||
}
|
||||
} // End of loop
|
||||
|
||||
// --- Finalization after loop ---
|
||||
|
||||
// If a tool use was open at the end
|
||||
if (currentToolUse) {
|
||||
// stream did not complete tool call, add it as partial
|
||||
// If a parameter was open within that tool use
|
||||
if (currentParamName) {
|
||||
// tool call has a parameter that was not completed
|
||||
// The remaining accumulator content belongs to this partial parameter
|
||||
currentToolUse.params[currentParamName] = accumulator.slice(currentParamValueStartIndex).trim()
|
||||
}
|
||||
// Add the potentially partial tool use block
|
||||
contentBlocks.push(currentToolUse)
|
||||
}
|
||||
|
||||
// Note: it doesn't matter if check for currentToolUse or currentTextContent, only one of them will be defined since only one can be partial at a time
|
||||
if (currentTextContent) {
|
||||
// stream did not complete text content, add it as partial
|
||||
contentBlocks.push(currentTextContent)
|
||||
// If text content was being accumulated at the end
|
||||
// Note: Only one of currentToolUse or currentTextContent can be defined here,
|
||||
// as starting a tool use finalizes the preceding text block.
|
||||
else if (currentTextContent) {
|
||||
// Update content one last time
|
||||
currentTextContent.content = accumulator.slice(currentTextContentStartIndex).trim()
|
||||
// Add the potentially partial text block only if it contains content
|
||||
if (currentTextContent.content.length > 0) {
|
||||
contentBlocks.push(currentTextContent)
|
||||
}
|
||||
}
|
||||
|
||||
return contentBlocks
|
||||
}
|
||||
|
||||
/**
|
||||
* @description **Version 2**
|
||||
* Parses an assistant message string potentially containing mixed text and tool usage blocks
|
||||
* marked with XML-like tags into an array of structured content objects.
|
||||
*
|
||||
* This version aims for efficiency by avoiding the character-by-character accumulator of V1.
|
||||
* It iterates through the string using an index `i`. At each position, it checks if the substring
|
||||
* *ending* at `i` matches any known opening or closing tags for tools or parameters using `startsWith`
|
||||
* with an offset.
|
||||
* It uses pre-computed Maps (`toolUseOpenTags`, `toolParamOpenTags`) for quick tag lookups.
|
||||
* State is managed using indices (`currentTextContentStart`, `currentToolUseStart`, `currentParamValueStart`)
|
||||
* pointing to the start of the current block within the original `assistantMessage` string.
|
||||
* Slicing is used to extract content only when a block (text, parameter, or tool use) is completed.
|
||||
* Special handling for `write_to_file` and `new_rule` content parameters is included, using `indexOf`
|
||||
* and `lastIndexOf` on the relevant slice to handle potentially nested closing tags.
|
||||
* If the input string ends mid-block, the last open block is added and marked as partial.
|
||||
*
|
||||
* @param assistantMessage The raw string output from the assistant.
|
||||
* @returns An array of `AssistantMessageContent` objects, which can be `TextContent` or `ToolUse`.
|
||||
* Blocks that were not fully closed by the end of the input string will have their `partial` flag set to `true`.
|
||||
*/
|
||||
export function parseAssistantMessageV2(assistantMessage: string): AssistantMessageContent[] {
|
||||
const contentBlocks: AssistantMessageContent[] = []
|
||||
let currentTextContentStart = 0 // Index where the current text block started
|
||||
let currentTextContent: TextContent | undefined = undefined
|
||||
let currentToolUseStart = 0 // Index *after* the opening tag of the current tool use
|
||||
let currentToolUse: ToolUse | undefined = undefined
|
||||
let currentParamValueStart = 0 // Index *after* the opening tag of the current param
|
||||
let currentParamName: ToolParamName | undefined = undefined
|
||||
|
||||
// Precompute tags for faster lookups
|
||||
const toolUseOpenTags = new Map<string, ToolUseName>()
|
||||
const toolParamOpenTags = new Map<string, ToolParamName>()
|
||||
for (const name of toolUseNames) {
|
||||
toolUseOpenTags.set(`<${name}>`, name)
|
||||
}
|
||||
for (const name of toolParamNames) {
|
||||
toolParamOpenTags.set(`<${name}>`, name)
|
||||
}
|
||||
|
||||
const len = assistantMessage.length
|
||||
for (let i = 0; i < len; i++) {
|
||||
const currentCharIndex = i
|
||||
|
||||
// --- State: Parsing a Tool Parameter ---
|
||||
if (currentToolUse && currentParamName) {
|
||||
const closeTag = `</${currentParamName}>`
|
||||
// Check if the string *ending* at index `i` matches the closing tag
|
||||
if (
|
||||
currentCharIndex >= closeTag.length - 1 &&
|
||||
assistantMessage.startsWith(
|
||||
closeTag,
|
||||
currentCharIndex - closeTag.length + 1, // Start checking from potential start of tag
|
||||
)
|
||||
) {
|
||||
// Found the closing tag for the parameter
|
||||
const value = assistantMessage
|
||||
.slice(
|
||||
currentParamValueStart, // Start after the opening tag
|
||||
currentCharIndex - closeTag.length + 1, // End before the closing tag
|
||||
)
|
||||
.trim()
|
||||
currentToolUse.params[currentParamName] = value
|
||||
currentParamName = undefined // Go back to parsing tool content
|
||||
// We don't continue loop here, need to check for tool close or other params at index i
|
||||
} else {
|
||||
continue // Still inside param value, move to next char
|
||||
}
|
||||
}
|
||||
|
||||
// --- State: Parsing a Tool Use (but not a specific parameter) ---
|
||||
if (currentToolUse && !currentParamName) {
|
||||
// Ensure we are not inside a parameter already
|
||||
// Check if starting a new parameter
|
||||
let startedNewParam = false
|
||||
for (const [tag, paramName] of toolParamOpenTags.entries()) {
|
||||
if (currentCharIndex >= tag.length - 1 && assistantMessage.startsWith(tag, currentCharIndex - tag.length + 1)) {
|
||||
currentParamName = paramName
|
||||
currentParamValueStart = currentCharIndex + 1 // Value starts after the tag
|
||||
startedNewParam = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (startedNewParam) {
|
||||
continue // Handled start of param, move to next char
|
||||
}
|
||||
|
||||
// Check if closing the current tool use
|
||||
const toolCloseTag = `</${currentToolUse.name}>`
|
||||
if (
|
||||
currentCharIndex >= toolCloseTag.length - 1 &&
|
||||
assistantMessage.startsWith(toolCloseTag, currentCharIndex - toolCloseTag.length + 1)
|
||||
) {
|
||||
// End of the tool use found
|
||||
// Special handling for content params *before* finalizing the tool
|
||||
const toolContentSlice = assistantMessage.slice(
|
||||
currentToolUseStart, // From after the tool opening tag
|
||||
currentCharIndex - toolCloseTag.length + 1, // To before the tool closing tag
|
||||
)
|
||||
|
||||
// Check if content parameter needs special handling (write_to_file/new_rule)
|
||||
// This check is important if the closing </content> tag was missed by the parameter parsing logic
|
||||
// (e.g., if content is empty or parsing logic prioritizes tool close)
|
||||
const contentParamName: ToolParamName = "content"
|
||||
if (
|
||||
currentToolUse.name === "write_to_file" /* || currentToolUse.name === "new_rule" */ &&
|
||||
toolContentSlice.includes(`<${contentParamName}>`)
|
||||
) {
|
||||
const contentStartTag = `<${contentParamName}>`
|
||||
const contentEndTag = `</${contentParamName}>`
|
||||
const contentStart = toolContentSlice.indexOf(contentStartTag)
|
||||
// Use lastIndexOf for robustness against nested tags
|
||||
const contentEnd = toolContentSlice.lastIndexOf(contentEndTag)
|
||||
|
||||
if (contentStart !== -1 && contentEnd !== -1 && contentEnd > contentStart) {
|
||||
const contentValue = toolContentSlice.slice(contentStart + contentStartTag.length, contentEnd).trim()
|
||||
currentToolUse.params[contentParamName] = contentValue
|
||||
}
|
||||
}
|
||||
|
||||
currentToolUse.partial = false // Mark as complete
|
||||
contentBlocks.push(currentToolUse)
|
||||
currentToolUse = undefined // Reset state
|
||||
currentTextContentStart = currentCharIndex + 1 // Potential text starts after this tag
|
||||
continue // Move to next char
|
||||
}
|
||||
// If not starting a param and not closing the tool, continue accumulating tool content implicitly
|
||||
continue
|
||||
}
|
||||
|
||||
// --- State: Parsing Text / Looking for Tool Start ---
|
||||
if (!currentToolUse) {
|
||||
// Check if starting a new tool use
|
||||
let startedNewTool = false
|
||||
for (const [tag, toolName] of toolUseOpenTags.entries()) {
|
||||
if (currentCharIndex >= tag.length - 1 && assistantMessage.startsWith(tag, currentCharIndex - tag.length + 1)) {
|
||||
// End current text block if one was active
|
||||
if (currentTextContent) {
|
||||
currentTextContent.content = assistantMessage
|
||||
.slice(
|
||||
currentTextContentStart, // From where text started
|
||||
currentCharIndex - tag.length + 1, // To before the tool tag starts
|
||||
)
|
||||
.trim()
|
||||
currentTextContent.partial = false // Ended because tool started
|
||||
if (currentTextContent.content.length > 0) {
|
||||
contentBlocks.push(currentTextContent)
|
||||
}
|
||||
currentTextContent = undefined
|
||||
} else {
|
||||
// Check for any text between the last block and this tag
|
||||
const potentialText = assistantMessage
|
||||
.slice(
|
||||
currentTextContentStart, // From where text *might* have started
|
||||
currentCharIndex - tag.length + 1, // To before the tool tag starts
|
||||
)
|
||||
.trim()
|
||||
if (potentialText.length > 0) {
|
||||
contentBlocks.push({
|
||||
type: "text",
|
||||
content: potentialText,
|
||||
partial: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Start the new tool use
|
||||
currentToolUse = {
|
||||
type: "tool_use",
|
||||
name: toolName,
|
||||
params: {},
|
||||
partial: true, // Assume partial until closing tag is found
|
||||
}
|
||||
currentToolUseStart = currentCharIndex + 1 // Tool content starts after the opening tag
|
||||
startedNewTool = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (startedNewTool) {
|
||||
continue // Handled start of tool, move to next char
|
||||
}
|
||||
|
||||
// If not starting a tool, it must be text content
|
||||
if (!currentTextContent) {
|
||||
// Start a new text block if we aren't already in one
|
||||
currentTextContentStart = currentCharIndex // Text starts at the current character
|
||||
// Check if the current char is the start of potential text *immediately* after a tag
|
||||
// This needs the previous state - simpler to let slicing handle it later.
|
||||
// Resetting start index accurately is key.
|
||||
// It should be the index *after* the last processed tag.
|
||||
// The logic managing currentTextContentStart after closing tags handles this.
|
||||
|
||||
currentTextContent = {
|
||||
type: "text",
|
||||
content: "", // Will be determined by slicing at the end or when a tool starts
|
||||
partial: true,
|
||||
}
|
||||
}
|
||||
// Continue accumulating text implicitly; content is extracted later.
|
||||
}
|
||||
} // End of loop
|
||||
|
||||
// --- Finalization after loop ---
|
||||
|
||||
// Finalize any open parameter within an open tool use
|
||||
if (currentToolUse && currentParamName) {
|
||||
currentToolUse.params[currentParamName] = assistantMessage
|
||||
.slice(currentParamValueStart) // From param start to end of string
|
||||
.trim()
|
||||
// Tool use remains partial
|
||||
}
|
||||
|
||||
// Finalize any open tool use (which might contain the finalized partial param)
|
||||
if (currentToolUse) {
|
||||
// Tool use is partial because the loop finished before its closing tag
|
||||
contentBlocks.push(currentToolUse)
|
||||
}
|
||||
// Finalize any trailing text content
|
||||
// Only possible if a tool use wasn't open at the very end
|
||||
else if (currentTextContent) {
|
||||
currentTextContent.content = assistantMessage
|
||||
.slice(currentTextContentStart) // From text start to end of string
|
||||
.trim()
|
||||
// Text is partial because the loop finished
|
||||
if (currentTextContent.content.length > 0) {
|
||||
contentBlocks.push(currentTextContent)
|
||||
}
|
||||
}
|
||||
|
||||
return contentBlocks
|
||||
|
||||
@@ -2,6 +2,7 @@ import * as vscode from "vscode"
|
||||
import crypto from "crypto"
|
||||
import { Controller } from "../index"
|
||||
import { storeSecret } from "../../storage/state"
|
||||
import { EmptyRequest, String } from "../../../shared/proto/common"
|
||||
|
||||
/**
|
||||
* Handles the user clicking the login link in the UI.
|
||||
@@ -11,7 +12,7 @@ import { storeSecret } from "../../storage/state"
|
||||
* @param controller The controller instance.
|
||||
* @returns The login URL as a string.
|
||||
*/
|
||||
export async function accountLoginClicked(controller: Controller): Promise<String> {
|
||||
export async function accountLoginClicked(controller: Controller, unused: EmptyRequest): Promise<String> {
|
||||
// Generate nonce for state validation
|
||||
const nonce = crypto.randomBytes(32).toString("hex")
|
||||
await storeSecret(controller.context, "authNonce", nonce)
|
||||
@@ -25,6 +26,8 @@ export async function accountLoginClicked(controller: Controller): Promise<Strin
|
||||
const authUrl = vscode.Uri.parse(
|
||||
`https://app.cline.bot/auth?state=${encodeURIComponent(nonce)}&callback_url=${encodeURIComponent(`${uriScheme || "vscode"}://saoudrizwan.claude-dev/auth`)}`,
|
||||
)
|
||||
vscode.env.openExternal(authUrl)
|
||||
return authUrl.toString()
|
||||
await vscode.env.openExternal(authUrl)
|
||||
return {
|
||||
value: authUrl.toString(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { UpdateBrowserSettingsRequest } from "../../../shared/proto/browser"
|
||||
import { Boolean } from "../../../shared/proto/common"
|
||||
import { Controller } from "../index"
|
||||
import { updateGlobalState } from "../../storage/state"
|
||||
import { BrowserSettings as SharedBrowserSettings } from "../../../shared/BrowserSettings"
|
||||
import { updateGlobalState, getGlobalState } from "../../storage/state"
|
||||
import { BrowserSettings as SharedBrowserSettings, DEFAULT_BROWSER_SETTINGS } from "../../../shared/BrowserSettings"
|
||||
|
||||
/**
|
||||
* Update browser settings
|
||||
@@ -12,23 +12,39 @@ import { BrowserSettings as SharedBrowserSettings } from "../../../shared/Browse
|
||||
*/
|
||||
export async function updateBrowserSettings(controller: Controller, request: UpdateBrowserSettingsRequest): Promise<Boolean> {
|
||||
try {
|
||||
// Convert from protobuf format to shared format
|
||||
const browserSettings: SharedBrowserSettings = {
|
||||
// Get current browser settings to preserve fields not in the request
|
||||
const currentSettings = (await getGlobalState(controller.context, "browserSettings")) as SharedBrowserSettings | undefined
|
||||
const mergedWithDefaults = { ...DEFAULT_BROWSER_SETTINGS, ...currentSettings }
|
||||
|
||||
// Convert from protobuf format to shared format, merging with existing settings
|
||||
const newBrowserSettings: SharedBrowserSettings = {
|
||||
...mergedWithDefaults, // Start with existing settings (and defaults)
|
||||
viewport: {
|
||||
width: request.viewport?.width || 900,
|
||||
height: request.viewport?.height || 600,
|
||||
// Apply updates from request
|
||||
width: request.viewport?.width || mergedWithDefaults.viewport.width,
|
||||
height: request.viewport?.height || mergedWithDefaults.viewport.height,
|
||||
},
|
||||
remoteBrowserEnabled: request.remoteBrowserEnabled || false,
|
||||
remoteBrowserHost: request.remoteBrowserHost || undefined,
|
||||
// Explicitly handle optional boolean and string fields from the request
|
||||
remoteBrowserEnabled:
|
||||
request.remoteBrowserEnabled === undefined
|
||||
? mergedWithDefaults.remoteBrowserEnabled
|
||||
: request.remoteBrowserEnabled,
|
||||
remoteBrowserHost:
|
||||
request.remoteBrowserHost === undefined ? mergedWithDefaults.remoteBrowserHost : request.remoteBrowserHost,
|
||||
chromeExecutablePath:
|
||||
// If chromeExecutablePath is explicitly in the request (even as ""), use it.
|
||||
// Otherwise, fall back to mergedWithDefaults.
|
||||
"chromeExecutablePath" in request ? request.chromeExecutablePath : mergedWithDefaults.chromeExecutablePath,
|
||||
disableToolUse: request.disableToolUse === undefined ? mergedWithDefaults.disableToolUse : request.disableToolUse,
|
||||
}
|
||||
|
||||
// Update global state with new settings
|
||||
await updateGlobalState(controller.context, "browserSettings", browserSettings)
|
||||
await updateGlobalState(controller.context, "browserSettings", newBrowserSettings)
|
||||
|
||||
// Update task browser settings if task exists
|
||||
if (controller.task) {
|
||||
controller.task.browserSettings = browserSettings
|
||||
controller.task.browserSession.browserSettings = browserSettings
|
||||
controller.task.browserSettings = newBrowserSettings
|
||||
controller.task.browserSession.browserSettings = newBrowserSettings
|
||||
}
|
||||
|
||||
// Post updated state to webview
|
||||
|
||||
@@ -12,6 +12,7 @@ import { handleStateServiceRequest, handleStateServiceStreamingRequest } from ".
|
||||
import { handleTaskServiceRequest, handleTaskServiceStreamingRequest } from "./task/index"
|
||||
import { handleWebServiceRequest, handleWebServiceStreamingRequest } from "./web/index"
|
||||
import { handleModelsServiceRequest, handleModelsServiceStreamingRequest } from "./models/index"
|
||||
import { handleSlashServiceRequest, handleSlashServiceStreamingRequest } from "./slash/index"
|
||||
|
||||
/**
|
||||
* Configuration for a service handler
|
||||
@@ -67,4 +68,8 @@ export const serviceHandlers: Record<string, ServiceHandlerConfig> = {
|
||||
requestHandler: handleModelsServiceRequest,
|
||||
streamingHandler: handleModelsServiceStreamingRequest,
|
||||
},
|
||||
"cline.SlashService": {
|
||||
requestHandler: handleSlashServiceRequest,
|
||||
streamingHandler: handleSlashServiceStreamingRequest,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -249,7 +249,7 @@ export class Controller {
|
||||
// If user already opted in to telemetry, enable telemetry service
|
||||
this.getStateToPostToWebview().then((state) => {
|
||||
const { telemetrySetting } = state
|
||||
const isOptedIn = telemetrySetting === "enabled"
|
||||
const isOptedIn = telemetrySetting !== "disabled"
|
||||
telemetryService.updateTelemetryState(isOptedIn)
|
||||
})
|
||||
break
|
||||
@@ -271,12 +271,6 @@ export class Controller {
|
||||
// initializing new instance of Cline will make sure that any agentically running promises in old instance don't affect our new task. this essentially creates a fresh slate for the new task
|
||||
await this.initTask(message.text, message.images)
|
||||
break
|
||||
case "condense":
|
||||
this.task?.handleWebviewAskResponse("yesButtonClicked")
|
||||
break
|
||||
case "reportBug":
|
||||
this.task?.handleWebviewAskResponse("yesButtonClicked")
|
||||
break
|
||||
case "apiConfiguration":
|
||||
if (message.apiConfiguration) {
|
||||
await updateApiConfiguration(this.context, message.apiConfiguration)
|
||||
@@ -334,9 +328,6 @@ export class Controller {
|
||||
case "resetState":
|
||||
await this.resetState()
|
||||
break
|
||||
case "refreshRequestyModels":
|
||||
await this.refreshRequestyModels()
|
||||
break
|
||||
case "refreshClineRules":
|
||||
await refreshClineRulesToggles(this.context, cwd)
|
||||
await refreshExternalRulesToggles(this.context, cwd)
|
||||
@@ -382,19 +373,6 @@ export class Controller {
|
||||
await this.fetchMcpMarketplace(message.bool)
|
||||
break
|
||||
}
|
||||
case "downloadMcp": {
|
||||
if (message.mcpId) {
|
||||
// 1. Toggle to act mode if we are in plan mode
|
||||
const { chatSettings } = await this.getStateToPostToWebview()
|
||||
if (chatSettings.mode === "plan") {
|
||||
await this.togglePlanActModeWithChatSettings({ mode: "act" })
|
||||
}
|
||||
|
||||
// 2. download MCP
|
||||
await this.downloadMcp(message.mcpId)
|
||||
}
|
||||
break
|
||||
}
|
||||
case "silentlyRefreshMcpMarketplace": {
|
||||
await this.silentlyRefreshMcpMarketplace()
|
||||
break
|
||||
@@ -612,27 +590,6 @@ export class Controller {
|
||||
this.postMessageToWebview({ type: "relinquishControl" })
|
||||
break
|
||||
}
|
||||
case "toggleFavoriteModel": {
|
||||
if (message.modelId) {
|
||||
const { apiConfiguration } = await getAllExtensionState(this.context)
|
||||
const favoritedModelIds = apiConfiguration.favoritedModelIds || []
|
||||
|
||||
// Toggle favorite status
|
||||
const updatedFavorites = favoritedModelIds.includes(message.modelId)
|
||||
? favoritedModelIds.filter((id) => id !== message.modelId)
|
||||
: [...favoritedModelIds, message.modelId]
|
||||
|
||||
await updateGlobalState(this.context, "favoritedModelIds", updatedFavorites)
|
||||
|
||||
// Capture telemetry for model favorite toggle
|
||||
const isFavorited = !favoritedModelIds.includes(message.modelId)
|
||||
telemetryService.captureModelFavoritesUsage(message.modelId, isFavorited)
|
||||
|
||||
// Post state to webview without changing any other configuration
|
||||
await this.postStateToWebview()
|
||||
}
|
||||
break
|
||||
}
|
||||
case "grpc_request": {
|
||||
if (message.grpc_request) {
|
||||
await handleGrpcRequest(this, message.grpc_request)
|
||||
@@ -676,7 +633,7 @@ export class Controller {
|
||||
|
||||
async updateTelemetrySetting(telemetrySetting: TelemetrySetting) {
|
||||
await updateGlobalState(this.context, "telemetrySetting", telemetrySetting)
|
||||
const isOptedIn = telemetrySetting === "enabled"
|
||||
const isOptedIn = telemetrySetting !== "disabled"
|
||||
telemetryService.updateTelemetryState(isOptedIn)
|
||||
}
|
||||
|
||||
@@ -756,6 +713,7 @@ export class Controller {
|
||||
break
|
||||
case "litellm":
|
||||
await updateGlobalState(this.context, "previousModeModelId", apiConfiguration.liteLlmModelId)
|
||||
await updateGlobalState(this.context, "previousModeModelInfo", apiConfiguration.liteLlmModelInfo)
|
||||
break
|
||||
case "requesty":
|
||||
await updateGlobalState(this.context, "previousModeModelId", apiConfiguration.requestyModelId)
|
||||
@@ -809,7 +767,8 @@ export class Controller {
|
||||
await updateGlobalState(this.context, "lmStudioModelId", newModelId)
|
||||
break
|
||||
case "litellm":
|
||||
await updateGlobalState(this.context, "liteLlmModelId", newModelId)
|
||||
await updateGlobalState(this.context, "previousModeModelId", apiConfiguration.liteLlmModelId)
|
||||
await updateGlobalState(this.context, "previousModeModelInfo", apiConfiguration.liteLlmModelInfo)
|
||||
break
|
||||
case "requesty":
|
||||
await updateGlobalState(this.context, "requestyModelId", newModelId)
|
||||
@@ -1028,92 +987,6 @@ export class Controller {
|
||||
}
|
||||
}
|
||||
|
||||
private async downloadMcp(mcpId: string) {
|
||||
try {
|
||||
// First check if we already have this MCP server installed
|
||||
const servers = this.mcpHub?.getServers() || []
|
||||
const isInstalled = servers.some((server: McpServer) => server.name === mcpId)
|
||||
|
||||
if (isInstalled) {
|
||||
throw new Error("This MCP server is already installed")
|
||||
}
|
||||
|
||||
// Fetch server details from marketplace
|
||||
const response = await axios.post<McpDownloadResponse>(
|
||||
"https://api.cline.bot/v1/mcp/download",
|
||||
{ mcpId },
|
||||
{
|
||||
headers: { "Content-Type": "application/json" },
|
||||
timeout: 10000,
|
||||
},
|
||||
)
|
||||
|
||||
if (!response.data) {
|
||||
throw new Error("Invalid response from MCP marketplace API")
|
||||
}
|
||||
|
||||
console.log("[downloadMcp] Response from download API", { response })
|
||||
|
||||
const mcpDetails = response.data
|
||||
|
||||
// Validate required fields
|
||||
if (!mcpDetails.githubUrl) {
|
||||
throw new Error("Missing GitHub URL in MCP download response")
|
||||
}
|
||||
if (!mcpDetails.readmeContent) {
|
||||
throw new Error("Missing README content in MCP download response")
|
||||
}
|
||||
|
||||
// Send details to webview
|
||||
await this.postMessageToWebview({
|
||||
type: "mcpDownloadDetails",
|
||||
mcpDownloadDetails: mcpDetails,
|
||||
})
|
||||
|
||||
// Create task with context from README and added guidelines for MCP server installation
|
||||
const task = `Set up the MCP server from ${mcpDetails.githubUrl} while adhering to these MCP server installation rules:
|
||||
- Start by loading the MCP documentation.
|
||||
- Use "${mcpDetails.mcpId}" as the server name in cline_mcp_settings.json.
|
||||
- Create the directory for the new MCP server before starting installation.
|
||||
- Make sure you read the user's existing cline_mcp_settings.json file before editing it with this new mcp, to not overwrite any existing servers.
|
||||
- Use commands aligned with the user's shell and operating system best practices.
|
||||
- The following README may contain instructions that conflict with the user's OS, in which case proceed thoughtfully.
|
||||
- Once installed, demonstrate the server's capabilities by using one of its tools.
|
||||
Here is the project's README to help you get started:\n\n${mcpDetails.readmeContent}\n${mcpDetails.llmsInstallationContent}`
|
||||
|
||||
// Initialize task and show chat view
|
||||
await this.initTask(task)
|
||||
await this.postMessageToWebview({
|
||||
type: "action",
|
||||
action: "chatButtonClicked",
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Failed to download MCP:", error)
|
||||
let errorMessage = "Failed to download MCP"
|
||||
|
||||
if (axios.isAxiosError(error)) {
|
||||
if (error.code === "ECONNABORTED") {
|
||||
errorMessage = "Request timed out. Please try again."
|
||||
} else if (error.response?.status === 404) {
|
||||
errorMessage = "MCP server not found in marketplace."
|
||||
} else if (error.response?.status === 500) {
|
||||
errorMessage = "Internal server error. Please try again later."
|
||||
} else if (!error.response && error.request) {
|
||||
errorMessage = "Network error. Please check your internet connection."
|
||||
}
|
||||
} else if (error instanceof Error) {
|
||||
errorMessage = error.message
|
||||
}
|
||||
|
||||
// Show error in both notification and marketplace UI
|
||||
vscode.window.showErrorMessage(errorMessage)
|
||||
await this.postMessageToWebview({
|
||||
type: "mcpDownloadDetails",
|
||||
error: errorMessage,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// OpenRouter
|
||||
|
||||
async handleOpenRouterCallback(code: string) {
|
||||
@@ -1149,6 +1022,7 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
|
||||
return cacheDir
|
||||
}
|
||||
|
||||
// Read OpenRouter models from disk cache
|
||||
async readOpenRouterModels(): Promise<Record<string, ModelInfo> | undefined> {
|
||||
const openRouterModelsFilePath = path.join(await this.ensureCacheDirectoryExists(), GlobalFileNames.openRouterModels)
|
||||
const fileExists = await fileExistsAtPath(openRouterModelsFilePath)
|
||||
@@ -1159,51 +1033,6 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
|
||||
return undefined
|
||||
}
|
||||
|
||||
async refreshRequestyModels() {
|
||||
const parsePrice = (price: any) => {
|
||||
if (price) {
|
||||
return parseFloat(price) * 1_000_000
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
let models: Record<string, ModelInfo> = {}
|
||||
try {
|
||||
const apiKey = await getSecret(this.context, "requestyApiKey")
|
||||
const headers = {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
}
|
||||
const response = await axios.get("https://router.requesty.ai/v1/models", { headers })
|
||||
if (response.data?.data) {
|
||||
for (const model of response.data.data) {
|
||||
const modelInfo: ModelInfo = {
|
||||
maxTokens: model.max_output_tokens || undefined,
|
||||
contextWindow: model.context_window,
|
||||
supportsImages: model.supports_vision || undefined,
|
||||
supportsPromptCache: model.supports_caching || undefined,
|
||||
inputPrice: parsePrice(model.input_price),
|
||||
outputPrice: parsePrice(model.output_price),
|
||||
cacheWritesPrice: parsePrice(model.caching_price),
|
||||
cacheReadsPrice: parsePrice(model.cached_price),
|
||||
description: model.description,
|
||||
}
|
||||
models[model.id] = modelInfo
|
||||
}
|
||||
console.log("Requesty models fetched", models)
|
||||
} else {
|
||||
console.error("Invalid response from Requesty API")
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching Requesty models:", error)
|
||||
}
|
||||
|
||||
await this.postMessageToWebview({
|
||||
type: "requestyModels",
|
||||
requestyModels: models,
|
||||
})
|
||||
return models
|
||||
}
|
||||
|
||||
// Context menus and code actions
|
||||
|
||||
getFileMentionFromPath(filePath: string) {
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
import { Controller } from ".."
|
||||
import { Empty, StringRequest } from "../../../shared/proto/common"
|
||||
import { McpServer, McpDownloadResponse } from "@shared/mcp"
|
||||
import axios from "axios"
|
||||
import * as vscode from "vscode"
|
||||
|
||||
/**
|
||||
* Download an MCP server from the marketplace
|
||||
* @param controller The controller instance
|
||||
* @param request The request containing the MCP ID
|
||||
* @returns Empty response
|
||||
*/
|
||||
export async function downloadMcp(controller: Controller, request: StringRequest): Promise<Empty> {
|
||||
try {
|
||||
// Check if mcpId is provided
|
||||
if (!request.value) {
|
||||
throw new Error("MCP ID is required")
|
||||
}
|
||||
|
||||
const mcpId = request.value
|
||||
|
||||
// Check if we already have this MCP server installed
|
||||
const servers = controller.mcpHub?.getServers() || []
|
||||
const isInstalled = servers.some((server: McpServer) => server.name === mcpId)
|
||||
|
||||
if (isInstalled) {
|
||||
throw new Error("This MCP server is already installed")
|
||||
}
|
||||
|
||||
// Fetch server details from marketplace
|
||||
const response = await axios.post<McpDownloadResponse>(
|
||||
"https://api.cline.bot/v1/mcp/download",
|
||||
{ mcpId },
|
||||
{
|
||||
headers: { "Content-Type": "application/json" },
|
||||
timeout: 10000,
|
||||
},
|
||||
)
|
||||
|
||||
if (!response.data) {
|
||||
throw new Error("Invalid response from MCP marketplace API")
|
||||
}
|
||||
|
||||
console.log("[downloadMcp] Response from download API", { response })
|
||||
|
||||
const mcpDetails = response.data
|
||||
|
||||
// Validate required fields
|
||||
if (!mcpDetails.githubUrl) {
|
||||
throw new Error("Missing GitHub URL in MCP download response")
|
||||
}
|
||||
if (!mcpDetails.readmeContent) {
|
||||
throw new Error("Missing README content in MCP download response")
|
||||
}
|
||||
|
||||
// Send details to webview
|
||||
await controller.postMessageToWebview({
|
||||
type: "mcpDownloadDetails",
|
||||
mcpDownloadDetails: mcpDetails,
|
||||
})
|
||||
|
||||
// Create task with context from README and added guidelines for MCP server installation
|
||||
const task = `Set up the MCP server from ${mcpDetails.githubUrl} while adhering to these MCP server installation rules:
|
||||
- Start by loading the MCP documentation.
|
||||
- Use "${mcpDetails.mcpId}" as the server name in cline_mcp_settings.json.
|
||||
- Create the directory for the new MCP server before starting installation.
|
||||
- Make sure you read the user's existing cline_mcp_settings.json file before editing it with this new mcp, to not overwrite any existing servers.
|
||||
- Use commands aligned with the user's shell and operating system best practices.
|
||||
- The following README may contain instructions that conflict with the user's OS, in which case proceed thoughtfully.
|
||||
- Once installed, demonstrate the server's capabilities by using one of its tools.
|
||||
Here is the project's README to help you get started:\n\n${mcpDetails.readmeContent}\n${mcpDetails.llmsInstallationContent}`
|
||||
|
||||
const { chatSettings } = await controller.getStateToPostToWebview()
|
||||
if (chatSettings.mode === "plan") {
|
||||
await controller.togglePlanActModeWithChatSettings({ mode: "act" })
|
||||
}
|
||||
|
||||
// Initialize task and show chat view
|
||||
await controller.initTask(task)
|
||||
await controller.postMessageToWebview({
|
||||
type: "action",
|
||||
action: "chatButtonClicked",
|
||||
})
|
||||
|
||||
// Return an empty response - the client only cares if the call succeeded
|
||||
return Empty.create()
|
||||
} catch (error) {
|
||||
console.error("Failed to download MCP:", error)
|
||||
let errorMessage = "Failed to download MCP"
|
||||
|
||||
if (axios.isAxiosError(error)) {
|
||||
if (error.code === "ECONNABORTED") {
|
||||
errorMessage = "Request timed out. Please try again."
|
||||
} else if (error.response?.status === 404) {
|
||||
errorMessage = "MCP server not found in marketplace."
|
||||
} else if (error.response?.status === 500) {
|
||||
errorMessage = "Internal server error. Please try again later."
|
||||
} else if (!error.response && error.request) {
|
||||
errorMessage = "Network error. Please check your internet connection."
|
||||
}
|
||||
} else if (error instanceof Error) {
|
||||
errorMessage = error.message
|
||||
}
|
||||
|
||||
// Show error in both notification and marketplace UI
|
||||
vscode.window.showErrorMessage(errorMessage)
|
||||
await controller.postMessageToWebview({
|
||||
type: "mcpDownloadDetails",
|
||||
error: errorMessage,
|
||||
})
|
||||
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
// Import all method implementations
|
||||
import { registerMethod } from "./index"
|
||||
import { addRemoteMcpServer } from "./addRemoteMcpServer"
|
||||
import { downloadMcp } from "./downloadMcp"
|
||||
import { toggleMcpServer } from "./toggleMcpServer"
|
||||
import { updateMcpTimeout } from "./updateMcpTimeout"
|
||||
|
||||
@@ -11,6 +12,7 @@ import { updateMcpTimeout } from "./updateMcpTimeout"
|
||||
export function registerAllMethods(): void {
|
||||
// Register each method with the registry
|
||||
registerMethod("addRemoteMcpServer", addRemoteMcpServer)
|
||||
registerMethod("downloadMcp", downloadMcp)
|
||||
registerMethod("toggleMcpServer", toggleMcpServer)
|
||||
registerMethod("updateMcpTimeout", updateMcpTimeout)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { getOllamaModels } from "./getOllamaModels"
|
||||
import { getVsCodeLmModels } from "./getVsCodeLmModels"
|
||||
import { refreshOpenAiModels } from "./refreshOpenAiModels"
|
||||
import { refreshOpenRouterModels } from "./refreshOpenRouterModels"
|
||||
import { refreshRequestyModels } from "./refreshRequestyModels"
|
||||
|
||||
// Register all models service methods
|
||||
export function registerAllMethods(): void {
|
||||
@@ -17,4 +18,5 @@ export function registerAllMethods(): void {
|
||||
registerMethod("getVsCodeLmModels", getVsCodeLmModels)
|
||||
registerMethod("refreshOpenAiModels", refreshOpenAiModels)
|
||||
registerMethod("refreshOpenRouterModels", refreshOpenRouterModels)
|
||||
registerMethod("refreshRequestyModels", refreshRequestyModels)
|
||||
}
|
||||
|
||||
@@ -29,9 +29,6 @@ export async function refreshOpenAiModels(controller: Controller, request: OpenA
|
||||
const modelsArray = response.data?.data?.map((model: any) => model.id) || []
|
||||
const models = [...new Set<string>(modelsArray)]
|
||||
|
||||
// Send models to webview
|
||||
controller.postMessageToWebview({ type: "openAiModels", openAiModels: models })
|
||||
|
||||
return StringArray.create({ values: models })
|
||||
} catch (error) {
|
||||
console.error("Error fetching OpenAI models:", error)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Controller } from ".."
|
||||
import { EmptyRequest } from "../../../shared/proto/common"
|
||||
import { OpenRouterModels, OpenRouterModelInfo } from "../../../shared/proto/models"
|
||||
import { OpenRouterCompatibleModelInfo, OpenRouterModelInfo } from "../../../shared/proto/models"
|
||||
import axios from "axios"
|
||||
import path from "path"
|
||||
import fs from "fs/promises"
|
||||
@@ -13,7 +13,10 @@ import { GlobalFileNames } from "@core/storage/disk"
|
||||
* @param request Empty request object
|
||||
* @returns Response containing the OpenRouter models
|
||||
*/
|
||||
export async function refreshOpenRouterModels(controller: Controller, request: EmptyRequest): Promise<OpenRouterModels> {
|
||||
export async function refreshOpenRouterModels(
|
||||
controller: Controller,
|
||||
request: EmptyRequest,
|
||||
): Promise<OpenRouterCompatibleModelInfo> {
|
||||
const openRouterModelsFilePath = path.join(await ensureCacheDirectoryExists(controller), GlobalFileNames.openRouterModels)
|
||||
|
||||
let models: Record<string, Partial<OpenRouterModelInfo>> = {}
|
||||
@@ -141,13 +144,7 @@ export async function refreshOpenRouterModels(controller: Controller, request: E
|
||||
}
|
||||
}
|
||||
|
||||
// Send models to webview
|
||||
await controller.postMessageToWebview({
|
||||
type: "openRouterModels",
|
||||
openRouterModels: typedModels,
|
||||
})
|
||||
|
||||
return OpenRouterModels.create({ models: typedModels })
|
||||
return OpenRouterCompatibleModelInfo.create({ models: typedModels })
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { Controller } from ".."
|
||||
import { EmptyRequest } from "../../../shared/proto/common"
|
||||
import { OpenRouterCompatibleModelInfo, OpenRouterModelInfo } from "../../../shared/proto/models"
|
||||
import axios from "axios"
|
||||
import { getSecret } from "@core/storage/state"
|
||||
|
||||
/**
|
||||
* Refreshes the Requesty models and returns the updated model list
|
||||
* @param controller The controller instance
|
||||
* @param request Empty request object
|
||||
* @returns Response containing the Requesty models
|
||||
*/
|
||||
export async function refreshRequestyModels(
|
||||
controller: Controller,
|
||||
request: EmptyRequest,
|
||||
): Promise<OpenRouterCompatibleModelInfo> {
|
||||
const parsePrice = (price: any) => {
|
||||
if (price) {
|
||||
return parseFloat(price) * 1_000_000
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
let models: Record<string, OpenRouterModelInfo> = {}
|
||||
try {
|
||||
const apiKey = await getSecret(controller.context, "requestyApiKey")
|
||||
const headers = {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
}
|
||||
const response = await axios.get("https://router.requesty.ai/v1/models", { headers })
|
||||
if (response.data?.data) {
|
||||
for (const model of response.data.data) {
|
||||
const modelInfo: OpenRouterModelInfo = {
|
||||
maxTokens: model.max_output_tokens || undefined,
|
||||
contextWindow: model.context_window,
|
||||
supportsImages: model.supports_vision || undefined,
|
||||
supportsPromptCache: model.supports_caching || undefined,
|
||||
inputPrice: parsePrice(model.input_price) || 0,
|
||||
outputPrice: parsePrice(model.output_price) || 0,
|
||||
cacheWritesPrice: parsePrice(model.caching_price) || 0,
|
||||
cacheReadsPrice: parsePrice(model.cached_price) || 0,
|
||||
description: model.description,
|
||||
}
|
||||
models[model.id] = modelInfo
|
||||
}
|
||||
console.log("Requesty models fetched", models)
|
||||
} else {
|
||||
console.error("Invalid response from Requesty API")
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching Requesty models:", error)
|
||||
}
|
||||
|
||||
return OpenRouterCompatibleModelInfo.create({ models })
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Controller } from ".."
|
||||
import { StringRequest, Empty } from "../../../shared/proto/common"
|
||||
|
||||
/**
|
||||
* Command slash command logic
|
||||
*/
|
||||
export async function condense(controller: Controller, request: StringRequest): Promise<Empty> {
|
||||
await controller.task?.handleWebviewAskResponse("yesButtonClicked")
|
||||
return Empty.create()
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
|
||||
// Generated by proto/build-proto.js
|
||||
|
||||
import { createServiceRegistry, ServiceMethodHandler, StreamingMethodHandler } from "../grpc-service"
|
||||
import { StreamingResponseHandler } from "../grpc-handler"
|
||||
import { registerAllMethods } from "./methods"
|
||||
|
||||
// Create slash service registry
|
||||
const slashService = createServiceRegistry("slash")
|
||||
|
||||
// Export the method handler types and registration function
|
||||
export type SlashMethodHandler = ServiceMethodHandler
|
||||
export type SlashStreamingMethodHandler = StreamingMethodHandler
|
||||
export const registerMethod = slashService.registerMethod
|
||||
|
||||
// Export the request handlers
|
||||
export const handleSlashServiceRequest = slashService.handleRequest
|
||||
export const handleSlashServiceStreamingRequest = slashService.handleStreamingRequest
|
||||
export const isStreamingMethod = slashService.isStreamingMethod
|
||||
|
||||
// Register all slash methods
|
||||
registerAllMethods()
|
||||
@@ -0,0 +1,14 @@
|
||||
// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
|
||||
// Generated by proto/build-proto.js
|
||||
|
||||
// Import all method implementations
|
||||
import { registerMethod } from "./index"
|
||||
import { condense } from "./condense"
|
||||
import { reportBug } from "./reportBug"
|
||||
|
||||
// Register all slash service methods
|
||||
export function registerAllMethods(): void {
|
||||
// Register each method with the registry
|
||||
registerMethod("condense", condense)
|
||||
registerMethod("reportBug", reportBug)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Controller } from ".."
|
||||
import { StringRequest, Empty } from "../../../shared/proto/common"
|
||||
|
||||
/**
|
||||
* Report bug slash command logic
|
||||
*/
|
||||
export async function reportBug(controller: Controller, request: StringRequest): Promise<Empty> {
|
||||
await controller.task?.handleWebviewAskResponse("yesButtonClicked")
|
||||
return Empty.create()
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
import { registerMethod } from "./index"
|
||||
import { getLatestState } from "./getLatestState"
|
||||
import { subscribeToState } from "./subscribeToState"
|
||||
import { toggleFavoriteModel } from "./toggleFavoriteModel"
|
||||
|
||||
// Streaming methods for this service
|
||||
export const streamingMethods = ["subscribeToState"]
|
||||
@@ -14,4 +15,5 @@ export function registerAllMethods(): void {
|
||||
// Register each method with the registry
|
||||
registerMethod("getLatestState", getLatestState)
|
||||
registerMethod("subscribeToState", subscribeToState, { isStreaming: true })
|
||||
registerMethod("toggleFavoriteModel", toggleFavoriteModel)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { telemetryService } from "@/services/posthog/telemetry/TelemetryService"
|
||||
import { Controller } from ".."
|
||||
import { Empty, StringRequest } from "../../../shared/proto/common"
|
||||
import { updateGlobalState } from "@/core/storage/state"
|
||||
|
||||
/**
|
||||
* Toggles a model's favorite status
|
||||
* @param controller The controller instance
|
||||
* @param request The request containing the model ID to toggle
|
||||
* @returns An empty response
|
||||
*/
|
||||
export async function toggleFavoriteModel(controller: Controller, request: StringRequest): Promise<Empty> {
|
||||
try {
|
||||
if (!request.value) {
|
||||
throw new Error("Model ID is required")
|
||||
}
|
||||
|
||||
const modelId = request.value
|
||||
const { apiConfiguration } = await controller.getStateToPostToWebview()
|
||||
|
||||
if (!apiConfiguration) {
|
||||
throw new Error("API configuration not found")
|
||||
}
|
||||
|
||||
const favoritedModelIds = apiConfiguration.favoritedModelIds || []
|
||||
|
||||
// Toggle favorite status
|
||||
const updatedFavorites = favoritedModelIds.includes(modelId)
|
||||
? favoritedModelIds.filter((id) => id !== modelId)
|
||||
: [...favoritedModelIds, modelId]
|
||||
|
||||
await updateGlobalState(controller.context, "favoritedModelIds", updatedFavorites)
|
||||
|
||||
// Capture telemetry for model favorite toggle
|
||||
const isFavorited = !favoritedModelIds.includes(modelId)
|
||||
telemetryService.captureModelFavoritesUsage(modelId, isFavorited)
|
||||
|
||||
// Post state to webview without changing any other configuration
|
||||
await controller.postStateToWebview()
|
||||
|
||||
return Empty.create()
|
||||
} catch (error) {
|
||||
console.error(`Failed to toggle favorite status for model ${request.value}:`, error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Controller } from ".."
|
||||
import { GetTaskHistoryRequest, TaskHistoryArray } from "../../../shared/proto/task"
|
||||
import { getGlobalState } from "../../storage/state"
|
||||
import { getWorkspacePath, arePathsEqual } from "../../../utils/path"
|
||||
|
||||
/**
|
||||
* Gets filtered task history
|
||||
@@ -10,22 +11,49 @@ import { getGlobalState } from "../../storage/state"
|
||||
*/
|
||||
export async function getTaskHistory(controller: Controller, request: GetTaskHistoryRequest): Promise<TaskHistoryArray> {
|
||||
try {
|
||||
const { favoritesOnly, searchQuery, sortBy } = request
|
||||
const { favoritesOnly, currentWorkspaceOnly, searchQuery, sortBy } = request
|
||||
|
||||
// Get task history from global state
|
||||
const taskHistory = ((await getGlobalState(controller.context, "taskHistory")) as any[]) || []
|
||||
const workspacePath = getWorkspacePath()
|
||||
|
||||
// Apply filters
|
||||
let filteredTasks = taskHistory.filter((item) => {
|
||||
// Basic filter: must have timestamp and task content
|
||||
const hasRequiredFields = item.ts && item.task
|
||||
|
||||
// Apply favorites filter if requested
|
||||
if (favoritesOnly && hasRequiredFields) {
|
||||
return item.isFavorited === true
|
||||
if (!hasRequiredFields) {
|
||||
return false
|
||||
}
|
||||
|
||||
return hasRequiredFields
|
||||
// Apply favorites filter if requested
|
||||
if (favoritesOnly && !item.isFavorited) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Apply current workspace filter if requested
|
||||
if (currentWorkspaceOnly) {
|
||||
let isInWorkspace = false
|
||||
|
||||
// First check the cwdOnTaskInitialization property - Only present on tasks from this change forward
|
||||
if (item.cwdOnTaskInitialization) {
|
||||
if (arePathsEqual(item.cwdOnTaskInitialization, workspacePath)) {
|
||||
isInWorkspace = true
|
||||
}
|
||||
}
|
||||
|
||||
// For tasks without cwdOnTaskInitialization, check the older shadowGitConfigWorkTree property
|
||||
if (!isInWorkspace && item.shadowGitConfigWorkTree) {
|
||||
if (arePathsEqual(item.shadowGitConfigWorkTree, workspacePath)) {
|
||||
isInWorkspace = true
|
||||
}
|
||||
}
|
||||
|
||||
if (!isInWorkspace) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
// Apply search if provided
|
||||
|
||||
@@ -11,6 +11,7 @@ export type SecretKey =
|
||||
| "deepSeekApiKey"
|
||||
| "requestyApiKey"
|
||||
| "togetherApiKey"
|
||||
| "fireworksApiKey"
|
||||
| "qwenApiKey"
|
||||
| "doubaoApiKey"
|
||||
| "mistralApiKey"
|
||||
@@ -67,7 +68,11 @@ export type GlobalStateKey =
|
||||
| "previousModeModelInfo"
|
||||
| "liteLlmBaseUrl"
|
||||
| "liteLlmModelId"
|
||||
| "liteLlmModelInfo"
|
||||
| "liteLlmUsePromptCache"
|
||||
| "fireworksModelId"
|
||||
| "fireworksModelMaxCompletionTokens"
|
||||
| "fireworksModelMaxTokens"
|
||||
| "qwenApiLine"
|
||||
| "requestyModelId"
|
||||
| "requestyModelInfo"
|
||||
|
||||
@@ -107,7 +107,12 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
vsCodeLmModelSelector,
|
||||
liteLlmBaseUrl,
|
||||
liteLlmModelId,
|
||||
liteLlmModelInfo,
|
||||
liteLlmUsePromptCache,
|
||||
fireworksApiKey,
|
||||
fireworksModelId,
|
||||
fireworksModelMaxCompletionTokens,
|
||||
fireworksModelMaxTokens,
|
||||
userInfo,
|
||||
previousModeApiProvider,
|
||||
previousModeModelId,
|
||||
@@ -186,7 +191,12 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
getGlobalState(context, "vsCodeLmModelSelector") as Promise<vscode.LanguageModelChatSelector | undefined>,
|
||||
getGlobalState(context, "liteLlmBaseUrl") as Promise<string | undefined>,
|
||||
getGlobalState(context, "liteLlmModelId") as Promise<string | undefined>,
|
||||
getGlobalState(context, "liteLlmModelInfo") as Promise<ModelInfo | undefined>,
|
||||
getGlobalState(context, "liteLlmUsePromptCache") as Promise<boolean | undefined>,
|
||||
getSecret(context, "fireworksApiKey") as Promise<string | undefined>,
|
||||
getGlobalState(context, "fireworksModelId") as Promise<string | undefined>,
|
||||
getGlobalState(context, "fireworksModelMaxCompletionTokens") as Promise<number | undefined>,
|
||||
getGlobalState(context, "fireworksModelMaxTokens") as Promise<number | undefined>,
|
||||
getGlobalState(context, "userInfo") as Promise<UserInfo | undefined>,
|
||||
getGlobalState(context, "previousModeApiProvider") as Promise<ApiProvider | undefined>,
|
||||
getGlobalState(context, "previousModeModelId") as Promise<string | undefined>,
|
||||
@@ -304,8 +314,13 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
reasoningEffort,
|
||||
liteLlmBaseUrl,
|
||||
liteLlmModelId,
|
||||
liteLlmModelInfo,
|
||||
liteLlmApiKey,
|
||||
liteLlmUsePromptCache,
|
||||
fireworksApiKey,
|
||||
fireworksModelId,
|
||||
fireworksModelMaxCompletionTokens,
|
||||
fireworksModelMaxTokens,
|
||||
asksageApiKey,
|
||||
asksageApiUrl,
|
||||
xaiApiKey,
|
||||
@@ -386,6 +401,7 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
|
||||
vsCodeLmModelSelector,
|
||||
liteLlmBaseUrl,
|
||||
liteLlmModelId,
|
||||
liteLlmModelInfo,
|
||||
liteLlmApiKey,
|
||||
liteLlmUsePromptCache,
|
||||
qwenApiLine,
|
||||
@@ -444,6 +460,7 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
|
||||
await updateGlobalState(context, "vsCodeLmModelSelector", vsCodeLmModelSelector)
|
||||
await updateGlobalState(context, "liteLlmBaseUrl", liteLlmBaseUrl)
|
||||
await updateGlobalState(context, "liteLlmModelId", liteLlmModelId)
|
||||
await updateGlobalState(context, "liteLlmModelInfo", liteLlmModelInfo)
|
||||
await updateGlobalState(context, "liteLlmUsePromptCache", liteLlmUsePromptCache)
|
||||
await updateGlobalState(context, "qwenApiLine", qwenApiLine)
|
||||
await updateGlobalState(context, "requestyModelId", requestyModelId)
|
||||
@@ -480,6 +497,7 @@ export async function resetExtensionState(context: vscode.ExtensionContext) {
|
||||
"mistralApiKey",
|
||||
"clineApiKey",
|
||||
"liteLlmApiKey",
|
||||
"fireworksApiKey",
|
||||
"asksageApiKey",
|
||||
"xaiApiKey",
|
||||
"sambanovaApiKey",
|
||||
|
||||
+20
-4
@@ -60,7 +60,7 @@ import { fileExistsAtPath } from "@utils/fs"
|
||||
import { createAndOpenGitHubIssue } from "@utils/github-url-utils"
|
||||
import { arePathsEqual, getReadablePath, isLocatedInWorkspace } from "@utils/path"
|
||||
import { fixModelHtmlEscaping, removeInvalidChars } from "@utils/string"
|
||||
import { AssistantMessageContent, parseAssistantMessage, ToolParamName, ToolUseName } from "@core/assistant-message"
|
||||
import { AssistantMessageContent, parseAssistantMessageV2, ToolParamName, ToolUseName } from "@core/assistant-message"
|
||||
import { constructNewFileContent } from "@core/assistant-message/diff"
|
||||
import { ClineIgnoreController } from "@core/ignore/ClineIgnoreController"
|
||||
import { parseMentions } from "@core/mentions"
|
||||
@@ -314,6 +314,7 @@ export class Task {
|
||||
totalCost: apiMetrics.totalCost,
|
||||
size: taskDirSize,
|
||||
shadowGitConfigWorkTree: await this.checkpointTracker?.getShadowGitConfigWorkTree(),
|
||||
cwdOnTaskInitialization: cwd,
|
||||
conversationHistoryDeletedRange: this.conversationHistoryDeletedRange,
|
||||
isFavorited: this.taskIsFavorited,
|
||||
})
|
||||
@@ -1442,13 +1443,28 @@ export class Task {
|
||||
return statusCode && !message.includes(statusCode.toString()) ? `${statusCode} - ${message}` : message
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrates the disableBrowserTool setting from VSCode configuration to browserSettings
|
||||
*/
|
||||
private async migrateDisableBrowserToolSetting(): Promise<void> {
|
||||
const config = vscode.workspace.getConfiguration("cline")
|
||||
const disableBrowserTool = vscode.workspace.getConfiguration("cline").get<boolean>("disableBrowserTool")
|
||||
|
||||
if (disableBrowserTool !== undefined) {
|
||||
this.browserSettings.disableToolUse = disableBrowserTool
|
||||
// Remove from VSCode configuration
|
||||
await config.update("disableBrowserTool", undefined, true)
|
||||
}
|
||||
}
|
||||
|
||||
async *attemptApiRequest(previousApiReqIndex: number): ApiStream {
|
||||
// Wait for MCP servers to be connected before generating system prompt
|
||||
await pWaitFor(() => this.mcpHub.isConnecting !== true, { timeout: 10_000 }).catch(() => {
|
||||
console.error("MCP servers failed to connect in time")
|
||||
})
|
||||
|
||||
const disableBrowserTool = vscode.workspace.getConfiguration("cline").get<boolean>("disableBrowserTool") ?? false
|
||||
await this.migrateDisableBrowserToolSetting()
|
||||
const disableBrowserTool = this.browserSettings.disableToolUse ?? false
|
||||
// cline browser tool uses image recognition for navigation (requires model image support).
|
||||
const modelSupportsBrowserUse = this.api.getModel().info.supportsImages ?? false
|
||||
|
||||
@@ -3244,7 +3260,7 @@ export class Task {
|
||||
await this.say("user_feedback", text ?? "", images)
|
||||
pushToolResult(
|
||||
formatResponse.toolResult(
|
||||
`The user provided feedback on the Github issue generated:\n<feedback>\n${text}\n</feedback>`,
|
||||
`The user did not submit the bug, and provided feedback on the Github issue generated instead:\n<feedback>\n${text}\n</feedback>`,
|
||||
images,
|
||||
),
|
||||
)
|
||||
@@ -3844,7 +3860,7 @@ export class Task {
|
||||
assistantMessage += chunk.text
|
||||
// parse raw assistant message into content blocks
|
||||
const prevLength = this.assistantMessageContent.length
|
||||
this.assistantMessageContent = parseAssistantMessage(assistantMessage)
|
||||
this.assistantMessageContent = parseAssistantMessageV2(assistantMessage)
|
||||
if (this.assistantMessageContent.length > prevLength) {
|
||||
this.userMessageContentReady = false // new content we need to present, reset to false in case previous content set this to true
|
||||
}
|
||||
|
||||
@@ -91,9 +91,9 @@ export class DiffViewProvider {
|
||||
const currentFirstVisibleLine = e.visibleRanges[0]?.start.line || 0
|
||||
|
||||
// If the first visible line moved upward, user scrolled up
|
||||
if (currentFirstVisibleLine < this.lastFirstVisibleLine) {
|
||||
this.shouldAutoScroll = false
|
||||
}
|
||||
// if (currentFirstVisibleLine < this.lastFirstVisibleLine) {
|
||||
// this.shouldAutoScroll = false
|
||||
// }
|
||||
|
||||
// Always update our tracking variable
|
||||
this.lastFirstVisibleLine = currentFirstVisibleLine
|
||||
|
||||
@@ -108,6 +108,47 @@ export class TerminalManager {
|
||||
if (disposable) {
|
||||
this.disposables.push(disposable)
|
||||
}
|
||||
|
||||
// Add a listener for terminal state changes to detect CWD updates
|
||||
try {
|
||||
const stateChangeDisposable = vscode.window.onDidChangeTerminalState((terminal) => {
|
||||
const terminalInfo = this.findTerminalInfoByTerminal(terminal)
|
||||
if (terminalInfo && terminalInfo.pendingCwdChange && terminalInfo.cwdResolved) {
|
||||
// Check if CWD has been updated to match the expected path
|
||||
if (this.isCwdMatchingExpected(terminalInfo)) {
|
||||
const resolver = terminalInfo.cwdResolved.resolve
|
||||
terminalInfo.pendingCwdChange = undefined
|
||||
terminalInfo.cwdResolved = undefined
|
||||
resolver()
|
||||
}
|
||||
}
|
||||
})
|
||||
this.disposables.push(stateChangeDisposable)
|
||||
} catch (error) {
|
||||
console.error("Error setting up onDidChangeTerminalState", error)
|
||||
}
|
||||
}
|
||||
|
||||
//Find a TerminalInfo by its VSCode Terminal instance
|
||||
private findTerminalInfoByTerminal(terminal: vscode.Terminal): TerminalInfo | undefined {
|
||||
const terminals = TerminalRegistry.getAllTerminals()
|
||||
return terminals.find((t) => t.terminal === terminal)
|
||||
}
|
||||
|
||||
//Check if a terminal's CWD matches its expected pending change
|
||||
private isCwdMatchingExpected(terminalInfo: TerminalInfo): boolean {
|
||||
if (!terminalInfo.pendingCwdChange) {
|
||||
return false
|
||||
}
|
||||
|
||||
const currentCwd = terminalInfo.terminal.shellIntegration?.cwd?.fsPath
|
||||
const targetCwd = vscode.Uri.file(terminalInfo.pendingCwdChange).fsPath
|
||||
|
||||
if (!currentCwd) {
|
||||
return false
|
||||
}
|
||||
|
||||
return arePathsEqual(currentCwd, targetCwd)
|
||||
}
|
||||
|
||||
runCommand(terminalInfo: TerminalInfo, command: string): TerminalProcessResultPromise {
|
||||
@@ -196,8 +237,37 @@ export class TerminalManager {
|
||||
// If no matching terminal exists, try to find any non-busy terminal
|
||||
const availableTerminal = terminals.find((t) => !t.busy)
|
||||
if (availableTerminal) {
|
||||
// Set up promise and tracking for CWD change
|
||||
const cwdPromise = new Promise<void>((resolve, reject) => {
|
||||
availableTerminal.pendingCwdChange = cwd
|
||||
availableTerminal.cwdResolved = { resolve, reject }
|
||||
})
|
||||
|
||||
// Navigate back to the desired directory
|
||||
await this.runCommand(availableTerminal, `cd "${cwd}"`)
|
||||
|
||||
// Either resolve immediately if CWD already updated or wait for event/timeout
|
||||
if (this.isCwdMatchingExpected(availableTerminal)) {
|
||||
if (availableTerminal.cwdResolved) {
|
||||
availableTerminal.cwdResolved.resolve()
|
||||
}
|
||||
availableTerminal.pendingCwdChange = undefined
|
||||
availableTerminal.cwdResolved = undefined
|
||||
} else {
|
||||
try {
|
||||
// Wait with a timeout for state change event to resolve
|
||||
await Promise.race([
|
||||
cwdPromise,
|
||||
new Promise<void>((_, reject) =>
|
||||
setTimeout(() => reject(new Error(`CWD timeout: Failed to update to ${cwd}`)), 1000),
|
||||
),
|
||||
])
|
||||
} catch (err) {
|
||||
// Clear pending state on timeout
|
||||
availableTerminal.pendingCwdChange = undefined
|
||||
availableTerminal.cwdResolved = undefined
|
||||
}
|
||||
}
|
||||
this.terminalIds.add(availableTerminal.id)
|
||||
return availableTerminal
|
||||
}
|
||||
|
||||
@@ -5,6 +5,11 @@ export interface TerminalInfo {
|
||||
busy: boolean
|
||||
lastCommand: string
|
||||
id: number
|
||||
pendingCwdChange?: string
|
||||
cwdResolved?: {
|
||||
resolve: () => void
|
||||
reject: (error: Error) => void
|
||||
}
|
||||
}
|
||||
|
||||
// Although vscode.window.terminals provides a list of all open terminals, there's no way to know whether they're busy or not (exitStatus does not provide useful information for most commands). In order to prevent creating too many terminals, we need to keep track of terminals through the life of the extension, as well as session specific terminals for the life of a task (to get latest unretrieved output).
|
||||
|
||||
@@ -16,6 +16,7 @@ import { discoverChromeInstances, testBrowserConnection, isPortOpen } from "./Br
|
||||
import * as chromeLauncher from "chrome-launcher"
|
||||
import { Controller } from "@core/controller"
|
||||
import { telemetryService } from "@/services/posthog/telemetry/TelemetryService"
|
||||
import os from "os"
|
||||
|
||||
interface PCRStats {
|
||||
puppeteer: { launch: typeof launch }
|
||||
@@ -67,11 +68,25 @@ export class BrowserSession {
|
||||
}
|
||||
}
|
||||
|
||||
async getDetectedChromePath(): Promise<{ path: string; isBundled: boolean }> {
|
||||
// First check VSCode config
|
||||
/**
|
||||
* Migrates the chromeExecutablePath setting from VSCode configuration to browserSettings
|
||||
*/
|
||||
private async migrateChromeExecutablePathSetting(): Promise<void> {
|
||||
const config = vscode.workspace.getConfiguration("cline")
|
||||
const configPath = vscode.workspace.getConfiguration("cline").get<string>("chromeExecutablePath")
|
||||
if (configPath && (await fileExistsAtPath(configPath))) {
|
||||
return { path: configPath, isBundled: false }
|
||||
|
||||
if (configPath !== undefined) {
|
||||
this.browserSettings.chromeExecutablePath = configPath
|
||||
// Remove from VSCode configuration
|
||||
await config.update("chromeExecutablePath", undefined, true)
|
||||
}
|
||||
}
|
||||
|
||||
async getDetectedChromePath(): Promise<{ path: string; isBundled: boolean }> {
|
||||
// First check browserSettings (from UI, stored in global state)
|
||||
await this.migrateChromeExecutablePathSetting()
|
||||
if (this.browserSettings.chromeExecutablePath && (await fileExistsAtPath(this.browserSettings.chromeExecutablePath))) {
|
||||
return { path: this.browserSettings.chromeExecutablePath, isBundled: false }
|
||||
}
|
||||
|
||||
// Then try to find system Chrome
|
||||
@@ -109,45 +124,20 @@ export class BrowserSession {
|
||||
}
|
||||
|
||||
async relaunchChromeDebugMode(controller: Controller) {
|
||||
const result = await vscode.window.showWarningMessage(
|
||||
"This will close your existing Chrome tabs and relaunch Chrome in debug mode. Are you sure?",
|
||||
{ modal: true },
|
||||
"Yes",
|
||||
)
|
||||
|
||||
if (result !== "Yes") {
|
||||
controller?.postMessageToWebview({
|
||||
type: "browserRelaunchResult",
|
||||
success: false,
|
||||
text: "Operation cancelled by user",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// Chrome-launcher's killAll only kills instances it launched
|
||||
// We need to handle system Chrome processes separately
|
||||
await this.killAllChromeBrowsers()
|
||||
|
||||
// Wait a moment for Chrome to fully shut down
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
|
||||
// Instead of using any default flags, use a minimal set to ensure session persistence
|
||||
// This closely mimics running "google-chrome-stable --remote-debugging-port=9222" from the CLI
|
||||
const chromeFlags = [
|
||||
"--remote-debugging-port=" + DEBUG_PORT,
|
||||
"--disable-notifications",
|
||||
// Do not add any flags that might interfere with profile data
|
||||
]
|
||||
|
||||
const userDataDir = path.join(os.tmpdir(), "chrome-debug-profile")
|
||||
const installation = chromeLauncher.Launcher.getFirstInstallation()
|
||||
if (!installation) {
|
||||
throw new Error("Could not find Chrome installation on this system")
|
||||
}
|
||||
console.info("chrome installation", installation)
|
||||
|
||||
// Prepare the command arguments
|
||||
const args = [`--remote-debugging-port=${DEBUG_PORT}`, "--disable-notifications", "chrome://newtab"]
|
||||
const args = [
|
||||
`--remote-debugging-port=${DEBUG_PORT}`,
|
||||
`--user-data-dir=${userDataDir}`,
|
||||
"--disable-notifications",
|
||||
"chrome://newtab",
|
||||
]
|
||||
|
||||
// Spawn Chrome as a detached process
|
||||
const chromeProcess = spawn(installation, args, {
|
||||
|
||||
@@ -9,6 +9,7 @@ class PostHogClientProvider {
|
||||
this.client = new PostHog(posthogConfig.apiKey, {
|
||||
host: posthogConfig.host,
|
||||
enableExceptionAutocapture: false,
|
||||
defaultOptIn: false,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,20 @@ interface Collection {
|
||||
properties: any
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents telemetry event categories that can be individually enabled or disabled
|
||||
* When adding a new category, add it both here and to the initial values in telemetryCategoryEnabled
|
||||
* Ensure `if (!this.isCategoryEnabled('<category_name>')` is added to the capture method
|
||||
*/
|
||||
type TelemetryCategory = "checkpoints" | "browser"
|
||||
|
||||
class PostHogClient {
|
||||
// Map to control specific telemetry categories (event types)
|
||||
private telemetryCategoryEnabled: Map<TelemetryCategory, boolean> = new Map([
|
||||
["checkpoints", false], // Checkpoints telemetry disabled
|
||||
["browser", true], // Browser telemetry enabled
|
||||
])
|
||||
|
||||
// Stores events when collect=true
|
||||
private collectedTasks: CollectedTasks[] = []
|
||||
// Event constants for tracking user interactions and system events
|
||||
@@ -63,6 +76,8 @@ class PostHogClient {
|
||||
BROWSER_TOOL_END: "task.browser_tool_end",
|
||||
// Tracks when browser errors occur
|
||||
BROWSER_ERROR: "task.browser_error",
|
||||
// Tracks Gemini API specific performance metrics
|
||||
GEMINI_API_PERFORMANCE: "task.gemini_api_performance",
|
||||
// Collection of all task events
|
||||
TASK_COLLECTION: "task.collection",
|
||||
},
|
||||
@@ -271,7 +286,6 @@ class PostHogClient {
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO
|
||||
* Records token usage metrics for cost tracking and usage analysis
|
||||
* @param taskId Unique identifier for the task
|
||||
* @param tokensIn Number of input tokens consumed
|
||||
@@ -365,6 +379,10 @@ class PostHogClient {
|
||||
durationMs?: number,
|
||||
collect: boolean = false,
|
||||
) {
|
||||
if (!this.isCategoryEnabled("checkpoints")) {
|
||||
return
|
||||
}
|
||||
|
||||
this.capture(
|
||||
{
|
||||
event: PostHogClient.EVENTS.TASK.CHECKPOINT_USED,
|
||||
@@ -584,6 +602,10 @@ class PostHogClient {
|
||||
* @param browserSettings The browser settings being used
|
||||
*/
|
||||
public captureBrowserToolStart(taskId: string, browserSettings: BrowserSettings, collect: boolean = false) {
|
||||
if (!this.isCategoryEnabled("browser")) {
|
||||
return
|
||||
}
|
||||
|
||||
this.capture(
|
||||
{
|
||||
event: PostHogClient.EVENTS.TASK.BROWSER_TOOL_START,
|
||||
@@ -613,6 +635,10 @@ class PostHogClient {
|
||||
},
|
||||
collect: boolean = false,
|
||||
) {
|
||||
if (!this.isCategoryEnabled("browser")) {
|
||||
return
|
||||
}
|
||||
|
||||
this.capture(
|
||||
{
|
||||
event: PostHogClient.EVENTS.TASK.BROWSER_TOOL_END,
|
||||
@@ -647,6 +673,10 @@ class PostHogClient {
|
||||
},
|
||||
collect: boolean = false,
|
||||
) {
|
||||
if (!this.isCategoryEnabled("browser")) {
|
||||
return
|
||||
}
|
||||
|
||||
this.capture(
|
||||
{
|
||||
event: PostHogClient.EVENTS.TASK.BROWSER_ERROR,
|
||||
@@ -702,6 +732,43 @@ class PostHogClient {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Captures Gemini API performance metrics.
|
||||
* @param taskId Unique identifier for the task
|
||||
* @param modelId Specific Gemini model ID
|
||||
* @param data Performance data including TTFT, durations, token counts, cache stats, and API success status
|
||||
* @param collect If true, collect event instead of sending
|
||||
*/
|
||||
public captureGeminiApiPerformance(
|
||||
taskId: string,
|
||||
modelId: string,
|
||||
data: {
|
||||
ttftSec?: number
|
||||
totalDurationSec?: number
|
||||
promptTokens: number
|
||||
outputTokens: number
|
||||
cacheReadTokens: number
|
||||
cacheHit: boolean
|
||||
cacheHitPercentage?: number
|
||||
apiSuccess: boolean
|
||||
apiError?: string
|
||||
throughputTokensPerSec?: number
|
||||
},
|
||||
collect: boolean = false,
|
||||
) {
|
||||
this.capture(
|
||||
{
|
||||
event: PostHogClient.EVENTS.TASK.GEMINI_API_PERFORMANCE,
|
||||
properties: {
|
||||
taskId,
|
||||
modelId,
|
||||
...data,
|
||||
},
|
||||
},
|
||||
collect,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Records when the user uses the model favorite button in the model picker
|
||||
* @param model The name of the model the user has interacted with
|
||||
@@ -720,10 +787,24 @@ class PostHogClient {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if telemetry is enabled
|
||||
* @returns Boolean indicating whether telemetry is enabled
|
||||
*/
|
||||
public isTelemetryEnabled(): boolean {
|
||||
return this.telemetryEnabled
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a specific telemetry category is enabled
|
||||
* @param category The telemetry category to check
|
||||
* @returns Boolean indicating whether the specified telemetry category is enabled
|
||||
*/
|
||||
public isCategoryEnabled(category: TelemetryCategory): boolean {
|
||||
// Default to true if category has not been explicitly configured
|
||||
return this.telemetryCategoryEnabled.get(category) ?? true
|
||||
}
|
||||
|
||||
public async sendCollectedEvents(taskId?: string): Promise<void> {
|
||||
if (this.collectedTasks.length > 0) {
|
||||
if (taskId) {
|
||||
|
||||
@@ -22,18 +22,18 @@ export interface AutoApprovalSettings {
|
||||
|
||||
export const DEFAULT_AUTO_APPROVAL_SETTINGS: AutoApprovalSettings = {
|
||||
version: 1,
|
||||
enabled: false,
|
||||
enabled: true,
|
||||
actions: {
|
||||
readFiles: false,
|
||||
readFiles: true,
|
||||
readFilesExternally: false,
|
||||
editFiles: false,
|
||||
editFilesExternally: false,
|
||||
executeSafeCommands: false,
|
||||
executeSafeCommands: true,
|
||||
executeAllCommands: false,
|
||||
useBrowser: false,
|
||||
useMcp: false,
|
||||
},
|
||||
maxRequests: 20,
|
||||
enableNotifications: false,
|
||||
favorites: [],
|
||||
favorites: ["enableAll", "readFiles", "editFiles"],
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ export interface BrowserSettings {
|
||||
// chromeType: "chromium" | "system"
|
||||
remoteBrowserHost?: string
|
||||
remoteBrowserEnabled?: boolean
|
||||
chromeExecutablePath?: string
|
||||
disableToolUse?: boolean
|
||||
}
|
||||
|
||||
export const DEFAULT_BROWSER_SETTINGS: BrowserSettings = {
|
||||
@@ -17,7 +19,9 @@ export const DEFAULT_BROWSER_SETTINGS: BrowserSettings = {
|
||||
},
|
||||
remoteBrowserEnabled: false,
|
||||
remoteBrowserHost: "http://localhost:9222",
|
||||
chromeExecutablePath: "", // Changed from undefined to empty string
|
||||
// chromeType: "chromium",
|
||||
disableToolUse: false,
|
||||
}
|
||||
|
||||
export const BROWSER_VIEWPORT_PRESETS = {
|
||||
|
||||
@@ -10,6 +10,7 @@ export type HistoryItem = {
|
||||
|
||||
size?: number
|
||||
shadowGitConfigWorkTree?: string
|
||||
cwdOnTaskInitialization?: string
|
||||
conversationHistoryDeletedRange?: [number, number]
|
||||
isFavorited?: boolean
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ export interface WebviewMessage {
|
||||
| "openInBrowser"
|
||||
| "openMention"
|
||||
| "showChatView"
|
||||
| "refreshRequestyModels"
|
||||
| "refreshClineRules"
|
||||
| "openMcpSettings"
|
||||
| "restartMcpServer"
|
||||
@@ -38,7 +37,6 @@ export interface WebviewMessage {
|
||||
| "authStateChanged"
|
||||
| "authCallback"
|
||||
| "fetchMcpMarketplace"
|
||||
| "downloadMcp"
|
||||
| "silentlyRefreshMcpMarketplace"
|
||||
| "searchCommits"
|
||||
| "fetchLatestMcpServersFromHub"
|
||||
@@ -55,7 +53,6 @@ export interface WebviewMessage {
|
||||
| "taskFeedback"
|
||||
| "scrollToSettings"
|
||||
| "searchFiles"
|
||||
| "toggleFavoriteModel"
|
||||
| "grpc_request"
|
||||
| "grpc_request_cancel"
|
||||
| "toggleClineRule"
|
||||
|
||||
+13
-10
@@ -19,6 +19,7 @@ export type ApiProvider =
|
||||
| "vscode-lm"
|
||||
| "cline"
|
||||
| "litellm"
|
||||
| "fireworks"
|
||||
| "asksage"
|
||||
| "xai"
|
||||
| "sambanova"
|
||||
@@ -33,6 +34,7 @@ export interface ApiHandlerOptions {
|
||||
liteLlmApiKey?: string
|
||||
liteLlmUsePromptCache?: boolean
|
||||
openAiHeaders?: Record<string, string> // Custom headers for OpenAI requests
|
||||
liteLlmModelInfo?: LiteLLMModelInfo
|
||||
anthropicBaseUrl?: string
|
||||
openRouterApiKey?: string
|
||||
openRouterModelId?: string
|
||||
@@ -69,6 +71,10 @@ export interface ApiHandlerOptions {
|
||||
requestyModelInfo?: ModelInfo
|
||||
togetherApiKey?: string
|
||||
togetherModelId?: string
|
||||
fireworksApiKey?: string
|
||||
fireworksModelId?: string
|
||||
fireworksModelMaxCompletionTokens?: number
|
||||
fireworksModelMaxTokens?: number
|
||||
qwenApiKey?: string
|
||||
doubaoApiKey?: string
|
||||
mistralApiKey?: string
|
||||
@@ -560,14 +566,6 @@ export const openAiModelInfoSaneDefaults: OpenAiCompatibleModelInfo = {
|
||||
export type GeminiModelId = keyof typeof geminiModels
|
||||
export const geminiDefaultModelId: GeminiModelId = "gemini-2.0-flash-001"
|
||||
export const geminiModels = {
|
||||
"gemini-2.5-pro-exp-03-25": {
|
||||
maxTokens: 65536,
|
||||
contextWindow: 1_048_576,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
},
|
||||
"gemini-2.5-pro-preview-05-06": {
|
||||
maxTokens: 65536,
|
||||
contextWindow: 1_048_576,
|
||||
@@ -1452,8 +1450,12 @@ export const mistralModels = {
|
||||
// LiteLLM
|
||||
// https://docs.litellm.ai/docs/
|
||||
export type LiteLLMModelId = string
|
||||
export const liteLlmDefaultModelId = "gpt-3.5-turbo"
|
||||
export const liteLlmModelInfoSaneDefaults: ModelInfo = {
|
||||
export const liteLlmDefaultModelId = "anthropic/claude-3-7-sonnet-20250219"
|
||||
export interface LiteLLMModelInfo extends ModelInfo {
|
||||
temperature?: number
|
||||
}
|
||||
|
||||
export const liteLlmModelInfoSaneDefaults: LiteLLMModelInfo = {
|
||||
maxTokens: -1,
|
||||
contextWindow: 128_000,
|
||||
supportsImages: true,
|
||||
@@ -1462,6 +1464,7 @@ export const liteLlmModelInfoSaneDefaults: ModelInfo = {
|
||||
outputPrice: 0,
|
||||
cacheWritesPrice: 0,
|
||||
cacheReadsPrice: 0,
|
||||
temperature: 0,
|
||||
}
|
||||
|
||||
// AskSage Models
|
||||
|
||||
@@ -36,6 +36,8 @@ export interface BrowserSettings {
|
||||
viewport?: Viewport | undefined
|
||||
remoteBrowserHost?: string | undefined
|
||||
remoteBrowserEnabled?: boolean | undefined
|
||||
chromeExecutablePath?: string | undefined
|
||||
disableToolUse?: boolean | undefined
|
||||
}
|
||||
|
||||
export interface UpdateBrowserSettingsRequest {
|
||||
@@ -43,6 +45,8 @@ export interface UpdateBrowserSettingsRequest {
|
||||
viewport?: Viewport | undefined
|
||||
remoteBrowserHost?: string | undefined
|
||||
remoteBrowserEnabled?: boolean | undefined
|
||||
chromeExecutablePath?: string | undefined
|
||||
disableToolUse?: boolean | undefined
|
||||
}
|
||||
|
||||
function createBaseBrowserConnectionInfo(): BrowserConnectionInfo {
|
||||
@@ -382,7 +386,13 @@ export const Viewport: MessageFns<Viewport> = {
|
||||
}
|
||||
|
||||
function createBaseBrowserSettings(): BrowserSettings {
|
||||
return { viewport: undefined, remoteBrowserHost: undefined, remoteBrowserEnabled: undefined }
|
||||
return {
|
||||
viewport: undefined,
|
||||
remoteBrowserHost: undefined,
|
||||
remoteBrowserEnabled: undefined,
|
||||
chromeExecutablePath: undefined,
|
||||
disableToolUse: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
export const BrowserSettings: MessageFns<BrowserSettings> = {
|
||||
@@ -396,6 +406,12 @@ export const BrowserSettings: MessageFns<BrowserSettings> = {
|
||||
if (message.remoteBrowserEnabled !== undefined) {
|
||||
writer.uint32(24).bool(message.remoteBrowserEnabled)
|
||||
}
|
||||
if (message.chromeExecutablePath !== undefined) {
|
||||
writer.uint32(34).string(message.chromeExecutablePath)
|
||||
}
|
||||
if (message.disableToolUse !== undefined) {
|
||||
writer.uint32(40).bool(message.disableToolUse)
|
||||
}
|
||||
return writer
|
||||
},
|
||||
|
||||
@@ -430,6 +446,22 @@ export const BrowserSettings: MessageFns<BrowserSettings> = {
|
||||
message.remoteBrowserEnabled = reader.bool()
|
||||
continue
|
||||
}
|
||||
case 4: {
|
||||
if (tag !== 34) {
|
||||
break
|
||||
}
|
||||
|
||||
message.chromeExecutablePath = reader.string()
|
||||
continue
|
||||
}
|
||||
case 5: {
|
||||
if (tag !== 40) {
|
||||
break
|
||||
}
|
||||
|
||||
message.disableToolUse = reader.bool()
|
||||
continue
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break
|
||||
@@ -446,6 +478,8 @@ export const BrowserSettings: MessageFns<BrowserSettings> = {
|
||||
remoteBrowserEnabled: isSet(object.remoteBrowserEnabled)
|
||||
? globalThis.Boolean(object.remoteBrowserEnabled)
|
||||
: undefined,
|
||||
chromeExecutablePath: isSet(object.chromeExecutablePath) ? globalThis.String(object.chromeExecutablePath) : undefined,
|
||||
disableToolUse: isSet(object.disableToolUse) ? globalThis.Boolean(object.disableToolUse) : undefined,
|
||||
}
|
||||
},
|
||||
|
||||
@@ -460,6 +494,12 @@ export const BrowserSettings: MessageFns<BrowserSettings> = {
|
||||
if (message.remoteBrowserEnabled !== undefined) {
|
||||
obj.remoteBrowserEnabled = message.remoteBrowserEnabled
|
||||
}
|
||||
if (message.chromeExecutablePath !== undefined) {
|
||||
obj.chromeExecutablePath = message.chromeExecutablePath
|
||||
}
|
||||
if (message.disableToolUse !== undefined) {
|
||||
obj.disableToolUse = message.disableToolUse
|
||||
}
|
||||
return obj
|
||||
},
|
||||
|
||||
@@ -472,12 +512,21 @@ export const BrowserSettings: MessageFns<BrowserSettings> = {
|
||||
object.viewport !== undefined && object.viewport !== null ? Viewport.fromPartial(object.viewport) : undefined
|
||||
message.remoteBrowserHost = object.remoteBrowserHost ?? undefined
|
||||
message.remoteBrowserEnabled = object.remoteBrowserEnabled ?? undefined
|
||||
message.chromeExecutablePath = object.chromeExecutablePath ?? undefined
|
||||
message.disableToolUse = object.disableToolUse ?? undefined
|
||||
return message
|
||||
},
|
||||
}
|
||||
|
||||
function createBaseUpdateBrowserSettingsRequest(): UpdateBrowserSettingsRequest {
|
||||
return { metadata: undefined, viewport: undefined, remoteBrowserHost: undefined, remoteBrowserEnabled: undefined }
|
||||
return {
|
||||
metadata: undefined,
|
||||
viewport: undefined,
|
||||
remoteBrowserHost: undefined,
|
||||
remoteBrowserEnabled: undefined,
|
||||
chromeExecutablePath: undefined,
|
||||
disableToolUse: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
export const UpdateBrowserSettingsRequest: MessageFns<UpdateBrowserSettingsRequest> = {
|
||||
@@ -494,6 +543,12 @@ export const UpdateBrowserSettingsRequest: MessageFns<UpdateBrowserSettingsReque
|
||||
if (message.remoteBrowserEnabled !== undefined) {
|
||||
writer.uint32(32).bool(message.remoteBrowserEnabled)
|
||||
}
|
||||
if (message.chromeExecutablePath !== undefined) {
|
||||
writer.uint32(42).string(message.chromeExecutablePath)
|
||||
}
|
||||
if (message.disableToolUse !== undefined) {
|
||||
writer.uint32(48).bool(message.disableToolUse)
|
||||
}
|
||||
return writer
|
||||
},
|
||||
|
||||
@@ -536,6 +591,22 @@ export const UpdateBrowserSettingsRequest: MessageFns<UpdateBrowserSettingsReque
|
||||
message.remoteBrowserEnabled = reader.bool()
|
||||
continue
|
||||
}
|
||||
case 5: {
|
||||
if (tag !== 42) {
|
||||
break
|
||||
}
|
||||
|
||||
message.chromeExecutablePath = reader.string()
|
||||
continue
|
||||
}
|
||||
case 6: {
|
||||
if (tag !== 48) {
|
||||
break
|
||||
}
|
||||
|
||||
message.disableToolUse = reader.bool()
|
||||
continue
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break
|
||||
@@ -553,6 +624,8 @@ export const UpdateBrowserSettingsRequest: MessageFns<UpdateBrowserSettingsReque
|
||||
remoteBrowserEnabled: isSet(object.remoteBrowserEnabled)
|
||||
? globalThis.Boolean(object.remoteBrowserEnabled)
|
||||
: undefined,
|
||||
chromeExecutablePath: isSet(object.chromeExecutablePath) ? globalThis.String(object.chromeExecutablePath) : undefined,
|
||||
disableToolUse: isSet(object.disableToolUse) ? globalThis.Boolean(object.disableToolUse) : undefined,
|
||||
}
|
||||
},
|
||||
|
||||
@@ -570,6 +643,12 @@ export const UpdateBrowserSettingsRequest: MessageFns<UpdateBrowserSettingsReque
|
||||
if (message.remoteBrowserEnabled !== undefined) {
|
||||
obj.remoteBrowserEnabled = message.remoteBrowserEnabled
|
||||
}
|
||||
if (message.chromeExecutablePath !== undefined) {
|
||||
obj.chromeExecutablePath = message.chromeExecutablePath
|
||||
}
|
||||
if (message.disableToolUse !== undefined) {
|
||||
obj.disableToolUse = message.disableToolUse
|
||||
}
|
||||
return obj
|
||||
},
|
||||
|
||||
@@ -584,6 +663,8 @@ export const UpdateBrowserSettingsRequest: MessageFns<UpdateBrowserSettingsReque
|
||||
object.viewport !== undefined && object.viewport !== null ? Viewport.fromPartial(object.viewport) : undefined
|
||||
message.remoteBrowserHost = object.remoteBrowserHost ?? undefined
|
||||
message.remoteBrowserEnabled = object.remoteBrowserEnabled ?? undefined
|
||||
message.chromeExecutablePath = object.chromeExecutablePath ?? undefined
|
||||
message.disableToolUse = object.disableToolUse ?? undefined
|
||||
return message
|
||||
},
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
/* eslint-disable */
|
||||
import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire"
|
||||
import { Metadata } from "./common"
|
||||
import { Empty, Metadata, StringRequest } from "./common"
|
||||
|
||||
export const protobufPackage = "cline"
|
||||
|
||||
@@ -1004,6 +1004,14 @@ export const McpServiceDefinition = {
|
||||
responseStream: false,
|
||||
options: {},
|
||||
},
|
||||
downloadMcp: {
|
||||
name: "downloadMcp",
|
||||
requestType: StringRequest,
|
||||
requestStream: false,
|
||||
responseType: Empty,
|
||||
responseStream: false,
|
||||
options: {},
|
||||
},
|
||||
},
|
||||
} as const
|
||||
|
||||
|
||||
+42
-29
@@ -23,7 +23,7 @@ export interface VsCodeLmModel {
|
||||
id: string
|
||||
}
|
||||
|
||||
/** For ModelInfo structure in OpenRouterModels */
|
||||
/** For OpenRouterCompatibleModelInfo structure in OpenRouterModels */
|
||||
export interface OpenRouterModelInfo {
|
||||
maxTokens: number
|
||||
contextWindow: number
|
||||
@@ -36,12 +36,12 @@ export interface OpenRouterModelInfo {
|
||||
description: string
|
||||
}
|
||||
|
||||
/** Response message for OpenRouter models */
|
||||
export interface OpenRouterModels {
|
||||
/** Shared response message for model information */
|
||||
export interface OpenRouterCompatibleModelInfo {
|
||||
models: { [key: string]: OpenRouterModelInfo }
|
||||
}
|
||||
|
||||
export interface OpenRouterModels_ModelsEntry {
|
||||
export interface OpenRouterCompatibleModelInfo_ModelsEntry {
|
||||
key: string
|
||||
value?: OpenRouterModelInfo | undefined
|
||||
}
|
||||
@@ -419,22 +419,22 @@ export const OpenRouterModelInfo: MessageFns<OpenRouterModelInfo> = {
|
||||
},
|
||||
}
|
||||
|
||||
function createBaseOpenRouterModels(): OpenRouterModels {
|
||||
function createBaseOpenRouterCompatibleModelInfo(): OpenRouterCompatibleModelInfo {
|
||||
return { models: {} }
|
||||
}
|
||||
|
||||
export const OpenRouterModels: MessageFns<OpenRouterModels> = {
|
||||
encode(message: OpenRouterModels, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
export const OpenRouterCompatibleModelInfo: MessageFns<OpenRouterCompatibleModelInfo> = {
|
||||
encode(message: OpenRouterCompatibleModelInfo, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
Object.entries(message.models).forEach(([key, value]) => {
|
||||
OpenRouterModels_ModelsEntry.encode({ key: key as any, value }, writer.uint32(10).fork()).join()
|
||||
OpenRouterCompatibleModelInfo_ModelsEntry.encode({ key: key as any, value }, writer.uint32(10).fork()).join()
|
||||
})
|
||||
return writer
|
||||
},
|
||||
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): OpenRouterModels {
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): OpenRouterCompatibleModelInfo {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
|
||||
let end = length === undefined ? reader.len : reader.pos + length
|
||||
const message = createBaseOpenRouterModels()
|
||||
const message = createBaseOpenRouterCompatibleModelInfo()
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32()
|
||||
switch (tag >>> 3) {
|
||||
@@ -443,7 +443,7 @@ export const OpenRouterModels: MessageFns<OpenRouterModels> = {
|
||||
break
|
||||
}
|
||||
|
||||
const entry1 = OpenRouterModels_ModelsEntry.decode(reader, reader.uint32())
|
||||
const entry1 = OpenRouterCompatibleModelInfo_ModelsEntry.decode(reader, reader.uint32())
|
||||
if (entry1.value !== undefined) {
|
||||
message.models[entry1.key] = entry1.value
|
||||
}
|
||||
@@ -458,7 +458,7 @@ export const OpenRouterModels: MessageFns<OpenRouterModels> = {
|
||||
return message
|
||||
},
|
||||
|
||||
fromJSON(object: any): OpenRouterModels {
|
||||
fromJSON(object: any): OpenRouterCompatibleModelInfo {
|
||||
return {
|
||||
models: isObject(object.models)
|
||||
? Object.entries(object.models).reduce<{ [key: string]: OpenRouterModelInfo }>((acc, [key, value]) => {
|
||||
@@ -469,7 +469,7 @@ export const OpenRouterModels: MessageFns<OpenRouterModels> = {
|
||||
}
|
||||
},
|
||||
|
||||
toJSON(message: OpenRouterModels): unknown {
|
||||
toJSON(message: OpenRouterCompatibleModelInfo): unknown {
|
||||
const obj: any = {}
|
||||
if (message.models) {
|
||||
const entries = Object.entries(message.models)
|
||||
@@ -483,11 +483,11 @@ export const OpenRouterModels: MessageFns<OpenRouterModels> = {
|
||||
return obj
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<OpenRouterModels>, I>>(base?: I): OpenRouterModels {
|
||||
return OpenRouterModels.fromPartial(base ?? ({} as any))
|
||||
create<I extends Exact<DeepPartial<OpenRouterCompatibleModelInfo>, I>>(base?: I): OpenRouterCompatibleModelInfo {
|
||||
return OpenRouterCompatibleModelInfo.fromPartial(base ?? ({} as any))
|
||||
},
|
||||
fromPartial<I extends Exact<DeepPartial<OpenRouterModels>, I>>(object: I): OpenRouterModels {
|
||||
const message = createBaseOpenRouterModels()
|
||||
fromPartial<I extends Exact<DeepPartial<OpenRouterCompatibleModelInfo>, I>>(object: I): OpenRouterCompatibleModelInfo {
|
||||
const message = createBaseOpenRouterCompatibleModelInfo()
|
||||
message.models = Object.entries(object.models ?? {}).reduce<{ [key: string]: OpenRouterModelInfo }>(
|
||||
(acc, [key, value]) => {
|
||||
if (value !== undefined) {
|
||||
@@ -501,12 +501,12 @@ export const OpenRouterModels: MessageFns<OpenRouterModels> = {
|
||||
},
|
||||
}
|
||||
|
||||
function createBaseOpenRouterModels_ModelsEntry(): OpenRouterModels_ModelsEntry {
|
||||
function createBaseOpenRouterCompatibleModelInfo_ModelsEntry(): OpenRouterCompatibleModelInfo_ModelsEntry {
|
||||
return { key: "", value: undefined }
|
||||
}
|
||||
|
||||
export const OpenRouterModels_ModelsEntry: MessageFns<OpenRouterModels_ModelsEntry> = {
|
||||
encode(message: OpenRouterModels_ModelsEntry, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
export const OpenRouterCompatibleModelInfo_ModelsEntry: MessageFns<OpenRouterCompatibleModelInfo_ModelsEntry> = {
|
||||
encode(message: OpenRouterCompatibleModelInfo_ModelsEntry, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
if (message.key !== "") {
|
||||
writer.uint32(10).string(message.key)
|
||||
}
|
||||
@@ -516,10 +516,10 @@ export const OpenRouterModels_ModelsEntry: MessageFns<OpenRouterModels_ModelsEnt
|
||||
return writer
|
||||
},
|
||||
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): OpenRouterModels_ModelsEntry {
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): OpenRouterCompatibleModelInfo_ModelsEntry {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
|
||||
let end = length === undefined ? reader.len : reader.pos + length
|
||||
const message = createBaseOpenRouterModels_ModelsEntry()
|
||||
const message = createBaseOpenRouterCompatibleModelInfo_ModelsEntry()
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32()
|
||||
switch (tag >>> 3) {
|
||||
@@ -548,14 +548,14 @@ export const OpenRouterModels_ModelsEntry: MessageFns<OpenRouterModels_ModelsEnt
|
||||
return message
|
||||
},
|
||||
|
||||
fromJSON(object: any): OpenRouterModels_ModelsEntry {
|
||||
fromJSON(object: any): OpenRouterCompatibleModelInfo_ModelsEntry {
|
||||
return {
|
||||
key: isSet(object.key) ? globalThis.String(object.key) : "",
|
||||
value: isSet(object.value) ? OpenRouterModelInfo.fromJSON(object.value) : undefined,
|
||||
}
|
||||
},
|
||||
|
||||
toJSON(message: OpenRouterModels_ModelsEntry): unknown {
|
||||
toJSON(message: OpenRouterCompatibleModelInfo_ModelsEntry): unknown {
|
||||
const obj: any = {}
|
||||
if (message.key !== "") {
|
||||
obj.key = message.key
|
||||
@@ -566,11 +566,15 @@ export const OpenRouterModels_ModelsEntry: MessageFns<OpenRouterModels_ModelsEnt
|
||||
return obj
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<OpenRouterModels_ModelsEntry>, I>>(base?: I): OpenRouterModels_ModelsEntry {
|
||||
return OpenRouterModels_ModelsEntry.fromPartial(base ?? ({} as any))
|
||||
create<I extends Exact<DeepPartial<OpenRouterCompatibleModelInfo_ModelsEntry>, I>>(
|
||||
base?: I,
|
||||
): OpenRouterCompatibleModelInfo_ModelsEntry {
|
||||
return OpenRouterCompatibleModelInfo_ModelsEntry.fromPartial(base ?? ({} as any))
|
||||
},
|
||||
fromPartial<I extends Exact<DeepPartial<OpenRouterModels_ModelsEntry>, I>>(object: I): OpenRouterModels_ModelsEntry {
|
||||
const message = createBaseOpenRouterModels_ModelsEntry()
|
||||
fromPartial<I extends Exact<DeepPartial<OpenRouterCompatibleModelInfo_ModelsEntry>, I>>(
|
||||
object: I,
|
||||
): OpenRouterCompatibleModelInfo_ModelsEntry {
|
||||
const message = createBaseOpenRouterCompatibleModelInfo_ModelsEntry()
|
||||
message.key = object.key ?? ""
|
||||
message.value =
|
||||
object.value !== undefined && object.value !== null ? OpenRouterModelInfo.fromPartial(object.value) : undefined
|
||||
@@ -709,7 +713,7 @@ export const ModelsServiceDefinition = {
|
||||
name: "refreshOpenRouterModels",
|
||||
requestType: EmptyRequest,
|
||||
requestStream: false,
|
||||
responseType: OpenRouterModels,
|
||||
responseType: OpenRouterCompatibleModelInfo,
|
||||
responseStream: false,
|
||||
options: {},
|
||||
},
|
||||
@@ -722,6 +726,15 @@ export const ModelsServiceDefinition = {
|
||||
responseStream: false,
|
||||
options: {},
|
||||
},
|
||||
/** Refreshes and returns Requesty models */
|
||||
refreshRequestyModels: {
|
||||
name: "refreshRequestyModels",
|
||||
requestType: EmptyRequest,
|
||||
requestStream: false,
|
||||
responseType: OpenRouterCompatibleModelInfo,
|
||||
responseStream: false,
|
||||
options: {},
|
||||
},
|
||||
},
|
||||
} as const
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-ts_proto v2.7.0
|
||||
// protoc v3.19.1
|
||||
// source: slash.proto
|
||||
|
||||
/* eslint-disable */
|
||||
import { Empty, StringRequest } from "./common"
|
||||
|
||||
export const protobufPackage = "cline"
|
||||
|
||||
/** SlashService provides methods for managing slash */
|
||||
export type SlashServiceDefinition = typeof SlashServiceDefinition
|
||||
export const SlashServiceDefinition = {
|
||||
name: "SlashService",
|
||||
fullName: "cline.SlashService",
|
||||
methods: {
|
||||
/** Sends button click message */
|
||||
reportBug: {
|
||||
name: "reportBug",
|
||||
requestType: StringRequest,
|
||||
requestStream: false,
|
||||
responseType: Empty,
|
||||
responseStream: false,
|
||||
options: {},
|
||||
},
|
||||
condense: {
|
||||
name: "condense",
|
||||
requestType: StringRequest,
|
||||
requestStream: false,
|
||||
responseType: Empty,
|
||||
responseStream: false,
|
||||
options: {},
|
||||
},
|
||||
},
|
||||
} as const
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
/* eslint-disable */
|
||||
import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire"
|
||||
import { EmptyRequest } from "./common"
|
||||
import { Empty, EmptyRequest, StringRequest } from "./common"
|
||||
|
||||
export const protobufPackage = "cline"
|
||||
|
||||
@@ -93,6 +93,14 @@ export const StateServiceDefinition = {
|
||||
responseStream: true,
|
||||
options: {},
|
||||
},
|
||||
toggleFavoriteModel: {
|
||||
name: "toggleFavoriteModel",
|
||||
requestType: StringRequest,
|
||||
requestStream: false,
|
||||
responseType: Empty,
|
||||
responseStream: false,
|
||||
options: {},
|
||||
},
|
||||
},
|
||||
} as const
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@ export interface GetTaskHistoryRequest {
|
||||
favoritesOnly: boolean
|
||||
searchQuery: string
|
||||
sortBy: string
|
||||
currentWorkspaceOnly: boolean
|
||||
}
|
||||
|
||||
/** Response for task history */
|
||||
@@ -550,7 +551,7 @@ export const DeleteNonFavoritedTasksResults: MessageFns<DeleteNonFavoritedTasksR
|
||||
}
|
||||
|
||||
function createBaseGetTaskHistoryRequest(): GetTaskHistoryRequest {
|
||||
return { metadata: undefined, favoritesOnly: false, searchQuery: "", sortBy: "" }
|
||||
return { metadata: undefined, favoritesOnly: false, searchQuery: "", sortBy: "", currentWorkspaceOnly: false }
|
||||
}
|
||||
|
||||
export const GetTaskHistoryRequest: MessageFns<GetTaskHistoryRequest> = {
|
||||
@@ -567,6 +568,9 @@ export const GetTaskHistoryRequest: MessageFns<GetTaskHistoryRequest> = {
|
||||
if (message.sortBy !== "") {
|
||||
writer.uint32(34).string(message.sortBy)
|
||||
}
|
||||
if (message.currentWorkspaceOnly !== false) {
|
||||
writer.uint32(40).bool(message.currentWorkspaceOnly)
|
||||
}
|
||||
return writer
|
||||
},
|
||||
|
||||
@@ -609,6 +613,14 @@ export const GetTaskHistoryRequest: MessageFns<GetTaskHistoryRequest> = {
|
||||
message.sortBy = reader.string()
|
||||
continue
|
||||
}
|
||||
case 5: {
|
||||
if (tag !== 40) {
|
||||
break
|
||||
}
|
||||
|
||||
message.currentWorkspaceOnly = reader.bool()
|
||||
continue
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break
|
||||
@@ -624,6 +636,7 @@ export const GetTaskHistoryRequest: MessageFns<GetTaskHistoryRequest> = {
|
||||
favoritesOnly: isSet(object.favoritesOnly) ? globalThis.Boolean(object.favoritesOnly) : false,
|
||||
searchQuery: isSet(object.searchQuery) ? globalThis.String(object.searchQuery) : "",
|
||||
sortBy: isSet(object.sortBy) ? globalThis.String(object.sortBy) : "",
|
||||
currentWorkspaceOnly: isSet(object.currentWorkspaceOnly) ? globalThis.Boolean(object.currentWorkspaceOnly) : false,
|
||||
}
|
||||
},
|
||||
|
||||
@@ -641,6 +654,9 @@ export const GetTaskHistoryRequest: MessageFns<GetTaskHistoryRequest> = {
|
||||
if (message.sortBy !== "") {
|
||||
obj.sortBy = message.sortBy
|
||||
}
|
||||
if (message.currentWorkspaceOnly !== false) {
|
||||
obj.currentWorkspaceOnly = message.currentWorkspaceOnly
|
||||
}
|
||||
return obj
|
||||
},
|
||||
|
||||
@@ -654,6 +670,7 @@ export const GetTaskHistoryRequest: MessageFns<GetTaskHistoryRequest> = {
|
||||
message.favoritesOnly = object.favoritesOnly ?? false
|
||||
message.searchQuery = object.searchQuery ?? ""
|
||||
message.sortBy = object.sortBy ?? ""
|
||||
message.currentWorkspaceOnly = object.currentWorkspaceOnly ?? false
|
||||
return message
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Public PostHog key (safe for open source)
|
||||
export const posthogConfig = {
|
||||
apiKey: "phc_qfOAGxZw2TL5O8p9KYd9ak3bPBFzfjC8fy5L6jNWY7K",
|
||||
host: "https://us.i.posthog.com",
|
||||
host: "https://data.cline.bot",
|
||||
uiHost: "https://us.posthog.com",
|
||||
}
|
||||
|
||||
@@ -53,14 +53,4 @@ describe("Extension Tests", function () {
|
||||
await vscode.commands.executeCommand("cline.historyButtonClicked")
|
||||
// Success if no error thrown
|
||||
})
|
||||
|
||||
it("should handle advanced settings configuration", async () => {
|
||||
// Test browser session setting
|
||||
await vscode.workspace.getConfiguration().update("cline.disableBrowserTool", true, true)
|
||||
const updatedConfig = vscode.workspace.getConfiguration("cline")
|
||||
expect(updatedConfig.get("disableBrowserTool")).to.be.true
|
||||
|
||||
// Reset settings
|
||||
await vscode.workspace.getConfiguration().update("cline.disableBrowserTool", undefined, true)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
25463
|
||||
Generated
+266
@@ -68,6 +68,7 @@
|
||||
"vitest": "^3.0.5"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rollup/rollup-linux-arm64-gnu": "^4.40.0",
|
||||
"@rollup/rollup-linux-x64-gnu": "^4.40.0",
|
||||
"@swc/core-linux-x64-gnu": "^1.11.0",
|
||||
"@tailwindcss/oxide-linux-x64-gnu": "^4.0.1",
|
||||
@@ -5439,6 +5440,34 @@
|
||||
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
"version": "4.40.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.1.tgz",
|
||||
"integrity": "sha512-kxz0YeeCrRUHz3zyqvd7n+TVRlNyTifBsmnmNPtk3hQURUyG9eAB+usz6DAwagMusjx/zb3AjvDUvhFGDAexGw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm64": {
|
||||
"version": "4.40.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.1.tgz",
|
||||
"integrity": "sha512-PPkxTOisoNC6TpnDKatjKkjRMsdaWIhyuMkA4UsBXT9WEZY4uHezBTjs6Vl4PbqQQeu6oION1w2voYZv9yquCw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||
"version": "4.40.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.1.tgz",
|
||||
@@ -5453,6 +5482,173 @@
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-x64": {
|
||||
"version": "4.40.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.1.tgz",
|
||||
"integrity": "sha512-nIwkXafAI1/QCS7pxSpv/ZtFW6TXcNUEHAIA9EIyw5OzxJZQ1YDrX+CL6JAIQgZ33CInl1R6mHet9Y/UZTg2Bw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-arm64": {
|
||||
"version": "4.40.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.1.tgz",
|
||||
"integrity": "sha512-BdrLJ2mHTrIYdaS2I99mriyJfGGenSaP+UwGi1kB9BLOCu9SR8ZpbkmmalKIALnRw24kM7qCN0IOm6L0S44iWw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-x64": {
|
||||
"version": "4.40.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.1.tgz",
|
||||
"integrity": "sha512-VXeo/puqvCG8JBPNZXZf5Dqq7BzElNJzHRRw3vjBE27WujdzuOPecDPc/+1DcdcTptNBep3861jNq0mYkT8Z6Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||
"version": "4.40.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.1.tgz",
|
||||
"integrity": "sha512-ehSKrewwsESPt1TgSE/na9nIhWCosfGSFqv7vwEtjyAqZcvbGIg4JAcV7ZEh2tfj/IlfBeZjgOXm35iOOjadcg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||
"version": "4.40.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.1.tgz",
|
||||
"integrity": "sha512-m39iO/aaurh5FVIu/F4/Zsl8xppd76S4qoID8E+dSRQvTyZTOI2gVk3T4oqzfq1PtcvOfAVlwLMK3KRQMaR8lg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||
"version": "4.40.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.2.tgz",
|
||||
"integrity": "sha512-KlE8IC0HFOC33taNt1zR8qNlBYHj31qGT1UqWqtvR/+NuCVhfufAq9fxO8BMFC22Wu0rxOwGVWxtCMvZVLmhQg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||
"version": "4.40.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.1.tgz",
|
||||
"integrity": "sha512-jEwjn3jCA+tQGswK3aEWcD09/7M5wGwc6+flhva7dsQNRZZTe30vkalgIzV4tjkopsTS9Jd7Y1Bsj6a4lzz8gQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-loongarch64-gnu": {
|
||||
"version": "4.40.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.1.tgz",
|
||||
"integrity": "sha512-ySyWikVhNzv+BV/IDCsrraOAZ3UaC8SZB67FZlqVwXwnFhPihOso9rPOxzZbjp81suB1O2Topw+6Ug3JNegejQ==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
|
||||
"version": "4.40.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.1.tgz",
|
||||
"integrity": "sha512-BvvA64QxZlh7WZWqDPPdt0GH4bznuL6uOO1pmgPnnv86rpUpc8ZxgZwcEgXvo02GRIZX1hQ0j0pAnhwkhwPqWg==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||
"version": "4.40.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.1.tgz",
|
||||
"integrity": "sha512-EQSP+8+1VuSulm9RKSMKitTav89fKbHymTf25n5+Yr6gAPZxYWpj3DzAsQqoaHAk9YX2lwEyAf9S4W8F4l3VBQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
||||
"version": "4.40.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.1.tgz",
|
||||
"integrity": "sha512-n/vQ4xRZXKuIpqukkMXZt9RWdl+2zgGNx7Uda8NtmLJ06NL8jiHxUawbwC+hdSq1rrw/9CghCpEONor+l1e2gA==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||
"version": "4.40.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.1.tgz",
|
||||
"integrity": "sha512-h8d28xzYb98fMQKUz0w2fMc1XuGzLLjdyxVIbhbil4ELfk5/orZlSTpF/xdI9C8K0I8lCkq+1En2RJsawZekkg==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||
"version": "4.40.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.1.tgz",
|
||||
@@ -5466,6 +5662,62 @@
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||
"version": "4.40.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.1.tgz",
|
||||
"integrity": "sha512-2BRORitq5rQ4Da9blVovzNCMaUlyKrzMSvkVR0D4qPuOy/+pMCrh1d7o01RATwVy+6Fa1WBw+da7QPeLWU/1mQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||
"version": "4.40.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.1.tgz",
|
||||
"integrity": "sha512-b2bcNm9Kbde03H+q+Jjw9tSfhYkzrDUf2d5MAd1bOJuVplXvFhWz7tRtWvD8/ORZi7qSCy0idW6tf2HgxSXQSg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||
"version": "4.40.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.1.tgz",
|
||||
"integrity": "sha512-DfcogW8N7Zg7llVEfpqWMZcaErKfsj9VvmfSyRjCyo4BI3wPEfrzTtJkZG6gKP/Z92wFm6rz2aDO7/JfiR/whA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||
"version": "4.40.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.1.tgz",
|
||||
"integrity": "sha512-ECyOuDeH3C1I8jH2MK1RtBJW+YPMvSfT0a5NN0nHfQYnDSJ6tUiZH3gzwVP5/Kfh/+Tt7tpWVF9LXNTnhTJ3kA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@rrweb/types": {
|
||||
"version": "2.0.0-alpha.17",
|
||||
"resolved": "https://registry.npmjs.org/@rrweb/types/-/types-2.0.0-alpha.17.tgz",
|
||||
@@ -13393,6 +13645,20 @@
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/rollup/node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||
"version": "4.40.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.1.tgz",
|
||||
"integrity": "sha512-Y+GHnGaku4aVLSgrT0uWe2o2Rq8te9hi+MwqGF9r9ORgXhmHK5Q71N757u0F8yU1OIwUIFy6YiJtKjtyktk5hg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/roughjs": {
|
||||
"version": "4.6.6",
|
||||
"resolved": "https://registry.npmjs.org/roughjs/-/roughjs-4.6.6.tgz",
|
||||
|
||||
@@ -74,6 +74,7 @@
|
||||
"vitest": "^3.0.5"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rollup/rollup-linux-arm64-gnu": "^4.40.0",
|
||||
"@rollup/rollup-linux-x64-gnu": "^4.40.0",
|
||||
"@swc/core-linux-x64-gnu": "^1.11.0",
|
||||
"@tailwindcss/oxide-linux-x64-gnu": "^4.0.1",
|
||||
|
||||
@@ -5,20 +5,32 @@ import { posthogConfig } from "@shared/services/config/posthog-config"
|
||||
import { useExtensionState } from "./context/ExtensionStateContext"
|
||||
|
||||
export function CustomPostHogProvider({ children }: { children: ReactNode }) {
|
||||
const { telemetrySetting } = useExtensionState()
|
||||
const isTelemetryEnabled = telemetrySetting === "enabled"
|
||||
const { telemetrySetting, vscMachineId } = useExtensionState()
|
||||
const isTelemetryEnabled = telemetrySetting !== "disabled"
|
||||
|
||||
useEffect(() => {
|
||||
if (vscMachineId.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
posthog.init(posthogConfig.apiKey, {
|
||||
api_host: posthogConfig.host,
|
||||
ui_host: posthogConfig.uiHost,
|
||||
opt_out_capturing_by_default: true,
|
||||
disable_session_recording: true,
|
||||
capture_pageview: false,
|
||||
capture_dead_clicks: true,
|
||||
bootstrap: {
|
||||
distinctID: vscMachineId,
|
||||
},
|
||||
})
|
||||
|
||||
if (isTelemetryEnabled) {
|
||||
posthog.init(posthogConfig.apiKey, {
|
||||
api_host: posthogConfig.host,
|
||||
autocapture: false,
|
||||
disable_session_recording: true,
|
||||
})
|
||||
posthog.opt_in_capturing()
|
||||
} else {
|
||||
posthog.opt_out_capturing()
|
||||
}
|
||||
}, [isTelemetryEnabled])
|
||||
}, [isTelemetryEnabled, vscMachineId])
|
||||
|
||||
return <PostHogProvider client={posthog}>{children}</PostHogProvider>
|
||||
}
|
||||
|
||||
@@ -378,7 +378,7 @@ export const ChatRowContent = ({
|
||||
marginBottom: "-1.5px",
|
||||
}}></span>
|
||||
),
|
||||
<span style={{ color: normalColor, fontWeight: "bold", wordBreak: "break-word" }}>
|
||||
<span className="ph-no-capture" style={{ color: normalColor, fontWeight: "bold", wordBreak: "break-word" }}>
|
||||
Cline wants to {mcpServerUse.type === "use_mcp_tool" ? "use a tool" : "access a resource"} on the{" "}
|
||||
<code style={{ wordBreak: "break-all" }}>
|
||||
{getMcpServerDisplayName(mcpServerUse.serverName, mcpMarketplaceCatalog)}
|
||||
@@ -493,7 +493,7 @@ export const ChatRowContent = ({
|
||||
}
|
||||
const toolIcon = (name: string, color?: string, rotation?: number, title?: string) => (
|
||||
<span
|
||||
className={`codicon codicon-${name}`}
|
||||
className={`codicon codicon-${name} ph-no-capture`}
|
||||
style={{
|
||||
color: color ? colorMap[color as keyof typeof colorMap] || color : "var(--vscode-foreground)",
|
||||
marginBottom: "-1.5px",
|
||||
@@ -577,6 +577,7 @@ export const ChatRowContent = ({
|
||||
}}>
|
||||
{tool.path?.startsWith(".") && <span>.</span>}
|
||||
<span
|
||||
className="ph-no-capture"
|
||||
style={{
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
@@ -999,12 +1000,13 @@ export const ChatRowContent = ({
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
{message.text}
|
||||
<span className="ph-no-capture">{message.text}</span>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ display: "flex", alignItems: "center" }}>
|
||||
<span style={{ fontWeight: "bold", marginRight: "4px" }}>Thinking:</span>
|
||||
<span
|
||||
className="ph-no-capture"
|
||||
style={{
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
|
||||
@@ -1036,6 +1036,8 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
return `vscode-lm:${apiConfiguration.vsCodeLmModelSelector ? `${apiConfiguration.vsCodeLmModelSelector.vendor ?? ""}/${apiConfiguration.vsCodeLmModelSelector.family ?? ""}` : unknownModel}`
|
||||
case "together":
|
||||
return `${selectedProvider}:${apiConfiguration.togetherModelId}`
|
||||
case "fireworks":
|
||||
return `fireworks:${apiConfiguration.fireworksModelId}`
|
||||
case "lmstudio":
|
||||
return `${selectedProvider}:${apiConfiguration.lmStudioModelId}`
|
||||
case "ollama":
|
||||
|
||||
@@ -18,11 +18,10 @@ import { combineCommandSequences } from "@shared/combineCommandSequences"
|
||||
import { getApiMetrics } from "@shared/getApiMetrics"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
import { TaskServiceClient } from "@/services/grpc-client"
|
||||
import { TaskServiceClient, SlashServiceClient } from "@/services/grpc-client"
|
||||
import HistoryPreview from "@/components/history/HistoryPreview"
|
||||
import { normalizeApiConfiguration } from "@/components/settings/ApiOptions"
|
||||
import Announcement from "@/components/chat/Announcement"
|
||||
import AutoApproveMenu from "@/components/chat/auto-approve-menu/AutoApproveMenu"
|
||||
import BrowserSessionRow from "@/components/chat/BrowserSessionRow"
|
||||
import ChatRow from "@/components/chat/ChatRow"
|
||||
import ChatTextArea from "@/components/chat/ChatTextArea"
|
||||
@@ -34,7 +33,7 @@ import remarkStringify from "remark-stringify"
|
||||
import rehypeRemark from "rehype-remark"
|
||||
import rehypeParse from "rehype-parse"
|
||||
import HomeHeader from "../welcome/HomeHeader"
|
||||
|
||||
import AutoApproveBar from "./auto-approve-menu/AutoApproveBar"
|
||||
interface ChatViewProps {
|
||||
isHidden: boolean
|
||||
showAnnouncement: boolean
|
||||
@@ -147,17 +146,62 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
if (window.getSelection) {
|
||||
const selection = window.getSelection()
|
||||
if (selection && selection.rangeCount > 0) {
|
||||
// Get the selected HTML content
|
||||
const range = selection.getRangeAt(0)
|
||||
const clonedSelection = range.cloneContents()
|
||||
const div = document.createElement("div")
|
||||
div.appendChild(clonedSelection)
|
||||
const selectedHtml = div.innerHTML
|
||||
const commonAncestor = range.commonAncestorContainer
|
||||
let textToCopy: string | null = null
|
||||
|
||||
// Convert HTML to Markdown
|
||||
const markdown = await convertHtmlToMarkdown(selectedHtml)
|
||||
vscode.postMessage({ type: "copyToClipboard", text: markdown })
|
||||
e.preventDefault()
|
||||
// Check if the selection is inside an element where plain text copy is preferred
|
||||
let currentElement =
|
||||
commonAncestor.nodeType === Node.ELEMENT_NODE
|
||||
? (commonAncestor as HTMLElement)
|
||||
: commonAncestor.parentElement
|
||||
let preferPlainTextCopy = false
|
||||
while (currentElement) {
|
||||
if (currentElement.tagName === "PRE" && currentElement.querySelector("code")) {
|
||||
preferPlainTextCopy = true
|
||||
break
|
||||
}
|
||||
// Check computed white-space style
|
||||
const computedStyle = window.getComputedStyle(currentElement)
|
||||
if (
|
||||
computedStyle.whiteSpace === "pre" ||
|
||||
computedStyle.whiteSpace === "pre-wrap" ||
|
||||
computedStyle.whiteSpace === "pre-line"
|
||||
) {
|
||||
// If the element itself or an ancestor has pre-like white-space,
|
||||
// and the selection is likely contained within it, prefer plain text.
|
||||
// This helps with elements like the TaskHeader's text display.
|
||||
preferPlainTextCopy = true
|
||||
break
|
||||
}
|
||||
|
||||
// Stop searching if we reach a known chat message boundary or body
|
||||
if (
|
||||
currentElement.classList.contains("chat-row-assistant-message-container") ||
|
||||
currentElement.classList.contains("chat-row-user-message-container") ||
|
||||
currentElement.tagName === "BODY"
|
||||
) {
|
||||
break
|
||||
}
|
||||
currentElement = currentElement.parentElement
|
||||
}
|
||||
|
||||
if (preferPlainTextCopy) {
|
||||
// For code blocks or elements with pre-formatted white-space, get plain text.
|
||||
textToCopy = selection.toString()
|
||||
} else {
|
||||
// For other content, use the existing HTML-to-Markdown conversion
|
||||
const clonedSelection = range.cloneContents()
|
||||
const div = document.createElement("div")
|
||||
div.appendChild(clonedSelection)
|
||||
const selectedHtml = div.innerHTML
|
||||
textToCopy = await convertHtmlToMarkdown(selectedHtml)
|
||||
}
|
||||
|
||||
if (textToCopy !== null) {
|
||||
vscode.postMessage({ type: "copyToClipboard", text: textToCopy })
|
||||
e.preventDefault()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -511,16 +555,10 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
})
|
||||
break
|
||||
case "condense":
|
||||
vscode.postMessage({
|
||||
type: "condense",
|
||||
text: lastMessage?.text,
|
||||
})
|
||||
await SlashServiceClient.condense({ value: lastMessage?.text }).catch((err) => console.error(err))
|
||||
break
|
||||
case "report_bug":
|
||||
vscode.postMessage({
|
||||
type: "reportBug",
|
||||
text: lastMessage?.text,
|
||||
})
|
||||
await SlashServiceClient.reportBug({ value: lastMessage?.text }).catch((err) => console.error(err))
|
||||
break
|
||||
}
|
||||
setSendingDisabled(true)
|
||||
@@ -1008,7 +1046,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!task && <AutoApproveMenu />}
|
||||
{!task && <AutoApproveBar />}
|
||||
|
||||
{task && (
|
||||
<>
|
||||
@@ -1042,7 +1080,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
initialTopMostItemIndex={groupedMessages.length - 1}
|
||||
/>
|
||||
</div>
|
||||
<AutoApproveMenu />
|
||||
<AutoApproveBar />
|
||||
{showScrollToBottom ? (
|
||||
<div
|
||||
style={{
|
||||
|
||||
@@ -93,8 +93,11 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
|
||||
if (option.value) {
|
||||
return (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 0 }}>
|
||||
<span style={{ lineHeight: "1.2" }}>{option.label}</span>
|
||||
<span className="ph-no-capture" style={{ lineHeight: "1.2" }}>
|
||||
{option.label}
|
||||
</span>
|
||||
<span
|
||||
className="ph-no-capture"
|
||||
style={{
|
||||
fontSize: "0.85em",
|
||||
opacity: 0.7,
|
||||
@@ -118,6 +121,7 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
|
||||
<span>/</span>
|
||||
{option.value?.startsWith("/.") && <span>.</span>}
|
||||
<span
|
||||
className="ph-no-capture"
|
||||
style={{
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
|
||||
@@ -51,6 +51,8 @@ export const OptionsButtons = ({
|
||||
</div> */}
|
||||
{options.map((option, index) => (
|
||||
<OptionButton
|
||||
id={`options-button-${index}`}
|
||||
className="options-button"
|
||||
key={index}
|
||||
isSelected={option === selected}
|
||||
isNotSelectable={hasSelected || !isActive}
|
||||
@@ -63,7 +65,7 @@ export const OptionsButtons = ({
|
||||
text: option + (inputValue ? `: ${inputValue?.trim()}` : ""),
|
||||
})
|
||||
}}>
|
||||
{option}
|
||||
<span className="ph-no-capture">{option}</span>
|
||||
</OptionButton>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -51,7 +51,8 @@ const SlashCommandMenu: React.FC<SlashCommandMenuProps> = ({ onSelect, selectedI
|
||||
filteredCommands.map((command, index) => (
|
||||
<div
|
||||
key={command.name}
|
||||
className={`py-2 px-3 cursor-pointer flex flex-col border-b border-[var(--vscode-editorGroup-border)] ${
|
||||
id={`slash-command-menu-item-${index}`}
|
||||
className={`slash-command-menu-item py-2 px-3 cursor-pointer flex flex-col border-b border-[var(--vscode-editorGroup-border)] ${
|
||||
// Corrected padding
|
||||
index === selectedIndex
|
||||
? "bg-[var(--vscode-quickInputList-focusBackground)] text-[var(--vscode-quickInputList-focusForeground)]"
|
||||
@@ -59,9 +60,11 @@ const SlashCommandMenu: React.FC<SlashCommandMenuProps> = ({ onSelect, selectedI
|
||||
} hover:bg-[var(--vscode-list-hoverBackground)]`}
|
||||
onClick={() => handleClick(command)}
|
||||
onMouseEnter={() => setSelectedIndex(index)}>
|
||||
<div className="font-bold whitespace-nowrap overflow-hidden text-ellipsis">/{command.name}</div>
|
||||
<div className="font-bold whitespace-nowrap overflow-hidden text-ellipsis">
|
||||
<span className="ph-no-capture">/{command.name}</span>
|
||||
</div>
|
||||
<div className="text-[0.85em] text-[var(--vscode-descriptionForeground)] whitespace-normal overflow-hidden text-ellipsis">
|
||||
{command.description}
|
||||
<span className="ph-no-capture">{command.description}</span>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
|
||||
@@ -256,7 +256,11 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
|
||||
Task
|
||||
{!isTaskExpanded && ":"}
|
||||
</span>
|
||||
{!isTaskExpanded && <span style={{ marginLeft: 4 }}>{highlightText(task.text, false)}</span>}
|
||||
{!isTaskExpanded && (
|
||||
<span className="ph-no-capture" style={{ marginLeft: 4 }}>
|
||||
{highlightText(task.text, false)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{isCostAvailable && (
|
||||
@@ -302,7 +306,7 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
|
||||
wordBreak: "break-word",
|
||||
overflowWrap: "anywhere",
|
||||
}}>
|
||||
{highlightText(task.text, false)}
|
||||
<span className="ph-no-capture">{highlightText(task.text, false)}</span>
|
||||
</div>
|
||||
{!isTextExpanded && showSeeMore && (
|
||||
<div
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useMemo, useState, useRef, useEffect } from "react"
|
||||
import React, { useMemo, useState, useRef, useEffect, useCallback } from "react"
|
||||
import { Virtuoso } from "react-virtuoso"
|
||||
import { ClineMessage } from "@shared/ExtensionMessage"
|
||||
import { combineApiRequests } from "@shared/combineApiRequests"
|
||||
import { combineCommandSequences } from "@shared/combineCommandSequences"
|
||||
@@ -94,8 +95,6 @@ const getBlockColor = (message: ClineMessage): string => {
|
||||
}
|
||||
|
||||
const TaskTimeline: React.FC<TaskTimelineProps> = ({ messages }) => {
|
||||
const [hoveredMessage, setHoveredMessage] = useState<ClineMessage | null>(null)
|
||||
const [tooltipPosition, setTooltipPosition] = useState<{ x: number; y: number } | null>(null)
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const scrollableRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
@@ -136,27 +135,48 @@ const TaskTimeline: React.FC<TaskTimelineProps> = ({ messages }) => {
|
||||
}
|
||||
}, [taskTimelinePropsMessages])
|
||||
|
||||
// Calculate the item size (width of block + gap)
|
||||
const itemWidth = parseInt(BLOCK_WIDTH.replace("px", "")) + parseInt(BLOCK_GAP.replace("px", ""))
|
||||
|
||||
// Virtuoso requires a reference to scroll to the end
|
||||
const virtuosoRef = useRef<any>(null)
|
||||
|
||||
// Render a timeline block
|
||||
const TimelineBlock = useCallback(
|
||||
(index: number) => {
|
||||
const message = taskTimelinePropsMessages[index]
|
||||
return (
|
||||
<TaskTimelineTooltip message={message}>
|
||||
<div
|
||||
style={{
|
||||
width: BLOCK_WIDTH,
|
||||
height: "100%",
|
||||
backgroundColor: getBlockColor(message),
|
||||
flexShrink: 0,
|
||||
cursor: "pointer",
|
||||
marginRight: BLOCK_GAP,
|
||||
}}
|
||||
/>
|
||||
</TaskTimelineTooltip>
|
||||
)
|
||||
},
|
||||
[taskTimelinePropsMessages],
|
||||
)
|
||||
|
||||
// Scroll to the end when messages change
|
||||
useEffect(() => {
|
||||
if (virtuosoRef.current && taskTimelinePropsMessages.length > 0) {
|
||||
virtuosoRef.current.scrollToIndex({
|
||||
index: taskTimelinePropsMessages.length - 1,
|
||||
align: "end",
|
||||
})
|
||||
}
|
||||
}, [taskTimelinePropsMessages])
|
||||
|
||||
if (taskTimelinePropsMessages.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const handleMouseEnter = (message: ClineMessage, event: React.MouseEvent<HTMLDivElement>) => {
|
||||
setHoveredMessage(message)
|
||||
|
||||
const viewportWidth = window.innerWidth
|
||||
const tooltipWidth = viewportWidth - TOOLTIP_MARGIN * 2
|
||||
|
||||
// Center the tooltip horizontally in the viewport
|
||||
const x = TOOLTIP_MARGIN
|
||||
|
||||
setTooltipPosition({ x, y: event.clientY })
|
||||
}
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
setHoveredMessage(null)
|
||||
setTooltipPosition(null)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
@@ -167,55 +187,32 @@ const TaskTimeline: React.FC<TaskTimelineProps> = ({ messages }) => {
|
||||
marginBottom: "4px",
|
||||
overflow: "hidden",
|
||||
}}>
|
||||
<div
|
||||
ref={scrollableRef}
|
||||
style={{
|
||||
display: "flex",
|
||||
height: TIMELINE_HEIGHT,
|
||||
overflowX: "auto",
|
||||
scrollbarWidth: "none",
|
||||
msOverflowStyle: "none",
|
||||
width: "100%",
|
||||
WebkitOverflowScrolling: "touch",
|
||||
gap: BLOCK_GAP, // Using flexbox gap instead of marginRight
|
||||
}}>
|
||||
<style>
|
||||
{`
|
||||
/* Hide scrollbar for Chrome, Safari and Opera */
|
||||
div::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
{taskTimelinePropsMessages.map((message, index) => (
|
||||
<div
|
||||
key={index}
|
||||
style={{
|
||||
width: BLOCK_WIDTH,
|
||||
height: "100%",
|
||||
backgroundColor: getBlockColor(message),
|
||||
flexShrink: 0,
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onMouseEnter={(e) => handleMouseEnter(message, e)}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<style>
|
||||
{`
|
||||
/* Hide scrollbar for Chrome, Safari and Opera */
|
||||
.timeline-virtuoso::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
.timeline-virtuoso {
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
|
||||
{hoveredMessage && containerRef.current && tooltipPosition && (
|
||||
<div
|
||||
style={{
|
||||
position: "fixed",
|
||||
left: `${tooltipPosition.x}px`,
|
||||
top: `${tooltipPosition.y + 20}px`,
|
||||
zIndex: 1000,
|
||||
pointerEvents: "none",
|
||||
width: `calc(100% - ${TOOLTIP_MARGIN * 2}px)`,
|
||||
}}>
|
||||
<TaskTimelineTooltip message={hoveredMessage} />
|
||||
</div>
|
||||
)}
|
||||
<Virtuoso
|
||||
ref={virtuosoRef}
|
||||
className="timeline-virtuoso"
|
||||
style={{
|
||||
height: TIMELINE_HEIGHT,
|
||||
width: "100%",
|
||||
}}
|
||||
totalCount={taskTimelinePropsMessages.length}
|
||||
itemContent={TimelineBlock}
|
||||
horizontalDirection={true}
|
||||
increaseViewportBy={12}
|
||||
fixedItemHeight={itemWidth}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import React from "react"
|
||||
import { ClineMessage } from "@shared/ExtensionMessage"
|
||||
import { COLOR_WHITE, COLOR_GRAY, COLOR_DARK_GRAY, COLOR_BEIGE, COLOR_BLUE, COLOR_RED, COLOR_PURPLE, COLOR_GREEN } from "./colors"
|
||||
import { Tooltip } from "@heroui/react"
|
||||
|
||||
// Color mapping for different message types
|
||||
|
||||
interface TaskTimelineTooltipProps {
|
||||
message: ClineMessage
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const TaskTimelineTooltip: React.FC<TaskTimelineTooltipProps> = ({ message }) => {
|
||||
const TaskTimelineTooltip = ({ message, children }: TaskTimelineTooltipProps) => {
|
||||
const getMessageDescription = (message: ClineMessage): string => {
|
||||
if (message.type === "say") {
|
||||
switch (message.say) {
|
||||
@@ -227,53 +229,61 @@ const TaskTimelineTooltip: React.FC<TaskTimelineTooltipProps> = ({ message }) =>
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: "var(--vscode-editor-background)",
|
||||
color: "var(--vscode-editor-foreground)",
|
||||
border: "1px solid var(--vscode-widget-border)",
|
||||
borderRadius: "3px",
|
||||
padding: "8px",
|
||||
width: "100%", // Fill the container width
|
||||
boxShadow: "0 2px 8px rgba(0, 0, 0, 0.15)",
|
||||
fontSize: "12px",
|
||||
}}>
|
||||
<div style={{ fontWeight: "bold", marginBottom: "4px", display: "flex", alignItems: "center" }}>
|
||||
<div
|
||||
style={{
|
||||
width: "10px",
|
||||
height: "10px",
|
||||
minWidth: "10px", // Ensure fixed width
|
||||
minHeight: "10px", // Ensure fixed height
|
||||
borderRadius: "50%",
|
||||
backgroundColor: getMessageColor(message),
|
||||
marginRight: "8px",
|
||||
display: "inline-block",
|
||||
flexShrink: 0, // Prevent shrinking when space is limited
|
||||
}}
|
||||
/>
|
||||
{getMessageDescription(message)}
|
||||
{getTimestamp(message) && (
|
||||
<span style={{ fontWeight: "normal", fontSize: "10px", marginLeft: "8px" }}>{getTimestamp(message)}</span>
|
||||
)}
|
||||
</div>
|
||||
{getMessageContent(message) && (
|
||||
<div
|
||||
style={{
|
||||
whiteSpace: "pre-wrap",
|
||||
wordBreak: "break-word",
|
||||
maxHeight: "150px",
|
||||
overflowY: "auto",
|
||||
fontSize: "11px",
|
||||
fontFamily: "var(--vscode-editor-font-family)",
|
||||
backgroundColor: "var(--vscode-textBlockQuote-background)",
|
||||
padding: "4px",
|
||||
borderRadius: "2px",
|
||||
}}>
|
||||
{getMessageContent(message)}
|
||||
<Tooltip
|
||||
content={
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-wrap items-center font-bold mb-1">
|
||||
<div className="mr-4 mb-0.5">
|
||||
<div
|
||||
style={{
|
||||
width: "10px",
|
||||
height: "10px",
|
||||
minWidth: "10px", // Ensure fixed width
|
||||
minHeight: "10px", // Ensure fixed height
|
||||
borderRadius: "50%",
|
||||
backgroundColor: getMessageColor(message),
|
||||
marginRight: "8px",
|
||||
display: "inline-block",
|
||||
flexShrink: 0, // Prevent shrinking when space is limited
|
||||
}}
|
||||
/>
|
||||
{getMessageDescription(message)}
|
||||
</div>
|
||||
{getTimestamp(message) && (
|
||||
<span className="font-normal text-tiny" style={{ fontWeight: "normal", fontSize: "10px" }}>
|
||||
{getTimestamp(message)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{getMessageContent(message) && (
|
||||
<div
|
||||
style={{
|
||||
whiteSpace: "pre-wrap",
|
||||
wordBreak: "break-word",
|
||||
maxHeight: "150px",
|
||||
overflowY: "auto",
|
||||
fontSize: "11px",
|
||||
fontFamily: "var(--vscode-editor-font-family)",
|
||||
backgroundColor: "var(--vscode-textBlockQuote-background)",
|
||||
padding: "4px",
|
||||
borderRadius: "2px",
|
||||
scrollbarWidth: "none",
|
||||
}}>
|
||||
{getMessageContent(message)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
classNames={{
|
||||
base: "bg-[var(--vscode-editor-background)] text-[var(--vscode-editor-foreground)] border-[var(--vscode-widget-border)] py-1 rounded-[3px] max-w-[calc(100dvw-2rem)] text-xs",
|
||||
}}
|
||||
shadow="sm"
|
||||
placement="bottom"
|
||||
disableAnimation
|
||||
closeDelay={100}
|
||||
isKeyboardDismissDisabled={true}>
|
||||
{children}
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ const UserMessage: React.FC<UserMessageProps> = ({ text, images, messageTs, send
|
||||
setIsEditing(false)
|
||||
} else if (e.key === "Enter" && e.metaKey && !checkpointTrackerErrorMessage) {
|
||||
handleRestoreWorkspace("taskAndWorkspace")
|
||||
} else if (e.key === "Enter" && !e.shiftKey) {
|
||||
} else if (e.key === "Enter" && !e.shiftKey && !e.nativeEvent.isComposing && e.keyCode !== 229) {
|
||||
e.preventDefault()
|
||||
handleRestoreWorkspace("task")
|
||||
}
|
||||
@@ -141,7 +141,9 @@ const UserMessage: React.FC<UserMessageProps> = ({ text, images, messageTs, send
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<span style={{ display: "block" }}>{highlightText(editedText || text)}</span>
|
||||
<span className="ph-no-capture" style={{ display: "block" }}>
|
||||
{highlightText(editedText || text)}
|
||||
</span>
|
||||
)}
|
||||
{images && images.length > 0 && <Thumbnails images={images} style={{ marginTop: "8px" }} />}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* UserMessage – IME composition Enter test
|
||||
* --------------------------------------------------
|
||||
* Confirm that sendMessageFromChatRow is not called
|
||||
* even if you confirm the IME conversion (Enter) in message re-edit mode.
|
||||
*/
|
||||
|
||||
import React from "react"
|
||||
import { render, fireEvent } from "@testing-library/react"
|
||||
import { describe, it, expect, vi } from "vitest"
|
||||
|
||||
vi.mock("@/context/ExtensionStateContext", () => ({
|
||||
__esModule: true,
|
||||
useExtensionState: () => ({
|
||||
state: {},
|
||||
dispatch: vi.fn(),
|
||||
}),
|
||||
}))
|
||||
|
||||
import UserMessage from "../UserMessage"
|
||||
|
||||
describe("UserMessage – IME composition handling", () => {
|
||||
it("does NOT send when IME composition Enter is pressed while editing", () => {
|
||||
const sendMessageFromChatRow = vi.fn()
|
||||
|
||||
const { getByText } = render(
|
||||
<UserMessage text="変換テスト" images={[]} messageTs={Date.now()} sendMessageFromChatRow={sendMessageFromChatRow} />,
|
||||
)
|
||||
|
||||
const editable = getByText("変換テスト") as HTMLElement
|
||||
editable.setAttribute("contenteditable", "true")
|
||||
editable.focus()
|
||||
|
||||
fireEvent.compositionStart(editable)
|
||||
fireEvent.keyDown(editable, {
|
||||
key: "Enter",
|
||||
keyCode: 13,
|
||||
nativeEvent: { isComposing: true },
|
||||
})
|
||||
fireEvent.compositionEnd(editable)
|
||||
|
||||
expect(sendMessageFromChatRow).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,132 @@
|
||||
import { useCallback, useRef, useState, useMemo } from "react"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { CODE_BLOCK_BG_COLOR } from "@/components/common/CodeBlock"
|
||||
import { getAsVar, VSC_TITLEBAR_INACTIVE_FOREGROUND } from "@/utils/vscStyles"
|
||||
import AutoApproveMenuItem from "./AutoApproveMenuItem"
|
||||
import AutoApproveModal from "./AutoApproveModal"
|
||||
import { ACTION_METADATA, NOTIFICATIONS_SETTING } from "./constants"
|
||||
import { ActionMetadata } from "./types"
|
||||
|
||||
interface AutoApproveBarProps {
|
||||
style?: React.CSSProperties
|
||||
}
|
||||
|
||||
const AutoApproveBar = ({ style }: AutoApproveBarProps) => {
|
||||
const { autoApprovalSettings } = useExtensionState()
|
||||
const [isModalVisible, setIsModalVisible] = useState(false)
|
||||
const buttonRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// Favorites are derived from autoApprovalSettings
|
||||
const favorites = useMemo(() => autoApprovalSettings.favorites || [], [autoApprovalSettings.favorites])
|
||||
|
||||
// Render a favorited item with a checkbox
|
||||
const renderFavoritedItem = (favId: string) => {
|
||||
const actions = [...ACTION_METADATA.flatMap((a) => [a, a.subAction]), NOTIFICATIONS_SETTING]
|
||||
const action = actions.find((a) => a?.id === favId)
|
||||
if (!action) return null
|
||||
|
||||
return (
|
||||
<AutoApproveMenuItem
|
||||
action={action}
|
||||
isChecked={isChecked}
|
||||
isFavorited={isFavorited}
|
||||
onToggle={updateAction}
|
||||
condensed={true}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const getQuickAccessItems = () => {
|
||||
const notificationsEnabled = autoApprovalSettings.enableNotifications
|
||||
const enabledActionsNames = Object.keys(autoApprovalSettings.actions).filter(
|
||||
(key) => autoApprovalSettings.actions[key as keyof typeof autoApprovalSettings.actions],
|
||||
)
|
||||
const enabledActions = enabledActionsNames.map((action) => {
|
||||
return ACTION_METADATA.flatMap((a) => [a, a.subAction]).find((a) => a?.id === action)
|
||||
})
|
||||
|
||||
let minusFavorites = enabledActions.filter((action) => !favorites.includes(action?.id ?? "") && action?.shortName)
|
||||
|
||||
if (notificationsEnabled) {
|
||||
minusFavorites.push(NOTIFICATIONS_SETTING)
|
||||
}
|
||||
|
||||
return [
|
||||
...favorites.map((favId) => renderFavoritedItem(favId)),
|
||||
minusFavorites.length > 0 ? (
|
||||
<span className="text-[color:var(--vscode-foreground-muted)] pl-[10px] opacity-60" key="separator">
|
||||
✓
|
||||
</span>
|
||||
) : null,
|
||||
...minusFavorites.map((action, index) => (
|
||||
<span className="text-[color:var(--vscode-foreground-muted)] opacity-60" key={action?.id}>
|
||||
{action?.shortName}
|
||||
{index < minusFavorites.length - 1 && ","}
|
||||
</span>
|
||||
)),
|
||||
]
|
||||
}
|
||||
|
||||
const isChecked = (action: ActionMetadata): boolean => {
|
||||
if (action.id === "enableNotifications") {
|
||||
return autoApprovalSettings.enableNotifications
|
||||
}
|
||||
if (action.id === "enableAll") {
|
||||
return Object.values(autoApprovalSettings.actions).every(Boolean)
|
||||
}
|
||||
return autoApprovalSettings.actions[action.id] ?? false
|
||||
}
|
||||
|
||||
const isFavorited = (action: ActionMetadata): boolean => {
|
||||
return favorites.includes(action.id)
|
||||
}
|
||||
|
||||
const updateAction = useCallback(() => {
|
||||
// This is just a placeholder since we need to pass it to AutoApproveMenuItem
|
||||
// The actual implementation is in the modal component
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div
|
||||
className="px-[10px] mx-[5px] select-none rounded-[10px_10px_0_0]"
|
||||
style={{
|
||||
borderTop: `0.5px solid color-mix(in srgb, ${getAsVar(VSC_TITLEBAR_INACTIVE_FOREGROUND)} 20%, transparent)`,
|
||||
overflowY: "auto",
|
||||
backgroundColor: isModalVisible ? CODE_BLOCK_BG_COLOR : "transparent",
|
||||
...style,
|
||||
}}>
|
||||
<div
|
||||
ref={buttonRef}
|
||||
className="cursor-pointer py-[8px] pr-[2px] flex items-center justify-between gap-[8px]"
|
||||
onClick={() => {
|
||||
setIsModalVisible((prev) => !prev)
|
||||
}}>
|
||||
<div
|
||||
className="flex flex-nowrap items-center overflow-x-auto gap-[4px] whitespace-nowrap"
|
||||
style={{
|
||||
msOverflowStyle: "none",
|
||||
scrollbarWidth: "none",
|
||||
WebkitOverflowScrolling: "touch",
|
||||
}}>
|
||||
<span>Auto-approve:</span>
|
||||
{getQuickAccessItems()}
|
||||
</div>
|
||||
{isModalVisible ? (
|
||||
<span className="codicon codicon-chevron-down" />
|
||||
) : (
|
||||
<span className="codicon codicon-chevron-up" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<AutoApproveModal
|
||||
isVisible={isModalVisible}
|
||||
setIsVisible={setIsModalVisible}
|
||||
buttonRef={buttonRef}
|
||||
ACTION_METADATA={ACTION_METADATA}
|
||||
NOTIFICATIONS_SETTING={NOTIFICATIONS_SETTING}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AutoApproveBar
|
||||
@@ -1,478 +0,0 @@
|
||||
import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { AutoApprovalSettings } from "@shared/AutoApprovalSettings"
|
||||
import { CODE_BLOCK_BG_COLOR } from "@/components/common/CodeBlock"
|
||||
import AutoApproveMenuItem from "./AutoApproveMenuItem"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
import { getAsVar, VSC_FOREGROUND, VSC_TITLEBAR_INACTIVE_FOREGROUND, VSC_DESCRIPTION_FOREGROUND } from "@/utils/vscStyles"
|
||||
import { useClickAway } from "react-use"
|
||||
import HeroTooltip from "@/components/common/HeroTooltip"
|
||||
|
||||
const breakpoint = 500
|
||||
|
||||
interface AutoApproveMenuProps {
|
||||
style?: React.CSSProperties
|
||||
}
|
||||
|
||||
export interface ActionMetadata {
|
||||
id: keyof AutoApprovalSettings["actions"] | "enableNotifications" | "enableAll"
|
||||
label: string
|
||||
shortName: string
|
||||
description: string
|
||||
icon: string
|
||||
subAction?: ActionMetadata
|
||||
sub?: boolean
|
||||
parentActionId?: string
|
||||
}
|
||||
|
||||
const ACTION_METADATA: ActionMetadata[] = [
|
||||
{
|
||||
id: "readFiles",
|
||||
label: "Read project files",
|
||||
shortName: "Read",
|
||||
description: "Allows Cline to read files within your workspace.",
|
||||
icon: "codicon-search",
|
||||
subAction: {
|
||||
id: "readFilesExternally",
|
||||
label: "Read all files",
|
||||
shortName: "Read (all)",
|
||||
description: "Allows Cline to read any file on your computer.",
|
||||
icon: "codicon-folder-opened",
|
||||
parentActionId: "readFiles",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "editFiles",
|
||||
label: "Edit project files",
|
||||
shortName: "Edit",
|
||||
description: "Allows Cline to modify files within your workspace.",
|
||||
icon: "codicon-edit",
|
||||
subAction: {
|
||||
id: "editFilesExternally",
|
||||
label: "Edit all files",
|
||||
shortName: "Edit (all)",
|
||||
description: "Allows Cline to modify any file on your computer.",
|
||||
icon: "codicon-files",
|
||||
parentActionId: "editFiles",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "executeSafeCommands",
|
||||
label: "Execute safe commands",
|
||||
shortName: "Safe Commands",
|
||||
description:
|
||||
"Allows Cline to execute safe terminal commands. If the model determines a command is potentially destructive, it will still require approval.",
|
||||
icon: "codicon-terminal",
|
||||
subAction: {
|
||||
id: "executeAllCommands",
|
||||
label: "Execute all commands",
|
||||
shortName: "All Commands",
|
||||
description: "Allows Cline to execute all terminal commands. Use at your own risk.",
|
||||
icon: "codicon-terminal-bash",
|
||||
parentActionId: "executeSafeCommands",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "useBrowser",
|
||||
label: "Use the browser",
|
||||
shortName: "Browser",
|
||||
description: "Allows Cline to launch and interact with any website in a browser.",
|
||||
icon: "codicon-globe",
|
||||
},
|
||||
{
|
||||
id: "useMcp",
|
||||
label: "Use MCP servers",
|
||||
shortName: "MCP",
|
||||
description: "Allows Cline to use configured MCP servers which may modify filesystem or interact with APIs.",
|
||||
icon: "codicon-server",
|
||||
},
|
||||
{
|
||||
id: "enableAll",
|
||||
label: "Enable all",
|
||||
shortName: "All",
|
||||
description: "Enable all actions.",
|
||||
icon: "codicon-checklist",
|
||||
},
|
||||
{
|
||||
id: "enableNotifications",
|
||||
label: "Enable notifications",
|
||||
shortName: "Notifications",
|
||||
description: "Receive system notifications when Cline requires approval to proceed or when a task is completed.",
|
||||
icon: "codicon-bell",
|
||||
},
|
||||
]
|
||||
|
||||
const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
|
||||
const { autoApprovalSettings } = useExtensionState()
|
||||
const [isExpanded, setIsExpanded] = useState(false)
|
||||
const [containerWidth, setContainerWidth] = useState(0)
|
||||
// Favorites are now derived from autoApprovalSettings
|
||||
const favorites = useMemo(() => autoApprovalSettings.favorites || [], [autoApprovalSettings.favorites])
|
||||
const menuRef = useRef<HTMLDivElement>(null)
|
||||
const itemsContainerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// Track container width for responsive layout
|
||||
useEffect(() => {
|
||||
if (!isExpanded) return
|
||||
|
||||
const updateWidth = () => {
|
||||
if (itemsContainerRef.current) {
|
||||
setContainerWidth(itemsContainerRef.current.offsetWidth)
|
||||
}
|
||||
}
|
||||
|
||||
// Initial measurement
|
||||
updateWidth()
|
||||
|
||||
// Set up resize observer
|
||||
const resizeObserver = new ResizeObserver(updateWidth)
|
||||
if (itemsContainerRef.current) {
|
||||
resizeObserver.observe(itemsContainerRef.current)
|
||||
}
|
||||
|
||||
// Clean up
|
||||
return () => {
|
||||
resizeObserver.disconnect()
|
||||
}
|
||||
}, [isExpanded])
|
||||
|
||||
const toggleFavorite = useCallback(
|
||||
(actionId: string) => {
|
||||
const currentFavorites = autoApprovalSettings.favorites || []
|
||||
let newFavorites: string[]
|
||||
|
||||
if (currentFavorites.includes(actionId)) {
|
||||
newFavorites = currentFavorites.filter((id) => id !== actionId)
|
||||
} else {
|
||||
newFavorites = [...currentFavorites, actionId]
|
||||
}
|
||||
|
||||
vscode.postMessage({
|
||||
type: "autoApprovalSettings",
|
||||
autoApprovalSettings: {
|
||||
...autoApprovalSettings,
|
||||
version: (autoApprovalSettings.version ?? 1) + 1,
|
||||
favorites: newFavorites,
|
||||
},
|
||||
})
|
||||
},
|
||||
[autoApprovalSettings],
|
||||
)
|
||||
|
||||
const updateAction = useCallback(
|
||||
(action: ActionMetadata, value: boolean) => {
|
||||
const actionId = action.id
|
||||
const subActionId = action.subAction?.id
|
||||
|
||||
if (actionId === "enableAll" || subActionId === "enableAll") {
|
||||
toggleAll(action, value)
|
||||
return
|
||||
}
|
||||
|
||||
if (actionId === "enableNotifications" || subActionId === "enableNotifications") {
|
||||
updateNotifications(action, value)
|
||||
return
|
||||
}
|
||||
|
||||
let newActions = {
|
||||
...autoApprovalSettings.actions,
|
||||
[actionId]: value,
|
||||
}
|
||||
|
||||
if (value === false && subActionId) {
|
||||
newActions[subActionId] = false
|
||||
}
|
||||
|
||||
if (value === true && action.parentActionId) {
|
||||
newActions[action.parentActionId as keyof AutoApprovalSettings["actions"]] = true
|
||||
}
|
||||
|
||||
// Check if this will result in any enabled actions
|
||||
const willHaveEnabledActions = Object.values(newActions).some(Boolean)
|
||||
|
||||
vscode.postMessage({
|
||||
type: "autoApprovalSettings",
|
||||
autoApprovalSettings: {
|
||||
...autoApprovalSettings,
|
||||
version: (autoApprovalSettings.version ?? 1) + 1,
|
||||
actions: newActions,
|
||||
enabled: willHaveEnabledActions,
|
||||
},
|
||||
})
|
||||
},
|
||||
[autoApprovalSettings],
|
||||
)
|
||||
|
||||
const updateMaxRequests = useCallback(
|
||||
(maxRequests: number) => {
|
||||
const currentSettings = autoApprovalSettings
|
||||
vscode.postMessage({
|
||||
type: "autoApprovalSettings",
|
||||
autoApprovalSettings: {
|
||||
...currentSettings,
|
||||
version: (currentSettings.version ?? 1) + 1,
|
||||
maxRequests,
|
||||
},
|
||||
})
|
||||
},
|
||||
[autoApprovalSettings],
|
||||
)
|
||||
|
||||
const updateNotifications = useCallback(
|
||||
(action: ActionMetadata, checked: boolean) => {
|
||||
if (action.id === "enableNotifications") {
|
||||
const currentSettings = autoApprovalSettings
|
||||
vscode.postMessage({
|
||||
type: "autoApprovalSettings",
|
||||
autoApprovalSettings: {
|
||||
...currentSettings,
|
||||
version: (currentSettings.version ?? 1) + 1,
|
||||
enableNotifications: checked,
|
||||
},
|
||||
})
|
||||
}
|
||||
},
|
||||
[autoApprovalSettings],
|
||||
)
|
||||
|
||||
const toggleAll = useCallback(
|
||||
(action: ActionMetadata, checked: boolean) => {
|
||||
let actions = { ...autoApprovalSettings.actions }
|
||||
|
||||
for (const action of Object.keys(actions)) {
|
||||
actions[action as keyof AutoApprovalSettings["actions"]] = checked
|
||||
}
|
||||
|
||||
vscode.postMessage({
|
||||
type: "autoApprovalSettings",
|
||||
autoApprovalSettings: {
|
||||
...autoApprovalSettings,
|
||||
version: (autoApprovalSettings.version ?? 1) + 1,
|
||||
actions,
|
||||
},
|
||||
})
|
||||
},
|
||||
[autoApprovalSettings],
|
||||
)
|
||||
|
||||
// Handle clicks outside the menu to close it
|
||||
useClickAway(menuRef, () => {
|
||||
if (isExpanded) {
|
||||
setIsExpanded(false)
|
||||
}
|
||||
})
|
||||
|
||||
// Render a favorited item with a checkbox
|
||||
const renderFavoritedItem = (favId: string) => {
|
||||
// Regular action item
|
||||
const action = ACTION_METADATA.flatMap((a) => [a, a.subAction]).find((a) => a?.id === favId)
|
||||
if (!action) return null
|
||||
|
||||
return (
|
||||
<AutoApproveMenuItem
|
||||
action={action}
|
||||
isChecked={isChecked}
|
||||
isFavorited={isFavorited}
|
||||
onToggle={updateAction}
|
||||
condensed={true}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const isChecked = (action: ActionMetadata): boolean => {
|
||||
if (action.id === "enableNotifications") {
|
||||
return autoApprovalSettings.enableNotifications
|
||||
}
|
||||
if (action.id === "enableAll") {
|
||||
return Object.values(autoApprovalSettings.actions).every(Boolean)
|
||||
}
|
||||
return autoApprovalSettings.actions[action.id] ?? false
|
||||
}
|
||||
|
||||
const isFavorited = (action: ActionMetadata): boolean => {
|
||||
return favorites.includes(action.id)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={menuRef}
|
||||
style={{
|
||||
padding: "0 10px",
|
||||
margin: "0 5px",
|
||||
userSelect: "none",
|
||||
borderTop: `0.5px solid color-mix(in srgb, ${getAsVar(VSC_TITLEBAR_INACTIVE_FOREGROUND)} 20%, transparent)`,
|
||||
overflowY: "auto",
|
||||
borderRadius: "10px 10px 0 0",
|
||||
backgroundColor: isExpanded ? CODE_BLOCK_BG_COLOR : "transparent",
|
||||
...style,
|
||||
}}>
|
||||
{/* Collapsed view with favorited items */}
|
||||
{!isExpanded && (
|
||||
<div
|
||||
onClick={() => setIsExpanded(true)}
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
paddingTop: "6px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
gap: "8px",
|
||||
}}>
|
||||
{favorites.length > 0 ? (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexWrap: "nowrap",
|
||||
alignItems: "center",
|
||||
overflowX: "auto",
|
||||
msOverflowStyle: "none",
|
||||
scrollbarWidth: "none",
|
||||
WebkitOverflowScrolling: "touch",
|
||||
gap: "4px",
|
||||
whiteSpace: "nowrap", // Prevent text wrapping
|
||||
}}>
|
||||
{favorites.map((favId) => renderFavoritedItem(favId))}
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
cursor: "pointer",
|
||||
}}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
|
||||
<HeroTooltip
|
||||
content="Auto-approve allows Cline to perform the following actions without asking for permission. Please use with caution and only enable if you understand the risks."
|
||||
placement="top">
|
||||
<span style={{ color: getAsVar(VSC_FOREGROUND), left: "0" }}>Auto-approve</span>
|
||||
</HeroTooltip>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<span className="codicon codicon-chevron-right" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Expanded view */}
|
||||
<div
|
||||
style={{
|
||||
maxHeight: isExpanded ? "1000px" : favorites.length > 0 ? "40px" : "22px", // Large enough to fit content
|
||||
opacity: isExpanded ? 1 : 0,
|
||||
overflow: "hidden",
|
||||
transition: "max-height 0.3s ease-in-out, opacity 0.3s ease-in-out", // Removed padding to transition
|
||||
}}>
|
||||
{isExpanded && ( // Re-added conditional rendering for content
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
padding: "8px 0",
|
||||
cursor: "pointer",
|
||||
position: "relative", // Added for positioning context
|
||||
}}
|
||||
onClick={() => setIsExpanded(false)}>
|
||||
<HeroTooltip
|
||||
content="Auto-approve allows Cline to perform the following actions without asking for permission. Please use with caution and only enable if you understand the risks."
|
||||
placement="top">
|
||||
<span style={{ color: getAsVar(VSC_FOREGROUND) }}>Auto-approve</span>
|
||||
</HeroTooltip>
|
||||
<span className="codicon codicon-chevron-down" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
ref={itemsContainerRef}
|
||||
style={{
|
||||
display: containerWidth > breakpoint ? "grid" : "flex",
|
||||
gridTemplateColumns: containerWidth > breakpoint ? "1fr 1fr" : "1fr",
|
||||
gridAutoRows: "min-content",
|
||||
flexDirection: "column",
|
||||
gap: "4px",
|
||||
margin: "8px 0",
|
||||
position: "relative", // For absolute positioning of the separator
|
||||
}}>
|
||||
{/* Vertical separator line - only visible in two-column mode */}
|
||||
{containerWidth > breakpoint && (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: "50%",
|
||||
top: "0",
|
||||
bottom: "0",
|
||||
width: "0.5px",
|
||||
background: getAsVar(VSC_TITLEBAR_INACTIVE_FOREGROUND),
|
||||
opacity: 0.2,
|
||||
transform: "translateX(-50%)", // Center the line
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* All items in a single list - CSS Grid will handle the column distribution */}
|
||||
{ACTION_METADATA.map((action) => (
|
||||
<div key={action.id} style={{ breakInside: "avoid" }}>
|
||||
<AutoApproveMenuItem
|
||||
action={action}
|
||||
isChecked={isChecked}
|
||||
isFavorited={isFavorited}
|
||||
onToggle={updateAction}
|
||||
onToggleFavorite={toggleFavorite}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
height: "0.5px",
|
||||
background: getAsVar(VSC_TITLEBAR_INACTIVE_FOREGROUND),
|
||||
margin: "10px 0",
|
||||
opacity: 0.2,
|
||||
}}
|
||||
/>
|
||||
<HeroTooltip
|
||||
content="Cline will automatically make this many API requests before asking for approval to proceed with the task."
|
||||
placement="top">
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "8px",
|
||||
width: "100%",
|
||||
paddingBottom: "10px",
|
||||
}}>
|
||||
<span className="codicon codicon-settings" style={{ color: "#CCCCCC", fontSize: "14px" }} />
|
||||
<span style={{ color: "#CCCCCC", fontSize: "12px", fontWeight: 500 }}>Max Requests:</span>
|
||||
<VSCodeTextField
|
||||
style={{ flex: "1", width: "100%" }}
|
||||
value={autoApprovalSettings.maxRequests.toString()}
|
||||
onInput={(e) => {
|
||||
const input = e.target as HTMLInputElement
|
||||
// Remove any non-numeric characters
|
||||
input.value = input.value.replace(/[^0-9]/g, "")
|
||||
const value = parseInt(input.value)
|
||||
if (!isNaN(value) && value > 0) {
|
||||
updateMaxRequests(value)
|
||||
}
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
// Prevent non-numeric keys (except for backspace, delete, arrows)
|
||||
if (
|
||||
!/^\d$/.test(e.key) &&
|
||||
!["Backspace", "Delete", "ArrowLeft", "ArrowRight"].includes(e.key)
|
||||
) {
|
||||
e.preventDefault()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</HeroTooltip>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AutoApproveMenu
|
||||
@@ -1,8 +1,7 @@
|
||||
import React, { type ChangeEvent, type ChangeEventHandler } from "react"
|
||||
import React from "react"
|
||||
import styled from "styled-components"
|
||||
import HeroTooltip from "@/components/common/HeroTooltip"
|
||||
import { ActionMetadata } from "./AutoApproveMenu"
|
||||
import { useState } from "react"
|
||||
import { ActionMetadata } from "./types"
|
||||
import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
|
||||
|
||||
interface AutoApproveMenuItemProps {
|
||||
@@ -22,7 +21,8 @@ const CheckboxContainer = styled.div<{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between; /* Push content to edges */
|
||||
padding: 0 4px;
|
||||
padding-left: 4px;
|
||||
padding-right: 1px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
@@ -51,7 +51,7 @@ const CheckboxContainer = styled.div<{
|
||||
.star {
|
||||
color: ${(props) => (props.isFavorited ? "var(--vscode-terminal-ansiYellow)" : "var(--vscode-descriptionForeground)")};
|
||||
opacity: ${(props) => (props.isFavorited ? 1 : 0.6)};
|
||||
font-size: 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
`
|
||||
|
||||
@@ -67,7 +67,7 @@ const SubOptionAnimateIn = styled.div<{ show: boolean }>`
|
||||
`
|
||||
|
||||
const ActionButtonContainer = styled.div`
|
||||
margin: 4px;
|
||||
padding: 2px;
|
||||
`
|
||||
|
||||
const AutoApproveMenuItem = ({
|
||||
@@ -78,21 +78,18 @@ const AutoApproveMenuItem = ({
|
||||
onToggleFavorite,
|
||||
condensed = false,
|
||||
}: AutoApproveMenuItemProps) => {
|
||||
const [isSubOptionOpen, setIsSubOptionOpen] = useState(isChecked(action))
|
||||
const checked = isChecked(action)
|
||||
const favorited = isFavorited?.(action)
|
||||
|
||||
const onChange = (e: Event) => {
|
||||
e.stopPropagation()
|
||||
const newChecked = !checked
|
||||
setIsSubOptionOpen(newChecked)
|
||||
onToggle(action, newChecked)
|
||||
onToggle(action, !checked)
|
||||
}
|
||||
|
||||
const content = (
|
||||
<div>
|
||||
<>
|
||||
<ActionButtonContainer>
|
||||
<HeroTooltip content={action.description} delay={200}>
|
||||
<HeroTooltip content={action.description} delay={500}>
|
||||
<CheckboxContainer isFavorited={favorited} onClick={onChange}>
|
||||
<div className="left-content">
|
||||
<VSCodeCheckbox checked={checked} />
|
||||
@@ -100,19 +97,26 @@ const AutoApproveMenuItem = ({
|
||||
<span className="label">{condensed ? action.shortName : action.label}</span>
|
||||
</div>
|
||||
{onToggleFavorite && !condensed && (
|
||||
<span
|
||||
className={`codicon codicon-${favorited ? "star-full" : "star-empty"} star`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onToggleFavorite?.(action.id)
|
||||
}}
|
||||
/>
|
||||
<HeroTooltip
|
||||
delay={500}
|
||||
content={favorited ? "Remove from quick-access menu" : "Add to quick-access menu"}>
|
||||
<span
|
||||
className={`p-0.5 codicon codicon-${favorited ? "star-full" : "star-empty"} star`}
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onToggleFavorite?.(action.id)
|
||||
}}
|
||||
/>
|
||||
</HeroTooltip>
|
||||
)}
|
||||
</CheckboxContainer>
|
||||
</HeroTooltip>
|
||||
</ActionButtonContainer>
|
||||
{action.subAction && !condensed && (
|
||||
<SubOptionAnimateIn show={isSubOptionOpen}>
|
||||
<SubOptionAnimateIn show={checked}>
|
||||
<AutoApproveMenuItem
|
||||
action={action.subAction}
|
||||
isChecked={isChecked}
|
||||
@@ -122,7 +126,7 @@ const AutoApproveMenuItem = ({
|
||||
/>
|
||||
</SubOptionAnimateIn>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
||||
return content
|
||||
|
||||
@@ -0,0 +1,333 @@
|
||||
import React, { useRef, useState, useEffect, useMemo, useCallback } from "react"
|
||||
import { useClickAway, useWindowSize } from "react-use"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { CODE_BLOCK_BG_COLOR } from "@/components/common/CodeBlock"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
import { VSCodeTextField, VSCodeButton } from "@vscode/webview-ui-toolkit/react"
|
||||
import { getAsVar, VSC_FOREGROUND, VSC_TITLEBAR_INACTIVE_FOREGROUND } from "@/utils/vscStyles"
|
||||
import { AutoApprovalSettings } from "@shared/AutoApprovalSettings"
|
||||
import HeroTooltip from "@/components/common/HeroTooltip"
|
||||
import AutoApproveMenuItem from "./AutoApproveMenuItem"
|
||||
import { ActionMetadata } from "./types"
|
||||
|
||||
const breakpoint = 500
|
||||
|
||||
interface AutoApproveModalProps {
|
||||
isVisible: boolean
|
||||
setIsVisible: (visible: boolean) => void
|
||||
buttonRef: React.RefObject<HTMLDivElement>
|
||||
ACTION_METADATA: ActionMetadata[]
|
||||
NOTIFICATIONS_SETTING: ActionMetadata
|
||||
}
|
||||
|
||||
const AutoApproveModal: React.FC<AutoApproveModalProps> = ({
|
||||
isVisible,
|
||||
setIsVisible,
|
||||
buttonRef,
|
||||
ACTION_METADATA,
|
||||
NOTIFICATIONS_SETTING,
|
||||
}) => {
|
||||
const { autoApprovalSettings } = useExtensionState()
|
||||
const modalRef = useRef<HTMLDivElement>(null)
|
||||
const itemsContainerRef = useRef<HTMLDivElement>(null)
|
||||
const { width: viewportWidth, height: viewportHeight } = useWindowSize()
|
||||
const [arrowPosition, setArrowPosition] = useState(0)
|
||||
const [menuPosition, setMenuPosition] = useState(0)
|
||||
const [containerWidth, setContainerWidth] = useState(0)
|
||||
|
||||
// Favorites are derived from autoApprovalSettings
|
||||
const favorites = useMemo(() => autoApprovalSettings.favorites || [], [autoApprovalSettings.favorites])
|
||||
|
||||
useClickAway(modalRef, (e) => {
|
||||
// Skip if click was on the button that toggles the modal
|
||||
if (buttonRef.current && buttonRef.current.contains(e.target as Node)) {
|
||||
return
|
||||
}
|
||||
setIsVisible(false)
|
||||
})
|
||||
|
||||
// Calculate positions for modal and arrow
|
||||
useEffect(() => {
|
||||
if (isVisible && buttonRef.current) {
|
||||
const buttonRect = buttonRef.current.getBoundingClientRect()
|
||||
const buttonCenter = buttonRect.left + buttonRect.width / 2
|
||||
const rightPosition = document.documentElement.clientWidth - buttonCenter - 5
|
||||
|
||||
setArrowPosition(rightPosition)
|
||||
setMenuPosition(buttonRect.top + 1)
|
||||
}
|
||||
}, [isVisible, viewportWidth, viewportHeight, buttonRef])
|
||||
|
||||
// Track container width for responsive layout
|
||||
useEffect(() => {
|
||||
if (!isVisible) return
|
||||
|
||||
const updateWidth = () => {
|
||||
if (itemsContainerRef.current) {
|
||||
setContainerWidth(itemsContainerRef.current.offsetWidth)
|
||||
}
|
||||
}
|
||||
|
||||
// Initial measurement
|
||||
updateWidth()
|
||||
|
||||
// Set up resize observer
|
||||
const resizeObserver = new ResizeObserver(updateWidth)
|
||||
if (itemsContainerRef.current) {
|
||||
resizeObserver.observe(itemsContainerRef.current)
|
||||
}
|
||||
|
||||
// Clean up
|
||||
return () => {
|
||||
resizeObserver.disconnect()
|
||||
}
|
||||
}, [isVisible])
|
||||
|
||||
const toggleFavorite = useCallback(
|
||||
(actionId: string) => {
|
||||
const currentFavorites = autoApprovalSettings.favorites || []
|
||||
let newFavorites: string[]
|
||||
|
||||
if (currentFavorites.includes(actionId)) {
|
||||
newFavorites = currentFavorites.filter((id) => id !== actionId)
|
||||
} else {
|
||||
newFavorites = [...currentFavorites, actionId]
|
||||
}
|
||||
|
||||
vscode.postMessage({
|
||||
type: "autoApprovalSettings",
|
||||
autoApprovalSettings: {
|
||||
...autoApprovalSettings,
|
||||
version: (autoApprovalSettings.version ?? 1) + 1,
|
||||
favorites: newFavorites,
|
||||
},
|
||||
})
|
||||
},
|
||||
[autoApprovalSettings],
|
||||
)
|
||||
|
||||
const updateAction = useCallback(
|
||||
(action: ActionMetadata, value: boolean) => {
|
||||
const actionId = action.id
|
||||
const subActionId = action.subAction?.id
|
||||
|
||||
if (actionId === "enableAll" || subActionId === "enableAll") {
|
||||
toggleAll(action, value)
|
||||
return
|
||||
}
|
||||
|
||||
if (actionId === "enableNotifications" || subActionId === "enableNotifications") {
|
||||
updateNotifications(action, value)
|
||||
return
|
||||
}
|
||||
|
||||
let newActions = {
|
||||
...autoApprovalSettings.actions,
|
||||
[actionId]: value,
|
||||
}
|
||||
|
||||
if (value === false && subActionId) {
|
||||
newActions[subActionId] = false
|
||||
}
|
||||
|
||||
if (value === true && action.parentActionId) {
|
||||
newActions[action.parentActionId as keyof AutoApprovalSettings["actions"]] = true
|
||||
}
|
||||
|
||||
// Check if this will result in any enabled actions
|
||||
const willHaveEnabledActions = Object.values(newActions).some(Boolean)
|
||||
|
||||
vscode.postMessage({
|
||||
type: "autoApprovalSettings",
|
||||
autoApprovalSettings: {
|
||||
...autoApprovalSettings,
|
||||
version: (autoApprovalSettings.version ?? 1) + 1,
|
||||
actions: newActions,
|
||||
enabled: willHaveEnabledActions,
|
||||
},
|
||||
})
|
||||
},
|
||||
[autoApprovalSettings],
|
||||
)
|
||||
|
||||
const updateMaxRequests = useCallback(
|
||||
(maxRequests: number) => {
|
||||
const currentSettings = autoApprovalSettings
|
||||
vscode.postMessage({
|
||||
type: "autoApprovalSettings",
|
||||
autoApprovalSettings: {
|
||||
...currentSettings,
|
||||
version: (currentSettings.version ?? 1) + 1,
|
||||
maxRequests,
|
||||
},
|
||||
})
|
||||
},
|
||||
[autoApprovalSettings],
|
||||
)
|
||||
|
||||
const updateNotifications = useCallback(
|
||||
(action: ActionMetadata, checked: boolean) => {
|
||||
if (action.id === "enableNotifications") {
|
||||
const currentSettings = autoApprovalSettings
|
||||
vscode.postMessage({
|
||||
type: "autoApprovalSettings",
|
||||
autoApprovalSettings: {
|
||||
...currentSettings,
|
||||
version: (currentSettings.version ?? 1) + 1,
|
||||
enableNotifications: checked,
|
||||
},
|
||||
})
|
||||
}
|
||||
},
|
||||
[autoApprovalSettings],
|
||||
)
|
||||
|
||||
const toggleAll = useCallback(
|
||||
(action: ActionMetadata, checked: boolean) => {
|
||||
let actions = { ...autoApprovalSettings.actions }
|
||||
|
||||
for (const action of Object.keys(actions)) {
|
||||
actions[action as keyof AutoApprovalSettings["actions"]] = checked
|
||||
}
|
||||
|
||||
vscode.postMessage({
|
||||
type: "autoApprovalSettings",
|
||||
autoApprovalSettings: {
|
||||
...autoApprovalSettings,
|
||||
version: (autoApprovalSettings.version ?? 1) + 1,
|
||||
actions,
|
||||
},
|
||||
})
|
||||
},
|
||||
[autoApprovalSettings],
|
||||
)
|
||||
|
||||
// Check if action is enabled
|
||||
const isChecked = (action: ActionMetadata): boolean => {
|
||||
if (action.id === "enableNotifications") {
|
||||
return autoApprovalSettings.enableNotifications
|
||||
}
|
||||
if (action.id === "enableAll") {
|
||||
return Object.values(autoApprovalSettings.actions).every(Boolean)
|
||||
}
|
||||
return autoApprovalSettings.actions[action.id] ?? false
|
||||
}
|
||||
|
||||
// Check if action is favorited
|
||||
const isFavorited = (action: ActionMetadata): boolean => {
|
||||
return favorites.includes(action.id)
|
||||
}
|
||||
|
||||
if (!isVisible) return null
|
||||
|
||||
return (
|
||||
<div ref={modalRef}>
|
||||
<div
|
||||
className="fixed left-[15px] right-[15px] border border-[var(--vscode-editorGroup-border)] p-3 rounded z-[1000] overflow-y-auto"
|
||||
style={{
|
||||
bottom: `calc(100vh - ${menuPosition}px + 6px)`,
|
||||
background: CODE_BLOCK_BG_COLOR,
|
||||
maxHeight: "calc(100vh - 100px)",
|
||||
overscrollBehavior: "contain",
|
||||
}}>
|
||||
<div
|
||||
className="fixed w-[10px] h-[10px] z-[-1] rotate-45 border-r border-b border-[var(--vscode-editorGroup-border)]"
|
||||
style={{
|
||||
bottom: `calc(100vh - ${menuPosition}px)`,
|
||||
right: arrowPosition,
|
||||
background: CODE_BLOCK_BG_COLOR,
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="flex justify-between items-center mb-3">
|
||||
<div className="m-0 text-base font-semibold">Auto-approve Settings</div>
|
||||
<VSCodeButton appearance="icon" onClick={() => setIsVisible(false)}>
|
||||
<span className="codicon codicon-close text-[10px]"></span>
|
||||
</VSCodeButton>
|
||||
</div>
|
||||
|
||||
<HeroTooltip
|
||||
content="Auto-approve allows Cline to perform the following actions without asking for permission. Please use with caution and only enable if you understand the risks."
|
||||
placement="top">
|
||||
<div className="mb-3">
|
||||
<span className="text-[color:var(--vscode-foreground)] font-medium">Actions:</span>
|
||||
</div>
|
||||
</HeroTooltip>
|
||||
|
||||
<div
|
||||
ref={itemsContainerRef}
|
||||
className="relative mb-6"
|
||||
style={{
|
||||
columnCount: containerWidth > breakpoint ? 2 : 1,
|
||||
columnGap: "4px",
|
||||
}}>
|
||||
{/* Vertical separator line - only visible in two-column mode */}
|
||||
{containerWidth > breakpoint && (
|
||||
<div
|
||||
className="absolute left-1/2 top-0 bottom-0 w-[0.5px] opacity-20"
|
||||
style={{
|
||||
background: getAsVar(VSC_TITLEBAR_INACTIVE_FOREGROUND),
|
||||
transform: "translateX(-50%)", // Center the line
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* All items in a single list - CSS Grid will handle the column distribution */}
|
||||
{ACTION_METADATA.map((action) => (
|
||||
<AutoApproveMenuItem
|
||||
key={action.id}
|
||||
action={action}
|
||||
isChecked={isChecked}
|
||||
isFavorited={isFavorited}
|
||||
onToggle={updateAction}
|
||||
onToggleFavorite={toggleFavorite}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mb-3">
|
||||
<span className="text-[color:var(--vscode-foreground)] font-medium">Quick Settings:</span>
|
||||
</div>
|
||||
|
||||
<AutoApproveMenuItem
|
||||
key={NOTIFICATIONS_SETTING.id}
|
||||
action={NOTIFICATIONS_SETTING}
|
||||
isChecked={isChecked}
|
||||
isFavorited={isFavorited}
|
||||
onToggle={updateAction}
|
||||
onToggleFavorite={toggleFavorite}
|
||||
/>
|
||||
|
||||
<HeroTooltip
|
||||
content="Cline will automatically make this many API requests before asking for approval to proceed with the task."
|
||||
placement="top">
|
||||
<div className="flex items-center pl-1.5 my-2">
|
||||
<span className="codicon codicon-settings text-[#CCCCCC] text-[14px]" />
|
||||
<span className="text-[#CCCCCC] text-xs font-medium ml-2">Max Requests:</span>
|
||||
<VSCodeTextField
|
||||
className="flex-1 w-full pr-[35px] ml-4"
|
||||
value={autoApprovalSettings.maxRequests.toString()}
|
||||
onInput={(e) => {
|
||||
const input = e.target as HTMLInputElement
|
||||
// Remove any non-numeric characters
|
||||
input.value = input.value.replace(/[^0-9]/g, "")
|
||||
const value = parseInt(input.value)
|
||||
if (!isNaN(value) && value > 0) {
|
||||
updateMaxRequests(value)
|
||||
}
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
// Prevent non-numeric keys (except for backspace, delete, arrows)
|
||||
if (!/^\d$/.test(e.key) && !["Backspace", "Delete", "ArrowLeft", "ArrowRight"].includes(e.key)) {
|
||||
e.preventDefault()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</HeroTooltip>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AutoApproveModal
|
||||
@@ -0,0 +1,79 @@
|
||||
import { ActionMetadata } from "./types"
|
||||
|
||||
export const ACTION_METADATA: ActionMetadata[] = [
|
||||
{
|
||||
id: "enableAll",
|
||||
label: "Enable all",
|
||||
shortName: "All",
|
||||
description: "Enable all actions.",
|
||||
icon: "codicon-checklist",
|
||||
},
|
||||
{
|
||||
id: "readFiles",
|
||||
label: "Read project files",
|
||||
shortName: "Read",
|
||||
description: "Allows Cline to read files within your workspace.",
|
||||
icon: "codicon-search",
|
||||
subAction: {
|
||||
id: "readFilesExternally",
|
||||
label: "Read all files",
|
||||
shortName: "Read (all)",
|
||||
description: "Allows Cline to read any file on your computer.",
|
||||
icon: "codicon-folder-opened",
|
||||
parentActionId: "readFiles",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "editFiles",
|
||||
label: "Edit project files",
|
||||
shortName: "Edit",
|
||||
description: "Allows Cline to modify files within your workspace.",
|
||||
icon: "codicon-edit",
|
||||
subAction: {
|
||||
id: "editFilesExternally",
|
||||
label: "Edit all files",
|
||||
shortName: "Edit (all)",
|
||||
description: "Allows Cline to modify any file on your computer.",
|
||||
icon: "codicon-files",
|
||||
parentActionId: "editFiles",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "executeSafeCommands",
|
||||
label: "Execute safe commands",
|
||||
shortName: "Safe Commands",
|
||||
description:
|
||||
"Allows Cline to execute safe terminal commands. If the model determines a command is potentially destructive, it will still require approval.",
|
||||
icon: "codicon-terminal",
|
||||
subAction: {
|
||||
id: "executeAllCommands",
|
||||
label: "Execute all commands",
|
||||
shortName: "All Commands",
|
||||
description: "Allows Cline to execute all terminal commands. Use at your own risk.",
|
||||
icon: "codicon-terminal-bash",
|
||||
parentActionId: "executeSafeCommands",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "useBrowser",
|
||||
label: "Use the browser",
|
||||
shortName: "Browser",
|
||||
description: "Allows Cline to launch and interact with any website in a browser.",
|
||||
icon: "codicon-globe",
|
||||
},
|
||||
{
|
||||
id: "useMcp",
|
||||
label: "Use MCP servers",
|
||||
shortName: "MCP",
|
||||
description: "Allows Cline to use configured MCP servers which may modify filesystem or interact with APIs.",
|
||||
icon: "codicon-server",
|
||||
},
|
||||
]
|
||||
|
||||
export const NOTIFICATIONS_SETTING: ActionMetadata = {
|
||||
id: "enableNotifications",
|
||||
label: "Enable notifications",
|
||||
shortName: "Notifications",
|
||||
description: "Receive system notifications when Cline requires approval to proceed or when a task is completed.",
|
||||
icon: "codicon-bell",
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { AutoApprovalSettings } from "@shared/AutoApprovalSettings"
|
||||
|
||||
export interface ActionMetadata {
|
||||
id: keyof AutoApprovalSettings["actions"] | "enableNotifications" | "enableAll"
|
||||
label: string
|
||||
shortName: string
|
||||
description: string
|
||||
icon: string
|
||||
subAction?: ActionMetadata
|
||||
sub?: boolean
|
||||
parentActionId?: string
|
||||
}
|
||||
@@ -74,7 +74,7 @@ const RuleRow: React.FC<{
|
||||
}`}>
|
||||
<span className="flex-1 overflow-hidden break-all whitespace-normal flex items-center mr-1" title={rulePath}>
|
||||
{getRuleTypeIcon() && <span className="mr-1.5">{getRuleTypeIcon()}</span>}
|
||||
{displayName}
|
||||
<span className="ph-no-capture">{displayName}</span>
|
||||
</span>
|
||||
|
||||
{/* Toggle Switch */}
|
||||
|
||||
@@ -151,7 +151,9 @@ const CodeBlock = memo(({ source, forceWrap = false }: CodeBlockProps) => {
|
||||
maxHeight: forceWrap ? "none" : "100%",
|
||||
backgroundColor: CODE_BLOCK_BG_COLOR,
|
||||
}}>
|
||||
<StyledMarkdown forceWrap={forceWrap}>{reactContent}</StyledMarkdown>
|
||||
<StyledMarkdown className="ph-no-capture" forceWrap={forceWrap}>
|
||||
{reactContent}
|
||||
</StyledMarkdown>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -339,7 +339,7 @@ const MarkdownBlock = memo(({ markdown }: MarkdownBlockProps) => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<StyledMarkdown>{reactContent}</StyledMarkdown>
|
||||
<StyledMarkdown className="ph-no-capture">{reactContent}</StyledMarkdown>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -12,6 +12,26 @@ const BannerContainer = styled.div`
|
||||
gap: 10px;
|
||||
flex-shrink: 0;
|
||||
margin-bottom: 6px;
|
||||
position: relative;
|
||||
`
|
||||
|
||||
const CloseButton = styled.button`
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--vscode-foreground);
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 4px;
|
||||
opacity: 0.7;
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
`
|
||||
|
||||
const ButtonContainer = styled.div`
|
||||
@@ -25,31 +45,31 @@ const ButtonContainer = styled.div`
|
||||
`
|
||||
|
||||
const TelemetryBanner = () => {
|
||||
const [hasChosen, setHasChosen] = useState(false)
|
||||
|
||||
const handleAllow = () => {
|
||||
setHasChosen(true)
|
||||
vscode.postMessage({ type: "telemetrySetting", telemetrySetting: "enabled" satisfies TelemetrySetting })
|
||||
}
|
||||
|
||||
const handleDeny = () => {
|
||||
setHasChosen(true)
|
||||
vscode.postMessage({ type: "telemetrySetting", telemetrySetting: "disabled" satisfies TelemetrySetting })
|
||||
}
|
||||
|
||||
const handleOpenSettings = () => {
|
||||
handleClose()
|
||||
vscode.postMessage({ type: "openSettings" })
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
vscode.postMessage({ type: "telemetrySetting", telemetrySetting: "enabled" satisfies TelemetrySetting })
|
||||
}
|
||||
|
||||
return (
|
||||
<BannerContainer>
|
||||
<CloseButton onClick={handleClose} aria-label="Close banner and enable telemetry">
|
||||
✕
|
||||
</CloseButton>
|
||||
<div>
|
||||
<strong>Help Improve Cline</strong>
|
||||
<i>
|
||||
<br />
|
||||
(and access experimental features)
|
||||
</i>
|
||||
<div style={{ marginTop: 4 }}>
|
||||
Send anonymous error and usage data to help us fix bugs and improve the extension. No code, prompts, or
|
||||
personal information is ever sent.
|
||||
Cline collects anonymous error and usage data to help us fix bugs and improve the extension. No code, prompts,
|
||||
or personal information is ever sent.
|
||||
<div style={{ marginTop: 4 }}>
|
||||
You can always change this in{" "}
|
||||
You can turn this setting off in{" "}
|
||||
<VSCodeLink href="#" onClick={handleOpenSettings}>
|
||||
settings
|
||||
</VSCodeLink>
|
||||
@@ -57,14 +77,6 @@ const TelemetryBanner = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ButtonContainer>
|
||||
<VSCodeButton appearance="primary" onClick={handleAllow} disabled={hasChosen}>
|
||||
Allow
|
||||
</VSCodeButton>
|
||||
<VSCodeButton appearance="secondary" onClick={handleDeny} disabled={hasChosen}>
|
||||
Deny
|
||||
</VSCodeButton>
|
||||
</ButtonContainer>
|
||||
</BannerContainer>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -105,6 +105,8 @@ const HistoryPreview = ({ showHistoryView }: HistoryPreviewProps) => {
|
||||
)}
|
||||
|
||||
<div
|
||||
id={`history-preview-task-${item.id}`}
|
||||
className="history-preview-task"
|
||||
style={{
|
||||
fontSize: "var(--vscode-font-size)",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
@@ -117,7 +119,7 @@ const HistoryPreview = ({ showHistoryView }: HistoryPreviewProps) => {
|
||||
wordBreak: "break-word",
|
||||
overflowWrap: "anywhere",
|
||||
}}>
|
||||
{item.task}
|
||||
<span className="ph-no-capture">{item.task}</span>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
|
||||
@@ -17,14 +17,44 @@ type HistoryViewProps = {
|
||||
|
||||
type SortOption = "newest" | "oldest" | "mostExpensive" | "mostTokens" | "mostRelevant"
|
||||
|
||||
// Tailwind-styled radio with custom icon support - works independently of VSCodeRadioGroup but looks the same
|
||||
// Used for workspace and favorites filters
|
||||
|
||||
interface CustomFilterRadioProps {
|
||||
checked: boolean
|
||||
onChange: () => void
|
||||
icon: string
|
||||
label: string
|
||||
}
|
||||
|
||||
const CustomFilterRadio = ({ checked, onChange, icon, label }: CustomFilterRadioProps) => {
|
||||
return (
|
||||
<div
|
||||
onClick={onChange}
|
||||
className="flex items-center cursor-pointer py-[0.3em] px-0 mr-[10px] text-[var(--vscode-font-size)] select-none">
|
||||
<div
|
||||
className={`w-[14px] h-[14px] rounded-full border border-[var(--vscode-checkbox-border)] relative flex justify-center items-center mr-[6px] ${
|
||||
checked ? "bg-[var(--vscode-checkbox-background)]" : "bg-transparent"
|
||||
}`}>
|
||||
{checked && <div className="w-[6px] h-[6px] rounded-full bg-[var(--vscode-checkbox-foreground)]" />}
|
||||
</div>
|
||||
<span className="flex items-center gap-[3px]">
|
||||
<div className={`codicon codicon-${icon} text-[var(--vscode-button-background)] text-base`} />
|
||||
{label}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const HistoryView = ({ onDone }: HistoryViewProps) => {
|
||||
const { taskHistory, totalTasksSize } = useExtensionState()
|
||||
const { taskHistory, totalTasksSize, filePaths } = useExtensionState()
|
||||
const [searchQuery, setSearchQuery] = useState("")
|
||||
const [sortOption, setSortOption] = useState<SortOption>("newest")
|
||||
const [lastNonRelevantSort, setLastNonRelevantSort] = useState<SortOption | null>("newest")
|
||||
const [deleteAllDisabled, setDeleteAllDisabled] = useState(false)
|
||||
const [selectedItems, setSelectedItems] = useState<string[]>([])
|
||||
const [showFavoritesOnly, setShowFavoritesOnly] = useState(false)
|
||||
const [showCurrentWorkspaceOnly, setShowCurrentWorkspaceOnly] = useState(false)
|
||||
|
||||
// Keep track of pending favorite toggle operations
|
||||
const [pendingFavoriteToggles, setPendingFavoriteToggles] = useState<Record<string, boolean>>({})
|
||||
@@ -39,24 +69,23 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
|
||||
favoritesOnly: showFavoritesOnly,
|
||||
searchQuery: searchQuery || undefined,
|
||||
sortBy: sortOption,
|
||||
currentWorkspaceOnly: showCurrentWorkspaceOnly,
|
||||
})
|
||||
setFilteredTasks(response.tasks || [])
|
||||
} catch (error) {
|
||||
console.error("Error loading task history:", error)
|
||||
// Fallback to client-side filtering
|
||||
setFilteredTasks(
|
||||
taskHistory.filter((item) => {
|
||||
const valid = item.ts && item.task
|
||||
return valid && (!showFavoritesOnly || item.isFavorited)
|
||||
}),
|
||||
)
|
||||
}
|
||||
}, [showFavoritesOnly, searchQuery, sortOption, taskHistory])
|
||||
}, [showFavoritesOnly, showCurrentWorkspaceOnly, searchQuery, sortOption, taskHistory])
|
||||
|
||||
// Load when filters change
|
||||
useEffect(() => {
|
||||
// Force a complete refresh when both filters are active
|
||||
// to ensure proper combined filtering
|
||||
if (showFavoritesOnly && showCurrentWorkspaceOnly) {
|
||||
setFilteredTasks([])
|
||||
}
|
||||
loadTaskHistory()
|
||||
}, [loadTaskHistory])
|
||||
}, [loadTaskHistory, showFavoritesOnly, showCurrentWorkspaceOnly])
|
||||
|
||||
const toggleFavorite = useCallback(
|
||||
async (taskId: string, currentValue: boolean) => {
|
||||
@@ -69,8 +98,8 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
|
||||
isFavorited: !currentValue,
|
||||
})
|
||||
|
||||
// Refresh if favorites filter is active
|
||||
if (showFavoritesOnly) {
|
||||
// Refresh if either filter is active to ensure proper combined filtering
|
||||
if (showFavoritesOnly || showCurrentWorkspaceOnly) {
|
||||
loadTaskHistory()
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -320,46 +349,18 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
|
||||
<VSCodeRadio value="mostRelevant" disabled={!searchQuery} style={{ opacity: searchQuery ? 1 : 0.5 }}>
|
||||
Most Relevant
|
||||
</VSCodeRadio>
|
||||
<div
|
||||
onClick={() => setShowFavoritesOnly(!showFavoritesOnly)}
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
marginLeft: "6px",
|
||||
cursor: "pointer",
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
width: "14px",
|
||||
height: "14px",
|
||||
borderRadius: "50%",
|
||||
border: "1px solid var(--vscode-checkbox-border)",
|
||||
backgroundColor: showFavoritesOnly ? "var(--vscode-checkbox-background)" : "transparent",
|
||||
position: "relative",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
marginRight: "6px",
|
||||
}}>
|
||||
{showFavoritesOnly && (
|
||||
<div
|
||||
style={{
|
||||
width: "6px",
|
||||
height: "6px",
|
||||
borderRadius: "50%",
|
||||
backgroundColor: "var(--vscode-checkbox-foreground)",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<span style={{ display: "flex", alignItems: "center", gap: "6px", userSelect: "none" }}>
|
||||
<div
|
||||
className="codicon codicon-star-full"
|
||||
style={{ color: "var(--vscode-button-background)", fontSize: "14px" }}
|
||||
/>
|
||||
Favorites
|
||||
</span>
|
||||
</div>
|
||||
<CustomFilterRadio
|
||||
checked={showCurrentWorkspaceOnly}
|
||||
onChange={() => setShowCurrentWorkspaceOnly(!showCurrentWorkspaceOnly)}
|
||||
icon="workspace"
|
||||
label="Workspace"
|
||||
/>
|
||||
<CustomFilterRadio
|
||||
checked={showFavoritesOnly}
|
||||
onChange={() => setShowFavoritesOnly(!showFavoritesOnly)}
|
||||
icon="star-full"
|
||||
label="Favorites"
|
||||
/>
|
||||
</VSCodeRadioGroup>
|
||||
|
||||
<div style={{ display: "flex", justifyContent: "flex-end", gap: "10px" }}>
|
||||
@@ -514,11 +515,14 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
|
||||
whiteSpace: "pre-wrap",
|
||||
wordBreak: "break-word",
|
||||
overflowWrap: "anywhere",
|
||||
}}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: item.task,
|
||||
}}
|
||||
/>
|
||||
}}>
|
||||
<span
|
||||
className="ph-no-capture"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: item.task,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useCallback, useState, useRef, useMemo } from "react"
|
||||
import styled from "styled-components"
|
||||
import { McpMarketplaceItem, McpServer } from "@shared/mcp"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
import { useEvent } from "react-use"
|
||||
import { McpServiceClient } from "@/services/grpc-client"
|
||||
|
||||
interface McpMarketplaceCardProps {
|
||||
item: McpMarketplaceItem
|
||||
@@ -107,15 +107,17 @@ const McpMarketplaceCard = ({ item, installedServers }: McpMarketplaceCardProps)
|
||||
{item.name}
|
||||
</h3>
|
||||
<div
|
||||
onClick={(e) => {
|
||||
onClick={async (e) => {
|
||||
e.preventDefault() // Prevent card click when clicking install
|
||||
e.stopPropagation() // Stop event from bubbling up to parent link
|
||||
if (!isInstalled && !isDownloading) {
|
||||
setIsDownloading(true)
|
||||
vscode.postMessage({
|
||||
type: "downloadMcp",
|
||||
mcpId: item.mcpId,
|
||||
})
|
||||
try {
|
||||
await McpServiceClient.downloadMcp({ value: item.mcpId })
|
||||
} catch (error) {
|
||||
setIsDownloading(false)
|
||||
console.error("Failed to download MCP:", error)
|
||||
}
|
||||
}
|
||||
}}
|
||||
style={{}}>
|
||||
|
||||
@@ -316,6 +316,7 @@ const ApiOptions = ({
|
||||
<VSCodeOption value="openai-native">OpenAI</VSCodeOption>
|
||||
<VSCodeOption value="vscode-lm">VS Code LM API</VSCodeOption>
|
||||
<VSCodeOption value="requesty">Requesty</VSCodeOption>
|
||||
<VSCodeOption value="fireworks">Fireworks</VSCodeOption>
|
||||
<VSCodeOption value="together">Together</VSCodeOption>
|
||||
<VSCodeOption value="qwen">Alibaba Qwen</VSCodeOption>
|
||||
<VSCodeOption value="doubao">Bytedance Doubao</VSCodeOption>
|
||||
@@ -1370,6 +1371,97 @@ const ApiOptions = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedProvider === "fireworks" && (
|
||||
<div>
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.fireworksApiKey || ""}
|
||||
style={{ width: "100%" }}
|
||||
type="password"
|
||||
onInput={handleInputChange("fireworksApiKey")}
|
||||
placeholder="Enter API Key...">
|
||||
<span style={{ fontWeight: 500 }}>Fireworks API Key</span>
|
||||
</VSCodeTextField>
|
||||
<p
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
marginTop: 3,
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
}}>
|
||||
This key is stored locally and only used to make API requests from this extension.
|
||||
{!apiConfiguration?.fireworksApiKey && (
|
||||
<VSCodeLink
|
||||
href="https://fireworks.ai/settings/users/api-keys"
|
||||
style={{
|
||||
display: "inline",
|
||||
fontSize: "inherit",
|
||||
}}>
|
||||
You can get a Fireworks API key by signing up here.
|
||||
</VSCodeLink>
|
||||
)}
|
||||
</p>
|
||||
<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={(e) => {
|
||||
const value = (e.target as HTMLInputElement).value
|
||||
if (!value) {
|
||||
return
|
||||
}
|
||||
const num = parseInt(value, 10)
|
||||
if (isNaN(num)) {
|
||||
return
|
||||
}
|
||||
handleInputChange("fireworksModelMaxCompletionTokens")({
|
||||
target: {
|
||||
value: num,
|
||||
},
|
||||
})
|
||||
}}
|
||||
placeholder={"2000"}>
|
||||
<span style={{ fontWeight: 500 }}>Max Completion Tokens</span>
|
||||
</VSCodeTextField>
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.fireworksModelMaxTokens?.toString() || ""}
|
||||
style={{ width: "100%", marginBottom: 8 }}
|
||||
onInput={(e) => {
|
||||
const value = (e.target as HTMLInputElement).value
|
||||
if (!value) {
|
||||
return
|
||||
}
|
||||
const num = parseInt(value)
|
||||
if (isNaN(num)) {
|
||||
return
|
||||
}
|
||||
handleInputChange("fireworksModelMaxTokens")({
|
||||
target: {
|
||||
value: num,
|
||||
},
|
||||
})
|
||||
}}
|
||||
placeholder={"4000"}>
|
||||
<span style={{ fontWeight: 500 }}>Max Context Tokens</span>
|
||||
</VSCodeTextField>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedProvider === "together" && (
|
||||
<div>
|
||||
<VSCodeTextField
|
||||
@@ -1531,14 +1623,6 @@ const ApiOptions = ({
|
||||
|
||||
{selectedProvider === "litellm" && (
|
||||
<div>
|
||||
<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?.liteLlmBaseUrl || ""}
|
||||
style={{ width: "100%" }}
|
||||
@@ -1547,11 +1631,19 @@ const ApiOptions = ({
|
||||
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. gpt-4"}>
|
||||
placeholder={"e.g. anthropic/claude-3-7-sonnet-20250219"}>
|
||||
<span style={{ fontWeight: 500 }}>Model ID</span>
|
||||
</VSCodeTextField>
|
||||
|
||||
@@ -1594,6 +1686,119 @@ const ApiOptions = ({
|
||||
</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",
|
||||
@@ -2276,7 +2481,7 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration):
|
||||
return {
|
||||
selectedProvider: provider,
|
||||
selectedModelId: apiConfiguration?.liteLlmModelId || "",
|
||||
selectedModelInfo: liteLlmModelInfoSaneDefaults,
|
||||
selectedModelInfo: apiConfiguration?.liteLlmModelInfo || liteLlmModelInfoSaneDefaults,
|
||||
}
|
||||
case "xai":
|
||||
return getProviderData(xaiModels, xaiDefaultModelId)
|
||||
|
||||
@@ -37,8 +37,22 @@ const ConnectionStatusIndicator = ({
|
||||
)
|
||||
}
|
||||
|
||||
const CollapsibleContent = styled.div<{ isOpen: boolean }>`
|
||||
overflow: hidden;
|
||||
transition:
|
||||
max-height 0.3s ease-in-out,
|
||||
opacity 0.3s ease-in-out,
|
||||
margin-top 0.3s ease-in-out,
|
||||
visibility 0.3s ease-in-out;
|
||||
max-height: ${({ isOpen }) => (isOpen ? "1000px" : "0")}; // Sufficiently large height
|
||||
opacity: ${({ isOpen }) => (isOpen ? 1 : 0)};
|
||||
margin-top: ${({ isOpen }) => (isOpen ? "15px" : "0")};
|
||||
visibility: ${({ isOpen }) => (isOpen ? "visible" : "hidden")};
|
||||
`
|
||||
|
||||
export const BrowserSettingsSection: React.FC = () => {
|
||||
const { browserSettings } = useExtensionState()
|
||||
const [localChromePath, setLocalChromePath] = useState(browserSettings.chromeExecutablePath || "")
|
||||
const [isCheckingConnection, setIsCheckingConnection] = useState(false)
|
||||
const [connectionStatus, setConnectionStatus] = useState<boolean | null>(null)
|
||||
const [relaunchResult, setRelaunchResult] = useState<{ success: boolean; message: string } | null>(null)
|
||||
@@ -91,6 +105,14 @@ export const BrowserSettingsSection: React.FC = () => {
|
||||
})
|
||||
}, [])
|
||||
|
||||
// Sync localChromePath with global state
|
||||
useEffect(() => {
|
||||
if (browserSettings.chromeExecutablePath !== localChromePath) {
|
||||
setLocalChromePath(browserSettings.chromeExecutablePath || "")
|
||||
}
|
||||
// Removed sync for local disableToolUse state
|
||||
}, [browserSettings.chromeExecutablePath, browserSettings.disableToolUse])
|
||||
|
||||
// Debounced connection check function
|
||||
const debouncedCheckConnection = useCallback(
|
||||
debounce(() => {
|
||||
@@ -147,6 +169,8 @@ export const BrowserSettingsSection: React.FC = () => {
|
||||
},
|
||||
remoteBrowserEnabled: browserSettings.remoteBrowserEnabled,
|
||||
remoteBrowserHost: browserSettings.remoteBrowserHost,
|
||||
chromeExecutablePath: browserSettings.chromeExecutablePath,
|
||||
disableToolUse: browserSettings.disableToolUse,
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response.value) {
|
||||
@@ -169,6 +193,8 @@ export const BrowserSettingsSection: React.FC = () => {
|
||||
remoteBrowserEnabled: enabled,
|
||||
// If disabling, also clear the host
|
||||
remoteBrowserHost: enabled ? browserSettings.remoteBrowserHost : undefined,
|
||||
chromeExecutablePath: browserSettings.chromeExecutablePath,
|
||||
disableToolUse: browserSettings.disableToolUse,
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response.value) {
|
||||
@@ -189,6 +215,55 @@ export const BrowserSettingsSection: React.FC = () => {
|
||||
},
|
||||
remoteBrowserEnabled: browserSettings.remoteBrowserEnabled,
|
||||
remoteBrowserHost: host,
|
||||
chromeExecutablePath: browserSettings.chromeExecutablePath,
|
||||
disableToolUse: browserSettings.disableToolUse,
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response.value) {
|
||||
console.error("Failed to update browser settings")
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error updating browser settings:", error)
|
||||
})
|
||||
}
|
||||
|
||||
const debouncedUpdateChromePath = useCallback(
|
||||
debounce((newPath: string | undefined) => {
|
||||
BrowserServiceClient.updateBrowserSettings({
|
||||
metadata: {},
|
||||
viewport: {
|
||||
width: browserSettings.viewport.width,
|
||||
height: browserSettings.viewport.height,
|
||||
},
|
||||
remoteBrowserEnabled: browserSettings.remoteBrowserEnabled,
|
||||
remoteBrowserHost: browserSettings.remoteBrowserHost,
|
||||
chromeExecutablePath: newPath,
|
||||
disableToolUse: browserSettings.disableToolUse,
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response.value) {
|
||||
console.error("Failed to update browser settings for chromeExecutablePath")
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error updating browser settings for chromeExecutablePath:", error)
|
||||
})
|
||||
}, 500),
|
||||
[browserSettings],
|
||||
)
|
||||
|
||||
const updateChromeExecutablePath = (path: string | undefined) => {
|
||||
BrowserServiceClient.updateBrowserSettings({
|
||||
metadata: {},
|
||||
viewport: {
|
||||
width: browserSettings.viewport.width,
|
||||
height: browserSettings.viewport.height,
|
||||
},
|
||||
remoteBrowserEnabled: browserSettings.remoteBrowserEnabled,
|
||||
remoteBrowserHost: browserSettings.remoteBrowserHost,
|
||||
chromeExecutablePath: path,
|
||||
disableToolUse: browserSettings.disableToolUse,
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response.value) {
|
||||
@@ -247,6 +322,28 @@ export const BrowserSettingsSection: React.FC = () => {
|
||||
return () => clearInterval(pollInterval)
|
||||
}, [browserSettings.remoteBrowserEnabled, checkConnectionOnce])
|
||||
|
||||
const updateDisableToolUse = (disabled: boolean) => {
|
||||
BrowserServiceClient.updateBrowserSettings({
|
||||
metadata: {},
|
||||
viewport: {
|
||||
width: browserSettings.viewport.width,
|
||||
height: browserSettings.viewport.height,
|
||||
},
|
||||
remoteBrowserEnabled: browserSettings.remoteBrowserEnabled,
|
||||
remoteBrowserHost: browserSettings.remoteBrowserHost,
|
||||
chromeExecutablePath: browserSettings.chromeExecutablePath,
|
||||
disableToolUse: disabled,
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response.value) {
|
||||
console.error("Failed to update disableToolUse setting")
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error updating disableToolUse setting:", error)
|
||||
})
|
||||
}
|
||||
|
||||
const relaunchChromeDebugMode = () => {
|
||||
setDebugMode(true)
|
||||
setRelaunchResult(null)
|
||||
@@ -260,121 +357,169 @@ export const BrowserSettingsSection: React.FC = () => {
|
||||
// Determine if we should show the relaunch button
|
||||
const isRemoteEnabled = Boolean(browserSettings.remoteBrowserEnabled)
|
||||
const shouldShowRelaunchButton = isRemoteEnabled && connectionStatus === false
|
||||
const isSubSettingsOpen = !(browserSettings.disableToolUse || false)
|
||||
|
||||
return (
|
||||
<div
|
||||
id="browser-settings-section"
|
||||
style={{ marginBottom: 20, borderTop: "1px solid var(--vscode-panel-border)", paddingTop: 15 }}>
|
||||
<h3 style={{ color: "var(--vscode-foreground)", margin: "0 0 10px 0", fontSize: "14px" }}>Browser Settings</h3>
|
||||
<div style={{ marginBottom: 15 }}>
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
<label style={{ fontWeight: "500", display: "block", marginBottom: 5 }}>Viewport size</label>
|
||||
<VSCodeDropdown
|
||||
style={{ width: "100%" }}
|
||||
value={
|
||||
Object.entries(BROWSER_VIEWPORT_PRESETS).find(([_, size]) => {
|
||||
const typedSize = size as { width: number; height: number }
|
||||
return (
|
||||
typedSize.width === browserSettings.viewport.width &&
|
||||
typedSize.height === browserSettings.viewport.height
|
||||
)
|
||||
})?.[0]
|
||||
}
|
||||
onChange={(event) => handleViewportChange(event as Event)}>
|
||||
{Object.entries(BROWSER_VIEWPORT_PRESETS).map(([name]) => (
|
||||
<VSCodeOption key={name} value={name}>
|
||||
{name}
|
||||
</VSCodeOption>
|
||||
))}
|
||||
</VSCodeDropdown>
|
||||
</div>
|
||||
|
||||
{/* Master Toggle */}
|
||||
<div style={{ marginBottom: isSubSettingsOpen ? 0 : 10 }}>
|
||||
<VSCodeCheckbox
|
||||
checked={browserSettings.disableToolUse || false}
|
||||
onChange={(e) => updateDisableToolUse((e.target as HTMLInputElement).checked)}>
|
||||
Disable browser tool usage
|
||||
</VSCodeCheckbox>
|
||||
<p
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
margin: 0,
|
||||
margin: "4px 0 0 0px",
|
||||
}}>
|
||||
Set the size of the browser viewport for screenshots and interactions.
|
||||
Prevent Cline from using browser actions (e.g. launch, click, type).
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: 0 }}>
|
||||
<div style={{ marginBottom: 4, display: "flex", alignItems: "center", justifyContent: "space-between" }}>
|
||||
<VSCodeCheckbox
|
||||
checked={browserSettings.remoteBrowserEnabled}
|
||||
onChange={(e) => updateRemoteBrowserEnabled((e.target as HTMLInputElement).checked)}>
|
||||
Use remote browser connection
|
||||
</VSCodeCheckbox>
|
||||
<ConnectionStatusIndicator
|
||||
isChecking={isCheckingConnection}
|
||||
isConnected={connectionStatus}
|
||||
remoteBrowserEnabled={browserSettings.remoteBrowserEnabled}
|
||||
/>
|
||||
<CollapsibleContent isOpen={isSubSettingsOpen}>
|
||||
<div style={{ marginBottom: 15 }}>
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
<label style={{ fontWeight: "500", display: "block", marginBottom: 5 }}>Viewport size</label>
|
||||
<VSCodeDropdown
|
||||
style={{ width: "100%" }}
|
||||
value={
|
||||
Object.entries(BROWSER_VIEWPORT_PRESETS).find(([_, size]) => {
|
||||
const typedSize = size as { width: number; height: number }
|
||||
return (
|
||||
typedSize.width === browserSettings.viewport.width &&
|
||||
typedSize.height === browserSettings.viewport.height
|
||||
)
|
||||
})?.[0]
|
||||
}
|
||||
onChange={(event) => handleViewportChange(event as Event)}>
|
||||
{Object.entries(BROWSER_VIEWPORT_PRESETS).map(([name]) => (
|
||||
<VSCodeOption key={name} value={name}>
|
||||
{name}
|
||||
</VSCodeOption>
|
||||
))}
|
||||
</VSCodeDropdown>
|
||||
</div>
|
||||
<p
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
margin: 0,
|
||||
}}>
|
||||
Set the size of the browser viewport for screenshots and interactions.
|
||||
</p>
|
||||
</div>
|
||||
<p
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
margin: "0 0 6px 0px",
|
||||
}}>
|
||||
Enable Cline to use your Chrome
|
||||
{isBundled ? "(not detected on your machine)" : detectedChromePath ? ` (${detectedChromePath})` : ""}. This
|
||||
requires starting Chrome in debug mode
|
||||
{browserSettings.remoteBrowserEnabled ? (
|
||||
<>
|
||||
{" "}
|
||||
manually (<code>--remote-debugging-port=9222</code>) or using the button below. Enter the host address
|
||||
or leave it blank for automatic discovery.
|
||||
</>
|
||||
) : (
|
||||
"."
|
||||
)}
|
||||
</p>
|
||||
|
||||
{browserSettings.remoteBrowserEnabled && (
|
||||
<div style={{ marginLeft: 0 }}>
|
||||
<VSCodeTextField
|
||||
value={browserSettings.remoteBrowserHost || ""}
|
||||
placeholder="http://localhost:9222"
|
||||
style={{ width: "100%", marginBottom: 8 }}
|
||||
onChange={(e: any) => updateRemoteBrowserHost(e.target.value || undefined)}
|
||||
<div style={{ marginBottom: 0 }}>
|
||||
{" "}
|
||||
{/* This div now contains Remote Connection & Chrome Path */}
|
||||
<div style={{ marginBottom: 4, display: "flex", alignItems: "center", justifyContent: "space-between" }}>
|
||||
<VSCodeCheckbox
|
||||
checked={browserSettings.remoteBrowserEnabled}
|
||||
onChange={(e) => updateRemoteBrowserEnabled((e.target as HTMLInputElement).checked)}>
|
||||
Use remote browser connection
|
||||
</VSCodeCheckbox>
|
||||
<ConnectionStatusIndicator
|
||||
isChecking={isCheckingConnection}
|
||||
isConnected={connectionStatus}
|
||||
remoteBrowserEnabled={browserSettings.remoteBrowserEnabled}
|
||||
/>
|
||||
|
||||
{shouldShowRelaunchButton && (
|
||||
<div style={{ display: "flex", gap: "10px", marginBottom: 8, justifyContent: "center" }}>
|
||||
<VSCodeButton style={{ flex: 1 }} disabled={debugMode} onClick={relaunchChromeDebugMode}>
|
||||
{debugMode ? "Relaunching Browser..." : "Relaunch Browser with Debug Mode"}
|
||||
</VSCodeButton>
|
||||
</div>
|
||||
</div>
|
||||
<p
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
margin: "0 0 6px 0px",
|
||||
}}>
|
||||
Enable Cline to use your Chrome
|
||||
{isBundled ? "(not detected on your machine)" : detectedChromePath ? ` (${detectedChromePath})` : ""}. You
|
||||
can specify a custom path below. Using a remote browser connection requires starting Chrome in debug mode
|
||||
{browserSettings.remoteBrowserEnabled ? (
|
||||
<>
|
||||
{" "}
|
||||
manually (<code>--remote-debugging-port=9222</code>) or using the button below. Enter the host
|
||||
address or leave it blank for automatic discovery.
|
||||
</>
|
||||
) : (
|
||||
"."
|
||||
)}
|
||||
</p>
|
||||
{/* Moved remote-specific settings to appear directly after enabling remote connection */}
|
||||
{browserSettings.remoteBrowserEnabled && (
|
||||
<div style={{ marginLeft: 0, marginTop: 8 }}>
|
||||
<VSCodeTextField
|
||||
value={browserSettings.remoteBrowserHost || ""}
|
||||
placeholder="http://localhost:9222"
|
||||
style={{ width: "100%", marginBottom: 8 }}
|
||||
onChange={(e: any) => updateRemoteBrowserHost(e.target.value || undefined)}
|
||||
/>
|
||||
|
||||
{relaunchResult && (
|
||||
<div
|
||||
{shouldShowRelaunchButton && (
|
||||
<div style={{ display: "flex", gap: "10px", marginBottom: 8, justifyContent: "center" }}>
|
||||
<VSCodeButton style={{ flex: 1 }} disabled={debugMode} onClick={relaunchChromeDebugMode}>
|
||||
{debugMode ? "Launching Browser..." : "Launch Browser with Debug Mode"}
|
||||
</VSCodeButton>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{relaunchResult && (
|
||||
<div
|
||||
style={{
|
||||
padding: "8px",
|
||||
marginBottom: "8px",
|
||||
backgroundColor: relaunchResult.success ? "rgba(0, 128, 0, 0.1)" : "rgba(255, 0, 0, 0.1)",
|
||||
color: relaunchResult.success
|
||||
? "var(--vscode-terminal-ansiGreen)"
|
||||
: "var(--vscode-terminal-ansiRed)",
|
||||
borderRadius: "3px",
|
||||
fontSize: "11px",
|
||||
whiteSpace: "pre-wrap",
|
||||
wordBreak: "break-word",
|
||||
}}>
|
||||
{relaunchResult.message}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p
|
||||
style={{
|
||||
padding: "8px",
|
||||
marginBottom: "8px",
|
||||
backgroundColor: relaunchResult.success ? "rgba(0, 128, 0, 0.1)" : "rgba(255, 0, 0, 0.1)",
|
||||
color: relaunchResult.success
|
||||
? "var(--vscode-terminal-ansiGreen)"
|
||||
: "var(--vscode-terminal-ansiRed)",
|
||||
borderRadius: "3px",
|
||||
fontSize: "11px",
|
||||
whiteSpace: "pre-wrap",
|
||||
wordBreak: "break-word",
|
||||
}}>
|
||||
{relaunchResult.message}
|
||||
</div>
|
||||
)}
|
||||
|
||||
fontSize: "12px",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
margin: 0,
|
||||
}}></p>
|
||||
</div>
|
||||
)}
|
||||
{/* Chrome Executable Path section now follows remote-specific settings */}
|
||||
<div style={{ marginBottom: 8, marginTop: 8 }}>
|
||||
<label htmlFor="chrome-executable-path" style={{ fontWeight: "500", display: "block", marginBottom: 5 }}>
|
||||
Chrome Executable Path (Optional)
|
||||
</label>
|
||||
<VSCodeTextField
|
||||
id="chrome-executable-path"
|
||||
value={localChromePath}
|
||||
placeholder="e.g., /usr/bin/google-chrome or C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe"
|
||||
style={{ width: "100%" }}
|
||||
onChange={(e: any) => {
|
||||
const newValue = e.target.value || ""
|
||||
setLocalChromePath(newValue)
|
||||
debouncedUpdateChromePath(newValue) // Send "" if empty, not undefined
|
||||
}}
|
||||
/>
|
||||
<p
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
margin: 0,
|
||||
}}></p>
|
||||
margin: "4px 0 0 0",
|
||||
}}>
|
||||
Leave blank to auto-detect.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useMount } from "react-use"
|
||||
import styled from "styled-components"
|
||||
import { openRouterDefaultModelId } from "@shared/api"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { ModelsServiceClient } from "@/services/grpc-client"
|
||||
import { ModelsServiceClient, StateServiceClient } from "@/services/grpc-client"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
import { highlight } from "../history/HistoryView"
|
||||
import { ModelInfoView, normalizeApiConfiguration } from "./ApiOptions"
|
||||
@@ -292,10 +292,9 @@ const OpenRouterModelPicker: React.FC<OpenRouterModelPickerProps> = ({ isPopup }
|
||||
isFavorite={isFavorite}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
vscode.postMessage({
|
||||
type: "toggleFavoriteModel",
|
||||
modelId: item.id,
|
||||
})
|
||||
StateServiceClient.toggleFavoriteModel({ value: item.id }).catch((error) =>
|
||||
console.error("Failed to toggle favorite model:", error),
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useMount } from "react-use"
|
||||
import styled from "styled-components"
|
||||
import { requestyDefaultModelId } from "../../../../src/shared/api"
|
||||
import { useExtensionState } from "../../context/ExtensionStateContext"
|
||||
import { vscode } from "../../utils/vscode"
|
||||
import { ModelsServiceClient } from "../../services/grpc-client"
|
||||
import { highlight } from "../history/HistoryView"
|
||||
import { ModelInfoView, normalizeApiConfiguration } from "./ApiOptions"
|
||||
import { CODE_BLOCK_BG_COLOR } from "../common/CodeBlock"
|
||||
@@ -43,7 +43,9 @@ const RequestyModelPicker: React.FC<RequestyModelPickerProps> = ({ isPopup }) =>
|
||||
}, [apiConfiguration])
|
||||
|
||||
useMount(() => {
|
||||
vscode.postMessage({ type: "refreshRequestyModels" })
|
||||
ModelsServiceClient.refreshRequestyModels({}).catch((err) => {
|
||||
console.error("Failed to refresh Requesty models:", err)
|
||||
})
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -101,6 +101,75 @@ vi.mock("../../../context/ExtensionStateContext", async (importOriginal) => {
|
||||
const actual = await importOriginal()
|
||||
return {
|
||||
...(actual || {}),
|
||||
useExtensionState: vi.fn(() => ({
|
||||
apiConfiguration: {
|
||||
apiProvider: "fireworks",
|
||||
fireworksApiKey: "",
|
||||
fireworksModelId: "",
|
||||
fireworksModelMaxCompletionTokens: 2000,
|
||||
fireworksModelMaxTokens: 4000,
|
||||
},
|
||||
setApiConfiguration: vi.fn(),
|
||||
uriScheme: "vscode",
|
||||
})),
|
||||
}
|
||||
})
|
||||
|
||||
describe("ApiOptions Component", () => {
|
||||
vi.clearAllMocks()
|
||||
const mockPostMessage = vi.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
//@ts-expect-error - vscode is not defined in the global namespace in test environment
|
||||
global.vscode = { postMessage: mockPostMessage }
|
||||
})
|
||||
|
||||
it("renders Fireworks API Key input", () => {
|
||||
render(
|
||||
<ExtensionStateContextProvider>
|
||||
<ApiOptions showModelOptions={true} />
|
||||
</ExtensionStateContextProvider>,
|
||||
)
|
||||
const apiKeyInput = screen.getByPlaceholderText("Enter API Key...")
|
||||
expect(apiKeyInput).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("renders Fireworks Model ID input", () => {
|
||||
render(
|
||||
<ExtensionStateContextProvider>
|
||||
<ApiOptions showModelOptions={true} />
|
||||
</ExtensionStateContextProvider>,
|
||||
)
|
||||
const modelIdInput = screen.getByPlaceholderText("Enter Model ID...")
|
||||
expect(modelIdInput).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("renders Fireworks Max Completion Tokens input", () => {
|
||||
render(
|
||||
<ExtensionStateContextProvider>
|
||||
<ApiOptions showModelOptions={true} />
|
||||
</ExtensionStateContextProvider>,
|
||||
)
|
||||
const maxCompletionTokensInput = screen.getByPlaceholderText("2000")
|
||||
expect(maxCompletionTokensInput).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("renders Fireworks Max Tokens input", () => {
|
||||
render(
|
||||
<ExtensionStateContextProvider>
|
||||
<ApiOptions showModelOptions={true} />
|
||||
</ExtensionStateContextProvider>,
|
||||
)
|
||||
const maxTokensInput = screen.getByPlaceholderText("4000")
|
||||
expect(maxTokensInput).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
vi.mock("../../../context/ExtensionStateContext", async (importOriginal) => {
|
||||
const actual = await importOriginal()
|
||||
return {
|
||||
...(actual || {}),
|
||||
// your mocked methods
|
||||
useExtensionState: vi.fn(() => ({
|
||||
apiConfiguration: {
|
||||
apiProvider: "openai",
|
||||
|
||||
@@ -11,6 +11,7 @@ import { StateServiceDefinition } from "@shared/proto/state"
|
||||
import { TaskServiceDefinition } from "@shared/proto/task"
|
||||
import { WebServiceDefinition } from "@shared/proto/web"
|
||||
import { ModelsServiceDefinition } from "@shared/proto/models"
|
||||
import { SlashServiceDefinition } from "@shared/proto/slash"
|
||||
|
||||
const AccountServiceClient = createGrpcClient(AccountServiceDefinition)
|
||||
const BrowserServiceClient = createGrpcClient(BrowserServiceDefinition)
|
||||
@@ -21,6 +22,7 @@ const StateServiceClient = createGrpcClient(StateServiceDefinition)
|
||||
const TaskServiceClient = createGrpcClient(TaskServiceDefinition)
|
||||
const WebServiceClient = createGrpcClient(WebServiceDefinition)
|
||||
const ModelsServiceClient = createGrpcClient(ModelsServiceDefinition)
|
||||
const SlashServiceClient = createGrpcClient(SlashServiceDefinition)
|
||||
|
||||
export {
|
||||
AccountServiceClient,
|
||||
@@ -32,4 +34,5 @@ export {
|
||||
TaskServiceClient,
|
||||
WebServiceClient,
|
||||
ModelsServiceClient,
|
||||
SlashServiceClient,
|
||||
}
|
||||
|
||||
@@ -73,6 +73,11 @@ export function validateApiConfiguration(apiConfiguration?: ApiConfiguration): s
|
||||
return "You must provide a valid API key or choose a different provider."
|
||||
}
|
||||
break
|
||||
case "fireworks":
|
||||
if (!apiConfiguration.fireworksApiKey || !apiConfiguration.fireworksModelId) {
|
||||
return "You must provide a valid API key or choose a different provider."
|
||||
}
|
||||
break
|
||||
case "together":
|
||||
if (!apiConfiguration.togetherApiKey || !apiConfiguration.togetherModelId) {
|
||||
return "You must provide a valid API key or choose a different provider."
|
||||
|
||||
Reference in New Issue
Block a user