Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 23 additions & 8 deletions webview-ui/src/components/chat/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1641,7 +1641,8 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
vscode.postMessage({ type: "condenseTaskContextRequest", text: taskId })
}

const areButtonsVisible = showScrollToBottom || primaryButtonText || secondaryButtonText
const hasApprovalButtons = Boolean(primaryButtonText || secondaryButtonText)
const areButtonsVisible = showScrollToBottom || hasApprovalButtons
const currentTaskAggregatedCosts = currentTaskId ? aggregatedCostsMap.get(currentTaskId) : undefined

return (
Expand Down Expand Up @@ -1735,7 +1736,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
className={`flex h-9 items-center mb-1 px-[15px] ${
showScrollToBottom ? "opacity-100" : enableButtons ? "opacity-100" : "opacity-50"
}`}>
{showScrollToBottom ? (
{showScrollToBottom && !hasApprovalButtons ? (
<>
<StandardTooltip content={t("chat:scrollToBottom")}>
<Button
Expand All @@ -1759,6 +1760,16 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
</>
) : (
<>
{showScrollToBottom && (
<StandardTooltip content={t("chat:scrollToBottom")}>
<Button
variant="secondary"
className="w-9 shrink-0 mr-[6px]"
onClick={handleScrollToBottomAndResetCheckpointCursor}>
<span className="codicon codicon-chevron-down"></span>
</Button>
</StandardTooltip>
)}
{primaryButtonText && (
<StandardTooltip
content={
Expand All @@ -1780,14 +1791,18 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
: primaryButtonText ===
t("chat:proceedWhileRunning.title")
? t("chat:proceedWhileRunning.tooltip")
: undefined
: primaryButtonText
}>
<Button
variant="primary"
disabled={!enableButtons}
className={secondaryButtonText ? "flex-1 mr-[6px]" : "flex-[2] mr-0"}
className={
secondaryButtonText
? "min-w-0 flex-1 mr-[6px]"
: "min-w-0 flex-[2] mr-0"
}
onClick={() => handlePrimaryButtonClick(inputValue, selectedImages)}>
{primaryButtonText}
<span className="min-w-0 truncate">{primaryButtonText}</span>
</Button>
</StandardTooltip>
)}
Expand All @@ -1802,14 +1817,14 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
? t("chat:terminate.tooltip")
: secondaryButtonText === t("chat:killCommand.title")
? t("chat:killCommand.tooltip")
: undefined
: secondaryButtonText
}>
<Button
variant="secondary"
disabled={!enableButtons}
className="flex-1 ml-[6px]"
className="min-w-0 flex-1 ml-[6px]"
onClick={() => handleSecondaryButtonClick(inputValue, selectedImages)}>
{secondaryButtonText}
<span className="min-w-0 truncate">{secondaryButtonText}</span>
</Button>
</StandardTooltip>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -98,7 +100,9 @@ vi.mock("@/components/ui", async (importOriginal) => {
const actual = await importOriginal<typeof import("@/components/ui")>()
return {
...actual,
StandardTooltip: ({ children }: { children: React.ReactNode }) => <>{children}</>,
StandardTooltip: ({ children, content }: { children: React.ReactNode; content?: string }) => (
<div data-tooltip={content}>{children}</div>
),
}
})

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -502,6 +515,78 @@ 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()
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 () => {
await hydrate(Number.POSITIVE_INFINITY)
await waitForCalls(1)
Expand Down
Loading