Skip to content

Commit adedf0a

Browse files
James Mtendamemacursoragent
andcommitted
feat(zoo-gateway): add settings UI, validation, and i18n
Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent f776483 commit adedf0a

30 files changed

Lines changed: 407 additions & 77 deletions

webview-ui/package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
"fzf": "^0.5.2",
4949
"hast-util-to-jsx-runtime": "^2.3.6",
5050
"i18next": "^25.0.0",
51+
"i18next-http-backend": "^3.0.2",
5152
"katex": "^0.16.11",
5253
"knuth-shuffle-seeded": "^1.0.6",
5354
"lru-cache": "^11.1.0",
@@ -93,19 +94,19 @@
9394
"@types/diff": "^5.2.1",
9495
"@types/jest": "^29.0.0",
9596
"@types/katex": "^0.16.7",
96-
"@types/node": "^20.19.25",
97+
"@types/node": "20.x",
9798
"@types/react": "^18.3.23",
9899
"@types/react-dom": "^18.3.5",
99100
"@types/shell-quote": "^1.7.5",
100101
"@types/stacktrace-js": "^2.0.3",
101102
"@types/vscode-webview": "^1.57.5",
102-
"@vitejs/plugin-react": "^5.2.0",
103+
"@vitejs/plugin-react": "^4.3.4",
103104
"@vitest/coverage-v8": "^3.2.3",
104105
"@vitest/ui": "^3.2.3",
105106
"babel-plugin-react-compiler": "^1.0.0",
106107
"identity-obj-proxy": "^3.0.0",
107108
"jsdom": "^26.0.0",
108-
"vite": "^8.0.13",
109+
"vite": "6.3.6",
109110
"vitest": "^3.2.3"
110111
}
111112
}

webview-ui/src/components/settings/ApiOptions.tsx

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {
3030
mainlandZAiDefaultModelId,
3131
fireworksDefaultModelId,
3232
vercelAiGatewayDefaultModelId,
33+
zooGatewayDefaultModelId,
3334
minimaxDefaultModelId,
3435
mimoDefaultModelId,
3536
unboundDefaultModelId,
@@ -93,6 +94,7 @@ import {
9394
ZAi,
9495
Fireworks,
9596
VercelAiGateway,
97+
ZooGateway,
9698
MiniMax,
9799
Mimo,
98100
} from "./providers"
@@ -133,7 +135,7 @@ const ApiOptions = ({
133135
setErrorMessage,
134136
}: ApiOptionsProps) => {
135137
const { t } = useAppTranslation()
136-
const { organizationAllowList, openAiCodexIsAuthenticated } = useExtensionState()
138+
const { organizationAllowList, openAiCodexIsAuthenticated, zooCodeIsAuthenticated } = useExtensionState()
137139

138140
const [customHeaders, setCustomHeaders] = useState<[string, string][]>(() => {
139141
const headers = apiConfiguration?.openAiHeaders || {}
@@ -270,9 +272,17 @@ const ApiOptions = ({
270272
apiConfiguration,
271273
routerModels,
272274
organizationAllowList,
275+
zooCodeIsAuthenticated,
273276
)
274277
setErrorMessage(apiValidationResult)
275-
}, [apiConfiguration, routerModels, organizationAllowList, setErrorMessage, isRetiredSelectedProvider])
278+
}, [
279+
apiConfiguration,
280+
routerModels,
281+
organizationAllowList,
282+
setErrorMessage,
283+
isRetiredSelectedProvider,
284+
zooCodeIsAuthenticated,
285+
])
276286

277287
const onProviderChange = useCallback(
278288
(value: ProviderName) => {
@@ -363,6 +373,7 @@ const ApiOptions = ({
363373
fireworks: { field: "apiModelId", default: fireworksDefaultModelId },
364374
poe: { field: "apiModelId", default: poeDefaultModelId },
365375
"vercel-ai-gateway": { field: "vercelAiGatewayModelId", default: vercelAiGatewayDefaultModelId },
376+
"zoo-gateway": { field: "zooGatewayModelId", default: zooGatewayDefaultModelId },
366377
openai: { field: "openAiModelId" },
367378
ollama: { field: "ollamaModelId" },
368379
lmstudio: { field: "lmStudioModelId" },
@@ -688,6 +699,17 @@ const ApiOptions = ({
688699
/>
689700
)}
690701

702+
{selectedProvider === "zoo-gateway" && (
703+
<ZooGateway
704+
apiConfiguration={apiConfiguration}
705+
setApiConfigurationField={setApiConfigurationField}
706+
routerModels={routerModels}
707+
organizationAllowList={organizationAllowList}
708+
modelValidationError={modelValidationError}
709+
simplifySettings={fromWelcomeView}
710+
/>
711+
)}
712+
691713
{selectedProvider === "fireworks" && (
692714
<Fireworks
693715
apiConfiguration={apiConfiguration}

webview-ui/src/components/settings/ModelPicker.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ type ModelIdKey = keyof Pick<
3434
| "openAiModelId"
3535
| "litellmModelId"
3636
| "vercelAiGatewayModelId"
37+
| "zooGatewayModelId"
3738
| "apiModelId"
3839
| "ollamaModelId"
3940
| "lmStudioModelId"

webview-ui/src/components/settings/__tests__/ApiOptions.spec.tsx

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { render, screen, fireEvent, within } from "@/utils/test-utils"
44
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
55

66
import { type ModelInfo, type ProviderSettings, openAiModelInfoSaneDefaults } from "@roo-code/types"
7-
import { openAiCodexDefaultModelId } from "@roo-code/types"
7+
import { openAiCodexDefaultModelId, zooGatewayDefaultModelId } from "@roo-code/types"
88

99
import * as ExtensionStateContext from "@src/context/ExtensionStateContext"
1010
const { ExtensionStateContextProvider } = ExtensionStateContext
@@ -300,6 +300,28 @@ describe("ApiOptions", () => {
300300
expect(mockSetApiConfigurationField).toHaveBeenCalledWith("apiModelId", openAiCodexDefaultModelId, false)
301301
})
302302

303+
it("initializes zooGatewayModelId to its default when switching provider to zoo-gateway", () => {
304+
// Regression: zoo-gateway was previously missing from PROVIDER_MODEL_CONFIG, so switching
305+
// providers never seeded zooGatewayModelId. Configs were left without a model id, which
306+
// blocked completion flows that require a dynamic-provider model id.
307+
const mockSetApiConfigurationField = vi.fn()
308+
309+
renderApiOptions({
310+
apiConfiguration: {
311+
apiProvider: "anthropic",
312+
// No prior zooGatewayModelId.
313+
},
314+
setApiConfigurationField: mockSetApiConfigurationField,
315+
})
316+
317+
const providerSelectContainer = screen.getByTestId("provider-select")
318+
const providerSelect = providerSelectContainer.querySelector("select") as HTMLSelectElement
319+
fireEvent.change(providerSelect, { target: { value: "zoo-gateway" } })
320+
321+
expect(mockSetApiConfigurationField).toHaveBeenCalledWith("apiProvider", "zoo-gateway")
322+
expect(mockSetApiConfigurationField).toHaveBeenCalledWith("zooGatewayModelId", zooGatewayDefaultModelId, false)
323+
})
324+
303325
it("shows temperature and rate limit controls by default", () => {
304326
renderApiOptions({
305327
apiConfiguration: {},

webview-ui/src/components/settings/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ export const PROVIDERS = [
6363
{ value: "zai", label: "Z.ai", proxy: false },
6464
{ value: "fireworks", label: "Fireworks AI", proxy: false },
6565
{ value: "vercel-ai-gateway", label: "Vercel AI Gateway", proxy: false },
66+
{ value: "zoo-gateway", label: "Zoo Gateway", proxy: false },
6667
{ value: "minimax", label: "MiniMax", proxy: false },
6768
{ value: "mimo", label: "Xiaomi MiMo", proxy: false },
6869
{ value: "baseten", label: "Baseten", proxy: false },
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import {
2+
type ProviderSettings,
3+
type OrganizationAllowList,
4+
type RouterModels,
5+
zooGatewayDefaultModelId,
6+
} from "@roo-code/types"
7+
8+
import { useExtensionState } from "@src/context/ExtensionStateContext"
9+
import { getZooCodeAuthUrl } from "@src/oauth/urls"
10+
import { useAppTranslation } from "@src/i18n/TranslationContext"
11+
12+
import { ModelPicker } from "../ModelPicker"
13+
14+
type ZooGatewayProps = {
15+
apiConfiguration: ProviderSettings
16+
setApiConfigurationField: (field: keyof ProviderSettings, value: ProviderSettings[keyof ProviderSettings]) => void
17+
routerModels?: RouterModels
18+
organizationAllowList: OrganizationAllowList
19+
modelValidationError?: string
20+
simplifySettings?: boolean
21+
}
22+
23+
export const ZooGateway = ({
24+
apiConfiguration,
25+
setApiConfigurationField,
26+
routerModels,
27+
organizationAllowList,
28+
modelValidationError,
29+
simplifySettings,
30+
}: ZooGatewayProps) => {
31+
const { t } = useAppTranslation()
32+
const { zooCodeIsAuthenticated, zooCodeUserEmail, zooCodeUserName, zooCodeBaseUrl, uriScheme, deviceName } =
33+
useExtensionState()
34+
35+
const authUrl = getZooCodeAuthUrl(uriScheme, zooCodeBaseUrl, deviceName)
36+
37+
return (
38+
<>
39+
{/* Zoo Code Authentication Section */}
40+
<div className="flex flex-col gap-1 rounded-md border border-vscode-panel-border p-2">
41+
<div className="flex items-center justify-between">
42+
<label className="block text-sm font-medium">{t("settings:providers.zooGateway.account")}</label>
43+
{zooCodeIsAuthenticated && zooCodeUserEmail && (
44+
<span className="text-xs text-vscode-descriptionForeground">{zooCodeUserEmail}</span>
45+
)}
46+
</div>
47+
{!zooCodeIsAuthenticated ? (
48+
<div className="flex flex-col gap-1">
49+
<p className="text-xs text-vscode-descriptionForeground">
50+
{t("settings:providers.zooGateway.signInDescription")}
51+
</p>
52+
<a
53+
href={authUrl}
54+
className="inline-flex w-fit items-center rounded-sm bg-vscode-button-background px-3 py-1 text-xs text-vscode-button-foreground no-underline hover:bg-vscode-button-hoverBackground">
55+
{t("settings:providers.zooGateway.signInButton")}
56+
</a>
57+
</div>
58+
) : (
59+
<div className="flex items-center gap-1">
60+
<span className="codicon codicon-check text-vscode-charts-green" />
61+
<span className="text-xs text-vscode-descriptionForeground">
62+
{zooCodeUserName
63+
? t("settings:providers.zooGateway.authenticatedAs", { name: zooCodeUserName })
64+
: t("settings:providers.zooGateway.authenticated")}
65+
</span>
66+
</div>
67+
)}
68+
</div>
69+
<ModelPicker
70+
apiConfiguration={apiConfiguration}
71+
setApiConfigurationField={setApiConfigurationField}
72+
defaultModelId={zooGatewayDefaultModelId}
73+
models={routerModels?.["zoo-gateway"] ?? {}}
74+
modelIdKey="zooGatewayModelId"
75+
serviceName="Zoo Gateway"
76+
serviceUrl="https://www.zoocode.dev/dashboard/models"
77+
organizationAllowList={organizationAllowList}
78+
errorMessage={modelValidationError}
79+
simplifySettings={simplifySettings}
80+
/>
81+
</>
82+
)
83+
}

webview-ui/src/components/settings/providers/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export { ZAi } from "./ZAi"
2222
export { LiteLLM } from "./LiteLLM"
2323
export { Fireworks } from "./Fireworks"
2424
export { VercelAiGateway } from "./VercelAiGateway"
25+
export { ZooGateway } from "./ZooGateway"
2526
export { MiniMax } from "./MiniMax"
2627
export { Mimo } from "./Mimo"
2728
export { Baseten } from "./Baseten"

webview-ui/src/components/ui/hooks/useSelectedModel.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,15 @@ function getSelectedModel({
346346
const info = routerModels["vercel-ai-gateway"]?.[id]
347347
return { id, info }
348348
}
349+
case "zoo-gateway": {
350+
const id = getValidatedModelId(
351+
apiConfiguration.zooGatewayModelId,
352+
routerModels["zoo-gateway"],
353+
defaultModelId,
354+
)
355+
const info = routerModels["zoo-gateway"]?.[id]
356+
return { id, info }
357+
}
349358
// case "anthropic":
350359
// case "fake-ai":
351360
default: {

webview-ui/src/components/welcome/WelcomeViewProvider.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ const getWelcomeApiConfiguration = (apiConfiguration?: ProviderSettings): Provid
3636
}
3737

3838
const WelcomeViewProvider = () => {
39-
const { apiConfiguration, currentApiConfigName, setApiConfiguration, uriScheme } = useExtensionState()
39+
const { apiConfiguration, currentApiConfigName, setApiConfiguration, uriScheme, zooCodeIsAuthenticated } =
40+
useExtensionState()
4041
const { t } = useAppTranslation()
4142
const [errorMessage, setErrorMessage] = useState<string | undefined>(undefined)
4243
const [showProviderSetup, setShowProviderSetup] = useState(false)
@@ -65,7 +66,7 @@ const WelcomeViewProvider = () => {
6566
return
6667
}
6768

68-
const error = validateApiConfiguration(effectiveApiConfiguration)
69+
const error = validateApiConfiguration(effectiveApiConfiguration, undefined, undefined, zooCodeIsAuthenticated)
6970

7071
if (error) {
7172
setErrorMessage(error)
@@ -78,7 +79,14 @@ const WelcomeViewProvider = () => {
7879
text: currentApiConfigName,
7980
apiConfiguration: effectiveApiConfiguration,
8081
})
81-
}, [showProviderSetup, apiConfiguration, setApiConfiguration, effectiveApiConfiguration, currentApiConfigName])
82+
}, [
83+
showProviderSetup,
84+
apiConfiguration,
85+
setApiConfiguration,
86+
effectiveApiConfiguration,
87+
currentApiConfigName,
88+
zooCodeIsAuthenticated,
89+
])
8290

8391
if (!showProviderSetup) {
8492
return (

webview-ui/src/i18n/locales/ca/settings.json

Lines changed: 10 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)