From dfc53e42f09bf597deff46d0f2d4fe9f17133733 Mon Sep 17 00:00:00 2001 From: Roomote Date: Thu, 20 Aug 2026 21:35:39 +0000 Subject: [PATCH] fix(webview): finish light theme token cleanup --- webview-ui/playwright/vscode-theme-light.css | 2 + .../src/components/chat/ChatTextArea.tsx | 27 ++--- .../components/chat/CodebaseSearchResult.tsx | 8 +- webview-ui/src/components/chat/IconButton.tsx | 8 +- .../components/chat/IndexingStatusBadge.tsx | 3 +- .../src/components/chat/LucideIconButton.tsx | 6 +- .../chat/UpdateTodoListToolBlock.tsx | 109 +++++------------- .../src/components/chat/WorktreeSelector.tsx | 3 +- .../__tests__/ChatControlButtons.spec.tsx | 42 +++++++ .../__tests__/CodebaseSearchResult.spec.tsx | 38 ++++++ .../__tests__/ThemeTokenCleanup.visual.tsx | 61 ++++++++++ .../UpdateTodoListToolBlock.spec.tsx | 42 +++++++ .../src/components/chat/chatControlStyles.ts | 5 + .../components/ui/__tests__/checkbox.spec.tsx | 12 ++ webview-ui/src/components/ui/checkbox.tsx | 7 +- 15 files changed, 260 insertions(+), 113 deletions(-) create mode 100644 webview-ui/src/components/chat/__tests__/ChatControlButtons.spec.tsx create mode 100644 webview-ui/src/components/chat/__tests__/CodebaseSearchResult.spec.tsx create mode 100644 webview-ui/src/components/chat/__tests__/ThemeTokenCleanup.visual.tsx create mode 100644 webview-ui/src/components/chat/__tests__/UpdateTodoListToolBlock.spec.tsx create mode 100644 webview-ui/src/components/chat/chatControlStyles.ts create mode 100644 webview-ui/src/components/ui/__tests__/checkbox.spec.tsx diff --git a/webview-ui/playwright/vscode-theme-light.css b/webview-ui/playwright/vscode-theme-light.css index c877cea470..5816e59760 100644 --- a/webview-ui/playwright/vscode-theme-light.css +++ b/webview-ui/playwright/vscode-theme-light.css @@ -7,6 +7,7 @@ --vscode-focusBorder: #0090f1; --vscode-editor-foreground: #333333; --vscode-editor-background: #ffffff; + --vscode-editorGroup-border: #cecece; --vscode-button-foreground: #ffffff; --vscode-button-background: #007acc; --vscode-button-hoverBackground: #0062a3; @@ -24,6 +25,7 @@ --vscode-toolbar-hoverBackground: #e8e8e8; --vscode-widget-border: #d4d4d4; --vscode-widget-shadow: #00000029; + --vscode-textLink-foreground: #006ab1; --vscode-menu-foreground: #3b3b3b; --vscode-menu-background: #ffffff; --vscode-editorHoverWidget-foreground: #3b3b3b; diff --git a/webview-ui/src/components/chat/ChatTextArea.tsx b/webview-ui/src/components/chat/ChatTextArea.tsx index 4cf57d1e3e..c761ab809b 100644 --- a/webview-ui/src/components/chat/ChatTextArea.tsx +++ b/webview-ui/src/components/chat/ChatTextArea.tsx @@ -33,6 +33,7 @@ import ContextMenu from "./ContextMenu" import { IndexingStatusBadge } from "./IndexingStatusBadge" import { ZooCodeAuthBadge } from "./ZooCodeAuthBadge" import { usePromptHistory } from "./hooks/usePromptHistory" +import { disabledChatControlClassName, enabledChatControlClassName } from "./chatControlStyles" interface ChatTextAreaProps { inputValue: string @@ -1138,12 +1139,9 @@ export const ChatTextArea = forwardRef( !shouldDisableImages ? "opacity-50 hover:opacity-100 delay-750 pointer-events-auto" : "opacity-0 pointer-events-none duration-200 delay-0", - !shouldDisableImages && - "hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)]", + !shouldDisableImages && enabledChatControlClassName, "focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder", - !shouldDisableImages && "active:bg-[rgba(255,255,255,0.1)]", - shouldDisableImages && - "opacity-40 cursor-not-allowed grayscale-[30%] hover:bg-transparent hover:border-[rgba(255,255,255,0.08)] active:bg-transparent", + shouldDisableImages && disabledChatControlClassName, )}> @@ -1160,9 +1158,8 @@ export const ChatTextArea = forwardRef( "rounded-md min-w-[28px] min-h-[28px]", "opacity-60 hover:opacity-100 text-vscode-descriptionForeground hover:text-vscode-foreground", "transition-all duration-150", - "hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)]", + enabledChatControlClassName, "focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder", - "active:bg-[rgba(255,255,255,0.1)]", "cursor-pointer", )}> @@ -1184,10 +1181,8 @@ export const ChatTextArea = forwardRef( hasInputContent ? "opacity-50 hover:opacity-100 delay-750 pointer-events-auto" : "opacity-0 pointer-events-none duration-200 delay-0", - hasInputContent && - "hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)]", + hasInputContent && enabledChatControlClassName, "focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder", - hasInputContent && "active:bg-[rgba(255,255,255,0.1)]", )}> ( "text-vscode-descriptionForeground hover:text-vscode-foreground", "transition-all duration-200", "opacity-100 hover:opacity-100 pointer-events-auto", - "hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)]", + enabledChatControlClassName, "focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder", - "active:bg-[rgba(255,255,255,0.1)]", "cursor-pointer", )}> @@ -1247,13 +1241,11 @@ export const ChatTextArea = forwardRef( ? "opacity-100 hover:opacity-100 pointer-events-auto" : "opacity-0 pointer-events-none", (isEditMode || isStreaming || hasInputContent) && - "hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)]", + enabledChatControlClassName, "focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder", - (isEditMode || isStreaming || hasInputContent) && - "active:bg-[rgba(255,255,255,0.1)]", (isEditMode || isStreaming || hasInputContent) && "cursor-pointer", isStreaming && - "bg-vscode-button-background hover:bg-vscode-button-background", + "bg-vscode-button-background hover:bg-vscode-button-background active:bg-vscode-button-background", )}> {isStreaming ? ( @@ -1333,9 +1325,8 @@ export const ChatTextArea = forwardRef( "rounded-md min-w-[28px] min-h-[28px]", "text-vscode-foreground opacity-85", "transition-all duration-150", - "hover:opacity-100 hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)]", + enabledChatControlClassName, "focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder", - "active:bg-[rgba(255,255,255,0.1)]", "cursor-pointer", )}> diff --git a/webview-ui/src/components/chat/CodebaseSearchResult.tsx b/webview-ui/src/components/chat/CodebaseSearchResult.tsx index 8280ea3d47..74249c6f73 100644 --- a/webview-ui/src/components/chat/CodebaseSearchResult.tsx +++ b/webview-ui/src/components/chat/CodebaseSearchResult.tsx @@ -30,15 +30,15 @@ const CodebaseSearchResult: React.FC = ({ filePath, s
+ className="group p-2 border border-vscode-editorGroup-border cursor-pointer hover:bg-vscode-list-hoverBackground">
- + {filePath.split("/").at(-1)}:{startLine === endLine ? startLine : `${startLine}-${endLine}`} - + {filePath.split("/").slice(0, -1).join("/")} - + {score.toFixed(3)}
diff --git a/webview-ui/src/components/chat/IconButton.tsx b/webview-ui/src/components/chat/IconButton.tsx index 00210ac5b3..d2acd53b00 100644 --- a/webview-ui/src/components/chat/IconButton.tsx +++ b/webview-ui/src/components/chat/IconButton.tsx @@ -1,5 +1,6 @@ import { cn } from "@src/lib/utils" import { Button, StandardTooltip } from "@src/components/ui" +import { disabledChatControlClassName, enabledChatControlClassName } from "./chatControlStyles" interface IconButtonProps extends React.ButtonHTMLAttributes { iconClass: string @@ -30,12 +31,9 @@ export const IconButton: React.FC = ({ "rounded-md min-w-[28px] min-h-[28px]", "text-vscode-foreground opacity-85", "transition-all duration-150", - "hover:opacity-100 hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)]", "focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder", - "active:bg-[rgba(255,255,255,0.1)]", - !disabled && "cursor-pointer", - disabled && - "opacity-40 cursor-not-allowed grayscale-[30%] hover:bg-transparent hover:border-[rgba(255,255,255,0.08)] active:bg-transparent", + !disabled && cn("cursor-pointer", enabledChatControlClassName), + disabled && disabledChatControlClassName, className, )} disabled={disabled} diff --git a/webview-ui/src/components/chat/IndexingStatusBadge.tsx b/webview-ui/src/components/chat/IndexingStatusBadge.tsx index 227df3e645..22dbe8f9d5 100644 --- a/webview-ui/src/components/chat/IndexingStatusBadge.tsx +++ b/webview-ui/src/components/chat/IndexingStatusBadge.tsx @@ -11,6 +11,7 @@ import { useExtensionState } from "@src/context/ExtensionStateContext" import { PopoverTrigger, StandardTooltip, Button } from "@src/components/ui" import { CodeIndexPopover } from "./CodeIndexPopover" +import { enabledChatControlClassName } from "./chatControlStyles" interface IndexingStatusBadgeProps { className?: string @@ -96,7 +97,7 @@ export const IndexingStatusBadge: React.FC = ({ classN className={cn( "relative h-5 w-5 p-0", "text-vscode-foreground opacity-85", - "hover:opacity-100 hover:bg-[rgba(255,255,255,0.03)]", + enabledChatControlClassName, "focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder", className, )}> diff --git a/webview-ui/src/components/chat/LucideIconButton.tsx b/webview-ui/src/components/chat/LucideIconButton.tsx index a527df3f1d..4f53b1248b 100644 --- a/webview-ui/src/components/chat/LucideIconButton.tsx +++ b/webview-ui/src/components/chat/LucideIconButton.tsx @@ -2,6 +2,7 @@ import { forwardRef } from "react" import { cn } from "@src/lib/utils" import { Button, StandardTooltip } from "@src/components/ui" import { Loader2, LucideIcon } from "lucide-react" +import { disabledChatControlClassName, enabledChatControlClassName } from "./chatControlStyles" interface LucideIconButtonProps extends React.ButtonHTMLAttributes { icon: LucideIcon @@ -27,9 +28,8 @@ export const LucideIconButton = forwardRef Math.random().toString(36).slice(2, 10) @@ -336,7 +333,7 @@ const UpdateTodoListToolBlock: React.FC = ({ style={{ border: "none", background: "transparent", - color: "#f14c4c", + color: "var(--vscode-errorForeground)", cursor: "pointer", fontSize: 14, marginLeft: 2, @@ -371,7 +368,7 @@ const UpdateTodoListToolBlock: React.FC = ({ fontSize: 13, marginRight: 6, padding: "1px 3px", - borderBottom: "1px solid #eee", + borderBottom: "1px solid var(--vscode-input-border)", }} />
- {/* Delete confirmation dialog */} - {deleteId && ( -
-
e.stopPropagation()}> -
- Are you sure you want to delete this todo item? -
-
- - -
-
-
- )} + + + Delete todo item + Are you sure you want to delete this todo item? + + + Cancel + + + Delete + + + + ) } diff --git a/webview-ui/src/components/chat/WorktreeSelector.tsx b/webview-ui/src/components/chat/WorktreeSelector.tsx index 938fa2cec7..0908516d32 100644 --- a/webview-ui/src/components/chat/WorktreeSelector.tsx +++ b/webview-ui/src/components/chat/WorktreeSelector.tsx @@ -11,6 +11,7 @@ import { vscode } from "@/utils/vscode" import { CreateWorktreeModal } from "../worktrees/CreateWorktreeModal" import { IconButton } from "./IconButton" +import { enabledChatControlClassName } from "./chatControlStyles" interface WorktreeSelectorProps { disabled?: boolean @@ -95,7 +96,7 @@ export const WorktreeSelector = ({ disabled = false }: WorktreeSelectorProps) => "transition-all duration-150 focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder focus-visible:ring-inset", disabled ? "opacity-50 cursor-not-allowed" - : "opacity-90 hover:opacity-100 hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)] cursor-pointer", + : cn("opacity-90 cursor-pointer", enabledChatControlClassName), )}> {t("worktrees:selector.worktree")}: diff --git a/webview-ui/src/components/chat/__tests__/ChatControlButtons.spec.tsx b/webview-ui/src/components/chat/__tests__/ChatControlButtons.spec.tsx new file mode 100644 index 0000000000..e22dca8984 --- /dev/null +++ b/webview-ui/src/components/chat/__tests__/ChatControlButtons.spec.tsx @@ -0,0 +1,42 @@ +import { fireEvent, render, screen } from "@/utils/test-utils" +import { CircleHelp } from "lucide-react" +import { describe, expect, test, vi } from "vitest" + +import { IconButton } from "../IconButton" +import { LucideIconButton } from "../LucideIconButton" + +describe("chat control buttons", () => { + test("invokes enabled codicon controls", () => { + const onClick = vi.fn() + render() + + fireEvent.click(screen.getByRole("button", { name: "Settings" })) + expect(onClick).toHaveBeenCalledOnce() + }) + + test("keeps disabled codicon controls inert", () => { + const onClick = vi.fn() + render( + , + ) + + const button = screen.getByRole("button", { name: "Settings" }) + expect(button).toBeDisabled() + fireEvent.click(button) + expect(onClick).not.toHaveBeenCalled() + }) + + test("renders enabled and disabled Lucide controls", () => { + const { rerender } = render() + expect(screen.getByRole("button", { name: "Help" })).toBeEnabled() + + rerender() + expect(screen.getByRole("button", { name: "Help" })).toBeDisabled() + }) +}) diff --git a/webview-ui/src/components/chat/__tests__/CodebaseSearchResult.spec.tsx b/webview-ui/src/components/chat/__tests__/CodebaseSearchResult.spec.tsx new file mode 100644 index 0000000000..3cebafa598 --- /dev/null +++ b/webview-ui/src/components/chat/__tests__/CodebaseSearchResult.spec.tsx @@ -0,0 +1,38 @@ +import { fireEvent, render, screen } from "@/utils/test-utils" +import { beforeEach, describe, expect, test, vi } from "vitest" + +import CodebaseSearchResult from "../CodebaseSearchResult" +import { vscode } from "@/utils/vscode" + +vi.mock("@/utils/vscode", () => ({ vscode: { postMessage: vi.fn() } })) + +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ t: (key: string) => key }), +})) + +describe("CodebaseSearchResult", () => { + beforeEach(() => vi.clearAllMocks()) + + test("opens the selected file at the result start line", () => { + render( + , + ) + + const fileName = screen.getByText("example.ts:12-18") + expect(fileName).toHaveClass("group-hover:text-vscode-list-hoverForeground") + expect(screen.getByText("src")).toHaveClass("group-hover:text-vscode-list-hoverForeground") + fireEvent.click(fileName) + expect(vscode.postMessage).toHaveBeenCalledWith({ + type: "openFile", + text: "./src/example.ts", + values: { line: 12 }, + }) + }) +}) diff --git a/webview-ui/src/components/chat/__tests__/ThemeTokenCleanup.visual.tsx b/webview-ui/src/components/chat/__tests__/ThemeTokenCleanup.visual.tsx new file mode 100644 index 0000000000..67f564c300 --- /dev/null +++ b/webview-ui/src/components/chat/__tests__/ThemeTokenCleanup.visual.tsx @@ -0,0 +1,61 @@ +import React from "react" + +import { expect, test } from "../../../../playwright/coverage-fixture" +import { Checkbox } from "@/components/ui/checkbox" +import { enabledChatControlClassName } from "../chatControlStyles" + +const themes = [ + { + name: "dark", + bodyClass: "vscode-dark", + themeId: "Default Dark Modern", + expected: { + hover: "rgb(42, 45, 46)", + active: "rgb(42, 45, 46)", + description: "rgb(157, 157, 157)", + background: "rgb(30, 30, 30)", + }, + }, + { + name: "light", + bodyClass: "vscode-light", + themeId: "Default Light Modern", + expected: { + hover: "rgb(232, 232, 232)", + active: "rgb(232, 232, 232)", + description: "rgb(113, 113, 113)", + background: "rgb(255, 255, 255)", + }, + }, +] as const + +for (const theme of themes) { + test(`renders remaining controls in the VS Code ${theme.name} theme`, async ({ mount, page }) => { + await page.evaluate(({ bodyClass, themeId }) => { + document.documentElement.className = bodyClass + document.body.className = bodyClass + document.body.dataset.vscodeThemeId = themeId + }, theme) + + const component = await mount( +
+ + +
, + ) + + const iconButton = component.getByRole("button", { name: "Settings" }) + await iconButton.hover() + await expect(iconButton).toHaveCSS("background-color", theme.expected.hover) + await iconButton.focus() + await page.mouse.down() + await expect(iconButton).toHaveCSS("background-color", theme.expected.active) + await page.mouse.up() + + const checkbox = component.getByRole("checkbox", { name: "Include optional context" }) + await expect(checkbox).toHaveCSS("background-color", theme.expected.description) + await expect(checkbox).toHaveCSS("color", theme.expected.background) + }) +} diff --git a/webview-ui/src/components/chat/__tests__/UpdateTodoListToolBlock.spec.tsx b/webview-ui/src/components/chat/__tests__/UpdateTodoListToolBlock.spec.tsx new file mode 100644 index 0000000000..a8ca2dcf09 --- /dev/null +++ b/webview-ui/src/components/chat/__tests__/UpdateTodoListToolBlock.spec.tsx @@ -0,0 +1,42 @@ +import { fireEvent, render, screen } from "@testing-library/react" +import { describe, expect, test, vi } from "vitest" + +import UpdateTodoListToolBlock from "../UpdateTodoListToolBlock" + +describe("UpdateTodoListToolBlock", () => { + test("renders theme-aware edit controls", () => { + render( + , + ) + + fireEvent.click(screen.getByRole("button", { name: "Edit" })) + expect(screen.getByTitle("Remove")).toBeInTheDocument() + expect(screen.getByDisplayValue("Ship the cleanup")).toBeInTheDocument() + + fireEvent.click(screen.getByRole("button", { name: "+ Add Todo" })) + expect(screen.getByPlaceholderText("Enter todo item, press Enter to add")).toBeInTheDocument() + }) + + test("cancels and confirms todo deletion", () => { + const onChange = vi.fn() + render( + , + ) + + fireEvent.click(screen.getByRole("button", { name: "Edit" })) + fireEvent.click(screen.getByTitle("Remove")) + expect(screen.getByRole("alertdialog")).toBeInTheDocument() + fireEvent.click(screen.getByRole("button", { name: "Cancel" })) + expect(onChange).not.toHaveBeenCalled() + + fireEvent.click(screen.getByTitle("Remove")) + fireEvent.click(screen.getByRole("button", { name: "Delete" })) + expect(onChange).toHaveBeenCalledWith([]) + }) +}) diff --git a/webview-ui/src/components/chat/chatControlStyles.ts b/webview-ui/src/components/chat/chatControlStyles.ts new file mode 100644 index 0000000000..a0beb91494 --- /dev/null +++ b/webview-ui/src/components/chat/chatControlStyles.ts @@ -0,0 +1,5 @@ +export const enabledChatControlClassName = + "hover:opacity-100 hover:bg-vscode-toolbar-hoverBackground active:bg-vscode-list-hoverBackground" + +export const disabledChatControlClassName = + "opacity-40 cursor-not-allowed grayscale-[30%] hover:bg-transparent active:bg-transparent" diff --git a/webview-ui/src/components/ui/__tests__/checkbox.spec.tsx b/webview-ui/src/components/ui/__tests__/checkbox.spec.tsx new file mode 100644 index 0000000000..ab02e4b8ae --- /dev/null +++ b/webview-ui/src/components/ui/__tests__/checkbox.spec.tsx @@ -0,0 +1,12 @@ +import { render, screen } from "@/utils/test-utils" +import { describe, expect, test } from "vitest" + +import { Checkbox } from "../checkbox" + +describe("Checkbox", () => { + test("renders the description variant as checked", () => { + render() + + expect(screen.getByRole("checkbox", { name: "Include optional context" })).toBeChecked() + }) +}) diff --git a/webview-ui/src/components/ui/checkbox.tsx b/webview-ui/src/components/ui/checkbox.tsx index a4b005d00a..bfc069f852 100644 --- a/webview-ui/src/components/ui/checkbox.tsx +++ b/webview-ui/src/components/ui/checkbox.tsx @@ -15,7 +15,7 @@ const checkboxVariants = cva( default: "border-vscode-foreground data-[state=checked]:bg-primary data-[state=checked]:text-foreground", description: - "border-vscode-descriptionForeground data-[state=checked]:bg-vscode-descriptionForeground data-[state=checked]:text-white", + "border-vscode-descriptionForeground data-[state=checked]:bg-vscode-descriptionForeground data-[state=checked]:text-vscode-editor-background", }, }, defaultVariants: { @@ -25,14 +25,13 @@ const checkboxVariants = cva( ) export interface CheckboxProps - extends React.ComponentPropsWithoutRef, - VariantProps {} + extends React.ComponentPropsWithoutRef, VariantProps {} const Checkbox = React.forwardRef, CheckboxProps>( ({ className, variant, ...props }, ref) => ( - + ),