Compare commits

...

1 Commits

Author SHA1 Message Date
abeatrix e5f48520b7 feat(tools): focus diff preview for write-to-file changes
- Add DiffUtils.createFocusedDiff to generate context-aware diffs (default 5 lines)
- Use focused diff in WriteToFileToolHandler partial and approval messages
  using diffViewProvider.originalContent + newContent
- Fall back to existing diff/content when focused diff is unavailable

This improves readability of change previews, reduces noise on large files,
and provides clearer context around edits without overwhelming the UI.
2025-11-13 23:31:13 -08:00
2 changed files with 117 additions and 2 deletions
@@ -16,6 +16,7 @@ import type { IFullyManagedTool } from "../ToolExecutorCoordinator"
import type { ToolValidator } from "../ToolValidator"
import type { TaskConfig } from "../types/TaskConfig"
import type { StronglyTypedUIHelpers } from "../types/UIHelpers"
import { DiffUtils } from "../utils/DiffUtils"
import { applyModelContentFixes } from "../utils/ModelContentProcessor"
import { ToolDisplayUtils } from "../utils/ToolDisplayUtils"
import { ToolResultUtils } from "../utils/ToolResultUtils"
@@ -52,13 +53,20 @@ export class WriteToFileToolHandler implements IFullyManagedTool {
const { relPath, absolutePath, fileExists, diff, content, newContent } = result
// Create and show partial UI message
// Create focused diff for the preview
const focusedDiff = DiffUtils.createFocusedDiff(
relPath,
config.services.diffViewProvider.originalContent || "",
newContent,
)
const sharedMessageProps: ClineSayTool = {
tool: fileExists ? "editedExistingFile" : "newFileCreated",
path: getReadablePath(
config.cwd,
uiHelpers.removeClosingTag(block, block.params.path ? "path" : "absolutePath", relPath),
),
content: diff || content,
content: focusedDiff || diff || content,
operationIsLocatedInWorkspace: await isLocatedInWorkspace(relPath),
}
const partialMessage = JSON.stringify(sharedMessageProps)
@@ -133,11 +141,18 @@ export class WriteToFileToolHandler implements IFullyManagedTool {
const { relPath, absolutePath, fileExists, diff, content, newContent, workspaceContext } = result
// Create focused diff for the preview
const focusedDiff = DiffUtils.createFocusedDiff(
relPath,
config.services.diffViewProvider.originalContent || "",
newContent,
)
// Handle approval flow
const sharedMessageProps: ClineSayTool = {
tool: fileExists ? "editedExistingFile" : "newFileCreated",
path: getReadablePath(config.cwd, relPath),
content: diff || content,
content: focusedDiff || diff || content,
operationIsLocatedInWorkspace: await isLocatedInWorkspace(relPath),
}
// if isEditingFile false, that means we have the full contents of the file already.
+100
View File
@@ -0,0 +1,100 @@
import * as diff from "diff"
export class DiffUtils {
/**
* Creates a focused diff showing the changes with context
* @param filename Name of the file being modified
* @param oldContent Original content of the file
* @param newContent New content after changes
* @param contextLines Number of lines of context to show before and after changes (default: 5)
* @returns Formatted diff string with context
*/
static createFocusedDiff(
filename: string,
oldContent: string = "",
newContent: string = "",
contextLines: number = 5,
): string {
const changes = diff.diffLines(oldContent, newContent, { newlineIsToken: true })
let result = `--- ${filename}\n+++ ${filename}\n`
let currentLine = 1
let inChangeBlock = false
let contextBuffer: string[] = []
let changeLines: string[] = []
const flushContextBuffer = () => {
if (contextBuffer.length > 0) {
result += contextBuffer.join("\n") + "\n"
contextBuffer = []
}
}
const addContextSeparator = () => {
if (result && !result.endsWith("...\n")) {
result += "...\n"
}
}
for (const part of changes) {
const lines = part.value.split("\n")
// Remove empty string that split adds when there's a trailing newline
if (lines[lines.length - 1] === "") {
lines.pop()
}
if (part.added || part.removed) {
if (!inChangeBlock) {
inChangeBlock = true
// Add context before the change
flushContextBuffer()
addContextSeparator()
}
const prefix = part.added ? "+" : "-"
changeLines.push(...lines.map((line) => (line ? `${prefix} ${line}` : prefix)))
} else {
if (inChangeBlock) {
// We're transitioning from changed lines to unchanged lines
inChangeBlock = false
// Add the changed lines
result += changeLines.join("\n") + "\n"
changeLines = []
// Add context after the change
const contextAfter = lines.slice(0, contextLines)
if (contextAfter.length > 0) {
result += contextAfter.map((line) => ` ${line}`).join("\n") + "\n"
}
if (lines.length > contextLines) {
result += "...\n"
}
} else {
// We're in an unchanged section, buffer the context
if (contextBuffer.length < contextLines) {
// Add to buffer if we're still collecting context
contextBuffer.push(...lines.map((line) => ` ${line}`))
// Keep only the last contextLines
if (contextBuffer.length > contextLines) {
contextBuffer = contextBuffer.slice(-contextLines)
if (contextBuffer[0] !== "...\n") {
contextBuffer.unshift("...\n")
}
}
}
}
currentLine += lines.length
}
}
// Handle any remaining changed lines at the end
if (changeLines.length > 0) {
flushContextBuffer()
addContextSeparator()
result += changeLines.join("\n") + "\n"
}
return result
}
}