mirror of
https://github.com/cline/cline.git
synced 2026-09-11 16:42:40 +08:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0ace878e09 | ||
|
|
e282ccf16f | ||
|
|
8ef8f92419 |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fixed diff editing support for claude 4 family of models
|
||||
Generated
+14
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "claude-dev",
|
||||
"version": "3.17.2",
|
||||
"version": "3.17.5",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "claude-dev",
|
||||
"version": "3.17.2",
|
||||
"version": "3.17.5",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/bedrock-sdk": "^0.12.4",
|
||||
@@ -27,6 +27,7 @@
|
||||
"@opentelemetry/sdk-trace-node": "^1.30.1",
|
||||
"@opentelemetry/semantic-conventions": "^1.30.0",
|
||||
"@sentry/browser": "^9.12.0",
|
||||
"@streamparser/json": "^0.0.22",
|
||||
"@vscode/codicons": "^0.0.36",
|
||||
"archiver": "^7.0.1",
|
||||
"axios": "^1.8.2",
|
||||
@@ -10895,6 +10896,12 @@
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@streamparser/json": {
|
||||
"version": "0.0.22",
|
||||
"resolved": "https://registry.npmjs.org/@streamparser/json/-/json-0.0.22.tgz",
|
||||
"integrity": "sha512-b6gTSBjJ8G8SuO3Gbbj+zXbVx8NSs1EbpbMKpzGLWMdkR+98McH9bEjSz3+0mPJf68c5nxa3CrJHp5EQNXM6zQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@szmarczak/http-timer": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz",
|
||||
@@ -34092,6 +34099,11 @@
|
||||
"integrity": "sha512-Pb6M8TDO9DtSVla9yXSTAxmo9GVEouq5P40DWXdOie69bXogZTkgvopCq+yEvTMA0F6PEvdJmbtTV3ccIp11VQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@streamparser/json": {
|
||||
"version": "0.0.22",
|
||||
"resolved": "https://registry.npmjs.org/@streamparser/json/-/json-0.0.22.tgz",
|
||||
"integrity": "sha512-b6gTSBjJ8G8SuO3Gbbj+zXbVx8NSs1EbpbMKpzGLWMdkR+98McH9bEjSz3+0mPJf68c5nxa3CrJHp5EQNXM6zQ=="
|
||||
},
|
||||
"@szmarczak/http-timer": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz",
|
||||
|
||||
@@ -362,6 +362,7 @@
|
||||
"@opentelemetry/sdk-trace-node": "^1.30.1",
|
||||
"@opentelemetry/semantic-conventions": "^1.30.0",
|
||||
"@sentry/browser": "^9.12.0",
|
||||
"@streamparser/json": "^0.0.22",
|
||||
"@vscode/codicons": "^0.0.36",
|
||||
"archiver": "^7.0.1",
|
||||
"axios": "^1.8.2",
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
import { JSONParser } from "@streamparser/json"
|
||||
|
||||
// Fallback type definition based on the error message: "Property 'value' is optional in type 'ParsedElementInfo'"
|
||||
type ParsedElementInfo = {
|
||||
value?: any
|
||||
key?: string | number
|
||||
parent?: any
|
||||
stack?: any[]
|
||||
}
|
||||
|
||||
export interface ReplacementItem {
|
||||
old_str: string
|
||||
new_str: string
|
||||
}
|
||||
|
||||
export interface ChangeLocation {
|
||||
startLine: number
|
||||
endLine: number
|
||||
startChar: number
|
||||
endChar: number
|
||||
}
|
||||
|
||||
export class StreamingJsonReplacer {
|
||||
private currentFileContent: string
|
||||
private parser: JSONParser
|
||||
private onContentUpdated: (newContent: string, isFinalItem: boolean, changeLocation?: ChangeLocation) => void
|
||||
private onErrorCallback: (error: Error) => void
|
||||
private itemsProcessed: number = 0
|
||||
private successfullyParsedItems: ReplacementItem[] = []
|
||||
|
||||
constructor(
|
||||
initialContent: string,
|
||||
onContentUpdatedCallback: (newContent: string, isFinalItem: boolean, changeLocation?: ChangeLocation) => void,
|
||||
onErrorCallback: (error: Error) => void,
|
||||
) {
|
||||
this.currentFileContent = initialContent
|
||||
this.onContentUpdated = onContentUpdatedCallback
|
||||
this.onErrorCallback = onErrorCallback
|
||||
|
||||
this.parser = new JSONParser({ paths: ["$.replacements.*"] })
|
||||
|
||||
this.parser.onValue = (parsedElementInfo: ParsedElementInfo) => {
|
||||
const { value } = parsedElementInfo // Destructure to get value, which might be undefined
|
||||
// This callback is triggered for each item matched by '$.replacements.*'
|
||||
if (value && typeof value === "object" && "old_str" in value && "new_str" in value) {
|
||||
const item = value as ReplacementItem // Value here is confirmed to be an object
|
||||
|
||||
if (typeof item.old_str === "string" && typeof item.new_str === "string") {
|
||||
this.successfullyParsedItems.push(item) // Store the structurally valid item
|
||||
|
||||
if (this.currentFileContent.includes(item.old_str)) {
|
||||
// Calculate the change location before making the replacement
|
||||
const changeLocation = this.calculateChangeLocation(item.old_str, item.new_str)
|
||||
|
||||
this.currentFileContent = this.currentFileContent.replace(item.old_str, item.new_str)
|
||||
this.itemsProcessed++
|
||||
// Notify that an item has been processed. The `isFinalItem` argument here is tricky
|
||||
// as we don't know from the parser alone if this is the *absolute* last item
|
||||
// until the stream ends. The caller (Task.ts) will manage the final update.
|
||||
// For now, we'll pass `false` and let Task.ts handle the final diff view update.
|
||||
this.onContentUpdated(this.currentFileContent, false, changeLocation)
|
||||
} else {
|
||||
const snippet = item.old_str.length > 50 ? item.old_str.substring(0, 47) + "..." : item.old_str
|
||||
const error = new Error(`Streaming Replacement failed: 'old_str' not found. Snippet: "${snippet}"`)
|
||||
this.onErrorCallback(error) // Call our own error callback
|
||||
}
|
||||
} else {
|
||||
const error = new Error(`Invalid item structure in replacements stream: ${JSON.stringify(item)}`)
|
||||
this.onErrorCallback(error) // Call our own error callback
|
||||
}
|
||||
} else if (value && (Array.isArray(value) || (typeof value === "object" && "replacements" in value))) {
|
||||
// This might be the 'replacements' array itself or the root object.
|
||||
// The `paths: ['$.replacements.*']` should mean we only get items.
|
||||
// If we get here, it's likely the root object if paths wasn't specific enough or if it's an empty replacements array.
|
||||
console.log("Streaming parser emitted container:", value)
|
||||
} else {
|
||||
// Value is not a ReplacementItem or a known container, could be an issue with the JSON structure or path.
|
||||
// If `paths` is correct, this path should ideally not be hit often for valid streams.
|
||||
console.warn("Streaming parser emitted unexpected value:", value)
|
||||
}
|
||||
}
|
||||
|
||||
this.parser.onError = (err: Error) => {
|
||||
// Propagate the error to the caller via the callback
|
||||
this.onErrorCallback(err)
|
||||
// Note: The @streamparser/json library might throw synchronously on write if onError is not set,
|
||||
// or if it re-throws. We'll ensure Task.ts wraps write/end in try-catch.
|
||||
}
|
||||
}
|
||||
|
||||
public write(jsonChunk: string): void {
|
||||
// Errors during write will be caught by the parser's onError or thrown.
|
||||
this.parser.write(jsonChunk)
|
||||
}
|
||||
|
||||
public getCurrentContent(): string {
|
||||
return this.currentFileContent
|
||||
}
|
||||
|
||||
public getSuccessfullyParsedItems(): ReplacementItem[] {
|
||||
return [...this.successfullyParsedItems] // Return a copy
|
||||
}
|
||||
|
||||
private calculateChangeLocation(oldStr: string, newStr: string): ChangeLocation {
|
||||
// Find the index where the old string starts
|
||||
const startIndex = this.currentFileContent.indexOf(oldStr)
|
||||
if (startIndex === -1) {
|
||||
// This shouldn't happen since we already checked includes(), but just in case
|
||||
return { startLine: 0, endLine: 0, startChar: 0, endChar: 0 }
|
||||
}
|
||||
|
||||
// Calculate line numbers by counting newlines before the start index
|
||||
const contentBeforeStart = this.currentFileContent.substring(0, startIndex)
|
||||
const startLine = (contentBeforeStart.match(/\n/g) || []).length
|
||||
|
||||
// Calculate the end index after replacement
|
||||
const endIndex = startIndex + oldStr.length
|
||||
const contentBeforeEnd = this.currentFileContent.substring(0, endIndex)
|
||||
const endLine = (contentBeforeEnd.match(/\n/g) || []).length
|
||||
|
||||
// Calculate character positions within their respective lines
|
||||
const lastNewlineBeforeStart = contentBeforeStart.lastIndexOf("\n")
|
||||
const startChar = lastNewlineBeforeStart === -1 ? startIndex : startIndex - lastNewlineBeforeStart - 1
|
||||
|
||||
const lastNewlineBeforeEnd = contentBeforeEnd.lastIndexOf("\n")
|
||||
const endChar = lastNewlineBeforeEnd === -1 ? endIndex : endIndex - lastNewlineBeforeEnd - 1
|
||||
|
||||
return {
|
||||
startLine,
|
||||
endLine,
|
||||
startChar,
|
||||
endChar,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ export const SYSTEM_PROMPT = async (
|
||||
supportsBrowserUse: boolean,
|
||||
mcpHub: McpHub,
|
||||
browserSettings: BrowserSettings,
|
||||
isClaude4ModelFamily: boolean,
|
||||
) => `You are Cline, a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices.
|
||||
|
||||
====
|
||||
@@ -71,7 +72,46 @@ Your file content here
|
||||
</write_to_file>
|
||||
|
||||
## replace_in_file
|
||||
Description: Request to replace sections of content in an existing file using SEARCH/REPLACE blocks that define exact changes to specific parts of the file. This tool should be used when you need to make targeted changes to specific parts of a file.
|
||||
${
|
||||
isClaude4ModelFamily
|
||||
? `
|
||||
"Description: Return your edits as a JSON object with a "replacements" array. Each replacement should have "old_str" and "new_str" fields. The old_str must match exactly what's in the file (including whitespace, indentation and new lines). You can edit multiple lines, but please keep the replacements as simple as possible.
|
||||
Both old_str and new_str can be multiline strings, but they must be valid JSON strings.
|
||||
|
||||
Usage:
|
||||
<replace_in_file>
|
||||
<path>File path here</path>
|
||||
<diff>
|
||||
{{
|
||||
"replacements": [
|
||||
{{
|
||||
"old_str": "exact string from file",
|
||||
"new_str": "replacement string"
|
||||
}}
|
||||
]
|
||||
}}
|
||||
</diff>
|
||||
</replace_in_file>
|
||||
|
||||
Important: Make sure each old_str matches the exact text in the file, character for character.
|
||||
Parameters:
|
||||
- path: (required) The path of the file to modify (relative to the current working directory ${cwd.toPosix()})
|
||||
- replacements_json: (required) A JSON string containing an object with a "replacements" array. Each object in the array must have "old_str" (the exact string to find in the file) and "new_str" (the string to replace it with). Refer to the example format in the main description.
|
||||
Usage:
|
||||
<replace_in_file>
|
||||
<path>File path here</path>
|
||||
<diff>
|
||||
{{
|
||||
"replacements": [
|
||||
{{
|
||||
"old_str": "exact string from file",
|
||||
"new_str": "replacement string"
|
||||
}}
|
||||
]
|
||||
}}
|
||||
</diff>
|
||||
</replace_in_file>`
|
||||
: `Description: Request to replace sections of content in an existing file using SEARCH/REPLACE blocks that define exact changes to specific parts of the file. This tool should be used when you need to make targeted changes to specific parts of a file.
|
||||
Parameters:
|
||||
- path: (required) The path of the file to modify (relative to the current working directory ${cwd.toPosix()})
|
||||
- diff: (required) One or more SEARCH/REPLACE blocks following this exact format:
|
||||
@@ -103,8 +143,9 @@ Usage:
|
||||
<path>File path here</path>
|
||||
<diff>
|
||||
Search and replace blocks here
|
||||
</diff>
|
||||
</replace_in_file>
|
||||
</diff>
|
||||
</replace_in_file>`
|
||||
}
|
||||
|
||||
## search_files
|
||||
Description: Request to perform a regex search across files in a specified directory, providing context-rich results. This tool searches for patterns or specific content across multiple files, displaying each match with encapsulating context.
|
||||
@@ -329,6 +370,31 @@ Usage:
|
||||
## Example 4: Requesting to make targeted edits to a file
|
||||
|
||||
<replace_in_file>
|
||||
${
|
||||
isClaude4ModelFamily
|
||||
? `
|
||||
<path>src/baseApp.py</path>
|
||||
<diff>
|
||||
{
|
||||
"replacements": [
|
||||
{
|
||||
"old_str": "def try_dotdotdots(whole, part, replace):",
|
||||
"new_str": "# Handles search/replace blocks that use ellipsis (...) to represent omitted code sections\n# Validates that ellipsis usage is consistent between search and replace blocks\ndef try_dotdotdots(whole, part, replace):"
|
||||
},
|
||||
{
|
||||
"old_str": "def strip_filename(filename, fence):",
|
||||
"new_str": "# Extracts and cleans filename from various markdown formatting styles\n# Handles filenames with different prefixes, suffixes, and decorations\ndef strip_filename(filename, fence):"
|
||||
},
|
||||
{
|
||||
"old_str": "def main():",
|
||||
"new_str": "# Main entry point for command-line usage\n# Processes chat history and displays diffs for all found edit blocks\ndef main():"
|
||||
}
|
||||
]
|
||||
}
|
||||
</diff>
|
||||
</replace_in_file>
|
||||
`
|
||||
: `
|
||||
<path>src/components/App.tsx</path>
|
||||
<diff>
|
||||
<<<<<<< SEARCH
|
||||
@@ -360,6 +426,9 @@ return (
|
||||
>>>>>>> REPLACE
|
||||
</diff>
|
||||
</replace_in_file>
|
||||
`
|
||||
}
|
||||
|
||||
|
||||
## Example 5: Requesting to use an MCP tool
|
||||
|
||||
@@ -529,9 +598,21 @@ You have access to two tools for working with files: **write_to_file** and **rep
|
||||
# Workflow Tips
|
||||
|
||||
1. Before editing, assess the scope of your changes and decide which tool to use.
|
||||
${
|
||||
isClaude4ModelFamily
|
||||
? `
|
||||
2. For major overhauls or initial file creation, rely on write_to_file.
|
||||
3. Once the file has been edited with either write_to_file or replace_in_file, the system will provide you with the final state of the modified file. Use this updated content as the reference point for any subsequent SEARCH/REPLACE operations, since it reflects any auto-formatting or user-applied changes.
|
||||
4. All edits are applied in sequence, in the order they are provided
|
||||
5. All edits must be valid for the operation to succeed - if any edit fails, none will be applied
|
||||
|
||||
`
|
||||
: `
|
||||
2. For targeted edits, apply replace_in_file with carefully crafted SEARCH/REPLACE blocks. If you need multiple changes, you can stack multiple SEARCH/REPLACE blocks within a single replace_in_file call.
|
||||
3. For major overhauls or initial file creation, rely on write_to_file.
|
||||
4. Once the file has been edited with either write_to_file or replace_in_file, the system will provide you with the final state of the modified file. Use this updated content as the reference point for any subsequent SEARCH/REPLACE operations, since it reflects any auto-formatting or user-applied changes.
|
||||
`
|
||||
}
|
||||
|
||||
By thoughtfully selecting between write_to_file and replace_in_file, you can make your file editing process smoother, safer, and more efficient.
|
||||
|
||||
@@ -602,9 +683,17 @@ RULES
|
||||
- When presented with images, utilize your vision capabilities to thoroughly examine them and extract meaningful information. Incorporate these insights into your thought process as you accomplish the user's task.
|
||||
- At the end of each user message, you will automatically receive environment_details. This information is not written by the user themselves, but is auto-generated to provide potentially relevant context about the project structure and environment. While this information can be valuable for understanding the project context, do not treat it as a direct part of the user's request or response. Use it to inform your actions and decisions, but don't assume the user is explicitly asking about or referring to this information unless they clearly do so in their message. When using environment_details, explain your actions clearly to ensure the user understands, as they may not be aware of these details.
|
||||
- Before executing commands, check the "Actively Running Terminals" section in environment_details. If present, consider how these active processes might impact your task. For example, if a local development server is already running, you wouldn't need to start it again. If no active terminals are listed, proceed with command execution as normal.
|
||||
${
|
||||
isClaude4ModelFamily
|
||||
? `
|
||||
- When using the replace_in_file tool, you must include complete lines
|
||||
`
|
||||
: `
|
||||
- When using the replace_in_file tool, you must include complete lines in your SEARCH blocks, not partial lines. The system requires exact line matches and cannot match partial lines. For example, if you want to match a line containing "const x = 5;", your SEARCH block must include the entire line, not just "x = 5" or other fragments.
|
||||
- When using the replace_in_file tool, if you use multiple SEARCH/REPLACE blocks, list them in the order they appear in the file. For example if you need to make changes to both line 10 and line 50, first include the SEARCH/REPLACE block for line 10, followed by the SEARCH/REPLACE block for line 50.
|
||||
- When using the replace_in_file tool, Do NOT add extra characters to the markers (e.g., <<<<<<< SEARCH> is INVALID). Do NOT forget to use the closing >>>>>>> REPLACE marker. Do NOT modify the marker format in any way. Malformed XML will cause complete tool failure and break the entire editing process.
|
||||
`
|
||||
}
|
||||
- It is critical you wait for the user's response after each tool use, in order to confirm the success of the tool use. For example, if asked to make a todo app, you would create a file, wait for the user's response it was created successfully, then create another file if needed, wait for the user's response it was created successfully, etc.${
|
||||
supportsBrowserUse
|
||||
? " Then if you want to test your work, you might use browser_action to launch the site, wait for the user's response confirming the site was launched along with a screenshot, then perhaps e.g., click a button to test functionality if needed, wait for the user's response confirming the button was clicked along with a screenshot of the new state, before finally closing the browser."
|
||||
|
||||
+161
-26
@@ -101,7 +101,7 @@ import { parseSlashCommands } from "@core/slash-commands"
|
||||
import WorkspaceTracker from "@integrations/workspace/WorkspaceTracker"
|
||||
import { McpHub } from "@services/mcp/McpHub"
|
||||
import { isInTestMode } from "../../services/test/TestMode"
|
||||
import { featureFlagsService } from "@/services/posthog/feature-flags/FeatureFlagsService"
|
||||
import { StreamingJsonReplacer, ChangeLocation } from "@core/assistant-message/diff-json"
|
||||
|
||||
export const cwd =
|
||||
vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) ?? path.join(os.homedir(), "Desktop") // may or may not exist but fs checking existence would immediately ask for permission which would be bad UX, need to come up with a better solution
|
||||
@@ -110,6 +110,9 @@ type ToolResponse = string | Array<Anthropic.TextBlockParam | Anthropic.ImageBlo
|
||||
type UserContent = Array<Anthropic.ContentBlockParam>
|
||||
|
||||
export class Task {
|
||||
private streamingJsonReplacer?: StreamingJsonReplacer
|
||||
private lastProcessedJsonLength: number = 0
|
||||
|
||||
// dependencies
|
||||
private context: vscode.ExtensionContext
|
||||
private mcpHub: McpHub
|
||||
@@ -1542,6 +1545,12 @@ export class Task {
|
||||
}
|
||||
}
|
||||
|
||||
private async isClaude4ModelFamily(): Promise<boolean> {
|
||||
const model = this.api.getModel()
|
||||
const modelId = model.id
|
||||
return modelId.includes("claude-sonnet-4") || modelId.includes("claude-opus-4")
|
||||
}
|
||||
|
||||
async *attemptApiRequest(previousApiReqIndex: number): ApiStream {
|
||||
// Wait for MCP servers to be connected before generating system prompt
|
||||
await pWaitFor(() => this.mcpHub.isConnecting !== true, { timeout: 10_000 }).catch(() => {
|
||||
@@ -1555,7 +1564,8 @@ export class Task {
|
||||
|
||||
const supportsBrowserUse = modelSupportsBrowserUse && !disableBrowserTool // only enable browser use if the model supports it and the user hasn't disabled it
|
||||
|
||||
let systemPrompt = await SYSTEM_PROMPT(cwd, supportsBrowserUse, this.mcpHub, this.browserSettings)
|
||||
const isClaude4ModelFamily = await this.isClaude4ModelFamily()
|
||||
let systemPrompt = await SYSTEM_PROMPT(cwd, supportsBrowserUse, this.mcpHub, this.browserSettings, isClaude4ModelFamily)
|
||||
|
||||
let settingsCustomInstructions = this.customInstructions?.trim()
|
||||
await this.migratePreferredLanguageToolSetting()
|
||||
@@ -1721,6 +1731,98 @@ export class Task {
|
||||
yield* iterator
|
||||
}
|
||||
|
||||
// Handle streaming JSON replacement for Claude 4 model family
|
||||
private async handleStreamingJsonReplacement(
|
||||
block: any,
|
||||
relPath: string,
|
||||
currentFullJson: string,
|
||||
): Promise<{ shouldBreak: boolean; newContent?: string; error?: string }> {
|
||||
// Calculate the delta - what's new since last time
|
||||
const newJsonChunk = currentFullJson.substring(this.lastProcessedJsonLength)
|
||||
|
||||
if (block.partial) {
|
||||
// Initialize on first chunk
|
||||
if (!this.streamingJsonReplacer) {
|
||||
if (!this.diffViewProvider.isEditing) {
|
||||
await this.diffViewProvider.open(relPath)
|
||||
}
|
||||
|
||||
// Set up callbacks
|
||||
const onContentUpdated = (newContent: string, _isFinalItem: boolean, changeLocation?: ChangeLocation) => {
|
||||
// Update diff view incrementally
|
||||
this.diffViewProvider.update(newContent, false, changeLocation)
|
||||
}
|
||||
|
||||
const onError = (error: Error) => {
|
||||
console.error("StreamingJsonReplacer error:", error)
|
||||
// Handle error: push tool result, cleanup
|
||||
this.userMessageContent.push({
|
||||
type: "text",
|
||||
text: formatResponse.toolError(`JSON replacement error: ${error.message}`),
|
||||
})
|
||||
this.didAlreadyUseTool = true
|
||||
this.userMessageContentReady = true
|
||||
this.streamingJsonReplacer = undefined
|
||||
this.lastProcessedJsonLength = 0
|
||||
throw error
|
||||
}
|
||||
|
||||
this.streamingJsonReplacer = new StreamingJsonReplacer(
|
||||
this.diffViewProvider.originalContent || "",
|
||||
onContentUpdated,
|
||||
onError,
|
||||
)
|
||||
this.lastProcessedJsonLength = 0
|
||||
}
|
||||
|
||||
// Feed only the new chunk
|
||||
if (newJsonChunk.length > 0) {
|
||||
try {
|
||||
this.streamingJsonReplacer.write(newJsonChunk)
|
||||
this.lastProcessedJsonLength = currentFullJson.length
|
||||
} catch (e) {
|
||||
// Handle write error
|
||||
return { shouldBreak: true, error: `Write error: ${e}` }
|
||||
}
|
||||
|
||||
const newContentParsed = this.streamingJsonReplacer.getSuccessfullyParsedItems()
|
||||
}
|
||||
|
||||
return { shouldBreak: true } // Wait for more chunks
|
||||
} else {
|
||||
// Final chunk (!block.partial)
|
||||
if (!this.streamingJsonReplacer) {
|
||||
// JSON came all at once, initialize
|
||||
if (!this.diffViewProvider.isEditing) {
|
||||
await this.diffViewProvider.open(relPath)
|
||||
}
|
||||
// Would need to initialize StreamingJsonReplacer here for non-streaming case
|
||||
this.lastProcessedJsonLength = 0
|
||||
return { shouldBreak: true }
|
||||
}
|
||||
|
||||
// Feed final delta
|
||||
if (newJsonChunk.length > 0) {
|
||||
this.streamingJsonReplacer.write(newJsonChunk)
|
||||
}
|
||||
|
||||
const newContent = this.streamingJsonReplacer.getCurrentContent()
|
||||
|
||||
// Get final list of replacements
|
||||
const allReplacements = this.streamingJsonReplacer.getSuccessfullyParsedItems()
|
||||
// console.log(`Total replacements applied: ${allReplacements.length}`)
|
||||
|
||||
// Cleanup
|
||||
this.streamingJsonReplacer = undefined
|
||||
this.lastProcessedJsonLength = 0
|
||||
|
||||
// Update diff view with final content
|
||||
await this.diffViewProvider.update(newContent, true)
|
||||
|
||||
return { shouldBreak: false, newContent }
|
||||
}
|
||||
}
|
||||
|
||||
async presentAssistantMessage() {
|
||||
if (this.abort) {
|
||||
throw new Error("Cline instance aborted")
|
||||
@@ -2017,6 +2119,7 @@ export class Task {
|
||||
try {
|
||||
// Construct newContent from diff
|
||||
let newContent: string
|
||||
newContent = "" // default to original content if not editing
|
||||
if (diff) {
|
||||
if (!this.api.getModel().id.includes("claude")) {
|
||||
// deepseek models tend to use unescaped html entities in diffs
|
||||
@@ -2030,34 +2133,66 @@ export class Task {
|
||||
await this.diffViewProvider.open(relPath)
|
||||
}
|
||||
|
||||
try {
|
||||
newContent = await constructNewFileContent(
|
||||
diff,
|
||||
this.diffViewProvider.originalContent || "",
|
||||
!block.partial,
|
||||
const currentFullJson = block.params.diff
|
||||
// Check if we should use streaming (e.g., for specific models)
|
||||
const isClaude4ModelFamily = await this.isClaude4ModelFamily()
|
||||
|
||||
// Going through claude family of models
|
||||
if (isClaude4ModelFamily && currentFullJson) {
|
||||
const streamingResult = await this.handleStreamingJsonReplacement(
|
||||
block,
|
||||
relPath,
|
||||
currentFullJson,
|
||||
)
|
||||
} catch (error) {
|
||||
await this.say("diff_error", relPath)
|
||||
|
||||
// Extract error type from error message if possible, or use a generic type
|
||||
const errorType =
|
||||
error instanceof Error && error.message.includes("does not match anything")
|
||||
? "search_not_found"
|
||||
: "other_diff_error"
|
||||
if (streamingResult.error) {
|
||||
await this.say("diff_error", relPath)
|
||||
pushToolResult(formatResponse.toolError(streamingResult.error))
|
||||
await this.diffViewProvider.revertChanges()
|
||||
await this.diffViewProvider.reset()
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
|
||||
// Add telemetry for diff edit failure
|
||||
telemetryService.captureDiffEditFailure(this.taskId, this.api.getModel().id, errorType)
|
||||
if (streamingResult.shouldBreak) {
|
||||
break // Wait for more chunks or handle initialization
|
||||
}
|
||||
|
||||
pushToolResult(
|
||||
formatResponse.toolError(
|
||||
`${(error as Error)?.message}\n\n` +
|
||||
formatResponse.diffError(relPath, this.diffViewProvider.originalContent),
|
||||
),
|
||||
)
|
||||
await this.diffViewProvider.revertChanges()
|
||||
await this.diffViewProvider.reset()
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
// If we get here, we have the final content
|
||||
if (streamingResult.newContent) {
|
||||
newContent = streamingResult.newContent
|
||||
// Continue with approval flow...
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
newContent = await constructNewFileContent(
|
||||
diff,
|
||||
this.diffViewProvider.originalContent || "",
|
||||
!block.partial,
|
||||
)
|
||||
} catch (error) {
|
||||
await this.say("diff_error", relPath)
|
||||
|
||||
// Extract error type from error message if possible, or use a generic type
|
||||
const errorType =
|
||||
error instanceof Error && error.message.includes("does not match anything")
|
||||
? "search_not_found"
|
||||
: "other_diff_error"
|
||||
|
||||
// Add telemetry for diff edit failure
|
||||
telemetryService.captureDiffEditFailure(this.taskId, this.api.getModel().id, errorType)
|
||||
|
||||
pushToolResult(
|
||||
formatResponse.toolError(
|
||||
`${(error as Error)?.message}\n\n` +
|
||||
formatResponse.diffError(relPath, this.diffViewProvider.originalContent),
|
||||
),
|
||||
)
|
||||
await this.diffViewProvider.revertChanges()
|
||||
await this.diffViewProvider.reset()
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
}
|
||||
} else if (content) {
|
||||
newContent = content
|
||||
|
||||
@@ -101,7 +101,11 @@ export class DiffViewProvider {
|
||||
})
|
||||
}
|
||||
|
||||
async update(accumulatedContent: string, isFinal: boolean) {
|
||||
async update(
|
||||
accumulatedContent: string,
|
||||
isFinal: boolean,
|
||||
changeLocation?: { startLine: number; endLine: number; startChar: number; endChar: number },
|
||||
) {
|
||||
if (!this.relPath || !this.activeLineController || !this.fadedOverlayController) {
|
||||
throw new Error("Required values not set")
|
||||
}
|
||||
@@ -148,29 +152,36 @@ export class DiffViewProvider {
|
||||
this.activeLineController.setActiveLine(currentLine)
|
||||
this.fadedOverlayController.updateOverlayAfterLine(currentLine, document.lineCount)
|
||||
|
||||
// Scroll to the last changed line only if the user hasn't scrolled up
|
||||
// Scroll to the actual change location if provided, otherwise use the old logic
|
||||
if (this.shouldAutoScroll) {
|
||||
if (diffLines.length <= 5) {
|
||||
// For small changes, just jump directly to the line
|
||||
this.scrollEditorToLine(currentLine)
|
||||
if (changeLocation) {
|
||||
// We have the actual location of the change, scroll to it
|
||||
const targetLine = changeLocation.startLine
|
||||
this.scrollEditorToLine(targetLine)
|
||||
} else {
|
||||
// For larger changes, create a quick scrolling animation
|
||||
const startLine = this.streamedLines.length
|
||||
const endLine = currentLine
|
||||
const totalLines = endLine - startLine
|
||||
const numSteps = 10 // Adjust this number to control animation speed
|
||||
const stepSize = Math.max(1, Math.floor(totalLines / numSteps))
|
||||
// Fallback to the old logic for non-replacement updates
|
||||
if (diffLines.length <= 5) {
|
||||
// For small changes, just jump directly to the line
|
||||
this.scrollEditorToLine(currentLine)
|
||||
} else {
|
||||
// For larger changes, create a quick scrolling animation
|
||||
const startLine = this.streamedLines.length
|
||||
const endLine = currentLine
|
||||
const totalLines = endLine - startLine
|
||||
const numSteps = 10 // Adjust this number to control animation speed
|
||||
const stepSize = Math.max(1, Math.floor(totalLines / numSteps))
|
||||
|
||||
// Create and await the smooth scrolling animation
|
||||
for (let line = startLine; line <= endLine; line += stepSize) {
|
||||
this.activeDiffEditor?.revealRange(
|
||||
new vscode.Range(line, 0, line, 0),
|
||||
vscode.TextEditorRevealType.InCenter,
|
||||
)
|
||||
await new Promise((resolve) => setTimeout(resolve, 16)) // ~60fps
|
||||
// Create and await the smooth scrolling animation
|
||||
for (let line = startLine; line <= endLine; line += stepSize) {
|
||||
this.activeDiffEditor?.revealRange(
|
||||
new vscode.Range(line, 0, line, 0),
|
||||
vscode.TextEditorRevealType.InCenter,
|
||||
)
|
||||
await new Promise((resolve) => setTimeout(resolve, 16)) // ~60fps
|
||||
}
|
||||
// Ensure we end at the final line
|
||||
this.scrollEditorToLine(currentLine)
|
||||
}
|
||||
// Ensure we end at the final line
|
||||
this.scrollEditorToLine(currentLine)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user