Browse Source

Send tools as says when alwaysAllowReadOnly

Saoud Rizwan 1 year ago
parent
commit
42e9d36580

+ 68 - 48
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))}`

+ 1 - 0
src/shared/ExtensionMessage.ts

@@ -61,6 +61,7 @@ export type ClaudeSay =
 	| "user_feedback"
 	| "api_req_retried"
 	| "command_output"
+	| "tool"
 
 export interface ClaudeSayTool {
 	tool:

+ 122 - 117
webview-ui/src/components/ChatRow.tsx

@@ -325,6 +325,8 @@ const ChatRow: React.FC<ChatRowProps> = ({
 								</div>
 							</>
 						)
+					case "tool":
+						return renderTool(message, headerStyle)
 					default:
 						return (
 							<>
@@ -341,123 +343,7 @@ const ChatRow: React.FC<ChatRowProps> = ({
 			case "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!}
-											syntaxHighlighterStyle={syntaxHighlighterStyle}
-											isExpanded={isExpanded}
-											onToggleExpand={onToggleExpand}
-										/>
-									</>
-								)
-							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!}
-											syntaxHighlighterStyle={syntaxHighlighterStyle}
-											isExpanded={isExpanded}
-											onToggleExpand={onToggleExpand}
-										/>
-									</>
-								)
-							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!}
-											syntaxHighlighterStyle={syntaxHighlighterStyle}
-											isExpanded={isExpanded}
-											onToggleExpand={onToggleExpand}
-										/>
-									</>
-								)
-							case "listFilesTopLevel":
-								return (
-									<>
-										<div style={headerStyle}>
-											{toolIcon("folder-opened")}
-											<span style={{ fontWeight: "bold" }}>
-												Claude wants to view the top level files in this directory:
-											</span>
-										</div>
-										<CodeBlock
-											code={tool.content!}
-											path={tool.path!}
-											language="shell-session"
-											syntaxHighlighterStyle={syntaxHighlighterStyle}
-											isExpanded={isExpanded}
-											onToggleExpand={onToggleExpand}
-										/>
-									</>
-								)
-							case "listFilesRecursive":
-								return (
-									<>
-										<div style={headerStyle}>
-											{toolIcon("folder-opened")}
-											<span style={{ fontWeight: "bold" }}>
-												Claude wants to recursively view all files in this directory:
-											</span>
-										</div>
-										<CodeBlock
-											code={tool.content!}
-											path={tool.path!}
-											language="shell-session"
-											syntaxHighlighterStyle={syntaxHighlighterStyle}
-											isExpanded={isExpanded}
-											onToggleExpand={onToggleExpand}
-										/>
-									</>
-								)
-							case "viewSourceCodeDefinitionsTopLevel":
-								return (
-									<>
-										<div style={headerStyle}>
-											{toolIcon("file-code")}
-											<span style={{ fontWeight: "bold" }}>
-												Claude wants to view source code definitions in files at the top level
-												of this directory:
-											</span>
-										</div>
-										<CodeBlock
-											code={tool.content!}
-											path={tool.path!}
-											syntaxHighlighterStyle={syntaxHighlighterStyle}
-											isExpanded={isExpanded}
-											onToggleExpand={onToggleExpand}
-										/>
-									</>
-								)
-						}
-						break
+						return renderTool(message, headerStyle)
 					case "request_limit_reached":
 						return (
 							<>
@@ -547,6 +433,125 @@ const ChatRow: React.FC<ChatRowProps> = ({
 		}
 	}
 
+	const renderTool = (message: ClaudeMessage, headerStyle: React.CSSProperties) => {
+		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!}
+							syntaxHighlighterStyle={syntaxHighlighterStyle}
+							isExpanded={isExpanded}
+							onToggleExpand={onToggleExpand}
+						/>
+					</>
+				)
+			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!}
+							syntaxHighlighterStyle={syntaxHighlighterStyle}
+							isExpanded={isExpanded}
+							onToggleExpand={onToggleExpand}
+						/>
+					</>
+				)
+			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!}
+							syntaxHighlighterStyle={syntaxHighlighterStyle}
+							isExpanded={isExpanded}
+							onToggleExpand={onToggleExpand}
+						/>
+					</>
+				)
+			case "listFilesTopLevel":
+				return (
+					<>
+						<div style={headerStyle}>
+							{toolIcon("folder-opened")}
+							<span style={{ fontWeight: "bold" }}>
+								Claude wants to view the top level files in this directory:
+							</span>
+						</div>
+						<CodeBlock
+							code={tool.content!}
+							path={tool.path!}
+							language="shell-session"
+							syntaxHighlighterStyle={syntaxHighlighterStyle}
+							isExpanded={isExpanded}
+							onToggleExpand={onToggleExpand}
+						/>
+					</>
+				)
+			case "listFilesRecursive":
+				return (
+					<>
+						<div style={headerStyle}>
+							{toolIcon("folder-opened")}
+							<span style={{ fontWeight: "bold" }}>
+								Claude wants to recursively view all files in this directory:
+							</span>
+						</div>
+						<CodeBlock
+							code={tool.content!}
+							path={tool.path!}
+							language="shell-session"
+							syntaxHighlighterStyle={syntaxHighlighterStyle}
+							isExpanded={isExpanded}
+							onToggleExpand={onToggleExpand}
+						/>
+					</>
+				)
+			case "viewSourceCodeDefinitionsTopLevel":
+				return (
+					<>
+						<div style={headerStyle}>
+							{toolIcon("file-code")}
+							<span style={{ fontWeight: "bold" }}>
+								Claude wants to view source code definitions in files at the top level of this
+								directory:
+							</span>
+						</div>
+						<CodeBlock
+							code={tool.content!}
+							path={tool.path!}
+							syntaxHighlighterStyle={syntaxHighlighterStyle}
+							isExpanded={isExpanded}
+							onToggleExpand={onToggleExpand}
+						/>
+					</>
+				)
+			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 (

+ 3 - 7
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