Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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
Loading
Loading