Skip to content

Commit babbe9e

Browse files
refactor(zoo-gateway): localize auth-state UX to provider component
Move the zoo-gateway sign-in error out of the shared form-validation effect in ApiOptions and into ZooGateway.tsx, where it renders inline via ApiErrorMessage. ApiOptions can then drop zooCodeIsAuthenticated from its useEffect dependency list, and validateApiConfigurationExcludingModelErrors short-circuits zoo-gateway entirely. Also rename the inline isSonnet45ModelId helper to isClaudeSonnetModelId and let pickZooGatewayDefaultModelId express the version-priority order directly, so the helper has no version baked into its name. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 3431fcc commit babbe9e

5 files changed

Lines changed: 134 additions & 80 deletions

File tree

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

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ const ApiOptions = ({
137137
setErrorMessage,
138138
}: ApiOptionsProps) => {
139139
const { t } = useAppTranslation()
140-
const { organizationAllowList, openAiCodexIsAuthenticated, zooCodeIsAuthenticated } = useExtensionState()
140+
const { organizationAllowList, openAiCodexIsAuthenticated } = useExtensionState()
141141

142142
const [customHeaders, setCustomHeaders] = useState<[string, string][]>(() => {
143143
const headers = apiConfiguration?.openAiHeaders || {}
@@ -270,21 +270,21 @@ const ApiOptions = ({
270270
return
271271
}
272272

273+
// Zoo Gateway renders its own auth-state error inline (sign-in card in
274+
// ZooGateway.tsx) so it can react to zooCodeIsAuthenticated changes
275+
// without re-running this effect or threading auth state through validation.
276+
if (apiConfiguration.apiProvider === "zoo-gateway") {
277+
setErrorMessage(undefined)
278+
return
279+
}
280+
273281
const apiValidationResult = validateApiConfigurationExcludingModelErrors(
274282
apiConfiguration,
275283
routerModels,
276284
organizationAllowList,
277-
zooCodeIsAuthenticated,
278285
)
279286
setErrorMessage(apiValidationResult)
280-
}, [
281-
apiConfiguration,
282-
routerModels,
283-
organizationAllowList,
284-
setErrorMessage,
285-
isRetiredSelectedProvider,
286-
zooCodeIsAuthenticated,
287-
])
287+
}, [apiConfiguration, routerModels, organizationAllowList, setErrorMessage, isRetiredSelectedProvider])
288288

289289
const onProviderChange = useCallback(
290290
(value: ProviderName) => {

webview-ui/src/components/settings/providers/ZooGateway.tsx

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { useAppTranslation } from "@src/i18n/TranslationContext"
1212
import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"
1313

1414
import { ModelPicker } from "../ModelPicker"
15+
import { ApiErrorMessage } from "../ApiErrorMessage"
1516

1617
type ZooGatewayProps = {
1718
apiConfiguration: ProviderSettings
@@ -22,31 +23,29 @@ type ZooGatewayProps = {
2223
simplifySettings?: boolean
2324
}
2425

25-
function isSonnet45ModelId(id: string) {
26-
return /sonnet-4[.-]5|sonnet-4\.5/i.test(id)
26+
function isClaudeSonnetModelId(id: string) {
27+
return /claude.*sonnet/i.test(id)
2728
}
2829

29-
// Exported for unit tests.
30+
// Exported for unit tests. Picks the default Zoo Gateway model id, preferring
31+
// Claude Sonnet 4.5 → Sonnet 4 → first available Sonnet → first model overall.
3032
export function pickZooGatewayDefaultModelId(modelIds: string[]) {
3133
if (modelIds.length === 0) {
3234
return zooGatewayDefaultModelId
3335
}
3436

35-
const sonnet45 = modelIds.filter(isSonnet45ModelId)
36-
if (sonnet45.length > 0) {
37-
return (
38-
sonnet45.find((id) => id === "anthropic/claude-sonnet-4.5") ??
39-
sonnet45.find((id) => id.includes("claude-sonnet-4.5")) ??
40-
sonnet45[0]
41-
)
37+
const sonnets = modelIds.filter(isClaudeSonnetModelId)
38+
if (sonnets.length === 0) {
39+
return modelIds[0]
4240
}
4341

44-
const sonnet4 = modelIds.filter((id) => /claude/i.test(id) && /sonnet/i.test(id) && /sonnet-4/i.test(id))
45-
if (sonnet4.length > 0) {
46-
return sonnet4[0]
47-
}
48-
49-
return modelIds[0]
42+
return (
43+
sonnets.find((id) => id === "anthropic/claude-sonnet-4.5") ??
44+
sonnets.find((id) => id.includes("claude-sonnet-4.5")) ??
45+
sonnets.find((id) => /sonnet-4[.-]5/i.test(id)) ??
46+
sonnets.find((id) => /sonnet-4(?![.-]?\d)/i.test(id)) ??
47+
sonnets[0]
48+
)
5049
}
5150

5251
export const ZooGateway = ({
@@ -90,6 +89,7 @@ export const ZooGateway = ({
9089
</div>
9190
{!zooCodeIsAuthenticated ? (
9291
<div className="flex flex-col gap-1">
92+
<ApiErrorMessage errorMessage={t("settings:validation.zooGatewaySignIn")} />
9393
<p className="text-xs text-vscode-descriptionForeground">
9494
{t("settings:providers.zooGateway.signInDescription")}
9595
</p>

webview-ui/src/components/settings/providers/__tests__/ZooGateway.spec.tsx

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React from "react"
2-
import { render, waitFor } from "@/utils/test-utils"
2+
import { render, screen, waitFor } from "@/utils/test-utils"
33
import type { ModelInfo, ProviderSettings, RouterModels } from "@roo-code/types"
44

55
import { ZooGateway, pickZooGatewayDefaultModelId } from "../ZooGateway"
@@ -10,15 +10,17 @@ vi.mock("@src/i18n/TranslationContext", () => ({
1010
}),
1111
}))
1212

13+
const extensionStateMock = {
14+
zooCodeIsAuthenticated: true,
15+
zooCodeUserEmail: "user@example.com",
16+
zooCodeUserName: "User",
17+
zooCodeBaseUrl: "https://www.zoocode.dev",
18+
uriScheme: "vscode",
19+
deviceName: "Test Device",
20+
}
21+
1322
vi.mock("@src/context/ExtensionStateContext", () => ({
14-
useExtensionState: () => ({
15-
zooCodeIsAuthenticated: true,
16-
zooCodeUserEmail: "user@example.com",
17-
zooCodeUserName: "User",
18-
zooCodeBaseUrl: "https://www.zoocode.dev",
19-
uriScheme: "vscode",
20-
deviceName: "Test Device",
21-
}),
23+
useExtensionState: () => extensionStateMock,
2224
}))
2325

2426
vi.mock("@src/oauth/urls", () => ({
@@ -165,4 +167,23 @@ describe("ZooGateway component", () => {
165167

166168
expect(setApiConfigurationField).not.toHaveBeenCalled()
167169
})
170+
171+
it("renders the sign-in validation error inline when not authenticated", () => {
172+
const original = extensionStateMock.zooCodeIsAuthenticated
173+
extensionStateMock.zooCodeIsAuthenticated = false
174+
try {
175+
render(
176+
<ZooGateway
177+
apiConfiguration={{ apiProvider: "zoo-gateway" } as ProviderSettings}
178+
setApiConfigurationField={vi.fn()}
179+
routerModels={buildRouterModels(["anthropic/claude-sonnet-4"])}
180+
organizationAllowList={baseProps.organizationAllowList}
181+
/>,
182+
)
183+
184+
expect(screen.getByText("settings:validation.zooGatewaySignIn")).toBeInTheDocument()
185+
} finally {
186+
extensionStateMock.zooCodeIsAuthenticated = original
187+
}
188+
})
168189
})

webview-ui/src/utils/__tests__/validate.spec.ts

Lines changed: 69 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,12 @@ vi.mock("i18next", () => ({
1616
},
1717
}))
1818

19-
import { getModelValidationError, validateApiConfigurationExcludingModelErrors, validateBedrockArn } from "../validate"
19+
import {
20+
getModelValidationError,
21+
validateApiConfiguration,
22+
validateApiConfigurationExcludingModelErrors,
23+
validateBedrockArn,
24+
} from "../validate"
2025

2126
describe("Model Validation Functions", () => {
2227
const mockRouterModels: RouterModels = {
@@ -213,50 +218,71 @@ describe("Model Validation Functions", () => {
213218
})
214219

215220
describe("Zoo Gateway validation", () => {
216-
it("returns a sign-in error when neither profile token nor Zoo auth is present", () => {
217-
const config: ProviderSettings = {
218-
apiProvider: "zoo-gateway",
219-
zooGatewayModelId: "anthropic/claude-sonnet-4",
220-
}
221-
222-
const result = validateApiConfigurationExcludingModelErrors(
223-
config,
224-
mockRouterModels,
225-
allowAllOrganization,
226-
false,
227-
)
228-
expect(result).toBe("settings:validation.zooGatewaySignIn")
221+
describe("validateApiConfiguration (welcome-view entry point)", () => {
222+
it("returns a sign-in error when neither profile token nor Zoo auth is present", () => {
223+
const config: ProviderSettings = {
224+
apiProvider: "zoo-gateway",
225+
zooGatewayModelId: "anthropic/claude-sonnet-4",
226+
}
227+
228+
const result = validateApiConfiguration(config, mockRouterModels, allowAllOrganization, false)
229+
expect(result).toBe("settings:validation.zooGatewaySignIn")
230+
})
231+
232+
it("returns undefined when Zoo Code auth is active without a profile token", () => {
233+
const config: ProviderSettings = {
234+
apiProvider: "zoo-gateway",
235+
zooGatewayModelId: "anthropic/claude-sonnet-4",
236+
}
237+
238+
const result = validateApiConfiguration(config, mockRouterModels, allowAllOrganization, true)
239+
expect(result).toBeUndefined()
240+
})
241+
242+
it("returns undefined when a profile session token is set", () => {
243+
const config: ProviderSettings = {
244+
apiProvider: "zoo-gateway",
245+
zooGatewayModelId: "anthropic/claude-sonnet-4",
246+
zooSessionToken: "zoo_ext_test_token",
247+
}
248+
249+
const result = validateApiConfiguration(config, mockRouterModels, allowAllOrganization, false)
250+
expect(result).toBeUndefined()
251+
})
229252
})
230253

231-
it("returns undefined when Zoo Code auth is active without a profile token", () => {
232-
const config: ProviderSettings = {
233-
apiProvider: "zoo-gateway",
234-
zooGatewayModelId: "anthropic/claude-sonnet-4",
235-
}
236-
237-
const result = validateApiConfigurationExcludingModelErrors(
238-
config,
239-
mockRouterModels,
240-
allowAllOrganization,
241-
true,
242-
)
243-
expect(result).toBeUndefined()
244-
})
245-
246-
it("returns undefined when a profile session token is set", () => {
247-
const config: ProviderSettings = {
248-
apiProvider: "zoo-gateway",
249-
zooGatewayModelId: "anthropic/claude-sonnet-4",
250-
zooSessionToken: "zoo_ext_test_token",
251-
}
252-
253-
const result = validateApiConfigurationExcludingModelErrors(
254-
config,
255-
mockRouterModels,
256-
allowAllOrganization,
257-
false,
258-
)
259-
expect(result).toBeUndefined()
254+
describe("validateApiConfigurationExcludingModelErrors (settings form)", () => {
255+
// The settings form short-circuits zoo-gateway and renders the sign-in
256+
// error inline in `ZooGateway.tsx`, so this entry point must never
257+
// surface a zoo-gateway-specific error regardless of auth state.
258+
it("returns undefined for zoo-gateway when unauthenticated and no token", () => {
259+
const config: ProviderSettings = {
260+
apiProvider: "zoo-gateway",
261+
zooGatewayModelId: "anthropic/claude-sonnet-4",
262+
}
263+
264+
const result = validateApiConfigurationExcludingModelErrors(
265+
config,
266+
mockRouterModels,
267+
allowAllOrganization,
268+
)
269+
expect(result).toBeUndefined()
270+
})
271+
272+
it("returns undefined for zoo-gateway when a profile token is set", () => {
273+
const config: ProviderSettings = {
274+
apiProvider: "zoo-gateway",
275+
zooGatewayModelId: "anthropic/claude-sonnet-4",
276+
zooSessionToken: "zoo_ext_test_token",
277+
}
278+
279+
const result = validateApiConfigurationExcludingModelErrors(
280+
config,
281+
mockRouterModels,
282+
allowAllOrganization,
283+
)
284+
expect(result).toBeUndefined()
285+
})
260286
})
261287
})
262288
})

webview-ui/src/utils/validate.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -291,14 +291,21 @@ export function getModelValidationError(
291291
* Validates API configuration but excludes model-specific errors.
292292
* This is used for the general API error display to prevent duplication
293293
* when model errors are shown in the model selector.
294+
*
295+
* Zoo Gateway intentionally short-circuits here — its sign-in error is rendered
296+
* inline by the `ZooGateway` provider component so the form-level error effect
297+
* does not need to track Zoo Code auth state.
294298
*/
295299
export function validateApiConfigurationExcludingModelErrors(
296300
apiConfiguration: ProviderSettings,
297301
_routerModels?: RouterModels, // Keeping this for compatibility with the old function.
298302
organizationAllowList?: OrganizationAllowList,
299-
zooCodeIsAuthenticated?: boolean,
300303
): string | undefined {
301-
const keysAndIdsPresentErrorMessage = validateModelsAndKeysProvided(apiConfiguration, zooCodeIsAuthenticated)
304+
if (apiConfiguration.apiProvider === "zoo-gateway") {
305+
return undefined
306+
}
307+
308+
const keysAndIdsPresentErrorMessage = validateModelsAndKeysProvided(apiConfiguration)
302309

303310
if (keysAndIdsPresentErrorMessage) {
304311
return keysAndIdsPresentErrorMessage

0 commit comments

Comments
 (0)