mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
chore: Prettier fix formatting
This commit is contained in:
parent
9de7253998
commit
25cd7cc8e2
106 changed files with 1624 additions and 5278 deletions
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"tabWidth": 4,
|
||||
"useTabs": true,
|
||||
"printWidth": 120,
|
||||
"printWidth": 130,
|
||||
"semi": false,
|
||||
"bracketSameLine": true
|
||||
}
|
||||
|
|
|
|||
6
.vscode/extensions.json
vendored
6
.vscode/extensions.json
vendored
|
|
@ -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
6
.vscode/tasks.json
vendored
|
|
@ -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"
|
||||
},
|
||||
|
|
|
|||
27
esbuild.js
27
esbuild.js
|
|
@ -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))
|
||||
})
|
||||
})
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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":
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
1934
src/core/Cline.ts
1934
src/core/Cline.ts
File diff suppressed because it is too large
Load diff
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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?")
|
||||
|
|
|
|||
|
|
@ -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[]) => {
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 }))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = []
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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", () => {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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/
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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!
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) => ({ "<": "<", ">": ">", "&": "&" })[c] || c,
|
||||
)
|
||||
.replace(
|
||||
mentionRegexGlobal,
|
||||
'<mark class="mention-context-textarea-highlight">$&</mark>',
|
||||
)
|
||||
.replace(/[<>&]/g, (c) => ({ "<": "<", ">": ">", "&": "&" })[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()
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
`
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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={{
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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) && (
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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} />,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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={{
|
||||
|
|
|
|||
|
|
@ -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 &&
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Add table
Reference in a new issue