Add in permissions logic for tools; Update README with permissions section

This commit is contained in:
Saoud Rizwan
2024-07-10 04:01:07 -04:00
parent 88e520ede7
commit 3d7abe4ba6
6 changed files with 145 additions and 125 deletions

View File

@@ -6,9 +6,12 @@ This project was developed for the [Build with Claude June 2024](https://docs.an
## How it works ## 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 ### 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. 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. 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. 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. 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 ## Screenshots
### 1. Give Claude Dev any task! ### 1. Give Claude Dev any task!

View File

@@ -340,58 +340,52 @@ ${openDocuments}`
if (fileExists) { if (fileExists) {
const originalContent = await fs.readFile(filePath, "utf-8") const originalContent = await fs.readFile(filePath, "utf-8")
const diffResult = diff.createPatch(filePath, originalContent, newContent) const diffResult = diff.createPatch(filePath, originalContent, newContent)
if (diffResult) { // Create diff for DiffCodeView.tsx
await fs.writeFile(filePath, newContent) 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 { response } = await this.ask(
const diffStringRaw = diff.diffLines(originalContent, newContent) "tool",
const diffStringConverted = diffStringRaw JSON.stringify({
.map((part, index) => { tool: "editedExistingFile",
const prefix = part.added ? "+ " : part.removed ? "- " : " " path: filePath,
return part.value diff: completeDiffStringConverted,
.split("\n") } as ClaudeSayTool)
.map((line, lineIndex) => { )
// avoid adding an extra empty line at the very end of the diff output if (response !== "yesButtonTapped") {
if ( return "This operation was not approved by the user."
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}`
} }
} else {
await fs.mkdir(path.dirname(filePath), { recursive: true })
await fs.writeFile(filePath, newContent) await fs.writeFile(filePath, newContent)
this.say( return `Changes applied to ${filePath}:\n${diffResult}`
} else {
const { response } = await this.ask(
"tool", "tool",
JSON.stringify({ tool: "newFileCreated", path: filePath, content: newContent } as ClaudeSayTool) 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}` return `New file created and content written to ${filePath}`
} }
} catch (error) { } catch (error) {
@@ -404,7 +398,13 @@ ${openDocuments}`
async readFile(filePath: string): Promise<string> { async readFile(filePath: string): Promise<string> {
try { try {
const content = await fs.readFile(filePath, "utf-8") 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 return content
} catch (error) { } catch (error) {
const errorString = `Error reading file: ${JSON.stringify(serializeError(error))}` const errorString = `Error reading file: ${JSON.stringify(serializeError(error))}`
@@ -419,7 +419,13 @@ ${openDocuments}`
const isRoot = absolutePath === root const isRoot = absolutePath === root
if (isRoot) { if (isRoot) {
if (shouldLog) { 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 return root
} }
@@ -434,7 +440,13 @@ ${openDocuments}`
const entries = await glob("*", options) const entries = await glob("*", options)
const result = entries.slice(0, 500).join("\n") // truncate to 500 entries const result = entries.slice(0, 500).join("\n") // truncate to 500 entries
if (shouldLog) { 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 return result
} catch (error) { } catch (error) {

View File

@@ -16,8 +16,8 @@ export interface ClaudeMessage {
text?: string text?: string
} }
export type ClaudeAsk = "request_limit_reached" | "followup" | "command" | "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" | "tool" | "command_output" | "completion_result" export type ClaudeSay = "task" | "error" | "api_req_started" | "api_req_finished" | "text" | "command_output" | "completion_result"
export interface ClaudeSayTool { export interface ClaudeSayTool {
tool: "editedExistingFile" | "newFileCreated" | "readFile" | "listFiles" tool: "editedExistingFile" | "newFileCreated" | "readFile" | "listFiles"

View File

@@ -39,7 +39,7 @@ const ChatRow: React.FC<ChatRowProps> = ({ message }) => {
<span <span
className="codicon codicon-terminal" className="codicon codicon-terminal"
style={{ color: normalColor, marginBottom: "-1.5px" }}></span>, style={{ color: normalColor, marginBottom: "-1.5px" }}></span>,
<span style={{ color: normalColor, fontWeight: "bold" }}>Command</span>, <span style={{ color: normalColor, fontWeight: "bold" }}>Claude wants to execute this command:</span>,
] ]
case "completion_result": case "completion_result":
return [ return [
@@ -113,57 +113,6 @@ const ChatRow: React.FC<ChatRowProps> = ({ message }) => {
) )
case "api_req_finished": case "api_req_finished":
return null // Hide this message type return null // Hide this message type
case "tool":
const tool = JSON.parse(message.text || "{}") as ClaudeSayTool
const toolIcon = (name: string) => (
<span
className={`codicon codicon-${name}`}
style={{ color: "var(--vscode-foreground)", marginBottom: "-1.5px" }}></span>
)
switch (tool.tool) {
case "editedExistingFile":
return (
<>
<div style={headerStyle}>
{toolIcon("edit")}
Edited file...
</div>
<CodeBlock diff={tool.diff!} path={tool.path!} />
</>
)
case "newFileCreated":
return (
<>
<div style={headerStyle}>
{toolIcon("new-file")}
Created new file...
</div>
<CodeBlock code={tool.content!} path={tool.path!} />
</>
)
case "readFile":
return (
<>
<div style={headerStyle}>
{toolIcon("file-code")}
Read file...
</div>
<CodeBlock code={tool.content!} path={tool.path!} />
</>
)
case "listFiles":
return (
<>
<div style={headerStyle}>
{toolIcon("folder-opened")}
Viewed contents of directory...
</div>
<CodeBlock code={tool.content!} path={tool.path!} language="shell-session" />
</>
)
}
break
case "text": case "text":
return <p style={contentStyle}>{message.text}</p> return <p style={contentStyle}>{message.text}</p>
case "error": case "error":
@@ -205,9 +154,59 @@ const ChatRow: React.FC<ChatRowProps> = ({ message }) => {
</> </>
) )
} }
break
case "ask": case "ask":
switch (message.ask) { switch (message.ask) {
case "tool":
const tool = JSON.parse(message.text || "{}") as ClaudeSayTool
const toolIcon = (name: string) => (
<span
className={`codicon codicon-${name}`}
style={{ color: "var(--vscode-foreground)", marginBottom: "-1.5px" }}></span>
)
switch (tool.tool) {
case "editedExistingFile":
return (
<>
<div style={headerStyle}>
{toolIcon("edit")}
<span style={{ fontWeight: "bold" }}>Claude wants to edit this file:</span>
</div>
<CodeBlock diff={tool.diff!} path={tool.path!} />
</>
)
case "newFileCreated":
return (
<>
<div style={headerStyle}>
{toolIcon("new-file")}
<span style={{ fontWeight: "bold" }}>Claude wants to create a new file:</span>
</div>
<CodeBlock code={tool.content!} path={tool.path!} />
</>
)
case "readFile":
return (
<>
<div style={headerStyle}>
{toolIcon("file-code")}
<span style={{ fontWeight: "bold" }}>Claude wants to read this file:</span>
</div>
<CodeBlock code={tool.content!} path={tool.path!} />
</>
)
case "listFiles":
return (
<>
<div style={headerStyle}>
{toolIcon("folder-opened")}
<span style={{ fontWeight: "bold" }}>Claude wants to view this directory:</span>
</div>
<CodeBlock code={tool.content!} path={tool.path!} language="shell-session" />
</>
)
}
break
case "request_limit_reached": case "request_limit_reached":
return ( return (
<> <>
@@ -240,11 +239,7 @@ const ChatRow: React.FC<ChatRowProps> = ({ message }) => {
{title} {title}
</div> </div>
<div style={contentStyle}> <div style={contentStyle}>
<p style={contentStyle}> <div>
Claude Dev wants to execute the following terminal command. Would you like to
proceed?
</p>
<div style={{ marginTop: "10px" }}>
<CodeBlock code={command} language="shell-session" /> <CodeBlock code={command} language="shell-session" />
</div> </div>

View File

@@ -82,11 +82,17 @@ const ChatView = ({ messages }: ChatViewProps) => {
setPrimaryButtonText(undefined) setPrimaryButtonText(undefined)
setSecondaryButtonText(undefined) setSecondaryButtonText(undefined)
break break
case "tool":
setTextAreaDisabled(true)
setClaudeAsk("tool")
setPrimaryButtonText("Approve")
setSecondaryButtonText("Cancel")
break
case "command": case "command":
setTextAreaDisabled(true) setTextAreaDisabled(true)
setClaudeAsk("command") setClaudeAsk("command")
setPrimaryButtonText("Yes") setPrimaryButtonText("Run Command")
setSecondaryButtonText("No") setSecondaryButtonText("Cancel")
break break
case "completion_result": case "completion_result":
// extension waiting for feedback. but we can just present a new task button // extension waiting for feedback. but we can just present a new task button
@@ -110,8 +116,6 @@ const ChatView = ({ messages }: ChatViewProps) => {
break break
case "text": case "text":
break break
case "tool":
break
case "command_output": case "command_output":
break break
case "completion_result": case "completion_result":
@@ -166,9 +170,8 @@ const ChatView = ({ messages }: ChatViewProps) => {
const handlePrimaryButtonClick = () => { const handlePrimaryButtonClick = () => {
switch (claudeAsk) { switch (claudeAsk) {
case "request_limit_reached": case "request_limit_reached":
vscode.postMessage({ type: "askResponse", askResponse: "yesButtonTapped" })
break
case "command": case "command":
case "tool":
vscode.postMessage({ type: "askResponse", askResponse: "yesButtonTapped" }) vscode.postMessage({ type: "askResponse", askResponse: "yesButtonTapped" })
break break
case "completion_result": case "completion_result":
@@ -185,6 +188,7 @@ const ChatView = ({ messages }: ChatViewProps) => {
const handleSecondaryButtonClick = () => { const handleSecondaryButtonClick = () => {
switch (claudeAsk) { switch (claudeAsk) {
case "request_limit_reached": 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() startNewTask()
break break
case "command": case "command":
@@ -267,7 +271,7 @@ const ChatView = ({ messages }: ChatViewProps) => {
<div style={{ padding: "0 25px" }}> <div style={{ padding: "0 25px" }}>
<h2>What can I do for you?</h2> <h2>What can I do for you?</h2>
<p> <p>
{/*prettier-ignore*/} {/* prettier-ignore */}
Thanks to <VSCodeLink href="https://www-cdn.anthropic.com/fed9cc193a14b84131812372d8d5857f8f304c52/Model_Card_Claude_3_Addendum.pdf" style={{ display: "inline" }}>Claude 3.5 Sonnet's agentic coding capabilities</VSCodeLink>, 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. Thanks to <VSCodeLink href="https://www-cdn.anthropic.com/fed9cc193a14b84131812372d8d5857f8f304c52/Model_Card_Claude_3_Addendum.pdf" style={{ display: "inline" }}>Claude 3.5 Sonnet's agentic coding capabilities</VSCodeLink>, 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.
</p> </p>
</div> </div>

View File

@@ -29,8 +29,8 @@ export const mockMessages: ClaudeMessage[] = [
}, },
{ {
ts: Date.now() - 3200000, ts: Date.now() - 3200000,
type: "say", type: "ask",
say: "tool", ask: "tool",
text: JSON.stringify({ text: JSON.stringify({
tool: "newFileCreated", tool: "newFileCreated",
path: "/src/components/TodoList.tsx", path: "/src/components/TodoList.tsx",
@@ -121,8 +121,8 @@ export const mockMessages: ClaudeMessage[] = [
}, },
{ {
ts: Date.now() - 2600000, ts: Date.now() - 2600000,
type: "say", type: "ask",
say: "tool", ask: "tool",
text: JSON.stringify({ text: JSON.stringify({
tool: "editedExistingFile", tool: "editedExistingFile",
path: "/src/components/TodoList.tsx", path: "/src/components/TodoList.tsx",
@@ -238,8 +238,8 @@ export const mockMessages: ClaudeMessage[] = [
}, },
{ {
ts: Date.now() - 1700000, ts: Date.now() - 1700000,
type: "say", type: "ask",
say: "tool", ask: "tool",
text: JSON.stringify({ text: JSON.stringify({
tool: "newFileCreated", tool: "newFileCreated",
path: "/src/app.js", path: "/src/app.js",
@@ -313,8 +313,8 @@ export const mockMessages: ClaudeMessage[] = [
}, },
{ {
ts: Date.now() - 1000000, ts: Date.now() - 1000000,
type: "say", type: "ask",
say: "tool", ask: "tool",
text: JSON.stringify({ text: JSON.stringify({
tool: "editedExistingFile", tool: "editedExistingFile",
path: "/src/app.js", path: "/src/app.js",