The client inspect catalog follows the seam signatures, the unknown-cast
baseline drops the two casts this branch retired, and the README pairing
records match the merged prose.
Master's rows and menus carry no answer of their own, so the served-page seams
thread the Slot-standard access Hooks through the workspace browser and the
session actions: New Session, workspace rename/delete/reorder, and a Session's
rename, pin, archive, and reorder appear only while their own resource allows
them. The rows spec keeps master's pointer-armed rename helper and supplies the
allow-all answers as props.
The owner-baseline gating commits are superseded by master's whenAccess seams,
so the remaining records revert to the master shapes: the deliverables opener
takes no owner answer, the Japanese dictionaries carry the surviving copy, and
the test splice and owner assertions match the current services.
Read the slot injection through its real rest-parameter function type and invoke it directly, so the static gate sees no new assertion to unknown. The client inspect catalog's expected uiWorkspace signature follows the generated catalog after the fork options became ForkSessionOptions.
P1 keeps only the client/host extension seams; the requestId deduplication,
the cancel recheck, and the named fork child are deferred to a later change.
The package, its README pair, and the persistence schema lines return to the
master state, so the branch adds no session-controller behaviour.
Type the tracked binding disposers by their real shape and let a disposed
context's shared stop surface its own loud unavailable error instead of
reporting a withdrawn permission.
The button re-read the live session/cancel answer but the fixed shortcut
received the raw stop, so a withdrawal still sent Esc-Esc cancellation. Build
one gated stop used by both entry points and cover the shared gate.
A rejection may be null, undefined, or any non-object; reading rpcError through
it threw a TypeError and replaced the branch-refusal notice with a crash. Narrow
the value first and cover the non-object rejections.
The long-lived plugin effect wrapped every Session-scope effect, so a released
binding's Context stayed referenced until plugin unload. Track the binding-scope
effects in a map that the binding cleanup and the plugin unload both drain;
nothing long-lived closes over a released binding.
The recheck used allows(), the list-filter view that never sees call arguments,
so a policy narrowing cancel to args.request refused a legitimate caller as
access-withdrawn forever. Ask decide() with the principal and the request, keep
the policy's own refusal reason, and cover both an args-independent role and an
args-dependent condition.
A first attempt that materialized the child and then failed at attachSession
was reported as failed, but its retry adopted the stored child and returned
success before attaching it, leaving the child ungrouped. The adoption path now
completes the missing attach and reports the same session/workspace-attach-failed
if that still rejects.
The Gateway already admits each call; P1 no longer re-reads the caller's access
after prompt admission. Cancellation still refuses delivery when the caller
aborts during admission, and the requestId dedup/conflict, cancel recheck, and
fork naming behavior stay. The README pair follows.
The published UiWorkspace no longer requires openListedBranch: that wait is an
internal navigation helper, and the service forkSession takes the branch
boundary and presentation choice as optional inputs. The Chat branch surface
now supplies only its branch point and analytics and lets Workspace navigation
decide whether the child may be named and when it becomes presentable.
A refused branch now reports itself on the owner's own page: the Chat
injection writes the notice on the addressed Session and the composer bar
renders it as an alert, so the owner-visible change has an assertion at both
seams.
The composer hook source and the drop gate now resolve the same live intake
answer from InputHub, so the policy and its two readers cannot drift; the
caller keeps ownership of the rejection copy.
A bar registration that cannot prove live file-intake permission omits the
source; the bar then offers no file controls, matching the no-session
placeholder's fail-closed answer. PropsHooks keeps required hook seats intact
and lets an optional source leave its seat undefined.
The composer notice store has no production reader through the published
SessionInput seam: the bar renders it through ComposerBarInjected.hooks.notices
and the only other reader was a test. Leave the store on SessionInputShell,
which the hook wiring uses, and assert the bar's hook in the Chat spec so the
contract surface only publishes members with real consumers.
The model-visible catalog inherited two versions of the same JSDoc sentence,
one of which contradicted the other. Describe the blank reuse once: the first
reusable listed blank, the requested preset or its default, one shared attempt
per Workspace and preset. The generated client inspect catalog follows.
Close the Session Controller's remaining per-file coverage debt: a named
fork adopting the child its earlier attempt stored, refusing a name another
Session holds or one seeded at a different cut, and the named-child
observation failures; prompt admission sharing one in-flight requestId and
refusing a duplicate from another caller; the retry read from a drained
splice; the access-withdrawn and aborted-admission delivery paths; cancel
after the caller's grant is gone; and the Assistant settlement that appends
past the last entry. Test-only: no production behavior changed.
The Gateway admits a prompt before the controller sees it, so a binding narrowed while the prompt waited must not still deliver: the Remote method passes a predicate that re-reads the caller's access after every await and immediately before delivery, and cancel re-reads it in the same synchronous step. One requestId still produces one message; a retry from another principal is refused as session/request-conflict. Fork callers may name childSessionId so an interrupted attempt resumes the child it stored; any other occupant of that id is refused as session/fork-conflict. The agent-controller admission pipeline this previously built on is not part of the current master, so the prompt keeps the direct admission path and the tool-execution turn carrier is gone.
The Workspace tree gains the Host-published branch projection plus the
access-snapshot reads its rows, menu entries, and shortcuts consult; the
Conversation package moves branch handling into its own module and exposes the
input and slot declarations a page-local plugin composes. Chat, sidebar,
deliverables, and open-in-app publish the slot surfaces those readers fill, and
the document-relative route and HMR endpoint seams keep the served page's own
base.
Both revocation cases restated the same grant and revoke helpers; one module-level pair keeps the two
cases reading the same install and disposal order.
The release trigger treats a user-sourced inbox insertion as a user steer and
only this wait responds to it. Both are known approximations for the
short-term fix: a shared steer signal should let every waiter decide locally
and let the input's producer own the decision. Mark both spots for the
follow-up and keep the public jobs contract unchanged.
The recorded-session fixture is a scenario-owned plugin that imports
`@deepseek-ai/dsh-llm` at runtime. Declare the workspace package at the root
so the snapshot lane resolves it from a clean install instead of relying on a
local node_modules layout.
`snapshots/sdk/background-wait-steer` replays a keyless SDK session whose
`job_output(wait: true)` is released by a user steer: the fixture steers once
the waiter registers and the producer has written its barrier line, and the
scenario asserts the early return with `user steered`, the surviving producer,
and exactly one completion notice for the settlement that the released read
did not consume.
The authored session takes its job id from the live request with
`{{fromRequest:(bash-[0-9a-f]{16})}}`, so an opaque id needs no fixture edit.
Registry-level cases pin the release claim, the settlement the read carries,
the withheld notice a cancelled read returns to delivery, parallel waits on one
job, and the model-killed settlement that must not repeat. A real AgentLoop
case covers the user steer releasing the wait while the producer survives.
A `job_output(wait: true)` wait now ends when the user steers the current
step, returning the job's real state and the output already read; the
producer keeps running and the pending input stays queued. The wait is ended
by a controller the tool owns, so the registry's abort semantics — and every
other consumer of `wait` — stay unchanged. The model sees why a running job
came back early in the unchanged status line (`user steered`).
A settlement that lands between the release and this call's own read is
carried by that read instead of a second notice; if the read never commits,
the notice is delivered when the claim releases. Settlements already delivered
elsewhere (the model's own `job_kill` result or another waiter's collection)
are not repeated.
- State the contract precisely: the cut never introduces a lone high
surrogate, and an unpaired surrogate the input already carries is not
repaired.
- Document the new export in the package README: a character-budget preview
section naming the persistent bash and pwsh shells and str_replace_editor,
plus a Known Limitations bullet that no longer contradicts it.
- Drop the assertion restatement from the two shell test comments.
- Let the str-replace test helper reuse a root so the clipped context reads
its file inside its own working directory.
The shipped Web composition gained the four Host Schedule tools, and the
other four minimal-agent assertions in this file already expect them. The
legacy profile-patch scenario still expected bash alone, so master's E2E
lane failed on it. Align it with its siblings.
Conflict resolution:
- docs/config-catalog and docs/module-graph: regenerate with the current
generators, which write the bilingual pages together.
- The conflict came from master's new short-fingerprint pairing records: our
branch still carried the previous full-hash records. Take master's record
format, then re-record the output-retention README pair after regenerating
its two sides.
The sidebar toggle is flush with `.logoRow`, which clips its own overflow, so
the standard outward ring lost every pixel that left the row: the reviewer saw
the ring cut on the toggle in both the expanded header and the settled rail.
The 36px rail toggle is also 1px wider than its 35px row, so the control's own
right edge was clipped there. Draw this control's ring inside its own box and
stop clipping the row once the rail has settled; the ring keeps the standard
`--dsw-focus-ring-width` colour and width, and pointer modality still resolves
`--dsw-focus-ring-color` to transparent.
`focus-rings.e2e.ts` asserts the whole ring against every clipping ancestor in
both layouts, in both themes.
Only the ui-theme README pairing record conflicted; re-recorded. The stylesheet
corpus spec passes over master's new client sheets, so no ring needed realigning.
`.selected` still named `--dsw-alias-brand-primary-new-colorprimary-new-color`, the
corrupted token left by a botched rename; its dark value is `deepseek-450` while
every focus ring now uses `deepseek-400`. The selection ring reads the clean
brand-blue alias instead, so one blue covers selection and focus.
Only the bilingual pairing records conflicted. Master moved Agent Note records to
per-heading hashes, so this branch's note record was re-recorded in that format;
the ui-primitives and ui-theme README records were re-recorded unchanged. The
stylesheet corpus spec still passes over master's new client sheets, so no ring
needed realigning this time.
master released 0.1.7-rc.1 while the package added by this branch still declared 0.1.7-alpha.2, which failed Dependency layout (verify-npm-install-layout), Pack npm tarballs (release members must share one version) and node 24 / static (constraints) from one cause. Merge origin/master and bump the new package to the root version.
The page carried a 28px top inset above its head row, so the window's topmost 28px on that page belonged to no drag region — the frame's former band covered it, and the per-row mark did not. The inset moves into the head rows themselves (.pageHead and .detailTop), which now start at the window's top edge while the content keeps its position. The browser lane pins each row's top edge at zero and the negative control confirms both assertions fail on the pre-fix sheet.
Resolving packaged-app icons grew the embedded C# until the generated
-EncodedCommand line reached 32,883 characters for a 222-character path,
past the 32767-character CreateProcess limit, so association queries stopped
starting for long multibyte paths. Write the adapter to a private temporary
script and pass it with -File instead; the command line no longer grows with
the path. The temporary directory is removed once the call settles.
The icon fallback also reads the handler name the caller already fetched
instead of calling GetName again, and the README records that icon
visibility depends on resolving the Shell's indirect reference.
With no OS-marked default the split button labelled the first registered
application and marked it "(default)" in the menu, but the primary action
still asked the OS to resolve the association again; on Windows that can
prompt or open a different application than the button promises. Send that
application's id explicitly and keep the association path for the case the
Host actually marked a default.
The snapshot scenario now clears the platform's own default marker and
re-enters the file, so the association is read again instead of reusing the
answer already held, and it asserts the launched target rather than only the
labels.
Review threads: shrink READ_LANG_BY_EXTENSION to the suffixes whose own name is the better label (tsx/jsx/tf/tfvars/gradle) since the other 41 values already equal the language short name; label .hcl as hcl and .tfvars as tfvars instead of tf; accept .groovy alongside .gradle with its own short name; document that LANG_ALIASES also carries the persisted short ids; route the three inline alias lookups through grammarForHint; state that a short name may equal the grammar id; fix the util README wording.
Tests: cover .groovy, the four self-naming suffixes, and every id each suffix reaches through the shipped surfaces (the hand-synced LAZY_ALIASES list could not catch an alias whose target is neither boot nor lazy).
The retained-tab-layout note still stated that existing window-drag region pulses remain; this layer's pulse is gone and the shell watcher replaces it. Rewrite both sides and re-record the pair.
The recall watcher now tolerates a short quiet window so a report that arrives before a CSS transition moves anything cannot end the loop on its first unchanged sample, re-arms on a transition or animation that touches a marked row, and re-arms on a child list change in a container holding one. A dock picture withdraws its strip's drag claim while a pane floats, because a float sorted before that row would otherwise sit earlier in the document and lose its own subtraction. The settings panel closes when an onboarding step appears, since the step's overlay marks only #root inert. Prose and the two Agent Notes record the Windows narrowing of the Platform overlay return bar, the collection rule, and the rows' one home in the ui-theme manifest.
Master's newest commits add `.close` in ImageLightbox and `.imageButton` in
MarkdownText, each declaring the brand token directly. Both now read
`--dsw-focus-ring-color` through `--dsw-focus-ring-width`, as the stylesheet
corpus spec requires of every client focus rule.
The new no-default scenario cleared the macOS application catalog, which
does nothing on Linux: `gio mime` owns the OS default there, and the
Linux stub never wrote the launch log, so CI timed out waiting for the
application gesture. Clear the marker each platform actually reads, log
launches from both stubs, and compare the launched id against the
platform's own registration id.
Conflict resolution:
- docs/config-catalog: regenerate the English page; the Chinese page follows the
three shifted Config sources and the bilingual pair record is re-recorded.
- docs/module-graph: the generator rewrites all three artifacts together.
- packages/shell/tool-pwsh-persistent/src/index.ts: keep master's rewrite of the
prompt handling and re-apply the surrogate cap and its import.
- The three package.json dependency entries follow master's workspace:* range
convention, and the lockfile importer specifiers match them.
Master's newest agent-preset sheet carries both the new `.helpButton` and the
retained `.iconButton`, whose focus ring still declared the brand token
directly. The stylesheet corpus spec rejects that form, so it now reads
`--dsw-focus-ring-color` with the brand-token fallback.
ui-theme/README.md, README.zh.md and README.i18n.yaml conflicted again: master
extended the `design-platform.css` sentence with the `--dsw-alias-file-diff-*`
palette. Kept that sentence together with this branch's `focus.css` paragraphs
and the seven-sheet entry list, then re-recorded the pairing hashes.
The reported defect is the branch where the Shell lists associated
applications but marks none as the default: the split button promoted
"Show file location" to the primary action and hid the applications
behind the menu.
Record that state in the keyless document-preview scenario. The new
golden pins the primary action to the opening control, and the gesture
log asserts the main button opens the file while the menu's marked
default launches the first registered application.
Two focus treatments still named a colour of their own: the TurnNavigator
mark's tick filled with `state-business-primary` under a comment that already
claimed the brand colour, and the plugin inventory search input drew its focus
border in `state-business-primary` while the shadow beside it read
`--dsw-focus-ring-color`. Both now read the same ring colour as the rest.
`Tooltip` states why it takes `pointerModality()`'s wide answer while focus
rings read the narrower `data-input-modality` attribute the module publishes.
The ui-web source map gains the two window-drag modules and its boot row names the watcher install; ui-settings-general states that the settings panel portals beside #root. Both pairs are re-recorded. The ui-settings-general manifest keeps master's dependency order so only the two new dev entries appear in its diff.
`welcome-startup.spec.ts` imports the unpackaged main entry, which now
requires `DSH_DESKTOP_PRIMARY_RUNTIME_DIR`. Stub it beside the neighbouring
`DSH_DESKTOP_*` values so the suite reaches the welcome startup it covers.
`prepareDevelopmentProject()` resolved the build target from the host
platform and architecture, so a Linux host threw `unsupported target
linux-x64` while writing the runtime descriptor. Pass the target in
instead: the launcher supplies the target it prepared, and isolated tests
supply a fixed supported target.
Extract `desktopTargetPlatform()` into desktop-build-paths.mjs so the
target-to-payload mapping has one home, and use it for the qualification
runner, which previously derived both its target directory and its
descriptor from the host architecture.
Correct the README limitation to name the module that now performs the
architecture check.
pnpm install --lockfile-only, run to align the new workspace edge's specifiers, also re-resolved unrelated entries: it pinned registry.npmjs.org tarball URLs on the two libreoffice-kit optional platform packages and downgraded glob 7.2.3 to 7.2.0. The tarball URLs fail verify-lockfile-policy against the mirror's published metadata.
Rebuild the file as the base lockfile plus only the three intended importer additions (the new package's block and its two consumers), so the resolution entries stay byte-identical to master.
The Summary narrated the change ("the suffixes the old read table recognized keep their historical value") rather than the contract, and at 109 words it exceeded the 100-word package Summary limit.
State the rule a reader at HEAD can act on: a suffix a recorded session already holds keeps its persisted value, every other suffix takes its language's short name. The dropped path-separator and leading-dot detail stays documented under Language selection. Both languages updated and the pairing record re-recorded.
The package joined the workspace as version 0.1.6-alpha.2 while the root release moved on, which failed three CI jobs from one cause: constraints (version must match root), Dependency layout (verify-npm-install-layout finds no workspace version), and Pack npm tarballs (dsh release members must share one version).
The open paragraph accepted Explorer's exit code 1 as a delegated handoff but
did not repeat the reveal paragraph's warning that the acknowledgement proves no
application opened the file, which the Agent Note records for the Remote answer
`{ opened: true }`.
The new JSDoc and test prose narrated the change instead of the shipped
behavior: "pre-unification", "the old read table", "used to persist ... it
now persists". A reader at HEAD cannot resolve those stages, and the
persisted-value rule they describe is a durability contract that stands on
its own.
Restate each as the present contract: a suffix whose value a recorded
session already holds keeps it byte-identical; every other suffix takes its
language's short name. Rename the test's transcribed expectation away from
the branch vantage and re-wrap the module comment the earlier edit left
over-long.
On Windows 11 ARM64 build 26200 Explorer splits its own command line at
commas and equals signs and rejects percent-encoded non-ASCII in a file
URI, falling back to the Documents folder in both cases without reporting
it. `explorerTarget()` now escapes `=` next to `,` and decodes Node's
non-ASCII escapes back to the literal characters the shell resolves, so
the two rules stay in one place. The Agent Note and the target-encoding
table record the measured behavior.
Four focus rings added on master after this branch's base still declared their
own colour: PresetGuideDialog's panel, CodeCard's action (state-business-primary),
SegmentedControl's tab, and AccountSection's link button (button-primary-fill).
The stylesheet corpus spec now rejects each of them, so they read
`--dsw-focus-ring-color` with the brand-token fallback like every other ring.
CodeCard keeps its 1px dense width; the other three take
`--dsw-focus-ring-width`.
Conflicts and how they were resolved:
- ui-agent-preset/AgentPresetSection.module.css: master replaced the icon-only
tooltip actions (`.iconButton`, `.iconDanger`, `.revealedPath`) with
`.cardHelp` + `.helpButton`, which deleted the one declaration this branch
edited. Took master's structure and re-applied the branch's change to the two
`.cardMain:focus-visible` rings, which master kept unchanged.
- ui-theme/README.md and README.zh.md: master documented the separately exported
`brand-font.css`; this branch documented `focus.css`. Kept both, with the
dynamic entry's sheet list at seven sheets including `focus.css`.
`README.i18n.yaml` re-recorded with `verify-translation-pairing --write`.
The recorded exit code 0 after about 30 seconds was not reproduced: an SSH
session 0 on Windows 11 ARM64 build 26200 measured exit code 1 after about a
second with no association invoked, the same exit code a delegated open
returns. The README, both Agent Note languages, and the `explorerTarget` JSDoc
now record that measurement, and the `Invoke-Item` contrast keeps its
qualitative part without the timing.
Pointer suppression previously cleared `outline-color` on the focused element
alone, so a ring painted by a descendant, a pseudo-element, or a `box-shadow`
stayed visible, and editable text controls lost the focus feedback they need on
click.
Ring colour now flows through `--dsw-focus-ring-color`, which component outlines
and focus-ring shadows read alongside the brand token. Every ring that inherits
the variable is suppressed together, and `body :focus-visible:not(:read-write)`
limits the suppression to rings while editable controls keep their own feedback.
Keyboard modality returns on Tab, Home, End, PageUp, or PageDown, and on focus
reaching a different control after a non-composing key. IME composition keys,
refocusing the same control, and window blur leave rings silent. The desktop
preload menubar suppresses its own ring inside its shadow root, where the
document-level attribute does not reach.
A verbatim `\\?\` prefix and a `\\.\` device namespace take Node's UNC
resolution, and the comma encoding has to survive them. The drive-root case is
pinned separately because Node restores a trailing separator against the host's
`path.sep`, which differs on the Windows host this opener runs on.
master grew a fourth drag owner after this branch: the Platform overlay's return
bar (ui-settings-account) declared `app-region: drag` on its own header. The
branch absorbs it into the contract — the header marks itself `data-window-drag`,
its own app-region drag/no-drag rules are deleted, the ui-theme manifest pins the
row and its 48px height, and the Agent Note's row list names it — so the tree
declares the drag surface in exactly two places again (the shell's mark rule and
the Windows caption row).
Earlier resolutions from the first merge stand: the dockkit strip keeps its mark,
the sidebar-right pulse stays deleted, ui-settings-general keeps upstream's
dsh-settings and this branch's react-dom, and tsconfig.host.json takes upstream's
`files: []`.
Checks: build, build:web, typecheck, test:gui, the ui-theme/base-styles gates, the
window-drag-coverage lane, doc-sync and translation pairing.
Conflict resolutions:
- ui-dockkit TabPanel: keep upstream's reworked tab strip and re-add the row's
`data-window-drag` mark on `css.tabStrip`.
- ui-sidebar-right SidebarRight.tsx/.module.css: keep upstream's retention
rewrite and re-delete its hand-written one-frame app-region pulse; the shell's
window-drag watcher owns recollection.
- ui-settings-general/package.json: keep upstream's `@deepseek-ai/dsh-settings`
dev dependency and this branch's `react-dom`.
- tsconfig.host.json: take upstream's `files: []`; client sources belong to the
client aggregate.
- pnpm-lock.yaml: master's lock plus only the react-dom importer entries.
The consolidated commit refreshed unrelated transitive resolutions while it
resolved the settings portal's new react-dom dependency. Restore those entries to
their previous versions and keep only the two importer entries the portal needs;
the lockfile already carries both snapshots.
The shared code-language table must not claim a suffix a dedicated viewer
declares, and the Host read card must keep the `lang` values it already
persisted.
- Drop `csv` from the shared table: Code registers before Spreadsheet, so
the earlier body would take `.csv` and the browser snapshot gate fails.
- Add `readLangHintForPath`, the read card's legacy projection over the
shared table: the 43 pre-unification suffixes keep their old short ids
byte for byte, new suffixes use the canonical id.
- Add an ownership regression test over the full preview registry that
turns red when `csv` returns, plus byte-level and alias-normalized tests
for the read projection.
Three tools capped the preview text they return with String.prototype.slice
at a UTF-16 code-unit budget. A cut that lands inside a surrogate pair
leaves a lone high surrogate, which is not well-formed text: a strict JSON
reader rejects the durable Session log carrying it, and the preview shows a
broken glyph. A lone surrogate retained in history has already blocked every
later Messages request for one session.
Centralize the cap in output-retention as
truncateWithoutSplittingSurrogatePair, and use it from the persistent bash
and pwsh shells and the string-replace editor. The cut drops the unpaired
half instead of keeping it, so the retained text stays a prefix and is at
most one code unit shorter than before.
The new dependency edges and the shifted Config sources regenerate
docs/module-graph and docs/config-catalog, including the committed
translation pair.
Refs #4817
The document Code preview and the Host read card each owned an
extension-to-language table that had drifted: the preview table had 13
extensions the read card lacked, and the two returned different language
ids for the same suffix. Both now resolve through the new zero-dependency
@deepseek-ai/dsh-util-code-language, which also adds common Windows,
config, markup, and language extensions. Every added grammar stays in the
lazy set; the boot grammars are unchanged.
Fixes#4822
- main-startup: assert primaryRuntime for the unpackaged Host and cover a
missing DSH_DESKTOP_PRIMARY_RUNTIME_DIR as a fatal startup dialog with no
Host started.
- test-workspace-updates: pass the prepared primary-runtime directory instead
of an application/runtime path the runner never creates.
- development-project: derive the descriptor platform/arch from
resolveDesktopBuildTarget so it matches the prepared payload.
- main: resolve the unpackaged primary runtime once at startup instead of
inside the Host factory.
- dev: keep the environment override symmetric with its neighbours.
- README: document the variable as required for unpackaged launches and record
the remaining win32-arm64 payload architecture limitation.
The macOS window's draggable area was a hand-written band: a 52px strip, deepened
to 76px by a marker the Conversation header had to keep in step with its own
height. It drifted from the layout — blank runs inside a chrome row did not drag,
and a control a few pixels away dragged the window instead.
Chrome rows now mark their own element (`data-window-drag`) and the shell declares
the surface once (`html[data-platform='darwin'] [data-window-drag]`), so a row's own
box is its draggable geometry and no fixed band has to match any row's height. The
frame band, its 52/76 figures, the marker, and the per-sheet drag rules are gone;
interactive controls and overlays portal beside `#root` where one rule subtracts
them.
Recollection becomes one shell watcher (`window-drag/recall.ts`): Electron rebuilds
the window's drag rects only when a computed app-region value changes
(electron#32341), so the watcher measures every marked row each frame while the
surface moves, sets `data-window-drag-recall` for the frames whose geometry changed,
and clears it on the frame that finds the geometry settled. The right panel's
hand-written pulse is deleted with it.
Coverage is a contract with executable parts. `window-drag/regions.ts` publishes the
composition rule and the interactive selector that both the sheet and its spec
compare against; the ui-theme gate asserts the package tree declares drag in exactly
two places and pairs every row's mark with its sheet, selector, and authored height;
and the browser lane boots the shipped composition in Chromium with the darwin
marker to assert per-row geometry, a content body under a marked head row, a
covering overlay, and the watcher's pulse. The plugin manager's detail views mark
the head row they share rather than the pane they render in, so their text and form
labels stay content.
Both real machines agree: the macOS matrix ran on a real `hiddenInset` window with
HID-level pointer input and window-frame readback, and the Windows matrix on a
Windows 11 ARM64 VM — chrome rows drag, content and the covering overlay do not,
and the controls keep their clicks.
Refs #4619
Every ring that used 2px now reads --dsw-focus-ring-width. The dense tables and
toolbars keep their 1px ring on purpose: at 2px they visibly outweigh the rows
they sit between, so the standard value and the dense exception stay distinct
rather than converging on a number nobody measured.