mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-11 21:02:12 +08:00
fix(desktop): reliably persist cloud org, unselect cloud on mode switch, keep Change-org button after restore
Three fixes from live testing the org persist/restore flow: 1. Org not persisting (stale closure). discoverCloud() resolves the org asynchronously from the NAS response and setCloudOrg() is a React state update, but connectCloudAgent read the cloudOrg value captured in its render closure — often still null when the user clicked Connect in the same tick, so no org was saved. Mirror the org into a ref (cloudOrgRef) updated synchronously alongside state; connect reads cloudOrgRef.current. 2. Cloud connection lingered after switching away. coerceDesktopConnectionConfig inherits existingBlock.url across mode switches (correct for remote↔local), so switching cloud→local/remote kept the cloud instance URL in the remote block — re-selecting Cloud then looked 'already connected' with no way to re-pick. Added a leavingCloud rule: when the saved block was cloud and the new mode isn't cloud, start from an empty block (drop the cloud url/org/token), cleanly unselecting the cloud gateway. remote↔local toggles still preserve a real remote URL. 3. Change-org button vanished after restore-open. It was gated on cloudOrgs.length > 1, but the restore path discovers straight into the saved org and never populates cloudOrgs. Gate on cloudOrg being set instead, via a new changeCloudOrg() that clears the org + agent list and re-discovers with no org arg (multi-org → NAS 409 picker; single-org → auto-resolve back). Depends on NAS #550 (echo resolved org), merged + live on prod (0dc86d0b). tsc + eslint clean; 57 node --test + 16 vitest pass; all three verified live on Ben's host (org persists + restores, cloud unselects on switch, Change-org shows after reopen). The benign 'Session not found' 404 on backend switch is left as-is (already handled by isSessionGoneError → fresh draft; dev-log noise only). cloud-auto-discovery Phase 3/4 follow-up.
This commit is contained in:
@@ -5124,7 +5124,16 @@ function coerceDesktopConnectionConfig(input = {}, existing = readDesktopConnect
|
||||
const remoteLike = modeIsRemoteLike(mode)
|
||||
|
||||
// The block being edited: a per-profile entry or the global remote block.
|
||||
const existingBlock = key ? existing.profiles?.[key] || {} : existing.remote || {}
|
||||
const rawExistingBlock = key ? existing.profiles?.[key] || {} : existing.remote || {}
|
||||
// Leaving a CLOUD connection unselects it: a cloud block's url/org/token
|
||||
// describe a discovered Hermes Cloud instance, NOT a user-owned remote gateway,
|
||||
// so switching to local or remote must NOT inherit them (otherwise the stale
|
||||
// cloud URL lingers and re-selecting Cloud looks "already connected"). When the
|
||||
// saved block was cloud and the new mode is not cloud, start from an empty
|
||||
// block. (remote↔local toggles still preserve a real remote URL as before.)
|
||||
const existingMode = key ? existing.profiles?.[key]?.mode : existing.mode
|
||||
const leavingCloud = existingMode === 'cloud' && mode !== 'cloud'
|
||||
const existingBlock = leavingCloud ? {} : rawExistingBlock
|
||||
const remoteUrl = String(input.remoteUrl ?? existingBlock.url ?? '').trim()
|
||||
// authMode: explicit input wins; otherwise inherit the saved value, default 'token'.
|
||||
const authMode = resolveAuthMode(input.remoteAuthMode, existingBlock.authMode)
|
||||
|
||||
@@ -128,7 +128,19 @@ export function GatewaySettings() {
|
||||
// list here and show a picker. `cloudOrg` is the chosen org slug/id (null =
|
||||
// not yet chosen / single-org user).
|
||||
const [cloudOrgs, setCloudOrgs] = useState<DesktopCloudOrg[]>([])
|
||||
const [cloudOrg, setCloudOrg] = useState<null | string>(null)
|
||||
const [cloudOrg, setCloudOrgState] = useState<null | string>(null)
|
||||
// Mirror the selected org into a ref so connect reads the CURRENT value, not a
|
||||
// value captured in a stale render closure. discoverCloud() resolves the org
|
||||
// asynchronously (from the NAS response) and a user can click Connect in the
|
||||
// same render tick; without the ref, connectCloudAgent could persist a null
|
||||
// org even though discovery just resolved one. Always set both together.
|
||||
const cloudOrgRef = useRef<null | string>(null)
|
||||
|
||||
const setCloudOrg = (value: null | string) => {
|
||||
cloudOrgRef.current = value
|
||||
setCloudOrgState(value)
|
||||
}
|
||||
|
||||
// Hermes Cloud is beta-gated: the selector ModeCard only appears when the main
|
||||
// process reports the BETA env flag is enabled. Default hidden until the async
|
||||
// check resolves, so it never flashes in for non-beta users.
|
||||
@@ -512,6 +524,16 @@ export function GatewaySettings() {
|
||||
void discoverCloud(ref)
|
||||
}
|
||||
|
||||
// "Change org": clear the selected org and re-discover with no org arg. A
|
||||
// multi-org user gets NAS's 409 → the picker; a single-org user auto-resolves
|
||||
// back to their one org. Also clear the agent list so the current org's
|
||||
// agents don't linger under the picker while discovery re-runs.
|
||||
const changeCloudOrg = () => {
|
||||
setCloudOrg(null)
|
||||
setCloudAgents([])
|
||||
void discoverCloud()
|
||||
}
|
||||
|
||||
// On entering cloud mode (or scope change), read the portal session status and
|
||||
// auto-discover when already signed in, so the picker is populated on open.
|
||||
useEffect(() => {
|
||||
@@ -642,12 +664,14 @@ export function GatewaySettings() {
|
||||
|
||||
// Persist a cloud-mode connection (remote-shaped, oauth) and reconnect.
|
||||
// Include the selected org so Settings reopens into the same org + instance.
|
||||
// Read the REF (not the cloudOrg state) so a just-resolved org from
|
||||
// discovery in this same render tick is captured, not a stale null.
|
||||
const next = await desktop.applyConnectionConfig({
|
||||
mode: 'cloud',
|
||||
profile: scope ?? undefined,
|
||||
remoteAuthMode: 'oauth',
|
||||
remoteUrl: agent.dashboardUrl,
|
||||
cloudOrg: cloudOrg ?? undefined
|
||||
cloudOrg: cloudOrgRef.current ?? undefined
|
||||
})
|
||||
|
||||
setState(next)
|
||||
@@ -837,9 +861,15 @@ export function GatewaySettings() {
|
||||
{g.cloudAgentsTitle}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{cloudOrgs.length > 1 ? (
|
||||
// Let a multi-org user switch back to the org picker.
|
||||
<Button onClick={() => setCloudOrg(null)} size="sm" variant="text">
|
||||
{cloudOrg ? (
|
||||
// Let the user switch orgs. Gating on cloudOrgs.length would
|
||||
// hide this after a restore-open (which discovers straight
|
||||
// into the saved org and never populates the org list). So
|
||||
// show it whenever an org is selected: clicking clears the
|
||||
// org and re-runs discovery with no org arg — a multi-org
|
||||
// user gets the picker (NAS 409), a single-org user simply
|
||||
// auto-resolves back to their one org (harmless).
|
||||
<Button onClick={() => changeCloudOrg()} size="sm" variant="text">
|
||||
{g.cloudOrgChange}
|
||||
</Button>
|
||||
) : null}
|
||||
|
||||
Reference in New Issue
Block a user