Successor to #17538 / PR #17531, which promoted the dock splitter's visual language from apps/ into the engine as --dock-splitter-* tokens. That leaf is resolved and correct at rest; this is a distinct defect on a surface it did not cover, filed as a successor rather than a reopen.
Found by @neo-gpt-emmy, after @tobiu corrected her first oracle: the source splitter may fade during a drag, but the drag PROXY must remain visible. That distinction is what makes this diagnosable — an oracle that accepts "the splitter disappeared" as expected hides it.
The Problem
Exact-head probe at origin/dev17d41fd622, crossing the real drag threshold and inspecting body > .neo-dragproxy.neo-dashboard-dock-splitter in both AgentOS and examples/dashboard/dock:
background: rgba(0, 0, 0, 0)
::after is 0×0 and transparent
--dock-splitter-background, --dock-splitter-background-active and --dock-splitter-handle-size all resolve empty
So the moment a user actually grabs the splitter, the affordance vanishes — the state where it exists to give feedback. Both consumers, so it is engine-tier; the FM flat handle is not the cause.
The Architectural Reality
The tokens do not fail to apply. They have nothing to inherit from.
src/draggable/DragZone.mjs:147-150 — proxyParentId_: 'document.body'. The proxy is a clone mounted at body, with splitter orientation, theme and neo-dragproxy classes but no .neo-dashboard ancestor and no app root.
resources/scss/src/dashboard/Container.scss — every --dock-splitter-* default is declared on .neo-dashboard, the class the adapter projects onto each zone. FM's values scope to .fm-fleet-cockpit.
An unscoped var() with no fallback resolves to nothing, which is exactly the measured rgba(0,0,0,0) and 0×0 handle.
DragZone already solves this exact category of problem for a sibling case.:311 reasons about pushing the theme class onto the proxy precisely because document.body keeps the boot theme — the file already understands that a proxy has no ancestors, so ancestor context must travel with it. Dock scope is the same category of context.
Why the existing tests did not catch it
PR #17531's active-state tests mouse-down only and never cross drag:start / createDragProxy. They exercised the surface that stays still and certified the one that moves. Any fix here needs a control that crosses the real drag threshold, or it re-certifies the resting state again under a new name.
Same shape one layer up, and worth recording because it is the recurring failure: PR #17614's AC-1 row originally certified this affordance as "verified still true" from a reading of the static rule. Corrected there.
The Fix
⚠️ RECOMMENDATION INVERTED 2026-08-23, after reading one layer further. The scope-class option below was recommended first and is now rejected on evidence. Recording the reversal rather than editing it away, because the reasoning is the useful part.
Why the scope class fails. It would carry the ENGINE FLOOR and nothing else. Both real consumers declare their splitter values as descendant selectors, not on a scoping root:
The clone keeps .neo-dashboard-dock-splitter and loses the ancestor, so those rules stop matching. Adding neo-dashboard to the proxy makes the engine defaults resolve and leaves every consumer's values still absent — a proxy that is visible but wrong, which is harder to notice than one that is invisible. The .neo-dashboard token-only measurement that made the scope class look safe is still true and no longer sufficient: it answered "is it harmful?" when the deciding question was "is it enough?".
Carrying the app-root class instead does not generalise: the class that scopes dock values is app-specific (fm-fleet-cockpit, workstation-workspace) with no shared prefix a generic walk can match, unlike the neo-theme- / neo-preview-lang- prefixes DockTabSortZone#getDragProxyConfig relies on.
Recommended — explicit token projection. At drag start, read the computed --dock-splitter-* values off the SOURCE element and set them as inline custom properties on the proxy. The source has already been through the real cascade, so this is exact by construction, consumer-agnostic, and immune to scoping entirely — it never needs to know which class carried the value or how deeply it was nested.
Cost: an enumeration of the token set that must track it. That is a real maintenance edge and should be mitigated by deriving the list from one place rather than restating it, plus a control that fails when a token is added to the engine and not to the projection.
The precedent to follow, and to learn from.Neo.dashboard.DockTabSortZone#getDragProxyConfig (:906-928) already solves this exact class of problem for the preview palette — it walks the parent chain for the nearest-ancestor theme because "app theme files project the --agent-dock-preview-* palette aliases onto the theme class, so carrying it makes the aliases resolve ON the proxy at its body mount." The tab sort zone learned that a body-mounted proxy loses its cascade; the splitter never did. Its walk works there because those values ARE theme-scoped; it does not transfer here because splitter values are app-root-scoped.
Placement.DockSplitter.mjs:111-123 creates a plain DragZone and already spreads ...me.dragZoneConfig, so the projection belongs on the splitter's own proxy config. Generic DragZone must not learn about --dock-* tokens.
A splitter proxy resolves the same --dock-splitter-* values its source does
Unscoped today → transparent; must not silently degrade to transparent again
Rationale beside the scope push, naming why a body-mounted clone loses inheritance
Cross the real drag threshold and compare proxy computed paint against the source splitter's
Consumer values (FM)
apps/agentos/.../cockpit/Container.scss
A consumer's splitter values must reach its own proxy, not just the engine floor
Engine floor if the consumer sets none
—
FM and the example both measured; the defect reproduces in both today
DragZone theme-class precedent
DragZone.mjs:311
Reuse the existing "context travels with the proxy" idiom
—
—
—
Acceptance Criteria
A splitter drag proxy remains VISIBLE in flight, carrying the resolved --dock-splitter-* values its source element computed.
Narrowed 2026-08-23 on @neo-gpt-emmy's review of PR #17617. This AC originally read "background, handle, and the active state". The operator's oracle is a visibility contract — the source may fade; the proxy must remain visible — and "active state" was scope I added on top of it, never implemented and never evidenced: the witness compares the proxy against the pre-gesture resting paint, which is not a drag-active palette. Narrowing to what is actually contracted and proven rather than leaving an AC that certifies more than the diff delivers. A rendered proxy active state remains a legitimate future ask; it is not this defect, and it does not inherit the operator's authority.
The control crosses drag:start / createDragProxy. A mouse-down-only arm does not satisfy this AC; that is precisely the gap that let the defect ship.
Non-vacuity: removing the fix returns the proxy to rgba(0,0,0,0) with a 0×0 handle, asserted — the control must fail on the defect it exists for.
Both consumers verified: AgentOS (consumer values) and examples/dashboard/dock (engine floor, no app CSS). The engine-floor case is the one that proves the fix is not FM-specific.
The source splitter's own behaviour during drag is unchanged — fading the source is permitted; this ticket does not alter it.
If the scope class is chosen, a note records that .neo-dashboard was verified token-only at implementation time, so a future non-token rule there is a known tripwire.
Out of Scope
The resting splitter affordance — shipped and correct (#17538 / PR #17531).
The edge-rail measure and revealed-tab state (#17211 / PR #17614). Rail tabs and the reveal overlay carry no DragZone references, so those tokens have no proxy exposure; verified, not assumed.
Any change to proxyParentId itself. Body-mounting is deliberate; the defect is the missing scope, not the mount point.
Avoided Traps
Blaming the FM flat handle. The first plausible story, and false: the defect reproduces in examples/dashboard/dock, which carries no app dock CSS at all.
Reopening #17538. It is a resolved leaf and its scope was the resting affordance; reopening would rewrite a closed record instead of stating a new defect.
Testing the fix at rest. The entire reason this shipped is a control that never crossed the drag threshold. A green resting arm proves nothing here.
Assuming the scope class is free. It is only free because .neo-dashboard was measured token-only; that measurement is a precondition of the recommendation, not a detail.
Related
#17538 / PR #17531 (the resting promotion this succeeds) · #17211 (AC-1, whose certification this corrects) · #17241 (the parent placement family) · PR #17614 · #17578 · ADR 0029
Diagnosis and exact-head probe: @neo-gpt-emmy, at 17d41fd622.
Live latest-open sweep: latest 10 open read at 2026-08-23T14:54:18Z plus a state:all search for drag-proxy/splitter token scope — nearest is #17242 (CLOSED, theme layer existence; different surface). No equivalent. A2A in-flight claim sweep: 30 most recent messages, window 12:49–14:53Z; no claim on this scope.
Retrieval Hint: dock splitter drag proxy invisible; neo-dragproxy mounted to document.body has no .neo-dashboard ancestor so --dock-splitter-* resolve empty; DragZone proxyParentId; tests mouse-down only never crossed createDragProxy
tobiu referenced in commit 26b506c - "fix(dashboard): the splitter's drag proxy carries the paint it was cloned from (#17616) (#17617) on Aug 23, 2026, 5:58 PM
Context
Successor to #17538 / PR #17531, which promoted the dock splitter's visual language from
apps/into the engine as--dock-splitter-*tokens. That leaf is resolved and correct at rest; this is a distinct defect on a surface it did not cover, filed as a successor rather than a reopen.Found by @neo-gpt-emmy, after @tobiu corrected her first oracle: the source splitter may fade during a drag, but the drag PROXY must remain visible. That distinction is what makes this diagnosable — an oracle that accepts "the splitter disappeared" as expected hides it.
The Problem
Exact-head probe at
origin/dev17d41fd622, crossing the real drag threshold and inspectingbody > .neo-dragproxy.neo-dashboard-dock-splitterin both AgentOS andexamples/dashboard/dock:background: rgba(0, 0, 0, 0)::afteris 0×0 and transparent--dock-splitter-background,--dock-splitter-background-activeand--dock-splitter-handle-sizeall resolve emptySo the moment a user actually grabs the splitter, the affordance vanishes — the state where it exists to give feedback. Both consumers, so it is engine-tier; the FM flat handle is not the cause.
The Architectural Reality
The tokens do not fail to apply. They have nothing to inherit from.
src/draggable/DragZone.mjs:147-150—proxyParentId_: 'document.body'. The proxy is a clone mounted atbody, with splitter orientation, theme andneo-dragproxyclasses but no.neo-dashboardancestor and no app root.resources/scss/src/dashboard/Container.scss— every--dock-splitter-*default is declared on.neo-dashboard, the class the adapter projects onto each zone. FM's values scope to.fm-fleet-cockpit.An unscoped
var()with no fallback resolves to nothing, which is exactly the measuredrgba(0,0,0,0)and 0×0 handle.DragZonealready solves this exact category of problem for a sibling case.:311reasons about pushing the theme class onto the proxy precisely becausedocument.bodykeeps the boot theme — the file already understands that a proxy has no ancestors, so ancestor context must travel with it. Dock scope is the same category of context.Why the existing tests did not catch it
PR #17531's active-state tests mouse-down only and never cross
drag:start/createDragProxy. They exercised the surface that stays still and certified the one that moves. Any fix here needs a control that crosses the real drag threshold, or it re-certifies the resting state again under a new name.Same shape one layer up, and worth recording because it is the recurring failure: PR #17614's AC-1 row originally certified this affordance as "verified still true" from a reading of the static rule. Corrected there.
The Fix
Why the scope class fails. It would carry the ENGINE FLOOR and nothing else. Both real consumers declare their splitter values as descendant selectors, not on a scoping root:
.fm-fleet-cockpit { .neo-dashboard-dock-splitter { --dock-splitter-background: …; } } /* cockpit/Container.scss:259 */ .workstation-workspace { .neo-dashboard-dock-splitter { --dock-splitter-background: …; } } /* Workspace.scss:38 */The clone keeps
.neo-dashboard-dock-splitterand loses the ancestor, so those rules stop matching. Addingneo-dashboardto the proxy makes the engine defaults resolve and leaves every consumer's values still absent — a proxy that is visible but wrong, which is harder to notice than one that is invisible. The.neo-dashboardtoken-only measurement that made the scope class look safe is still true and no longer sufficient: it answered "is it harmful?" when the deciding question was "is it enough?".Carrying the app-root class instead does not generalise: the class that scopes dock values is app-specific (
fm-fleet-cockpit,workstation-workspace) with no shared prefix a generic walk can match, unlike theneo-theme-/neo-preview-lang-prefixesDockTabSortZone#getDragProxyConfigrelies on.Recommended — explicit token projection. At drag start, read the computed
--dock-splitter-*values off the SOURCE element and set them as inline custom properties on the proxy. The source has already been through the real cascade, so this is exact by construction, consumer-agnostic, and immune to scoping entirely — it never needs to know which class carried the value or how deeply it was nested.Cost: an enumeration of the token set that must track it. That is a real maintenance edge and should be mitigated by deriving the list from one place rather than restating it, plus a control that fails when a token is added to the engine and not to the projection.
The precedent to follow, and to learn from.
Neo.dashboard.DockTabSortZone#getDragProxyConfig(:906-928) already solves this exact class of problem for the preview palette — it walks the parent chain for the nearest-ancestor theme because "app theme files project the--agent-dock-preview-*palette aliases onto the theme class, so carrying it makes the aliases resolve ON the proxy at its body mount." The tab sort zone learned that a body-mounted proxy loses its cascade; the splitter never did. Its walk works there because those values ARE theme-scoped; it does not transfer here because splitter values are app-root-scoped.Placement.
DockSplitter.mjs:111-123creates a plainDragZoneand already spreads...me.dragZoneConfig, so the projection belongs on the splitter's own proxy config. GenericDragZonemust not learn about--dock-*tokens.Contract Ledger Matrix
src/draggable/DragZone.mjs:147-150(proxy mount) +Container.scss.neo-dashboardtoken block--dock-splitter-*values its source doesapps/agentos/.../cockpit/Container.scssDragZonetheme-class precedentDragZone.mjs:311Acceptance Criteria
A splitter drag proxy remains VISIBLE in flight, carrying the resolved
--dock-splitter-*values its source element computed.The control crosses
drag:start/createDragProxy. A mouse-down-only arm does not satisfy this AC; that is precisely the gap that let the defect ship.Non-vacuity: removing the fix returns the proxy to
rgba(0,0,0,0)with a 0×0 handle, asserted — the control must fail on the defect it exists for.Both consumers verified: AgentOS (consumer values) and
examples/dashboard/dock(engine floor, no app CSS). The engine-floor case is the one that proves the fix is not FM-specific.The source splitter's own behaviour during drag is unchanged — fading the source is permitted; this ticket does not alter it.
If the scope class is chosen, a note records that
.neo-dashboardwas verified token-only at implementation time, so a future non-token rule there is a known tripwire.Out of Scope
DragZonereferences, so those tokens have no proxy exposure; verified, not assumed.proxyParentIditself. Body-mounting is deliberate; the defect is the missing scope, not the mount point.Avoided Traps
examples/dashboard/dock, which carries no app dock CSS at all..neo-dashboardwas measured token-only; that measurement is a precondition of the recommendation, not a detail.Related
#17538 / PR #17531 (the resting promotion this succeeds) · #17211 (AC-1, whose certification this corrects) · #17241 (the parent placement family) · PR #17614 · #17578 · ADR 0029
Diagnosis and exact-head probe: @neo-gpt-emmy, at
17d41fd622.Live latest-open sweep: latest 10 open read at 2026-08-23T14:54:18Z plus a
state:allsearch for drag-proxy/splitter token scope — nearest is #17242 (CLOSED, theme layer existence; different surface). No equivalent. A2A in-flight claim sweep: 30 most recent messages, window 12:49–14:53Z; no claim on this scope.Origin Session ID: eb671e6e-ca17-4a53-8069-64fd5885ce84
Retrieval Hint:
dock splitter drag proxy invisible; neo-dragproxy mounted to document.body has no .neo-dashboard ancestor so --dock-splitter-* resolve empty; DragZone proxyParentId; tests mouse-down only never crossed createDragProxy