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 master merge added a required archivedSessionIds field to WorkspaceListState;
the read-card spec's DetailsPanel fixture state needs it to typecheck.
Public snapshot state stays in the store engine's plain-data vocabulary
(immer drafts reject Sets without the MapSet plugin, which stays off):
manager/service/contract carry readonly SessionId[] in Host order, and
the tree derivations build their own transient Set — the
expandedProjects pattern. Membership-unchanged installs still keep the
array reference for Object.is short-circuits.
WorkspaceListState gains archivedSessionIds (ReadonlySet, replaced only
on membership change), installed as full snapshots from the list
baseline, the unary echo, and the changed frame. Archiving the current
session clears the selection into the New Session view state. Test
doubles (test-runtime, fake APIs, fixture client) follow the widened
IWorkspaces/IApiClient faces.
Review caught a real divergence the earlier measurements missed: mirroring
an offset is only correct while both layers can reach it, and for a draft
ending in a newline the backdrop could not.
A textarea reserves a line box for the caret after a final newline.
`white-space: pre-wrap` collapses a text node's trailing newline and
generates none. So a draft ending in a newline made the backdrop exactly one
line shorter than the textarea — measured 628 against 652 — and the mirrored
assignment clamped, leaving the glyphs one line behind the caret at the very
bottom of the draft.
The backdrop now carries the same trailing-line sentinel the mirror div has
carried all along: its content is the decoration walk plus one newline. The
same pre-wrap collapse absorbs it when the draft does not end in a newline,
so it costs no height in the ordinary case, and it supplies the missing line
box when it does. Verified in isolation first: a bare pre-wrap div measures
180/180/198 against a textarea's 180/198/216 for zero, one and two trailing
newlines, and 180/198/216 with the sentinel.
Coverage for the shape that exposed it: the browser scenario asserts the two
extents are equal before asserting the glyphs reach the end, observing each
layer's maximum by asking for an impossible offset and reading back the
clamp rather than computing it from scrollHeight, and the golden records the
relation. The unit spec pins the backdrop's text as the draft plus exactly
one newline. Removing the sentinel fails both, the e2e with the same 628
against 652.
The scrollbar-gutter half of the same review point does not reproduce here:
both layers measure clientWidth 776 against a border box of 776 while the
draft overflows, so this engine's textarea scrollbar is an overlay and takes
no width out of the wrap.
The first version coupled the layers from two places: a `scroll` listener
and a layout effect keyed on the committed draft. Mutation-testing each
hook alone against the built client shows the effect never fires the only
assignment that matters — with just the layout effect disabled the browser
scenario stays green, while disabling just the listener fails it.
Both premises behind the effect were wrong. Typing scrolls the caret into
view, which is an ordinary `scroll`. A draft that shrinks past the current
offset clamps both layers to the same maximum, because their extents are
equal — measured in chromium at 964/964, 1012/1012, 844/844 and 820/820 for
plain, soft-wrapped, unbreakable-run and highlighted drafts — and the
textarea's clamp fires `scroll` too.
The hazard the effect was imagined to cover does not exist either: React
replacing every child of the backdrop when the decoration set changes shape
preserves `scrollTop` (measured: 300 stays 300 through a full child
replacement), and the only replacement that zeroes it shrinks the content
below the offset, which is the clamp case already covered.
The e2e's edit case survives, retitled to say what it actually pins: that
typing is not a separate case needing its own mirror. The unit spec now
asserts the backdrop tracks a second move back to the top, which a one-shot
mirror would fail.
A composer draft past the 14-line cap could not be scrolled: the caret and
the selection moved, but the words stayed frozen at line 1, so the tail of
anything longer than the cap was unreachable while writing it.
The composer paints its text in two stacked layers. The textarea owns the
value, the selection and the caret but renders its own glyphs transparent;
every visible character is painted by the decoration backdrop beneath it,
which also carries the claim-token highlight, the chips and the ghost hint.
The backdrop is `inset: 0; overflow: hidden` — clipped, not scrolled — and
nothing linked its offset to the textarea's. Below the cap both layers rest
at 0, which is why the defect hid behind every short-draft screenshot and
fixture.
InputBar now mirrors the textarea's scrollTop onto the backdrop, from a
`scroll` listener (every gesture and every caret-driven scroll) and from a
layout effect keyed on the committed draft (an edit reflows both layers
without necessarily firing a scroll event).
Scrolling is layout, so jsdom cannot show this: the unit spec stubs both
offsets and proves the mirroring paths run, while a new browser scenario
measures the user-visible fact against the built client with a DOM Range
over the backdrop's own text — after a wheel gesture over a 40-line draft
the last line is on screen and the first has scrolled out. Confirmed both
directions: with the mirroring reverted and the packages rebuilt, the
golden reads `last draft line is on screen: false` while `textarea moved:
true`.
master's locale rollout renders the details Output section title through
t('details.output') (输出 under the zh seat the test uses). The spec still
asserted the English 'Output' literal and failed. Match the sibling
terminal-card suite's localized assertion.
master's locale rollout made `t` a required prop on GenericToolCardProps and
the details slot; the web-card spec predates it. Supply the makeTranslate seat
the sibling suites use.