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

Playwright E2E Test Rules

Hard rules for tests under web/tests/e2e/. Read before adding or modifying a spec.

For the broader Onyx testing strategy and where Playwright fits among unit / external-dependency / integration tests, see backend/AGENTS.md ("Testing Strategy") and backend/tests/README.md. For Jest + React Testing Library guidance for component tests, see web/tests/README.md.

1. Use the Page Object Model

All locators and interactions for a UI surface live on a Page Object class — never inline in a spec.

  • One class per surface (e.g. ChatPage, InputBar, AdminUsersPage).
  • Page objects live in tests/e2e/pages/ — one file per class, named after the surface. Keep spec-only helpers (flow glue, assertion utilities) out of pages/; those stay beside their specs or in tests/e2e/utils/.
  • Composite pages expose nested objects: chatPage.inputBar.someMethod().
  • Specs call methods on the page object. They do not construct locators.
  • When extending coverage for an area that has no page object, create one before writing the spec.
// ✅ Good — spec calls into the POM
await chatPage.goto();
await chatPage.inputBar.type("hello");
await chatPage.inputBar.send();
await chatPage.expectHumanMessage("hello");

// ❌ Bad — raw locators in the spec
await page.goto("/app");
await page.locator('[contenteditable="true"]').fill("hello");
await page.keyboard.press("Enter");
await expect(page.locator(".message")).toContainText("hello");

Why: specs that read like a description of user behavior are easier to scan, review, and refactor. Locator churn changes one POM method, not every spec that touched the surface.

Locator priority — when defining locators on a page object, prefer in this order:

  1. data-testid / aria-label (getByTestId, getByLabel) — preferred for Onyx components.
  2. Role-based (getByRole) — standard HTML elements.
  3. Text / label (getByText, getByLabel) — visible text content.
  4. CSS selectors (locator(...)) — last resort, only when nothing above works.

Never reach for complex CSS/XPath when a built-in locator fits.

2. Use auto-retrying matchers — never getAttribute / evaluate for async state

Playwright's expect(locator).* matchers retry until the assertion passes or the timeout expires. locator.getAttribute() and page.evaluate() are single snapshots — they read the DOM exactly once and fail immediately on a stale read.

If the value can be set by a React state update, an effect, a microtask, or anything else asynchronous, snapshot reads will flake.

Asserting on Use Don't use
Attribute expect(locator).toHaveAttribute(name, value) locator.getAttribute(name) then expect(...)
Class expect(locator).toHaveClass(/regex/) / .not.toHaveClass(...) page.evaluate(el => el.classList.contains(...))
Text expect(locator).toHaveText(value) / toContainText(value) locator.textContent() then expect(...)
Count expect(locator).toHaveCount(n) locator.count() then expect(...)
Visibility expect(locator).toBeVisible() / toBeHidden() manual isVisible() checks
Value expect(locator).toHaveValue(value) locator.inputValue() then expect(...)
// ✅ Good — retries until the attribute settles
await expect(tile).toHaveAttribute("data-text", "modified text");

// ❌ Bad — one-shot read, flakes when the attribute updates after a React render
const text = await tile.getAttribute("data-text");
expect(text).toBe("modified text");

// ✅ Good — retries until the class settles
await expect(tile.first()).toHaveClass(/rich-input-tile-selected/);

// ❌ Bad — one-shot DOM snapshot
const selected = await page.evaluate(
  () => !!document.querySelector(".rich-input-tile-selected")
);
expect(selected).toBe(true);

getAttribute / evaluate / textContent / count are still appropriate when you need the value for control flow inside the spec (e.g. branching on it, logging it). They are not appropriate as the basis of an assertion on async state.

3. The LLM is the scripted mock LLM server

Every LLM call of the Playwright deployment goes to the scripted mock LLM server (backend/tests/integration/mock_services/mock_llm_server/server.py). Global setup registers its default script and makes the public openai_compatible provider "PW Mock LLM" the default. The script's default reply answers every request that no conversation serves with This is a mock LLM response.

To script a turn, use tests/e2e/utils/mockLlm.ts. Add a conversation to the default script that matches a nonce, and put the nonce in the chat message. Do not change the default provider to script a spec: all workers share it.

const nonce = mockLlmNonce();
await addMockLlmConversation({
  name: nonce,
  conditions: { prompt_contains: [nonce] },
  replies: [
    { tool_calls: [{ id: `call-${nonce}`, name: "tool_0", arguments: {} }] },
    { text: "Done.", conditions: { has_results_for: [`call-${nonce}`] } },
  ],
});
await sendMessage(page, `Run tool_0. ${nonce}`);

A tool-free request (no tools and no tool results), such as session naming, gets the default reply unless a conversation or reply conditions set has_tools: false. The web app names a new chat after the first answer without waiting for it, so its naming request can overlap the next turn. Set has_tools: false only on a reply that must answer a tool-free chat turn.

To run the specs locally, start the server with the rest of the stack:

cd deployment/docker_compose
docker compose -f docker-compose.yml -f docker-compose.dev.yml -f docker-compose.mock-llm-test.yml up -d

You can also run it on the host with cd backend && MOCK_LLM_SERVER_PORT=8095 python -m tests.integration.mock_services.mock_llm_server.server. The runner reaches the server at MOCK_LLM_SERVER_URL (default http://localhost:8095), and the backend reaches it at MOCK_LLM_BACKEND_URL (default http://mock_llm_server:8095). When the backend runs on the host, set MOCK_LLM_BACKEND_URL=http://localhost:8095. Global setup replaces the default LLM of the target deployment.