diff --git a/AGENTS.md b/AGENTS.md index 199aaf7a6a..62fd175ac1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -18,6 +18,8 @@ Local run troubleshooting notes: - If local runtime startup fails with `duplicate session: test-session`, clear the stale tmux session on the default socket: `tmux -S /tmp/tmux-$(id -u)/default kill-session -t test-session`. - Local runtime browser startup expects Playwright browsers under `~/.cache/playwright`; if needed run `PLAYWRIGHT_BROWSERS_PATH=$HOME/.cache/playwright poetry run playwright install chromium`. - In this sandbox environment, an inherited `SESSION_API_KEY` can make `/api/v1/settings` return 401 in the browser. Unset it before `make run` when you want to use the local web UI directly. +- In this sandbox, `frontend`'s `npm run dev:mock` / `dev:mock:saas` can start but still be awkward to browse through the work-host proxy. For PR QA screenshots, a reliable fallback is to `npm run build` with the desired `VITE_MOCK_*` env, then serve `build/` with a tiny custom HTTP server that returns the minimal mock JSON endpoints needed by the settings page. + IMPORTANT: Before making any changes to the codebase, ALWAYS run `make install-pre-commit-hooks` to ensure pre-commit hooks are properly installed. diff --git a/frontend/__tests__/components/features/settings/sdk-settings/sdk-section-page.test.tsx b/frontend/__tests__/components/features/settings/sdk-settings/sdk-section-page.test.tsx index cbcd3f75f9..e574b03598 100644 --- a/frontend/__tests__/components/features/settings/sdk-settings/sdk-section-page.test.tsx +++ b/frontend/__tests__/components/features/settings/sdk-settings/sdk-section-page.test.tsx @@ -435,6 +435,20 @@ describe("SdkSectionPage", () => { + it("shows the advanced toggle when it is forced for a critical-only schema", async () => { + vi.spyOn(SettingsService, "getSettings").mockResolvedValue(buildSavableSettings()); + + renderSdkSectionPage({ + sectionKeys: ["llm"], + forceShowAdvancedView: true, + }); + + await screen.findByTestId("sdk-section-basic-toggle"); + expect(screen.getByTestId("sdk-section-advanced-toggle")).toBeInTheDocument(); + expect(screen.queryByTestId("sdk-section-all-toggle")).not.toBeInTheDocument(); + }); + + it("shows the all toggle instead of an empty advanced tier for minor-only schemas", async () => { const schema: NonNullable = { model_name: "AgentSettings", diff --git a/frontend/__tests__/routes/llm-settings.test.tsx b/frontend/__tests__/routes/llm-settings.test.tsx index d45036e276..ffc3a18586 100644 --- a/frontend/__tests__/routes/llm-settings.test.tsx +++ b/frontend/__tests__/routes/llm-settings.test.tsx @@ -243,6 +243,49 @@ describe("LlmSettingsScreen", () => { expect(screen.getByTestId("base-url-input")).toBeInTheDocument(); }); + it("shows Advanced and All toggles in OSS mode for the default LLM route schema", async () => { + vi.spyOn(SettingsService, "getSettings").mockResolvedValue(buildSettings()); + + renderLlmSettingsScreen({ appMode: "oss" }); + + await screen.findByTestId("llm-settings-screen"); + expect( + screen.getByTestId("sdk-section-advanced-toggle"), + ).toBeInTheDocument(); + expect(screen.getByTestId("sdk-section-all-toggle")).toBeInTheDocument(); + }); + + it("keeps Advanced visible but hides All in SaaS mode for the default LLM route schema", async () => { + vi.spyOn( + organizationService, + "getOrganizationAgentSettings", + ).mockResolvedValue( + buildSettings({ + agent_settings: { + llm: { + model: "openai/gpt-4o", + }, + }, + }), + ); + + renderLlmSettingsScreen({ appMode: "saas", scope: "org" }); + + await screen.findByTestId("llm-settings-screen"); + expect( + screen.getByTestId("sdk-section-advanced-toggle"), + ).toBeInTheDocument(); + expect( + screen.queryByTestId("sdk-section-all-toggle"), + ).not.toBeInTheDocument(); + + await userEvent.click(screen.getByTestId("sdk-section-advanced-toggle")); + + expect(screen.getByTestId("llm-settings-form-advanced")).toBeInTheDocument(); + expect(screen.getByTestId("llm-custom-model-input")).toBeInTheDocument(); + expect(screen.getByTestId("base-url-input")).toBeInTheDocument(); + }); + it("uses schema defaults for custom-rendered advanced fields", async () => { const schema = structuredClone( MOCK_DEFAULT_USER_SETTINGS.agent_settings_schema!, diff --git a/frontend/src/components/features/settings/sdk-settings/sdk-section-page.tsx b/frontend/src/components/features/settings/sdk-settings/sdk-section-page.tsx index db2130cc63..882667c26b 100644 --- a/frontend/src/components/features/settings/sdk-settings/sdk-section-page.tsx +++ b/frontend/src/components/features/settings/sdk-settings/sdk-section-page.tsx @@ -49,6 +49,35 @@ const getLessDetailedView = ( ): SettingsView => VIEW_ORDER[nextView] < VIEW_ORDER[currentView] ? nextView : currentView; +const normalizeView = ( + view: SettingsView, + { + showAdvanced, + showAll, + }: { + showAdvanced: boolean; + showAll: boolean; + }, +): SettingsView => { + if (view === "all") { + if (showAll) { + return "all"; + } + + return showAdvanced ? "advanced" : "basic"; + } + + if (view === "advanced") { + if (showAdvanced) { + return "advanced"; + } + + return showAll ? "all" : "basic"; + } + + return "basic"; +}; + export interface SdkSectionHeaderProps { values: SettingsFormValues; isDisabled: boolean; @@ -75,6 +104,8 @@ export function SdkSectionPage({ buildPayload, onSaveSuccess, getInitialView, + forceShowAdvancedView = false, + allowAllView = true, testId = "sdk-section-settings-screen", }: { sectionKeys: string[]; @@ -97,6 +128,8 @@ export function SdkSectionPage({ settings: Settings, filteredSchema: SettingsSchema, ) => SettingsView; + forceShowAdvancedView?: boolean; + allowAllView?: boolean; testId?: string; }) { const { t } = useTranslation(); @@ -148,8 +181,9 @@ export function SdkSectionPage({ }; }, [schema, stableSectionKeys]); - const showAdvanced = hasAdvancedSettings(filteredSchema); - const showAll = hasMinorSettings(filteredSchema); + const showAdvanced = + forceShowAdvancedView || hasAdvancedSettings(filteredSchema); + const showAll = allowAllView && hasMinorSettings(filteredSchema); const initialValues = React.useMemo(() => { if (!settings || !filteredSchema) return null; @@ -162,10 +196,20 @@ export function SdkSectionPage({ const initialView = React.useMemo(() => { if (!settings || !filteredSchema) return null; - return getInitialView + + const resolvedInitialView = getInitialView ? getInitialView(settings, filteredSchema) : inferInitialView(settings, filteredSchema, settingsSource); - }, [settings, filteredSchema, getInitialView, settingsSource]); + + return normalizeView(resolvedInitialView, { showAdvanced, showAll }); + }, [ + settings, + filteredSchema, + getInitialView, + settingsSource, + showAdvanced, + showAll, + ]); React.useEffect(() => { hasHydratedViewRef.current = false; diff --git a/frontend/src/mocks/settings-handlers.ts b/frontend/src/mocks/settings-handlers.ts index c27d85a008..10cc432fe4 100644 --- a/frontend/src/mocks/settings-handlers.ts +++ b/frontend/src/mocks/settings-handlers.ts @@ -120,6 +120,20 @@ const MOCK_AGENT_SETTINGS_SCHEMA: NonNullable< secret: false, required: false, }, + { + key: "llm.temperature", + label: "Temperature", + description: "Adjust randomness for non-deterministic model outputs.", + section: "llm", + section_label: "LLM", + value_type: "number", + default: null, + choices: [], + depends_on: [], + prominence: "minor", + secret: false, + required: false, + }, ], }, { diff --git a/frontend/src/routes/llm-settings.tsx b/frontend/src/routes/llm-settings.tsx index 6e94322e58..960bb7a3e3 100644 --- a/frontend/src/routes/llm-settings.tsx +++ b/frontend/src/routes/llm-settings.tsx @@ -391,6 +391,8 @@ export function LlmSettingsScreen({ header={buildHeader} buildPayload={buildPayload} getInitialView={getInitialView} + forceShowAdvancedView + allowAllView={!isSaasMode} testId="llm-settings-screen" /> );