Compare commits

...
Author SHA1 Message Date
pashpashpash e5bcceccae moved request to summarize to recursivelymakeclinerequests instead of pushtoolresult 2025-07-31 17:49:56 -07:00
pashpashpash 78473d62fe sigh 2025-07-30 18:39:40 -07:00
pashpashpash aa44872c5b turning off old truncation for now.. 2025-07-29 22:15:36 -07:00
pashpashpash 128728440e context working + ui but still need to turn off old truncation system 2025-07-29 20:55:12 -07:00
pashpashpash 0eedf3b443 laying the groundwork for auto context management 2025-07-29 18:49:31 -07:00
Bee a28b995ab1 Fix styled-components prop warnings (#5181)
* Fix styled-components prop warnings

- Fix styled-components shouldForwardProp warnings by filtering non-DOM props
- Clean up unused imports in ChatTextArea and other components

* use mjs

* later

* remove unused imports
2025-07-28 11:19:32 -07:00
Bee a91878efc6 Fix: webview panel state change steals focus (#5193)
* Fix: webview panel state change steals focus

Fix webview visibility detection to check both visible and active states before taking focus. If a panel is visible but not active (focused), it should not steals editor focus.

Also removes unused import & add type imports

* add changeset
2025-07-28 10:59:00 -07:00
Jonathan BarazanyandJonathan Barazany 65c21e7b7d Bug fix: VSCode LM API token counting for Claude models (#5051)
* Improve token counting for Claude models in VSCode LM provider

- Reorder imports for better organization
- Add extractTextFromMessage helper method
- Add isClaudeModel detection method
- Use 4:1 character-to-token ratio for Claude models instead of VSCode's inaccurate counting
- Fallback to existing VSCode LM token counting for non-Claude models

* Update version to 3.18.3-r1 and refactor token calculation in VsCodeLmHandler

* 3.19.5-r1

* Add smart jobs impress changeset for VSCode LM API token counting fix

---------

Co-authored-by: Jonathan Barazany <jbarazany@microsoft.com>
2025-07-28 02:09:40 -07:00
Sarah Fortuneandellipsis-dev[bot] 56e388c90f Add a check to the proto scripts to warn about using int64 types. (#5174)
* Add a check to the proto scripts to warn about using int64 types.

Javascript cannot represent the full range of int64. So, when the protos are deserialized from JSON int64's are converted to strings. The typescript code is expecting a number and not a string, and this causes errors.

This was noticed before now because in the vscode protobus and hostbridge, the proto messages are not serialized and deserialized, they are just passed around as JS objects.

However, in IntelliJ the protos are serialized when they are sent through the ProtoBus. When the response messages contains and int64, it is deserialized to a string instead of a number for safety. This is causes parts of Cline to fail in IntelliJ, e.g. the task history view won't load because `Task.getTotalTasksSize()` returns a string when it is expecting a number.

* Make checkProtos shorter

* Update scripts/build-proto.mjs

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

* Update scripts/build-proto.mjs

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

* Update scripts/build-proto.mjs

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

* Fix typo

* Fix typo

* Fix bad merge

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-07-27 22:02:12 -04:00
pashpashpash 586d804a01 Revert "Spruce up HistoryPreview (#4101)" (#5207)
This reverts commit cdfffb8464.
2025-07-27 16:48:49 -07:00
Saoud Rizwan 85fbbcbe3f Revise contributing guidelines and fix PR template link to feature requests board (#5195)
* Revise contributing guidelines and fix PR template link to feature requests board

* Fix wording
2025-07-26 12:43:59 -07:00
ZhangZhiheng 25c5310383 Fix url no trim (#4641)
* fix: trim input value for URL fields in BaseUrlField and DebouncedTextField components (#4051)

* chore: add changeset
2025-07-26 12:23:09 -07:00
30 changed files with 700 additions and 220 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Fixed token counting when using VSCode LM API provider
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
fix: only focus on editor panel that is visible and active to stop input field stealing issue
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
trim input value for URL fields
+2 -2
View File
@@ -2,14 +2,14 @@
Thank you for contributing to Cline!
⚠️ Important: Before submitting this PR, please ensure you have:
- For feature requests: Created a discussion in our [Feature Requests board](https://github.com/cline/cline/discussions/categories/feature-requests) and received approval from core maintainers before implementation
- For feature requests: Created a discussion in our Feature Requests discussions board https://github.com/cline/cline/discussions/categories/feature-requests and received approval from core maintainers before implementation
- For all changes: Link the associated issue/discussion in the "Related Issue" section below
Limited exceptions:
Small bug fixes, typo corrections, minor wording improvements, or simple type fixes that don't change functionality may be submitted directly without prior discussion.
Why this requirement?
We deeply appreciate all community contributions - they are essential to Cline's success! To ensure the best use of everyone's time and maintain project direction, we use GitHub Discussions to gauge community interest and validate feature ideas before implementation begins. This helps us focus development efforts on features that will benefit the most users.
We deeply appreciate all community contributions - they are essential to Cline's success! To ensure the best use of everyone's time and maintain project direction, we use our Feature Requests discussions board to gauge community interest and validate feature ideas before implementation begins. This helps us focus development efforts on features that will benefit the most users.
-->
### Related Issue
+5 -8
View File
@@ -14,14 +14,11 @@ Bug reports help make Cline better for everyone! Before creating a new issue, pl
## Before Contributing
All contributions must begin with a GitHub Issue, unless the change is for small bug fixes, typo corrections, minor wording improvements, or simple type fixes that don't change functionality.
- **Check existing issues**: Search [GitHub Issues](https://github.com/cline/cline/issues).
- **Create an issue**: Use appropriate templates:
- **Contributions:** Use the "Contribution Request" template to propose what you'd like to work on.
- **Bugs:** "Bug Report" template for reporting issues.
- **Features:** "Detailed Feature Proposal" template for suggesting new features.
- **Wait for approval**: A core Cline contributor must approve your contribution request before you start implementation.
- **Claim issues**: Once approved, the issue will be assigned to you.
**For features and contributions**:
- First check the [Feature Requests discussions board](https://github.com/cline/cline/discussions/categories/feature-requests) for similar ideas
- If your idea is new, create a new feature request
- Wait for approval from core maintainers before starting implementation
- Once approved, feel free to begin working on a PR with the help of our community!
**PRs without approved issues may be closed.**
+1 -1
View File
@@ -345,7 +345,7 @@
"watch:esbuild": "node esbuild.mjs --watch",
"watch:tsc": "tsc --noEmit --watch --project tsconfig.json",
"package": "npm run check-types && npm run build:webview && npm run lint && node esbuild.mjs --production",
"protos": "node scripts/build-proto.mjs && node scripts/generate-protobus-setup.mjs && node scripts/generate-host-bridge-client.mjs",
"protos": "node scripts/build-proto.mjs",
"postprotos": "prettier src/shared/proto src/core/controller src/hosts/ webview-ui/src/services src/generated --write --log-level warn",
"clean": "rimraf dist dist-standalone webview-ui/build src/generated out/",
"compile-tests": "node ./scripts/build-tests.js",
+55 -3
View File
@@ -7,8 +7,10 @@ import { globby } from "globby"
import { createRequire } from "module"
import os from "os"
import * as path from "path"
import { fileURLToPath } from "url"
import { rmrf } from "./file-utils.mjs"
import { main as generateProtoBusSetup } from "./generate-protobus-setup.mjs"
import { main as generateHostBridgeClient } from "./generate-host-bridge-client.mjs"
import { loadProtoDescriptorSet } from "./proto-utils.mjs"
const require = createRequire(import.meta.url)
const PROTOC = path.join(require.resolve("grpc-tools"), "../bin/protoc")
@@ -34,9 +36,14 @@ const TS_PROTO_OPTIONS = [
]
async function main() {
console.log(chalk.bold.blue("Compiling Protocol Buffers..."))
await cleanup()
await compileProtos()
await checkProtos()
await generateProtoBusSetup()
await generateHostBridgeClient()
}
async function compileProtos() {
console.log(chalk.bold.blue("Compiling Protocol Buffers..."))
// Check for Apple Silicon compatibility before proceeding
checkAppleSiliconCompatibility()
@@ -180,6 +187,51 @@ function checkAppleSiliconCompatibility() {
}
}
const int64TypeNames = ["TYPE_INT64", "TYPE_UINT64", "TYPE_SINT64", "TYPE_FIXED64", "TYPE_SFIXED64"]
async function checkProtos() {
const proto = await loadProtoDescriptorSet()
const int64Fields = []
for (const [packageName, packageDef] of Object.entries(proto)) {
for (const [messageName, def] of Object.entries(packageDef)) {
// Skip service definitions
if (def && typeof def === "object" && "service" in def) {
continue
}
// Check message fields
if (def && def.type && def.type.field) {
for (const field of def.type.field) {
if (int64TypeNames.includes(field.type)) {
const name = `${packageName}.${messageName}.${field.name}`
int64Fields.push({
name: name,
type: field.type,
})
}
}
}
}
}
if (int64Fields.length > 0) {
console.log(chalk.yellow(`\nWarning: Found ${int64Fields.length} fields using 64-bit integer types`))
for (const field of int64Fields) {
const typeNames = {
TYPE_INT64: "int64",
TYPE_UINT64: "uint64",
TYPE_SINT64: "sint64",
TYPE_FIXED64: "fixed64",
TYPE_SFIXED64: "sfixed64",
}
log_verbose(chalk.yellow(` - ${field.name} (${typeNames[field.type]})`))
}
log_verbose(chalk.yellow("\nWARNING: 64-bit integer fields detected in proto definitions"))
log_verbose(chalk.yellow("JavaScript cannot safely represent integers larger than 2^53-1 (Number.MAX_SAFE_INTEGER)."))
log_verbose(chalk.yellow("Consider using string representation for large numbers or implementing BigInt support.\n"))
}
}
function log_verbose(s) {
if (process.argv.includes("-v") || process.argv.includes("--verbose")) {
console.log(s)
+8 -6
View File
@@ -15,7 +15,7 @@ const VSCODE_CLIENT_FILE = path.resolve("src/generated/hosts/vscode/hostbridge-g
/**
* Main function to generate the host bridge client
*/
async function main() {
export async function main() {
const { hostServices } = await loadServicesFromProtoDescriptor()
await generateTypesFile(hostServices)
@@ -234,8 +234,10 @@ const ${name}ServiceRegistry = createServiceRegistry("${name}")
${methods}`
}
// Run the main function
main().catch((error) => {
console.error(chalk.red("Error:"), error)
process.exit(1)
})
// Only run main if this script is executed directly
if (import.meta.url === `file://${process.argv[1]}`) {
main().catch((error) => {
console.error(chalk.red("Error:"), error)
process.exit(1)
})
}
+8 -2
View File
@@ -12,7 +12,7 @@ const STANDALONE_SERVER_SETUP_FILE = path.resolve("src/generated/hosts/standalon
const SCRIPT_NAME = path.relative(process.cwd(), fileURLToPath(import.meta.url))
async function main() {
export async function main() {
const { protobusServices } = await loadServicesFromProtoDescriptor()
await generateWebviewProtobusClients(protobusServices)
await generateVscodeServiceTypes(protobusServices)
@@ -205,4 +205,10 @@ function getDirName(serviceName) {
return domain.charAt(0).toLowerCase() + domain.slice(1)
}
main()
// Only run main if this script is executed directly
if (import.meta.url === `file://${process.argv[1]}`) {
main().catch((error) => {
console.error(chalk.red("Error:"), error)
process.exit(1)
})
}
+8 -4
View File
@@ -10,7 +10,7 @@ const DESCRIPTOR_SET = path.resolve("dist-standalone/proto/descriptor_set.pb")
const typeNameToFQN = new Map()
function addTypeNameToFqn(name, fqn) {
if (typeNameToFQN.has(name)) {
if (typeNameToFQN.has(name) && typeNameToFQN.get(name) !== fqn) {
throw new Error(`Proto type ${name} redefined (${fqn}).`)
}
typeNameToFQN.set(name, fqn)
@@ -23,11 +23,15 @@ export function getFqn(name) {
return typeNameToFQN.get(name)
}
export async function loadServicesFromProtoDescriptor() {
// Load service definitions from descriptor set
export async function loadProtoDescriptorSet() {
const descriptorBuffer = await fs.readFile(DESCRIPTOR_SET)
const packageDefinition = protoLoader.loadFileDescriptorSetFromBuffer(descriptorBuffer)
const proto = grpc.loadPackageDefinition(packageDefinition)
return grpc.loadPackageDefinition(packageDefinition)
}
export async function loadServicesFromProtoDescriptor() {
// Load service definitions from descriptor set
const proto = await loadProtoDescriptorSet()
// Extract host services and proto messages from the proto definition
const hostServices = {}
+29 -13
View File
@@ -1,13 +1,13 @@
import { Anthropic } from "@anthropic-ai/sdk"
import * as vscode from "vscode"
import { ApiHandler, SingleCompletionHandler } from "../"
import { calculateApiCostAnthropic } from "@utils/cost"
import { ApiStream } from "@api/transform/stream"
import { convertToVsCodeLmMessages } from "@api/transform/vscode-lm-format"
import { SELECTOR_SEPARATOR, stringifyVsCodeLmModelSelector } from "@shared/vsCodeSelectorUtils"
import { ModelInfo, openAiModelInfoSaneDefaults } from "@shared/api"
import type { LanguageModelChatSelector as LanguageModelChatSelectorFromTypes } from "./types"
import { SELECTOR_SEPARATOR, stringifyVsCodeLmModelSelector } from "@shared/vsCodeSelectorUtils"
import { calculateApiCostAnthropic } from "@utils/cost"
import * as vscode from "vscode"
import { ApiHandler, SingleCompletionHandler } from "../"
import { withRetry } from "../retry"
import type { LanguageModelChatSelector as LanguageModelChatSelectorFromTypes } from "./types"
interface VsCodeLmHandlerOptions {
vsCodeLmModelSelector?: any
@@ -237,7 +237,28 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
}
}
private extractTextFromMessage(message: vscode.LanguageModelChatMessage): string {
if (Array.isArray(message.content)) {
return message.content
.filter((part) => part instanceof vscode.LanguageModelTextPart)
.map((part) => (part as vscode.LanguageModelTextPart).value)
.join("")
}
return ""
}
private isClaudeModel(): boolean {
return this.client?.family?.startsWith("claude") || false
}
private async countTokens(text: string | vscode.LanguageModelChatMessage): Promise<number> {
// For Claude models, use character-to-token ratio instead of VSCode LM's inaccurate counting
if (this.isClaudeModel()) {
const textContent = typeof text === "string" ? text : this.extractTextFromMessage(text)
// Use 4 character-to-token ratio for Claude models
return Math.ceil(textContent.length / 4)
}
// Check for required dependencies
if (!this.client) {
console.warn("Cline <Language Model API>: No client available for token counting")
@@ -304,15 +325,10 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
}
}
private async calculateTotalInputTokens(
systemPrompt: string,
vsCodeLmMessages: vscode.LanguageModelChatMessage[],
): Promise<number> {
const systemTokens: number = await this.countTokens(systemPrompt)
private async calculateTotalInputTokens(vsCodeLmMessages: vscode.LanguageModelChatMessage[]): Promise<number> {
const messageTokens: number[] = await Promise.all(vsCodeLmMessages.map((msg) => this.countTokens(msg)))
return systemTokens + messageTokens.reduce((sum: number, tokens: number): number => sum + tokens, 0)
return messageTokens.reduce((sum: number, tokens: number): number => sum + tokens, 0)
}
private ensureCleanState(): void {
@@ -434,7 +450,7 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
this.currentRequestCancellation = new vscode.CancellationTokenSource()
// Calculate input tokens before starting the stream
const totalInputTokens: number = await this.calculateTotalInputTokens(systemPrompt, vsCodeLmMessages)
const totalInputTokens: number = await this.calculateTotalInputTokens(vsCodeLmMessages)
// Accumulate the text and count at the end of the stream to reduce token counting overhead.
let accumulatedText: string = ""
+1
View File
@@ -28,6 +28,7 @@ export const toolUseNames = [
"report_bug",
"new_rule",
"web_fetch",
"summarize_task",
] as const
// Converts array of tool call names into a union type ("execute_command" | "read_file" | ...)
@@ -8,6 +8,10 @@ import cloneDeep from "clone-deep"
import { ClineApiReqInfo, ClineMessage } from "@shared/ExtensionMessage"
import { ApiHandler } from "@api/index"
import { Anthropic } from "@anthropic-ai/sdk"
import { findLastIndex } from "@shared/array"
import { TaskState } from "../../task/TaskState"
import { summarizeTask } from "../../prompts/contextManagement"
enum EditType {
UNDEFINED = 0,
@@ -115,6 +119,7 @@ export class ContextManager {
conversationHistoryDeletedRange: [number, number] | undefined,
previousApiReqIndex: number,
taskDirectory: string,
taskState: TaskState,
) {
let updatedConversationHistoryDeletedRange = false
@@ -122,56 +127,11 @@ export class ContextManager {
if (previousApiReqIndex >= 0) {
const previousRequest = clineMessages[previousApiReqIndex]
if (previousRequest && previousRequest.text) {
const timestamp = previousRequest.ts
const { tokensIn, tokensOut, cacheWrites, cacheReads }: ClineApiReqInfo = JSON.parse(previousRequest.text)
const totalTokens = (tokensIn || 0) + (tokensOut || 0) + (cacheWrites || 0) + (cacheReads || 0)
const { maxAllowedSize } = getContextWindowInfo(api)
const { maxAllowedSize, contextWindow } = 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
const keep = totalTokens / 2 > maxAllowedSize ? "quarter" : "half"
// we later check how many chars we trim to determine if we should still truncate history
let [anyContextUpdates, uniqueFileReadIndices] = this.applyContextOptimizations(
apiConversationHistory,
conversationHistoryDeletedRange ? conversationHistoryDeletedRange[1] + 1 : 2,
timestamp,
)
let needToTruncate = true
if (anyContextUpdates) {
// determine whether we've saved enough chars to not truncate
const charactersSavedPercentage = this.calculateContextOptimizationMetrics(
apiConversationHistory,
conversationHistoryDeletedRange,
uniqueFileReadIndices,
)
if (charactersSavedPercentage >= 0.3) {
needToTruncate = false
}
}
if (needToTruncate) {
// go ahead with truncation
anyContextUpdates = this.applyStandardContextTruncationNoticeChange(timestamp) || anyContextUpdates
// 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
}
// if we alter the context history, save the updated version to disk
if (anyContextUpdates) {
await this.saveContextHistory(taskDirectory)
}
}
// Context window management logic can be added here if needed
}
}
@@ -848,4 +808,52 @@ export class ContextManager {
return percentCharactersSaved
}
/**
* Check if conversation should trigger automatic summarization based on token usage
*/
public shouldTriggerSummarization(
apiConversationHistory: Anthropic.Messages.MessageParam[],
clineMessages: ClineMessage[],
api: ApiHandler,
): {
totalTokens: number
maxAllowedSize: number
contextWindow: number
shouldSummarize: boolean
} {
let totalTokens = 0
let maxAllowedSize = 0
let contextWindow = 0
let shouldSummarize = false
const lastApiReqIndex = findLastIndex(clineMessages, (m) => m.say === "api_req_started")
if (lastApiReqIndex >= 0) {
const previousRequest = clineMessages[lastApiReqIndex]
if (previousRequest?.text) {
try {
const apiReqInfo: ClineApiReqInfo = JSON.parse(previousRequest.text)
const { tokensIn = 0, tokensOut = 0, cacheWrites = 0, cacheReads = 0 } = apiReqInfo
totalTokens = tokensIn + tokensOut + cacheWrites + cacheReads
const info = getContextWindowInfo(api)
maxAllowedSize = info.maxAllowedSize
contextWindow = info.contextWindow
shouldSummarize = totalTokens >= maxAllowedSize
} catch (error) {
console.error("Error parsing API request info for summarization threshold check:", error)
}
}
}
return {
totalTokens,
maxAllowedSize,
contextWindow,
shouldSummarize,
}
}
}
+70
View File
@@ -0,0 +1,70 @@
export const summarizeTask = (totalTokens: number, maxAllowedSize: number, contextWindow: number) =>
`<explicit_instructions type="summarize_task">
The current conversation is rapidly running out of context (${totalTokens}/${contextWindow} tokens used). Now, your urgent task is to create a detailed, comprehensive summary of the conversation so far, paying close attention to the user's explicit requests and your previous actions.
This summary should be thorough in capturing technical details, code patterns, and architectural decisions that would be essential for continuing development work without losing context.
Before providing your final summary, wrap your analysis in <thinking> tags to organize your thoughts and ensure you've covered all necessary points. In your analysis process:
1. Chronologically analyze each message and section of the conversation. For each section thoroughly identify:
- The user's explicit requests and intents
- Your approach to addressing the user's requests
- Key decisions, technical concepts and code patterns
- Specific details like file names, full code snippets, function signatures, file edits, etc
2. Double-check for technical accuracy and completeness, addressing each required element thoroughly.
Your summary should include the following sections:
1. Primary Request and Intent: Capture all of the user's explicit requests and intents in detail
2. Key Technical Concepts: List all important technical concepts, technologies, and frameworks discussed.
3. Files and Code Sections: Enumerate specific files and code sections examined, modified, or created. Pay special attention to the most recent messages and include full code snippets where applicable and include a summary of why this file read or edit is important.
4. Problem Solving: Document problems solved and any ongoing troubleshooting efforts.
5. Pending Tasks: Outline any pending tasks that you have explicitly been asked to work on.
6. Current Work: Describe in detail precisely what was being worked on immediately before this summary request, paying special attention to the most recent messages from both user and assistant. Include file names and code snippets where applicable.
7. Optional Next Step: List the next step that you will take that is related to the most recent work you were doing. IMPORTANT: ensure that this step is DIRECTLY in line with the user's explicit requests, and the task you were working on immediately before this summary request. If your last task was concluded, then only list next steps if they are explicitly in line with the users request. Do not start on tangential requests without confirming with the user first.
If there is a next step, include direct quotes from the most recent conversation showing exactly what task you were working on and where you left off. This should be verbatim to ensure there's no drift in task interpretation.
Usage:
<summarize_task>
<context>Your detailed summary</context>
</summarize_task>
Here's an example of how your output should be structured:
<example>
<thinking>
[Your thought process, ensuring all points are covered thoroughly and accurately]
</thinking>
<summarize_task>
<context>
1. Primary Request and Intent:
[Detailed description]
2. Key Technical Concepts:
- [Concept 1]
- [Concept 2]
- [...]
3. Files and Code Sections:
- [File Name 1]
- [Summary of why this file is important]
- [Summary of the changes made to this file, if any]
- [Important Code Snippet]
- [File Name 2]
- [Important Code Snippet]
- [...]
4. Problem Solving:
[Description of solved problems and ongoing troubleshooting]
5. Pending Tasks:
- [Task 1]
- [Task 2]
- [...]
6. Current Work:
[Precise description of current work]
7. Optional Next Step:
[Optional Next step to take]
</context>
</summarize_task>
</example>
</explicit_instructions>\n
`
export const continuationPrompt = (summaryText: string) => `
This session is being continued from a previous conversation that ran out of context. The conversation is summarized below:
${summaryText}.
Please continue the conversation from where we left it off without asking the user any further questions. Continue with the last task that you were asked to work on.
`
+1
View File
@@ -41,6 +41,7 @@ export class TaskState {
didRejectTool = false
didAlreadyUseTool = false
didEditFile: boolean = false
currentlySummarizing = false
// Consecutive request tracking
consecutiveAutoApprovedRequestsCount: number = 0
+113
View File
@@ -56,6 +56,9 @@ import { AutoApprove } from "./tools/autoApprove"
import { showNotificationForApprovalIfAutoApprovalEnabled } from "./utils"
import { ChatSettings } from "@/shared/ChatSettings"
import { summarizeTask, continuationPrompt } from "../prompts/contextManagement"
import { getContextWindowInfo } from "../context/context-management/context-window-utils"
export class ToolExecutor {
private autoApprover: AutoApprove
@@ -125,6 +128,7 @@ export class ToolExecutor {
private pushToolResult = (content: ToolResponse, block: ToolUse) => {
const isNextGenModel = isClaude4ModelFamily(this.api) || isGemini2dot5ModelFamily(this.api)
// 1. ADD NORMAL TOOL RESULT (existing logic)
if (typeof content === "string") {
const resultText = content || "(tool did not return anything)"
@@ -148,6 +152,7 @@ export class ToolExecutor {
} else {
this.taskState.userMessageContent.push(...content)
}
// once a tool result has been collected, ignore all other tool uses since we should only ever present one tool result per message
this.taskState.didAlreadyUseTool = true
}
@@ -188,6 +193,8 @@ export class ToolExecutor {
return `[${block.name} for creating a new task]`
case "condense":
return `[${block.name}]`
case "summarize_task":
return `[${block.name}]`
case "report_bug":
return `[${block.name}]`
case "new_rule":
@@ -1772,6 +1779,112 @@ export class ToolExecutor {
break
}
}
case "summarize_task": {
const context: string | undefined = block.params.context
try {
if (block.partial) {
// Show streaming summary generation in tool UI
const partialMessage = JSON.stringify({
tool: "summarizeTask",
content: this.removeClosingTag(block, "context", context),
} satisfies ClineSayTool)
await this.say("tool", partialMessage, undefined, undefined, block.partial)
break
} else {
if (!context) {
this.taskState.consecutiveMistakeCount++
this.pushToolResult(await this.sayAndCreateMissingParamError("summarize_task", "context"), block)
await this.saveCheckpoint()
break
}
this.taskState.consecutiveMistakeCount = 0
// Show completed summary in tool UI
const completeMessage = JSON.stringify({
tool: "summarizeTask",
content: context,
} satisfies ClineSayTool)
await this.say("tool", completeMessage, undefined, undefined, false)
// Auto-execute conversation replacement (no user approval needed)
// Clear the existing user message content that triggered the summary.
this.taskState.userMessageContent = []
this.pushToolResult(formatResponse.toolResult(continuationPrompt(context)), block)
// Replace conversation history (same logic as condense when approved)
const apiConversationHistory = this.messageStateHandler.getApiConversationHistory()
const keepStrategy = "none"
// Clear the context history at this point in time
this.taskState.conversationHistoryDeletedRange = this.contextManager.getNextTruncationRange(
apiConversationHistory,
this.taskState.conversationHistoryDeletedRange,
keepStrategy,
)
// Actually apply the truncation to the stored conversation history
const truncatedHistory = this.contextManager.getTruncatedMessages(
apiConversationHistory,
this.taskState.conversationHistoryDeletedRange,
)
await this.messageStateHandler.overwriteApiConversationHistory(truncatedHistory)
// Clear the deleted range now that it has been applied
this.taskState.conversationHistoryDeletedRange = undefined
await this.messageStateHandler.saveClineMessagesAndUpdateHistory()
console.log("TOOLEXECUTOR: ARE WE CURRENTLY SUMMARIZING?", this.taskState.currentlySummarizing)
// LOG: Debug what actually happens after summarization
console.log("=== SUMMARIZATION COMPLETE - DEBUG INFO ===")
console.log("Deleted Range:", this.taskState.conversationHistoryDeletedRange)
const currentApiHistory = this.messageStateHandler.getApiConversationHistory()
console.log("Total API messages after truncation:", currentApiHistory.length)
// Log the structure of what remains
currentApiHistory.forEach((msg, index) => {
const preview = Array.isArray(msg.content)
? msg.content
.map((block) => {
if (block.type === "text") {
return `text(${block.text.length} chars): "${block.text.substring(0, 100)}..."`
}
return `${block.type}`
})
.join(", ")
: typeof msg.content === "string"
? `string(${msg.content.length} chars): "${msg.content.substring(0, 100)}..."`
: "other"
console.log(`Message ${index} (${msg.role}): ${preview}`)
})
// Log what will be in the next API request
console.log("Current user message content that will be sent:")
this.taskState.userMessageContent.forEach((content, index) => {
if (content.type === "text") {
console.log(
`UserContent ${index}: text(${content.text.length} chars): "${content.text.substring(0, 200)}..."`,
)
} else {
console.log(`UserContent ${index}: ${content.type}`)
}
})
console.log("=== END SUMMARIZATION DEBUG ===")
}
await this.saveCheckpoint()
break
} catch (error) {
// Reset flag on error to prevent getting stuck
this.taskState.currentlySummarizing = false
await this.handleError("summarizing conversation", error, block)
await this.saveCheckpoint()
break
}
}
case "condense": {
const context: string | undefined = block.params.context
try {
+55
View File
@@ -82,6 +82,7 @@ import { isClaude4ModelFamily, isGemini2dot5ModelFamily } from "@utils/model-uti
import { isInTestMode } from "../../services/test/TestMode"
import { ensureLocalClineDirExists } from "../context/instructions/user-instructions/rule-helpers"
import { refreshWorkflowToggles } from "../context/instructions/user-instructions/workflows"
import { summarizeTask } from "../prompts/contextManagement"
import { MessageStateHandler } from "./message-state"
import { TaskState } from "./TaskState"
import { ToolExecutor } from "./ToolExecutor"
@@ -1744,6 +1745,7 @@ export class Task {
this.taskState.conversationHistoryDeletedRange,
previousApiReqIndex,
await ensureTaskDirectoryExists(this.getContext(), this.taskId),
this.taskState,
)
if (contextManagementMetadata.updatedConversationHistoryDeletedRange) {
@@ -1752,6 +1754,30 @@ export class Task {
// saves task history item which we use to keep track of conversation history deleted range
}
// LOG: Debug what messages are ACTUALLY sent to the API
console.log("=== ACTUAL API REQUEST MESSAGES ===")
console.log("System prompt length:", systemPrompt.length)
console.log("Total messages being sent to API:", contextManagementMetadata.truncatedConversationHistory.length)
console.log("Conversation history deleted range:", this.taskState.conversationHistoryDeletedRange)
contextManagementMetadata.truncatedConversationHistory.forEach((msg, index) => {
const preview = Array.isArray(msg.content)
? msg.content
.map((block) => {
if (block.type === "text") {
return `text(${block.text.length} chars): "${block.text.substring(0, 100)}..."`
}
return `${block.type}`
})
.join(", ")
: typeof msg.content === "string"
? `string(${msg.content.length} chars): "${msg.content.substring(0, 100)}..."`
: "other"
console.log(`API Message ${index} (${msg.role}): ${preview}`)
})
console.log("=== END API REQUEST MESSAGES ===")
let stream = this.api.createMessage(systemPrompt, contextManagementMetadata.truncatedConversationHistory)
const iterator = stream[Symbol.asyncIterator]()
@@ -2096,6 +2122,35 @@ export class Task {
// get previous api req's index to check token usage and determine if we need to truncate conversation history
const previousApiReqIndex = findLastIndex(this.messageStateHandler.getClineMessages(), (m) => m.say === "api_req_started")
console.log("ARE WE CURRENTLY SUMMARIZING?", this.taskState.currentlySummarizing)
// CHECK IF WE NEED SUMMARIZATION (only if not already summarizing)
if (this.taskState.currentlySummarizing) {
this.taskState.currentlySummarizing = false
} else {
const { totalTokens, maxAllowedSize, contextWindow, shouldSummarize } =
this.contextManager.shouldTriggerSummarization(
this.messageStateHandler.getApiConversationHistory(),
this.messageStateHandler.getClineMessages(),
this.api,
)
if (shouldSummarize) {
console.log("----------- WE SHOULD SUMMARIZE, SENDING SUMMARIZATION REQUEST -------------")
console.log(
`Total Tokens: ${totalTokens}, Max Allowed Size: ${maxAllowedSize}, Context Window: ${contextWindow}, Should Summarize: ${shouldSummarize}`,
)
console.log("----------- WE SHOULD SUMMARIZE, SENDING SUMMARIZATION REQUEST -------------")
// SET FLAG AND ADD SUMMARIZATION PROMPT
this.taskState.currentlySummarizing = true
userContent.push({
type: "text",
text: summarizeTask(totalTokens, maxAllowedSize, contextWindow),
})
}
}
// Save checkpoint if this is the first API request
const isFirstRequest = this.messageStateHandler.getClineMessages().filter((m) => m.say === "api_req_started").length === 0
+31 -34
View File
@@ -1,11 +1,11 @@
import { ExtensionMessage } from "@/shared/ExtensionMessage"
import { WebviewProviderType } from "@/shared/webview/types"
import { sendThemeEvent } from "@core/controller/ui/subscribeToTheme"
import { getTheme } from "@integrations/theme/getTheme"
import * as vscode from "vscode"
import { Uri } from "vscode"
import { WebviewProvider } from "@core/webview"
import { sendDidBecomeVisibleEvent } from "@core/controller/ui/subscribeToDidBecomeVisible"
import { sendThemeEvent } from "@core/controller/ui/subscribeToTheme"
import { WebviewProvider } from "@core/webview"
import { getTheme } from "@integrations/theme/getTheme"
import type { Uri } from "vscode"
import * as vscode from "vscode"
import type { ExtensionMessage } from "@/shared/ExtensionMessage"
import type { WebviewProviderType } from "@/shared/webview/types"
/*
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
@@ -68,8 +68,9 @@ export class VscodeWebviewProvider extends WebviewProvider implements vscode.Web
// WebviewView and WebviewPanel have all the same properties except for this visibility listener
// panel
webviewView.onDidChangeViewState(
async () => {
if (this.webview?.visible) {
async (e) => {
if (e?.webviewPanel?.visible && e.webviewPanel?.active) {
// Only send the event if the webview is active (focused)
await sendDidBecomeVisibleEvent(this.controller.id)
}
},
@@ -99,35 +100,31 @@ export class VscodeWebviewProvider extends WebviewProvider implements vscode.Web
this.disposables,
)
// // if the extension is starting a new session, clear previous task state
// this.clearTask()
{
// Listen for configuration changes
vscode.workspace.onDidChangeConfiguration(
async (e) => {
if (e && e.affectsConfiguration("workbench.colorTheme")) {
// Send theme update via gRPC subscription
const theme = await getTheme()
if (theme) {
await sendThemeEvent(JSON.stringify(theme))
}
// Listen for configuration changes
vscode.workspace.onDidChangeConfiguration(
async (e) => {
if (e && e.affectsConfiguration("workbench.colorTheme")) {
// Send theme update via gRPC subscription
const theme = await getTheme()
if (theme) {
await sendThemeEvent(JSON.stringify(theme))
}
if (e && e.affectsConfiguration("cline.mcpMarketplace.enabled")) {
// Update state when marketplace tab setting changes
await this.controller.postStateToWebview()
}
},
null,
this.disposables,
)
}
if (e && e.affectsConfiguration("cline.mcpMarketplace.enabled")) {
// Update state when marketplace tab setting changes
await this.controller.postStateToWebview()
}
},
null,
this.disposables,
)
// if the extension is starting a new session, clear previous task state
this.controller.clearTask()
// if the extension is starting a new session, clear previous task state
this.controller.clearTask()
this.outputChannel.appendLine("Webview view resolved")
this.outputChannel.appendLine("Webview view resolved")
// Title setting logic removed to allow VSCode to use the container title primarily.
}
// Title setting logic removed to allow VSCode to use the container title primarily.
}
/**
+1
View File
@@ -136,6 +136,7 @@ export interface ClineSayTool {
| "listCodeDefinitionNames"
| "searchFiles"
| "webFetch"
| "summarizeTask"
path?: string
diff?: string
content?: string
@@ -584,6 +584,81 @@ export const ChatRowContent = memo(
/>
</>
)
case "summarizeTask":
return (
<>
<div style={headerStyle}>
{toolIcon("book")}
<span style={{ fontWeight: "bold" }}>Cline is condensing the conversation:</span>
</div>
<div
style={{
borderRadius: 3,
backgroundColor: CODE_BLOCK_BG_COLOR,
overflow: "hidden",
border: "1px solid var(--vscode-editorGroup-border)",
}}>
<div
style={{
color: "var(--vscode-descriptionForeground)",
padding: "9px 10px",
cursor: "pointer",
userSelect: "none",
WebkitUserSelect: "none",
MozUserSelect: "none",
msUserSelect: "none",
}}
onClick={handleToggle}>
{isExpanded ? (
<div>
<div style={{ display: "flex", alignItems: "center", marginBottom: "8px" }}>
<span style={{ fontWeight: "bold", marginRight: "4px" }}>Summary:</span>
<div style={{ flexGrow: 1 }}></div>
<span
className="codicon codicon-chevron-up"
style={{
fontSize: 13.5,
margin: "1px 0",
}}></span>
</div>
<span
className="ph-no-capture"
style={{
whiteSpace: "pre-wrap",
wordBreak: "break-word",
overflowWrap: "anywhere",
}}>
{tool.content}
</span>
</div>
) : (
<div style={{ display: "flex", alignItems: "center" }}>
<span
className="ph-no-capture"
style={{
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
marginRight: "8px",
direction: "rtl",
textAlign: "left",
flex: 1,
}}>
{tool.content + "\u200E"}
</span>
<span
className="codicon codicon-chevron-down"
style={{
fontSize: 13.5,
margin: "1px 0",
flexShrink: 0,
}}></span>
</div>
)}
</div>
</div>
</>
)
case "webFetch":
return (
<>
+27 -22
View File
@@ -1,23 +1,34 @@
import { CHAT_CONSTANTS } from "@/components/chat/chat-view/constants"
const { MAX_IMAGES_AND_FILES_PER_MESSAGE } = CHAT_CONSTANTS
import type { ChatSettings } from "@shared/ChatSettings"
import { mentionRegex, mentionRegexGlobal } from "@shared/context-mentions"
import { EmptyRequest, StringRequest } from "@shared/proto/cline/common"
import { FileSearchRequest, RelativePathsRequest } from "@shared/proto/cline/file"
import { UpdateApiConfigurationRequest } from "@shared/proto/cline/models"
import { PlanActMode, TogglePlanActModeRequest } from "@shared/proto/cline/state"
import { convertApiConfigurationToProto } from "@shared/proto-conversions/models/api-configuration-conversion"
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import type React from "react"
import { forwardRef, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"
import DynamicTextArea from "react-textarea-autosize"
import { useClickAway, useWindowSize } from "react-use"
import styled from "styled-components"
import ContextMenu from "@/components/chat/ContextMenu"
import { CHAT_CONSTANTS } from "@/components/chat/chat-view/constants"
import SlashCommandMenu from "@/components/chat/SlashCommandMenu"
import { CODE_BLOCK_BG_COLOR } from "@/components/common/CodeBlock"
import Thumbnails from "@/components/common/Thumbnails"
import Tooltip from "@/components/common/Tooltip"
import ApiOptions from "@/components/settings/ApiOptions"
import { normalizeApiConfiguration, getModeSpecificFields } from "@/components/settings/utils/providerUtils"
import { getModeSpecificFields, normalizeApiConfiguration } from "@/components/settings/utils/providerUtils"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { FileServiceClient, StateServiceClient, ModelsServiceClient } from "@/services/grpc-client"
import { FileServiceClient, ModelsServiceClient, StateServiceClient } from "@/services/grpc-client"
import {
ContextMenuOptionType,
getContextMenuOptions,
getContextMenuOptionIndex,
getContextMenuOptions,
insertMention,
insertMentionDirectly,
removeMention,
SearchResult,
type SearchResult,
shouldShowContextMenu,
} from "@/utils/context-mentions"
import { useMetaKeyDetection, useShortcut } from "@/utils/hooks"
@@ -25,27 +36,17 @@ import {
getMatchingSlashCommands,
insertSlashCommand,
removeSlashCommand,
type SlashCommand,
shouldShowSlashCommandsMenu,
SlashCommand,
slashCommandDeleteRegex,
validateSlashCommand,
} from "@/utils/slash-commands"
import { validateApiConfiguration, validateModelId } from "@/utils/validate"
import { ChatSettings } from "@shared/ChatSettings"
import { mentionRegex, mentionRegexGlobal } from "@shared/context-mentions"
import { EmptyRequest, StringRequest } from "@shared/proto/cline/common"
import { FileInfo, FileSearchRequest, RelativePathsRequest } from "@shared/proto/cline/file"
import { UpdateApiConfigurationRequest } from "@shared/proto/cline/models"
import { convertApiConfigurationToProto } from "@shared/proto-conversions/models/api-configuration-conversion"
import { PlanActMode, TogglePlanActModeRequest } from "@shared/proto/cline/state"
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import React, { forwardRef, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"
import DynamicTextArea from "react-textarea-autosize"
import { useClickAway, useEvent, useWindowSize } from "react-use"
import styled from "styled-components"
import ClineRulesToggleModal from "../cline-rules/ClineRulesToggleModal"
import ServersToggleModal from "./ServersToggleModal"
const { MAX_IMAGES_AND_FILES_PER_MESSAGE } = CHAT_CONSTANTS
const getImageDimensions = (dataUrl: string): Promise<{ width: number; height: number }> => {
return new Promise((resolve, reject) => {
const img = new Image()
@@ -93,7 +94,9 @@ interface GitCommit {
const PLAN_MODE_COLOR = "var(--vscode-inputValidation-warningBorder)"
const SwitchOption = styled.div<{ isActive: boolean }>`
const SwitchOption = styled.div.withConfig({
shouldForwardProp: (prop) => !["isActive"].includes(prop),
})<{ isActive: boolean }>`
padding: 2px 8px;
color: ${(props) => (props.isActive ? "white" : "var(--vscode-input-foreground)")};
z-index: 1;
@@ -122,7 +125,9 @@ const SwitchContainer = styled.div<{ disabled: boolean }>`
user-select: none; // Prevent text selection
`
const Slider = styled.div<{ isAct: boolean; isPlan?: boolean }>`
const Slider = styled.div.withConfig({
shouldForwardProp: (prop) => !["isAct", "isPlan"].includes(prop),
})<{ isAct: boolean; isPlan?: boolean }>`
position: absolute;
height: 100%;
width: 50%;
@@ -14,11 +14,9 @@ interface AutoApproveMenuItemProps {
showIcon?: boolean
}
const CheckboxContainer = styled.div<{
isFavorited?: boolean
onClick?: (e: MouseEvent) => void
onMouseDown?: (e: React.MouseEvent) => void
}>`
const CheckboxContainer = styled.div.withConfig({
shouldForwardProp: (prop) => !["isFavorited"].includes(prop),
})<{ isFavorited?: boolean; onClick?: (e: MouseEvent) => void; onMouseDown?: (e: React.MouseEvent) => void }>`
display: flex;
align-items: center;
justify-content: space-between; /* Push content to edges */
@@ -14,7 +14,9 @@ export const ChatLayout: React.FC<ChatLayoutProps> = ({ isHidden, children }) =>
return <ChatLayoutContainer isHidden={isHidden}>{children}</ChatLayoutContainer>
}
const ChatLayoutContainer = styled.div<{ isHidden: boolean }>`
const ChatLayoutContainer = styled.div.withConfig({
shouldForwardProp: (prop) => !["isHidden"].includes(prop),
})<{ isHidden: boolean }>`
position: fixed;
top: 0;
left: 0;
@@ -1,7 +1,7 @@
import { useEffect } from "react"
import { useDeepCompareEffect } from "react-use"
import { ClineMessage, ClineSayTool } from "@shared/ExtensionMessage"
import { ChatState } from "../types/chatTypes"
import { useDeepCompareEffect } from "react-use"
/**
* Custom hook for managing button state based on messages
@@ -39,19 +39,66 @@ const HistoryPreview = ({ showHistoryView }: HistoryPreviewProps) => {
}
return (
<div className="flex-shrink-0">
<div style={{ flexShrink: 0 }}>
<style>
{`
.history-preview-item {
background-color: color-mix(in srgb, var(--vscode-toolbar-hoverBackground) 65%, transparent);
border-radius: 4px;
position: relative;
overflow: hidden;
opacity: 0.8;
cursor: pointer;
margin-bottom: 12px;
}
.history-preview-item:hover {
background-color: color-mix(in srgb, var(--vscode-toolbar-hoverBackground) 100%, transparent);
opacity: 1;
pointer-events: auto;
}
.history-header {
cursor: pointer;
user-select: none;
}
.history-header:hover {
opacity: 0.8;
}
`}
</style>
<div
className="flex items-center gap-2 mx-5 my-2 cursor-pointer select-none text-[var(--vscode-descriptionForeground)] hover:opacity-80 transition-all duration-200 rounded-lg px-2 py-1 hover:bg-[var(--vscode-toolbar-hoverBackground)]"
onClick={toggleExpanded}>
className="history-header"
onClick={toggleExpanded}
style={{
color: "var(--vscode-descriptionForeground)",
margin: "10px 20px 10px 20px",
display: "flex",
alignItems: "center",
}}>
<span
className={`codicon codicon-chevron-${isExpanded ? "down" : "right"} scale-90 transition-transform duration-200`}
/>
<span className="codicon codicon-comment-discussion scale-90" />
<span className="font-medium text-xs uppercase tracking-wide">Recent Tasks</span>
className={`codicon codicon-chevron-${isExpanded ? "down" : "right"}`}
style={{
marginRight: "4px",
transform: "scale(0.9)",
}}></span>
<span
className="codicon codicon-comment-discussion"
style={{
marginRight: "4px",
transform: "scale(0.9)",
}}></span>
<span
style={{
fontWeight: 500,
fontSize: "0.85em",
textTransform: "uppercase",
}}>
Recent Tasks
</span>
</div>
{isExpanded && (
<div className="px-5 space-y-3">
<div style={{ padding: "0px 20px 0 20px" }}>
{taskHistory.filter((item) => item.ts && item.task).length > 0 ? (
<>
{taskHistory
@@ -60,58 +107,61 @@ const HistoryPreview = ({ showHistoryView }: HistoryPreviewProps) => {
.map((item) => (
<div
key={item.id}
className="relative rounded-xl p-3 cursor-pointer overflow-hidden transition-all duration-150 ease-out hover:scale-[1.02] hover:shadow-xl group hover:bg-[color-mix(in_srgb,var(--vscode-toolbar-hoverBackground)_50%,transparent)] hover:border-[color-mix(in_srgb,var(--vscode-panel-border)_80%,transparent)]"
style={{
backgroundColor:
"color-mix(in srgb, var(--vscode-toolbar-hoverBackground) 30%, transparent)",
border: "1px solid color-mix(in srgb, var(--vscode-panel-border) 50%, transparent)",
backdropFilter: "blur(8px)",
}}
className="history-preview-item"
onClick={() => handleHistorySelect(item.id)}>
{/* Subtle gradient overlay for extra depth */}
<div
className="absolute inset-0 transition-all duration-150 rounded-xl opacity-0 group-hover:opacity-100"
style={{
background:
"linear-gradient(135deg, color-mix(in srgb, var(--vscode-button-background) 5%, transparent) 0%, color-mix(in srgb, var(--vscode-focusBorder) 3%, transparent) 100%)",
}}
/>
{item.isFavorited && (
<div
className="absolute top-3 right-3 z-20 drop-shadow-sm"
style={{ color: "var(--vscode-button-background)" }}>
<span className="codicon codicon-star-full" aria-label="Favorited" />
</div>
)}
<div className="relative z-10">
<div className="mb-2">
<span className="text-[var(--vscode-descriptionForeground)] font-medium text-xs uppercase tracking-wider opacity-80">
<div style={{ padding: "12px" }}>
<div style={{ marginBottom: "8px" }}>
<span
style={{
color: "var(--vscode-descriptionForeground)",
fontWeight: 500,
fontSize: "0.85em",
textTransform: "uppercase",
}}>
{formatDate(item.ts)}
</span>
</div>
{item.isFavorited && (
<div
style={{
position: "absolute",
top: "12px",
right: "12px",
color: "var(--vscode-button-background)",
}}>
<span className="codicon codicon-star-full" aria-label="Favorited" />
</div>
)}
<div
id={`history-preview-task-${item.id}`}
className="text-[var(--vscode-descriptionForeground)] mb-2 line-clamp-3 whitespace-pre-wrap break-words"
style={{ fontSize: "var(--vscode-font-size)" }}>
className="history-preview-task"
style={{
fontSize: "var(--vscode-font-size)",
color: "var(--vscode-descriptionForeground)",
marginBottom: "8px",
display: "-webkit-box",
WebkitLineClamp: 3,
WebkitBoxOrient: "vertical",
overflow: "hidden",
whiteSpace: "pre-wrap",
wordBreak: "break-word",
overflowWrap: "anywhere",
}}>
<span className="ph-no-capture">{item.task}</span>
</div>
<div className="text-xs text-[var(--vscode-descriptionForeground)] opacity-75 space-x-1">
<div
style={{
fontSize: "0.85em",
color: "var(--vscode-descriptionForeground)",
}}>
<span>
Tokens: {formatLargeNumber(item.tokensIn || 0)}
{formatLargeNumber(item.tokensOut || 0)}
</span>
{!!item.cacheWrites && (
<>
<span
style={{
color: "color-mix(in srgb, var(--vscode-descriptionForeground) 40%, transparent)",
}}>
</span>
{" • "}
<span>
Cache: +{formatLargeNumber(item.cacheWrites || 0)} {" "}
{formatLargeNumber(item.cacheReads || 0)}
@@ -120,12 +170,7 @@ const HistoryPreview = ({ showHistoryView }: HistoryPreviewProps) => {
)}
{!!item.totalCost && (
<>
<span
style={{
color: "color-mix(in srgb, var(--vscode-descriptionForeground) 40%, transparent)",
}}>
</span>
{" • "}
<span>API Cost: ${item.totalCost?.toFixed(4)}</span>
</>
)}
@@ -133,27 +178,35 @@ const HistoryPreview = ({ showHistoryView }: HistoryPreviewProps) => {
</div>
</div>
))}
<div className="flex items-center justify-center pt-2">
<button
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
}}>
<VSCodeButton
appearance="icon"
onClick={() => showHistoryView()}
className="cursor-pointer text-center transition-all duration-150 hover:opacity-80 flex items-center gap-1 bg-transparent border-none outline-none focus:outline-none"
style={{
color: "var(--vscode-descriptionForeground)",
fontSize: "var(--vscode-font-size)",
opacity: 0.9,
}}>
<span className="codicon codicon-history scale-90"></span>
<span className="font-medium">View all history</span>
</button>
<div
style={{
fontSize: "var(--vscode-font-size)",
color: "var(--vscode-descriptionForeground)",
}}>
View all history
</div>
</VSCodeButton>
</div>
</>
) : (
<div
className="text-center text-[var(--vscode-descriptionForeground)] py-4 rounded-xl"
style={{
textAlign: "center",
color: "var(--vscode-descriptionForeground)",
fontSize: "var(--vscode-font-size)",
backgroundColor: "color-mix(in srgb, var(--vscode-toolbar-hoverBackground) 20%, transparent)",
border: "1px solid color-mix(in srgb, var(--vscode-panel-border) 30%, transparent)",
backdropFilter: "blur(8px)",
padding: "10px 0",
}}>
No recent tasks
</div>
@@ -1,6 +1,5 @@
import { useExtensionState } from "@/context/ExtensionStateContext"
import { McpServiceClient } from "@/services/grpc-client"
import { vscode } from "@/utils/vscode"
import { McpViewTab } from "@shared/mcp"
import { EmptyRequest } from "@shared/proto/cline/common"
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
@@ -113,7 +112,9 @@ const McpConfigurationView = ({ onDone, initialTab }: McpViewProps) => {
)
}
const StyledTabButton = styled.button<{ isActive: boolean; disabled?: boolean }>`
const StyledTabButton = styled.button.withConfig({
shouldForwardProp: (prop) => !["isActive"].includes(prop),
})<{ isActive: boolean; disabled?: boolean }>`
background: none;
border: none;
border-bottom: 2px solid ${(props) => (props.isActive ? "var(--vscode-foreground)" : "transparent")};
@@ -44,7 +44,7 @@ export const BaseUrlField = ({
value={localValue}
style={{ width: "100%", marginTop: 3 }}
type="url"
onInput={(e: any) => setLocalValue(e.target.value)}
onInput={(e: any) => setLocalValue(e.target.value.trim())}
placeholder={placeholder}
/>
)}
@@ -22,11 +22,18 @@ interface DebouncedTextFieldProps {
* A wrapper around VSCodeTextField that automatically handles debounced input
* to prevent excessive API calls while typing
*/
export const DebouncedTextField = ({ initialValue, onChange, children, ...otherProps }: DebouncedTextFieldProps) => {
export const DebouncedTextField = ({ initialValue, onChange, children, type, ...otherProps }: DebouncedTextFieldProps) => {
const [localValue, setLocalValue] = useDebouncedInput(initialValue, onChange)
return (
<VSCodeTextField {...otherProps} value={localValue} onInput={(e: any) => setLocalValue(e.target.value)}>
<VSCodeTextField
{...otherProps}
type={type}
value={localValue}
onInput={(e: any) => {
const value = e.target.value
setLocalValue(type === "url" ? value.trim() : value)
}}>
{children}
</VSCodeTextField>
)
+1 -1
View File
@@ -4,7 +4,7 @@
/* @import "tailwindcss/preflight.css" layer(base); */
@import "tailwindcss/utilities.css" layer(utilities);
@config "../tailwind.config.js";
@config "../tailwind.config.mjs";
/* Import Azeret Mono font from local package */
@import "@fontsource/azeret-mono/300.css";
@@ -1,7 +1,7 @@
const { heroui } = require("@heroui/react")
import { heroui } from "@heroui/react"
/** @type {import('tailwindcss').Config} */
module.exports = {
const config = {
content: ["./src/**/*.{js,ts,jsx,tsx,mdx}", "./node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}"],
theme: {
extend: {
@@ -24,3 +24,4 @@ module.exports = {
}),
],
}
export default config