Skip to content

feat(agentos): FM cockpit keyboard a11y — roving focus, Enter/Space activation, aria-live + landmarks (#14619)#15094

Draft
neo-opus-vega wants to merge 9 commits into
devfrom
agent/14619-fm-focus-order
Draft

feat(agentos): FM cockpit keyboard a11y — roving focus, Enter/Space activation, aria-live + landmarks (#14619)#15094
neo-opus-vega wants to merge 9 commits into
devfrom
agent/14619-fm-focus-order

Conversation

@neo-opus-vega

@neo-opus-vega neo-opus-vega commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Resolves #14619 (FM cockpit accessibility pass — keyboard-operability slice).

Consolidates the two #14619 keyboard-a11y drafts into one coherent PR per the v13.2 convergence window (Euclid's option B). Supersedes #15081 (aria-live + landmark regions), merged in here — one complete keyboard-a11y surface, not two partial ones.

Evidence: 51/51 unit tests green across agentCard/fleetGrid/activityStream/agentDetail (chroma-less runner, head 0f25e1853). Keyboard operability proven at the controller + roving layer; Space/Enter activation wired through the shared neo-selection scroll-key rule.

What ships

  • Roving-tabindex focus order (FleetGrid): arrows move a single tab stop across the card ring, clamped at both ends. A roster rebuild keeps the tab stop on the resident agent IDENTITY — it captures the focused card's record.agentId before the rebuild and restores that agent's NEW index, clamping to a valid card only when the agent has left the roster (so a joiner/leaver above the focus never silently hands the tab stop to a different agent — @neo-gpt identity-focus falsifier).
  • Keyboard-operable cards (AgentCard): tabIndex 0 + role=button + record-derived aria-label; Enter AND Space drill in (ARIA button semantics); control-cluster keydowns carve out to lifecycle intent, not drill.
  • Space scroll-prevention (src/main/DomEvents.mjs): the neo-selection scroll-key rule now covers Space alongside the arrows, so activation never scrolls the page. The app still receives the keydown (sendMessageToApp fires first) — only the browser scroll default is suppressed.
  • aria-live activity log + named landmark regions (ActivityStream, AgentDetail, FleetGrid): folded in from feat(agentos): FM cockpit a11y — ActivityStream aria-live log region (#14619) [aria slice, draft] #15081.

Test Evidence

  • fleetGrid.spec: roving-tabindex — arrows clamp at both ends; a roster rebuild follows the resident agent identity (joiner above the focus → tab stop follows the same agentId to its new index; focused agent removed → clamps to a valid card, never orphaned) AND the pre-existing stale-focusIndex clamp-into-range still holds.
  • agentCard.spec: keyboard-operable drill target — focusable, button-announced, Enter/Space drill, non-activation + control-cluster keydowns carved out.
  • activityStream.spec + agentDetail.spec: aria-live region persists; roster/detail are named landmark regions.
  • 51 passed, tmp/unit-no-chroma.config.mjs, head 0f25e18. Hosted CI runs the authoritative suite + the src/main DomEvents coverage.

Post-Merge Validation

  • whitebox-e2e (remaining B step, in-window sequencing — NOT a cut): Tab/arrow/Enter/Space roving on a running cockpit — the mount-authority the unit layer guards off. This is the SOLE viable witness for the DomEvents Space scroll-suppression: a unit witness of DomEvents.onKeyDown is infeasible (it is singleton:true + Neo.setupClass — self-instantiates on import, needs the main-thread worker harness), so the real-browser e2e runs the real primitive. Per NO-CUTS, sequenced (immediate fast-follow), never dropped. The controller-side Space/Enter activation IS already unit-witnessed (agentCard.spec).
  • Live cockpit smoke: focus visibly moves card-to-card following the same agent across a roster refresh; Space activates without page scroll.

Deltas

Authored by Vega (Claude Opus 4.8, Claude Code). Session d3fb82dd.

…14619)

FM a11y pass (non-colliding aria slice, off dev — ActivityStream is not in the #15063 drill-spine diff): the cockpit's live activity feed had zero aria, so screen-reader users got no announcement of new events. onConstructed now makes the root a named, polite log region (aria-live=polite, role=log, aria-label), set before the first refreshFeed render flushes the vdom. Construct-probe test added. The focus-order half of #14619 follows once #15063 lands (it touches the drill spine); the contrast tokens were routed to Grace earlier this pass.
…ross feed updates (#14619)

Adversarial self-review hardening: the construct-probe proved the live region exists
at mount, but a live region's whole value is persisting across UPDATES. refreshFeed
swaps child items (removeAll + add) and never the vdom root, so the aria-live/role/
aria-label survive — but nothing pinned that, leaving a future refreshFeed refactor
free to replace the root and silently re-silence the feed to screen readers after the
first event. Now a post-update assertion (stream.events reset → refreshFeed) locks it.
…ns (#14619)

The a11y pass's landmark-structure slice, now un-gated by #15063 landing the drill.
FleetGrid (roster) and AgentDetail (drill) each become a named `role='region'`
with an `aria-label` ('Fleet roster' / 'Agent detail') so screen-reader users can
navigate the cockpit's primary surfaces as distinct landmarks — completing the
3-region set with ActivityStream's existing `log` region. Set in onConstructed before
the first render flush; both re-render paths preserve the root (refreshGrid mutates the
child cards container; applyRecord re-seats via child-reference .set()), so the region
survives store-driven re-renders + record re-seats — asserted in both specs. Roving-
tabindex focus-order (the keyboard-nav slice) follows on this branch.
…ntCards (Enter-primary) (#14619)

The FM cockpit was mouse-only (zero tabindex/keydown across the fleet components). AgentCard is now a keyboard-operable drill target: focusable (tabIndex 0), announced as role=button with the resident's name (record-derived aria-label, updates in place on re-seat), and Enter fires the same agentSelect drill as a body click.

onCardSelect is now shared by click + keydown: it filters keydowns to Enter (a click has no data.key and passes) and reuses the control-cluster path carve-out — keydown event data carries path too (DomEvents.getKeyboardEventData), so a keydown bubbling up from a focused control button is a lifecycle intent, not a drill.

Space activation + roving-tabindex are deferred to the KeyNavigation follow-up: Space's scroll default cannot be cleanly prevented on a raw keydown across the worker boundary. Enter-primary is the clean, correct baseline.
…rd nav on FleetGrid (#14619)

Atop Tier-1's keyboard-operable cards, the FleetGrid card set is now a single tab stop with arrow-key roving. The keys config auto-wires KeyNavigation (Up/Left step back, Down/Right advance); focusIndex tracks the active card (tabIndex 0, every other card -1); moveFocus clamps to the card count and moves DOM focus (mounted-guarded); refreshGrid re-establishes the tab stop on a roster rebuild WITHOUT stealing focus. The neo-selection class opts the card region into the main-thread arrow-key preventDefault rule so navigation never scrolls the viewport.

The roving state machine (focusIndex + tabindex + clamp + rebuild-reclamp) is unit-verified (12/12 fleetGrid.spec); the actual DOM focus-movement is the whitebox-e2e layer (mounted-guarded off in units).
@neo-opus-vega neo-opus-vega changed the title feat(agentos): FM cockpit focus-order slice 1 — keyboard-operable AgentCards (Enter-primary) (#14619) feat(agentos): FM cockpit focus-order — keyboard-operable cards + roving-tabindex (#14619) Jul 12, 2026
@neo-opus-vega neo-opus-vega changed the title feat(agentos): FM cockpit focus-order — keyboard-operable cards + roving-tabindex (#14619) feat(agentos): FM cockpit keyboard a11y — roving focus, Enter/Space activation, aria-live + landmarks (#14619) Jul 12, 2026
…ross roster rebuild; correct AgentCard Space-activation doc (#14619)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

FM cockpit accessibility pass: focus order, contrast, reduced motion

1 participant