feat(desktop): unify Bot Mode across registered gateways - #91134
Conversation
|
Heads-up: PR #91279 just merged (7af4b8e) and reworks the group-room sync layer in 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 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 ( |
212f002 to
e616159
Compare
|
Rebased onto current main after #91279 and re-scoped the overlap. This branch now consumes the v3 |
68780d7 to
001ad33
Compare
00161a9 to
19b1b0d
Compare
19b1b0d to
d74569f
Compare
095dd36 to
c08e2cb
Compare
|
Rebased onto current |
c08e2cb to
8c2ecf0
Compare
8c2ecf0 to
d49cb8c
Compare
|
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. |
d49cb8c to
7f43fae
Compare
|
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. |
7f43fae to
6dab737
Compare
580217c to
1fd2bef
Compare
|
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 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:
Verification on the rebased head: 472/472 plugin tests, 5,538/5,538 UI, 1,598/1,598 electron (3 skipped), 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 |
… 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>
… 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>
… 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>
… 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>
… 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>
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
What changes
Global roster and progressive navigation
Bot-owned workspace
Group continuity and outage UX
Architecture and scope
This branch is the post-#92731 UX-only re-scope requested in the maintainer review.
profile-delete-routing.ts,session-request-router.ts, ortranscript-tail.ts, and it adds no second owner-routing substrate.mergeMultiSourceRosterimplementation remains authoritative. The UX call site only excludes presentation-only outage ghosts before merging and annotates returned rows with source health.hermes-botsplugin is updated in place; this does not revive or depend on the discontinued standalone Bot plugin.Related work
message_agentrelay; this PR organizes and owns the corresponding Desktop surfaces without replacing its peer transport.(profile, session titled "Bot Chat")behavior formalized by fix(bot-mode): a bot row opens the bot's canonical Bot Chat #92042 and fix(bot-mode): the canonical Bot Chat is found by name — session-id pins removed #92129.keepAllProfilesScope: falseSessions behavior while Bot tabs use their own workspace scope.How to test
Validation
Validated on head
580217c22a8989e285d878181bd84f05fffbaa2d, rebased onto upstream boundary5c1a304ce890276a4334d8ced3f29ffeedbbbf93(including #92784, #92815, and the >512 KiB bundled-plugin loader fix).git diff --checkand per-commitgit show --check: passed across all 9 commits.npm run typecheck: passed.npm run lint: 0 errors; 117 existing warnings in the checked tree.npm run test:desktop:all: passed; production renderer/Electron build, signed macOS arm64 app, DMG, install stamp, renderer payload, and nativenode-ptypayload validated. Notarization was not attempted because notarization credentials are not configured locally.Session not foundprobes.Screenshots
Multi-gateway roster and exact-owner Bot Chat
Cross-gateway group chat
Progressive filters