From 09934e20f7bddc4d45ab92a618b838d828d92732 Mon Sep 17 00:00:00 2001 From: Justin Quan Date: Sun, 15 Dec 2024 23:13:32 -0800 Subject: [PATCH] only play sounds on errors, task completion, or when user intervention is needed --- webview-ui/src/components/chat/ChatView.tsx | 181 ++++++++++---------- 1 file changed, 90 insertions(+), 91 deletions(-) diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index e4e0880..777425e 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -64,7 +64,6 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie const [isAtBottom, setIsAtBottom] = useState(false) const [wasStreaming, setWasStreaming] = useState(false) - const [hasStarted, setHasStarted] = useState(false) // UI layout depends on the last 2 messages // (since it relies on the content of these messages, we are deep comparing. i.e. the button state after hitting button sets enableButtons to false, and this effect otherwise would have to true again even if messages didn't change @@ -75,12 +74,6 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie vscode.postMessage({ type: "playSound", audioType }) } - function playSoundOnMessage(audioType: AudioType) { - if (hasStarted && !isStreaming) { - playSound(audioType) - } - } - useDeepCompareEffect(() => { // if last message is an ask, show user ask UI // if user finished a task, then start a new task with a new conversation history since in this moment that the extension is waiting for user response, the user could close the extension and the conversation history would be lost. @@ -91,7 +84,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie const isPartial = lastMessage.partial === true switch (lastMessage.ask) { case "api_req_failed": - playSoundOnMessage("progress_loop") + playSound("progress_loop") setTextAreaDisabled(true) setClineAsk("api_req_failed") setEnableButtons(true) @@ -99,7 +92,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie setSecondaryButtonText("Start New Task") break case "mistake_limit_reached": - playSoundOnMessage("progress_loop") + playSound("progress_loop") setTextAreaDisabled(false) setClineAsk("mistake_limit_reached") setEnableButtons(true) @@ -107,7 +100,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie setSecondaryButtonText("Start New Task") break case "followup": - playSoundOnMessage("notification") + playSound("notification") setTextAreaDisabled(isPartial) setClineAsk("followup") setEnableButtons(isPartial) @@ -115,7 +108,9 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie // setSecondaryButtonText(undefined) break case "tool": - playSoundOnMessage("notification") + if (!isAutoApproved(lastMessage)) { + playSound("notification") + } setTextAreaDisabled(isPartial) setClineAsk("tool") setEnableButtons(!isPartial) @@ -134,7 +129,9 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie } break case "browser_action_launch": - playSoundOnMessage("notification") + if (!isAutoApproved(lastMessage)) { + playSound("notification") + } setTextAreaDisabled(isPartial) setClineAsk("browser_action_launch") setEnableButtons(!isPartial) @@ -142,7 +139,9 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie setSecondaryButtonText("Reject") break case "command": - playSoundOnMessage("notification") + if (!isAutoApproved(lastMessage)) { + playSound("notification") + } setTextAreaDisabled(isPartial) setClineAsk("command") setEnableButtons(!isPartial) @@ -150,7 +149,6 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie setSecondaryButtonText("Reject") break case "command_output": - playSoundOnMessage("notification") setTextAreaDisabled(false) setClineAsk("command_output") setEnableButtons(true) @@ -166,7 +164,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie break case "completion_result": // extension waiting for feedback. but we can just present a new task button - playSoundOnMessage("celebration") + playSound("celebration") setTextAreaDisabled(isPartial) setClineAsk("completion_result") setEnableButtons(!isPartial) @@ -174,7 +172,6 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie setSecondaryButtonText(undefined) break case "resume_task": - playSoundOnMessage("notification") setTextAreaDisabled(false) setClineAsk("resume_task") setEnableButtons(true) @@ -183,7 +180,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie setDidClickCancel(false) // special case where we reset the cancel button state break case "resume_completed_task": - playSoundOnMessage("celebration") + playSound("celebration") setTextAreaDisabled(false) setClineAsk("resume_completed_task") setEnableButtons(true) @@ -482,30 +479,86 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie return true }) }, [modifiedMessages]) - useEffect(() => { - if (isStreaming) { - // Set to true once any request has started - setHasStarted(true) + + const isReadOnlyToolAction = (message: ClineMessage | undefined) => { + if (message?.type === "ask" && message.text) { + const tool = JSON.parse(message.text) + return ["readFile", "listFiles", "listFilesTopLevel", "listFilesRecursive", "listCodeDefinitionNames", "searchFiles"].includes(tool.tool) } + return false + } + + const isWriteToolAction = (message: ClineMessage | undefined) => { + if (message?.type === "ask" && message.text) { + const tool = JSON.parse(message.text) + return ["editedExistingFile", "appliedDiff", "newFileCreated"].includes(tool.tool) + } + return false + } + + const isMcpToolAlwaysAllowed = (message: ClineMessage | undefined) => { + if (message?.type === "ask" && message.ask === "use_mcp_server" && message.text) { + const mcpServerUse = JSON.parse(message.text) as { type: string; serverName: string; toolName: string } + if (mcpServerUse.type === "use_mcp_tool") { + const server = mcpServers?.find((s: McpServer) => s.name === mcpServerUse.serverName) + const tool = server?.tools?.find((t: McpTool) => t.name === mcpServerUse.toolName) + return tool?.alwaysAllow || false + } + } + return false + } + + const isAllowedCommand = (message: ClineMessage | undefined) => { + if (message?.type === "ask" && message.text) { + const command = message.text + + // Split command by chaining operators + const commands = command.split(/&&|\|\||;|\||\$\(|`/).map(cmd => cmd.trim()) + + // Check if all individual commands are allowed + return commands.every((cmd) => { + const trimmedCommand = cmd.toLowerCase() + return allowedCommands?.some((prefix) => trimmedCommand.startsWith(prefix.toLowerCase())) + }) + } + return false + } + + const isAutoApproved = (message: ClineMessage | undefined) => { + if (!message || message.type !== "ask") return false + + return ( + (alwaysAllowBrowser && message.ask === "browser_action_launch") || + (alwaysAllowReadOnly && message.ask === "tool" && isReadOnlyToolAction(message)) || + (alwaysAllowWrite && message.ask === "tool" && isWriteToolAction(message)) || + (alwaysAllowExecute && message.ask === "command" && isAllowedCommand(message)) || + (alwaysAllowMcp && message.ask === "use_mcp_server" && isMcpToolAlwaysAllowed(message)) + ) + } + + useEffect(() => { // Only execute when isStreaming changes from true to false if (wasStreaming && !isStreaming && lastMessage) { // Play appropriate sound based on lastMessage content if (lastMessage.type === "ask") { - switch (lastMessage.ask) { - case "api_req_failed": - case "mistake_limit_reached": - playSound("progress_loop") - break - case "tool": - case "followup": - case "browser_action_launch": - case "resume_task": - playSound("notification") - break - case "completion_result": - case "resume_completed_task": - playSound("celebration") - break + // Don't play sounds for auto-approved actions + if (!isAutoApproved(lastMessage)) { + switch (lastMessage.ask) { + case "api_req_failed": + case "mistake_limit_reached": + playSound("progress_loop") + break + case "tool": + case "followup": + case "browser_action_launch": + case "resume_task": + playSound("notification") + break + case "completion_result": + case "resume_completed_task": + playSound("celebration") + break + } } } } @@ -750,61 +803,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie // Only proceed if we have an ask and buttons are enabled if (!clineAsk || !enableButtons) return - const isReadOnlyToolAction = () => { - const lastMessage = messages.at(-1) - if (lastMessage?.type === "ask" && lastMessage.text) { - const tool = JSON.parse(lastMessage.text) - return ["readFile", "listFiles", "listFilesTopLevel", "listFilesRecursive", "listCodeDefinitionNames", "searchFiles"].includes(tool.tool) - } - return false - } - - const isWriteToolAction = () => { - const lastMessage = messages.at(-1) - if (lastMessage?.type === "ask" && lastMessage.text) { - const tool = JSON.parse(lastMessage.text) - return ["editedExistingFile", "appliedDiff", "newFileCreated"].includes(tool.tool) - } - return false - } - - const isMcpToolAlwaysAllowed = () => { - const lastMessage = messages.at(-1) - if (lastMessage?.type === "ask" && lastMessage.ask === "use_mcp_server" && lastMessage.text) { - const mcpServerUse = JSON.parse(lastMessage.text) as { type: string; serverName: string; toolName: string } - if (mcpServerUse.type === "use_mcp_tool") { - const server = mcpServers?.find((s: McpServer) => s.name === mcpServerUse.serverName) - const tool = server?.tools?.find((t: McpTool) => t.name === mcpServerUse.toolName) - return tool?.alwaysAllow || false - } - } - return false - } - - const isAllowedCommand = () => { - const lastMessage = messages.at(-1) - if (lastMessage?.type === "ask" && lastMessage.text) { - const command = lastMessage.text - - // Split command by chaining operators - const commands = command.split(/&&|\|\||;|\||\$\(|`/).map(cmd => cmd.trim()) - - // Check if all individual commands are allowed - return commands.every((cmd) => { - const trimmedCommand = cmd.toLowerCase() - return allowedCommands?.some((prefix) => trimmedCommand.startsWith(prefix.toLowerCase())) - }) - } - return false - } - - if ( - (alwaysAllowBrowser && clineAsk === "browser_action_launch") || - (alwaysAllowReadOnly && clineAsk === "tool" && isReadOnlyToolAction()) || - (alwaysAllowWrite && clineAsk === "tool" && isWriteToolAction()) || - (alwaysAllowExecute && clineAsk === "command" && isAllowedCommand()) || - (alwaysAllowMcp && clineAsk === "use_mcp_server" && isMcpToolAlwaysAllowed()) - ) { + if (isAutoApproved(lastMessage)) { handlePrimaryButtonClick() } }, [clineAsk, enableButtons, handlePrimaryButtonClick, alwaysAllowBrowser, alwaysAllowReadOnly, alwaysAllowWrite, alwaysAllowExecute, alwaysAllowMcp, messages, allowedCommands, mcpServers])