Files
onyx/web/tests/e2e/admin/disable_default_agent.spec.ts
Raunak Bhagat 31461f9c8c feat(admin): reorganise chat preferences and language models settings (#16435)
## Description

Reorganise the Chat Preferences and Language Models admin pages, tighten
their copy, and give their model selects one loading and failure
pattern. This PR also carries the former #3 (model load states and
hidden models), which was folded in. Each UI change is one bullet.

### Chat Preferences
- Split the settings into two cards: a **Chat** card and a **Search**
card.
- Moved **Default Language** to the top of the Chat card, as a
horizontal row.
- Chat card order: Default Language, Chat Naming Model, Multi-Model
Generation, Deep Research, Chat Auto-Scroll.
- Search card order: Search Selection Model, Search Mode, Auto-Detect
Search Filters.
- Removed the "Model selection" heading; the two task-model rows now sit
in the cards.
- Renamed "Chat naming model" to **Chat Naming Model**, and "Search
selection model" to **Search Selection Model**, with new copy.
- Removed the **Reset** buttons and the **(Optional)** suffix from both
task-model rows.
- Added an unset row at the top of both task-model selects, above a
divider; picking it clears the setting:
- **Chat Naming Model:** "Chat Default". Naming only runs in chats, on
the chat's own model.
- **Search Selection Model:** "Global Default", described as "Each
chat's model". Selection also runs in the Search UI, where the unset
fallback is the workspace default model.
- Clicking a task-model row's title opens its select.
- Removed the **beta** tag from Multi-Model Generation.
- Renamed "Disable Default Chat" to **Always Start with an Agent**, with
new copy and a link to featured agents.
- Made "without logging in" bold in the Allow Anonymous Users
description.
- Moved **Temperature Control** and **Reasoning Control** to the
Language Models page.
- New copy for Default Language and Search Selection Model.

### Language Models
- Card order: Default Model, Auto Mode, Router Model, a divider,
Reasoning Control, Temperature Control, Hide Provider Grouping.
- New copy for Default Model, Reasoning Control and Temperature Control.
- **Auto Mode** stays locked until the router list has loaded with at
least one router, with a "No router models found" tooltip when there are
none. Turning it off is always possible.
- The **Router Model** row shows only when a router model exists, and
its select can be cleared.
- With Auto Mode on and no usable router (none chosen, cleared, or
deleted since), an inline error under the Router Model row reads "Choose
a router model to enable users to use the auto routing mode." With no
router models at all, the error sits under Auto Mode instead.

### Settings (personal)
- **Display Language** now shares one component with the workspace
Default Language. It starts with a **Global Default** row (the workspace
language as its description) above a divider; picking it clears the
override.

### Model selects (task models and Router Model)
- **Loading:** a `DropdownLoader` stands in for the select.
- **Load failure:** the select stays, disabled with the error border,
and a persistent error toast offers a Retry button. The two task-model
rows share one toast.
- **Hidden models** are never offered, except a hidden model already in
use.

### Across these pages
- Removed **success toasts** from Chat Preferences, Language Models
(including cost overrides), Settings and Decision Models' provider
delete. Failures still show an error toast. e2e helpers wait for the
save response instead of the toast.

### Code changes behind the UI
- `LanguageSelect` (`lib/app/components.tsx`) serves both language
pickers.
- `filterModelConfigurations` loses its `visibleOnly` option; every
caller now lists visible models only (plus `keep`).
- `SimpleModelSelector`: its Global Default row takes a `label`, always
shows a divider (also in an ungrouped list), and the select takes
`isError`.
- Opal `InputHorizontal` and `InputVertical` take `error` and `warning`
props for inputs outside Formik.

## Changes

### Language Models

No "router model" configured, "Auto Mode" disabled:

<img width="872" height="452" alt="image"
src="https://github.com/user-attachments/assets/3894cb0f-9805-4909-99e9-f2bfe3ed92ab"
/>

"Auto Mode" on and no "Router model" configured:

<img width="877" height="526" alt="image"
src="https://github.com/user-attachments/assets/2ba22737-ddc7-4836-9ed8-83d2106aa54f"
/>

"Auto Mode" on and "Router model" configured:

<img width="863" height="506" alt="image"
src="https://github.com/user-attachments/assets/748db170-376b-4955-8d73-0c5a064d8a2b"
/>

### Chat Preferences

A few things have just been moved around here. The "beta" tag was also
removed from "Multi-Model Generation".

<img width="877" height="688" alt="image"
src="https://github.com/user-attachments/assets/e21336a8-4667-4110-a11e-e98f0488e0ad"
/>


<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Reorganizes the admin Chat Preferences and Language Models pages and
tightens their copy. Chat Preferences now splits into Chat and Search
cards, Temperature Control and Reasoning Control move to Language
Models, and the task-model rows are renamed to Chat Naming Model and
Search Selection Model.

**Behavior changes**
- Task-model selectors start with a Global Default row that clears the
designation; chat naming reads Chat Default and search selection keys
off the workspace default. Reset buttons and the Optional suffix are
gone.
- Auto Mode stays locked until router models load and can only be
enabled when a router exists; it can always be turned off. The Router
Model row only shows when a router exists, and its select is clearable
with an inline error when Auto Mode needs a router.
- Failed model loads keep the select, disabled with an error border, and
raise a persistent error toast with a retry button; loading shows a
dropdown-loader shimmer.
- "Disable Default Chat" is renamed to "Always Start with an Agent" with
new copy and a link to featured agents.
- Success toasts are removed from these pages, including cost overrides
and Settings, and Decision Models no longer toasts provider deletion;
failures still show an error toast. e2e helpers wait for the save
response instead.
- `LanguageSelect` is shared between the workspace Default Language and
the personal Display Language; the personal picker offers a Global
Default row.
- Model pickers no longer list hidden models; they show visible models
only, plus the currently selected value.

<sup>Written for commit d523995b8d.
Summary will update on new commits.</sup>

<a
href="https://cubic.dev/pr/onyx-dot-app/onyx/pull/16435?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="View guided diff"
src="https://www.cubic.dev/buttons/review-in-cubic-light.svg"></picture></a>
<a
href="https://www.cubic.dev/action/auto-fix/pr/onyx-dot-app/onyx/16435?returnTo=https%3A%2F%2Fgithub.com%2Fonyx-dot-app%2Fonyx%2Fpull%2F16435&source=description"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/turn-on-auto-fix-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/turn-on-auto-fix-light.svg"><img
alt="Turn on auto-fix"
src="https://www.cubic.dev/buttons/turn-on-auto-fix-light.svg"></picture></a>

<!-- End of auto-generated description by cubic. -->



<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
<!-- coderabbit-ai-generated:v1 -->

## Summary by CodeRabbit

- **New Features**
- Chat Preferences separates chat and search settings into dedicated
sections.
- Chat-naming and search-selection models can be configured
independently, with a Global Default option that clears each
task-specific selection.
- Language-model settings include per-chat reasoning and temperature
controls, plus guidance for unavailable router models.
- Workspace and user language selection supports a Global Default
option.

- **Improvements**
- Model selectors exclude hidden models except when a hidden model is
already selected.
- Settings provide inline loading and error feedback; success
notifications are no longer shown for many actions.
- Updated terminology and translations, including the setting for
starting chats with an agent.
  - Input layouts support explicit error and warning messages.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-10-22 01:47:24 +00:00

263 lines
9.2 KiB
TypeScript

import { test, expect, Page } from "@playwright/test";
import { ADMIN_ROUTES } from "@/lib/admin-routes";
import { loginAs } from "@tests/e2e/utils/auth";
import { createAgent } from "@tests/e2e/utils/agentUtils";
import { OnyxApiClient } from "@tests/e2e/utils/onyxApiClient";
const MAX_SETTING_SAVE_ATTEMPTS = 5;
const SETTING_SAVE_RETRY_DELAY_MS = 750;
/**
* Expand the "Advanced Options" collapsible section on the Chat Preferences page.
* The section is closed by default (`defaultOpen={false}`).
* Only expands if not already open (checks for the switch element visibility).
*/
async function expandAdvancedOptions(page: Page): Promise<void> {
// Wait for the page title to be visible, signalling the form has loaded
await expect(page.locator('[aria-label="admin-page-title"]')).toBeVisible({
timeout: 10000,
});
// Check if the switch is already visible (section already expanded)
const switchEl = page.locator("#disable_default_assistant");
const alreadyVisible = await switchEl.isVisible().catch(() => false);
if (alreadyVisible) return;
const header = page.getByText("Advanced Options", { exact: true });
await expect(header).toBeVisible({ timeout: 10000 });
await header.scrollIntoViewIfNeeded();
await header.click();
// Wait for the collapsible content to expand and switch to appear
await expect(switchEl).toBeVisible({ timeout: 5000 });
}
/**
* Toggle the "Always Start with an Agent" setting on the Chat Preferences page.
* Uses auto-save via the SwitchField.
*
* The switch is a SwitchField with name="disable_default_assistant" which renders
* `<button role="switch" id="disable_default_assistant" aria-checked="...">`.
*/
async function setDisableDefaultAssistantSetting(
page: Page,
isDisabled: boolean
): Promise<void> {
let lastCheckedState = false;
for (let attempt = 0; attempt < MAX_SETTING_SAVE_ATTEMPTS; attempt += 1) {
await page.goto(ADMIN_ROUTES.CHAT_PREFERENCES.path);
await page.waitForLoadState("networkidle");
// Expand "Advanced Options" collapsible (closed by default)
await expandAdvancedOptions(page);
const switchEl = page.locator("#disable_default_assistant");
await expect(switchEl).toBeVisible({ timeout: 5000 });
const currentState = await switchEl.getAttribute("aria-checked");
lastCheckedState = currentState === "true";
if (lastCheckedState === isDisabled) {
return;
}
// Toggle the switch and wait for the auto-save
const saved = page.waitForResponse(
(response) =>
new URL(response.url()).pathname === "/api/admin/settings" &&
response.request().method() === "PATCH"
);
await switchEl.click();
expect((await saved).ok()).toBeTruthy();
await page.waitForTimeout(SETTING_SAVE_RETRY_DELAY_MS);
// Verify persistence after reload
await page.reload();
await page.waitForLoadState("networkidle");
// Re-expand Advanced Options (closed by default after reload)
await expandAdvancedOptions(page);
const newState = await switchEl.getAttribute("aria-checked");
lastCheckedState = newState === "true";
if (lastCheckedState === isDisabled) {
return;
}
}
throw new Error(
`Failed to persist Always Start with an Agent setting after ${MAX_SETTING_SAVE_ATTEMPTS} attempts (expected ${isDisabled}, last=${lastCheckedState}).`
);
}
test.describe("Disable Default Agent Setting @exclusive", () => {
let createdAssistantId: number | null = null;
test.beforeEach(async ({ page }) => {
// Log in as admin
await page.context().clearCookies();
await loginAs(page, "admin");
});
test.afterEach(async ({ page }) => {
// Clean up any assistant created during the test
if (createdAssistantId !== null) {
const client = new OnyxApiClient(page.request);
await client.deleteAgent(createdAssistantId);
createdAssistantId = null;
}
// Ensure default agent is enabled (switch unchecked) after each test
// to avoid interfering with other tests
await setDisableDefaultAssistantSetting(page, false);
});
test("admin can enable and disable the setting in chat preferences", async ({
page,
}) => {
await setDisableDefaultAssistantSetting(page, true);
await setDisableDefaultAssistantSetting(page, false);
await setDisableDefaultAssistantSetting(page, true);
});
test("new session button uses current agent when setting is enabled", async ({
page,
}) => {
// Create the agent before enabling the setting, not after. With the setting
// on and nothing but the default agent available, there is no agent to chat
// with and /app correctly refuses to render one - which puts the UI that
// creates agents out of reach.
await page.goto("/app");
const agentName = `Test Assistant ${Date.now()}`;
await createAgent(page, {
name: agentName,
description: "Test assistant for new session button test",
instructions: "You are a helpful test assistant.",
});
// Extract the assistant ID from the URL
const currentUrl = page.url();
const agentIdMatch = currentUrl.match(/agentId=(\d+)/);
expect(agentIdMatch).toBeTruthy();
// Store for cleanup
if (agentIdMatch) {
createdAssistantId = Number(agentIdMatch[1]);
}
await setDisableDefaultAssistantSetting(page, true);
// The helper leaves the browser on the admin page, which has no app sidebar.
await page.goto("/app");
// Click the "New Session" button
const newSessionButton = page.locator(
'[data-testid="AppSidebar/new-session"]'
);
await newSessionButton.click();
// Verify the WelcomeMessage shown is NOT from the default agent
// Default agent shows onyx-logo, custom agents show agent-name-display
await expect(page.locator('[data-testid="onyx-logo"]')).not.toBeVisible();
await expect(
page.locator('[data-testid="agent-name-display"]')
).toBeVisible();
});
test("direct navigation to /app uses first pinned assistant when setting is enabled", async ({
page,
}) => {
// First enable the setting
await setDisableDefaultAssistantSetting(page, true);
// Navigate directly to /app
await page.goto("/app");
// Verify that we didn't land on the default agent (ID 0)
// The assistant selection should be a pinned or available assistant (not ID 0)
const currentUrl = page.url();
// If agentId is in URL, it should not be 0
if (currentUrl.includes("agentId=")) {
expect(currentUrl).not.toContain("agentId=0");
}
});
test("chat preferences shows disabled state when setting is enabled", async ({
page,
}) => {
// First enable the setting
await setDisableDefaultAssistantSetting(page, true);
// Navigate to chat preferences configuration page
await page.goto(ADMIN_ROUTES.CHAT_PREFERENCES.path);
await page.waitForLoadState("networkidle");
// Wait for the page to fully render (page title signals form is loaded)
await expect(page.locator('[aria-label="admin-page-title"]')).toHaveText(
/^Chat Preferences/,
{ timeout: 10000 }
);
// The new page wraps Connectors + Actions & Tools in <Disabled disabled={values.disable_default_assistant}>
// When disabled, the section should have reduced opacity / disabled styling
// The "Modify Prompt" button should still be accessible (it's outside the Disabled wrapper)
// Use text locator (Opal Button wraps text in Interactive.Base > Slot which may
// not expose role="button" to Playwright's getByRole)
await expect(page.getByText("Modify Prompt")).toBeVisible({
timeout: 5000,
});
// The "Actions & Tools" section text should still be present but visually disabled
await expect(page.getByText("Actions & Tools")).toBeVisible();
});
test("chat preferences shows full configuration UI when setting is disabled", async ({
page,
}) => {
// Ensure setting is disabled
await setDisableDefaultAssistantSetting(page, false);
// Navigate to chat preferences configuration page
await page.goto(ADMIN_ROUTES.CHAT_PREFERENCES.path);
await page.waitForLoadState("networkidle");
// Verify configuration UI is shown (Actions & Tools section should be visible and enabled)
await expect(page.getByText("Actions & Tools")).toBeVisible({
timeout: 10000,
});
// Verify the page title
await expect(page.locator('[aria-label="admin-page-title"]')).toHaveText(
/^Chat Preferences/
);
});
test("default agent is available again when setting is disabled", async ({
page,
}) => {
// Navigate to settings and ensure setting is disabled
await setDisableDefaultAssistantSetting(page, false);
// Navigate directly to /app without parameters
await page.goto("/app");
// The default agent (ID 0) should be available
// We can verify this by checking that the app loads successfully
// and doesn't force navigation to a specific assistant
expect(page.url()).toContain("/app");
// Verify the new session button navigates to /app without agentId
const newSessionButton = page.locator(
'[data-testid="AppSidebar/new-session"]'
);
await newSessionButton.click();
// Should navigate to /app without agentId parameter
const newUrl = page.url();
expect(newUrl).toContain("/app");
});
});