LearnNewsExamplesServices
Frontmatter
id19989
titleA tab moved into a popup vessel leaves its header behind in the source strip
stateClosed
labels
bugaidashboard
assigneesneo-gemini-maryam
createdAtOct 22, 2026, 12:06 PM
updatedAtOct 22, 2026, 6:33 PM
githubUrlhttps://github.com/neomjs/neo/issues/19989
authorneo-opus-ada
commentsCount1
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtOct 22, 2026, 6:33 PM

A tab moved into a popup vessel leaves its header behind in the source strip

Closed Backlog/active-chunk-10 bugaidashboard
neo-opus-ada
neo-opus-ada commented on Oct 22, 2026, 12:06 PM

Context

Found in manual testing of apps/workstation on dev (2026-10-22). @tobiu tore one tab out into its own window, dropped it, then dragged a second tab (System Metrics) across into that window.

Afterwards the "Metrics" tab header exists twice. The popup shows "Structured Log Console | Metrics" with the Metrics content. The main window's right-top pane still shows "Metrics | Audit", with Metrics selected and a blank body. A body that exists once is right; the header must not exist twice either. A screenshot follows in a comment.

Measurements: the live session, read-only over the Neural Link

Session 0c35d63f… (appName workstation), main window c4b5ec51…, popup afb06b86…. Nothing in the session was mutated.

History. The Group history holds 10 committed rows: two dock-transfers, then eight native-popup-moves (window moves).

  1. transferItem logs: workstation-main heavy-tabs → new vessel workstation-vessel:logs (restoreTab), at +7.5 s.
  2. transferItem metrics: workstation-main right-top-tabs ([metrics, audit]) → workstation-vessel:logs (addTab; the vessel tabs become [logs, metrics]), at +16.3 s.

Console, 52 ms after row 2:

Dock projection failed (retired-staged); scheduling one clean re-projection neo-dock-workspace-1
  Dock projection tab chrome "right-top-tabs" has an inexact item set
  at Reconciler.reconcileTabChrome → reconcileProjection → Workspace.refreshDockWorkspace

49 ms later: … the repair attempt failed too, not retrying again.

State afterwards:

surface value
main document right-top-tabs {items: ["audit"], activeItemId: "audit"} (correct)
vessel document [logs, metrics], active metrics (correct)
main strip neo-tab-header-toolbar-6 Metrics (button-16, pressed, mounted) + Audit. verify_component_consistency: items, vdom and DOM agree, so the stale button is a fully live member.
vessel strip neo-tab-header-toolbar-11 Structured Log Console + Metrics ⠿ (correct)

So the document is right and main's rendered strip is not. The projection that should have retired Metrics from right-top-tabs threw twice and gave up, leaving the obsolete header and a blank body until a reload.

The Problem

VesselEmbodiment hands the cleanup to the next projection. Its docblock: "The next document projection owns placeholder + obsolete-button cleanup." That projection cannot do it once the pane has left the workspace.

Reconciler.reconcileTabChrome() builds its retirements from liveItems that are no longer desired. It locates each one with findItemState(pane) and drops any it cannot locate (.filter(retirement => retirement.state)). The Metrics pane now lives in the vessel, another workspace in another window, so no retirement entry exists for its obsolete button. The exactness guard added by #18667 then compares buttons (2) and body items against the desired set (1) and throws inexact item set. The one repair attempt runs the same plan, throws again, and stops.

That the pane's absence is what drops the retirement is read from source and matches the counts. It was not stepped through live.

The Architectural Reality

  • src/dashboard/dock/window/VesselEmbodiment.mjs: promote() leaves the placeholder and the obsolete tab button to "the next document projection".
  • src/dashboard/dock/projection/Reconciler.mjs reconcileTabChrome(): the retirement pass (retirementsByBody, keyed by the located body), then the exactness guard that throws.
  • src/dashboard/dock/Workspace.mjs: the refresh's one-shot repair (… scheduling one clean re-projection / … not retrying again).
  • #18621 / #18667 (closed): the projection refuses a tab pairing it cannot trust. The guard is right to refuse to pair a button with the wrong pane; the gap is that the hand-off leaves it nothing to retire.

The Fix

When an item leaves a workspace through a transfer, its source-side header and placeholder must be retired even though its pane is no longer in a body there. Either:

  • (a) Retire by item. The retirement pass removes the obsolete button and placeholder by item identity, via the strip's dockItemIds and the placeholder's item id, when the pane cannot be located. The pane belongs to the other workspace and is untouched.
  • (b) Retire at promote time. VesselEmbodiment#promote() retires them itself, in the source window, instead of deferring to a projection that cannot find the pair.

Prefer (a): it keeps one owner for strip membership, and the guard keeps refusing genuinely untrusted pairings.

Acceptance Criteria

  • After a tab is moved from main into an existing popup vessel, main's source strip shows exactly its document's items: no obsolete button, no placeholder, no blank card.
  • No Dock projection failed … inexact item set warning is logged for that transfer.
  • The vessel's strip and document stay as they are today ([logs, metrics], active metrics).
  • E2E: a new whitebox Neural Link spec, test/playwright/e2e/workstation/WorkstationTransferIntoVesselHeaderNL.spec.mjs, uses the existing tear-out helpers (test/playwright/e2e/utils/workstationPopOut.mjs / workstationGesture.mjs). It tears out one tab into a vessel, moves a second tab from another main pane into that vessel by the cross-window drag path, and asserts main's source strip equals its document and the console has no projection-failure warning. It must be red on dev (stale button plus the warning) and green with the fix, and it joins the hosted Workstation NL tier (#19655).
  • The #18667 guard specs stay green: an untrusted pairing is still refused.

Out of Scope

  • A ghost riding-window header, also observed. neo-tab-header-button-24 / toolbar-12 reads mounted: true while bound to windowId 3cb52067…, the riding window that carried Metrics, which is no longer connected. It is not user-visible. Retire it here if it shares the same promote/retire path; otherwise it gets a defect-note.
  • Moving the tab back to main, the operator's next step. No third transfer committed, so there is nothing to replay for it.

Related

Live latest-open sweep: checked latest 20 open issues at 2026-10-22T10:05:19Z; no equivalent found. Keyword sweep: "inexact item set", "duplicate tab header transfer", "stale tab header vessel", "transferItem projection failed"; closed adjacent only (#18621/#18667, #19081, #16622, #19248). A2A claim sweep: no overlapping lane-claim. MC sweep: "tab header duplicated after transferring a tab into a popup vessel…", 6 results; a 2026-09-24 note records promote()'s deferred cleanup, and there is no prior decision. Own-assignment sweep: none overlapping.

Origin Session ID: 70e59a84-efd6-4e7a-9262-c5478bd314ff Retrieval Hint: "tab moved into popup vessel leaves header in source strip, inexact item set, VesselEmbodiment promote deferred cleanup"

tobiu referenced in commit dffddbd - "fix(dashboard): retire pre-detached source tab header on vessel transfer (#19989) (#20003) on Oct 22, 2026, 6:33 PM
tobiu closed this issue on Oct 22, 2026, 6:33 PM