mirror of
https://github.com/cline/cline.git
synced 2026-09-06 20:41:02 +08:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e8c7dc1e7f | |||
| 47aecdfc75 | |||
| fb037a05c1 | |||
| d9cedc41b7 | |||
| f978ecce52 | |||
| 4c28760557 | |||
| ba79a51dd7 | |||
| 4e5cc92065 | |||
| b42c0f2571 | |||
| 386d5e41e7 | |||
| 2823e6c845 | |||
| cef9af16a4 | |||
| 49d3bcfedc | |||
| d36a44ec38 | |||
| cbcf89d634 | |||
| 6a10e30436 | |||
| 2c0afbc3be | |||
| 2ef4e56bca | |||
| e26d001585 | |||
| 6fc2cb128e |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
better prompting around downloading mcp servers to not overwrite existing server configs
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Add button to open config to MCP toggle modal
|
||||
Vendored
+13
@@ -16,6 +16,19 @@
|
||||
"IS_DEV": "true",
|
||||
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Run Extension (Test Mode)",
|
||||
"type": "extensionHost",
|
||||
"request": "launch",
|
||||
"args": ["--extensionDevelopmentPath=${workspaceFolder}"],
|
||||
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
|
||||
"preLaunchTask": "${defaultBuildTask}",
|
||||
"env": {
|
||||
"IS_DEV": "true",
|
||||
"IS_TEST": "true",
|
||||
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Vendored
+51
@@ -14,6 +14,14 @@
|
||||
"isDefault": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "watch:test",
|
||||
"dependsOn": ["npm: build:webview:test", "npm: dev:webview", "npm: watch:tsc", "npm: watch:esbuild:test"],
|
||||
"presentation": {
|
||||
"reveal": "never"
|
||||
},
|
||||
"group": "build"
|
||||
},
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "build:webview",
|
||||
@@ -32,6 +40,25 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "build:webview:test",
|
||||
"group": "build",
|
||||
"problemMatcher": [],
|
||||
"isBackground": true,
|
||||
"label": "npm: build:webview:test",
|
||||
"presentation": {
|
||||
"group": "watch",
|
||||
"reveal": "never",
|
||||
"close": true
|
||||
},
|
||||
"options": {
|
||||
"env": {
|
||||
"IS_DEV": "true",
|
||||
"IS_TEST": "true"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "dev:webview",
|
||||
@@ -77,6 +104,30 @@
|
||||
"group": "watch",
|
||||
"reveal": "never",
|
||||
"close": true
|
||||
},
|
||||
"options": {
|
||||
"env": {
|
||||
"IS_DEV": "true"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "watch:esbuild:test",
|
||||
"group": "build",
|
||||
"problemMatcher": "$esbuild-watch",
|
||||
"isBackground": true,
|
||||
"label": "npm: watch:esbuild:test",
|
||||
"presentation": {
|
||||
"group": "watch",
|
||||
"reveal": "never",
|
||||
"close": true
|
||||
},
|
||||
"options": {
|
||||
"env": {
|
||||
"IS_DEV": "true",
|
||||
"IS_TEST": "true"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
+7
-1
@@ -1,10 +1,16 @@
|
||||
# Changelog
|
||||
|
||||
## [3.11.0]
|
||||
|
||||
- Redesign checkpoint UI to declutter chat view by using a subtle indicator line that expands to a popover on hover, with a new date indicator for when it was created
|
||||
- Add support for xAI's provider's Grok 3 models
|
||||
- Add more robust error tracking for users opted in to telemetry (thank you for helping us make Cline better!)
|
||||
|
||||
## [3.10.1]
|
||||
|
||||
- Add CMD+' keyboard shortcut to add selected text to Cline
|
||||
- Cline now auto focuses the text field when using 'Add to Cline' shortcut
|
||||
- Add new 'Create New Task' tool to let Cline start a new task autonomously!
|
||||
- Add new 'Create New Task' tool to let Cline start a new task autonomously!
|
||||
- Fix Mermaid diagram issues
|
||||
- Fix Gemini provider cost calculation to take new tiered pricing structure into account
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ const path = require("path")
|
||||
|
||||
const production = process.argv.includes("--production")
|
||||
const watch = process.argv.includes("--watch")
|
||||
const test = process.env.IS_TEST === "true"
|
||||
|
||||
/**
|
||||
* @type {import('esbuild').Plugin}
|
||||
@@ -68,6 +69,10 @@ const extensionConfig = {
|
||||
minify: production,
|
||||
sourcemap: !production,
|
||||
logLevel: "silent",
|
||||
define: {
|
||||
"process.env.IS_DEV": JSON.stringify(!production),
|
||||
"process.env.IS_TEST": JSON.stringify(test),
|
||||
},
|
||||
plugins: [
|
||||
copyWasmFiles,
|
||||
/* add to the end of plugins array */
|
||||
|
||||
Generated
+78
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "claude-dev",
|
||||
"version": "3.10.0",
|
||||
"version": "3.10.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "claude-dev",
|
||||
"version": "3.10.0",
|
||||
"version": "3.10.1",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/bedrock-sdk": "^0.12.4",
|
||||
@@ -23,6 +23,7 @@
|
||||
"@opentelemetry/sdk-node": "^0.39.1",
|
||||
"@opentelemetry/sdk-trace-node": "^1.30.1",
|
||||
"@opentelemetry/semantic-conventions": "^1.30.0",
|
||||
"@sentry/browser": "^9.12.0",
|
||||
"@vscode/codicons": "^0.0.36",
|
||||
"axios": "^1.8.2",
|
||||
"cheerio": "^1.0.0",
|
||||
@@ -6856,6 +6857,81 @@
|
||||
"integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@sentry-internal/browser-utils": {
|
||||
"version": "9.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-9.12.0.tgz",
|
||||
"integrity": "sha512-GXuDEG2Ix8DmVtTkjsItWdusk2CvJ6EPWKYVqFKifxt+IAT3ZbhGZd99Rg3wdRmt9xhCNuS4QrDzDTPMPgfdCw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sentry/core": "9.12.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry-internal/feedback": {
|
||||
"version": "9.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-9.12.0.tgz",
|
||||
"integrity": "sha512-3+UxoT97QIXNSUQS4ATL1FFws0RkUb6PeaQN8CPndI6mFlqTW5tuVVLNg9Eo1seNg7R/dfk6WHCWrYN1NbFFKQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sentry/core": "9.12.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry-internal/replay": {
|
||||
"version": "9.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-9.12.0.tgz",
|
||||
"integrity": "sha512-njEQosFeO/UX+gG+DMRANkPUuz6OIJLb+A1GVylhq9adUgFQydQ9Ay3v7/x1gMhdfHVP6Jeb27qkti0BWYbzBQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sentry-internal/browser-utils": "9.12.0",
|
||||
"@sentry/core": "9.12.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry-internal/replay-canvas": {
|
||||
"version": "9.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-9.12.0.tgz",
|
||||
"integrity": "sha512-p8LuKZgWT/CoQBbDOXkSGjWWnc8WsnAayWgna8M/ZFWNITCNEM2rCuqZOyWOElIlrni+M7qoEA3jS7MZe8Ejxw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sentry-internal/replay": "9.12.0",
|
||||
"@sentry/core": "9.12.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/browser": {
|
||||
"version": "9.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-9.12.0.tgz",
|
||||
"integrity": "sha512-4xQYoZqi+VVhNvlhWiwRd57+SMr3Og4sLjuayAA+zIp1Wx/bDcIld697cugLwml/BR+mVJI2eokkgh1CBl6zag==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sentry-internal/browser-utils": "9.12.0",
|
||||
"@sentry-internal/feedback": "9.12.0",
|
||||
"@sentry-internal/replay": "9.12.0",
|
||||
"@sentry-internal/replay-canvas": "9.12.0",
|
||||
"@sentry/core": "9.12.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/core": {
|
||||
"version": "9.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-9.12.0.tgz",
|
||||
"integrity": "sha512-jOqQK/90uzHmsBvkPTj/DAEFvA5poX4ZRyC7LE1zjg4F5jdOp3+M4W3qCy0CkSTu88Zu5VWBoppCU2Bs34XEqg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@sindresorhus/merge-streams": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz",
|
||||
|
||||
+6
-1
@@ -2,7 +2,7 @@
|
||||
"name": "claude-dev",
|
||||
"displayName": "Cline",
|
||||
"description": "Autonomous coding agent right in your IDE, capable of creating/editing files, running commands, using the browser, and more with your permission every step of the way.",
|
||||
"version": "3.10.1",
|
||||
"version": "3.11.1",
|
||||
"icon": "assets/icons/icon.png",
|
||||
"engines": {
|
||||
"vscode": "^1.84.0"
|
||||
@@ -302,8 +302,12 @@
|
||||
"compile": "npm run check-types && npm run lint && node esbuild.js",
|
||||
"watch": "npm-run-all -p watch:*",
|
||||
"watch:esbuild": "node esbuild.js --watch",
|
||||
"watch:esbuild:test": "IS_TEST=true node esbuild.js --watch",
|
||||
"watch:tsc": "tsc --noEmit --watch --project tsconfig.json",
|
||||
"package": "npm run build:webview && npm run check-types && npm run lint && node esbuild.js --production",
|
||||
"package:test": "IS_TEST=true npm run build:webview:test && npm run check-types && npm run lint && IS_TEST=true node esbuild.js --production",
|
||||
"build:webview:test": "cd webview-ui && IS_TEST=true npm run build",
|
||||
"watch:test": "IS_TEST=true npm-run-all -p watch:tsc watch:esbuild:test",
|
||||
"compile-tests": "tsc -p ./tsconfig.test.json --outDir out",
|
||||
"watch-tests": "tsc -p . -w --outDir out",
|
||||
"pretest": "npm run compile-tests && npm run compile && npm run lint",
|
||||
@@ -371,6 +375,7 @@
|
||||
"@opentelemetry/sdk-node": "^0.39.1",
|
||||
"@opentelemetry/sdk-trace-node": "^1.30.1",
|
||||
"@opentelemetry/semantic-conventions": "^1.30.0",
|
||||
"@sentry/browser": "^9.12.0",
|
||||
"@vscode/codicons": "^0.0.36",
|
||||
"axios": "^1.8.2",
|
||||
"cheerio": "^1.0.0",
|
||||
|
||||
@@ -24,7 +24,6 @@ export class XAIHandler implements ApiHandler {
|
||||
temperature: 0,
|
||||
messages: [{ role: "system", content: systemPrompt }, ...convertToOpenAiMessages(messages)],
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
})
|
||||
|
||||
for await (const chunk of stream) {
|
||||
|
||||
+6
-6
@@ -1,13 +1,13 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { ClineApiReqInfo, ClineMessage } from "../../shared/ExtensionMessage"
|
||||
import { ApiHandler } from "../../api"
|
||||
import { getContextWindowInfo } from "./context-window-utils"
|
||||
import { formatResponse } from "../prompts/responses"
|
||||
import { GlobalFileNames } from "../storage/disk"
|
||||
import { fileExistsAtPath } from "../../utils/fs"
|
||||
import { formatResponse } from "../../prompts/responses"
|
||||
import { GlobalFileNames } from "../../storage/disk"
|
||||
import { fileExistsAtPath } from "../../../utils/fs"
|
||||
import * as path from "path"
|
||||
import fs from "fs/promises"
|
||||
import cloneDeep from "clone-deep"
|
||||
import { ClineApiReqInfo, ClineMessage } from "../../../shared/ExtensionMessage"
|
||||
import { ApiHandler } from "../../../api"
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
|
||||
enum EditType {
|
||||
UNDEFINED = 0,
|
||||
@@ -0,0 +1,102 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { ClineApiReqInfo, ClineMessage } from "../../../shared/ExtensionMessage"
|
||||
import { ApiHandler } from "../../../api"
|
||||
import { getContextWindowInfo } from "./context-window-utils"
|
||||
|
||||
export class ContextManager {
|
||||
getNewContextMessagesAndMetadata(
|
||||
apiConversationHistory: Anthropic.Messages.MessageParam[],
|
||||
clineMessages: ClineMessage[],
|
||||
api: ApiHandler,
|
||||
conversationHistoryDeletedRange: [number, number] | undefined,
|
||||
previousApiReqIndex: number,
|
||||
) {
|
||||
let updatedConversationHistoryDeletedRange = false
|
||||
|
||||
// If the previous API request's total token usage is close to the context window, truncate the conversation history to free up space for the new request
|
||||
if (previousApiReqIndex >= 0) {
|
||||
const previousRequest = clineMessages[previousApiReqIndex]
|
||||
if (previousRequest && previousRequest.text) {
|
||||
const { tokensIn, tokensOut, cacheWrites, cacheReads }: ClineApiReqInfo = JSON.parse(previousRequest.text)
|
||||
const totalTokens = (tokensIn || 0) + (tokensOut || 0) + (cacheWrites || 0) + (cacheReads || 0)
|
||||
const { maxAllowedSize } = getContextWindowInfo(api)
|
||||
|
||||
// This is the most reliable way to know when we're close to hitting the context window.
|
||||
if (totalTokens >= maxAllowedSize) {
|
||||
// Since the user may switch between models with different context windows, truncating half may not be enough (ie if switching from claude 200k to deepseek 64k, half truncation will only remove 100k tokens, but we need to remove much more)
|
||||
// So if totalTokens/2 is greater than maxAllowedSize, we truncate 3/4 instead of 1/2
|
||||
// FIXME: truncating the conversation in a way that is optimal for prompt caching AND takes into account multi-context window complexity is something we need to improve
|
||||
const keep = totalTokens / 2 > maxAllowedSize ? "quarter" : "half"
|
||||
|
||||
// NOTE: it's okay that we overwriteConversationHistory in resume task since we're only ever removing the last user message and not anything in the middle which would affect this range
|
||||
conversationHistoryDeletedRange = this.getNextTruncationRange(
|
||||
apiConversationHistory,
|
||||
conversationHistoryDeletedRange,
|
||||
keep,
|
||||
)
|
||||
|
||||
updatedConversationHistoryDeletedRange = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// conversationHistoryDeletedRange is updated only when we're close to hitting the context window, so we don't continuously break the prompt cache
|
||||
const truncatedConversationHistory = this.getTruncatedMessages(apiConversationHistory, conversationHistoryDeletedRange)
|
||||
|
||||
return {
|
||||
conversationHistoryDeletedRange: conversationHistoryDeletedRange,
|
||||
updatedConversationHistoryDeletedRange: updatedConversationHistoryDeletedRange,
|
||||
truncatedConversationHistory: truncatedConversationHistory,
|
||||
}
|
||||
}
|
||||
|
||||
public getNextTruncationRange(
|
||||
apiMessages: Anthropic.Messages.MessageParam[],
|
||||
currentDeletedRange: [number, number] | undefined,
|
||||
keep: "half" | "quarter",
|
||||
): [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
|
||||
|
||||
let messagesToRemove: number
|
||||
if (keep === "half") {
|
||||
// Remove half of remaining user-assistant pairs
|
||||
// We first calculate half of the messages then divide by 2 to get the number of pairs.
|
||||
// After flooring, we multiply by 2 to get the number of messages.
|
||||
// Note that this will also always be an even number.
|
||||
messagesToRemove = Math.floor((apiMessages.length - startOfRest) / 4) * 2 // Keep even number
|
||||
} else {
|
||||
// Remove 3/4 of remaining user-assistant pairs
|
||||
// We calculate 3/4ths of the messages then divide by 2 to get the number of pairs.
|
||||
// After flooring, we multiply by 2 to get the number of messages.
|
||||
// Note that this will also always be an even number.
|
||||
messagesToRemove = Math.floor(((apiMessages.length - startOfRest) * 3) / 4 / 2) * 2
|
||||
}
|
||||
|
||||
let rangeEndIndex = startOfRest + messagesToRemove - 1
|
||||
|
||||
// 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-assistant-user-assistant, while openai format messages can have multiple user messages in a row (we use anthropic format throughout cline)
|
||||
if (apiMessages[rangeEndIndex].role !== "user") {
|
||||
rangeEndIndex -= 1
|
||||
}
|
||||
|
||||
// this is an inclusive range that will be removed from the conversation history
|
||||
return [rangeStartIndex, rangeEndIndex]
|
||||
}
|
||||
|
||||
public 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)]
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import { ApiHandler } from "../../api"
|
||||
import { OpenAiHandler } from "../../api/providers/openai"
|
||||
import { ApiHandler } from "../../../api"
|
||||
import { OpenAiHandler } from "../../../api/providers/openai"
|
||||
|
||||
/**
|
||||
* Gets context window information for the given API handler
|
||||
+8
@@ -10,8 +10,16 @@ export interface FileMetadataEntry {
|
||||
user_edit_date?: number | null
|
||||
}
|
||||
|
||||
export interface ModelMetadataEntry {
|
||||
ts: number
|
||||
model_id: string
|
||||
model_provider_id: string
|
||||
mode: string
|
||||
}
|
||||
|
||||
export interface TaskMetadata {
|
||||
files_in_context: FileMetadataEntry[]
|
||||
model_usage: ModelMetadataEntry[]
|
||||
}
|
||||
|
||||
// Interface for the controller to avoid direct dependency
|
||||
+3
-3
@@ -4,8 +4,8 @@ import * as sinon from "sinon"
|
||||
import * as vscode from "vscode"
|
||||
import * as path from "path"
|
||||
import { FileContextTracker } from "./FileContextTracker"
|
||||
import * as diskModule from "../storage/disk"
|
||||
import type { TaskMetadata, ControllerLike, FileMetadataEntry } from "./FileContextTrackerTypes"
|
||||
import * as diskModule from "../../storage/disk"
|
||||
import type { TaskMetadata, ControllerLike, FileMetadataEntry } from "./ContextTrackerTypes"
|
||||
|
||||
describe("FileContextTracker", () => {
|
||||
let sandbox: sinon.SinonSandbox
|
||||
@@ -53,7 +53,7 @@ describe("FileContextTracker", () => {
|
||||
}
|
||||
|
||||
// Mock disk module functions
|
||||
mockTaskMetadata = { files_in_context: [] }
|
||||
mockTaskMetadata = { files_in_context: [], model_usage: [] }
|
||||
getTaskMetadataStub = sandbox.stub(diskModule, "getTaskMetadata").resolves(mockTaskMetadata)
|
||||
saveTaskMetadataStub = sandbox.stub(diskModule, "saveTaskMetadata").resolves()
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
import * as path from "path"
|
||||
import * as vscode from "vscode"
|
||||
import { getTaskMetadata, saveTaskMetadata } from "../storage/disk"
|
||||
import type { FileMetadataEntry, ControllerLike } from "./FileContextTrackerTypes"
|
||||
import { getTaskMetadata, saveTaskMetadata } from "../../storage/disk"
|
||||
import type { FileMetadataEntry, ControllerLike } from "./ContextTrackerTypes"
|
||||
|
||||
// This class is responsible for tracking file operations that may result in stale context.
|
||||
// If a user modifies a file outside of Cline, the context may become stale and need to be updated.
|
||||
@@ -0,0 +1,211 @@
|
||||
import { describe, it, beforeEach, afterEach } from "mocha"
|
||||
import { expect } from "chai"
|
||||
import * as sinon from "sinon"
|
||||
import * as vscode from "vscode"
|
||||
import { ModelContextTracker } from "./ModelContextTracker"
|
||||
import * as diskModule from "../../storage/disk"
|
||||
import type { TaskMetadata, ControllerLike } from "./ContextTrackerTypes"
|
||||
|
||||
describe("ModelContextTracker", () => {
|
||||
let sandbox: sinon.SinonSandbox
|
||||
let mockController: ControllerLike
|
||||
let mockContext: vscode.ExtensionContext
|
||||
let tracker: ModelContextTracker
|
||||
let taskId: string
|
||||
let mockTaskMetadata: TaskMetadata
|
||||
let getTaskMetadataStub: sinon.SinonStub
|
||||
let saveTaskMetadataStub: sinon.SinonStub
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox()
|
||||
|
||||
// Mock controller and context
|
||||
mockContext = {
|
||||
globalStorageUri: { fsPath: "/mock/storage" },
|
||||
} as unknown as vscode.ExtensionContext
|
||||
|
||||
mockController = {
|
||||
context: mockContext,
|
||||
}
|
||||
|
||||
// Mock disk module functions
|
||||
mockTaskMetadata = { files_in_context: [], model_usage: [] }
|
||||
getTaskMetadataStub = sandbox.stub(diskModule, "getTaskMetadata").resolves(mockTaskMetadata)
|
||||
saveTaskMetadataStub = sandbox.stub(diskModule, "saveTaskMetadata").resolves()
|
||||
|
||||
// Create tracker instance
|
||||
taskId = "test-task-id"
|
||||
tracker = new ModelContextTracker(mockController, taskId)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
sandbox.restore()
|
||||
})
|
||||
|
||||
it("should record model usage with correct data", async () => {
|
||||
// Test data
|
||||
const apiProviderId = "anthropic"
|
||||
const modelId = "claude-3-opus"
|
||||
const mode = "act"
|
||||
|
||||
// Use a fake timer to have a predictable timestamp
|
||||
const fakeNow = 1617293940000 // Some fixed timestamp
|
||||
const clock = sandbox.useFakeTimers(fakeNow)
|
||||
|
||||
try {
|
||||
// Call the method being tested
|
||||
await tracker.recordModelUsage(apiProviderId, modelId, mode)
|
||||
|
||||
// Verify getTaskMetadata was called with correct parameters
|
||||
expect(getTaskMetadataStub.calledOnce).to.be.true
|
||||
expect(getTaskMetadataStub.firstCall.args[1]).to.equal(taskId)
|
||||
|
||||
// Verify saveTaskMetadata was called with the correct data
|
||||
expect(saveTaskMetadataStub.calledOnce).to.be.true
|
||||
|
||||
// Extract the saved metadata from the call arguments
|
||||
const savedMetadata = saveTaskMetadataStub.firstCall.args[2]
|
||||
|
||||
// Verify model_usage array has one entry
|
||||
expect(savedMetadata.model_usage.length).to.equal(1)
|
||||
|
||||
// Verify the entry has the correct properties
|
||||
const modelUsageEntry = savedMetadata.model_usage[0]
|
||||
expect(modelUsageEntry.ts).to.equal(fakeNow)
|
||||
expect(modelUsageEntry.model_id).to.equal(modelId)
|
||||
expect(modelUsageEntry.model_provider_id).to.equal(apiProviderId)
|
||||
expect(modelUsageEntry.mode).to.equal(mode)
|
||||
} finally {
|
||||
// Restore the clock
|
||||
clock.restore()
|
||||
}
|
||||
})
|
||||
|
||||
it("should throw an error when controller is dereferenced", async () => {
|
||||
// Create a new tracker with a controller that will be garbage collected
|
||||
const weakMockController = { context: mockContext }
|
||||
const weakTracker = new ModelContextTracker(weakMockController, taskId)
|
||||
|
||||
// Force the WeakRef to return null by overriding the deref method
|
||||
const weakRef = { deref: sandbox.stub().returns(null) }
|
||||
sandbox.stub(WeakRef.prototype, "deref").callsFake(() => weakRef.deref())
|
||||
|
||||
try {
|
||||
// Try to call the method - this should throw
|
||||
await weakTracker.recordModelUsage("any-provider", "any-model", "any-mode")
|
||||
|
||||
// If we get here, the test should fail
|
||||
expect.fail("Expected an error to be thrown")
|
||||
} catch (error) {
|
||||
// Verify the error message
|
||||
expect(error.message).to.equal("Unable to access extension context")
|
||||
}
|
||||
})
|
||||
|
||||
it("should append model usage to existing entries", async () => {
|
||||
// Add an existing model usage entry
|
||||
const existingTimestamp = 1617200000000
|
||||
mockTaskMetadata.model_usage = [
|
||||
{
|
||||
ts: existingTimestamp,
|
||||
model_id: "existing-model",
|
||||
model_provider_id: "existing-provider",
|
||||
mode: "plan",
|
||||
},
|
||||
]
|
||||
|
||||
// Test data for new entry
|
||||
const apiProviderId = "anthropic"
|
||||
const modelId = "claude-3-sonnet"
|
||||
const mode = "act"
|
||||
|
||||
// Use a fake timer
|
||||
const newTimestamp = 1617300000000
|
||||
const clock = sandbox.useFakeTimers(newTimestamp)
|
||||
|
||||
try {
|
||||
// Call the method being tested
|
||||
await tracker.recordModelUsage(apiProviderId, modelId, mode)
|
||||
|
||||
// Verify saveTaskMetadata was called
|
||||
expect(saveTaskMetadataStub.calledOnce).to.be.true
|
||||
|
||||
// Extract the saved metadata
|
||||
const savedMetadata = saveTaskMetadataStub.firstCall.args[2]
|
||||
|
||||
// Verify model_usage array now has two entries
|
||||
expect(savedMetadata.model_usage.length).to.equal(2)
|
||||
|
||||
// Verify the existing entry is preserved
|
||||
expect(savedMetadata.model_usage[0]).to.deep.equal({
|
||||
ts: existingTimestamp,
|
||||
model_id: "existing-model",
|
||||
model_provider_id: "existing-provider",
|
||||
mode: "plan",
|
||||
})
|
||||
|
||||
// Verify the new entry has correct data
|
||||
expect(savedMetadata.model_usage[1]).to.deep.equal({
|
||||
ts: newTimestamp,
|
||||
model_id: modelId,
|
||||
model_provider_id: apiProviderId,
|
||||
mode: mode,
|
||||
})
|
||||
} finally {
|
||||
clock.restore()
|
||||
}
|
||||
})
|
||||
|
||||
it("should handle multiple model usages in sequence", async () => {
|
||||
// Test data for sequential calls
|
||||
const usages = [
|
||||
{ provider: "anthropic", model: "claude-3-opus", mode: "plan" },
|
||||
{ provider: "openai", model: "gpt-4", mode: "act" },
|
||||
{ provider: "anthropic", model: "claude-3-haiku", mode: "plan" },
|
||||
]
|
||||
|
||||
// Use a fake timer that advances with each call
|
||||
const startTime = 1617300000000
|
||||
const clock = sandbox.useFakeTimers(startTime)
|
||||
|
||||
try {
|
||||
// Record multiple model usages
|
||||
for (let i = 0; i < usages.length; i++) {
|
||||
const { provider, model, mode } = usages[i]
|
||||
|
||||
// Advance time by 1 second for each call
|
||||
clock.tick(1000)
|
||||
const expectedTime = startTime + (i + 1) * 1000
|
||||
|
||||
// Reset history between calls to check individual call behavior
|
||||
getTaskMetadataStub.resetHistory()
|
||||
saveTaskMetadataStub.resetHistory()
|
||||
|
||||
// Reset mock metadata for each iteration to avoid accumulation
|
||||
mockTaskMetadata.model_usage = []
|
||||
|
||||
// Call the method
|
||||
await tracker.recordModelUsage(provider, model, mode)
|
||||
|
||||
// Verify interaction with disk module
|
||||
expect(getTaskMetadataStub.calledOnce).to.be.true
|
||||
expect(saveTaskMetadataStub.calledOnce).to.be.true
|
||||
|
||||
// Get the saved metadata
|
||||
const savedMetadata = saveTaskMetadataStub.firstCall.args[2]
|
||||
|
||||
// Since we reset the array for each call, we should always have 1 entry
|
||||
expect(savedMetadata.model_usage.length).to.equal(1)
|
||||
|
||||
// Check the entry
|
||||
const entry = savedMetadata.model_usage[0]
|
||||
expect(entry.ts).to.equal(expectedTime)
|
||||
expect(entry.model_id).to.equal(model)
|
||||
expect(entry.model_provider_id).to.equal(provider)
|
||||
expect(entry.mode).to.equal(mode)
|
||||
}
|
||||
} finally {
|
||||
clock.restore()
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,52 @@
|
||||
import * as vscode from "vscode"
|
||||
import { getTaskMetadata, saveTaskMetadata } from "../../storage/disk"
|
||||
import type { ControllerLike } from "./ContextTrackerTypes"
|
||||
|
||||
export class ModelContextTracker {
|
||||
readonly taskId: string
|
||||
private controllerRef: WeakRef<ControllerLike>
|
||||
|
||||
constructor(controller: ControllerLike, taskId: string) {
|
||||
this.controllerRef = new WeakRef(controller)
|
||||
this.taskId = taskId
|
||||
}
|
||||
|
||||
// While a task is ref'd by a controller, it will always have access to the extension context
|
||||
// This error is thrown if the controller derefs the task after e.g., aborting the task
|
||||
private context(): vscode.ExtensionContext {
|
||||
const context = this.controllerRef.deref()?.context
|
||||
if (!context) {
|
||||
throw new Error("Unable to access extension context")
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
async recordModelUsage(apiProviderId: string, modelId: string, mode: string) {
|
||||
const context = this.context()
|
||||
const metadata = await getTaskMetadata(context, this.taskId)
|
||||
|
||||
if (!metadata.model_usage) {
|
||||
metadata.model_usage = []
|
||||
}
|
||||
|
||||
// check to see if the last entry is the same as the new one
|
||||
const lastEntry = metadata.model_usage[metadata.model_usage.length - 1]
|
||||
if (
|
||||
lastEntry &&
|
||||
lastEntry.model_id === modelId &&
|
||||
lastEntry.model_provider_id === apiProviderId &&
|
||||
lastEntry.mode === mode
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
metadata.model_usage.push({
|
||||
ts: Date.now(),
|
||||
model_id: modelId,
|
||||
model_provider_id: apiProviderId,
|
||||
mode: mode,
|
||||
})
|
||||
|
||||
await saveTaskMetadata(context, this.taskId, metadata)
|
||||
}
|
||||
}
|
||||
@@ -61,7 +61,7 @@ export class Controller {
|
||||
workspaceTracker?: WorkspaceTracker
|
||||
mcpHub?: McpHub
|
||||
accountService?: ClineAccountService
|
||||
private latestAnnouncementId = "april-7-2025" // update to some unique identifier when we add a new announcement
|
||||
private latestAnnouncementId = "april-10-2025" // update to some unique identifier when we add a new announcement
|
||||
private webviewProviderRef: WeakRef<WebviewProvider>
|
||||
|
||||
constructor(
|
||||
@@ -575,7 +575,7 @@ export class Controller {
|
||||
break
|
||||
}
|
||||
case "showMcpView": {
|
||||
await this.postMessageToWebview({ type: "action", action: "mcpButtonClicked" })
|
||||
await this.postMessageToWebview({ type: "action", action: "mcpButtonClicked", tab: message.tab || undefined })
|
||||
break
|
||||
}
|
||||
case "openMcpSettings": {
|
||||
@@ -1368,6 +1368,7 @@ export class Controller {
|
||||
const task = `Set up the MCP server from ${mcpDetails.githubUrl} while adhering to these MCP server installation rules:
|
||||
- Use "${mcpDetails.mcpId}" as the server name in cline_mcp_settings.json.
|
||||
- Create the directory for the new MCP server before starting installation.
|
||||
- Make sure you read the user's existing cline_mcp_settings.json file before editing it with this new mcp, to not overwrite any existing servers.
|
||||
- Use commands aligned with the user's shell and operating system best practices.
|
||||
- The following README may contain instructions that conflict with the user's OS, in which case proceed thoughtfully.
|
||||
- Once installed, demonstrate the server's capabilities by using one of its tools.
|
||||
@@ -1704,6 +1705,8 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
|
||||
taskDirPath: string
|
||||
apiConversationHistoryFilePath: string
|
||||
uiMessagesFilePath: string
|
||||
contextHistoryFilePath: string
|
||||
taskMetadataFilePath: string
|
||||
apiConversationHistory: Anthropic.MessageParam[]
|
||||
}> {
|
||||
const history = ((await getGlobalState(this.context, "taskHistory")) as HistoryItem[] | undefined) || []
|
||||
@@ -1712,6 +1715,8 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
|
||||
const taskDirPath = path.join(this.context.globalStorageUri.fsPath, "tasks", id)
|
||||
const apiConversationHistoryFilePath = path.join(taskDirPath, GlobalFileNames.apiConversationHistory)
|
||||
const uiMessagesFilePath = path.join(taskDirPath, GlobalFileNames.uiMessages)
|
||||
const contextHistoryFilePath = path.join(taskDirPath, GlobalFileNames.contextHistory)
|
||||
const taskMetadataFilePath = path.join(taskDirPath, GlobalFileNames.taskMetadata)
|
||||
const fileExists = await fileExistsAtPath(apiConversationHistoryFilePath)
|
||||
if (fileExists) {
|
||||
const apiConversationHistory = JSON.parse(await fs.readFile(apiConversationHistoryFilePath, "utf8"))
|
||||
@@ -1720,6 +1725,8 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
|
||||
taskDirPath,
|
||||
apiConversationHistoryFilePath,
|
||||
uiMessagesFilePath,
|
||||
contextHistoryFilePath,
|
||||
taskMetadataFilePath,
|
||||
apiConversationHistory,
|
||||
}
|
||||
}
|
||||
@@ -1791,22 +1798,28 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
|
||||
console.debug("cleared task")
|
||||
}
|
||||
|
||||
const { taskDirPath, apiConversationHistoryFilePath, uiMessagesFilePath } = await this.getTaskWithId(id)
|
||||
|
||||
const {
|
||||
taskDirPath,
|
||||
apiConversationHistoryFilePath,
|
||||
uiMessagesFilePath,
|
||||
contextHistoryFilePath,
|
||||
taskMetadataFilePath,
|
||||
} = await this.getTaskWithId(id)
|
||||
const legacyMessagesFilePath = path.join(taskDirPath, "claude_messages.json")
|
||||
const updatedTaskHistory = await this.deleteTaskFromState(id)
|
||||
|
||||
// Delete the task files
|
||||
const apiConversationHistoryFileExists = await fileExistsAtPath(apiConversationHistoryFilePath)
|
||||
if (apiConversationHistoryFileExists) {
|
||||
await fs.unlink(apiConversationHistoryFilePath)
|
||||
}
|
||||
const uiMessagesFileExists = await fileExistsAtPath(uiMessagesFilePath)
|
||||
if (uiMessagesFileExists) {
|
||||
await fs.unlink(uiMessagesFilePath)
|
||||
}
|
||||
const legacyMessagesFilePath = path.join(taskDirPath, "claude_messages.json")
|
||||
if (await fileExistsAtPath(legacyMessagesFilePath)) {
|
||||
await fs.unlink(legacyMessagesFilePath)
|
||||
for (const filePath of [
|
||||
apiConversationHistoryFilePath,
|
||||
uiMessagesFilePath,
|
||||
contextHistoryFilePath,
|
||||
taskMetadataFilePath,
|
||||
legacyMessagesFilePath,
|
||||
]) {
|
||||
const fileExists = await fileExistsAtPath(filePath)
|
||||
if (fileExists) {
|
||||
await fs.unlink(filePath)
|
||||
}
|
||||
}
|
||||
|
||||
await fs.rmdir(taskDirPath) // succeeds if the dir is empty
|
||||
|
||||
@@ -10,7 +10,7 @@ import { diagnosticsToProblemsString } from "../../integrations/diagnostics"
|
||||
import { getLatestTerminalOutput } from "../../integrations/terminal/get-latest-output"
|
||||
import { getCommitInfo } from "../../utils/git"
|
||||
import { getWorkingState } from "../../utils/git"
|
||||
import { FileContextTracker } from "../context-tracking/FileContextTracker"
|
||||
import { FileContextTracker } from "../context/context-tracking/FileContextTracker"
|
||||
|
||||
export function openMention(mention?: string): void {
|
||||
if (!mention) {
|
||||
|
||||
@@ -130,8 +130,8 @@ Otherwise, if you have not completed the task and do not need additional informa
|
||||
cwd: string,
|
||||
wasRecent: boolean | 0 | undefined,
|
||||
responseText?: string,
|
||||
) => {
|
||||
return `[TASK RESUMPTION] ${
|
||||
): [string, string] => {
|
||||
const taskResumptionMessage = `[TASK RESUMPTION] ${
|
||||
mode === "plan"
|
||||
? `This task was interrupted ${agoText}. The conversation may have been incomplete. Be aware that the project state may have changed since then. The current working directory is now '${cwd.toPosix()}'.\n\nNote: If you previously attempted a tool use that the user did not provide a result for, you should assume the tool use was not successful. However you are in PLAN MODE, so rather than continuing the task, you must respond to the user's message.`
|
||||
: `This task was interrupted ${agoText}. It may or may not be complete, so please reassess the task context. Be aware that the project state may have changed since then. The current working directory is now '${cwd.toPosix()}'. If the task has not been completed, retry the last step before interruption and proceed with completing the task.\n\nNote: If you previously attempted a tool use that the user did not provide a result for, you should assume the tool use was not successful and assess whether you should retry. If the last tool was a browser_action, the browser has been closed and you must launch a new browser if needed.`
|
||||
@@ -139,13 +139,17 @@ Otherwise, if you have not completed the task and do not need additional informa
|
||||
wasRecent
|
||||
? "\n\nIMPORTANT: If the last tool use was a replace_in_file or write_to_file that was interrupted, the file was reverted back to its original state before the interrupted edit, and you do NOT need to re-read the file as you already have its up-to-date contents."
|
||||
: ""
|
||||
}${
|
||||
}`
|
||||
|
||||
const userResponseMessage = `${
|
||||
responseText
|
||||
? `\n\n${mode === "plan" ? "New message to respond to with plan_mode_respond tool (be sure to provide your response in the <response> parameter)" : "New instructions for task continuation"}:\n<user_message>\n${responseText}\n</user_message>`
|
||||
? `${mode === "plan" ? "New message to respond to with plan_mode_respond tool (be sure to provide your response in the <response> parameter)" : "New instructions for task continuation"}:\n<user_message>\n${responseText}\n</user_message>`
|
||||
: mode === "plan"
|
||||
? "(The user did not provide a new message. Consider asking them how they'd like you to proceed, or to switch to Act mode to continue with the task.)"
|
||||
: ""
|
||||
}`
|
||||
|
||||
return [taskResumptionMessage, userResponseMessage]
|
||||
},
|
||||
|
||||
planModeInstructions: () => {
|
||||
|
||||
@@ -4,20 +4,7 @@ import fs from "fs/promises"
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { fileExistsAtPath } from "../../utils/fs"
|
||||
import { ClineMessage } from "../../shared/ExtensionMessage"
|
||||
|
||||
export interface FileMetadataEntry {
|
||||
path: string
|
||||
record_state: "active" | "stale"
|
||||
record_source: "read_tool" | "user_edited" | "cline_edited" | "file_mentioned"
|
||||
cline_read_date: number | null
|
||||
cline_edit_date: number | null
|
||||
user_edit_date?: number | null
|
||||
}
|
||||
|
||||
export interface TaskMetadata {
|
||||
files_in_context: FileMetadataEntry[]
|
||||
}
|
||||
|
||||
import { TaskMetadata } from "../context/context-tracking/ContextTrackerTypes"
|
||||
export const GlobalFileNames = {
|
||||
apiConversationHistory: "api_conversation_history.json",
|
||||
contextHistory: "context_history.json",
|
||||
@@ -96,7 +83,7 @@ export async function getTaskMetadata(context: vscode.ExtensionContext, taskId:
|
||||
} catch (error) {
|
||||
console.error("Failed to read task metadata:", error)
|
||||
}
|
||||
return { files_in_context: [] }
|
||||
return { files_in_context: [], model_usage: [] }
|
||||
}
|
||||
|
||||
export async function saveTaskMetadata(context: vscode.ExtensionContext, taskId: string, metadata: TaskMetadata) {
|
||||
|
||||
+111
-63
@@ -13,7 +13,7 @@ import { ApiHandler, buildApiHandler } from "../../api"
|
||||
import { AnthropicHandler } from "../../api/providers/anthropic"
|
||||
import { ClineHandler } from "../../api/providers/cline"
|
||||
import { OpenRouterHandler } from "../../api/providers/openrouter"
|
||||
import { getContextWindowInfo } from "../context-management/context-window-utils"
|
||||
import { getContextWindowInfo } from "../context/context-management/context-window-utils"
|
||||
import { ApiStream } from "../../api/transform/stream"
|
||||
import CheckpointTracker from "../../integrations/checkpoints/CheckpointTracker"
|
||||
import { DIFF_VIEW_URI_SCHEME, DiffViewProvider } from "../../integrations/editor/DiffViewProvider"
|
||||
@@ -60,16 +60,18 @@ import { arePathsEqual, getReadablePath } from "../../utils/path"
|
||||
import { fixModelHtmlEscaping, removeInvalidChars } from "../../utils/string"
|
||||
import { AssistantMessageContent, parseAssistantMessage, ToolParamName, ToolUseName } from ".././assistant-message"
|
||||
import { constructNewFileContent } from ".././assistant-message/diff"
|
||||
import { ContextManager } from ".././context-management/ContextManager"
|
||||
import { ContextManager } from "../context/context-management/ContextManager"
|
||||
import { ClineIgnoreController } from ".././ignore/ClineIgnoreController"
|
||||
import { parseMentions } from ".././mentions"
|
||||
import { formatResponse } from ".././prompts/responses"
|
||||
import { addUserInstructions, SYSTEM_PROMPT } from ".././prompts/system"
|
||||
import { FileContextTracker } from "../context-tracking/FileContextTracker"
|
||||
import { FileContextTracker } from "../context/context-tracking/FileContextTracker"
|
||||
import { ModelContextTracker } from "../context/context-tracking/ModelContextTracker"
|
||||
|
||||
import {
|
||||
checkIsAnthropicContextWindowError,
|
||||
checkIsOpenRouterContextWindowError,
|
||||
} from "../context-management/context-error-handling"
|
||||
} from "../context/context-management/context-error-handling"
|
||||
import { Controller } from "../controller"
|
||||
import {
|
||||
ensureTaskDirectoryExists,
|
||||
@@ -78,6 +80,7 @@ import {
|
||||
saveApiConversationHistory,
|
||||
saveClineMessages,
|
||||
GlobalFileNames,
|
||||
getTaskMetadata,
|
||||
} from "../storage/disk"
|
||||
|
||||
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) ?? path.join(os.homedir(), "Desktop") // may or may not exist but fs checking existence would immediately ask for permission which would be bad UX, need to come up with a better solution
|
||||
@@ -119,8 +122,9 @@ export class Task {
|
||||
isAwaitingPlanResponse = false
|
||||
didRespondToPlanAskBySwitchingMode = false
|
||||
|
||||
// File tracking
|
||||
// Metadata tracking
|
||||
private fileContextTracker: FileContextTracker
|
||||
private modelContextTracker: ModelContextTracker
|
||||
|
||||
// streaming
|
||||
isWaitingForFirstChunk = false
|
||||
@@ -175,7 +179,7 @@ export class Task {
|
||||
|
||||
// Initialize file context tracker
|
||||
this.fileContextTracker = new FileContextTracker(controller, this.taskId)
|
||||
|
||||
this.modelContextTracker = new ModelContextTracker(controller, this.taskId)
|
||||
// Now that taskId is initialized, we can build the API handler
|
||||
this.api = buildApiHandler({
|
||||
...apiConfiguration,
|
||||
@@ -328,10 +332,10 @@ export class Task {
|
||||
await this.overwriteApiConversationHistory(newConversationHistory)
|
||||
|
||||
// update the context history state
|
||||
await this.contextManager.truncateContextHistory(
|
||||
message.ts,
|
||||
await ensureTaskDirectoryExists(this.getContext(), this.taskId),
|
||||
)
|
||||
// await this.contextManager.truncateContextHistory(
|
||||
// message.ts,
|
||||
// await ensureTaskDirectoryExists(this.getContext(), this.taskId),
|
||||
// )
|
||||
|
||||
// aggregate deleted api reqs info so we don't lose costs/tokens
|
||||
const deletedMessages = this.clineMessages.slice(messageIndex + 1)
|
||||
@@ -865,7 +869,7 @@ export class Task {
|
||||
this.apiConversationHistory = await getSavedApiConversationHistory(this.getContext(), this.taskId)
|
||||
|
||||
// load the context history state
|
||||
await this.contextManager.initializeContextHistory(await ensureTaskDirectoryExists(this.getContext(), this.taskId))
|
||||
// await this.contextManager.initializeContextHistory(await ensureTaskDirectoryExists(this.getContext(), this.taskId))
|
||||
|
||||
const lastClineMessage = this.clineMessages
|
||||
.slice()
|
||||
@@ -886,6 +890,7 @@ export class Task {
|
||||
let responseImages: string[] | undefined
|
||||
if (response === "messageResponse") {
|
||||
await this.say("user_feedback", text, images)
|
||||
await this.saveCheckpoint()
|
||||
responseText = text
|
||||
responseImages = images
|
||||
}
|
||||
@@ -942,16 +947,27 @@ export class Task {
|
||||
|
||||
const wasRecent = lastClineMessage?.ts && Date.now() - lastClineMessage.ts < 30_000
|
||||
|
||||
newUserContent.push({
|
||||
type: "text",
|
||||
text: formatResponse.taskResumption(
|
||||
this.chatSettings?.mode === "plan" ? "plan" : "act",
|
||||
agoText,
|
||||
cwd,
|
||||
wasRecent,
|
||||
responseText,
|
||||
),
|
||||
})
|
||||
const [taskResumptionMessage, userResponseMessage] = formatResponse.taskResumption(
|
||||
this.chatSettings?.mode === "plan" ? "plan" : "act",
|
||||
agoText,
|
||||
cwd,
|
||||
wasRecent,
|
||||
responseText,
|
||||
)
|
||||
|
||||
if (taskResumptionMessage !== "") {
|
||||
newUserContent.push({
|
||||
type: "text",
|
||||
text: taskResumptionMessage,
|
||||
})
|
||||
}
|
||||
|
||||
if (userResponseMessage !== "") {
|
||||
newUserContent.push({
|
||||
type: "text",
|
||||
text: userResponseMessage,
|
||||
})
|
||||
}
|
||||
|
||||
if (responseImages && responseImages.length > 0) {
|
||||
newUserContent.push(...formatResponse.imageBlocks(responseImages))
|
||||
@@ -1131,6 +1147,7 @@ export class Task {
|
||||
|
||||
if (userFeedback) {
|
||||
await this.say("user_feedback", userFeedback.text, userFeedback.images)
|
||||
await this.saveCheckpoint()
|
||||
return [
|
||||
true,
|
||||
formatResponse.toolResult(
|
||||
@@ -1269,13 +1286,15 @@ export class Task {
|
||||
preferredLanguageInstructions,
|
||||
)
|
||||
}
|
||||
const contextManagementMetadata = await this.contextManager.getNewContextMessagesAndMetadata(
|
||||
|
||||
// await
|
||||
const contextManagementMetadata = this.contextManager.getNewContextMessagesAndMetadata(
|
||||
this.apiConversationHistory,
|
||||
this.clineMessages,
|
||||
this.api,
|
||||
this.conversationHistoryDeletedRange,
|
||||
previousApiReqIndex,
|
||||
await ensureTaskDirectoryExists(this.getContext(), this.taskId),
|
||||
// await ensureTaskDirectoryExists(this.getContext(), this.taskId),
|
||||
)
|
||||
|
||||
if (contextManagementMetadata.updatedConversationHistoryDeletedRange) {
|
||||
@@ -1548,6 +1567,7 @@ export class Task {
|
||||
if (text || images?.length) {
|
||||
pushAdditionalToolFeedback(text, images)
|
||||
await this.say("user_feedback", text, images)
|
||||
await this.saveCheckpoint()
|
||||
}
|
||||
this.didRejectTool = true // Prevent further tool uses in this message
|
||||
return false
|
||||
@@ -1556,6 +1576,7 @@ export class Task {
|
||||
if (text || images?.length) {
|
||||
pushAdditionalToolFeedback(text, images)
|
||||
await this.say("user_feedback", text, images)
|
||||
await this.saveCheckpoint()
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -1629,7 +1650,7 @@ export class Task {
|
||||
if (!accessAllowed) {
|
||||
await this.say("clineignore_error", relPath)
|
||||
pushToolResult(formatResponse.toolError(formatResponse.clineIgnoreError(relPath)))
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
|
||||
@@ -1685,6 +1706,7 @@ export class Task {
|
||||
)
|
||||
await this.diffViewProvider.revertChanges()
|
||||
await this.diffViewProvider.reset()
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
} else if (content) {
|
||||
@@ -1747,14 +1769,14 @@ export class Task {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(await this.sayAndCreateMissingParamError("replace_in_file", "diff"))
|
||||
await this.diffViewProvider.reset()
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
if (block.name === "write_to_file" && !content) {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(await this.sayAndCreateMissingParamError("write_to_file", "content"))
|
||||
await this.diffViewProvider.reset()
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
|
||||
@@ -1813,6 +1835,7 @@ export class Task {
|
||||
if (text || images?.length) {
|
||||
pushAdditionalToolFeedback(text, images)
|
||||
await this.say("user_feedback", text, images)
|
||||
await this.saveCheckpoint()
|
||||
}
|
||||
this.didRejectTool = true
|
||||
didApprove = false
|
||||
@@ -1822,12 +1845,14 @@ export class Task {
|
||||
if (text || images?.length) {
|
||||
pushAdditionalToolFeedback(text, images)
|
||||
await this.say("user_feedback", text, images)
|
||||
await this.saveCheckpoint()
|
||||
}
|
||||
telemetryService.captureToolUsage(this.taskId, block.name, false, true)
|
||||
}
|
||||
|
||||
if (!didApprove) {
|
||||
await this.diffViewProvider.revertChanges()
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -1888,7 +1913,7 @@ export class Task {
|
||||
await handleError("writing file", error)
|
||||
await this.diffViewProvider.revertChanges()
|
||||
await this.diffViewProvider.reset()
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -1916,7 +1941,7 @@ export class Task {
|
||||
if (!relPath) {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(await this.sayAndCreateMissingParamError("read_file", "path"))
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
|
||||
@@ -1924,7 +1949,7 @@ export class Task {
|
||||
if (!accessAllowed) {
|
||||
await this.say("clineignore_error", relPath)
|
||||
pushToolResult(formatResponse.toolError(formatResponse.clineIgnoreError(relPath)))
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
|
||||
@@ -1946,6 +1971,7 @@ export class Task {
|
||||
this.removeLastPartialMessageIfExistsWithType("say", "tool")
|
||||
const didApprove = await askApproval("tool", completeMessage)
|
||||
if (!didApprove) {
|
||||
await this.saveCheckpoint()
|
||||
telemetryService.captureToolUsage(this.taskId, block.name, false, false)
|
||||
break
|
||||
}
|
||||
@@ -1958,12 +1984,12 @@ export class Task {
|
||||
await this.fileContextTracker.trackFileContext(relPath, "read_tool")
|
||||
|
||||
pushToolResult(content)
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
} catch (error) {
|
||||
await handleError("reading file", error)
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -1993,7 +2019,7 @@ export class Task {
|
||||
if (!relDirPath) {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(await this.sayAndCreateMissingParamError("list_files", "path"))
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
this.consecutiveMistakeCount = 0
|
||||
@@ -2024,18 +2050,19 @@ export class Task {
|
||||
this.removeLastPartialMessageIfExistsWithType("say", "tool")
|
||||
const didApprove = await askApproval("tool", completeMessage)
|
||||
if (!didApprove) {
|
||||
await this.saveCheckpoint()
|
||||
telemetryService.captureToolUsage(this.taskId, block.name, false, false)
|
||||
break
|
||||
}
|
||||
telemetryService.captureToolUsage(this.taskId, block.name, false, true)
|
||||
}
|
||||
pushToolResult(result)
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
} catch (error) {
|
||||
await handleError("listing files", error)
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -2063,7 +2090,7 @@ export class Task {
|
||||
if (!relDirPath) {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(await this.sayAndCreateMissingParamError("list_code_definition_names", "path"))
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
|
||||
@@ -2091,18 +2118,19 @@ export class Task {
|
||||
this.removeLastPartialMessageIfExistsWithType("say", "tool")
|
||||
const didApprove = await askApproval("tool", completeMessage)
|
||||
if (!didApprove) {
|
||||
await this.saveCheckpoint()
|
||||
telemetryService.captureToolUsage(this.taskId, block.name, false, false)
|
||||
break
|
||||
}
|
||||
telemetryService.captureToolUsage(this.taskId, block.name, false, true)
|
||||
}
|
||||
pushToolResult(result)
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
} catch (error) {
|
||||
await handleError("parsing source code definitions", error)
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -2134,13 +2162,13 @@ export class Task {
|
||||
if (!relDirPath) {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(await this.sayAndCreateMissingParamError("search_files", "path"))
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
if (!regex) {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(await this.sayAndCreateMissingParamError("search_files", "regex"))
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
this.consecutiveMistakeCount = 0
|
||||
@@ -2170,18 +2198,19 @@ export class Task {
|
||||
this.removeLastPartialMessageIfExistsWithType("say", "tool")
|
||||
const didApprove = await askApproval("tool", completeMessage)
|
||||
if (!didApprove) {
|
||||
await this.saveCheckpoint()
|
||||
telemetryService.captureToolUsage(this.taskId, block.name, false, false)
|
||||
break
|
||||
}
|
||||
telemetryService.captureToolUsage(this.taskId, block.name, false, true)
|
||||
}
|
||||
pushToolResult(results)
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
} catch (error) {
|
||||
await handleError("searching files", error)
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -2197,6 +2226,7 @@ export class Task {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(await this.sayAndCreateMissingParamError("browser_action", "action"))
|
||||
await this.browserSession.closeBrowser()
|
||||
await this.saveCheckpoint()
|
||||
}
|
||||
break
|
||||
}
|
||||
@@ -2240,7 +2270,7 @@ export class Task {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(await this.sayAndCreateMissingParamError("browser_action", "url"))
|
||||
await this.browserSession.closeBrowser()
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
this.consecutiveMistakeCount = 0
|
||||
@@ -2256,6 +2286,7 @@ export class Task {
|
||||
this.removeLastPartialMessageIfExistsWithType("say", "browser_action_launch")
|
||||
const didApprove = await askApproval("browser_action_launch", url)
|
||||
if (!didApprove) {
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -2282,7 +2313,7 @@ export class Task {
|
||||
await this.sayAndCreateMissingParamError("browser_action", "coordinate"),
|
||||
)
|
||||
await this.browserSession.closeBrowser()
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break // can't be within an inner switch
|
||||
}
|
||||
}
|
||||
@@ -2291,7 +2322,7 @@ export class Task {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(await this.sayAndCreateMissingParamError("browser_action", "text"))
|
||||
await this.browserSession.closeBrowser()
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -2340,7 +2371,7 @@ export class Task {
|
||||
browserActionResult.screenshot ? [browserActionResult.screenshot] : [],
|
||||
),
|
||||
)
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
case "close":
|
||||
pushToolResult(
|
||||
@@ -2348,7 +2379,7 @@ export class Task {
|
||||
`The browser has been closed. You may now proceed to using other tools.`,
|
||||
),
|
||||
)
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
|
||||
@@ -2357,7 +2388,7 @@ export class Task {
|
||||
} catch (error) {
|
||||
await this.browserSession.closeBrowser() // if any error occurs, the browser session is terminated
|
||||
await handleError("executing browser action", error)
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -2385,7 +2416,7 @@ export class Task {
|
||||
if (!command) {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(await this.sayAndCreateMissingParamError("execute_command", "command"))
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
if (!requiresApprovalRaw) {
|
||||
@@ -2393,7 +2424,7 @@ export class Task {
|
||||
pushToolResult(
|
||||
await this.sayAndCreateMissingParamError("execute_command", "requires_approval"),
|
||||
)
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
this.consecutiveMistakeCount = 0
|
||||
@@ -2441,6 +2472,7 @@ export class Task {
|
||||
`${this.shouldAutoApproveTool(block.name) && requiresApprovalPerLLM ? COMMAND_REQ_APP_STRING : ""}`, // ugly hack until we refactor combineCommandSequences
|
||||
)
|
||||
if (!didApprove) {
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -2476,7 +2508,7 @@ export class Task {
|
||||
}
|
||||
} catch (error) {
|
||||
await handleError("executing command", error)
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -2506,13 +2538,13 @@ export class Task {
|
||||
if (!server_name) {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(await this.sayAndCreateMissingParamError("use_mcp_tool", "server_name"))
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
if (!tool_name) {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(await this.sayAndCreateMissingParamError("use_mcp_tool", "tool_name"))
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
// arguments are optional, but if they are provided they must be valid JSON
|
||||
@@ -2536,7 +2568,7 @@ export class Task {
|
||||
formatResponse.invalidMcpToolArgumentError(server_name, tool_name),
|
||||
),
|
||||
)
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -2564,6 +2596,7 @@ export class Task {
|
||||
this.removeLastPartialMessageIfExistsWithType("say", "use_mcp_server")
|
||||
const didApprove = await askApproval("use_mcp_server", completeMessage)
|
||||
if (!didApprove) {
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -2599,7 +2632,7 @@ export class Task {
|
||||
}
|
||||
} catch (error) {
|
||||
await handleError("executing MCP tool", error)
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -2627,13 +2660,13 @@ export class Task {
|
||||
if (!server_name) {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(await this.sayAndCreateMissingParamError("access_mcp_resource", "server_name"))
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
if (!uri) {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(await this.sayAndCreateMissingParamError("access_mcp_resource", "uri"))
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
this.consecutiveMistakeCount = 0
|
||||
@@ -2654,6 +2687,7 @@ export class Task {
|
||||
this.removeLastPartialMessageIfExistsWithType("say", "use_mcp_server")
|
||||
const didApprove = await askApproval("use_mcp_server", completeMessage)
|
||||
if (!didApprove) {
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -2673,12 +2707,12 @@ export class Task {
|
||||
.join("\n\n") || "(Empty response)"
|
||||
await this.say("mcp_server_response", resourceResultPretty)
|
||||
pushToolResult(formatResponse.toolResult(resourceResultPretty))
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
} catch (error) {
|
||||
await handleError("accessing MCP resource", error)
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -2697,7 +2731,7 @@ export class Task {
|
||||
if (!question) {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(await this.sayAndCreateMissingParamError("ask_followup_question", "question"))
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
this.consecutiveMistakeCount = 0
|
||||
@@ -2734,12 +2768,12 @@ export class Task {
|
||||
}
|
||||
|
||||
pushToolResult(formatResponse.toolResult(`<answer>\n${text}\n</answer>`, images))
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
} catch (error) {
|
||||
await handleError("asking question", error)
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -2753,6 +2787,7 @@ export class Task {
|
||||
if (!context) {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(await this.sayAndCreateMissingParamError("new_task", "context"))
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
this.consecutiveMistakeCount = 0
|
||||
@@ -2781,10 +2816,12 @@ export class Task {
|
||||
formatResponse.toolResult(`The user has created a new task with the provided context.`),
|
||||
)
|
||||
}
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
} catch (error) {
|
||||
await handleError("creating new task", error)
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -2845,6 +2882,7 @@ export class Task {
|
||||
if (text || images?.length) {
|
||||
telemetryService.captureOptionsIgnored(this.taskId, options.length, "plan")
|
||||
await this.say("user_feedback", text ?? "", images)
|
||||
await this.saveCheckpoint()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2976,12 +3014,14 @@ export class Task {
|
||||
// complete command message
|
||||
const didApprove = await askApproval("command", command)
|
||||
if (!didApprove) {
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
const [userRejected, execCommandResult] = await this.executeCommandTool(command!)
|
||||
if (userRejected) {
|
||||
this.didRejectTool = true
|
||||
pushToolResult(execCommandResult)
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
// user didn't reject, but the command may have output
|
||||
@@ -3000,6 +3040,7 @@ export class Task {
|
||||
break
|
||||
}
|
||||
await this.say("user_feedback", text ?? "", images)
|
||||
await this.saveCheckpoint()
|
||||
|
||||
const toolResults: (Anthropic.TextBlockParam | Anthropic.ImageBlockParam)[] = []
|
||||
if (commandResult) {
|
||||
@@ -3028,7 +3069,7 @@ export class Task {
|
||||
}
|
||||
} catch (error) {
|
||||
await handleError("attempting completion", error)
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -3072,6 +3113,13 @@ export class Task {
|
||||
throw new Error("Cline instance aborted")
|
||||
}
|
||||
|
||||
// Used to know what models were used in the task if user wants to export metadata for error reporting purposes
|
||||
if (this.apiProvider && this.api.getModel().id) {
|
||||
try {
|
||||
await this.modelContextTracker.recordModelUsage(this.apiProvider, this.api.getModel().id, this.chatSettings.mode)
|
||||
} catch {}
|
||||
}
|
||||
|
||||
if (this.consecutiveMistakeCount >= 3) {
|
||||
if (this.autoApprovalSettings.enabled && this.autoApprovalSettings.enableNotifications) {
|
||||
showSystemNotification({
|
||||
@@ -3678,7 +3726,7 @@ export class Task {
|
||||
const lastApiReqTotalTokens = lastApiReqMessage ? getTotalTokensFromApiReqMessage(lastApiReqMessage) : 0
|
||||
const usagePercentage = Math.round((lastApiReqTotalTokens / contextWindow) * 100)
|
||||
|
||||
details += "\n# Context Window Usage"
|
||||
details += "\n\n# Context Window Usage"
|
||||
details += `\n${lastApiReqTotalTokens.toLocaleString()} / ${(contextWindow / 1000).toLocaleString()}K tokens used (${usagePercentage}%)`
|
||||
|
||||
details += "\n\n# Current Mode"
|
||||
|
||||
+20
-7
@@ -9,6 +9,8 @@ import { DIFF_VIEW_URI_SCHEME } from "./integrations/editor/DiffViewProvider"
|
||||
import assert from "node:assert"
|
||||
import { telemetryService } from "./services/telemetry/TelemetryService"
|
||||
import { WebviewProvider } from "./core/webview"
|
||||
import { createTestServer, shutdownTestServer } from "./services/test/TestServer"
|
||||
import { ErrorService } from "./services/error/ErrorService"
|
||||
|
||||
/*
|
||||
Built using https://github.com/microsoft/vscode-webview-ui-toolkit
|
||||
@@ -27,12 +29,14 @@ export function activate(context: vscode.ExtensionContext) {
|
||||
outputChannel = vscode.window.createOutputChannel("Cline")
|
||||
context.subscriptions.push(outputChannel)
|
||||
|
||||
ErrorService.initialize()
|
||||
Logger.initialize(outputChannel)
|
||||
Logger.log("Cline extension activated")
|
||||
|
||||
const sidebarWebview = new WebviewProvider(context, outputChannel)
|
||||
|
||||
vscode.commands.executeCommand("setContext", "cline.isDevMode", IS_DEV && IS_DEV === "true")
|
||||
vscode.commands.executeCommand("setContext", "cline.isTestMode", IS_TEST && IS_TEST === "true")
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.window.registerWebviewViewProvider(WebviewProvider.sideBarId, sidebarWebview, {
|
||||
@@ -396,20 +400,24 @@ export function activate(context: vscode.ExtensionContext) {
|
||||
return createClineAPI(outputChannel, sidebarWebview.controller)
|
||||
}
|
||||
|
||||
// This method is called when your extension is deactivated
|
||||
export function deactivate() {
|
||||
telemetryService.shutdown()
|
||||
Logger.log("Cline extension deactivated")
|
||||
}
|
||||
|
||||
// TODO: Find a solution for automatically removing DEV related content from production builds.
|
||||
// This type of code is fine in production to keep. We just will want to remove it from production builds
|
||||
// to bring down built asset sizes.
|
||||
//
|
||||
// This is a workaround to reload the extension when the source code changes
|
||||
// since vscode doesn't support hot reload for extensions
|
||||
const { IS_DEV, DEV_WORKSPACE_FOLDER } = process.env
|
||||
const { IS_DEV, DEV_WORKSPACE_FOLDER, IS_TEST } = process.env
|
||||
|
||||
// This method is called when your extension is deactivated
|
||||
export function deactivate() {
|
||||
// Shutdown the test server if it exists
|
||||
shutdownTestServer()
|
||||
|
||||
telemetryService.shutdown()
|
||||
Logger.log("Cline extension deactivated")
|
||||
}
|
||||
|
||||
// Set up development mode file watcher
|
||||
if (IS_DEV && IS_DEV !== "false") {
|
||||
assert(DEV_WORKSPACE_FOLDER, "DEV_WORKSPACE_FOLDER must be set in development")
|
||||
const watcher = vscode.workspace.createFileSystemWatcher(new vscode.RelativePattern(DEV_WORKSPACE_FOLDER, "src/**/*"))
|
||||
@@ -420,3 +428,8 @@ if (IS_DEV && IS_DEV !== "false") {
|
||||
vscode.commands.executeCommand("workbench.action.reloadWindow")
|
||||
})
|
||||
}
|
||||
|
||||
// Set up test server if in test mode
|
||||
if (IS_TEST && IS_TEST === "true") {
|
||||
createTestServer()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
import * as Sentry from "@sentry/browser"
|
||||
import * as vscode from "vscode"
|
||||
import { telemetryService } from "../telemetry/TelemetryService"
|
||||
import * as pkg from "../../../package.json"
|
||||
|
||||
let telemetryLevel = vscode.workspace.getConfiguration("telemetry").get<string>("telemetryLevel", "all")
|
||||
let isTelemetryEnabled = ["all", "error"].includes(telemetryLevel)
|
||||
|
||||
vscode.workspace.onDidChangeConfiguration(() => {
|
||||
telemetryLevel = vscode.workspace.getConfiguration("telemetry").get<string>("telemetryLevel", "all")
|
||||
isTelemetryEnabled = ["all", "error"].includes(telemetryLevel)
|
||||
ErrorService.toggleEnabled(isTelemetryEnabled)
|
||||
if (isTelemetryEnabled) {
|
||||
ErrorService.setLevel(telemetryLevel as "error" | "all")
|
||||
}
|
||||
})
|
||||
|
||||
export class ErrorService {
|
||||
private static serviceEnabled: boolean
|
||||
private static serviceLevel: string
|
||||
|
||||
static initialize() {
|
||||
// Initialize sentry
|
||||
Sentry.init({
|
||||
dsn: "https://7936780e3f0f0290fcf8d4a395c249b7@o4509028819664896.ingest.us.sentry.io/4509052955983872",
|
||||
environment: process.env.NODE_ENV,
|
||||
release: `cline@${pkg.version}`,
|
||||
integrations: [Sentry.browserTracingIntegration(), Sentry.replayIntegration()],
|
||||
beforeSend(event) {
|
||||
// TelemetryService keeps track of whether the user has opted in to telemetry/error reporting
|
||||
const isUserManuallyOptedIn = telemetryService.isTelemetryEnabled()
|
||||
if (isUserManuallyOptedIn && ErrorService.isEnabled()) {
|
||||
return event
|
||||
}
|
||||
return null
|
||||
},
|
||||
})
|
||||
|
||||
ErrorService.toggleEnabled(true)
|
||||
ErrorService.setLevel("error")
|
||||
}
|
||||
|
||||
static toggleEnabled(state: boolean) {
|
||||
if (state === false) {
|
||||
ErrorService.serviceEnabled = false
|
||||
return
|
||||
}
|
||||
// If we are trying to enable the service, check that we are allowed to.
|
||||
if (isTelemetryEnabled) {
|
||||
ErrorService.serviceEnabled = true
|
||||
}
|
||||
}
|
||||
|
||||
static setLevel(level: "error" | "all") {
|
||||
switch (telemetryLevel) {
|
||||
case "error": {
|
||||
if (level === "error") {
|
||||
ErrorService.serviceLevel = level
|
||||
}
|
||||
break
|
||||
}
|
||||
default: {
|
||||
ErrorService.serviceLevel = level
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static logException(error: Error): void {
|
||||
// Don't log if telemetry is off
|
||||
const isUserManuallyOptedIn = telemetryService.isTelemetryEnabled()
|
||||
if (!isUserManuallyOptedIn || !ErrorService.isEnabled()) {
|
||||
return
|
||||
}
|
||||
// Log the error to Sentry
|
||||
Sentry.captureException(error)
|
||||
}
|
||||
|
||||
static logMessage(message: string, level: "error" | "warning" | "log" | "debug" | "info" = "log"): void {
|
||||
// Don't log if telemetry is off
|
||||
const isUserManuallyOptedIn = telemetryService.isTelemetryEnabled()
|
||||
if (!isUserManuallyOptedIn || !ErrorService.isEnabled()) {
|
||||
return
|
||||
}
|
||||
if (ErrorService.serviceLevel === "error" && level === "error") {
|
||||
// Log the message if allowed
|
||||
Sentry.captureMessage(message, { level })
|
||||
return
|
||||
}
|
||||
// Log the message if allowed
|
||||
Sentry.captureMessage(message, { level })
|
||||
}
|
||||
|
||||
static isEnabled(): boolean {
|
||||
return ErrorService.serviceEnabled
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { OutputChannel } from "vscode"
|
||||
import { ErrorService } from "../error/ErrorService"
|
||||
|
||||
/**
|
||||
* Simple logging utility for the extension's backend code.
|
||||
@@ -12,7 +13,25 @@ export class Logger {
|
||||
Logger.outputChannel = outputChannel
|
||||
}
|
||||
|
||||
static error(message: string, exception?: Error) {
|
||||
Logger.outputChannel.appendLine(`ERROR: ${message}`)
|
||||
ErrorService.logMessage(message, "error")
|
||||
exception && ErrorService.logException(exception)
|
||||
}
|
||||
static warn(message: string) {
|
||||
Logger.outputChannel.appendLine(`WARN: ${message}`)
|
||||
ErrorService.logMessage(message, "warning")
|
||||
}
|
||||
static log(message: string) {
|
||||
Logger.outputChannel.appendLine(message)
|
||||
Logger.outputChannel.appendLine(`LOG: ${message}`)
|
||||
}
|
||||
static debug(message: string) {
|
||||
Logger.outputChannel.appendLine(`DEBUG: ${message}`)
|
||||
}
|
||||
static info(message: string) {
|
||||
Logger.outputChannel.appendLine(`INFO: ${message}`)
|
||||
}
|
||||
static trace(message: string) {
|
||||
Logger.outputChannel.appendLine(`TRACE: ${message}`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
import * as http from "http"
|
||||
import { Logger } from "../../services/logging/Logger"
|
||||
import { WebviewProvider } from "../../core/webview"
|
||||
|
||||
let testServer: http.Server | undefined
|
||||
|
||||
/**
|
||||
* Creates and starts an HTTP server for test automation
|
||||
* @returns The created HTTP server instance
|
||||
*/
|
||||
export function createTestServer(): http.Server {
|
||||
const PORT = 9876
|
||||
|
||||
testServer = http.createServer((req, res) => {
|
||||
// Set CORS headers
|
||||
res.setHeader("Access-Control-Allow-Origin", "*")
|
||||
res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS")
|
||||
res.setHeader("Access-Control-Allow-Headers", "Content-Type")
|
||||
|
||||
// Handle preflight requests
|
||||
if (req.method === "OPTIONS") {
|
||||
res.writeHead(204)
|
||||
res.end()
|
||||
return
|
||||
}
|
||||
|
||||
// Only handle POST requests to /task
|
||||
if (req.method !== "POST" || req.url !== "/task") {
|
||||
res.writeHead(404)
|
||||
res.end(JSON.stringify({ error: "Not found" }))
|
||||
return
|
||||
}
|
||||
|
||||
// Parse the request body
|
||||
let body = ""
|
||||
req.on("data", (chunk) => {
|
||||
body += chunk.toString()
|
||||
})
|
||||
|
||||
req.on("end", async () => {
|
||||
try {
|
||||
// Parse the JSON body
|
||||
const { task } = JSON.parse(body)
|
||||
|
||||
if (!task) {
|
||||
res.writeHead(400)
|
||||
res.end(JSON.stringify({ error: "Missing task parameter" }))
|
||||
return
|
||||
}
|
||||
|
||||
// Get a visible webview instance
|
||||
const visibleWebview = WebviewProvider.getVisibleInstance()
|
||||
if (!visibleWebview || !visibleWebview.controller) {
|
||||
res.writeHead(500)
|
||||
res.end(JSON.stringify({ error: "No active Cline instance found" }))
|
||||
return
|
||||
}
|
||||
|
||||
// Initiate a new task
|
||||
Logger.log(`Test server initiating task: ${task}`)
|
||||
|
||||
try {
|
||||
// Clear any existing task
|
||||
await visibleWebview.controller.clearTask()
|
||||
|
||||
// Ensure we're in Act mode before initiating the task
|
||||
const { chatSettings } = await visibleWebview.controller.getStateToPostToWebview()
|
||||
if (chatSettings.mode === "plan") {
|
||||
// Switch to Act mode if currently in Plan mode
|
||||
await visibleWebview.controller.togglePlanActModeWithChatSettings({ mode: "act" })
|
||||
}
|
||||
|
||||
// Initiate the new task
|
||||
const taskId = await visibleWebview.controller.initClineWithTask(task)
|
||||
|
||||
// Return success response with the task ID
|
||||
res.writeHead(200, { "Content-Type": "application/json" })
|
||||
res.end(JSON.stringify({ success: true, taskId }))
|
||||
} catch (error) {
|
||||
Logger.log(`Error initiating task: ${error}`)
|
||||
res.writeHead(500)
|
||||
res.end(JSON.stringify({ error: `Failed to initiate task: ${error}` }))
|
||||
}
|
||||
} catch (error) {
|
||||
res.writeHead(400)
|
||||
res.end(JSON.stringify({ error: `Invalid JSON: ${error}` }))
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
testServer.listen(PORT, () => {
|
||||
Logger.log(`Test server listening on port ${PORT}`)
|
||||
})
|
||||
|
||||
// Handle server errors
|
||||
testServer.on("error", (error) => {
|
||||
Logger.log(`Test server error: ${error}`)
|
||||
})
|
||||
|
||||
return testServer
|
||||
}
|
||||
|
||||
/**
|
||||
* Shuts down the test server if it exists
|
||||
*/
|
||||
export function shutdownTestServer() {
|
||||
if (testServer) {
|
||||
testServer.close()
|
||||
Logger.log("Test server shut down")
|
||||
testServer = undefined
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import { AutoApprovalSettings } from "./AutoApprovalSettings"
|
||||
import { BrowserSettings } from "./BrowserSettings"
|
||||
import { ChatSettings } from "./ChatSettings"
|
||||
import { HistoryItem } from "./HistoryItem"
|
||||
import { McpServer, McpMarketplaceCatalog, McpMarketplaceItem, McpDownloadResponse } from "./mcp"
|
||||
import { McpServer, McpMarketplaceCatalog, McpMarketplaceItem, McpDownloadResponse, McpViewTab } from "./mcp"
|
||||
import { TelemetrySetting } from "./TelemetrySetting"
|
||||
import type { BalanceResponse, UsageTransaction, PaymentTransaction } from "../shared/ClineAccount"
|
||||
|
||||
@@ -106,6 +106,7 @@ export interface ExtensionMessage {
|
||||
serverName: string
|
||||
error?: string
|
||||
}
|
||||
tab?: McpViewTab
|
||||
}
|
||||
|
||||
export type Invoke = "sendMessage" | "primaryButtonClick" | "secondaryButtonClick"
|
||||
|
||||
@@ -5,6 +5,7 @@ import { ChatSettings } from "./ChatSettings"
|
||||
import { UserInfo } from "./UserInfo"
|
||||
import { ChatContent } from "./ChatContent"
|
||||
import { TelemetrySetting } from "./TelemetrySetting"
|
||||
import { McpViewTab } from "./mcp"
|
||||
|
||||
export interface WebviewMessage {
|
||||
type:
|
||||
@@ -93,6 +94,7 @@ export interface WebviewMessage {
|
||||
chatContent?: ChatContent
|
||||
mcpId?: string
|
||||
timeout?: number
|
||||
tab?: McpViewTab
|
||||
// For toggleToolAutoApprove
|
||||
serverName?: string
|
||||
serverUrl?: string
|
||||
|
||||
+37
-1
@@ -1319,8 +1319,44 @@ export const askSageModels = {
|
||||
// X AI
|
||||
// https://docs.x.ai/docs/api-reference
|
||||
export type XAIModelId = keyof typeof xaiModels
|
||||
export const xaiDefaultModelId: XAIModelId = "grok-2-latest"
|
||||
export const xaiDefaultModelId: XAIModelId = "grok-3-beta"
|
||||
export const xaiModels = {
|
||||
"grok-3-beta": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 131072,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 3.0,
|
||||
outputPrice: 15.0,
|
||||
description: "X AI's Grok-3 beta model with 131K context window",
|
||||
},
|
||||
"grok-3-fast-beta": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 131072,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 5.0,
|
||||
outputPrice: 25.0,
|
||||
description: "X AI's Grok-3 fast beta model with 131K context window",
|
||||
},
|
||||
"grok-3-mini-beta": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 131072,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.3,
|
||||
outputPrice: 0.5,
|
||||
description: "X AI's Grok-3 mini beta model with 131K context window",
|
||||
},
|
||||
"grok-3-mini-fast-beta": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 131072,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.6,
|
||||
outputPrice: 4.0,
|
||||
description: "X AI's Grok-3 mini fast beta model with 131K context window",
|
||||
},
|
||||
"grok-2-latest": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 131072,
|
||||
|
||||
@@ -105,3 +105,5 @@ export interface McpDownloadResponse {
|
||||
llmsInstallationContent: string
|
||||
requiresApiKey: boolean
|
||||
}
|
||||
|
||||
export type McpViewTab = "marketplace" | "addRemote" | "installed"
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"build:test": "tsc -b && vite build",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint .",
|
||||
"test": "vitest run",
|
||||
|
||||
@@ -10,6 +10,7 @@ import { ExtensionStateContextProvider, useExtensionState } from "./context/Exte
|
||||
import { FirebaseAuthProvider } from "./context/FirebaseAuthContext"
|
||||
import { vscode } from "./utils/vscode"
|
||||
import McpView from "./components/mcp/configuration/McpConfigurationView"
|
||||
import { McpViewTab } from "@shared/mcp"
|
||||
|
||||
const AppContent = () => {
|
||||
const { didHydrateState, showWelcome, shouldShowAnnouncement, telemetrySetting, vscMachineId } = useExtensionState()
|
||||
@@ -18,6 +19,7 @@ const AppContent = () => {
|
||||
const [showMcp, setShowMcp] = useState(false)
|
||||
const [showAccount, setShowAccount] = useState(false)
|
||||
const [showAnnouncement, setShowAnnouncement] = useState(false)
|
||||
const [mcpTab, setMcpTab] = useState<McpViewTab | undefined>(undefined)
|
||||
|
||||
const handleMessage = useCallback((e: MessageEvent) => {
|
||||
const message: ExtensionMessage = e.data
|
||||
@@ -39,6 +41,9 @@ const AppContent = () => {
|
||||
case "mcpButtonClicked":
|
||||
setShowSettings(false)
|
||||
setShowHistory(false)
|
||||
if (message.tab) {
|
||||
setMcpTab(message.tab)
|
||||
}
|
||||
setShowMcp(true)
|
||||
setShowAccount(false)
|
||||
break
|
||||
@@ -89,7 +94,7 @@ const AppContent = () => {
|
||||
<>
|
||||
{showSettings && <SettingsView onDone={() => setShowSettings(false)} />}
|
||||
{showHistory && <HistoryView onDone={() => setShowHistory(false)} />}
|
||||
{showMcp && <McpView onDone={() => setShowMcp(false)} />}
|
||||
{showMcp && <McpView initialTab={mcpTab} onDone={() => setShowMcp(false)} />}
|
||||
{showAccount && <AccountView onDone={() => setShowAccount(false)} />}
|
||||
{/* Do not conditionally load ChatView, it's expensive and there's state we don't want to lose (user input, disableInput, askResponse promise, etc.) */}
|
||||
<ChatView
|
||||
|
||||
@@ -41,6 +41,15 @@ const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
|
||||
<h3 style={h3TitleStyle}>
|
||||
🎉{" "}New in v{minorVersion}
|
||||
</h3>
|
||||
<ul style={ulStyle}>
|
||||
<li>
|
||||
<b>Redesigned Checkpoints:</b> Checkpoints are now created more often through the task, and will appear as
|
||||
line indicators on the left edge of chat. Hover over them to expand and see details like when they were
|
||||
created!
|
||||
</li>
|
||||
</ul>
|
||||
<h4 style={{ margin: "5px 0 5px" }}>From v3.10:</h4>
|
||||
|
||||
<ul style={ulStyle}>
|
||||
<li>
|
||||
<b>Browser Tool Upgrades:</b> Use your local Chrome browser for session-based browsing, enabling debugging and
|
||||
|
||||
@@ -275,7 +275,6 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
|
||||
consoleLogs: currentPage?.currentState.consoleLogs,
|
||||
screenshot: currentPage?.currentState.screenshot,
|
||||
}
|
||||
|
||||
const [actionContent, { height: actionHeight }] = useSize(
|
||||
<div>
|
||||
{currentPage?.nextAction?.messages.map((message) => (
|
||||
|
||||
@@ -17,6 +17,7 @@ import { COMMAND_OUTPUT_STRING, COMMAND_REQ_APP_STRING } from "@shared/combineCo
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { findMatchingResourceOrTemplate, getMcpServerDisplayName } from "@/utils/mcp"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
import { useChatRowStyles } from "@/hooks/useChatRowStyles"
|
||||
import { CheckmarkControl } from "@/components/common/CheckmarkControl"
|
||||
import { CheckpointControls, CheckpointOverlay } from "../common/CheckpointControls"
|
||||
import CodeAccordian, { cleanPathPrefix } from "../common/CodeAccordian"
|
||||
@@ -47,11 +48,12 @@ interface ChatRowProps {
|
||||
isExpanded: boolean
|
||||
onToggleExpand: () => void
|
||||
lastModifiedMessage?: ClineMessage
|
||||
isFirst: boolean
|
||||
isLast: boolean
|
||||
onHeightChange: (isTaller: boolean) => void
|
||||
}
|
||||
|
||||
interface ChatRowContentProps extends Omit<ChatRowProps, "onHeightChange"> {}
|
||||
interface ChatRowContentProps extends Omit<ChatRowProps, "onHeightChange" | "isFirst"> {}
|
||||
|
||||
export const ProgressIndicator = () => (
|
||||
<div
|
||||
@@ -84,36 +86,40 @@ const Markdown = memo(({ markdown }: { markdown?: string }) => {
|
||||
|
||||
const ChatRow = memo(
|
||||
(props: ChatRowProps) => {
|
||||
const { isLast, onHeightChange, message, lastModifiedMessage } = props
|
||||
const { isLast, isFirst, onHeightChange, message } = 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)
|
||||
// Calculate dynamic styles using the custom hook
|
||||
const { ...chatRowStyles } = useChatRowStyles(message)
|
||||
|
||||
// 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")
|
||||
const isCheckpointMessage = message.say === "checkpoint_created"
|
||||
|
||||
if (shouldShowCheckpoints && isLast) {
|
||||
shouldShowCheckpoints =
|
||||
lastModifiedMessage?.ask === "resume_completed_task" || lastModifiedMessage?.ask === "resume_task"
|
||||
}
|
||||
// Special handling for first row checkpoint
|
||||
const checkpointStyles = useMemo(() => {
|
||||
if (isCheckpointMessage) {
|
||||
// Apply additional styles for first row checkpoints
|
||||
if (isFirst) {
|
||||
return {
|
||||
...chatRowStyles,
|
||||
marginTop: "3px", // Add a small margin to ensure visibility
|
||||
}
|
||||
}
|
||||
return chatRowStyles
|
||||
}
|
||||
return chatRowStyles
|
||||
}, [chatRowStyles, isCheckpointMessage, isFirst])
|
||||
|
||||
// ChatRowContainer with updated styles
|
||||
const [chatrow, { height }] = useSize(
|
||||
<ChatRowContainer>
|
||||
<ChatRowContainer style={checkpointStyles}>
|
||||
<ChatRowContent {...props} />
|
||||
{shouldShowCheckpoints && <CheckpointOverlay messageTs={message.ts} />}
|
||||
</ChatRowContainer>,
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
// Skip height change effects for checkpoint messages
|
||||
if (isCheckpointMessage) return
|
||||
|
||||
// used for partials command output etc.
|
||||
// NOTE: it's important we don't distinguish between partial or complete here since our scroll effects in chatview need to handle height change during partial -> complete
|
||||
const isInitialRender = prevHeightRef.current === 0 // prevents scrolling when new element is added since we already scroll for that
|
||||
@@ -124,7 +130,7 @@ const ChatRow = memo(
|
||||
}
|
||||
prevHeightRef.current = height
|
||||
}
|
||||
}, [height, isLast, onHeightChange, message])
|
||||
}, [height, isLast, onHeightChange, message, isCheckpointMessage])
|
||||
|
||||
// we cannot return null as virtuoso does not support it so we use a separate visibleMessages array to filter out messages that should not be rendered
|
||||
return chatrow
|
||||
@@ -987,7 +993,11 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi
|
||||
case "checkpoint_created":
|
||||
return (
|
||||
<>
|
||||
<CheckmarkControl messageTs={message.ts} isCheckpointCheckedOut={message.isCheckpointCheckedOut} />
|
||||
<CheckmarkControl
|
||||
messageTs={message.ts}
|
||||
isCheckpointCheckedOut={message.isCheckpointCheckedOut}
|
||||
isLastRow={isLast}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
case "completion_result":
|
||||
|
||||
@@ -779,6 +779,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
onToggleExpand={() => toggleRowExpansion(messageOrGroup.ts)}
|
||||
lastModifiedMessage={modifiedMessages.at(-1)}
|
||||
isLast={index === groupedMessages.length - 1}
|
||||
isFirst={index === 0}
|
||||
onHeightChange={handleRowHeightChange}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -73,10 +73,22 @@ const ServersToggleModal: React.FC = () => {
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="m-0 mb-2.5">MCP Servers</div>
|
||||
<div style={{ marginBottom: "-10px" }}>
|
||||
<ServersToggleList servers={mcpServers} isExpandable={false} hasTrashIcon={false} listGap="small" />
|
||||
<div className="flex justify-between items-center mb-2.5">
|
||||
<h3 className="m-0">MCP Servers</h3>
|
||||
<VSCodeButton
|
||||
appearance="icon"
|
||||
onClick={() => {
|
||||
vscode.postMessage({
|
||||
type: "showMcpView",
|
||||
tab: "installed",
|
||||
})
|
||||
setIsVisible(false)
|
||||
}}>
|
||||
<span className="codicon codicon-gear text-[10px]"></span>
|
||||
</VSCodeButton>
|
||||
</div>
|
||||
|
||||
<ServersToggleList servers={mcpServers} isExpandable={false} hasTrashIcon={false} listGap="small" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useRef, useState, useEffect } from "react"
|
||||
import { useCallback, useRef, useState, useEffect, useMemo } from "react"
|
||||
import { useEvent } from "react-use"
|
||||
import styled from "styled-components"
|
||||
import { ExtensionMessage } from "@shared/ExtensionMessage"
|
||||
@@ -11,17 +11,20 @@ import { useFloating, offset, flip, shift } from "@floating-ui/react"
|
||||
interface CheckmarkControlProps {
|
||||
messageTs?: number
|
||||
isCheckpointCheckedOut?: boolean
|
||||
/** Whether this is the last row in the chat */
|
||||
isLastRow?: boolean
|
||||
}
|
||||
|
||||
export const CheckmarkControl = ({ messageTs, isCheckpointCheckedOut }: CheckmarkControlProps) => {
|
||||
export const CheckmarkControl = ({ messageTs, isCheckpointCheckedOut, isLastRow = false }: CheckmarkControlProps) => {
|
||||
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)
|
||||
const [isComponentHovered, setIsComponentHovered] = useState(false)
|
||||
const [isLineHovered, setIsLineHovered] = useState(false)
|
||||
|
||||
const { refs, floatingStyles, update, placement } = useFloating({
|
||||
placement: "bottom-end",
|
||||
@@ -35,6 +38,65 @@ export const CheckmarkControl = ({ messageTs, isCheckpointCheckedOut }: Checkmar
|
||||
],
|
||||
})
|
||||
|
||||
// Simple time formatter if date-fns is not available
|
||||
const getSimpleRelativeTime = (date: Date) => {
|
||||
const now = new Date()
|
||||
const diffMs = now.getTime() - date.getTime()
|
||||
|
||||
const seconds = Math.floor(diffMs / 1000)
|
||||
// Handle "just now" case in the fallback too
|
||||
if (seconds < 180) return "now" // 3 minutes = 180 seconds
|
||||
|
||||
const minutes = Math.floor(seconds / 60)
|
||||
if (minutes < 60) return `${minutes}m`
|
||||
|
||||
const hours = Math.floor(minutes / 60)
|
||||
if (hours < 24) return `${hours}h`
|
||||
|
||||
const days = Math.floor(hours / 24)
|
||||
return `${days}d`
|
||||
}
|
||||
|
||||
// Format the timestamp for relative time display
|
||||
const relativeTime = useMemo(() => {
|
||||
if (!messageTs) return ""
|
||||
|
||||
// Create a Date object from the timestamp
|
||||
const date = new Date(messageTs)
|
||||
const now = new Date()
|
||||
|
||||
// Calculate time difference in milliseconds
|
||||
const diffMs = now.getTime() - date.getTime()
|
||||
const diffMinutes = Math.floor(diffMs / (1000 * 60))
|
||||
|
||||
// Show "just now" if less than 3 minutes
|
||||
if (diffMinutes < 3) {
|
||||
return "now"
|
||||
}
|
||||
|
||||
// Fallback formatting if date-fns errors
|
||||
return getSimpleRelativeTime(date)
|
||||
}, [messageTs])
|
||||
|
||||
// Format the full timestamp for the detailed display (without year)
|
||||
const formattedTime = useMemo(() => {
|
||||
if (!messageTs) return ""
|
||||
const date = new Date(messageTs)
|
||||
return date.toLocaleString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
hour12: true,
|
||||
})
|
||||
}, [messageTs])
|
||||
|
||||
// Combined time format with relative time and full date
|
||||
const combinedTimeFormat = useMemo(() => {
|
||||
if (!relativeTime || !formattedTime) return ""
|
||||
return `${relativeTime} · ${formattedTime}`
|
||||
}, [relativeTime, formattedTime])
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
update()
|
||||
@@ -119,199 +181,334 @@ export const CheckmarkControl = ({ messageTs, isCheckpointCheckedOut }: Checkmar
|
||||
|
||||
useEvent("message", handleMessage)
|
||||
|
||||
// Modified: Only show the expanded UI on hover (not permanently for checked out checkpoints)
|
||||
// This way checked out checkpoints only show the line indicator unless hovered
|
||||
const showExpandedUI =
|
||||
(isLineHovered || isComponentHovered || showRestoreConfirm) &&
|
||||
!(showRestoreConfirm === false && isComponentHovered === false && isLineHovered === false)
|
||||
|
||||
// The line should still be highlighted when the checkpoint is checked out
|
||||
const shouldShowHoveredLine = isCheckpointCheckedOut || isLineHovered || isComponentHovered || showRestoreConfirm
|
||||
|
||||
return (
|
||||
<Container isMenuOpen={showRestoreConfirm} $isCheckedOut={isCheckpointCheckedOut} onMouseLeave={handleControlsMouseLeave}>
|
||||
<i
|
||||
className="codicon codicon-bookmark"
|
||||
style={{
|
||||
color: isCheckpointCheckedOut ? "var(--vscode-textLink-foreground)" : "var(--vscode-descriptionForeground)",
|
||||
fontSize: "12px",
|
||||
flexShrink: 0,
|
||||
<Container isMenuOpen={showRestoreConfirm} $isCheckedOut={isCheckpointCheckedOut}>
|
||||
{/* Line indicator is still styled differently for checked out checkpoints */}
|
||||
<CheckpointIndicator
|
||||
$isCheckedOut={isCheckpointCheckedOut}
|
||||
$isHovered={shouldShowHoveredLine}
|
||||
onMouseEnter={() => setIsLineHovered(true)}
|
||||
onMouseLeave={() => {
|
||||
setTimeout(() => {
|
||||
if (!isComponentHovered && !showRestoreConfirm) {
|
||||
setIsLineHovered(false)
|
||||
}
|
||||
}, 50)
|
||||
}}
|
||||
/>
|
||||
<Label $isCheckedOut={isCheckpointCheckedOut}>
|
||||
{isCheckpointCheckedOut ? "Checkpoint (restored)" : "Checkpoint"}
|
||||
</Label>
|
||||
<DottedLine $isCheckedOut={isCheckpointCheckedOut} />
|
||||
<ButtonGroup>
|
||||
<CustomButton
|
||||
|
||||
<HoverArea
|
||||
onMouseEnter={() => setIsLineHovered(true)}
|
||||
onMouseLeave={() => {
|
||||
if (!isComponentHovered && !showRestoreConfirm) {
|
||||
setIsLineHovered(false)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{showExpandedUI && (
|
||||
<ExpandedUI
|
||||
$isCheckedOut={isCheckpointCheckedOut}
|
||||
disabled={compareDisabled}
|
||||
style={{ cursor: compareDisabled ? "wait" : "pointer" }}
|
||||
onClick={() => {
|
||||
setCompareDisabled(true)
|
||||
vscode.postMessage({
|
||||
type: "checkpointDiff",
|
||||
number: messageTs,
|
||||
})
|
||||
$isLastRow={isLastRow}
|
||||
onMouseEnter={() => {
|
||||
setIsComponentHovered(true)
|
||||
setIsLineHovered(true)
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (!showRestoreConfirm) {
|
||||
setIsComponentHovered(false)
|
||||
setIsLineHovered(false)
|
||||
} else {
|
||||
handleControlsMouseLeave(e)
|
||||
}
|
||||
}}>
|
||||
Compare
|
||||
</CustomButton>
|
||||
<DottedLine small $isCheckedOut={isCheckpointCheckedOut} />
|
||||
<div ref={refs.setReference} style={{ position: "relative", marginTop: -2 }}>
|
||||
<CustomButton
|
||||
$isCheckedOut={isCheckpointCheckedOut}
|
||||
isActive={showRestoreConfirm}
|
||||
onClick={() => setShowRestoreConfirm(true)}>
|
||||
Restore
|
||||
</CustomButton>
|
||||
{showRestoreConfirm &&
|
||||
createPortal(
|
||||
<RestoreConfirmTooltip
|
||||
ref={refs.setFloating}
|
||||
style={floatingStyles}
|
||||
data-placement={placement}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}>
|
||||
<RestoreOption>
|
||||
<VSCodeButton
|
||||
onClick={handleRestoreWorkspace}
|
||||
disabled={restoreWorkspaceDisabled}
|
||||
style={{
|
||||
cursor: restoreWorkspaceDisabled ? "wait" : "pointer",
|
||||
width: "100%",
|
||||
marginBottom: "10px",
|
||||
}}>
|
||||
Restore Files
|
||||
</VSCodeButton>
|
||||
<p>
|
||||
Restores your project's files back to a snapshot taken at this point (use "Compare" to see
|
||||
what will be reverted)
|
||||
</p>
|
||||
</RestoreOption>
|
||||
<RestoreOption>
|
||||
<VSCodeButton
|
||||
onClick={handleRestoreTask}
|
||||
disabled={restoreTaskDisabled}
|
||||
style={{
|
||||
cursor: restoreTaskDisabled ? "wait" : "pointer",
|
||||
width: "100%",
|
||||
marginBottom: "10px",
|
||||
}}>
|
||||
Restore Task Only
|
||||
</VSCodeButton>
|
||||
<p>Deletes messages after this point (does not affect workspace files)</p>
|
||||
</RestoreOption>
|
||||
<RestoreOption>
|
||||
<VSCodeButton
|
||||
onClick={handleRestoreBoth}
|
||||
disabled={restoreBothDisabled}
|
||||
style={{
|
||||
cursor: restoreBothDisabled ? "wait" : "pointer",
|
||||
width: "100%",
|
||||
marginBottom: "10px",
|
||||
}}>
|
||||
Restore Files & Task
|
||||
</VSCodeButton>
|
||||
<p>Restores your project's files and deletes all messages after this point</p>
|
||||
</RestoreOption>
|
||||
</RestoreConfirmTooltip>,
|
||||
document.body,
|
||||
)}
|
||||
</div>
|
||||
<DottedLine small $isCheckedOut={isCheckpointCheckedOut} />
|
||||
</ButtonGroup>
|
||||
<SimpleLayout>
|
||||
<LabelColumn>
|
||||
<div style={{ display: "flex", alignItems: "center" }}>
|
||||
<i
|
||||
className="codicon codicon-bookmark"
|
||||
style={{
|
||||
color: isCheckpointCheckedOut
|
||||
? "var(--vscode-textLink-foreground)"
|
||||
: "var(--vscode-descriptionForeground)",
|
||||
fontSize: "12px",
|
||||
marginRight: "6px",
|
||||
}}
|
||||
/>
|
||||
<Label $isCheckedOut={isCheckpointCheckedOut}>
|
||||
{isCheckpointCheckedOut ? "Checkpoint (restored)" : "Checkpoint"}
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<TimeLabel $isCheckedOut={isCheckpointCheckedOut}>{combinedTimeFormat}</TimeLabel>
|
||||
</LabelColumn>
|
||||
|
||||
<ButtonsWrapper>
|
||||
<EnhancedButton
|
||||
$isCheckedOut={isCheckpointCheckedOut}
|
||||
disabled={compareDisabled}
|
||||
style={{ cursor: compareDisabled ? "wait" : "pointer" }}
|
||||
onClick={() => {
|
||||
setCompareDisabled(true)
|
||||
vscode.postMessage({
|
||||
type: "checkpointDiff",
|
||||
number: messageTs,
|
||||
})
|
||||
}}>
|
||||
Compare
|
||||
</EnhancedButton>
|
||||
|
||||
<div ref={refs.setReference} style={{ position: "relative" }}>
|
||||
<EnhancedButton
|
||||
$isCheckedOut={isCheckpointCheckedOut}
|
||||
isActive={showRestoreConfirm}
|
||||
onClick={() => setShowRestoreConfirm(true)}>
|
||||
Restore
|
||||
</EnhancedButton>
|
||||
{showRestoreConfirm &&
|
||||
createPortal(
|
||||
<RestoreConfirmTooltip
|
||||
ref={refs.setFloating}
|
||||
style={floatingStyles}
|
||||
data-placement={placement}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}>
|
||||
<RestoreOption>
|
||||
<VSCodeButton
|
||||
onClick={handleRestoreWorkspace}
|
||||
disabled={restoreWorkspaceDisabled}
|
||||
style={{
|
||||
cursor: restoreWorkspaceDisabled ? "wait" : "pointer",
|
||||
width: "100%",
|
||||
marginBottom: "10px",
|
||||
}}>
|
||||
Restore Files
|
||||
</VSCodeButton>
|
||||
<p>
|
||||
Restores your project's files back to a snapshot taken at this point (use
|
||||
"Compare" to see what will be reverted)
|
||||
</p>
|
||||
</RestoreOption>
|
||||
<RestoreOption>
|
||||
<VSCodeButton
|
||||
onClick={handleRestoreTask}
|
||||
disabled={restoreTaskDisabled}
|
||||
style={{
|
||||
cursor: restoreTaskDisabled ? "wait" : "pointer",
|
||||
width: "100%",
|
||||
marginBottom: "10px",
|
||||
}}>
|
||||
Restore Task Only
|
||||
</VSCodeButton>
|
||||
<p>Deletes messages after this point (does not affect workspace files)</p>
|
||||
</RestoreOption>
|
||||
<RestoreOption>
|
||||
<VSCodeButton
|
||||
onClick={handleRestoreBoth}
|
||||
disabled={restoreBothDisabled}
|
||||
style={{
|
||||
cursor: restoreBothDisabled ? "wait" : "pointer",
|
||||
width: "100%",
|
||||
marginBottom: "10px",
|
||||
}}>
|
||||
Restore Files & Task
|
||||
</VSCodeButton>
|
||||
<p>Restores your project's files and deletes all messages after this point</p>
|
||||
</RestoreOption>
|
||||
</RestoreConfirmTooltip>,
|
||||
document.body,
|
||||
)}
|
||||
</div>
|
||||
</ButtonsWrapper>
|
||||
</SimpleLayout>
|
||||
</ExpandedUI>
|
||||
)}
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled.div<{ isMenuOpen?: boolean; $isCheckedOut?: boolean }>`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 4px 0;
|
||||
gap: 4px;
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
margin-top: -10px;
|
||||
margin-bottom: -10px;
|
||||
opacity: ${(props) => (props.$isCheckedOut ? 1 : props.isMenuOpen ? 1 : 0.5)};
|
||||
// Updated Container styling - doesn't need to handle as many hover events
|
||||
const Container = styled.div<{
|
||||
isMenuOpen?: boolean
|
||||
$isCheckedOut?: boolean
|
||||
}>`
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
height: 0;
|
||||
z-index: 10;
|
||||
pointer-events: auto;
|
||||
`
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
// Invisible hover area just around the line indicator
|
||||
const HoverArea = styled.div`
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: -6px;
|
||||
width: 20px; /* Wide enough to easily hover */
|
||||
height: 15px; /* Tall enough to catch hover events */
|
||||
cursor: pointer;
|
||||
`
|
||||
|
||||
// Make the highlighted line more visible for checked out checkpoints
|
||||
const CheckpointIndicator = styled.div<{
|
||||
$isCheckedOut?: boolean
|
||||
$isHovered?: boolean
|
||||
}>`
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
/* Make checked out checkpoints have a more visible line */
|
||||
width: ${(props) =>
|
||||
props.$isCheckedOut ? "10px" /* Wider default for checked out checkpoints */ : props.$isHovered ? "12px" : "8px"};
|
||||
height: 3px;
|
||||
background-color: ${(props) =>
|
||||
props.$isCheckedOut ? "var(--vscode-textLink-foreground)" : "var(--vscode-descriptionForeground)"};
|
||||
opacity: ${(props) => (props.$isCheckedOut ? 1 /* Always full opacity for checked out */ : props.$isHovered ? 1 : 0.6)};
|
||||
transition:
|
||||
opacity 0.15s ease-in-out,
|
||||
width 0.15s ease-in-out;
|
||||
cursor: pointer;
|
||||
border-top-right-radius: 2px;
|
||||
border-bottom-right-radius: 2px;
|
||||
z-index: 5;
|
||||
`
|
||||
|
||||
// Added label column component for consistent height
|
||||
const LabelColumn = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
`
|
||||
|
||||
// Updated SimpleLayout with center alignment
|
||||
const SimpleLayout = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center; /* Center align items vertically */
|
||||
width: 100%;
|
||||
`
|
||||
|
||||
// Updated ButtonsWrapper with center alignment
|
||||
const ButtonsWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px; /* Reduced gap */
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
// Container for the expanded UI that appears on hover
|
||||
const ExpandedUI = styled.div<{
|
||||
$isCheckedOut?: boolean
|
||||
$isLastRow?: boolean
|
||||
}>`
|
||||
position: absolute;
|
||||
left: 15px;
|
||||
right: 7px;
|
||||
${
|
||||
(props) =>
|
||||
props.$isLastRow
|
||||
? "bottom: -3px;" // Position above for last row
|
||||
: "top: -3px;" // Position below for normal rows
|
||||
}
|
||||
background-color: var(--vscode-editor-background);
|
||||
border-radius: 3px;
|
||||
padding: 10px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
border: 1px solid var(--vscode-widget-border);
|
||||
z-index: 20;
|
||||
animation: ${(props) => (props.$isLastRow ? "fadeInUp" : "fadeIn")} 0.15s ease-in-out;
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(5px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
// Updated Label with slightly smaller font size
|
||||
const Label = styled.span<{ $isCheckedOut?: boolean }>`
|
||||
color: ${(props) => (props.$isCheckedOut ? "var(--vscode-textLink-foreground)" : "var(--vscode-descriptionForeground)")};
|
||||
font-size: 9px;
|
||||
font-size: 11px; // Reduced from 12px
|
||||
font-weight: 500;
|
||||
flex-shrink: 0;
|
||||
`
|
||||
|
||||
const DottedLine = styled.div<{ small?: boolean; $isCheckedOut?: boolean }>`
|
||||
flex: ${(props) => (props.small ? "0 0 5px" : "1")};
|
||||
min-width: ${(props) => (props.small ? "5px" : "5px")};
|
||||
height: 1px;
|
||||
background-image: linear-gradient(
|
||||
to right,
|
||||
${(props) => (props.$isCheckedOut ? "var(--vscode-textLink-foreground)" : "var(--vscode-descriptionForeground)")} 50%,
|
||||
transparent 50%
|
||||
);
|
||||
background-size: 4px 1px;
|
||||
background-repeat: repeat-x;
|
||||
// Updated TimeLabel with word-break enabled
|
||||
const TimeLabel = styled.span<{ $isCheckedOut?: boolean }>`
|
||||
font-size: 10px;
|
||||
color: ${(props) => (props.$isCheckedOut ? "var(--vscode-textLink-foreground)" : "var(--vscode-descriptionForeground)")};
|
||||
margin-top: 3px;
|
||||
margin-left: 20px;
|
||||
word-break: break-word; /* Allow breaking words */
|
||||
white-space: normal; /* Changed from nowrap to allow text to wrap */
|
||||
max-width: 100%;
|
||||
display: block; /* Ensure it takes full width for proper breaking */
|
||||
`
|
||||
|
||||
const ButtonGroup = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
flex-shrink: 0;
|
||||
`
|
||||
|
||||
const CustomButton = styled.button<{ disabled?: boolean; isActive?: boolean; $isCheckedOut?: boolean }>`
|
||||
// Simplified button styling with minimal padding and no extra styling
|
||||
const EnhancedButton = styled.button<{
|
||||
disabled?: boolean
|
||||
isActive?: boolean
|
||||
$isCheckedOut?: boolean
|
||||
}>`
|
||||
background: ${(props) =>
|
||||
props.isActive || props.disabled
|
||||
? props.$isCheckedOut
|
||||
? "var(--vscode-textLink-foreground)"
|
||||
: "var(--vscode-descriptionForeground)"
|
||||
: "transparent"};
|
||||
border: none;
|
||||
: "var(--vscode-editor-background)"};
|
||||
border: 1px solid
|
||||
${(props) => (props.$isCheckedOut ? "var(--vscode-textLink-foreground)" : "var(--vscode-descriptionForeground)")};
|
||||
color: ${(props) =>
|
||||
props.isActive || props.disabled
|
||||
? "var(--vscode-editor-background)"
|
||||
: props.$isCheckedOut
|
||||
? "var(--vscode-textLink-foreground)"
|
||||
: "var(--vscode-descriptionForeground)"};
|
||||
padding: 2px 6px;
|
||||
font-size: 9px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
border-radius: 1px;
|
||||
background-image: ${(props) =>
|
||||
props.isActive || props.disabled
|
||||
? "none"
|
||||
: `linear-gradient(to right, ${props.$isCheckedOut ? "var(--vscode-textLink-foreground)" : "var(--vscode-descriptionForeground)"} 50%, transparent 50%),
|
||||
linear-gradient(to bottom, ${props.$isCheckedOut ? "var(--vscode-textLink-foreground)" : "var(--vscode-descriptionForeground)"} 50%, transparent 50%),
|
||||
linear-gradient(to right, ${props.$isCheckedOut ? "var(--vscode-textLink-foreground)" : "var(--vscode-descriptionForeground)"} 50%, transparent 50%),
|
||||
linear-gradient(to bottom, ${props.$isCheckedOut ? "var(--vscode-textLink-foreground)" : "var(--vscode-descriptionForeground)"} 50%, transparent 50%)`};
|
||||
background-size: ${(props) => (props.isActive || props.disabled ? "auto" : `4px 1px, 1px 4px, 4px 1px, 1px 4px`)};
|
||||
background-repeat: repeat-x, repeat-y, repeat-x, repeat-y;
|
||||
background-position:
|
||||
0 0,
|
||||
100% 0,
|
||||
0 100%,
|
||||
0 0;
|
||||
}
|
||||
border-radius: 3px;
|
||||
padding: 3px 4px; /* Minimal padding */
|
||||
font-size: 10px;
|
||||
cursor: ${(props) => (props.disabled ? "wait" : "pointer")};
|
||||
line-height: 1;
|
||||
height: auto;
|
||||
margin: 0;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: ${(props) =>
|
||||
props.$isCheckedOut ? "var(--vscode-textLink-foreground)" : "var(--vscode-descriptionForeground)"};
|
||||
color: var(--vscode-editor-background);
|
||||
&::before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
`
|
||||
|
||||
|
||||
@@ -6,16 +6,18 @@ import { vscode } from "@/utils/vscode"
|
||||
import AddRemoteServerForm from "./tabs/add-server/AddRemoteServerForm"
|
||||
import McpMarketplaceView from "./tabs/marketplace/McpMarketplaceView"
|
||||
import InstalledServersView from "./tabs/installed/InstalledServersView"
|
||||
import { McpViewTab } from "@shared/mcp"
|
||||
|
||||
type McpViewProps = {
|
||||
onDone: () => void
|
||||
initialTab?: McpViewTab
|
||||
}
|
||||
|
||||
const McpConfigurationView = ({ onDone }: McpViewProps) => {
|
||||
const McpConfigurationView = ({ onDone, initialTab }: McpViewProps) => {
|
||||
const { mcpMarketplaceEnabled } = useExtensionState()
|
||||
const [activeTab, setActiveTab] = useState(mcpMarketplaceEnabled ? "marketplace" : "installed")
|
||||
const [activeTab, setActiveTab] = useState<McpViewTab>(initialTab || (mcpMarketplaceEnabled ? "marketplace" : "installed"))
|
||||
|
||||
const handleTabChange = (tab: string) => {
|
||||
const handleTabChange = (tab: McpViewTab) => {
|
||||
setActiveTab(tab)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { CSSProperties, useMemo } from "react"
|
||||
import { ClineMessage } from "@shared/ExtensionMessage"
|
||||
|
||||
/**
|
||||
* Custom hook to determine the dynamic styles for a ChatRowContainer.
|
||||
*
|
||||
* This hook provides styles for checkpoint messages that control their
|
||||
* visual appearance based on hover state and whether they're checked out.
|
||||
*
|
||||
* @param message - The chat message object for the current row.
|
||||
* @returns An object containing the calculated style properties.
|
||||
*/
|
||||
export const useChatRowStyles = (message: ClineMessage): CSSProperties => {
|
||||
return useMemo(() => {
|
||||
// Check if the current message is a checkpoint creation message.
|
||||
const isCheckpointMessage = message.say === "checkpoint_created"
|
||||
|
||||
// For checkpoint messages, make the row take up minimal space
|
||||
// The checkpoint component will be absolutely positioned
|
||||
if (isCheckpointMessage) {
|
||||
return {
|
||||
padding: 0,
|
||||
// we can't set height to 0 because virtuoso needs a height to render the row, we can't set to 1 because it results in a visual artifact bug, so we use this hack to make the row almost invisible (and the checkpoint indicator positioned absolutely at this point in the list)
|
||||
height: 8,
|
||||
marginTop: -4,
|
||||
marginBottom: -4,
|
||||
overflow: "visible", // Allow the absolutely positioned content to be visible
|
||||
}
|
||||
}
|
||||
|
||||
// For non-checkpoint messages, return normal styling
|
||||
return {
|
||||
padding: undefined,
|
||||
minHeight: undefined,
|
||||
height: undefined,
|
||||
overflow: undefined,
|
||||
position: undefined,
|
||||
}
|
||||
}, [message.say])
|
||||
}
|
||||
@@ -44,6 +44,7 @@ export default defineConfig({
|
||||
"process.env": {
|
||||
NODE_ENV: JSON.stringify(process.env.IS_DEV ? "development" : "production"),
|
||||
IS_DEV: JSON.stringify(process.env.IS_DEV),
|
||||
IS_TEST: JSON.stringify(process.env.IS_TEST),
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
|
||||
Reference in New Issue
Block a user