diff --git a/packages/types/src/provider-settings.ts b/packages/types/src/provider-settings.ts index dc51188df9..e2e75d6483 100644 --- a/packages/types/src/provider-settings.ts +++ b/packages/types/src/provider-settings.ts @@ -132,6 +132,8 @@ const vertexSchema = apiModelIdProviderModelSchema.extend({ vertexJsonCredentials: z.string().optional(), vertexProjectId: z.string().optional(), vertexRegion: z.string().optional(), + enableUrlContext: z.boolean().optional(), + enableGrounding: z.boolean().optional(), }) const openAiSchema = baseProviderSettingsSchema.extend({ diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index 204abe9c0f..35a6cd9684 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -466,7 +466,11 @@ const ApiOptions = ({ )} {selectedProvider === "vertex" && ( - + )} {selectedProvider === "gemini" && ( diff --git a/webview-ui/src/components/settings/providers/Vertex.tsx b/webview-ui/src/components/settings/providers/Vertex.tsx index 19a136927a..1a57f4fa5e 100644 --- a/webview-ui/src/components/settings/providers/Vertex.tsx +++ b/webview-ui/src/components/settings/providers/Vertex.tsx @@ -1,4 +1,5 @@ import { useCallback } from "react" +import { Checkbox } from "vscrui" import { VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react" import { type ProviderSettings, VERTEX_REGIONS } from "@roo-code/types" @@ -11,9 +12,10 @@ import { inputEventTransform } from "../transforms" type VertexProps = { apiConfiguration: ProviderSettings setApiConfigurationField: (field: keyof ProviderSettings, value: ProviderSettings[keyof ProviderSettings]) => void + fromWelcomeView?: boolean } -export const Vertex = ({ apiConfiguration, setApiConfigurationField }: VertexProps) => { +export const Vertex = ({ apiConfiguration, setApiConfigurationField, fromWelcomeView }: VertexProps) => { const { t } = useAppTranslation() const handleInputChange = useCallback( @@ -91,6 +93,30 @@ export const Vertex = ({ apiConfiguration, setApiConfigurationField }: VertexPro + + {!fromWelcomeView && apiConfiguration.apiModelId?.startsWith("gemini") && ( +
+ setApiConfigurationField("enableUrlContext", checked)}> + {t("settings:providers.geminiParameters.urlContext.title")} + +
+ {t("settings:providers.geminiParameters.urlContext.description")} +
+ + setApiConfigurationField("enableGrounding", checked)}> + {t("settings:providers.geminiParameters.groundingSearch.title")} + +
+ {t("settings:providers.geminiParameters.groundingSearch.description")} +
+
+ )} ) } diff --git a/webview-ui/src/components/settings/providers/__tests__/Vertex.spec.tsx b/webview-ui/src/components/settings/providers/__tests__/Vertex.spec.tsx index 4ef949f9e0..4abe1e8847 100644 --- a/webview-ui/src/components/settings/providers/__tests__/Vertex.spec.tsx +++ b/webview-ui/src/components/settings/providers/__tests__/Vertex.spec.tsx @@ -1,61 +1,284 @@ -// Tests for VERTEX_REGIONS "global" region handling +import { render, screen } from "@testing-library/react" +import userEvent from "@testing-library/user-event" +import { Vertex } from "../Vertex" +import type { ProviderSettings } from "@roo-code/types" +import { VERTEX_REGIONS } from "@roo-code/types" -import { describe, it, expect } from "vitest" -import { VERTEX_REGIONS } from "../../../../../../packages/types/src/providers/vertex" +vi.mock("@vscode/webview-ui-toolkit/react", () => ({ + VSCodeTextField: ({ children, value, onInput, type }: any) => ( +
+ {children} + onInput(e)} /> +
+ ), + VSCodeLink: ({ children, href }: any) => {children}, +})) -describe("VERTEX_REGIONS", () => { - it('should include the "global" region as the first entry', () => { - expect(VERTEX_REGIONS[0]).toEqual({ value: "global", label: "global" }) +vi.mock("vscrui", () => ({ + Checkbox: ({ children, checked, onChange, "data-testid": testId }: any) => ( + + ), +})) + +vi.mock("@src/i18n/TranslationContext", () => ({ + useAppTranslation: () => ({ t: (key: string) => key }), +})) + +vi.mock("@src/components/ui", () => ({ + Select: ({ children, value, onValueChange }: any) => ( +
+ {children} +
+ ), + SelectContent: ({ children }: any) =>
{children}
, + SelectItem: ({ children, value }: any) =>
{children}
, + SelectTrigger: ({ children }: any) =>
{children}
, + SelectValue: ({ placeholder }: any) =>
{placeholder}
, +})) + +describe("Vertex", () => { + const defaultApiConfiguration: ProviderSettings = { + vertexKeyFile: "", + vertexJsonCredentials: "", + vertexProjectId: "", + vertexRegion: "", + enableUrlContext: false, + enableGrounding: false, + apiModelId: "gemini-2.0-flash-001", + } + + const mockSetApiConfigurationField = vi.fn() + + beforeEach(() => { + vi.clearAllMocks() }) - it('should contain "global" region exactly once', () => { - const globalRegions = VERTEX_REGIONS.filter((r: { value: string; label: string }) => r.value === "global") - expect(globalRegions).toHaveLength(1) + describe("VERTEX_REGIONS", () => { + it('should include the "global" region as the first entry', () => { + expect(VERTEX_REGIONS[0]).toEqual({ value: "global", label: "global" }) + }) + + it('should contain "global" region exactly once', () => { + const globalRegions = VERTEX_REGIONS.filter((r: { value: string; label: string }) => r.value === "global") + expect(globalRegions).toHaveLength(1) + }) + + it('should contain all expected regions including "global"', () => { + // The expected list is the imported VERTEX_REGIONS itself + expect(VERTEX_REGIONS).toEqual([ + { value: "global", label: "global" }, + { value: "us-central1", label: "us-central1" }, + { value: "us-east1", label: "us-east1" }, + { value: "us-east4", label: "us-east4" }, + { value: "us-east5", label: "us-east5" }, + { value: "us-west1", label: "us-west1" }, + { value: "us-west2", label: "us-west2" }, + { value: "us-west3", label: "us-west3" }, + { value: "us-west4", label: "us-west4" }, + { value: "northamerica-northeast1", label: "northamerica-northeast1" }, + { value: "northamerica-northeast2", label: "northamerica-northeast2" }, + { value: "southamerica-east1", label: "southamerica-east1" }, + { value: "europe-west1", label: "europe-west1" }, + { value: "europe-west2", label: "europe-west2" }, + { value: "europe-west3", label: "europe-west3" }, + { value: "europe-west4", label: "europe-west4" }, + { value: "europe-west6", label: "europe-west6" }, + { value: "europe-central2", label: "europe-central2" }, + { value: "asia-east1", label: "asia-east1" }, + { value: "asia-east2", label: "asia-east2" }, + { value: "asia-northeast1", label: "asia-northeast1" }, + { value: "asia-northeast2", label: "asia-northeast2" }, + { value: "asia-northeast3", label: "asia-northeast3" }, + { value: "asia-south1", label: "asia-south1" }, + { value: "asia-south2", label: "asia-south2" }, + { value: "asia-southeast1", label: "asia-southeast1" }, + { value: "asia-southeast2", label: "asia-southeast2" }, + { value: "australia-southeast1", label: "australia-southeast1" }, + { value: "australia-southeast2", label: "australia-southeast2" }, + { value: "me-west1", label: "me-west1" }, + { value: "me-central1", label: "me-central1" }, + { value: "africa-south1", label: "africa-south1" }, + ]) + }) + + it('should contain "asia-east1" region exactly once', () => { + const asiaEast1Regions = VERTEX_REGIONS.filter( + (r: { value: string; label: string }) => r.value === "asia-east1" && r.label === "asia-east1", + ) + expect(asiaEast1Regions).toHaveLength(1) + expect(asiaEast1Regions[0]).toEqual({ value: "asia-east1", label: "asia-east1" }) + }) }) - it('should contain all expected regions including "global"', () => { - // The expected list is the imported VERTEX_REGIONS itself - expect(VERTEX_REGIONS).toEqual([ - { value: "global", label: "global" }, - { value: "us-central1", label: "us-central1" }, - { value: "us-east1", label: "us-east1" }, - { value: "us-east4", label: "us-east4" }, - { value: "us-east5", label: "us-east5" }, - { value: "us-west1", label: "us-west1" }, - { value: "us-west2", label: "us-west2" }, - { value: "us-west3", label: "us-west3" }, - { value: "us-west4", label: "us-west4" }, - { value: "northamerica-northeast1", label: "northamerica-northeast1" }, - { value: "northamerica-northeast2", label: "northamerica-northeast2" }, - { value: "southamerica-east1", label: "southamerica-east1" }, - { value: "europe-west1", label: "europe-west1" }, - { value: "europe-west2", label: "europe-west2" }, - { value: "europe-west3", label: "europe-west3" }, - { value: "europe-west4", label: "europe-west4" }, - { value: "europe-west6", label: "europe-west6" }, - { value: "europe-central2", label: "europe-central2" }, - { value: "asia-east1", label: "asia-east1" }, - { value: "asia-east2", label: "asia-east2" }, - { value: "asia-northeast1", label: "asia-northeast1" }, - { value: "asia-northeast2", label: "asia-northeast2" }, - { value: "asia-northeast3", label: "asia-northeast3" }, - { value: "asia-south1", label: "asia-south1" }, - { value: "asia-south2", label: "asia-south2" }, - { value: "asia-southeast1", label: "asia-southeast1" }, - { value: "asia-southeast2", label: "asia-southeast2" }, - { value: "australia-southeast1", label: "australia-southeast1" }, - { value: "australia-southeast2", label: "australia-southeast2" }, - { value: "me-west1", label: "me-west1" }, - { value: "me-central1", label: "me-central1" }, - { value: "africa-south1", label: "africa-south1" }, - ]) + describe("URL Context Checkbox", () => { + it("should render URL context checkbox unchecked by default for Gemini models", () => { + render( + , + ) + + const urlContextCheckbox = screen.getByTestId("checkbox-url-context") + const checkbox = urlContextCheckbox.querySelector("input[type='checkbox']") as HTMLInputElement + expect(checkbox.checked).toBe(false) + }) + + it("should NOT render URL context checkbox for non-Gemini models", () => { + const apiConfiguration = { ...defaultApiConfiguration, apiModelId: "claude-3-opus@20240229" } + render( + , + ) + + const urlContextCheckbox = screen.queryByTestId("checkbox-url-context") + expect(urlContextCheckbox).toBeNull() + }) + + it("should NOT render URL context checkbox when fromWelcomeView is true", () => { + render( + , + ) + + const urlContextCheckbox = screen.queryByTestId("checkbox-url-context") + expect(urlContextCheckbox).toBeNull() + }) + + it("should render URL context checkbox checked when enableUrlContext is true for Gemini models", () => { + const apiConfiguration = { + ...defaultApiConfiguration, + enableUrlContext: true, + apiModelId: "gemini-2.0-flash-001", + } + render( + , + ) + + const urlContextCheckbox = screen.getByTestId("checkbox-url-context") + const checkbox = urlContextCheckbox.querySelector("input[type='checkbox']") as HTMLInputElement + expect(checkbox.checked).toBe(true) + }) + + it("should call setApiConfigurationField with correct parameters when URL context checkbox is toggled", async () => { + const user = userEvent.setup() + render( + , + ) + + const urlContextCheckbox = screen.getByTestId("checkbox-url-context") + const checkbox = urlContextCheckbox.querySelector("input[type='checkbox']") as HTMLInputElement + + await user.click(checkbox) + + expect(mockSetApiConfigurationField).toHaveBeenCalledWith("enableUrlContext", true) + }) }) - it('should contain "asia-east1" region exactly once', () => { - const asiaEast1Regions = VERTEX_REGIONS.filter( - (r: { value: string; label: string }) => r.value === "asia-east1" && r.label === "asia-east1", - ) - expect(asiaEast1Regions).toHaveLength(1) - expect(asiaEast1Regions[0]).toEqual({ value: "asia-east1", label: "asia-east1" }) + describe("Grounding with Google Search Checkbox", () => { + it("should render grounding search checkbox unchecked by default for Gemini models", () => { + render( + , + ) + + const groundingCheckbox = screen.getByTestId("checkbox-grounding-search") + const checkbox = groundingCheckbox.querySelector("input[type='checkbox']") as HTMLInputElement + expect(checkbox.checked).toBe(false) + }) + + it("should NOT render grounding search checkbox for non-Gemini models", () => { + const apiConfiguration = { ...defaultApiConfiguration, apiModelId: "claude-3-opus@20240229" } + render( + , + ) + + const groundingCheckbox = screen.queryByTestId("checkbox-grounding-search") + expect(groundingCheckbox).toBeNull() + }) + + it("should NOT render grounding search checkbox when fromWelcomeView is true", () => { + render( + , + ) + + const groundingCheckbox = screen.queryByTestId("checkbox-grounding-search") + expect(groundingCheckbox).toBeNull() + }) + + it("should render grounding search checkbox checked when enableGrounding is true for Gemini models", () => { + const apiConfiguration = { + ...defaultApiConfiguration, + enableGrounding: true, + apiModelId: "gemini-2.0-flash-001", + } + render( + , + ) + + const groundingCheckbox = screen.getByTestId("checkbox-grounding-search") + const checkbox = groundingCheckbox.querySelector("input[type='checkbox']") as HTMLInputElement + expect(checkbox.checked).toBe(true) + }) + + it("should call setApiConfigurationField with correct parameters when grounding search checkbox is toggled", async () => { + const user = userEvent.setup() + render( + , + ) + + const groundingCheckbox = screen.getByTestId("checkbox-grounding-search") + const checkbox = groundingCheckbox.querySelector("input[type='checkbox']") as HTMLInputElement + + await user.click(checkbox) + + expect(mockSetApiConfigurationField).toHaveBeenCalledWith("enableGrounding", true) + }) + }) + + describe("Both checkboxes interaction", () => { + it("should be able to toggle both checkboxes independently", async () => { + const user = userEvent.setup() + render( + , + ) + + const urlContextCheckbox = screen.getByTestId("checkbox-url-context") + const urlCheckbox = urlContextCheckbox.querySelector("input[type='checkbox']") as HTMLInputElement + + const groundingCheckbox = screen.getByTestId("checkbox-grounding-search") + const groundCheckbox = groundingCheckbox.querySelector("input[type='checkbox']") as HTMLInputElement + + // Toggle URL context + await user.click(urlCheckbox) + expect(mockSetApiConfigurationField).toHaveBeenCalledWith("enableUrlContext", true) + + // Toggle grounding + await user.click(groundCheckbox) + expect(mockSetApiConfigurationField).toHaveBeenCalledWith("enableGrounding", true) + + // Both should have been called + expect(mockSetApiConfigurationField).toHaveBeenCalledTimes(2) + }) }) })