fix(web): a page host sized by its content renders narrow
Nightly Build / build (push) Successful in 9s

Models → TTS rendered as a 45px strip in the middle of an empty
workspace. The cause was not inside the page: `models-tts-section` had
no CSS rule at all, and an unknown custom element is `display: inline`.
Its host is switched to `display: flex` by JS, so the section became a
content-sized flex item in a row container. Its three siblings escaped
only because they were named in a sizing block that TTS was never added
to.

Replace that enumeration with `tasks-page > *, projects-page > *,
models-hub-page > *`. The three multiplexers render exactly one section
at a time as their only child, so a new section now inherits the sizing
by existing — the list of names was itself the bug, and forgetting it
was silent: no console error, no failed build, just a narrow page.

Also give every remaining page host the two properties that made this
survivable elsewhere: `flex-direction: column` on agent-inbox,
approval-rules, approval-groups, llm-providers and models-hub (in the
default `row` the width depends on the inner div remembering
`width: 100%`), and `min-width: 0` on agents, config and dashboard so a
wide table cannot push the workspace past the viewport.

Measured in headless Chrome against the real stylesheets: the TTS
section goes from 45px to 589px in a 590px host, matching its siblings.

Record the trap in dev-docs/frontend.md, which had no page-shell section
at all — and add the models-tts row missing from its component table.
This commit is contained in:
Daniele
2026-08-24 18:28:01 +01:00
parent c14cbc3626
commit e5e8ccc92f
9 changed files with 45 additions and 28 deletions
+15 -1
View File
@@ -32,6 +32,19 @@ All extend `LightElement` from `web/lib/base.js` (Lit). `ChatSession` (`web/lib/
The role editor (`roles-page.js`) sets the default group + an allowed-groups checklist (→ `attrs.permission_groups`) + a **default-assistant** select (→ `attrs.chat_agent`) fed by `GET /api/agents` filtered to `type:chat` minus `project-coordinator` (source-driven); the same exclusion is enforced server-side in the roles API (`validate_chat_agent`).
## The page shell — a new page renders narrow until it is sized (`web/css/page-shell.css`)
**The trap, in one sentence: a custom element the browser has never heard of is `display: inline`, and every page host in this app is a `display: flex` container — so a page nobody wrote a CSS rule for becomes a content-sized flex item and renders as a narrow column in the middle of the workspace.** It looks like a broken stylesheet inside the page; it is the absence of a rule *about* the page. `<models-tts-section>` shipped this way — added to the Models hub beside its three siblings, never added to the sizing block that listed them by name.
Two independent things have to be true, and both were violated at some point:
- **The host needs `flex-direction: column`.** Every page host is toggled `display: none``flex` by its own component (`this.style.display = this._open ? 'flex' : 'none'`), which means the value in CSS is always `none` and the flex direction is never obvious from reading the block. In the default `row`, a child is sized by its content on the main axis; in `column`, the cross axis stretches and the child fills the width no matter what it declares. Several pages hid this for years behind a `width: 100%` on their own root `<div>` (`.pv-page`, `.page-panel`, `.apr-page`, `.llmr-page`) — which works, but only defends the one page that remembered it. `.llm-page`, shared by all four Models sections, carries `max-width: 100%` and no `width`, which is why the section with no host rule collapsed and its three siblings did not.
- **The child needs `flex: 1; min-height: 0; min-width: 0`.** Otherwise it fills the width but not the height, and a wide table inside it pushes the whole workspace row wider than the viewport.
**The fix is a descendant selector, not a longer list.** The three multiplexer pages (`tasks-page`, `projects-page`, `models-hub-page`) each render exactly one sub-section at a time as their only child, so `page-shell.css` sizes them as `tasks-page > *, projects-page > *, models-hub-page > *`. The enumeration *was* the bug: a list of element names is a thing to forget, and forgetting it is silent — no console error, no failed build, just a narrow page. A new section now inherits the sizing by existing. `plugin-page-host > [plugin-id]` had already reached the same conclusion for plugin-contributed fragments.
**So: adding a page to the app is two edits, not one.** Write the component, *and* give the element a rule — `display: none` + `flex-direction: column` + `flex: 1` + `min-width: 0` — in `page-shell.css` or the page's own stylesheet. If it hangs off one of the three multiplexers, the `> *` rule already covers it and you write nothing. To check the whole set at once, look for a host that sets `style.display = 'flex'` in `web/components/` and has no matching element selector in `web/css/`.
| File | Element | Notes |
| ---- | ------- | ----- |
| `copilot.js` | `<app-copilot>` | The chat surface (`_wsSource='web'`): full/dock roving layout, welcome hero empty state, privacy chip, composer with model pill, slash-command autocomplete |
@@ -61,9 +74,10 @@ The role editor (`roles-page.js`) sets the default group + an allowed-groups che
| `connector-detail.js` | `<connector-detail-page>` | A connector's own page (`#connector?name=X`): env/secret form + Test, the **OAuth login panel** (sign in → paste code → complete, §15), global enable. Access grants live **only** on the Users page (`users-page.js` — the `#users/{id}` page's connectors section, with the plugin grants right below it), so "who has what" has a single surface |
| `shared/connector-common.js` | (helpers) | Shared Connectors vocabulary: `statusOf` (incl. `needs_login` for a pending OAuth row), `STATUS_LABEL`, schema normalization, `jf` fetch |
| `llm-providers.js` | `<llm-providers-page>` | LLM provider management |
| `models-hub.js` | `<models-hub-page>` | Models hub landing (LLM / Transcription / Image) |
| `models-hub.js` | `<models-hub-page>` | Models hub landing (LLM / Transcription / Image / TTS); renders one section at a time, sized by the `models-hub-page > *` rule |
| `models-llm.js` | `<models-llm-section>` | LLM model CRUD + drag-and-drop priority |
| `models-transcribe.js` | `<models-transcribe-section>` | Transcription model CRUD |
| `models-image.js` | `<models-image-section>` | Image generation model CRUD |
| `models-tts.js` | `<models-tts-section>` | Text-to-speech model CRUD |
| `mobile-app.js` | `<mobile-app>` | Mobile app shell |
| `shared/settings-page.js` | `<settings-page>` | Mobile settings: per-user avatar, locale picker (`I18nMixin`), profile/preferences |