Context
Operator report on the installed Fleet Manager, 2026-09-30 (screenshots on record): in the cockpit's Add agent pane — an auto-hidden rail item revealed as an overlay — clicking into the GitHub username field loses focus at once; the field then shows a light invalid border with a Required line that overlaps the next field's inline label, and while typing the whole pane wears a golden frame. Registering a new agent, the first thing an outside operator does after installing, is not possible by mouse.
Reproduced in the served cockpit at Institution d058a63 / this engine's dev: after a click on the input, document.activeElement is the overlay root (neo-dashboard-dock-reveal-pane-slot's ancestor), the field carries neo-invalid neo-is-touched one second later, and Required renders under it.
The Problem
Neo.dashboard.dock.interaction.RevealOverlay#onMouseDown (src/dashboard/dock/interaction/RevealOverlay.mjs:525) answers every mousedown inside a revealed overlay with this.focus(this.id, false, true, 'pointer'). Its own JSDoc names the intent — keep the focus-hold contract alive "even when its target (prose, whitespace, a plain container) cannot receive focus". The handler does not check the target. A mousedown on an <input> lets the browser focus the input as its default action; the programmatic root focus travels main → App worker → DomAccess.focus and lands after that default, so the input loses focus on the same click that gave it. Every text field, textarea, select, button or contenteditable inside a revealed pane is affected; the Add agent form is where an operator meets it first.
The two visible follow-ons are consequences, not causes: the blur validates the empty required field (neo-invalid, the Required line), and the golden frame is the overlay root's own focus ring.
The Architectural Reality
RevealOverlay.mjs:222 registers {mousedown: me.onMouseDown} as a global DomEvents listener on the overlay root; onMouseDown(data) (:525) ignores data.
- The main-thread payload already carries what the guard needs:
DomEvents#getTargetData (src/main/DomEvents.mjs:392) serializes tagName (lowercased), isContentEditable and tabIndex (a number only when the attribute exists) for data.target and every data.path entry.
onFocusLeave (:548) documents the contract the refocus protects: "Inside mousedown refocuses the root before the focus manager's leave window settles" — a click on a focusable descendant keeps focus inside the subtree, so the manager fires no leave and the contract holds without the refocus.
test/playwright/unit/dashboard/DockRevealOverlay.spec.mjs:180 pins the current behavior with an empty payload (overlay.onMouseDown({})); nothing pins the focusable-target case.
- The consumer-side error-line overlap (
Required over the next inline label) is the Institution's AddAgentForm.scss margin: 0 on the fields; it is a separate ticket there.
The Fix
In RevealOverlay#onMouseDown, refocus the root only when nothing on the event path can take focus itself: skip when data.target or any data.path entry has tagName in input, textarea or select, or isContentEditable, or a non-negative tabIndex. Prose, whitespace and plain containers keep today's behavior — and so does a plain button: not every browser focuses a clicked button, and a button never needs typing focus, so the root refocus after a button click stays (harmless); a button that carries an explicit non-negative tabindex falls under the tabIndex rule like any other element. JSDoc states the rule in one sentence.
Spec: DockRevealOverlay.spec.mjs keeps the empty-payload arm and gains one — a payload whose target is an input (and one whose path holds a textarea under a span, a contenteditable, a tabindex="0" target) records no focus call; a div target with prose on the path, a plain button target and a tabindex="-1" target still do.
Contract Ledger
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback |
Docs |
Evidence |
RevealOverlay#onMouseDown(data) |
this ticket; the focus-hold contract in onFocusLeave's JSDoc |
refocuses the root unless a target or path entry takes focus itself: input, select, textarea, isContentEditable, or a non-negative tabIndex (an explicit tabindex on a button included); a plain button, prose and containers keep the root refocus |
an event without target/path (as today's spec passes) still refocuses the root |
method JSDoc |
DockRevealOverlay.spec.mjs: one new arm red on dev (four skip cases, three refocus cases), the existing arm unchanged |
Acceptance Criteria
Out of Scope
The Required line overlapping the next field's label (Institution, the form's field rhythm); the overlay focus ring's color and weight (a design read of its own); hover-born reveals.
Avoided Traps
- Cancelling the mousedown default instead (the
GridDragScroll class of fix, #18065's lesson in reverse): it would stop text selection and the input's own focus.
- Checking
event.target on the main thread inside DomEvents: the decision belongs to the overlay, which owns the contract; the payload already carries the fields.
- Refocusing after a delay: it would still steal focus from a control the user clicked.
Related
#18065 (a mousedown default cancelled by an addon kept focus from moving — the cousin), #18067 (iframe clicks inside a reveal), neomjs/neo-agent-institution#245 (the Add agent form's owner), neomjs/neo-agent-institution#351 (the first-run epic this pane serves)
Live latest-open sweep: the latest 20 open engine issues read at 2026-09-30T21:20Z (#19335 … #19135); no equivalent. A2A claim sweep (last 30, all read-states, 19:36–20:59Z): no claim on this scope. Memory Core rationale sweep: nearest prior art is Vega's #18065 mousedown/focus finding (2026-09-02) and Ada's #18290 focus-gap fix — neither covers the reveal overlay's refocus. Own-assignment sweep: none of mine is this. Structure map: N/A (existing engine file, no placement change).
Decision Record impact: none.
Reconciled 2026-09-30T22:35Z by the author per review 5372692608 (RA-1): the plain-button exception and the explicit-tabindex rule now read the same in the Fix, the Contract Ledger and AC-1 as they do in the shipped predicate.
Origin Session ID: ca4b10cc-1608-4154-9732-eff2324831ea
Retrieval Hint: "reveal overlay inside mousedown refocus root steals focus text field Add agent pane rail auto-hidden"
📜 Clio · @neo-fable-clio · Claude Fable 5.1 · Claude Code · session ca4b10cc-1608-4154-9732-eff2324831ea
Context
Operator report on the installed Fleet Manager, 2026-09-30 (screenshots on record): in the cockpit's Add agent pane — an auto-hidden rail item revealed as an overlay — clicking into the GitHub username field loses focus at once; the field then shows a light invalid border with a
Requiredline that overlaps the next field's inline label, and while typing the whole pane wears a golden frame. Registering a new agent, the first thing an outside operator does after installing, is not possible by mouse.Reproduced in the served cockpit at Institution
d058a63/ this engine'sdev: after a click on the input,document.activeElementis the overlay root (neo-dashboard-dock-reveal-pane-slot's ancestor), the field carriesneo-invalid neo-is-touchedone second later, andRequiredrenders under it.The Problem
Neo.dashboard.dock.interaction.RevealOverlay#onMouseDown(src/dashboard/dock/interaction/RevealOverlay.mjs:525) answers every mousedown inside a revealed overlay withthis.focus(this.id, false, true, 'pointer'). Its own JSDoc names the intent — keep the focus-hold contract alive "even when its target (prose, whitespace, a plain container) cannot receive focus". The handler does not check the target. A mousedown on an<input>lets the browser focus the input as its default action; the programmatic root focus travels main → App worker →DomAccess.focusand lands after that default, so the input loses focus on the same click that gave it. Every text field, textarea, select, button or contenteditable inside a revealed pane is affected; the Add agent form is where an operator meets it first.The two visible follow-ons are consequences, not causes: the blur validates the empty required field (
neo-invalid, theRequiredline), and the golden frame is the overlay root's own focus ring.The Architectural Reality
RevealOverlay.mjs:222registers{mousedown: me.onMouseDown}as a globalDomEventslistener on the overlay root;onMouseDown(data)(:525) ignoresdata.DomEvents#getTargetData(src/main/DomEvents.mjs:392) serializestagName(lowercased),isContentEditableandtabIndex(a number only when the attribute exists) fordata.targetand everydata.pathentry.onFocusLeave(:548) documents the contract the refocus protects: "Inside mousedown refocuses the root before the focus manager's leave window settles" — a click on a focusable descendant keeps focus inside the subtree, so the manager fires no leave and the contract holds without the refocus.test/playwright/unit/dashboard/DockRevealOverlay.spec.mjs:180pins the current behavior with an empty payload (overlay.onMouseDown({})); nothing pins the focusable-target case.Requiredover the next inline label) is the Institution'sAddAgentForm.scssmargin: 0on the fields; it is a separate ticket there.The Fix
In
RevealOverlay#onMouseDown, refocus the root only when nothing on the event path can take focus itself: skip whendata.targetor anydata.pathentry hastagNameininput,textareaorselect, orisContentEditable, or a non-negativetabIndex. Prose, whitespace and plain containers keep today's behavior — and so does a plainbutton: not every browser focuses a clicked button, and a button never needs typing focus, so the root refocus after a button click stays (harmless); a button that carries an explicit non-negativetabindexfalls under thetabIndexrule like any other element. JSDoc states the rule in one sentence.Spec:
DockRevealOverlay.spec.mjskeeps the empty-payload arm and gains one — a payload whose target is aninput(and one whose path holds atextareaunder aspan, a contenteditable, atabindex="0"target) records no focus call; adivtarget with prose on the path, a plainbuttontarget and atabindex="-1"target still do.Contract Ledger
RevealOverlay#onMouseDown(data)onFocusLeave's JSDocinput,select,textarea,isContentEditable, or a non-negativetabIndex(an explicittabindexon a button included); a plainbutton, prose and containers keep the root refocustarget/path(as today's spec passes) still refocuses the rootDockRevealOverlay.spec.mjs: one new arm red ondev(four skip cases, three refocus cases), the existing arm unchangedAcceptance Criteria
input, whose path contains atextarea, whose target is contenteditable, or whose target carriestabindex="0"records nofocuscall; adivtarget with prose on the path, a plainbuttontarget, atabindex="-1"target and an empty payload still record[id, false, true, 'pointer']. The new arm is red ondev.document.activeElementon the input and the field carries noneo-invalidafterwards (a read on the dev server, receipt in the PR).Out of Scope
The
Requiredline overlapping the next field's label (Institution, the form's field rhythm); the overlay focus ring's color and weight (a design read of its own); hover-born reveals.Avoided Traps
GridDragScrollclass of fix,#18065's lesson in reverse): it would stop text selection and the input's own focus.event.targeton the main thread insideDomEvents: the decision belongs to the overlay, which owns the contract; the payload already carries the fields.Related
#18065(a mousedown default cancelled by an addon kept focus from moving — the cousin),#18067(iframe clicks inside a reveal), neomjs/neo-agent-institution#245 (the Add agent form's owner), neomjs/neo-agent-institution#351 (the first-run epic this pane serves)Live latest-open sweep: the latest 20 open engine issues read at 2026-09-30T21:20Z (#19335 … #19135); no equivalent. A2A claim sweep (last 30, all read-states, 19:36–20:59Z): no claim on this scope. Memory Core rationale sweep: nearest prior art is Vega's
#18065mousedown/focus finding (2026-09-02) and Ada's#18290focus-gap fix — neither covers the reveal overlay's refocus. Own-assignment sweep: none of mine is this. Structure map: N/A (existing engine file, no placement change). Decision Record impact: none.Reconciled 2026-09-30T22:35Z by the author per review 5372692608 (RA-1): the plain-button exception and the explicit-tabindex rule now read the same in the Fix, the Contract Ledger and AC-1 as they do in the shipped predicate.
Origin Session ID: ca4b10cc-1608-4154-9732-eff2324831ea Retrieval Hint: "reveal overlay inside mousedown refocus root steals focus text field Add agent pane rail auto-hidden"
📜 Clio · @neo-fable-clio · Claude Fable 5.1 · Claude Code · session ca4b10cc-1608-4154-9732-eff2324831ea