From e5e8ccc92f6dd4586bf71f00a2a72e38c4f54e34 Mon Sep 17 00:00:00 2001 From: Daniele Date: Mon, 24 Aug 2026 18:28:01 +0100 Subject: [PATCH] fix(web): a page host sized by its content renders narrow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- CHANGELOG.md | 3 +++ dev-docs/frontend.md | 16 ++++++++++++++- web/css/agent-inbox.css | 3 ++- web/css/agents.css | 3 ++- web/css/approval-groups.css | 1 + web/css/approval-rules.css | 1 + web/css/config.css | 3 ++- web/css/home.css | 3 ++- web/css/page-shell.css | 40 ++++++++++++++++--------------------- 9 files changed, 45 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ae500f..0955c00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -75,6 +75,9 @@ release PR may merge — and a section is closed at the commit that bumps it. ### Fixed +- **Models → Text-to-speech** now fills the window like every other page. It was rendering + as a narrow strip in the middle of an otherwise empty screen, which made the model list + and its forms unreadably cramped. - A connector that fails to start no longer leaves its process behind. One that started but answered the handshake wrong — a broken or mismatched connector — was left running on every retry, and the accumulated processes eventually used up every file handle the diff --git a/dev-docs/frontend.md b/dev-docs/frontend.md index dfd0fd0..2eed84b 100644 --- a/dev-docs/frontend.md +++ b/dev-docs/frontend.md @@ -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. `` 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 `
` (`.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` | `` | 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` | `` | 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 provider management | -| `models-hub.js` | `` | Models hub landing (LLM / Transcription / Image) | +| `models-hub.js` | `` | Models hub landing (LLM / Transcription / Image / TTS); renders one section at a time, sized by the `models-hub-page > *` rule | | `models-llm.js` | `` | LLM model CRUD + drag-and-drop priority | | `models-transcribe.js` | `` | Transcription model CRUD | | `models-image.js` | `` | Image generation model CRUD | +| `models-tts.js` | `` | Text-to-speech model CRUD | | `mobile-app.js` | `` | Mobile app shell | | `shared/settings-page.js` | `` | Mobile settings: per-user avatar, locale picker (`I18nMixin`), profile/preferences | diff --git a/web/css/agent-inbox.css b/web/css/agent-inbox.css index aea0e17..7373bae 100644 --- a/web/css/agent-inbox.css +++ b/web/css/agent-inbox.css @@ -1,7 +1,8 @@ /* Agent Inbox page — desktop layout. Card styles live in inbox-cards.css. */ agent-inbox-page { - display: none; + display: none; /* toggled by JS → flex */ + flex-direction: column; flex: 1; min-width: 0; overflow: hidden; diff --git a/web/css/agents.css b/web/css/agents.css index e6616db..2507622 100644 --- a/web/css/agents.css +++ b/web/css/agents.css @@ -1,9 +1,10 @@ /* ── Agents page layout ───────────────────────────────────────────────────── */ agents-page { - display: none; + display: none; /* toggled by JS → flex */ flex-direction: column; flex: 1; + min-width: 0; overflow-y: auto; background: var(--bs-body-bg); } diff --git a/web/css/approval-groups.css b/web/css/approval-groups.css index d3ed6a6..bc1c364 100644 --- a/web/css/approval-groups.css +++ b/web/css/approval-groups.css @@ -2,6 +2,7 @@ approval-groups-page { display: none; /* toggled by JS */ + flex-direction: column; flex: 1; min-width: 0; overflow-y: auto; diff --git a/web/css/approval-rules.css b/web/css/approval-rules.css index 0e284eb..9bbc419 100644 --- a/web/css/approval-rules.css +++ b/web/css/approval-rules.css @@ -2,6 +2,7 @@ approval-rules-page { display: none; /* toggled by JS */ + flex-direction: column; flex: 1; min-width: 0; overflow-y: auto; diff --git a/web/css/config.css b/web/css/config.css index 2827d7d..9079d1a 100644 --- a/web/css/config.css +++ b/web/css/config.css @@ -1,7 +1,8 @@ config-page { - display: none; + display: none; /* toggled by JS → flex */ flex-direction: column; flex: 1; + min-width: 0; overflow-y: auto; background: var(--bs-body-bg); } diff --git a/web/css/home.css b/web/css/home.css index d450cec..e3ab5bb 100644 --- a/web/css/home.css +++ b/web/css/home.css @@ -1,9 +1,10 @@ /* ── Dashboard page ────────────────────────────────────────────────────────── */ dashboard-page { - display: none; + display: none; /* toggled by JS → flex */ flex-direction: column; flex: 1; + min-width: 0; overflow-y: auto; } diff --git a/web/css/page-shell.css b/web/css/page-shell.css index fde277d..e70f730 100644 --- a/web/css/page-shell.css +++ b/web/css/page-shell.css @@ -1,5 +1,12 @@ /* ── Custom element display + layout shell ──────────────────────────────────── */ +/* Every page host below is switched to `display: flex` by its component, so its + root
is a flex item. `flex-direction: column` is therefore load-bearing + and not cosmetic: in the default `row`, a flex item is sized by its content, + so a page whose root div forgot `width: 100%` renders as a narrow column in + the middle of the workspace. Column direction makes the cross axis stretch, + which fills the width whatever the inner div declares. Never omit it. */ + tasks-page { display: none; /* toggled by JS */ flex-direction: column; @@ -10,36 +17,16 @@ tasks-page { border-right: 1px solid var(--toolbar-border, #e5e9f0); } -task-running-section, -task-cron-jobs-section, -task-scheduled-section, -task-history-section { - display: flex; - flex-direction: column; - flex: 1; - min-height: 0; - min-width: 0; -} - llm-providers-page, models-hub-page { display: none; /* toggled by JS */ + flex-direction: column; flex: 1; min-width: 0; overflow-y: auto; border-right: 1px solid var(--toolbar-border, #e5e9f0); } -models-llm-section, -models-transcribe-section, -models-image-section { - display: flex; - flex-direction: column; - flex: 1; - min-height: 0; - min-width: 0; -} - projects-page { display: none; /* toggled by JS */ flex-direction: column; @@ -50,8 +37,15 @@ projects-page { border-right: 1px solid var(--toolbar-border, #e5e9f0); } -project-list-section, -project-board-section { +/* The three section multiplexers (`#tasks`, `#projects`, `#models`) render one + sub-section element at a time as their only child. Sized by descendant + selector rather than by naming each section: the enumeration was the bug — + `models-tts-section` was added to the hub and never listed here, so it fell + back to an inline element sized by its content and the page rendered narrow. + A new section now inherits the sizing by existing. */ +tasks-page > *, +projects-page > *, +models-hub-page > * { display: flex; flex-direction: column; flex: 1;