diff --git a/src/ClaudeDev.ts b/src/ClaudeDev.ts index 56ac26b..7a866c5 100644 --- a/src/ClaudeDev.ts +++ b/src/ClaudeDev.ts @@ -877,17 +877,25 @@ export class ClaudeDev { try { const absolutePath = path.resolve(cwd, relPath) const content = await fs.readFile(absolutePath, "utf-8") - const { response, text, images } = await this.ask( - "tool", - JSON.stringify({ tool: "readFile", path: this.getReadablePath(relPath), content } as ClaudeSayTool) - ) - if (response !== "yesButtonTapped") { - if (response === "messageResponse") { - await this.say("user_feedback", text, images) - return this.formatIntoToolResponse(this.formatGenericToolFeedback(text), images) + + const message = JSON.stringify({ + tool: "readFile", + path: this.getReadablePath(relPath), + content, + } as ClaudeSayTool) + if (this.alwaysAllowReadOnly) { + await this.say("tool", message) + } else { + const { response, text, images } = await this.ask("tool", message) + if (response !== "yesButtonTapped") { + if (response === "messageResponse") { + await this.say("user_feedback", text, images) + return this.formatIntoToolResponse(this.formatGenericToolFeedback(text), images) + } + return "The user denied this operation." } - return "The user denied this operation." } + return content } catch (error) { const errorString = `Error reading file: ${JSON.stringify(serializeError(error))}` @@ -911,21 +919,25 @@ export class ClaudeDev { const absolutePath = path.resolve(cwd, relDirPath) const files = await listFiles(absolutePath, false) const result = this.formatFilesList(absolutePath, files) - const { response, text, images } = await this.ask( - "tool", - JSON.stringify({ - tool: "listFilesTopLevel", - path: this.getReadablePath(relDirPath), - content: result, - } as ClaudeSayTool) - ) - if (response !== "yesButtonTapped") { - if (response === "messageResponse") { - await this.say("user_feedback", text, images) - return this.formatIntoToolResponse(this.formatGenericToolFeedback(text), images) + + const message = JSON.stringify({ + tool: "listFilesTopLevel", + path: this.getReadablePath(relDirPath), + content: result, + } as ClaudeSayTool) + if (this.alwaysAllowReadOnly) { + await this.say("tool", message) + } else { + const { response, text, images } = await this.ask("tool", message) + if (response !== "yesButtonTapped") { + if (response === "messageResponse") { + await this.say("user_feedback", text, images) + return this.formatIntoToolResponse(this.formatGenericToolFeedback(text), images) + } + return "The user denied this operation." } - return "The user denied this operation." } + return result } catch (error) { const errorString = `Error listing files and directories: ${JSON.stringify(serializeError(error))}` @@ -951,21 +963,25 @@ export class ClaudeDev { const absolutePath = path.resolve(cwd, relDirPath) const files = await listFiles(absolutePath, true) const result = this.formatFilesList(absolutePath, files) - const { response, text, images } = await this.ask( - "tool", - JSON.stringify({ - tool: "listFilesRecursive", - path: this.getReadablePath(relDirPath), - content: result, - } as ClaudeSayTool) - ) - if (response !== "yesButtonTapped") { - if (response === "messageResponse") { - await this.say("user_feedback", text, images) - return this.formatIntoToolResponse(this.formatGenericToolFeedback(text), images) + + const message = JSON.stringify({ + tool: "listFilesRecursive", + path: this.getReadablePath(relDirPath), + content: result, + } as ClaudeSayTool) + if (this.alwaysAllowReadOnly) { + await this.say("tool", message) + } else { + const { response, text, images } = await this.ask("tool", message) + if (response !== "yesButtonTapped") { + if (response === "messageResponse") { + await this.say("user_feedback", text, images) + return this.formatIntoToolResponse(this.formatGenericToolFeedback(text), images) + } + return "The user denied this operation." } - return "The user denied this operation." } + return result } catch (error) { const errorString = `Error listing files recursively: ${JSON.stringify(serializeError(error))}` @@ -1037,21 +1053,25 @@ export class ClaudeDev { try { const absolutePath = path.resolve(cwd, relDirPath) const result = await parseSourceCodeForDefinitionsTopLevel(absolutePath) - const { response, text, images } = await this.ask( - "tool", - JSON.stringify({ - tool: "viewSourceCodeDefinitionsTopLevel", - path: this.getReadablePath(relDirPath), - content: result, - } as ClaudeSayTool) - ) - if (response !== "yesButtonTapped") { - if (response === "messageResponse") { - await this.say("user_feedback", text, images) - return this.formatIntoToolResponse(this.formatGenericToolFeedback(text), images) + + const message = JSON.stringify({ + tool: "viewSourceCodeDefinitionsTopLevel", + path: this.getReadablePath(relDirPath), + content: result, + } as ClaudeSayTool) + if (this.alwaysAllowReadOnly) { + await this.say("tool", message) + } else { + const { response, text, images } = await this.ask("tool", message) + if (response !== "yesButtonTapped") { + if (response === "messageResponse") { + await this.say("user_feedback", text, images) + return this.formatIntoToolResponse(this.formatGenericToolFeedback(text), images) + } + return "The user denied this operation." } - return "The user denied this operation." } + return result } catch (error) { const errorString = `Error parsing source code definitions: ${JSON.stringify(serializeError(error))}` diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 7ae96d4..5e60256 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -61,6 +61,7 @@ export type ClaudeSay = | "user_feedback" | "api_req_retried" | "command_output" + | "tool" export interface ClaudeSayTool { tool: diff --git a/webview-ui/src/components/ChatRow.tsx b/webview-ui/src/components/ChatRow.tsx index 7ac8474..fecdae6 100644 --- a/webview-ui/src/components/ChatRow.tsx +++ b/webview-ui/src/components/ChatRow.tsx @@ -325,6 +325,8 @@ const ChatRow: React.FC = ({ ) + case "tool": + return renderTool(message, headerStyle) default: return ( <> @@ -341,123 +343,7 @@ const ChatRow: React.FC = ({ 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 "listFilesTopLevel": - return ( - <> -
- {toolIcon("folder-opened")} - - Claude wants to view the top level files in this directory: - -
- - - ) - case "listFilesRecursive": - return ( - <> -
- {toolIcon("folder-opened")} - - Claude wants to recursively view all files in this directory: - -
- - - ) - case "viewSourceCodeDefinitionsTopLevel": - return ( - <> -
- {toolIcon("file-code")} - - Claude wants to view source code definitions in files at the top level - of this directory: - -
- - - ) - } - break + return renderTool(message, headerStyle) case "request_limit_reached": return ( <> @@ -547,6 +433,125 @@ const ChatRow: React.FC = ({ } } + const renderTool = (message: ClaudeMessage, headerStyle: React.CSSProperties) => { + 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 "listFilesTopLevel": + return ( + <> +
+ {toolIcon("folder-opened")} + + Claude wants to view the top level files in this directory: + +
+ + + ) + case "listFilesRecursive": + return ( + <> +
+ {toolIcon("folder-opened")} + + Claude wants to recursively view all files in this directory: + +
+ + + ) + case "viewSourceCodeDefinitionsTopLevel": + return ( + <> +
+ {toolIcon("file-code")} + + Claude wants to view source code definitions in files at the top level of this + directory: + +
+ + + ) + default: + return null + } + } + // NOTE: we cannot return null as virtuoso does not support it, so we must use a separate visibleMessages array to filter out messages that should not be rendered return ( diff --git a/webview-ui/src/components/ChatView.tsx b/webview-ui/src/components/ChatView.tsx index 4ff9883..d93176f 100644 --- a/webview-ui/src/components/ChatView.tsx +++ b/webview-ui/src/components/ChatView.tsx @@ -165,10 +165,6 @@ const ChatView = ({ case "say": // don't want to reset since there could be a "say" after an "ask" while ask is waiting for response switch (lastMessage.say) { - case "task": - break - case "error": - break case "api_req_started": if (messages.at(-2)?.ask === "command_output") { // if the last ask is a command_output, and we receive an api_req_started, then that means the command has finished and we don't need input from the user anymore (in every other case, the user has to interact with input field or buttons to continue, which does the following automatically) @@ -179,13 +175,13 @@ const ChatView = ({ setEnableButtons(false) } break + case "task": + case "error": case "api_req_finished": - break case "text": - break case "command_output": - break case "completion_result": + case "tool": break } break