Skip to content

feat(sidebar): FLEET header Add-machine + Refresh actions, visible focus control #1764

Open

Description

@jeonghun-jj-lee

Important

Problem — The FLEET sidebar section offers no way to add a machine or to refresh, unlike the Research and Development headers, which carry actions. Each machine row also shows a blank white pill: the focus control from #1451, shipped with no icon, label, or styling. It changes which machine new chats default to, yet the sidebar never shows which machine is focused, because the host never re-renders the section on a focus change — #1451's "the focused machine renders" criterion was only ever exercised by unit tests. The section's change watcher arms only if the fleet directory already exists when the sidebar loads, so a machine that joins a fleet later falls back to the 30-second timer. Separately, Troubleshoot — from the sidebar and from the boot-time fleet warning — silently does nothing when the chat panel is closed.
Approach — Extend the existing sidebar seams. The FLEET header gains Add machine (+) and Refresh (↻), built the same way as the Research and Development header actions. Add machine opens a chat running the guided skill chosen by the section's rendered state: create-a-fleet when standalone, add-fleet-device otherwise. Refresh re-reads the roster now and re-arms the change watcher. The focus control gets a target glyph, a tooltip, and styling, and the host re-renders the section on every focus change. A shared chat-session launcher opens or reveals the chat panel and delivers the prompt exactly once; Add machine and both Troubleshoot entry points use it.
Scope — in: the two header actions; the watcher re-arm; the focus control's visuals, state, and re-render; the shared launcher; "machine" wording in the section's own strings; two new glossary entries plus one corrected entry. out: persisting focus across reloads; picker support for machines that own no sessions; moving New Project and New Environment onto the launcher; a live reachability probe; Troubleshoot as a header icon; a notice for an absent focused machine; automatic watcher re-arm without a Refresh click (the 30-second sweep keeps covering that case); the connect Quick Pick's wording (ADR 0031 retires it).
Assumptions — The add-fleet-device and create-a-fleet skills ship with the extension and drive enrollment themselves. Each machine's own heartbeat stays the only writer of its roster row. The chat app announces app-ready on every mount.

Acceptance Criteria

  • The FLEET header shows "Add machine to fleet" then "Refresh fleet" after its title, each with an accessible name and a tooltip; clicking either neither collapses nor drags the section.
  • Add machine routes on the section's last-posted state: standalone sends create-a-fleet; populated, empty, unreachable, or nothing posted yet sends add-fleet-device.
  • One Add machine click opens exactly one chat session, whether the chat panel was closed, still loading, or open and ready; a later app remount never re-sends it; a repeat launch of the same prompt within 1000 ms opens nothing more (the launcher owns this guard, so both Troubleshoot entry points inherit it).
  • A launch queued for app-ready that does not see app-ready within 15 seconds is dropped with a warning that the chat did not finish loading — never a silent no-op. Launches queue independently: two different launches while the panel loads both deliver, once each.
  • The chat panel's ready state resets when the panel is disposed and whenever the host sets the panel's frame; a launch in either window is queued, not posted.
  • Add machine or Troubleshoot clicked before the engine is ready shows the same "server isn't ready yet" warning New Project shows.
  • Both Troubleshoot entry points open the chat panel when it is closed and start the troubleshoot-fleet session.
  • Refresh marks its button busy immediately, re-subscribes the change watcher exactly once, and only then re-reads the roster — a client re-fetches only the hub's roster and contacts no individual peer. The host posts the section update after every refresh, changed or not, succeeded or failed (a failed local or hub read posts the unreachable state, as today), and any section update clears busy. If the 10-second backstop fires instead, the button's tooltip says the last refresh did not complete until the next update arrives. Clicks while busy are ignored; with reduced motion preferred, busy shows without spinning.
  • On a machine whose fleet directory appeared after the sidebar loaded, a roster change after a refresh re-renders the section without waiting for the 30-second timer.
  • Every row's focus control shows a target glyph with the tooltip "Focus ‹machine name›"; the focused row shows the glyph filled, always visible, and pressed, with the tooltip "Focused"; other rows reveal the glyph on hover or keyboard focus.
  • Focus is matched on the roster machine id, byte-for-byte as the focus control posts it; focusing the local row clears focus to home. At home, the local row — the self-row the view-model builds from this machine's own identity — is the focused row; with a remote machine focused, only that row is; when the focused machine is absent from the list, no row is.
  • A focus change notifies the chat overlay first and then re-renders the FLEET section in the same update; a sidebar render failure never blocks the overlay notification.
  • The standalone notice reads "This machine isn't in a fleet yet." and the empty state reads "No machines reporting yet".
  • The glossary gains Machine and Focused machine entries, and its Sidebar entry states that the fleet section offers navigation actions only and never writes fleet config or roster rows.
  • The fast suite and the boot smoke pass.

Testing Decisions

Reuse-first. Extend the sidebar actions suite (flip its "Fleet gets none" assertion; add the new labels and message kinds), the bridge dispatch tests (the two new kinds), the sidebar host suite (routing for each of the four section states and for nothing posted yet; refresh re-subscribes once and before the read; a roster change landing between re-arm and read still updates the section; a refresh with an unchanged roster clears busy without the backstop; a client refresh calls only the hub-roster read; focus reaching the view-model; the new style rules), and the fleet-section DOM suite (focused-row rules, a focus round trip from the row's own machine id, glyph and tooltips, copy). The focus host suite gains a case where the sidebar hook throws and the overlay is still notified. One new suite covers the launcher: engine not ready; panel ready; panel absent or loading; two app-ready events yielding one delivery; two different launches queued while loading, each delivered once; a repeat launch inside 1000 ms suppressed; app-ready never arriving (deadline warning, no post); a stale ready state after dispose-and-reopen and after the host resets the frame (queued, then delivered once). The chat panel's ready tracking gets a unit test, and the existing New Project tests stay green unchanged. Structural source checks cover the activation wiring that cannot be unit-tested: the focus host receives its change hook; the fleet section's dependencies bind the focused-machine read to the real focus host; the boot-warning Troubleshoot no longer navigates through the peek-only path; and the three routed skills ship in the extension's skill library. A manual Extension Development Host pass covers both themes, keyboard reveal of the focus glyph, and Add machine with the chat panel closed.

Key Decisions

  • Fleet members are "machine" in every string the section renders; "device" stays reserved for a QPU or emulator behind a Connection. The unrelated QICK "Refresh device status" command is untouched.
  • add-fleet-machine carries no payload: the host routes on its cache of the view-model it last posted (standalone, populated, empty, unreachable), at click time — it never recomputes stance from fleet config, so the button and the section agree by construction. When nothing has been posted yet, add-fleet-device is safe because that skill's own first step hands off to create-a-fleet when no fleet exists. The header stays static; no header re-render path is added.
  • The refresh reply is the existing section-update message, posted unconditionally after every refresh (the host does not de-duplicate unchanged renders), including the unreachable state when a read fails. It carries no refresh correlation: any section update clears busy, and a timer or watcher update landing mid-refresh is accepted as equally fresh.
  • Refresh is read-only: health comes from each machine's own heartbeat, which the sidebar cannot make fresher.
  • The focus read returns the focus host's scoped machine id, or nothing for home. The focus host stores exactly the machine id the focus control posted (the roster machine id), unnormalized; a local selection stores nothing (home). The local row is the self-row the view-model synthesizes from this machine's own identity, present in every section state that lists rows.
  • The focus host notifies the chat overlay before it calls the sidebar re-render hook, and isolates that hook, so focus for new chats never depends on the sidebar rendering.
  • The focus tooltip uses the domain term only, because the new-session picker cannot yet list a machine that owns no sessions.
  • At home the local row is the focused row, so exactly one row is focused whenever the focused machine is listed (ADR 0031 §D4: single-select, default this machine). No row tint: glyph shape plus the pressed state carry focus, keeping the connect click and focus visibly distinct.
  • Launcher contract: it posts the existing navigate message with the new-session path, the prompt, and auto-send set — the same message New Project and Troubleshoot post today, which the chat app already honours, so no overlay change. The prompts are exactly "/create-a-fleet", "/add-fleet-device", and "/troubleshoot-fleet". Delivery is immediate only when the panel's app has signalled ready since its last dispose or since the host last set its frame; otherwise the launch is queued for app-ready with a 15-second deadline. Each launch queues independently; a sent-guard backstops any second delivery; a repeat of the same prompt within 1000 ms is suppressed in the launcher.
  • Named residual: a reload the chat app starts on its own, which the host cannot observe, can still race a launch until the next app-ready; closing it needs a per-navigate acknowledgement from the app, an overlay change deferred.
  • New Project and New Environment keep their existing path untouched and do not read the new ready state.
  • Build order inside the slice — checkpoints, each with its own green tests: (1) the launcher and the panel's ready state; (2) the Troubleshoot migration and Add machine on the launcher; (3) Refresh with the watcher re-arm; (4) the focus glyph, read seam, and re-render; (5) wording and glossary. Never build on New Project's existing path. One issue rather than sub-issues is deliberate (decided in the design review): one branch, one review of one sidebar section; checkpoints 3–5 share no seam with the launcher and are bundled for that review, not for coupling.
  • Field contracts: routing reads the posted view-model's section state field (its four values); the local self-row is the row whose local flag is set, carrying this machine's own machine id (the canonical host on a server, the hostname on a client); the focus re-render rebuilds the whole view-model from the same reads as every other push, so it also refreshes the routing cache; the webview clears the Refresh button's busy state from its existing section-update handler.
  • The launcher adds no app-ready callback when it posts immediately. A queued launch whose panel is disposed before app-ready is dropped with the same warning as the deadline, never delivered into a later panel. New Project's lingering one-shot callback is pre-existing: any later mount — including one the launcher triggers — can fire it exactly as any mount does today; the follow-up that moves New Project onto the launcher closes it.
  • The focus host logs (to the extension output channel) when its isolated sidebar hook throws, and the launcher logs when its sent-guard trips, so neither failure is silent in the log.
  • New message kinds add-fleet-machine and refresh-fleet. The fleet host dependencies gain one read seam for the focused machine and no write seam.

Constraints & Invariants

  • The sidebar emits no fleet-config or roster-write message (ADR 0026, as amended by ADR 0030 and 0031). Enrollment happens only inside the chat, under the skill's confirmation steps.
  • The row click still posts connect-to-device unchanged; focus and connect stay independent (Fleet Studio wiring — W3: wire the fleet focus selector into the sidebar #1451).
  • No engine or overlay change, so no binary rebuild.
  • Icon-only controls carry accessible names; the hidden focus control stays in tab order; nothing shifts layout on hover, press, or busy.

Prior Art

  • The Research and Development header actions (sidebar webview) — the pattern to mirror, including the header's collapse and drag guards.
  • The fleet section renderer and view-model builder (sidebar_fleet_section) — focused-row marking already exists; production never supplies the focused id.
  • The focus host (fleet_focus) — its change hook is documented as the sidebar re-render hook but never passed.
  • New Project's session launch — the open-or-reveal plus app-ready pattern, and its lingering one-shot callback.
  • The heartbeat producer (fleet_heartbeat) and the new-session machine picker — why refresh cannot freshen peers, and what focus drives today.

Source

Follows #1451 (its render criterion unmet in production), #1321, and #1409. Ships into feature/free-tier-fleet, which reaches main through #1407; the fleet section does not exist on main yet. Checked against ADR 0026, ADR 0030, ADR 0031, and the repo glossary. Design session: brainstorming → grill-with-docs, 2026-10-11.

Notes

Follow-ups, out of scope: persist focus across reloads; let the new-session picker list roster machines that own no sessions; move New Project and New Environment onto the launcher (same latent double-send); a corrupt fleet config reads as standalone, which routes Add machine to create-a-fleet; a notice when the focused machine is not reporting; automatic watcher re-arm without a Refresh click; a per-navigate acknowledgement from the chat app; a live reachability probe as a stronger refresh.

Spec review: rounds 1–3 all approved (0 blocking each; 18, 22, and 18 advisories), every lens run, critics on the vendored engine. Advisories folded into this text where they sharpen a contract; the Field contracts, logging, and checkpoint lines in Key Decisions were added after round 3 and are clarifications only, unreviewed as edits. Waived with reasons: splitting into sub-issues and an absent-focus notice (both decided in the design review); surfacing a failed watcher re-arm (the 30-second sweep still updates the section); a boot-smoke focus assertion (activation is not unit-constructible; structural checks plus the manual pass cover it); the self-reload residual and an any-update busy clear (named above as accepted residuals).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions