Optimize diff storage and improve Open Diff button behavior

- Stop storing full file contents in conversation history to reduce memory
- Compute and store only startLine/startLines for accurate line numbers on reload
- Open Diff button now only shows on last pending edit request
- Button uses stored diff data directly instead of re-reading file
- Hide button when edit result arrives (edit no longer pending)
- Show simple completion messages for Edit/MultiEdit/Write results
- Use virtual document scheme (claude-diff:) for read-only diff viewer
- Simplify tool result handling for Read/TodoWrite

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
andrepimenta
2025-12-01 23:19:04 +00:00
parent d20d8667f3
commit da46d5e3d9
2 changed files with 317 additions and 169 deletions

View File

@@ -6,6 +6,17 @@ import getHtml from './ui';
const exec = util.promisify(cp.exec);
// Storage for diff content (used by DiffContentProvider)
const diffContentStore = new Map<string, string>();
// Custom TextDocumentContentProvider for read-only diff views
class DiffContentProvider implements vscode.TextDocumentContentProvider {
provideTextDocumentContent(uri: vscode.Uri): string {
const content = diffContentStore.get(uri.path);
return content || '';
}
}
export function activate(context: vscode.ExtensionContext) {
console.log('Claude Code Chat extension is being activated!');
const provider = new ClaudeChatProvider(context.extensionUri, context);
@@ -23,6 +34,10 @@ export function activate(context: vscode.ExtensionContext) {
const webviewProvider = new ClaudeChatWebviewProvider(context.extensionUri, context, provider);
vscode.window.registerWebviewViewProvider('claude-code-chat.chat', webviewProvider);
// Register custom content provider for read-only diff views
const diffProvider = new DiffContentProvider();
context.subscriptions.push(vscode.workspace.registerTextDocumentContentProvider('claude-diff', diffProvider));
// Listen for configuration changes
const configChangeDisposable = vscode.workspace.onDidChangeConfiguration(event => {
if (event.affectsConfiguration('claudeCodeChat.wsl')) {
@@ -300,6 +315,9 @@ class ClaudeChatProvider {
case 'openDiff':
this._openDiffEditor(message.oldContent, message.newContent, message.filePath);
return;
case 'openDiffByIndex':
this._openDiffByMessageIndex(message.messageIndex);
return;
case 'createImageFile':
this._createImageFile(message.imageData, message.imageType);
return;
@@ -657,7 +675,7 @@ class ClaudeChatProvider {
});
}
private _processJsonStreamData(jsonData: any) {
private async _processJsonStreamData(jsonData: any) {
switch (jsonData.type) {
case 'system':
if (jsonData.subtype === 'init') {
@@ -717,6 +735,7 @@ class ClaudeChatProvider {
// Show tool execution with better formatting
const toolInfo = `🔧 Executing: ${content.name}`;
let toolInput = '';
let fileContentBefore: string | undefined;
if (content.input) {
// Special formatting for TodoWrite to make it more readable
@@ -731,6 +750,44 @@ class ClaudeChatProvider {
// Send raw input to UI for formatting
toolInput = '';
}
// For Edit/MultiEdit/Write, read current file content (before state)
if ((content.name === 'Edit' || content.name === 'MultiEdit' || content.name === 'Write') && content.input.file_path) {
try {
const fileUri = vscode.Uri.file(content.input.file_path);
const fileData = await vscode.workspace.fs.readFile(fileUri);
fileContentBefore = Buffer.from(fileData).toString('utf8');
} catch {
// File might not exist yet (for Write), that's ok
fileContentBefore = '';
}
}
}
// Compute startLine(s) while we have the file content
let startLine: number | undefined;
let startLines: number[] | undefined;
if (fileContentBefore !== undefined) {
if (content.name === 'Edit' && content.input.old_string) {
const position = fileContentBefore.indexOf(content.input.old_string);
if (position !== -1) {
const textBefore = fileContentBefore.substring(0, position);
startLine = (textBefore.match(/\n/g) || []).length + 1;
} else {
startLine = 1;
}
} else if (content.name === 'MultiEdit' && content.input.edits) {
startLines = content.input.edits.map((edit: any) => {
if (edit.old_string) {
const position = fileContentBefore!.indexOf(edit.old_string);
if (position !== -1) {
const textBefore = fileContentBefore!.substring(0, position);
return (textBefore.match(/\n/g) || []).length + 1;
}
}
return 1;
});
}
}
// Show tool use and save to conversation
@@ -740,7 +797,10 @@ class ClaudeChatProvider {
toolInfo: toolInfo,
toolInput: toolInput,
rawInput: content.input,
toolName: content.name
toolName: content.name,
fileContentBefore: fileContentBefore,
startLine: startLine,
startLines: startLines
}
});
}
@@ -762,11 +822,25 @@ class ClaudeChatProvider {
const isError = content.is_error || false;
// Find the last tool use to get the tool name and input
// Find the last tool use to get the tool name, input, and computed startLine
const lastToolUse = this._currentConversation[this._currentConversation.length - 1]
const toolName = lastToolUse?.data?.toolName;
const rawInput = lastToolUse?.data?.rawInput;
const startLine = lastToolUse?.data?.startLine;
const startLines = lastToolUse?.data?.startLines;
// For Edit/MultiEdit/Write, read current file content (after state)
let fileContentAfter: string | undefined;
if ((toolName === 'Edit' || toolName === 'MultiEdit' || toolName === 'Write') && rawInput?.file_path && !isError) {
try {
const fileUri = vscode.Uri.file(rawInput.file_path);
const fileData = await vscode.workspace.fs.readFile(fileUri);
fileContentAfter = Buffer.from(fileData).toString('utf8');
} catch {
// File read failed, that's ok
}
}
// Don't send tool result for Read and TodoWrite tools unless there's an error
if ((toolName === 'Read' || toolName === 'TodoWrite') && !isError) {
@@ -791,7 +865,10 @@ class ClaudeChatProvider {
isError: isError,
toolUseId: content.tool_use_id,
toolName: toolName,
rawInput: rawInput
rawInput: rawInput,
fileContentAfter: fileContentAfter,
startLine: startLine,
startLines: startLines
}
});
}
@@ -1818,14 +1895,30 @@ class ClaudeChatProvider {
this._conversationStartTime = new Date().toISOString();
}
// The message index will be the current length (0-indexed position after push)
const messageIndex = this._currentConversation.length;
// For tool messages that support diff, include the message index
const messageToSend = (message.type === 'toolUse' || message.type === 'toolResult')
? { ...message, data: { ...message.data, messageIndex } }
: message;
// Send to UI using the helper method
this._postMessage(message);
this._postMessage(messageToSend);
// Strip fileContentBefore/fileContentAfter from saved data to reduce storage
// Keep startLine/startLines which are small and needed for accurate line numbers on reload
let dataToSave = message.data;
if (message.type === 'toolUse' || message.type === 'toolResult') {
const { fileContentBefore, fileContentAfter, ...rest } = message.data || {};
dataToSave = rest; // startLine and startLines are preserved in rest
}
// Save to conversation
this._currentConversation.push({
timestamp: new Date().toISOString(),
messageType: message.type,
data: message.data
data: dataToSave
});
// Persist conversation
@@ -2099,9 +2192,14 @@ class ClaudeChatProvider {
}
}
// For tool messages, include the message index so Open Diff buttons work
const messageData = (message.messageType === 'toolUse' || message.messageType === 'toolResult')
? { ...message.data, messageIndex: i }
: message.data;
this._postMessage({
type: message.messageType,
data: message.data
data: messageData
});
if (message.messageType === 'userInput') {
try {
@@ -2343,35 +2441,77 @@ class ClaudeChatProvider {
}
}
private async _openDiffEditor(oldContent: string, newContent: string, filePath: string) {
private async _openDiffByMessageIndex(messageIndex: number) {
try {
const storageUri = this._context.storageUri;
if (!storageUri) {
vscode.window.showErrorMessage('No storage location available');
const message = this._currentConversation[messageIndex];
if (!message) {
console.error('Message not found at index:', messageIndex);
return;
}
const baseName = path.basename(filePath);
const ext = path.extname(filePath);
const nameWithoutExt = baseName.slice(0, -ext.length) || baseName;
const timestamp = Date.now();
const data = message.data;
const toolName = data.toolName;
const rawInput = data.rawInput;
let filePath = rawInput?.file_path || '';
let oldContent = '';
let newContent = '';
// Create temp files in extension's storage directory
const tempDirUri = vscode.Uri.joinPath(storageUri, 'diff-temp');
// Ensure temp directory exists
try {
await vscode.workspace.fs.createDirectory(tempDirUri);
} catch {
// Directory might already exist, ignore error
if (!filePath) {
console.error('No file path found for message at index:', messageIndex);
return;
}
const oldUri = vscode.Uri.joinPath(tempDirUri, `${nameWithoutExt}.old.${timestamp}${ext}`);
const newUri = vscode.Uri.joinPath(tempDirUri, `${nameWithoutExt}.new.${timestamp}${ext}`);
// Read current file from disk - this is the "before" state since edit hasn't been applied yet
try {
const fileUri = vscode.Uri.file(filePath);
const fileData = await vscode.workspace.fs.readFile(fileUri);
oldContent = Buffer.from(fileData).toString('utf8');
} catch {
// File might not exist yet (for Write creating new file)
oldContent = '';
}
// Write content to temp files using VS Code filesystem API
await vscode.workspace.fs.writeFile(oldUri, Buffer.from(oldContent, 'utf8'));
await vscode.workspace.fs.writeFile(newUri, Buffer.from(newContent, 'utf8'));
// Compute "after" state by applying the edit to current file
if (toolName === 'Edit' && rawInput?.old_string && rawInput?.new_string) {
newContent = oldContent.replace(rawInput.old_string, rawInput.new_string);
} else if (toolName === 'MultiEdit' && rawInput?.edits) {
newContent = oldContent;
for (const edit of rawInput.edits) {
if (edit.old_string && edit.new_string) {
newContent = newContent.replace(edit.old_string, edit.new_string);
}
}
} else if (toolName === 'Write' && rawInput?.content) {
newContent = rawInput.content;
}
if (oldContent !== newContent) {
await this._openDiffEditor(oldContent, newContent, filePath);
} else {
vscode.window.showInformationMessage('No changes to show - the edit may have already been applied.');
}
} catch (error) {
console.error('Error opening diff by message index:', error);
}
}
private async _openDiffEditor(oldContent: string, newContent: string, filePath: string) {
try {
// oldContent and newContent are now full file contents passed from the webview
const baseName = path.basename(filePath);
const timestamp = Date.now();
// Create unique paths for the virtual documents
const oldPath = `/${timestamp}/old/${baseName}`;
const newPath = `/${timestamp}/new/${baseName}`;
// Store content in the global store for the content provider
diffContentStore.set(oldPath, oldContent);
diffContentStore.set(newPath, newContent);
// Create URIs with our custom scheme
const oldUri = vscode.Uri.parse(`claude-diff:${oldPath}`);
const newUri = vscode.Uri.parse(`claude-diff:${newPath}`);
// Ensure side-by-side diff mode is enabled
const diffConfig = vscode.workspace.getConfiguration('diffEditor');
@@ -2383,27 +2523,20 @@ class ClaudeChatProvider {
// Open diff editor
await vscode.commands.executeCommand('vscode.diff', oldUri, newUri, `${baseName} (Changes)`);
// Track which files need to be cleaned up
const filesToCleanup = new Set([oldUri.toString(), newUri.toString()]);
// Listen for document close events to clean up temp files
const closeListener = vscode.workspace.onDidCloseTextDocument(async (doc) => {
if (filesToCleanup.has(doc.uri.toString())) {
filesToCleanup.delete(doc.uri.toString());
try {
await vscode.workspace.fs.delete(doc.uri, { useTrash: false });
} catch {
// File might already be deleted, ignore
}
// Dispose listener when both files are cleaned up
if (filesToCleanup.size === 0) {
closeListener.dispose();
}
// Clean up stored content when documents are closed
const closeListener = vscode.workspace.onDidCloseTextDocument((doc) => {
if (doc.uri.toString() === oldUri.toString()) {
diffContentStore.delete(oldPath);
}
if (doc.uri.toString() === newUri.toString()) {
diffContentStore.delete(newPath);
}
// Dispose listener when both are cleaned up
if (!diffContentStore.has(oldPath) && !diffContentStore.has(newPath)) {
closeListener.dispose();
}
});
// Also add to disposables to clean up on extension deactivate
this._disposables.push(closeListener);
} catch (error) {
vscode.window.showErrorMessage(`Failed to open diff editor: ${error}`);

View File

@@ -15,22 +15,19 @@ const getScript = (isTelemetryEnabled: boolean) => `<script>
let selectedFileIndex = -1;
let planModeEnabled = false;
let thinkingModeEnabled = false;
let lastPendingEditIndex = -1; // Track the last Edit/MultiEdit/Write toolUse without result
let lastPendingEditData = null; // Store diff data for the pending edit { filePath, oldContent, newContent }
// Storage for diff data to enable "Open Diff" functionality
const diffDataStore = {};
function openDiffEditor(diffId) {
const data = diffDataStore[diffId];
if (!data) {
console.error('Diff data not found for id:', diffId);
return;
// Open diff using stored data (no file read needed)
function openDiffEditor() {
if (lastPendingEditData) {
vscode.postMessage({
type: 'openDiff',
filePath: lastPendingEditData.filePath,
oldContent: lastPendingEditData.oldContent,
newContent: lastPendingEditData.newContent
});
}
vscode.postMessage({
type: 'openDiff',
oldContent: data.oldString,
newContent: data.newString,
filePath: data.filePath
});
}
function shouldAutoScroll(messagesDiv) {
@@ -179,12 +176,52 @@ const getScript = (isTelemetryEnabled: boolean) => `<script>
} else {
// Format raw input with expandable content for long values
// Use diff format for Edit, MultiEdit, and Write tools, regular format for others
if (data.toolName === 'Edit') {
contentDiv.innerHTML = formatEditToolDiff(data.rawInput);
} else if (data.toolName === 'MultiEdit') {
contentDiv.innerHTML = formatMultiEditToolDiff(data.rawInput);
} else if (data.toolName === 'Write') {
contentDiv.innerHTML = formatWriteToolDiff(data.rawInput);
if (data.toolName === 'Edit' || data.toolName === 'MultiEdit' || data.toolName === 'Write') {
// Only show Open Diff button if we have fileContentBefore (live session, not reload)
const showButton = data.fileContentBefore !== undefined && data.messageIndex >= 0;
// Hide any existing pending edit button before showing new one
if (showButton && lastPendingEditIndex >= 0) {
const prevContent = document.querySelector('[data-edit-message-index="' + lastPendingEditIndex + '"]');
if (prevContent) {
const btn = prevContent.querySelector('.diff-open-btn');
if (btn) btn.style.display = 'none';
}
lastPendingEditData = null;
}
if (showButton) {
lastPendingEditIndex = data.messageIndex;
contentDiv.setAttribute('data-edit-message-index', data.messageIndex);
// Compute and store diff data for when button is clicked
const oldContent = data.fileContentBefore || '';
let newContent = oldContent;
if (data.toolName === 'Edit' && data.rawInput.old_string && data.rawInput.new_string) {
newContent = oldContent.replace(data.rawInput.old_string, data.rawInput.new_string);
} else if (data.toolName === 'MultiEdit' && data.rawInput.edits) {
for (const edit of data.rawInput.edits) {
if (edit.old_string && edit.new_string) {
newContent = newContent.replace(edit.old_string, edit.new_string);
}
}
} else if (data.toolName === 'Write' && data.rawInput.content) {
newContent = data.rawInput.content;
}
lastPendingEditData = {
filePath: data.rawInput.file_path,
oldContent: oldContent,
newContent: newContent
};
}
if (data.toolName === 'Edit') {
contentDiv.innerHTML = formatEditToolDiff(data.rawInput, data.fileContentBefore, showButton, data.startLine);
} else if (data.toolName === 'MultiEdit') {
contentDiv.innerHTML = formatMultiEditToolDiff(data.rawInput, data.fileContentBefore, showButton, data.startLines);
} else {
contentDiv.innerHTML = formatWriteToolDiff(data.rawInput, data.fileContentBefore, showButton);
}
} else {
contentDiv.innerHTML = formatToolInputUI(data.rawInput);
}
@@ -243,94 +280,42 @@ const getScript = (isTelemetryEnabled: boolean) => `<script>
const messagesDiv = document.getElementById('messages');
const shouldScroll = shouldAutoScroll(messagesDiv);
// Show detailed diff for Edit, MultiEdit, and Write tools
if (!data.isError && data.rawInput) {
if (data.toolName === 'Edit' && data.rawInput.file_path && data.rawInput.old_string && data.rawInput.new_string) {
const parsed = parseToolResult(data.content);
const diffHTML = generateUnifiedDiffHTML(
data.rawInput.old_string,
data.rawInput.new_string,
data.rawInput.file_path,
parsed.startLine
);
const messageDiv = document.createElement('div');
messageDiv.className = 'message tool-result';
messageDiv.innerHTML = diffHTML;
messagesDiv.appendChild(messageDiv);
scrollToBottomIfNeeded(messagesDiv, shouldScroll);
return;
} else if (data.toolName === 'MultiEdit' && data.rawInput.file_path && data.rawInput.edits) {
const parsed = parseToolResult(data.content);
let html = '';
const formattedPath = formatFilePath(data.rawInput.file_path);
html += '<div class="diff-file-path" onclick="openFileInEditor(\\\'' + escapeHtml(data.rawInput.file_path) + '\\\')">' + formattedPath + '</div>\\n';
html += '<div class="diff-container">';
html += '<div class="diff-header">' + data.rawInput.edits.length + ' edit' + (data.rawInput.edits.length > 1 ? 's' : '') + '</div>';
for (let i = 0; i < data.rawInput.edits.length; i++) {
const edit = data.rawInput.edits[i];
if (i > 0) html += '<div class="diff-edit-separator"></div>';
html += '<div class="edit-number">Edit #' + (i + 1) + '</div>';
const editDiff = generateUnifiedDiffHTML(edit.old_string, edit.new_string, data.rawInput.file_path, parsed.startLine);
html += editDiff;
// When result comes in for Edit/MultiEdit/Write, hide the Open Diff button on the request
// since the edit has now been applied (no longer pending)
if (lastPendingEditIndex >= 0) {
// Find and hide the button on the corresponding toolUse
const toolUseContent = document.querySelector('[data-edit-message-index="' + lastPendingEditIndex + '"]');
if (toolUseContent) {
const btn = toolUseContent.querySelector('.diff-open-btn');
if (btn) {
btn.style.display = 'none';
}
html += '</div>';
const messageDiv = document.createElement('div');
messageDiv.className = 'message tool-result';
messageDiv.innerHTML = html;
messagesDiv.appendChild(messageDiv);
scrollToBottomIfNeeded(messagesDiv, shouldScroll);
return;
} else if (data.toolName === 'Write' && data.rawInput.file_path && data.rawInput.content) {
const parsed = parseToolResult(data.content);
const newLines = data.rawInput.content.split('\\n');
let html = '';
const formattedPath = formatFilePath(data.rawInput.file_path);
html += '<div class="diff-file-path" onclick="openFileInEditor(\\\'' + escapeHtml(data.rawInput.file_path) + '\\\')">' + formattedPath + '</div>\\n';
html += '<div class="diff-container">';
html += '<div class="diff-header">New file: Lines 1-' + newLines.length + '</div>';
for (let i = 0; i < newLines.length; i++) {
const lineNumStr = (i + 1).toString().padStart(3);
html += '<div class="diff-line added">+' + lineNumStr + ' ' + escapeHtml(newLines[i]) + '</div>';
}
html += '</div>';
html += '<div class="diff-summary">Summary: +' + newLines.length + ' line' + (newLines.length > 1 ? 's' : '') + ' added</div>';
const messageDiv = document.createElement('div');
messageDiv.className = 'message tool-result';
messageDiv.innerHTML = html;
messagesDiv.appendChild(messageDiv);
scrollToBottomIfNeeded(messagesDiv, shouldScroll);
return;
}
lastPendingEditIndex = -1;
lastPendingEditData = null;
}
// For Read and TodoWrite tools with hidden flag, just hide loading state and show completion message
if (data.hidden && (data.toolName === 'Read' || data.toolName === 'TodoWrite') && !data.isError) {
return
// Show completion message
const toolName = data.toolName;
// For Read and TodoWrite tools, just hide loading state (no result message needed)
if ((data.toolName === 'Read' || data.toolName === 'TodoWrite') && !data.isError) {
return;
}
// For Edit/MultiEdit/Write, show simple completion message (diff is already shown on request)
if ((data.toolName === 'Edit' || data.toolName === 'MultiEdit' || data.toolName === 'Write') && !data.isError) {
let completionText;
if (toolName === 'Read') {
completionText = '✅ Read completed';
} else if (toolName === 'TodoWrite') {
completionText = '✅ Update Todos completed';
if (data.toolName === 'Edit') {
completionText = '✅ Edit completed';
} else if (data.toolName === 'MultiEdit') {
completionText = '✅ MultiEdit completed';
} else {
completionText = '✅ ' + toolName + ' completed';
completionText = '✅ Write completed';
}
addMessage(completionText, 'system');
return; // Don't show the result message
scrollToBottomIfNeeded(messagesDiv, shouldScroll);
return;
}
if(data.isError && data.content === "File has not been read yet. Read it first before writing to it."){
if(data.isError && data.content?.includes("File has not been read yet. Read it first before writing to it.")){
return addMessage("File has not been read yet. Let me read it first before writing to it.", 'system');
}
@@ -521,21 +506,15 @@ const getScript = (isTelemetryEnabled: boolean) => `<script>
}
// Generate unified diff HTML with line numbers
function generateUnifiedDiffHTML(oldString, newString, filePath, startLine = 1) {
// showButton controls whether to show the "Open Diff" button
function generateUnifiedDiffHTML(oldString, newString, filePath, startLine = 1, showButton = false) {
const oldLines = oldString.split('\\n');
const newLines = newString.split('\\n');
const diff = computeLineDiff(oldLines, newLines);
// Generate unique ID for this diff
// Generate unique ID for this diff (used for truncation)
const diffId = 'diff_' + Math.random().toString(36).substr(2, 9);
// Store diff data for "Open Diff" functionality
diffDataStore[diffId] = {
oldString: oldString,
newString: newString,
filePath: filePath
};
let html = '';
const formattedPath = formatFilePath(filePath);
@@ -630,16 +609,18 @@ const getScript = (isTelemetryEnabled: boolean) => `<script>
if (summary) {
html += '<div class="diff-summary-row">';
html += '<span class="diff-summary">Summary: ' + summary + '</span>';
html += '<button class="diff-open-btn" onclick="openDiffEditor(\\\'' + diffId + '\\\')" title="Open side-by-side diff in VS Code">';
html += '<svg width="14" height="14" viewBox="0 0 16 16"><rect x="1" y="1" width="6" height="14" rx="1" fill="none" stroke="currentColor" stroke-opacity="0.5"/><rect x="9" y="1" width="6" height="14" rx="1" fill="none" stroke="currentColor" stroke-opacity="0.5"/><line x1="2.5" y1="4" x2="5.5" y2="4" stroke="#f85149" stroke-width="1.5"/><line x1="2.5" y1="7" x2="5.5" y2="7" stroke="currentColor" stroke-opacity="0.4" stroke-width="1.5"/><line x1="2.5" y1="10" x2="5.5" y2="10" stroke="currentColor" stroke-opacity="0.4" stroke-width="1.5"/><line x1="10.5" y1="4" x2="13.5" y2="4" stroke="currentColor" stroke-opacity="0.4" stroke-width="1.5"/><line x1="10.5" y1="7" x2="13.5" y2="7" stroke="#3fb950" stroke-width="1.5"/><line x1="10.5" y1="10" x2="13.5" y2="10" stroke="#3fb950" stroke-width="1.5"/></svg>';
html += 'Open Diff</button>';
if (showButton) {
html += '<button class="diff-open-btn" onclick="openDiffEditor()" title="Open side-by-side diff in VS Code">';
html += '<svg width="14" height="14" viewBox="0 0 16 16"><rect x="1" y="1" width="6" height="14" rx="1" fill="none" stroke="currentColor" stroke-opacity="0.5"/><rect x="9" y="1" width="6" height="14" rx="1" fill="none" stroke="currentColor" stroke-opacity="0.5"/><line x1="2.5" y1="4" x2="5.5" y2="4" stroke="#f85149" stroke-width="1.5"/><line x1="2.5" y1="7" x2="5.5" y2="7" stroke="currentColor" stroke-opacity="0.4" stroke-width="1.5"/><line x1="2.5" y1="10" x2="5.5" y2="10" stroke="currentColor" stroke-opacity="0.4" stroke-width="1.5"/><line x1="10.5" y1="4" x2="13.5" y2="4" stroke="currentColor" stroke-opacity="0.4" stroke-width="1.5"/><line x1="10.5" y1="7" x2="13.5" y2="7" stroke="#3fb950" stroke-width="1.5"/><line x1="10.5" y1="10" x2="13.5" y2="10" stroke="#3fb950" stroke-width="1.5"/></svg>';
html += 'Open Diff</button>';
}
html += '</div>';
}
return html;
}
function formatEditToolDiff(input) {
function formatEditToolDiff(input, fileContentBefore, showButton = false, providedStartLine = null) {
if (!input || typeof input !== 'object') {
return formatToolInputUI(input);
}
@@ -649,11 +630,21 @@ const getScript = (isTelemetryEnabled: boolean) => `<script>
return formatToolInputUI(input);
}
// Show full diff (line numbers will be approximate until result comes back)
return generateUnifiedDiffHTML(input.old_string, input.new_string, input.file_path, 1);
// Use provided startLine if available (from saved data), otherwise compute from fileContentBefore
let startLine = providedStartLine || 1;
if (!providedStartLine && fileContentBefore) {
const position = fileContentBefore.indexOf(input.old_string);
if (position !== -1) {
// Count newlines before the match to get line number
const textBefore = fileContentBefore.substring(0, position);
startLine = (textBefore.match(/\\n/g) || []).length + 1;
}
}
return generateUnifiedDiffHTML(input.old_string, input.new_string, input.file_path, startLine, showButton);
}
function formatMultiEditToolDiff(input) {
function formatMultiEditToolDiff(input, fileContentBefore, showButton = false, providedStartLines = null) {
if (!input || typeof input !== 'object') {
return formatToolInputUI(input);
}
@@ -674,39 +665,60 @@ const getScript = (isTelemetryEnabled: boolean) => `<script>
if (index > 0) {
html += '<div class="diff-edit-separator"></div>';
}
// Use provided startLine if available, otherwise compute from fileContentBefore
let startLine = (providedStartLines && providedStartLines[index]) || 1;
if (!providedStartLines && fileContentBefore) {
const position = fileContentBefore.indexOf(edit.old_string);
if (position !== -1) {
const textBefore = fileContentBefore.substring(0, position);
startLine = (textBefore.match(/\\n/g) || []).length + 1;
}
}
const oldLines = edit.old_string.split('\\n');
const newLines = edit.new_string.split('\\n');
const diff = computeLineDiff(oldLines, newLines);
html += '<div class="diff-container">';
html += '<div class="diff-header">Edit ' + (index + 1) + '</div>';
html += '<div class="diff-header">Edit ' + (index + 1) + ' (Line ' + startLine + ')</div>';
let addedCount = 0;
let removedCount = 0;
let lineNum = startLine;
for (const change of diff) {
let prefix, cssClass;
if (change.type === 'context') {
prefix = ' ';
cssClass = 'context';
lineNum++;
} else if (change.type === 'added') {
prefix = '+';
cssClass = 'added';
addedCount++;
lineNum++;
} else {
prefix = '-';
cssClass = 'removed';
removedCount++;
}
html += '<div class="diff-line ' + cssClass + '">' + prefix + ' ' + escapeHtml(change.content) + '</div>';
const lineNumStr = (change.type === 'removed' ? '' : lineNum - 1).toString().padStart(3);
html += '<div class="diff-line ' + cssClass + '">' + prefix + lineNumStr + ' ' + escapeHtml(change.content) + '</div>';
}
html += '</div>';
}
});
// Add summary row with Open Diff button
html += '<div class="diff-summary-row">';
html += '<span class="diff-summary">Summary: ' + input.edits.length + ' edit' + (input.edits.length > 1 ? 's' : '') + '</span>';
if (showButton) {
html += '<button class="diff-open-btn" onclick="openDiffEditor()" title="Open side-by-side diff in VS Code">';
html += '<svg width="14" height="14" viewBox="0 0 16 16"><rect x="1" y="1" width="6" height="14" rx="1" fill="none" stroke="currentColor" stroke-opacity="0.5"/><rect x="9" y="1" width="6" height="14" rx="1" fill="none" stroke="currentColor" stroke-opacity="0.5"/><line x1="2.5" y1="4" x2="5.5" y2="4" stroke="#f85149" stroke-width="1.5"/><line x1="2.5" y1="7" x2="5.5" y2="7" stroke="currentColor" stroke-opacity="0.4" stroke-width="1.5"/><line x1="2.5" y1="10" x2="5.5" y2="10" stroke="currentColor" stroke-opacity="0.4" stroke-width="1.5"/><line x1="10.5" y1="4" x2="13.5" y2="4" stroke="currentColor" stroke-opacity="0.4" stroke-width="1.5"/><line x1="10.5" y1="7" x2="13.5" y2="7" stroke="#3fb950" stroke-width="1.5"/><line x1="10.5" y1="10" x2="13.5" y2="10" stroke="#3fb950" stroke-width="1.5"/></svg>';
html += 'Open Diff</button>';
}
html += '</div>';
return html;
}
function formatWriteToolDiff(input) {
function formatWriteToolDiff(input, fileContentBefore, showButton = false) {
if (!input || typeof input !== 'object') {
return formatToolInputUI(input);
}
@@ -716,8 +728,11 @@ const getScript = (isTelemetryEnabled: boolean) => `<script>
return formatToolInputUI(input);
}
// Show full content as added lines (new file)
return generateUnifiedDiffHTML('', input.content, input.file_path, 1);
// fileContentBefore may be empty string if new file, or existing content if overwriting
const fullFileBefore = fileContentBefore || '';
// Show full content as added lines (new file or replacement)
return generateUnifiedDiffHTML(fullFileBefore, input.content, input.file_path, 1, showButton);
}
function escapeHtml(text) {