Conversation
Per-session source picker for the sidebar 'new session' action. With more than one connection registered (Local / Remote gateway / SSH / Hermes Cloud), clicking new session opens a dropdown to pick where that session runs, instead of always creating it on the active agent. Single-source installs keep the one-click behavior unchanged. Reuses selectConnection() + startFreshSessionDraft(); no storage/core changes. Session->source affinity persistence is a documented follow-up. Co-authored-by: valvesss <valvesss@users.noreply.github.com>
…eader '+' button; add e2e The header '+' new-session button (shown when showAllProfiles is false) called onNewSessionInWorkspace(null) directly, bypassing the source picker — so with multiple registered connections clicking it created a session on the active source instead of showing the picker. Wire it through the same NewSessionSourcePicker, gated on hasMultipleConnections. Add an e2e spec that seeds a two-source v2 registry before launch and asserts the sidebar 'New session' row opens the picker showing both sources.
Nice UX scoping — gating the picker behind multiple registered sources keeps single-source installs byte-for-byte identical, and the contrib plumbing (
Minor: the four-branch ternary for |
… re-home selectConnection was doing a hard re-home (session-list wipe, profile jump, double session create, refreshActiveProfile) — the 'flicker + profile switch' the user hit. startSessionOnSource now dials the chosen source via ensureGatewayAgent and opens a fresh draft, keeping the current profile and its list in place.
Active connection highlighted with --ui-control-active-background, icon + check in --ui-accent, native label styling. onSelect untouched.
Surfacing the owning CONNECTION on each session row (icon + label) when the active source is a remote/SSH/cloud gateway, so a foreign connection's session list is clearly labelled as such instead of silently appearing inside the local profile's view. Local 'This device' stays unlabelled (the normal case). Mirrors the cross-profile ProfileTag pattern (NousResearch#66003) on the connection axis, threaded through the section + virtual list to the row.
Each registered connection other than the active one is fetched via a connectionId-scoped REST call (listGatewayRecentSessions) into a keyed store (), rendered as an 'other gateways' sidebar section badged with its origin, and resumed routed to its owning gateway (requestSessionResume with ownerRoute). Same local profile, gateway per session. Additive: never touches $sessions/the active list.
Address the automated review on the PR: - wiring: wrap startSessionOnSource in .catch() so a failed gateway switch surfaces an in-app error notification instead of an unhandled promise rejection (point 1). - startSessionOnSource: add a store test asserting a dial that fails to bring the target active rejects AND does NOT open a draft on the original source (point 2 - behavior already guarded, now locked). - new-session-source-picker: render an explicit empty state when no gateways are registered, instead of a bare label + separator (point 3), with a test. - new-session-source-picker: KIND_LABEL record replaces the 4-branch kindLabel ternary (minor), keeping icon/label mappings in lockstep. - i18n: add settings.connections.emptySources + startFailed keys.
|
Thanks for the review — all points addressed in
Tests: |
|
Flagging a bug that lives right next to this PR's "Layer 2 — same local profile, gateway per session": #94811. Short version: when two registered connections expose the same profile name ( The reason it's relevant here: the root is that the primary connection has no connection id in the renderer's gateway store.
That last one's own doc comment already names the exact hazard, for events:
The routing and activation paths still do that bare comparison. So "gateway per session" can't fully land while the primary is unaddressable: a per-session route pointing at the local connection still collapses the moment its profile name matches the primary's. If you're already introducing per-session connection identity here, giving the primary a real connection id (and making the three comparisons above connection-aware rather than name-aware) would close this at the same time. Happy to send that as a separate PR if you'd rather keep this one scoped — just didn't want to land a competing mechanism underneath an active rewrite. Full reproduction, trace output, and analysis are in #94811. |
|
Thanks for the detailed trace and analysis — agreed, this is squarely in the Layer-2 ("gateway per session") territory this PR opens. The root cause is exactly as you describe: the primary carries no connection id, so the bare I reviewed #94864 and it looks like the right place for the fix: preserving a connection-qualified owner for ordinary (sessions-mode) tiles via Also acknowledging cmoiccool's independent reproduction and the acceptance matrix — the inverse-topology row (remote primary + local secondary, both Happy to coordinate the landing order (#94864 → #94914 → #94457 rebased). Let me know if you'd prefer any of the ownership plumbing folded into #94457's per-session rework instead. |
|
Small correction to my note above, to keep the coordination honest: it's actually three files both branches touch, not one — I did a
So the rebase stays small, and I'll take it once you're in (or fold |
…on tooltip wiring) The picker's trigger sits inside a Tip in the real sidebar; a shared asChild button must not swallow the pointerdown that opens the menu. Adds that regression case and gates the empty-state assertion with waitFor to avoid flakiness under Radix pointer-capture.
startSessionOnSource still called ensureGatewayAgent, which re-homed the ConnectionSwitcher and profile rail. Pin the draft to the chosen source and route create/submit/resume on that socket instead. Co-authored-by: Cursor <cursoragent@cursor.com>
ee6ec3a to
62606d5
Compare
Isolated serve inherited dashboard.public_url from ~/.hermes/.env (dotenv override), engaged the OAuth gate, and rejected the desktop session token on /api/ws. HERMES_DESKTOP=1 now ignores that public URL so loopback children stay in token mode; lockfile protocol 2 forces a respawn of already-gated processes. Sessions-mode tiles keep ownerRoute so RPCs hit the chosen gateway, and the sidebar origin tag names it. Co-authored-by: Cursor <cursoragent@cursor.com>
|
Closing in favor of a product pivot. Why: Gateway belongs on the project (host + folder at create time; sessions inherit), not on a per-click New session source picker. That UX was the wrong home for multi-gateway. Continues in: #95179 — keeps the useful multi-gateway infra from this PR (pin without re-home, origin tags, foreign sessions / |
|
Superseded by #95179 (project as gateway home). |
What
Per-session gateway source in the Hermes desktop, delivered in two layers:
Layer 1 — choose where a new session runs (shipped previously)
This device+ a remote/SSH gateway such asmimir), both nav New session and the header+open a dropdown to pick the connection the session runs on. Single-gateway installs see zero change.selectConnection(which wiped the list, jumped profiles, and created the session twice). It now dials only the source viaensureGatewayAgentand opens there — no flicker, no profile jump.Layer 2 — same local profile, gateway per session (new in this PR)
listGatewayRecentSessions) into a keyed store ($foreignGatewaySessions), rendered as an "other gateways" sidebar section under the active profile, each row badged with its origin.requestSessionResumes it with anownerRoute {connectionId, profile}so the session-scoped RPCs dispatch on that gateway's own socket viarequestForSessionProfile → requestGatewayForAgent— resuming on the owning gateway while the local profile scope stays put.Additive by design:
$sessions(the active connection's own list) is never clobbered; aggregation is opt-in per registered connection and pruned when a gateway is deregistered.Validation
listGatewayRecentSessionspinsconnectionId+profileon amin_messages=1recents path;$foreignGatewaySessionskeyed per connectionId without touching$sessions;refreshForeignGatewaySessionsfetches each foreign gateway and stores per-connection (and stays stale when a fetch errors).Commits
+button (f130db8843)cc5f127cf8)c010e461d4)e6f8321bcf)13bddd672c)