Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 57 additions & 34 deletions webview-ui/src/components/chat/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
const textAreaRef = useRef<HTMLTextAreaElement>(null)
const [sendingDisabled, setSendingDisabled] = useState(false)
const [selectedImages, setSelectedImages] = useState<string[]>([])
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<ClineAsk | undefined>(undefined)
Expand Down Expand Up @@ -514,6 +515,32 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
prevExpandedRowsRef.current = expandedRows // Store current state for next comparison
}, [expandedRows])

// Helper function to check if an API request is actively in progress
const checkApiRequestInProgress = useCallback((messages: ClineMessage[]) => {
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
Expand All @@ -531,32 +558,15 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
return false
}

const isLastMessagePartial = modifiedMessages.at(-1)?.partial === true
return checkApiRequestInProgress(modifiedMessages)
}, [modifiedMessages, clineAsk, enableButtons, primaryButtonText, checkApiRequestInProgress])

if (isLastMessagePartial) {
return true
} else {
const lastApiReqStarted = findLast(
modifiedMessages,
(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
}, [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")
Expand Down Expand Up @@ -733,9 +743,13 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro

const trimmedInput = text?.trim()

if (isStreaming) {
if ((isStreaming || isApiRequestInProgress) && !isCancelPending) {
// Prevent rapid clicks by setting pending state
setIsCancelPending(true)
vscode.postMessage({ type: "cancelTask" })
setDidClickCancel(true)
// Reset pending state after a short delay
setTimeout(() => setIsCancelPending(false), 1000)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Memory leak: the setTimeout at line 752 is not cleaned up if the component unmounts before the timeout fires. If a user cancels a task and quickly navigates away or the component unmounts, the setTimeout will still fire and attempt to call setIsCancelPending on an unmounted component, causing a memory leak and potential React warnings.

Fix it with Roo Code or mention @roomote and request a fix.

return
}

Expand Down Expand Up @@ -773,7 +787,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
setClineAsk(undefined)
setEnableButtons(false)
},
[clineAsk, startNewTask, isStreaming],
[clineAsk, startNewTask, isStreaming, isApiRequestInProgress, isCancelPending],
)

const { info: model } = useSelectedModel(apiConfiguration)
Expand Down Expand Up @@ -1763,7 +1777,8 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
vscode.postMessage({ type: "condenseTaskContextRequest", text: taskId })
}

const areButtonsVisible = showScrollToBottom || primaryButtonText || secondaryButtonText || isStreaming
const areButtonsVisible =
showScrollToBottom || primaryButtonText || secondaryButtonText || isStreaming || isApiRequestInProgress

return (
<div
Expand Down Expand Up @@ -1887,7 +1902,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
className={`flex h-9 items-center mb-1 px-[15px] ${
showScrollToBottom
? "opacity-100"
: enableButtons || (isStreaming && !didClickCancel)
: enableButtons || ((isStreaming || isApiRequestInProgress) && !didClickCancel)
? "opacity-100"
: "opacity-50"
}`}>
Expand Down Expand Up @@ -1937,10 +1952,10 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
</VSCodeButton>
</StandardTooltip>
)}
{(secondaryButtonText || isStreaming) && (
{(secondaryButtonText || isStreaming || isApiRequestInProgress) && (
<StandardTooltip
content={
isStreaming
isStreaming || isApiRequestInProgress
? t("chat:cancel.tooltip")
: secondaryButtonText === t("chat:startNewTask.title")
? t("chat:startNewTask.tooltip")
Expand All @@ -1952,10 +1967,18 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
}>
<VSCodeButton
appearance="secondary"
disabled={!enableButtons && !(isStreaming && !didClickCancel)}
className={isStreaming ? "flex-[2] ml-0" : "flex-1 ml-[6px]"}
disabled={
!enableButtons && !(isApiRequestInProgress && !didClickCancel)
}
className={
isStreaming || isApiRequestInProgress
? "flex-[2] ml-0"
: "flex-1 ml-[6px]"
}
onClick={() => handleSecondaryButtonClick(inputValue, selectedImages)}>
{isStreaming ? t("chat:cancel.title") : secondaryButtonText}
{isStreaming || isApiRequestInProgress
? t("chat:cancel.title")
: secondaryButtonText}
</VSCodeButton>
</StandardTooltip>
)}
Expand Down