diff --git a/webview-ui/src/components/chat/__tests__/ChatView.cancel-button.spec.tsx b/webview-ui/src/components/chat/__tests__/ChatView.cancel-button.spec.tsx
deleted file mode 100644
index 5b6c3a34b0..0000000000
--- a/webview-ui/src/components/chat/__tests__/ChatView.cancel-button.spec.tsx
+++ /dev/null
@@ -1,514 +0,0 @@
-// npx vitest run src/components/chat/__tests__/ChatView.cancel-button.spec.tsx
-
-import React from "react"
-import { render, waitFor, fireEvent } from "@/utils/test-utils"
-import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
-
-import { ExtensionStateContextProvider } from "@src/context/ExtensionStateContext"
-import { vscode } from "@src/utils/vscode"
-
-import ChatView, { ChatViewProps } from "../ChatView"
-
-// Define minimal types needed for testing
-interface ClineMessage {
- type: "say" | "ask"
- say?: string
- ask?: string
- ts: number
- text?: string
- partial?: boolean
-}
-
-interface ExtensionState {
- version: string
- clineMessages: ClineMessage[]
- taskHistory: any[]
- shouldShowAnnouncement: boolean
- allowedCommands: string[]
- alwaysAllowExecute: boolean
- [key: string]: any
-}
-
-// Mock vscode API
-vi.mock("@src/utils/vscode", () => ({
- vscode: {
- postMessage: vi.fn(),
- },
-}))
-
-// Mock use-sound hook
-const mockPlayFunction = vi.fn()
-vi.mock("use-sound", () => ({
- default: vi.fn().mockImplementation(() => {
- return [mockPlayFunction]
- }),
-}))
-
-// Mock components
-vi.mock("../BrowserSessionRow", () => ({
- default: function MockBrowserSessionRow() {
- return
- },
-}))
-
-vi.mock("../ChatRow", () => ({
- default: function MockChatRow() {
- return
- },
-}))
-
-vi.mock("../AutoApproveMenu", () => ({
- default: () => null,
-}))
-
-vi.mock("../../common/VersionIndicator", () => ({
- default: () => null,
-}))
-
-vi.mock("../Announcement", () => ({
- default: () => null,
-}))
-
-vi.mock("@/components/common/DismissibleUpsell", () => ({
- default: () => null,
-}))
-
-vi.mock("../QueuedMessages", () => ({
- QueuedMessages: () => null,
-}))
-
-vi.mock("@src/components/welcome/RooTips", () => ({
- default: () => null,
-}))
-
-vi.mock("@src/components/welcome/RooHero", () => ({
- default: () => null,
-}))
-
-vi.mock("../common/TelemetryBanner", () => ({
- default: () => null,
-}))
-
-// Mock i18n
-vi.mock("react-i18next", () => ({
- useTranslation: () => ({
- t: (key: string) => key,
- }),
- initReactI18next: {
- type: "3rdParty",
- init: () => {},
- },
- Trans: ({ children }: { children?: React.ReactNode }) => <>{children}>,
-}))
-
-vi.mock("../ChatTextArea", () => {
- // eslint-disable-next-line @typescript-eslint/no-require-imports
- const mockReact = require("react")
-
- return {
- default: mockReact.forwardRef(function MockChatTextArea(_: any, ref: React.ForwardedRef) {
- mockReact.useImperativeHandle(ref, () => ({
- focus: vi.fn(),
- }))
-
- return
- }),
- ChatTextArea: () => ,
- }
-})
-
-// Mock VSCode components
-vi.mock("@vscode/webview-ui-toolkit/react", () => ({
- VSCodeButton: function MockVSCodeButton({
- children,
- onClick,
- disabled,
- }: {
- children: React.ReactNode
- onClick?: () => void
- disabled?: boolean
- }) {
- return (
-
- )
- },
- VSCodeTextField: () => null,
- VSCodeLink: () => null,
-}))
-
-// Helper to mock window.postMessage for state hydration
-const mockPostMessage = (state: Partial) => {
- window.postMessage(
- {
- type: "state",
- state: {
- version: "1.0.0",
- clineMessages: [],
- taskHistory: [],
- shouldShowAnnouncement: false,
- allowedCommands: [],
- alwaysAllowExecute: false,
- cloudIsAuthenticated: false,
- telemetrySetting: "enabled",
- ...state,
- },
- },
- "*",
- )
-}
-
-const defaultProps: ChatViewProps = {
- isHidden: false,
- showAnnouncement: false,
- hideAnnouncement: () => {},
-}
-
-const queryClient = new QueryClient()
-
-const renderChatView = (props: Partial = {}) => {
- return render(
-
-
-
-
- ,
- )
-}
-
-describe("ChatView - Cancel Button Tests", () => {
- beforeEach(() => {
- vi.clearAllMocks()
- vi.useFakeTimers()
- })
-
- afterEach(() => {
- vi.useRealTimers()
- })
-
- it("keeps cancel button enabled during API request even with tool approval dialog", async () => {
- const { container } = renderChatView()
-
- // First hydrate state with initial task
- mockPostMessage({
- clineMessages: [
- {
- type: "say",
- say: "task",
- ts: Date.now() - 3000,
- text: "Initial task",
- },
- ],
- })
-
- // Add API request in progress (no cost)
- mockPostMessage({
- clineMessages: [
- {
- type: "say",
- say: "task",
- ts: Date.now() - 3000,
- text: "Initial task",
- },
- {
- type: "say",
- say: "api_req_started",
- ts: Date.now() - 2000,
- text: JSON.stringify({ apiProtocol: "anthropic" }), // No cost = streaming
- },
- ],
- })
-
- // Clear any initial calls
- vi.mocked(vscode.postMessage).mockClear()
-
- // Add tool approval dialog while API is still running
- mockPostMessage({
- clineMessages: [
- {
- type: "say",
- say: "task",
- ts: Date.now() - 3000,
- text: "Initial task",
- },
- {
- type: "say",
- say: "api_req_started",
- ts: Date.now() - 2000,
- text: JSON.stringify({ apiProtocol: "anthropic" }), // Still no cost
- },
- {
- type: "ask",
- ask: "tool",
- ts: Date.now(),
- text: JSON.stringify({ tool: "readFile", path: "test.txt" }),
- partial: false,
- },
- ],
- })
-
- // Wait for component to update
- await waitFor(() => {
- const buttons = container.querySelectorAll('[data-testid="vscode-button"]')
- // Find the cancel button (should be displayed when isApiRequestInProgress is true)
- const cancelButton = Array.from(buttons).find((btn) => btn.textContent?.includes("chat:cancel.title"))
-
- expect(cancelButton).toBeTruthy()
- expect(cancelButton?.getAttribute("disabled")).toBe("false")
- })
- })
-
- it("disables cancel button after it is clicked (pending state)", async () => {
- const { container } = renderChatView()
-
- // Set up state with API request in progress
- mockPostMessage({
- clineMessages: [
- {
- type: "say",
- say: "task",
- ts: Date.now() - 2000,
- text: "Initial task",
- },
- {
- type: "say",
- say: "api_req_started",
- ts: Date.now(),
- text: JSON.stringify({ apiProtocol: "anthropic" }), // No cost = streaming
- },
- ],
- })
-
- // Clear any initial calls
- vi.mocked(vscode.postMessage).mockClear()
-
- // Wait for cancel button to be rendered
- await waitFor(() => {
- const buttons = container.querySelectorAll('[data-testid="vscode-button"]')
- const cancelButton = Array.from(buttons).find((btn) => btn.textContent?.includes("chat:cancel.title"))
- expect(cancelButton).toBeTruthy()
- })
-
- // Find and click the cancel button
- const buttons = container.querySelectorAll('[data-testid="vscode-button"]')
- const cancelButton = Array.from(buttons).find((btn) =>
- btn.textContent?.includes("chat:cancel.title"),
- ) as HTMLButtonElement
-
- fireEvent.click(cancelButton)
-
- // Check that cancel message was sent
- await waitFor(() => {
- expect(vscode.postMessage).toHaveBeenCalledWith({
- type: "cancelTask",
- })
- })
-
- // Cancel button should be temporarily disabled to prevent rapid clicks
- // Note: The implementation sets isCancelPending which affects the disabled state
- expect(cancelButton.disabled).toBe(true)
-
- // After timeout, button should be enabled again (if still showing)
- vi.advanceTimersByTime(1000)
-
- // Note: In the real implementation, the button might disappear after cancel,
- // but the isCancelPending flag would be reset after 1 second
- })
-
- it("shows cancel button when API request is in progress without tool dialog", async () => {
- const { container } = renderChatView()
-
- // Set up state with only API request (no tool dialog)
- mockPostMessage({
- clineMessages: [
- {
- type: "say",
- say: "task",
- ts: Date.now() - 2000,
- text: "Initial task",
- },
- {
- type: "say",
- say: "api_req_started",
- ts: Date.now(),
- text: JSON.stringify({ apiProtocol: "anthropic" }), // No cost = streaming
- },
- ],
- })
-
- // Wait for cancel button to appear
- await waitFor(() => {
- const buttons = container.querySelectorAll('[data-testid="vscode-button"]')
- const cancelButton = Array.from(buttons).find((btn) => btn.textContent?.includes("chat:cancel.title"))
-
- expect(cancelButton).toBeTruthy()
- expect(cancelButton?.getAttribute("disabled")).toBe("false")
- })
- })
-
- it("hides cancel button when API request completes (cost present)", async () => {
- const { container } = renderChatView()
-
- // Start with API request in progress
- mockPostMessage({
- clineMessages: [
- {
- type: "say",
- say: "task",
- ts: Date.now() - 2000,
- text: "Initial task",
- },
- {
- type: "say",
- say: "api_req_started",
- ts: Date.now() - 1000,
- text: JSON.stringify({ apiProtocol: "anthropic" }), // No cost = streaming
- },
- ],
- })
-
- // Wait for cancel button to appear
- await waitFor(() => {
- const buttons = container.querySelectorAll('[data-testid="vscode-button"]')
- const cancelButton = Array.from(buttons).find((btn) => btn.textContent?.includes("chat:cancel.title"))
- expect(cancelButton).toBeTruthy()
- })
-
- // Complete the API request (add cost)
- mockPostMessage({
- clineMessages: [
- {
- type: "say",
- say: "task",
- ts: Date.now() - 2000,
- text: "Initial task",
- },
- {
- type: "say",
- say: "api_req_started",
- ts: Date.now() - 1000,
- text: JSON.stringify({
- apiProtocol: "anthropic",
- cost: 0.05, // Cost present = completed
- tokensIn: 100,
- tokensOut: 50,
- }),
- },
- ],
- })
-
- // Cancel button should disappear when API request completes
- await waitFor(() => {
- const buttons = container.querySelectorAll('[data-testid="vscode-button"]')
- const cancelButton = Array.from(buttons).find((btn) => btn.textContent?.includes("chat:cancel.title"))
-
- // Cancel button should not be present when API is complete
- expect(cancelButton).toBeFalsy()
- })
- })
-
- it("tracks API request state separately from UI streaming state", async () => {
- const { container } = renderChatView()
-
- // Set up complex state with tool dialog AND API request
- mockPostMessage({
- clineMessages: [
- {
- type: "say",
- say: "task",
- ts: Date.now() - 3000,
- text: "Initial task",
- },
- {
- type: "say",
- say: "api_req_started",
- ts: Date.now() - 2000,
- text: JSON.stringify({ apiProtocol: "anthropic" }), // No cost = streaming
- },
- {
- type: "ask",
- ask: "tool",
- ts: Date.now(),
- text: JSON.stringify({ tool: "editedExistingFile", path: "test.txt" }),
- partial: false, // Tool dialog is complete
- },
- ],
- })
-
- // Wait for buttons to be rendered
- await waitFor(() => {
- const buttons = container.querySelectorAll('[data-testid="vscode-button"]')
- expect(buttons.length).toBeGreaterThan(0)
- })
-
- // Should have both approve/reject buttons AND cancel button
- const buttons = container.querySelectorAll('[data-testid="vscode-button"]')
-
- // Find buttons by their text content
- const approveButton = Array.from(buttons).find(
- (btn) => btn.textContent?.includes("chat:save.title") || btn.textContent?.includes("chat:approve.title"),
- )
- const rejectButton = Array.from(buttons).find((btn) => btn.textContent?.includes("chat:reject.title"))
- const cancelButton = Array.from(buttons).find((btn) => btn.textContent?.includes("chat:cancel.title"))
-
- // Tool dialog buttons should be present
- expect(approveButton).toBeTruthy()
- expect(rejectButton).toBeTruthy()
-
- // Cancel button should also be present due to API request in progress
- expect(cancelButton).toBeTruthy()
- expect(cancelButton?.getAttribute("disabled")).toBe("false")
- })
-
- it("prevents multiple rapid cancel button clicks", async () => {
- const { container } = renderChatView()
-
- // Set up state with API request in progress
- mockPostMessage({
- clineMessages: [
- {
- type: "say",
- say: "task",
- ts: Date.now() - 2000,
- text: "Initial task",
- },
- {
- type: "say",
- say: "api_req_started",
- ts: Date.now(),
- text: JSON.stringify({ apiProtocol: "anthropic" }), // No cost = streaming
- },
- ],
- })
-
- // Clear any initial calls
- vi.mocked(vscode.postMessage).mockClear()
-
- // Wait for cancel button to be rendered
- await waitFor(() => {
- const buttons = container.querySelectorAll('[data-testid="vscode-button"]')
- const cancelButton = Array.from(buttons).find((btn) => btn.textContent?.includes("chat:cancel.title"))
- expect(cancelButton).toBeTruthy()
- })
-
- // Find the cancel button
- const buttons = container.querySelectorAll('[data-testid="vscode-button"]')
- const cancelButton = Array.from(buttons).find((btn) =>
- btn.textContent?.includes("chat:cancel.title"),
- ) as HTMLButtonElement
-
- // Click cancel button multiple times rapidly
- fireEvent.click(cancelButton)
- fireEvent.click(cancelButton)
- fireEvent.click(cancelButton)
-
- // Should only send one cancel message despite multiple clicks
- await waitFor(() => {
- expect(vscode.postMessage).toHaveBeenCalledTimes(1)
- expect(vscode.postMessage).toHaveBeenCalledWith({
- type: "cancelTask",
- })
- })
- })
-})