diff --git a/README.md b/README.md index dfc6603..8e5b322 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,12 @@ This project was developed for the [Build with Claude June 2024](https://docs.an ## How it works +Claude Dev uses an agentic loop style implementation using chain-of-thought prompting and access to powerful tools that give him the ability to accomplish nearly everything. From building softwware projects to running system operations, Claude Dev is only limited by your imagination. + + ### Tools -Claude Dev has access to the following tools: +Claude has access to the following tools: 1. **execute_command**: Execute CLI commands on the system. 2. **list_files**: List all files and directories at the top level of the specified directory. @@ -17,6 +20,12 @@ Claude Dev has access to the following tools: 5. **ask_followup_question**: Ask the user a question to gather additional information needed to complete a task. 6. **attempt_completion**: Present the result to the user after completing a task. +### Only With Your Permission + +Claude always asks for your permission first before any tools are executed or information is sent back to the API. This puts you in control of this agentic loop, every step of the way. + +![Asks_for_approval](https://private-user-images.githubusercontent.com/7799382/347307158-a40a941a-8881-425a-a318-f9ff1636595d.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MjA1OTc1NTAsIm5iZiI6MTcyMDU5NzI1MCwicGF0aCI6Ii83Nzk5MzgyLzM0NzMwNzE1OC1hNDBhOTQxYS04ODgxLTQyNWEtYTMxOC1mOWZmMTYzNjU5NWQucG5nP1gtQW16LUFsZ29yaXRobT1BV1M0LUhNQUMtU0hBMjU2JlgtQW16LUNyZWRlbnRpYWw9QUtJQVZDT0RZTFNBNTNQUUs0WkElMkYyMDI0MDcxMCUyRnVzLWVhc3QtMSUyRnMzJTJGYXdzNF9yZXF1ZXN0JlgtQW16LURhdGU9MjAyNDA3MTBUMDc0MDUwWiZYLUFtei1FeHBpcmVzPTMwMCZYLUFtei1TaWduYXR1cmU9ZGVmYzUzNzZmMzI0OGE0NjIyYzA3OGZlNDgyMjdkMDA3NjI0YTEzYjc4MjAwZDU4MjU3N2YyNDNkZDlmNzYzYSZYLUFtei1TaWduZWRIZWFkZXJzPWhvc3QmYWN0b3JfaWQ9MCZrZXlfaWQ9MCZyZXBvX2lkPTAifQ.CKPPQAhYK-DJB_XXGTuLtel4q0F0XAiCWpk88wPZGIs) + ## Screenshots ### 1. Give Claude Dev any task! diff --git a/src/ClaudeDev.ts b/src/ClaudeDev.ts index 5bf4d4e..e77610d 100644 --- a/src/ClaudeDev.ts +++ b/src/ClaudeDev.ts @@ -340,58 +340,52 @@ ${openDocuments}` if (fileExists) { const originalContent = await fs.readFile(filePath, "utf-8") const diffResult = diff.createPatch(filePath, originalContent, newContent) - if (diffResult) { - await fs.writeFile(filePath, newContent) + // Create diff for DiffCodeView.tsx + const completeDiffStringRaw = diff.diffLines(originalContent, newContent) + const completeDiffStringConverted = completeDiffStringRaw + .map((part, index) => { + const prefix = part.added ? "+ " : part.removed ? "- " : " " + return part.value + .split("\n") + .map((line, lineIndex) => { + // avoid adding an extra empty line at the very end of the diff output + if ( + line === "" && + index === completeDiffStringRaw.length - 1 && + lineIndex === part.value.split("\n").length - 1 + ) { + return null + } + return prefix + line + "\n" + }) + .join("") + }) + .join("") - // Create diff for DiffCodeView.tsx - const diffStringRaw = diff.diffLines(originalContent, newContent) - const diffStringConverted = diffStringRaw - .map((part, index) => { - const prefix = part.added ? "+ " : part.removed ? "- " : " " - return part.value - .split("\n") - .map((line, lineIndex) => { - // avoid adding an extra empty line at the very end of the diff output - if ( - line === "" && - index === diffStringRaw.length - 1 && - lineIndex === part.value.split("\n").length - 1 - ) { - return null - } - return prefix + line + "\n" - }) - .join("") - }) - .join("") - this.say( - "tool", - JSON.stringify({ - tool: "editedExistingFile", - path: filePath, - diff: diffStringConverted, - } as ClaudeSayTool) - ) - - return `Changes applied to ${filePath}:\n${diffResult}` - } else { - this.say( - "tool", - JSON.stringify({ - tool: "editedExistingFile", - path: filePath, - content: "No changes.", - } as ClaudeSayTool) - ) - return `Tool succeeded, however there were no changes detected to ${filePath}` + const { response } = await this.ask( + "tool", + JSON.stringify({ + tool: "editedExistingFile", + path: filePath, + diff: completeDiffStringConverted, + } as ClaudeSayTool) + ) + if (response !== "yesButtonTapped") { + return "This operation was not approved by the user." } - } else { - await fs.mkdir(path.dirname(filePath), { recursive: true }) + await fs.writeFile(filePath, newContent) - this.say( + return `Changes applied to ${filePath}:\n${diffResult}` + } else { + const { response } = await this.ask( "tool", JSON.stringify({ tool: "newFileCreated", path: filePath, content: newContent } as ClaudeSayTool) ) + if (response !== "yesButtonTapped") { + return "This operation was not approved by the user." + } + await fs.mkdir(path.dirname(filePath), { recursive: true }) + await fs.writeFile(filePath, newContent) return `New file created and content written to ${filePath}` } } catch (error) { @@ -404,7 +398,13 @@ ${openDocuments}` async readFile(filePath: string): Promise { try { const content = await fs.readFile(filePath, "utf-8") - this.say("tool", JSON.stringify({ tool: "readFile", path: filePath, content } as ClaudeSayTool)) + const { response } = await this.ask( + "tool", + JSON.stringify({ tool: "readFile", path: filePath, content } as ClaudeSayTool) + ) + if (response !== "yesButtonTapped") { + return "This operation was not approved by the user." + } return content } catch (error) { const errorString = `Error reading file: ${JSON.stringify(serializeError(error))}` @@ -419,7 +419,13 @@ ${openDocuments}` const isRoot = absolutePath === root if (isRoot) { if (shouldLog) { - this.say("tool", JSON.stringify({ tool: "listFiles", path: dirPath, content: root } as ClaudeSayTool)) + const { response } = await this.ask( + "tool", + JSON.stringify({ tool: "listFiles", path: dirPath, content: root } as ClaudeSayTool) + ) + if (response !== "yesButtonTapped") { + return "This operation was not approved by the user." + } } return root } @@ -434,7 +440,13 @@ ${openDocuments}` const entries = await glob("*", options) const result = entries.slice(0, 500).join("\n") // truncate to 500 entries if (shouldLog) { - this.say("tool", JSON.stringify({ tool: "listFiles", path: dirPath, content: result } as ClaudeSayTool)) + const { response } = await this.ask( + "tool", + JSON.stringify({ tool: "listFiles", path: dirPath, content: result } as ClaudeSayTool) + ) + if (response !== "yesButtonTapped") { + return "This operation was not approved by the user." + } } return result } catch (error) { diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 86e6e2a..78a3b8c 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -16,8 +16,8 @@ export interface ClaudeMessage { text?: string } -export type ClaudeAsk = "request_limit_reached" | "followup" | "command" | "completion_result" -export type ClaudeSay = "task" | "error" | "api_req_started" | "api_req_finished" | "text" | "tool" | "command_output" | "completion_result" +export type ClaudeAsk = "request_limit_reached" | "followup" | "command" | "completion_result" | "tool" +export type ClaudeSay = "task" | "error" | "api_req_started" | "api_req_finished" | "text" | "command_output" | "completion_result" export interface ClaudeSayTool { tool: "editedExistingFile" | "newFileCreated" | "readFile" | "listFiles" diff --git a/webview-ui/src/components/ChatRow.tsx b/webview-ui/src/components/ChatRow.tsx index 9051cd1..5194669 100644 --- a/webview-ui/src/components/ChatRow.tsx +++ b/webview-ui/src/components/ChatRow.tsx @@ -39,7 +39,7 @@ const ChatRow: React.FC = ({ message }) => { , - Command, + Claude wants to execute this command:, ] case "completion_result": return [ @@ -113,57 +113,6 @@ const ChatRow: React.FC = ({ message }) => { ) case "api_req_finished": return null // Hide this message type - case "tool": - const tool = JSON.parse(message.text || "{}") as ClaudeSayTool - const toolIcon = (name: string) => ( - - ) - - switch (tool.tool) { - case "editedExistingFile": - return ( - <> -
- {toolIcon("edit")} - Edited file... -
- - - ) - case "newFileCreated": - return ( - <> -
- {toolIcon("new-file")} - Created new file... -
- - - ) - case "readFile": - return ( - <> -
- {toolIcon("file-code")} - Read file... -
- - - ) - case "listFiles": - return ( - <> -
- {toolIcon("folder-opened")} - Viewed contents of directory... -
- - - ) - } - break case "text": return

{message.text}

case "error": @@ -205,9 +154,59 @@ const ChatRow: React.FC = ({ message }) => { ) } - break case "ask": switch (message.ask) { + case "tool": + const tool = JSON.parse(message.text || "{}") as ClaudeSayTool + const toolIcon = (name: string) => ( + + ) + + switch (tool.tool) { + case "editedExistingFile": + return ( + <> +
+ {toolIcon("edit")} + Claude wants to edit this file: +
+ + + ) + case "newFileCreated": + return ( + <> +
+ {toolIcon("new-file")} + Claude wants to create a new file: +
+ + + ) + case "readFile": + return ( + <> +
+ {toolIcon("file-code")} + Claude wants to read this file: +
+ + + ) + case "listFiles": + return ( + <> +
+ {toolIcon("folder-opened")} + Claude wants to view this directory: +
+ + + ) + } + break case "request_limit_reached": return ( <> @@ -240,11 +239,7 @@ const ChatRow: React.FC = ({ message }) => { {title}
-

- Claude Dev wants to execute the following terminal command. Would you like to - proceed? -

-
+
diff --git a/webview-ui/src/components/ChatView.tsx b/webview-ui/src/components/ChatView.tsx index 34c09f0..78a042e 100644 --- a/webview-ui/src/components/ChatView.tsx +++ b/webview-ui/src/components/ChatView.tsx @@ -82,11 +82,17 @@ const ChatView = ({ messages }: ChatViewProps) => { setPrimaryButtonText(undefined) setSecondaryButtonText(undefined) break + case "tool": + setTextAreaDisabled(true) + setClaudeAsk("tool") + setPrimaryButtonText("Approve") + setSecondaryButtonText("Cancel") + break case "command": setTextAreaDisabled(true) setClaudeAsk("command") - setPrimaryButtonText("Yes") - setSecondaryButtonText("No") + setPrimaryButtonText("Run Command") + setSecondaryButtonText("Cancel") break case "completion_result": // extension waiting for feedback. but we can just present a new task button @@ -110,8 +116,6 @@ const ChatView = ({ messages }: ChatViewProps) => { break case "text": break - case "tool": - break case "command_output": break case "completion_result": @@ -166,9 +170,8 @@ const ChatView = ({ messages }: ChatViewProps) => { const handlePrimaryButtonClick = () => { switch (claudeAsk) { case "request_limit_reached": - vscode.postMessage({ type: "askResponse", askResponse: "yesButtonTapped" }) - break case "command": + case "tool": vscode.postMessage({ type: "askResponse", askResponse: "yesButtonTapped" }) break case "completion_result": @@ -185,6 +188,7 @@ const ChatView = ({ messages }: ChatViewProps) => { const handleSecondaryButtonClick = () => { switch (claudeAsk) { case "request_limit_reached": + case "tool": // TODO: for now when a user cancels, it starts a new task. But we could easily just respond to the API with a "This operation failed" and let it try again. startNewTask() break case "command": @@ -267,7 +271,7 @@ const ChatView = ({ messages }: ChatViewProps) => {

What can I do for you?

- {/*prettier-ignore*/} + {/* prettier-ignore */} Thanks to Claude 3.5 Sonnet's agentic coding capabilities, I can handle complex software development tasks step-by-step. With tools that let me read & write files, create entire projects from scratch, and execute terminal commands (after you grant permission), I can assist you in ways that go beyond simple code completion or tech support.

diff --git a/webview-ui/src/utilities/mockMessages.ts b/webview-ui/src/utilities/mockMessages.ts index d6da2ff..78b88c2 100644 --- a/webview-ui/src/utilities/mockMessages.ts +++ b/webview-ui/src/utilities/mockMessages.ts @@ -29,8 +29,8 @@ export const mockMessages: ClaudeMessage[] = [ }, { ts: Date.now() - 3200000, - type: "say", - say: "tool", + type: "ask", + ask: "tool", text: JSON.stringify({ tool: "newFileCreated", path: "/src/components/TodoList.tsx", @@ -121,8 +121,8 @@ export const mockMessages: ClaudeMessage[] = [ }, { ts: Date.now() - 2600000, - type: "say", - say: "tool", + type: "ask", + ask: "tool", text: JSON.stringify({ tool: "editedExistingFile", path: "/src/components/TodoList.tsx", @@ -238,8 +238,8 @@ export const mockMessages: ClaudeMessage[] = [ }, { ts: Date.now() - 1700000, - type: "say", - say: "tool", + type: "ask", + ask: "tool", text: JSON.stringify({ tool: "newFileCreated", path: "/src/app.js", @@ -313,8 +313,8 @@ export const mockMessages: ClaudeMessage[] = [ }, { ts: Date.now() - 1000000, - type: "say", - say: "tool", + type: "ask", + ask: "tool", text: JSON.stringify({ tool: "editedExistingFile", path: "/src/app.js",