Hide All toggle on SaaS LLM settings (#14013)

Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: allhands-bot <allhands-bot@users.noreply.github.com>
This commit is contained in:
Graham Neubig
2026-04-22 15:13:57 -04:00
committed by GitHub
parent b357c0c3bb
commit d64d0d6bf6
6 changed files with 123 additions and 4 deletions

View File

@@ -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.

View File

@@ -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<Settings["agent_settings_schema"]> = {
model_name: "AgentSettings",

View File

@@ -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!,

View File

@@ -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;

View File

@@ -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,
},
],
},
{

View File

@@ -391,6 +391,8 @@ export function LlmSettingsScreen({
header={buildHeader}
buildPayload={buildPayload}
getInitialView={getInitialView}
forceShowAdvancedView
allowAllView={!isSaasMode}
testId="llm-settings-screen"
/>
);