mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
Checkpoints
This commit is contained in:
parent
b0396a6083
commit
882d80e14e
52 changed files with 2495 additions and 274 deletions
|
|
@ -1,5 +1,13 @@
|
|||
# Change Log
|
||||
|
||||
## [3.1.0]
|
||||
|
||||
- Added checkpoints: Snapshots of workspace are automatically created whenever Cline uses a tool
|
||||
- Compare changes: Hover over any tool use to see a diff between the snapshot and current workspace state
|
||||
- Restore options: Choose to restore just the task state, just the workspace files, or both
|
||||
- New 'See new changes' button appears after task completion, providing an overview of all workspace changes
|
||||
- Task header now shows disk space usage with a delete button to help manage snapshot storage
|
||||
|
||||
## [3.0.12]
|
||||
|
||||
- Fix DeepSeek API cost reporting (input price is 0 since it's all either a cache read or write, different than how Anthropic reports cache usage)
|
||||
|
|
|
|||
16
README.md
16
README.md
|
|
@ -114,6 +114,22 @@ Thanks to the [Model Context Protocol](https://github.com/modelcontextprotocol),
|
|||
|
||||
**`@folder`:** Adds folder's files all at once to speed up your workflow even more
|
||||
|
||||
<!-- Transparent pixel to create line break after floating image -->
|
||||
|
||||
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
|
||||
|
||||
<img align="right" width="350" src="https://github.com/user-attachments/assets/140c8606-d3bf-41b9-9a1f-4dbf0d4c90cb">
|
||||
|
||||
### Checkpoints: Compare and Restore
|
||||
|
||||
As Cline works through a task, the extension takes a snapshot of your workspace at each step. You can use the 'Compare' button to see a diff between the snapshot and your current workspace, and the 'Restore' button to roll back to that point.
|
||||
|
||||
For example, when working with a local web server, you can use 'Restore Workspace Only' to quickly test different versions of your app, then use 'Restore Task and Workspace' when you find the version you want to continue building from. This lets you safely explore different approaches without losing progress.
|
||||
|
||||
<!-- Transparent pixel to create line break after floating image -->
|
||||
|
||||
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
|
||||
|
||||
## Contributing
|
||||
|
||||
To contribute to the project, start with our [Contributing Guide](CONTRIBUTING.md) to learn the basics. You can also join our [Discord](https://discord.gg/cline) to chat with other contributors in the `#contributors` channel. If you're looking for full-time work, check out our open positions on our [careers page](https://cline.bot/join-us)!
|
||||
|
|
|
|||
58
package-lock.json
generated
58
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "claude-dev",
|
||||
"version": "3.0.9",
|
||||
"version": "3.0.12",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "claude-dev",
|
||||
"version": "3.0.9",
|
||||
"version": "3.0.12",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/bedrock-sdk": "^0.10.2",
|
||||
|
|
@ -15,6 +15,7 @@
|
|||
"@google/generative-ai": "^0.18.0",
|
||||
"@modelcontextprotocol/sdk": "^1.0.1",
|
||||
"@types/clone-deep": "^4.0.4",
|
||||
"@types/get-folder-size": "^3.0.4",
|
||||
"@types/pdf-parse": "^1.1.4",
|
||||
"@types/turndown": "^5.0.5",
|
||||
"@vscode/codicons": "^0.0.36",
|
||||
|
|
@ -27,6 +28,7 @@
|
|||
"diff": "^5.2.0",
|
||||
"execa": "^9.5.2",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"get-folder-size": "^5.0.0",
|
||||
"globby": "^14.0.2",
|
||||
"isbinaryfile": "^5.0.2",
|
||||
"mammoth": "^1.8.0",
|
||||
|
|
@ -38,6 +40,7 @@
|
|||
"puppeteer-chromium-resolver": "^23.0.0",
|
||||
"puppeteer-core": "^23.4.0",
|
||||
"serialize-error": "^11.0.3",
|
||||
"simple-git": "^3.27.0",
|
||||
"strip-ansi": "^7.1.0",
|
||||
"tree-sitter-wasms": "^0.1.11",
|
||||
"turndown": "^7.2.0",
|
||||
|
|
@ -2777,6 +2780,21 @@
|
|||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@kwsites/file-exists": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz",
|
||||
"integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^4.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@kwsites/promise-deferred": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz",
|
||||
"integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@mixmark-io/domino": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@mixmark-io/domino/-/domino-2.2.0.tgz",
|
||||
|
|
@ -4546,6 +4564,15 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/get-folder-size": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/get-folder-size/-/get-folder-size-3.0.4.tgz",
|
||||
"integrity": "sha512-tSf/k7Undx6jKRwpChR9tl+0ZPf0BVwkjBRtJ5qSnz6iWm2ZRYMAS2MktC2u7YaTAFHmxpL/LBxI85M7ioJCSg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/istanbul-lib-coverage": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz",
|
||||
|
|
@ -7198,6 +7225,18 @@
|
|||
"node": "6.* || 8.* || >= 10.*"
|
||||
}
|
||||
},
|
||||
"node_modules/get-folder-size": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/get-folder-size/-/get-folder-size-5.0.0.tgz",
|
||||
"integrity": "sha512-+fgtvbL83tSDypEK+T411GDBQVQtxv+qtQgbV+HVa/TYubqDhNd5ghH/D6cOHY9iC5/88GtOZB7WI8PXy2A3bg==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"get-folder-size": "bin/get-folder-size.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.11.0"
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
|
||||
|
|
@ -10464,6 +10503,21 @@
|
|||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/simple-git": {
|
||||
"version": "3.27.0",
|
||||
"resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.27.0.tgz",
|
||||
"integrity": "sha512-ivHoFS9Yi9GY49ogc6/YAi3Fl9ROnF4VyubNylgCkA+RVqLaKWnDSzXOVzya8csELIaWaYNutsEuAhZrtOjozA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@kwsites/file-exists": "^1.1.1",
|
||||
"@kwsites/promise-deferred": "^1.1.1",
|
||||
"debug": "^4.3.5"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/steveukx/git-js?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/slash": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz",
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"name": "claude-dev",
|
||||
"displayName": "Cline (prev. Claude Dev)",
|
||||
"description": "Autonomous coding agent right in your IDE, capable of creating/editing files, running commands, using the browser, and more with your permission every step of the way.",
|
||||
"version": "3.0.12",
|
||||
"version": "3.1.1",
|
||||
"icon": "assets/icons/icon.png",
|
||||
"galleryBanner": {
|
||||
"color": "#617A91",
|
||||
|
|
@ -171,6 +171,7 @@
|
|||
"@google/generative-ai": "^0.18.0",
|
||||
"@modelcontextprotocol/sdk": "^1.0.1",
|
||||
"@types/clone-deep": "^4.0.4",
|
||||
"@types/get-folder-size": "^3.0.4",
|
||||
"@types/pdf-parse": "^1.1.4",
|
||||
"@types/turndown": "^5.0.5",
|
||||
"@vscode/codicons": "^0.0.36",
|
||||
|
|
@ -183,6 +184,7 @@
|
|||
"diff": "^5.2.0",
|
||||
"execa": "^9.5.2",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"get-folder-size": "^5.0.0",
|
||||
"globby": "^14.0.2",
|
||||
"isbinaryfile": "^5.0.2",
|
||||
"mammoth": "^1.8.0",
|
||||
|
|
@ -194,6 +196,7 @@
|
|||
"puppeteer-chromium-resolver": "^23.0.0",
|
||||
"puppeteer-core": "^23.4.0",
|
||||
"serialize-error": "^11.0.3",
|
||||
"simple-git": "^3.27.0",
|
||||
"strip-ansi": "^7.1.0",
|
||||
"tree-sitter-wasms": "^0.1.11",
|
||||
"turndown": "^7.2.0",
|
||||
|
|
|
|||
|
|
@ -39,7 +39,13 @@ export class AnthropicHandler implements ApiHandler {
|
|||
model: modelId,
|
||||
max_tokens: this.getModel().info.maxTokens || 8192,
|
||||
temperature: 0,
|
||||
system: [{ text: systemPrompt, type: "text", cache_control: { type: "ephemeral" } }], // setting cache breakpoint for system prompt so new tasks can reuse it
|
||||
system: [
|
||||
{
|
||||
text: systemPrompt,
|
||||
type: "text",
|
||||
cache_control: { type: "ephemeral" },
|
||||
},
|
||||
], // setting cache breakpoint for system prompt so new tasks can reuse it
|
||||
messages: messages.map((message, index) => {
|
||||
if (index === lastUserMsgIndex || index === secondLastMsgUserIndex) {
|
||||
return {
|
||||
|
|
@ -50,12 +56,19 @@ export class AnthropicHandler implements ApiHandler {
|
|||
{
|
||||
type: "text",
|
||||
text: message.content,
|
||||
cache_control: { type: "ephemeral" },
|
||||
cache_control: {
|
||||
type: "ephemeral",
|
||||
},
|
||||
},
|
||||
]
|
||||
: message.content.map((content, contentIndex) =>
|
||||
contentIndex === message.content.length - 1
|
||||
? { ...content, cache_control: { type: "ephemeral" } }
|
||||
? {
|
||||
...content,
|
||||
cache_control: {
|
||||
type: "ephemeral",
|
||||
},
|
||||
}
|
||||
: content,
|
||||
),
|
||||
}
|
||||
|
|
@ -77,7 +90,9 @@ export class AnthropicHandler implements ApiHandler {
|
|||
case "claude-3-opus-20240229":
|
||||
case "claude-3-haiku-20240307":
|
||||
return {
|
||||
headers: { "anthropic-beta": "prompt-caching-2024-07-31" },
|
||||
headers: {
|
||||
"anthropic-beta": "prompt-caching-2024-07-31",
|
||||
},
|
||||
}
|
||||
default:
|
||||
return undefined
|
||||
|
|
@ -165,6 +180,9 @@ export class AnthropicHandler implements ApiHandler {
|
|||
const id = modelId as AnthropicModelId
|
||||
return { id, info: anthropicModels[id] }
|
||||
}
|
||||
return { id: anthropicDefaultModelId, info: anthropicModels[anthropicDefaultModelId] }
|
||||
return {
|
||||
id: anthropicDefaultModelId,
|
||||
info: anthropicModels[anthropicDefaultModelId],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -107,6 +107,9 @@ export class AwsBedrockHandler implements ApiHandler {
|
|||
const id = modelId as BedrockModelId
|
||||
return { id, info: bedrockModels[id] }
|
||||
}
|
||||
return { id: bedrockDefaultModelId, info: bedrockModels[bedrockDefaultModelId] }
|
||||
return {
|
||||
id: bedrockDefaultModelId,
|
||||
info: bedrockModels[bedrockDefaultModelId],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,6 +56,9 @@ export class DeepSeekHandler implements ApiHandler {
|
|||
const id = modelId as DeepSeekModelId
|
||||
return { id, info: deepSeekModels[id] }
|
||||
}
|
||||
return { id: deepSeekDefaultModelId, info: deepSeekModels[deepSeekDefaultModelId] }
|
||||
return {
|
||||
id: deepSeekDefaultModelId,
|
||||
info: deepSeekModels[deepSeekDefaultModelId],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,6 +51,9 @@ export class GeminiHandler implements ApiHandler {
|
|||
const id = modelId as GeminiModelId
|
||||
return { id, info: geminiModels[id] }
|
||||
}
|
||||
return { id: geminiDefaultModelId, info: geminiModels[geminiDefaultModelId] }
|
||||
return {
|
||||
id: geminiDefaultModelId,
|
||||
info: geminiModels[geminiDefaultModelId],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -80,6 +80,9 @@ export class OpenAiNativeHandler implements ApiHandler {
|
|||
const id = modelId as OpenAiNativeModelId
|
||||
return { id, info: openAiNativeModels[id] }
|
||||
}
|
||||
return { id: openAiNativeDefaultModelId, info: openAiNativeModels[openAiNativeDefaultModelId] }
|
||||
return {
|
||||
id: openAiNativeDefaultModelId,
|
||||
info: openAiNativeModels[openAiNativeDefaultModelId],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -176,6 +176,9 @@ export class OpenRouterHandler implements ApiHandler {
|
|||
if (modelId && modelInfo) {
|
||||
return { id: modelId, info: modelInfo }
|
||||
}
|
||||
return { id: openRouterDefaultModelId, info: openRouterDefaultModelInfo }
|
||||
return {
|
||||
id: openRouterDefaultModelId,
|
||||
info: openRouterDefaultModelInfo,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -81,6 +81,9 @@ export class VertexHandler implements ApiHandler {
|
|||
const id = modelId as VertexModelId
|
||||
return { id, info: vertexModels[id] }
|
||||
}
|
||||
return { id: vertexDefaultModelId, info: vertexModels[vertexDefaultModelId] }
|
||||
return {
|
||||
id: vertexDefaultModelId,
|
||||
info: vertexModels[vertexDefaultModelId],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -244,7 +244,10 @@ const toolNames = [
|
|||
"attempt_completion",
|
||||
]
|
||||
|
||||
function parseAIResponse(response: string): { normalText: string; toolCalls: ToolCall[] } {
|
||||
function parseAIResponse(response: string): {
|
||||
normalText: string
|
||||
toolCalls: ToolCall[]
|
||||
} {
|
||||
// Create a regex pattern to match any tool call opening tag
|
||||
const toolCallPattern = new RegExp(`<(${toolNames.join("|")})`, "i")
|
||||
const match = response.match(toolCallPattern)
|
||||
|
|
|
|||
|
|
@ -8,7 +8,10 @@ export function convertToOpenAiMessages(
|
|||
|
||||
for (const anthropicMessage of anthropicMessages) {
|
||||
if (typeof anthropicMessage.content === "string") {
|
||||
openAiMessages.push({ role: anthropicMessage.role, content: anthropicMessage.content })
|
||||
openAiMessages.push({
|
||||
role: anthropicMessage.role,
|
||||
content: anthropicMessage.content,
|
||||
})
|
||||
} else {
|
||||
// image_url.url is base64 encoded image data
|
||||
// ensure it contains the content-type of the image: data:image/png;base64,
|
||||
|
|
@ -85,7 +88,9 @@ export function convertToOpenAiMessages(
|
|||
if (part.type === "image") {
|
||||
return {
|
||||
type: "image_url",
|
||||
image_url: { url: `data:${part.source.media_type};base64,${part.source.data}` },
|
||||
image_url: {
|
||||
url: `data:${part.source.media_type};base64,${part.source.data}`,
|
||||
},
|
||||
}
|
||||
}
|
||||
return { type: "text", text: part.text }
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -67,7 +67,10 @@ Otherwise, if you have not completed the task and do not need additional informa
|
|||
return 1
|
||||
}
|
||||
// Otherwise, sort alphabetically
|
||||
return aParts[i].localeCompare(bParts[i], undefined, { numeric: true, sensitivity: "base" })
|
||||
return aParts[i].localeCompare(bParts[i], undefined, {
|
||||
numeric: true,
|
||||
sensitivity: "base",
|
||||
})
|
||||
}
|
||||
}
|
||||
// If all parts are the same up to the length of the shorter path,
|
||||
|
|
@ -103,7 +106,11 @@ const formatImagesIntoBlocks = (images?: string[]): Anthropic.ImageBlockParam[]
|
|||
const mimeType = rest.split(":")[1].split(";")[0]
|
||||
return {
|
||||
type: "image",
|
||||
source: { type: "base64", media_type: mimeType, data: base64 },
|
||||
source: {
|
||||
type: "base64",
|
||||
media_type: mimeType,
|
||||
data: base64,
|
||||
},
|
||||
} as Anthropic.ImageBlockParam
|
||||
})
|
||||
: []
|
||||
|
|
|
|||
|
|
@ -8,17 +8,82 @@ a 200k context, we can assume that the first half is likely irrelevant to their
|
|||
Therefore, this function should only be called when absolutely necessary to fit within
|
||||
context limits, not as a continuous process.
|
||||
*/
|
||||
export function truncateHalfConversation(messages: Anthropic.Messages.MessageParam[]): Anthropic.Messages.MessageParam[] {
|
||||
// API expects messages to be in user-assistant order, and tool use messages must be followed by tool results. We need to maintain this structure while truncating.
|
||||
// export function truncateHalfConversation(
|
||||
// messages: Anthropic.Messages.MessageParam[],
|
||||
// ): Anthropic.Messages.MessageParam[] {
|
||||
// // API expects messages to be in user-assistant order, and tool use messages must be followed by tool results. We need to maintain this structure while truncating.
|
||||
|
||||
// Always keep the first Task message (this includes the project's file structure in environment_details)
|
||||
const truncatedMessages = [messages[0]]
|
||||
// // Always keep the first Task message (this includes the project's file structure in environment_details)
|
||||
// const truncatedMessages = [messages[0]]
|
||||
|
||||
// // Remove half of user-assistant pairs
|
||||
// const messagesToRemove = Math.floor(messages.length / 4) * 2 // has to be even number
|
||||
|
||||
// const remainingMessages = messages.slice(messagesToRemove + 1) // has to start with assistant message since tool result cannot follow assistant message with no tool use
|
||||
// truncatedMessages.push(...remainingMessages)
|
||||
|
||||
// return truncatedMessages
|
||||
// }
|
||||
|
||||
/*
|
||||
getNextTruncationRange: Calculates the next range of messages to be "deleted"
|
||||
- Takes the full messages array and optional current deleted range
|
||||
- Always preserves the first message (task message)
|
||||
- Removes 1/2 of remaining messages (rounded down to even number) after current deleted range
|
||||
- Returns [startIndex, endIndex] representing inclusive range to delete
|
||||
|
||||
getTruncatedMessages: Constructs the truncated array using the deleted range
|
||||
- Takes full messages array and optional deleted range
|
||||
- Returns new array with messages in deleted range removed
|
||||
- Preserves order and structure of remaining messages
|
||||
|
||||
The range is represented as [startIndex, endIndex] where both indices are inclusive
|
||||
The functions maintain the original array integrity while allowing progressive truncation
|
||||
through the deletedRange parameter
|
||||
|
||||
Usage example:
|
||||
const messages = [user1, assistant1, user2, assistant2, user3, assistant3];
|
||||
let deletedRange = getNextTruncationRange(messages); // [1,2] (assistant1,user2)
|
||||
let truncated = getTruncatedMessages(messages, deletedRange);
|
||||
// [user1, assistant2, user3, assistant3]
|
||||
|
||||
deletedRange = getNextTruncationRange(messages, deletedRange); // [2,3] (assistant2,user3)
|
||||
truncated = getTruncatedMessages(messages, deletedRange);
|
||||
// [user1, assistant3]
|
||||
*/
|
||||
|
||||
export function getNextTruncationRange(
|
||||
messages: Anthropic.Messages.MessageParam[],
|
||||
currentDeletedRange: [number, number] | undefined = undefined,
|
||||
): [number, number] {
|
||||
// Since we always keep the first message, currentDeletedRange[0] will always be 1 (for now until we have a smarter truncation algorithm)
|
||||
const rangeStartIndex = 1
|
||||
const startOfRest = currentDeletedRange ? currentDeletedRange[1] + 1 : 1
|
||||
|
||||
// Remove half of user-assistant pairs
|
||||
const messagesToRemove = Math.floor(messages.length / 4) * 2 // has to be even number
|
||||
const messagesToRemove = Math.floor((messages.length - startOfRest) / 4) * 2 // Keep even number
|
||||
let rangeEndIndex = startOfRest + messagesToRemove - 1
|
||||
|
||||
const remainingMessages = messages.slice(messagesToRemove + 1) // has to start with assistant message since tool result cannot follow assistant message with no tool use
|
||||
truncatedMessages.push(...remainingMessages)
|
||||
// Make sure the last message being removed is a user message, so that the next message after the initial task message is an assistant message. This preservers the user-assistant-user-assistant structure.
|
||||
// NOTE: anthropic format messages are always user-assitant-user-assistant, while openai format messages can have multiple user messages in a row (we use anthropic format throughout cline)
|
||||
if (messages[rangeEndIndex].role !== "user") {
|
||||
rangeEndIndex -= 1
|
||||
}
|
||||
|
||||
return truncatedMessages
|
||||
// this is an inclusive range that will be removed from the conversation history
|
||||
return [rangeStartIndex, rangeEndIndex]
|
||||
}
|
||||
|
||||
export function getTruncatedMessages(
|
||||
messages: Anthropic.Messages.MessageParam[],
|
||||
deletedRange: [number, number] | undefined,
|
||||
): Anthropic.Messages.MessageParam[] {
|
||||
if (!deletedRange) {
|
||||
return messages
|
||||
}
|
||||
|
||||
const [start, end] = deletedRange
|
||||
// the range is inclusive - both start and end indices and everything in between will be removed from the final result.
|
||||
// NOTE: if you try to console log these, don't forget that logging a reference to an array may not provide the same result as logging a slice() snapshot of that array at that exact moment. The following DOES in fact include the latest assistant message.
|
||||
return [...messages.slice(0, start), ...messages.slice(end + 1)]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,9 +14,9 @@ import WorkspaceTracker from "../../integrations/workspace/WorkspaceTracker"
|
|||
import { McpHub } from "../../services/mcp/McpHub"
|
||||
import { ApiProvider, ModelInfo } from "../../shared/api"
|
||||
import { findLast } from "../../shared/array"
|
||||
import { ExtensionMessage } from "../../shared/ExtensionMessage"
|
||||
import { ExtensionMessage, ExtensionState } from "../../shared/ExtensionMessage"
|
||||
import { HistoryItem } from "../../shared/HistoryItem"
|
||||
import { WebviewMessage } from "../../shared/WebviewMessage"
|
||||
import { ClineCheckpointRestore, WebviewMessage } from "../../shared/WebviewMessage"
|
||||
import { fileExistsAtPath } from "../../utils/fs"
|
||||
import { Cline } from "../Cline"
|
||||
import { openMention } from "../mentions"
|
||||
|
|
@ -79,7 +79,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
private cline?: Cline
|
||||
private workspaceTracker?: WorkspaceTracker
|
||||
mcpHub?: McpHub
|
||||
private latestAnnouncementId = "dec-17-2024" // update to some unique identifier when we add a new announcement
|
||||
private latestAnnouncementId = "jan-6-2025" // update to some unique identifier when we add a new announcement
|
||||
|
||||
constructor(
|
||||
readonly context: vscode.ExtensionContext,
|
||||
|
|
@ -152,7 +152,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
webviewView.onDidChangeViewState(
|
||||
() => {
|
||||
if (this.view?.visible) {
|
||||
this.postMessageToWebview({ type: "action", action: "didBecomeVisible" })
|
||||
this.postMessageToWebview({
|
||||
type: "action",
|
||||
action: "didBecomeVisible",
|
||||
})
|
||||
}
|
||||
},
|
||||
null,
|
||||
|
|
@ -163,7 +166,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
webviewView.onDidChangeVisibility(
|
||||
() => {
|
||||
if (this.view?.visible) {
|
||||
this.postMessageToWebview({ type: "action", action: "didBecomeVisible" })
|
||||
this.postMessageToWebview({
|
||||
type: "action",
|
||||
action: "didBecomeVisible",
|
||||
})
|
||||
}
|
||||
},
|
||||
null,
|
||||
|
|
@ -186,7 +192,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
async (e) => {
|
||||
if (e && e.affectsConfiguration("workbench.colorTheme")) {
|
||||
// Sends latest theme name to webview
|
||||
await this.postMessageToWebview({ type: "theme", text: JSON.stringify(await getTheme()) })
|
||||
await this.postMessageToWebview({
|
||||
type: "theme",
|
||||
text: JSON.stringify(await getTheme()),
|
||||
})
|
||||
}
|
||||
},
|
||||
null,
|
||||
|
|
@ -312,11 +321,19 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
case "webviewDidLaunch":
|
||||
this.postStateToWebview()
|
||||
this.workspaceTracker?.initializeFilePaths() // don't await
|
||||
getTheme().then((theme) => this.postMessageToWebview({ type: "theme", text: JSON.stringify(theme) }))
|
||||
getTheme().then((theme) =>
|
||||
this.postMessageToWebview({
|
||||
type: "theme",
|
||||
text: JSON.stringify(theme),
|
||||
}),
|
||||
)
|
||||
// post last cached models in case the call to endpoint fails
|
||||
this.readOpenRouterModels().then((openRouterModels) => {
|
||||
if (openRouterModels) {
|
||||
this.postMessageToWebview({ type: "openRouterModels", openRouterModels })
|
||||
this.postMessageToWebview({
|
||||
type: "openRouterModels",
|
||||
openRouterModels,
|
||||
})
|
||||
}
|
||||
})
|
||||
// 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.
|
||||
|
|
@ -433,7 +450,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
break
|
||||
case "selectImages":
|
||||
const images = await selectImages()
|
||||
await this.postMessageToWebview({ type: "selectedImages", images })
|
||||
await this.postMessageToWebview({
|
||||
type: "selectedImages",
|
||||
images,
|
||||
})
|
||||
break
|
||||
case "exportCurrentTask":
|
||||
const currentTaskId = this.cline?.taskId
|
||||
|
|
@ -455,11 +475,17 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
break
|
||||
case "requestOllamaModels":
|
||||
const ollamaModels = await this.getOllamaModels(message.text)
|
||||
this.postMessageToWebview({ type: "ollamaModels", ollamaModels })
|
||||
this.postMessageToWebview({
|
||||
type: "ollamaModels",
|
||||
ollamaModels,
|
||||
})
|
||||
break
|
||||
case "requestLmStudioModels":
|
||||
const lmStudioModels = await this.getLmStudioModels(message.text)
|
||||
this.postMessageToWebview({ type: "lmStudioModels", lmStudioModels })
|
||||
this.postMessageToWebview({
|
||||
type: "lmStudioModels",
|
||||
lmStudioModels,
|
||||
})
|
||||
break
|
||||
case "refreshOpenRouterModels":
|
||||
await this.refreshOpenRouterModels()
|
||||
|
|
@ -473,23 +499,35 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
case "openMention":
|
||||
openMention(message.text)
|
||||
break
|
||||
case "cancelTask":
|
||||
if (this.cline) {
|
||||
const { historyItem } = await this.getTaskWithId(this.cline.taskId)
|
||||
this.cline.abortTask()
|
||||
await pWaitFor(() => this.cline === undefined || this.cline.didFinishAborting, {
|
||||
case "checkpointDiff": {
|
||||
if (message.number) {
|
||||
await this.cline?.presentMultifileDiff(message.number, false)
|
||||
}
|
||||
break
|
||||
}
|
||||
case "checkpointRestore": {
|
||||
await this.cancelTask() // we cannot alter message history say if the task is active, as it could be in the middle of editing a file or running a command, which expect the ask to be responded to rather than being superceded by a new message eg add deleted_api_reqs
|
||||
// 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 abort task")
|
||||
console.error("Failed to init new cline instance")
|
||||
})
|
||||
if (this.cline) {
|
||||
// 'abandoned' will prevent this cline instance from affecting future cline instance gui. this may happen if its hanging on a streaming request
|
||||
this.cline.abandoned = true
|
||||
}
|
||||
await this.initClineWithHistoryItem(historyItem) // clears task again, so we need to abortTask manually above
|
||||
// await this.postStateToWebview() // new Cline instance will post state when it's ready. having this here sent an empty messages array to webview leading to virtuoso having to reload the entire list
|
||||
// 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)
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
case "taskCompletionViewChanges": {
|
||||
if (message.number) {
|
||||
await this.cline?.presentMultifileDiff(message.number, true)
|
||||
}
|
||||
break
|
||||
}
|
||||
case "cancelTask":
|
||||
this.cancelTask()
|
||||
break
|
||||
case "openMcpSettings": {
|
||||
const mcpSettingsFilePath = await this.mcpHub?.getMcpSettingsFilePath()
|
||||
|
|
@ -515,6 +553,31 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
)
|
||||
}
|
||||
|
||||
async cancelTask() {
|
||||
if (this.cline) {
|
||||
const { historyItem } = await this.getTaskWithId(this.cline.taskId)
|
||||
try {
|
||||
await this.cline.abortTask()
|
||||
} catch (error) {
|
||||
console.error("Failed to abort task", error)
|
||||
}
|
||||
await pWaitFor(
|
||||
() => this.cline === undefined || this.cline.isStreaming === false || this.cline.didFinishAbortingStream,
|
||||
{
|
||||
timeout: 3_000,
|
||||
},
|
||||
).catch(() => {
|
||||
console.error("Failed to abort task")
|
||||
})
|
||||
if (this.cline) {
|
||||
// 'abandoned' will prevent this cline instance from affecting future cline instance gui. this may happen if its hanging on a streaming request
|
||||
this.cline.abandoned = true
|
||||
}
|
||||
await this.initClineWithHistoryItem(historyItem) // clears task again, so we need to abortTask manually above
|
||||
// await this.postStateToWebview() // new Cline instance will post state when it's ready. having this here sent an empty messages array to webview leading to virtuoso having to reload the entire list
|
||||
}
|
||||
}
|
||||
|
||||
async updateCustomInstructions(instructions?: string) {
|
||||
// User may be clearing the field
|
||||
await this.updateGlobalState("customInstructions", instructions || undefined)
|
||||
|
|
@ -601,7 +664,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
await this.storeSecret("openRouterApiKey", apiKey)
|
||||
await this.postStateToWebview()
|
||||
if (this.cline) {
|
||||
this.cline.api = buildApiHandler({ apiProvider: openrouter, openRouterApiKey: apiKey })
|
||||
this.cline.api = buildApiHandler({
|
||||
apiProvider: openrouter,
|
||||
openRouterApiKey: apiKey,
|
||||
})
|
||||
}
|
||||
// await this.postMessageToWebview({ type: "action", action: "settingsButtonClicked" }) // bad ux if user is on welcome
|
||||
}
|
||||
|
|
@ -732,7 +798,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
console.error("Error fetching OpenRouter models:", error)
|
||||
}
|
||||
|
||||
await this.postMessageToWebview({ type: "openRouterModels", openRouterModels: models })
|
||||
await this.postMessageToWebview({
|
||||
type: "openRouterModels",
|
||||
openRouterModels: models,
|
||||
})
|
||||
return models
|
||||
}
|
||||
|
||||
|
|
@ -775,7 +844,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
const { historyItem } = await this.getTaskWithId(id)
|
||||
await this.initClineWithHistoryItem(historyItem) // clears existing task
|
||||
}
|
||||
await this.postMessageToWebview({ type: "action", action: "chatButtonClicked" })
|
||||
await this.postMessageToWebview({
|
||||
type: "action",
|
||||
action: "chatButtonClicked",
|
||||
})
|
||||
}
|
||||
|
||||
async exportTaskWithId(id: string) {
|
||||
|
|
@ -805,6 +877,18 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
if (await fileExistsAtPath(legacyMessagesFilePath)) {
|
||||
await fs.unlink(legacyMessagesFilePath)
|
||||
}
|
||||
|
||||
// Delete the checkpoints directory if it exists
|
||||
const checkpointsDir = path.join(taskDirPath, "checkpoints")
|
||||
if (await fileExistsAtPath(checkpointsDir)) {
|
||||
try {
|
||||
await fs.rm(checkpointsDir, { recursive: true, force: true })
|
||||
} catch (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
|
||||
}
|
||||
}
|
||||
|
||||
await fs.rmdir(taskDirPath) // succeeds if the dir is empty
|
||||
}
|
||||
|
||||
|
|
@ -823,7 +907,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
this.postMessageToWebview({ type: "state", state })
|
||||
}
|
||||
|
||||
async getStateToPostToWebview() {
|
||||
async getStateToPostToWebview(): Promise<ExtensionState> {
|
||||
const { apiConfiguration, lastShownAnnouncementId, customInstructions, taskHistory, autoApprovalSettings } =
|
||||
await this.getState()
|
||||
return {
|
||||
|
|
@ -831,6 +915,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
apiConfiguration,
|
||||
customInstructions,
|
||||
uriScheme: vscode.env.uriScheme,
|
||||
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,
|
||||
|
|
@ -1084,6 +1170,9 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
}
|
||||
vscode.window.showInformationMessage("State reset")
|
||||
await this.postStateToWebview()
|
||||
await this.postMessageToWebview({ type: "action", action: "chatButtonClicked" })
|
||||
await this.postMessageToWebview({
|
||||
type: "action",
|
||||
action: "chatButtonClicked",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,10 @@ export function createClineAPI(outputChannel: vscode.OutputChannel, sidebarProvi
|
|||
outputChannel.appendLine("Starting new task")
|
||||
await sidebarProvider.clearTask()
|
||||
await sidebarProvider.postStateToWebview()
|
||||
await sidebarProvider.postMessageToWebview({ type: "action", action: "chatButtonClicked" })
|
||||
await sidebarProvider.postMessageToWebview({
|
||||
type: "action",
|
||||
action: "chatButtonClicked",
|
||||
})
|
||||
await sidebarProvider.postMessageToWebview({
|
||||
type: "invoke",
|
||||
invoke: "sendMessage",
|
||||
|
|
|
|||
|
|
@ -39,13 +39,19 @@ export function activate(context: vscode.ExtensionContext) {
|
|||
outputChannel.appendLine("Plus button Clicked")
|
||||
await sidebarProvider.clearTask()
|
||||
await sidebarProvider.postStateToWebview()
|
||||
await sidebarProvider.postMessageToWebview({ type: "action", action: "chatButtonClicked" })
|
||||
await sidebarProvider.postMessageToWebview({
|
||||
type: "action",
|
||||
action: "chatButtonClicked",
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand("cline.mcpButtonClicked", () => {
|
||||
sidebarProvider.postMessageToWebview({ type: "action", action: "mcpButtonClicked" })
|
||||
sidebarProvider.postMessageToWebview({
|
||||
type: "action",
|
||||
action: "mcpButtonClicked",
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
|
|
@ -88,13 +94,19 @@ export function activate(context: vscode.ExtensionContext) {
|
|||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand("cline.settingsButtonClicked", () => {
|
||||
//vscode.window.showInformationMessage(message)
|
||||
sidebarProvider.postMessageToWebview({ type: "action", action: "settingsButtonClicked" })
|
||||
sidebarProvider.postMessageToWebview({
|
||||
type: "action",
|
||||
action: "settingsButtonClicked",
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand("cline.historyButtonClicked", () => {
|
||||
sidebarProvider.postMessageToWebview({ type: "action", action: "historyButtonClicked" })
|
||||
sidebarProvider.postMessageToWebview({
|
||||
type: "action",
|
||||
action: "historyButtonClicked",
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
|
|
|
|||
390
src/integrations/checkpoints/CheckpointTracker.ts
Normal file
390
src/integrations/checkpoints/CheckpointTracker.ts
Normal file
|
|
@ -0,0 +1,390 @@
|
|||
import fs from "fs/promises"
|
||||
import os from "os"
|
||||
import * as path from "path"
|
||||
import simpleGit from "simple-git"
|
||||
import * as vscode from "vscode"
|
||||
import { ClineProvider } from "../../core/webview/ClineProvider"
|
||||
import { fileExistsAtPath } from "../../utils/fs"
|
||||
import { globby } from "globby"
|
||||
|
||||
class CheckpointTracker {
|
||||
private providerRef: WeakRef<ClineProvider>
|
||||
private taskId: string
|
||||
private disposables: vscode.Disposable[] = []
|
||||
private cwd: string
|
||||
private lastRetrievedShadowGitConfigWorkTree?: string
|
||||
lastCheckpointHash?: string
|
||||
|
||||
private constructor(provider: ClineProvider, taskId: string, cwd: string) {
|
||||
this.providerRef = new WeakRef(provider)
|
||||
this.taskId = taskId
|
||||
this.cwd = cwd
|
||||
}
|
||||
|
||||
public static async create(taskId: string, provider?: ClineProvider): Promise<CheckpointTracker> {
|
||||
try {
|
||||
if (!provider) {
|
||||
throw new Error("Provider is required to create a checkpoint tracker")
|
||||
}
|
||||
|
||||
// Check if git is installed by attempting to get version
|
||||
try {
|
||||
await simpleGit().version()
|
||||
} catch (error) {
|
||||
throw new Error("Git must be installed to use checkpoints.") // FIXME: must match what we check for in TaskHeader to show link
|
||||
}
|
||||
|
||||
const cwd = await CheckpointTracker.getWorkingDirectory()
|
||||
const newTracker = new CheckpointTracker(provider, taskId, cwd)
|
||||
await newTracker.initShadowGit()
|
||||
return newTracker
|
||||
} catch (error) {
|
||||
console.error("Failed to create CheckpointTracker:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
private static async getWorkingDirectory(): Promise<string> {
|
||||
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.")
|
||||
}
|
||||
const homedir = os.homedir()
|
||||
const desktopPath = path.join(homedir, "Desktop")
|
||||
const documentsPath = path.join(homedir, "Documents")
|
||||
const downloadsPath = path.join(homedir, "Downloads")
|
||||
|
||||
switch (cwd) {
|
||||
case homedir:
|
||||
throw new Error("Cannot use checkpoints in home directory")
|
||||
case desktopPath:
|
||||
throw new Error("Cannot use checkpoints in Desktop directory")
|
||||
case documentsPath:
|
||||
throw new Error("Cannot use checkpoints in Documents directory")
|
||||
case downloadsPath:
|
||||
throw new Error("Cannot use checkpoints in Downloads directory")
|
||||
default:
|
||||
return cwd
|
||||
}
|
||||
}
|
||||
|
||||
private async getShadowGitPath(): Promise<string> {
|
||||
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")
|
||||
await fs.mkdir(checkpointsDir, { recursive: true })
|
||||
const gitPath = path.join(checkpointsDir, ".git")
|
||||
return gitPath
|
||||
}
|
||||
|
||||
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")
|
||||
return await fileExistsAtPath(gitPath)
|
||||
}
|
||||
|
||||
public async initShadowGit(): Promise<string> {
|
||||
const gitPath = await this.getShadowGitPath()
|
||||
if (await fileExistsAtPath(gitPath)) {
|
||||
// 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)
|
||||
}
|
||||
|
||||
return gitPath
|
||||
} else {
|
||||
const checkpointsDir = path.dirname(gitPath)
|
||||
const git = simpleGit(checkpointsDir)
|
||||
await git.init()
|
||||
|
||||
await git.addConfig("core.worktree", this.cwd) // sets the working tree to the current workspace
|
||||
|
||||
// Add basic excludes directly in git config, while respecting any .gitignore in the workspace
|
||||
// .git/info/exclude is local to the shadow git repo, so it's not shared with the main repo - and won't conflict with user's .gitignore
|
||||
// TODO: let user customize these
|
||||
const excludesPath = path.join(gitPath, "info", "exclude")
|
||||
await fs.mkdir(path.join(gitPath, "info"), { recursive: true })
|
||||
await fs.writeFile(
|
||||
excludesPath,
|
||||
[
|
||||
".git/", // ignore the user's .git
|
||||
`.git${GIT_DISABLED_SUFFIX}/`, // ignore the disabled nested git repos
|
||||
".DS_Store",
|
||||
"*.log",
|
||||
"node_modules/",
|
||||
"__pycache__/",
|
||||
"env/",
|
||||
"venv/",
|
||||
"target/dependency/",
|
||||
"build/dependencies/",
|
||||
"dist/",
|
||||
"out/",
|
||||
"bundle/",
|
||||
"vendor/",
|
||||
"tmp/",
|
||||
"temp/",
|
||||
"deps/",
|
||||
"pkg/",
|
||||
"Pods/",
|
||||
// Media files
|
||||
"*.jpg",
|
||||
"*.jpeg",
|
||||
"*.png",
|
||||
"*.gif",
|
||||
"*.bmp",
|
||||
"*.ico",
|
||||
// "*.svg",
|
||||
"*.mp3",
|
||||
"*.mp4",
|
||||
"*.wav",
|
||||
"*.avi",
|
||||
"*.mov",
|
||||
"*.wmv",
|
||||
"*.webm",
|
||||
"*.webp",
|
||||
"*.m4a",
|
||||
"*.flac",
|
||||
// Build and dependency directories
|
||||
"build/",
|
||||
"bin/",
|
||||
"obj/",
|
||||
".gradle/",
|
||||
".idea/",
|
||||
".vscode/",
|
||||
".vs/",
|
||||
"coverage/",
|
||||
".next/",
|
||||
".nuxt/",
|
||||
// Cache and temporary files
|
||||
"*.cache",
|
||||
"*.tmp",
|
||||
"*.temp",
|
||||
"*.swp",
|
||||
"*.swo",
|
||||
"*.pyc",
|
||||
"*.pyo",
|
||||
".pytest_cache/",
|
||||
".eslintcache",
|
||||
// Environment and config files
|
||||
".env*",
|
||||
"*.local",
|
||||
"*.development",
|
||||
"*.production",
|
||||
// Large data files
|
||||
"*.zip",
|
||||
"*.tar",
|
||||
"*.gz",
|
||||
"*.rar",
|
||||
"*.7z",
|
||||
"*.iso",
|
||||
"*.bin",
|
||||
"*.exe",
|
||||
"*.dll",
|
||||
"*.so",
|
||||
"*.dylib",
|
||||
// Database files
|
||||
"*.sqlite",
|
||||
"*.db",
|
||||
"*.sql",
|
||||
// Log files
|
||||
"*.logs",
|
||||
"*.error",
|
||||
"npm-debug.log*",
|
||||
"yarn-debug.log*",
|
||||
"yarn-error.log*",
|
||||
].join("\n"),
|
||||
)
|
||||
|
||||
// Set up git identity (git throws an error if user.name or user.email is not set)
|
||||
await git.addConfig("user.name", "Cline Checkpoint")
|
||||
await git.addConfig("user.email", "noreply@example.com")
|
||||
|
||||
// Initial commit (--allow-empty ensures it works even with no files)
|
||||
await this.renameNestedGitRepos(true)
|
||||
await git.add(".")
|
||||
await this.renameNestedGitRepos(false)
|
||||
await git.commit("initial commit", { "--allow-empty": null })
|
||||
|
||||
return gitPath
|
||||
}
|
||||
}
|
||||
|
||||
public async getShadowGitConfigWorkTree(): Promise<string | undefined> {
|
||||
if (this.lastRetrievedShadowGitConfigWorkTree) {
|
||||
return this.lastRetrievedShadowGitConfigWorkTree
|
||||
}
|
||||
try {
|
||||
const gitPath = await this.getShadowGitPath()
|
||||
const git = simpleGit(path.dirname(gitPath))
|
||||
const worktree = await git.getConfig("core.worktree")
|
||||
this.lastRetrievedShadowGitConfigWorkTree = worktree.value || undefined
|
||||
return this.lastRetrievedShadowGitConfigWorkTree
|
||||
} catch (error) {
|
||||
console.error("Failed to get shadow git config worktree:", error)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
public async commit(): Promise<string | undefined> {
|
||||
try {
|
||||
const gitPath = await this.getShadowGitPath()
|
||||
const git = simpleGit(path.dirname(gitPath))
|
||||
await this.renameNestedGitRepos(true)
|
||||
await git.add(".")
|
||||
await this.renameNestedGitRepos(false)
|
||||
const result = await git.commit("checkpoint", {
|
||||
"--allow-empty": null,
|
||||
})
|
||||
const commitHash = result.commit || ""
|
||||
this.lastCheckpointHash = commitHash
|
||||
return commitHash
|
||||
} catch (error) {
|
||||
console.error("Failed to create checkpoint:", error)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
public async resetHead(commitHash: string): Promise<void> {
|
||||
const gitPath = await this.getShadowGitPath()
|
||||
const git = simpleGit(path.dirname(gitPath))
|
||||
|
||||
// Clean working directory and force reset
|
||||
// This ensures that the operation will succeed regardless of:
|
||||
// - Untracked files in the workspace
|
||||
// - Staged changes
|
||||
// - Unstaged changes
|
||||
// - Partial commits
|
||||
// - Merge conflicts
|
||||
await git.clean("f", ["-d", "-f"]) // Remove untracked files and directories
|
||||
await git.reset(["--hard", commitHash]) // Hard reset to target commit
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an array describing changed files between one commit and either:
|
||||
* - another commit, or
|
||||
* - the current working directory (including uncommitted changes).
|
||||
*
|
||||
* If `rhsHash` is omitted, compares `lhsHash` to the working directory.
|
||||
* If you want truly untracked files to appear, `git add` them first.
|
||||
*
|
||||
* @param lhsHash - The commit to compare from (older commit)
|
||||
* @param rhsHash - The commit to compare to (newer commit).
|
||||
* If omitted, we compare to the working directory.
|
||||
* @returns Array of file changes with before/after content
|
||||
*/
|
||||
public async getDiffSet(
|
||||
lhsHash?: string,
|
||||
rhsHash?: string,
|
||||
): Promise<
|
||||
Array<{
|
||||
relativePath: string
|
||||
absolutePath: string
|
||||
before: string
|
||||
after: string
|
||||
}>
|
||||
> {
|
||||
const gitPath = await this.getShadowGitPath()
|
||||
const git = simpleGit(path.dirname(gitPath))
|
||||
|
||||
// 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"])
|
||||
baseHash = rootCommit.trim()
|
||||
}
|
||||
|
||||
// Stage all changes so that untracked files appear in diff summary
|
||||
await this.renameNestedGitRepos(true)
|
||||
await git.add(".")
|
||||
await this.renameNestedGitRepos(false)
|
||||
|
||||
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 || ""
|
||||
|
||||
for (const file of diffSummary.files) {
|
||||
const filePath = file.file
|
||||
const absolutePath = path.join(cwdPath, filePath)
|
||||
|
||||
let beforeContent = ""
|
||||
try {
|
||||
beforeContent = await git.show([`${baseHash}:${filePath}`])
|
||||
} catch (_) {
|
||||
// file didn't exist in older commit => remains empty
|
||||
}
|
||||
|
||||
let afterContent = ""
|
||||
if (rhsHash) {
|
||||
// if user provided a newer commit, use git.show at that commit
|
||||
try {
|
||||
afterContent = await git.show([`${rhsHash}:${filePath}`])
|
||||
} catch (_) {
|
||||
// file didn't exist in newer commit => remains empty
|
||||
}
|
||||
} else {
|
||||
// otherwise, read from disk (includes uncommitted changes)
|
||||
try {
|
||||
afterContent = await fs.readFile(absolutePath, "utf8")
|
||||
} catch (_) {
|
||||
// file might be deleted => remains empty
|
||||
}
|
||||
}
|
||||
|
||||
result.push({
|
||||
relativePath: filePath,
|
||||
absolutePath,
|
||||
before: beforeContent,
|
||||
after: afterContent,
|
||||
})
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// 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,
|
||||
})
|
||||
|
||||
// For each nested .git directory, rename it based on operation
|
||||
for (const gitPath of gitPaths) {
|
||||
const fullPath = path.join(this.cwd, gitPath)
|
||||
let newPath: string
|
||||
if (disable) {
|
||||
newPath = fullPath + GIT_DISABLED_SUFFIX
|
||||
} else {
|
||||
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}`)
|
||||
} catch (error) {
|
||||
console.error(`CheckpointTracker failed to ${disable ? "disable" : "enable"} nested git repo ${gitPath}:`, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
this.disposables.forEach((d) => d.dispose())
|
||||
this.disposables = []
|
||||
}
|
||||
}
|
||||
|
||||
const GIT_DISABLED_SUFFIX = "_disabled"
|
||||
|
||||
export default CheckpointTracker
|
||||
|
|
@ -162,7 +162,9 @@ export class DiffViewProvider {
|
|||
// get text after save in case there is any auto-formatting done by the editor
|
||||
const postSaveContent = updatedDocument.getText()
|
||||
|
||||
await vscode.window.showTextDocument(vscode.Uri.file(absolutePath), { preview: false })
|
||||
await vscode.window.showTextDocument(vscode.Uri.file(absolutePath), {
|
||||
preview: false,
|
||||
})
|
||||
await this.closeAllDiffViews()
|
||||
|
||||
/*
|
||||
|
|
@ -220,7 +222,12 @@ export class DiffViewProvider {
|
|||
)
|
||||
}
|
||||
|
||||
return { newProblemsMessage, userEdits, autoFormattingEdits, finalContent: normalizedPostSaveContent }
|
||||
return {
|
||||
newProblemsMessage,
|
||||
userEdits,
|
||||
autoFormattingEdits,
|
||||
finalContent: normalizedPostSaveContent,
|
||||
}
|
||||
}
|
||||
|
||||
async revertChanges(): Promise<void> {
|
||||
|
|
|
|||
|
|
@ -166,7 +166,10 @@ export class BrowserSession {
|
|||
async navigateToUrl(url: string): Promise<BrowserActionResult> {
|
||||
return this.doAction(async (page) => {
|
||||
// networkidle2 isn't good enough since page may take some time to load. we can assume locally running dev sites will reach networkidle0 in a reasonable amount of time
|
||||
await page.goto(url, { timeout: 7_000, waitUntil: ["domcontentloaded", "networkidle2"] })
|
||||
await page.goto(url, {
|
||||
timeout: 7_000,
|
||||
waitUntil: ["domcontentloaded", "networkidle2"],
|
||||
})
|
||||
// await page.goto(url, { timeout: 10_000, waitUntil: "load" })
|
||||
await this.waitTillHTMLStable(page) // in case the page is loading more resources
|
||||
})
|
||||
|
|
|
|||
|
|
@ -71,7 +71,10 @@ export class UrlContentFetcher {
|
|||
- domcontentloaded is when the basic DOM is loaded
|
||||
this should be sufficient for most doc sites
|
||||
*/
|
||||
await this.page.goto(url, { timeout: 10_000, waitUntil: ["domcontentloaded", "networkidle2"] })
|
||||
await this.page.goto(url, {
|
||||
timeout: 10_000,
|
||||
waitUntil: ["domcontentloaded", "networkidle2"],
|
||||
})
|
||||
const content = await this.page.content()
|
||||
|
||||
// use cheerio to parse and clean up the HTML
|
||||
|
|
|
|||
|
|
@ -50,7 +50,10 @@ export async function parseSourceCodeForDefinitionsTopLevel(dirPath: string): Pr
|
|||
return result ? result : "No source code definitions found."
|
||||
}
|
||||
|
||||
function separateFiles(allFiles: string[]): { filesToParse: string[]; remainingFiles: string[] } {
|
||||
function separateFiles(allFiles: string[]): {
|
||||
filesToParse: string[]
|
||||
remainingFiles: string[]
|
||||
} {
|
||||
const extensions = [
|
||||
"js",
|
||||
"jsx",
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ export interface ExtensionMessage {
|
|||
| "partialMessage"
|
||||
| "openRouterModels"
|
||||
| "mcpServers"
|
||||
| "relinquishControl"
|
||||
text?: string
|
||||
action?: "chatButtonClicked" | "mcpButtonClicked" | "settingsButtonClicked" | "historyButtonClicked" | "didBecomeVisible"
|
||||
invoke?: "sendMessage" | "primaryButtonClick" | "secondaryButtonClick"
|
||||
|
|
@ -37,6 +38,8 @@ export interface ExtensionState {
|
|||
apiConfiguration?: ApiConfiguration
|
||||
customInstructions?: string
|
||||
uriScheme?: string
|
||||
currentTaskItem?: HistoryItem
|
||||
checkpointTrackerErrorMessage?: string
|
||||
clineMessages: ClineMessage[]
|
||||
taskHistory: HistoryItem[]
|
||||
shouldShowAnnouncement: boolean
|
||||
|
|
@ -51,6 +54,9 @@ export interface ClineMessage {
|
|||
text?: string
|
||||
images?: string[]
|
||||
partial?: boolean
|
||||
lastCheckpointHash?: string
|
||||
conversationHistoryIndex?: number
|
||||
conversationHistoryDeletedRange?: [number, number] // for when conversation history is truncated for API requests
|
||||
}
|
||||
|
||||
export type ClineAsk =
|
||||
|
|
@ -88,6 +94,7 @@ export type ClineSay =
|
|||
| "mcp_server_response"
|
||||
| "use_mcp_server"
|
||||
| "diff_error"
|
||||
| "deleted_api_reqs"
|
||||
|
||||
export interface ClineSayTool {
|
||||
tool:
|
||||
|
|
@ -142,3 +149,5 @@ export interface ClineApiReqInfo {
|
|||
}
|
||||
|
||||
export type ClineApiReqCancelReason = "streaming_failed" | "user_cancelled"
|
||||
|
||||
export const COMPLETION_RESULT_CHANGES_FLAG = "HAS_CHANGES"
|
||||
|
|
|
|||
|
|
@ -7,4 +7,8 @@ export type HistoryItem = {
|
|||
cacheWrites?: number
|
||||
cacheReads?: number
|
||||
totalCost: number
|
||||
|
||||
size?: number
|
||||
shadowGitConfigWorkTree?: string
|
||||
conversationHistoryDeletedRange?: [number, number]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,12 +26,18 @@ export interface WebviewMessage {
|
|||
| "openMcpSettings"
|
||||
| "restartMcpServer"
|
||||
| "autoApprovalSettings"
|
||||
| "checkpointDiff"
|
||||
| "checkpointRestore"
|
||||
| "taskCompletionViewChanges"
|
||||
text?: string
|
||||
askResponse?: ClineAskResponse
|
||||
apiConfiguration?: ApiConfiguration
|
||||
images?: string[]
|
||||
bool?: boolean
|
||||
number?: number
|
||||
autoApprovalSettings?: AutoApprovalSettings
|
||||
}
|
||||
|
||||
export type ClineAskResponse = "yesButtonClicked" | "noButtonClicked" | "messageResponse"
|
||||
|
||||
export type ClineCheckpointRestore = "task" | "workspace" | "taskAndWorkspace"
|
||||
|
|
|
|||
|
|
@ -29,7 +29,10 @@ export function combineApiRequests(messages: ClineMessage[]): ClineMessage[] {
|
|||
while (j < messages.length) {
|
||||
if (messages[j].type === "say" && messages[j].say === "api_req_finished") {
|
||||
let finishedRequest = JSON.parse(messages[j].text || "{}")
|
||||
let combinedRequest = { ...startedRequest, ...finishedRequest }
|
||||
let combinedRequest = {
|
||||
...startedRequest,
|
||||
...finishedRequest,
|
||||
}
|
||||
|
||||
combinedApiRequests.push({
|
||||
...messages[i],
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ interface ApiMetrics {
|
|||
* Calculates API metrics from an array of ClineMessages.
|
||||
*
|
||||
* This function processes 'api_req_started' messages that have been combined with their
|
||||
* corresponding 'api_req_finished' messages by the combineApiRequests function.
|
||||
* corresponding 'api_req_finished' messages by the combineApiRequests function. It also takes into account 'deleted_api_reqs' messages, which are aggregated from deleted messages.
|
||||
* It extracts and sums up the tokensIn, tokensOut, cacheWrites, cacheReads, and cost from these messages.
|
||||
*
|
||||
* @param messages - An array of ClineMessage objects to process.
|
||||
|
|
@ -35,7 +35,7 @@ export function getApiMetrics(messages: ClineMessage[]): ApiMetrics {
|
|||
}
|
||||
|
||||
messages.forEach((message) => {
|
||||
if (message.type === "say" && message.say === "api_req_started" && 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
|
||||
|
|
|
|||
33
webview-ui/package-lock.json
generated
33
webview-ui/package-lock.json
generated
|
|
@ -19,6 +19,7 @@
|
|||
"debounce": "^2.1.1",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fuse.js": "^7.0.0",
|
||||
"pretty-bytes": "^6.1.1",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-remark": "^2.1.0",
|
||||
|
|
@ -16061,12 +16062,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/pretty-bytes": {
|
||||
"version": "5.6.0",
|
||||
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz",
|
||||
"integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==",
|
||||
"version": "6.1.1",
|
||||
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.1.1.tgz",
|
||||
"integrity": "sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
"node": "^14.13.1 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
|
|
@ -20557,6 +20558,18 @@
|
|||
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/workbox-build/node_modules/pretty-bytes": {
|
||||
"version": "5.6.0",
|
||||
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz",
|
||||
"integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/workbox-build/node_modules/source-map": {
|
||||
"version": "0.8.0-beta.0",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz",
|
||||
|
|
@ -20730,6 +20743,18 @@
|
|||
"webpack": "^4.4.0 || ^5.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/workbox-webpack-plugin/node_modules/pretty-bytes": {
|
||||
"version": "5.6.0",
|
||||
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz",
|
||||
"integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/workbox-webpack-plugin/node_modules/source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
"debounce": "^2.1.1",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fuse.js": "^7.0.0",
|
||||
"pretty-bytes": "^6.1.1",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-remark": "^2.1.0",
|
||||
|
|
|
|||
|
|
@ -31,39 +31,39 @@ const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
|
|||
</h3>
|
||||
<ul style={{ margin: "0 0 8px", paddingLeft: "12px" }}>
|
||||
<li>
|
||||
<b>Auto-approve menu:</b> You can now specify which tools require approval, set a max # of auto-approved API
|
||||
requests, and enable system notifications for when Cline completes a task.
|
||||
<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
|
||||
className="codicon codicon-diff-multiple"
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
marginRight: "4px",
|
||||
}}></span>
|
||||
<b>Compare</b> shows you a diff between the snapshot and your current workspace
|
||||
</li>
|
||||
<li>
|
||||
<span
|
||||
className="codicon codicon-discard"
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
marginRight: "4px",
|
||||
}}></span>
|
||||
<b>Restore</b> lets you revert your project's files back to that point in the task
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<b>New diff editing for large files:</b> Cline now uses an efficient search & replace approach when modifying
|
||||
large files for faster, more reliable edits (no more "<code>{"// rest of code here"}</code>" deletions).
|
||||
</li>
|
||||
<li>
|
||||
<b>.clinerules:</b> Add a root-level <code>.clinerules</code> file to specify custom instructions for the
|
||||
project.
|
||||
</li>
|
||||
</ul>
|
||||
<p style={{ margin: "5px 0px", fontWeight: "bold" }}>v2.2 Updates:</p>
|
||||
<ul style={{ margin: "0 0 8px", paddingLeft: "12px" }}>
|
||||
<li>
|
||||
Add and configure{" "}
|
||||
<VSCodeLink href="https://github.com/modelcontextprotocol/servers" style={{ display: "inline" }}>
|
||||
MCP servers
|
||||
</VSCodeLink>
|
||||
by clicking the new <span className="codicon codicon-server" style={{ fontSize: "10px" }}></span> icon in the
|
||||
menu bar.
|
||||
</li>
|
||||
<li>
|
||||
Cline can also create custom tools–just say "add a tool that...", and watch him create the MCP server and
|
||||
install it in the extension, ready to use in future tasks.
|
||||
</li>
|
||||
<li>
|
||||
Try it yourself by asking Cline to "add a tool that gets the latest npm docs", or
|
||||
<VSCodeLink href="https://x.com/sdrzn/status/1867271665086074969" style={{ display: "inline" }}>
|
||||
see a demo of MCP in action here.
|
||||
</VSCodeLink>
|
||||
<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/1876378124126236949" style={{ display: "inline" }}>
|
||||
See a demo of Checkpoints here!
|
||||
</VSCodeLink>
|
||||
</p>
|
||||
{/*<ul style={{ margin: "0 0 8px", paddingLeft: "12px" }}>
|
||||
<li>
|
||||
OpenRouter now supports prompt caching! They also have much higher rate limits than other providers,
|
||||
|
|
|
|||
|
|
@ -157,7 +157,9 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
|
|||
}
|
||||
}}>
|
||||
<VSCodeCheckbox
|
||||
style={{ pointerEvents: hasEnabledActions ? "auto" : "none" }}
|
||||
style={{
|
||||
pointerEvents: hasEnabledActions ? "auto" : "none",
|
||||
}}
|
||||
checked={hasEnabledActions && autoApprovalSettings.enabled}
|
||||
disabled={!hasEnabledActions}
|
||||
// onChange={(e) => {
|
||||
|
|
@ -182,7 +184,13 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
|
|||
setIsExpanded((prev) => !prev)
|
||||
}
|
||||
}}>
|
||||
<span style={{ color: "var(--vscode-foreground)", whiteSpace: "nowrap" }}>Auto-approve:</span>
|
||||
<span
|
||||
style={{
|
||||
color: "var(--vscode-foreground)",
|
||||
whiteSpace: "nowrap",
|
||||
}}>
|
||||
Auto-approve:
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
whiteSpace: "nowrap",
|
||||
|
|
|
|||
|
|
@ -6,6 +6,9 @@ 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 { findLast } from "../../../../src/shared/array"
|
||||
|
||||
interface BrowserSessionRowProps {
|
||||
messages: ClineMessage[]
|
||||
|
|
@ -137,6 +140,11 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
|
|||
return launchMessage?.say === "browser_action_launch"
|
||||
}, [messages])
|
||||
|
||||
const lastCheckpointMessageTs = useMemo(() => {
|
||||
const lastCheckpointMessage = findLast(messages, (m) => m.lastCheckpointHash !== undefined)
|
||||
return lastCheckpointMessage?.ts
|
||||
}, [messages])
|
||||
|
||||
// Find the latest available URL and screenshot
|
||||
const latestState = useMemo(() => {
|
||||
for (let i = pages.length - 1; i >= 0; i--) {
|
||||
|
|
@ -150,7 +158,12 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
|
|||
}
|
||||
}
|
||||
}
|
||||
return { url: undefined, mousePosition: undefined, consoleLogs: undefined, screenshot: undefined }
|
||||
return {
|
||||
url: undefined,
|
||||
mousePosition: undefined,
|
||||
consoleLogs: undefined,
|
||||
screenshot: undefined,
|
||||
}
|
||||
}, [pages])
|
||||
|
||||
const currentPage = pages[currentPageIndex]
|
||||
|
|
@ -212,15 +225,29 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
|
|||
// Use latest click position while browsing, otherwise use display state
|
||||
const mousePosition = isBrowsing ? latestClickPosition || displayState.mousePosition : displayState.mousePosition
|
||||
|
||||
let shouldShowCheckpoints = true
|
||||
if (isLast) {
|
||||
shouldShowCheckpoints = lastModifiedMessage?.ask === "resume_completed_task" || lastModifiedMessage?.ask === "resume_task"
|
||||
}
|
||||
|
||||
const [browserSessionRow, { height }] = useSize(
|
||||
<div style={{ padding: "10px 6px 10px 15px", marginBottom: -10 }}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "10px", marginBottom: "10px" }}>
|
||||
<BrowserSessionRowContainer style={{ marginBottom: -10 }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "10px",
|
||||
marginBottom: "10px",
|
||||
}}>
|
||||
{isBrowsing ? (
|
||||
<ProgressIndicator />
|
||||
) : (
|
||||
<span
|
||||
className={`codicon codicon-inspect`}
|
||||
style={{ color: "var(--vscode-foreground)", marginBottom: "-1.5px" }}></span>
|
||||
style={{
|
||||
color: "var(--vscode-foreground)",
|
||||
marginBottom: "-1.5px",
|
||||
}}></span>
|
||||
)}
|
||||
<span style={{ fontWeight: "bold" }}>
|
||||
<>{isAutoApproved ? "Cline is using the browser:" : "Cline wants to use the browser:"}</>
|
||||
|
|
@ -300,7 +327,10 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
|
|||
}}>
|
||||
<span
|
||||
className="codicon codicon-globe"
|
||||
style={{ fontSize: "80px", color: "var(--vscode-descriptionForeground)" }}
|
||||
style={{
|
||||
fontSize: "80px",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -370,7 +400,9 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>,
|
||||
|
||||
{shouldShowCheckpoints && <CheckpointOverlay messageTs={lastCheckpointMessageTs} />}
|
||||
</BrowserSessionRowContainer>,
|
||||
)
|
||||
|
||||
// Height change effect
|
||||
|
|
@ -536,4 +568,13 @@ const BrowserCursor: React.FC<{ style?: React.CSSProperties }> = ({ style }) =>
|
|||
)
|
||||
}
|
||||
|
||||
const BrowserSessionRowContainer = styled.div`
|
||||
padding: 10px 6px 10px 15px;
|
||||
position: relative;
|
||||
|
||||
&:hover ${CheckpointControls} {
|
||||
opacity: 1;
|
||||
}
|
||||
`
|
||||
|
||||
export default BrowserSessionRow
|
||||
|
|
|
|||
|
|
@ -1,20 +1,39 @@
|
|||
import { VSCodeBadge, VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react"
|
||||
import deepEqual from "fast-deep-equal"
|
||||
import React, { memo, useEffect, useMemo, useRef } from "react"
|
||||
import { useSize } from "react-use"
|
||||
import { ClineApiReqInfo, ClineAskUseMcpServer, ClineMessage, ClineSayTool } from "../../../../src/shared/ExtensionMessage"
|
||||
import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"
|
||||
import { useEvent, useSize } from "react-use"
|
||||
import styled from "styled-components"
|
||||
import {
|
||||
ClineApiReqInfo,
|
||||
ClineAskUseMcpServer,
|
||||
ClineMessage,
|
||||
ClineSayTool,
|
||||
ExtensionMessage,
|
||||
COMPLETION_RESULT_CHANGES_FLAG,
|
||||
} from "../../../../src/shared/ExtensionMessage"
|
||||
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 CodeBlock, { CODE_BLOCK_BG_COLOR } from "../common/CodeBlock"
|
||||
import MarkdownBlock from "../common/MarkdownBlock"
|
||||
import SuccessButton from "../common/SuccessButton"
|
||||
import Thumbnails from "../common/Thumbnails"
|
||||
import McpResourceRow from "../mcp/McpResourceRow"
|
||||
import McpToolRow from "../mcp/McpToolRow"
|
||||
import { highlightMentions } from "./TaskHeader"
|
||||
|
||||
const ChatRowContainer = styled.div`
|
||||
padding: 10px 6px 10px 15px;
|
||||
position: relative;
|
||||
|
||||
&:hover ${CheckpointControls} {
|
||||
opacity: 1;
|
||||
}
|
||||
`
|
||||
|
||||
interface ChatRowProps {
|
||||
message: ClineMessage
|
||||
isExpanded: boolean
|
||||
|
|
@ -28,18 +47,33 @@ interface ChatRowContentProps extends Omit<ChatRowProps, "onHeightChange"> {}
|
|||
|
||||
const ChatRow = memo(
|
||||
(props: ChatRowProps) => {
|
||||
const { isLast, onHeightChange, message } = props
|
||||
const { isLast, onHeightChange, message, lastModifiedMessage } = props
|
||||
// Store the previous height to compare with the current height
|
||||
// This allows us to detect changes without causing re-renders
|
||||
const prevHeightRef = useRef(0)
|
||||
|
||||
// NOTE: for tools that are interrupted and not responded to (approved or rejected), there won't be a checkpoint hash
|
||||
let shouldShowCheckpoints =
|
||||
message.lastCheckpointHash != null &&
|
||||
(message.say === "tool" ||
|
||||
message.ask === "tool" ||
|
||||
message.say === "command" ||
|
||||
message.ask === "command" ||
|
||||
message.say === "completion_result" ||
|
||||
message.ask === "completion_result" ||
|
||||
message.say === "use_mcp_server" ||
|
||||
message.ask === "use_mcp_server")
|
||||
|
||||
if (shouldShowCheckpoints && isLast) {
|
||||
shouldShowCheckpoints =
|
||||
lastModifiedMessage?.ask === "resume_completed_task" || lastModifiedMessage?.ask === "resume_task"
|
||||
}
|
||||
|
||||
const [chatrow, { height }] = useSize(
|
||||
<div
|
||||
style={{
|
||||
padding: "10px 6px 10px 15px",
|
||||
}}>
|
||||
<ChatRowContainer>
|
||||
<ChatRowContent {...props} />
|
||||
</div>,
|
||||
{shouldShowCheckpoints && <CheckpointOverlay messageTs={message.ts} />}
|
||||
</ChatRowContainer>,
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -66,6 +100,9 @@ export default ChatRow
|
|||
|
||||
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)
|
||||
|
|
@ -92,21 +129,48 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi
|
|||
const successColor = "var(--vscode-charts-green)"
|
||||
const cancelledColor = "var(--vscode-descriptionForeground)"
|
||||
|
||||
const handleMessage = useCallback((event: MessageEvent) => {
|
||||
const message: ExtensionMessage = event.data
|
||||
switch (message.type) {
|
||||
case "relinquishControl": {
|
||||
setSeeNewChangesDisabled(false)
|
||||
break
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEvent("message", handleMessage)
|
||||
|
||||
const [icon, title] = useMemo(() => {
|
||||
switch (type) {
|
||||
case "error":
|
||||
return [
|
||||
<span className="codicon codicon-error" style={{ color: errorColor, marginBottom: "-1.5px" }}></span>,
|
||||
<span
|
||||
className="codicon codicon-error"
|
||||
style={{
|
||||
color: errorColor,
|
||||
marginBottom: "-1.5px",
|
||||
}}></span>,
|
||||
<span style={{ color: errorColor, fontWeight: "bold" }}>Error</span>,
|
||||
]
|
||||
case "mistake_limit_reached":
|
||||
return [
|
||||
<span className="codicon codicon-error" style={{ color: errorColor, marginBottom: "-1.5px" }}></span>,
|
||||
<span
|
||||
className="codicon codicon-error"
|
||||
style={{
|
||||
color: errorColor,
|
||||
marginBottom: "-1.5px",
|
||||
}}></span>,
|
||||
<span style={{ color: errorColor, fontWeight: "bold" }}>Cline is having trouble...</span>,
|
||||
]
|
||||
case "auto_approval_max_req_reached":
|
||||
return [
|
||||
<span className="codicon codicon-warning" style={{ color: errorColor, marginBottom: "-1.5px" }}></span>,
|
||||
<span
|
||||
className="codicon codicon-warning"
|
||||
style={{
|
||||
color: errorColor,
|
||||
marginBottom: "-1.5px",
|
||||
}}></span>,
|
||||
<span style={{ color: errorColor, fontWeight: "bold" }}>Maximum Requests Reached</span>,
|
||||
]
|
||||
case "command":
|
||||
|
|
@ -114,7 +178,12 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi
|
|||
isCommandExecuting ? (
|
||||
<ProgressIndicator />
|
||||
) : (
|
||||
<span className="codicon codicon-terminal" style={{ color: normalColor, marginBottom: "-1.5px" }}></span>
|
||||
<span
|
||||
className="codicon codicon-terminal"
|
||||
style={{
|
||||
color: normalColor,
|
||||
marginBottom: "-1.5px",
|
||||
}}></span>
|
||||
),
|
||||
<span style={{ color: normalColor, fontWeight: "bold" }}>
|
||||
{message.type === "ask" ? "Cline wants to execute this command:" : "Cline executed this command:"}
|
||||
|
|
@ -126,7 +195,12 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi
|
|||
isMcpServerResponding ? (
|
||||
<ProgressIndicator />
|
||||
) : (
|
||||
<span className="codicon codicon-server" style={{ color: normalColor, marginBottom: "-1.5px" }}></span>
|
||||
<span
|
||||
className="codicon codicon-server"
|
||||
style={{
|
||||
color: normalColor,
|
||||
marginBottom: "-1.5px",
|
||||
}}></span>
|
||||
),
|
||||
<span style={{ color: normalColor, fontWeight: "bold" }}>
|
||||
{message.type === "ask" ? (
|
||||
|
|
@ -144,7 +218,12 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi
|
|||
]
|
||||
case "completion_result":
|
||||
return [
|
||||
<span className="codicon codicon-check" style={{ color: successColor, marginBottom: "-1.5px" }}></span>,
|
||||
<span
|
||||
className="codicon codicon-check"
|
||||
style={{
|
||||
color: successColor,
|
||||
marginBottom: "-1.5px",
|
||||
}}></span>,
|
||||
<span style={{ color: successColor, fontWeight: "bold" }}>Task Completed</span>,
|
||||
]
|
||||
case "api_req_started":
|
||||
|
|
@ -182,9 +261,21 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi
|
|||
),
|
||||
apiReqCancelReason != null ? (
|
||||
apiReqCancelReason === "user_cancelled" ? (
|
||||
<span style={{ color: normalColor, fontWeight: "bold" }}>API Request Cancelled</span>
|
||||
<span
|
||||
style={{
|
||||
color: normalColor,
|
||||
fontWeight: "bold",
|
||||
}}>
|
||||
API Request Cancelled
|
||||
</span>
|
||||
) : (
|
||||
<span style={{ color: errorColor, fontWeight: "bold" }}>API Streaming Failed</span>
|
||||
<span
|
||||
style={{
|
||||
color: errorColor,
|
||||
fontWeight: "bold",
|
||||
}}>
|
||||
API Streaming Failed
|
||||
</span>
|
||||
)
|
||||
) : cost != null ? (
|
||||
<span style={{ color: normalColor, fontWeight: "bold" }}>API Request</span>
|
||||
|
|
@ -196,7 +287,12 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi
|
|||
]
|
||||
case "followup":
|
||||
return [
|
||||
<span className="codicon codicon-question" style={{ color: normalColor, marginBottom: "-1.5px" }}></span>,
|
||||
<span
|
||||
className="codicon codicon-question"
|
||||
style={{
|
||||
color: normalColor,
|
||||
marginBottom: "-1.5px",
|
||||
}}></span>,
|
||||
<span style={{ color: normalColor, fontWeight: "bold" }}>Cline has a question:</span>,
|
||||
]
|
||||
default:
|
||||
|
|
@ -217,7 +313,7 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi
|
|||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "10px",
|
||||
marginBottom: "10px",
|
||||
marginBottom: "12px",
|
||||
}
|
||||
|
||||
const pStyle: React.CSSProperties = {
|
||||
|
|
@ -238,7 +334,10 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi
|
|||
const toolIcon = (name: string) => (
|
||||
<span
|
||||
className={`codicon codicon-${name}`}
|
||||
style={{ color: "var(--vscode-foreground)", marginBottom: "-1.5px" }}></span>
|
||||
style={{
|
||||
color: "var(--vscode-foreground)",
|
||||
marginBottom: "-1.5px",
|
||||
}}></span>
|
||||
)
|
||||
|
||||
switch (tool.tool) {
|
||||
|
|
@ -313,7 +412,10 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi
|
|||
msUserSelect: "none",
|
||||
}}
|
||||
onClick={() => {
|
||||
vscode.postMessage({ type: "openFile", text: tool.content })
|
||||
vscode.postMessage({
|
||||
type: "openFile",
|
||||
text: tool.content,
|
||||
})
|
||||
}}>
|
||||
{tool.path?.startsWith(".") && <span>.</span>}
|
||||
<span
|
||||
|
|
@ -330,7 +432,10 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi
|
|||
<div style={{ flexGrow: 1 }}></div>
|
||||
<span
|
||||
className={`codicon codicon-link-external`}
|
||||
style={{ fontSize: 13.5, margin: "1px 0" }}></span>
|
||||
style={{
|
||||
fontSize: 13.5,
|
||||
margin: "1px 0",
|
||||
}}></span>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
|
@ -532,7 +637,7 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi
|
|||
gap: 10,
|
||||
padding: 8,
|
||||
fontSize: "12px",
|
||||
color: "var(--vscode-errorForeground)",
|
||||
color: "var(--vscode-editorWarning-foreground)",
|
||||
}}>
|
||||
<i className="codicon codicon-warning"></i>
|
||||
<span>The model has determined this command requires explicit approval.</span>
|
||||
|
|
@ -632,11 +737,19 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi
|
|||
msUserSelect: "none",
|
||||
}}
|
||||
onClick={onToggleExpand}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "10px" }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "10px",
|
||||
}}>
|
||||
{icon}
|
||||
{title}
|
||||
{/* Need to render this everytime since it affects height of row by 2px */}
|
||||
<VSCodeBadge style={{ opacity: cost != null && cost > 0 ? 1 : 0 }}>
|
||||
<VSCodeBadge
|
||||
style={{
|
||||
opacity: cost != null && cost > 0 ? 1 : 0,
|
||||
}}>
|
||||
${Number(cost || 0)?.toFixed(4)}
|
||||
</VSCodeBadge>
|
||||
</div>
|
||||
|
|
@ -644,7 +757,11 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi
|
|||
</div>
|
||||
{((cost == null && apiRequestFailedMessage) || apiReqStreamingFailedMessage) && (
|
||||
<>
|
||||
<p style={{ ...pStyle, color: "var(--vscode-errorForeground)" }}>
|
||||
<p
|
||||
style={{
|
||||
...pStyle,
|
||||
color: "var(--vscode-errorForeground)",
|
||||
}}>
|
||||
{apiRequestFailedMessage || apiReqStreamingFailedMessage}
|
||||
{apiRequestFailedMessage?.toLowerCase().includes("powershell") && (
|
||||
<>
|
||||
|
|
@ -653,7 +770,10 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi
|
|||
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" }}>
|
||||
style={{
|
||||
color: "inherit",
|
||||
textDecoration: "underline",
|
||||
}}>
|
||||
troubleshooting guide
|
||||
</a>
|
||||
.
|
||||
|
|
@ -758,7 +878,13 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi
|
|||
{title}
|
||||
</div>
|
||||
)}
|
||||
<p style={{ ...pStyle, color: "var(--vscode-errorForeground)" }}>{message.text}</p>
|
||||
<p
|
||||
style={{
|
||||
...pStyle,
|
||||
color: "var(--vscode-errorForeground)",
|
||||
}}>
|
||||
{message.text}
|
||||
</p>
|
||||
</>
|
||||
)
|
||||
case "diff_error":
|
||||
|
|
@ -773,7 +899,12 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi
|
|||
borderRadius: 3,
|
||||
fontSize: 12,
|
||||
}}>
|
||||
<div style={{ display: "flex", alignItems: "center", marginBottom: 4 }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
marginBottom: 4,
|
||||
}}>
|
||||
<i
|
||||
className="codicon codicon-error"
|
||||
style={{
|
||||
|
|
@ -781,7 +912,13 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi
|
|||
fontSize: 18,
|
||||
color: "#FFA500",
|
||||
}}></i>
|
||||
<span style={{ fontWeight: 500, color: "#FFA500" }}>Diff Edit Failed</span>
|
||||
<span
|
||||
style={{
|
||||
fontWeight: 500,
|
||||
color: "#FFA500",
|
||||
}}>
|
||||
Diff Edit Failed
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
This usually happens when the model uses search patterns that don't match anything in the
|
||||
|
|
@ -791,15 +928,45 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi
|
|||
</>
|
||||
)
|
||||
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
|
||||
return (
|
||||
<>
|
||||
<div style={headerStyle}>
|
||||
<div
|
||||
style={{
|
||||
...headerStyle,
|
||||
marginBottom: "10px",
|
||||
}}>
|
||||
{icon}
|
||||
{title}
|
||||
</div>
|
||||
<div style={{ color: "var(--vscode-charts-green)", paddingTop: 10 }}>
|
||||
<Markdown markdown={message.text} />
|
||||
<div
|
||||
style={{
|
||||
color: "var(--vscode-charts-green)",
|
||||
paddingTop: 10,
|
||||
}}>
|
||||
<Markdown markdown={text} />
|
||||
</div>
|
||||
{message.partial !== true && hasChanges && (
|
||||
<div style={{ paddingTop: 17 }}>
|
||||
<SuccessButton
|
||||
disabled={seeNewChangesDisabled}
|
||||
onClick={() => {
|
||||
setSeeNewChangesDisabled(true)
|
||||
vscode.postMessage({
|
||||
type: "taskCompletionViewChanges",
|
||||
number: message.ts,
|
||||
})
|
||||
}}
|
||||
style={{
|
||||
width: "100%",
|
||||
cursor: seeNewChangesDisabled ? "wait" : "pointer",
|
||||
}}>
|
||||
<i className="codicon codicon-new-file" style={{ marginRight: 6 }} />
|
||||
See new changes
|
||||
</SuccessButton>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
case "shell_integration_warning":
|
||||
|
|
@ -814,7 +981,12 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi
|
|||
borderRadius: 3,
|
||||
fontSize: 12,
|
||||
}}>
|
||||
<div style={{ display: "flex", alignItems: "center", marginBottom: 4 }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
marginBottom: 4,
|
||||
}}>
|
||||
<i
|
||||
className="codicon codicon-warning"
|
||||
style={{
|
||||
|
|
@ -822,7 +994,13 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi
|
|||
fontSize: 18,
|
||||
color: "#FFA500",
|
||||
}}></i>
|
||||
<span style={{ fontWeight: 500, color: "#FFA500" }}>Shell Integration Unavailable</span>
|
||||
<span
|
||||
style={{
|
||||
fontWeight: 500,
|
||||
color: "#FFA500",
|
||||
}}>
|
||||
Shell Integration Unavailable
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
Cline won't be able to view the command's output. Please update VSCode (
|
||||
|
|
@ -831,7 +1009,10 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi
|
|||
Profile").{" "}
|
||||
<a
|
||||
href="https://github.com/cline/cline/wiki/Troubleshooting-%E2%80%90-Shell-Integration-Unavailable"
|
||||
style={{ color: "inherit", textDecoration: "underline" }}>
|
||||
style={{
|
||||
color: "inherit",
|
||||
textDecoration: "underline",
|
||||
}}>
|
||||
Still having trouble?
|
||||
</a>
|
||||
</div>
|
||||
|
|
@ -884,7 +1065,13 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi
|
|||
{icon}
|
||||
{title}
|
||||
</div>
|
||||
<p style={{ ...pStyle, color: "var(--vscode-errorForeground)" }}>{message.text}</p>
|
||||
<p
|
||||
style={{
|
||||
...pStyle,
|
||||
color: "var(--vscode-errorForeground)",
|
||||
}}>
|
||||
{message.text}
|
||||
</p>
|
||||
</>
|
||||
)
|
||||
case "auto_approval_max_req_reached":
|
||||
|
|
@ -894,19 +1081,59 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi
|
|||
{icon}
|
||||
{title}
|
||||
</div>
|
||||
<p style={{ ...pStyle, color: "var(--vscode-errorForeground)" }}>{message.text}</p>
|
||||
<p
|
||||
style={{
|
||||
...pStyle,
|
||||
color: "var(--vscode-errorForeground)",
|
||||
}}>
|
||||
{message.text}
|
||||
</p>
|
||||
</>
|
||||
)
|
||||
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
|
||||
return (
|
||||
<div>
|
||||
<div style={headerStyle}>
|
||||
<div
|
||||
style={{
|
||||
...headerStyle,
|
||||
marginBottom: "10px",
|
||||
}}>
|
||||
{icon}
|
||||
{title}
|
||||
</div>
|
||||
<div style={{ color: "var(--vscode-charts-green)", paddingTop: 10 }}>
|
||||
<Markdown markdown={message.text} />
|
||||
<div
|
||||
style={{
|
||||
color: "var(--vscode-charts-green)",
|
||||
paddingTop: 10,
|
||||
}}>
|
||||
<Markdown markdown={text} />
|
||||
{message.partial !== true && hasChanges && (
|
||||
<div style={{ marginTop: 15 }}>
|
||||
<SuccessButton
|
||||
appearance="secondary"
|
||||
disabled={seeNewChangesDisabled}
|
||||
onClick={() => {
|
||||
setSeeNewChangesDisabled(true)
|
||||
vscode.postMessage({
|
||||
type: "taskCompletionViewChanges",
|
||||
number: message.ts,
|
||||
})
|
||||
}}>
|
||||
<i
|
||||
className="codicon codicon-new-file"
|
||||
style={{
|
||||
marginRight: 6,
|
||||
cursor: seeNewChangesDisabled ? "wait" : "pointer",
|
||||
}}
|
||||
/>
|
||||
See new changes
|
||||
</SuccessButton>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
@ -950,7 +1177,13 @@ export const ProgressIndicator = () => (
|
|||
|
||||
const Markdown = memo(({ markdown }: { markdown?: string }) => {
|
||||
return (
|
||||
<div style={{ wordBreak: "break-word", overflowWrap: "anywhere", marginBottom: -15, marginTop: -15 }}>
|
||||
<div
|
||||
style={{
|
||||
wordBreak: "break-word",
|
||||
overflowWrap: "anywhere",
|
||||
marginBottom: -15,
|
||||
marginTop: -15,
|
||||
}}>
|
||||
<MarkdownBlock markdown={markdown} />
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -553,7 +553,12 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|||
bottom: 9.5, // should be 10 but doesnt look good on mac
|
||||
zIndex: 2,
|
||||
}}>
|
||||
<div style={{ display: "flex", flexDirection: "row", alignItems: "center" }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
}}>
|
||||
<div
|
||||
className={`input-icon-button ${shouldDisableImages ? "disabled" : ""} codicon codicon-device-camera`}
|
||||
onClick={() => {
|
||||
|
|
|
|||
|
|
@ -312,7 +312,10 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
|||
case "resume_task":
|
||||
case "mistake_limit_reached":
|
||||
case "auto_approval_max_req_reached":
|
||||
vscode.postMessage({ type: "askResponse", askResponse: "yesButtonClicked" })
|
||||
vscode.postMessage({
|
||||
type: "askResponse",
|
||||
askResponse: "yesButtonClicked",
|
||||
})
|
||||
break
|
||||
case "completion_result":
|
||||
case "resume_completed_task":
|
||||
|
|
@ -346,7 +349,10 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
|||
case "browser_action_launch":
|
||||
case "use_mcp_server":
|
||||
// responds to the API with a "This operation failed" and lets it try again
|
||||
vscode.postMessage({ type: "askResponse", askResponse: "noButtonClicked" })
|
||||
vscode.postMessage({
|
||||
type: "askResponse",
|
||||
askResponse: "noButtonClicked",
|
||||
})
|
||||
break
|
||||
}
|
||||
setTextAreaDisabled(true)
|
||||
|
|
@ -444,6 +450,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
|||
switch (message.say) {
|
||||
case "api_req_finished": // combineApiRequests removes this from modifiedMessages anyways
|
||||
case "api_req_retried": // this message is used to update the latest api_req_started that the request was retried
|
||||
case "deleted_api_reqs": // aggregated api_req metrics from deleted messages
|
||||
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)
|
||||
|
|
@ -786,7 +793,10 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
|||
Footer: () => <div style={{ height: 5 }} />, // Add empty padding at the bottom
|
||||
}}
|
||||
// increasing top by 3_000 to prevent jumping around when user collapses a row
|
||||
increaseViewportBy={{ top: 3_000, bottom: Number.MAX_SAFE_INTEGER }} // hack to make sure the last message is always rendered to get truly perfect scroll to bottom animation when new messages are added (Number.MAX_SAFE_INTEGER is safe for arithmetic operations, which is all virtuoso uses this value for in src/sizeRangeSystem.ts)
|
||||
increaseViewportBy={{
|
||||
top: 3_000,
|
||||
bottom: Number.MAX_SAFE_INTEGER,
|
||||
}} // hack to make sure the last message is always rendered to get truly perfect scroll to bottom animation when new messages are added (Number.MAX_SAFE_INTEGER is safe for arithmetic operations, which is all virtuoso uses this value for in src/sizeRangeSystem.ts)
|
||||
data={groupedMessages} // messages is the raw format returned by extension, modifiedMessages is the manipulated structure that combines certain messages of related type, and visibleMessages is the filtered structure that removes messages that should not be rendered
|
||||
itemContent={itemContent}
|
||||
atBottomStateChange={(isAtBottom) => {
|
||||
|
|
|
|||
|
|
@ -153,7 +153,11 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
|
|||
}}>
|
||||
<i
|
||||
className={`codicon codicon-${getIconForOption(option)}`}
|
||||
style={{ marginRight: "8px", flexShrink: 0, fontSize: "14px" }}
|
||||
style={{
|
||||
marginRight: "8px",
|
||||
flexShrink: 0,
|
||||
fontSize: "14px",
|
||||
}}
|
||||
/>
|
||||
{renderOptionContent(option)}
|
||||
</div>
|
||||
|
|
@ -161,13 +165,24 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
|
|||
!option.value && (
|
||||
<i
|
||||
className="codicon codicon-chevron-right"
|
||||
style={{ fontSize: "14px", flexShrink: 0, marginLeft: 8 }}
|
||||
style={{
|
||||
fontSize: "14px",
|
||||
flexShrink: 0,
|
||||
marginLeft: 8,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{(option.type === ContextMenuOptionType.Problems ||
|
||||
((option.type === ContextMenuOptionType.File || option.type === ContextMenuOptionType.Folder) &&
|
||||
option.value)) && (
|
||||
<i className="codicon codicon-add" style={{ fontSize: "14px", flexShrink: 0, marginLeft: 8 }} />
|
||||
<i
|
||||
className="codicon codicon-add"
|
||||
style={{
|
||||
fontSize: "14px",
|
||||
flexShrink: 0,
|
||||
marginLeft: 8,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
|
||||
import React, { memo, useEffect, useMemo, useRef, useState } from "react"
|
||||
import { useWindowSize } from "react-use"
|
||||
import { mentionRegexGlobal } from "../../../../src/shared/context-mentions"
|
||||
import { ClineMessage } from "../../../../src/shared/ExtensionMessage"
|
||||
import { useExtensionState } from "../../context/ExtensionStateContext"
|
||||
import { formatLargeNumber } from "../../utils/format"
|
||||
import { formatSize } from "../../utils/size"
|
||||
import { vscode } from "../../utils/vscode"
|
||||
import Thumbnails from "../common/Thumbnails"
|
||||
import { mentionRegexGlobal } from "../../../../src/shared/context-mentions"
|
||||
import { formatLargeNumber } from "../../utils/format"
|
||||
|
||||
interface TaskHeaderProps {
|
||||
task: ClineMessage
|
||||
|
|
@ -29,7 +30,7 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
|
|||
totalCost,
|
||||
onClose,
|
||||
}) => {
|
||||
const { apiConfiguration } = useExtensionState()
|
||||
const { apiConfiguration, currentTaskItem, checkpointTrackerErrorMessage } = useExtensionState()
|
||||
const [isTaskExpanded, setIsTaskExpanded] = useState(true)
|
||||
const [isTextExpanded, setIsTextExpanded] = useState(false)
|
||||
const [showSeeMore, setShowSeeMore] = useState(false)
|
||||
|
|
@ -137,7 +138,12 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
|
|||
minWidth: 0, // This allows the div to shrink below its content size
|
||||
}}
|
||||
onClick={() => setIsTaskExpanded(!isTaskExpanded)}>
|
||||
<div style={{ display: "flex", alignItems: "center", flexShrink: 0 }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
flexShrink: 0,
|
||||
}}>
|
||||
<span className={`codicon codicon-chevron-${isTaskExpanded ? "down" : "right"}`}></span>
|
||||
</div>
|
||||
<div
|
||||
|
|
@ -242,47 +248,102 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
|
|||
</div>
|
||||
)}
|
||||
{task.images && task.images.length > 0 && <Thumbnails images={task.images} />}
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "4px" }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "4px",
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
}}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "4px", flexWrap: "wrap" }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "4px",
|
||||
flexWrap: "wrap",
|
||||
}}>
|
||||
<span style={{ fontWeight: "bold" }}>Tokens:</span>
|
||||
<span style={{ display: "flex", alignItems: "center", gap: "3px" }}>
|
||||
<span
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "3px",
|
||||
}}>
|
||||
<i
|
||||
className="codicon codicon-arrow-up"
|
||||
style={{ fontSize: "12px", fontWeight: "bold", marginBottom: "-2px" }}
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
fontWeight: "bold",
|
||||
marginBottom: "-2px",
|
||||
}}
|
||||
/>
|
||||
{formatLargeNumber(tokensIn || 0)}
|
||||
</span>
|
||||
<span style={{ display: "flex", alignItems: "center", gap: "3px" }}>
|
||||
<span
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "3px",
|
||||
}}>
|
||||
<i
|
||||
className="codicon codicon-arrow-down"
|
||||
style={{ fontSize: "12px", fontWeight: "bold", marginBottom: "-2px" }}
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
fontWeight: "bold",
|
||||
marginBottom: "-2px",
|
||||
}}
|
||||
/>
|
||||
{formatLargeNumber(tokensOut || 0)}
|
||||
</span>
|
||||
</div>
|
||||
{!isCostAvailable && <ExportButton />}
|
||||
{!isCostAvailable && (
|
||||
<DeleteButton taskSize={formatSize(currentTaskItem?.size)} taskId={currentTaskItem?.id} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{shouldShowPromptCacheInfo && (cacheReads !== undefined || cacheWrites !== undefined) && (
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "4px", flexWrap: "wrap" }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "4px",
|
||||
flexWrap: "wrap",
|
||||
}}>
|
||||
<span style={{ fontWeight: "bold" }}>Cache:</span>
|
||||
<span style={{ display: "flex", alignItems: "center", gap: "3px" }}>
|
||||
<span
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "3px",
|
||||
}}>
|
||||
<i
|
||||
className="codicon codicon-database"
|
||||
style={{ fontSize: "12px", fontWeight: "bold", marginBottom: "-1px" }}
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
fontWeight: "bold",
|
||||
marginBottom: "-1px",
|
||||
}}
|
||||
/>
|
||||
+{formatLargeNumber(cacheWrites || 0)}
|
||||
</span>
|
||||
<span style={{ display: "flex", alignItems: "center", gap: "3px" }}>
|
||||
<span
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "3px",
|
||||
}}>
|
||||
<i
|
||||
className="codicon codicon-arrow-right"
|
||||
style={{ fontSize: "12px", fontWeight: "bold", marginBottom: 0 }}
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
fontWeight: "bold",
|
||||
marginBottom: 0,
|
||||
}}
|
||||
/>
|
||||
{formatLargeNumber(cacheReads || 0)}
|
||||
</span>
|
||||
|
|
@ -295,11 +356,44 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
|
|||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
}}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "4px" }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "4px",
|
||||
}}>
|
||||
<span style={{ fontWeight: "bold" }}>API Cost:</span>
|
||||
<span>${totalCost?.toFixed(4)}</span>
|
||||
</div>
|
||||
<ExportButton />
|
||||
<DeleteButton taskSize={formatSize(currentTaskItem?.size)} taskId={currentTaskItem?.id} />
|
||||
</div>
|
||||
)}
|
||||
{checkpointTrackerErrorMessage && (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "8px",
|
||||
color: "var(--vscode-editorWarning-foreground)",
|
||||
fontSize: "11px",
|
||||
}}>
|
||||
<i className="codicon codicon-warning" />
|
||||
<span>
|
||||
{checkpointTrackerErrorMessage}
|
||||
{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",
|
||||
}}>
|
||||
See here for instructions.
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -360,18 +454,41 @@ export const highlightMentions = (text?: string, withShadow = true) => {
|
|||
})
|
||||
}
|
||||
|
||||
const ExportButton = () => (
|
||||
const DeleteButton: React.FC<{
|
||||
taskSize: string
|
||||
taskId?: string
|
||||
}> = ({ taskSize, taskId }) => (
|
||||
<VSCodeButton
|
||||
appearance="icon"
|
||||
onClick={() => vscode.postMessage({ type: "exportCurrentTask" })}
|
||||
style={
|
||||
{
|
||||
// marginBottom: "-2px",
|
||||
// marginRight: "-2.5px",
|
||||
}
|
||||
}>
|
||||
<div style={{ fontSize: "10.5px", fontWeight: "bold", opacity: 0.6 }}>EXPORT</div>
|
||||
onClick={() => vscode.postMessage({ type: "deleteTaskWithId", text: taskId })}
|
||||
style={{ padding: "0px 0px" }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "3px",
|
||||
fontSize: "10px",
|
||||
fontWeight: "bold",
|
||||
opacity: 0.6,
|
||||
}}>
|
||||
<i className={`codicon codicon-trash`} />
|
||||
{taskSize}
|
||||
</div>
|
||||
</VSCodeButton>
|
||||
)
|
||||
|
||||
// const ExportButton = () => (
|
||||
// <VSCodeButton
|
||||
// appearance="icon"
|
||||
// onClick={() => vscode.postMessage({ type: "exportCurrentTask" })}
|
||||
// style={
|
||||
// {
|
||||
// // marginBottom: "-2px",
|
||||
// // marginRight: "-2.5px",
|
||||
// }
|
||||
// }>
|
||||
// <div style={{ fontSize: "10.5px", fontWeight: "bold", opacity: 0.6 }}>EXPORT</div>
|
||||
// </VSCodeButton>
|
||||
// )
|
||||
|
||||
export default memo(TaskHeader)
|
||||
|
|
|
|||
268
webview-ui/src/components/common/CheckpointControls.tsx
Normal file
268
webview-ui/src/components/common/CheckpointControls.tsx
Normal file
|
|
@ -0,0 +1,268 @@
|
|||
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
|
||||
import { useCallback, useRef, useState } from "react"
|
||||
import { useClickAway, useEvent } from "react-use"
|
||||
import styled from "styled-components"
|
||||
import { ExtensionMessage } from "../../../../src/shared/ExtensionMessage"
|
||||
import { vscode } from "../../utils/vscode"
|
||||
import { CODE_BLOCK_BG_COLOR } from "./CodeBlock"
|
||||
import { ClineCheckpointRestore } from "../../../../src/shared/WebviewMessage"
|
||||
|
||||
interface CheckpointOverlayProps {
|
||||
messageTs?: number
|
||||
}
|
||||
|
||||
export const CheckpointOverlay = ({ messageTs }: CheckpointOverlayProps) => {
|
||||
const [compareDisabled, setCompareDisabled] = useState(false)
|
||||
const [restoreTaskDisabled, setRestoreTaskDisabled] = useState(false)
|
||||
const [restoreWorkspaceDisabled, setRestoreWorkspaceDisabled] = useState(false)
|
||||
const [restoreBothDisabled, setRestoreBothDisabled] = useState(false)
|
||||
const [showRestoreConfirm, setShowRestoreConfirm] = useState(false)
|
||||
const [hasMouseEntered, setHasMouseEntered] = useState(false)
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const tooltipRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useClickAway(containerRef, () => {
|
||||
if (showRestoreConfirm) {
|
||||
setShowRestoreConfirm(false)
|
||||
setHasMouseEntered(false)
|
||||
}
|
||||
})
|
||||
|
||||
const handleMessage = useCallback((event: MessageEvent) => {
|
||||
const message: ExtensionMessage = event.data
|
||||
switch (message.type) {
|
||||
case "relinquishControl": {
|
||||
setCompareDisabled(false)
|
||||
setRestoreTaskDisabled(false)
|
||||
setRestoreWorkspaceDisabled(false)
|
||||
setRestoreBothDisabled(false)
|
||||
setShowRestoreConfirm(false)
|
||||
break
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEvent("message", handleMessage)
|
||||
|
||||
const handleRestoreTask = () => {
|
||||
setRestoreTaskDisabled(true)
|
||||
vscode.postMessage({
|
||||
type: "checkpointRestore",
|
||||
number: messageTs,
|
||||
text: "task" satisfies ClineCheckpointRestore,
|
||||
})
|
||||
}
|
||||
|
||||
const handleRestoreWorkspace = () => {
|
||||
setRestoreWorkspaceDisabled(true)
|
||||
vscode.postMessage({
|
||||
type: "checkpointRestore",
|
||||
number: messageTs,
|
||||
text: "workspace" satisfies ClineCheckpointRestore,
|
||||
})
|
||||
}
|
||||
|
||||
const handleRestoreBoth = () => {
|
||||
setRestoreBothDisabled(true)
|
||||
vscode.postMessage({
|
||||
type: "checkpointRestore",
|
||||
number: messageTs,
|
||||
text: "taskAndWorkspace" satisfies ClineCheckpointRestore,
|
||||
})
|
||||
}
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
setHasMouseEntered(true)
|
||||
}
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
if (hasMouseEntered) {
|
||||
setShowRestoreConfirm(false)
|
||||
setHasMouseEntered(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleControlsMouseLeave = (e: React.MouseEvent) => {
|
||||
const tooltipElement = tooltipRef.current
|
||||
|
||||
if (tooltipElement && showRestoreConfirm) {
|
||||
const tooltipRect = tooltipElement.getBoundingClientRect()
|
||||
|
||||
// If mouse is moving towards the tooltip, don't close it
|
||||
if (
|
||||
e.clientY >= tooltipRect.top &&
|
||||
e.clientY <= tooltipRect.bottom &&
|
||||
e.clientX >= tooltipRect.left &&
|
||||
e.clientX <= tooltipRect.right
|
||||
) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
setShowRestoreConfirm(false)
|
||||
setHasMouseEntered(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<CheckpointControls onMouseLeave={handleControlsMouseLeave}>
|
||||
<VSCodeButton
|
||||
title="Compare"
|
||||
appearance="secondary"
|
||||
disabled={compareDisabled}
|
||||
style={{ cursor: compareDisabled ? "wait" : "pointer" }}
|
||||
onClick={() => {
|
||||
setCompareDisabled(true)
|
||||
vscode.postMessage({
|
||||
type: "checkpointDiff",
|
||||
number: messageTs,
|
||||
})
|
||||
}}>
|
||||
<i className="codicon codicon-diff-multiple" style={{ position: "absolute" }} />
|
||||
</VSCodeButton>
|
||||
<div style={{ position: "relative" }} ref={containerRef}>
|
||||
<VSCodeButton
|
||||
title="Restore"
|
||||
appearance="secondary"
|
||||
style={{ cursor: "pointer" }}
|
||||
onClick={() => setShowRestoreConfirm(true)}>
|
||||
<i className="codicon codicon-discard" style={{ position: "absolute" }} />
|
||||
</VSCodeButton>
|
||||
{showRestoreConfirm && (
|
||||
<RestoreConfirmTooltip ref={tooltipRef} onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
|
||||
<RestoreOption>
|
||||
<VSCodeButton
|
||||
onClick={handleRestoreBoth}
|
||||
disabled={restoreBothDisabled}
|
||||
style={{
|
||||
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>
|
||||
</RestoreOption>
|
||||
<RestoreOption>
|
||||
<VSCodeButton
|
||||
onClick={handleRestoreTask}
|
||||
disabled={restoreTaskDisabled}
|
||||
style={{
|
||||
cursor: restoreTaskDisabled ? "wait" : "pointer",
|
||||
}}>
|
||||
Restore Task Only
|
||||
</VSCodeButton>
|
||||
<p>Deletes messages after this point (does not affect workspace)</p>
|
||||
</RestoreOption>
|
||||
<RestoreOption>
|
||||
<VSCodeButton
|
||||
onClick={handleRestoreWorkspace}
|
||||
disabled={restoreWorkspaceDisabled}
|
||||
style={{
|
||||
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>
|
||||
</RestoreOption>
|
||||
</RestoreConfirmTooltip>
|
||||
)}
|
||||
</div>
|
||||
</CheckpointControls>
|
||||
)
|
||||
}
|
||||
|
||||
export const CheckpointControls = styled.div`
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
right: 6px;
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
opacity: 0;
|
||||
background-color: var(--vscode-sideBar-background);
|
||||
padding: 3px 0 3px 3px;
|
||||
|
||||
& > vscode-button,
|
||||
& > div > vscode-button {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
& > vscode-button i,
|
||||
& > div > vscode-button i {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
`
|
||||
|
||||
const RestoreOption = styled.div`
|
||||
&:not(:last-child) {
|
||||
margin-bottom: 10px;
|
||||
padding-bottom: 4px;
|
||||
border-bottom: 1px solid var(--vscode-editorGroup-border);
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0 0 2px 0;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
font-size: 11px;
|
||||
line-height: 14px;
|
||||
}
|
||||
|
||||
&:last-child p {
|
||||
margin: 0 0 -2px 0;
|
||||
}
|
||||
|
||||
vscode-button {
|
||||
width: 100%;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
`
|
||||
|
||||
const RestoreConfirmTooltip = styled.div`
|
||||
position: absolute;
|
||||
top: calc(100% - 0.5px);
|
||||
right: 0;
|
||||
background: ${CODE_BLOCK_BG_COLOR};
|
||||
border: 1px solid var(--vscode-editorGroup-border);
|
||||
padding: 12px;
|
||||
border-radius: 3px;
|
||||
margin-top: 8px;
|
||||
width: calc(100vw - 57px);
|
||||
min-width: 0px;
|
||||
max-width: 100vw;
|
||||
z-index: 1000;
|
||||
|
||||
// Add invisible padding to create a safe hover zone
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -8px; // Same as margin-top
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
// Adjust arrow to be above the padding
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -6px;
|
||||
right: 6px;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background: ${CODE_BLOCK_BG_COLOR};
|
||||
border-left: 1px solid var(--vscode-editorGroup-border);
|
||||
border-top: 1px solid var(--vscode-editorGroup-border);
|
||||
transform: rotate(45deg);
|
||||
z-index: 1; // Ensure arrow stays above the padding
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0 0 6px 0;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
font-size: 12px;
|
||||
white-space: normal;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
`
|
||||
30
webview-ui/src/components/common/SuccessButton.tsx
Normal file
30
webview-ui/src/components/common/SuccessButton.tsx
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
|
||||
import styled from "styled-components"
|
||||
|
||||
const StyledButton = styled(VSCodeButton)`
|
||||
--success-button-bg: #176f2c;
|
||||
--success-button-hover: #197f31;
|
||||
--success-button-active: #156528;
|
||||
|
||||
background-color: var(--success-button-bg) !important;
|
||||
border-color: var(--success-button-bg) !important;
|
||||
color: #ffffff !important;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--success-button-hover) !important;
|
||||
border-color: var(--success-button-hover) !important;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: var(--success-button-active) !important;
|
||||
border-color: var(--success-button-active) !important;
|
||||
}
|
||||
`
|
||||
|
||||
interface SuccessButtonProps extends React.ComponentProps<typeof VSCodeButton> {}
|
||||
|
||||
const SuccessButton: React.FC<SuccessButtonProps> = (props) => {
|
||||
return <StyledButton {...props} />
|
||||
}
|
||||
|
||||
export default SuccessButton
|
||||
|
|
@ -59,7 +59,10 @@ const HistoryPreview = ({ showHistoryView }: HistoryPreviewProps) => {
|
|||
}}>
|
||||
<span
|
||||
className="codicon codicon-comment-discussion"
|
||||
style={{ marginRight: "4px", transform: "scale(0.9)" }}></span>
|
||||
style={{
|
||||
marginRight: "4px",
|
||||
transform: "scale(0.9)",
|
||||
}}></span>
|
||||
<span
|
||||
style={{
|
||||
fontWeight: 500,
|
||||
|
|
@ -103,7 +106,11 @@ const HistoryPreview = ({ showHistoryView }: HistoryPreviewProps) => {
|
|||
}}>
|
||||
{item.task}
|
||||
</div>
|
||||
<div style={{ fontSize: "0.85em", color: "var(--vscode-descriptionForeground)" }}>
|
||||
<div
|
||||
style={{
|
||||
fontSize: "0.85em",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
}}>
|
||||
<span>
|
||||
Tokens: ↑{formatLargeNumber(item.tokensIn || 0)} ↓{formatLargeNumber(item.tokensOut || 0)}
|
||||
</span>
|
||||
|
|
@ -126,7 +133,12 @@ const HistoryPreview = ({ showHistoryView }: HistoryPreviewProps) => {
|
|||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div style={{ display: "flex", alignItems: "center", justifyContent: "center" }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}>
|
||||
<VSCodeButton
|
||||
appearance="icon"
|
||||
onClick={() => showHistoryView()}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { Virtuoso } from "react-virtuoso"
|
|||
import { memo, useMemo, useState, useEffect } from "react"
|
||||
import Fuse, { FuseResult } from "fuse.js"
|
||||
import { formatLargeNumber } from "../../utils/format"
|
||||
import { formatSize } from "../../utils/size"
|
||||
|
||||
type HistoryViewProps = {
|
||||
onDone: () => void
|
||||
|
|
@ -136,11 +137,22 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
|
|||
alignItems: "center",
|
||||
padding: "10px 17px 10px 20px",
|
||||
}}>
|
||||
<h3 style={{ color: "var(--vscode-foreground)", margin: 0 }}>History</h3>
|
||||
<h3
|
||||
style={{
|
||||
color: "var(--vscode-foreground)",
|
||||
margin: 0,
|
||||
}}>
|
||||
History
|
||||
</h3>
|
||||
<VSCodeButton onClick={onDone}>Done</VSCodeButton>
|
||||
</div>
|
||||
<div style={{ padding: "5px 17px 6px 17px" }}>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "6px" }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "6px",
|
||||
}}>
|
||||
<VSCodeTextField
|
||||
style={{ width: "100%" }}
|
||||
placeholder="Fuzzy search history..."
|
||||
|
|
@ -156,7 +168,11 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
|
|||
<div
|
||||
slot="start"
|
||||
className="codicon codicon-search"
|
||||
style={{ fontSize: 13, marginTop: 2.5, opacity: 0.8 }}></div>
|
||||
style={{
|
||||
fontSize: 13,
|
||||
marginTop: 2.5,
|
||||
opacity: 0.8,
|
||||
}}></div>
|
||||
{searchQuery && (
|
||||
<div
|
||||
className="input-icon-button codicon codicon-close"
|
||||
|
|
@ -248,8 +264,19 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
|
|||
e.stopPropagation()
|
||||
handleDeleteHistoryItem(item.id)
|
||||
}}
|
||||
className="delete-button">
|
||||
<span className="codicon codicon-trash"></span>
|
||||
className="delete-button"
|
||||
style={{ padding: "0px 0px" }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "3px",
|
||||
fontSize: "11px",
|
||||
// fontWeight: "bold",
|
||||
}}>
|
||||
<span className="codicon codicon-trash"></span>
|
||||
{formatSize(item.size)}
|
||||
</div>
|
||||
</VSCodeButton>
|
||||
</div>
|
||||
<div
|
||||
|
|
@ -264,9 +291,16 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
|
|||
wordBreak: "break-word",
|
||||
overflowWrap: "anywhere",
|
||||
}}
|
||||
dangerouslySetInnerHTML={{ __html: item.task }}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: item.task,
|
||||
}}
|
||||
/>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "4px" }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "4px",
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
|
|
@ -384,7 +418,12 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
|
|||
alignItems: "center",
|
||||
marginTop: -2,
|
||||
}}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "4px" }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "4px",
|
||||
}}>
|
||||
<span
|
||||
style={{
|
||||
fontWeight: 500,
|
||||
|
|
@ -392,7 +431,10 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
|
|||
}}>
|
||||
API Cost:
|
||||
</span>
|
||||
<span style={{ color: "var(--vscode-descriptionForeground)" }}>
|
||||
<span
|
||||
style={{
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
}}>
|
||||
${item.totalCost?.toFixed(4)}
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -37,7 +37,13 @@ const McpToolRow = ({ tool }: McpToolRowProps) => {
|
|||
borderRadius: "3px",
|
||||
padding: "8px",
|
||||
}}>
|
||||
<div style={{ marginBottom: "4px", opacity: 0.8, fontSize: "11px", textTransform: "uppercase" }}>
|
||||
<div
|
||||
style={{
|
||||
marginBottom: "4px",
|
||||
opacity: 0.8,
|
||||
fontSize: "11px",
|
||||
textTransform: "uppercase",
|
||||
}}>
|
||||
Parameters
|
||||
</div>
|
||||
{Object.entries(tool.inputSchema.properties as Record<string, any>).map(([paramName, schema]) => {
|
||||
|
|
@ -61,7 +67,14 @@ const McpToolRow = ({ tool }: McpToolRowProps) => {
|
|||
marginRight: "8px",
|
||||
}}>
|
||||
{paramName}
|
||||
{isRequired && <span style={{ color: "var(--vscode-errorForeground)" }}>*</span>}
|
||||
{isRequired && (
|
||||
<span
|
||||
style={{
|
||||
color: "var(--vscode-errorForeground)",
|
||||
}}>
|
||||
*
|
||||
</span>
|
||||
)}
|
||||
</code>
|
||||
<span
|
||||
style={{
|
||||
|
|
|
|||
|
|
@ -120,7 +120,12 @@ const McpView = ({ onDone }: McpViewProps) => {
|
|||
|
||||
{/* Server List */}
|
||||
{servers.length > 0 && (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "10px" }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "10px",
|
||||
}}>
|
||||
{servers.map((server) => (
|
||||
<ServerRow key={server.name} server={server} />
|
||||
))}
|
||||
|
|
@ -224,7 +229,10 @@ const ServerRow = ({ server }: { server: McpServer }) => {
|
|||
appearance="secondary"
|
||||
onClick={handleRestart}
|
||||
disabled={server.status === "connecting"}
|
||||
style={{ width: "calc(100% - 20px)", margin: "0 10px 10px 10px" }}>
|
||||
style={{
|
||||
width: "calc(100% - 20px)",
|
||||
margin: "0 10px 10px 10px",
|
||||
}}>
|
||||
{server.status === "connecting" ? "Retrying..." : "Retry Connection"}
|
||||
</VSCodeButton>
|
||||
</div>
|
||||
|
|
@ -245,13 +253,23 @@ const ServerRow = ({ server }: { server: McpServer }) => {
|
|||
|
||||
<VSCodePanelView id="tools-view">
|
||||
{server.tools && server.tools.length > 0 ? (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "8px", width: "100%" }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "8px",
|
||||
width: "100%",
|
||||
}}>
|
||||
{server.tools.map((tool) => (
|
||||
<McpToolRow key={tool.name} tool={tool} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ padding: "10px 0", color: "var(--vscode-descriptionForeground)" }}>
|
||||
<div
|
||||
style={{
|
||||
padding: "10px 0",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
}}>
|
||||
No tools found
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -260,7 +278,13 @@ const ServerRow = ({ server }: { server: McpServer }) => {
|
|||
<VSCodePanelView id="resources-view">
|
||||
{(server.resources && server.resources.length > 0) ||
|
||||
(server.resourceTemplates && server.resourceTemplates.length > 0) ? (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "8px", width: "100%" }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "8px",
|
||||
width: "100%",
|
||||
}}>
|
||||
{[...(server.resourceTemplates || []), ...(server.resources || [])].map((item) => (
|
||||
<McpResourceRow
|
||||
key={"uriTemplate" in item ? item.uriTemplate : item.uri}
|
||||
|
|
@ -269,7 +293,11 @@ const ServerRow = ({ server }: { server: McpServer }) => {
|
|||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ padding: "10px 0", color: "var(--vscode-descriptionForeground)" }}>
|
||||
<div
|
||||
style={{
|
||||
padding: "10px 0",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
}}>
|
||||
No resources found
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -280,7 +308,10 @@ const ServerRow = ({ server }: { server: McpServer }) => {
|
|||
appearance="secondary"
|
||||
onClick={handleRestart}
|
||||
disabled={server.status === "connecting"}
|
||||
style={{ width: "calc(100% - 14px)", margin: "0 7px 3px 7px" }}>
|
||||
style={{
|
||||
width: "calc(100% - 14px)",
|
||||
margin: "0 7px 3px 7px",
|
||||
}}>
|
||||
{server.status === "connecting" ? "Restarting..." : "Restart Server"}
|
||||
</VSCodeButton>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -50,7 +50,10 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage }:
|
|||
const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false)
|
||||
|
||||
const handleInputChange = (field: keyof ApiConfiguration) => (event: any) => {
|
||||
setApiConfiguration({ ...apiConfiguration, [field]: event.target.value })
|
||||
setApiConfiguration({
|
||||
...apiConfiguration,
|
||||
[field]: event.target.value,
|
||||
})
|
||||
}
|
||||
|
||||
const { selectedProvider, selectedModelId, selectedModelInfo } = useMemo(() => {
|
||||
|
|
@ -60,9 +63,15 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage }:
|
|||
// Poll ollama/lmstudio models
|
||||
const requestLocalModels = useCallback(() => {
|
||||
if (selectedProvider === "ollama") {
|
||||
vscode.postMessage({ type: "requestOllamaModels", text: apiConfiguration?.ollamaBaseUrl })
|
||||
vscode.postMessage({
|
||||
type: "requestOllamaModels",
|
||||
text: apiConfiguration?.ollamaBaseUrl,
|
||||
})
|
||||
} else if (selectedProvider === "lmstudio") {
|
||||
vscode.postMessage({ type: "requestLmStudioModels", text: apiConfiguration?.lmStudioBaseUrl })
|
||||
vscode.postMessage({
|
||||
type: "requestLmStudioModels",
|
||||
text: apiConfiguration?.lmStudioBaseUrl,
|
||||
})
|
||||
}
|
||||
}, [selectedProvider, apiConfiguration?.ollamaBaseUrl, apiConfiguration?.lmStudioBaseUrl])
|
||||
useEffect(() => {
|
||||
|
|
@ -124,7 +133,11 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage }:
|
|||
id="api-provider"
|
||||
value={selectedProvider}
|
||||
onChange={handleInputChange("apiProvider")}
|
||||
style={{ minWidth: 130, position: "relative", zIndex: OPENROUTER_MODEL_PICKER_Z_INDEX + 1 }}>
|
||||
style={{
|
||||
minWidth: 130,
|
||||
position: "relative",
|
||||
zIndex: OPENROUTER_MODEL_PICKER_Z_INDEX + 1,
|
||||
}}>
|
||||
<VSCodeOption value="openrouter">OpenRouter</VSCodeOption>
|
||||
<VSCodeOption value="anthropic">Anthropic</VSCodeOption>
|
||||
<VSCodeOption value="gemini">Google Gemini</VSCodeOption>
|
||||
|
|
@ -155,7 +168,10 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage }:
|
|||
const isChecked = e.target.checked === true
|
||||
setAnthropicBaseUrlSelected(isChecked)
|
||||
if (!isChecked) {
|
||||
setApiConfiguration({ ...apiConfiguration, anthropicBaseUrl: "" })
|
||||
setApiConfiguration({
|
||||
...apiConfiguration,
|
||||
anthropicBaseUrl: "",
|
||||
})
|
||||
}
|
||||
}}>
|
||||
Use custom base URL
|
||||
|
|
@ -181,7 +197,10 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage }:
|
|||
{!apiConfiguration?.apiKey && (
|
||||
<VSCodeLink
|
||||
href="https://console.anthropic.com/settings/keys"
|
||||
style={{ display: "inline", fontSize: "inherit" }}>
|
||||
style={{
|
||||
display: "inline",
|
||||
fontSize: "inherit",
|
||||
}}>
|
||||
You can get an Anthropic API key by signing up here.
|
||||
</VSCodeLink>
|
||||
)}
|
||||
|
|
@ -209,7 +228,10 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage }:
|
|||
{!apiConfiguration?.openAiNativeApiKey && (
|
||||
<VSCodeLink
|
||||
href="https://platform.openai.com/api-keys"
|
||||
style={{ display: "inline", fontSize: "inherit" }}>
|
||||
style={{
|
||||
display: "inline",
|
||||
fontSize: "inherit",
|
||||
}}>
|
||||
You can get an OpenAI API key by signing up here.
|
||||
</VSCodeLink>
|
||||
)}
|
||||
|
|
@ -235,7 +257,12 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage }:
|
|||
}}>
|
||||
This key is stored locally and only used to make API requests from this extension.
|
||||
{!apiConfiguration?.deepSeekApiKey && (
|
||||
<VSCodeLink href="https://www.deepseek.com/" style={{ display: "inline", fontSize: "inherit" }}>
|
||||
<VSCodeLink
|
||||
href="https://www.deepseek.com/"
|
||||
style={{
|
||||
display: "inline",
|
||||
fontSize: "inherit",
|
||||
}}>
|
||||
You can get a DeepSeek API key by signing up here.
|
||||
</VSCodeLink>
|
||||
)}
|
||||
|
|
@ -279,7 +306,12 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage }:
|
|||
)}
|
||||
|
||||
{selectedProvider === "bedrock" && (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 5 }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 5,
|
||||
}}>
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.awsAccessKey || ""}
|
||||
style={{ width: "100%" }}
|
||||
|
|
@ -345,7 +377,10 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage }:
|
|||
checked={apiConfiguration?.awsUseCrossRegionInference || false}
|
||||
onChange={(e: any) => {
|
||||
const isChecked = e.target.checked === true
|
||||
setApiConfiguration({ ...apiConfiguration, awsUseCrossRegionInference: isChecked })
|
||||
setApiConfiguration({
|
||||
...apiConfiguration,
|
||||
awsUseCrossRegionInference: isChecked,
|
||||
})
|
||||
}}>
|
||||
Use cross-region inference
|
||||
</VSCodeCheckbox>
|
||||
|
|
@ -363,7 +398,12 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage }:
|
|||
)}
|
||||
|
||||
{apiConfiguration?.apiProvider === "vertex" && (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 5 }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 5,
|
||||
}}>
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.vertexProjectId || ""}
|
||||
style={{ width: "100%" }}
|
||||
|
|
@ -427,7 +467,12 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage }:
|
|||
}}>
|
||||
This key is stored locally and only used to make API requests from this extension.
|
||||
{!apiConfiguration?.geminiApiKey && (
|
||||
<VSCodeLink href="https://ai.google.dev/" style={{ display: "inline", fontSize: "inherit" }}>
|
||||
<VSCodeLink
|
||||
href="https://ai.google.dev/"
|
||||
style={{
|
||||
display: "inline",
|
||||
fontSize: "inherit",
|
||||
}}>
|
||||
You can get a Gemini API key by signing up here.
|
||||
</VSCodeLink>
|
||||
)}
|
||||
|
|
@ -466,7 +511,10 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage }:
|
|||
const isChecked = e.target.checked === true
|
||||
setAzureApiVersionSelected(isChecked)
|
||||
if (!isChecked) {
|
||||
setApiConfiguration({ ...apiConfiguration, azureApiVersion: "" })
|
||||
setApiConfiguration({
|
||||
...apiConfiguration,
|
||||
azureApiVersion: "",
|
||||
})
|
||||
}
|
||||
}}>
|
||||
Set Azure API version
|
||||
|
|
@ -768,7 +816,12 @@ export const ModelInfoView = ({
|
|||
].filter(Boolean)
|
||||
|
||||
return (
|
||||
<p style={{ fontSize: "12px", marginTop: "2px", color: "var(--vscode-descriptionForeground)" }}>
|
||||
<p
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
marginTop: "2px",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
}}>
|
||||
{infoItems.map((item, index) => (
|
||||
<Fragment key={index}>
|
||||
{item}
|
||||
|
|
@ -821,7 +874,11 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration) {
|
|||
selectedModelId = defaultId
|
||||
selectedModelInfo = models[defaultId]
|
||||
}
|
||||
return { selectedProvider: provider, selectedModelId, selectedModelInfo }
|
||||
return {
|
||||
selectedProvider: provider,
|
||||
selectedModelId,
|
||||
selectedModelInfo,
|
||||
}
|
||||
}
|
||||
switch (provider) {
|
||||
case "anthropic":
|
||||
|
|
|
|||
|
|
@ -153,7 +153,11 @@ const OpenRouterModelPicker: React.FC = () => {
|
|||
}}
|
||||
onFocus={() => setIsDropdownVisible(true)}
|
||||
onKeyDown={handleKeyDown}
|
||||
style={{ width: "100%", zIndex: OPENROUTER_MODEL_PICKER_Z_INDEX, position: "relative" }}>
|
||||
style={{
|
||||
width: "100%",
|
||||
zIndex: OPENROUTER_MODEL_PICKER_Z_INDEX,
|
||||
position: "relative",
|
||||
}}>
|
||||
{searchTerm && (
|
||||
<div
|
||||
className="input-icon-button codicon codicon-close"
|
||||
|
|
|
|||
|
|
@ -23,7 +23,10 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
|||
setModelIdErrorMessage(modelIdValidationResult)
|
||||
if (!apiValidationResult && !modelIdValidationResult) {
|
||||
vscode.postMessage({ type: "apiConfiguration", apiConfiguration })
|
||||
vscode.postMessage({ type: "customInstructions", text: customInstructions })
|
||||
vscode.postMessage({
|
||||
type: "customInstructions",
|
||||
text: customInstructions,
|
||||
})
|
||||
onDone()
|
||||
}
|
||||
}
|
||||
|
|
@ -73,7 +76,14 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
|||
<h3 style={{ color: "var(--vscode-foreground)", margin: 0 }}>Settings</h3>
|
||||
<VSCodeButton onClick={handleSubmit}>Done</VSCodeButton>
|
||||
</div>
|
||||
<div style={{ flexGrow: 1, overflowY: "scroll", paddingRight: 8, display: "flex", flexDirection: "column" }}>
|
||||
<div
|
||||
style={{
|
||||
flexGrow: 1,
|
||||
overflowY: "scroll",
|
||||
paddingRight: 8,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}}>
|
||||
<div style={{ marginBottom: 5 }}>
|
||||
<ApiOptions
|
||||
showModelOptions={true}
|
||||
|
|
@ -127,13 +137,25 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
|||
marginTop: "auto",
|
||||
padding: "10px 8px 15px 0px",
|
||||
}}>
|
||||
<p style={{ wordWrap: "break-word", margin: 0, padding: 0 }}>
|
||||
<p
|
||||
style={{
|
||||
wordWrap: "break-word",
|
||||
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" }}>
|
||||
https://github.com/cline/cline
|
||||
</VSCodeLink>
|
||||
</p>
|
||||
<p style={{ fontStyle: "italic", margin: "10px 0 0 0", padding: 0 }}>v{version}</p>
|
||||
<p
|
||||
style={{
|
||||
fontStyle: "italic",
|
||||
margin: "10px 0 0 0",
|
||||
padding: 0,
|
||||
}}>
|
||||
v{version}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -21,7 +21,15 @@ const WelcomeView = () => {
|
|||
}, [apiConfiguration])
|
||||
|
||||
return (
|
||||
<div style={{ position: "fixed", top: 0, left: 0, right: 0, bottom: 0, padding: "0 20px" }}>
|
||||
<div
|
||||
style={{
|
||||
position: "fixed",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
padding: "0 20px",
|
||||
}}>
|
||||
<h2>Hi, I'm Cline</h2>
|
||||
<p>
|
||||
I can do all kinds of tasks thanks to the latest breakthroughs in{" "}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,9 @@ interface ExtensionStateContextType extends ExtensionState {
|
|||
|
||||
const ExtensionStateContext = createContext<ExtensionStateContextType | undefined>(undefined)
|
||||
|
||||
export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
export const ExtensionStateContextProvider: React.FC<{
|
||||
children: React.ReactNode
|
||||
}> = ({ children }) => {
|
||||
const [state, setState] = useState<ExtensionState>({
|
||||
version: "",
|
||||
clineMessages: [],
|
||||
|
|
@ -116,9 +118,21 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||
openRouterModels,
|
||||
mcpServers,
|
||||
filePaths,
|
||||
setApiConfiguration: (value) => setState((prevState) => ({ ...prevState, apiConfiguration: value })),
|
||||
setCustomInstructions: (value) => setState((prevState) => ({ ...prevState, customInstructions: value })),
|
||||
setShowAnnouncement: (value) => setState((prevState) => ({ ...prevState, shouldShowAnnouncement: value })),
|
||||
setApiConfiguration: (value) =>
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
apiConfiguration: value,
|
||||
})),
|
||||
setCustomInstructions: (value) =>
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
customInstructions: value,
|
||||
})),
|
||||
setShowAnnouncement: (value) =>
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
shouldShowAnnouncement: value,
|
||||
})),
|
||||
}
|
||||
|
||||
return <ExtensionStateContext.Provider value={contextValue}>{children}</ExtensionStateContext.Provider>
|
||||
|
|
|
|||
|
|
@ -64,14 +64,20 @@ export function getContextMenuOptions(
|
|||
if (selectedType === ContextMenuOptionType.File) {
|
||||
const files = queryItems
|
||||
.filter((item) => item.type === ContextMenuOptionType.File)
|
||||
.map((item) => ({ type: ContextMenuOptionType.File, value: item.value }))
|
||||
.map((item) => ({
|
||||
type: ContextMenuOptionType.File,
|
||||
value: item.value,
|
||||
}))
|
||||
return files.length > 0 ? files : [{ type: ContextMenuOptionType.NoResults }]
|
||||
}
|
||||
|
||||
if (selectedType === ContextMenuOptionType.Folder) {
|
||||
const folders = queryItems
|
||||
.filter((item) => item.type === ContextMenuOptionType.Folder)
|
||||
.map((item) => ({ type: ContextMenuOptionType.Folder, value: item.value }))
|
||||
.map((item) => ({
|
||||
type: ContextMenuOptionType.Folder,
|
||||
value: item.value,
|
||||
}))
|
||||
return folders.length > 0 ? folders : [{ type: ContextMenuOptionType.NoResults }]
|
||||
}
|
||||
|
||||
|
|
|
|||
9
webview-ui/src/utils/size.ts
Normal file
9
webview-ui/src/utils/size.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import prettyBytes from "pretty-bytes"
|
||||
|
||||
export function formatSize(bytes?: number) {
|
||||
if (bytes === undefined) {
|
||||
return "--kb"
|
||||
}
|
||||
|
||||
return prettyBytes(bytes)
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue