Fix: Chat UI consistency and layout shifts (#5237)

This commit is contained in:
SeeD 2025-07-03 06:02:00 +02:00 committed by GitHub
parent e4b051bb71
commit abea1920ac
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 83 additions and 77 deletions

View file

@ -142,7 +142,7 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
display: "flex",
alignItems: "center",
gap: "8px",
padding: isExpanded ? "8px 0" : "8px 0 0 0",
padding: isExpanded ? "8px 0" : "2px 0 0 0",
cursor: "pointer",
}}
onClick={toggleExpanded}>

View file

@ -787,10 +787,10 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
"relative",
"flex",
"flex-col",
"gap-2",
"gap-1",
"bg-editor-background",
"m-2 mt-1",
"p-1.5",
"px-1.5",
"pb-1",
"outline-none",
"border",
"border-none",
@ -998,7 +998,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
/>
)}
<div className={cn("flex", "justify-between", "items-center", "mt-auto", "pt-0.5")}>
<div className={cn("flex", "justify-between", "items-center", "mt-auto")}>
<div className={cn("flex", "items-center", "gap-1", "min-w-0")}>
<div className="shrink-0">
<ModeSelector

View file

@ -1424,6 +1424,8 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
vscode.postMessage({ type: "condenseTaskContextRequest", text: taskId })
}
const areButtonsVisible = showScrollToBottom || primaryButtonText || secondaryButtonText || isStreaming
return (
<div className={isHidden ? "hidden" : "fixed top-0 left-0 right-0 bottom-0 flex flex-col overflow-hidden"}>
{(showAnnouncement || showAnnouncementModal) && (
@ -1525,7 +1527,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
// but becomes scrollable when the viewport is too small
*/}
{!task && (
<div className="mb-[-2px] flex-initial min-h-0">
<div className="mb-1 flex-initial min-h-0">
<AutoApproveMenu />
</div>
)}
@ -1536,7 +1538,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
<Virtuoso
ref={virtuosoRef}
key={task.ts} // trick to make sure virtuoso re-renders when task changes, and we use initialTopMostItemIndex to start at the bottom
className="scrollable grow overflow-y-scroll mb-[5px]"
className="scrollable grow overflow-y-scroll mb-1"
// increasing top by 3_000 to prevent jumping around when user collapses a row
increaseViewportBy={{ top: 3_000, bottom: Number.MAX_SAFE_INTEGER }} // hack to make sure the last message is always rendered to get truly perfect scroll to bottom animation when new messages are added (Number.MAX_SAFE_INTEGER is safe for arithmetic operations, which is all virtuoso uses this value for in src/sizeRangeSystem.ts)
data={groupedMessages} // messages is the raw format returned by extension, modifiedMessages is the manipulated structure that combines certain messages of related type, and visibleMessages is the filtered structure that removes messages that should not be rendered
@ -1552,83 +1554,87 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
initialTopMostItemIndex={groupedMessages.length - 1}
/>
</div>
<AutoApproveMenu />
{showScrollToBottom ? (
<div className="flex px-[15px] pt-[10px]">
<StandardTooltip content={t("chat:scrollToBottom")}>
<div
className="bg-[color-mix(in_srgb,_var(--vscode-toolbar-hoverBackground)_55%,_transparent)] rounded-[3px] overflow-hidden cursor-pointer flex justify-center items-center flex-1 h-[25px] hover:bg-[color-mix(in_srgb,_var(--vscode-toolbar-hoverBackground)_90%,_transparent)] active:bg-[color-mix(in_srgb,_var(--vscode-toolbar-hoverBackground)_70%,_transparent)]"
onClick={() => {
scrollToBottomSmooth()
disableAutoScrollRef.current = false
}}>
<span className="codicon codicon-chevron-down text-[18px]"></span>
</div>
</StandardTooltip>
</div>
) : (
<div className={`flex-initial min-h-0 ${!areButtonsVisible ? "mb-1" : ""}`}>
<AutoApproveMenu />
</div>
{areButtonsVisible && (
<div
className={`flex ${
primaryButtonText || secondaryButtonText || isStreaming ? "px-[15px] pt-[10px]" : "p-0"
} ${
primaryButtonText || secondaryButtonText || isStreaming
? enableButtons || (isStreaming && !didClickCancel)
className={`flex h-9 items-center mb-1 px-[15px] ${
showScrollToBottom
? "opacity-100"
: enableButtons || (isStreaming && !didClickCancel)
? "opacity-100"
: "opacity-50"
: "opacity-0"
}`}>
{primaryButtonText && !isStreaming && (
<StandardTooltip
content={
primaryButtonText === t("chat:retry.title")
? t("chat:retry.tooltip")
: primaryButtonText === t("chat:save.title")
? t("chat:save.tooltip")
: primaryButtonText === t("chat:approve.title")
? t("chat:approve.tooltip")
: primaryButtonText === t("chat:runCommand.title")
? t("chat:runCommand.tooltip")
: primaryButtonText === t("chat:startNewTask.title")
? t("chat:startNewTask.tooltip")
: primaryButtonText === t("chat:resumeTask.title")
? t("chat:resumeTask.tooltip")
: primaryButtonText === t("chat:proceedAnyways.title")
? t("chat:proceedAnyways.tooltip")
: primaryButtonText ===
t("chat:proceedWhileRunning.title")
? t("chat:proceedWhileRunning.tooltip")
: undefined
}>
<VSCodeButton
appearance="primary"
disabled={!enableButtons}
className={secondaryButtonText ? "flex-1 mr-[6px]" : "flex-[2] mr-0"}
onClick={() => handlePrimaryButtonClick(inputValue, selectedImages)}>
{primaryButtonText}
</VSCodeButton>
</StandardTooltip>
)}
{(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
}>
{showScrollToBottom ? (
<StandardTooltip content={t("chat:scrollToBottom")}>
<VSCodeButton
appearance="secondary"
disabled={!enableButtons && !(isStreaming && !didClickCancel)}
className={isStreaming ? "flex-[2] ml-0" : "flex-1 ml-[6px]"}
onClick={() => handleSecondaryButtonClick(inputValue, selectedImages)}>
{isStreaming ? t("chat:cancel.title") : secondaryButtonText}
className="flex-[2]"
onClick={() => {
scrollToBottomSmooth()
disableAutoScrollRef.current = false
}}>
<span className="codicon codicon-chevron-down"></span>
</VSCodeButton>
</StandardTooltip>
) : (
<>
{primaryButtonText && !isStreaming && (
<StandardTooltip
content={
primaryButtonText === t("chat:retry.title")
? t("chat:retry.tooltip")
: primaryButtonText === t("chat:save.title")
? t("chat:save.tooltip")
: primaryButtonText === t("chat:approve.title")
? t("chat:approve.tooltip")
: primaryButtonText === t("chat:runCommand.title")
? t("chat:runCommand.tooltip")
: primaryButtonText === t("chat:startNewTask.title")
? t("chat:startNewTask.tooltip")
: primaryButtonText === t("chat:resumeTask.title")
? t("chat:resumeTask.tooltip")
: primaryButtonText ===
t("chat:proceedAnyways.title")
? t("chat:proceedAnyways.tooltip")
: primaryButtonText ===
t("chat:proceedWhileRunning.title")
? t("chat:proceedWhileRunning.tooltip")
: undefined
}>
<VSCodeButton
appearance="primary"
disabled={!enableButtons}
className={secondaryButtonText ? "flex-1 mr-[6px]" : "flex-[2] mr-0"}
onClick={() => handlePrimaryButtonClick(inputValue, selectedImages)}>
{primaryButtonText}
</VSCodeButton>
</StandardTooltip>
)}
{(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
}>
<VSCodeButton
appearance="secondary"
disabled={!enableButtons && !(isStreaming && !didClickCancel)}
className={isStreaming ? "flex-[2] ml-0" : "flex-1 ml-[6px]"}
onClick={() => handleSecondaryButtonClick(inputValue, selectedImages)}>
{isStreaming ? t("chat:cancel.title") : secondaryButtonText}
</VSCodeButton>
</StandardTooltip>
)}
</>
)}
</div>
)}

View file

@ -68,7 +68,7 @@ const TaskItem = ({
</div>
)}
<div className="flex-1">
<div className="flex-1 min-w-0">
{/* Header with metadata */}
<TaskItemHeader item={item} isSelectionMode={isSelectionMode} onDelete={onDelete} />