Skip to content

feat(desktop): unify Bot Mode across registered gateways - #91134

Merged
teknium1 merged 10 commits into
NousResearch:mainfrom
dokterdok:feat/desktop-global-bot-roster-ux-scoped
Aug 23, 2026
Merged

feat(desktop): unify Bot Mode across registered gateways#91134
teknium1 merged 10 commits into
NousResearch:mainfrom
dokterdok:feat/desktop-global-bot-roster-ux-scoped

Conversation

@dokterdok

@dokterdok dokterdok commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Why

Bot Mode is a fleet view: one Desktop can reach bots on this device, remote gateways, SSH hosts, and Hermes Cloud. Sessions is intentionally different and remains focused on one gateway at a time.

Previously, the Bots roster followed the Sessions gateway. Bots appeared to move or disappear when Sessions switched, same-named bots were hard to distinguish, and Bot workspace tabs could resume without enough owner context.

This PR gives Bot Mode one owner-qualified roster and workspace across every registered gateway, while leaving Sessions gateway behavior unchanged.

User value

  • See and search every reachable bot and group chat in one place.
  • Keep a quiet flat roster for one gateway; add collapsible gateway sections only when they are useful.
  • Filter by item type, activity, or gateway without cluttering smaller setups.
  • Pin, hide, and recover bots without confusing same-named bots on different gateways.
  • Open each bot's continuous chat on its exact owner and restore those Bot tabs after relaunch.
  • Keep cross-gateway group drafts, active threads, and attachments when switching views or closing and reopening a room.
  • Keep work on other gateways running through connection changes.
  • Get a clear retry or update path when a gateway is unavailable or too old for Bot Mode.

What changes

Global roster and progressive navigation

  • Presents the upstream owner-qualified agent roster as one Bot Mode list.
  • Groups only multi-gateway setups; the single-gateway experience stays flat.
  • Adds progressive search plus type, activity, and gateway filters.
  • Scopes selection, pin/hide state, activity, unread state, and cached outage rows to the exact owner.
  • Uses the same gateway-kind icons as the Sessions switcher.

Bot-owned workspace

  • Keeps Bot Mode selection independent from the active Sessions gateway.
  • Adds an explicit Bot workspace scope for home, bot chats, group chats, and owner-aware side sessions.
  • Persists credential-free Bot tab ownership separately from Sessions and restores it after relaunch.
  • Revalidates owner state around bounded wake/retry and ignores late completions after the user moves elsewhere.
  • Keeps the Bot home loading/empty/error states stable instead of briefly exposing a generic Sessions composer.

Group continuity and outage UX

  • Keeps group composer state keyed by durable room ID: main/reply drafts, active thread, and attachments survive owner switches and actual tab close/remount.
  • Keeps an unavailable selected bot identifiable, disables dead actions, and offers retry when its gateway returns.
  • Uses the merged v3 group projection as its source of truth; no competing room-sync layer is added.

Architecture and scope

This branch is the post-#92731 UX-only re-scope requested in the maintainer review.

  • feat(desktop): remote bots open their own Bot Chat without re-homing Desktop (salvage #90006) #92731 supplies the immutable owner route, request dispatch, backend target mapping, owner-qualified metadata keys, roster merge, canonical-chat owner routing, and delete/session/transcript routing.
  • This PR consumes that plumbing; it does not modify profile-delete-routing.ts, session-request-router.ts, or transcript-tail.ts, and it adds no second owner-routing substrate.
  • The upstream mergeMultiSourceRoster implementation remains authoritative. The UX call site only excludes presentation-only outage ghosts before merging and annotates returned rows with source health.
  • The active Sessions gateway is not changed by Bot roster selection.
  • Backend jobs are not stopped when a user switches Bot owners or tabs.
  • Secrets are never persisted in workspace/tab state; only owner identifiers and public route metadata are stored.
  • The bundled hermes-bots plugin is updated in place; this does not revive or depend on the discontinued standalone Bot plugin.

Related work

How to test

  1. Register two gateways with at least one Bot profile each.
  2. Open Bots and confirm both gateway sections appear while Sessions remains on its current gateway.
  3. Open a bot on each gateway, switch between their Bot Chat tabs, and create an owner-scoped side session.
  4. Relaunch Desktop and confirm the last Bot workspace and exact-owner tabs restore.
  5. Create a cross-gateway group, type a draft and attach a file, switch views, close/reopen the group tab, and confirm composer state remains.
  6. Stop one disposable gateway, select its cached bot, then restore the gateway and retry. The offline owner should remain identifiable and work on the other gateway should continue.

Validation

Validated on head 580217c22a8989e285d878181bd84f05fffbaa2d, rebased onto upstream boundary 5c1a304ce890276a4334d8ced3f29ffeedbbbf93 (including #92784, #92815, and the >512 KiB bundled-plugin loader fix).

  • git diff --check and per-commit git show --check: passed across all 9 commits.
  • npm run typecheck: passed.
  • npm run lint: 0 errors; 117 existing warnings in the checked tree.
  • Bundled plugin suite: 465/465 passed.
  • Electron suite: 1,598 passed, 3 skipped across 113 files.
  • Full UI suite: 5,504/5,504 passed across 574 files.
  • npm run test:desktop:all: passed; production renderer/Electron build, signed macOS arm64 app, DMG, install stamp, renderer payload, and native node-pty payload validated. Notarization was not attempted because notarization credentials are not configured locally.
  • Real-gateway UAT on macOS: two live remote gateways loaded in one roster; a Bot Chat on each owner opened without changing the Sessions gateway; repeated owner switching produced no renderer errors or wrong-backend Session not found probes.
  • Isolated two-gateway QA with generic data reproduced the roster, filters, direct Bot Chat, and cross-gateway group flow used below.

Screenshots

Multi-gateway roster and exact-owner Bot Chat

Bot Mode showing two registered gateways with a selected bot and its continuous Bot Chat

Cross-gateway group chat

A cross-gateway Bot Mode group chat with one visible bot response

Progressive filters

Bot Mode filters for bots and groups, activity, and gateway

@alt-glitch alt-glitch added type/feature New feature or request P3 Low — cosmetic, nice to have comp/desktop Electron desktop app (apps/desktop/*) labels Aug 20, 2026
@teknium1

Copy link
Copy Markdown
Contributor

Heads-up: PR #91279 just merged (7af4b8e) and reworks the group-room sync layer in plugin.js substantially — it salvaged #89369's projection + gateway CAS and added durable roomId keys (v3 ui_meta envelope), final id-tombstones, and fan-out of the room mirror to every registered gateway with per-gateway CAS revision streams.

That overlaps part of this draft's territory: rooms (with history, members, name, picture) now already surface on every connected gateway, and cold clients hydrate them via profiles.list. The roster/UX unification this draft goes after is still open ground.

When you're ready to take it out of draft, please rebase onto current main and re-scope against what landed — happy to review the rebased version. If the sync plumbing here duplicates the merged fan-out, building the UX layer on top of the v3 projection (hermes-bots-groups, rooms keyed id:<roomId>) is the path that will merge cleanly.

@dokterdok
dokterdok force-pushed the feat/desktop-global-bot-roster-ux-scoped branch from 212f002 to e616159 Compare August 21, 2026 05:22
@dokterdok

Copy link
Copy Markdown
Contributor Author

Rebased onto current main after #91279 and re-scoped the overlap. This branch now consumes the v3 hermes-bots-groups projection and adds no room fan-out, CAS, migration, or sync plumbing; it stays in global roster, exact-owner selection/workspace, and outage UX. I also refreshed the tests, screenshots, and related-work notes in the description.

@dokterdok
dokterdok marked this pull request as ready for review August 21, 2026 05:23
@dokterdok
dokterdok marked this pull request as draft August 21, 2026 05:53
@dokterdok
dokterdok force-pushed the feat/desktop-global-bot-roster-ux-scoped branch from 68780d7 to 001ad33 Compare August 21, 2026 06:17
@dokterdok
dokterdok marked this pull request as ready for review August 21, 2026 06:22
@dokterdok
dokterdok marked this pull request as draft August 21, 2026 06:40
@dokterdok
dokterdok force-pushed the feat/desktop-global-bot-roster-ux-scoped branch 2 times, most recently from 00161a9 to 19b1b0d Compare August 21, 2026 10:35
@dokterdok
dokterdok marked this pull request as ready for review August 21, 2026 10:41
@dokterdok
dokterdok marked this pull request as draft August 21, 2026 11:45
@dokterdok
dokterdok force-pushed the feat/desktop-global-bot-roster-ux-scoped branch from 19b1b0d to d74569f Compare August 21, 2026 12:40
@dokterdok
dokterdok marked this pull request as ready for review August 21, 2026 12:42
@dokterdok
dokterdok force-pushed the feat/desktop-global-bot-roster-ux-scoped branch 2 times, most recently from 095dd36 to c08e2cb Compare August 22, 2026 09:47
@dokterdok

Copy link
Copy Markdown
Contributor Author

Rebased onto current main. The branch now consumes #92042/#92129's canonical (profile, session titled "Bot Chat") identity via canonical_session; #91791's keepAllProfilesScope: false remains, and the superseded newest-visible follow-up is dropped. The remaining diff is the owner-qualified global roster, coherent workspace/outage behavior, and source-qualified activity/unread state. Exact-head CI is green.

@dokterdok
dokterdok marked this pull request as draft August 22, 2026 11:13
@dokterdok
dokterdok force-pushed the feat/desktop-global-bot-roster-ux-scoped branch from c08e2cb to 8c2ecf0 Compare August 23, 2026 01:27
@dokterdok
dokterdok marked this pull request as ready for review August 23, 2026 02:10
@dokterdok
dokterdok force-pushed the feat/desktop-global-bot-roster-ux-scoped branch from 8c2ecf0 to d49cb8c Compare August 23, 2026 06:00
@teknium1

Copy link
Copy Markdown
Contributor

Post-#92731 status (verified against main @ e132e11): routing plumbing superseded — the UX layer survives and is wanted.

Superseded: the owner-route substrate — immutable (connectionId, profile) routes, requestForBot/botConnectionRoute/backendTargetProfile, connectionId::name botMetaKey, mergeMultiSourceRoster, canonical chats opening on the owner source, profile-delete-routing.ts, session-request-router/transcript-tail keying. All landed via #92731 (salvage of #90006).

Survives (zero hits on main): the owner-qualified global roster UI (rosterGatewaySections, section headers, filters, progressive search), the Bot Mode workspace decoupling (workspace-scope.ts, BotsHomeView, relaunch persistence), and the outage/update-path UX. Same drill as your #91279/#92042 re-scopes — rebase onto the merged plumbing and keep the UX layer; happy to review that slice.

@dokterdok
dokterdok marked this pull request as draft August 23, 2026 07:22
@dokterdok
dokterdok force-pushed the feat/desktop-global-bot-roster-ux-scoped branch from d49cb8c to 7f43fae Compare August 23, 2026 09:20
@dokterdok

dokterdok commented Aug 23, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto #92731 and #92784 and dropped the superseded owner-routing substrate. The branch now keeps the global roster + filters, Bot workspace/tab persistence, group draft continuity, and outage/update UX; the routing files listed above are no longer touched. A final two-gateway UAT also caught and fixed an owner leak in the tile-listing effect: messages were using the correct source, but the same session ID was still being probed on ambient profiles. 580217c22a8 fixes that. Final suites: 465 plugin tests, 1,598 Electron tests, and 5,504 UI tests all pass.

@dokterdok
dokterdok marked this pull request as ready for review August 23, 2026 09:21
@dokterdok
dokterdok force-pushed the feat/desktop-global-bot-roster-ux-scoped branch from 7f43fae to 6dab737 Compare August 23, 2026 09:26
@teknium1
teknium1 force-pushed the feat/desktop-global-bot-roster-ux-scoped branch from 580217c to 1fd2bef Compare August 23, 2026 11:04
@teknium1

Copy link
Copy Markdown
Contributor

Rebased this branch onto current main for you (maintainer edit) — main moved substantially today: the cross-connection relay (#92784), Cloud alias identity (#92860), tempfile cleanup (#92857), hidden-canonical-chat peer lookup (#92854), fail-closed /p/<profile> addressing (#92871), one-shot reply linger (#92861), and the per-profile remote override UI (#92864) all landed.

Your 9 commits are intact with authorship preserved; one labeled fixup commit of mine on top resolves the three conflict points, all in plugin.js against the new alias-identity work:

  1. openRosterBot() now coexists with the alias-aware displayName from fix(desktop): Bot Mode Cloud alias identity survives hosted handoff and Cloud-only rosters #92860 (the duplicate definition your branch carried from the pre-alias base was repaired to main's canonical version — alias claim > title > display_name > Hermes fallback).
  2. The roster fetch keeps fix(desktop): Bot Mode Cloud alias identity survives hosted handoff and Cloud-only rosters #92860's indexAliasRoutes() refresh but drops the pre-feat(desktop): remote bots open their own Bot Chat without re-homing Desktop (salvage #90006) #92731 activeBotRoute() capture — your own multi-source-roster source contract caught my first (wrong) resolution here, which is exactly what a good test is for.
  3. The displayName remoteSource guard resolved to main's alias/title-aware form.

Verification on the rebased head: 472/472 plugin tests, 5,538/5,538 UI, 1,598/1,598 electron (3 skipped), tsc --noEmit clean, attribution audit clean.

Worth knowing for your own re-check: your fleet view now sits on much stronger ground than when you started this branch — rosters are relay-synced across every connection, rows are all genuinely messageable via message_agent, and alias rows keep their configured names. Your outage UX + owner-scoped workspace is the missing top layer. Please give the rebased head a once-over (especially the three spots above); CI is running on the new head now.

@teknium1
teknium1 merged commit 2ec229e into NousResearch:main Aug 23, 2026
29 checks passed
@dokterdok
dokterdok deleted the feat/desktop-global-bot-roster-ux-scoped branch August 23, 2026 13:30
Zeus-Deus added a commit to Zeus-Deus/hermes-agent that referenced this pull request Aug 26, 2026
… on one strip

With several gateways registered, the Sessions profile rail only ever showed
the active gateway's profiles; reaching a bot on another machine meant a
gateway switch first, then a click on the rail that appeared afterwards. Bot
Mode (NousResearch#91134) and Capabilities already read the union agent roster; the rail
is now its third consumer.

- Every registered gateway's profiles sit on the one strip, in registry order
  (This device first, then by label), each group headed by that gateway's
  kind glyph. The active gateway's squares are unchanged; the others are
  "at rest" (dimmed) with tooltips/accessible names qualified by machine
  (`inbox · Homelab`), so same-named profiles never read alike.
- Clicking an at-rest square performs the same dial → commit → re-home as
  the statusbar switcher, landing on that exact (gateway, profile):
  `selectConnection(id, { profile })`. The spinner sits on the clicked
  square; the previous source stays painted until the target answers.
  Groups keep their slots whichever gateway is active, so a square never
  moves under the pointer that clicked it.
- Right-click on an at-rest square: Switch to / Color / Rename / Edit
  SOUL.md / Delete, executed on the owning gateway (renameProfile,
  getProfileSoul and updateProfileSoul accept the same scope deleteProfile
  already had); the delete confirmation names the machine. The legacy
  per-profile "Connect to a remote host…" item is hidden on multi-gateway
  setups, where the rail shows machines directly.
- Unreachable gateways keep their squares with an amber dot on the glyph;
  two registrations of one backend collapse to one group; past thirteen
  squares across the fleet the strip condenses into a menu sectioned by
  gateway. Roster is fetched on mount / focus / registry change only — no
  periodic fleet polling.
- Single-gateway Desktops render exactly as before: no roster fetch, same DOM.

Also fixes a boot race the e2e surfaced: initializeConnectionsRegistry()
"restored" the launch-mode source over a switch the user had already made
while boot was settling (same class as NousResearch#91047). The restore now yields when
a switch is pending or already landed.

Tests: pure grouping (fleet-rail.test.ts), rail component fleet mode
(profile-rail-fleet.test.tsx), store (explicit profile pick; restore yields),
and a Playwright e2e (fleet-profile-rail.spec.ts) that boots Desktop with two
REAL backends — the local one plus a second `hermes serve` registered as a
remote URL connection — and verifies layout, a real re-home, gateway-scoped
actions, and order stability.

Docs: multi-connection-desktop.md describes the fleet rail.

Refs NousResearch#89304, NousResearch#92384, NousResearch#91047, NousResearch#94724

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
teknium1 pushed a commit that referenced this pull request Aug 26, 2026
… on one strip

With several gateways registered, the Sessions profile rail only ever showed
the active gateway's profiles; reaching a bot on another machine meant a
gateway switch first, then a click on the rail that appeared afterwards. Bot
Mode (#91134) and Capabilities already read the union agent roster; the rail
is now its third consumer.

- Every registered gateway's profiles sit on the one strip, in registry order
  (This device first, then by label), each group headed by that gateway's
  kind glyph. The active gateway's squares are unchanged; the others are
  "at rest" (dimmed) with tooltips/accessible names qualified by machine
  (`inbox · Homelab`), so same-named profiles never read alike.
- Clicking an at-rest square performs the same dial → commit → re-home as
  the statusbar switcher, landing on that exact (gateway, profile):
  `selectConnection(id, { profile })`. The spinner sits on the clicked
  square; the previous source stays painted until the target answers.
  Groups keep their slots whichever gateway is active, so a square never
  moves under the pointer that clicked it.
- Right-click on an at-rest square: Switch to / Color / Rename / Edit
  SOUL.md / Delete, executed on the owning gateway (renameProfile,
  getProfileSoul and updateProfileSoul accept the same scope deleteProfile
  already had); the delete confirmation names the machine. The legacy
  per-profile "Connect to a remote host…" item is hidden on multi-gateway
  setups, where the rail shows machines directly.
- Unreachable gateways keep their squares with an amber dot on the glyph;
  two registrations of one backend collapse to one group; past thirteen
  squares across the fleet the strip condenses into a menu sectioned by
  gateway. Roster is fetched on mount / focus / registry change only — no
  periodic fleet polling.
- Single-gateway Desktops render exactly as before: no roster fetch, same DOM.

Also fixes a boot race the e2e surfaced: initializeConnectionsRegistry()
"restored" the launch-mode source over a switch the user had already made
while boot was settling (same class as #91047). The restore now yields when
a switch is pending or already landed.

Tests: pure grouping (fleet-rail.test.ts), rail component fleet mode
(profile-rail-fleet.test.tsx), store (explicit profile pick; restore yields),
and a Playwright e2e (fleet-profile-rail.spec.ts) that boots Desktop with two
REAL backends — the local one plus a second `hermes serve` registered as a
remote URL connection — and verifies layout, a real re-home, gateway-scoped
actions, and order stability.

Docs: multi-connection-desktop.md describes the fleet rail.

Refs #89304, #92384, #91047, #94724

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
and7777 pushed a commit to and7777/hermes-agent that referenced this pull request Aug 27, 2026
… on one strip

With several gateways registered, the Sessions profile rail only ever showed
the active gateway's profiles; reaching a bot on another machine meant a
gateway switch first, then a click on the rail that appeared afterwards. Bot
Mode (NousResearch#91134) and Capabilities already read the union agent roster; the rail
is now its third consumer.

- Every registered gateway's profiles sit on the one strip, in registry order
  (This device first, then by label), each group headed by that gateway's
  kind glyph. The active gateway's squares are unchanged; the others are
  "at rest" (dimmed) with tooltips/accessible names qualified by machine
  (`inbox · Homelab`), so same-named profiles never read alike.
- Clicking an at-rest square performs the same dial → commit → re-home as
  the statusbar switcher, landing on that exact (gateway, profile):
  `selectConnection(id, { profile })`. The spinner sits on the clicked
  square; the previous source stays painted until the target answers.
  Groups keep their slots whichever gateway is active, so a square never
  moves under the pointer that clicked it.
- Right-click on an at-rest square: Switch to / Color / Rename / Edit
  SOUL.md / Delete, executed on the owning gateway (renameProfile,
  getProfileSoul and updateProfileSoul accept the same scope deleteProfile
  already had); the delete confirmation names the machine. The legacy
  per-profile "Connect to a remote host…" item is hidden on multi-gateway
  setups, where the rail shows machines directly.
- Unreachable gateways keep their squares with an amber dot on the glyph;
  two registrations of one backend collapse to one group; past thirteen
  squares across the fleet the strip condenses into a menu sectioned by
  gateway. Roster is fetched on mount / focus / registry change only — no
  periodic fleet polling.
- Single-gateway Desktops render exactly as before: no roster fetch, same DOM.

Also fixes a boot race the e2e surfaced: initializeConnectionsRegistry()
"restored" the launch-mode source over a switch the user had already made
while boot was settling (same class as NousResearch#91047). The restore now yields when
a switch is pending or already landed.

Tests: pure grouping (fleet-rail.test.ts), rail component fleet mode
(profile-rail-fleet.test.tsx), store (explicit profile pick; restore yields),
and a Playwright e2e (fleet-profile-rail.spec.ts) that boots Desktop with two
REAL backends — the local one plus a second `hermes serve` registered as a
remote URL connection — and verifies layout, a real re-home, gateway-scoped
actions, and order stability.

Docs: multi-connection-desktop.md describes the fleet rail.

Refs NousResearch#89304, NousResearch#92384, NousResearch#91047, NousResearch#94724

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
melon-xf added a commit to melon-xf/hermes-agent that referenced this pull request Sep 3, 2026
… on one strip

With several gateways registered, the Sessions profile rail only ever showed
the active gateway's profiles; reaching a bot on another machine meant a
gateway switch first, then a click on the rail that appeared afterwards. Bot
Mode (NousResearch#91134) and Capabilities already read the union agent roster; the rail
is now its third consumer.

- Every registered gateway's profiles sit on the one strip, in registry order
  (This device first, then by label), each group headed by that gateway's
  kind glyph. The active gateway's squares are unchanged; the others are
  "at rest" (dimmed) with tooltips/accessible names qualified by machine
  (`inbox · Homelab`), so same-named profiles never read alike.
- Clicking an at-rest square performs the same dial → commit → re-home as
  the statusbar switcher, landing on that exact (gateway, profile):
  `selectConnection(id, { profile })`. The spinner sits on the clicked
  square; the previous source stays painted until the target answers.
  Groups keep their slots whichever gateway is active, so a square never
  moves under the pointer that clicked it.
- Right-click on an at-rest square: Switch to / Color / Rename / Edit
  SOUL.md / Delete, executed on the owning gateway (renameProfile,
  getProfileSoul and updateProfileSoul accept the same scope deleteProfile
  already had); the delete confirmation names the machine. The legacy
  per-profile "Connect to a remote host…" item is hidden on multi-gateway
  setups, where the rail shows machines directly.
- Unreachable gateways keep their squares with an amber dot on the glyph;
  two registrations of one backend collapse to one group; past thirteen
  squares across the fleet the strip condenses into a menu sectioned by
  gateway. Roster is fetched on mount / focus / registry change only — no
  periodic fleet polling.
- Single-gateway Desktops render exactly as before: no roster fetch, same DOM.

Also fixes a boot race the e2e surfaced: initializeConnectionsRegistry()
"restored" the launch-mode source over a switch the user had already made
while boot was settling (same class as NousResearch#91047). The restore now yields when
a switch is pending or already landed.

Tests: pure grouping (fleet-rail.test.ts), rail component fleet mode
(profile-rail-fleet.test.tsx), store (explicit profile pick; restore yields),
and a Playwright e2e (fleet-profile-rail.spec.ts) that boots Desktop with two
REAL backends — the local one plus a second `hermes serve` registered as a
remote URL connection — and verifies layout, a real re-home, gateway-scoped
actions, and order stability.

Docs: multi-connection-desktop.md describes the fleet rail.

Refs NousResearch#89304, NousResearch#92384, NousResearch#91047, NousResearch#94724

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
zapabob pushed a commit to zapabob/hermes-agent-windows that referenced this pull request Sep 5, 2026
… on one strip

With several gateways registered, the Sessions profile rail only ever showed
the active gateway's profiles; reaching a bot on another machine meant a
gateway switch first, then a click on the rail that appeared afterwards. Bot
Mode (NousResearch#91134) and Capabilities already read the union agent roster; the rail
is now its third consumer.

- Every registered gateway's profiles sit on the one strip, in registry order
  (This device first, then by label), each group headed by that gateway's
  kind glyph. The active gateway's squares are unchanged; the others are
  "at rest" (dimmed) with tooltips/accessible names qualified by machine
  (`inbox · Homelab`), so same-named profiles never read alike.
- Clicking an at-rest square performs the same dial → commit → re-home as
  the statusbar switcher, landing on that exact (gateway, profile):
  `selectConnection(id, { profile })`. The spinner sits on the clicked
  square; the previous source stays painted until the target answers.
  Groups keep their slots whichever gateway is active, so a square never
  moves under the pointer that clicked it.
- Right-click on an at-rest square: Switch to / Color / Rename / Edit
  SOUL.md / Delete, executed on the owning gateway (renameProfile,
  getProfileSoul and updateProfileSoul accept the same scope deleteProfile
  already had); the delete confirmation names the machine. The legacy
  per-profile "Connect to a remote host…" item is hidden on multi-gateway
  setups, where the rail shows machines directly.
- Unreachable gateways keep their squares with an amber dot on the glyph;
  two registrations of one backend collapse to one group; past thirteen
  squares across the fleet the strip condenses into a menu sectioned by
  gateway. Roster is fetched on mount / focus / registry change only — no
  periodic fleet polling.
- Single-gateway Desktops render exactly as before: no roster fetch, same DOM.

Also fixes a boot race the e2e surfaced: initializeConnectionsRegistry()
"restored" the launch-mode source over a switch the user had already made
while boot was settling (same class as NousResearch#91047). The restore now yields when
a switch is pending or already landed.

Tests: pure grouping (fleet-rail.test.ts), rail component fleet mode
(profile-rail-fleet.test.tsx), store (explicit profile pick; restore yields),
and a Playwright e2e (fleet-profile-rail.spec.ts) that boots Desktop with two
REAL backends — the local one plus a second `hermes serve` registered as a
remote URL connection — and verifies layout, a real re-home, gateway-scoped
actions, and order stability.

Docs: multi-connection-desktop.md describes the fleet rail.

Refs NousResearch#89304, NousResearch#92384, NousResearch#91047, NousResearch#94724

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/desktop Electron desktop app (apps/desktop/*) P3 Low — cosmetic, nice to have type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants