mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-20 12:21:13 -05:00
Make diff view editable
This commit is contained in:
140
src/ClaudeDev.ts
140
src/ClaudeDev.ts
@@ -796,77 +796,51 @@ export class ClaudeDev {
|
||||
.then(() => true)
|
||||
.catch(() => false)
|
||||
|
||||
let originalContent: string
|
||||
if (fileExists) {
|
||||
const originalContent = await fs.readFile(absolutePath, "utf-8")
|
||||
originalContent = await fs.readFile(absolutePath, "utf-8")
|
||||
// fix issue where claude always removes newline from the file
|
||||
if (originalContent.endsWith("\n") && !newContent.endsWith("\n")) {
|
||||
newContent += "\n"
|
||||
const eol = originalContent.includes("\r\n") ? "\r\n" : "\n"
|
||||
if (originalContent.endsWith(eol) && !newContent.endsWith(eol)) {
|
||||
newContent += eol
|
||||
}
|
||||
} else {
|
||||
originalContent = ""
|
||||
}
|
||||
// condensed patch to return to claude
|
||||
const diffResult = diff.createPatch(absolutePath, originalContent, newContent)
|
||||
// full diff representation for webview
|
||||
const diffRepresentation = diff
|
||||
.diffLines(originalContent, newContent)
|
||||
.map((part) => {
|
||||
const prefix = part.added ? "+" : part.removed ? "-" : " "
|
||||
return (part.value || "")
|
||||
.split("\n")
|
||||
.map((line) => (line ? prefix + line : ""))
|
||||
.join("\n")
|
||||
})
|
||||
.join("")
|
||||
|
||||
// Create virtual document with new file, then open diff editor
|
||||
const fileName = path.basename(absolutePath)
|
||||
// Create a temporary file with the new content
|
||||
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "claude-dev-"))
|
||||
const tempFilePath = path.join(tempDir, path.basename(absolutePath))
|
||||
await fs.writeFile(tempFilePath, newContent)
|
||||
|
||||
vscode.commands.executeCommand(
|
||||
"vscode.diff",
|
||||
vscode.Uri.file(absolutePath),
|
||||
// to create a virtual doc we use a uri scheme registered in extension.ts, which then converts this base64 content into a text document
|
||||
// (providing file name with extension in the uri lets vscode know the language of the file and apply syntax highlighting)
|
||||
vscode.Uri.parse(`claude-dev-diff:${fileName}`).with({
|
||||
query: Buffer.from(newContent).toString("base64"),
|
||||
fileExists
|
||||
? vscode.Uri.file(absolutePath)
|
||||
: vscode.Uri.parse(`claude-dev-diff:${path.basename(absolutePath)}`).with({
|
||||
query: Buffer.from("").toString("base64"),
|
||||
}),
|
||||
`${fileName}: Original ↔ Suggested Changes`
|
||||
vscode.Uri.file(tempFilePath),
|
||||
`${path.basename(absolutePath)}: ${fileExists ? "Original ↔ Suggested Changes" : "New File"} (Editable)`
|
||||
)
|
||||
|
||||
const { response, text, images } = await this.ask(
|
||||
let userResponse: {
|
||||
response: ClaudeAskResponse
|
||||
text?: string
|
||||
images?: string[]
|
||||
}
|
||||
if (fileExists) {
|
||||
const suggestedDiff = diff.createPatch(relPath, originalContent, newContent)
|
||||
userResponse = await this.ask(
|
||||
"tool",
|
||||
JSON.stringify({
|
||||
tool: "editedExistingFile",
|
||||
path: this.getReadablePath(relPath),
|
||||
diff: diffRepresentation,
|
||||
diff: suggestedDiff,
|
||||
} as ClaudeSayTool)
|
||||
)
|
||||
if (response !== "yesButtonTapped") {
|
||||
if (isLast) {
|
||||
await this.closeDiffViews()
|
||||
}
|
||||
if (response === "messageResponse") {
|
||||
await this.say("user_feedback", text, images)
|
||||
return this.formatIntoToolResponse(await this.formatGenericToolFeedback(text), images)
|
||||
}
|
||||
return "The user denied this operation."
|
||||
}
|
||||
await fs.writeFile(absolutePath, newContent)
|
||||
// Finish by opening the edited file in the editor
|
||||
await vscode.window.showTextDocument(vscode.Uri.file(absolutePath), { preview: false })
|
||||
if (isLast) {
|
||||
await this.closeDiffViews()
|
||||
}
|
||||
return `Changes applied to ${relPath}:\n${diffResult}`
|
||||
} else {
|
||||
const fileName = path.basename(absolutePath)
|
||||
vscode.commands.executeCommand(
|
||||
"vscode.diff",
|
||||
vscode.Uri.parse(`claude-dev-diff:${fileName}`).with({
|
||||
query: Buffer.from("").toString("base64"),
|
||||
}),
|
||||
vscode.Uri.parse(`claude-dev-diff:${fileName}`).with({
|
||||
query: Buffer.from(newContent).toString("base64"),
|
||||
}),
|
||||
`${fileName}: New File`
|
||||
)
|
||||
const { response, text, images } = await this.ask(
|
||||
userResponse = await this.ask(
|
||||
"tool",
|
||||
JSON.stringify({
|
||||
tool: "newFileCreated",
|
||||
@@ -874,23 +848,62 @@ export class ClaudeDev {
|
||||
content: newContent,
|
||||
} as ClaudeSayTool)
|
||||
)
|
||||
}
|
||||
const { response, text, images } = userResponse
|
||||
|
||||
if (response !== "yesButtonTapped") {
|
||||
if (isLast) {
|
||||
await this.closeDiffViews()
|
||||
}
|
||||
// Clean up the temporary file
|
||||
await fs.rm(tempDir, { recursive: true, force: true })
|
||||
if (response === "messageResponse") {
|
||||
await this.say("user_feedback", text, images)
|
||||
return this.formatIntoToolResponse(await this.formatGenericToolFeedback(text), images)
|
||||
}
|
||||
return "The user denied this operation."
|
||||
}
|
||||
|
||||
// Save any unsaved changes in the diff editor
|
||||
const diffDocument = vscode.workspace.textDocuments.find((doc) => doc.uri.fsPath === tempFilePath)
|
||||
if (diffDocument && diffDocument.isDirty) {
|
||||
console.log("saving diff document")
|
||||
await diffDocument.save()
|
||||
}
|
||||
|
||||
// Read the potentially edited content from the temp file
|
||||
const editedContent = await fs.readFile(tempFilePath, "utf-8")
|
||||
if (!fileExists) {
|
||||
await fs.mkdir(path.dirname(absolutePath), { recursive: true })
|
||||
await fs.writeFile(absolutePath, newContent)
|
||||
}
|
||||
await fs.writeFile(absolutePath, editedContent)
|
||||
|
||||
// Clean up the temporary file
|
||||
await fs.rm(tempDir, { recursive: true, force: true })
|
||||
|
||||
// Finish by opening the edited file in the editor
|
||||
await vscode.window.showTextDocument(vscode.Uri.file(absolutePath), { preview: false })
|
||||
if (isLast) {
|
||||
await this.closeDiffViews()
|
||||
}
|
||||
return `New file created and content written to ${relPath}`
|
||||
|
||||
if (editedContent !== newContent) {
|
||||
const diffResult = diff.createPatch(relPath, originalContent, editedContent)
|
||||
const userDiff = diff.createPatch(relPath, newContent, editedContent)
|
||||
await this.say(
|
||||
"user_feedback_diff",
|
||||
JSON.stringify({
|
||||
tool: fileExists ? "editedExistingFile" : "newFileCreated",
|
||||
path: this.getReadablePath(relPath),
|
||||
diff: userDiff,
|
||||
} as ClaudeSayTool)
|
||||
)
|
||||
return `${
|
||||
fileExists ? "Changes applied" : "New file written"
|
||||
} to ${relPath}:\n${diffResult}, the user applied these changes:\n${userDiff}`
|
||||
} else {
|
||||
const diffResult = diff.createPatch(relPath, originalContent, newContent)
|
||||
return `${fileExists ? "Changes applied" : "New file written"} to ${relPath}:\n${diffResult}`
|
||||
}
|
||||
} catch (error) {
|
||||
const errorString = `Error writing file: ${JSON.stringify(serializeError(error))}`
|
||||
@@ -906,10 +919,15 @@ export class ClaudeDev {
|
||||
const tabs = vscode.window.tabGroups.all
|
||||
.map((tg) => tg.tabs)
|
||||
.flat()
|
||||
.filter(
|
||||
(tab) =>
|
||||
tab.input instanceof vscode.TabInputTextDiff && tab.input?.modified?.scheme === "claude-dev-diff"
|
||||
)
|
||||
.filter((tab) => {
|
||||
if (tab.input instanceof vscode.TabInputTextDiff) {
|
||||
const originalPath = (tab.input.original as vscode.Uri).toString()
|
||||
const modifiedPath = (tab.input.modified as vscode.Uri).toString()
|
||||
return originalPath.includes("claude-dev-") || modifiedPath.includes("claude-dev-")
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
for (const tab of tabs) {
|
||||
await vscode.window.tabGroups.close(tab)
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ export function activate(context: vscode.ExtensionContext) {
|
||||
)
|
||||
|
||||
/*
|
||||
We use the text document content provider API to show a diff view for new files/edits by creating a virtual document for the new content.
|
||||
We use the text document content provider API to show an empty text doc for diff view for new files by creating a virtual document for the new content.
|
||||
|
||||
- This API allows you to create readonly documents in VSCode from arbitrary sources, and works by claiming an uri-scheme for which your provider then returns text contents. The scheme must be provided when registering a provider and cannot change afterwards.
|
||||
- Note how the provider doesn't create uris for virtual documents - its role is to provide contents given such an uri. In return, content providers are wired into the open document logic so that providers are always considered.
|
||||
|
||||
@@ -53,6 +53,7 @@ export type ClaudeSay =
|
||||
| "text"
|
||||
| "completion_result"
|
||||
| "user_feedback"
|
||||
| "user_feedback_diff"
|
||||
| "api_req_retried"
|
||||
| "command_output"
|
||||
| "tool"
|
||||
|
||||
@@ -338,6 +338,35 @@ const ChatRow: React.FC<ChatRowProps> = ({
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
case "user_feedback_diff":
|
||||
const tool = JSON.parse(message.text || "{}") as ClaudeSayTool
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: "var(--vscode-editor-inactiveSelectionBackground)",
|
||||
borderRadius: "3px",
|
||||
padding: "8px",
|
||||
whiteSpace: "pre-line",
|
||||
wordWrap: "break-word",
|
||||
}}>
|
||||
<span
|
||||
style={{
|
||||
display: "block",
|
||||
fontStyle: "italic",
|
||||
marginBottom: "8px",
|
||||
opacity: 0.8,
|
||||
}}>
|
||||
The user made the following changes:
|
||||
</span>
|
||||
<CodeBlock
|
||||
diff={tool.diff!}
|
||||
path={tool.path!}
|
||||
syntaxHighlighterStyle={syntaxHighlighterStyle}
|
||||
isExpanded={isExpanded}
|
||||
onToggleExpand={onToggleExpand}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
case "error":
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -4,7 +4,7 @@ import vsDarkPlus from "react-syntax-highlighter/dist/esm/styles/prism/vsc-dark-
|
||||
import DynamicTextArea from "react-textarea-autosize"
|
||||
import { useEvent, useMount } from "react-use"
|
||||
import { Virtuoso, type VirtuosoHandle } from "react-virtuoso"
|
||||
import { ClaudeAsk, ExtensionMessage } from "../../../src/shared/ExtensionMessage"
|
||||
import { ClaudeAsk, ClaudeSayTool, ExtensionMessage } from "../../../src/shared/ExtensionMessage"
|
||||
import { combineApiRequests } from "../../../src/shared/combineApiRequests"
|
||||
import { combineCommandSequences } from "../../../src/shared/combineCommandSequences"
|
||||
import { getApiMetrics } from "../../../src/shared/getApiMetrics"
|
||||
@@ -121,9 +121,22 @@ const ChatView = ({
|
||||
setTextAreaDisabled(false)
|
||||
setClaudeAsk("tool")
|
||||
setEnableButtons(true)
|
||||
const tool = JSON.parse(lastMessage.text || "{}") as ClaudeSayTool
|
||||
switch (tool.tool) {
|
||||
case "editedExistingFile":
|
||||
setPrimaryButtonText("Save")
|
||||
setSecondaryButtonText("Reject")
|
||||
break
|
||||
case "newFileCreated":
|
||||
setPrimaryButtonText("Create")
|
||||
setSecondaryButtonText("Reject")
|
||||
break
|
||||
default:
|
||||
setPrimaryButtonText("Approve")
|
||||
setSecondaryButtonText("Reject")
|
||||
break
|
||||
}
|
||||
break
|
||||
case "command":
|
||||
setTextAreaDisabled(false)
|
||||
setClaudeAsk("command")
|
||||
|
||||
Reference in New Issue
Block a user