The keyed toolviews (read, file-mutation, web) each draw the same ToolRow
summary-line chrome; the read row's copy trips the duplication gate against the
file-mutation row. Extracting the shared row is a separate change tracked for
all rows at once, so this scopes the exemption to the read row's block.
A first visit resolved to Chinese regardless of the browser: LocaleService
read `dsh.locale` and fell straight back to `zh` when nothing was stored,
ignoring the languages the browser already states it reads.
The initial locale now resolves through three ordered sources — the persisted
preference, then `navigator` (first entry of the ordered language list whose
primary subtag names a shipped locale, so `zh-Hans-CN` -> zh and `en-GB` ->
en), then `FALLBACK_LOCALE`. An explicit choice still wins and nothing writes
the detected locale back to storage, so "has the user chosen?" stays a
question only the stored value answers.
Specs asserting the shipped Chinese copy now state the browser they assume:
the web e2e scenarios open their page with `locale: ZH_BROWSER_LOCALE`, and
package specs pin it through the new `pinBrowserLanguages` test helper.
`settings-chrome.e2e.ts` gains an English-browser scenario as the
assembled-app proof.
The composer paints its draft in two layers — the textarea owns the value,
the selection and the caret, the backdrop paints every visible glyph — and
they had one scroll offset each, kept equal by a `scroll` listener. That
holds at rest and not in motion: a wheel gesture scrolls the textarea on the
compositor, the listener runs afterwards, and for those frames the caret sits
at the new offset with the words at the old one. Measured on a harness of the
same geometry, a 200px offset change separates caret from glyphs by ~200px
(chromium 203, firefox 202, WebKit 203) until a later frame — the caret
flying out of its own text when a user swipes a long draft quickly.
Both layers now ride one scrollport: `[data-input-scroll]` carries the
14-line cap, the auto-grow stack inside it is as tall as the whole draft, and
the textarea holds no scrollable overflow of its own. The browser applies one
offset to both layers in the same frame, so the coupling is structural rather
than maintained. The backdrop's trailing-line sentinel and the cross-engine
wrap-width premise go with the mirror: the layers now share a containing
block, which closes the WebKit 768-against-776 gap by construction.
The test inherited an open dialog with the DeepSeek editor already expanded
from the credential test above it. That test now reloads the page to
exercise the welcome step, so nothing carries the dialog across and the
catalog test timed out looking for a fold that was no longer on screen.
The review flagged this coupling as two links deep; the merge proved it.
Review follow-up: the llm/adapters-updated listener's disposer was
discarded, leaving it firing (harmlessly, behind isDisposed()) between
TUI shutdown and fiber disposal, asymmetric with the sibling channel
listeners. The controller now exposes detach(), and the channel's
detachListeners() calls it on both the dispose() and startup-failure
paths.
The TUI still renders the unlisted current model as its own row and marks it
current; Web shows the unset trigger label and asks for a replacement. The
note recorded the Host decision but not that the two surfaces present it
differently, which reads as a missed migration rather than a choice.
Also records why `trigger.selectAria` and `trigger.fallback` hold identical
strings, so a future coalescing cleanup does not merge them.
The previous assertion pinned the literal `bg-layer-3` fill that was just
reverted. What matters is the relationship it broke: `bg-layer-3` and
`bg-module-platform` both resolve to neutral-bluish-800 under the dark
theme, so filling the row with either erases the nested editor's boundary.
The typed text was held in one active buffer, so editing a second row
displaced the first — which then fell back to rendering its stored NaN as
the literal `NaN`, losing the text the row was supposed to let the user
correct. Reset dropped the override but kept the buffer, leaving typed text
over an inherited row that no settings layer stores, and an unreadable
buffer never settles, so it stayed there.
There is now one entry per row. `remove` re-keys around the dropped row so
text travels with its own row, and reset clears them all because the rows
they annotated are gone.
Model ids are compared trimmed as well: `" "` passed the required check,
and `"model "` slipped past the duplicate check against its own twin before
silently failing to match at the adapter. A pasted id settles on blur rather
than being trimmed per keystroke, which would stop an interior space.
The master merge added a required archivedSessionIds field to WorkspaceListState;
the read-card spec's DetailsPanel fixture state needs it to typecheck.
The card is a React child of the wrapper, so capture-phase presses on it
reached the wrapper's dismissal handler: the first pointerdown of a text
selection closed the card, contradicting its JSDoc contract. Restrict the
immediate close to presses outside the card, keeping it mounted under a
held press (and the browser click with it), and align onPointerLeave's
grace arming with Menu (only while open).
Pin both new behaviors in hover-card.spec and update the bilingual Agent
Note.
Restore defaults dropped the user's `models` override from the draft but the
rows kept showing it, so the catalog only looked restored after closing and
reopening the card.
The inherited rows were read from the namespace's effective value, which
still carries the stored override until the unset is applied — so dropping
the override echoed it straight back. They now come from the layer beneath
the user's: what the composition entry pinned, or else the schema default
that resolution would supply.