Compare commits

..

25 Commits

Author SHA1 Message Date
Evan 93d52288ea Cline.ts - Factor out overwriteApiConversationHistory [5/8] (#2361)
* factor out getSavedClineMessages

* changeset

* refactor addToApiConversationHistory

* changeset

* refactor overwriteApiConversationHistory

* changeset

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-03-21 13:16:24 -07:00
Evan 0e1ba91862 Cline.ts - Factor out addToApiConversationHistory [4/8] (#2360)
* factor out getSavedClineMessages

* changeset

* refactor addToApiConversationHistory

* changeset
2025-03-21 13:04:17 -07:00
Evan 809557540b Cline.ts - Factor out get saved cline messages [3/8] (#2358)
* factor out getSavedClineMessages

* changeset
2025-03-20 18:23:23 -07:00
celestial-vault 754ee766d1 delete extra changeset 2025-03-20 13:50:57 -07:00
celestial-vault d2e393e603 changeset 2025-03-20 13:48:45 -07:00
celestial-vault b0baefc3ba refactor out getSavedApiConversationHistory 2025-03-20 13:48:20 -07:00
celestial-vault 27571b4617 changeset 2025-03-20 13:41:26 -07:00
celestial-vault bf9164e1ae factor out saveApiConversationHistory 2025-03-20 13:40:52 -07:00
celestial-vault eea2c97835 changeset 2025-03-20 13:27:25 -07:00
celestial-vault cc67a77759 merge conflicts 2025-03-20 13:25:44 -07:00
celestial-vault 3d33834ce5 refactor out ensureTaskDirectoryExists 2025-03-20 13:21:58 -07:00
Evan 58305dd6b3 move global file names to constants (#2353) 2025-03-20 12:03:02 -07:00
Toshii 88c33ee96d encapsulate ContextManager (#2337)
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-03-20 02:50:40 -07:00
Dennis Bartlett 23a0d41646 Add saito-sv as CodeOwner (#2339)
* Add saito-sv as CodeOwner

* Create tricky-glasses-cheer.md
2025-03-19 22:14:45 -07:00
Dennis Bartlett 974f698e54 Revert "Added option for skipping diff animation. Useful when diff animation …" (#2341)
This reverts commit 12f16a47e3.
2025-03-19 20:12:07 -07:00
deepman 12f16a47e3 Added option for skipping diff animation. Useful when diff animation … (#1765)
* Added option for skipping diff animation. Useful when diff animation is slow.

* Update README.md

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

* fixed copyright symbol and default value

* Update README.md

---------

Co-authored-by: Mandeep Kumar <mandeep@deepcognition.ai>
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
Co-authored-by: Dennis Bartlett <bartlett.dc.1@gmail.com>
2025-03-19 16:34:51 -07:00
Lobay bc7d6f0c66 fix: improve Azure API detection to support custom endpoints (#2332)
* fix: improve Azure API detection to support custom endpoints

* chore: add changeset for Azure API version detection fix
2025-03-19 11:48:42 -07:00
Saoud Rizwan c8b446ab6c Refactor 2025-03-19 11:24:13 -07:00
Evan 2cc64973fb Size up the Task (#2246)
* Calculate and display total tasks and checkpoints size

* changeset

* Remove logs and add try/catch to file deletion

* call postMessage from HistoryView instead
2025-03-18 11:34:35 -07:00
Evan 599c3f6717 SuccessButton -> TW (#2289)
* migrate SuccessButton to Tailwind

* changeset
2025-03-17 23:54:25 -07:00
Saoud Rizwan 1c491bf719 Prepare for release 2025-03-17 21:04:51 -07:00
canvrno 10ac28b9d6 Issue with 'See More' button in task header (#2302)
* Fixes issue with 'See More' button in task header

* Removed requestAnimationFrame

* Cleanup

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-03-17 20:59:14 -07:00
omercelik 95b2e327a8 Added mistral-small-latest (Mistral Small 3.1) (#2300) 2025-03-17 17:38:37 -07:00
Evan 53d11c88bb Upgrade MCP SDK (#2301)
* upgrade mcp sdk

* changeset
2025-03-17 17:38:09 -07:00
canvrno 7ef0f6d1b6 Add --no-verify to checkpoint commits (#2298)
* initial

* Corrected options
2025-03-17 13:53:29 -07:00
37 changed files with 1186 additions and 429 deletions
+7
View File
@@ -0,0 +1,7 @@
---
"claude-dev": minor
---
Add Bedrock prompt caching support (optional).
This feature protected under checkbox because it is not yet rolled out to everyone, and if you will try to send cache headers, and its not enabled for you, you will get error.
@@ -2,4 +2,4 @@
"claude-dev": patch
---
Test Patch
refactor
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
refactor
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
refactor
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
fixes an issue with Azure API version detection in the OpenAI provider
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Add size calculation to "Delete all Tasks" button
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
refactor
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
Test Minor
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
SuccessButton to Tailwind
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
refactor
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Add saito-sv as CodeOwner
+1 -1
View File
@@ -1 +1 @@
* @saoudrizwan @ocasta181 @NightTrek @pashpashpash @dcbartlett
* @saoudrizwan @ocasta181 @NightTrek @pashpashpash @dcbartlett @saito-sv
@@ -26,65 +26,39 @@ import sys
CHANGELOG_PATH = os.environ.get("CHANGELOG_PATH", "CHANGELOG.md")
VERSION = os.environ['VERSION']
PREV_VERSION = os.environ.get("PREV_VERSION", "")
NEW_CONTENT = os.environ.get("NEW_CONTENT", "")
# Find the section for the specified version
version_index = -1
version_pattern = f"## {VERSION}\n"
bracketed_version_pattern = f"## [{VERSION}]\n"
header_end_index = 0
print(f"latest version: {VERSION}")
def overwrite_changelog_section(changelog_text: str, new_content: str):
# Find the section for the specified version
version_pattern = f"## {VERSION}\n"
bracketed_version_pattern = f"## [{VERSION}]\n"
prev_version_pattern = f"## [{PREV_VERSION}]\n"
print(f"latest version: {VERSION}")
print(f"prev_version: {PREV_VERSION}")
def fetch_changelog_header(changelog_text: str):
global version_pattern, version_index, bracketed_version_pattern, header_end_index
header = ""
print(f"Starting fetch_changelog_header")
# Try both unbracketed and bracketed version patterns
version_index = changelog_text.find(version_pattern)
if version_index == -1:
print("Version not found, trying bracketed version pattern")
version_index = changelog_text.find(bracketed_version_pattern)
if version_index == -1:
print("Bracketed version not found, adding new version header")
# If version not found, add it at the top (after the first line)
first_newline = changelog_text.find('\n')
print(f"First newline index: {first_newline}")
if first_newline == -1:
print("No newline found, prepending new version header")
# If no newline found, just prepend
header = f"## [{VERSION}]\n\n"
header = f"{changelog_text[:first_newline + 1]}\n## [{VERSION}]\n\n"
return f"## [{VERSION}]\n\n{changelog_text}"
return f"{changelog_text[:first_newline + 1]}## [{VERSION}]\n\n{changelog_text[first_newline + 1:]}"
else:
# Using bracketed version
version_pattern = bracketed_version_pattern
header = changelog_text[:version_index]
else:
header = changelog_text[:version_index]
header_end_index = len(header)
return header
def generate_changelog_section(changelog_text: str, new_content: str):
global version_pattern, version_index, header_end_index
print(f"Starting generate_changelog_section")
print(f"Version index: {version_index}")
print(f"Version pattern: {version_pattern} {len(version_pattern)}")
print(f"Header end index: {header_end_index}")
prev_version_pattern = "## ["
prev_version_index = changelog_text[header_end_index:].find(prev_version_pattern)
print(f"Previous version index: {prev_version_index}")
notes_start_index = version_index + len(version_pattern)
notes_end_index = changelog_text.find(prev_version_pattern, notes_start_index) if PREV_VERSION and prev_version_pattern in changelog_text else len(changelog_text)
if new_content:
print("Detected new content, overwriting existing changeset")
return f"{new_content}\n" + changelog_text[prev_version_index:]
return changelog_text[:notes_start_index] + f"{new_content}\n" + changelog_text[notes_end_index:]
else:
print("No new content provided, reformatting existing changeset")
changeset_lines = changelog_text[header_end_index:prev_version_index].split("\n")
print(f"Changeset lines: {changeset_lines}")
changeset_lines = changelog_text[notes_start_index:notes_end_index].split("\n")
# Ensure we have at least 2 lines before removing them
if len(changeset_lines) < 2:
print("Warning: Changeset content has fewer than 2 lines")
@@ -92,22 +66,11 @@ def generate_changelog_section(changelog_text: str, new_content: str):
else:
# Remove the first two lines from the regular changeset format, ex: \n### Patch Changes
parsed_lines = "\n".join(changeset_lines[2:])
# Reconstruct the changelog with the new content
updated_changelog = parsed_lines + changelog_text[prev_version_index:]
updated_changelog = changelog_text[:notes_start_index] + parsed_lines + changelog_text[notes_end_index:]
# Ensure version number is bracketed
updated_changelog = updated_changelog.replace(f"## {VERSION}", f"## [{VERSION}]")
return updated_changelog
def overwrite_changelog_section(changelog_text: str, new_content: str):
print(f"Starting overwrite_changelog_section")
header = fetch_changelog_header(changelog_text)
body = generate_changelog_section(changelog_text, new_content)
print(f"Header: {header}")
return header + body
try:
print(f"Reading changelog from: {CHANGELOG_PATH}")
with open(CHANGELOG_PATH, 'r') as f:
@@ -122,7 +85,7 @@ try:
print("New changelog content:")
print("----------------------------------------------------------------------------------")
# print(new_changelog)
print(new_changelog)
print("----------------------------------------------------------------------------------")
print(f"Writing updated changelog back to: {CHANGELOG_PATH}")
@@ -18,7 +18,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
with:
fetch-depth: 0
ref: ${{ github.event.pull_request.head.sha }}
+26 -36
View File
@@ -6,7 +6,7 @@ on:
release-type:
description: "Choose release type (release or pre-release)"
required: true
default: "pre-release"
default: "release"
type: choice
options:
- pre-release
@@ -19,11 +19,11 @@ permissions:
pull-requests: write
jobs:
# test:
# uses: ./.github/workflows/test.yml
test:
uses: ./.github/workflows/test.yml
publish:
# needs: test
needs: test
name: Publish Extension
runs-on: ubuntu-latest
environment: publish
@@ -75,8 +75,8 @@ jobs:
VERSION=v${{ steps.get_version.outputs.version }}
echo "tag=$VERSION" >> $GITHUB_OUTPUT
echo "Tagging with $VERSION"
# git tag "$VERSION"
# git push origin "$VERSION"
git tag "$VERSION"
git push origin "$VERSION"
- name: Package and Publish Extension
env:
@@ -87,39 +87,29 @@ jobs:
vsce package --out "cline-${{ steps.get_version.outputs.version }}.vsix"
if [ "${{ github.event.inputs.release-type }}" = "pre-release" ]; then
# npm run publish:marketplace:prerelease
npm run publish:marketplace:prerelease
echo "Successfully published pre-release version ${{ steps.get_version.outputs.version }} to VS Code Marketplace and Open VSX Registry"
else
# npm run publish:marketplace
npm run publish:marketplace
echo "Successfully published release version ${{ steps.get_version.outputs.version }} to VS Code Marketplace and Open VSX Registry"
fi
# - name: Create Changelog Entry
# id: changesets
# uses: changesets/action@v1
# env:
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Create Changelog Entry
id: changesets
env:
VERSION: ${{ steps.get_version.outputs.version }}
run: |
python .github/scripts/overwrite_changeset_changelog.py
- name: Get Changelog Entry
id: changelog
uses: mindsers/changelog-reader-action@v2
with:
version: ${{ steps.get_version.outputs.version }}
# - name: Create GitHub Release
# uses: softprops/action-gh-release@v1
# - name: Get Changelog Entry
# id: changelog
# uses: mindsers/changelog-reader-action@v2
# with:
# tag_name: ${{ steps.create_tag.outputs.tag }}
# files: "*.vsix"
# # body: ${{ steps.fetch-changelog.outputs.content }}
# generate_release_notes: true
# prerelease: ${{ github.event.inputs.release-type == 'pre-release' }}
# env:
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# # This expects a standard Keep a Changelog format
# # "latest" means it will read whichever is the most recent version
# # set in "## [1.2.3] - 2025-01-28" style
# version: latest
- name: Create GitHub Release
uses: softprops/action-gh-release@v1
with:
tag_name: ${{ steps.create_tag.outputs.tag }}
files: "*.vsix"
# body: ${{ steps.changelog.outputs.content }}
generate_release_notes: true
prerelease: ${{ github.event.inputs.release-type == 'pre-release' }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+2 -3
View File
@@ -2,9 +2,8 @@
## [3.7.1]
- Tests
- Tests
- Tests
- Fix issue with 'See more' button in task header not showing when starting new tasks
- Fix issue with checkpoints using local git commit hooks
## [3.7.0]
+787 -62
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -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.7.0",
"version": "3.7.1",
"icon": "assets/icons/icon.png",
"galleryBanner": {
"color": "#617A91",
@@ -271,7 +271,7 @@
"@google-cloud/vertexai": "^1.9.3",
"@google/generative-ai": "^0.18.0",
"@mistralai/mistralai": "^1.5.0",
"@modelcontextprotocol/sdk": "^1.0.1",
"@modelcontextprotocol/sdk": "^1.7.0",
"@types/clone-deep": "^4.0.4",
"@types/get-folder-size": "^3.0.4",
"@types/pdf-parse": "^1.1.4",
+2 -1
View File
@@ -15,7 +15,8 @@ export class OpenAiHandler implements ApiHandler {
constructor(options: ApiHandlerOptions) {
this.options = options
// Azure API shape slightly differs from the core API shape: https://github.com/openai/openai-node?tab=readme-ov-file#microsoft-azure-openai
if (this.options.openAiBaseUrl?.toLowerCase().includes("azure.com")) {
// Use azureApiVersion to determine if this is an Azure endpoint, since the URL may not always contain 'azure.com'
if (this.options.azureApiVersion || this.options.openAiBaseUrl?.toLowerCase().includes("azure.com")) {
this.client = new AzureOpenAI({
baseURL: this.options.openAiBaseUrl,
apiKey: this.options.openAiApiKey,
+47 -80
View File
@@ -57,14 +57,21 @@ import { ClineIgnoreController, LOCK_TEXT_SYMBOL } from "./ignore/ClineIgnoreCon
import { parseMentions } from "./mentions"
import { formatResponse } from "./prompts/responses"
import { addUserInstructions, SYSTEM_PROMPT } from "./prompts/system"
import { getNextTruncationRange, getTruncatedMessages } from "./sliding-window"
import { ContextManager } from "./context-management/ContextManager"
import { OpenAiHandler } from "../api/providers/openai"
import { ApiStream } from "../api/transform/stream"
import { ClineHandler } from "../api/providers/cline"
import { ClineProvider, GlobalFileNames } from "./webview/ClineProvider"
import { ClineProvider } from "./webview/ClineProvider"
import { DEFAULT_LANGUAGE_SETTINGS, getLanguageKey, LanguageDisplay, LanguageKey } from "../shared/Languages"
import { telemetryService } from "../services/telemetry/TelemetryService"
import pTimeout from "p-timeout"
import { GlobalFileNames } from "../global-constants"
import {
ensureTaskDirectoryExists,
getSavedApiConversationHistory,
getSavedClineMessages,
saveApiConversationHistory,
} from "./messages-io"
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
@@ -78,6 +85,7 @@ export class Cline {
private terminalManager: TerminalManager
private urlContentFetcher: UrlContentFetcher
browserSession: BrowserSession
contextManager: ContextManager
private didEditFile: boolean = false
customInstructions?: string
autoApprovalSettings: AutoApprovalSettings
@@ -139,6 +147,7 @@ export class Cline {
this.terminalManager = new TerminalManager()
this.urlContentFetcher = new UrlContentFetcher(provider.context)
this.browserSession = new BrowserSession(provider.context, browserSettings)
this.contextManager = new ContextManager()
this.diffViewProvider = new DiffViewProvider(cwd)
this.customInstructions = customInstructions
this.autoApprovalSettings = autoApprovalSettings
@@ -173,63 +182,6 @@ export class Cline {
this.chatSettings = chatSettings
}
// Storing task to disk for history
private async ensureTaskDirectoryExists(): Promise<string> {
const globalStoragePath = this.providerRef.deref()?.context.globalStorageUri.fsPath
if (!globalStoragePath) {
throw new Error("Global storage uri is invalid")
}
const taskDir = path.join(globalStoragePath, "tasks", this.taskId)
await fs.mkdir(taskDir, { recursive: true })
return taskDir
}
private async getSavedApiConversationHistory(): Promise<Anthropic.MessageParam[]> {
const filePath = path.join(await this.ensureTaskDirectoryExists(), GlobalFileNames.apiConversationHistory)
const fileExists = await fileExistsAtPath(filePath)
if (fileExists) {
return JSON.parse(await fs.readFile(filePath, "utf8"))
}
return []
}
private async addToApiConversationHistory(message: Anthropic.MessageParam) {
this.apiConversationHistory.push(message)
await this.saveApiConversationHistory()
}
private async overwriteApiConversationHistory(newHistory: Anthropic.MessageParam[]) {
this.apiConversationHistory = newHistory
await this.saveApiConversationHistory()
}
private async saveApiConversationHistory() {
try {
const filePath = path.join(await this.ensureTaskDirectoryExists(), GlobalFileNames.apiConversationHistory)
await fs.writeFile(filePath, JSON.stringify(this.apiConversationHistory))
} catch (error) {
// in the off chance this fails, we don't want to stop the task
console.error("Failed to save API conversation history:", error)
}
}
private async getSavedClineMessages(): Promise<ClineMessage[]> {
const filePath = path.join(await this.ensureTaskDirectoryExists(), GlobalFileNames.uiMessages)
if (await fileExistsAtPath(filePath)) {
return JSON.parse(await fs.readFile(filePath, "utf8"))
} else {
// check old location
const oldPath = path.join(await this.ensureTaskDirectoryExists(), "claude_messages.json")
if (await fileExistsAtPath(oldPath)) {
const data = JSON.parse(await fs.readFile(oldPath, "utf8"))
await fs.unlink(oldPath) // remove old file
return data
}
}
return []
}
private async addToClineMessages(message: ClineMessage) {
// these values allow us to reconstruct the conversation history at the time this cline message was created
// it's important that apiConversationHistory is initialized before we add cline messages
@@ -246,7 +198,9 @@ export class Cline {
private async saveClineMessages() {
try {
const taskDir = await this.ensureTaskDirectoryExists()
const globalStoragePath = this.providerRef.deref()?.context.globalStorageUri.fsPath
const taskId = this.taskId
const taskDir = await ensureTaskDirectoryExists(globalStoragePath, taskId)
const filePath = path.join(taskDir, GlobalFileNames.uiMessages)
await fs.writeFile(filePath, JSON.stringify(this.clineMessages))
// combined as they are in ChatView
@@ -333,7 +287,11 @@ export class Cline {
0,
(message.conversationHistoryIndex || 0) + 2,
) // +1 since this index corresponds to the last user message, and another +1 since slice end index is exclusive
await this.overwriteApiConversationHistory(newConversationHistory)
this.apiConversationHistory = newConversationHistory
const globalStoragePath = this.providerRef.deref()?.context.globalStorageUri.fsPath
const taskId = this.taskId
await saveApiConversationHistory(globalStoragePath, taskId, this.apiConversationHistory)
// aggregate deleted api reqs info so we don't lose costs/tokens
const deletedMessages = this.clineMessages.slice(messageIndex + 1)
@@ -837,8 +795,9 @@ export class Cline {
// if (!doesShadowGitExist) {
// this.checkpointTrackerErrorMessage = "Checkpoints are only available for new tasks"
// }
const modifiedClineMessages = await this.getSavedClineMessages()
const globalStoragePath = this.providerRef.deref()?.context.globalStorageUri.fsPath
const taskId = this.taskId
const modifiedClineMessages = await getSavedClineMessages(globalStoragePath, taskId)
// Remove any resume messages that may have been added before
const lastRelevantMessageIndex = findLastIndex(
@@ -863,11 +822,12 @@ export class Cline {
}
await this.overwriteClineMessages(modifiedClineMessages)
this.clineMessages = await this.getSavedClineMessages()
this.clineMessages = await getSavedClineMessages(globalStoragePath, taskId)
// Now present the cline messages to the user and ask if they want to resume (NOTE: we ran into a bug before where the apiconversationhistory wouldnt be initialized when opening a old task, and it was because we were waiting for resume)
// This is important in case the user deletes messages without resuming the task first
this.apiConversationHistory = await this.getSavedApiConversationHistory()
this.apiConversationHistory = await getSavedApiConversationHistory(globalStoragePath, taskId)
const lastClineMessage = this.clineMessages
.slice()
@@ -904,7 +864,10 @@ export class Cline {
// need to make sure that the api conversation history can be resumed by the api, even if it goes out of sync with cline messages
const existingApiConversationHistory: Anthropic.Messages.MessageParam[] = await this.getSavedApiConversationHistory()
const existingApiConversationHistory: Anthropic.Messages.MessageParam[] = await getSavedApiConversationHistory(
globalStoragePath,
taskId,
)
// if the last message is an assistant message, we need to check if there's tool use since every tool use has to have a tool response
// if there's no tool use and only a text block, then we can just add a user message
@@ -1039,7 +1002,9 @@ export class Cline {
newUserContent.push(...formatResponse.imageBlocks(responseImages))
}
await this.overwriteApiConversationHistory(modifiedApiConversationHistory)
this.apiConversationHistory = modifiedApiConversationHistory
await saveApiConversationHistory(globalStoragePath, taskId, this.apiConversationHistory)
await this.initiateTaskLoop(newUserContent, false)
}
@@ -1380,19 +1345,18 @@ export class Cline {
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
this.conversationHistoryDeletedRange = getNextTruncationRange(
this.conversationHistoryDeletedRange = this.contextManager.getNextTruncationRange(
this.apiConversationHistory,
this.conversationHistoryDeletedRange,
keep,
)
await this.saveClineMessages() // saves task history item which we use to keep track of conversation history deleted range
// await this.overwriteApiConversationHistory(truncatedMessages)
}
}
}
// conversationHistoryDeletedRange is updated only when we're close to hitting the context window, so we don't continuously break the prompt cache
const truncatedConversationHistory = getTruncatedMessages(
const truncatedConversationHistory = this.contextManager.getTruncatedMessages(
this.apiConversationHistory,
this.conversationHistoryDeletedRange,
)
@@ -3155,10 +3119,13 @@ export class Cline {
// add environment details as its own text block, separate from tool results
userContent.push({ type: "text", text: environmentDetails })
await this.addToApiConversationHistory({
this.apiConversationHistory.push({
role: "user",
content: userContent,
})
const globalStoragePath = this.providerRef.deref()?.context.globalStorageUri.fsPath
const taskId = this.taskId
await saveApiConversationHistory(globalStoragePath, taskId, this.apiConversationHistory)
telemetryService.captureConversationTurnEvent(this.taskId, this.apiProvider, this.api.getModel().id, "user")
@@ -3217,7 +3184,8 @@ export class Cline {
}
// Let assistant know their response was interrupted for when task is resumed
await this.addToApiConversationHistory({
this.apiConversationHistory.push({
role: "assistant",
content: [
{
@@ -3232,6 +3200,7 @@ export class Cline {
},
],
})
await saveApiConversationHistory(globalStoragePath, taskId, this.apiConversationHistory)
// update api_req_started to have cancelled and cost, so that we can display the cost of the partial stream
updateApiReqMsg(cancelReason, streamingFailedMessage)
@@ -3383,10 +3352,11 @@ export class Cline {
if (assistantMessage.length > 0) {
telemetryService.captureConversationTurnEvent(this.taskId, this.apiProvider, this.api.getModel().id, "assistant")
await this.addToApiConversationHistory({
this.apiConversationHistory.push({
role: "assistant",
content: [{ type: "text", text: assistantMessage }],
})
await saveApiConversationHistory(globalStoragePath, taskId, this.apiConversationHistory)
// NOTE: this comment is here for future reference - this was a workaround for userMessageContent not getting set to true. It was due to it not recursively calling for partial blocks when didRejectTool, so it would get stuck waiting for a partial block to complete before it could continue.
// in case the content blocks finished
@@ -3418,15 +3388,12 @@ export class Cline {
"error",
"Unexpected API Response: The language model did not provide any assistant messages. This may indicate an issue with the API or the model's output.",
)
await this.addToApiConversationHistory({
this.apiConversationHistory.push({
role: "assistant",
content: [
{
type: "text",
text: "Failure: I did not provide a response.",
},
],
content: [{ type: "text", text: "Failure: I did not provide a response." }],
})
await saveApiConversationHistory(globalStoragePath, taskId, this.apiConversationHistory)
}
return didEndLoop // will always be false for now
@@ -0,0 +1,47 @@
import { Anthropic } from "@anthropic-ai/sdk"
export class ContextManager {
getNextTruncationRange(
messages: Anthropic.Messages.MessageParam[],
currentDeletedRange: [number, number] | undefined = undefined,
keep: "half" | "quarter" = "half",
): [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 user-assistant pairs
messagesToRemove = Math.floor((messages.length - startOfRest) / 4) * 2 // Keep even number
} else {
// Remove 3/4 of user-assistant pairs
messagesToRemove = Math.floor((messages.length - startOfRest) / 8) * 3 * 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 (messages[rangeEndIndex].role !== "user") {
rangeEndIndex -= 1
}
// this is an inclusive range that will be removed from the conversation history
return [rangeStartIndex, rangeEndIndex]
}
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)]
}
}
+60
View File
@@ -0,0 +1,60 @@
import fs from "fs/promises"
import path from "path"
import { GlobalFileNames } from "../global-constants"
import Anthropic from "@anthropic-ai/sdk"
import { fileExistsAtPath } from "../utils/fs"
import { ClineMessage } from "../shared/ExtensionMessage"
export async function ensureTaskDirectoryExists(globalStoragePath: string | undefined, taskId: string): Promise<string> {
if (!globalStoragePath) {
throw new Error("Global storage uri is invalid")
}
const taskDir = path.join(globalStoragePath, "tasks", taskId)
await fs.mkdir(taskDir, { recursive: true })
return taskDir
}
export async function saveApiConversationHistory(
globalStoragePath: string | undefined,
taskId: string,
apiConversationHistory: Anthropic.MessageParam[],
) {
try {
const filePath = path.join(
await ensureTaskDirectoryExists(globalStoragePath, taskId),
GlobalFileNames.apiConversationHistory,
)
await fs.writeFile(filePath, JSON.stringify(apiConversationHistory))
} catch (error) {
// in the off chance this fails, we don't want to stop the task
console.error("Failed to save API conversation history:", error)
}
}
export async function getSavedApiConversationHistory(
globalStoragePath: string | undefined,
taskId: string,
): Promise<Anthropic.MessageParam[]> {
const filePath = path.join(await ensureTaskDirectoryExists(globalStoragePath, taskId), GlobalFileNames.apiConversationHistory)
const fileExists = await fileExistsAtPath(filePath)
if (fileExists) {
return JSON.parse(await fs.readFile(filePath, "utf8"))
}
return []
}
export async function getSavedClineMessages(globalStoragePath: string | undefined, taskId: string): Promise<ClineMessage[]> {
const filePath = path.join(await ensureTaskDirectoryExists(globalStoragePath, taskId), GlobalFileNames.uiMessages)
if (await fileExistsAtPath(filePath)) {
return JSON.parse(await fs.readFile(filePath, "utf8"))
} else {
// check old location
const oldPath = path.join(await ensureTaskDirectoryExists(globalStoragePath, taskId), "claude_messages.json")
if (await fileExistsAtPath(oldPath)) {
const data = JSON.parse(await fs.readFile(oldPath, "utf8"))
await fs.unlink(oldPath) // remove old file
return data
}
}
return []
}
-97
View File
@@ -1,97 +0,0 @@
import { Anthropic } from "@anthropic-ai/sdk"
/*
We can't implement a dynamically updating sliding window as it would break prompt cache
every time. To maintain the benefits of caching, we need to keep conversation history
static. This operation should be performed as infrequently as possible. If a user reaches
a 200k context, we can assume that the first half is likely irrelevant to their current task.
Therefore, this function should only be called when absolutely necessary to fit within
context limits, not as a continuous process.
*/
// export function truncateHalfConversation(
// messages: Anthropic.Messages.MessageParam[],
// ): Anthropic.Messages.MessageParam[] {
// // API expects messages to be in user-assistant order, and tool use messages must be followed by tool results. We need to maintain this structure while truncating.
// // Always keep the first Task message (this includes the project's file structure in environment_details)
// const truncatedMessages = [messages[0]]
// // Remove half of user-assistant pairs
// const messagesToRemove = Math.floor(messages.length / 4) * 2 // has to be even number
// const remainingMessages = messages.slice(messagesToRemove + 1) // has to start with assistant message since tool result cannot follow assistant message with no tool use
// truncatedMessages.push(...remainingMessages)
// return truncatedMessages
// }
/*
getNextTruncationRange: Calculates the next range of messages to be "deleted"
- Takes the full messages array and optional current deleted range
- Always preserves the first message (task message)
- Removes 1/2 of remaining messages (rounded down to even number) after current deleted range
- Returns [startIndex, endIndex] representing inclusive range to delete
getTruncatedMessages: Constructs the truncated array using the deleted range
- Takes full messages array and optional deleted range
- Returns new array with messages in deleted range removed
- Preserves order and structure of remaining messages
The range is represented as [startIndex, endIndex] where both indices are inclusive
The functions maintain the original array integrity while allowing progressive truncation
through the deletedRange parameter
Usage example:
const messages = [user1, assistant1, user2, assistant2, user3, assistant3];
let deletedRange = getNextTruncationRange(messages); // [1,2] (assistant1,user2)
let truncated = getTruncatedMessages(messages, deletedRange);
// [user1, assistant2, user3, assistant3]
deletedRange = getNextTruncationRange(messages, deletedRange); // [2,3] (assistant2,user3)
truncated = getTruncatedMessages(messages, deletedRange);
// [user1, assistant3]
*/
export function getNextTruncationRange(
messages: Anthropic.Messages.MessageParam[],
currentDeletedRange: [number, number] | undefined = undefined,
keep: "half" | "quarter" = "half",
): [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 user-assistant pairs
messagesToRemove = Math.floor((messages.length - startOfRest) / 4) * 2 // Keep even number
} else {
// Remove 3/4 of user-assistant pairs
messagesToRemove = Math.floor((messages.length - startOfRest) / 8) * 3 * 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 (messages[rangeEndIndex].role !== "user") {
rangeEndIndex -= 1
}
// this is an inclusive range that will be removed from the conversation history
return [rangeStartIndex, rangeEndIndex]
}
export function getTruncatedMessages(
messages: Anthropic.Messages.MessageParam[],
deletedRange: [number, number] | undefined,
): Anthropic.Messages.MessageParam[] {
if (!deletedRange) {
return messages
}
const [start, end] = deletedRange
// the range is inclusive - both start and end indices and everything in between will be removed from the final result.
// NOTE: if you try to console log these, don't forget that logging a reference to an array may not provide the same result as logging a slice() snapshot of that array at that exact moment. The following DOES in fact include the latest assistant message.
return [...messages.slice(0, start), ...messages.slice(end + 1)]
}
+54 -43
View File
@@ -36,6 +36,8 @@ import { telemetryService } from "../../services/telemetry/TelemetryService"
import { TelemetrySetting } from "../../shared/TelemetrySetting"
import { cleanupLegacyCheckpoints } from "../../integrations/checkpoints/CheckpointMigration"
import CheckpointTracker from "../../integrations/checkpoints/CheckpointTracker"
import { getTotalTasksSize } from "../../utils/storage"
import { GlobalFileNames } from "../../global-constants"
/*
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
@@ -109,14 +111,6 @@ type GlobalStateKey =
| "thinkingBudgetTokens"
| "planActSeparateModelsSetting"
export const GlobalFileNames = {
apiConversationHistory: "api_conversation_history.json",
uiMessages: "ui_messages.json",
openRouterModels: "openrouter_models.json",
mcpSettings: "cline_mcp_settings.json",
clineRules: ".clinerules",
}
export class ClineProvider implements vscode.WebviewViewProvider {
public static readonly sideBarId = "claude-dev.SidebarProvider" // used in package.json as the view's id. This value cannot be changed due to how vscode caches views based on their id, and updating the id would break existing instances of the extension.
public static readonly tabPanelId = "claude-dev.TabPanelProvider"
@@ -816,6 +810,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
}
break
}
case "requestTotalTasksSize": {
this.refreshTotalTasksSize()
break
}
case "restartMcpServer": {
try {
await this.mcpHub?.restartConnection(message.text!)
@@ -917,6 +915,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
case "clearAllTaskHistory": {
await this.deleteAllTaskHistory()
await this.postStateToWebview()
this.refreshTotalTasksSize()
this.postMessageToWebview({ type: "relinquishControl" })
break
}
@@ -1788,46 +1787,56 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
// await this.postStateToWebview()
}
async refreshTotalTasksSize() {
getTotalTasksSize(this.context.globalStorageUri.fsPath)
.then((newTotalSize) => {
this.postMessageToWebview({
type: "totalTasksSize",
totalTasksSize: newTotalSize,
})
})
.catch((error) => {
console.error("Error calculating total tasks size:", error)
})
}
async deleteTaskWithId(id: string) {
console.info("deleteTaskWithId: ", id)
if (id === this.cline?.taskId) {
await this.clearTask()
console.debug("cleared task")
try {
if (id === this.cline?.taskId) {
await this.clearTask()
console.debug("cleared task")
}
const { taskDirPath, apiConversationHistoryFilePath, uiMessagesFilePath } = await this.getTaskWithId(id)
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)
}
await fs.rmdir(taskDirPath) // succeeds if the dir is empty
if (updatedTaskHistory.length === 0) {
await this.deleteAllTaskHistory()
}
} catch (error) {
console.debug(`Error deleting task:`, error)
}
const { taskDirPath, apiConversationHistoryFilePath, uiMessagesFilePath } = await this.getTaskWithId(id)
// Delete checkpoints
console.info("deleting checkpoints")
const taskHistory = ((await this.getGlobalState("taskHistory")) as HistoryItem[] | undefined) || []
const historyItem = taskHistory.find((item) => item.id === id)
//console.log("historyItem: ", historyItem)
// if (historyItem) {
// try {
// await CheckpointTracker.deleteCheckpoints(id, historyItem, this.context.globalStorageUri.fsPath)
// } catch (error) {
// console.error(`Failed to delete checkpoints for task ${id}:`, error)
// }
// }
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)
}
await fs.rmdir(taskDirPath) // succeeds if the dir is empty
this.refreshTotalTasksSize()
}
async deleteTaskFromState(id: string) {
@@ -1838,6 +1847,8 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
// Notify the webview that the task has been deleted
await this.postStateToWebview()
return updatedTaskHistory
}
async postStateToWebview() {
+8
View File
@@ -0,0 +1,8 @@
// NOTE: These are here temporarily until we find a better home for them
export const GlobalFileNames = {
apiConversationHistory: "api_conversation_history.json",
uiMessages: "ui_messages.json",
openRouterModels: "openrouter_models.json",
mcpSettings: "cline_mcp_settings.json",
clineRules: ".clinerules",
}
@@ -164,6 +164,7 @@ class CheckpointTracker {
console.info(`Creating checkpoint commit with message: ${commitMessage}`)
const result = await git.commit(commitMessage, {
"--allow-empty": null,
"--no-verify": null,
})
const commitHash = result.commit || ""
console.warn(`Checkpoint commit created.`)
+2 -1
View File
@@ -14,7 +14,7 @@ import * as fs from "fs/promises"
import * as path from "path"
import * as vscode from "vscode"
import { z } from "zod"
import { ClineProvider, GlobalFileNames } from "../../core/webview/ClineProvider"
import { ClineProvider } from "../../core/webview/ClineProvider"
import {
DEFAULT_MCP_TIMEOUT_SECONDS,
McpMode,
@@ -29,6 +29,7 @@ import {
import { fileExistsAtPath } from "../../utils/fs"
import { arePathsEqual } from "../../utils/path"
import { secondsToMs } from "../../utils/time"
import { GlobalFileNames } from "../../global-constants"
export type McpConnection = {
server: McpServer
client: Client
+13 -11
View File
@@ -34,6 +34,7 @@ export interface ExtensionMessage {
| "openGraphData"
| "isImageUrlResult"
| "didUpdateSettings"
| "totalTasksSize"
text?: string
action?:
| "chatButtonClicked"
@@ -69,6 +70,7 @@ export interface ExtensionMessage {
}
url?: string
isImage?: boolean
totalTasksSize?: number | null
}
export type Invoke = "sendMessage" | "primaryButtonClick" | "secondaryButtonClick"
@@ -78,27 +80,27 @@ export type Platform = "aix" | "darwin" | "freebsd" | "linux" | "openbsd" | "sun
export const DEFAULT_PLATFORM = "unknown"
export interface ExtensionState {
version: string
apiConfiguration?: ApiConfiguration
customInstructions?: string
uriScheme?: string
currentTaskItem?: HistoryItem
checkpointTrackerErrorMessage?: string
clineMessages: ClineMessage[]
taskHistory: HistoryItem[]
shouldShowAnnouncement: boolean
autoApprovalSettings: AutoApprovalSettings
browserSettings: BrowserSettings
chatSettings: ChatSettings
checkpointTrackerErrorMessage?: string
clineMessages: ClineMessage[]
currentTaskItem?: HistoryItem
customInstructions?: string
mcpMarketplaceEnabled?: boolean
planActSeparateModelsSetting: boolean
platform: Platform
shouldShowAnnouncement: boolean
taskHistory: HistoryItem[]
telemetrySetting: TelemetrySetting
uriScheme?: string
userInfo?: {
displayName: string | null
email: string | null
photoURL: string | null
}
mcpMarketplaceEnabled?: boolean
telemetrySetting: TelemetrySetting
planActSeparateModelsSetting: boolean
version: string
vscMachineId: string
}
+1
View File
@@ -62,6 +62,7 @@ export interface WebviewMessage {
| "updateSettings"
| "clearAllTaskHistory"
| "optionsResponse"
| "requestTotalTasksSize"
// | "relaunchChromeDebugMode"
text?: string
disabled?: boolean
+8
View File
@@ -1088,6 +1088,14 @@ export const mistralModels = {
inputPrice: 0.1,
outputPrice: 0.1,
},
"mistral-small-latest": {
maxTokens: 131_000,
contextWindow: 131_000,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0.1,
outputPrice: 0.3,
},
"mistral-small-2501": {
maxTokens: 32_000,
contextWindow: 32_000,
+21
View File
@@ -0,0 +1,21 @@
import path from "path"
import getFolderSize from "get-folder-size"
/**
* Gets the total size of tasks and checkpoints directories
* @param storagePath The base storage path (typically globalStorageUri.fsPath)
* @returns The total size in bytes, or null if calculation fails
*/
export async function getTotalTasksSize(storagePath: string): Promise<number | null> {
const tasksDir = path.join(storagePath, "tasks")
const checkpointsDir = path.join(storagePath, "checkpoints")
try {
const tasksSize = await getFolderSize.loose(tasksDir)
const checkpointsSize = await getFolderSize.loose(checkpointsDir)
return tasksSize + checkpointsSize
} catch (error) {
console.error("Failed to calculate total task size:", error)
return null
}
}
+2 -2
View File
@@ -22,13 +22,13 @@ import { CheckpointControls, CheckpointOverlay } from "../common/CheckpointContr
import CodeAccordian, { cleanPathPrefix } from "../common/CodeAccordian"
import CodeBlock, { CODE_BLOCK_BG_COLOR } from "../common/CodeBlock"
import MarkdownBlock from "../common/MarkdownBlock"
import SuccessButton from "../common/SuccessButton"
import Thumbnails from "../common/Thumbnails"
import McpResourceRow from "../mcp/McpResourceRow"
import McpToolRow from "../mcp/McpToolRow"
import CreditLimitError from "./CreditLimitError"
import { OptionsButtons } from "./OptionsButtons"
import { highlightMentions } from "./TaskHeader"
import SuccessButton from "../common/SuccessButton"
const ChatRowContainer = styled.div`
padding: 10px 6px 10px 15px;
@@ -1041,8 +1041,8 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi
})
}}
style={{
width: "100%",
cursor: seeNewChangesDisabled ? "wait" : "pointer",
width: "100%",
}}>
<i className="codicon codicon-new-file" style={{ marginRight: 6 }} />
See new changes
@@ -94,19 +94,20 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
}, [isTextExpanded, windowHeight])
useEffect(() => {
if (textRef.current && textContainerRef.current) {
if (isTaskExpanded && textRef.current && textContainerRef.current) {
let textContainerHeight = textContainerRef.current.clientHeight
if (!textContainerHeight) {
textContainerHeight = textContainerRef.current.getBoundingClientRect().height
}
const isOverflowing = textRef.current.scrollHeight > textContainerHeight
// necessary to show see more button again if user resizes window to expand and then back to collapse
if (!isOverflowing) {
setIsTextExpanded(false)
}
setShowSeeMore(isOverflowing)
}
}, [task.text, windowWidth])
}, [task.text, windowWidth, isTaskExpanded])
const isCostAvailable = useMemo(() => {
const openAiCompatHasPricing =
@@ -1,30 +1,25 @@
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import styled from "styled-components"
const StyledButton = styled(VSCodeButton)`
--success-button-bg: #176f2c;
--success-button-hover: #197f31;
--success-button-active: #156528;
interface SuccessButtonTWProps extends React.ComponentProps<typeof VSCodeButton> {}
background-color: var(--success-button-bg) !important;
border-color: var(--success-button-bg) !important;
color: #ffffff !important;
&:hover {
background-color: var(--success-button-hover) !important;
border-color: var(--success-button-hover) !important;
}
&:active {
background-color: var(--success-button-active) !important;
border-color: var(--success-button-active) !important;
}
`
interface SuccessButtonProps extends React.ComponentProps<typeof VSCodeButton> {}
const SuccessButton: React.FC<SuccessButtonProps> = (props) => {
return <StyledButton {...props} />
const SuccessButtonTW: React.FC<SuccessButtonTWProps> = (props) => {
return (
<VSCodeButton
{...props}
className={`
!bg-[#176f2c]
!border-[#176f2c]
!text-white
hover:!bg-[#197f31]
hover:!border-[#197f31]
active:!bg-[#156528]
active:!border-[#156528]
${props.className || ""}
`
.replace(/\s+/g, " ")
.trim()}
/>
)
}
export default SuccessButton
export default SuccessButtonTW
@@ -17,7 +17,7 @@ type HistoryViewProps = {
type SortOption = "newest" | "oldest" | "mostExpensive" | "mostTokens" | "mostRelevant"
const HistoryView = ({ onDone }: HistoryViewProps) => {
const { taskHistory } = useExtensionState()
const { taskHistory, totalTasksSize } = useExtensionState()
const [searchQuery, setSearchQuery] = useState("")
const [sortOption, setSortOption] = useState<SortOption>("newest")
const [lastNonRelevantSort, setLastNonRelevantSort] = useState<SortOption | null>("newest")
@@ -28,9 +28,13 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
setDeleteAllDisabled(false)
}
}, [])
useEvent("message", handleMessage)
// Request total tasks size when component mounts
useEffect(() => {
vscode.postMessage({ type: "requestTotalTasksSize" })
}, [])
useEffect(() => {
if (searchQuery && sortOption !== "mostRelevant" && !lastNonRelevantSort) {
setLastNonRelevantSort(sortOption)
@@ -471,7 +475,7 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
setDeleteAllDisabled(true)
vscode.postMessage({ type: "clearAllTaskHistory" })
}}>
Delete All History
Delete All History{totalTasksSize !== null ? ` (${formatSize(totalTasksSize)})` : ""}
</DangerButton>
</div>
</div>
@@ -20,6 +20,7 @@ interface ExtensionStateContextType extends ExtensionState {
mcpServers: McpServer[]
mcpMarketplaceCatalog: McpMarketplaceCatalog
filePaths: string[]
totalTasksSize: number | null
setApiConfiguration: (config: ApiConfiguration) => void
setCustomInstructions: (value?: string) => void
setTelemetrySetting: (value: TelemetrySetting) => void
@@ -52,6 +53,7 @@ export const ExtensionStateContextProvider: React.FC<{
const [openRouterModels, setOpenRouterModels] = useState<Record<string, ModelInfo>>({
[openRouterDefaultModelId]: openRouterDefaultModelInfo,
})
const [totalTasksSize, setTotalTasksSize] = useState<number | null>(null)
const [openAiModels, setOpenAiModels] = useState<string[]>([])
const [mcpServers, setMcpServers] = useState<McpServer[]>([])
@@ -137,6 +139,10 @@ export const ExtensionStateContextProvider: React.FC<{
}
break
}
case "totalTasksSize": {
setTotalTasksSize(message.totalTasksSize ?? null)
break
}
}
}, [])
@@ -156,6 +162,7 @@ export const ExtensionStateContextProvider: React.FC<{
mcpServers,
mcpMarketplaceCatalog,
filePaths,
totalTasksSize,
setApiConfiguration: (value) =>
setState((prevState) => ({
...prevState,