only play sounds on errors, task completion, or when user intervention is needed

This commit is contained in:
Justin Quan
2024-12-15 23:13:32 -08:00
parent 63c766f380
commit 09934e20f7

View File

@@ -64,7 +64,6 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
const [isAtBottom, setIsAtBottom] = useState(false) const [isAtBottom, setIsAtBottom] = useState(false)
const [wasStreaming, setWasStreaming] = useState<boolean>(false) const [wasStreaming, setWasStreaming] = useState<boolean>(false)
const [hasStarted, setHasStarted] = useState(false)
// UI layout depends on the last 2 messages // 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 // (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 }) vscode.postMessage({ type: "playSound", audioType })
} }
function playSoundOnMessage(audioType: AudioType) {
if (hasStarted && !isStreaming) {
playSound(audioType)
}
}
useDeepCompareEffect(() => { useDeepCompareEffect(() => {
// if last message is an ask, show user ask UI // 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. // 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 const isPartial = lastMessage.partial === true
switch (lastMessage.ask) { switch (lastMessage.ask) {
case "api_req_failed": case "api_req_failed":
playSoundOnMessage("progress_loop") playSound("progress_loop")
setTextAreaDisabled(true) setTextAreaDisabled(true)
setClineAsk("api_req_failed") setClineAsk("api_req_failed")
setEnableButtons(true) setEnableButtons(true)
@@ -99,7 +92,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
setSecondaryButtonText("Start New Task") setSecondaryButtonText("Start New Task")
break break
case "mistake_limit_reached": case "mistake_limit_reached":
playSoundOnMessage("progress_loop") playSound("progress_loop")
setTextAreaDisabled(false) setTextAreaDisabled(false)
setClineAsk("mistake_limit_reached") setClineAsk("mistake_limit_reached")
setEnableButtons(true) setEnableButtons(true)
@@ -107,7 +100,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
setSecondaryButtonText("Start New Task") setSecondaryButtonText("Start New Task")
break break
case "followup": case "followup":
playSoundOnMessage("notification") playSound("notification")
setTextAreaDisabled(isPartial) setTextAreaDisabled(isPartial)
setClineAsk("followup") setClineAsk("followup")
setEnableButtons(isPartial) setEnableButtons(isPartial)
@@ -115,7 +108,9 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
// setSecondaryButtonText(undefined) // setSecondaryButtonText(undefined)
break break
case "tool": case "tool":
playSoundOnMessage("notification") if (!isAutoApproved(lastMessage)) {
playSound("notification")
}
setTextAreaDisabled(isPartial) setTextAreaDisabled(isPartial)
setClineAsk("tool") setClineAsk("tool")
setEnableButtons(!isPartial) setEnableButtons(!isPartial)
@@ -134,7 +129,9 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
} }
break break
case "browser_action_launch": case "browser_action_launch":
playSoundOnMessage("notification") if (!isAutoApproved(lastMessage)) {
playSound("notification")
}
setTextAreaDisabled(isPartial) setTextAreaDisabled(isPartial)
setClineAsk("browser_action_launch") setClineAsk("browser_action_launch")
setEnableButtons(!isPartial) setEnableButtons(!isPartial)
@@ -142,7 +139,9 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
setSecondaryButtonText("Reject") setSecondaryButtonText("Reject")
break break
case "command": case "command":
playSoundOnMessage("notification") if (!isAutoApproved(lastMessage)) {
playSound("notification")
}
setTextAreaDisabled(isPartial) setTextAreaDisabled(isPartial)
setClineAsk("command") setClineAsk("command")
setEnableButtons(!isPartial) setEnableButtons(!isPartial)
@@ -150,7 +149,6 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
setSecondaryButtonText("Reject") setSecondaryButtonText("Reject")
break break
case "command_output": case "command_output":
playSoundOnMessage("notification")
setTextAreaDisabled(false) setTextAreaDisabled(false)
setClineAsk("command_output") setClineAsk("command_output")
setEnableButtons(true) setEnableButtons(true)
@@ -166,7 +164,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
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
playSoundOnMessage("celebration") playSound("celebration")
setTextAreaDisabled(isPartial) setTextAreaDisabled(isPartial)
setClineAsk("completion_result") setClineAsk("completion_result")
setEnableButtons(!isPartial) setEnableButtons(!isPartial)
@@ -174,7 +172,6 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
setSecondaryButtonText(undefined) setSecondaryButtonText(undefined)
break break
case "resume_task": case "resume_task":
playSoundOnMessage("notification")
setTextAreaDisabled(false) setTextAreaDisabled(false)
setClineAsk("resume_task") setClineAsk("resume_task")
setEnableButtons(true) setEnableButtons(true)
@@ -183,7 +180,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
setDidClickCancel(false) // special case where we reset the cancel button state setDidClickCancel(false) // special case where we reset the cancel button state
break break
case "resume_completed_task": case "resume_completed_task":
playSoundOnMessage("celebration") playSound("celebration")
setTextAreaDisabled(false) setTextAreaDisabled(false)
setClineAsk("resume_completed_task") setClineAsk("resume_completed_task")
setEnableButtons(true) setEnableButtons(true)
@@ -482,15 +479,70 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
return true return true
}) })
}, [modifiedMessages]) }, [modifiedMessages])
useEffect(() => {
if (isStreaming) { const isReadOnlyToolAction = (message: ClineMessage | undefined) => {
// Set to true once any request has started if (message?.type === "ask" && message.text) {
setHasStarted(true) 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 // Only execute when isStreaming changes from true to false
if (wasStreaming && !isStreaming && lastMessage) { if (wasStreaming && !isStreaming && lastMessage) {
// Play appropriate sound based on lastMessage content // Play appropriate sound based on lastMessage content
if (lastMessage.type === "ask") { if (lastMessage.type === "ask") {
// Don't play sounds for auto-approved actions
if (!isAutoApproved(lastMessage)) {
switch (lastMessage.ask) { switch (lastMessage.ask) {
case "api_req_failed": case "api_req_failed":
case "mistake_limit_reached": case "mistake_limit_reached":
@@ -509,6 +561,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
} }
} }
} }
}
// Update previous value // Update previous value
setWasStreaming(isStreaming) setWasStreaming(isStreaming)
}, [isStreaming, lastMessage, wasStreaming]) }, [isStreaming, lastMessage, wasStreaming])
@@ -750,61 +803,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
// Only proceed if we have an ask and buttons are enabled // Only proceed if we have an ask and buttons are enabled
if (!clineAsk || !enableButtons) return if (!clineAsk || !enableButtons) return
const isReadOnlyToolAction = () => { if (isAutoApproved(lastMessage)) {
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())
) {
handlePrimaryButtonClick() handlePrimaryButtonClick()
} }
}, [clineAsk, enableButtons, handlePrimaryButtonClick, alwaysAllowBrowser, alwaysAllowReadOnly, alwaysAllowWrite, alwaysAllowExecute, alwaysAllowMcp, messages, allowedCommands, mcpServers]) }, [clineAsk, enableButtons, handlePrimaryButtonClick, alwaysAllowBrowser, alwaysAllowReadOnly, alwaysAllowWrite, alwaysAllowExecute, alwaysAllowMcp, messages, allowedCommands, mcpServers])