LearnNewsExamplesServices
Frontmatter
id17553
titleFleet grid: animated sortable roster; selection drives the panes
stateClosed
labels
enhancementdesignaiagent-os
assigneesneo-gpt-emmy
createdAtAug 22, 2026, 5:55 PM
updatedAtAug 23, 2026, 1:21 PM
githubUrlhttps://github.com/neomjs/neo/issues/17553
authorneo-fable-clio
commentsCount1
parentIssue17559
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[x] 17552 list.plugin.Animate: resize-aware fluid columns + component-list parity
blocking[ ] 17601 The AgentCard contract still declares a retired Button the card''s only disclosure route
closedAtAug 23, 2026, 1:21 PM

Fleet grid: animated sortable roster; selection drives the panes

Closed Backlog/active-chunk-18 enhancementdesignaiagent-os
neo-fable-clio
neo-fable-clio commented on Aug 22, 2026, 5:55 PM

Context

Operator brief 2026-08-22 (paired session, verbatim intent): "in neo we have ANIMATED component-based lists, that can move cards, fade-in, fade-out, select(!). we could add different sorting options (social name, online+social name, latest activity (that would move a lot => animation firework), filter => hide offline and benched. now when we have selections, the redundant bottom agent selection buttons can go away => terrible UX anyway. so much room to do better here — the product needs love."

Measured realities (dev cockpit, 2026-08-22): grid measure 935px → 2-col; every card uniformly 447×126 (the anatomy is already uniform — a fixed itemHeight is a measured constant, not a compromise); the card region is the scroll owner (scrollHeight 666 vs 275 visible). The live-fleet deployment renders 9 agents 3-col with the same shape.

Intake correction — 2026-08-23: the consumed-surface Contract Ledger is backfilled below; native parent #17559 and merged prerequisites are folded into Related; ADR 0029 wording now cites its actual §2.6 pane-layout-blindness contract. Clio remains the originating author; Emmy owns the operator-directed completion from parked head 11d564107d.

The Problem

Three structural gaps, one product feel:

  1. The roster is a rebuild, not a list. FleetGrid (apps/agentos/view/fleet/FleetGrid.mjs) is a container that removeAll(true) + re-adds every card on every store load and every tier-moving recordChange — which is why ~60 lines of focus-continuity machinery exist (the FocusManager.history intersection, SEMANTIC_CHILD_REFS restore, the Up/Down drill jump): they compensate for destroy/recreate. Nothing moves, nothing fades; a roster change flashes.
  2. The order is fixed and the operator has no lever. rankFleet hard-ranks online → idle (folds ≥ foldThreshold) → benched, each tier sorted by agentId — not even by name. No sort choice, no filters.
  3. Selection does not exist — so two surfaces invent pickers. A card's dedicated drill Button fires a one-shot agentSelect (AgentCardController.onCardSelect) toward the detail pane, and MemoriesPane renders its OWN roster copy as chip buttons (agentOptions_, pushed by the cockpit at FleetCockpit.mjs:1525/2811) — the redundant bottom picker the operator names as terrible UX. There is no cockpit-level "selected agent" state.

The Architectural Reality

  • The store is already animation-ready: reconcileRoster (FleetCockpit.mjs) mutates record-grain — record.set(row) in place, one batched add for joiners, remove for leavers. A store-driven list animates exactly these events; the ingest path needs no change.
  • The primitive exists: Neo.list.Component (component-based items with instance reuse via createItemContent — sibling pattern src/calendar/view/calendars/List.mjs) + Neo.list.plugin.Animate (explicit component-list sort support). The plugin's blocking gaps (resize awareness, fluid item width, owner scroll height, reduced motion, component filter parity) are #17552 — this ticket is blocked by it.
  • Selection is a solved contract: Neo.selection.ListModel — single-select, selectionChange, selectedCls, built-in item focus/arrow navigation — retiring the grid's hand-rolled machinery wholesale.
  • The provider is the seam: the cockpit's stateProvider hosts stores.fleetRoster (FleetCockpit.mjs:246); the selected agent joins it as provider data so dock-projected panes (docked, vesseled, torn out) read one truth.
  • Sort axes on FleetAgent: displayName (Brain-folded social name — the sample roster renders Clio/Ada/Grace/…), state (ok|idle|wedged|limited|off), participationStatus (operator_benched is the authoritative bench fact), presence.lastSeenAt (plane axis; null while presence is unobservable — today's in-process mode). No latest-activity stamp exists — Fix 4.

The Fix

  1. FleetGrid rebuilds on Neo.list.Componentanimate: true, itemHeight = the measured 126px anatomy constant, fluid columns via #17552's minItemWidth, preserving today's ladder (1-up ≤640, 2-up default, 3-up ≥1280 of grid measure; the container-query values move from FleetGrid.scss into the JS ladder input — ADR 0029 §2.6 pane-layout-blindness intact: the list measures only its own rendered surface). AgentCard becomes the component item (create/reuse keyed by record, calendar-List pattern); its inner anatomy, controller and lifecycle events are untouched. The stable head (title · stale marker · presence-cap chip · HealthBar) stays the sibling above the list; the empty-fleet CTA renders outside the list while the store is empty.
  2. A compact sort cluster in the grid head (grid-owned — the cockpit top toolbar is #17549's surface, untouched). Three options:
    • Online first · name (DEFAULT — today's tier order expressed as a sorter: a derived tierRank model field from state, then case-folded displayName; continuity, not a behavior change),
    • Name A–Z (displayName),
    • Latest activity (DESC on lastActivityAt, nulls last — "that would move a lot": the firework is real once the axis is stamped). Mechanics = the store's own sorters (examples/list/animate pattern: store.sorters[0].set({property, direction})).
  3. Filters: hide offline (state === 'off'), hide benched (participationStatus === 'operator_benched'), and the density fold re-expressed as a filter preset: at foldThreshold (≥12) the idle tier filters out by default while the head renders the honest count ("+N idle · show") toggling it — the fold row leaves the card flow (a non-record row cannot live in a store-driven list; the count moves to the head, same truth). Store filters → the plugin fades. Session-local, not persisted.
  4. lastActivityAt — stamped Brain-side, never view-derived: the roster DTO row (ai/services/fleet/fleetCockpitStatus.mjs, the openLaneCount precedent) gains a tri-state lastActivityAt (ISO string | null) stamped by the assembler from the newest attributable per-agent activity timestamp it already holds (activity-adapter occurredAt fold; presence.lastSeenAt merges in when the presence axis is wired). mapRosterRow passes it through. Null = not stamped → tail of the sort, honestly (in today's bearerless dev mode every row is null and the sort degrades to name order — correct, not broken).
  5. Selection drives the panes: single-select selectionModel on the list; card click / Enter selects; selectionChange → the cockpit writes data.selectedAgentId on its provider AND runs today's detail path (record → detailRecord → reveal). The in-card lifecycle Buttons (toggle/restart) are carved out of the selection path — a control click never changes selection. The dedicated drill Button retires (the item itself is the target); agentSelect goes with it.
  6. The redundant picker dies: MemoriesPane drops agentOptions_ + the chip row; activeAgent follows the provider's selectedAgentId (write-through replaces the pushes at 1525/2811). Null selection renders the existing "Pick an agent…" empty state — now satisfied by clicking a CARD: one picker for the whole cockpit. The pane keeps a one-line "for: " context label. (#17268 keeps the pane's content design; only the target-selection mechanism changes here.)
  7. Retire the compensation machinery: the Up/Down KeyNavigation drill jump, SEMANTIC_CHILD_REFS, and the gate-3 focus restore in refreshGrid delete — native list item focus/arrow navigation + no-rebuild reconciliation replace them; their specs migrate to the list behaviors.
  8. SCSS: .fm-fleet-cards's CSS grid + container queries retire in favor of plugin geometry; token-only skin for the sort/filter cluster and the selected-card state (a --fm-signal ring — selection is a state the theme owns).

Contract Ledger Matrix

Target Surface Source of Authority Proposed Behavior Fallback / Edge Case Docs Evidence
AgentOS.view.fleet.roster.List (new consumed view) #17559 laws 0–2; Neo.list.Component; Neo.list.plugin.Animate provider-owned fleetRoster records render as stable AgentCard instances; sort moves and filters fade the same instances empty Store renders the existing CTA outside the list; reduced motion collapses transitions class JSDoc roster unit identity/motion fixtures + headed join/leave/reorder
roster sort/filter controls + FleetAgent.tierRank operator brief; existing state, displayName, participationStatus fields online/name default, name A–Z, latest-activity DESC; offline/benched/fold filters mutate Store sorters/filters null activity sorts behind stamped rows; controls are session-local model/control JSDoc producer/view unit matrix + mounted motion
FleetAgent.lastActivityAt + roster DTO field (new) Brain-side roster assembler; activity occurredAt and observable presence timestamps ISO timestamp or null, stamped before the view; mapRosterRow passes it through unchanged no attributable/observable timestamp → null; the view never invents one producer + model JSDoc assembler fixture + model passthrough + sort fixture
cockpit provider selectedAgentId / selectedAgentIdentity (new data keys) #17559 law 2; root state.Provider; one-detail/one-memories product semantic single list selection writes record id + canonical @github identity and drives detail/memories consumers clear/missing record writes both keys null; no second picker truth provider/roster-controller JSDoc selection unit + mounted click/Enter + clear path
lifecycle-control selection carve-out existing AgentCard lifecycle controls + Neo.selection.ListModel item click/Enter selects; lifecycle button interaction never changes selection unsupported target/path → no selection mutation SelectionModel JSDoc path fixtures + headed control click
AgentOS.view.fleet.memories.Container target selection provider selection pair above; existing memories read contract removes agentOptions chip row; selected provider identity drives the request and a “for: name” label null selection keeps the existing pick-an-agent empty state class/controller JSDoc unit target transitions + headed pane retarget
.fm-fleet-cards list/card skin FM token layer + #17553 selected-card AC; generic list skin is not product authority list li stays a transparent zero-padding geometry carrier in every state; AgentCard owns background, selection and focus paint both themes resolve local neutral list tokens; stale theme assets fail the headed witness SCSS intent comments both-theme computed styles, item/card rect equality, hover/active/selected/focus checks

Acceptance Criteria

  • A sort change MOVES cards (translate transition; the same AgentCard instances survive, witnessed by instance identity across the sort) — no rebuild, no flash.
  • Filter toggles fade cards out/in; the fold preset auto-arms at foldThreshold with the honest head count and toggles live.
  • Selecting a card (click / Enter) sets the provider's selectedAgentId, reveals/updates the detail pane, and re-targets MemoriesPane — whose chip row is GONE; a control-button click never changes selection.
  • lastActivityAt is stamped Brain-side (tri-state), passed through mapRosterRow, and the latest-activity sort orders stamped rows DESC with nulls last (unit witnesses producer-side and view-side).
  • Live reconcile animates: a joiner fades in, a leaver fades out, a tier-moving state change repositions (witnessed against reconcileRoster's record-grain writes).
  • The focus-restore machinery and drill jump are deleted with their specs migrated; keyboard: arrows move item focus, Enter selects, Tab reaches the in-card controls.
  • Reduced motion honored end-to-end (the #17552 rule; no cockpit-side motion overrides).
  • npm run check-theme-surfaces green; tokens only.

Out of Scope

  • Card CONTENT anatomy (what a card says stays as shipped; #17549 owns header/detail IA).
  • MemoriesPane content design (#17268), the activity-stream buffered list (#17550), sort/filter persistence (cockpit presets), FleetCockpit decomposition (#17335 — additions here stay lean and factored).
  • Multi-select (single is the product semantic: one detail pane, one memories target).

Avoided Traps

  • View-derived activity recency: folding the activity feed into lastActivityAt client-side would create a second truth the producer cannot correct — the DTO owns the stamp end-to-end (the openLaneCount precedent; the model's own passthrough discipline).
  • Keeping both pickers "for safety": the chips' only job was selection; with a selection model they are dead weight — a half-removal leaves two sources of target truth.
  • Hand-rolled animation on the container: the grid could translate its own children, but that re-implements the plugin badly and keeps the rebuild. The primitive exists — consume it.

Related

  • Prerequisites landed: #17552 / PR #17556 (list.plugin.Animate) and #17560 / PR #17566 (fleet topology).
  • Parent: #17559. Related umbrella: #14560.
  • Boundaries: #17549 (header/detail IA), #17268 (pane content design), #17550 / PR #17585 (activity list + reusable list fixes), #17335 (cockpit LOC bar), #16824 (empty-roster truth — the CTA/empty semantics defer to it).
  • Post-merge: #14618 re-records the visual baselines (standing owner; the goldens are rendered-platform artifacts per its ledger).

Decision Record impact: aligned-with ADR 0029 §2.6 (pane-layout-blind geometry — the roster measures its own rendered list surface and contains no dock/viewport-placement knowledge).

Live latest-open sweep: checked latest 20 open issues at 2026-08-22T15:51:54Z (gh issue list --state open --limit 20); no equivalent found. A2A claim sweep at 15:52Z (latest 14, recency-bound): no competing claim; the adjacent #17550 lane-pick (Emmy) is a disjoint surface.

Origin Session ID: 14acab5a-4b6c-4987-91c7-f683e39baa55 Retrieval Hint: "fleet roster animated list sort filter selection replaces memories chips"

tobiu cross-referenced by PR #17584 on Aug 23, 2026, 12:58 AM
tobiu unassigned from @tobiu on Aug 23, 2026, 2:35 AM
tobiu referenced in commit 4699d22 - "feat(agentos): animate and select the Fleet roster (#17553) (#17593) on Aug 23, 2026, 1:21 PM
tobiu closed this issue on Aug 23, 2026, 1:21 PM