Skip to content
Draft
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
14 changes: 14 additions & 0 deletions webview-ui/playwright/vscode-theme-base.css
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,20 @@ body {
padding: 16px;
}

.vscode-text-field-mock {
box-sizing: border-box;
background: var(--vscode-input-background);
border: 1px solid var(--vscode-input-border, transparent);
color: var(--vscode-input-foreground);
font: inherit;
padding: 4px 6px;
}

.vscode-text-field-mock::placeholder {
color: var(--vscode-input-placeholderForeground, var(--vscode-descriptionForeground));
opacity: 1;
}

/* @vscode/webview-ui-toolkit/react renders VSCodeButton as a bare
<button appearance="primary|secondary"> outside VS Code's web-component
runtime. Style it here so button screenshots match the real appearance. */
Expand Down
12 changes: 9 additions & 3 deletions webview-ui/src/__mocks__/@vscode/webview-ui-toolkit/react.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,15 @@ export const VSCodeLink = ({ children, href, ...props }: any) => (
</a>
)

export const VSCodeTextField = ({ value, onInput, "data-testid": dataTestId, children, ...props }: any) => (
<div>
<input data-testid={dataTestId} value={value} onInput={onInput} {...props} />
export const VSCodeTextField = ({ value, onInput, "data-testid": dataTestId, children, className, ...props }: any) => (
<div className={className}>
<input
className="vscode-text-field-mock w-full"
data-testid={dataTestId}
value={value}
onInput={onInput}
{...props}
/>
{children}
</div>
)
Expand Down
17 changes: 11 additions & 6 deletions webview-ui/src/components/chat/CommandPatternSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,15 +71,15 @@ export const CommandPatternSelector: React.FC<CommandPatternSelectorProps> = ({
<span
className={cn(
"text-sm flex-1 group-hover:opacity-100",
isExpanded ? "opacity-100" : "opacity-40",
isExpanded ? "text-vscode-foreground" : "text-vscode-descriptionForeground",
)}>
<CheckCheck className="size-3 inline-block mr-2" />
{t("chat:commandExecution.manageCommands")}
</span>
<ChevronUp
className={cn(
"group-hover:opacity-100 size-4 transition-transform",
isExpanded ? "opacity-100" : "opacity-40 -rotate-180",
isExpanded ? "text-vscode-foreground" : "text-vscode-descriptionForeground -rotate-180",
)}
/>
</div>
Expand Down Expand Up @@ -135,12 +135,14 @@ export const CommandPatternSelector: React.FC<CommandPatternSelectorProps> = ({
)}>
<button
className={cn("p-1 rounded transition-all cursor-pointer", {
"bg-green-500/20 text-green-500 hover:bg-green-500/30":
"bg-vscode-list-activeSelectionBackground text-vscode-list-activeSelectionForeground hover:bg-vscode-list-focusBackground":
status === "allowed",
"text-vscode-descriptionForeground hover:text-green-500 hover:bg-green-500/10":
"text-vscode-descriptionForeground hover:text-vscode-foreground hover:bg-vscode-list-hoverBackground":
status !== "allowed",
})}
onClick={() => onAllowPatternChange(editState.value)}
aria-pressed={status === "allowed"}
data-testid="allow-command-pattern"
aria-label={t(
status === "allowed"
? "chat:commandExecution.removeFromAllowed"
Expand All @@ -157,11 +159,14 @@ export const CommandPatternSelector: React.FC<CommandPatternSelectorProps> = ({
)}>
<button
className={cn("p-1 rounded transition-all cursor-pointer", {
"bg-red-500/20 text-red-500 hover:bg-red-500/30": status === "denied",
"text-vscode-descriptionForeground hover:text-red-500 hover:bg-red-500/10":
"bg-vscode-list-activeSelectionBackground text-vscode-list-activeSelectionForeground hover:bg-vscode-list-focusBackground":
status === "denied",
"text-vscode-descriptionForeground hover:text-vscode-foreground hover:bg-vscode-list-hoverBackground":
status !== "denied",
})}
onClick={() => onDenyPatternChange(editState.value)}
aria-pressed={status === "denied"}
data-testid="deny-command-pattern"
aria-label={t(
status === "denied"
? "chat:commandExecution.removeFromDenied"
Expand Down
12 changes: 6 additions & 6 deletions webview-ui/src/components/chat/ContextMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
style={{
fontWeight: "bold",
fontSize: "0.85em",
opacity: 0.8,
color: "var(--vscode-descriptionForeground)",
}}>
{option.label}
</span>
Expand All @@ -95,7 +95,7 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
{option.description && (
<span
style={{
opacity: 0.5,
color: "var(--vscode-descriptionForeground)",
fontSize: "0.9em",
lineHeight: "1.2",
whiteSpace: "nowrap",
Expand All @@ -115,7 +115,7 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
{option.argumentHint && (
<span
style={{
opacity: 0.5,
color: "var(--vscode-descriptionForeground)",
fontSize: "0.9em",
lineHeight: "1.2",
}}>
Expand All @@ -126,7 +126,7 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
{option.description && (
<span
style={{
opacity: 0.5,
color: "var(--vscode-descriptionForeground)",
fontSize: "0.9em",
lineHeight: "1.2",
whiteSpace: "nowrap",
Expand Down Expand Up @@ -154,7 +154,7 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
<span
style={{
fontSize: "0.85em",
opacity: 0.7,
color: "var(--vscode-descriptionForeground)",
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
Expand Down Expand Up @@ -197,7 +197,7 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
direction: "rtl",
textAlign: "right",
flex: 1,
opacity: 0.75,
color: "var(--vscode-descriptionForeground)",
fontSize: "0.75em",
}}>
{folderPath}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import React from "react"

import McpToolRow from "../../mcp/McpToolRow"
import { CommandPatternSelector } from "../CommandPatternSelector"

export function ThemeSensitiveStatusFixture() {
return (
<main
data-testid="status-surface"
className="w-[480px] space-y-5 rounded-lg border border-vscode-panel-border bg-vscode-editor-background p-4 text-vscode-editor-foreground">
<section aria-labelledby="commands-title" className="space-y-2">
<h2 id="commands-title" className="m-0 text-base font-semibold">
Command permissions
</h2>
<CommandPatternSelector
patterns={[
{ pattern: "pnpm test", description: "Run the test suite" },
{ pattern: "rm -rf", description: "Remove files recursively" },
]}
allowedCommands={["pnpm test"]}
deniedCommands={["rm -rf"]}
onAllowPatternChange={() => undefined}
onDenyPatternChange={() => undefined}
/>
</section>

<section aria-labelledby="mcp-title" className="space-y-2">
<h2 id="mcp-title" className="m-0 text-base font-semibold">
MCP tools
</h2>
<McpToolRow
tool={{
name: "search_workspace",
description: "Search files and symbols in the current workspace",
enabledForPrompt: false,
inputSchema: { type: "object", properties: {} },
}}
serverName="workspace-tools"
/>
</section>
</main>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import React from "react"

import { expect, test } from "../../../../playwright/coverage-fixture"
import { AppProviders } from "../../../../playwright/AppProviders"
import { expectContrast } from "../../../../playwright/contrast"
import { applyVisualTheme, visualThemes } from "../../../../playwright/themes"
import { ThemeSensitiveStatusFixture } from "./ThemeSensitiveStatus.visual.fixture"

for (const theme of visualThemes) {
test(`audits status controls in the VS Code ${theme.name} theme`, async ({ mount, page }) => {
await applyVisualTheme(page, theme)
const component = await mount(
<AppProviders>
<ThemeSensitiveStatusFixture />
</AppProviders>,
)
const surface = component.getByTestId("status-surface")

await component.getByRole("button", { name: /auto-approved commands/i }).click()
const allowButton = component.getByTestId("allow-command-pattern").first()
const denyButton = component.getByTestId("deny-command-pattern").nth(1)
await expect(allowButton).toHaveAttribute("aria-pressed", "true")
await expect(denyButton).toHaveAttribute("aria-pressed", "true")
await expectContrast(allowButton, {
background: allowButton,
minimum: 3,
label: `${theme.name} allowed command indicator`,
})
await expectContrast(denyButton, {
background: denyButton,
minimum: 3,
label: `${theme.name} denied command indicator`,
})
await expectContrast(component.getByText("Search files and symbols in the current workspace"), {
background: surface,
label: `${theme.name} disabled MCP description`,
})

await expect(component).toHaveScreenshot(`theme-sensitive-status-${theme.name}.png`)
})
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 5 additions & 8 deletions webview-ui/src/components/chat/checkpoints/CheckpointSaved.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,17 +87,14 @@ export const CheckpointSaved = ({
className="flex items-center justify-between gap-2 pt-2 pb-3"
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}>
<div className="flex items-center gap-2 text-blue-400 whitespace-nowrap">
<div className="flex items-center gap-2 text-vscode-textLink-foreground whitespace-nowrap">
<GitCommitVertical className="w-4" />
<span className="font-semibold">{t("chat:checkpoint.regular")}</span>
{isCurrent && <span className="text-muted">({t("chat:checkpoint.current")})</span>}
{isCurrent && (
<span className="text-vscode-descriptionForeground">({t("chat:checkpoint.current")})</span>
)}
</div>
<span
className="block w-full h-[2px] mt-[2px] text-xs"
style={{
backgroundImage:
"linear-gradient(90deg, rgba(0, 188, 255, .65), rgba(0, 188, 255, .65) 80%, rgba(0, 188, 255, 0) 99%)",
}}></span>
<span className="block w-full h-[2px] mt-[2px] bg-vscode-textLink-foreground text-xs" aria-hidden="true" />

{/* Keep menu visible while hovering, popover is open, or briefly after close to prevent jump */}
<div data-testid="checkpoint-menu-container" className={cn("h-4 -mt-2", menuVisible ? "block" : "hidden")}>
Expand Down
12 changes: 7 additions & 5 deletions webview-ui/src/components/common/CodeBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ import { ChevronDown, ChevronUp, Copy, Check } from "lucide-react"
import { useAppTranslation } from "@src/i18n/TranslationContext"
import { StandardTooltip } from "@/components/ui"

export const CODE_BLOCK_BG_COLOR = "var(--vscode-editor-background, --vscode-sideBar-background, rgb(30 30 30))"
export const WRAPPER_ALPHA = "cc" // 80% opacity
export const CODE_BLOCK_BG_COLOR =
"var(--vscode-textCodeBlock-background, var(--vscode-editor-background, var(--vscode-sideBar-background, rgb(30 30 30))))"
export const CODE_BLOCK_TOOLBAR_BG_COLOR =
"color-mix(in srgb, var(--vscode-editor-background, var(--vscode-sideBar-background, rgb(30 30 30))) 90%, transparent)"

// Configuration constants
export const WINDOW_SHADE_SETTINGS = {
Expand Down Expand Up @@ -42,14 +44,14 @@ interface CodeBlockProps {
const CodeBlockButton = styled.button`
background: transparent;
border: none;
color: var(--vscode-foreground);
color: var(--vscode-icon-foreground, var(--vscode-foreground));
cursor: var(--copy-button-cursor, default);
padding: 4px;
margin: 0 0px;
display: flex;
align-items: center;
justify-content: center;
opacity: 0.4;
opacity: 1;
border-radius: 3px;
pointer-events: var(--copy-button-events, none);
margin-left: 4px;
Expand All @@ -73,7 +75,7 @@ const CodeBlockButtonWrapper = styled.div`
right: var(--copy-button-right, 8px);
height: auto;
z-index: 40;
background: ${CODE_BLOCK_BG_COLOR}${WRAPPER_ALPHA};
background: ${CODE_BLOCK_TOOLBAR_BG_COLOR};
overflow: visible;
pointer-events: none;
opacity: var(--copy-button-opacity, 0);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import React from "react"

import { TerminalOutput } from "../../chat/TerminalOutput"
import { StyledPre } from "../CodeBlock"
import DiffView from "../DiffView"

const diff = `@@ -1,3 +1,3 @@
export function greet(name: string) {
- return "Hello " + name
+ return \`Hello \${name}\`
}`

export function RenderedContentContrastFixture() {
return (
<main className="w-[620px] space-y-4 rounded-lg border border-vscode-panel-border bg-vscode-editor-background p-4 text-vscode-editor-foreground">
<section aria-labelledby="code-title" className="space-y-2">
<h2 id="code-title" className="m-0 text-base font-semibold">
Code response
</h2>
<div data-testid="code-block">
<StyledPre wordwrap="true" windowshade="false">
<pre>
<code>const greeting = &quot;Hello, Zoo Code&quot;</code>
</pre>
</StyledPre>
</div>
</section>

<section aria-labelledby="diff-title" className="space-y-2">
<h2 id="diff-title" className="m-0 text-base font-semibold">
Proposed edit
</h2>
<div data-testid="diff-view">
<DiffView source={diff} />
</div>
</section>

<section aria-labelledby="terminal-title" className="space-y-2">
<h2 id="terminal-title" className="m-0 text-base font-semibold">
Terminal output
</h2>
<div data-testid="terminal-output">
<TerminalOutput
content={"Tests passed\n\u001b[32m24 passing\u001b[0m \u001b[33m1 skipped\u001b[0m"}
/>
</div>
</section>
</main>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React from "react"

import { expect, test } from "../../../../playwright/coverage-fixture"
import { expectContrast } from "../../../../playwright/contrast"
import { applyVisualTheme, visualThemes } from "../../../../playwright/themes"
import { RenderedContentContrastFixture } from "./RenderedContentContrast.visual.fixture"

for (const theme of visualThemes) {
test(`audits rendered content in the VS Code ${theme.name} theme`, async ({ mount, page }) => {
await applyVisualTheme(page, theme)
const component = await mount(<RenderedContentContrastFixture />)

const code = component.getByTestId("code-block").locator("code")
await expect(code).toContainText("Hello, Zoo Code")
await expectContrast(code, {
background: component.getByTestId("code-block").locator("pre"),
label: `${theme.name} code text`,
})

const inserted = component.getByTestId("diff-view").locator(".diff-content-inserted").first()
const removed = component.getByTestId("diff-view").locator(".diff-content-removed").first()
await expectContrast(inserted, { background: inserted, label: `${theme.name} inserted diff text` })
await expectContrast(removed, { background: removed, label: `${theme.name} removed diff text` })

const terminal = component.getByTestId("terminal-output").locator("pre")
await expectContrast(terminal, { background: terminal, label: `${theme.name} terminal text` })

await expect(component).toHaveScreenshot(`rendered-content-${theme.name}.png`)
})
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading