diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index 929fa9427aa0..21cf071a03de 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -176,6 +176,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction(null) const [sendingDisabled, setSendingDisabled] = useState(false) const [selectedImages, setSelectedImages] = useState([]) + const [isCancelPending, setIsCancelPending] = useState(false) // we need to hold on to the ask because useEffect > lastMessage will always let us know when an ask comes in and handle it, but by the time handleMessage is called, the last message might not be the ask anymore (it could be a say that followed) const [clineAsk, setClineAsk] = useState(undefined) @@ -514,6 +515,32 @@ const ChatViewComponent: React.ForwardRefRenderFunction { + const isLastMessagePartial = messages.at(-1)?.partial === true + + if (isLastMessagePartial) { + return true + } + + const lastApiReqStarted = findLast(messages, (message: ClineMessage) => message.say === "api_req_started") + + if ( + lastApiReqStarted && + lastApiReqStarted.text !== null && + lastApiReqStarted.text !== undefined && + lastApiReqStarted.say === "api_req_started" + ) { + const cost = JSON.parse(lastApiReqStarted.text).cost + + if (cost === undefined) { + return true // API request has not finished yet. + } + } + + return false + }, []) + const isStreaming = useMemo(() => { // Checking clineAsk isn't enough since messages effect may be called // again for a tool for example, set clineAsk to its value, and if the @@ -531,32 +558,15 @@ const ChatViewComponent: React.ForwardRefRenderFunction message.say === "api_req_started", - ) - - if ( - lastApiReqStarted && - lastApiReqStarted.text !== null && - lastApiReqStarted.text !== undefined && - lastApiReqStarted.say === "api_req_started" - ) { - const cost = JSON.parse(lastApiReqStarted.text).cost - - if (cost === undefined) { - return true // API request has not finished yet. - } - } - } - - return false - }, [modifiedMessages, clineAsk, enableButtons, primaryButtonText]) + // Track the actual streaming state for the cancel button separately. + // This ensures the cancel button remains enabled during API requests, + // even when there's a tool approval dialog. + const isApiRequestInProgress = useMemo(() => { + return checkApiRequestInProgress(modifiedMessages) + }, [modifiedMessages, checkApiRequestInProgress]) const markFollowUpAsAnswered = useCallback(() => { const lastFollowUpMessage = messagesRef.current.findLast((msg: ClineMessage) => msg.ask === "followup") @@ -733,9 +743,13 @@ const ChatViewComponent: React.ForwardRefRenderFunction setIsCancelPending(false), 1000) return } @@ -773,7 +787,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction @@ -1937,10 +1952,10 @@ const ChatViewComponent: React.ForwardRefRenderFunction )} - {(secondaryButtonText || isStreaming) && ( + {(secondaryButtonText || isStreaming || isApiRequestInProgress) && ( handleSecondaryButtonClick(inputValue, selectedImages)}> - {isStreaming ? t("chat:cancel.title") : secondaryButtonText} + {isStreaming || isApiRequestInProgress + ? t("chat:cancel.title") + : secondaryButtonText} )}