LearnNewsExamplesServices
Frontmatter
id18148
titleLock and pop-out never reach a dock workspace that configures nothing
stateClosed
labels
enhancementaiarchitecturedashboard
assignees[]
createdAtSep 3, 2026, 12:14 AM
updatedAtSep 3, 2026, 12:56 AM
githubUrlhttps://github.com/neomjs/neo/issues/18148
authorneo-opus-vega
commentsCount1
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtSep 3, 2026, 12:56 AM

Lock and pop-out never reach a dock workspace that configures nothing

Closed v13.2.0/archive-v13-2-0-chunk-18 enhancementaiarchitecturedashboard
neo-opus-vega
neo-opus-vega commented on Sep 3, 2026, 12:14 AM

Context

Operator ruling 2026-09-03: a dock workspace that configures nothing must project the full header action rail — "lock => GOAL was neo default true", "popout MUST BE THERE BY DEFAULT".

Measured on a downstream workspace that overrides none of the six enableDock*Action flags (headless Chromium against the running app, action nodes read per header): focusing a center-owned pane yields [maximize, close]; focusing an edge-owned pane yields [unpin, maximize, close]. Lock and pop-out never appear.

Both absences are correct against currently accepted ACs. That is what this ticket revises — it is a successor to two deliberate decisions, not a defect report against them.

The Problem

1. Lock. #18014 AC-1 reads: "… and retain explicit false opt-out; enableDockLockAction remains default-off." The action itself shipped complete under #17949 (fa-lock ↔ fa-lock-open, aria lock/unlock, focus-gated while unlocked and persistent while locked) and #18044 added per-pane delegation (dockLock(locked)) — but no default consumer renders it. The consequence was named on 2026-09-01: default-off plus no Workstation opt-in means the operator "has not felt it by hand", with a proposed opt-in step that never executed.

2. Pop-out. #17947 AC-1 witnessed a DOUBLE gate as correct: "Default off: byte-identical projection. On without enableDockTearOutLifecycle: still byte-identical (double gate witnessed)." The consequence is that enableDockPopOutAction defaults true while being structurally unsatisfiable: dockPopOutActionActive = enableDockPopOutAction === true && !!this.tearOutHandlers, and handlers are composed only under enableDockTearOutLifecycle, which defaults false. A host reads an enabled flag, gets no action, and receives no runtime signal.

Pop-out is the sharper half. An enabled flag that can never render is a workspace-wide constant falsehood, not per-item variance — and enableDockTearOutLifecycle's own JSDoc frames its default as temporary: "Disabled by default so ordinary workspaces and hosts carrying their own legacy lifecycle remain byte-behaviorally unchanged until their migration leaf." This is that leaf.

The Architectural Reality

  • src/dashboard/dock/Workspace.mjs:145 — enableDockLockAction: false; :215 — enableDockPopOutAction: true; :266 — enableDockTearOutLifecycle: false.
  • Workspace#dockPopOutActionActive is the single effective predicate ("Two questions must never disagree"), passed to the adapter as dockPopOutActionAvailable.
  • src/dashboard/dock/projection/LayoutAdapter.mjs:1113 builds headerActions: lock enters only under context.enableDockLockAction; pop-out enters under enableDockPopOutAction but with hidden: !activeItemId || !context.dockPopOutActionAvailable.
  • A withdrawn focus-gated action carries no DOM node (#18015), so an absent action is indistinguishable from a disabled flag by inspection. That is why this went unnoticed by eye.

The Fix

  1. enableDockLockAction defaults true; explicit false retained as the opt-out. Amends #18014 AC-1.

  2. Pop-out becomes available to a default workspace. Two candidate shapes:

    • (a) enableDockTearOutLifecycle defaults true — the migration leaf its JSDoc anticipates. Moves tear-out lifecycle ownership for every existing host that carries its own.
    • (b) Decouple availability from the legacy-lifecycle flag: the engine composes its own tearOutHandlers when a host supplies none, leaving enableDockTearOutLifecycle as the opt-in for hosts that want the engine to own the WHOLE lifecycle.

    (b) is the recommendation — it makes the enabled flag honest without moving lifecycle ownership under any host that already has one.

  3. Whichever shape lands: an enabled action that cannot render must not be silently inert. A dev-mode warning names the action and the missing capability.

Contract Ledger Matrix

Target Surface Source of Authority Proposed Behavior Fallback Docs Evidence
Workspace#enableDockLockAction src/dashboard/dock/Workspace.mjs:145 default true explicit false removes the action, unchanged JSDoc default-vector arm, red when reverted
Workspace#enableDockTearOutLifecycle :266 unchanged under (b); true under (a) hosts with their own lifecycle keep it JSDoc byte-identical projection arm for existing hosts
Workspace#dockPopOutActionActive Workspace.mjs (getter) true for a default workspace a host-supplied bundle still wins JSDoc availability arm on a bare Workspace
dev-mode inert-action warning new names the action + the missing capability silent in production JSDoc one arm asserting the warning fires

Decision Record impact

amends ADR 0029 — the docking design record's tear-out lifecycle and default-posture rows. Also amends the accepted ACs of #18014 (AC-1) and #17947 (AC-1); both tickets are CLOSED with merged PRs, so this is a successor, never a reopen.

Acceptance Criteria

  • AC-1 A Neo.dashboard.dock.Workspace subclass that overrides nothing projects lock at the frozen slot; explicit enableDockLockAction: false still removes it.
  • AC-2 The same bare workspace projects a pop-out that is reachable (not hidden) on a focused item, with the host supplying no tearOutHandlers.
  • AC-3 Existing hosts carrying their own tear-out lifecycle keep byte-identical projection and exactly one lifecycle owner — no second handler, listener or vessel owner (the invariant #17947 AC-1 and #18014 AC-9 protect).
  • AC-4 An enabled action that cannot render logs one dev-mode warning naming the action and the missing capability; production stays silent.
  • AC-5 A red-first witness asserts the DEFAULT action vector for a bare workspace and is red at the current head. Absence is asserted as node presence, not display (#18015).
  • AC-6 The frozen order lock · reload · pin · pop-out · maximize · close is unchanged; close stays persistent and last.
  • AC-7 Exact-head unit + component suites green; the lock and pop-out arms mutation-proven.

Out of Scope

  • Reload's default availability — its own successor, filed alongside this one.
  • Pin's edge-ownership gate: verified working (present on an edge-owned pane, withheld on a center-owned one). A consumer layout concern, not an engine one.
  • The locked-pane presentation design pass — #18044 shipped delegation; the gray-out question stays open.

Avoided Traps

  • Flipping enableDockTearOutLifecycle to true as the obvious one-line fix. Its default-false is load-bearing for hosts carrying a legacy lifecycle, and #18014 AC-9 requires exactly one lifecycle owner. A one-line flip moves ownership under those hosts silently — the reason (b) is recommended over (a).
  • Reading an absent DOM node as a disabled flag. Withdrawn actions carry no node, so presence is the only honest instrument. A grep for these flags across a consumer tree returns nothing precisely because they are already true.

Related

  • #18014 (closed) — AC-1 set the default vector and kept lock off; this amends it.
  • #17947 (closed) — AC-1 witnessed the pop-out double gate; this amends it.
  • #17949 (closed) — the lock action itself. #18044 (closed) — per-pane dockLock delegation.
  • #18015 — withdrawn actions have zero extent and no node.
  • ADR 0029 — docking design record.

Live latest-open sweep: checked the latest 25 open issues at 2026-09-02T22:12Z; A2A in-flight claim sweep over the most recent 12 messages at 22:14Z — no equivalent ticket and no competing claim.

Origin Session ID: a5db4dc8-310d-4702-9bfa-3780d32e932b

Retrieval Hint: "dock header action defaults lock pop-out enableDockTearOutLifecycle double gate bare default workspace"