mirror of
https://github.com/cline/cline.git
synced 2026-09-14 19:39:22 +08:00
Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e5bcceccae | ||
|
|
78473d62fe | ||
|
|
aa44872c5b | ||
|
|
128728440e | ||
|
|
0eedf3b443 | ||
|
|
a28b995ab1 | ||
|
|
a91878efc6 | ||
|
|
65c21e7b7d | ||
|
|
56e388c90f | ||
|
|
586d804a01 | ||
|
|
85fbbcbe3f | ||
|
|
25c5310383 |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fixed token counting when using VSCode LM API provider
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
fix: only focus on editor panel that is visible and active to stop input field stealing issue
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
trim input value for URL fields
|
||||
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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 = {}
|
||||
|
||||
@@ -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 = ""
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
`
|
||||
@@ -41,6 +41,7 @@ export class TaskState {
|
||||
didRejectTool = false
|
||||
didAlreadyUseTool = false
|
||||
didEditFile: boolean = false
|
||||
currentlySummarizing = false
|
||||
|
||||
// Consecutive request tracking
|
||||
consecutiveAutoApprovedRequestsCount: number = 0
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user