Skip to content

feat(console): Fleet Room — a ⌘K morph-view for the fleet#2129

Merged
mabry1985 merged 6 commits into
mainfrom
feat/fleet-room-cmdk
Jul 22, 2026
Merged

feat(console): Fleet Room — a ⌘K morph-view for the fleet#2129
mabry1985 merged 6 commits into
mainfrom
feat/fleet-room-cmdk

Conversation

@mabry1985

@mabry1985 mabry1985 commented Jul 22, 2026

Copy link
Copy Markdown
Member

What

Turn the fleet from a page-load switcher into a co-present room summoned from ⌘K — the first piece of the command-palette UX overhaul, and a console-native take on Block/Buzz's "workspace = members-in-a-room" idea (adapted onto our A2A + fleet-proxy substrate — no Nostr, no new infra).

⌘K → Fleet Room (Agents group) morphs the palette into a native view — a sibling of PaletteChat — showing every workspace agent as a presence-aware member:

  • Roster + presence — dots encode running / stopped / remote / unreachable / this-instance, with bundle · :port meta. Straight from api.fleet().
  • Address in place — click a member, type, ↵ → a slug-targeted send to that member's /api/chat through the hub proxy. No navigation, no leaving the room.
  • Broadcast — ⌘↵ (or the target chip) fans the message out to every online member.
  • Open / Start / Stop — per-row, reusing the existing fleet wires.

Why core + default

The fleet was already a first-party palette citizen (quick-chat #1733, toggle #1769) and PaletteChat is the exact precedent for a native morph-view — so this is a small extension of existing code in usePaletteRegistry, not a plugin. Ungated, on by default.

How

  • FleetRoom.tsx — the native PaletteView; styled entirely through DS tokens (--pl-color-*) so it tracks the console theme in light + dark, no hand-picked colors.
  • api.sendToAgent(slug, msg) — non-streaming /api/chat through /agents/<slug>/…; works in the browser and the desktop WKWebView shell alike (no SSE body to read). Broadcast is a fire-and-forget fan-out.
  • usePaletteRegistry — registers the view + a "Fleet Room" command at the top of the Agents group; opening a member routes through the palette nav chokepoint, so it forwards correctly from the frameless desktop launcher too.

Scope

Additive — the existing quick-chat/toggle palette entries are untouched; folding them into the room is the natural next step of the overhaul. Deferred to v1 (additive layers on this shell): the live fleet-wide activity feed (aggregate each member's event bus, ADR 0039) + inline reply transcripts.

Testing

  • tsc --noEmit, vitest unit, vite build (incl. the css-comment prebuild) — all clean.
  • e2e: new ⌘K → Fleet Room lists members with presence and addresses one; full fleet.spec.ts (12) + keybindings.spec.ts (7) green.

Draft — this is a UX call

Leaving as draft, no auto-merge: the "feel" is the reviewable part, and CI can't judge it. The shipped view uses real DS tokens so it's closer to the console than the earlier standalone concept mockup (kept for reference: https://claude.ai/code/artifact/ed0bb3df-35ee-4337-a676-83449e1df8c4).

🤖 Generated with Claude Code

https://claude.ai/code/session_01LyQuxgkFpMUJAQbVXnqz2d

Summary by CodeRabbit

  • New Features

    • Added a Fleet Room accessible from the command palette.
    • View fleet members, presence status, and live fleet activity in one place.
    • Open direct chats with individual members, with separate conversation history.
    • Broadcast messages to all online members and receive delivery feedback.
    • Added controls for starting or stopping supported local members.
    • Added live activity updates, including member status changes and ongoing work.
  • Tests

    • Expanded end-to-end coverage for Fleet Room navigation, chat, broadcasting, roster presence, and activity feeds.

Turn the fleet from a page-load switcher into a co-present room summoned from
the command palette (part of the ⌘K palette-UX overhaul). ⌘K → "Fleet Room"
(Agents group) morphs the palette into a native view — a sibling of PaletteChat
— showing every workspace agent as a presence-aware member you can Open,
start/stop, or address in place, plus a one-key broadcast to everyone online.

- FleetRoom.tsx: native PaletteView (roster + presence + address/broadcast),
  styled entirely through DS tokens so it tracks the console theme in light+dark.
- api.sendToAgent(slug, msg): slug-targeted, non-streaming /api/chat send through
  the hub's per-agent proxy (works in the browser and desktop WKWebView alike).
  Broadcast fans it out to every online member.
- usePaletteRegistry: register the view + a "Fleet Room" command at the top of
  the Agents group; opening a member routes through the palette nav chokepoint,
  so it forwards correctly from the frameless desktop launcher too.
- e2e: ⌘K → Fleet Room lists members with presence and addresses one.

Core + default, ungated. Additive — the existing quick-chat/toggle palette
entries are untouched; folding them into the room is the next step of the
overhaul. Deferred to v1: the live fleet-wide activity feed (aggregate each
member's event bus, ADR 0039) + inline reply transcripts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyQuxgkFpMUJAQbVXnqz2d
@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 36ff0f9e-cbd5-4c85-92d7-4dc853f6532b

📥 Commits

Reviewing files that changed from the base of the PR and between bfae50d and 24e7242.

📒 Files selected for processing (10)
  • apps/web/e2e/fleet.spec.ts
  • apps/web/e2e/mock-server.mjs
  • apps/web/src/app/FleetActivity.tsx
  • apps/web/src/app/FleetRoom.tsx
  • apps/web/src/app/PaletteChat.tsx
  • apps/web/src/app/fleet-activity.css
  • apps/web/src/app/fleet-room.css
  • apps/web/src/app/paletteChatStore.ts
  • apps/web/src/app/usePaletteRegistry.ts
  • apps/web/src/lib/api.ts

Walkthrough

The PR adds a Fleet Room command-palette view with a sorted member roster, member DMs, start/stop controls, broadcast messaging, and a live activity feed. It adds member-scoped API routing and transcript persistence, plus Playwright coverage and mock chat handling.

Changes

Fleet Room

Layer / File(s) Summary
Member communication APIs
apps/web/src/lib/api.ts, apps/web/e2e/mock-server.mjs
Adds member URL and SSE-token helpers, fire-and-forget broadcasts, member-scoped streaming chat, and a non-streaming mock chat response.
Scoped member chat persistence
apps/web/src/app/paletteChatStore.ts, apps/web/src/app/PaletteChat.tsx
Namespaces persisted transcripts by member, changes DM clearing behavior, routes DM streams to the selected member, and adds the member DM palette view.
Live fleet activity feed
apps/web/src/app/FleetActivity.tsx, apps/web/src/app/fleet-activity.css
Tracks roster transitions and member SSE events, deduplicates and maps feed events, stores up to 60 items, and renders the activity column.
Fleet Room UI and palette wiring
apps/web/src/app/FleetRoom.tsx, apps/web/src/app/fleet-room.css, apps/web/src/app/usePaletteRegistry.ts, apps/web/e2e/fleet.spec.ts
Adds the roster, presence states, controls, broadcast composer, styling, command registration, navigation wiring, and end-to-end tests.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant Palette
  participant FleetRoom
  participant FleetAPI
  participant FleetMember
  participant ActivityFeed

  User->>Palette: Select Fleet Room
  Palette->>FleetRoom: Open fleet-room view
  FleetRoom->>FleetAPI: Fetch fleet state
  FleetAPI-->>FleetRoom: Return roster
  FleetRoom->>ActivityFeed: Start activity tracking
  ActivityFeed->>FleetMember: Open SSE stream
  User->>FleetRoom: Broadcast message
  FleetRoom->>FleetAPI: Send message to online members
  FleetAPI->>FleetMember: POST SendStreamingMessage
  FleetRoom->>ActivityFeed: Push broadcast event
Loading
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/fleet-room-cmdk

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

mabry1985 and others added 5 commits July 22, 2026 01:11
Rework the Fleet Room into the Discord model: click a member to DM it (the wired
⌘K chat, retargeted to that member), and the bottom bar broadcasts to everyone
online. Replaces v0's fire-and-forget "address", which sent into a hidden session
with no visible reply — testing showed that felt broken even though it worked.

- api.streamChat: new opts.agentSlug streams the turn to a SPECIFIC member via the
  hub proxy (/agents/<slug>/a2a, /api/chat fallback, desktop Tauri path included).
  Adds memberPath() (sendToAgent now shares it).
- PaletteChat: optional agentSlug prop → a DM pane; paletteChatStore threads are
  scoped per member (dm:<slug>) so DMing different members never crosses transcripts.
- memberDmView: registers the DM as a palette stack view, so Back/Escape return to
  the roster.
- FleetRoom: a member row → ctx.enter("member-dm", {slug,name}); the composer is now
  broadcast-only (fire-and-forget fan-out — the only path that stays async, since you
  can't stream N replies into one pane). Also toasts the single send immediately.
- e2e: DM a running member (asserts the retargeted chat opens) + Back + broadcast toast.

Almost entirely reuse — the DM IS the existing wired chat, just pointed at the
member; the only new transport is a slug target on streamChat.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyQuxgkFpMUJAQbVXnqz2d
Adds a right-side popout drawer with a live, fleet-wide event feed (the "one
event log" idea, on our own infra) — opened from the Fleet Room's Activity
button or the ⌘K "Fleet Activity" command; slides over the console, Esc/scrim
to close. Also cleans up the Fleet Room footer (keycap chips) and adds a top bar
(online/total count + the Activity toggle).

- FleetActivity.tsx: the drawer (portal at app root) + a small zustand store +
  a roster-diff capture hook. v1 sources REAL events only — member presence
  transitions (online/offline/joined/left, diffed from the fleet poll) and the
  broadcasts you send. Richer cross-member events (PRs, approvals, a member's
  running turn) are the next step — aggregate each member's event bus (ADR 0039)
  — and are deliberately NOT faked.
- Mounted once at the app root (App.tsx) so the feed accumulates even while closed.
- FleetRoom: a top bar (count + Activity button) and a keycap footer; broadcast
  pushes an event into the feed.
- usePaletteRegistry: a "Fleet Activity" ⌘K command.
- e2e: open the drawer from the Fleet Room + Escape to close.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyQuxgkFpMUJAQbVXnqz2d
…rawer

The activity feed is the RIGHT COLUMN of the Fleet Room dialog (next to the
roster), matching the concept mockup — not a separate console slide-over.

- FleetActivity.tsx: drop the portal drawer + open state; expose a headless
  FleetActivityCapture (roster-diff, runs at app root) + a FleetActivityFeed
  column the room renders.
- FleetRoom: two-column body (roster | activity feed), broadcast bar spanning
  below; widened the view to fit. Removed the Activity toggle button + top bar.
- App: mount the headless capture (was the drawer). usePaletteRegistry: drop the
  separate "Fleet Activity" command — it lives in the room now.
- e2e: assert the roster + activity feed render side by side in the dialog.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyQuxgkFpMUJAQbVXnqz2d
The activity column now streams REAL fleet activity — not just presence + your
own broadcasts. While the Fleet Room is open it opens an SSE stream per ONLINE
member (/agents/<slug>/api/events via the hub proxy, ADR 0039) and maps the
event-bus topics into the feed: turn.started/finished, activity.message,
inbox.item, scheduler.fired, goal.achieved/failed, background.completed. So a
broadcast to a member now surfaces "<member> is running a turn" → "finished a turn".

- api.sseTokenFor(slug): a member-scoped SSE token (best-effort; open-mode
  instances connect tokenless).
- FleetActivity.useFleetStreams: opens/closes EventSources as members come online /
  go offline; dedup by (slug, seq); an errored stream reopens on the next roster
  poll. mapTopic curates the useful topics and skips the noisy ones.
- Capture now runs only WHILE the Fleet Room is open (was always-on at app root) —
  the module store keeps the log across opens, and we don't hold SSE connections
  idle. This also removes the always-on load that flaked unrelated e2e specs.
- e2e: the feed populates from the mock's member event streams.

Real events only — no faked PR/approval rows; wiring the GitHub plugin + HITL
inbox as event sources is the next step for those richer types.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyQuxgkFpMUJAQbVXnqz2d
…y feed

Diagnosed live: a live member turn only publishes `turn.usage` to its event bus at
completion (started/tool frames stay on the turn's own SSE, not the bus), and the
non-streaming /api/chat path — which broadcast used — publishes nothing at all. So
the feed never showed members responding.

- FleetActivity.mapTopic: map `turn.usage` → "finished a turn" (state=failed →
  "hit an error"), the real per-turn signal. (Kept turn.started/finished too.)
- api.sendToAgent: fire the broadcast at the member's /a2a (SendStreamingMessage)
  instead of /api/chat, so the streaming turn publishes turn.usage. Fire-and-forget:
  the A2A task is durable, so we send then cancel the response stream and the turn
  finishes + emits its bus event. DMs already stream via /a2a, so they surface too.

Now broadcasting or DMing a member shows "<member> finished a turn" in the feed a
few seconds later — the round-trip you were missing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyQuxgkFpMUJAQbVXnqz2d
@mabry1985
mabry1985 marked this pull request as ready for review July 22, 2026 10:51
@mabry1985
mabry1985 merged commit 18b3c88 into main Jul 22, 2026
10 checks passed
@mabry1985
mabry1985 deleted the feat/fleet-room-cmdk branch July 22, 2026 10:51

@protoreview protoreview Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

QA panel review — FAIL

code-review-structural · head 24e7242c0ec8 · formal

[review-synthesizer completed: workflow code-review-structural:report]

Prose Brief

This PR carries moderate risk due to the SSE error-recovery gap in FleetActivity.tsx: a transient stream error on a running agent kills the EventSource permanently — the onerror handler explicitly closes the connection and the useEffect won't re-fire until the roster's running-agent set changes, which could be indefinitely. Fix that first. The three minor hygiene findings (a duplicated helper, two unreferenced CSS rules) are cost-free to land alongside but shouldn't gate the PR. The panel was unanimous — all findings confirmed, none refuted. No gaps from the structural pass surfaced findings beyond the panel's. Coverage looks complete: all touched files were read and assessed.

[
  {
    "file": "apps/web/src/app/FleetActivity.tsx",
    "line": 167,
    "severity": "major",
    "category": "correctness",
    "claim": "SSE streams dropped on error are never reopened: the onerror handler deletes the stream from the ref map and calls es.close() (preventing browser auto-reconnect), but the useEffect depends only on sig (a stable sorted-set string of running agent slugs), so it does not re-fire on routine roster polls — the stream stays dead until the roster composition actually changes.",
    "evidence": "useEffect(…, [sig]) at line 174 depends on the derived `sig` string (lines 130-133), not on `data`; es.onerror at lines 165-167 deletes from `streams.current` and calls `es.close()`. The comment on line 167 says 'the next roster poll reopens' but only a sig-changing poll triggers the effect.",
    "verdict": "confirmed",
    "note": "sig is derived from running-agent slugs; react-query structural sharing means data ref is stable across equal polls, so sig stays identical — the effect won't re-fire after a stream error unless a member goes on/offline or is added/removed. The error handler's es.close() also defeats EventSource auto-reconnect. Genuine gap.",
    "source": "correctness"
  },
  {
    "file": "apps/web/src/app/FleetRoom.tsx",
    "line": 29,
    "severity": "minor",
    "category": "conventions",
    "claim": "The `slugOf` helper is duplicated identically between FleetRoom.tsx and FleetActivity.tsx; FleetRoom already imports from FleetActivity and should reuse the existing definition.",
    "evidence": "FleetRoom.tsx line 29: `const slugOf = (a: FleetAgent): string => (a.host ? \"host\" : a.id);` — identical to FleetActivity.tsx line 54. FleetRoom already imports `{ FleetActivityFeed, pushFleetEvent }` from `./FleetActivity`.",
    "verdict": "confirmed",
    "note": "Both files define identical `const slugOf = (a: FleetAgent): string => (a.host ? \"host\" : a.id);`. FleetRoom already imports from './FleetActivity' — adding `slugOf` to that import would deduplicate cleanly.",
    "source": "conventions"
  },
  {
    "file": "apps/web/src/app/fleet-room.css",
    "line": 81,
    "severity": "minor",
    "category": "conventions",
    "claim": "Dead CSS: `.flr__member.is-target` is defined but never applied to any element in FleetRoom.tsx.",
    "evidence": "fleet-room.css lines 81-84 define `.flr__member.is-target { background: color-mix(...); border-color: color-mix(...); }`. FleetRoom.tsx only sets `is-down` dynamically: `className={`flr__member${a.running ? \"\" : \" is-down\"}`}` — `is-target` is never set.",
    "verdict": "confirmed",
    "note": "The only dynamic class on `.flr__member` in FleetRoom.tsx is `is-down`. No code in any changed TSX file sets `is-target` on a `.flr__member` element.",
    "source": "conventions"
  },
  {
    "file": "apps/web/src/app/fleet-room.css",
    "line": 226,
    "severity": "minor",
    "category": "conventions",
    "claim": "Dead CSS: `.flr__target-x` is defined but never referenced in any TSX file.",
    "evidence": "fleet-room.css lines 226-229 define `.flr__target-x { opacity: 0.55; font-weight: 700; }`. A search of all changed TSX files finds no usage of `flr__target-x` or `target-x`.",
    "verdict": "confirmed",
    "note": "`target-x` appears only in the CSS definition. The semantically close class `.flr__target` IS used (the broadcast badge), but `.flr__target-x` is a separate, unreferenced rule — likely a vestige of a planned close/dismiss button.",
    "source": "conventions"
  }
]

mabry1985 added a commit that referenced this pull request Jul 22, 2026
…" pill

Enriches the activity feed + roster from the signals that actually flow on the member
buses (no faked rows):
- Live "running a turn" roster pill: autonomous member turns bracket with
  turn.started/turn.finished (already on the bus); a direct broadcast is marked
  optimistically on send and cleared by its terminal turn.usage.
- More mapped topics + richer text: chat.resumed, goal.changed, plus the existing
  turn.usage / activity.message (with text) / inbox.item / scheduler (with name) /
  goal.achieved|failed / background.completed.

Deferred (need NEW sources — documented, not faked):
- "needs approval" pill/rows: requires a bus publish on the executor's input_required
  frame (a2a_impl/executor.py:517 → notify → server/a2a.py publishes "turn.input_required").
  Touches the core HITL path, so it's a supervised change, not done unattended overnight.
- "opened PR" rows: the GitHub plugin is out-of-tree; it would publish github.pr.* and
  the feed would map it.

Follow-up to #2129 / #2132.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyQuxgkFpMUJAQbVXnqz2d
mabry1985 added a commit that referenced this pull request Jul 22, 2026
…roval + running pills, host-scoping (#2132)

* feat(console): Fleet Room composer — @-address a member (regression fix)

Restores addressing a specific member from the composer — removed in the DM rework,
which left it broadcast-only (you couldn't @ an agent in the input anymore). Type "@"
in the composer → a member picker → an address chip; sending to that member opens its
DM with the message pre-sent (the wired chat streams the reply), so you see the response
instead of firing into the void. Broadcast stays the default (no chip, or ⌘↵).

- FleetRoom: @-mention autocomplete + address-target chip + send routing (a member →
  its DM with the message; otherwise broadcast). Footer: "@ address in composer".
- PaletteChat: optional `initial` prop auto-sends once on open; memberDmView threads it
  through from ctx.props.
- e2e: @-address ava → send → ava's DM opens.

Follow-up to #2129. Still real-events-only in the activity feed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyQuxgkFpMUJAQbVXnqz2d

* feat(console): Fleet Room — wire real event sources + a live "running" pill

Enriches the activity feed + roster from the signals that actually flow on the member
buses (no faked rows):
- Live "running a turn" roster pill: autonomous member turns bracket with
  turn.started/turn.finished (already on the bus); a direct broadcast is marked
  optimistically on send and cleared by its terminal turn.usage.
- More mapped topics + richer text: chat.resumed, goal.changed, plus the existing
  turn.usage / activity.message (with text) / inbox.item / scheduler (with name) /
  goal.achieved|failed / background.completed.

Deferred (need NEW sources — documented, not faked):
- "needs approval" pill/rows: requires a bus publish on the executor's input_required
  frame (a2a_impl/executor.py:517 → notify → server/a2a.py publishes "turn.input_required").
  Touches the core HITL path, so it's a supervised change, not done unattended overnight.
- "opened PR" rows: the GitHub plugin is out-of-tree; it would publish github.pr.* and
  the feed would map it.

Follow-up to #2129 / #2132.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyQuxgkFpMUJAQbVXnqz2d

* fix(console): Fleet Room review fixes — running-pill correctness + edge cases

From a self-review pass on the branch:
- Broadcast send-failure now clears the optimistic "running" mark (was stuck forever
  and survived room reopens via the module-level store).
- The running count decrements ONLY on turn.usage (not turn.finished) — autonomous
  turns emit both, which double-decremented and corrupted a concurrent member's count.
- A member's in-flight count resets when its stream closes / it goes offline; the pill
  renders only on online members.
- Errored SSE streams reopen on a backoff (the roster sig doesn't change, so the effect
  wouldn't re-run); the token-fetch continuation bails if unmounted.
- Addressing a member who left the fleet no longer silently broadcasts — it toasts and
  resets to broadcast.
- The DM initial-message auto-send skips when the member's thread has a turn still
  streaming (the self-heal owns that reconnect), avoiding a clobbering concurrent send.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyQuxgkFpMUJAQbVXnqz2d

* feat: wire the deferred Fleet Room event sources (HITL approval + GitHub PRs)

Both sources the activity feed was missing are now real, end to end.

HITL "needs approval" (in-tree):
- a2a_impl/executor.py: the input_required branch now fires the existing best-effort
  progress hook with {phase: "input_required", prompt}. The hook swallows everything by
  contract, so a parked HITL turn can't be broken by a consumer.
- server/a2a.py: _a2a_progress publishes `turn.input_required` for EVERY context (it
  previously early-returned on non-background), carrying task/context ids + a short prompt.
- Console: maps it to an actionable "⚠ needs your approval — <what>" feed row and a
  roster "needs approval" pill; the pill clears when the parked turn reaches turn.usage.
  A member awaiting input is no longer counted as "running".

GitHub PR events (github-plugin, separate PR):
- The plugin emits `github.pr.opened` / `github.pr.merged` via registry.emit (ADR 0039,
  auto-namespaced), threaded in as a guarded optional `emit` so an older host still loads.
- Console maps both to "opened PR #N · title" / "merged PR #N".

Gates: pytest ✓, ruff ✓, import contracts ✓ (a2a_impl/server layering kept), tsc ✓,
build ✓, e2e 21 ✓, unit 628 ✓; github-plugin 91 ✓.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyQuxgkFpMUJAQbVXnqz2d

* feat(console): fold fleet controls into the Fleet Room + HITL resume hand-back

Folds the two remaining standalone ⌘K fleet features into the room:
- quick-chat (#1733): the per-member root commands are gone; member names ride
  the Fleet Room command's keywords (typing "ava" surfaces the room) and the
  roster row's DM/open cover the flows. fleetPalette.ts slims to the recency
  store.
- Toggle Fleet Agent (#1769): the submorph + root command are gone; the roster
  row's Start/Stop is the control.

Completes the HITL loop 8e0d44a opened (turn.input_required):
- executor turn_started frames now carry resumed=<bool>; the server publishes
  turn.resumed for every context, so "needs approval" hands BACK to "running"
  the moment the answer lands (previously the pill sat on needs-approval until
  the terminal frame). Console consumes it (pill + "resumed — input received"
  feed row). 8 new tests cover the publisher + the executor frames — both were
  untested.

DM polish: the member-dm header now names the member ("@ava"). PaletteView
titles are registration-static, so the DM body records its target in a tiny
store the title node reads — the known rough edge from the DM rework.

e2e: the toggle-picker specs became room-based coverage (stop/start from the
roster); new specs pin the fold (no per-member root commands; names route to
the room) and walk the full HITL pill lifecycle over a slug-gated mock stream
(x-e2e-hitl); the DM test asserts the "@ava" header.

Gates: pytest 3956 ✓ · ruff ✓ · lint-imports ✓ · live-smoke ✓ (isolated
host-config — the local box host-config.yaml binds 0.0.0.0, which trips the
open-bind guard outside CI) · tsc/build ✓ · vitest 618 ✓ · e2e fleet 15 +
keybindings 7 ✓

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(console): Fleet Room — a typed "@name" addresses, and clarify the broadcast count

Two things that bit during QA:

- Typing "@scout do the thing" and hitting ↵ BROADCAST the literal text, because the
  address chip was only ever set by picking from the @-picker. A typed leading "@name" is
  now resolved on send (exact match, else prefix, excluding this instance) and stripped
  from the message — the picker is a convenience, not a requirement. "@name" with no
  message toasts instead of silently broadcasting, and an explicit chip still wins.
- The broadcast chip read "All online · 2" beside a "3 online · 3" header, which reads
  like a bug. It deliberately excludes the instance you're already in, so it's relabelled
  "Everyone else · N" with a tooltip that says exactly that.

e2e: a TYPED @name (never touching the picker) opens that member's DM and does not broadcast.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyQuxgkFpMUJAQbVXnqz2d

* feat(console): host-scope the Fleet Room command (ADR 0042)

The fleet is managed from its HOST instance. On a member window `/api/fleet` is a
fleet-of-one by construction, so the room rendered an empty, misleading surface with no
DM or broadcast targets. The command is now disabled there with a "host instance only"
hint, reusing `fleetSettingsDisabledReason` so the room and Fleet settings gate on the
exact same host/member/standalone matrix — a standalone instance stays enabled (it's
where you create your first member), and a remote member reached directly stays enabled
because it may legitimately run its own fleet.

Disabled rather than hidden, so the command stays discoverable and explains itself.

Closes the last open Fleet Room item.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyQuxgkFpMUJAQbVXnqz2d

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

1 participant