A non-`auto` `scrollbar-width` or `scrollbar-color` makes Chromium and Safari
discard every `::-webkit-scrollbar*` rule for that element, including
`::-webkit-scrollbar-thumb:hover`. Declaring both unconditionally left the
hover tokens rendering nowhere: the engines implementing the hover
pseudo-element are exactly the ones the standard properties silence, and
Firefox has no hover pseudo-element to fall back on. Both hover tokens and all
four elevated surfaces' hover rebinds were therefore dead code.
Measured in chromium on probe elements with `scrollbar-gutter: stable`: an 8px
`::-webkit-scrollbar` alone reserved a 30px band, and adding
`scrollbar-width: thin` dropped it to the 10px `thin` reserves.
The standard properties now sit inside `@supports not
selector(::-webkit-scrollbar)`, so Firefox takes them and WebKit-based engines
take the pseudo-elements. The WebKit rules stay ungated: an engine without
those pseudo-elements drops them as unknown selectors, and gating them would
hide them from an engine that implements them without `selector()` — the
pre-16.4 Safari the ungated form serves correctly.
Three unit assertions pin the split by source offset, which the existing
at-rule-flattening parser cannot see. The web e2e now reads the path chromium
actually takes: the `auto` standard properties as the gate's signature, the
pseudo-element sizing and track, the indirection variables resolved per
throwaway probe, and the hover declaration as cascade rule text — chromium
folds the `:hover` rule into `getComputedStyle(el,
'::-webkit-scrollbar-thumb')`, so no computed query separates the states.
The bash tool already declares the `card: 'terminal'` render intent for
both its call and its result, and host/connection/runtime already deliver
it to the browser as callView/resultView. The Web client ignored it:
rows derived from raw args, and the details panel flattened every tool's
content into one soft-wrapping `<pre>`. Column-aligned output folded into
a paragraph and a long listing stretched the panel without bound.
`TerminalBlock` (ui-primitives) renders a command as a terminal surface:
a shortened-cwd prompt line, output at `white-space: pre` in a
horizontally scrolling box, a head/tail height cap with an expand
control, an exit-code/signal status pill, and a copy control for the raw
output. ANSI SGR runs are parsed with `anser` and resolved onto `--dsw-*`
theme tokens, with literal rgb kept for values the design system has no
token for. Geometry and fonts mirror CodeBlock; the clipboard write both
need moved into a package-internal `clipboard.ts`.
Both Web render sites for a bash call consume the intent through one
derivation (`terminal-card-model.ts`), so they cannot disagree about a
command, its cwd, or its exit status: the keyed BashRow carries the card
resident below its summary row, and the render-site fallback row keeps it
behind its existing expand control. Rows cap at 8 lines against the
panel's 16.
Inline output in the chat row reverses this package's stated
no-inline-output convention, on the owner's explicit decision; the Agent
Note records the reversal and its bound.
Tests: TerminalBlock/ansi/clipboard unit specs, ui-conversation wiring
specs at every render site, a built-client-graph snapshot covering both
chat-row shapes, and a real-browser e2e asserting the no-wrap layout and
the page's own Clipboard API.
The only browser-trust guard covered host.pickDirectory, while the
consequential methods (session.prompt drives bash) accepted any Host —
open to DNS rebinding, where a rebound page reads and writes the API as
if same-origin and only the Host header betrays the attacker's domain.
The pickDirectory-specific loopback guard becomes a prefix-wide fence:
Host must be loopback or an exact host[:port] from the new trustedHosts
config, an attached Origin must equal that authority, and explicit
cross-site markers are refused; requests without browser markers (curl,
tests, native clients) pass, because without a browser there is no
confused deputy. The loopback-socket check is dropped — binding policy
expresses reachability, and the fence is not an auth layer. The Agent
Note records the full threat model and the alternatives.
Remove the duplicate model-facing list tool from this branch; directory orientation remains available through bash ls. Keep the glob sampling fix, add a real ACP composition snapshot, and narrow the decision record to the shipped bug fix.
Master had advanced 392 commits. Every conflict was a derived artifact both
sides had regenerated — `docs/tool-catalog.md`, four recorded headless
session logs, and one TUI terminal expectation — so each was resolved by
taking master's version and re-running the generator and the keyless
snapshot refresh on the merged tree. No source file conflicted.
The new `examples/jsonrpc-agent` fixtures keep master's scrubbed
`{{system}}` / `{{tools}}` headers: that suite scrubs request headers, so a
new tool changes nothing there and the refresh output was discarded.
`ConnectionHandle.sessionSearchResultLimit` mirrored
`SESSION_SEARCH_RESULT_LIMIT` as per-connection state, giving one fact two
homes in the same module and implying a transport-varying or server-negotiated
bound that the response schema's fixed `max` forbids. `SessionsService` now
reads the constant directly and its constructor drops the parameter; the
connection handle and its unreachable `/client` value re-export go away.
The import comes from the inline-safe wire layer rather than the connection
plugin's `/client` surface, which the bundle-purity gate rejects for value
imports.
The review made every name a JSON string with `<`, `>`, and `&` escaped. The
hazards behind that are real and each is now covered: a control character
splits one entry across lines, `</` closes the envelope, and a regular file
named `x@` reads as a socket named `x` under the non-regular marker.
Quote those, and only those. `list` is the tool an agent reaches for first
and its output sits in every transcript, so `"archive"/` on every ordinary
line is a permanent cost for a case that almost never occurs. A name is now
emitted verbatim unless it matches a control character, a leading quote, a
backslash, `</`, or a trailing `@`, and is otherwise a JSON string with `</`
neutralized — the delimiter treatment `dsh-workspace-context` already applies
to instruction text, extended to an interpolated path as its
`instruction-frame-paths` TODO asks.
Registrant-private reactive facts previously reached components as raw
observables that each component subscribed by hand (InputBar notices/
lexicon via uSES, SettingsRoot via a version/subscribe/getter triple).
The inject face now carries a reserved hooks compartment of bare
sources; the renderer binds each into a use<Name> selector hook through
the same machinery as the provide channel, so components consume
useNotices/useLexicon/useSections and never see a subscription
primitive. InputBar and SettingsRoot are the first two consumers.
Verifying browser-visible plugin CSS needs a rebuild build:web does not
perform: WorkspaceBrowser.module.css never reaches apps/web/dist, because
ui-workspace loads as a runtime plugin with its CSS inlined into
lib/client.js by that package's own bundle script. A negative control that
reruns only build:web exercises a stale bundle and passes with the
declaration removed, which reads as a vacuous test rather than an invalid
control. No script in the web lane does this rebuild, so every scroll-region
or plugin-CSS change hits the same trap.
Agent Note for the two changes: what the session log showed, what ordering
can and cannot fix (measured, correcting the first diagnosis), why the page
is sampled only past the cap, why `list` is needed alongside it, and the
alternatives each one beat.
design-platform.css declared four --dsw-alias-scrollbar-* tokens in both
palettes that no rule read, so every scrolling region rendered the user
agent's own scrollbar and the dark theme showed a light native bar against
dark surfaces.
The symptom that surfaced the gap was in the sidebar: the workspace
browser's session list is its only scrolling region, and each row's
trailing content (the relative timestamp, and the hover action buttons
that replace it) is `flex: none` flush against the row's 8px right
padding, so an overlaid scrollbar painted on top of the timestamp.
ui-theme/styles/scrollbar.css becomes the sole consumer of the four
tokens, imported by the web shell's base.css after design-platform.css
because it reads that sheet's tokens. The rules sit on `body`, not
`html`: the alias tokens are declared on `body`, custom properties
inherit only downward, and from `html` they resolve to the
guaranteed-invalid value with scrollbar-color computing to `auto`.
scrollbar-width and scrollbar-color are declared on `body, body *` rather
than inherited, because inheritance would carry the color already
substituted at `body` and an elevated surface could not retint its own
thumb; scrollbar-width does not inherit at all.
Both the standard properties and the ::-webkit-scrollbar pseudo-elements
read one indirection pair bound to the l1 tokens, so an elevated surface
rebinds that pair to the l2 tokens once and retints both renderings. The
command popup, slash menu, model-select panel, and settings panel do so,
which gives the l2 tokens their first consumers.
WorkspaceBrowser's `.list` declares scrollbar-gutter: stable, keeping the
bar beside the rows. `stable` rather than `auto` so the reservation holds
when the list is short enough not to scroll: expanding a workspace group
would otherwise shift every row sideways at the moment it starts
scrolling.