# Web App: config forms stop inviting saved logins, panels and pages stop growing scrollbars, blank replies and duplicated outcomes disappear, and Benchmark costs follow the selected currency

Four rendering and input-handling fixes: the browser stops filling account credentials into model configuration, a closed docked panel stops painting a stray 1px divider next to the open one, a provider-signature message that carries no text stops drawing an empty reply bubble, and Benchmark costs use the display currency selected in settings.

## Browser autofill no longer drops account credentials into model configuration

Opening the model dialog could hand the browser's saved login straight into the configuration: the account password landed in the API-key box, the username in whichever field sat above it. Two causes, both of them the browser doing what it thinks is helpful:

- The dialog's fields belong to no `<form>` element — the app has exactly one real form, on the login page — so the browser groups the page's unowned fields into a synthetic one and picks a "username" box on its own.
- The API-key box already declared `autocomplete="off"`, which **Chrome and Safari ignore on a password field**. They offer the saved login anyway.

Every shared form control now opts out unless its caller declares a real credential role. Fields go out as `autocomplete="off"`, and an opted-out *secret* field as `autocomplete="new-password"` — the one value password managers read as "not the account password" — with the manager-extension opt-outs (1Password, LastPass, Bitwarden, Dashlane) alongside, since those read their own attributes rather than `autocomplete`. Nothing in the app's forms wanted autofill in the first place: they hold API keys, model ids, URLs, directories and prices.

The login page and the password dialogs are untouched — they declare `username` / `current-password` / `new-password`, and a declared role passes through as the caller's decision, so saving and filling a real password keeps working.

The policy lives in the shared `Input` / `Textarea` rather than at each call site, so it also covers the pages that were never reported (Agents, skills, schedules, the vault); the handful of raw inputs that bypass those components — the model and skills menu search boxes, the goal budget, the Workspace path editor — carry the same opt-out explicitly.

## The closed docked panel no longer paints a stray divider

Opening the Workspace files panel showed what looked like a second, empty panel wedged between the conversation and the files: a hairline, a gap, then the real divider.

Both docked panels (Workspace files, agents) stay mounted while closed — the width transition needs the node — collapsed to a zero-width clipping window. Under border-box sizing their left border still painted its 1px there, so the closed agents panel left a line stranded beside the open one, with the open panel's resize gutter as the gap between them. With both panels closed, the two leftover lines stacked at the window's right edge.

The divider now belongs to the open state only, so a closed panel takes no width at all and exactly one divider separates the conversation from whichever panel is open.

## No more empty reply bubble after a thinking segment

An empty `assistant:` bubble sometimes appeared right after the model finished thinking — no text, just a blank block taking up a line.

It was not a stray empty string. Core deliberately emits a **complete text or thinking message with an empty body** when a provider attaches an opaque `fidelity` payload to an otherwise empty part — a Gemini `thoughtSignature` on a text part, a GPT-5 `fidelity.phase` segment marker, GPT-5's encrypted reasoning on the thinking side. That message has to exist, or the signature is lost from history and the next request goes out without it. It simply has nothing to display, and it lands at the thinking→text boundary, which is why the gap showed up after thinking and only on some providers and some turns.

A blank body now produces no item at all, on both paths that can deliver one. When the message arrives on its own — a history rebuild, or joining a stream mid-flight — it is skipped rather than appended. When a whitespace-only segment actually streamed (core opens a text segment on the first *truthy* delta, and `"\n\n"` is truthy), the in-flight fragment is discarded rather than settled in place, so a live view and a reloaded page render the same thing instead of the blank bubble disappearing on refresh. Whitespace-only counts as empty throughout, matching what the reply-copy path already treated as nothing.

## Benchmark costs follow the selected display currency

Benchmark costs used to stay in USD after the display currency was switched to CNY. The page called the shared money formatter without the selected currency, so its USD default remained active while the model library, Cost center, chat, and Trace views followed the setting.

The cost trend axis and tooltip, evaluation totals, case totals, and individual run costs now all receive the selected display currency. Score and duration formatting are unchanged.

## The tool card row states an outcome once

The collapsed tool card said the same thing twice: the status icon on the left already distinguishes done from failed from aborted, and a pill to the right of the duration spelled the word out again. Both pills are gone — the call's and the output's — leaving the icon, with its title and accessible name, as the single source.

## A page can no longer grow the document into a second scrollbar

With a second Agent below a long session list, the Traces page and the Agent settings page grew the **document**: a second scrollbar appeared, and dragging it pulled the whole app shell up, leaving blank space at the top. Both import controls keep their file input visually hidden but Tab-focusable, which means `position: absolute`, and neither scroller was a containing block — so nothing clipped those boxes, and their position past the fold became document-level overflow. One Agent hid it entirely: that control sits at the top of the tree.

The rule now lives in one place instead of in each scroller — every scrollable container is its own containing block, declared once in the stylesheet — and the visually-hidden file input became a shared control carrying its own containing block wherever it is dropped. An end-to-end sweep guards the invariant across the pages: with two Agents and a list long enough to push the second past the fold, no page may grow the document.
