mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-20 04:11:10 -05:00
Make diff view editable
This commit is contained in:
172
src/ClaudeDev.ts
172
src/ClaudeDev.ts
@@ -796,77 +796,51 @@ export class ClaudeDev {
|
|||||||
.then(() => true)
|
.then(() => true)
|
||||||
.catch(() => false)
|
.catch(() => false)
|
||||||
|
|
||||||
|
let originalContent: string
|
||||||
if (fileExists) {
|
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
|
// fix issue where claude always removes newline from the file
|
||||||
if (originalContent.endsWith("\n") && !newContent.endsWith("\n")) {
|
const eol = originalContent.includes("\r\n") ? "\r\n" : "\n"
|
||||||
newContent += "\n"
|
if (originalContent.endsWith(eol) && !newContent.endsWith(eol)) {
|
||||||
|
newContent += eol
|
||||||
}
|
}
|
||||||
// condensed patch to return to claude
|
} else {
|
||||||
const diffResult = diff.createPatch(absolutePath, originalContent, newContent)
|
originalContent = ""
|
||||||
// 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
|
// Create a temporary file with the new content
|
||||||
const fileName = path.basename(absolutePath)
|
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "claude-dev-"))
|
||||||
vscode.commands.executeCommand(
|
const tempFilePath = path.join(tempDir, path.basename(absolutePath))
|
||||||
"vscode.diff",
|
await fs.writeFile(tempFilePath, newContent)
|
||||||
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"),
|
|
||||||
}),
|
|
||||||
`${fileName}: Original ↔ Suggested Changes`
|
|
||||||
)
|
|
||||||
|
|
||||||
const { response, text, images } = await this.ask(
|
vscode.commands.executeCommand(
|
||||||
|
"vscode.diff",
|
||||||
|
fileExists
|
||||||
|
? vscode.Uri.file(absolutePath)
|
||||||
|
: vscode.Uri.parse(`claude-dev-diff:${path.basename(absolutePath)}`).with({
|
||||||
|
query: Buffer.from("").toString("base64"),
|
||||||
|
}),
|
||||||
|
vscode.Uri.file(tempFilePath),
|
||||||
|
`${path.basename(absolutePath)}: ${fileExists ? "Original ↔ Suggested Changes" : "New File"} (Editable)`
|
||||||
|
)
|
||||||
|
|
||||||
|
let userResponse: {
|
||||||
|
response: ClaudeAskResponse
|
||||||
|
text?: string
|
||||||
|
images?: string[]
|
||||||
|
}
|
||||||
|
if (fileExists) {
|
||||||
|
const suggestedDiff = diff.createPatch(relPath, originalContent, newContent)
|
||||||
|
userResponse = await this.ask(
|
||||||
"tool",
|
"tool",
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
tool: "editedExistingFile",
|
tool: "editedExistingFile",
|
||||||
path: this.getReadablePath(relPath),
|
path: this.getReadablePath(relPath),
|
||||||
diff: diffRepresentation,
|
diff: suggestedDiff,
|
||||||
} as ClaudeSayTool)
|
} 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 {
|
} else {
|
||||||
const fileName = path.basename(absolutePath)
|
userResponse = await this.ask(
|
||||||
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(
|
|
||||||
"tool",
|
"tool",
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
tool: "newFileCreated",
|
tool: "newFileCreated",
|
||||||
@@ -874,23 +848,62 @@ export class ClaudeDev {
|
|||||||
content: newContent,
|
content: newContent,
|
||||||
} as ClaudeSayTool)
|
} as ClaudeSayTool)
|
||||||
)
|
)
|
||||||
if (response !== "yesButtonTapped") {
|
}
|
||||||
if (isLast) {
|
const { response, text, images } = userResponse
|
||||||
await this.closeDiffViews()
|
|
||||||
}
|
if (response !== "yesButtonTapped") {
|
||||||
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.mkdir(path.dirname(absolutePath), { recursive: true })
|
|
||||||
await fs.writeFile(absolutePath, newContent)
|
|
||||||
await vscode.window.showTextDocument(vscode.Uri.file(absolutePath), { preview: false })
|
|
||||||
if (isLast) {
|
if (isLast) {
|
||||||
await this.closeDiffViews()
|
await this.closeDiffViews()
|
||||||
}
|
}
|
||||||
return `New file created and content written to ${relPath}`
|
// 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, 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()
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
} catch (error) {
|
||||||
const errorString = `Error writing file: ${JSON.stringify(serializeError(error))}`
|
const errorString = `Error writing file: ${JSON.stringify(serializeError(error))}`
|
||||||
@@ -906,10 +919,15 @@ export class ClaudeDev {
|
|||||||
const tabs = vscode.window.tabGroups.all
|
const tabs = vscode.window.tabGroups.all
|
||||||
.map((tg) => tg.tabs)
|
.map((tg) => tg.tabs)
|
||||||
.flat()
|
.flat()
|
||||||
.filter(
|
.filter((tab) => {
|
||||||
(tab) =>
|
if (tab.input instanceof vscode.TabInputTextDiff) {
|
||||||
tab.input instanceof vscode.TabInputTextDiff && tab.input?.modified?.scheme === "claude-dev-diff"
|
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) {
|
for (const tab of tabs) {
|
||||||
await vscode.window.tabGroups.close(tab)
|
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.
|
- 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.
|
- 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"
|
| "text"
|
||||||
| "completion_result"
|
| "completion_result"
|
||||||
| "user_feedback"
|
| "user_feedback"
|
||||||
|
| "user_feedback_diff"
|
||||||
| "api_req_retried"
|
| "api_req_retried"
|
||||||
| "command_output"
|
| "command_output"
|
||||||
| "tool"
|
| "tool"
|
||||||
|
|||||||
@@ -338,6 +338,35 @@ const ChatRow: React.FC<ChatRowProps> = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</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":
|
case "error":
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import vsDarkPlus from "react-syntax-highlighter/dist/esm/styles/prism/vsc-dark-
|
|||||||
import DynamicTextArea from "react-textarea-autosize"
|
import DynamicTextArea from "react-textarea-autosize"
|
||||||
import { useEvent, useMount } from "react-use"
|
import { useEvent, useMount } from "react-use"
|
||||||
import { Virtuoso, type VirtuosoHandle } from "react-virtuoso"
|
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 { combineApiRequests } from "../../../src/shared/combineApiRequests"
|
||||||
import { combineCommandSequences } from "../../../src/shared/combineCommandSequences"
|
import { combineCommandSequences } from "../../../src/shared/combineCommandSequences"
|
||||||
import { getApiMetrics } from "../../../src/shared/getApiMetrics"
|
import { getApiMetrics } from "../../../src/shared/getApiMetrics"
|
||||||
@@ -121,8 +121,21 @@ const ChatView = ({
|
|||||||
setTextAreaDisabled(false)
|
setTextAreaDisabled(false)
|
||||||
setClaudeAsk("tool")
|
setClaudeAsk("tool")
|
||||||
setEnableButtons(true)
|
setEnableButtons(true)
|
||||||
setPrimaryButtonText("Approve")
|
const tool = JSON.parse(lastMessage.text || "{}") as ClaudeSayTool
|
||||||
setSecondaryButtonText("Reject")
|
switch (tool.tool) {
|
||||||
|
case "editedExistingFile":
|
||||||
|
setPrimaryButtonText("Save")
|
||||||
|
setSecondaryButtonText("Reject")
|
||||||
|
break
|
||||||
|
case "newFileCreated":
|
||||||
|
setPrimaryButtonText("Create")
|
||||||
|
setSecondaryButtonText("Reject")
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
setPrimaryButtonText("Approve")
|
||||||
|
setSecondaryButtonText("Reject")
|
||||||
|
break
|
||||||
|
}
|
||||||
break
|
break
|
||||||
case "command":
|
case "command":
|
||||||
setTextAreaDisabled(false)
|
setTextAreaDisabled(false)
|
||||||
|
|||||||
Reference in New Issue
Block a user