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
1 change: 1 addition & 0 deletions webview-ui/playwright/vscode-theme-light.css
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
--vscode-list-activeSelectionBackground: #0060c0;
--vscode-list-activeSelectionForeground: #ffffff;
--vscode-toolbar-hoverBackground: #e8e8e8;
--vscode-panel-border: #cecece;
--vscode-widget-border: #d4d4d4;
--vscode-widget-shadow: #00000029;
--vscode-menu-foreground: #3b3b3b;
Expand Down
8 changes: 3 additions & 5 deletions webview-ui/src/components/chat/ApiConfigSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useState, useMemo, useCallback } from "react"
import { Fzf } from "fzf"

import { cn } from "@/lib/utils"
import { enabledSelectorTriggerClassName, selectorTriggerClassName } from "@/components/ui/selectorTriggerStyles"
import { useRooPortal } from "@/components/ui/hooks/useRooPortal"
import { Popover, PopoverContent, PopoverTrigger, StandardTooltip } from "@/components/ui"
import { useAppTranslation } from "@/i18n/TranslationContext"
Expand Down Expand Up @@ -153,11 +154,8 @@ export const ApiConfigSelector = ({
data-testid="dropdown-trigger"
className={cn(
"min-w-0 inline-flex items-center relative whitespace-nowrap px-1.5 py-1 text-xs",
"bg-transparent border border-[rgba(255,255,255,0.08)] rounded-md text-vscode-foreground",
"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",
selectorTriggerClassName,
disabled ? "opacity-50 cursor-not-allowed" : enabledSelectorTriggerClassName,
triggerClassName,
)}>
<span className="truncate">{displayName}</span>
Expand Down
8 changes: 3 additions & 5 deletions webview-ui/src/components/chat/AutoApproveDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { ListChecks, LayoutList, Settings, CheckCheck, X } from "lucide-react"
import { vscode } from "@/utils/vscode"

import { cn } from "@/lib/utils"
import { enabledSelectorTriggerClassName, selectorTriggerClassName } from "@/components/ui/selectorTriggerStyles"

import { useExtensionState } from "@/context/ExtensionStateContext"

Expand Down Expand Up @@ -152,12 +153,9 @@ export const AutoApproveDropdown = ({ disabled = false, triggerClassName = "" }:
data-testid="auto-approve-dropdown-trigger"
className={cn(
"inline-flex items-center gap-1.5 relative whitespace-nowrap px-1.5 py-1 text-xs",
"bg-transparent border border-[rgba(255,255,255,0.08)] rounded-md text-vscode-foreground",
"transition-all duration-150 focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder focus-visible:ring-inset",
selectorTriggerClassName,
"max-[300px]:shrink-0",
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",
disabled ? "opacity-50 cursor-not-allowed" : enabledSelectorTriggerClassName,
triggerClassName,
)}>
{!effectiveAutoApprovalEnabled ? (
Expand Down
8 changes: 3 additions & 5 deletions webview-ui/src/components/chat/ModeSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { type Mode, getAllModes, defaultModeSlug } from "@roo/modes"
import { vscode } from "@/utils/vscode"
import { telemetryClient } from "@/utils/TelemetryClient"
import { cn } from "@/lib/utils"
import { enabledSelectorTriggerClassName, selectorTriggerClassName } from "@/components/ui/selectorTriggerStyles"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { useAppTranslation } from "@/i18n/TranslationContext"
import { useRooPortal } from "@/components/ui/hooks/useRooPortal"
Expand Down Expand Up @@ -223,11 +224,8 @@ export const ModeSelector = ({
data-testid="mode-selector-trigger"
className={cn(
"inline-flex items-center relative whitespace-nowrap px-1.5 py-1 text-xs",
"bg-transparent border border-[rgba(255,255,255,0.08)] rounded-md text-vscode-foreground",
"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",
selectorTriggerClassName,
disabled ? "opacity-50 cursor-not-allowed" : enabledSelectorTriggerClassName,
triggerClassName,
!disabled && !hasOpenedModeSelector
? "bg-primary opacity-90 hover:bg-primary-hover text-vscode-button-foreground"
Expand Down
87 changes: 27 additions & 60 deletions webview-ui/src/components/chat/UpdateTodoListToolBlock.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
import React, { useState, useEffect, useRef } from "react"
import { ToolUseBlock, ToolUseBlockHeader } from "../common/ToolUseBlock"
import MarkdownBlock from "../common/MarkdownBlock"
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogTitle,
} from "@/components/ui"

interface TodoItem {
id?: string
Expand Down Expand Up @@ -430,67 +439,25 @@ const UpdateTodoListToolBlock: React.FC<UpdateTodoListToolBlockProps> = ({
<MarkdownBlock markdown={content} />
)}
</div>
{/* Delete confirmation dialog */}
{deleteId && (
<div
style={{
position: "fixed",
left: 0,
top: 0,
right: 0,
bottom: 0,
background: "rgba(0,0,0,0.15)",
zIndex: 9999,
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
onClick={cancelDelete}>
<div
style={{
background: "#fff",
borderRadius: 8,
boxShadow: "0 2px 16px rgba(0,0,0,0.15)",
padding: "16px 20px",
minWidth: 200,
zIndex: 10000,
}}
onClick={(e) => e.stopPropagation()}>
<div style={{ marginBottom: 12, fontSize: 14, color: "#333" }}>
Are you sure you want to delete this todo item?
</div>
<div style={{ display: "flex", justifyContent: "flex-end", gap: 8 }}>
<button
onClick={cancelDelete}
style={{
border: "1px solid #bbb",
background: "transparent",
color: "#888",
borderRadius: 4,
padding: "2px 10px",
cursor: "pointer",
fontSize: 12,
}}>
Cancel
</button>
<button
onClick={confirmDelete}
style={{
border: "1px solid #f14c4c",
background: "#f14c4c",
color: "#fff",
borderRadius: 4,
padding: "2px 10px",
cursor: "pointer",
fontSize: 12,
}}>
Delete
</button>
</div>
</div>
</div>
)}
</ToolUseBlock>
<AlertDialog open={deleteId !== null} onOpenChange={cancelDelete}>
<AlertDialogContent className="max-w-xs">
<AlertDialogTitle className="sr-only">Delete todo item</AlertDialogTitle>
<AlertDialogDescription>Are you sure you want to delete this todo item?</AlertDialogDescription>
<AlertDialogFooter>
<AlertDialogCancel
onClick={cancelDelete}
className="bg-transparent text-vscode-foreground border-vscode-dropdown-border hover:bg-vscode-toolbar-hoverBackground">
Cancel
</AlertDialogCancel>
<AlertDialogAction
onClick={confirmDelete}
className="bg-transparent text-vscode-errorForeground border-vscode-errorForeground hover:bg-vscode-toolbar-hoverBackground">
Delete
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</>
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { render, screen } from "@/utils/test-utils"
import { describe, expect, test, vi } from "vitest"

import { AutoApproveDropdown } from "../AutoApproveDropdown"

vi.mock("@/utils/vscode", () => ({ vscode: { postMessage: vi.fn() } }))

vi.mock("@/context/ExtensionStateContext", () => ({
useExtensionState: () => ({
autoApprovalEnabled: false,
setAutoApprovalEnabled: vi.fn(),
setAlwaysAllowReadOnly: vi.fn(),
setAlwaysAllowWrite: vi.fn(),
setAlwaysAllowExecute: vi.fn(),
setAlwaysAllowMcp: vi.fn(),
setAlwaysAllowModeSwitch: vi.fn(),
setAlwaysAllowSubtasks: vi.fn(),
setAlwaysAllowFollowupQuestions: vi.fn(),
}),
}))

vi.mock("@/i18n/TranslationContext", () => ({
useAppTranslation: () => ({ t: (key: string) => key }),
}))

vi.mock("@/hooks/useAutoApprovalToggles", () => ({
useAutoApprovalToggles: () => ({
alwaysAllowReadOnly: false,
alwaysAllowWrite: false,
alwaysAllowExecute: false,
alwaysAllowMcp: false,
alwaysAllowModeSwitch: false,
alwaysAllowSubtasks: false,
alwaysAllowFollowupQuestions: false,
}),
}))

vi.mock("@/hooks/useAutoApprovalState", () => ({
useAutoApprovalState: () => ({ effectiveAutoApprovalEnabled: false }),
}))

vi.mock("@/components/ui/hooks/useRooPortal", () => ({
useRooPortal: () => document.body,
}))

describe("AutoApproveDropdown", () => {
test("enables the trigger by default", () => {
render(<AutoApproveDropdown />)

expect(screen.getByTestId("auto-approve-dropdown-trigger")).toBeEnabled()
})

test("disables the trigger when auto-approval controls are unavailable", () => {
render(<AutoApproveDropdown disabled />)

expect(screen.getByTestId("auto-approve-dropdown-trigger")).toBeDisabled()
})
})
14 changes: 14 additions & 0 deletions webview-ui/src/components/chat/__tests__/ModeSelector.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,20 @@ describe("ModeSelector", () => {
expect(screen.getByTestId("mode-selector-trigger")).toBeInTheDocument()
})

test("disables the trigger when mode selection is unavailable", () => {
render(
<ModeSelector
title="Mode Selector"
value={"code" as Mode}
onChange={vi.fn()}
modeShortcutText="Ctrl+M"
disabled
/>,
)

expect(screen.getByTestId("mode-selector-trigger")).toBeDisabled()
})

test("shows search bar when there are more than 6 modes", () => {
mockModes = Array.from({ length: 7 }, (_, i) => ({
slug: `mode-${i}`,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import React from "react"

import { expect, test } from "../../../../playwright/coverage-fixture"
import UpdateTodoListToolBlock from "../UpdateTodoListToolBlock"
import { SelectDropdown } from "@/components/ui/select-dropdown"

const themes = [
{
name: "dark",
bodyClass: "vscode-dark",
themeId: "Default Dark Modern",
expected: {
background: "rgb(30, 30, 30)",
description: "rgb(157, 157, 157)",
dropdownBorder: "rgb(60, 60, 60)",
hoverBackground: "rgb(42, 45, 46)",
focusBorder: "rgb(0, 127, 212)",
error: "rgb(244, 135, 113)",
panelBorder: "rgb(43, 43, 43)",
},
},
{
name: "light",
bodyClass: "vscode-light",
themeId: "Default Light Modern",
expected: {
background: "rgb(255, 255, 255)",
description: "rgb(113, 113, 113)",
dropdownBorder: "rgb(206, 206, 206)",
hoverBackground: "rgb(232, 232, 232)",
focusBorder: "rgb(0, 144, 241)",
error: "rgb(161, 38, 13)",
panelBorder: "rgb(206, 206, 206)",
},
},
] as const

for (const theme of themes) {
test(`renders selectors and confirmation dialogs 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(
<div className="flex flex-col gap-4 w-96">
<SelectDropdown value="code" options={[{ value: "code", label: "Code" }]} onChange={() => undefined} />
<UpdateTodoListToolBlock
todos={[{ id: "todo-1", content: "Ship the follow-up", status: "in_progress" }]}
onChange={() => undefined}
/>
</div>,
)

await component.evaluate(async () => {
await document.fonts.ready
await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()))
})

await expect(component).toHaveScreenshot(`chat-controls-resting-${theme.name}.png`)

const trigger = component.getByTestId("dropdown-trigger")
await expect(trigger).toHaveCSS("border-color", theme.expected.dropdownBorder)
await trigger.hover()
await expect(trigger).toHaveCSS("background-color", theme.expected.hoverBackground)
await expect(trigger).toHaveCSS("border-color", theme.expected.focusBorder)
await page.keyboard.press("Tab")
await expect(trigger).toBeFocused()
await expect
.poll(() => trigger.evaluate((element) => getComputedStyle(element).boxShadow))
.toContain(theme.expected.focusBorder)

await expect(component).toHaveScreenshot(`chat-controls-focus-${theme.name}.png`)

await component.getByRole("button", { name: "Edit" }).click()
await component.getByTitle("Remove").click()
const dialog = page.getByRole("alertdialog")
await expect(dialog).toBeVisible()
await expect(dialog).toHaveCSS("background-color", theme.expected.background)
await expect(dialog).toHaveCSS("border-color", theme.expected.panelBorder)
await expect(page.getByText("Are you sure you want to delete this todo item?")).toHaveCSS(
"color",
theme.expected.description,
)
await expect(page.getByRole("button", { name: "Delete" })).toHaveCSS("color", theme.expected.error)

await expect(dialog).toHaveScreenshot(`chat-controls-delete-dialog-${theme.name}.png`)
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { fireEvent, render, screen, waitFor } from "@testing-library/react"
import { beforeEach, describe, expect, it, vi } from "vitest"

import UpdateTodoListToolBlock from "../UpdateTodoListToolBlock"

describe("UpdateTodoListToolBlock", () => {
const onChange = vi.fn()

beforeEach(() => {
vi.clearAllMocks()
})

function renderEditableList() {
return render(
<UpdateTodoListToolBlock
todos={[{ id: "todo-1", content: "Ship the follow-up", status: "in_progress" }]}
onChange={onChange}
/>,
)
}

it("cancels deletion without changing the todo list", async () => {
renderEditableList()
fireEvent.click(screen.getByRole("button", { name: "Edit" }))
fireEvent.click(screen.getByTitle("Remove"))

expect(screen.getByRole("alertdialog")).toBeInTheDocument()
expect(screen.getByText("Are you sure you want to delete this todo item?")).toBeInTheDocument()
fireEvent.click(screen.getByRole("button", { name: "Cancel" }))

await waitFor(() => expect(screen.queryByRole("alertdialog")).not.toBeInTheDocument())
expect(onChange).not.toHaveBeenCalled()
})

it("deletes the selected todo after confirmation", async () => {
renderEditableList()
fireEvent.click(screen.getByRole("button", { name: "Edit" }))
fireEvent.click(screen.getByTitle("Remove"))
fireEvent.click(screen.getByRole("button", { name: "Delete" }))

await waitFor(() => expect(screen.queryByRole("alertdialog")).not.toBeInTheDocument())
expect(onChange).toHaveBeenCalledWith([])
})
})
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
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