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
669 changes: 276 additions & 393 deletions webview-ui/src/components/prompts/PromptsView.tsx

Large diffs are not rendered by default.

139 changes: 77 additions & 62 deletions webview-ui/src/components/prompts/__tests__/PromptsView.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,22 @@ jest.mock("@src/utils/vscode", () => ({
},
}))

// Mock all lucide-react icons with a proxy to handle any icon requested
jest.mock("lucide-react", () => {
return new Proxy(
{},
{
get: function (obj, prop) {
// Return a component factory for any icon that's requested
if (prop === "__esModule") {
return true
}
return () => <div data-testid={`${String(prop)}-icon`}>{String(prop)}</div>
},
},
)
})

const mockExtensionState = {
customModePrompts: {},
listApiConfigMeta: [
Expand All @@ -19,6 +35,9 @@ const mockExtensionState = {
enhancementApiConfigId: "",
setEnhancementApiConfigId: jest.fn(),
mode: "code",
customModes: [],
customSupportPrompts: [],
currentApiConfigName: "",
customInstructions: "Initial instructions",
setCustomInstructions: jest.fn(),
}
Expand All @@ -32,69 +51,67 @@ const renderPromptsView = (props = {}) => {
)
}

class MockResizeObserver {
observe() {}
unobserve() {}
disconnect() {}
}

global.ResizeObserver = MockResizeObserver

Element.prototype.scrollIntoView = jest.fn()

describe("PromptsView", () => {
beforeEach(() => {
jest.clearAllMocks()
})

it("renders all mode tabs", () => {
renderPromptsView()
expect(screen.getByTestId("code-tab")).toBeInTheDocument()
expect(screen.getByTestId("ask-tab")).toBeInTheDocument()
expect(screen.getByTestId("architect-tab")).toBeInTheDocument()
it("displays the current mode name in the select trigger", () => {
renderPromptsView({ mode: "code" })
const selectTrigger = screen.getByTestId("mode-select-trigger")
expect(selectTrigger).toHaveTextContent("Code")
})

it("defaults to current mode as active tab", () => {
renderPromptsView({ mode: "ask" })

const codeTab = screen.getByTestId("code-tab")
const askTab = screen.getByTestId("ask-tab")
const architectTab = screen.getByTestId("architect-tab")

expect(askTab).toHaveAttribute("data-active", "true")
expect(codeTab).toHaveAttribute("data-active", "false")
expect(architectTab).toHaveAttribute("data-active", "false")
it("opens the mode selection popover when the trigger is clicked", async () => {
renderPromptsView()
const selectTrigger = screen.getByTestId("mode-select-trigger")
fireEvent.click(selectTrigger)
await waitFor(() => {
expect(selectTrigger).toHaveAttribute("aria-expanded", "true")
})
})

it("switches between tabs correctly", async () => {
const { rerender } = render(
<ExtensionStateContext.Provider value={{ ...mockExtensionState, mode: "code" } as any}>
<PromptsView onDone={jest.fn()} />
</ExtensionStateContext.Provider>,
)

const codeTab = screen.getByTestId("code-tab")
const askTab = screen.getByTestId("ask-tab")
const architectTab = screen.getByTestId("architect-tab")
it("filters mode options based on search input", async () => {
renderPromptsView()
const selectTrigger = screen.getByTestId("mode-select-trigger")
fireEvent.click(selectTrigger)

// Initial state matches current mode (code)
expect(codeTab).toHaveAttribute("data-active", "true")
expect(askTab).toHaveAttribute("data-active", "false")
expect(architectTab).toHaveAttribute("data-active", "false")
const searchInput = screen.getByTestId("mode-search-input")
fireEvent.change(searchInput, { target: { value: "ask" } })

// Click Ask tab and update context
fireEvent.click(askTab)
rerender(
<ExtensionStateContext.Provider value={{ ...mockExtensionState, mode: "ask" } as any}>
<PromptsView onDone={jest.fn()} />
</ExtensionStateContext.Provider>,
)
await waitFor(() => {
expect(screen.getByTestId("mode-option-ask")).toBeInTheDocument()
expect(screen.queryByTestId("mode-option-code")).not.toBeInTheDocument()
expect(screen.queryByTestId("mode-option-architect")).not.toBeInTheDocument()
})
})

expect(askTab).toHaveAttribute("data-active", "true")
expect(codeTab).toHaveAttribute("data-active", "false")
expect(architectTab).toHaveAttribute("data-active", "false")
it("selects a mode from the dropdown and sends update message", async () => {
renderPromptsView()
const selectTrigger = screen.getByTestId("mode-select-trigger")
fireEvent.click(selectTrigger)

// Click Architect tab and update context
fireEvent.click(architectTab)
rerender(
<ExtensionStateContext.Provider value={{ ...mockExtensionState, mode: "architect" } as any}>
<PromptsView onDone={jest.fn()} />
</ExtensionStateContext.Provider>,
)
const askOption = await waitFor(() => screen.getByTestId("mode-option-ask"))
fireEvent.click(askOption)

expect(architectTab).toHaveAttribute("data-active", "true")
expect(askTab).toHaveAttribute("data-active", "false")
expect(codeTab).toHaveAttribute("data-active", "false")
expect(mockExtensionState.setEnhancementApiConfigId).not.toHaveBeenCalled() // Ensure this is not called by mode switch
expect(vscode.postMessage).toHaveBeenCalledWith({
type: "mode",
text: "ask",
})
await waitFor(() => {
expect(selectTrigger).toHaveAttribute("aria-expanded", "false")
})
})

it("handles prompt changes correctly", async () => {
Expand Down Expand Up @@ -159,21 +176,19 @@ describe("PromptsView", () => {
it("handles API configuration selection", async () => {
renderPromptsView()

// Click the ENHANCE tab first to show the API config dropdown
const enhanceTab = screen.getByTestId("ENHANCE-tab")
fireEvent.click(enhanceTab)
const trigger = screen.getByTestId("support-prompt-select-trigger")
fireEvent.click(trigger)

// Wait for the ENHANCE tab click to take effect
const dropdown = await waitFor(() => screen.getByTestId("api-config-dropdown"))
fireEvent.change(dropdown, {
target: { value: "config1" },
})
const enhanceOption = await waitFor(() => screen.getByTestId("ENHANCE-option"))
fireEvent.click(enhanceOption)

expect(mockExtensionState.setEnhancementApiConfigId).toHaveBeenCalledWith("config1")
expect(vscode.postMessage).toHaveBeenCalledWith({
type: "enhancementApiConfigId",
text: "config1",
})
const apiConfig = await waitFor(() => screen.getByTestId("api-config-select"))
fireEvent.click(apiConfig)

const config1 = await waitFor(() => screen.getByTestId("config1-option"))
fireEvent.click(config1)

expect(mockExtensionState.setEnhancementApiConfigId).toHaveBeenCalledWith("config1") // Ensure this is not called by mode switch
})

it("handles clearing custom instructions correctly", async () => {
Expand Down
12 changes: 6 additions & 6 deletions webview-ui/src/components/settings/AutoApproveSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import { HTMLAttributes, useState } from "react"
import { X } from "lucide-react"

import { useAppTranslation } from "@/i18n/TranslationContext"
import { VSCodeTextField, VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
import { vscode } from "@/utils/vscode"
import { Button, Slider } from "@/components/ui"
import { Button, Input, Slider } from "@/components/ui"

import { SetCachedStateField } from "./types"
import { SectionHeader } from "./SectionHeader"
Expand Down Expand Up @@ -203,9 +203,9 @@ export const AutoApproveSettings = ({
</div>

<div className="flex gap-2">
<VSCodeTextField
<Input
value={commandInput}
onInput={(e: any) => setCommandInput(e.target.value)}
onChange={(e: any) => setCommandInput(e.target.value)}
onKeyDown={(e: any) => {
if (e.key === "Enter") {
e.preventDefault()
Expand All @@ -216,7 +216,7 @@ export const AutoApproveSettings = ({
className="grow"
data-testid="command-input"
/>
<Button onClick={handleAddCommand} data-testid="add-command-button">
<Button className="h-8" onClick={handleAddCommand} data-testid="add-command-button">
{t("settings:autoApprove.execute.addButton")}
</Button>
</div>
Expand All @@ -234,7 +234,7 @@ export const AutoApproveSettings = ({
}}>
<div className="flex flex-row items-center gap-1">
<div>{cmd}</div>
<X className="text-primary-foreground scale-75" />
<X className="text-foreground scale-75" />
</div>
</Button>
))}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
import { Database } from "lucide-react"

import { cn } from "@/lib/utils"
import { Slider } from "@/components/ui"
import { Input, Slider } from "@/components/ui"

import { SetCachedStateField } from "./types"
import { SectionHeader } from "./SectionHeader"
Expand Down Expand Up @@ -96,7 +96,7 @@ export const ContextManagementSettings = ({
<div className="flex flex-col gap-2">
<span className="font-medium">{t("settings:contextManagement.maxReadFile.label")}</span>
<div className="flex items-center gap-4">
<input
<Input
type="number"
pattern="-?[0-9]*"
className="w-24 bg-vscode-input-background text-vscode-input-foreground border border-vscode-input-border px-2 py-1 rounded text-right [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none disabled:opacity-50"
Expand Down
4 changes: 2 additions & 2 deletions webview-ui/src/components/ui/alert-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ function AlertDialogAction({ className, ...props }: React.ComponentProps<typeof
<AlertDialogPrimitive.Action
className={cn(
buttonVariants(),
"bg-vscode-button-background text-vscode-button-foreground hover:bg-vscode-button-hoverBackground border border-transparent h-6 px-3 py-1",
"bg-vscode-button-background text-vscode-button-foreground hover:bg-vscode-button-hoverBackground h-6 px-3 py-1 border",
className,
)}
{...props}
Expand All @@ -103,7 +103,7 @@ function AlertDialogCancel({ className, ...props }: React.ComponentProps<typeof
<AlertDialogPrimitive.Cancel
className={cn(
buttonVariants({ variant: "outline" }),
"bg-vscode-button-secondaryBackground text-vscode-button-secondaryForeground hover:bg-vscode-button-secondaryHoverBackground border border-vscode-button-border h-6 px-3 py-1",
"bg-vscode-button-secondaryBackground text-vscode-button-secondaryForeground hover:bg-vscode-button-secondaryHoverBackground h-6 px-3 py-1 border",
className,
)}
{...props}
Expand Down
9 changes: 4 additions & 5 deletions webview-ui/src/components/ui/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,12 @@ const buttonVariants = cva(
{
variants: {
variant: {
default:
"border border-vscode-input-border bg-primary text-primary-foreground shadow hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
default: "border border-vscode-input-border bg-primary text-primary-foreground hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-vscode-input-border bg-transparent shadow-sm hover:bg-accent hover:text-accent-foreground",
"border border-vscode-input-border bg-transparent hover:bg-accent hover:text-accent-foreground",
secondary:
"border border-vscode-input-border bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
"border border-vscode-input-border bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
combobox:
Expand Down
4 changes: 2 additions & 2 deletions webview-ui/src/components/ui/slider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ const Slider = React.forwardRef<
ref={ref}
className={cn("relative flex w-full touch-none select-none items-center", className)}
{...props}>
<SliderPrimitive.Track className="relative w-full h-[8px] grow overflow-hidden bg-accent border border-[#767676] dark:border-[#858585] rounded-sm">
<SliderPrimitive.Track className="relative w-full h-[8px] grow overflow-hidden bg-accent rounded-sm border">
<SliderPrimitive.Range className="absolute h-full bg-vscode-button-background" />
</SliderPrimitive.Track>
<SliderPrimitive.Thumb className="block h-3 w-3 rounded-full border border-primary/50 bg-primary shadow transition-colors cursor-pointer focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50" />
<SliderPrimitive.Thumb className="block h-3 w-3 rounded-full border border-primary/50 bg-vscode-button-background transition-colors cursor-pointer focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50" />
</SliderPrimitive.Root>
))
Slider.displayName = SliderPrimitive.Root.displayName
Expand Down
4 changes: 2 additions & 2 deletions webview-ui/src/components/ui/textarea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, React.ComponentProps<"tex
return (
<textarea
className={cn(
"flex min-h-[60px] w-full rounded-xs px-3 py-2 text-base shadow-sm placeholder:text-muted-foreground focus:outline-0 focus-visible:outline-none focus-visible:border-vscode-focusBorder disabled:cursor-not-allowed disabled:opacity-50",
"border-[var(--vscode-input-border,var(--vscode-input-background))] focus-visible:border-vscode-focusBorder",
"flex min-h-[60px] w-full rounded-xs px-3 py-2 text-base placeholder:text-muted-foreground focus:outline-0 focus-visible:outline-none focus-visible:border-vscode-focusBorder disabled:cursor-not-allowed disabled:opacity-50",
"border border-[var(--vscode-input-border,var(--vscode-input-background))] focus-visible:border-vscode-focusBorder",
"bg-vscode-input-background",
"text-vscode-input-foreground",
className,
Expand Down
3 changes: 2 additions & 1 deletion webview-ui/src/i18n/locales/ca/prompts.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"editModesConfig": "Editar configuració de modes",
"editGlobalModes": "Editar modes globals",
"editProjectModes": "Editar modes de projecte (.roomodes)",
"createModeHelpText": "Feu clic a + per crear un nou mode personalitzat, o simplement demaneu a Roo al xat que en creï un per a vostè!"
"createModeHelpText": "Feu clic a + per crear un nou mode personalitzat, o simplement demaneu a Roo al xat que en creï un per a vostè!",
"selectMode": "Cerqueu modes"
},
"apiConfiguration": {
"title": "Configuració d'API",
Expand Down
3 changes: 2 additions & 1 deletion webview-ui/src/i18n/locales/de/prompts.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"editModesConfig": "Moduskonfiguration bearbeiten",
"editGlobalModes": "Globale Modi bearbeiten",
"editProjectModes": "Projektmodi bearbeiten (.roomodes)",
"createModeHelpText": "Klicke auf +, um einen neuen benutzerdefinierten Modus zu erstellen, oder bitte Roo einfach im Chat, einen für dich zu erstellen!"
"createModeHelpText": "Klicke auf +, um einen neuen benutzerdefinierten Modus zu erstellen, oder bitte Roo einfach im Chat, einen für dich zu erstellen!",
"selectMode": "Modi suchen"
},
"apiConfiguration": {
"title": "API-Konfiguration",
Expand Down
3 changes: 2 additions & 1 deletion webview-ui/src/i18n/locales/en/prompts.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"editModesConfig": "Edit modes configuration",
"editGlobalModes": "Edit Global Modes",
"editProjectModes": "Edit Project Modes (.roomodes)",
"createModeHelpText": "Hit the + to create a new custom mode, or just ask Roo in chat to create one for you!"
"createModeHelpText": "Hit the + to create a new custom mode, or just ask Roo in chat to create one for you!",
"selectMode": "Search modes"
},
"apiConfiguration": {
"title": "API Configuration",
Expand Down
3 changes: 2 additions & 1 deletion webview-ui/src/i18n/locales/es/prompts.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"editModesConfig": "Editar configuración de modos",
"editGlobalModes": "Editar modos globales",
"editProjectModes": "Editar modos del proyecto (.roomodes)",
"createModeHelpText": "¡Haz clic en + para crear un nuevo modo personalizado, o simplemente pídele a Roo en el chat que te cree uno!"
"createModeHelpText": "¡Haz clic en + para crear un nuevo modo personalizado, o simplemente pídele a Roo en el chat que te cree uno!",
"selectMode": "Buscar modos"
},
"apiConfiguration": {
"title": "Configuración de API",
Expand Down
3 changes: 2 additions & 1 deletion webview-ui/src/i18n/locales/fr/prompts.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"editModesConfig": "Modifier la configuration des modes",
"editGlobalModes": "Modifier les modes globaux",
"editProjectModes": "Modifier les modes du projet (.roomodes)",
"createModeHelpText": "Cliquez sur + pour créer un nouveau mode personnalisé, ou demandez simplement à Roo dans le chat de vous en créer un !"
"createModeHelpText": "Cliquez sur + pour créer un nouveau mode personnalisé, ou demandez simplement à Roo dans le chat de vous en créer un !",
"selectMode": "Rechercher les modes"
},
"apiConfiguration": {
"title": "Configuration API",
Expand Down
3 changes: 2 additions & 1 deletion webview-ui/src/i18n/locales/hi/prompts.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"editModesConfig": "मोड कॉन्फ़िगरेशन संपादित करें",
"editGlobalModes": "ग्लोबल मोड्स संपादित करें",
"editProjectModes": "प्रोजेक्ट मोड्स संपादित करें (.roomodes)",
"createModeHelpText": "नया कस्टम मोड बनाने के लिए + पर क्लिक करें, या बस चैट में Roo से आपके लिए एक बनाने को कहें!"
"createModeHelpText": "नया कस्टम मोड बनाने के लिए + पर क्लिक करें, या बस चैट में Roo से आपके लिए एक बनाने को कहें!",
"selectMode": "मोड खोजें"
},
"apiConfiguration": {
"title": "API कॉन्फ़िगरेशन",
Expand Down
3 changes: 2 additions & 1 deletion webview-ui/src/i18n/locales/it/prompts.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"editModesConfig": "Modifica configurazione modalità",
"editGlobalModes": "Modifica modalità globali",
"editProjectModes": "Modifica modalità di progetto (.roomodes)",
"createModeHelpText": "Clicca sul + per creare una nuova modalità personalizzata, o chiedi semplicemente a Roo nella chat di crearne una per te!"
"createModeHelpText": "Clicca sul + per creare una nuova modalità personalizzata, o chiedi semplicemente a Roo nella chat di crearne una per te!",
"selectMode": "Cerca modalità"
},
"apiConfiguration": {
"title": "Configurazione API",
Expand Down
3 changes: 2 additions & 1 deletion webview-ui/src/i18n/locales/ja/prompts.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"editModesConfig": "モード設定を編集",
"editGlobalModes": "グローバルモードを編集",
"editProjectModes": "プロジェクトモードを編集 (.roomodes)",
"createModeHelpText": "+ をクリックして新しいカスタムモードを作成するか、チャットで Roo に作成を依頼してください!"
"createModeHelpText": "+ をクリックして新しいカスタムモードを作成するか、チャットで Roo に作成を依頼してください!",
"selectMode": "モードを検索"
},
"apiConfiguration": {
"title": "API設定",
Expand Down
3 changes: 2 additions & 1 deletion webview-ui/src/i18n/locales/ko/prompts.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"editModesConfig": "모드 구성 편집",
"editGlobalModes": "전역 모드 편집",
"editProjectModes": "프로젝트 모드 편집 (.roomodes)",
"createModeHelpText": "새 커스텀 모드를 만들려면 + 버튼을 클릭하거나, 채팅에서 Roo에게 만들어달라고 요청하세요!"
"createModeHelpText": "새 커스텀 모드를 만들려면 + 버튼을 클릭하거나, 채팅에서 Roo에게 만들어달라고 요청하세요!",
"selectMode": "모드 검색"
},
"apiConfiguration": {
"title": "API 구성",
Expand Down
Loading
Loading