feat: move cancel button from bar into input text area

- Add isStreaming and onCancelTask props to ChatTextArea component
- Add cancel button (X icon) inside input area when streaming
- Remove large cancel button bar from ChatView when streaming
- Keep other buttons (primary/secondary) in the bar for non-streaming actions
- Cleaner, more compact UI during task execution

Closes #10086
This commit is contained in:
Roo Code 2025-12-16 09:11:55 +00:00
parent 596783d365
commit ff37ca8acb
2 changed files with 68 additions and 44 deletions

View file

@ -1,7 +1,7 @@
import React, { forwardRef, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"
import { useEvent } from "react-use"
import DynamicTextArea from "react-textarea-autosize"
import { VolumeX, Image, WandSparkles, SendHorizontal, MessageSquareX } from "lucide-react"
import { VolumeX, Image, WandSparkles, SendHorizontal, MessageSquareX, X } from "lucide-react"
import { mentionRegex, mentionRegexGlobal, commandRegexGlobal, unescapeSpaces } from "@roo/context-mentions"
import { WebviewMessage } from "@roo/WebviewMessage"
@ -54,6 +54,9 @@ interface ChatTextAreaProps {
// Browser session status
isBrowserSessionActive?: boolean
showBrowserDockToggle?: boolean
// Streaming/cancel props
isStreaming?: boolean
onCancelTask?: () => void
}
export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
@ -76,6 +79,8 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
onCancel,
isBrowserSessionActive = false,
showBrowserDockToggle = false,
isStreaming = false,
onCancelTask,
},
ref,
) => {
@ -1184,30 +1189,53 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
</button>
</StandardTooltip>
)}
<StandardTooltip
content={t("chat:pressToSend", { keyCombination: sendKeyCombination })}>
<button
aria-label={t("chat:pressToSend", { keyCombination: sendKeyCombination })}
disabled={false}
onClick={onSend}
className={cn(
"relative inline-flex items-center justify-center",
"bg-transparent border-none p-1.5",
"rounded-md min-w-[28px] min-h-[28px]",
"text-vscode-descriptionForeground hover:text-vscode-foreground",
"transition-all duration-200",
hasInputContent
? "opacity-100 hover:opacity-100 pointer-events-auto"
: "opacity-0 pointer-events-none",
hasInputContent &&
{!isEditMode && isStreaming && onCancelTask && (
<StandardTooltip content={t("chat:cancel.tooltip")}>
<button
aria-label={t("chat:cancel.title")}
onClick={onCancelTask}
className={cn(
"relative inline-flex items-center justify-center",
"bg-transparent border-none p-1.5",
"rounded-md min-w-[28px] min-h-[28px]",
"text-vscode-errorForeground hover:text-vscode-errorForeground",
"transition-all duration-150",
"opacity-100 pointer-events-auto",
"hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)]",
"focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder",
hasInputContent && "active:bg-[rgba(255,255,255,0.1)]",
hasInputContent && "cursor-pointer",
)}>
<SendHorizontal className="w-4 h-4" />
</button>
</StandardTooltip>
"focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder",
"active:bg-[rgba(255,255,255,0.1)]",
"cursor-pointer",
)}>
<X className="w-4 h-4" />
</button>
</StandardTooltip>
)}
{(!isStreaming || isEditMode) && (
<StandardTooltip
content={t("chat:pressToSend", { keyCombination: sendKeyCombination })}>
<button
aria-label={t("chat:pressToSend", { keyCombination: sendKeyCombination })}
disabled={false}
onClick={onSend}
className={cn(
"relative inline-flex items-center justify-center",
"bg-transparent border-none p-1.5",
"rounded-md min-w-[28px] min-h-[28px]",
"text-vscode-descriptionForeground hover:text-vscode-foreground",
"transition-all duration-200",
hasInputContent
? "opacity-100 hover:opacity-100 pointer-events-auto"
: "opacity-0 pointer-events-none",
hasInputContent &&
"hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)]",
"focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder",
hasInputContent && "active:bg-[rgba(255,255,255,0.1)]",
hasInputContent && "cursor-pointer",
)}>
<SendHorizontal className="w-4 h-4" />
</button>
</StandardTooltip>
)}
</div>
{!inputValue && (

View file

@ -144,7 +144,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
const [enableButtons, setEnableButtons] = useState<boolean>(false)
const [primaryButtonText, setPrimaryButtonText] = useState<string | undefined>(undefined)
const [secondaryButtonText, setSecondaryButtonText] = useState<string | undefined>(undefined)
const [didClickCancel, setDidClickCancel] = useState(false)
const [_didClickCancel, setDidClickCancel] = useState(false)
const virtuosoRef = useRef<VirtuosoHandle>(null)
const [expandedRows, setExpandedRows] = useState<Record<number, boolean>>({})
const prevExpandedRowsRef = useRef<Record<number, boolean>>()
@ -1386,7 +1386,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
vscode.postMessage({ type: "condenseTaskContextRequest", text: taskId })
}
const areButtonsVisible = showScrollToBottom || primaryButtonText || secondaryButtonText || isStreaming
const areButtonsVisible = showScrollToBottom || primaryButtonText || secondaryButtonText
return (
<div
@ -1489,11 +1489,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
{areButtonsVisible && (
<div
className={`flex h-9 items-center mb-1 px-[15px] ${
showScrollToBottom
? "opacity-100"
: enableButtons || (isStreaming && !didClickCancel)
? "opacity-100"
: "opacity-50"
showScrollToBottom ? "opacity-100" : enableButtons ? "opacity-100" : "opacity-50"
}`}>
{showScrollToBottom ? (
<StandardTooltip content={t("chat:scrollToBottom")}>
@ -1545,25 +1541,23 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
</Button>
</StandardTooltip>
)}
{(secondaryButtonText || isStreaming) && (
{secondaryButtonText && !isStreaming && (
<StandardTooltip
content={
isStreaming
? t("chat:cancel.tooltip")
: secondaryButtonText === t("chat:startNewTask.title")
? t("chat:startNewTask.tooltip")
: secondaryButtonText === t("chat:reject.title")
? t("chat:reject.tooltip")
: secondaryButtonText === t("chat:terminate.title")
? t("chat:terminate.tooltip")
: undefined
secondaryButtonText === t("chat:startNewTask.title")
? t("chat:startNewTask.tooltip")
: secondaryButtonText === t("chat:reject.title")
? t("chat:reject.tooltip")
: secondaryButtonText === t("chat:terminate.title")
? t("chat:terminate.tooltip")
: undefined
}>
<Button
variant="secondary"
disabled={!enableButtons && !(isStreaming && !didClickCancel)}
className={isStreaming ? "flex-[2] ml-0" : "flex-1 ml-[6px]"}
disabled={!enableButtons}
className="flex-1 ml-[6px]"
onClick={() => handleSecondaryButtonClick(inputValue, selectedImages)}>
{isStreaming ? t("chat:cancel.title") : secondaryButtonText}
{secondaryButtonText}
</Button>
</StandardTooltip>
)}
@ -1612,6 +1606,8 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
modeShortcutText={modeShortcutText}
isBrowserSessionActive={!!isBrowserSessionActive}
showBrowserDockToggle={showBrowserDockToggle}
isStreaming={isStreaming}
onCancelTask={() => handleSecondaryButtonClick(inputValue, selectedImages)}
/>
{isProfileDisabled && (