Skip to content

Commit dfed27d

Browse files
zoomote[bot]roomote
andauthored
[Fix] Approval controls disappear when user scrolls through chat (#1448)
* fix(chat): keep approvals visible while scrolled * test(chat): cover scrolled approval interactions --------- Co-authored-by: Roomote <[email protected]>
1 parent cd09db1 commit dfed27d

2 files changed

Lines changed: 109 additions & 9 deletions

File tree

‎webview-ui/src/components/chat/ChatView.tsx‎

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1641,7 +1641,8 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
16411641
vscode.postMessage({ type: "condenseTaskContextRequest", text: taskId })
16421642
}
16431643

1644-
const areButtonsVisible = showScrollToBottom || primaryButtonText || secondaryButtonText
1644+
const hasApprovalButtons = Boolean(primaryButtonText || secondaryButtonText)
1645+
const areButtonsVisible = showScrollToBottom || hasApprovalButtons
16451646
const currentTaskAggregatedCosts = currentTaskId ? aggregatedCostsMap.get(currentTaskId) : undefined
16461647

16471648
return (
@@ -1735,7 +1736,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
17351736
className={`flex h-9 items-center mb-1 px-[15px] ${
17361737
showScrollToBottom ? "opacity-100" : enableButtons ? "opacity-100" : "opacity-50"
17371738
}`}>
1738-
{showScrollToBottom ? (
1739+
{showScrollToBottom && !hasApprovalButtons ? (
17391740
<>
17401741
<StandardTooltip content={t("chat:scrollToBottom")}>
17411742
<Button
@@ -1759,6 +1760,16 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
17591760
</>
17601761
) : (
17611762
<>
1763+
{showScrollToBottom && (
1764+
<StandardTooltip content={t("chat:scrollToBottom")}>
1765+
<Button
1766+
variant="secondary"
1767+
className="w-9 shrink-0 mr-[6px]"
1768+
onClick={handleScrollToBottomAndResetCheckpointCursor}>
1769+
<span className="codicon codicon-chevron-down"></span>
1770+
</Button>
1771+
</StandardTooltip>
1772+
)}
17621773
{primaryButtonText && (
17631774
<StandardTooltip
17641775
content={
@@ -1780,14 +1791,18 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
17801791
: primaryButtonText ===
17811792
t("chat:proceedWhileRunning.title")
17821793
? t("chat:proceedWhileRunning.tooltip")
1783-
: undefined
1794+
: primaryButtonText
17841795
}>
17851796
<Button
17861797
variant="primary"
17871798
disabled={!enableButtons}
1788-
className={secondaryButtonText ? "flex-1 mr-[6px]" : "flex-[2] mr-0"}
1799+
className={
1800+
secondaryButtonText
1801+
? "min-w-0 flex-1 mr-[6px]"
1802+
: "min-w-0 flex-[2] mr-0"
1803+
}
17891804
onClick={() => handlePrimaryButtonClick(inputValue, selectedImages)}>
1790-
{primaryButtonText}
1805+
<span className="min-w-0 truncate">{primaryButtonText}</span>
17911806
</Button>
17921807
</StandardTooltip>
17931808
)}
@@ -1802,14 +1817,14 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
18021817
? t("chat:terminate.tooltip")
18031818
: secondaryButtonText === t("chat:killCommand.title")
18041819
? t("chat:killCommand.tooltip")
1805-
: undefined
1820+
: secondaryButtonText
18061821
}>
18071822
<Button
18081823
variant="secondary"
18091824
disabled={!enableButtons}
1810-
className="flex-1 ml-[6px]"
1825+
className="min-w-0 flex-1 ml-[6px]"
18111826
onClick={() => handleSecondaryButtonClick(inputValue, selectedImages)}>
1812-
{secondaryButtonText}
1827+
<span className="min-w-0 truncate">{secondaryButtonText}</span>
18131828
</Button>
18141829
</StandardTooltip>
18151830
)}

‎webview-ui/src/components/chat/__tests__/ChatView.scroll-debug-repro.spec.tsx‎

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { act, fireEvent, renderWithExtensionState } from "@/utils/test-utils"
33

44
import type { ClineMessage } from "@roo-code/types"
55

6+
import { vscode } from "@src/utils/vscode"
7+
68
import ChatView, { type ChatViewProps } from "../ChatView"
79

810
type FollowOutput = ((isAtBottom: boolean) => "auto" | false) | "auto" | false
@@ -98,7 +100,9 @@ vi.mock("@/components/ui", async (importOriginal) => {
98100
const actual = await importOriginal<typeof import("@/components/ui")>()
99101
return {
100102
...actual,
101-
StandardTooltip: ({ children }: { children: React.ReactNode }) => <>{children}</>,
103+
StandardTooltip: ({ children, content }: { children: React.ReactNode; content?: string }) => (
104+
<div data-tooltip={content}>{children}</div>
105+
),
102106
}
103107
})
104108

@@ -357,9 +361,18 @@ const getScrollToCheckpointButton = (): HTMLButtonElement => {
357361
return button
358362
}
359363

364+
const getButtonByText = (text: string): HTMLButtonElement => {
365+
const button = Array.from(document.querySelectorAll("button")).find((candidate) => candidate.textContent === text)
366+
if (!(button instanceof HTMLButtonElement)) {
367+
throw new Error(`Expected button with text: ${text}`)
368+
}
369+
return button
370+
}
371+
360372
describe("ChatView scroll behavior regression coverage", () => {
361373
beforeEach(() => {
362374
vi.useFakeTimers()
375+
vi.mocked(vscode.postMessage).mockClear()
363376
harness.scrollCalls = 0
364377
harness.scrollToIndexArgs = []
365378
harness.atBottomAfterCalls = Number.POSITIVE_INFINITY
@@ -502,6 +515,78 @@ describe("ChatView scroll behavior regression coverage", () => {
502515
await expectChevronVisible()
503516
})
504517

518+
it("keeps pending approval controls visible while browsing history", async () => {
519+
const baseTs = Date.now() - 3_000
520+
const initialMessages = buildMessagesWithCheckpoint(baseTs)
521+
await hydrate(2, initialMessages)
522+
await waitForCalls(2)
523+
await waitForCallsSettled()
524+
525+
const scrollable = getScrollable()
526+
await act(async () => {
527+
fireEvent.wheel(scrollable, { deltaY: -120 })
528+
})
529+
await expectChevronVisible()
530+
531+
await act(async () => {
532+
postState([
533+
...initialMessages,
534+
{
535+
type: "ask",
536+
ask: "tool",
537+
ts: baseTs + 4,
538+
text: JSON.stringify({ tool: "finishTask" }),
539+
},
540+
])
541+
})
542+
await flushEffects()
543+
544+
expect(document.querySelector(".codicon-chevron-down")).toBeTruthy()
545+
expect(document.querySelector("button[aria-label='chat:scrollToLatestCheckpoint']")).toBeNull()
546+
const completeButton = getButtonByText("chat:completeSubtaskAndReturn")
547+
expect(completeButton.disabled).toBe(false)
548+
expect(completeButton.closest("[data-tooltip]")?.getAttribute("data-tooltip")).toBe(
549+
"chat:completeSubtaskAndReturn",
550+
)
551+
552+
fireEvent.click(completeButton)
553+
expect(vscode.postMessage).toHaveBeenCalledWith({ type: "askResponse", askResponse: "yesButtonClicked" })
554+
})
555+
556+
it("keeps two-button approvals actionable while browsing history", async () => {
557+
const baseTs = Date.now() - 3_000
558+
await hydrate(2, buildMessages(baseTs))
559+
await waitForCalls(2)
560+
await waitForCallsSettled()
561+
562+
await act(async () => {
563+
fireEvent.wheel(getScrollable(), { deltaY: -120 })
564+
postState([
565+
...buildMessages(baseTs),
566+
{
567+
type: "ask",
568+
ask: "tool",
569+
ts: baseTs + 3,
570+
text: JSON.stringify({ tool: "editedExistingFile", batchDiffs: [] }),
571+
},
572+
])
573+
})
574+
await flushEffects()
575+
576+
expect(document.querySelector(".codicon-chevron-down")).toBeTruthy()
577+
const approveButton = getButtonByText("chat:edit-batch.approve.title")
578+
const denyButton = getButtonByText("chat:edit-batch.deny.title")
579+
expect(approveButton.disabled).toBe(false)
580+
expect(denyButton.disabled).toBe(false)
581+
expect(approveButton.closest("[data-tooltip]")?.getAttribute("data-tooltip")).toBe(
582+
"chat:edit-batch.approve.title",
583+
)
584+
expect(denyButton.closest("[data-tooltip]")?.getAttribute("data-tooltip")).toBe("chat:edit-batch.deny.title")
585+
586+
fireEvent.click(denyButton)
587+
expect(vscode.postMessage).toHaveBeenCalledWith({ type: "askResponse", askResponse: "noButtonClicked" })
588+
})
589+
505590
it("hydration completion cannot override user escape hatch", async () => {
506591
await hydrate(Number.POSITIVE_INFINITY)
507592
await waitForCalls(1)

0 commit comments

Comments
 (0)