Fix queued prompt row alignment and auto-scroll when queueing a message (#12767)

* Fix queued prompt row alignment and auto-scroll on queue

Center the dot, badges, and cancel button on the first text line of each queued prompt row (the X previously sat ~3px below the text), and re-pin the chat view to the bottom when a prompt is queued so the queue banner doesn't cover the end of the conversation.

* Don't treat task switches as queue growth for auto-scroll

Guard the queued-prompt auto-scroll effect on the displayed task's ts: switching to a task that already has queued prompts grows the count without a send from this webview, and should not hijack the newly opened conversation's scroll position.
This commit is contained in:
Saoud Rizwan
2026-07-30 17:33:19 -07:00
committed by GitHub
parent 56fd6bb1ce
commit 4ec2b68c7c
2 changed files with 40 additions and 5 deletions
@@ -339,6 +339,36 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
// Use scroll behavior hook
const scrollBehavior = useScrollBehavior(displayMessages, visibleMessages, groupedMessages, expandedRows, setExpandedRows)
const { scrollToBottomSmooth, scrollToBottomAuto, disableAutoScrollRef } = scrollBehavior
// When a prompt gets queued, the queue banner mounts (or grows) in the footer, which
// shrinks the messages area and visually covers the bottom of the conversation. No new
// chat row is added, so the list-length-based auto-scroll never fires — re-pin to the
// bottom here so the latest content stays visible.
const queuedPromptCount = queuedPrompts?.length ?? 0
const taskTs = task?.ts
const prevQueuedPromptCountRef = useRef(queuedPromptCount)
const prevQueuedPromptTaskTsRef = useRef(taskTs)
useEffect(() => {
const previousCount = prevQueuedPromptCountRef.current
const previousTaskTs = prevQueuedPromptTaskTsRef.current
prevQueuedPromptCountRef.current = queuedPromptCount
prevQueuedPromptTaskTsRef.current = taskTs
// A task switch can grow the count without a send from this webview (the newly
// displayed task may already have queued prompts) — don't hijack its scroll position.
if (taskTs !== previousTaskTs || queuedPromptCount <= previousCount) {
return
}
// Queueing is a deliberate send, so re-engage bottom pinning like handleSendMessage does.
disableAutoScrollRef.current = false
scrollToBottomSmooth()
// Settle with an instant scroll once the footer's layout change has landed.
setTimeout(() => {
if (!disableAutoScrollRef.current) {
scrollToBottomAuto()
}
}, 50)
}, [queuedPromptCount, taskTs, scrollToBottomSmooth, scrollToBottomAuto, disableAutoScrollRef])
const placeholderText = useMemo(() => {
const text = task ? "Type a message..." : "Type your task here..."
@@ -64,25 +64,30 @@ export function QueuedPrompts({ items = [] }: QueuedPromptsProps) {
const attachments = attachmentLabel(item.attachmentCount)
const isSteer = item.delivery === "steer"
const isCancelling = cancellingIds.has(item.id)
// The prompt text renders in spans whose line boxes are 5 spacing units tall
// (the global `span { @apply leading-5 }` base style), so every control in the
// row is sized/offset against that same 5-unit first line to stay vertically
// centered with it: the dot ((5 - 1.5) / 2 units), the h-5 badges, and the
// size-5 (26px) cancel button pulled in by -my-1.5 ((5u - size-5) / 2).
return (
<div
className="flex items-start gap-2 rounded-[3px] bg-input-background/40 px-2 py-1.5 text-xs leading-snug"
className="flex items-start gap-2 rounded-[3px] bg-input-background/40 px-2 py-1.5 text-xs"
key={item.id}>
<span aria-hidden="true" className="mt-[5px] size-1.5 shrink-0 rounded-full bg-description/70" />
<span aria-hidden="true" className="mt-1.75 size-1.5 shrink-0 rounded-full bg-description/70" />
<span className="min-w-0 flex-1 break-words text-foreground">{truncatePrompt(item.prompt)}</span>
{isSteer && (
<span className="shrink-0 rounded-[3px] border border-editor-group-border px-1.5 py-[1px] text-[10px] leading-4 text-description">
<span className="flex h-5 shrink-0 items-center rounded-[3px] border border-editor-group-border px-1.5 text-[10px] leading-none text-description">
Steer
</span>
)}
{attachments && (
<span className="shrink-0 rounded-[3px] border border-editor-group-border px-1.5 py-[1px] text-[10px] leading-4 text-description">
<span className="flex h-5 shrink-0 items-center rounded-[3px] border border-editor-group-border px-1.5 text-[10px] leading-none text-description">
{attachments}
</span>
)}
<button
aria-label="Cancel queued message"
className="mt-[-2px] flex size-5 shrink-0 items-center justify-center rounded-[3px] text-description hover:bg-toolbar-hover-background hover:text-foreground disabled:pointer-events-none disabled:opacity-50"
className="-my-1.5 flex size-5 shrink-0 items-center justify-center rounded-[3px] text-description hover:bg-toolbar-hover-background hover:text-foreground disabled:pointer-events-none disabled:opacity-50"
disabled={isCancelling}
onClick={() => cancelQueuedPrompt(item.id)}
title="Cancel queued message"