From 346d34fd0055ee3f9c6dc816e00199ba8e2378fd Mon Sep 17 00:00:00 2001 From: Roomote Date: Sun, 30 Aug 2026 02:42:34 +0000 Subject: [PATCH 1/2] fix(chat): keep approvals visible while scrolled --- webview-ui/src/components/chat/ChatView.tsx | 127 +++++++++--------- .../ChatView.scroll-debug-repro.spec.tsx | 35 +++++ 2 files changed, 101 insertions(+), 61 deletions(-) diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index b6c3b0bdf0..ddf174aa74 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -1641,7 +1641,8 @@ const ChatViewComponent: React.ForwardRefRenderFunction - {showScrollToBottom ? ( + {showScrollToBottom && ( <> - {hasLatestCheckpoint && ( + {hasLatestCheckpoint && !hasApprovalButtons && ( - - )} - {secondaryButtonText && ( - - - - )} - + )} + {primaryButtonText && ( + + + + )} + {secondaryButtonText && ( + + + )} )} diff --git a/webview-ui/src/components/chat/__tests__/ChatView.scroll-debug-repro.spec.tsx b/webview-ui/src/components/chat/__tests__/ChatView.scroll-debug-repro.spec.tsx index bdbd202830..e5c0920026 100644 --- a/webview-ui/src/components/chat/__tests__/ChatView.scroll-debug-repro.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/ChatView.scroll-debug-repro.spec.tsx @@ -502,6 +502,41 @@ describe("ChatView scroll behavior regression coverage", () => { await expectChevronVisible() }) + it("keeps pending approval controls visible while browsing history", async () => { + const baseTs = Date.now() - 3_000 + const initialMessages = buildMessagesWithCheckpoint(baseTs) + await hydrate(2, initialMessages) + await waitForCalls(2) + await waitForCallsSettled() + + const scrollable = getScrollable() + await act(async () => { + fireEvent.wheel(scrollable, { deltaY: -120 }) + }) + await expectChevronVisible() + + await act(async () => { + postState([ + ...initialMessages, + { + type: "ask", + ask: "tool", + ts: baseTs + 4, + text: JSON.stringify({ tool: "finishTask" }), + }, + ]) + }) + await flushEffects() + + expect(document.querySelector(".codicon-chevron-down")).toBeTruthy() + expect(document.querySelector("button[aria-label='chat:scrollToLatestCheckpoint']")).toBeNull() + expect( + Array.from(document.querySelectorAll("button")).some( + (button) => button.textContent === "chat:completeSubtaskAndReturn", + ), + ).toBe(true) + }) + it("hydration completion cannot override user escape hatch", async () => { await hydrate(Number.POSITIVE_INFINITY) await waitForCalls(1) From 4db0139df98054c532937171b74ab56c9f9adc40 Mon Sep 17 00:00:00 2001 From: Roomote Date: Mon, 31 Aug 2026 03:02:26 +0000 Subject: [PATCH 2/2] test(chat): cover scrolled approval interactions --- webview-ui/src/components/chat/ChatView.tsx | 138 ++++++++++-------- .../ChatView.scroll-debug-repro.spec.tsx | 62 +++++++- 2 files changed, 130 insertions(+), 70 deletions(-) diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index ddf174aa74..988c98ba30 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -1736,23 +1736,17 @@ const ChatViewComponent: React.ForwardRefRenderFunction - {showScrollToBottom && ( + {showScrollToBottom && !hasApprovalButtons ? ( <> - {hasLatestCheckpoint && !hasApprovalButtons && ( + {hasLatestCheckpoint && ( - - )} - {secondaryButtonText && ( - - - + ) : ( + <> + {showScrollToBottom && ( + + + + )} + {primaryButtonText && ( + + + + )} + {secondaryButtonText && ( + + + + )} + )} )} diff --git a/webview-ui/src/components/chat/__tests__/ChatView.scroll-debug-repro.spec.tsx b/webview-ui/src/components/chat/__tests__/ChatView.scroll-debug-repro.spec.tsx index e5c0920026..56b008b862 100644 --- a/webview-ui/src/components/chat/__tests__/ChatView.scroll-debug-repro.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/ChatView.scroll-debug-repro.spec.tsx @@ -3,6 +3,8 @@ import { act, fireEvent, renderWithExtensionState } from "@/utils/test-utils" import type { ClineMessage } from "@roo-code/types" +import { vscode } from "@src/utils/vscode" + import ChatView, { type ChatViewProps } from "../ChatView" type FollowOutput = ((isAtBottom: boolean) => "auto" | false) | "auto" | false @@ -98,7 +100,9 @@ vi.mock("@/components/ui", async (importOriginal) => { const actual = await importOriginal() return { ...actual, - StandardTooltip: ({ children }: { children: React.ReactNode }) => <>{children}, + StandardTooltip: ({ children, content }: { children: React.ReactNode; content?: string }) => ( +
{children}
+ ), } }) @@ -357,9 +361,18 @@ const getScrollToCheckpointButton = (): HTMLButtonElement => { return button } +const getButtonByText = (text: string): HTMLButtonElement => { + const button = Array.from(document.querySelectorAll("button")).find((candidate) => candidate.textContent === text) + if (!(button instanceof HTMLButtonElement)) { + throw new Error(`Expected button with text: ${text}`) + } + return button +} + describe("ChatView scroll behavior regression coverage", () => { beforeEach(() => { vi.useFakeTimers() + vi.mocked(vscode.postMessage).mockClear() harness.scrollCalls = 0 harness.scrollToIndexArgs = [] harness.atBottomAfterCalls = Number.POSITIVE_INFINITY @@ -530,11 +543,48 @@ describe("ChatView scroll behavior regression coverage", () => { expect(document.querySelector(".codicon-chevron-down")).toBeTruthy() expect(document.querySelector("button[aria-label='chat:scrollToLatestCheckpoint']")).toBeNull() - expect( - Array.from(document.querySelectorAll("button")).some( - (button) => button.textContent === "chat:completeSubtaskAndReturn", - ), - ).toBe(true) + const completeButton = getButtonByText("chat:completeSubtaskAndReturn") + expect(completeButton.disabled).toBe(false) + expect(completeButton.closest("[data-tooltip]")?.getAttribute("data-tooltip")).toBe( + "chat:completeSubtaskAndReturn", + ) + + fireEvent.click(completeButton) + expect(vscode.postMessage).toHaveBeenCalledWith({ type: "askResponse", askResponse: "yesButtonClicked" }) + }) + + it("keeps two-button approvals actionable while browsing history", async () => { + const baseTs = Date.now() - 3_000 + await hydrate(2, buildMessages(baseTs)) + await waitForCalls(2) + await waitForCallsSettled() + + await act(async () => { + fireEvent.wheel(getScrollable(), { deltaY: -120 }) + postState([ + ...buildMessages(baseTs), + { + type: "ask", + ask: "tool", + ts: baseTs + 3, + text: JSON.stringify({ tool: "editedExistingFile", batchDiffs: [] }), + }, + ]) + }) + await flushEffects() + + expect(document.querySelector(".codicon-chevron-down")).toBeTruthy() + const approveButton = getButtonByText("chat:edit-batch.approve.title") + const denyButton = getButtonByText("chat:edit-batch.deny.title") + expect(approveButton.disabled).toBe(false) + expect(denyButton.disabled).toBe(false) + expect(approveButton.closest("[data-tooltip]")?.getAttribute("data-tooltip")).toBe( + "chat:edit-batch.approve.title", + ) + expect(denyButton.closest("[data-tooltip]")?.getAttribute("data-tooltip")).toBe("chat:edit-batch.deny.title") + + fireEvent.click(denyButton) + expect(vscode.postMessage).toHaveBeenCalledWith({ type: "askResponse", askResponse: "noButtonClicked" }) }) it("hydration completion cannot override user escape hatch", async () => {