chore: Prettier fix formatting

This commit is contained in:
Saoud Rizwan 2025-01-06 12:41:33 -08:00
parent 9de7253998
commit 25cd7cc8e2
106 changed files with 1624 additions and 5278 deletions

View file

@ -1,7 +1,7 @@
{
"tabWidth": 4,
"useTabs": true,
"printWidth": 120,
"printWidth": 130,
"semi": false,
"bracketSameLine": true
}

View file

@ -1,9 +1,5 @@
{
// See http://go.microsoft.com/fwlink/?LinkId=827846
// for the documentation about the extensions.json format
"recommendations": [
"dbaeumer.vscode-eslint",
"connor4312.esbuild-problem-matchers",
"ms-vscode.extension-test-runner"
]
"recommendations": ["dbaeumer.vscode-eslint", "connor4312.esbuild-problem-matchers", "ms-vscode.extension-test-runner"]
}

6
.vscode/tasks.json vendored
View file

@ -5,11 +5,7 @@
"tasks": [
{
"label": "watch",
"dependsOn": [
"npm: build:webview",
"npm: watch:tsc",
"npm: watch:esbuild"
],
"dependsOn": ["npm: build:webview", "npm: watch:tsc", "npm: watch:esbuild"],
"presentation": {
"reveal": "never"
},

View file

@ -18,9 +18,7 @@ const esbuildProblemMatcherPlugin = {
build.onEnd((result) => {
result.errors.forEach(({ text, location }) => {
console.error(`✘ [ERROR] ${text}`)
console.error(
` ${location.file}:${location.line}:${location.column}:`,
)
console.error(` ${location.file}:${location.line}:${location.column}:`)
})
console.log("[watch] build finished")
})
@ -32,26 +30,14 @@ const copyWasmFiles = {
setup(build) {
build.onEnd(() => {
// tree sitter
const sourceDir = path.join(
__dirname,
"node_modules",
"web-tree-sitter",
)
const sourceDir = path.join(__dirname, "node_modules", "web-tree-sitter")
const targetDir = path.join(__dirname, "dist")
// Copy tree-sitter.wasm
fs.copyFileSync(
path.join(sourceDir, "tree-sitter.wasm"),
path.join(targetDir, "tree-sitter.wasm"),
)
fs.copyFileSync(path.join(sourceDir, "tree-sitter.wasm"), path.join(targetDir, "tree-sitter.wasm"))
// Copy language-specific WASM files
const languageWasmDir = path.join(
__dirname,
"node_modules",
"tree-sitter-wasms",
"out",
)
const languageWasmDir = path.join(__dirname, "node_modules", "tree-sitter-wasms", "out")
const languages = [
"typescript",
"tsx",
@ -70,10 +56,7 @@ const copyWasmFiles = {
languages.forEach((lang) => {
const filename = `tree-sitter-${lang}.wasm`
fs.copyFileSync(
path.join(languageWasmDir, filename),
path.join(targetDir, filename),
)
fs.copyFileSync(path.join(languageWasmDir, filename), path.join(targetDir, filename))
})
})
},

View file

@ -13,10 +13,7 @@ import { ApiStream } from "./transform/stream"
import { DeepSeekHandler } from "./providers/deepseek"
export interface ApiHandler {
createMessage(
systemPrompt: string,
messages: Anthropic.Messages.MessageParam[],
): ApiStream
createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream
getModel(): { id: string; info: ModelInfo }
}

View file

@ -1,12 +1,6 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { Stream as AnthropicStream } from "@anthropic-ai/sdk/streaming"
import {
anthropicDefaultModelId,
AnthropicModelId,
anthropicModels,
ApiHandlerOptions,
ModelInfo,
} from "../../shared/api"
import { anthropicDefaultModelId, AnthropicModelId, anthropicModels, ApiHandlerOptions, ModelInfo } from "../../shared/api"
import { ApiHandler } from "../index"
import { ApiStream } from "../transform/stream"
@ -22,10 +16,7 @@ export class AnthropicHandler implements ApiHandler {
})
}
async *createMessage(
systemPrompt: string,
messages: Anthropic.Messages.MessageParam[],
): ApiStream {
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
let stream: AnthropicStream<Anthropic.Beta.PromptCaching.Messages.RawPromptCachingBetaMessageStreamEvent>
const modelId = this.getModel().id
switch (modelId) {
@ -38,14 +29,11 @@ export class AnthropicHandler implements ApiHandler {
The latest message will be the new user message, one before will be the assistant message from a previous request, and the user message before that will be a previously cached user message. So we need to mark the latest user message as ephemeral to cache it for the next request, and mark the second to last user message as ephemeral to let the server know the last message to retrieve from the cache for the current request..
*/
const userMsgIndices = messages.reduce(
(acc, msg, index) =>
msg.role === "user" ? [...acc, index] : acc,
(acc, msg, index) => (msg.role === "user" ? [...acc, index] : acc),
[] as number[],
)
const lastUserMsgIndex =
userMsgIndices[userMsgIndices.length - 1] ?? -1
const secondLastMsgUserIndex =
userMsgIndices[userMsgIndices.length - 2] ?? -1
const lastUserMsgIndex = userMsgIndices[userMsgIndices.length - 1] ?? -1
const secondLastMsgUserIndex = userMsgIndices[userMsgIndices.length - 2] ?? -1
stream = await this.client.beta.promptCaching.messages.create(
{
model: modelId,
@ -59,10 +47,7 @@ export class AnthropicHandler implements ApiHandler {
},
], // setting cache breakpoint for system prompt so new tasks can reuse it
messages: messages.map((message, index) => {
if (
index === lastUserMsgIndex ||
index === secondLastMsgUserIndex
) {
if (index === lastUserMsgIndex || index === secondLastMsgUserIndex) {
return {
...message,
content:
@ -76,19 +61,15 @@ export class AnthropicHandler implements ApiHandler {
},
},
]
: message.content.map(
(content, contentIndex) =>
contentIndex ===
message.content.length -
1
? {
...content,
cache_control:
{
type: "ephemeral",
},
}
: content,
: message.content.map((content, contentIndex) =>
contentIndex === message.content.length - 1
? {
...content,
cache_control: {
type: "ephemeral",
},
}
: content,
),
}
}
@ -110,8 +91,7 @@ export class AnthropicHandler implements ApiHandler {
case "claude-3-haiku-20240307":
return {
headers: {
"anthropic-beta":
"prompt-caching-2024-07-31",
"anthropic-beta": "prompt-caching-2024-07-31",
},
}
default:
@ -145,10 +125,8 @@ export class AnthropicHandler implements ApiHandler {
type: "usage",
inputTokens: usage.input_tokens || 0,
outputTokens: usage.output_tokens || 0,
cacheWriteTokens:
usage.cache_creation_input_tokens || undefined,
cacheReadTokens:
usage.cache_read_input_tokens || undefined,
cacheWriteTokens: usage.cache_creation_input_tokens || undefined,
cacheReadTokens: usage.cache_read_input_tokens || undefined,
}
break
case "message_delta":

View file

@ -1,13 +1,7 @@
import AnthropicBedrock from "@anthropic-ai/bedrock-sdk"
import { Anthropic } from "@anthropic-ai/sdk"
import { ApiHandler } from "../"
import {
ApiHandlerOptions,
bedrockDefaultModelId,
BedrockModelId,
bedrockModels,
ModelInfo,
} from "../../shared/api"
import { ApiHandlerOptions, bedrockDefaultModelId, BedrockModelId, bedrockModels, ModelInfo } from "../../shared/api"
import { ApiStream } from "../transform/stream"
// https://docs.anthropic.com/en/api/claude-on-amazon-bedrock
@ -20,15 +14,9 @@ export class AwsBedrockHandler implements ApiHandler {
this.client = new AnthropicBedrock({
// Authenticate by either providing the keys below or use the default AWS credential providers, such as
// using ~/.aws/credentials or the "AWS_SECRET_ACCESS_KEY" and "AWS_ACCESS_KEY_ID" environment variables.
...(this.options.awsAccessKey
? { awsAccessKey: this.options.awsAccessKey }
: {}),
...(this.options.awsSecretKey
? { awsSecretKey: this.options.awsSecretKey }
: {}),
...(this.options.awsSessionToken
? { awsSessionToken: this.options.awsSessionToken }
: {}),
...(this.options.awsAccessKey ? { awsAccessKey: this.options.awsAccessKey } : {}),
...(this.options.awsSecretKey ? { awsSecretKey: this.options.awsSecretKey } : {}),
...(this.options.awsSessionToken ? { awsSessionToken: this.options.awsSessionToken } : {}),
// awsRegion changes the aws region to which the request is made. By default, we read AWS_REGION,
// and if that's not present, we default to us-east-1. Note that we do not read ~/.aws/config for the region.
@ -36,10 +24,7 @@ export class AwsBedrockHandler implements ApiHandler {
})
}
async *createMessage(
systemPrompt: string,
messages: Anthropic.Messages.MessageParam[],
): ApiStream {
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
// cross region inference requires prefixing the model id with the region
let modelId: string
if (this.options.awsUseCrossRegionInference) {

View file

@ -1,13 +1,7 @@
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import { ApiHandler } from "../"
import {
ApiHandlerOptions,
DeepSeekModelId,
ModelInfo,
deepSeekDefaultModelId,
deepSeekModels,
} from "../../shared/api"
import { ApiHandlerOptions, DeepSeekModelId, ModelInfo, deepSeekDefaultModelId, deepSeekModels } from "../../shared/api"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
@ -23,18 +17,12 @@ export class DeepSeekHandler implements ApiHandler {
})
}
async *createMessage(
systemPrompt: string,
messages: Anthropic.Messages.MessageParam[],
): ApiStream {
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const stream = await this.client.chat.completions.create({
model: this.getModel().id,
max_completion_tokens: this.getModel().info.maxTokens,
temperature: 0,
messages: [
{ role: "system", content: systemPrompt },
...convertToOpenAiMessages(messages),
],
messages: [{ role: "system", content: systemPrompt }, ...convertToOpenAiMessages(messages)],
stream: true,
stream_options: { include_usage: true },
})

View file

@ -1,13 +1,7 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { GoogleGenerativeAI } from "@google/generative-ai"
import { ApiHandler } from "../"
import {
ApiHandlerOptions,
geminiDefaultModelId,
GeminiModelId,
geminiModels,
ModelInfo,
} from "../../shared/api"
import { ApiHandlerOptions, geminiDefaultModelId, GeminiModelId, geminiModels, ModelInfo } from "../../shared/api"
import { convertAnthropicMessageToGemini } from "../transform/gemini-format"
import { ApiStream } from "../transform/stream"
@ -23,10 +17,7 @@ export class GeminiHandler implements ApiHandler {
this.client = new GoogleGenerativeAI(options.geminiApiKey)
}
async *createMessage(
systemPrompt: string,
messages: Anthropic.Messages.MessageParam[],
): ApiStream {
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const model = this.client.getGenerativeModel({
model: this.getModel().id,
systemInstruction: systemPrompt,

View file

@ -1,11 +1,7 @@
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import { ApiHandler } from "../"
import {
ApiHandlerOptions,
ModelInfo,
openAiModelInfoSaneDefaults,
} from "../../shared/api"
import { ApiHandlerOptions, ModelInfo, openAiModelInfoSaneDefaults } from "../../shared/api"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
@ -16,17 +12,12 @@ export class LmStudioHandler implements ApiHandler {
constructor(options: ApiHandlerOptions) {
this.options = options
this.client = new OpenAI({
baseURL:
(this.options.lmStudioBaseUrl || "http://localhost:1234") +
"/v1",
baseURL: (this.options.lmStudioBaseUrl || "http://localhost:1234") + "/v1",
apiKey: "noop",
})
}
async *createMessage(
systemPrompt: string,
messages: Anthropic.Messages.MessageParam[],
): ApiStream {
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{ role: "system", content: systemPrompt },
...convertToOpenAiMessages(messages),

View file

@ -1,11 +1,7 @@
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import { ApiHandler } from "../"
import {
ApiHandlerOptions,
ModelInfo,
openAiModelInfoSaneDefaults,
} from "../../shared/api"
import { ApiHandlerOptions, ModelInfo, openAiModelInfoSaneDefaults } from "../../shared/api"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
@ -16,17 +12,12 @@ export class OllamaHandler implements ApiHandler {
constructor(options: ApiHandlerOptions) {
this.options = options
this.client = new OpenAI({
baseURL:
(this.options.ollamaBaseUrl || "http://localhost:11434") +
"/v1",
baseURL: (this.options.ollamaBaseUrl || "http://localhost:11434") + "/v1",
apiKey: "ollama",
})
}
async *createMessage(
systemPrompt: string,
messages: Anthropic.Messages.MessageParam[],
): ApiStream {
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{ role: "system", content: systemPrompt },
...convertToOpenAiMessages(messages),

View file

@ -22,20 +22,14 @@ export class OpenAiNativeHandler implements ApiHandler {
})
}
async *createMessage(
systemPrompt: string,
messages: Anthropic.Messages.MessageParam[],
): ApiStream {
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
switch (this.getModel().id) {
case "o1-preview":
case "o1-mini": {
// o1 doesnt support streaming, non-1 temp, or system prompt
const response = await this.client.chat.completions.create({
model: this.getModel().id,
messages: [
{ role: "user", content: systemPrompt },
...convertToOpenAiMessages(messages),
],
messages: [{ role: "user", content: systemPrompt }, ...convertToOpenAiMessages(messages)],
})
yield {
type: "text",
@ -53,10 +47,7 @@ export class OpenAiNativeHandler implements ApiHandler {
model: this.getModel().id,
// max_completion_tokens: this.getModel().info.maxTokens,
temperature: 0,
messages: [
{ role: "system", content: systemPrompt },
...convertToOpenAiMessages(messages),
],
messages: [{ role: "system", content: systemPrompt }, ...convertToOpenAiMessages(messages)],
stream: true,
stream_options: { include_usage: true },
})

View file

@ -1,11 +1,6 @@
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI, { AzureOpenAI } from "openai"
import {
ApiHandlerOptions,
azureOpenAiDefaultApiVersion,
ModelInfo,
openAiModelInfoSaneDefaults,
} from "../../shared/api"
import { ApiHandlerOptions, azureOpenAiDefaultApiVersion, ModelInfo, openAiModelInfoSaneDefaults } from "../../shared/api"
import { ApiHandler } from "../index"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
@ -21,9 +16,7 @@ export class OpenAiHandler implements ApiHandler {
this.client = new AzureOpenAI({
baseURL: this.options.openAiBaseUrl,
apiKey: this.options.openAiApiKey,
apiVersion:
this.options.azureApiVersion ||
azureOpenAiDefaultApiVersion,
apiVersion: this.options.azureApiVersion || azureOpenAiDefaultApiVersion,
})
} else {
this.client = new OpenAI({
@ -33,10 +26,7 @@ export class OpenAiHandler implements ApiHandler {
}
}
async *createMessage(
systemPrompt: string,
messages: Anthropic.Messages.MessageParam[],
): ApiStream {
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{ role: "system", content: systemPrompt },
...convertToOpenAiMessages(messages),

View file

@ -2,12 +2,7 @@ import { Anthropic } from "@anthropic-ai/sdk"
import axios from "axios"
import OpenAI from "openai"
import { ApiHandler } from "../"
import {
ApiHandlerOptions,
ModelInfo,
openRouterDefaultModelId,
openRouterDefaultModelInfo,
} from "../../shared/api"
import { ApiHandlerOptions, ModelInfo, openRouterDefaultModelId, openRouterDefaultModelInfo } from "../../shared/api"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
import delay from "delay"
@ -28,10 +23,7 @@ export class OpenRouterHandler implements ApiHandler {
})
}
async *createMessage(
systemPrompt: string,
messages: Anthropic.Messages.MessageParam[],
): ApiStream {
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
// Convert Anthropic messages to OpenAI format
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{ role: "system", content: systemPrompt },
@ -66,18 +58,14 @@ export class OpenRouterHandler implements ApiHandler {
}
// Add cache_control to the last two user messages
// (note: this works because we only ever add one user message at a time, but if we added multiple we'd need to mark the user message before the last assistant message)
const lastTwoUserMessages = openAiMessages
.filter((msg) => msg.role === "user")
.slice(-2)
const lastTwoUserMessages = openAiMessages.filter((msg) => msg.role === "user").slice(-2)
lastTwoUserMessages.forEach((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()
let lastTextPart = msg.content.filter((part) => part.type === "text").pop()
if (!lastTextPart) {
lastTextPart = { type: "text", text: "..." }
@ -109,8 +97,7 @@ export class OpenRouterHandler implements ApiHandler {
}
// Removes messages in the middle when close to context window limit. Should not be applied to models that support prompt caching since it would continuously break the cache.
let shouldApplyMiddleOutTransform =
!this.getModel().info.supportsPromptCache
let shouldApplyMiddleOutTransform = !this.getModel().info.supportsPromptCache
// except for deepseek (which we set supportsPromptCache to true for), where because the context window is so small our truncation algo might miss and we should use openrouter's middle-out transform as a fallback to ensure we don't exceed the context window (FIXME: once we have a more robust token estimator we should not rely on this)
if (this.getModel().id === "deepseek/deepseek-chat") {
shouldApplyMiddleOutTransform = true
@ -123,9 +110,7 @@ export class OpenRouterHandler implements ApiHandler {
temperature: 0,
messages: openAiMessages,
stream: true,
transforms: shouldApplyMiddleOutTransform
? ["middle-out"]
: undefined,
transforms: shouldApplyMiddleOutTransform ? ["middle-out"] : undefined,
})
let genId: string | undefined
@ -134,12 +119,8 @@ export class OpenRouterHandler implements ApiHandler {
// openrouter returns an error object instead of the openai sdk throwing an error
if ("error" in chunk) {
const error = chunk.error as { message?: string; code?: number }
console.error(
`OpenRouter API Error: ${error?.code} - ${error?.message}`,
)
throw new Error(
`OpenRouter API Error ${error?.code}: ${error?.message}`,
)
console.error(`OpenRouter API Error: ${error?.code} - ${error?.message}`)
throw new Error(`OpenRouter API Error ${error?.code}: ${error?.message}`)
}
if (!genId && chunk.id) {
@ -165,15 +146,12 @@ export class OpenRouterHandler implements ApiHandler {
await delay(500) // FIXME: necessary delay to ensure generation endpoint is ready
try {
const response = await axios.get(
`https://openrouter.ai/api/v1/generation?id=${genId}`,
{
headers: {
Authorization: `Bearer ${this.options.openRouterApiKey}`,
},
timeout: 5_000, // this request hangs sometimes
const response = await axios.get(`https://openrouter.ai/api/v1/generation?id=${genId}`, {
headers: {
Authorization: `Bearer ${this.options.openRouterApiKey}`,
},
)
timeout: 5_000, // this request hangs sometimes
})
const generation = response.data?.data
console.log("OpenRouter generation details:", response.data)
@ -188,10 +166,7 @@ export class OpenRouterHandler implements ApiHandler {
}
} catch (error) {
// ignore if fails
console.error(
"Error fetching OpenRouter generation details:",
error,
)
console.error("Error fetching OpenRouter generation details:", error)
}
}

View file

@ -1,13 +1,7 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { AnthropicVertex } from "@anthropic-ai/vertex-sdk"
import { ApiHandler } from "../"
import {
ApiHandlerOptions,
ModelInfo,
vertexDefaultModelId,
VertexModelId,
vertexModels,
} from "../../shared/api"
import { ApiHandlerOptions, ModelInfo, vertexDefaultModelId, VertexModelId, vertexModels } from "../../shared/api"
import { ApiStream } from "../transform/stream"
// https://docs.anthropic.com/en/api/claude-on-vertex-ai
@ -24,10 +18,7 @@ export class VertexHandler implements ApiHandler {
})
}
async *createMessage(
systemPrompt: string,
messages: Anthropic.Messages.MessageParam[],
): ApiStream {
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const stream = await this.client.messages.create({
model: this.getModel().id,
max_tokens: this.getModel().info.maxTokens || 8192,

View file

@ -62,20 +62,10 @@ export function convertAnthropicContentToGemini(
} as FunctionResponsePart
} else {
// The only case when tool_result could be array is when the tool failed and we're providing ie user feedback potentially with images
const textParts = block.content.filter(
(part) => part.type === "text",
)
const imageParts = block.content.filter(
(part) => part.type === "image",
)
const text =
textParts.length > 0
? textParts.map((part) => part.text).join("\n\n")
: ""
const imageText =
imageParts.length > 0
? "\n\n(See next part for image)"
: ""
const textParts = block.content.filter((part) => part.type === "text")
const imageParts = block.content.filter((part) => part.type === "image")
const text = textParts.length > 0 ? textParts.map((part) => part.text).join("\n\n") : ""
const imageText = imageParts.length > 0 ? "\n\n(See next part for image)" : ""
return [
{
functionResponse: {
@ -98,40 +88,32 @@ export function convertAnthropicContentToGemini(
]
}
default:
throw new Error(
`Unsupported content block type: ${(block as any).type}`,
)
throw new Error(`Unsupported content block type: ${(block as any).type}`)
}
})
}
export function convertAnthropicMessageToGemini(
message: Anthropic.Messages.MessageParam,
): Content {
export function convertAnthropicMessageToGemini(message: Anthropic.Messages.MessageParam): Content {
return {
role: message.role === "assistant" ? "model" : "user",
parts: convertAnthropicContentToGemini(message.content),
}
}
export function convertAnthropicToolToGemini(
tool: Anthropic.Messages.Tool,
): FunctionDeclaration {
export function convertAnthropicToolToGemini(tool: Anthropic.Messages.Tool): FunctionDeclaration {
return {
name: tool.name,
description: tool.description || "",
parameters: {
type: SchemaType.OBJECT,
properties: Object.fromEntries(
Object.entries(tool.input_schema.properties || {}).map(
([key, value]) => [
key,
{
type: (value as any).type.toUpperCase(),
description: (value as any).description || "",
},
],
),
Object.entries(tool.input_schema.properties || {}).map(([key, value]) => [
key,
{
type: (value as any).type.toUpperCase(),
description: (value as any).description || "",
},
]),
),
required: (tool.input_schema.required as string[]) || [],
},
@ -142,17 +124,10 @@ export function convertAnthropicToolToGemini(
It looks like gemini likes to double escape certain characters when writing file contents: https://discuss.ai.google.dev/t/function-call-string-property-is-double-escaped/37867
*/
export function unescapeGeminiContent(content: string) {
return content
.replace(/\\n/g, "\n")
.replace(/\\'/g, "'")
.replace(/\\"/g, '"')
.replace(/\\r/g, "\r")
.replace(/\\t/g, "\t")
return content.replace(/\\n/g, "\n").replace(/\\'/g, "'").replace(/\\"/g, '"').replace(/\\r/g, "\r").replace(/\\t/g, "\t")
}
export function convertGeminiResponseToAnthropic(
response: EnhancedGenerateContentResponse,
): Anthropic.Messages.Message {
export function convertGeminiResponseToAnthropic(response: EnhancedGenerateContentResponse): Anthropic.Messages.Message {
const content: Anthropic.Messages.ContentBlock[] = []
// Add the main text response
@ -165,10 +140,7 @@ export function convertGeminiResponseToAnthropic(
const functionCalls = response.functionCalls()
if (functionCalls) {
functionCalls.forEach((call, index) => {
if (
"content" in call.args &&
typeof call.args.content === "string"
) {
if ("content" in call.args && typeof call.args.content === "string") {
call.args.content = unescapeGeminiContent(call.args.content)
}
content.push({

View file

@ -272,9 +272,7 @@ function parseToolCalls(toolCallsText: string): ToolCall[] {
let remainingText = toolCallsText
while (remainingText.length > 0) {
const toolMatch = toolNames.find((tool) =>
new RegExp(`<${tool}`, "i").test(remainingText),
)
const toolMatch = toolNames.find((tool) => new RegExp(`<${tool}`, "i").test(remainingText))
if (!toolMatch) {
break // No more tool calls found
@ -289,10 +287,7 @@ function parseToolCalls(toolCallsText: string): ToolCall[] {
break // Malformed XML, no closing tag found
}
const toolCallContent = remainingText.slice(
startIndex,
endIndex + endTag.length,
)
const toolCallContent = remainingText.slice(startIndex, endIndex + endTag.length)
remainingText = remainingText.slice(endIndex + endTag.length).trim()
const toolCall = parseToolCall(toolMatch, toolCallContent)
@ -308,9 +303,7 @@ function parseToolCall(toolName: string, content: string): ToolCall | null {
const tool_input: Record<string, string> = {}
// Remove the outer tool tags
const innerContent = content
.replace(new RegExp(`^<${toolName}>|</${toolName}>$`, "g"), "")
.trim()
const innerContent = content.replace(new RegExp(`^<${toolName}>|</${toolName}>$`, "g"), "").trim()
// Parse nested XML elements
const paramRegex = /<(\w+)>([\s\S]*?)<\/\1>/gs
@ -331,10 +324,7 @@ function parseToolCall(toolName: string, content: string): ToolCall | null {
return { tool: toolName, tool_input }
}
function validateToolInput(
toolName: string,
tool_input: Record<string, string>,
): boolean {
function validateToolInput(toolName: string, tool_input: Record<string, string>): boolean {
switch (toolName) {
case "execute_command":
return "command" in tool_input
@ -376,9 +366,7 @@ export function convertO1ResponseToAnthropicMessage(
completion: OpenAI.Chat.Completions.ChatCompletion,
): Anthropic.Messages.Message {
const openAiMessage = completion.choices[0].message
const { normalText, toolCalls } = parseAIResponse(
openAiMessage.content || "",
)
const { normalText, toolCalls } = parseAIResponse(openAiMessage.content || "")
const anthropicMessage: Anthropic.Messages.Message = {
id: completion.id,
@ -413,16 +401,14 @@ export function convertO1ResponseToAnthropicMessage(
if (toolCalls.length > 0) {
anthropicMessage.content.push(
...toolCalls.map(
(toolCall: ToolCall, index: number): Anthropic.ToolUseBlock => {
return {
type: "tool_use",
id: `call_${index}_${Date.now()}`, // Generate a unique ID for each tool call
name: toolCall.tool,
input: toolCall.tool_input,
}
},
),
...toolCalls.map((toolCall: ToolCall, index: number): Anthropic.ToolUseBlock => {
return {
type: "tool_use",
id: `call_${index}_${Date.now()}`, // Generate a unique ID for each tool call
name: toolCall.tool,
input: toolCall.tool_input,
}
}),
)
}

View file

@ -22,27 +22,20 @@ export function convertToOpenAiMessages(
{ role: "tool", tool_call_id: "", content: ""}
*/
if (anthropicMessage.role === "user") {
const { nonToolMessages, toolMessages } =
anthropicMessage.content.reduce<{
nonToolMessages: (
| Anthropic.TextBlockParam
| Anthropic.ImageBlockParam
)[]
toolMessages: Anthropic.ToolResultBlockParam[]
}>(
(acc, part) => {
if (part.type === "tool_result") {
acc.toolMessages.push(part)
} else if (
part.type === "text" ||
part.type === "image"
) {
acc.nonToolMessages.push(part)
} // user cannot send tool_use messages
return acc
},
{ nonToolMessages: [], toolMessages: [] },
)
const { nonToolMessages, toolMessages } = anthropicMessage.content.reduce<{
nonToolMessages: (Anthropic.TextBlockParam | Anthropic.ImageBlockParam)[]
toolMessages: Anthropic.ToolResultBlockParam[]
}>(
(acc, part) => {
if (part.type === "tool_result") {
acc.toolMessages.push(part)
} else if (part.type === "text" || part.type === "image") {
acc.nonToolMessages.push(part)
} // user cannot send tool_use messages
return acc
},
{ nonToolMessages: [], toolMessages: [] },
)
// Process tool result messages FIRST since they must follow the tool use messages
let toolResultImages: Anthropic.Messages.ImageBlockParam[] = []
@ -105,27 +98,20 @@ export function convertToOpenAiMessages(
})
}
} else if (anthropicMessage.role === "assistant") {
const { nonToolMessages, toolMessages } =
anthropicMessage.content.reduce<{
nonToolMessages: (
| Anthropic.TextBlockParam
| Anthropic.ImageBlockParam
)[]
toolMessages: Anthropic.ToolUseBlockParam[]
}>(
(acc, part) => {
if (part.type === "tool_use") {
acc.toolMessages.push(part)
} else if (
part.type === "text" ||
part.type === "image"
) {
acc.nonToolMessages.push(part)
} // assistant cannot send tool_result messages
return acc
},
{ nonToolMessages: [], toolMessages: [] },
)
const { nonToolMessages, toolMessages } = anthropicMessage.content.reduce<{
nonToolMessages: (Anthropic.TextBlockParam | Anthropic.ImageBlockParam)[]
toolMessages: Anthropic.ToolUseBlockParam[]
}>(
(acc, part) => {
if (part.type === "tool_use") {
acc.toolMessages.push(part)
} else if (part.type === "text" || part.type === "image") {
acc.nonToolMessages.push(part)
} // assistant cannot send tool_result messages
return acc
},
{ nonToolMessages: [], toolMessages: [] },
)
// Process non-tool messages
let content: string | undefined
@ -141,16 +127,15 @@ export function convertToOpenAiMessages(
}
// Process tool use messages
let tool_calls: OpenAI.Chat.ChatCompletionMessageToolCall[] =
toolMessages.map((toolMessage) => ({
id: toolMessage.id,
type: "function",
function: {
name: toolMessage.name,
// json string
arguments: JSON.stringify(toolMessage.input),
},
}))
let tool_calls: OpenAI.Chat.ChatCompletionMessageToolCall[] = toolMessages.map((toolMessage) => ({
id: toolMessage.id,
type: "function",
function: {
name: toolMessage.name,
// json string
arguments: JSON.stringify(toolMessage.input),
},
}))
openAiMessages.push({
role: "assistant",
@ -166,9 +151,7 @@ export function convertToOpenAiMessages(
}
// Convert OpenAI response to Anthropic format
export function convertToAnthropicMessage(
completion: OpenAI.Chat.Completions.ChatCompletion,
): Anthropic.Messages.Message {
export function convertToAnthropicMessage(completion: OpenAI.Chat.Completions.ChatCompletion): Anthropic.Messages.Message {
const openAiMessage = completion.choices[0].message
const anthropicMessage: Anthropic.Messages.Message = {
id: completion.id,
@ -203,24 +186,20 @@ export function convertToAnthropicMessage(
if (openAiMessage.tool_calls && openAiMessage.tool_calls.length > 0) {
anthropicMessage.content.push(
...openAiMessage.tool_calls.map(
(toolCall): Anthropic.ToolUseBlock => {
let parsedInput = {}
try {
parsedInput = JSON.parse(
toolCall.function.arguments || "{}",
)
} catch (error) {
console.error("Failed to parse tool arguments:", error)
}
return {
type: "tool_use",
id: toolCall.id,
name: toolCall.function.name,
input: parsedInput,
}
},
),
...openAiMessage.tool_calls.map((toolCall): Anthropic.ToolUseBlock => {
let parsedInput = {}
try {
parsedInput = JSON.parse(toolCall.function.arguments || "{}")
} catch (error) {
console.error("Failed to parse tool arguments:", error)
}
return {
type: "tool_use",
id: toolCall.id,
name: toolCall.function.name,
input: parsedInput,
}
}),
)
}
return anthropicMessage

File diff suppressed because it is too large Load diff

View file

@ -6,11 +6,7 @@
*
* Returns [matchIndexStart, matchIndexEnd] if found, or false if not found.
*/
function lineTrimmedFallbackMatch(
originalContent: string,
searchContent: string,
startIndex: number,
): [number, number] | false {
function lineTrimmedFallbackMatch(originalContent: string, searchContent: string, startIndex: number): [number, number] | false {
// Split both contents into lines
const originalLines = originalContent.split("\n")
const searchLines = searchContent.split("\n")
@ -29,11 +25,7 @@ function lineTrimmedFallbackMatch(
}
// For each possible starting position in original content
for (
let i = startLineNum;
i <= originalLines.length - searchLines.length;
i++
) {
for (let i = startLineNum; i <= originalLines.length - searchLines.length; i++) {
let matches = true
// Try to match all search lines from this position
@ -95,11 +87,7 @@ function lineTrimmedFallbackMatch(
* @param startIndex - The character index in originalContent where to start searching
* @returns A tuple of [startIndex, endIndex] if a match is found, false otherwise
*/
function blockAnchorFallbackMatch(
originalContent: string,
searchContent: string,
startIndex: number,
): [number, number] | false {
function blockAnchorFallbackMatch(originalContent: string, searchContent: string, startIndex: number): [number, number] | false {
const originalLines = originalContent.split("\n")
const searchLines = searchContent.split("\n")
@ -126,11 +114,7 @@ function blockAnchorFallbackMatch(
}
// Look for matching start and end anchors
for (
let i = startLineNum;
i <= originalLines.length - searchBlockSize;
i++
) {
for (let i = startLineNum; i <= originalLines.length - searchBlockSize; i++) {
// Check if first line matches
if (originalLines[i].trim() !== firstLineSearch) {
continue
@ -216,11 +200,7 @@ function blockAnchorFallbackMatch(
* - If the search block cannot be matched using any of the available matching strategies,
* an error is thrown.
*/
export async function constructNewFileContent(
diffContent: string,
originalContent: string,
isFinal: boolean,
): Promise<string> {
export async function constructNewFileContent(diffContent: string, originalContent: string, isFinal: boolean): Promise<string> {
let result = ""
let lastProcessedIndex = 0
@ -239,9 +219,7 @@ export async function constructNewFileContent(
const lastLine = lines[lines.length - 1]
if (
lines.length > 0 &&
(lastLine.startsWith("<") ||
lastLine.startsWith("=") ||
lastLine.startsWith(">")) &&
(lastLine.startsWith("<") || lastLine.startsWith("=") || lastLine.startsWith(">")) &&
lastLine !== "<<<<<<< SEARCH" &&
lastLine !== "=======" &&
lastLine !== ">>>>>>> REPLACE"
@ -290,29 +268,18 @@ export async function constructNewFileContent(
// }
// Exact search match scenario
const exactIndex = originalContent.indexOf(
currentSearchContent,
lastProcessedIndex,
)
const exactIndex = originalContent.indexOf(currentSearchContent, lastProcessedIndex)
if (exactIndex !== -1) {
searchMatchIndex = exactIndex
searchEndIndex = exactIndex + currentSearchContent.length
} else {
// Attempt fallback line-trimmed matching
const lineMatch = lineTrimmedFallbackMatch(
originalContent,
currentSearchContent,
lastProcessedIndex,
)
const lineMatch = lineTrimmedFallbackMatch(originalContent, currentSearchContent, lastProcessedIndex)
if (lineMatch) {
;[searchMatchIndex, searchEndIndex] = lineMatch
} else {
// Try block anchor fallback for larger blocks
const blockMatch = blockAnchorFallbackMatch(
originalContent,
currentSearchContent,
lastProcessedIndex,
)
const blockMatch = blockAnchorFallbackMatch(originalContent, currentSearchContent, lastProcessedIndex)
if (blockMatch) {
;[searchMatchIndex, searchEndIndex] = blockMatch
} else {
@ -325,10 +292,7 @@ export async function constructNewFileContent(
}
// Output everything up to the match location
result += originalContent.slice(
lastProcessedIndex,
searchMatchIndex,
)
result += originalContent.slice(lastProcessedIndex, searchMatchIndex)
continue
}

View file

@ -60,9 +60,7 @@ export interface ToolUse {
export interface ExecuteCommandToolUse extends ToolUse {
name: "execute_command"
// Pick<Record<ToolParamName, string>, "command"> makes "command" required, but Partial<> makes it optional
params: Partial<
Pick<Record<ToolParamName, string>, "command" | "requires_approval">
>
params: Partial<Pick<Record<ToolParamName, string>, "command" | "requires_approval">>
}
export interface ReadFileToolUse extends ToolUse {
@ -82,9 +80,7 @@ export interface ReplaceInFileToolUse extends ToolUse {
export interface SearchFilesToolUse extends ToolUse {
name: "search_files"
params: Partial<
Pick<Record<ToolParamName, string>, "path" | "regex" | "file_pattern">
>
params: Partial<Pick<Record<ToolParamName, string>, "path" | "regex" | "file_pattern">>
}
export interface ListFilesToolUse extends ToolUse {
@ -99,22 +95,12 @@ export interface ListCodeDefinitionNamesToolUse extends ToolUse {
export interface BrowserActionToolUse extends ToolUse {
name: "browser_action"
params: Partial<
Pick<
Record<ToolParamName, string>,
"action" | "url" | "coordinate" | "text"
>
>
params: Partial<Pick<Record<ToolParamName, string>, "action" | "url" | "coordinate" | "text">>
}
export interface UseMcpToolToolUse extends ToolUse {
name: "use_mcp_tool"
params: Partial<
Pick<
Record<ToolParamName, string>,
"server_name" | "tool_name" | "arguments"
>
>
params: Partial<Pick<Record<ToolParamName, string>, "server_name" | "tool_name" | "arguments">>
}
export interface AccessMcpResourceToolUse extends ToolUse {

View file

@ -1,12 +1,4 @@
import {
AssistantMessageContent,
TextContent,
ToolUse,
ToolParamName,
toolParamNames,
toolUseNames,
ToolUseName,
} from "."
import { AssistantMessageContent, TextContent, ToolUse, ToolParamName, toolParamNames, toolUseNames, ToolUseName } from "."
export function parseAssistantMessage(assistantMessage: string) {
let contentBlocks: AssistantMessageContent[] = []
@ -24,15 +16,11 @@ export function parseAssistantMessage(assistantMessage: string) {
// there should not be a param without a tool use
if (currentToolUse && currentParamName) {
const currentParamValue = accumulator.slice(
currentParamValueStartIndex,
)
const currentParamValue = accumulator.slice(currentParamValueStartIndex)
const paramClosingTag = `</${currentParamName}>`
if (currentParamValue.endsWith(paramClosingTag)) {
// end of param value
currentToolUse.params[currentParamName] = currentParamValue
.slice(0, -paramClosingTag.length)
.trim()
currentToolUse.params[currentParamName] = currentParamValue.slice(0, -paramClosingTag.length).trim()
currentParamName = undefined
continue
} else {
@ -53,16 +41,11 @@ export function parseAssistantMessage(assistantMessage: string) {
currentToolUse = undefined
continue
} else {
const possibleParamOpeningTags = toolParamNames.map(
(name) => `<${name}>`,
)
const possibleParamOpeningTags = toolParamNames.map((name) => `<${name}>`)
for (const paramOpeningTag of possibleParamOpeningTags) {
if (accumulator.endsWith(paramOpeningTag)) {
// start of a new parameter
currentParamName = paramOpeningTag.slice(
1,
-1,
) as ToolParamName
currentParamName = paramOpeningTag.slice(1, -1) as ToolParamName
currentParamValueStartIndex = accumulator.length
break
}
@ -72,28 +55,14 @@ export function parseAssistantMessage(assistantMessage: string) {
// 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.
const contentParamName: ToolParamName = "content"
if (
currentToolUse.name === "write_to_file" &&
accumulator.endsWith(`</${contentParamName}>`)
) {
const toolContent = accumulator.slice(
currentToolUseStartIndex,
)
if (currentToolUse.name === "write_to_file" && accumulator.endsWith(`</${contentParamName}>`)) {
const toolContent = accumulator.slice(currentToolUseStartIndex)
const contentStartTag = `<${contentParamName}>`
const contentEndTag = `</${contentParamName}>`
const contentStartIndex =
toolContent.indexOf(contentStartTag) +
contentStartTag.length
const contentEndIndex =
toolContent.lastIndexOf(contentEndTag)
if (
contentStartIndex !== -1 &&
contentEndIndex !== -1 &&
contentEndIndex > contentStartIndex
) {
currentToolUse.params[contentParamName] = toolContent
.slice(contentStartIndex, contentEndIndex)
.trim()
const contentStartIndex = toolContent.indexOf(contentStartTag) + contentStartTag.length
const contentEndIndex = toolContent.lastIndexOf(contentEndTag)
if (contentStartIndex !== -1 && contentEndIndex !== -1 && contentEndIndex > contentStartIndex) {
currentToolUse.params[contentParamName] = toolContent.slice(contentStartIndex, contentEndIndex).trim()
}
}
@ -105,9 +74,7 @@ export function parseAssistantMessage(assistantMessage: string) {
// no currentToolUse
let didStartToolUse = false
const possibleToolUseOpeningTags = toolUseNames.map(
(name) => `<${name}>`,
)
const possibleToolUseOpeningTags = toolUseNames.map((name) => `<${name}>`)
for (const toolUseOpeningTag of possibleToolUseOpeningTags) {
if (accumulator.endsWith(toolUseOpeningTag)) {
// start of a new tool use
@ -151,9 +118,7 @@ export function parseAssistantMessage(assistantMessage: string) {
// stream did not complete tool call, add it as partial
if (currentParamName) {
// tool call has a parameter that was not completed
currentToolUse.params[currentParamName] = accumulator
.slice(currentParamValueStartIndex)
.trim()
currentToolUse.params[currentParamName] = accumulator.slice(currentParamValueStartIndex).trim()
}
contentBlocks.push(currentToolUse)
}

View file

@ -15,18 +15,13 @@ export function openMention(mention?: string): void {
if (mention.startsWith("/")) {
const relPath = mention.slice(1)
const cwd = vscode.workspace.workspaceFolders
?.map((folder) => folder.uri.fsPath)
.at(0)
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0)
if (!cwd) {
return
}
const absPath = path.resolve(cwd, relPath)
if (mention.endsWith("/")) {
vscode.commands.executeCommand(
"revealInExplorer",
vscode.Uri.file(absPath),
)
vscode.commands.executeCommand("revealInExplorer", vscode.Uri.file(absPath))
// vscode.commands.executeCommand("vscode.openFolder", , { forceNewWindow: false }) opens in new window
} else {
openFile(absPath)
@ -38,11 +33,7 @@ export function openMention(mention?: string): void {
}
}
export async function parseMentions(
text: string,
cwd: string,
urlContentFetcher: UrlContentFetcher,
): Promise<string> {
export async function parseMentions(text: string, cwd: string, urlContentFetcher: UrlContentFetcher): Promise<string> {
const mentions: Set<string> = new Set()
let parsedText = text.replace(mentionRegexGlobal, (match, mention) => {
mentions.add(mention)
@ -59,18 +50,14 @@ export async function parseMentions(
return match
})
const urlMention = Array.from(mentions).find((mention) =>
mention.startsWith("http"),
)
const urlMention = Array.from(mentions).find((mention) => mention.startsWith("http"))
let launchBrowserError: Error | undefined
if (urlMention) {
try {
await urlContentFetcher.launchBrowser()
} catch (error) {
launchBrowserError = error
vscode.window.showErrorMessage(
`Error fetching content for ${urlMention}: ${error.message}`,
)
vscode.window.showErrorMessage(`Error fetching content for ${urlMention}: ${error.message}`)
}
}
@ -81,13 +68,10 @@ export async function parseMentions(
result = `Error fetching content: ${launchBrowserError.message}`
} else {
try {
const markdown =
await urlContentFetcher.urlToMarkdown(mention)
const markdown = await urlContentFetcher.urlToMarkdown(mention)
result = markdown
} catch (error) {
vscode.window.showErrorMessage(
`Error fetching content for ${mention}: ${error.message}`,
)
vscode.window.showErrorMessage(`Error fetching content for ${mention}: ${error.message}`)
result = `Error fetching content: ${error.message}`
}
}
@ -129,10 +113,7 @@ export async function parseMentions(
return parsedText
}
async function getFileOrFolderContent(
mentionPath: string,
cwd: string,
): Promise<string> {
async function getFileOrFolderContent(mentionPath: string, cwd: string): Promise<string> {
const absPath = path.resolve(cwd, mentionPath)
try {
@ -160,14 +141,11 @@ async function getFileOrFolderContent(
fileContentPromises.push(
(async () => {
try {
const isBinary = await isBinaryFile(
absoluteFilePath,
).catch(() => false)
const isBinary = await isBinaryFile(absoluteFilePath).catch(() => false)
if (isBinary) {
return undefined
}
const content =
await extractTextFromFile(absoluteFilePath)
const content = await extractTextFromFile(absoluteFilePath)
return `<file_content path="${filePath.toPosix()}">\n${content}\n</file_content>`
} catch (error) {
return undefined
@ -181,17 +159,13 @@ async function getFileOrFolderContent(
folderContent += `${linePrefix}${entry.name}\n`
}
})
const fileContents = (
await Promise.all(fileContentPromises)
).filter((content) => content)
const fileContents = (await Promise.all(fileContentPromises)).filter((content) => content)
return `${folderContent}\n${fileContents.join("\n\n")}`.trim()
} else {
return `(Failed to read contents of ${mentionPath})`
}
} catch (error) {
throw new Error(
`Failed to access path "${mentionPath}": ${error.message}`,
)
throw new Error(`Failed to access path "${mentionPath}": ${error.message}`)
}
}

View file

@ -8,8 +8,7 @@ export const formatResponse = {
toolDeniedWithFeedback: (feedback?: string) =>
`The user denied this operation and provided the following feedback:\n<feedback>\n${feedback}\n</feedback>`,
toolError: (error?: string) =>
`The tool execution failed with the following error:\n<error>\n${error}\n</error>`,
toolError: (error?: string) => `The tool execution failed with the following error:\n<error>\n${error}\n</error>`,
noToolsUsed: () =>
`[ERROR] You did not use a tool in your previous response! Please retry with a tool use.
@ -32,14 +31,10 @@ Otherwise, if you have not completed the task and do not need additional informa
invalidMcpToolArgumentError: (serverName: string, toolName: string) =>
`Invalid JSON argument used with ${serverName} for ${toolName}. Please retry with a properly formatted JSON argument.`,
toolResult: (
text: string,
images?: string[],
): string | Array<Anthropic.TextBlockParam | Anthropic.ImageBlockParam> => {
toolResult: (text: string, images?: string[]): string | Array<Anthropic.TextBlockParam | Anthropic.ImageBlockParam> => {
if (images && images.length > 0) {
const textBlock: Anthropic.TextBlockParam = { type: "text", text }
const imageBlocks: Anthropic.ImageBlockParam[] =
formatImagesIntoBlocks(images)
const imageBlocks: Anthropic.ImageBlockParam[] = formatImagesIntoBlocks(images)
// Placing images after text leads to better results
return [textBlock, ...imageBlocks]
} else {
@ -51,11 +46,7 @@ Otherwise, if you have not completed the task and do not need additional informa
return formatImagesIntoBlocks(images)
},
formatFilesList: (
absolutePath: string,
files: string[],
didHitLimit: boolean,
): string => {
formatFilesList: (absolutePath: string, files: string[], didHitLimit: boolean): string => {
const sorted = files
.map((file) => {
// convert absolute path to relative path
@ -66,11 +57,7 @@ Otherwise, if you have not completed the task and do not need additional informa
.sort((a, b) => {
const aParts = a.split("/") // only works if we use toPosix first
const bParts = b.split("/")
for (
let i = 0;
i < Math.min(aParts.length, bParts.length);
i++
) {
for (let i = 0; i < Math.min(aParts.length, bParts.length); i++) {
if (aParts[i] !== bParts[i]) {
// If one is a directory and the other isn't at this level, sort the directory first
if (i + 1 === aParts.length && i + 1 < bParts.length) {
@ -94,27 +81,16 @@ Otherwise, if you have not completed the task and do not need additional informa
return `${sorted.join(
"\n",
)}\n\n(File list truncated. Use list_files on specific subdirectories if you need to explore further.)`
} else if (
sorted.length === 0 ||
(sorted.length === 1 && sorted[0] === "")
) {
} else if (sorted.length === 0 || (sorted.length === 1 && sorted[0] === "")) {
return "No files found."
} else {
return sorted.join("\n")
}
},
createPrettyPatch: (
filename = "file",
oldStr?: string,
newStr?: string,
) => {
createPrettyPatch: (filename = "file", oldStr?: string, newStr?: string) => {
// strings cannot be undefined or diff throws exception
const patch = diff.createPatch(
filename.toPosix(),
oldStr || "",
newStr || "",
)
const patch = diff.createPatch(filename.toPosix(), oldStr || "", newStr || "")
const lines = patch.split("\n")
const prettyPatchLines = lines.slice(4)
return prettyPatchLines.join("\n")
@ -122,9 +98,7 @@ Otherwise, if you have not completed the task and do not need additional informa
}
// to avoid circular dependency
const formatImagesIntoBlocks = (
images?: string[],
): Anthropic.ImageBlockParam[] => {
const formatImagesIntoBlocks = (images?: string[]): Anthropic.ImageBlockParam[] => {
return images
? images.map((dataUrl) => {
// data:image/png;base64,base64string

View file

@ -362,17 +362,11 @@ ${
.join("\n\n")
const templates = server.resourceTemplates
?.map(
(template) =>
`- ${template.uriTemplate} (${template.name}): ${template.description}`,
)
?.map((template) => `- ${template.uriTemplate} (${template.name}): ${template.description}`)
.join("\n")
const resources = server.resources
?.map(
(resource) =>
`- ${resource.uri} (${resource.name}): ${resource.description}`,
)
?.map((resource) => `- ${resource.uri} (${resource.name}): ${resource.description}`)
.join("\n")
const config = JSON.parse(server.config)
@ -380,12 +374,8 @@ ${
return (
`## ${server.name} (\`${config.command}${config.args && Array.isArray(config.args) ? ` ${config.args.join(" ")}` : ""}\`)` +
(tools ? `\n\n### Available Tools\n${tools}` : "") +
(templates
? `\n\n### Resource Templates\n${templates}`
: "") +
(resources
? `\n\n### Direct Resources\n${resources}`
: "")
(templates ? `\n\n### Resource Templates\n${templates}` : "") +
(resources ? `\n\n### Direct Resources\n${resources}` : "")
)
})
.join("\n\n")}`
@ -899,10 +889,7 @@ You accomplish a given task iteratively, breaking it down into clear steps and w
4. Once you've completed the user's task, you must use the attempt_completion tool to present the result of the task to the user. You may also provide a CLI command to showcase the result of your task; this can be particularly useful for web development tasks, where you can run e.g. \`open index.html\` to show the website you've built.
5. The user may provide feedback, which you can use to make improvements and try again. But DO NOT continue in pointless back and forth conversations, i.e. don't end your responses with questions or offers for further assistance.`
export function addUserInstructions(
settingsCustomInstructions?: string,
clineRulesFileInstructions?: string,
) {
export function addUserInstructions(settingsCustomInstructions?: string, clineRulesFileInstructions?: string) {
let customInstructions = ""
if (settingsCustomInstructions) {
customInstructions += settingsCustomInstructions + "\n\n"

View file

@ -16,19 +16,13 @@ import { ApiProvider, ModelInfo } from "../../shared/api"
import { findLast } from "../../shared/array"
import { ExtensionMessage, ExtensionState } from "../../shared/ExtensionMessage"
import { HistoryItem } from "../../shared/HistoryItem"
import {
ClineCheckpointRestore,
WebviewMessage,
} from "../../shared/WebviewMessage"
import { ClineCheckpointRestore, WebviewMessage } from "../../shared/WebviewMessage"
import { fileExistsAtPath } from "../../utils/fs"
import { Cline } from "../Cline"
import { openMention } from "../mentions"
import { getNonce } from "./getNonce"
import { getUri } from "./getUri"
import {
AutoApprovalSettings,
DEFAULT_AUTO_APPROVAL_SETTINGS,
} from "../../shared/AutoApprovalSettings"
import { AutoApprovalSettings, DEFAULT_AUTO_APPROVAL_SETTINGS } from "../../shared/AutoApprovalSettings"
/*
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
@ -125,10 +119,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
}
public static getVisibleInstance(): ClineProvider | undefined {
return findLast(
Array.from(this.activeInstances),
(instance) => instance.view?.visible === true,
)
return findLast(Array.from(this.activeInstances), (instance) => instance.view?.visible === true)
}
resolveWebviewView(
@ -219,22 +210,13 @@ export class ClineProvider implements vscode.WebviewViewProvider {
async initClineWithTask(task?: string, images?: string[]) {
await this.clearTask() // ensures that an exising task doesn't exist before starting a new one, although this shouldn't be possible since user must clear task before starting a new one
const { apiConfiguration, customInstructions, autoApprovalSettings } =
await this.getState()
this.cline = new Cline(
this,
apiConfiguration,
autoApprovalSettings,
customInstructions,
task,
images,
)
const { apiConfiguration, customInstructions, autoApprovalSettings } = await this.getState()
this.cline = new Cline(this, apiConfiguration, autoApprovalSettings, customInstructions, task, images)
}
async initClineWithHistoryItem(historyItem: HistoryItem) {
await this.clearTask()
const { apiConfiguration, customInstructions, autoApprovalSettings } =
await this.getState()
const { apiConfiguration, customInstructions, autoApprovalSettings } = await this.getState()
this.cline = new Cline(
this,
apiConfiguration,
@ -267,21 +249,9 @@ export class ClineProvider implements vscode.WebviewViewProvider {
// then convert it to a uri we can use in the webview.
// The CSS file from the React build output
const stylesUri = getUri(webview, this.context.extensionUri, [
"webview-ui",
"build",
"static",
"css",
"main.css",
])
const stylesUri = getUri(webview, this.context.extensionUri, ["webview-ui", "build", "static", "css", "main.css"])
// The JS file from the React build output
const scriptUri = getUri(webview, this.context.extensionUri, [
"webview-ui",
"build",
"static",
"js",
"main.js",
])
const scriptUri = getUri(webview, this.context.extensionUri, ["webview-ui", "build", "static", "js", "main.js"])
// The codicon font from the React build output
// https://github.com/microsoft/vscode-extension-samples/blob/main/webview-codicons-sample/src/extension.ts
@ -369,25 +339,19 @@ export class ClineProvider implements vscode.WebviewViewProvider {
// gui relies on model info to be up-to-date to provide the most accurate pricing, so we need to fetch the latest details on launch.
// we do this for all users since many users switch between api providers and if they were to switch back to openrouter it would be showing outdated model info if we hadn't retrieved the latest at this point
// (see normalizeApiConfiguration > openrouter)
this.refreshOpenRouterModels().then(
async (openRouterModels) => {
if (openRouterModels) {
// update model info in state (this needs to be done here since we don't want to update state while settings is open, and we may refresh models there)
const { apiConfiguration } =
await this.getState()
if (apiConfiguration.openRouterModelId) {
await this.updateGlobalState(
"openRouterModelInfo",
openRouterModels[
apiConfiguration
.openRouterModelId
],
)
await this.postStateToWebview()
}
this.refreshOpenRouterModels().then(async (openRouterModels) => {
if (openRouterModels) {
// update model info in state (this needs to be done here since we don't want to update state while settings is open, and we may refresh models there)
const { apiConfiguration } = await this.getState()
if (apiConfiguration.openRouterModelId) {
await this.updateGlobalState(
"openRouterModelInfo",
openRouterModels[apiConfiguration.openRouterModelId],
)
await this.postStateToWebview()
}
},
)
}
})
break
case "newTask":
// Code that should run in response to the hello message command
@ -398,10 +362,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
// Could also do this in extension .ts
//this.postMessageToWebview({ type: "text", text: `Extension: ${Date.now()}` })
// 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.initClineWithTask(
message.text,
message.images,
)
await this.initClineWithTask(message.text, message.images)
break
case "apiConfiguration":
if (message.apiConfiguration) {
@ -432,92 +393,33 @@ export class ClineProvider implements vscode.WebviewViewProvider {
openRouterModelId,
openRouterModelInfo,
} = message.apiConfiguration
await this.updateGlobalState(
"apiProvider",
apiProvider,
)
await this.updateGlobalState(
"apiModelId",
apiModelId,
)
await this.updateGlobalState("apiProvider", apiProvider)
await this.updateGlobalState("apiModelId", apiModelId)
await this.storeSecret("apiKey", apiKey)
await this.storeSecret(
"openRouterApiKey",
openRouterApiKey,
)
await this.storeSecret("openRouterApiKey", openRouterApiKey)
await this.storeSecret("awsAccessKey", awsAccessKey)
await this.storeSecret("awsSecretKey", awsSecretKey)
await this.storeSecret(
"awsSessionToken",
awsSessionToken,
)
await this.storeSecret("awsSessionToken", awsSessionToken)
await this.updateGlobalState("awsRegion", awsRegion)
await this.updateGlobalState(
"awsUseCrossRegionInference",
awsUseCrossRegionInference,
)
await this.updateGlobalState(
"vertexProjectId",
vertexProjectId,
)
await this.updateGlobalState(
"vertexRegion",
vertexRegion,
)
await this.updateGlobalState(
"openAiBaseUrl",
openAiBaseUrl,
)
await this.updateGlobalState("awsUseCrossRegionInference", awsUseCrossRegionInference)
await this.updateGlobalState("vertexProjectId", vertexProjectId)
await this.updateGlobalState("vertexRegion", vertexRegion)
await this.updateGlobalState("openAiBaseUrl", openAiBaseUrl)
await this.storeSecret("openAiApiKey", openAiApiKey)
await this.updateGlobalState(
"openAiModelId",
openAiModelId,
)
await this.updateGlobalState(
"ollamaModelId",
ollamaModelId,
)
await this.updateGlobalState(
"ollamaBaseUrl",
ollamaBaseUrl,
)
await this.updateGlobalState(
"lmStudioModelId",
lmStudioModelId,
)
await this.updateGlobalState(
"lmStudioBaseUrl",
lmStudioBaseUrl,
)
await this.updateGlobalState(
"anthropicBaseUrl",
anthropicBaseUrl,
)
await this.updateGlobalState("openAiModelId", openAiModelId)
await this.updateGlobalState("ollamaModelId", ollamaModelId)
await this.updateGlobalState("ollamaBaseUrl", ollamaBaseUrl)
await this.updateGlobalState("lmStudioModelId", lmStudioModelId)
await this.updateGlobalState("lmStudioBaseUrl", lmStudioBaseUrl)
await this.updateGlobalState("anthropicBaseUrl", anthropicBaseUrl)
await this.storeSecret("geminiApiKey", geminiApiKey)
await this.storeSecret(
"openAiNativeApiKey",
openAiNativeApiKey,
)
await this.storeSecret(
"deepSeekApiKey",
deepSeekApiKey,
)
await this.updateGlobalState(
"azureApiVersion",
azureApiVersion,
)
await this.updateGlobalState(
"openRouterModelId",
openRouterModelId,
)
await this.updateGlobalState(
"openRouterModelInfo",
openRouterModelInfo,
)
await this.storeSecret("openAiNativeApiKey", openAiNativeApiKey)
await this.storeSecret("deepSeekApiKey", deepSeekApiKey)
await this.updateGlobalState("azureApiVersion", azureApiVersion)
await this.updateGlobalState("openRouterModelId", openRouterModelId)
await this.updateGlobalState("openRouterModelInfo", openRouterModelInfo)
if (this.cline) {
this.cline.api = buildApiHandler(
message.apiConfiguration,
)
this.cline.api = buildApiHandler(message.apiConfiguration)
}
}
await this.postStateToWebview()
@ -527,23 +429,15 @@ export class ClineProvider implements vscode.WebviewViewProvider {
break
case "autoApprovalSettings":
if (message.autoApprovalSettings) {
await this.updateGlobalState(
"autoApprovalSettings",
message.autoApprovalSettings,
)
await this.updateGlobalState("autoApprovalSettings", message.autoApprovalSettings)
if (this.cline) {
this.cline.autoApprovalSettings =
message.autoApprovalSettings
this.cline.autoApprovalSettings = message.autoApprovalSettings
}
await this.postStateToWebview()
}
break
case "askResponse":
this.cline?.handleWebviewAskResponse(
message.askResponse!,
message.text,
message.images,
)
this.cline?.handleWebviewAskResponse(message.askResponse!, message.text, message.images)
break
case "clearTask":
// newTask will start a new task with a given task text, while clear task resets the current session and allows for a new task to be started
@ -551,10 +445,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
await this.postStateToWebview()
break
case "didShowAnnouncement":
await this.updateGlobalState(
"lastShownAnnouncementId",
this.latestAnnouncementId,
)
await this.updateGlobalState("lastShownAnnouncementId", this.latestAnnouncementId)
await this.postStateToWebview()
break
case "selectImages":
@ -583,18 +474,14 @@ export class ClineProvider implements vscode.WebviewViewProvider {
await this.resetState()
break
case "requestOllamaModels":
const ollamaModels = await this.getOllamaModels(
message.text,
)
const ollamaModels = await this.getOllamaModels(message.text)
this.postMessageToWebview({
type: "ollamaModels",
ollamaModels,
})
break
case "requestLmStudioModels":
const lmStudioModels = await this.getLmStudioModels(
message.text,
)
const lmStudioModels = await this.getLmStudioModels(message.text)
this.postMessageToWebview({
type: "lmStudioModels",
lmStudioModels,
@ -614,10 +501,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
break
case "checkpointDiff": {
if (message.number) {
await this.cline?.presentMultifileDiff(
message.number,
false,
)
await this.cline?.presentMultifileDiff(message.number, false)
}
break
}
@ -626,30 +510,19 @@ export class ClineProvider implements vscode.WebviewViewProvider {
// cancel task waits for any open editor to be reverted and starts a new cline instance
if (message.number) {
// wait for messages to be loaded
await pWaitFor(
() => this.cline?.isInitialized === true,
{
timeout: 3_000,
},
).catch(() => {
console.error(
"Failed to init new cline instance",
)
await pWaitFor(() => this.cline?.isInitialized === true, {
timeout: 3_000,
}).catch(() => {
console.error("Failed to init new cline instance")
})
// NOTE: cancelTask awaits abortTask, which awaits diffViewProvider.revertChanges, which reverts any edited files, allowing us to reset to a checkpoint rather than running into a state where the revertChanges function is called alongside or after the checkpoint reset
await this.cline?.restoreCheckpoint(
message.number,
message.text! as ClineCheckpointRestore,
)
await this.cline?.restoreCheckpoint(message.number, message.text! as ClineCheckpointRestore)
}
break
}
case "taskCompletionViewChanges": {
if (message.number) {
await this.cline?.presentMultifileDiff(
message.number,
true,
)
await this.cline?.presentMultifileDiff(message.number, true)
}
break
}
@ -657,8 +530,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
this.cancelTask()
break
case "openMcpSettings": {
const mcpSettingsFilePath =
await this.mcpHub?.getMcpSettingsFilePath()
const mcpSettingsFilePath = await this.mcpHub?.getMcpSettingsFilePath()
if (mcpSettingsFilePath) {
openFile(mcpSettingsFilePath)
}
@ -668,10 +540,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
try {
await this.mcpHub?.restartConnection(message.text!)
} catch (error) {
console.error(
`Failed to retry connection for ${message.text}:`,
error,
)
console.error(`Failed to retry connection for ${message.text}:`, error)
}
break
}
@ -693,10 +562,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
console.error("Failed to abort task", error)
}
await pWaitFor(
() =>
this.cline === undefined ||
this.cline.isStreaming === false ||
this.cline.didFinishAbortingStream,
() => this.cline === undefined || this.cline.isStreaming === false || this.cline.didFinishAbortingStream,
{
timeout: 3_000,
},
@ -714,10 +580,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
async updateCustomInstructions(instructions?: string) {
// User may be clearing the field
await this.updateGlobalState(
"customInstructions",
instructions || undefined,
)
await this.updateGlobalState("customInstructions", instructions || undefined)
if (this.cline) {
this.cline.customInstructions = instructions || undefined
}
@ -727,12 +590,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
// MCP
async ensureMcpServersDirectoryExists(): Promise<string> {
const mcpServersDir = path.join(
os.homedir(),
"Documents",
"Cline",
"MCP",
)
const mcpServersDir = path.join(os.homedir(), "Documents", "Cline", "MCP")
try {
await fs.mkdir(mcpServersDir, { recursive: true })
} catch (error) {
@ -742,10 +600,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
}
async ensureSettingsDirectoryExists(): Promise<string> {
const settingsDir = path.join(
this.context.globalStorageUri.fsPath,
"settings",
)
const settingsDir = path.join(this.context.globalStorageUri.fsPath, "settings")
await fs.mkdir(settingsDir, { recursive: true })
return settingsDir
}
@ -761,8 +616,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
return []
}
const response = await axios.get(`${baseUrl}/api/tags`)
const modelsArray =
response.data?.models?.map((model: any) => model.name) || []
const modelsArray = response.data?.models?.map((model: any) => model.name) || []
const models = [...new Set<string>(modelsArray)]
return models
} catch (error) {
@ -781,8 +635,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
return []
}
const response = await axios.get(`${baseUrl}/v1/models`)
const modelsArray =
response.data?.data?.map((model: any) => model.id) || []
const modelsArray = response.data?.data?.map((model: any) => model.id) || []
const models = [...new Set<string>(modelsArray)]
return models
} catch (error) {
@ -795,10 +648,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
async handleOpenRouterCallback(code: string) {
let apiKey: string
try {
const response = await axios.post(
"https://openrouter.ai/api/v1/auth/keys",
{ code },
)
const response = await axios.post("https://openrouter.ai/api/v1/auth/keys", { code })
if (response.data && response.data.key) {
apiKey = response.data.key
} else {
@ -823,43 +673,27 @@ export class ClineProvider implements vscode.WebviewViewProvider {
}
private async ensureCacheDirectoryExists(): Promise<string> {
const cacheDir = path.join(
this.context.globalStorageUri.fsPath,
"cache",
)
const cacheDir = path.join(this.context.globalStorageUri.fsPath, "cache")
await fs.mkdir(cacheDir, { recursive: true })
return cacheDir
}
async readOpenRouterModels(): Promise<
Record<string, ModelInfo> | undefined
> {
const openRouterModelsFilePath = path.join(
await this.ensureCacheDirectoryExists(),
GlobalFileNames.openRouterModels,
)
async readOpenRouterModels(): Promise<Record<string, ModelInfo> | undefined> {
const openRouterModelsFilePath = path.join(await this.ensureCacheDirectoryExists(), GlobalFileNames.openRouterModels)
const fileExists = await fileExistsAtPath(openRouterModelsFilePath)
if (fileExists) {
const fileContents = await fs.readFile(
openRouterModelsFilePath,
"utf8",
)
const fileContents = await fs.readFile(openRouterModelsFilePath, "utf8")
return JSON.parse(fileContents)
}
return undefined
}
async refreshOpenRouterModels() {
const openRouterModelsFilePath = path.join(
await this.ensureCacheDirectoryExists(),
GlobalFileNames.openRouterModels,
)
const openRouterModelsFilePath = path.join(await this.ensureCacheDirectoryExists(), GlobalFileNames.openRouterModels)
let models: Record<string, ModelInfo> = {}
try {
const response = await axios.get(
"https://openrouter.ai/api/v1/models",
)
const response = await axios.get("https://openrouter.ai/api/v1/models")
/*
{
"id": "anthropic/claude-3.5-sonnet",
@ -898,8 +732,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
const modelInfo: ModelInfo = {
maxTokens: rawModel.top_provider?.max_completion_tokens,
contextWindow: rawModel.context_length,
supportsImages:
rawModel.architecture?.modality?.includes("image"),
supportsImages: rawModel.architecture?.modality?.includes("image"),
supportsPromptCache: false,
inputPrice: parsePrice(rawModel.pricing?.prompt),
outputPrice: parsePrice(rawModel.pricing?.completion),
@ -981,32 +814,15 @@ export class ClineProvider implements vscode.WebviewViewProvider {
uiMessagesFilePath: string
apiConversationHistory: Anthropic.MessageParam[]
}> {
const history =
((await this.getGlobalState("taskHistory")) as
| HistoryItem[]
| undefined) || []
const history = ((await this.getGlobalState("taskHistory")) as HistoryItem[] | undefined) || []
const historyItem = history.find((item) => item.id === id)
if (historyItem) {
const taskDirPath = path.join(
this.context.globalStorageUri.fsPath,
"tasks",
id,
)
const apiConversationHistoryFilePath = path.join(
taskDirPath,
GlobalFileNames.apiConversationHistory,
)
const uiMessagesFilePath = path.join(
taskDirPath,
GlobalFileNames.uiMessages,
)
const fileExists = await fileExistsAtPath(
apiConversationHistoryFilePath,
)
const taskDirPath = path.join(this.context.globalStorageUri.fsPath, "tasks", id)
const apiConversationHistoryFilePath = path.join(taskDirPath, GlobalFileNames.apiConversationHistory)
const uiMessagesFilePath = path.join(taskDirPath, GlobalFileNames.uiMessages)
const fileExists = await fileExistsAtPath(apiConversationHistoryFilePath)
if (fileExists) {
const apiConversationHistory = JSON.parse(
await fs.readFile(apiConversationHistoryFilePath, "utf8"),
)
const apiConversationHistory = JSON.parse(await fs.readFile(apiConversationHistoryFilePath, "utf8"))
return {
historyItem,
taskDirPath,
@ -1035,8 +851,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
}
async exportTaskWithId(id: string) {
const { historyItem, apiConversationHistory } =
await this.getTaskWithId(id)
const { historyItem, apiConversationHistory } = await this.getTaskWithId(id)
await downloadTask(historyItem.ts, apiConversationHistory)
}
@ -1045,18 +860,12 @@ export class ClineProvider implements vscode.WebviewViewProvider {
await this.clearTask()
}
const {
taskDirPath,
apiConversationHistoryFilePath,
uiMessagesFilePath,
} = await this.getTaskWithId(id)
const { taskDirPath, apiConversationHistoryFilePath, uiMessagesFilePath } = await this.getTaskWithId(id)
await this.deleteTaskFromState(id)
// Delete the task files
const apiConversationHistoryFileExists = await fileExistsAtPath(
apiConversationHistoryFilePath,
)
const apiConversationHistoryFileExists = await fileExistsAtPath(apiConversationHistoryFilePath)
if (apiConversationHistoryFileExists) {
await fs.unlink(apiConversationHistoryFilePath)
}
@ -1064,10 +873,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
if (uiMessagesFileExists) {
await fs.unlink(uiMessagesFilePath)
}
const legacyMessagesFilePath = path.join(
taskDirPath,
"claude_messages.json",
)
const legacyMessagesFilePath = path.join(taskDirPath, "claude_messages.json")
if (await fileExistsAtPath(legacyMessagesFilePath)) {
await fs.unlink(legacyMessagesFilePath)
}
@ -1078,10 +884,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
try {
await fs.rm(checkpointsDir, { recursive: true, force: true })
} catch (error) {
console.error(
`Failed to delete checkpoints directory for task ${id}:`,
error,
)
console.error(`Failed to delete checkpoints directory for task ${id}:`, error)
// Continue with deletion of task directory - don't throw since this is a cleanup operation
}
}
@ -1091,10 +894,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
async deleteTaskFromState(id: string) {
// Remove the task from history
const taskHistory =
((await this.getGlobalState("taskHistory")) as
| HistoryItem[]
| undefined) || []
const taskHistory = ((await this.getGlobalState("taskHistory")) as HistoryItem[] | undefined) || []
const updatedTaskHistory = taskHistory.filter((task) => task.id !== id)
await this.updateGlobalState("taskHistory", updatedTaskHistory)
@ -1108,31 +908,18 @@ export class ClineProvider implements vscode.WebviewViewProvider {
}
async getStateToPostToWebview(): Promise<ExtensionState> {
const {
apiConfiguration,
lastShownAnnouncementId,
customInstructions,
taskHistory,
autoApprovalSettings,
} = await this.getState()
const { apiConfiguration, lastShownAnnouncementId, customInstructions, taskHistory, autoApprovalSettings } =
await this.getState()
return {
version: this.context.extension?.packageJSON?.version ?? "",
apiConfiguration,
customInstructions,
uriScheme: vscode.env.uriScheme,
currentTaskItem: this.cline?.taskId
? (taskHistory || []).find(
(item) => item.id === this.cline?.taskId,
)
: undefined,
checkpointTrackerErrorMessage:
this.cline?.checkpointTrackerErrorMessage,
currentTaskItem: this.cline?.taskId ? (taskHistory || []).find((item) => item.id === this.cline?.taskId) : undefined,
checkpointTrackerErrorMessage: this.cline?.checkpointTrackerErrorMessage,
clineMessages: this.cline?.clineMessages || [],
taskHistory: (taskHistory || [])
.filter((item) => item.ts && item.task)
.sort((a, b) => b.ts - a.ts),
shouldShowAnnouncement:
lastShownAnnouncementId !== this.latestAnnouncementId,
taskHistory: (taskHistory || []).filter((item) => item.ts && item.task).sort((a, b) => b.ts - a.ts),
shouldShowAnnouncement: lastShownAnnouncementId !== this.latestAnnouncementId,
autoApprovalSettings,
}
}
@ -1220,9 +1007,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
taskHistory,
autoApprovalSettings,
] = await Promise.all([
this.getGlobalState("apiProvider") as Promise<
ApiProvider | undefined
>,
this.getGlobalState("apiProvider") as Promise<ApiProvider | undefined>,
this.getGlobalState("apiModelId") as Promise<string | undefined>,
this.getSecret("apiKey") as Promise<string | undefined>,
this.getSecret("openRouterApiKey") as Promise<string | undefined>,
@ -1230,51 +1015,27 @@ export class ClineProvider implements vscode.WebviewViewProvider {
this.getSecret("awsSecretKey") as Promise<string | undefined>,
this.getSecret("awsSessionToken") as Promise<string | undefined>,
this.getGlobalState("awsRegion") as Promise<string | undefined>,
this.getGlobalState("awsUseCrossRegionInference") as Promise<
boolean | undefined
>,
this.getGlobalState("vertexProjectId") as Promise<
string | undefined
>,
this.getGlobalState("awsUseCrossRegionInference") as Promise<boolean | undefined>,
this.getGlobalState("vertexProjectId") as Promise<string | undefined>,
this.getGlobalState("vertexRegion") as Promise<string | undefined>,
this.getGlobalState("openAiBaseUrl") as Promise<string | undefined>,
this.getSecret("openAiApiKey") as Promise<string | undefined>,
this.getGlobalState("openAiModelId") as Promise<string | undefined>,
this.getGlobalState("ollamaModelId") as Promise<string | undefined>,
this.getGlobalState("ollamaBaseUrl") as Promise<string | undefined>,
this.getGlobalState("lmStudioModelId") as Promise<
string | undefined
>,
this.getGlobalState("lmStudioBaseUrl") as Promise<
string | undefined
>,
this.getGlobalState("anthropicBaseUrl") as Promise<
string | undefined
>,
this.getGlobalState("lmStudioModelId") as Promise<string | undefined>,
this.getGlobalState("lmStudioBaseUrl") as Promise<string | undefined>,
this.getGlobalState("anthropicBaseUrl") as Promise<string | undefined>,
this.getSecret("geminiApiKey") as Promise<string | undefined>,
this.getSecret("openAiNativeApiKey") as Promise<string | undefined>,
this.getSecret("deepSeekApiKey") as Promise<string | undefined>,
this.getGlobalState("azureApiVersion") as Promise<
string | undefined
>,
this.getGlobalState("openRouterModelId") as Promise<
string | undefined
>,
this.getGlobalState("openRouterModelInfo") as Promise<
ModelInfo | undefined
>,
this.getGlobalState("lastShownAnnouncementId") as Promise<
string | undefined
>,
this.getGlobalState("customInstructions") as Promise<
string | undefined
>,
this.getGlobalState("taskHistory") as Promise<
HistoryItem[] | undefined
>,
this.getGlobalState("autoApprovalSettings") as Promise<
AutoApprovalSettings | undefined
>,
this.getGlobalState("azureApiVersion") as Promise<string | undefined>,
this.getGlobalState("openRouterModelId") as Promise<string | undefined>,
this.getGlobalState("openRouterModelInfo") as Promise<ModelInfo | undefined>,
this.getGlobalState("lastShownAnnouncementId") as Promise<string | undefined>,
this.getGlobalState("customInstructions") as Promise<string | undefined>,
this.getGlobalState("taskHistory") as Promise<HistoryItem[] | undefined>,
this.getGlobalState("autoApprovalSettings") as Promise<AutoApprovalSettings | undefined>,
])
let apiProvider: ApiProvider
@ -1322,14 +1083,12 @@ export class ClineProvider implements vscode.WebviewViewProvider {
lastShownAnnouncementId,
customInstructions,
taskHistory,
autoApprovalSettings:
autoApprovalSettings || DEFAULT_AUTO_APPROVAL_SETTINGS, // default value can be 0 or empty string
autoApprovalSettings: autoApprovalSettings || DEFAULT_AUTO_APPROVAL_SETTINGS, // default value can be 0 or empty string
}
}
async updateTaskHistory(item: HistoryItem): Promise<HistoryItem[]> {
const history =
((await this.getGlobalState("taskHistory")) as HistoryItem[]) || []
const history = ((await this.getGlobalState("taskHistory")) as HistoryItem[]) || []
const existingItemIndex = history.findIndex((h) => h.id === item.id)
if (existingItemIndex !== -1) {
history[existingItemIndex] = item

View file

@ -8,8 +8,7 @@
*/
export function getNonce() {
let text = ""
const possible =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
for (let i = 0; i < 32; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length))
}

View file

@ -10,10 +10,6 @@ import { Uri, Webview } from "vscode"
* @param pathList An array of strings representing the path to a file/resource
* @returns A URI pointing to the file/resource
*/
export function getUri(
webview: Webview,
extensionUri: Uri,
pathList: string[],
) {
export function getUri(webview: Webview, extensionUri: Uri, pathList: string[]) {
return webview.asWebviewUri(Uri.joinPath(extensionUri, ...pathList))
}

View file

@ -7,9 +7,7 @@ The Cline extension exposes an API that can be used by other extensions. To use
3. Get access to the API with the following code:
```ts
const clineExtension = vscode.extensions.getExtension<ClineAPI>(
"saoudrizwan.claude-dev",
)
const clineExtension = vscode.extensions.getExtension<ClineAPI>("saoudrizwan.claude-dev")
if (!clineExtension?.isActive) {
throw new Error("Cline extension is not activated")
@ -31,9 +29,7 @@ The Cline extension exposes an API that can be used by other extensions. To use
await cline.startNewTask("Hello, Cline! Let's make a new project...")
// Start a new task with an initial message and images
await cline.startNewTask("Use this design language", [
"data:image/webp;base64,...",
])
await cline.startNewTask("Use this design language", ["data:image/webp;base64,..."])
// Send a message to the current task
await cline.sendMessage("Can you fix the @problems?")

View file

@ -2,10 +2,7 @@ import * as vscode from "vscode"
import { ClineProvider } from "../core/webview/ClineProvider"
import { ClineAPI } from "./cline"
export function createClineAPI(
outputChannel: vscode.OutputChannel,
sidebarProvider: ClineProvider,
): ClineAPI {
export function createClineAPI(outputChannel: vscode.OutputChannel, sidebarProvider: ClineProvider): ClineAPI {
const api: ClineAPI = {
setCustomInstructions: async (value: string) => {
await sidebarProvider.updateCustomInstructions(value)
@ -13,9 +10,7 @@ export function createClineAPI(
},
getCustomInstructions: async () => {
return (await sidebarProvider.getGlobalState(
"customInstructions",
)) as string | undefined
return (await sidebarProvider.getGlobalState("customInstructions")) as string | undefined
},
startNewTask: async (task?: string, images?: string[]) => {

View file

@ -29,13 +29,9 @@ export function activate(context: vscode.ExtensionContext) {
const sidebarProvider = new ClineProvider(context, outputChannel)
context.subscriptions.push(
vscode.window.registerWebviewViewProvider(
ClineProvider.sideBarId,
sidebarProvider,
{
webviewOptions: { retainContextWhenHidden: true },
},
),
vscode.window.registerWebviewViewProvider(ClineProvider.sideBarId, sidebarProvider, {
webviewOptions: { retainContextWhenHidden: true },
}),
)
context.subscriptions.push(
@ -65,48 +61,25 @@ export function activate(context: vscode.ExtensionContext) {
// https://github.com/microsoft/vscode-extension-samples/blob/main/webview-sample/src/extension.ts
const tabProvider = new ClineProvider(context, outputChannel)
//const column = vscode.window.activeTextEditor ? vscode.window.activeTextEditor.viewColumn : undefined
const lastCol = Math.max(
...vscode.window.visibleTextEditors.map(
(editor) => editor.viewColumn || 0,
),
)
const lastCol = Math.max(...vscode.window.visibleTextEditors.map((editor) => editor.viewColumn || 0))
// Check if there are any visible text editors, otherwise open a new group to the right
const hasVisibleEditors = vscode.window.visibleTextEditors.length > 0
if (!hasVisibleEditors) {
await vscode.commands.executeCommand(
"workbench.action.newGroupRight",
)
await vscode.commands.executeCommand("workbench.action.newGroupRight")
}
const targetCol = hasVisibleEditors
? Math.max(lastCol + 1, 1)
: vscode.ViewColumn.Two
const targetCol = hasVisibleEditors ? Math.max(lastCol + 1, 1) : vscode.ViewColumn.Two
const panel = vscode.window.createWebviewPanel(
ClineProvider.tabPanelId,
"Cline",
targetCol,
{
enableScripts: true,
retainContextWhenHidden: true,
localResourceRoots: [context.extensionUri],
},
)
const panel = vscode.window.createWebviewPanel(ClineProvider.tabPanelId, "Cline", targetCol, {
enableScripts: true,
retainContextWhenHidden: true,
localResourceRoots: [context.extensionUri],
})
// TODO: use better svg icon with light and dark variants (see https://stackoverflow.com/questions/58365687/vscode-extension-iconpath)
panel.iconPath = {
light: vscode.Uri.joinPath(
context.extensionUri,
"assets",
"icons",
"robot_panel_light.png",
),
dark: vscode.Uri.joinPath(
context.extensionUri,
"assets",
"icons",
"robot_panel_dark.png",
),
light: vscode.Uri.joinPath(context.extensionUri, "assets", "icons", "robot_panel_light.png"),
dark: vscode.Uri.joinPath(context.extensionUri, "assets", "icons", "robot_panel_dark.png"),
}
tabProvider.resolveWebviewView(panel)
@ -115,18 +88,8 @@ export function activate(context: vscode.ExtensionContext) {
await vscode.commands.executeCommand("workbench.action.lockEditorGroup")
}
context.subscriptions.push(
vscode.commands.registerCommand(
"cline.popoutButtonClicked",
openClineInNewTab,
),
)
context.subscriptions.push(
vscode.commands.registerCommand(
"cline.openInNewTab",
openClineInNewTab,
),
)
context.subscriptions.push(vscode.commands.registerCommand("cline.popoutButtonClicked", openClineInNewTab))
context.subscriptions.push(vscode.commands.registerCommand("cline.openInNewTab", openClineInNewTab))
context.subscriptions.push(
vscode.commands.registerCommand("cline.settingsButtonClicked", () => {
@ -154,19 +117,12 @@ export function activate(context: vscode.ExtensionContext) {
- Note how the provider doesn't create uris for virtual documents - its role is to provide contents given such an uri. In return, content providers are wired into the open document logic so that providers are always considered.
https://code.visualstudio.com/api/extension-guides/virtual-documents
*/
const diffContentProvider = new (class
implements vscode.TextDocumentContentProvider
{
const diffContentProvider = new (class implements vscode.TextDocumentContentProvider {
provideTextDocumentContent(uri: vscode.Uri): string {
return Buffer.from(uri.query, "base64").toString("utf-8")
}
})()
context.subscriptions.push(
vscode.workspace.registerTextDocumentContentProvider(
DIFF_VIEW_URI_SCHEME,
diffContentProvider,
),
)
context.subscriptions.push(vscode.workspace.registerTextDocumentContentProvider(DIFF_VIEW_URI_SCHEME, diffContentProvider))
// URI Handler
const handleUri = async (uri: vscode.Uri) => {

View file

@ -21,15 +21,10 @@ class CheckpointTracker {
this.cwd = cwd
}
public static async create(
taskId: string,
provider?: ClineProvider,
): Promise<CheckpointTracker> {
public static async create(taskId: string, provider?: ClineProvider): Promise<CheckpointTracker> {
try {
if (!provider) {
throw new Error(
"Provider is required to create a checkpoint tracker",
)
throw new Error("Provider is required to create a checkpoint tracker")
}
// Check if git is installed by attempting to get version
@ -50,13 +45,9 @@ class CheckpointTracker {
}
private static async getWorkingDirectory(): Promise<string> {
const cwd = vscode.workspace.workspaceFolders
?.map((folder) => folder.uri.fsPath)
.at(0)
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0)
if (!cwd) {
throw new Error(
"No workspace detected. Please open Cline in a workspace to use checkpoints.",
)
throw new Error("No workspace detected. Please open Cline in a workspace to use checkpoints.")
}
const homedir = os.homedir()
const desktopPath = path.join(homedir, "Desktop")
@ -78,37 +69,22 @@ class CheckpointTracker {
}
private async getShadowGitPath(): Promise<string> {
const globalStoragePath =
this.providerRef.deref()?.context.globalStorageUri.fsPath
const globalStoragePath = this.providerRef.deref()?.context.globalStorageUri.fsPath
if (!globalStoragePath) {
throw new Error("Global storage uri is invalid")
}
const checkpointsDir = path.join(
globalStoragePath,
"tasks",
this.taskId,
"checkpoints",
)
const checkpointsDir = path.join(globalStoragePath, "tasks", this.taskId, "checkpoints")
await fs.mkdir(checkpointsDir, { recursive: true })
const gitPath = path.join(checkpointsDir, ".git")
return gitPath
}
public static async doesShadowGitExist(
taskId: string,
provider?: ClineProvider,
): Promise<boolean> {
public static async doesShadowGitExist(taskId: string, provider?: ClineProvider): Promise<boolean> {
const globalStoragePath = provider?.context.globalStorageUri.fsPath
if (!globalStoragePath) {
return false
}
const gitPath = path.join(
globalStoragePath,
"tasks",
taskId,
"checkpoints",
".git",
)
const gitPath = path.join(globalStoragePath, "tasks", taskId, "checkpoints", ".git")
return await fileExistsAtPath(gitPath)
}
@ -118,10 +94,7 @@ class CheckpointTracker {
// Make sure it's the same cwd as the configured worktree
const worktree = await this.getShadowGitConfigWorkTree()
if (worktree !== this.cwd) {
throw new Error(
"Checkpoints can only be used in the original workspace: " +
worktree,
)
throw new Error("Checkpoints can only be used in the original workspace: " + worktree)
}
return gitPath
@ -250,8 +223,7 @@ class CheckpointTracker {
const gitPath = await this.getShadowGitPath()
const git = simpleGit(path.dirname(gitPath))
const worktree = await git.getConfig("core.worktree")
this.lastRetrievedShadowGitConfigWorkTree =
worktree.value || undefined
this.lastRetrievedShadowGitConfigWorkTree = worktree.value || undefined
return this.lastRetrievedShadowGitConfigWorkTree
} catch (error) {
console.error("Failed to get shadow git config worktree:", error)
@ -323,11 +295,7 @@ class CheckpointTracker {
// If lhsHash is missing, use the initial commit of the repo
let baseHash = lhsHash
if (!baseHash) {
const rootCommit = await git.raw([
"rev-list",
"--max-parents=0",
"HEAD",
])
const rootCommit = await git.raw(["rev-list", "--max-parents=0", "HEAD"])
baseHash = rootCommit.trim()
}
@ -336,14 +304,11 @@ class CheckpointTracker {
await git.add(".")
await this.renameNestedGitRepos(false)
const diffSummary = rhsHash
? await git.diffSummary([`${baseHash}..${rhsHash}`])
: await git.diffSummary([baseHash])
const diffSummary = rhsHash ? await git.diffSummary([`${baseHash}..${rhsHash}`]) : await git.diffSummary([baseHash])
// For each changed file, gather before/after content
const result = []
const cwdPath =
(await this.getShadowGitConfigWorkTree()) || this.cwd || ""
const cwdPath = (await this.getShadowGitConfigWorkTree()) || this.cwd || ""
for (const file of diffSummary.files) {
const filePath = file.file
@ -387,16 +352,13 @@ class CheckpointTracker {
// Since we use git to track checkpoints, we need to temporarily disable nested git repos to work around git's requirement of using submodules for nested repos.
async renameNestedGitRepos(disable: boolean) {
// Find all .git directories that are not at the root level
const gitPaths = await globby(
"**/.git" + (disable ? "" : GIT_DISABLED_SUFFIX),
{
cwd: this.cwd,
onlyDirectories: true,
ignore: [".git"], // Ignore root level .git
dot: true,
markDirectories: false,
},
)
const gitPaths = await globby("**/.git" + (disable ? "" : GIT_DISABLED_SUFFIX), {
cwd: this.cwd,
onlyDirectories: true,
ignore: [".git"], // Ignore root level .git
dot: true,
markDirectories: false,
})
// For each nested .git directory, rename it based on operation
for (const gitPath of gitPaths) {
@ -405,21 +367,14 @@ class CheckpointTracker {
if (disable) {
newPath = fullPath + GIT_DISABLED_SUFFIX
} else {
newPath = fullPath.endsWith(GIT_DISABLED_SUFFIX)
? fullPath.slice(0, -GIT_DISABLED_SUFFIX.length)
: fullPath
newPath = fullPath.endsWith(GIT_DISABLED_SUFFIX) ? fullPath.slice(0, -GIT_DISABLED_SUFFIX.length) : fullPath
}
try {
await fs.rename(fullPath, newPath)
console.log(
`CheckpointTracker ${disable ? "disabled" : "enabled"} nested git repo ${gitPath}`,
)
console.log(`CheckpointTracker ${disable ? "disabled" : "enabled"} nested git repo ${gitPath}`)
} catch (error) {
console.error(
`CheckpointTracker failed to ${disable ? "disable" : "enable"} nested git repo ${gitPath}:`,
error,
)
console.error(`CheckpointTracker failed to ${disable ? "disable" : "enable"} nested git repo ${gitPath}:`, error)
}
}
}

View file

@ -11,10 +11,7 @@ export function getNewDiagnostics(
for (const [uri, newDiags] of newDiagnostics) {
const oldDiags = oldMap.get(uri) || []
const newProblemsForUri = newDiags.filter(
(newDiag) =>
!oldDiags.some((oldDiag) => deepEqual(oldDiag, newDiag)),
)
const newProblemsForUri = newDiags.filter((newDiag) => !oldDiags.some((oldDiag) => deepEqual(oldDiag, newDiag)))
if (newProblemsForUri.length > 0) {
newProblems.push([uri, newProblemsForUri])
@ -80,9 +77,7 @@ export function diagnosticsToProblemsString(
): string {
let result = ""
for (const [uri, fileDiagnostics] of diagnostics) {
const problems = fileDiagnostics.filter((d) =>
severities.includes(d.severity),
)
const problems = fileDiagnostics.filter((d) => severities.includes(d.severity))
if (problems.length > 0) {
result += `\n\n${path.relative(cwd, uri.fsPath).toPosix()}`
for (const diagnostic of problems) {

View file

@ -1,12 +1,10 @@
import * as vscode from "vscode"
const fadedOverlayDecorationType = vscode.window.createTextEditorDecorationType(
{
backgroundColor: "rgba(255, 255, 0, 0.1)",
opacity: "0.4",
isWholeLine: true,
},
)
const fadedOverlayDecorationType = vscode.window.createTextEditorDecorationType({
backgroundColor: "rgba(255, 255, 0, 0.1)",
opacity: "0.4",
isWholeLine: true,
})
const activeLineDecorationType = vscode.window.createTextEditorDecorationType({
backgroundColor: "rgba(255, 255, 0, 0.3)",
@ -44,20 +42,10 @@ export class DecorationController {
const lastRange = this.ranges[this.ranges.length - 1]
if (lastRange && lastRange.end.line === startIndex - 1) {
this.ranges[this.ranges.length - 1] = lastRange.with(
undefined,
lastRange.end.translate(numLines),
)
this.ranges[this.ranges.length - 1] = lastRange.with(undefined, lastRange.end.translate(numLines))
} else {
const endLine = startIndex + numLines - 1
this.ranges.push(
new vscode.Range(
startIndex,
0,
endLine,
Number.MAX_SAFE_INTEGER,
),
)
this.ranges.push(new vscode.Range(startIndex, 0, endLine, Number.MAX_SAFE_INTEGER))
}
this.editor.setDecorations(this.getDecoration(), this.ranges)
@ -75,13 +63,7 @@ export class DecorationController {
// Add a new range for all lines after the current line
if (line < totalLines - 1) {
this.ranges.push(
new vscode.Range(
new vscode.Position(line + 1, 0),
new vscode.Position(
totalLines - 1,
Number.MAX_SAFE_INTEGER,
),
),
new vscode.Range(new vscode.Position(line + 1, 0), new vscode.Position(totalLines - 1, Number.MAX_SAFE_INTEGER)),
)
}

View file

@ -33,9 +33,7 @@ export class DiffViewProvider {
this.isEditing = true
// if the file is already open, ensure it's not dirty before getting its contents
if (fileExists) {
const existingDocument = vscode.workspace.textDocuments.find(
(doc) => arePathsEqual(doc.uri.fsPath, absolutePath),
)
const existingDocument = vscode.workspace.textDocuments.find((doc) => arePathsEqual(doc.uri.fsPath, absolutePath))
if (existingDocument && existingDocument.isDirty) {
await existingDocument.save()
}
@ -61,11 +59,7 @@ export class DiffViewProvider {
const tabs = vscode.window.tabGroups.all
.map((tg) => tg.tabs)
.flat()
.filter(
(tab) =>
tab.input instanceof vscode.TabInputText &&
arePathsEqual(tab.input.uri.fsPath, absolutePath),
)
.filter((tab) => tab.input instanceof vscode.TabInputText && arePathsEqual(tab.input.uri.fsPath, absolutePath))
for (const tab of tabs) {
if (!tab.isDirty) {
await vscode.window.tabGroups.close(tab)
@ -73,29 +67,16 @@ export class DiffViewProvider {
this.documentWasOpen = true
}
this.activeDiffEditor = await this.openDiffEditor()
this.fadedOverlayController = new DecorationController(
"fadedOverlay",
this.activeDiffEditor,
)
this.activeLineController = new DecorationController(
"activeLine",
this.activeDiffEditor,
)
this.fadedOverlayController = new DecorationController("fadedOverlay", this.activeDiffEditor)
this.activeLineController = new DecorationController("activeLine", this.activeDiffEditor)
// Apply faded overlay to all lines initially
this.fadedOverlayController.addLines(
0,
this.activeDiffEditor.document.lineCount,
)
this.fadedOverlayController.addLines(0, this.activeDiffEditor.document.lineCount)
this.scrollEditorToLine(0) // will this crash for new files?
this.streamedLines = []
}
async update(accumulatedContent: string, isFinal: boolean) {
if (
!this.relPath ||
!this.activeLineController ||
!this.fadedOverlayController
) {
if (!this.relPath || !this.activeLineController || !this.fadedOverlayController) {
throw new Error("Required values not set")
}
this.newContent = accumulatedContent
@ -113,10 +94,7 @@ export class DiffViewProvider {
// Place cursor at the beginning of the diff editor to keep it out of the way of the stream animation
const beginningOfDocument = new vscode.Position(0, 0)
diffEditor.selection = new vscode.Selection(
beginningOfDocument,
beginningOfDocument,
)
diffEditor.selection = new vscode.Selection(beginningOfDocument, beginningOfDocument)
for (let i = 0; i < diffLines.length; i++) {
const currentLine = this.streamedLines.length + i
@ -124,16 +102,12 @@ export class DiffViewProvider {
// This is necessary (as compared to inserting one line at a time) to handle cases where html tags on previous lines are auto closed for example
const edit = new vscode.WorkspaceEdit()
const rangeToReplace = new vscode.Range(0, 0, currentLine + 1, 0)
const contentToReplace =
accumulatedLines.slice(0, currentLine + 1).join("\n") + "\n"
const contentToReplace = accumulatedLines.slice(0, currentLine + 1).join("\n") + "\n"
edit.replace(document.uri, rangeToReplace, contentToReplace)
await vscode.workspace.applyEdit(edit)
// Update decorations
this.activeLineController.setActiveLine(currentLine)
this.fadedOverlayController.updateOverlayAfterLine(
currentLine,
document.lineCount,
)
this.fadedOverlayController.updateOverlayAfterLine(currentLine, document.lineCount)
// Scroll to the current line
this.scrollEditorToLine(currentLine)
}
@ -143,15 +117,7 @@ export class DiffViewProvider {
// Handle any remaining lines if the new content is shorter than the original
if (this.streamedLines.length < document.lineCount) {
const edit = new vscode.WorkspaceEdit()
edit.delete(
document.uri,
new vscode.Range(
this.streamedLines.length,
0,
document.lineCount,
0,
),
)
edit.delete(document.uri, new vscode.Range(this.streamedLines.length, 0, document.lineCount, 0))
await vscode.workspace.applyEdit(edit)
}
// Add empty last line if original content had one
@ -227,31 +193,19 @@ export class DiffViewProvider {
this.cwd,
) // will be empty string if no errors
const newProblemsMessage =
newProblems.length > 0
? `\n\nNew problems detected after saving the file:\n${newProblems}`
: ""
newProblems.length > 0 ? `\n\nNew problems detected after saving the file:\n${newProblems}` : ""
// If the edited content has different EOL characters, we don't want to show a diff with all the EOL differences.
const newContentEOL = this.newContent.includes("\r\n") ? "\r\n" : "\n"
const normalizedPreSaveContent =
preSaveContent.replace(/\r\n|\n/g, newContentEOL).trimEnd() +
newContentEOL // trimEnd to fix issue where editor adds in extra new line automatically
const normalizedPostSaveContent =
postSaveContent.replace(/\r\n|\n/g, newContentEOL).trimEnd() +
newContentEOL // this is the final content we return to the model to use as the new baseline for future edits
const normalizedPreSaveContent = preSaveContent.replace(/\r\n|\n/g, newContentEOL).trimEnd() + newContentEOL // trimEnd to fix issue where editor adds in extra new line automatically
const normalizedPostSaveContent = postSaveContent.replace(/\r\n|\n/g, newContentEOL).trimEnd() + newContentEOL // this is the final content we return to the model to use as the new baseline for future edits
// just in case the new content has a mix of varying EOL characters
const normalizedNewContent =
this.newContent.replace(/\r\n|\n/g, newContentEOL).trimEnd() +
newContentEOL
const normalizedNewContent = this.newContent.replace(/\r\n|\n/g, newContentEOL).trimEnd() + newContentEOL
let userEdits: string | undefined
if (normalizedPreSaveContent !== normalizedNewContent) {
// user made changes before approving edit. let the model know about user made changes (not including post-save auto-formatting changes)
userEdits = formatResponse.createPrettyPatch(
this.relPath.toPosix(),
normalizedNewContent,
normalizedPreSaveContent,
)
userEdits = formatResponse.createPrettyPatch(this.relPath.toPosix(), normalizedNewContent, normalizedPreSaveContent)
// return { newProblemsMessage, userEdits, finalContent: normalizedPostSaveContent }
} else {
// no changes to cline's edits
@ -292,9 +246,7 @@ export class DiffViewProvider {
// Remove only the directories we created, in reverse order
for (let i = this.createdDirs.length - 1; i >= 0; i--) {
await fs.rmdir(this.createdDirs[i])
console.log(
`Directory ${this.createdDirs[i]} has been deleted.`,
)
console.log(`Directory ${this.createdDirs[i]} has been deleted.`)
}
console.log(`File ${absolutePath} has been deleted.`)
} else {
@ -304,24 +256,15 @@ export class DiffViewProvider {
updatedDocument.positionAt(0),
updatedDocument.positionAt(updatedDocument.getText().length),
)
edit.replace(
updatedDocument.uri,
fullRange,
this.originalContent ?? "",
)
edit.replace(updatedDocument.uri, fullRange, this.originalContent ?? "")
// Apply the edit and save, since contents shouldnt have changed this wont show in local history unless of course the user made changes and saved during the edit
await vscode.workspace.applyEdit(edit)
await updatedDocument.save()
console.log(
`File ${absolutePath} has been reverted to its original content.`,
)
console.log(`File ${absolutePath} has been reverted to its original content.`)
if (this.documentWasOpen) {
await vscode.window.showTextDocument(
vscode.Uri.file(absolutePath),
{
preview: false,
},
)
await vscode.window.showTextDocument(vscode.Uri.file(absolutePath), {
preview: false,
})
}
await this.closeAllDiffViews()
}
@ -333,11 +276,7 @@ export class DiffViewProvider {
private async closeAllDiffViews() {
const tabs = vscode.window.tabGroups.all
.flatMap((tg) => tg.tabs)
.filter(
(tab) =>
tab.input instanceof vscode.TabInputTextDiff &&
tab.input?.original?.scheme === DIFF_VIEW_URI_SCHEME,
)
.filter((tab) => tab.input instanceof vscode.TabInputTextDiff && tab.input?.original?.scheme === DIFF_VIEW_URI_SCHEME)
for (const tab of tabs) {
// trying to close dirty views results in save popup
if (!tab.isDirty) {
@ -361,32 +300,23 @@ export class DiffViewProvider {
arePathsEqual(tab.input.modified.fsPath, uri.fsPath),
)
if (diffTab && diffTab.input instanceof vscode.TabInputTextDiff) {
const editor = await vscode.window.showTextDocument(
diffTab.input.modified,
)
const editor = await vscode.window.showTextDocument(diffTab.input.modified)
return editor
}
// Open new diff editor
return new Promise<vscode.TextEditor>((resolve, reject) => {
const fileName = path.basename(uri.fsPath)
const fileExists = this.editType === "modify"
const disposable = vscode.window.onDidChangeActiveTextEditor(
(editor) => {
if (
editor &&
arePathsEqual(editor.document.uri.fsPath, uri.fsPath)
) {
disposable.dispose()
resolve(editor)
}
},
)
const disposable = vscode.window.onDidChangeActiveTextEditor((editor) => {
if (editor && arePathsEqual(editor.document.uri.fsPath, uri.fsPath)) {
disposable.dispose()
resolve(editor)
}
})
vscode.commands.executeCommand(
"vscode.diff",
vscode.Uri.parse(`${DIFF_VIEW_URI_SCHEME}:${fileName}`).with({
query: Buffer.from(this.originalContent ?? "").toString(
"base64",
),
query: Buffer.from(this.originalContent ?? "").toString("base64"),
}),
uri,
`${fileName}: ${fileExists ? "Original ↔ Cline's Changes" : "New File"} (Editable)`,
@ -394,11 +324,7 @@ export class DiffViewProvider {
// This may happen on very slow machines ie project idx
setTimeout(() => {
disposable.dispose()
reject(
new Error(
"Failed to open diff editor, please try again...",
),
)
reject(new Error("Failed to open diff editor, please try again..."))
}, 10_000)
})
}

View file

@ -6,21 +6,10 @@ import * as vscode from "vscode"
* @param newFileContent The new content of the file to check.
* @returns True if a potential omission is detected, false otherwise.
*/
function detectCodeOmission(
originalFileContent: string,
newFileContent: string,
): boolean {
function detectCodeOmission(originalFileContent: string, newFileContent: string): boolean {
const originalLines = originalFileContent.split("\n")
const newLines = newFileContent.split("\n")
const omissionKeywords = [
"remain",
"remains",
"unchanged",
"rest",
"previous",
"existing",
"...",
]
const omissionKeywords = ["remain", "remains", "unchanged", "rest", "previous", "existing", "..."]
const commentPatterns = [
/^\s*\/\//, // Single-line comment for most languages
@ -49,10 +38,7 @@ function detectCodeOmission(
* @param originalFileContent The original content of the file.
* @param newFileContent The new content of the file to check.
*/
export function showOmissionWarning(
originalFileContent: string,
newFileContent: string,
): void {
export function showOmissionWarning(originalFileContent: string, newFileContent: string): void {
if (detectCodeOmission(originalFileContent, newFileContent)) {
vscode.window
.showWarningMessage(

View file

@ -3,10 +3,7 @@ import os from "os"
import * as path from "path"
import * as vscode from "vscode"
export async function downloadTask(
dateTs: number,
conversationHistory: Anthropic.MessageParam[],
) {
export async function downloadTask(dateTs: number, conversationHistory: Anthropic.MessageParam[]) {
// File name
const date = new Date(dateTs)
const month = date.toLocaleString("en-US", { month: "short" }).toLowerCase()
@ -23,12 +20,9 @@ export async function downloadTask(
// Generate markdown
const markdownContent = conversationHistory
.map((message) => {
const role =
message.role === "user" ? "**User:**" : "**Assistant:**"
const role = message.role === "user" ? "**User:**" : "**Assistant:**"
const content = Array.isArray(message.content)
? message.content
.map((block) => formatContentBlockToMarkdown(block))
.join("\n")
? message.content.map((block) => formatContentBlockToMarkdown(block)).join("\n")
: message.content
return `${role}\n\n${content}\n\n`
})
@ -37,27 +31,18 @@ export async function downloadTask(
// Prompt user for save location
const saveUri = await vscode.window.showSaveDialog({
filters: { Markdown: ["md"] },
defaultUri: vscode.Uri.file(
path.join(os.homedir(), "Downloads", fileName),
),
defaultUri: vscode.Uri.file(path.join(os.homedir(), "Downloads", fileName)),
})
if (saveUri) {
// Write content to the selected location
await vscode.workspace.fs.writeFile(
saveUri,
Buffer.from(markdownContent),
)
await vscode.workspace.fs.writeFile(saveUri, Buffer.from(markdownContent))
vscode.window.showTextDocument(saveUri, { preview: true })
}
}
export function formatContentBlockToMarkdown(
block:
| Anthropic.TextBlockParam
| Anthropic.ImageBlockParam
| Anthropic.ToolUseBlockParam
| Anthropic.ToolResultBlockParam,
block: Anthropic.TextBlockParam | Anthropic.ImageBlockParam | Anthropic.ToolUseBlockParam | Anthropic.ToolResultBlockParam,
// messages: Anthropic.MessageParam[]
): string {
switch (block.type) {
@ -69,10 +54,7 @@ export function formatContentBlockToMarkdown(
let input: string
if (typeof block.input === "object" && block.input !== null) {
input = Object.entries(block.input)
.map(
([key, value]) =>
`${key.charAt(0).toUpperCase() + key.slice(1)}: ${value}`,
)
.map(([key, value]) => `${key.charAt(0).toUpperCase() + key.slice(1)}: ${value}`)
.join("\n")
} else {
input = String(block.input)
@ -86,9 +68,7 @@ export function formatContentBlockToMarkdown(
return `[${toolName}${block.is_error ? " (Error)" : ""}]\n${block.content}`
} else if (Array.isArray(block.content)) {
return `[${toolName}${block.is_error ? " (Error)" : ""}]\n${block.content
.map((contentBlock) =>
formatContentBlockToMarkdown(contentBlock),
)
.map((contentBlock) => formatContentBlockToMarkdown(contentBlock))
.join("\n")}`
} else {
return `[${toolName}${block.is_error ? " (Error)" : ""}]`
@ -98,10 +78,7 @@ export function formatContentBlockToMarkdown(
}
}
export function findToolName(
toolCallId: string,
messages: Anthropic.MessageParam[],
): string {
export function findToolName(toolCallId: string, messages: Anthropic.MessageParam[]): string {
for (const message of messages) {
if (Array.isArray(message.content)) {
for (const block of message.content) {

View file

@ -24,9 +24,7 @@ export async function extractTextFromFile(filePath: string): Promise<string> {
if (!isBinary) {
return await fs.readFile(filePath, "utf8")
} else {
throw new Error(
`Cannot read text for file type: ${fileExtension}`,
)
throw new Error(`Cannot read text for file type: ${fileExtension}`)
}
}
}
@ -48,10 +46,7 @@ async function extractTextFromIPYNB(filePath: string): Promise<string> {
let extractedText = ""
for (const cell of notebook.cells) {
if (
(cell.cell_type === "markdown" || cell.cell_type === "code") &&
cell.source
) {
if ((cell.cell_type === "markdown" || cell.cell_type === "code") && cell.source) {
extractedText += cell.source.join("\n") + "\n"
}
}

View file

@ -11,19 +11,10 @@ export async function openImage(dataUri: string) {
}
const [, format, base64Data] = matches
const imageBuffer = Buffer.from(base64Data, "base64")
const tempFilePath = path.join(
os.tmpdir(),
`temp_image_${Date.now()}.${format}`,
)
const tempFilePath = path.join(os.tmpdir(), `temp_image_${Date.now()}.${format}`)
try {
await vscode.workspace.fs.writeFile(
vscode.Uri.file(tempFilePath),
imageBuffer,
)
await vscode.commands.executeCommand(
"vscode.open",
vscode.Uri.file(tempFilePath),
)
await vscode.workspace.fs.writeFile(vscode.Uri.file(tempFilePath), imageBuffer)
await vscode.commands.executeCommand("vscode.open", vscode.Uri.file(tempFilePath))
} catch (error) {
vscode.window.showErrorMessage(`Error opening image: ${error}`)
}
@ -37,21 +28,12 @@ export async function openFile(absolutePath: string) {
try {
for (const group of vscode.window.tabGroups.all) {
const existingTab = group.tabs.find(
(tab) =>
tab.input instanceof vscode.TabInputText &&
arePathsEqual(tab.input.uri.fsPath, uri.fsPath),
(tab) => tab.input instanceof vscode.TabInputText && arePathsEqual(tab.input.uri.fsPath, uri.fsPath),
)
if (existingTab) {
const activeColumn =
vscode.window.activeTextEditor?.viewColumn
const tabColumn = vscode.window.tabGroups.all.find(
(group) => group.tabs.includes(existingTab),
)?.viewColumn
if (
activeColumn &&
activeColumn !== tabColumn &&
!existingTab.isDirty
) {
const activeColumn = vscode.window.activeTextEditor?.viewColumn
const tabColumn = vscode.window.tabGroups.all.find((group) => group.tabs.includes(existingTab))?.viewColumn
if (activeColumn && activeColumn !== tabColumn && !existingTab.isDirty) {
await vscode.window.tabGroups.close(existingTab)
}
break

View file

@ -7,9 +7,7 @@ interface NotificationOptions {
message: string
}
async function showMacOSNotification(
options: NotificationOptions,
): Promise<void> {
async function showMacOSNotification(options: NotificationOptions): Promise<void> {
const { title, subtitle = "", message } = options
const script = `display notification "${message}" with title "${title}" subtitle "${subtitle}" sound name "Tink"`
@ -21,9 +19,7 @@ async function showMacOSNotification(
}
}
async function showWindowsNotification(
options: NotificationOptions,
): Promise<void> {
async function showWindowsNotification(options: NotificationOptions): Promise<void> {
const { subtitle, message } = options
const script = `
@ -54,9 +50,7 @@ async function showWindowsNotification(
}
}
async function showLinuxNotification(
options: NotificationOptions,
): Promise<void> {
async function showLinuxNotification(options: NotificationOptions): Promise<void> {
const { title = "", subtitle = "", message } = options
// Combine subtitle and message if subtitle exists
@ -69,9 +63,7 @@ async function showLinuxNotification(
}
}
export async function showSystemNotification(
options: NotificationOptions,
): Promise<void> {
export async function showSystemNotification(options: NotificationOptions): Promise<void> {
try {
const { title = "Cline", message } = options

View file

@ -1,11 +1,7 @@
import pWaitFor from "p-wait-for"
import * as vscode from "vscode"
import { arePathsEqual } from "../../utils/path"
import {
mergePromise,
TerminalProcess,
TerminalProcessResultPromise,
} from "./TerminalProcess"
import { mergePromise, TerminalProcess, TerminalProcessResultPromise } from "./TerminalProcess"
import { TerminalInfo, TerminalRegistry } from "./TerminalRegistry"
/*
@ -101,9 +97,7 @@ export class TerminalManager {
constructor() {
let disposable: vscode.Disposable | undefined
try {
disposable = (
vscode.window as vscode.Window
).onDidStartTerminalShellExecution?.(async (e) => {
disposable = (vscode.window as vscode.Window).onDidStartTerminalShellExecution?.(async (e) => {
// Creating a read stream here results in a more consistent output. This is most obvious when running the `date` command.
e?.execution?.read()
})
@ -115,10 +109,7 @@ export class TerminalManager {
}
}
runCommand(
terminalInfo: TerminalInfo,
command: string,
): TerminalProcessResultPromise {
runCommand(terminalInfo: TerminalInfo, command: string): TerminalProcessResultPromise {
terminalInfo.busy = true
terminalInfo.lastCommand = command
const process = new TerminalProcess()
@ -130,9 +121,7 @@ export class TerminalManager {
// if shell integration is not available, remove terminal so it does not get reused as it may be running a long-running process
process.once("no_shell_integration", () => {
console.log(
`no_shell_integration received for terminal ${terminalInfo.id}`,
)
console.log(`no_shell_integration received for terminal ${terminalInfo.id}`)
// Remove the terminal so we can't reuse it (in case it's running a long-running process)
TerminalRegistry.removeTerminal(terminalInfo.id)
this.terminalIds.delete(terminalInfo.id)
@ -155,15 +144,9 @@ export class TerminalManager {
process.run(terminalInfo.terminal, command)
} else {
// docs recommend waiting 3s for shell integration to activate
pWaitFor(
() => terminalInfo.terminal.shellIntegration !== undefined,
{ timeout: 4000 },
).finally(() => {
pWaitFor(() => terminalInfo.terminal.shellIntegration !== undefined, { timeout: 4000 }).finally(() => {
const existingProcess = this.processes.get(terminalInfo.id)
if (
existingProcess &&
existingProcess.waitForShellIntegration
) {
if (existingProcess && existingProcess.waitForShellIntegration) {
existingProcess.waitForShellIntegration = false
existingProcess.run(terminalInfo.terminal, command)
}
@ -175,21 +158,16 @@ export class TerminalManager {
async getOrCreateTerminal(cwd: string): Promise<TerminalInfo> {
// Find available terminal from our pool first (created for this task)
const availableTerminal = TerminalRegistry.getAllTerminals().find(
(t) => {
if (t.busy) {
return false
}
const terminalCwd = t.terminal.shellIntegration?.cwd // one of cline's commands could have changed the cwd of the terminal
if (!terminalCwd) {
return false
}
return arePathsEqual(
vscode.Uri.file(cwd).fsPath,
terminalCwd.fsPath,
)
},
)
const availableTerminal = TerminalRegistry.getAllTerminals().find((t) => {
if (t.busy) {
return false
}
const terminalCwd = t.terminal.shellIntegration?.cwd // one of cline's commands could have changed the cwd of the terminal
if (!terminalCwd) {
return false
}
return arePathsEqual(vscode.Uri.file(cwd).fsPath, terminalCwd.fsPath)
})
if (availableTerminal) {
this.terminalIds.add(availableTerminal.id)
return availableTerminal
@ -203,9 +181,7 @@ export class TerminalManager {
getTerminals(busy: boolean): { id: number; lastCommand: string }[] {
return Array.from(this.terminalIds)
.map((id) => TerminalRegistry.getTerminal(id))
.filter(
(t): t is TerminalInfo => t !== undefined && t.busy === busy,
)
.filter((t): t is TerminalInfo => t !== undefined && t.busy === busy)
.map((t) => ({ id: t.id, lastCommand: t.lastCommand }))
}

View file

@ -27,10 +27,7 @@ export class TerminalProcess extends EventEmitter<TerminalProcessEvents> {
// super()
async run(terminal: vscode.Terminal, command: string) {
if (
terminal.shellIntegration &&
terminal.shellIntegration.executeCommand
) {
if (terminal.shellIntegration && terminal.shellIntegration.executeCommand) {
const execution = terminal.shellIntegration.executeCommand(command)
const stream = execution.read()
// todo: need to handle errors
@ -63,9 +60,7 @@ export class TerminalProcess extends EventEmitter<TerminalProcessEvents> {
// Once we've retrieved any potential output between sequences, we can remove everything up to end of the last sequence
// https://code.visualstudio.com/docs/terminal/shell-integration#_vs-code-custom-sequences-osc-633-st
const vscodeSequenceRegex = /\x1b\]633;.[^\x07]*\x07/g
const lastMatch = [
...data.matchAll(vscodeSequenceRegex),
].pop()
const lastMatch = [...data.matchAll(vscodeSequenceRegex)].pop()
if (lastMatch && lastMatch.index !== undefined) {
data = data.slice(lastMatch.index + lastMatch[0].length)
}
@ -82,11 +77,7 @@ export class TerminalProcess extends EventEmitter<TerminalProcessEvents> {
lines[0] = lines[0].replace(/[^\x20-\x7E]/g, "")
}
// Check if first two characters are the same, if so remove the first character
if (
lines.length > 0 &&
lines[0].length >= 2 &&
lines[0][0] === lines[0][1]
) {
if (lines.length > 0 && lines[0].length >= 2 && lines[0][0] === lines[0][1]) {
lines[0] = lines[0].slice(1)
}
// Remove everything up to the first alphanumeric character for first two lines
@ -129,14 +120,7 @@ export class TerminalProcess extends EventEmitter<TerminalProcessEvents> {
clearTimeout(this.hotTimer)
}
// these markers indicate the command is some kind of local dev server recompiling the app, which we want to wait for output of before sending request to cline
const compilingMarkers = [
"compiling",
"building",
"bundling",
"transpiling",
"generating",
"starting",
]
const compilingMarkers = ["compiling", "building", "bundling", "transpiling", "generating", "starting"]
const markerNullifiers = [
"compiled",
"success",
@ -152,19 +136,13 @@ export class TerminalProcess extends EventEmitter<TerminalProcessEvents> {
"fail",
]
const isCompiling =
compilingMarkers.some((marker) =>
data.toLowerCase().includes(marker.toLowerCase()),
) &&
!markerNullifiers.some((nullifier) =>
data.toLowerCase().includes(nullifier.toLowerCase()),
)
compilingMarkers.some((marker) => data.toLowerCase().includes(marker.toLowerCase())) &&
!markerNullifiers.some((nullifier) => data.toLowerCase().includes(nullifier.toLowerCase()))
this.hotTimer = setTimeout(
() => {
this.isHot = false
},
isCompiling
? PROCESS_HOT_TIMEOUT_COMPILING
: PROCESS_HOT_TIMEOUT_NORMAL,
isCompiling ? PROCESS_HOT_TIMEOUT_COMPILING : PROCESS_HOT_TIMEOUT_NORMAL,
)
// For non-immediately returning commands we want to show loading spinner right away but this wouldnt happen until it emits a line break, so as soon as we get any output we emit "" to let webview know to show spinner
@ -176,8 +154,7 @@ export class TerminalProcess extends EventEmitter<TerminalProcessEvents> {
this.fullOutput += data
if (this.isListening) {
this.emitIfEol(data)
this.lastRetrievedIndex =
this.fullOutput.length - this.buffer.length
this.lastRetrievedIndex = this.fullOutput.length - this.buffer.length
}
}
@ -260,20 +237,10 @@ export class TerminalProcess extends EventEmitter<TerminalProcessEvents> {
export type TerminalProcessResultPromise = TerminalProcess & Promise<void>
// Similar to execa's ResultPromise, this lets us create a mixin of both a TerminalProcess and a Promise: https://github.com/sindresorhus/execa/blob/main/lib/methods/promise.js
export function mergePromise(
process: TerminalProcess,
promise: Promise<void>,
): TerminalProcessResultPromise {
export function mergePromise(process: TerminalProcess, promise: Promise<void>): TerminalProcessResultPromise {
const nativePromisePrototype = (async () => {})().constructor.prototype
const descriptors = ["then", "catch", "finally"].map(
(property) =>
[
property,
Reflect.getOwnPropertyDescriptor(
nativePromisePrototype,
property,
),
] as const,
(property) => [property, Reflect.getOwnPropertyDescriptor(nativePromisePrototype, property)] as const,
)
for (const [property, descriptor] of descriptors) {
if (descriptor) {

View file

@ -50,9 +50,7 @@ export class TerminalRegistry {
}
static getAllTerminals(): TerminalInfo[] {
this.terminals = this.terminals.filter(
(t) => !this.isTerminalClosed(t.terminal),
)
this.terminals = this.terminals.filter((t) => !this.isTerminalClosed(t.terminal))
return this.terminals
}

View file

@ -154,10 +154,7 @@
}
},
{
"scope": [
"keyword.operator.or.regexp",
"keyword.control.anchor.regexp"
],
"scope": ["keyword.operator.or.regexp", "keyword.control.anchor.regexp"],
"settings": {
"foreground": "#DCDCAA"
}

View file

@ -345,10 +345,7 @@
}
},
{
"scope": [
"punctuation.section.embedded.begin.php",
"punctuation.section.embedded.end.php"
],
"scope": ["punctuation.section.embedded.begin.php", "punctuation.section.embedded.end.php"],
"settings": {
"foreground": "#569cd6"
}
@ -367,11 +364,7 @@
},
{
"name": "coloring of the Java import and package identifiers",
"scope": [
"storage.modifier.import.java",
"variable.language.wildcard.java",
"storage.modifier.package.java"
],
"scope": ["storage.modifier.import.java", "variable.language.wildcard.java", "storage.modifier.package.java"],
"settings": {
"foreground": "#d4d4d4"
}

View file

@ -57,12 +57,7 @@
}
},
{
"scope": [
"constant.numeric",
"constant.other.color.rgb-value",
"constant.other.rgb-value",
"support.constant.color"
],
"scope": ["constant.numeric", "constant.other.color.rgb-value", "constant.other.rgb-value", "support.constant.color"],
"settings": {
"foreground": "#b5cea8"
}
@ -320,11 +315,7 @@
},
{
"name": "coloring of the Java import and package identifiers",
"scope": [
"storage.modifier.import.java",
"variable.language.wildcard.java",
"storage.modifier.package.java"
],
"scope": ["storage.modifier.import.java", "variable.language.wildcard.java", "storage.modifier.package.java"],
"settings": {
"foreground": "#d4d4d4"
}
@ -410,11 +401,7 @@
},
{
"name": "Variable and parameter name",
"scope": [
"variable",
"meta.definition.variable.name",
"support.variable"
],
"scope": ["variable", "meta.definition.variable.name", "support.variable"],
"settings": {
"foreground": "#9CDCFE"
}

View file

@ -3,11 +3,7 @@
"name": "Light High Contrast",
"tokenColors": [
{
"scope": [
"meta.embedded",
"source.groovy.embedded",
"variable.legacy.builtin.python"
],
"scope": ["meta.embedded", "source.groovy.embedded", "variable.legacy.builtin.python"],
"settings": {
"foreground": "#292929"
}
@ -150,10 +146,7 @@
}
},
{
"scope": [
"punctuation.definition.quote.begin.markdown",
"punctuation.definition.list.begin.markdown"
],
"scope": ["punctuation.definition.quote.begin.markdown", "punctuation.definition.list.begin.markdown"],
"settings": {
"foreground": "#0451A5"
}
@ -335,10 +328,7 @@
}
},
{
"scope": [
"punctuation.section.embedded.begin.php",
"punctuation.section.embedded.end.php"
],
"scope": ["punctuation.section.embedded.begin.php", "punctuation.section.embedded.end.php"],
"settings": {
"foreground": "#0F4A85"
}
@ -356,11 +346,7 @@
}
},
{
"scope": [
"storage.modifier.import.java",
"variable.language.wildcard.java",
"storage.modifier.package.java"
],
"scope": ["storage.modifier.import.java", "variable.language.wildcard.java", "storage.modifier.package.java"],
"settings": {
"foreground": "#000000"
}
@ -519,10 +505,7 @@
}
},
{
"scope": [
"keyword.operator.or.regexp",
"keyword.control.anchor.regexp"
],
"scope": ["keyword.operator.or.regexp", "keyword.control.anchor.regexp"],
"settings": {
"foreground": "#EE0000"
}

View file

@ -160,10 +160,7 @@
}
},
{
"scope": [
"keyword.operator.or.regexp",
"keyword.control.anchor.regexp"
],
"scope": ["keyword.operator.or.regexp", "keyword.control.anchor.regexp"],
"settings": {
"foreground": "#EE0000"
}

View file

@ -185,10 +185,7 @@
}
},
{
"scope": [
"punctuation.definition.quote.begin.markdown",
"punctuation.definition.list.begin.markdown"
],
"scope": ["punctuation.definition.quote.begin.markdown", "punctuation.definition.list.begin.markdown"],
"settings": {
"foreground": "#0451a5"
}
@ -373,10 +370,7 @@
}
},
{
"scope": [
"punctuation.section.embedded.begin.php",
"punctuation.section.embedded.end.php"
],
"scope": ["punctuation.section.embedded.begin.php", "punctuation.section.embedded.end.php"],
"settings": {
"foreground": "#800000"
}
@ -395,11 +389,7 @@
},
{
"name": "coloring of the Java import and package identifiers",
"scope": [
"storage.modifier.import.java",
"variable.language.wildcard.java",
"storage.modifier.package.java"
],
"scope": ["storage.modifier.import.java", "variable.language.wildcard.java", "storage.modifier.package.java"],
"settings": {
"foreground": "#000000"
}

View file

@ -32,10 +32,7 @@ function parseThemeString(themeString: string | undefined): any {
export async function getTheme() {
let currentTheme = undefined
const colorTheme =
vscode.workspace
.getConfiguration("workbench")
.get<string>("colorTheme") || "Default Dark Modern"
const colorTheme = vscode.workspace.getConfiguration("workbench").get<string>("colorTheme") || "Default Dark Modern"
try {
for (let i = vscode.extensions.all.length - 1; i >= 0; i--) {
@ -46,10 +43,7 @@ export async function getTheme() {
if (extension.packageJSON?.contributes?.themes?.length > 0) {
for (const theme of extension.packageJSON.contributes.themes) {
if (theme.label === colorTheme) {
const themePath = path.join(
extension.extensionPath,
theme.path,
)
const themePath = path.join(extension.extensionPath, theme.path)
currentTheme = await fs.readFile(themePath, "utf-8")
break
}
@ -60,14 +54,7 @@ export async function getTheme() {
if (currentTheme === undefined && defaultThemes[colorTheme]) {
const filename = `${defaultThemes[colorTheme]}.json`
currentTheme = await fs.readFile(
path.join(
getExtensionUri().fsPath,
"src",
"integrations",
"theme",
"default-themes",
filename,
),
path.join(getExtensionUri().fsPath, "src", "integrations", "theme", "default-themes", filename),
"utf-8",
)
}
@ -77,14 +64,7 @@ export async function getTheme() {
if (parsed.include) {
const includeThemeString = await fs.readFile(
path.join(
getExtensionUri().fsPath,
"src",
"integrations",
"theme",
"default-themes",
parsed.include,
),
path.join(getExtensionUri().fsPath, "src", "integrations", "theme", "default-themes", parsed.include),
"utf-8",
)
const includeTheme = parseThemeString(includeThemeString)
@ -94,11 +74,7 @@ export async function getTheme() {
const converted = convertTheme(parsed)
converted.base = (
["vs", "hc-black"].includes(converted.base)
? converted.base
: colorTheme.includes("Light")
? "vs"
: "vs-dark"
["vs", "hc-black"].includes(converted.base) ? converted.base : colorTheme.includes("Light") ? "vs" : "vs-dark"
) as any
return converted
@ -134,11 +110,7 @@ export function mergeJson(
// Merge keys are used to determine whether an item form the second object should override one from the first
const keptFromFirst: any[] = []
firstValue.forEach((item: any) => {
if (
!secondValue.some((item2: any) =>
mergeKeys[key](item, item2),
)
) {
if (!secondValue.some((item2: any) => mergeKeys[key](item, item2))) {
keptFromFirst.push(item)
}
})
@ -146,16 +118,9 @@ export function mergeJson(
} else {
copyOfFirst[key] = [...firstValue, ...secondValue]
}
} else if (
typeof secondValue === "object" &&
typeof firstValue === "object"
) {
} else if (typeof secondValue === "object" && typeof firstValue === "object") {
// Object
copyOfFirst[key] = mergeJson(
firstValue,
secondValue,
mergeBehavior,
)
copyOfFirst[key] = mergeJson(firstValue, secondValue, mergeBehavior)
} else {
// Other (boolean, number, string)
copyOfFirst[key] = secondValue
@ -172,6 +137,5 @@ export function mergeJson(
}
function getExtensionUri(): vscode.Uri {
return vscode.extensions.getExtension("saoudrizwan.claude-dev")!
.extensionUri
return vscode.extensions.getExtension("saoudrizwan.claude-dev")!.extensionUri
}

View file

@ -3,9 +3,7 @@ import * as path from "path"
import { listFiles } from "../../services/glob/list-files"
import { ClineProvider } from "../../core/webview/ClineProvider"
const cwd = vscode.workspace.workspaceFolders
?.map((folder) => folder.uri.fsPath)
.at(0)
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0)
// Note: this is not a drop-in replacement for listFiles at the start of tasks, since that will be done for Desktops when there is no workspace selected
class WorkspaceTracker {
@ -24,28 +22,20 @@ class WorkspaceTracker {
return
}
const [files, _] = await listFiles(cwd, true, 1_000)
files.forEach((file) =>
this.filePaths.add(this.normalizeFilePath(file)),
)
files.forEach((file) => this.filePaths.add(this.normalizeFilePath(file)))
this.workspaceDidUpdate()
}
private registerListeners() {
// Listen for file creation
// .bind(this) ensures the callback refers to class instance when using this, not necessary when using arrow function
this.disposables.push(
vscode.workspace.onDidCreateFiles(this.onFilesCreated.bind(this)),
)
this.disposables.push(vscode.workspace.onDidCreateFiles(this.onFilesCreated.bind(this)))
// Listen for file deletion
this.disposables.push(
vscode.workspace.onDidDeleteFiles(this.onFilesDeleted.bind(this)),
)
this.disposables.push(vscode.workspace.onDidDeleteFiles(this.onFilesDeleted.bind(this)))
// Listen for file renaming
this.disposables.push(
vscode.workspace.onDidRenameFiles(this.onFilesRenamed.bind(this)),
)
this.disposables.push(vscode.workspace.onDidRenameFiles(this.onFilesRenamed.bind(this)))
/*
An event that is emitted when a workspace folder is added or removed.
@ -105,23 +95,16 @@ class WorkspaceTracker {
}
private normalizeFilePath(filePath: string): string {
const resolvedPath = cwd
? path.resolve(cwd, filePath)
: path.resolve(filePath)
const resolvedPath = cwd ? path.resolve(cwd, filePath) : path.resolve(filePath)
return filePath.endsWith("/") ? resolvedPath + "/" : resolvedPath
}
private async addFilePath(filePath: string): Promise<string> {
const normalizedPath = this.normalizeFilePath(filePath)
try {
const stat = await vscode.workspace.fs.stat(
vscode.Uri.file(normalizedPath),
)
const stat = await vscode.workspace.fs.stat(vscode.Uri.file(normalizedPath))
const isDirectory = (stat.type & vscode.FileType.Directory) !== 0
const pathWithSlash =
isDirectory && !normalizedPath.endsWith("/")
? normalizedPath + "/"
: normalizedPath
const pathWithSlash = isDirectory && !normalizedPath.endsWith("/") ? normalizedPath + "/" : normalizedPath
this.filePaths.add(pathWithSlash)
return pathWithSlash
} catch {
@ -133,10 +116,7 @@ class WorkspaceTracker {
private async removeFilePath(filePath: string): Promise<boolean> {
const normalizedPath = this.normalizeFilePath(filePath)
return (
this.filePaths.delete(normalizedPath) ||
this.filePaths.delete(normalizedPath + "/")
)
return this.filePaths.delete(normalizedPath) || this.filePaths.delete(normalizedPath + "/")
}
public dispose() {

View file

@ -33,9 +33,7 @@ export async function getPythonEnvPath(): Promise<string | undefined> {
return undefined
}
// Get the active python environment path for the current workspace
const pythonEnv = await pythonApi?.environments?.getActiveEnvironmentPath(
workspaceFolder.uri,
)
const pythonEnv = await pythonApi?.environments?.getActiveEnvironmentPath(workspaceFolder.uri)
if (pythonEnv && pythonEnv.path) {
return pythonEnv.path
} else {

View file

@ -1,13 +1,7 @@
import * as vscode from "vscode"
import * as fs from "fs/promises"
import * as path from "path"
import {
Browser,
Page,
ScreenshotOptions,
TimeoutError,
launch,
} from "puppeteer-core"
import { Browser, Page, ScreenshotOptions, TimeoutError, launch } from "puppeteer-core"
// @ts-ignore
import PCR from "puppeteer-chromium-resolver"
import pWaitFor from "p-wait-for"
@ -85,9 +79,7 @@ export class BrowserSession {
return {}
}
async doAction(
action: (page: Page) => Promise<void>,
): Promise<BrowserActionResult> {
async doAction(action: (page: Page) => Promise<void>): Promise<BrowserActionResult> {
if (!this.page) {
throw new Error(
"Browser is not launched. This may occur if the browser was automatically closed by a non-`browser_action` tool.",

View file

@ -3,15 +3,10 @@ import os from "os"
import * as path from "path"
import { arePathsEqual } from "../../utils/path"
export async function listFiles(
dirPath: string,
recursive: boolean,
limit: number,
): Promise<[string[], boolean]> {
export async function listFiles(dirPath: string, recursive: boolean, limit: number): Promise<[string[], boolean]> {
const absolutePath = path.resolve(dirPath)
// Do not allow listing files in root or home directory, which cline tends to want to do when the user's prompt is vague.
const root =
process.platform === "win32" ? path.parse(absolutePath).root : "/"
const root = process.platform === "win32" ? path.parse(absolutePath).root : "/"
const isRoot = arePathsEqual(absolutePath, root)
if (isRoot) {
return [[root], false]
@ -51,9 +46,7 @@ export async function listFiles(
onlyFiles: false, // true by default, false means it will list directories on their own too
}
// * globs all files in one dir, ** globs files in nested directories
const files = recursive
? await globbyLevelByLevel(limit, options)
: (await globby("*", options)).slice(0, limit)
const files = recursive ? await globbyLevelByLevel(limit, options) : (await globby("*", options)).slice(0, limit)
return [files, files.length >= limit]
}

View file

@ -1,8 +1,5 @@
import { Client } from "@modelcontextprotocol/sdk/client/index.js"
import {
StdioClientTransport,
StdioServerParameters,
} from "@modelcontextprotocol/sdk/client/stdio.js"
import { StdioClientTransport, StdioServerParameters } from "@modelcontextprotocol/sdk/client/stdio.js"
import {
CallToolResultSchema,
ListResourcesResultSchema,
@ -17,18 +14,8 @@ import * as fs from "fs/promises"
import * as path from "path"
import * as vscode from "vscode"
import { z } from "zod"
import {
ClineProvider,
GlobalFileNames,
} from "../../core/webview/ClineProvider"
import {
McpResource,
McpResourceResponse,
McpResourceTemplate,
McpServer,
McpTool,
McpToolCallResponse,
} from "../../shared/mcp"
import { ClineProvider, GlobalFileNames } from "../../core/webview/ClineProvider"
import { McpResource, McpResourceResponse, McpResourceTemplate, McpServer, McpTool, McpToolCallResponse } from "../../shared/mcp"
import { fileExistsAtPath } from "../../utils/fs"
import { arePathsEqual } from "../../utils/path"
@ -81,10 +68,7 @@ export class McpHub {
if (!provider) {
throw new Error("Provider not available")
}
const mcpSettingsFilePath = path.join(
await provider.ensureSettingsDirectoryExists(),
GlobalFileNames.mcpSettings,
)
const mcpSettingsFilePath = path.join(await provider.ensureSettingsDirectoryExists(), GlobalFileNames.mcpSettings)
const fileExists = await fileExistsAtPath(mcpSettingsFilePath)
if (!fileExists) {
await fs.writeFile(
@ -120,20 +104,11 @@ export class McpHub {
return
}
try {
vscode.window.showInformationMessage(
"Updating MCP servers...",
)
await this.updateServerConnections(
result.data.mcpServers || {},
)
vscode.window.showInformationMessage(
"MCP servers updated",
)
vscode.window.showInformationMessage("Updating MCP servers...")
await this.updateServerConnections(result.data.mcpServers || {})
vscode.window.showInformationMessage("MCP servers updated")
} catch (error) {
console.error(
"Failed to process MCP settings change:",
error,
)
console.error("Failed to process MCP settings change:", error)
}
}
}),
@ -151,23 +126,16 @@ export class McpHub {
}
}
private async connectToServer(
name: string,
config: StdioServerParameters,
): Promise<void> {
private async connectToServer(name: string, config: StdioServerParameters): Promise<void> {
// Remove existing connection if it exists (should never happen, the connection should be deleted beforehand)
this.connections = this.connections.filter(
(conn) => conn.server.name !== name,
)
this.connections = this.connections.filter((conn) => conn.server.name !== name)
try {
// Each MCP server requires its own transport connection and has unique capabilities, configurations, and error handling. Having separate clients also allows proper scoping of resources/tools and independent server management like reconnection.
const client = new Client(
{
name: "Cline",
version:
this.providerRef.deref()?.context.extension?.packageJSON
?.version ?? "1.0.0",
version: this.providerRef.deref()?.context.extension?.packageJSON?.version ?? "1.0.0",
},
{
capabilities: {},
@ -187,9 +155,7 @@ export class McpHub {
transport.onerror = async (error) => {
console.error(`Transport error for "${name}":`, error)
const connection = this.connections.find(
(conn) => conn.server.name === name,
)
const connection = this.connections.find((conn) => conn.server.name === name)
if (connection) {
connection.server.status = "disconnected"
this.appendErrorMessage(connection, error.message)
@ -198,9 +164,7 @@ export class McpHub {
}
transport.onclose = async () => {
const connection = this.connections.find(
(conn) => conn.server.name === name,
)
const connection = this.connections.find((conn) => conn.server.name === name)
if (connection) {
connection.server.status = "disconnected"
}
@ -209,9 +173,7 @@ export class McpHub {
// If the config is invalid, show an error
if (!StdioConfigSchema.safeParse(config).success) {
console.error(
`Invalid config for "${name}": missing or invalid parameters`,
)
console.error(`Invalid config for "${name}": missing or invalid parameters`)
const connection: McpConnection = {
server: {
name,
@ -246,9 +208,7 @@ export class McpHub {
stderrStream.on("data", async (data: Buffer) => {
const errorOutput = data.toString()
console.error(`Server "${name}" stderr:`, errorOutput)
const connection = this.connections.find(
(conn) => conn.server.name === name,
)
const connection = this.connections.find((conn) => conn.server.name === name)
if (connection) {
// NOTE: we do not set server status to "disconnected" because stderr logs do not necessarily mean the server crashed or disconnected, it could just be informational. In fact when the server first starts up, it immediately logs "<name> server running on stdio" to stderr.
this.appendErrorMessage(connection, errorOutput)
@ -293,28 +253,20 @@ export class McpHub {
// Initial fetch of tools and resources
connection.server.tools = await this.fetchToolsList(name)
connection.server.resources = await this.fetchResourcesList(name)
connection.server.resourceTemplates =
await this.fetchResourceTemplatesList(name)
connection.server.resourceTemplates = await this.fetchResourceTemplatesList(name)
} catch (error) {
// Update status with error
const connection = this.connections.find(
(conn) => conn.server.name === name,
)
const connection = this.connections.find((conn) => conn.server.name === name)
if (connection) {
connection.server.status = "disconnected"
this.appendErrorMessage(
connection,
error instanceof Error ? error.message : String(error),
)
this.appendErrorMessage(connection, error instanceof Error ? error.message : String(error))
}
throw error
}
}
private appendErrorMessage(connection: McpConnection, error: string) {
const newError = connection.server.error
? `${connection.server.error}\n${error}`
: error
const newError = connection.server.error ? `${connection.server.error}\n${error}` : error
connection.server.error = newError //.slice(0, 800)
}
@ -322,10 +274,7 @@ export class McpHub {
try {
const response = await this.connections
.find((conn) => conn.server.name === serverName)
?.client.request(
{ method: "tools/list" },
ListToolsResultSchema,
)
?.client.request({ method: "tools/list" }, ListToolsResultSchema)
return response?.tools || []
} catch (error) {
// console.error(`Failed to fetch tools for ${serverName}:`, error)
@ -333,16 +282,11 @@ export class McpHub {
}
}
private async fetchResourcesList(
serverName: string,
): Promise<McpResource[]> {
private async fetchResourcesList(serverName: string): Promise<McpResource[]> {
try {
const response = await this.connections
.find((conn) => conn.server.name === serverName)
?.client.request(
{ method: "resources/list" },
ListResourcesResultSchema,
)
?.client.request({ method: "resources/list" }, ListResourcesResultSchema)
return response?.resources || []
} catch (error) {
// console.error(`Failed to fetch resources for ${serverName}:`, error)
@ -350,16 +294,11 @@ export class McpHub {
}
}
private async fetchResourceTemplatesList(
serverName: string,
): Promise<McpResourceTemplate[]> {
private async fetchResourceTemplatesList(serverName: string): Promise<McpResourceTemplate[]> {
try {
const response = await this.connections
.find((conn) => conn.server.name === serverName)
?.client.request(
{ method: "resources/templates/list" },
ListResourceTemplatesResultSchema,
)
?.client.request({ method: "resources/templates/list" }, ListResourceTemplatesResultSchema)
return response?.resourceTemplates || []
} catch (error) {
// console.error(`Failed to fetch resource templates for ${serverName}:`, error)
@ -368,9 +307,7 @@ export class McpHub {
}
async deleteConnection(name: string): Promise<void> {
const connection = this.connections.find(
(conn) => conn.server.name === name,
)
const connection = this.connections.find((conn) => conn.server.name === name)
if (connection) {
try {
// connection.client.removeNotificationHandler("notifications/tools/list_changed")
@ -382,20 +319,14 @@ export class McpHub {
} catch (error) {
console.error(`Failed to close transport for ${name}:`, error)
}
this.connections = this.connections.filter(
(conn) => conn.server.name !== name,
)
this.connections = this.connections.filter((conn) => conn.server.name !== name)
}
}
async updateServerConnections(
newServers: Record<string, any>,
): Promise<void> {
async updateServerConnections(newServers: Record<string, any>): Promise<void> {
this.isConnecting = true
this.removeAllFileWatchers()
const currentNames = new Set(
this.connections.map((conn) => conn.server.name),
)
const currentNames = new Set(this.connections.map((conn) => conn.server.name))
const newNames = new Set(Object.keys(newServers))
// Delete removed servers
@ -408,9 +339,7 @@ export class McpHub {
// Update or add servers
for (const [name, config] of Object.entries(newServers)) {
const currentConnection = this.connections.find(
(conn) => conn.server.name === name,
)
const currentConnection = this.connections.find((conn) => conn.server.name === name)
if (!currentConnection) {
// New server
@ -418,27 +347,17 @@ export class McpHub {
this.setupFileWatcher(name, config)
await this.connectToServer(name, config)
} catch (error) {
console.error(
`Failed to connect to new MCP server ${name}:`,
error,
)
console.error(`Failed to connect to new MCP server ${name}:`, error)
}
} else if (
!deepEqual(JSON.parse(currentConnection.server.config), config)
) {
} else if (!deepEqual(JSON.parse(currentConnection.server.config), config)) {
// Existing server with changed config
try {
this.setupFileWatcher(name, config)
await this.deleteConnection(name)
await this.connectToServer(name, config)
console.log(
`Reconnected MCP server with updated config: ${name}`,
)
console.log(`Reconnected MCP server with updated config: ${name}`)
} catch (error) {
console.error(
`Failed to reconnect MCP server ${name}:`,
error,
)
console.error(`Failed to reconnect MCP server ${name}:`, error)
}
}
// If server exists with same config, do nothing
@ -448,9 +367,7 @@ export class McpHub {
}
private setupFileWatcher(name: string, config: any) {
const filePath = config.args?.find((arg: string) =>
arg.includes("build/index.js"),
)
const filePath = config.args?.find((arg: string) => arg.includes("build/index.js"))
if (filePath) {
// we use chokidar instead of onDidSaveTextDocument because it doesn't require the file to be open in the editor. The settings config is better suited for onDidSave since that will be manually updated by the user or Cline (and we want to detect save events, not every file change)
const watcher = chokidar.watch(filePath, {
@ -460,9 +377,7 @@ export class McpHub {
})
watcher.on("change", () => {
console.log(
`Detected change in ${filePath}. Restarting server ${name}...`,
)
console.log(`Detected change in ${filePath}. Restarting server ${name}...`)
this.restartConnection(name)
})
@ -483,14 +398,10 @@ export class McpHub {
}
// Get existing connection and update its status
const connection = this.connections.find(
(conn) => conn.server.name === serverName,
)
const connection = this.connections.find((conn) => conn.server.name === serverName)
const config = connection?.server.config
if (config) {
vscode.window.showInformationMessage(
`Restarting ${serverName} MCP server...`,
)
vscode.window.showInformationMessage(`Restarting ${serverName} MCP server...`)
connection.server.status = "connecting"
connection.server.error = ""
await this.notifyWebviewOfServerChanges()
@ -499,17 +410,10 @@ export class McpHub {
await this.deleteConnection(serverName)
// Try to connect again using existing config
await this.connectToServer(serverName, JSON.parse(config))
vscode.window.showInformationMessage(
`${serverName} MCP server connected`,
)
vscode.window.showInformationMessage(`${serverName} MCP server connected`)
} catch (error) {
console.error(
`Failed to restart connection for ${serverName}:`,
error,
)
vscode.window.showErrorMessage(
`Failed to connect to ${serverName} MCP server`,
)
console.error(`Failed to restart connection for ${serverName}:`, error)
vscode.window.showErrorMessage(`Failed to connect to ${serverName} MCP server`)
}
}
@ -537,13 +441,8 @@ export class McpHub {
// Using server
async readResource(
serverName: string,
uri: string,
): Promise<McpResourceResponse> {
const connection = this.connections.find(
(conn) => conn.server.name === serverName,
)
async readResource(serverName: string, uri: string): Promise<McpResourceResponse> {
const connection = this.connections.find((conn) => conn.server.name === serverName)
if (!connection) {
throw new Error(`No connection found for server: ${serverName}`)
}
@ -558,14 +457,8 @@ export class McpHub {
)
}
async callTool(
serverName: string,
toolName: string,
toolArguments?: Record<string, unknown>,
): Promise<McpToolCallResponse> {
const connection = this.connections.find(
(conn) => conn.server.name === serverName,
)
async callTool(serverName: string, toolName: string, toolArguments?: Record<string, unknown>): Promise<McpToolCallResponse> {
const connection = this.connections.find((conn) => conn.server.name === serverName)
if (!connection) {
throw new Error(
`No connection found for server: ${serverName}. Please make sure to use MCP servers available under 'Connected MCP Servers'.`,
@ -589,10 +482,7 @@ export class McpHub {
try {
await this.deleteConnection(connection.server.name)
} catch (error) {
console.error(
`Failed to close connection for ${connection.server.name}:`,
error,
)
console.error(`Failed to close connection for ${connection.server.name}:`, error)
}
}
this.connections = []

View file

@ -122,12 +122,7 @@ async function execRipgrep(bin: string, args: string[]): Promise<string> {
})
}
export async function regexSearchFiles(
cwd: string,
directoryPath: string,
regex: string,
filePattern?: string,
): Promise<string> {
export async function regexSearchFiles(cwd: string, directoryPath: string, regex: string, filePattern?: string): Promise<string> {
const vscodeAppRoot = vscode.env.appRoot
const rgPath = await getBinPath(vscodeAppRoot)
@ -135,16 +130,7 @@ export async function regexSearchFiles(
throw new Error("Could not find ripgrep binary")
}
const args = [
"--json",
"-e",
regex,
"--glob",
filePattern || "*",
"--context",
"1",
directoryPath,
]
const args = ["--json", "-e", regex, "--glob", filePattern || "*", "--context", "1", directoryPath]
let output: string
try {
@ -173,9 +159,7 @@ export async function regexSearchFiles(
}
} else if (parsed.type === "context" && currentResult) {
if (parsed.data.line_number < currentResult.line!) {
currentResult.beforeContext!.push(
parsed.data.lines.text,
)
currentResult.beforeContext!.push(parsed.data.lines.text)
} else {
currentResult.afterContext!.push(parsed.data.lines.text)
}
@ -216,11 +200,7 @@ function formatResults(results: SearchResult[], cwd: string): string {
output += `${filePath.toPosix()}\n│----\n`
fileResults.forEach((result, index) => {
const allLines = [
...result.beforeContext,
result.match,
...result.afterContext,
]
const allLines = [...result.beforeContext, result.match, ...result.afterContext]
allLines.forEach((line) => {
output += `${line?.trimEnd() ?? ""}\n`
})

View file

@ -5,9 +5,7 @@ import { LanguageParser, loadRequiredLanguageParsers } from "./languageParser"
import { fileExistsAtPath } from "../../utils/fs"
// TODO: implement caching behavior to avoid having to keep analyzing project for new tasks.
export async function parseSourceCodeForDefinitionsTopLevel(
dirPath: string,
): Promise<string> {
export async function parseSourceCodeForDefinitionsTopLevel(dirPath: string): Promise<string> {
// check if the path exists
const dirExists = await fileExistsAtPath(path.resolve(dirPath))
if (!dirExists) {
@ -79,12 +77,8 @@ function separateFiles(allFiles: string[]): {
"php",
"swift",
].map((e) => `.${e}`)
const filesToParse = allFiles
.filter((file) => extensions.includes(path.extname(file)))
.slice(0, 50) // 50 files max
const remainingFiles = allFiles.filter(
(file) => !filesToParse.includes(file),
)
const filesToParse = allFiles.filter((file) => extensions.includes(path.extname(file))).slice(0, 50) // 50 files max
const remainingFiles = allFiles.filter((file) => !filesToParse.includes(file))
return { filesToParse, remainingFiles }
}
@ -104,10 +98,7 @@ This approach allows us to focus on the most relevant parts of the code (defined
- https://github.com/tree-sitter/tree-sitter/blob/master/lib/binding_web/test/helper.js
- https://tree-sitter.github.io/tree-sitter/code-navigation-systems
*/
async function parseFile(
filePath: string,
languageParsers: LanguageParser,
): Promise<string | undefined> {
async function parseFile(filePath: string, languageParsers: LanguageParser): Promise<string | undefined> {
const fileContent = await fs.readFile(filePath, "utf8")
const ext = path.extname(filePath).toLowerCase().slice(1)
@ -127,9 +118,7 @@ async function parseFile(
const captures = query.captures(tree.rootNode)
// Sort captures by their start position
captures.sort(
(a, b) => a.node.startPosition.row - b.node.startPosition.row,
)
captures.sort((a, b) => a.node.startPosition.row - b.node.startPosition.row)
// Split the file content into individual lines
const lines = fileContent.split("\n")

View file

@ -23,9 +23,7 @@ export interface LanguageParser {
}
async function loadLanguage(langName: string) {
return await Parser.Language.load(
path.join(__dirname, `tree-sitter-${langName}.wasm`),
)
return await Parser.Language.load(path.join(__dirname, `tree-sitter-${langName}.wasm`))
}
let isParserInitialized = false
@ -59,13 +57,9 @@ Sources:
- https://github.com/tree-sitter/tree-sitter/blob/master/lib/binding_web/README.md
- https://github.com/tree-sitter/tree-sitter/blob/master/lib/binding_web/test/query-test.js
*/
export async function loadRequiredLanguageParsers(
filesToParse: string[],
): Promise<LanguageParser> {
export async function loadRequiredLanguageParsers(filesToParse: string[]): Promise<LanguageParser> {
await initializeParser()
const extensionsToLoad = new Set(
filesToParse.map((file) => path.extname(file).toLowerCase().slice(1)),
)
const extensionsToLoad = new Set(filesToParse.map((file) => path.extname(file).toLowerCase().slice(1)))
const parsers: LanguageParser = {}
for (const ext of extensionsToLoad) {
let language: Parser.Language

View file

@ -21,12 +21,7 @@ export interface ExtensionMessage {
| "mcpServers"
| "relinquishControl"
text?: string
action?:
| "chatButtonClicked"
| "mcpButtonClicked"
| "settingsButtonClicked"
| "historyButtonClicked"
| "didBecomeVisible"
action?: "chatButtonClicked" | "mcpButtonClicked" | "settingsButtonClicked" | "historyButtonClicked" | "didBecomeVisible"
invoke?: "sendMessage" | "primaryButtonClick" | "secondaryButtonClick"
state?: ExtensionState
images?: string[]
@ -118,14 +113,7 @@ export interface ClineSayTool {
}
// must keep in sync with system prompt
export const browserActions = [
"launch",
"click",
"type",
"scroll_down",
"scroll_up",
"close",
] as const
export const browserActions = ["launch", "click", "type", "scroll_down", "scroll_up", "close"] as const
export type BrowserAction = (typeof browserActions)[number]
export interface ClineSayBrowserAction {

View file

@ -38,9 +38,6 @@ export interface WebviewMessage {
autoApprovalSettings?: AutoApprovalSettings
}
export type ClineAskResponse =
| "yesButtonClicked"
| "noButtonClicked"
| "messageResponse"
export type ClineAskResponse = "yesButtonClicked" | "noButtonClicked" | "messageResponse"
export type ClineCheckpointRestore = "task" | "workspace" | "taskAndWorkspace"

View file

@ -59,8 +59,7 @@ export interface ModelInfo {
// Anthropic
// https://docs.anthropic.com/en/docs/about-claude/models
export type AnthropicModelId = keyof typeof anthropicModels
export const anthropicDefaultModelId: AnthropicModelId =
"claude-3-5-sonnet-20241022"
export const anthropicDefaultModelId: AnthropicModelId = "claude-3-5-sonnet-20241022"
export const anthropicModels = {
"claude-3-5-sonnet-20241022": {
maxTokens: 8192,
@ -108,8 +107,7 @@ export const anthropicModels = {
// AWS Bedrock
// https://docs.aws.amazon.com/bedrock/latest/userguide/conversation-inference.html
export type BedrockModelId = keyof typeof bedrockModels
export const bedrockDefaultModelId: BedrockModelId =
"anthropic.claude-3-5-sonnet-20241022-v2:0"
export const bedrockDefaultModelId: BedrockModelId = "anthropic.claude-3-5-sonnet-20241022-v2:0"
export const bedrockModels = {
"anthropic.claude-3-5-sonnet-20241022-v2:0": {
maxTokens: 8192,
@ -182,8 +180,7 @@ export const openRouterDefaultModelInfo: ModelInfo = {
// Vertex AI
// https://cloud.google.com/vertex-ai/generative-ai/docs/partner-models/use-claude
export type VertexModelId = keyof typeof vertexModels
export const vertexDefaultModelId: VertexModelId =
"claude-3-5-sonnet-v2@20241022"
export const vertexDefaultModelId: VertexModelId = "claude-3-5-sonnet-v2@20241022"
export const vertexModels = {
"claude-3-5-sonnet-v2@20241022": {
maxTokens: 8192,
@ -240,8 +237,7 @@ export const openAiModelInfoSaneDefaults: ModelInfo = {
// Gemini
// https://ai.google.dev/gemini-api/docs/models/gemini
export type GeminiModelId = keyof typeof geminiModels
export const geminiDefaultModelId: GeminiModelId =
"gemini-2.0-flash-thinking-exp-1219"
export const geminiDefaultModelId: GeminiModelId = "gemini-2.0-flash-thinking-exp-1219"
export const geminiModels = {
"gemini-2.0-flash-thinking-exp-1219": {
maxTokens: 8192,

View file

@ -6,10 +6,7 @@
* order, until it finds one where predicate returns true. If such an element is found,
* findLastIndex immediately returns that element index. Otherwise, findLastIndex returns -1.
*/
export function findLastIndex<T>(
array: Array<T>,
predicate: (value: T, index: number, obj: T[]) => boolean,
): number {
export function findLastIndex<T>(array: Array<T>, predicate: (value: T, index: number, obj: T[]) => boolean): number {
let l = array.length
while (l--) {
if (predicate(array[l], l, array)) {
@ -19,10 +16,7 @@ export function findLastIndex<T>(
return -1
}
export function findLast<T>(
array: Array<T>,
predicate: (value: T, index: number, obj: T[]) => boolean,
): T | undefined {
export function findLast<T>(array: Array<T>, predicate: (value: T, index: number, obj: T[]) => boolean): T | undefined {
const index = findLastIndex(array, predicate)
return index === -1 ? undefined : array[index]
}

View file

@ -22,18 +22,12 @@ export function combineApiRequests(messages: ClineMessage[]): ClineMessage[] {
const combinedApiRequests: ClineMessage[] = []
for (let i = 0; i < messages.length; i++) {
if (
messages[i].type === "say" &&
messages[i].say === "api_req_started"
) {
if (messages[i].type === "say" && messages[i].say === "api_req_started") {
let startedRequest = JSON.parse(messages[i].text || "{}")
let j = i + 1
while (j < messages.length) {
if (
messages[j].type === "say" &&
messages[j].say === "api_req_finished"
) {
if (messages[j].type === "say" && messages[j].say === "api_req_finished") {
let finishedRequest = JSON.parse(messages[j].text || "{}")
let combinedRequest = {
...startedRequest,
@ -60,14 +54,10 @@ export function combineApiRequests(messages: ClineMessage[]): ClineMessage[] {
// Replace original api_req_started and remove api_req_finished
return messages
.filter(
(msg) => !(msg.type === "say" && msg.say === "api_req_finished"),
)
.filter((msg) => !(msg.type === "say" && msg.say === "api_req_finished"))
.map((msg) => {
if (msg.type === "say" && msg.say === "api_req_started") {
const combinedRequest = combinedApiRequests.find(
(req) => req.ts === msg.ts,
)
const combinedRequest = combinedApiRequests.find((req) => req.ts === msg.ts)
return combinedRequest || msg
}
return msg

View file

@ -20,34 +20,22 @@ import { ClineMessage } from "./ExtensionMessage"
* const result = simpleCombineCommandSequences(messages);
* // Result: [{ type: 'ask', ask: 'command', text: 'ls\nfile1.txt\nfile2.txt', ts: 1625097600000 }]
*/
export function combineCommandSequences(
messages: ClineMessage[],
): ClineMessage[] {
export function combineCommandSequences(messages: ClineMessage[]): ClineMessage[] {
const combinedCommands: ClineMessage[] = []
// First pass: combine commands with their outputs
for (let i = 0; i < messages.length; i++) {
if (
messages[i].type === "ask" &&
(messages[i].ask === "command" || messages[i].say === "command")
) {
if (messages[i].type === "ask" && (messages[i].ask === "command" || messages[i].say === "command")) {
let combinedText = messages[i].text || ""
let didAddOutput = false
let j = i + 1
while (j < messages.length) {
if (
messages[j].type === "ask" &&
(messages[j].ask === "command" ||
messages[j].say === "command")
) {
if (messages[j].type === "ask" && (messages[j].ask === "command" || messages[j].say === "command")) {
// Stop if we encounter the next command
break
}
if (
messages[j].ask === "command_output" ||
messages[j].say === "command_output"
) {
if (messages[j].ask === "command_output" || messages[j].say === "command_output") {
if (!didAddOutput) {
// Add a newline before the first output
combinedText += `\n${COMMAND_OUTPUT_STRING}`
@ -73,18 +61,10 @@ export function combineCommandSequences(
// Second pass: remove command_outputs and replace original commands with combined ones
return messages
.filter(
(msg) =>
!(msg.ask === "command_output" || msg.say === "command_output"),
)
.filter((msg) => !(msg.ask === "command_output" || msg.say === "command_output"))
.map((msg) => {
if (
msg.type === "ask" &&
(msg.ask === "command" || msg.say === "command")
) {
const combinedCommand = combinedCommands.find(
(cmd) => cmd.ts === msg.ts,
)
if (msg.type === "ask" && (msg.ask === "command" || msg.say === "command")) {
const combinedCommand = combinedCommands.find((cmd) => cmd.ts === msg.ts)
return combinedCommand || msg
}
return msg

View file

@ -44,6 +44,5 @@ Mention regex:
- `mentionRegexGlobal`: Creates a global version of the `mentionRegex` to find all matches within a given string.
*/
export const mentionRegex =
/@((?:\/|\w+:\/\/)[^\s]+?|problems\b)(?=[.,;:!?]?(?=[\s\r\n]|$))/
export const mentionRegex = /@((?:\/|\w+:\/\/)[^\s]+?|problems\b)(?=[.,;:!?]?(?=[\s\r\n]|$))/
export const mentionRegexGlobal = new RegExp(mentionRegex.source, "g")

View file

@ -35,16 +35,10 @@ export function getApiMetrics(messages: ClineMessage[]): ApiMetrics {
}
messages.forEach((message) => {
if (
message.type === "say" &&
(message.say === "api_req_started" ||
message.say === "deleted_api_reqs") &&
message.text
) {
if (message.type === "say" && (message.say === "api_req_started" || message.say === "deleted_api_reqs") && message.text) {
try {
const parsedData = JSON.parse(message.text)
const { tokensIn, tokensOut, cacheWrites, cacheReads, cost } =
parsedData
const { tokensIn, tokensOut, cacheWrites, cacheReads, cost } = parsedData
if (typeof tokensIn === "number") {
result.totalTokensIn += tokensIn
@ -53,12 +47,10 @@ export function getApiMetrics(messages: ClineMessage[]): ApiMetrics {
result.totalTokensOut += tokensOut
}
if (typeof cacheWrites === "number") {
result.totalCacheWrites =
(result.totalCacheWrites ?? 0) + cacheWrites
result.totalCacheWrites = (result.totalCacheWrites ?? 0) + cacheWrites
}
if (typeof cacheReads === "number") {
result.totalCacheReads =
(result.totalCacheReads ?? 0) + cacheReads
result.totalCacheReads = (result.totalCacheReads ?? 0) + cacheReads
}
if (typeof cost === "number") {
result.totalCost += cost

View file

@ -10,19 +10,15 @@ export function calculateApiCost(
const modelCacheWritesPrice = modelInfo.cacheWritesPrice
let cacheWritesCost = 0
if (cacheCreationInputTokens && modelCacheWritesPrice) {
cacheWritesCost =
(modelCacheWritesPrice / 1_000_000) * cacheCreationInputTokens
cacheWritesCost = (modelCacheWritesPrice / 1_000_000) * cacheCreationInputTokens
}
const modelCacheReadsPrice = modelInfo.cacheReadsPrice
let cacheReadsCost = 0
if (cacheReadInputTokens && modelCacheReadsPrice) {
cacheReadsCost =
(modelCacheReadsPrice / 1_000_000) * cacheReadInputTokens
cacheReadsCost = (modelCacheReadsPrice / 1_000_000) * cacheReadInputTokens
}
const baseInputCost =
((modelInfo.inputPrice || 0) / 1_000_000) * inputTokens
const baseInputCost = ((modelInfo.inputPrice || 0) / 1_000_000) * inputTokens
const outputCost = ((modelInfo.outputPrice || 0) / 1_000_000) * outputTokens
const totalCost =
cacheWritesCost + cacheReadsCost + baseInputCost + outputCost
const totalCost = cacheWritesCost + cacheReadsCost + baseInputCost + outputCost
return totalCost
}

View file

@ -6,10 +6,7 @@ import "should"
import { createDirectoriesForFile, fileExistsAtPath } from "./fs"
describe("Filesystem Utilities", () => {
const tmpDir = path.join(
os.tmpdir(),
"cline-test-" + Math.random().toString(36).slice(2),
)
const tmpDir = path.join(os.tmpdir(), "cline-test-" + Math.random().toString(36).slice(2))
// Clean up after tests
after(async () => {
@ -39,13 +36,7 @@ describe("Filesystem Utilities", () => {
describe("createDirectoriesForFile", () => {
it("should create all necessary directories", async () => {
const deepPath = path.join(
tmpDir,
"deep",
"nested",
"dir",
"file.txt",
)
const deepPath = path.join(tmpDir, "deep", "nested", "dir", "file.txt")
const createdDirs = await createDirectoriesForFile(deepPath)
// Verify directories were created
@ -68,14 +59,7 @@ describe("Filesystem Utilities", () => {
})
it("should normalize paths", async () => {
const unnormalizedPath = path.join(
tmpDir,
"a",
"..",
"b",
".",
"file.txt",
)
const unnormalizedPath = path.join(tmpDir, "a", "..", "b", ".", "file.txt")
const createdDirs = await createDirectoriesForFile(unnormalizedPath)
// Should create only the necessary directory

View file

@ -8,9 +8,7 @@ import * as path from "path"
* @param filePath - The full path to a file.
* @returns A promise that resolves to an array of newly created directories.
*/
export async function createDirectoriesForFile(
filePath: string,
): Promise<string[]> {
export async function createDirectoriesForFile(filePath: string): Promise<string[]> {
const newDirectories: string[] = []
const normalizedFilePath = path.normalize(filePath) // Normalize path for cross-platform compatibility
const directoryPath = path.dirname(normalizedFilePath)

View file

@ -30,9 +30,7 @@ describe("Path Utilities", () => {
it("should handle desktop path", () => {
const desktop = path.join(os.homedir(), "Desktop")
const testPath = path.join(desktop, "test.txt")
getReadablePath(desktop, "test.txt").should.equal(
testPath.replace(/\\/g, "/"),
)
getReadablePath(desktop, "test.txt").should.equal(testPath.replace(/\\/g, "/"))
})
it("should show relative paths within cwd", () => {

View file

@ -72,10 +72,7 @@ function normalizePath(p: string): string {
let normalized = path.normalize(p)
// however it doesn't remove trailing slashes
// remove trailing slash, except for root paths
if (
normalized.length > 1 &&
(normalized.endsWith("/") || normalized.endsWith("\\"))
) {
if (normalized.length > 1 && (normalized.endsWith("/") || normalized.endsWith("\\"))) {
normalized = normalized.slice(0, -1)
}
return normalized

View file

@ -5,9 +5,7 @@
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app" />
<meta name="description" content="Web site created using create-react-app" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/

View file

@ -5,16 +5,12 @@ import ChatView from "./components/chat/ChatView"
import HistoryView from "./components/history/HistoryView"
import SettingsView from "./components/settings/SettingsView"
import WelcomeView from "./components/welcome/WelcomeView"
import {
ExtensionStateContextProvider,
useExtensionState,
} from "./context/ExtensionStateContext"
import { ExtensionStateContextProvider, useExtensionState } from "./context/ExtensionStateContext"
import { vscode } from "./utils/vscode"
import McpView from "./components/mcp/McpView"
const AppContent = () => {
const { didHydrateState, showWelcome, shouldShowAnnouncement } =
useExtensionState()
const { didHydrateState, showWelcome, shouldShowAnnouncement } = useExtensionState()
const [showSettings, setShowSettings] = useState(false)
const [showHistory, setShowHistory] = useState(false)
const [showMcp, setShowMcp] = useState(false)
@ -69,12 +65,8 @@ const AppContent = () => {
<WelcomeView />
) : (
<>
{showSettings && (
<SettingsView onDone={() => setShowSettings(false)} />
)}
{showHistory && (
<HistoryView onDone={() => setShowHistory(false)} />
)}
{showSettings && <SettingsView onDone={() => setShowSettings(false)} />}
{showHistory && <HistoryView onDone={() => setShowHistory(false)} />}
{showMcp && <McpView onDone={() => setShowMcp(false)} />}
{/* Do not conditionally load ChatView, it's expensive and there's state we don't want to lose (user input, disableInput, askResponse promise, etc.) */}
<ChatView

View file

@ -16,18 +16,14 @@ const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
return (
<div
style={{
backgroundColor:
"var(--vscode-editor-inactiveSelectionBackground)",
backgroundColor: "var(--vscode-editor-inactiveSelectionBackground)",
borderRadius: "3px",
padding: "12px 16px",
margin: "5px 15px 5px 15px",
position: "relative",
flexShrink: 0,
}}>
<VSCodeButton
appearance="icon"
onClick={hideAnnouncement}
style={{ position: "absolute", top: "8px", right: "8px" }}>
<VSCodeButton appearance="icon" onClick={hideAnnouncement} style={{ position: "absolute", top: "8px", right: "8px" }}>
<span className="codicon codicon-close"></span>
</VSCodeButton>
<h3 style={{ margin: "0 0 8px" }}>
@ -35,9 +31,8 @@ const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
</h3>
<ul style={{ margin: "0 0 8px", paddingLeft: "12px" }}>
<li>
<b>Checkpoints are here!</b> Cline now saves a snapshot of
your workspace at each step of the task. Hover over any
message to see two new buttons:
<b>Checkpoints are here!</b> Cline now saves a snapshot of your workspace at each step of the task. Hover over
any message to see two new buttons:
<ul style={{ margin: "4px 0", paddingLeft: 22 }}>
<li>
<span
@ -46,8 +41,7 @@ const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
fontSize: "12px",
marginRight: "4px",
}}></span>
<b>Compare</b> shows you a diff between the snapshot
and your current workspace
<b>Compare</b> shows you a diff between the snapshot and your current workspace
</li>
<li>
<span
@ -56,21 +50,17 @@ const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
fontSize: "12px",
marginRight: "4px",
}}></span>
<b>Restore</b> lets you revert your project's files
back to that point in the task
<b>Restore</b> lets you revert your project's files back to that point in the task
</li>
</ul>
</li>
<li>
<b>'See new changes' button</b> when a task is completed,
showing you an overview of all the changes Cline made to
your workspace throughout the task
<b>'See new changes' button</b> when a task is completed, showing you an overview of all the changes Cline
made to your workspace throughout the task
</li>
</ul>
<p style={{ margin: "8px 0" }}>
<VSCodeLink
href="https://x.com/sdrzn/status/1867271665086074969"
style={{ display: "inline" }}>
<VSCodeLink href="https://x.com/sdrzn/status/1867271665086074969" style={{ display: "inline" }}>
See a demo of Checkpoints here!
</VSCodeLink>
</p>
@ -131,9 +121,7 @@ const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
/>
<p style={{ margin: "0" }}>
Join
<VSCodeLink
style={{ display: "inline" }}
href="https://discord.gg/cline">
<VSCodeLink style={{ display: "inline" }} href="https://discord.gg/cline">
discord.gg/cline
</VSCodeLink>
for more updates!

View file

@ -1,7 +1,4 @@
import {
VSCodeCheckbox,
VSCodeTextField,
} from "@vscode/webview-ui-toolkit/react"
import { VSCodeCheckbox, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import { useCallback, useState } from "react"
import styled from "styled-components"
import { useExtensionState } from "../../context/ExtensionStateContext"
@ -41,32 +38,25 @@ const ACTION_METADATA: {
id: "useBrowser",
label: "Use the browser",
shortName: "Browser",
description:
"Allows ability to launch and interact with any website in a headless browser.",
description: "Allows ability to launch and interact with any website in a headless browser.",
},
{
id: "useMcp",
label: "Use MCP servers",
shortName: "MCP",
description:
"Allows use of configured MCP servers which may modify filesystem or interact with APIs.",
description: "Allows use of configured MCP servers which may modify filesystem or interact with APIs.",
},
]
const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
const { autoApprovalSettings } = useExtensionState()
const [isExpanded, setIsExpanded] = useState(false)
const [isHoveringCollapsibleSection, setIsHoveringCollapsibleSection] =
useState(false)
const [isHoveringCollapsibleSection, setIsHoveringCollapsibleSection] = useState(false)
// Careful not to use partials to mutate since spread operator only does shallow copy
const enabledActions = ACTION_METADATA.filter(
(action) => autoApprovalSettings.actions[action.id],
)
const enabledActionsList = enabledActions
.map((action) => action.shortName)
.join(", ")
const enabledActions = ACTION_METADATA.filter((action) => autoApprovalSettings.actions[action.id])
const enabledActionsList = enabledActions.map((action) => action.shortName).join(", ")
const hasEnabledActions = enabledActions.length > 0
const updateEnabled = useCallback(
@ -91,8 +81,7 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
}
// Check if this will result in any enabled actions
const willHaveEnabledActions =
Object.values(newActions).some(Boolean)
const willHaveEnabledActions = Object.values(newActions).some(Boolean)
vscode.postMessage({
type: "autoApprovalSettings",
@ -100,9 +89,7 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
...autoApprovalSettings,
actions: newActions,
// If no actions will be enabled, ensure the main toggle is off
enabled: willHaveEnabledActions
? autoApprovalSettings.enabled
: false,
enabled: willHaveEnabledActions ? autoApprovalSettings.enabled : false,
},
})
},
@ -210,9 +197,7 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
overflow: "hidden",
textOverflow: "ellipsis",
}}>
{enabledActions.length === 0
? "None"
: enabledActionsList}
{enabledActions.length === 0 ? "None" : enabledActionsList}
</span>
<span
className={`codicon codicon-chevron-${isExpanded ? "down" : "right"}`}
@ -231,20 +216,15 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
color: "var(--vscode-descriptionForeground)",
fontSize: "12px",
}}>
Auto-approve allows Cline to perform the following
actions without asking for permission. Please use with
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.
</div>
{ACTION_METADATA.map((action) => (
<div key={action.id} style={{ margin: "6px 0" }}>
<VSCodeCheckbox
checked={
autoApprovalSettings.actions[action.id]
}
checked={autoApprovalSettings.actions[action.id]}
onChange={(e) => {
const checked = (
e.target as HTMLInputElement
).checked
const checked = (e.target as HTMLInputElement).checked
updateAction(action.id, checked)
}}>
{action.label}
@ -262,8 +242,7 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
<div
style={{
height: "0.5px",
background:
"var(--vscode-titleBar-inactiveForeground)",
background: "var(--vscode-titleBar-inactiveForeground)",
margin: "15px 0",
opacity: 0.2,
}}
@ -277,9 +256,7 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
marginBottom: "8px",
color: "var(--vscode-foreground)",
}}>
<span style={{ flexShrink: 1, minWidth: 0 }}>
Max Requests:
</span>
<span style={{ flexShrink: 1, minWidth: 0 }}>Max Requests:</span>
<VSCodeTextField
// placeholder={DEFAULT_AUTO_APPROVAL_SETTINGS.maxRequests.toString()}
value={autoApprovalSettings.maxRequests.toString()}
@ -294,15 +271,7 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
}}
onKeyDown={(e) => {
// Prevent non-numeric keys (except for backspace, delete, arrows)
if (
!/^\d$/.test(e.key) &&
![
"Backspace",
"Delete",
"ArrowLeft",
"ArrowRight",
].includes(e.key)
) {
if (!/^\d$/.test(e.key) && !["Backspace", "Delete", "ArrowLeft", "ArrowRight"].includes(e.key)) {
e.preventDefault()
}
}}
@ -315,15 +284,13 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
fontSize: "12px",
marginBottom: "10px",
}}>
Cline will automatically make this many API requests
before asking for approval to proceed with the task.
Cline will automatically make this many API requests before asking for approval to proceed with the task.
</div>
<div style={{ margin: "6px 0" }}>
<VSCodeCheckbox
checked={autoApprovalSettings.enableNotifications}
onChange={(e) => {
const checked = (e.target as HTMLInputElement)
.checked
const checked = (e.target as HTMLInputElement).checked
updateNotifications(checked)
}}>
Enable Notifications
@ -334,8 +301,7 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
color: "var(--vscode-descriptionForeground)",
fontSize: "12px",
}}>
Receive system notifications when Cline requires
approval to proceed or when a task is completed.
Receive system notifications when Cline requires approval to proceed or when a task is completed.
</div>
</div>
</div>
@ -348,10 +314,7 @@ const CollapsibleSection = styled.div<{ isHovered?: boolean }>`
display: flex;
align-items: center;
gap: 4px;
color: ${(props) =>
props.isHovered
? "var(--vscode-foreground)"
: "var(--vscode-descriptionForeground)"};
color: ${(props) => (props.isHovered ? "var(--vscode-foreground)" : "var(--vscode-descriptionForeground)")};
flex: 1;
min-width: 0;

View file

@ -1,21 +1,13 @@
import deepEqual from "fast-deep-equal"
import React, { memo, useEffect, useMemo, useRef, useState } from "react"
import { useSize } from "react-use"
import {
BrowserAction,
BrowserActionResult,
ClineMessage,
ClineSayBrowserAction,
} from "../../../../src/shared/ExtensionMessage"
import { BrowserAction, BrowserActionResult, ClineMessage, ClineSayBrowserAction } from "../../../../src/shared/ExtensionMessage"
import { vscode } from "../../utils/vscode"
import CodeBlock, { CODE_BLOCK_BG_COLOR } from "../common/CodeBlock"
import { ChatRowContent, ProgressIndicator } from "./ChatRow"
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import styled from "styled-components"
import {
CheckpointControls,
CheckpointOverlay,
} from "../common/CheckpointControls"
import { CheckpointControls, CheckpointOverlay } from "../common/CheckpointControls"
import { findLast } from "../../../../src/shared/array"
interface BrowserSessionRowProps {
@ -35,17 +27,14 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
const isLastApiReqInterrupted = useMemo(() => {
// Check if last api_req_started is cancelled
const lastApiReqStarted = [...messages]
.reverse()
.find((m) => m.say === "api_req_started")
const lastApiReqStarted = [...messages].reverse().find((m) => m.say === "api_req_started")
if (lastApiReqStarted?.text != null) {
const info = JSON.parse(lastApiReqStarted.text)
if (info.cancelReason != null) {
return true
}
}
const lastApiReqFailed =
isLast && lastModifiedMessage?.ask === "api_req_failed"
const lastApiReqFailed = isLast && lastModifiedMessage?.ask === "api_req_failed"
if (lastApiReqFailed) {
return true
}
@ -53,11 +42,7 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
}, [messages, lastModifiedMessage, isLast])
const isBrowsing = useMemo(() => {
return (
isLast &&
messages.some((m) => m.say === "browser_action_result") &&
!isLastApiReqInterrupted
) // after user approves, browser_action_result with "" is sent to indicate that the session has started
return isLast && messages.some((m) => m.say === "browser_action_result") && !isLastApiReqInterrupted // after user approves, browser_action_result with "" is sent to indicate that the session has started
}, [isLast, messages, isLastApiReqInterrupted])
// Organize messages into pages with current state and next action
@ -79,10 +64,7 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
let nextActionMessages: ClineMessage[] = []
messages.forEach((message) => {
if (
message.ask === "browser_action_launch" ||
message.say === "browser_action_launch"
) {
if (message.ask === "browser_action_launch" || message.say === "browser_action_launch") {
// Start first page
currentStateMessages = [message]
} else if (message.say === "browser_action_result") {
@ -92,9 +74,7 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
}
// Complete current state
currentStateMessages.push(message)
const resultData = JSON.parse(
message.text || "{}",
) as BrowserActionResult
const resultData = JSON.parse(message.text || "{}") as BrowserActionResult
// Add page with current state and previous next actions
result.push({
@ -116,11 +96,7 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
// Reset for next page
currentStateMessages = []
nextActionMessages = []
} else if (
message.say === "api_req_started" ||
message.say === "text" ||
message.say === "browser_action"
) {
} else if (message.say === "api_req_started" || message.say === "text" || message.say === "browser_action") {
// These messages lead to the next result, so they should always go in nextActionMessages
nextActionMessages.push(message)
} else {
@ -155,28 +131,17 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
// Get initial URL from launch message
const initialUrl = useMemo(() => {
const launchMessage = messages.find(
(m) =>
m.ask === "browser_action_launch" ||
m.say === "browser_action_launch",
)
const launchMessage = messages.find((m) => m.ask === "browser_action_launch" || m.say === "browser_action_launch")
return launchMessage?.text || ""
}, [messages])
const isAutoApproved = useMemo(() => {
const launchMessage = messages.find(
(m) =>
m.ask === "browser_action_launch" ||
m.say === "browser_action_launch",
)
const launchMessage = messages.find((m) => m.ask === "browser_action_launch" || m.say === "browser_action_launch")
return launchMessage?.say === "browser_action_launch"
}, [messages])
const lastCheckpointMessageTs = useMemo(() => {
const lastCheckpointMessage = findLast(
messages,
(m) => m.lastCheckpointHash !== undefined,
)
const lastCheckpointMessage = findLast(messages, (m) => m.lastCheckpointHash !== undefined)
return lastCheckpointMessage?.ts
}, [messages])
@ -207,23 +172,14 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
// Use latest state if we're on the last page and don't have a state yet
const displayState = isLastPage
? {
url:
currentPage?.currentState.url ||
latestState.url ||
initialUrl,
mousePosition:
currentPage?.currentState.mousePosition ||
latestState.mousePosition ||
"700,400",
url: currentPage?.currentState.url || latestState.url || initialUrl,
mousePosition: currentPage?.currentState.mousePosition || latestState.mousePosition || "700,400",
consoleLogs: currentPage?.currentState.consoleLogs,
screenshot:
currentPage?.currentState.screenshot ||
latestState.screenshot,
screenshot: currentPage?.currentState.screenshot || latestState.screenshot,
}
: {
url: currentPage?.currentState.url || initialUrl,
mousePosition:
currentPage?.currentState.mousePosition || "700,400",
mousePosition: currentPage?.currentState.mousePosition || "700,400",
consoleLogs: currentPage?.currentState.consoleLogs,
screenshot: currentPage?.currentState.screenshot,
}
@ -231,18 +187,11 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
const [actionContent, { height: actionHeight }] = useSize(
<div>
{currentPage?.nextAction?.messages.map((message) => (
<BrowserSessionRowContent
key={message.ts}
{...props}
message={message}
setMaxActionHeight={setMaxActionHeight}
/>
<BrowserSessionRowContent key={message.ts} {...props} message={message} setMaxActionHeight={setMaxActionHeight} />
))}
{!isBrowsing &&
messages.some((m) => m.say === "browser_action_result") &&
currentPageIndex === 0 && (
<BrowserActionBox action={"launch"} text={initialUrl} />
)}
{!isBrowsing && messages.some((m) => m.say === "browser_action_result") && currentPageIndex === 0 && (
<BrowserActionBox action={"launch"} text={initialUrl} />
)}
</div>,
)
@ -264,13 +213,8 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
for (let i = actions.length - 1; i >= 0; i--) {
const message = actions[i]
if (message.say === "browser_action") {
const browserAction = JSON.parse(
message.text || "{}",
) as ClineSayBrowserAction
if (
browserAction.action === "click" &&
browserAction.coordinate
) {
const browserAction = JSON.parse(message.text || "{}") as ClineSayBrowserAction
if (browserAction.action === "click" && browserAction.coordinate) {
return browserAction.coordinate
}
}
@ -279,15 +223,11 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
}, [isBrowsing, currentPage?.nextAction?.messages])
// Use latest click position while browsing, otherwise use display state
const mousePosition = isBrowsing
? latestClickPosition || displayState.mousePosition
: displayState.mousePosition
const mousePosition = isBrowsing ? latestClickPosition || displayState.mousePosition : displayState.mousePosition
let shouldShowCheckpoints = true
if (isLast) {
shouldShowCheckpoints =
lastModifiedMessage?.ask === "resume_completed_task" ||
lastModifiedMessage?.ask === "resume_task"
shouldShowCheckpoints = lastModifiedMessage?.ask === "resume_completed_task" || lastModifiedMessage?.ask === "resume_task"
}
const [browserSessionRow, { height }] = useSize(
@ -310,11 +250,7 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
}}></span>
)}
<span style={{ fontWeight: "bold" }}>
<>
{isAutoApproved
? "Cline is using the browser:"
: "Cline wants to use the browser:"}
</>
<>{isAutoApproved ? "Cline is using the browser:" : "Cline wants to use the browser:"}</>
</span>
</div>
<div
@ -338,9 +274,7 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
display: "flex",
alignItems: "center",
justifyContent: "center",
color: displayState.url
? "var(--vscode-input-foreground)"
: "var(--vscode-descriptionForeground)",
color: displayState.url ? "var(--vscode-input-foreground)" : "var(--vscode-descriptionForeground)",
fontSize: "12px",
}}>
<div
@ -406,8 +340,7 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
position: "absolute",
top: `${(parseInt(mousePosition.split(",")[1]) / 600) * 100}%`,
left: `${(parseInt(mousePosition.split(",")[0]) / 900) * 100}%`,
transition:
"top 0.3s ease-out, left 0.3s ease-out",
transition: "top 0.3s ease-out, left 0.3s ease-out",
}}
/>
)}
@ -427,14 +360,11 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
cursor: "pointer",
padding: `9px 8px ${consoleLogsExpanded ? 0 : 8}px 8px`,
}}>
<span
className={`codicon codicon-chevron-${consoleLogsExpanded ? "down" : "right"}`}></span>
<span className={`codicon codicon-chevron-${consoleLogsExpanded ? "down" : "right"}`}></span>
<span style={{ fontSize: "0.8em" }}>Console Logs</span>
</div>
{consoleLogsExpanded && (
<CodeBlock
source={`${"```"}shell\n${displayState.consoleLogs || "(No new logs)"}\n${"```"}`}
/>
<CodeBlock source={`${"```"}shell\n${displayState.consoleLogs || "(No new logs)"}\n${"```"}`} />
)}
</div>
</div>
@ -463,10 +393,7 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
Previous
</VSCodeButton>
<VSCodeButton
disabled={
currentPageIndex === pages.length - 1 ||
isBrowsing
}
disabled={currentPageIndex === pages.length - 1 || isBrowsing}
onClick={() => setCurrentPageIndex((i) => i + 1)}>
Next
</VSCodeButton>
@ -474,21 +401,14 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
</div>
)}
{shouldShowCheckpoints && (
<CheckpointOverlay messageTs={lastCheckpointMessageTs} />
)}
{shouldShowCheckpoints && <CheckpointOverlay messageTs={lastCheckpointMessageTs} />}
</BrowserSessionRowContainer>,
)
// Height change effect
useEffect(() => {
const isInitialRender = prevHeightRef.current === 0
if (
isLast &&
height !== 0 &&
height !== Infinity &&
height !== prevHeightRef.current
) {
if (isLast && height !== 0 && height !== Infinity && height !== prevHeightRef.current) {
if (!isInitialRender) {
onHeightChange(height > prevHeightRef.current)
}
@ -499,8 +419,7 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
return browserSessionRow
}, deepEqual)
interface BrowserSessionRowContentProps
extends Omit<BrowserSessionRowProps, "messages"> {
interface BrowserSessionRowContentProps extends Omit<BrowserSessionRowProps, "messages"> {
message: ClineMessage
setMaxActionHeight: (height: number) => void
}
@ -520,16 +439,11 @@ const BrowserSessionRowContent = ({
marginBottom: "10px",
}
if (
message.ask === "browser_action_launch" ||
message.say === "browser_action_launch"
) {
if (message.ask === "browser_action_launch" || message.say === "browser_action_launch") {
return (
<>
<div style={headerStyle}>
<span style={{ fontWeight: "bold" }}>
Browser Session Started
</span>
<span style={{ fontWeight: "bold" }}>Browser Session Started</span>
</div>
<div
style={{
@ -538,10 +452,7 @@ const BrowserSessionRowContent = ({
overflow: "hidden",
backgroundColor: CODE_BLOCK_BG_COLOR,
}}>
<CodeBlock
source={`${"```"}shell\n${message.text}\n${"```"}`}
forceWrap={true}
/>
<CodeBlock source={`${"```"}shell\n${message.text}\n${"```"}`} forceWrap={true} />
</div>
</>
)
@ -570,9 +481,7 @@ const BrowserSessionRowContent = ({
)
case "browser_action":
const browserAction = JSON.parse(
message.text || "{}",
) as ClineSayBrowserAction
const browserAction = JSON.parse(message.text || "{}") as ClineSayBrowserAction
return (
<BrowserActionBox
action={browserAction.action}
@ -593,20 +502,8 @@ const BrowserSessionRowContent = ({
}
}
const BrowserActionBox = ({
action,
coordinate,
text,
}: {
action: BrowserAction
coordinate?: string
text?: string
}) => {
const getBrowserActionText = (
action: BrowserAction,
coordinate?: string,
text?: string,
) => {
const BrowserActionBox = ({ action, coordinate, text }: { action: BrowserAction; coordinate?: string; text?: string }) => {
const getBrowserActionText = (action: BrowserAction, coordinate?: string, text?: string) => {
switch (action) {
case "launch":
return `Launch browser at ${text}`
@ -653,9 +550,7 @@ const BrowserActionBox = ({
)
}
const BrowserCursor: React.FC<{ style?: React.CSSProperties }> = ({
style,
}) => {
const BrowserCursor: React.FC<{ style?: React.CSSProperties }> = ({ style }) => {
// (can't use svgs in vsc extensions)
const cursorBase64 =
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABUAAAAYCAYAAAAVibZIAAAAAXNSR0IArs4c6QAAAERlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAFaADAAQAAAABAAAAGAAAAADwi9a/AAADGElEQVQ4EZ2VbUiTURTH772be/PxZdsz3cZwC4RVaB8SAjMpxQwSWZbQG/TFkN7oW1Df+h6IRV9C+hCpKUSIZUXOfGM5tAKViijFFEyfZ7Ol29S1Pbdzl8Uw9+aBu91zzv3/nt17zt2DEZjBYOAkKrtFMXIghAWM8U2vMN/FctsxGRMpM7NbEEYNMM2CYUSInlJx3OpawO9i+XSNQYkmk2uFb9njzkcfVSr1p/GJiQKMULVaw2WuBv296UKRxWJR6wxGCmM1EAhSNppv33GBH9qI32cPTAtss9lUm6EM3N7R+RbigT+5/CeosFCZKpjEW+iorS1pb30wDUXzQfHqtD/9L3ieZ2ee1OJCmbL8QHnRs+4uj0wmW4QzrpCwvJ8zGg3JqAmhTLynuLiwv8/5KyND8Q3cEkUEDWu15oJE4KRQJt5hs1rcriGNRqP+DK4dyyWXXm/aFQ+cEpSJ8/LyDGPuEZNOmzsOroUSOqzXG/dtBU4ZysTZYKNut91sNo2Cq6cE9enz86s2g9OCMrFSqVC5hgb32u072W3jKMU90Hb1seC0oUwsB+t92bO/rKx0EFGkgFCnjjc1/gVvC8rE0L+4o63t4InjxwbAJQjTe3qD8QrLkXA4DC24fWtuajp06cLFYSBIFKGmXKPRRmAnME9sPt+yLwIWb9WN69fKoTneQz4Dh2mpPNkvfeV0jjecb9wNAkwIEVQq5VJOds4Kb+DXoAsiVquVwI1Dougpij6UyGYx+5cKroeDEFibm5lWRRMbH1+npmYrq6qhwlQHIbajZEf1fElcqGGFpGg9HMuKzpfBjhytCTMgkJ56RX09zy/ysENTBElmjIgJnmNChJqohDVQqpEfwkILE8v/o0GAnV9F1eEvofVQCbiTBEXOIPQh5PGgefDZeAcjrpGZjULBr/m3tZOnz7oEQWRAQZLjWlEU/XEJWySiILgRc5Cz1DkcAyuBFcnpfF0JiXWKpcolQXizhS5hKAqFpr0MVbgbuxJ6+5xX+P4wNpbqPPrugZfbmIbLmgQR3Aw8QSi66hUXulOFbF73GxqjE5BNXWNeAAAAAElFTkSuQmCC"

View file

@ -1,16 +1,6 @@
import {
VSCodeBadge,
VSCodeProgressRing,
} from "@vscode/webview-ui-toolkit/react"
import { VSCodeBadge, VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react"
import deepEqual from "fast-deep-equal"
import React, {
memo,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react"
import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"
import { useEvent, useSize } from "react-use"
import styled from "styled-components"
import {
@ -21,20 +11,12 @@ import {
ExtensionMessage,
COMPLETION_RESULT_CHANGES_FLAG,
} from "../../../../src/shared/ExtensionMessage"
import {
COMMAND_OUTPUT_STRING,
COMMAND_REQ_APP_STRING,
} from "../../../../src/shared/combineCommandSequences"
import { COMMAND_OUTPUT_STRING, COMMAND_REQ_APP_STRING } from "../../../../src/shared/combineCommandSequences"
import { useExtensionState } from "../../context/ExtensionStateContext"
import { findMatchingResourceOrTemplate } from "../../utils/mcp"
import { vscode } from "../../utils/vscode"
import {
CheckpointControls,
CheckpointOverlay,
} from "../common/CheckpointControls"
import CodeAccordian, {
removeLeadingNonAlphanumeric,
} from "../common/CodeAccordian"
import { CheckpointControls, CheckpointOverlay } from "../common/CheckpointControls"
import CodeAccordian, { removeLeadingNonAlphanumeric } from "../common/CodeAccordian"
import CodeBlock, { CODE_BLOCK_BG_COLOR } from "../common/CodeBlock"
import MarkdownBlock from "../common/MarkdownBlock"
import SuccessButton from "../common/SuccessButton"
@ -84,16 +66,13 @@ const ChatRow = memo(
if (shouldShowCheckpoints && isLast) {
shouldShowCheckpoints =
lastModifiedMessage?.ask === "resume_completed_task" ||
lastModifiedMessage?.ask === "resume_task"
lastModifiedMessage?.ask === "resume_completed_task" || lastModifiedMessage?.ask === "resume_task"
}
const [chatrow, { height }] = useSize(
<ChatRowContainer>
<ChatRowContent {...props} />
{shouldShowCheckpoints && (
<CheckpointOverlay messageTs={message.ts} />
)}
{shouldShowCheckpoints && <CheckpointOverlay messageTs={message.ts} />}
</ChatRowContainer>,
)
@ -102,12 +81,7 @@ const ChatRow = memo(
// NOTE: it's important we don't distinguish between partial or complete here since our scroll effects in chatview need to handle height change during partial -> complete
const isInitialRender = prevHeightRef.current === 0 // prevents scrolling when new element is added since we already scroll for that
// height starts off at Infinity
if (
isLast &&
height !== 0 &&
height !== Infinity &&
height !== prevHeightRef.current
) {
if (isLast && height !== 0 && height !== Infinity && height !== prevHeightRef.current) {
if (!isInitialRender) {
onHeightChange(height > prevHeightRef.current)
}
@ -124,29 +98,18 @@ const ChatRow = memo(
export default ChatRow
export const ChatRowContent = ({
message,
isExpanded,
onToggleExpand,
lastModifiedMessage,
isLast,
}: ChatRowContentProps) => {
export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifiedMessage, isLast }: ChatRowContentProps) => {
const { mcpServers } = useExtensionState()
const [seeNewChangesDisabled, setSeeNewChangesDisabled] = useState(false)
const [cost, apiReqCancelReason, apiReqStreamingFailedMessage] =
useMemo(() => {
if (message.text != null && message.say === "api_req_started") {
const info: ClineApiReqInfo = JSON.parse(message.text)
return [
info.cost,
info.cancelReason,
info.streamingFailedMessage,
]
}
return [undefined, undefined, undefined]
}, [message.text, message.say])
const [cost, apiReqCancelReason, apiReqStreamingFailedMessage] = useMemo(() => {
if (message.text != null && message.say === "api_req_started") {
const info: ClineApiReqInfo = JSON.parse(message.text)
return [info.cost, info.cancelReason, info.streamingFailedMessage]
}
return [undefined, undefined, undefined]
}, [message.text, message.say])
// when resuming task, last wont be api_req_failed but a resume_task message, so api_req_started will show loading spinner. that's why we just remove the last api_req_started that failed without streaming anything
const apiRequestFailedMessage =
isLast && lastModifiedMessage?.ask === "api_req_failed" // if request is retried then the latest message is a api_req_retried
@ -154,12 +117,10 @@ export const ChatRowContent = ({
: undefined
const isCommandExecuting =
isLast &&
(lastModifiedMessage?.ask === "command" ||
lastModifiedMessage?.say === "command") &&
(lastModifiedMessage?.ask === "command" || lastModifiedMessage?.say === "command") &&
lastModifiedMessage?.text?.includes(COMMAND_OUTPUT_STRING)
const isMcpServerResponding =
isLast && lastModifiedMessage?.say === "mcp_server_request_started"
const isMcpServerResponding = isLast && lastModifiedMessage?.say === "mcp_server_request_started"
const type = message.type === "ask" ? message.ask : message.say
@ -190,9 +151,7 @@ export const ChatRowContent = ({
color: errorColor,
marginBottom: "-1.5px",
}}></span>,
<span style={{ color: errorColor, fontWeight: "bold" }}>
Error
</span>,
<span style={{ color: errorColor, fontWeight: "bold" }}>Error</span>,
]
case "mistake_limit_reached":
return [
@ -202,9 +161,7 @@ export const ChatRowContent = ({
color: errorColor,
marginBottom: "-1.5px",
}}></span>,
<span style={{ color: errorColor, fontWeight: "bold" }}>
Cline is having trouble...
</span>,
<span style={{ color: errorColor, fontWeight: "bold" }}>Cline is having trouble...</span>,
]
case "auto_approval_max_req_reached":
return [
@ -214,9 +171,7 @@ export const ChatRowContent = ({
color: errorColor,
marginBottom: "-1.5px",
}}></span>,
<span style={{ color: errorColor, fontWeight: "bold" }}>
Maximum Requests Reached
</span>,
<span style={{ color: errorColor, fontWeight: "bold" }}>Maximum Requests Reached</span>,
]
case "command":
return [
@ -231,15 +186,11 @@ export const ChatRowContent = ({
}}></span>
),
<span style={{ color: normalColor, fontWeight: "bold" }}>
{message.type === "ask"
? "Cline wants to execute this command:"
: "Cline executed this command:"}
{message.type === "ask" ? "Cline wants to execute this command:" : "Cline executed this command:"}
</span>,
]
case "use_mcp_server":
const mcpServerUse = JSON.parse(
message.text || "{}",
) as ClineAskUseMcpServer
const mcpServerUse = JSON.parse(message.text || "{}") as ClineAskUseMcpServer
return [
isMcpServerResponding ? (
<ProgressIndicator />
@ -254,21 +205,13 @@ export const ChatRowContent = ({
<span style={{ color: normalColor, fontWeight: "bold" }}>
{message.type === "ask" ? (
<>
Cline wants to{" "}
{mcpServerUse.type === "use_mcp_tool"
? "use a tool"
: "access a resource"}{" "}
on the <code>{mcpServerUse.serverName}</code>{" "}
MCP server:
Cline wants to {mcpServerUse.type === "use_mcp_tool" ? "use a tool" : "access a resource"} on the{" "}
<code>{mcpServerUse.serverName}</code> MCP server:
</>
) : (
<>
Cline{" "}
{mcpServerUse.type === "use_mcp_tool"
? "used a tool"
: "accessed a resource"}{" "}
on the <code>{mcpServerUse.serverName}</code>{" "}
MCP server:
Cline {mcpServerUse.type === "use_mcp_tool" ? "used a tool" : "accessed a resource"} on the{" "}
<code>{mcpServerUse.serverName}</code> MCP server:
</>
)}
</span>,
@ -281,9 +224,7 @@ export const ChatRowContent = ({
color: successColor,
marginBottom: "-1.5px",
}}></span>,
<span style={{ color: successColor, fontWeight: "bold" }}>
Task Completed
</span>,
<span style={{ color: successColor, fontWeight: "bold" }}>Task Completed</span>,
]
case "api_req_started":
const getIconSpan = (iconName: string, color: string) => (
@ -337,19 +278,11 @@ export const ChatRowContent = ({
</span>
)
) : cost != null ? (
<span
style={{ color: normalColor, fontWeight: "bold" }}>
API Request
</span>
<span style={{ color: normalColor, fontWeight: "bold" }}>API Request</span>
) : apiRequestFailedMessage ? (
<span style={{ color: errorColor, fontWeight: "bold" }}>
API Request Failed
</span>
<span style={{ color: errorColor, fontWeight: "bold" }}>API Request Failed</span>
) : (
<span
style={{ color: normalColor, fontWeight: "bold" }}>
API Request...
</span>
<span style={{ color: normalColor, fontWeight: "bold" }}>API Request...</span>
),
]
case "followup":
@ -360,9 +293,7 @@ export const ChatRowContent = ({
color: normalColor,
marginBottom: "-1.5px",
}}></span>,
<span style={{ color: normalColor, fontWeight: "bold" }}>
Cline has a question:
</span>,
<span style={{ color: normalColor, fontWeight: "bold" }}>Cline has a question:</span>,
]
default:
return [null, null]
@ -416,9 +347,7 @@ export const ChatRowContent = ({
<div style={headerStyle}>
{toolIcon("edit")}
<span style={{ fontWeight: "bold" }}>
{message.type === "ask"
? "Cline wants to edit this file:"
: "Cline is editing this file:"}
{message.type === "ask" ? "Cline wants to edit this file:" : "Cline is editing this file:"}
</span>
</div>
<CodeAccordian
@ -436,9 +365,7 @@ export const ChatRowContent = ({
<div style={headerStyle}>
{toolIcon("new-file")}
<span style={{ fontWeight: "bold" }}>
{message.type === "ask"
? "Cline wants to create a new file:"
: "Cline is creating a new file:"}
{message.type === "ask" ? "Cline wants to create a new file:" : "Cline is creating a new file:"}
</span>
</div>
<CodeAccordian
@ -456,9 +383,7 @@ export const ChatRowContent = ({
<div style={headerStyle}>
{toolIcon("file-code")}
<span style={{ fontWeight: "bold" }}>
{message.type === "ask"
? "Cline wants to read this file:"
: "Cline read this file:"}
{message.type === "ask" ? "Cline wants to read this file:" : "Cline read this file:"}
</span>
</div>
{/* <CodeAccordian
@ -502,9 +427,7 @@ export const ChatRowContent = ({
direction: "rtl",
textAlign: "left",
}}>
{removeLeadingNonAlphanumeric(
tool.path ?? "",
) + "\u200E"}
{removeLeadingNonAlphanumeric(tool.path ?? "") + "\u200E"}
</span>
<div style={{ flexGrow: 1 }}></div>
<span
@ -584,25 +507,18 @@ export const ChatRowContent = ({
<span style={{ fontWeight: "bold" }}>
{message.type === "ask" ? (
<>
Cline wants to search this directory for{" "}
<code>{tool.regex}</code>:
Cline wants to search this directory for <code>{tool.regex}</code>:
</>
) : (
<>
Cline searched this directory for{" "}
<code>{tool.regex}</code>:
Cline searched this directory for <code>{tool.regex}</code>:
</>
)}
</span>
</div>
<CodeAccordian
code={tool.content!}
path={
tool.path! +
(tool.filePattern
? `/(${tool.filePattern})`
: "")
}
path={tool.path! + (tool.filePattern ? `/(${tool.filePattern})` : "")}
language="plaintext"
isExpanded={isExpanded}
onToggleExpand={onToggleExpand}
@ -673,9 +589,7 @@ export const ChatRowContent = ({
const { command: rawCommand, output } = splitMessage(message.text || "")
const requestsApproval = rawCommand.endsWith(COMMAND_REQ_APP_STRING)
const command = requestsApproval
? rawCommand.slice(0, -COMMAND_REQ_APP_STRING.length)
: rawCommand
const command = requestsApproval ? rawCommand.slice(0, -COMMAND_REQ_APP_STRING.length) : rawCommand
return (
<>
@ -694,10 +608,7 @@ export const ChatRowContent = ({
overflow: "hidden",
backgroundColor: CODE_BLOCK_BG_COLOR,
}}>
<CodeBlock
source={`${"```"}shell\n${command}\n${"```"}`}
forceWrap={true}
/>
<CodeBlock source={`${"```"}shell\n${command}\n${"```"}`} forceWrap={true} />
{output.length > 0 && (
<div style={{ width: "100%" }}>
<div
@ -711,17 +622,10 @@ export const ChatRowContent = ({
cursor: "pointer",
padding: `2px 8px ${isExpanded ? 0 : 8}px 8px`,
}}>
<span
className={`codicon codicon-chevron-${isExpanded ? "down" : "right"}`}></span>
<span style={{ fontSize: "0.8em" }}>
Command Output
</span>
<span className={`codicon codicon-chevron-${isExpanded ? "down" : "right"}`}></span>
<span style={{ fontSize: "0.8em" }}>Command Output</span>
</div>
{isExpanded && (
<CodeBlock
source={`${"```"}shell\n${output}\n${"```"}`}
/>
)}
{isExpanded && <CodeBlock source={`${"```"}shell\n${output}\n${"```"}`} />}
</div>
)}
</div>
@ -736,10 +640,7 @@ export const ChatRowContent = ({
color: "var(--vscode-errorForeground)",
}}>
<i className="codicon codicon-warning"></i>
<span>
The model has determined this command requires
explicit approval.
</span>
<span>The model has determined this command requires explicit approval.</span>
</div>
)}
</>
@ -747,12 +648,8 @@ export const ChatRowContent = ({
}
if (message.ask === "use_mcp_server" || message.say === "use_mcp_server") {
const useMcpServer = JSON.parse(
message.text || "{}",
) as ClineAskUseMcpServer
const server = mcpServers.find(
(server) => server.name === useMcpServer.serverName,
)
const useMcpServer = JSON.parse(message.text || "{}") as ClineAskUseMcpServer
const server = mcpServers.find((server) => server.name === useMcpServer.serverName)
return (
<>
<div style={headerStyle}>
@ -792,33 +689,28 @@ export const ChatRowContent = ({
tool={{
name: useMcpServer.toolName || "",
description:
server?.tools?.find(
(tool) =>
tool.name ===
useMcpServer.toolName,
)?.description || "",
server?.tools?.find((tool) => tool.name === useMcpServer.toolName)?.description || "",
}}
/>
{useMcpServer.arguments &&
useMcpServer.arguments !== "{}" && (
<div style={{ marginTop: "8px" }}>
<div
style={{
marginBottom: "4px",
opacity: 0.8,
fontSize: "12px",
textTransform: "uppercase",
}}>
Arguments
</div>
<CodeAccordian
code={useMcpServer.arguments}
language="json"
isExpanded={true}
onToggleExpand={onToggleExpand}
/>
{useMcpServer.arguments && useMcpServer.arguments !== "{}" && (
<div style={{ marginTop: "8px" }}>
<div
style={{
marginBottom: "4px",
opacity: 0.8,
fontSize: "12px",
textTransform: "uppercase",
}}>
Arguments
</div>
)}
<CodeAccordian
code={useMcpServer.arguments}
language="json"
isExpanded={true}
onToggleExpand={onToggleExpand}
/>
</div>
)}
</>
)}
</div>
@ -836,11 +728,7 @@ export const ChatRowContent = ({
style={{
...headerStyle,
marginBottom:
(cost == null &&
apiRequestFailedMessage) ||
apiReqStreamingFailedMessage
? 10
: 0,
(cost == null && apiRequestFailedMessage) || apiReqStreamingFailedMessage ? 10 : 0,
justifyContent: "space-between",
cursor: "pointer",
userSelect: "none",
@ -860,42 +748,31 @@ export const ChatRowContent = ({
{/* Need to render this everytime since it affects height of row by 2px */}
<VSCodeBadge
style={{
opacity:
cost != null && cost > 0
? 1
: 0,
opacity: cost != null && cost > 0 ? 1 : 0,
}}>
${Number(cost || 0)?.toFixed(4)}
</VSCodeBadge>
</div>
<span
className={`codicon codicon-chevron-${isExpanded ? "up" : "down"}`}></span>
<span className={`codicon codicon-chevron-${isExpanded ? "up" : "down"}`}></span>
</div>
{((cost == null && apiRequestFailedMessage) ||
apiReqStreamingFailedMessage) && (
{((cost == null && apiRequestFailedMessage) || apiReqStreamingFailedMessage) && (
<>
<p
style={{
...pStyle,
color: "var(--vscode-errorForeground)",
}}>
{apiRequestFailedMessage ||
apiReqStreamingFailedMessage}
{apiRequestFailedMessage
?.toLowerCase()
.includes("powershell") && (
{apiRequestFailedMessage || apiReqStreamingFailedMessage}
{apiRequestFailedMessage?.toLowerCase().includes("powershell") && (
<>
<br />
<br />
It seems like you're having
Windows PowerShell issues,
please see this{" "}
It seems like you're having Windows PowerShell issues, please see this{" "}
<a
href="https://github.com/cline/cline/wiki/TroubleShooting-%E2%80%90-%22PowerShell-is-not-recognized-as-an-internal-or-external-command%22"
style={{
color: "inherit",
textDecoration:
"underline",
textDecoration: "underline",
}}>
troubleshooting guide
</a>
@ -942,10 +819,7 @@ export const ChatRowContent = ({
{isExpanded && (
<div style={{ marginTop: "10px" }}>
<CodeAccordian
code={
JSON.parse(message.text || "{}")
.request
}
code={JSON.parse(message.text || "{}").request}
language="markdown"
isExpanded={true}
onToggleExpand={onToggleExpand}
@ -966,29 +840,21 @@ export const ChatRowContent = ({
return (
<div
style={{
backgroundColor:
"var(--vscode-badge-background)",
backgroundColor: "var(--vscode-badge-background)",
color: "var(--vscode-badge-foreground)",
borderRadius: "3px",
padding: "9px",
whiteSpace: "pre-line",
wordWrap: "break-word",
}}>
<span style={{ display: "block" }}>
{highlightMentions(message.text)}
</span>
<span style={{ display: "block" }}>{highlightMentions(message.text)}</span>
{message.images && message.images.length > 0 && (
<Thumbnails
images={message.images}
style={{ marginTop: "8px" }}
/>
<Thumbnails images={message.images} style={{ marginTop: "8px" }} />
)}
</div>
)
case "user_feedback_diff":
const tool = JSON.parse(
message.text || "{}",
) as ClineSayTool
const tool = JSON.parse(message.text || "{}") as ClineSayTool
return (
<div
style={{
@ -1055,24 +921,15 @@ export const ChatRowContent = ({
</span>
</div>
<div>
This usually happens when the model uses
search patterns that don't match anything in
the file. Retrying...
This usually happens when the model uses search patterns that don't match anything in the
file. Retrying...
</div>
</div>
</>
)
case "completion_result":
const hasChanges =
message.text?.endsWith(
COMPLETION_RESULT_CHANGES_FLAG,
) ?? false
const text = hasChanges
? message.text?.slice(
0,
-COMPLETION_RESULT_CHANGES_FLAG.length,
)
: message.text
const hasChanges = message.text?.endsWith(COMPLETION_RESULT_CHANGES_FLAG) ?? false
const text = hasChanges ? message.text?.slice(0, -COMPLETION_RESULT_CHANGES_FLAG.length) : message.text
return (
<>
<div
@ -1103,14 +960,9 @@ export const ChatRowContent = ({
}}
style={{
width: "100%",
cursor: seeNewChangesDisabled
? "wait"
: "pointer",
cursor: seeNewChangesDisabled ? "wait" : "pointer",
}}>
<i
className="codicon codicon-new-file"
style={{ marginRight: 6 }}
/>
<i className="codicon codicon-new-file" style={{ marginRight: 6 }} />
See new changes
</SuccessButton>
</div>
@ -1151,14 +1003,10 @@ export const ChatRowContent = ({
</span>
</div>
<div>
Cline won't be able to view the command's
output. Please update VSCode (
<code>CMD/CTRL + Shift + P</code>
"Update") and make sure you're using a
supported shell: zsh, bash, fish, or
PowerShell (
<code>CMD/CTRL + Shift + P</code>
"Terminal: Select Default Profile").{" "}
Cline won't be able to view the command's output. Please update VSCode (
<code>CMD/CTRL + Shift + P</code> "Update") and make sure you're using a supported shell:
zsh, bash, fish, or PowerShell (<code>CMD/CTRL + Shift + P</code> "Terminal: Select Default
Profile").{" "}
<a
href="https://github.com/cline/cline/wiki/Troubleshooting-%E2%80%90-Shell-Integration-Unavailable"
style={{
@ -1245,16 +1093,8 @@ export const ChatRowContent = ({
case "completion_result":
if (message.text) {
// FIXME: is this ever even used?
const hasChanges =
message.text.endsWith(
COMPLETION_RESULT_CHANGES_FLAG,
) ?? false
const text = hasChanges
? message.text.slice(
0,
-COMPLETION_RESULT_CHANGES_FLAG.length,
)
: message.text
const hasChanges = message.text.endsWith(COMPLETION_RESULT_CHANGES_FLAG) ?? false
const text = hasChanges ? message.text.slice(0, -COMPLETION_RESULT_CHANGES_FLAG.length) : message.text
return (
<div>
<div
@ -1277,9 +1117,7 @@ export const ChatRowContent = ({
appearance="secondary"
disabled={seeNewChangesDisabled}
onClick={() => {
setSeeNewChangesDisabled(
true,
)
setSeeNewChangesDisabled(true)
vscode.postMessage({
type: "taskCompletionViewChanges",
number: message.ts,
@ -1289,9 +1127,7 @@ export const ChatRowContent = ({
className="codicon codicon-new-file"
style={{
marginRight: 6,
cursor: seeNewChangesDisabled
? "wait"
: "pointer",
cursor: seeNewChangesDisabled ? "wait" : "pointer",
}}
/>
See new changes

View file

@ -1,17 +1,6 @@
import React, {
forwardRef,
useCallback,
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState,
} from "react"
import React, { forwardRef, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"
import DynamicTextArea from "react-textarea-autosize"
import {
mentionRegex,
mentionRegexGlobal,
} from "../../../../src/shared/context-mentions"
import { mentionRegex, mentionRegexGlobal } from "../../../../src/shared/context-mentions"
import { useExtensionState } from "../../context/ExtensionStateContext"
import {
ContextMenuOptionType,
@ -56,9 +45,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
const { filePaths } = useExtensionState()
const [isTextAreaFocused, setIsTextAreaFocused] = useState(false)
const [thumbnailsHeight, setThumbnailsHeight] = useState(0)
const [textAreaBaseHeight, setTextAreaBaseHeight] = useState<
number | undefined
>(undefined)
const [textAreaBaseHeight, setTextAreaBaseHeight] = useState<number | undefined>(undefined)
const [showContextMenu, setShowContextMenu] = useState(false)
const [cursorPosition, setCursorPosition] = useState(0)
const [searchQuery, setSearchQuery] = useState("")
@ -66,13 +53,9 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
const [isMouseDownOnMenu, setIsMouseDownOnMenu] = useState(false)
const highlightLayerRef = useRef<HTMLDivElement>(null)
const [selectedMenuIndex, setSelectedMenuIndex] = useState(-1)
const [selectedType, setSelectedType] =
useState<ContextMenuOptionType | null>(null)
const [justDeletedSpaceAfterMention, setJustDeletedSpaceAfterMention] =
useState(false)
const [intendedCursorPosition, setIntendedCursorPosition] = useState<
number | null
>(null)
const [selectedType, setSelectedType] = useState<ContextMenuOptionType | null>(null)
const [justDeletedSpaceAfterMention, setJustDeletedSpaceAfterMention] = useState(false)
const [intendedCursorPosition, setIntendedCursorPosition] = useState<number | null>(null)
const contextMenuContainerRef = useRef<HTMLDivElement>(null)
const queryItems = useMemo(() => {
@ -81,9 +64,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
...filePaths
.map((file) => "/" + file)
.map((path) => ({
type: path.endsWith("/")
? ContextMenuOptionType.Folder
: ContextMenuOptionType.File,
type: path.endsWith("/") ? ContextMenuOptionType.Folder : ContextMenuOptionType.File,
value: path,
})),
]
@ -91,12 +72,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
contextMenuContainerRef.current &&
!contextMenuContainerRef.current.contains(
event.target as Node,
)
) {
if (contextMenuContainerRef.current && !contextMenuContainerRef.current.contains(event.target as Node)) {
setShowContextMenu(false)
}
}
@ -116,10 +92,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
return
}
if (
type === ContextMenuOptionType.File ||
type === ContextMenuOptionType.Folder
) {
if (type === ContextMenuOptionType.File || type === ContextMenuOptionType.Folder) {
if (!value) {
setSelectedType(type)
setSearchQuery("")
@ -134,27 +107,16 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
let insertValue = value || ""
if (type === ContextMenuOptionType.URL) {
insertValue = value || ""
} else if (
type === ContextMenuOptionType.File ||
type === ContextMenuOptionType.Folder
) {
} else if (type === ContextMenuOptionType.File || type === ContextMenuOptionType.Folder) {
insertValue = value || ""
} else if (type === ContextMenuOptionType.Problems) {
insertValue = "problems"
}
const { newValue, mentionIndex } = insertMention(
textAreaRef.current.value,
cursorPosition,
insertValue,
)
const { newValue, mentionIndex } = insertMention(textAreaRef.current.value, cursorPosition, insertValue)
setInputValue(newValue)
const newCursorPosition =
newValue.indexOf(
" ",
mentionIndex + insertValue.length,
) + 1
const newCursorPosition = newValue.indexOf(" ", mentionIndex + insertValue.length) + 1
setCursorPosition(newCursorPosition)
setIntendedCursorPosition(newCursorPosition)
// textAreaRef.current.focus()
@ -185,11 +147,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
event.preventDefault()
setSelectedMenuIndex((prevIndex) => {
const direction = event.key === "ArrowUp" ? -1 : 1
const options = getContextMenuOptions(
searchQuery,
selectedType,
queryItems,
)
const options = getContextMenuOptions(searchQuery, selectedType, queryItems)
const optionsLength = options.length
if (optionsLength === 0) return prevIndex
@ -197,54 +155,31 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
// Find selectable options (non-URL types)
const selectableOptions = options.filter(
(option) =>
option.type !== ContextMenuOptionType.URL &&
option.type !==
ContextMenuOptionType.NoResults,
option.type !== ContextMenuOptionType.URL && option.type !== ContextMenuOptionType.NoResults,
)
if (selectableOptions.length === 0) return -1 // No selectable options
// Find the index of the next selectable option
const currentSelectableIndex =
selectableOptions.findIndex(
(option) => option === options[prevIndex],
)
const currentSelectableIndex = selectableOptions.findIndex((option) => option === options[prevIndex])
const newSelectableIndex =
(currentSelectableIndex +
direction +
selectableOptions.length) %
selectableOptions.length
(currentSelectableIndex + direction + selectableOptions.length) % selectableOptions.length
// Find the index of the selected option in the original options array
return options.findIndex(
(option) =>
option ===
selectableOptions[newSelectableIndex],
)
return options.findIndex((option) => option === selectableOptions[newSelectableIndex])
})
return
}
if (
(event.key === "Enter" || event.key === "Tab") &&
selectedMenuIndex !== -1
) {
if ((event.key === "Enter" || event.key === "Tab") && selectedMenuIndex !== -1) {
event.preventDefault()
const selectedOption = getContextMenuOptions(
searchQuery,
selectedType,
queryItems,
)[selectedMenuIndex]
const selectedOption = getContextMenuOptions(searchQuery, selectedType, queryItems)[selectedMenuIndex]
if (
selectedOption &&
selectedOption.type !== ContextMenuOptionType.URL &&
selectedOption.type !==
ContextMenuOptionType.NoResults
selectedOption.type !== ContextMenuOptionType.NoResults
) {
handleMentionSelect(
selectedOption.type,
selectedOption.value,
)
handleMentionSelect(selectedOption.type, selectedOption.value)
}
return
}
@ -261,37 +196,25 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
const charAfterCursor = inputValue[cursorPosition + 1]
const charBeforeIsWhitespace =
charBeforeCursor === " " ||
charBeforeCursor === "\n" ||
charBeforeCursor === "\r\n"
charBeforeCursor === " " || charBeforeCursor === "\n" || charBeforeCursor === "\r\n"
const charAfterIsWhitespace =
charAfterCursor === " " ||
charAfterCursor === "\n" ||
charAfterCursor === "\r\n"
charAfterCursor === " " || charAfterCursor === "\n" || charAfterCursor === "\r\n"
// checks if char before cusor is whitespace after a mention
if (
charBeforeIsWhitespace &&
inputValue
.slice(0, cursorPosition - 1)
.match(new RegExp(mentionRegex.source + "$")) // "$" is added to ensure the match occurs at the end of the string
inputValue.slice(0, cursorPosition - 1).match(new RegExp(mentionRegex.source + "$")) // "$" is added to ensure the match occurs at the end of the string
) {
const newCursorPosition = cursorPosition - 1
// if mention is followed by another word, then instead of deleting the space separating them we just move the cursor to the end of the mention
if (!charAfterIsWhitespace) {
event.preventDefault()
textAreaRef.current?.setSelectionRange(
newCursorPosition,
newCursorPosition,
)
textAreaRef.current?.setSelectionRange(newCursorPosition, newCursorPosition)
setCursorPosition(newCursorPosition)
}
setCursorPosition(newCursorPosition)
setJustDeletedSpaceAfterMention(true)
} else if (justDeletedSpaceAfterMention) {
const { newText, newPosition } = removeMention(
inputValue,
cursorPosition,
)
const { newText, newPosition } = removeMention(inputValue, cursorPosition)
if (newText !== inputValue) {
event.preventDefault()
setInputValue(newText)
@ -321,10 +244,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
useLayoutEffect(() => {
if (intendedCursorPosition !== null && textAreaRef.current) {
textAreaRef.current.setSelectionRange(
intendedCursorPosition,
intendedCursorPosition,
)
textAreaRef.current.setSelectionRange(intendedCursorPosition, intendedCursorPosition)
setIntendedCursorPosition(null) // Reset the state
}
}, [inputValue, intendedCursorPosition])
@ -335,21 +255,12 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
const newCursorPosition = e.target.selectionStart
setInputValue(newValue)
setCursorPosition(newCursorPosition)
const showMenu = shouldShowContextMenu(
newValue,
newCursorPosition,
)
const showMenu = shouldShowContextMenu(newValue, newCursorPosition)
setShowContextMenu(showMenu)
if (showMenu) {
const lastAtIndex = newValue.lastIndexOf(
"@",
newCursorPosition - 1,
)
const query = newValue.slice(
lastAtIndex + 1,
newCursorPosition,
)
const lastAtIndex = newValue.lastIndexOf("@", newCursorPosition - 1)
const query = newValue.slice(lastAtIndex + 1, newCursorPosition)
setSearchQuery(query)
if (query.length > 0) {
setSelectedMenuIndex(0)
@ -388,14 +299,9 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
if (urlRegex.test(pastedText.trim())) {
e.preventDefault()
const trimmedUrl = pastedText.trim()
const newValue =
inputValue.slice(0, cursorPosition) +
trimmedUrl +
" " +
inputValue.slice(cursorPosition)
const newValue = inputValue.slice(0, cursorPosition) + trimmedUrl + " " + inputValue.slice(cursorPosition)
setInputValue(newValue)
const newCursorPosition =
cursorPosition + trimmedUrl.length + 1
const newCursorPosition = cursorPosition + trimmedUrl.length + 1
setCursorPosition(newCursorPosition)
setIntendedCursorPosition(newCursorPosition)
setShowContextMenu(false)
@ -430,47 +336,27 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
const reader = new FileReader()
reader.onloadend = () => {
if (reader.error) {
console.error(
"Error reading file:",
reader.error,
)
console.error("Error reading file:", reader.error)
resolve(null)
} else {
const result = reader.result
resolve(
typeof result === "string"
? result
: null,
)
resolve(typeof result === "string" ? result : null)
}
}
reader.readAsDataURL(blob)
})
})
const imageDataArray = await Promise.all(imagePromises)
const dataUrls = imageDataArray.filter(
(dataUrl): dataUrl is string => dataUrl !== null,
)
const dataUrls = imageDataArray.filter((dataUrl): dataUrl is string => dataUrl !== null)
//.map((dataUrl) => dataUrl.split(",")[1]) // strip the mime type prefix, sharp doesn't need it
if (dataUrls.length > 0) {
setSelectedImages((prevImages) =>
[...prevImages, ...dataUrls].slice(
0,
MAX_IMAGES_PER_MESSAGE,
),
)
setSelectedImages((prevImages) => [...prevImages, ...dataUrls].slice(0, MAX_IMAGES_PER_MESSAGE))
} else {
console.warn("No valid images were processed")
}
}
},
[
shouldDisableImages,
setSelectedImages,
cursorPosition,
setInputValue,
inputValue,
],
[shouldDisableImages, setSelectedImages, cursorPosition, setInputValue, inputValue],
)
const handleThumbnailsHeightChange = useCallback((height: number) => {
@ -494,18 +380,11 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
highlightLayerRef.current.innerHTML = text
.replace(/\n$/, "\n\n")
.replace(
/[<>&]/g,
(c) => ({ "<": "&lt;", ">": "&gt;", "&": "&amp;" })[c] || c,
)
.replace(
mentionRegexGlobal,
'<mark class="mention-context-textarea-highlight">$&</mark>',
)
.replace(/[<>&]/g, (c) => ({ "<": "&lt;", ">": "&gt;", "&": "&amp;" })[c] || c)
.replace(mentionRegexGlobal, '<mark class="mention-context-textarea-highlight">$&</mark>')
highlightLayerRef.current.scrollTop = textAreaRef.current.scrollTop
highlightLayerRef.current.scrollLeft =
textAreaRef.current.scrollLeft
highlightLayerRef.current.scrollLeft = textAreaRef.current.scrollLeft
}, [])
useLayoutEffect(() => {
@ -520,16 +399,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
const handleKeyUp = useCallback(
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (
[
"ArrowLeft",
"ArrowRight",
"ArrowUp",
"ArrowDown",
"Home",
"End",
].includes(e.key)
) {
if (["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", "Home", "End"].includes(e.key)) {
updateCursorPosition()
}
},
@ -618,10 +488,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
onSelect={updateCursorPosition}
onMouseUp={updateCursorPosition}
onHeightChange={(height) => {
if (
textAreaBaseHeight === undefined ||
height < textAreaBaseHeight
) {
if (textAreaBaseHeight === undefined || height < textAreaBaseHeight) {
setTextAreaBaseHeight(height)
}
onHeightChange?.(height)
@ -693,9 +560,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
alignItems: "center",
}}>
<div
className={`input-icon-button ${
shouldDisableImages ? "disabled" : ""
} codicon codicon-device-camera`}
className={`input-icon-button ${shouldDisableImages ? "disabled" : ""} codicon codicon-device-camera`}
onClick={() => {
if (!shouldDisableImages) {
onSelectImages()

View file

@ -35,30 +35,14 @@ interface ChatViewProps {
export const MAX_IMAGES_PER_MESSAGE = 20 // Anthropic limits to 20 images
const ChatView = ({
isHidden,
showAnnouncement,
hideAnnouncement,
showHistoryView,
}: ChatViewProps) => {
const {
version,
clineMessages: messages,
taskHistory,
apiConfiguration,
} = useExtensionState()
const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryView }: ChatViewProps) => {
const { version, clineMessages: messages, taskHistory, apiConfiguration } = useExtensionState()
//const task = messages.length > 0 ? (messages[0].say === "task" ? messages[0] : undefined) : undefined) : undefined
const task = useMemo(() => messages.at(0), [messages]) // leaving this less safe version here since if the first message is not a task, then the extension is in a bad state and needs to be debugged (see Cline.abort)
const modifiedMessages = useMemo(
() => combineApiRequests(combineCommandSequences(messages.slice(1))),
[messages],
)
const modifiedMessages = useMemo(() => combineApiRequests(combineCommandSequences(messages.slice(1))), [messages])
// has to be after api_req_finished are all reduced into api_req_started messages
const apiMetrics = useMemo(
() => getApiMetrics(modifiedMessages),
[modifiedMessages],
)
const apiMetrics = useMemo(() => getApiMetrics(modifiedMessages), [modifiedMessages])
const [inputValue, setInputValue] = useState("")
const textAreaRef = useRef<HTMLTextAreaElement>(null)
@ -68,17 +52,11 @@ const ChatView = ({
// we need to hold on to the ask because useEffect > lastMessage will always let us know when an ask comes in and handle it, but by the time handleMessage is called, the last message might not be the ask anymore (it could be a say that followed)
const [clineAsk, setClineAsk] = useState<ClineAsk | undefined>(undefined)
const [enableButtons, setEnableButtons] = useState<boolean>(false)
const [primaryButtonText, setPrimaryButtonText] = useState<
string | undefined
>("Approve")
const [secondaryButtonText, setSecondaryButtonText] = useState<
string | undefined
>("Reject")
const [primaryButtonText, setPrimaryButtonText] = useState<string | undefined>("Approve")
const [secondaryButtonText, setSecondaryButtonText] = useState<string | undefined>("Reject")
const [didClickCancel, setDidClickCancel] = useState(false)
const virtuosoRef = useRef<VirtuosoHandle>(null)
const [expandedRows, setExpandedRows] = useState<Record<number, boolean>>(
{},
)
const [expandedRows, setExpandedRows] = useState<Record<number, boolean>>({})
const scrollContainerRef = useRef<HTMLDivElement>(null)
const disableAutoScrollRef = useRef(false)
const [showScrollToBottom, setShowScrollToBottom] = useState(false)
@ -129,9 +107,7 @@ const ChatView = ({
setTextAreaDisabled(isPartial)
setClineAsk("tool")
setEnableButtons(!isPartial)
const tool = JSON.parse(
lastMessage.text || "{}",
) as ClineSayTool
const tool = JSON.parse(lastMessage.text || "{}") as ClineSayTool
switch (tool.tool) {
case "editedExistingFile":
case "newFileCreated":
@ -255,11 +231,7 @@ const ChatView = ({
const isStreaming = useMemo(() => {
const isLastAsk = !!modifiedMessages.at(-1)?.ask // checking clineAsk isn't enough since messages effect may be called again for a tool for example, set clineAsk to its value, and if the next message is not an ask then it doesn't reset. This is likely due to how much more often we're updating messages as compared to before, and should be resolved with optimizations as it's likely a rendering bug. but as a final guard for now, the cancel button will show if the last message is not an ask
const isToolCurrentlyAsking =
isLastAsk &&
clineAsk !== undefined &&
enableButtons &&
primaryButtonText !== undefined
const isToolCurrentlyAsking = isLastAsk && clineAsk !== undefined && enableButtons && primaryButtonText !== undefined
if (isToolCurrentlyAsking) {
return false
}
@ -268,15 +240,8 @@ const ChatView = ({
if (isLastMessagePartial) {
return true
} else {
const lastApiReqStarted = findLast(
modifiedMessages,
(message) => message.say === "api_req_started",
)
if (
lastApiReqStarted &&
lastApiReqStarted.text != null &&
lastApiReqStarted.say === "api_req_started"
) {
const lastApiReqStarted = findLast(modifiedMessages, (message) => message.say === "api_req_started")
if (lastApiReqStarted && lastApiReqStarted.text != null && lastApiReqStarted.say === "api_req_started") {
const cost = JSON.parse(lastApiReqStarted.text).cost
if (cost === undefined) {
// api request has not finished yet
@ -411,9 +376,7 @@ const ChatView = ({
}, [])
const shouldDisableImages =
!selectedModelInfo.supportsImages ||
textAreaDisabled ||
selectedImages.length >= MAX_IMAGES_PER_MESSAGE
!selectedModelInfo.supportsImages || textAreaDisabled || selectedImages.length >= MAX_IMAGES_PER_MESSAGE
const handleMessage = useCallback(
(e: MessageEvent) => {
@ -422,11 +385,7 @@ const ChatView = ({
case "action":
switch (message.action!) {
case "didBecomeVisible":
if (
!isHidden &&
!textAreaDisabled &&
!enableButtons
) {
if (!isHidden && !textAreaDisabled && !enableButtons) {
textAreaRef.current?.focus()
}
break
@ -435,21 +394,13 @@ const ChatView = ({
case "selectedImages":
const newImages = message.images ?? []
if (newImages.length > 0) {
setSelectedImages((prevImages) =>
[...prevImages, ...newImages].slice(
0,
MAX_IMAGES_PER_MESSAGE,
),
)
setSelectedImages((prevImages) => [...prevImages, ...newImages].slice(0, MAX_IMAGES_PER_MESSAGE))
}
break
case "invoke":
switch (message.invoke!) {
case "sendMessage":
handleSendMessage(
message.text ?? "",
message.images ?? [],
)
handleSendMessage(message.text ?? "", message.images ?? [])
break
case "primaryButtonClick":
handlePrimaryButtonClick()
@ -461,14 +412,7 @@ const ChatView = ({
}
// textAreaRef.current is not explicitly required here since react gaurantees that ref will be stable across re-renders, and we're not using its value but its reference.
},
[
isHidden,
textAreaDisabled,
enableButtons,
handleSendMessage,
handlePrimaryButtonClick,
handleSecondaryButtonClick,
],
[isHidden, textAreaDisabled, enableButtons, handleSendMessage, handlePrimaryButtonClick, handleSecondaryButtonClick],
)
useEvent("message", handleMessage)
@ -510,10 +454,7 @@ const ChatView = ({
return false
case "text":
// Sometimes cline returns an empty text message, we don't want to render these. (We also use a say text for user messages, so in case they just sent images we still render that)
if (
(message.text ?? "") === "" &&
(message.images?.length ?? 0) === 0
) {
if ((message.text ?? "") === "" && (message.images?.length ?? 0) === 0) {
return false
}
break
@ -530,13 +471,9 @@ const ChatView = ({
return ["browser_action_launch"].includes(message.ask!)
}
if (message.type === "say") {
return [
"browser_action_launch",
"api_req_started",
"text",
"browser_action",
"browser_action_result",
].includes(message.say!)
return ["browser_action_launch", "api_req_started", "text", "browser_action", "browser_action_result"].includes(
message.say!,
)
}
return false
}
@ -555,10 +492,7 @@ const ChatView = ({
}
visibleMessages.forEach((message) => {
if (
message.ask === "browser_action_launch" ||
message.say === "browser_action_launch"
) {
if (message.ask === "browser_action_launch" || message.say === "browser_action_launch") {
// complete existing browser session if any
endBrowserSession()
// start new
@ -569,9 +503,7 @@ const ChatView = ({
if (message.say === "api_req_started") {
// get last api_req_started in currentGroup to check if it's cancelled. If it is then this api req is not part of the current browser session
const lastApiReqStarted = [...currentGroup]
.reverse()
.find((m) => m.say === "api_req_started")
const lastApiReqStarted = [...currentGroup].reverse().find((m) => m.say === "api_req_started")
if (lastApiReqStarted?.text != null) {
const info = JSON.parse(lastApiReqStarted.text)
const isCancelled = info.cancelReason != null
@ -588,9 +520,7 @@ const ChatView = ({
// Check if this is a close action
if (message.say === "browser_action") {
const browserAction = JSON.parse(
message.text || "{}",
) as ClineSayBrowserAction
const browserAction = JSON.parse(message.text || "{}") as ClineSayBrowserAction
if (browserAction.action === "close") {
endBrowserSession()
}
@ -642,9 +572,7 @@ const ChatView = ({
(ts: number) => {
const isCollapsing = expandedRows[ts] ?? false
const lastGroup = groupedMessages.at(-1)
const isLast = Array.isArray(lastGroup)
? lastGroup[0].ts === ts
: lastGroup?.ts === ts
const isLast = Array.isArray(lastGroup) ? lastGroup[0].ts === ts : lastGroup?.ts === ts
const secondToLastGroup = groupedMessages.at(-2)
const isSecondToLast = Array.isArray(secondToLastGroup)
? secondToLastGroup[0].ts === ts
@ -721,9 +649,7 @@ const ChatView = ({
const handleWheel = useCallback((event: Event) => {
const wheelEvent = event as WheelEvent
if (wheelEvent.deltaY && wheelEvent.deltaY < 0) {
if (
scrollContainerRef.current?.contains(wheelEvent.target as Node)
) {
if (scrollContainerRef.current?.contains(wheelEvent.target as Node)) {
// user scrolled up
disableAutoScrollRef.current = true
}
@ -732,9 +658,7 @@ const ChatView = ({
useEvent("wheel", handleWheel, window, { passive: true }) // passive improves scrolling performance
const placeholderText = useMemo(() => {
const text = task
? "Type a message (@ to add context)..."
: "Type your task here (@ to add context)..."
const text = task ? "Type a message (@ to add context)..." : "Type your task here (@ to add context)..."
return text
}, [task])
@ -749,9 +673,7 @@ const ChatView = ({
lastModifiedMessage={modifiedMessages.at(-1)}
onHeightChange={handleRowHeightChange}
// Pass handlers for each message in the group
isExpanded={(messageTs: number) =>
expandedRows[messageTs] ?? false
}
isExpanded={(messageTs: number) => expandedRows[messageTs] ?? false}
onToggleExpand={(messageTs: number) => {
setExpandedRows((prev) => ({
...prev,
@ -775,13 +697,7 @@ const ChatView = ({
/>
)
},
[
expandedRows,
modifiedMessages,
groupedMessages.length,
toggleRowExpansion,
handleRowHeightChange,
],
[expandedRows, modifiedMessages, groupedMessages.length, toggleRowExpansion, handleRowHeightChange],
)
return (
@ -801,9 +717,7 @@ const ChatView = ({
task={task}
tokensIn={apiMetrics.totalTokensIn}
tokensOut={apiMetrics.totalTokensOut}
doesModelSupportPromptCache={
selectedModelInfo.supportsPromptCache
}
doesModelSupportPromptCache={selectedModelInfo.supportsPromptCache}
cacheWrites={apiMetrics.totalCacheWrites}
cacheReads={apiMetrics.totalCacheReads}
totalCost={apiMetrics.totalCost}
@ -819,12 +733,7 @@ const ChatView = ({
flexDirection: "column",
paddingBottom: "10px",
}}>
{showAnnouncement && (
<Announcement
version={version}
hideAnnouncement={hideAnnouncement}
/>
)}
{showAnnouncement && <Announcement version={version} hideAnnouncement={hideAnnouncement} />}
<div style={{ padding: "0 20px", flexShrink: 0 }}>
<h2>What can I do for you?</h2>
<p>
@ -834,18 +743,13 @@ const ChatView = ({
style={{ display: "inline" }}>
Claude 3.5 Sonnet's agentic coding capabilities,
</VSCodeLink>{" "}
I can handle complex software development tasks
step-by-step. With tools that let me create & edit
files, explore complex projects, use the browser,
and execute terminal commands (after you grant
permission), I can assist you in ways that go beyond
code completion or tech support. I can even use MCP
to create new tools and extend my own capabilities.
I can handle complex software development tasks step-by-step. With tools that let me create & edit
files, explore complex projects, use the browser, and execute terminal commands (after you grant
permission), I can assist you in ways that go beyond code completion or tech support. I can even use
MCP to create new tools and extend my own capabilities.
</p>
</div>
{taskHistory.length > 0 && (
<HistoryPreview showHistoryView={showHistoryView} />
)}
{taskHistory.length > 0 && <HistoryPreview showHistoryView={showHistoryView} />}
</div>
)}
@ -876,9 +780,7 @@ const ChatView = ({
{task && (
<>
<div
style={{ flexGrow: 1, display: "flex" }}
ref={scrollContainerRef}>
<div style={{ flexGrow: 1, display: "flex" }} ref={scrollContainerRef}>
<Virtuoso
ref={virtuosoRef}
key={task.ts} // trick to make sure virtuoso re-renders when task changes, and we use initialTopMostItemIndex to start at the bottom
@ -902,9 +804,7 @@ const ChatView = ({
if (isAtBottom) {
disableAutoScrollRef.current = false
}
setShowScrollToBottom(
disableAutoScrollRef.current && !isAtBottom,
)
setShowScrollToBottom(disableAutoScrollRef.current && !isAtBottom)
}}
atBottomThreshold={10} // anything lower causes issues with followOutput
initialTopMostItemIndex={groupedMessages.length - 1}
@ -922,20 +822,15 @@ const ChatView = ({
scrollToBottomSmooth()
disableAutoScrollRef.current = false
}}>
<span
className="codicon codicon-chevron-down"
style={{ fontSize: "18px" }}></span>
<span className="codicon codicon-chevron-down" style={{ fontSize: "18px" }}></span>
</ScrollToBottomButton>
</div>
) : (
<div
style={{
opacity:
primaryButtonText ||
secondaryButtonText ||
isStreaming
? enableButtons ||
(isStreaming && !didClickCancel)
primaryButtonText || secondaryButtonText || isStreaming
? enableButtons || (isStreaming && !didClickCancel)
? 1
: 0.5
: 0,
@ -948,9 +843,7 @@ const ChatView = ({
disabled={!enableButtons}
style={{
flex: secondaryButtonText ? 1 : 2,
marginRight: secondaryButtonText
? "6px"
: "0",
marginRight: secondaryButtonText ? "6px" : "0",
}}
onClick={handlePrimaryButtonClick}>
{primaryButtonText}
@ -959,18 +852,13 @@ const ChatView = ({
{(secondaryButtonText || isStreaming) && (
<VSCodeButton
appearance="secondary"
disabled={
!enableButtons &&
!(isStreaming && !didClickCancel)
}
disabled={!enableButtons && !(isStreaming && !didClickCancel)}
style={{
flex: isStreaming ? 2 : 1,
marginLeft: isStreaming ? 0 : "6px",
}}
onClick={handleSecondaryButtonClick}>
{isStreaming
? "Cancel"
: secondaryButtonText}
{isStreaming ? "Cancel" : secondaryButtonText}
</VSCodeButton>
)}
</div>
@ -999,11 +887,7 @@ const ChatView = ({
}
const ScrollToBottomButton = styled.div`
background-color: color-mix(
in srgb,
var(--vscode-toolbar-hoverBackground) 55%,
transparent
);
background-color: color-mix(in srgb, var(--vscode-toolbar-hoverBackground) 55%, transparent);
border-radius: 3px;
overflow: hidden;
cursor: pointer;
@ -1014,19 +898,11 @@ const ScrollToBottomButton = styled.div`
height: 24px;
&:hover {
background-color: color-mix(
in srgb,
var(--vscode-toolbar-hoverBackground) 90%,
transparent
);
background-color: color-mix(in srgb, var(--vscode-toolbar-hoverBackground) 90%, transparent);
}
&:active {
background-color: color-mix(
in srgb,
var(--vscode-toolbar-hoverBackground) 70%,
transparent
);
background-color: color-mix(in srgb, var(--vscode-toolbar-hoverBackground) 70%, transparent);
}
`

View file

@ -1,9 +1,5 @@
import React, { useEffect, useMemo, useRef } from "react"
import {
ContextMenuOptionType,
ContextMenuQueryItem,
getContextMenuOptions,
} from "../../utils/context-mentions"
import { ContextMenuOptionType, ContextMenuQueryItem, getContextMenuOptions } from "../../utils/context-mentions"
import { removeLeadingNonAlphanumeric } from "../common/CodeAccordian"
interface ContextMenuProps {
@ -34,16 +30,13 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
useEffect(() => {
if (menuRef.current) {
const selectedElement = menuRef.current.children[
selectedIndex
] as HTMLElement
const selectedElement = menuRef.current.children[selectedIndex] as HTMLElement
if (selectedElement) {
const menuRect = menuRef.current.getBoundingClientRect()
const selectedRect = selectedElement.getBoundingClientRect()
if (selectedRect.bottom > menuRect.bottom) {
menuRef.current.scrollTop +=
selectedRect.bottom - menuRect.bottom
menuRef.current.scrollTop += selectedRect.bottom - menuRect.bottom
} else if (selectedRect.top < menuRect.top) {
menuRef.current.scrollTop -= menuRect.top - selectedRect.top
}
@ -74,21 +67,12 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
direction: "rtl",
textAlign: "left",
}}>
{removeLeadingNonAlphanumeric(
option.value || "",
) + "\u200E"}
{removeLeadingNonAlphanumeric(option.value || "") + "\u200E"}
</span>
</>
)
} else {
return (
<span>
Add{" "}
{option.type === ContextMenuOptionType.File
? "File"
: "Folder"}
</span>
)
return <span>Add {option.type === ContextMenuOptionType.File ? "File" : "Folder"}</span>
}
}
}
@ -111,10 +95,7 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
}
const isOptionSelectable = (option: ContextMenuQueryItem): boolean => {
return (
option.type !== ContextMenuOptionType.NoResults &&
option.type !== ContextMenuOptionType.URL
)
return option.type !== ContextMenuOptionType.NoResults && option.type !== ContextMenuOptionType.URL
}
return (
@ -144,35 +125,24 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
{filteredOptions.map((option, index) => (
<div
key={`${option.type}-${option.value || index}`}
onClick={() =>
isOptionSelectable(option) &&
onSelect(option.type, option.value)
}
onClick={() => isOptionSelectable(option) && onSelect(option.type, option.value)}
style={{
padding: "8px 12px",
cursor: isOptionSelectable(option)
? "pointer"
: "default",
cursor: isOptionSelectable(option) ? "pointer" : "default",
color:
index === selectedIndex &&
isOptionSelectable(option)
index === selectedIndex && isOptionSelectable(option)
? "var(--vscode-quickInputList-focusForeground)"
: "",
borderBottom:
"1px solid var(--vscode-editorGroup-border)",
borderBottom: "1px solid var(--vscode-editorGroup-border)",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
backgroundColor:
index === selectedIndex &&
isOptionSelectable(option)
index === selectedIndex && isOptionSelectable(option)
? "var(--vscode-quickInputList-focusBackground)"
: "",
}}
onMouseEnter={() =>
isOptionSelectable(option) &&
setSelectedIndex(index)
}>
onMouseEnter={() => isOptionSelectable(option) && setSelectedIndex(index)}>
<div
style={{
display: "flex",
@ -191,8 +161,7 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
/>
{renderOptionContent(option)}
</div>
{(option.type === ContextMenuOptionType.File ||
option.type === ContextMenuOptionType.Folder) &&
{(option.type === ContextMenuOptionType.File || option.type === ContextMenuOptionType.Folder) &&
!option.value && (
<i
className="codicon codicon-chevron-right"
@ -204,8 +173,7 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
/>
)}
{(option.type === ContextMenuOptionType.Problems ||
((option.type === ContextMenuOptionType.File ||
option.type === ContextMenuOptionType.Folder) &&
((option.type === ContextMenuOptionType.File || option.type === ContextMenuOptionType.Folder) &&
option.value)) && (
<i
className="codicon codicon-add"

View file

@ -30,8 +30,7 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
totalCost,
onClose,
}) => {
const { apiConfiguration, currentTaskItem, checkpointTrackerErrorMessage } =
useExtensionState()
const { apiConfiguration, currentTaskItem, checkpointTrackerErrorMessage } = useExtensionState()
const [isTaskExpanded, setIsTaskExpanded] = useState(true)
const [isTextExpanded, setIsTextExpanded] = useState(false)
const [showSeeMore, setShowSeeMore] = useState(false)
@ -83,11 +82,9 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
if (textRef.current && textContainerRef.current) {
let textContainerHeight = textContainerRef.current.clientHeight
if (!textContainerHeight) {
textContainerHeight =
textContainerRef.current.getBoundingClientRect().height
textContainerHeight = textContainerRef.current.getBoundingClientRect().height
}
const isOverflowing =
textRef.current.scrollHeight > textContainerHeight
const isOverflowing = textRef.current.scrollHeight > textContainerHeight
// necessary to show see more button again if user resizes window to expand and then back to collapse
if (!isOverflowing) {
setIsTextExpanded(false)
@ -105,9 +102,7 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
)
}, [apiConfiguration?.apiProvider])
const shouldShowPromptCacheInfo =
doesModelSupportPromptCache &&
apiConfiguration?.apiProvider !== "openrouter"
const shouldShowPromptCacheInfo = doesModelSupportPromptCache && apiConfiguration?.apiProvider !== "openrouter"
return (
<div style={{ padding: "10px 13px 10px 13px" }}>
@ -149,8 +144,7 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
alignItems: "center",
flexShrink: 0,
}}>
<span
className={`codicon codicon-chevron-${isTaskExpanded ? "down" : "right"}`}></span>
<span className={`codicon codicon-chevron-${isTaskExpanded ? "down" : "right"}`}></span>
</div>
<div
style={{
@ -161,22 +155,15 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
flexGrow: 1,
minWidth: 0, // This allows the div to shrink below its content size
}}>
<span style={{ fontWeight: "bold" }}>
Task{!isTaskExpanded && ":"}
</span>
{!isTaskExpanded && (
<span style={{ marginLeft: 4 }}>
{highlightMentions(task.text, false)}
</span>
)}
<span style={{ fontWeight: "bold" }}>Task{!isTaskExpanded && ":"}</span>
{!isTaskExpanded && <span style={{ marginLeft: 4 }}>{highlightMentions(task.text, false)}</span>}
</div>
</div>
{!isTaskExpanded && isCostAvailable && (
<div
style={{
marginLeft: 10,
backgroundColor:
"color-mix(in srgb, var(--vscode-badge-foreground) 70%, transparent)",
backgroundColor: "color-mix(in srgb, var(--vscode-badge-foreground) 70%, transparent)",
color: "var(--vscode-badge-background)",
padding: "2px 4px",
borderRadius: "500px",
@ -188,10 +175,7 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
${totalCost?.toFixed(4)}
</div>
)}
<VSCodeButton
appearance="icon"
onClick={onClose}
style={{ marginLeft: 6, flexShrink: 0 }}>
<VSCodeButton appearance="icon" onClick={onClose} style={{ marginLeft: 6, flexShrink: 0 }}>
<span className="codicon codicon-close"></span>
</VSCodeButton>
</div>
@ -211,9 +195,7 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
ref={textRef}
style={{
display: "-webkit-box",
WebkitLineClamp: isTextExpanded
? "unset"
: 3,
WebkitLineClamp: isTextExpanded ? "unset" : 3,
WebkitBoxOrient: "vertical",
overflow: "hidden",
whiteSpace: "pre-wrap",
@ -235,8 +217,7 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
style={{
width: 30,
height: "1.2em",
background:
"linear-gradient(to right, transparent, var(--vscode-badge-background))",
background: "linear-gradient(to right, transparent, var(--vscode-badge-background))",
}}
/>
<div
@ -245,12 +226,9 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
color: "var(--vscode-textLink-foreground)",
paddingRight: 0,
paddingLeft: 3,
backgroundColor:
"var(--vscode-badge-background)",
backgroundColor: "var(--vscode-badge-background)",
}}
onClick={() =>
setIsTextExpanded(!isTextExpanded)
}>
onClick={() => setIsTextExpanded(!isTextExpanded)}>
See more
</div>
</div>
@ -265,15 +243,11 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
textAlign: "right",
paddingRight: 2,
}}
onClick={() =>
setIsTextExpanded(!isTextExpanded)
}>
onClick={() => setIsTextExpanded(!isTextExpanded)}>
See less
</div>
)}
{task.images && task.images.length > 0 && (
<Thumbnails images={task.images} />
)}
{task.images && task.images.length > 0 && <Thumbnails images={task.images} />}
<div
style={{
display: "flex",
@ -293,9 +267,7 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
gap: "4px",
flexWrap: "wrap",
}}>
<span style={{ fontWeight: "bold" }}>
Tokens:
</span>
<span style={{ fontWeight: "bold" }}>Tokens:</span>
<span
style={{
display: "flex",
@ -330,65 +302,53 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
</span>
</div>
{!isCostAvailable && (
<DeleteButton
taskSize={formatSize(
currentTaskItem?.size,
)}
taskId={currentTaskItem?.id}
/>
<DeleteButton taskSize={formatSize(currentTaskItem?.size)} taskId={currentTaskItem?.id} />
)}
</div>
{shouldShowPromptCacheInfo &&
(cacheReads !== undefined ||
cacheWrites !== undefined) && (
<div
{shouldShowPromptCacheInfo && (cacheReads !== undefined || cacheWrites !== undefined) && (
<div
style={{
display: "flex",
alignItems: "center",
gap: "4px",
flexWrap: "wrap",
}}>
<span style={{ fontWeight: "bold" }}>Cache:</span>
<span
style={{
display: "flex",
alignItems: "center",
gap: "4px",
flexWrap: "wrap",
gap: "3px",
}}>
<span style={{ fontWeight: "bold" }}>
Cache:
</span>
<span
<i
className="codicon codicon-database"
style={{
display: "flex",
alignItems: "center",
gap: "3px",
}}>
<i
className="codicon codicon-database"
style={{
fontSize: "12px",
fontWeight: "bold",
marginBottom: "-1px",
}}
/>
+
{formatLargeNumber(
cacheWrites || 0,
)}
</span>
<span
fontSize: "12px",
fontWeight: "bold",
marginBottom: "-1px",
}}
/>
+{formatLargeNumber(cacheWrites || 0)}
</span>
<span
style={{
display: "flex",
alignItems: "center",
gap: "3px",
}}>
<i
className="codicon codicon-arrow-right"
style={{
display: "flex",
alignItems: "center",
gap: "3px",
}}>
<i
className="codicon codicon-arrow-right"
style={{
fontSize: "12px",
fontWeight: "bold",
marginBottom: 0,
}}
/>
{formatLargeNumber(cacheReads || 0)}
</span>
</div>
)}
fontSize: "12px",
fontWeight: "bold",
marginBottom: 0,
}}
/>
{formatLargeNumber(cacheReads || 0)}
</span>
</div>
)}
{isCostAvailable && (
<div
style={{
@ -402,17 +362,10 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
alignItems: "center",
gap: "4px",
}}>
<span style={{ fontWeight: "bold" }}>
API Cost:
</span>
<span style={{ fontWeight: "bold" }}>API Cost:</span>
<span>${totalCost?.toFixed(4)}</span>
</div>
<DeleteButton
taskSize={formatSize(
currentTaskItem?.size,
)}
taskId={currentTaskItem?.id}
/>
<DeleteButton taskSize={formatSize(currentTaskItem?.size)} taskId={currentTaskItem?.id} />
</div>
)}
{checkpointTrackerErrorMessage && (
@ -427,17 +380,14 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
<i className="codicon codicon-warning" />
<span>
{checkpointTrackerErrorMessage}
{checkpointTrackerErrorMessage.includes(
"Git must be installed to use checkpoints.",
) && (
{checkpointTrackerErrorMessage.includes("Git must be installed to use checkpoints.") && (
<>
{" "}
<a
href="https://github.com/cline/cline/wiki/Installing-Git-for-Checkpoints"
style={{
color: "inherit",
textDecoration:
"underline",
textDecoration: "underline",
}}>
See here for instructions.
</a>
@ -494,15 +444,9 @@ export const highlightMentions = (text?: string, withShadow = true) => {
return (
<span
key={index}
className={
withShadow
? "mention-context-highlight-with-shadow"
: "mention-context-highlight"
}
className={withShadow ? "mention-context-highlight-with-shadow" : "mention-context-highlight"}
style={{ cursor: "pointer" }}
onClick={() =>
vscode.postMessage({ type: "openMention", text: part })
}>
onClick={() => vscode.postMessage({ type: "openMention", text: part })}>
@{part}
</span>
)
@ -516,9 +460,7 @@ const DeleteButton: React.FC<{
}> = ({ taskSize, taskId }) => (
<VSCodeButton
appearance="icon"
onClick={() =>
vscode.postMessage({ type: "deleteTaskWithId", text: taskId })
}
onClick={() => vscode.postMessage({ type: "deleteTaskWithId", text: taskId })}
style={{ padding: "0px 0px" }}>
<div
style={{

View file

@ -14,8 +14,7 @@ interface CheckpointOverlayProps {
export const CheckpointOverlay = ({ messageTs }: CheckpointOverlayProps) => {
const [compareDisabled, setCompareDisabled] = useState(false)
const [restoreTaskDisabled, setRestoreTaskDisabled] = useState(false)
const [restoreWorkspaceDisabled, setRestoreWorkspaceDisabled] =
useState(false)
const [restoreWorkspaceDisabled, setRestoreWorkspaceDisabled] = useState(false)
const [restoreBothDisabled, setRestoreBothDisabled] = useState(false)
const [showRestoreConfirm, setShowRestoreConfirm] = useState(false)
const [hasMouseEntered, setHasMouseEntered] = useState(false)
@ -118,10 +117,7 @@ export const CheckpointOverlay = ({ messageTs }: CheckpointOverlayProps) => {
number: messageTs,
})
}}>
<i
className="codicon codicon-diff-multiple"
style={{ position: "absolute" }}
/>
<i className="codicon codicon-diff-multiple" style={{ position: "absolute" }} />
</VSCodeButton>
<div style={{ position: "relative" }} ref={containerRef}>
<VSCodeButton
@ -129,64 +125,42 @@ export const CheckpointOverlay = ({ messageTs }: CheckpointOverlayProps) => {
appearance="secondary"
style={{ cursor: "pointer" }}
onClick={() => setShowRestoreConfirm(true)}>
<i
className="codicon codicon-discard"
style={{ position: "absolute" }}
/>
<i className="codicon codicon-discard" style={{ position: "absolute" }} />
</VSCodeButton>
{showRestoreConfirm && (
<RestoreConfirmTooltip
ref={tooltipRef}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}>
<RestoreConfirmTooltip ref={tooltipRef} onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
<RestoreOption>
<VSCodeButton
onClick={handleRestoreBoth}
disabled={restoreBothDisabled}
style={{
cursor: restoreBothDisabled
? "wait"
: "pointer",
cursor: restoreBothDisabled ? "wait" : "pointer",
}}>
Restore Task and Workspace
</VSCodeButton>
<p>
Restores the task and your project's files back
to a snapshot taken at this point
</p>
<p>Restores the task and your project's files back to a snapshot taken at this point</p>
</RestoreOption>
<RestoreOption>
<VSCodeButton
onClick={handleRestoreTask}
disabled={restoreTaskDisabled}
style={{
cursor: restoreTaskDisabled
? "wait"
: "pointer",
cursor: restoreTaskDisabled ? "wait" : "pointer",
}}>
Restore Task Only
</VSCodeButton>
<p>
Deletes messages after this point (does not
affect workspace)
</p>
<p>Deletes messages after this point (does not affect workspace)</p>
</RestoreOption>
<RestoreOption>
<VSCodeButton
onClick={handleRestoreWorkspace}
disabled={restoreWorkspaceDisabled}
style={{
cursor: restoreWorkspaceDisabled
? "wait"
: "pointer",
cursor: restoreWorkspaceDisabled ? "wait" : "pointer",
}}>
Restore Workspace Only
</VSCodeButton>
<p>
Restores your project's files to a snapshot
taken at this point (task may become out of
sync)
</p>
<p>Restores your project's files to a snapshot taken at this point (task may become out of sync)</p>
</RestoreOption>
</RestoreConfirmTooltip>
)}

View file

@ -20,8 +20,7 @@ We need to remove leading non-alphanumeric characters from the path in order for
[^a-zA-Z0-9]+: Matches one or more characters that are not alphanumeric.
The replace method removes these matched characters, effectively trimming the string up to the first alphanumeric character.
*/
export const removeLeadingNonAlphanumeric = (path: string): string =>
path.replace(/^[^a-zA-Z0-9]+/, "")
export const removeLeadingNonAlphanumeric = (path: string): string => path.replace(/^[^a-zA-Z0-9]+/, "")
const CodeAccordian = ({
code,
@ -35,9 +34,7 @@ const CodeAccordian = ({
isLoading,
}: CodeAccordianProps) => {
const inferredLanguage = useMemo(
() =>
code &&
(language ?? (path ? getLanguageFromPath(path) : undefined)),
() => code && (language ?? (path ? getLanguageFromPath(path) : undefined)),
[path, language, code],
)
@ -93,14 +90,12 @@ const CodeAccordian = ({
direction: "rtl",
textAlign: "left",
}}>
{removeLeadingNonAlphanumeric(path ?? "") +
"\u200E"}
{removeLeadingNonAlphanumeric(path ?? "") + "\u200E"}
</span>
</>
)}
<div style={{ flexGrow: 1 }}></div>
<span
className={`codicon codicon-chevron-${isExpanded ? "up" : "down"}`}></span>
<span className={`codicon codicon-chevron-${isExpanded ? "up" : "down"}`}></span>
</div>
)}
{(!(path || isFeedback || isConsoleLogs) || isExpanded) && (

View file

@ -5,8 +5,7 @@ import styled from "styled-components"
import { visit } from "unist-util-visit"
import { useExtensionState } from "../../context/ExtensionStateContext"
export const CODE_BLOCK_BG_COLOR =
"var(--vscode-editor-background, --vscode-sideBar-background, rgb(30 30 30))"
export const CODE_BLOCK_BG_COLOR = "var(--vscode-editor-background, --vscode-sideBar-background, rgb(30 30 30))"
/*
overflowX: auto + inner div with padding results in an issue where the top/left/bottom padding renders but the right padding inside does not count as overflow as the width of the element is not exceeded. Once the inner div is outside the boundaries of the parent it counts as overflow.
@ -60,10 +59,7 @@ const StyledMarkdown = styled.div<{ forceWrap: boolean }>`
word-wrap: break-word;
border-radius: 5px;
background-color: ${CODE_BLOCK_BG_COLOR};
font-size: var(
--vscode-editor-font-size,
var(--vscode-font-size, 12px)
);
font-size: var(--vscode-editor-font-size, var(--vscode-font-size, 12px));
font-family: var(--vscode-editor-font-family);
}
@ -139,9 +135,7 @@ const CodeBlock = memo(({ source, forceWrap = false }: CodeBlockProps) => {
],
rehypeReactOptions: {
components: {
pre: ({ node, ...preProps }: any) => (
<StyledPre {...preProps} theme={theme} />
),
pre: ({ node, ...preProps }: any) => <StyledPre {...preProps} theme={theme} />,
},
},
})
@ -157,9 +151,7 @@ const CodeBlock = memo(({ source, forceWrap = false }: CodeBlockProps) => {
maxHeight: forceWrap ? "none" : "100%",
backgroundColor: CODE_BLOCK_BG_COLOR,
}}>
<StyledMarkdown forceWrap={forceWrap}>
{reactContent}
</StyledMarkdown>
<StyledMarkdown forceWrap={forceWrap}>{reactContent}</StyledMarkdown>
</div>
)
})

View file

@ -57,67 +57,43 @@ function Demo() {
<div className="grid gap-3 p-2 place-items-start">
<VSCodeDataGrid>
<VSCodeDataGridRow row-type="header">
<VSCodeDataGridCell
cell-type="columnheader"
grid-column="1">
<VSCodeDataGridCell cell-type="columnheader" grid-column="1">
A Custom Header Title
</VSCodeDataGridCell>
<VSCodeDataGridCell
cell-type="columnheader"
grid-column="2">
<VSCodeDataGridCell cell-type="columnheader" grid-column="2">
Another Custom Title
</VSCodeDataGridCell>
<VSCodeDataGridCell
cell-type="columnheader"
grid-column="3">
<VSCodeDataGridCell cell-type="columnheader" grid-column="3">
Title Is Custom
</VSCodeDataGridCell>
<VSCodeDataGridCell
cell-type="columnheader"
grid-column="4">
<VSCodeDataGridCell cell-type="columnheader" grid-column="4">
Custom Title
</VSCodeDataGridCell>
</VSCodeDataGridRow>
{rowData.map((row, index) => (
<VSCodeDataGridRow key={index}>
<VSCodeDataGridCell grid-column="1">
{row.cell1}
</VSCodeDataGridCell>
<VSCodeDataGridCell grid-column="2">
{row.cell2}
</VSCodeDataGridCell>
<VSCodeDataGridCell grid-column="3">
{row.cell3}
</VSCodeDataGridCell>
<VSCodeDataGridCell grid-column="4">
{row.cell4}
</VSCodeDataGridCell>
<VSCodeDataGridCell grid-column="1">{row.cell1}</VSCodeDataGridCell>
<VSCodeDataGridCell grid-column="2">{row.cell2}</VSCodeDataGridCell>
<VSCodeDataGridCell grid-column="3">{row.cell3}</VSCodeDataGridCell>
<VSCodeDataGridCell grid-column="4">{row.cell4}</VSCodeDataGridCell>
</VSCodeDataGridRow>
))}
</VSCodeDataGrid>
<VSCodeTextField>
<section
slot="end"
style={{ display: "flex", alignItems: "center" }}>
<section slot="end" style={{ display: "flex", alignItems: "center" }}>
<VSCodeButton appearance="icon" aria-label="Match Case">
<span className="codicon codicon-case-sensitive"></span>
</VSCodeButton>
<VSCodeButton
appearance="icon"
aria-label="Match Whole Word">
<VSCodeButton appearance="icon" aria-label="Match Whole Word">
<span className="codicon codicon-whole-word"></span>
</VSCodeButton>
<VSCodeButton
appearance="icon"
aria-label="Use Regular Expression">
<VSCodeButton appearance="icon" aria-label="Use Regular Expression">
<span className="codicon codicon-regex"></span>
</VSCodeButton>
</section>
</VSCodeTextField>
<span
slot="end"
className="codicon codicon-chevron-right"></span>
<span slot="end" className="codicon codicon-chevron-right"></span>
<span className="flex gap-3">
<VSCodeProgressRing />

View file

@ -81,10 +81,7 @@ const StyledMarkdown = styled.div`
word-wrap: break-word;
border-radius: 3px;
background-color: ${CODE_BLOCK_BG_COLOR};
font-size: var(
--vscode-editor-font-size,
var(--vscode-font-size, 12px)
);
font-size: var(--vscode-editor-font-size, var(--vscode-font-size, 12px));
font-family: var(--vscode-editor-font-family);
}
@ -184,9 +181,7 @@ const MarkdownBlock = memo(({ markdown }: MarkdownBlockProps) => {
],
rehypeReactOptions: {
components: {
pre: ({ node, ...preProps }: any) => (
<StyledPre {...preProps} theme={theme} />
),
pre: ({ node, ...preProps }: any) => <StyledPre {...preProps} theme={theme} />,
},
},
})

View file

@ -21,8 +21,7 @@ const StyledButton = styled(VSCodeButton)`
}
`
interface SuccessButtonProps
extends React.ComponentProps<typeof VSCodeButton> {}
interface SuccessButtonProps extends React.ComponentProps<typeof VSCodeButton> {}
const SuccessButton: React.FC<SuccessButtonProps> = (props) => {
return <StyledButton {...props} />

View file

@ -9,12 +9,7 @@ interface ThumbnailsProps {
onHeightChange?: (height: number) => void
}
const Thumbnails = ({
images,
style,
setImages,
onHeightChange,
}: ThumbnailsProps) => {
const Thumbnails = ({ images, style, setImages, onHeightChange }: ThumbnailsProps) => {
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null)
const containerRef = useRef<HTMLDivElement>(null)
const { width } = useWindowSize()
@ -79,8 +74,7 @@ const Thumbnails = ({
width: 13,
height: 13,
borderRadius: "50%",
backgroundColor:
"var(--vscode-badge-background)",
backgroundColor: "var(--vscode-badge-background)",
display: "flex",
justifyContent: "center",
alignItems: "center",

View file

@ -7,11 +7,7 @@ interface VSCodeButtonLinkProps {
[key: string]: any
}
const VSCodeButtonLink: React.FC<VSCodeButtonLinkProps> = ({
href,
children,
...props
}) => {
const VSCodeButtonLink: React.FC<VSCodeButtonLinkProps> = ({ href, children, ...props }) => {
return (
<a
href={href}

View file

@ -78,10 +78,7 @@ const HistoryPreview = ({ showHistoryView }: HistoryPreviewProps) => {
.filter((item) => item.ts && item.task)
.slice(0, 3)
.map((item) => (
<div
key={item.id}
className="history-preview-item"
onClick={() => handleHistorySelect(item.id)}>
<div key={item.id} className="history-preview-item" onClick={() => handleHistorySelect(item.id)}>
<div style={{ padding: "12px" }}>
<div style={{ marginBottom: "8px" }}>
<span
@ -115,33 +112,21 @@ const HistoryPreview = ({ showHistoryView }: HistoryPreviewProps) => {
color: "var(--vscode-descriptionForeground)",
}}>
<span>
Tokens:
{formatLargeNumber(item.tokensIn || 0)}{" "}
{formatLargeNumber(item.tokensOut || 0)}
Tokens: {formatLargeNumber(item.tokensIn || 0)} {formatLargeNumber(item.tokensOut || 0)}
</span>
{!!item.cacheWrites && (
<>
{" • "}
<span>
Cache: +
{formatLargeNumber(
item.cacheWrites || 0,
)}{" "}
{" "}
{formatLargeNumber(
item.cacheReads || 0,
)}
Cache: +{formatLargeNumber(item.cacheWrites || 0)} {" "}
{formatLargeNumber(item.cacheReads || 0)}
</span>
</>
)}
{!!item.totalCost && (
<>
{" • "}
<span>
API Cost: $
{item.totalCost?.toFixed(4)}
</span>
<span>API Cost: ${item.totalCost?.toFixed(4)}</span>
</>
)}
</div>

View file

@ -1,9 +1,4 @@
import {
VSCodeButton,
VSCodeTextField,
VSCodeRadioGroup,
VSCodeRadio,
} from "@vscode/webview-ui-toolkit/react"
import { VSCodeButton, VSCodeTextField, VSCodeRadioGroup, VSCodeRadio } from "@vscode/webview-ui-toolkit/react"
import { useExtensionState } from "../../context/ExtensionStateContext"
import { vscode } from "../../utils/vscode"
import { Virtuoso } from "react-virtuoso"
@ -16,33 +11,19 @@ type HistoryViewProps = {
onDone: () => void
}
type SortOption =
| "newest"
| "oldest"
| "mostExpensive"
| "mostTokens"
| "mostRelevant"
type SortOption = "newest" | "oldest" | "mostExpensive" | "mostTokens" | "mostRelevant"
const HistoryView = ({ onDone }: HistoryViewProps) => {
const { taskHistory } = useExtensionState()
const [searchQuery, setSearchQuery] = useState("")
const [sortOption, setSortOption] = useState<SortOption>("newest")
const [lastNonRelevantSort, setLastNonRelevantSort] =
useState<SortOption | null>("newest")
const [lastNonRelevantSort, setLastNonRelevantSort] = useState<SortOption | null>("newest")
useEffect(() => {
if (
searchQuery &&
sortOption !== "mostRelevant" &&
!lastNonRelevantSort
) {
if (searchQuery && sortOption !== "mostRelevant" && !lastNonRelevantSort) {
setLastNonRelevantSort(sortOption)
setSortOption("mostRelevant")
} else if (
!searchQuery &&
sortOption === "mostRelevant" &&
lastNonRelevantSort
) {
} else if (!searchQuery && sortOption === "mostRelevant" && lastNonRelevantSort) {
setSortOption(lastNonRelevantSort)
setLastNonRelevantSort(null)
}
@ -88,9 +69,7 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
}, [presentableTasks])
const taskHistorySearchResults = useMemo(() => {
let results = searchQuery
? highlight(fuse.search(searchQuery))
: presentableTasks
let results = searchQuery ? highlight(fuse.search(searchQuery)) : presentableTasks
results.sort((a, b) => {
switch (sortOption) {
@ -104,10 +83,7 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
(b.tokensOut || 0) +
(b.cacheWrites || 0) +
(b.cacheReads || 0) -
((a.tokensIn || 0) +
(a.tokensOut || 0) +
(a.cacheWrites || 0) +
(a.cacheReads || 0))
((a.tokensIn || 0) + (a.tokensOut || 0) + (a.cacheWrites || 0) + (a.cacheReads || 0))
)
case "mostRelevant":
// NOTE: you must never sort directly on object since it will cause members to be reordered
@ -182,14 +158,9 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
placeholder="Fuzzy search history..."
value={searchQuery}
onInput={(e) => {
const newValue = (e.target as HTMLInputElement)
?.value
const newValue = (e.target as HTMLInputElement)?.value
setSearchQuery(newValue)
if (
newValue &&
!searchQuery &&
sortOption !== "mostRelevant"
) {
if (newValue && !searchQuery && sortOption !== "mostRelevant") {
setLastNonRelevantSort(sortOption)
setSortOption("mostRelevant")
}
@ -220,24 +191,12 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
<VSCodeRadioGroup
style={{ display: "flex", flexWrap: "wrap" }}
value={sortOption}
onChange={(e) =>
setSortOption(
(e.target as HTMLInputElement)
.value as SortOption,
)
}>
onChange={(e) => setSortOption((e.target as HTMLInputElement).value as SortOption)}>
<VSCodeRadio value="newest">Newest</VSCodeRadio>
<VSCodeRadio value="oldest">Oldest</VSCodeRadio>
<VSCodeRadio value="mostExpensive">
Most Expensive
</VSCodeRadio>
<VSCodeRadio value="mostTokens">
Most Tokens
</VSCodeRadio>
<VSCodeRadio
value="mostRelevant"
disabled={!searchQuery}
style={{ opacity: searchQuery ? 1 : 0.5 }}>
<VSCodeRadio value="mostExpensive">Most Expensive</VSCodeRadio>
<VSCodeRadio value="mostTokens">Most Tokens</VSCodeRadio>
<VSCodeRadio value="mostRelevant" disabled={!searchQuery} style={{ opacity: searchQuery ? 1 : 0.5 }}>
Most Relevant
</VSCodeRadio>
</VSCodeRadioGroup>
@ -273,9 +232,7 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
style={{
cursor: "pointer",
borderBottom:
index < taskHistory.length - 1
? "1px solid var(--vscode-panel-border)"
: "none",
index < taskHistory.length - 1 ? "1px solid var(--vscode-panel-border)" : "none",
}}
onClick={() => handleHistorySelect(item.id)}>
<div
@ -376,13 +333,10 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
style={{
fontSize: "12px",
fontWeight: "bold",
marginBottom:
"-2px",
marginBottom: "-2px",
}}
/>
{formatLargeNumber(
item.tokensIn || 0,
)}
{formatLargeNumber(item.tokensIn || 0)}
</span>
<span
style={{
@ -396,20 +350,13 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
style={{
fontSize: "12px",
fontWeight: "bold",
marginBottom:
"-2px",
marginBottom: "-2px",
}}
/>
{formatLargeNumber(
item.tokensOut || 0,
)}
{formatLargeNumber(item.tokensOut || 0)}
</span>
</div>
{!item.totalCost && (
<ExportButton
itemId={item.id}
/>
)}
{!item.totalCost && <ExportButton itemId={item.id} />}
</div>
{!!item.cacheWrites && (
@ -439,14 +386,10 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
style={{
fontSize: "12px",
fontWeight: "bold",
marginBottom:
"-1px",
marginBottom: "-1px",
}}
/>
+
{formatLargeNumber(
item.cacheWrites || 0,
)}
+{formatLargeNumber(item.cacheWrites || 0)}
</span>
<span
style={{
@ -463,9 +406,7 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
marginBottom: 0,
}}
/>
{formatLargeNumber(
item.cacheReads || 0,
)}
{formatLargeNumber(item.cacheReads || 0)}
</span>
</div>
)}
@ -473,8 +414,7 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
<div
style={{
display: "flex",
justifyContent:
"space-between",
justifyContent: "space-between",
alignItems: "center",
marginTop: -2,
}}>
@ -495,15 +435,10 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
style={{
color: "var(--vscode-descriptionForeground)",
}}>
$
{item.totalCost?.toFixed(
4,
)}
${item.totalCost?.toFixed(4)}
</span>
</div>
<ExportButton
itemId={item.id}
/>
<ExportButton itemId={item.id} />
</div>
)}
</div>
@ -525,17 +460,12 @@ const ExportButton = ({ itemId }: { itemId: string }) => (
e.stopPropagation()
vscode.postMessage({ type: "exportTaskWithId", text: itemId })
}}>
<div style={{ fontSize: "11px", fontWeight: 500, opacity: 1 }}>
EXPORT
</div>
<div style={{ fontSize: "11px", fontWeight: 500, opacity: 1 }}>EXPORT</div>
</VSCodeButton>
)
// https://gist.github.com/evenfrost/1ba123656ded32fb7a0cd4651efd4db0
export const highlight = (
fuseSearchResult: FuseResult<any>[],
highlightClassName: string = "history-item-highlight",
) => {
export const highlight = (fuseSearchResult: FuseResult<any>[], highlightClassName: string = "history-item-highlight") => {
const set = (obj: Record<string, any>, path: string, value: any) => {
const pathValue = path.split(".")
let i: number
@ -571,10 +501,7 @@ export const highlight = (
return merged
}
const generateHighlightedText = (
inputText: string,
regions: [number, number][] = [],
) => {
const generateHighlightedText = (inputText: string, regions: [number, number][] = []) => {
if (regions.length === 0) {
return inputText
}
@ -591,10 +518,7 @@ export const highlight = (
const lastRegionNextIndex = end + 1
content += [
inputText.substring(
nextUnhighlightedRegionStartingIndex,
start,
),
inputText.substring(nextUnhighlightedRegionStartingIndex, start),
`<span class="${highlightClassName}">`,
inputText.substring(start, lastRegionNextIndex),
"</span>",
@ -614,18 +538,10 @@ export const highlight = (
const highlightedItem = { ...item }
matches?.forEach((match) => {
if (
match.key &&
typeof match.value === "string" &&
match.indices
) {
if (match.key && typeof match.value === "string" && match.indices) {
// Merge overlapping regions before generating highlighted text
const mergedIndices = mergeRegions([...match.indices])
set(
highlightedItem,
match.key,
generateHighlightedText(match.value, mergedIndices),
)
set(highlightedItem, match.key, generateHighlightedText(match.value, mergedIndices))
}
})

View file

@ -20,13 +20,8 @@ const McpResourceRow = ({ item }: McpResourceRowProps) => {
alignItems: "center",
marginBottom: "4px",
}}>
<span
className={`codicon codicon-symbol-file`}
style={{ marginRight: "6px" }}
/>
<span style={{ fontWeight: 500, wordBreak: "break-all" }}>
{uri}
</span>
<span className={`codicon codicon-symbol-file`} style={{ marginRight: "6px" }} />
<span style={{ fontWeight: 500, wordBreak: "break-all" }}>{uri}</span>
</div>
<div
style={{

View file

@ -12,9 +12,7 @@ const McpToolRow = ({ tool }: McpToolRowProps) => {
padding: "3px 0",
}}>
<div style={{ display: "flex" }}>
<span
className="codicon codicon-symbol-method"
style={{ marginRight: "6px" }}></span>
<span className="codicon codicon-symbol-method" style={{ marginRight: "6px" }}></span>
<span style={{ fontWeight: 500 }}>{tool.name}</span>
</div>
{tool.description && (
@ -30,8 +28,7 @@ const McpToolRow = ({ tool }: McpToolRowProps) => {
)}
{tool.inputSchema &&
"properties" in tool.inputSchema &&
Object.keys(tool.inputSchema.properties as Record<string, any>)
.length > 0 && (
Object.keys(tool.inputSchema.properties as Record<string, any>).length > 0 && (
<div
style={{
marginTop: "8px",
@ -49,9 +46,7 @@ const McpToolRow = ({ tool }: McpToolRowProps) => {
}}>
Parameters
</div>
{Object.entries(
tool.inputSchema.properties as Record<string, any>,
).map(([paramName, schema]) => {
{Object.entries(tool.inputSchema.properties as Record<string, any>).map(([paramName, schema]) => {
const isRequired =
tool.inputSchema &&
"required" in tool.inputSchema &&

View file

@ -1,10 +1,4 @@
import {
VSCodeButton,
VSCodeLink,
VSCodePanels,
VSCodePanelTab,
VSCodePanelView,
} from "@vscode/webview-ui-toolkit/react"
import { VSCodeButton, VSCodeLink, VSCodePanels, VSCodePanelTab, VSCodePanelView } from "@vscode/webview-ui-toolkit/react"
import { useState } from "react"
import { vscode } from "../../utils/vscode"
import { useExtensionState } from "../../context/ExtensionStateContext"
@ -97,9 +91,7 @@ const McpView = ({ onDone }: McpViewProps) => {
alignItems: "center",
padding: "10px 17px 10px 20px",
}}>
<h3 style={{ color: "var(--vscode-foreground)", margin: 0 }}>
MCP Servers
</h3>
<h3 style={{ color: "var(--vscode-foreground)", margin: 0 }}>MCP Servers</h3>
<VSCodeButton onClick={onDone}>Done</VSCodeButton>
</div>
@ -112,24 +104,16 @@ const McpView = ({ onDone }: McpViewProps) => {
marginTop: "5px",
}}>
The{" "}
<VSCodeLink
href="https://github.com/modelcontextprotocol"
style={{ display: "inline" }}>
<VSCodeLink href="https://github.com/modelcontextprotocol" style={{ display: "inline" }}>
Model Context Protocol
</VSCodeLink>{" "}
enables communication with locally running MCP servers that
provide additional tools and resources to extend Cline's
capabilities. You can use{" "}
<VSCodeLink
href="https://github.com/modelcontextprotocol/servers"
style={{ display: "inline" }}>
enables communication with locally running MCP servers that provide additional tools and resources to extend
Cline's capabilities. You can use{" "}
<VSCodeLink href="https://github.com/modelcontextprotocol/servers" style={{ display: "inline" }}>
community-made servers
</VSCodeLink>{" "}
or ask Cline to create new tools specific to your workflow
(e.g., "add a tool that gets the latest npm docs").{" "}
<VSCodeLink
href="https://x.com/sdrzn/status/1867271665086074969"
style={{ display: "inline" }}>
or ask Cline to create new tools specific to your workflow (e.g., "add a tool that gets the latest npm docs").{" "}
<VSCodeLink href="https://x.com/sdrzn/status/1867271665086074969" style={{ display: "inline" }}>
See a demo here.
</VSCodeLink>
</div>
@ -156,9 +140,7 @@ const McpView = ({ onDone }: McpViewProps) => {
onClick={() => {
vscode.postMessage({ type: "openMcpSettings" })
}}>
<span
className="codicon codicon-edit"
style={{ marginRight: "6px" }}></span>
<span className="codicon codicon-edit" style={{ marginRight: "6px" }}></span>
Edit MCP Settings
</VSCodeButton>
</div>
@ -207,15 +189,11 @@ const ServerRow = ({ server }: { server: McpServer }) => {
padding: "8px",
background: "var(--vscode-textCodeBlock-background)",
cursor: server.error ? "default" : "pointer",
borderRadius:
isExpanded || server.error ? "4px 4px 0 0" : "4px",
borderRadius: isExpanded || server.error ? "4px 4px 0 0" : "4px",
}}
onClick={handleRowClick}>
{!server.error && (
<span
className={`codicon codicon-chevron-${isExpanded ? "down" : "right"}`}
style={{ marginRight: "8px" }}
/>
<span className={`codicon codicon-chevron-${isExpanded ? "down" : "right"}`} style={{ marginRight: "8px" }} />
)}
<span style={{ flex: 1 }}>{server.name}</span>
<div
@ -255,32 +233,22 @@ const ServerRow = ({ server }: { server: McpServer }) => {
width: "calc(100% - 20px)",
margin: "0 10px 10px 10px",
}}>
{server.status === "connecting"
? "Retrying..."
: "Retry Connection"}
{server.status === "connecting" ? "Retrying..." : "Retry Connection"}
</VSCodeButton>
</div>
) : (
isExpanded && (
<div
style={{
background:
"var(--vscode-textCodeBlock-background)",
background: "var(--vscode-textCodeBlock-background)",
padding: "0 10px 10px 10px",
fontSize: "13px",
borderRadius: "0 0 4px 4px",
}}>
<VSCodePanels>
<VSCodePanelTab id="tools">
Tools ({server.tools?.length || 0})
</VSCodePanelTab>
<VSCodePanelTab id="tools">Tools ({server.tools?.length || 0})</VSCodePanelTab>
<VSCodePanelTab id="resources">
Resources (
{[
...(server.resourceTemplates || []),
...(server.resources || []),
].length || 0}
)
Resources ({[...(server.resourceTemplates || []), ...(server.resources || [])].length || 0})
</VSCodePanelTab>
<VSCodePanelView id="tools-view">
@ -293,10 +261,7 @@ const ServerRow = ({ server }: { server: McpServer }) => {
width: "100%",
}}>
{server.tools.map((tool) => (
<McpToolRow
key={tool.name}
tool={tool}
/>
<McpToolRow key={tool.name} tool={tool} />
))}
</div>
) : (
@ -311,10 +276,8 @@ const ServerRow = ({ server }: { server: McpServer }) => {
</VSCodePanelView>
<VSCodePanelView id="resources-view">
{(server.resources &&
server.resources.length > 0) ||
(server.resourceTemplates &&
server.resourceTemplates.length > 0) ? (
{(server.resources && server.resources.length > 0) ||
(server.resourceTemplates && server.resourceTemplates.length > 0) ? (
<div
style={{
display: "flex",
@ -322,16 +285,9 @@ const ServerRow = ({ server }: { server: McpServer }) => {
gap: "8px",
width: "100%",
}}>
{[
...(server.resourceTemplates || []),
...(server.resources || []),
].map((item) => (
{[...(server.resourceTemplates || []), ...(server.resources || [])].map((item) => (
<McpResourceRow
key={
"uriTemplate" in item
? item.uriTemplate
: item.uri
}
key={"uriTemplate" in item ? item.uriTemplate : item.uri}
item={item}
/>
))}
@ -356,9 +312,7 @@ const ServerRow = ({ server }: { server: McpServer }) => {
width: "calc(100% - 14px)",
margin: "0 7px 3px 7px",
}}>
{server.status === "connecting"
? "Restarting..."
: "Restart Server"}
{server.status === "connecting" ? "Restarting..." : "Restart Server"}
</VSCodeButton>
</div>
)

View file

@ -7,14 +7,7 @@ import {
VSCodeRadioGroup,
VSCodeTextField,
} from "@vscode/webview-ui-toolkit/react"
import {
Fragment,
memo,
useCallback,
useEffect,
useMemo,
useState,
} from "react"
import { Fragment, memo, useCallback, useEffect, useMemo, useState } from "react"
import { useEvent, useInterval } from "react-use"
import {
ApiConfiguration,
@ -40,10 +33,7 @@ import { ExtensionMessage } from "../../../../src/shared/ExtensionMessage"
import { useExtensionState } from "../../context/ExtensionStateContext"
import { vscode } from "../../utils/vscode"
import VSCodeButtonLink from "../common/VSCodeButtonLink"
import OpenRouterModelPicker, {
ModelDescriptionMarkdown,
OPENROUTER_MODEL_PICKER_Z_INDEX,
} from "./OpenRouterModelPicker"
import OpenRouterModelPicker, { ModelDescriptionMarkdown, OPENROUTER_MODEL_PICKER_Z_INDEX } from "./OpenRouterModelPicker"
interface ApiOptionsProps {
showModelOptions: boolean
@ -51,35 +41,24 @@ interface ApiOptionsProps {
modelIdErrorMessage?: string
}
const ApiOptions = ({
showModelOptions,
apiErrorMessage,
modelIdErrorMessage,
}: ApiOptionsProps) => {
const { apiConfiguration, setApiConfiguration, uriScheme } =
useExtensionState()
const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) => {
const { apiConfiguration, setApiConfiguration, uriScheme } = useExtensionState()
const [ollamaModels, setOllamaModels] = useState<string[]>([])
const [lmStudioModels, setLmStudioModels] = useState<string[]>([])
const [anthropicBaseUrlSelected, setAnthropicBaseUrlSelected] = useState(
!!apiConfiguration?.anthropicBaseUrl,
)
const [azureApiVersionSelected, setAzureApiVersionSelected] = useState(
!!apiConfiguration?.azureApiVersion,
)
const [anthropicBaseUrlSelected, setAnthropicBaseUrlSelected] = useState(!!apiConfiguration?.anthropicBaseUrl)
const [azureApiVersionSelected, setAzureApiVersionSelected] = useState(!!apiConfiguration?.azureApiVersion)
const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false)
const handleInputChange =
(field: keyof ApiConfiguration) => (event: any) => {
setApiConfiguration({
...apiConfiguration,
[field]: event.target.value,
})
}
const handleInputChange = (field: keyof ApiConfiguration) => (event: any) => {
setApiConfiguration({
...apiConfiguration,
[field]: event.target.value,
})
}
const { selectedProvider, selectedModelId, selectedModelInfo } =
useMemo(() => {
return normalizeApiConfiguration(apiConfiguration)
}, [apiConfiguration])
const { selectedProvider, selectedModelId, selectedModelInfo } = useMemo(() => {
return normalizeApiConfiguration(apiConfiguration)
}, [apiConfiguration])
// Poll ollama/lmstudio models
const requestLocalModels = useCallback(() => {
@ -94,31 +73,19 @@ const ApiOptions = ({
text: apiConfiguration?.lmStudioBaseUrl,
})
}
}, [
selectedProvider,
apiConfiguration?.ollamaBaseUrl,
apiConfiguration?.lmStudioBaseUrl,
])
}, [selectedProvider, apiConfiguration?.ollamaBaseUrl, apiConfiguration?.lmStudioBaseUrl])
useEffect(() => {
if (selectedProvider === "ollama" || selectedProvider === "lmstudio") {
requestLocalModels()
}
}, [selectedProvider, requestLocalModels])
useInterval(
requestLocalModels,
selectedProvider === "ollama" || selectedProvider === "lmstudio"
? 2000
: null,
)
useInterval(requestLocalModels, selectedProvider === "ollama" || selectedProvider === "lmstudio" ? 2000 : null)
const handleMessage = useCallback((event: MessageEvent) => {
const message: ExtensionMessage = event.data
if (message.type === "ollamaModels" && message.ollamaModels) {
setOllamaModels(message.ollamaModels)
} else if (
message.type === "lmStudioModels" &&
message.lmStudioModels
) {
} else if (message.type === "lmStudioModels" && message.lmStudioModels) {
setLmStudioModels(message.lmStudioModels)
}
}, [])
@ -178,9 +145,7 @@ const ApiOptions = ({
<VSCodeOption value="vertex">GCP Vertex AI</VSCodeOption>
<VSCodeOption value="bedrock">AWS Bedrock</VSCodeOption>
<VSCodeOption value="openai-native">OpenAI</VSCodeOption>
<VSCodeOption value="openai">
OpenAI Compatible
</VSCodeOption>
<VSCodeOption value="openai">OpenAI Compatible</VSCodeOption>
<VSCodeOption value="lmstudio">LM Studio</VSCodeOption>
<VSCodeOption value="ollama">Ollama</VSCodeOption>
</VSCodeDropdown>
@ -194,9 +159,7 @@ const ApiOptions = ({
type="password"
onInput={handleInputChange("apiKey")}
placeholder="Enter API Key...">
<span style={{ fontWeight: 500 }}>
Anthropic API Key
</span>
<span style={{ fontWeight: 500 }}>Anthropic API Key</span>
</VSCodeTextField>
<VSCodeCheckbox
@ -230,8 +193,7 @@ const ApiOptions = ({
marginTop: 3,
color: "var(--vscode-descriptionForeground)",
}}>
This key is stored locally and only used to make API
requests from this extension.
This key is stored locally and only used to make API requests from this extension.
{!apiConfiguration?.apiKey && (
<VSCodeLink
href="https://console.anthropic.com/settings/keys"
@ -239,8 +201,7 @@ const ApiOptions = ({
display: "inline",
fontSize: "inherit",
}}>
You can get an Anthropic API key by signing up
here.
You can get an Anthropic API key by signing up here.
</VSCodeLink>
)}
</p>
@ -263,8 +224,7 @@ const ApiOptions = ({
marginTop: 3,
color: "var(--vscode-descriptionForeground)",
}}>
This key is stored locally and only used to make API
requests from this extension.
This key is stored locally and only used to make API requests from this extension.
{!apiConfiguration?.openAiNativeApiKey && (
<VSCodeLink
href="https://platform.openai.com/api-keys"
@ -272,8 +232,7 @@ const ApiOptions = ({
display: "inline",
fontSize: "inherit",
}}>
You can get an OpenAI API key by signing up
here.
You can get an OpenAI API key by signing up here.
</VSCodeLink>
)}
</p>
@ -288,9 +247,7 @@ const ApiOptions = ({
type="password"
onInput={handleInputChange("deepSeekApiKey")}
placeholder="Enter API Key...">
<span style={{ fontWeight: 500 }}>
DeepSeek API Key
</span>
<span style={{ fontWeight: 500 }}>DeepSeek API Key</span>
</VSCodeTextField>
<p
style={{
@ -298,8 +255,7 @@ const ApiOptions = ({
marginTop: 3,
color: "var(--vscode-descriptionForeground)",
}}>
This key is stored locally and only used to make API
requests from this extension.
This key is stored locally and only used to make API requests from this extension.
{!apiConfiguration?.deepSeekApiKey && (
<VSCodeLink
href="https://www.deepseek.com/"
@ -307,8 +263,7 @@ const ApiOptions = ({
display: "inline",
fontSize: "inherit",
}}>
You can get a DeepSeek API key by signing up
here.
You can get a DeepSeek API key by signing up here.
</VSCodeLink>
)}
</p>
@ -323,9 +278,7 @@ const ApiOptions = ({
type="password"
onInput={handleInputChange("openRouterApiKey")}
placeholder="Enter API Key...">
<span style={{ fontWeight: 500 }}>
OpenRouter API Key
</span>
<span style={{ fontWeight: 500 }}>OpenRouter API Key</span>
</VSCodeTextField>
{!apiConfiguration?.openRouterApiKey && (
<VSCodeButtonLink
@ -341,8 +294,7 @@ const ApiOptions = ({
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}}>
This key is stored locally and only used to make API
requests from this extension.{" "}
This key is stored locally and only used to make API requests from this extension.{" "}
{/* {!apiConfiguration?.openRouterApiKey && (
<span style={{ color: "var(--vscode-charts-green)" }}>
(<span style={{ fontWeight: 500 }}>Note:</span> OpenRouter is recommended for high rate
@ -382,9 +334,7 @@ const ApiOptions = ({
type="password"
onInput={handleInputChange("awsSessionToken")}
placeholder="Enter Session Token...">
<span style={{ fontWeight: 500 }}>
AWS Session Token
</span>
<span style={{ fontWeight: 500 }}>AWS Session Token</span>
</VSCodeTextField>
<div className="dropdown-container">
<label htmlFor="aws-region-dropdown">
@ -395,75 +345,36 @@ const ApiOptions = ({
value={apiConfiguration?.awsRegion || ""}
style={{ width: "100%" }}
onChange={handleInputChange("awsRegion")}>
<VSCodeOption value="">
Select a region...
</VSCodeOption>
<VSCodeOption value="">Select a region...</VSCodeOption>
{/* The user will have to choose a region that supports the model they use, but this shouldn't be a problem since they'd have to request access for it in that region in the first place. */}
<VSCodeOption value="us-east-1">
us-east-1
</VSCodeOption>
<VSCodeOption value="us-east-2">
us-east-2
</VSCodeOption>
<VSCodeOption value="us-east-1">us-east-1</VSCodeOption>
<VSCodeOption value="us-east-2">us-east-2</VSCodeOption>
{/* <VSCodeOption value="us-west-1">us-west-1</VSCodeOption> */}
<VSCodeOption value="us-west-2">
us-west-2
</VSCodeOption>
<VSCodeOption value="us-west-2">us-west-2</VSCodeOption>
{/* <VSCodeOption value="af-south-1">af-south-1</VSCodeOption> */}
{/* <VSCodeOption value="ap-east-1">ap-east-1</VSCodeOption> */}
<VSCodeOption value="ap-south-1">
ap-south-1
</VSCodeOption>
<VSCodeOption value="ap-northeast-1">
ap-northeast-1
</VSCodeOption>
<VSCodeOption value="ap-northeast-2">
ap-northeast-2
</VSCodeOption>
<VSCodeOption value="ap-south-1">ap-south-1</VSCodeOption>
<VSCodeOption value="ap-northeast-1">ap-northeast-1</VSCodeOption>
<VSCodeOption value="ap-northeast-2">ap-northeast-2</VSCodeOption>
{/* <VSCodeOption value="ap-northeast-3">ap-northeast-3</VSCodeOption> */}
<VSCodeOption value="ap-southeast-1">
ap-southeast-1
</VSCodeOption>
<VSCodeOption value="ap-southeast-2">
ap-southeast-2
</VSCodeOption>
<VSCodeOption value="ca-central-1">
ca-central-1
</VSCodeOption>
<VSCodeOption value="eu-central-1">
eu-central-1
</VSCodeOption>
<VSCodeOption value="eu-central-2">
eu-central-2
</VSCodeOption>
<VSCodeOption value="eu-west-1">
eu-west-1
</VSCodeOption>
<VSCodeOption value="eu-west-2">
eu-west-2
</VSCodeOption>
<VSCodeOption value="eu-west-3">
eu-west-3
</VSCodeOption>
<VSCodeOption value="ap-southeast-1">ap-southeast-1</VSCodeOption>
<VSCodeOption value="ap-southeast-2">ap-southeast-2</VSCodeOption>
<VSCodeOption value="ca-central-1">ca-central-1</VSCodeOption>
<VSCodeOption value="eu-central-1">eu-central-1</VSCodeOption>
<VSCodeOption value="eu-central-2">eu-central-2</VSCodeOption>
<VSCodeOption value="eu-west-1">eu-west-1</VSCodeOption>
<VSCodeOption value="eu-west-2">eu-west-2</VSCodeOption>
<VSCodeOption value="eu-west-3">eu-west-3</VSCodeOption>
{/* <VSCodeOption value="eu-north-1">eu-north-1</VSCodeOption> */}
{/* <VSCodeOption value="me-south-1">me-south-1</VSCodeOption> */}
<VSCodeOption value="sa-east-1">
sa-east-1
</VSCodeOption>
<VSCodeOption value="us-gov-east-1">
us-gov-east-1
</VSCodeOption>
<VSCodeOption value="us-gov-west-1">
us-gov-west-1
</VSCodeOption>
<VSCodeOption value="sa-east-1">sa-east-1</VSCodeOption>
<VSCodeOption value="us-gov-east-1">us-gov-east-1</VSCodeOption>
<VSCodeOption value="us-gov-west-1">us-gov-west-1</VSCodeOption>
{/* <VSCodeOption value="us-gov-east-1">us-gov-east-1</VSCodeOption> */}
</VSCodeDropdown>
</div>
<VSCodeCheckbox
checked={
apiConfiguration?.awsUseCrossRegionInference ||
false
}
checked={apiConfiguration?.awsUseCrossRegionInference || false}
onChange={(e: any) => {
const isChecked = e.target.checked === true
setApiConfiguration({
@ -479,10 +390,8 @@ const ApiOptions = ({
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}}>
Authenticate by either providing the keys above or use
the default AWS credential providers, i.e.
~/.aws/credentials or environment variables. These
credentials are only used locally to make API requests
Authenticate by either providing the keys above or use the default AWS credential providers, i.e.
~/.aws/credentials or environment variables. These credentials are only used locally to make API requests
from this extension.
</p>
</div>
@ -500,39 +409,23 @@ const ApiOptions = ({
style={{ width: "100%" }}
onInput={handleInputChange("vertexProjectId")}
placeholder="Enter Project ID...">
<span style={{ fontWeight: 500 }}>
Google Cloud Project ID
</span>
<span style={{ fontWeight: 500 }}>Google Cloud Project ID</span>
</VSCodeTextField>
<div className="dropdown-container">
<label htmlFor="vertex-region-dropdown">
<span style={{ fontWeight: 500 }}>
Google Cloud Region
</span>
<span style={{ fontWeight: 500 }}>Google Cloud Region</span>
</label>
<VSCodeDropdown
id="vertex-region-dropdown"
value={apiConfiguration?.vertexRegion || ""}
style={{ width: "100%" }}
onChange={handleInputChange("vertexRegion")}>
<VSCodeOption value="">
Select a region...
</VSCodeOption>
<VSCodeOption value="us-east5">
us-east5
</VSCodeOption>
<VSCodeOption value="us-central1">
us-central1
</VSCodeOption>
<VSCodeOption value="europe-west1">
europe-west1
</VSCodeOption>
<VSCodeOption value="europe-west4">
europe-west4
</VSCodeOption>
<VSCodeOption value="asia-southeast1">
asia-southeast1
</VSCodeOption>
<VSCodeOption value="">Select a region...</VSCodeOption>
<VSCodeOption value="us-east5">us-east5</VSCodeOption>
<VSCodeOption value="us-central1">us-central1</VSCodeOption>
<VSCodeOption value="europe-west1">europe-west1</VSCodeOption>
<VSCodeOption value="europe-west4">europe-west4</VSCodeOption>
<VSCodeOption value="asia-southeast1">asia-southeast1</VSCodeOption>
</VSCodeDropdown>
</div>
<p
@ -545,16 +438,12 @@ const ApiOptions = ({
<VSCodeLink
href="https://cloud.google.com/vertex-ai/generative-ai/docs/partner-models/use-claude#before_you_begin"
style={{ display: "inline", fontSize: "inherit" }}>
{
"1) create a Google Cloud account enable the Vertex AI API enable the desired Claude models,"
}
{"1) create a Google Cloud account enable the Vertex AI API enable the desired Claude models,"}
</VSCodeLink>{" "}
<VSCodeLink
href="https://cloud.google.com/docs/authentication/provide-credentials-adc#google-idp"
style={{ display: "inline", fontSize: "inherit" }}>
{
"2) install the Google Cloud CLI configure Application Default Credentials."
}
{"2) install the Google Cloud CLI configure Application Default Credentials."}
</VSCodeLink>
</p>
</div>
@ -576,8 +465,7 @@ const ApiOptions = ({
marginTop: 3,
color: "var(--vscode-descriptionForeground)",
}}>
This key is stored locally and only used to make API
requests from this extension.
This key is stored locally and only used to make API requests from this extension.
{!apiConfiguration?.geminiApiKey && (
<VSCodeLink
href="https://ai.google.dev/"
@ -645,12 +533,9 @@ const ApiOptions = ({
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 style={{ color: "var(--vscode-errorForeground)" }}>
(<span style={{ fontWeight: 500 }}>Note:</span> Cline uses complex prompts and works best with Claude
models. Less capable models may not work as expected.)
</span>
</p>
</div>
@ -664,9 +549,7 @@ const ApiOptions = ({
type="url"
onInput={handleInputChange("lmStudioBaseUrl")}
placeholder={"Default: http://localhost:1234"}>
<span style={{ fontWeight: 500 }}>
Base URL (optional)
</span>
<span style={{ fontWeight: 500 }}>Base URL (optional)</span>
</VSCodeTextField>
<VSCodeTextField
value={apiConfiguration?.lmStudioModelId || ""}
@ -678,15 +561,12 @@ const ApiOptions = ({
{lmStudioModels.length > 0 && (
<VSCodeRadioGroup
value={
lmStudioModels.includes(
apiConfiguration?.lmStudioModelId || "",
)
lmStudioModels.includes(apiConfiguration?.lmStudioModelId || "")
? apiConfiguration?.lmStudioModelId
: ""
}
onChange={(e) => {
const value = (e.target as HTMLInputElement)
?.value
const value = (e.target as HTMLInputElement)?.value
// need to check value first since radio group returns empty string sometimes
if (value) {
handleInputChange("lmStudioModelId")({
@ -695,13 +575,7 @@ const ApiOptions = ({
}
}}>
{lmStudioModels.map((model) => (
<VSCodeRadio
key={model}
value={model}
checked={
apiConfiguration?.lmStudioModelId ===
model
}>
<VSCodeRadio key={model} value={model} checked={apiConfiguration?.lmStudioModelId === model}>
{model}
</VSCodeRadio>
))}
@ -713,12 +587,9 @@ const ApiOptions = ({
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}}>
LM Studio allows you to run models locally on your
computer. For instructions on how to get started, see
LM Studio allows you to run models locally on your computer. For instructions on how to get started, see
their
<VSCodeLink
href="https://lmstudio.ai/docs"
style={{ display: "inline", fontSize: "inherit" }}>
<VSCodeLink href="https://lmstudio.ai/docs" style={{ display: "inline", fontSize: "inherit" }}>
quickstart guide.
</VSCodeLink>
You will also need to start LM Studio's{" "}
@ -728,12 +599,9 @@ const ApiOptions = ({
local server
</VSCodeLink>{" "}
feature to use it with this extension.{" "}
<span
style={{ color: "var(--vscode-errorForeground)" }}>
(<span style={{ fontWeight: 500 }}>Note:</span>{" "}
Cline uses complex prompts and works best with
Claude models. Less capable models may not work as
expected.)
<span style={{ color: "var(--vscode-errorForeground)" }}>
(<span style={{ fontWeight: 500 }}>Note:</span> Cline uses complex prompts and works best with Claude
models. Less capable models may not work as expected.)
</span>
</p>
</div>
@ -747,9 +615,7 @@ const ApiOptions = ({
type="url"
onInput={handleInputChange("ollamaBaseUrl")}
placeholder={"Default: http://localhost:11434"}>
<span style={{ fontWeight: 500 }}>
Base URL (optional)
</span>
<span style={{ fontWeight: 500 }}>Base URL (optional)</span>
</VSCodeTextField>
<VSCodeTextField
value={apiConfiguration?.ollamaModelId || ""}
@ -761,15 +627,12 @@ const ApiOptions = ({
{ollamaModels.length > 0 && (
<VSCodeRadioGroup
value={
ollamaModels.includes(
apiConfiguration?.ollamaModelId || "",
)
ollamaModels.includes(apiConfiguration?.ollamaModelId || "")
? apiConfiguration?.ollamaModelId
: ""
}
onChange={(e) => {
const value = (e.target as HTMLInputElement)
?.value
const value = (e.target as HTMLInputElement)?.value
// need to check value first since radio group returns empty string sometimes
if (value) {
handleInputChange("ollamaModelId")({
@ -778,13 +641,7 @@ const ApiOptions = ({
}
}}>
{ollamaModels.map((model) => (
<VSCodeRadio
key={model}
value={model}
checked={
apiConfiguration?.ollamaModelId ===
model
}>
<VSCodeRadio key={model} value={model} checked={apiConfiguration?.ollamaModelId === model}>
{model}
</VSCodeRadio>
))}
@ -796,20 +653,16 @@ const ApiOptions = ({
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}}>
Ollama allows you to run models locally on your
computer. For instructions on how to get started, see
Ollama allows you to run models locally on your computer. For instructions on how to get started, see
their
<VSCodeLink
href="https://github.com/ollama/ollama/blob/main/README.md"
style={{ display: "inline", fontSize: "inherit" }}>
quickstart guide.
</VSCodeLink>
<span
style={{ color: "var(--vscode-errorForeground)" }}>
(<span style={{ fontWeight: 500 }}>Note:</span>{" "}
Cline uses complex prompts and works best with
Claude models. Less capable models may not work as
expected.)
<span style={{ color: "var(--vscode-errorForeground)" }}>
(<span style={{ fontWeight: 500 }}>Note:</span> Cline uses complex prompts and works best with Claude
models. Less capable models may not work as expected.)
</span>
</p>
</div>
@ -826,9 +679,7 @@ const ApiOptions = ({
</p>
)}
{selectedProvider === "openrouter" && showModelOptions && (
<OpenRouterModelPicker />
)}
{selectedProvider === "openrouter" && showModelOptions && <OpenRouterModelPicker />}
{selectedProvider !== "openrouter" &&
selectedProvider !== "openai" &&
@ -840,18 +691,12 @@ const ApiOptions = ({
<label htmlFor="model-id">
<span style={{ fontWeight: 500 }}>Model</span>
</label>
{selectedProvider === "anthropic" &&
createDropdown(anthropicModels)}
{selectedProvider === "bedrock" &&
createDropdown(bedrockModels)}
{selectedProvider === "vertex" &&
createDropdown(vertexModels)}
{selectedProvider === "gemini" &&
createDropdown(geminiModels)}
{selectedProvider === "openai-native" &&
createDropdown(openAiNativeModels)}
{selectedProvider === "deepseek" &&
createDropdown(deepSeekModels)}
{selectedProvider === "anthropic" && createDropdown(anthropicModels)}
{selectedProvider === "bedrock" && createDropdown(bedrockModels)}
{selectedProvider === "vertex" && createDropdown(vertexModels)}
{selectedProvider === "gemini" && createDropdown(geminiModels)}
{selectedProvider === "openai-native" && createDropdown(openAiNativeModels)}
{selectedProvider === "deepseek" && createDropdown(deepSeekModels)}
</div>
<ModelInfoView
@ -934,44 +779,36 @@ export const ModelInfoView = ({
),
modelInfo.maxTokens !== undefined && modelInfo.maxTokens > 0 && (
<span key="maxTokens">
<span style={{ fontWeight: 500 }}>Max output:</span>{" "}
{modelInfo.maxTokens?.toLocaleString()} tokens
<span style={{ fontWeight: 500 }}>Max output:</span> {modelInfo.maxTokens?.toLocaleString()} tokens
</span>
),
modelInfo.inputPrice !== undefined && modelInfo.inputPrice > 0 && (
<span key="inputPrice">
<span style={{ fontWeight: 500 }}>Input price:</span>{" "}
{formatPrice(modelInfo.inputPrice)}/million tokens
<span style={{ fontWeight: 500 }}>Input price:</span> {formatPrice(modelInfo.inputPrice)}/million tokens
</span>
),
modelInfo.supportsPromptCache && modelInfo.cacheWritesPrice && (
<span key="cacheWritesPrice">
<span style={{ fontWeight: 500 }}>Cache writes price:</span>{" "}
{formatPrice(modelInfo.cacheWritesPrice || 0)}/million tokens
<span style={{ fontWeight: 500 }}>Cache writes price:</span> {formatPrice(modelInfo.cacheWritesPrice || 0)}
/million tokens
</span>
),
modelInfo.supportsPromptCache && modelInfo.cacheReadsPrice && (
<span key="cacheReadsPrice">
<span style={{ fontWeight: 500 }}>Cache reads price:</span>{" "}
{formatPrice(modelInfo.cacheReadsPrice || 0)}/million tokens
<span style={{ fontWeight: 500 }}>Cache reads price:</span> {formatPrice(modelInfo.cacheReadsPrice || 0)}/million
tokens
</span>
),
modelInfo.outputPrice !== undefined && modelInfo.outputPrice > 0 && (
<span key="outputPrice">
<span style={{ fontWeight: 500 }}>Output price:</span>{" "}
{formatPrice(modelInfo.outputPrice)}/million tokens
<span style={{ fontWeight: 500 }}>Output price:</span> {formatPrice(modelInfo.outputPrice)}/million tokens
</span>
),
isGemini && (
<span key="geminiInfo" style={{ fontStyle: "italic" }}>
* Free up to{" "}
{selectedModelId && selectedModelId.includes("flash")
? "15"
: "2"}{" "}
requests per minute. After that, billing depends on prompt size.{" "}
<VSCodeLink
href="https://ai.google.dev/pricing"
style={{ display: "inline", fontSize: "inherit" }}>
* Free up to {selectedModelId && selectedModelId.includes("flash") ? "15" : "2"} requests per minute. After that,
billing depends on prompt size.{" "}
<VSCodeLink href="https://ai.google.dev/pricing" style={{ display: "inline", fontSize: "inherit" }}>
For more info, see pricing details.
</VSCodeLink>
</span>
@ -1007,9 +844,7 @@ const ModelInfoSupportsItem = ({
<span
style={{
fontWeight: 500,
color: isSupported
? "var(--vscode-charts-green)"
: "var(--vscode-errorForeground)",
color: isSupported ? "var(--vscode-charts-green)" : "var(--vscode-errorForeground)",
}}>
<i
className={`codicon codicon-${isSupported ? "check" : "x"}`}
@ -1029,10 +864,7 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration) {
const provider = apiConfiguration?.apiProvider || "anthropic"
const modelId = apiConfiguration?.apiModelId
const getProviderData = (
models: Record<string, ModelInfo>,
defaultId: string,
) => {
const getProviderData = (models: Record<string, ModelInfo>, defaultId: string) => {
let selectedModelId: string
let selectedModelInfo: ModelInfo
if (modelId && modelId in models) {
@ -1058,21 +890,14 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration) {
case "gemini":
return getProviderData(geminiModels, geminiDefaultModelId)
case "openai-native":
return getProviderData(
openAiNativeModels,
openAiNativeDefaultModelId,
)
return getProviderData(openAiNativeModels, openAiNativeDefaultModelId)
case "deepseek":
return getProviderData(deepSeekModels, deepSeekDefaultModelId)
case "openrouter":
return {
selectedProvider: provider,
selectedModelId:
apiConfiguration?.openRouterModelId ||
openRouterDefaultModelId,
selectedModelInfo:
apiConfiguration?.openRouterModelInfo ||
openRouterDefaultModelInfo,
selectedModelId: apiConfiguration?.openRouterModelId || openRouterDefaultModelId,
selectedModelInfo: apiConfiguration?.openRouterModelInfo || openRouterDefaultModelInfo,
}
case "openai":
return {

View file

@ -1,13 +1,6 @@
import { VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import Fuse from "fuse.js"
import React, {
KeyboardEvent,
memo,
useEffect,
useMemo,
useRef,
useState,
} from "react"
import React, { KeyboardEvent, memo, useEffect, useMemo, useRef, useState } from "react"
import { useRemark } from "react-remark"
import { useMount } from "react-use"
import styled from "styled-components"
@ -18,11 +11,8 @@ import { highlight } from "../history/HistoryView"
import { ModelInfoView, normalizeApiConfiguration } from "./ApiOptions"
const OpenRouterModelPicker: React.FC = () => {
const { apiConfiguration, setApiConfiguration, openRouterModels } =
useExtensionState()
const [searchTerm, setSearchTerm] = useState(
apiConfiguration?.openRouterModelId || openRouterDefaultModelId,
)
const { apiConfiguration, setApiConfiguration, openRouterModels } = useExtensionState()
const [searchTerm, setSearchTerm] = useState(apiConfiguration?.openRouterModelId || openRouterDefaultModelId)
const [isDropdownVisible, setIsDropdownVisible] = useState(false)
const [selectedIndex, setSelectedIndex] = useState(-1)
const dropdownRef = useRef<HTMLDivElement>(null)
@ -50,10 +40,7 @@ const OpenRouterModelPicker: React.FC = () => {
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
dropdownRef.current &&
!dropdownRef.current.contains(event.target as Node)
) {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsDropdownVisible(false)
}
}
@ -101,9 +88,7 @@ const OpenRouterModelPicker: React.FC = () => {
switch (event.key) {
case "ArrowDown":
event.preventDefault()
setSelectedIndex((prev) =>
prev < modelSearchResults.length - 1 ? prev + 1 : prev,
)
setSelectedIndex((prev) => (prev < modelSearchResults.length - 1 ? prev + 1 : prev))
break
case "ArrowUp":
event.preventDefault()
@ -111,10 +96,7 @@ const OpenRouterModelPicker: React.FC = () => {
break
case "Enter":
event.preventDefault()
if (
selectedIndex >= 0 &&
selectedIndex < modelSearchResults.length
) {
if (selectedIndex >= 0 && selectedIndex < modelSearchResults.length) {
handleModelChange(modelSearchResults[selectedIndex].id)
setIsDropdownVisible(false)
}
@ -127,9 +109,7 @@ const OpenRouterModelPicker: React.FC = () => {
}
const hasInfo = useMemo(() => {
return modelIds.some(
(id) => id.toLowerCase() === searchTerm.toLowerCase(),
)
return modelIds.some((id) => id.toLowerCase() === searchTerm.toLowerCase())
}, [modelIds, searchTerm])
useEffect(() => {
@ -168,11 +148,7 @@ const OpenRouterModelPicker: React.FC = () => {
placeholder="Search and select a model..."
value={searchTerm}
onInput={(e) => {
handleModelChange(
(
e.target as HTMLInputElement
)?.value?.toLowerCase(),
)
handleModelChange((e.target as HTMLInputElement)?.value?.toLowerCase())
setIsDropdownVisible(true)
}}
onFocus={() => setIsDropdownVisible(true)}
@ -236,26 +212,17 @@ const OpenRouterModelPicker: React.FC = () => {
marginTop: 0,
color: "var(--vscode-descriptionForeground)",
}}>
The extension automatically fetches the latest list of
models available on{" "}
<VSCodeLink
style={{ display: "inline", fontSize: "inherit" }}
href="https://openrouter.ai/models">
The extension automatically fetches the latest list of models available on{" "}
<VSCodeLink style={{ display: "inline", fontSize: "inherit" }} href="https://openrouter.ai/models">
OpenRouter.
</VSCodeLink>
If you're unsure which model to choose, Cline works best
with{" "}
If you're unsure which model to choose, Cline works best with{" "}
<VSCodeLink
style={{ display: "inline", fontSize: "inherit" }}
onClick={() =>
handleModelChange(
"anthropic/claude-3.5-sonnet:beta",
)
}>
onClick={() => handleModelChange("anthropic/claude-3.5-sonnet:beta")}>
anthropic/claude-3.5-sonnet:beta.
</VSCodeLink>
You can also try searching "free" for no-cost options
currently available.
You can also try searching "free" for no-cost options currently available.
</p>
)}
</>
@ -293,10 +260,7 @@ const DropdownItem = styled.div<{ isSelected: boolean }>`
word-break: break-all;
white-space: normal;
background-color: ${({ isSelected }) =>
isSelected
? "var(--vscode-list-activeSelectionBackground)"
: "inherit"};
background-color: ${({ isSelected }) => (isSelected ? "var(--vscode-list-activeSelectionBackground)" : "inherit")};
&:hover {
background-color: var(--vscode-list-activeSelectionBackground);
@ -385,9 +349,7 @@ export const ModelDescriptionMarkdown = memo(
}, [reactContent, setIsExpanded])
return (
<StyledMarkdown
key={key}
style={{ display: "inline-block", marginBottom: 0 }}>
<StyledMarkdown key={key} style={{ display: "inline-block", marginBottom: 0 }}>
<div
ref={textContainerRef}
style={{
@ -422,8 +384,7 @@ export const ModelDescriptionMarkdown = memo(
style={{
width: 30,
height: "1.2em",
background:
"linear-gradient(to right, transparent, var(--vscode-sideBar-background))",
background: "linear-gradient(to right, transparent, var(--vscode-sideBar-background))",
}}
/>
<VSCodeLink
@ -433,8 +394,7 @@ export const ModelDescriptionMarkdown = memo(
fontSize: "inherit",
paddingRight: 0,
paddingLeft: 3,
backgroundColor:
"var(--vscode-sideBar-background)",
backgroundColor: "var(--vscode-sideBar-background)",
}}
onClick={() => setIsExpanded(true)}>
See more

View file

@ -1,8 +1,4 @@
import {
VSCodeButton,
VSCodeLink,
VSCodeTextArea,
} from "@vscode/webview-ui-toolkit/react"
import { VSCodeButton, VSCodeLink, VSCodeTextArea } from "@vscode/webview-ui-toolkit/react"
import { memo, useEffect, useState } from "react"
import { useExtensionState } from "../../context/ExtensionStateContext"
import { validateApiConfiguration, validateModelId } from "../../utils/validate"
@ -16,25 +12,12 @@ type SettingsViewProps = {
}
const SettingsView = ({ onDone }: SettingsViewProps) => {
const {
apiConfiguration,
version,
customInstructions,
setCustomInstructions,
openRouterModels,
} = useExtensionState()
const [apiErrorMessage, setApiErrorMessage] = useState<string | undefined>(
undefined,
)
const [modelIdErrorMessage, setModelIdErrorMessage] = useState<
string | undefined
>(undefined)
const { apiConfiguration, version, customInstructions, setCustomInstructions, openRouterModels } = useExtensionState()
const [apiErrorMessage, setApiErrorMessage] = useState<string | undefined>(undefined)
const [modelIdErrorMessage, setModelIdErrorMessage] = useState<string | undefined>(undefined)
const handleSubmit = () => {
const apiValidationResult = validateApiConfiguration(apiConfiguration)
const modelIdValidationResult = validateModelId(
apiConfiguration,
openRouterModels,
)
const modelIdValidationResult = validateModelId(apiConfiguration, openRouterModels)
setApiErrorMessage(apiValidationResult)
setModelIdErrorMessage(modelIdValidationResult)
@ -90,9 +73,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
marginBottom: "17px",
paddingRight: 17,
}}>
<h3 style={{ color: "var(--vscode-foreground)", margin: 0 }}>
Settings
</h3>
<h3 style={{ color: "var(--vscode-foreground)", margin: 0 }}>Settings</h3>
<VSCodeButton onClick={handleSubmit}>Done</VSCodeButton>
</div>
<div
@ -116,15 +97,9 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
value={customInstructions ?? ""}
style={{ width: "100%" }}
rows={4}
placeholder={
'e.g. "Run unit tests at the end", "Use TypeScript with async/await", "Speak in Spanish"'
}
onInput={(e: any) =>
setCustomInstructions(e.target?.value ?? "")
}>
<span style={{ fontWeight: "500" }}>
Custom Instructions
</span>
placeholder={'e.g. "Run unit tests at the end", "Use TypeScript with async/await", "Speak in Spanish"'}
onInput={(e: any) => setCustomInstructions(e.target?.value ?? "")}>
<span style={{ fontWeight: "500" }}>Custom Instructions</span>
</VSCodeTextArea>
<p
style={{
@ -132,19 +107,14 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}}>
These instructions are added to the end of the system
prompt sent with every request.
These instructions are added to the end of the system prompt sent with every request.
</p>
</div>
{IS_DEV && (
<>
<div style={{ marginTop: "10px", marginBottom: "4px" }}>
Debug
</div>
<VSCodeButton
onClick={handleResetState}
style={{ marginTop: "5px", width: "auto" }}>
<div style={{ marginTop: "10px", marginBottom: "4px" }}>Debug</div>
<VSCodeButton onClick={handleResetState} style={{ marginTop: "5px", width: "auto" }}>
Reset State
</VSCodeButton>
<p
@ -153,8 +123,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}}>
This will reset all global state and secret storage
in the extension.
This will reset all global state and secret storage in the extension.
</p>
</>
)}
@ -174,11 +143,8 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
margin: 0,
padding: 0,
}}>
If you have any questions or feedback, feel free to open
an issue at{" "}
<VSCodeLink
href="https://github.com/cline/cline"
style={{ display: "inline" }}>
If you have any questions or feedback, feel free to open an issue at{" "}
<VSCodeLink href="https://github.com/cline/cline" style={{ display: "inline" }}>
https://github.com/cline/cline
</VSCodeLink>
</p>

View file

@ -18,12 +18,7 @@ type TooltipProps = {
align?: "left" | "center" | "right"
}
const Tooltip: React.FC<TooltipProps> = ({
text,
isVisible,
position,
align = "center",
}) => {
const Tooltip: React.FC<TooltipProps> = ({ text, isVisible, position, align = "center" }) => {
let leftPosition = position.x
let triangleStyle: React.CSSProperties = {
left: "50%",
@ -54,8 +49,7 @@ const Tooltip: React.FC<TooltipProps> = ({
transform: align === "center" ? "translateX(-50%)" : "none",
opacity: isVisible ? 1 : 0,
visibility: isVisible ? "visible" : "hidden",
transition:
"opacity 0.1s ease-out 0.1s, visibility 0.1s ease-out 0.1s",
transition: "opacity 0.1s ease-out 0.1s, visibility 0.1s ease-out 0.1s",
backgroundColor: "var(--vscode-editorHoverWidget-background)",
color: "var(--vscode-editorHoverWidget-foreground)",
padding: "4px 8px",
@ -75,8 +69,7 @@ const Tooltip: React.FC<TooltipProps> = ({
...triangleStyle,
borderLeft: "5px solid transparent",
borderRight: "5px solid transparent",
borderBottom:
"5px solid var(--vscode-editorHoverWidget-border)",
borderBottom: "5px solid var(--vscode-editorHoverWidget-border)",
}}
/>
<div
@ -86,8 +79,7 @@ const Tooltip: React.FC<TooltipProps> = ({
...triangleStyle,
borderLeft: "5px solid transparent",
borderRight: "5px solid transparent",
borderBottom:
"5px solid var(--vscode-editorHoverWidget-background)",
borderBottom: "5px solid var(--vscode-editorHoverWidget-background)",
}}
/>
{text}
@ -95,11 +87,7 @@ const Tooltip: React.FC<TooltipProps> = ({
)
}
const TabNavbar = ({
onPlusClick,
onHistoryClick,
onSettingsClick,
}: TabNavbarProps) => {
const TabNavbar = ({ onPlusClick, onHistoryClick, onSettingsClick }: TabNavbarProps) => {
const [tooltip, setTooltip] = useState<TooltipProps>({
text: "",
isVisible: false,
@ -107,11 +95,7 @@ const TabNavbar = ({
align: "center",
})
const showTooltip = (
text: string,
event: React.MouseEvent,
align: "left" | "center" | "right" = "center",
) => {
const showTooltip = (text: string, event: React.MouseEvent, align: "left" | "center" | "right" = "center") => {
const rect = event.currentTarget.getBoundingClientRect()
setTooltip({
text,

View file

@ -8,9 +8,7 @@ import ApiOptions from "../settings/ApiOptions"
const WelcomeView = () => {
const { apiConfiguration } = useExtensionState()
const [apiErrorMessage, setApiErrorMessage] = useState<string | undefined>(
undefined,
)
const [apiErrorMessage, setApiErrorMessage] = useState<string | undefined>(undefined)
const disableLetsGoButton = apiErrorMessage != null
@ -34,30 +32,22 @@ const WelcomeView = () => {
}}>
<h2>Hi, I'm Cline</h2>
<p>
I can do all kinds of tasks thanks to the latest breakthroughs
in{" "}
I can do all kinds of tasks thanks to the latest breakthroughs in{" "}
<VSCodeLink
href="https://www-cdn.anthropic.com/fed9cc193a14b84131812372d8d5857f8f304c52/Model_Card_Claude_3_Addendum.pdf"
style={{ display: "inline" }}>
Claude 3.5 Sonnet's agentic coding capabilities
</VSCodeLink>{" "}
and access to tools that let me create & edit files, explore
complex projects, use the browser, and execute terminal commands
(with your permission, of course). I can even use MCP to create
new tools and extend my own capabilities.
and access to tools that let me create & edit files, explore complex projects, use the browser, and execute
terminal commands (with your permission, of course). I can even use MCP to create new tools and extend my own
capabilities.
</p>
<b>
To get started, this extension needs an API provider for Claude
3.5 Sonnet.
</b>
<b>To get started, this extension needs an API provider for Claude 3.5 Sonnet.</b>
<div style={{ marginTop: "10px" }}>
<ApiOptions showModelOptions={false} />
<VSCodeButton
onClick={handleSubmit}
disabled={disableLetsGoButton}
style={{ marginTop: "3px" }}>
<VSCodeButton onClick={handleSubmit} disabled={disableLetsGoButton} style={{ marginTop: "3px" }}>
Let's go!
</VSCodeButton>
</div>

View file

@ -1,22 +1,8 @@
import React, {
createContext,
useCallback,
useContext,
useEffect,
useState,
} from "react"
import React, { createContext, useCallback, useContext, useEffect, useState } from "react"
import { useEvent } from "react-use"
import { DEFAULT_AUTO_APPROVAL_SETTINGS } from "../../../src/shared/AutoApprovalSettings"
import {
ExtensionMessage,
ExtensionState,
} from "../../../src/shared/ExtensionMessage"
import {
ApiConfiguration,
ModelInfo,
openRouterDefaultModelId,
openRouterDefaultModelInfo,
} from "../../../src/shared/api"
import { ExtensionMessage, ExtensionState } from "../../../src/shared/ExtensionMessage"
import { ApiConfiguration, ModelInfo, openRouterDefaultModelId, openRouterDefaultModelInfo } from "../../../src/shared/api"
import { findLastIndex } from "../../../src/shared/array"
import { McpServer } from "../../../src/shared/mcp"
import { convertTextMateToHljs } from "../utils/textMateToHljs"
@ -34,9 +20,7 @@ interface ExtensionStateContextType extends ExtensionState {
setShowAnnouncement: (value: boolean) => void
}
const ExtensionStateContext = createContext<
ExtensionStateContextType | undefined
>(undefined)
const ExtensionStateContext = createContext<ExtensionStateContextType | undefined>(undefined)
export const ExtensionStateContextProvider: React.FC<{
children: React.ReactNode
@ -52,9 +36,7 @@ export const ExtensionStateContextProvider: React.FC<{
const [showWelcome, setShowWelcome] = useState(false)
const [theme, setTheme] = useState<any>(undefined)
const [filePaths, setFilePaths] = useState<string[]>([])
const [openRouterModels, setOpenRouterModels] = useState<
Record<string, ModelInfo>
>({
const [openRouterModels, setOpenRouterModels] = useState<Record<string, ModelInfo>>({
[openRouterDefaultModelId]: openRouterDefaultModelInfo,
})
const [mcpServers, setMcpServers] = useState<McpServer[]>([])
@ -97,10 +79,7 @@ export const ExtensionStateContextProvider: React.FC<{
const partialMessage = message.partialMessage!
setState((prevState) => {
// worth noting it will never be possible for a more up-to-date message to be sent here or in normal messages post since the presentAssistantContent function uses lock
const lastIndex = findLastIndex(
prevState.clineMessages,
(msg) => msg.ts === partialMessage.ts,
)
const lastIndex = findLastIndex(prevState.clineMessages, (msg) => msg.ts === partialMessage.ts)
if (lastIndex !== -1) {
const newClineMessages = [...prevState.clineMessages]
newClineMessages[lastIndex] = partialMessage
@ -156,19 +135,13 @@ export const ExtensionStateContextProvider: React.FC<{
})),
}
return (
<ExtensionStateContext.Provider value={contextValue}>
{children}
</ExtensionStateContext.Provider>
)
return <ExtensionStateContext.Provider value={contextValue}>{children}</ExtensionStateContext.Provider>
}
export const useExtensionState = () => {
const context = useContext(ExtensionStateContext)
if (context === undefined) {
throw new Error(
"useExtensionState must be used within an ExtensionStateContextProvider",
)
throw new Error("useExtensionState must be used within an ExtensionStateContextProvider")
}
return context
}

Some files were not shown because too many files have changed in this diff Show more