Skip to content

feat(desktop): client + backend version buttons and remote backend update flow - #40781

Closed
yoniebans wants to merge 16 commits into
mainfrom
feat/desktop-remote-update-skew
Closed

feat(desktop): client + backend version buttons and remote backend update flow#40781
yoniebans wants to merge 16 commits into
mainfrom
feat/desktop-remote-update-skew

Conversation

@yoniebans

@yoniebans yoniebans commented Jun 6, 2026

Copy link
Copy Markdown
Collaborator

Summary

When the gateway is switched to a remote backend (the GUI on one machine, the backend on another), the status bar collapsed a two-artifact reality into one version badge — and "update" was ambiguous. The badge read the local client clone's version, so updating the remote backend never moved it, and the overlay had no way to show what the backend was behind by. #40017 added the skew warning; this adds the update surface it points at.

The desktop and the remote backend are independently updatable. They deserve independent version buttons.

Changes

Backend/api/hermes/update/check returns a new commits field for git/pip installs that are behind: a list of {sha, summary, author, at} measured HEAD..origin/main (same ref the behind-count uses). Additive; existing consumers ignore it.

Desktop — when the gateway is pointed at a remote backend, the status bar shows two buttons:

  • Client (always) — the local Electron clone's version.
  • Backend (remote backend only) — the connected backend's version + (+N) behind-count.

Each opens the update overlay scoped to its target, rendering that target's commit changelog (grouped: Fixed / Improved / Other). A local gateway is unchanged — a single version pill. The store splits into independent client/backend update atoms so the two surfaces never share state; the backend button is gated on connection.mode === 'remote'.

Backend Install flow (apply path). Clicking Install on the backend overlay calls the pre-existing POST /api/hermes/update (unchanged), which runs hermes update on the backend host. The remote-update UX was built out around it:

  • The apply poll detects the connection drop when the backend restarts, shows a remote-aware "restarting…" state, then polls the backend until it answers again and closes the overlay cleanly (no bounce through stale views, no false "you're all set"). The backend pill refreshes to its new behind-count.
  • If the backend never returns (failed restart), it surfaces an honest error — the client can't know the update's exit code once the connection drops, so it states the update may not have completed rather than claiming success. Retry re-attempts; closing clears apply state and leaves the pill at its true (unchanged) behind-count.
  • Copy is target-aware: "the backend is running the latest version", not a client/backend conflation.

Env-override fix (a5e5f28b4) is intentionally included: the remote feature is unreachable via HERMES_DESKTOP_REMOTE_URL without it (the Gateway panel mislabelled the connection and locked its controls). Kept in this PR because the feature is unusable in env-override remote mode otherwise.

Deployment note

When the gateway points at a remote backend, the backend Install restarts the dashboard, so the dashboard must be able to come back on its own. Run it under a supervisor with restart-on-stop — systemd Restart=always — which is what the new "wait for backend to return" flow expects. An unsupervised bare process will not restart after the update: the connection stays down until the dashboard is manually restarted. Note the bundled templates/hermes-dashboard.service uses Restart=on-failure, which is NOT sufficient (a SIGTERM stop is "clean", so on-failure skips the relaunch) — use Restart=always for a remote-backend deployment.

Notes

Test plan

  • tsc -b clean.
  • Backend endpoint suite: TestUpdateCheckEndpoint (6).
  • Desktop store/copy suites: updates.test.ts + update-copy.test.ts (13).
  • Endpoint returns the real commit list over HTTP; overlay renders grouped changelog against a remote backend.
  • Local↔remote gateway switch: status bar returns to single pill on a local gateway.
  • Apply/install from the backend overlay — verified live against a real remote git install on a throwaway VM: happy path (pull + restart + clean overlay close + pill → (+0)) and failure path (backend doesn't return → honest error, retry works, pill stays accurate).

Known follow-ups (separate issues, not blocking)

  • Install against a backend on a non-main branch silently switches it to main (hermes update default-branch behavior; pre-existing). Worth a guard.
  • The toast notifications surface backend-aligned state for a remote gateway — needs a design pass on what each notification targets.

@github-actions

github-actions Bot commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

🔎 Lint report: feat/desktop-remote-update-skew vs origin/main

ruff

Total: 0 on HEAD, 0 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 0 pre-existing issues carried over.

ty (type checker)

Total: 10393 on HEAD, 10393 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 5426 pre-existing issues carried over.

Diagnostics are surfaced as warnings — this check never fails the build.

@alt-glitch alt-glitch added type/feature New feature or request P3 Low — cosmetic, nice to have comp/cli CLI entry point, hermes_cli/, setup wizard labels Jun 6, 2026
yoniebans added 9 commits June 7, 2026 10:17
Add a best-effort `commits` list (sha/summary/author/at) to the update-check
response for git/pip installs that are behind upstream, so the desktop's
remote update overlay can show what's changed before applying.

Additive and non-breaking: existing consumers (legacy dashboard, tests using
subset assertions) ignore the new field. Leaves the shared check_for_updates()
int contract untouched — commits come from a separate best-effort git call.
…mote

In remote thin-client mode the Electron client and the backend it connects to
are separate installs that drift independently. The status bar previously showed
only the client version, hiding skew (e.g. client 0.15.1 talking to backend
0.16.0 looked fine).

Add a pure resolveVersionBar() helper (unit-tested) that, gated on
connection.mode === 'remote', renders both 'client vX · backend vY' from the
desktop appVersion and StatusResponse.version, and flags skew. Local mode is
byte-identical to before. Wire it into the status-bar version item.
In remote mode, checkUpdates()/applyUpdates() branch on connection.mode and
drive the existing updates overlay from the connected backend instead of the
local Electron git bridge:

- checkUpdates -> GET /api/hermes/update/check, mapped onto DesktopUpdateStatus
  (behind, commits, supported=can_apply, message). The overlay renders the
  commit list as 'what's changed' and shows guidance (not Install) when the
  backend install can't self-apply (docker/nix).
- applyUpdates -> POST /api/hermes/update (the proven command-center path),
  polling the action to completion and handling the expected mid-update
  connection drop as the restart phase.

Local mode is unchanged. Adds checkHermesUpdate() to hermes.ts and a
BackendUpdateCheckResponse type.
…eam}

The behind-count (banner._check_via_local_git) measures HEAD..origin/main, but
_recent_upstream_commits logged HEAD..@{upstream}. On a feature-branch checkout
@{upstream} is the branch's own tip (0 commits), so the changelog came back
empty while behind>0 — the overlay then showed generic filler instead of what
changed. Pin the commit range to origin/main so count and changelog agree.

Verified against a checkout 11 behind origin/main: now returns 11 commits.
…log copy

The updates overlay showed generic 'New update available / improvements and
fixes' with no indication of whether it was updating the client or the backend.
In remote mode it now reads 'Backend update available' and names the connected
backend, and when there's no commit changelog (e.g. pip/non-git backend) it
degrades to honest 'release notes aren't available for this install type' copy
instead of filler.

Copy selection extracted to a pure resolveUpdateCopy() helper (unit-tested);
threads target ('client'|'backend') from connection.mode through the overlay.
The status bar merged both versions into one pill with a single click target,
so there was no way to tell which artifact an update acted on — and the apply
path was overloaded by connection mode. Separate them:

- store: independent client (checkUpdates/applyUpdates) and backend
  (checkBackendUpdates/applyBackendUpdate) flows with their own status/apply
  atoms; openUpdateOverlayFor(target) drives the overlay.
- status bar: two buttons — client vX (always) and backend vY (+N) (remote
  only), each with its own behind-count, opening the overlay for its target.
- overlay: reads the active target's atoms; install/check route per target.

Removes the version-bar merge helper (no longer merging the two versions).
Two follow-ups from testing the two-button bar:

- The background poller and focus handler only checked the client, so the
  backend behind-count and changelog stayed empty until the user opened the
  overlay — and the overlay's first render then hit the empty-commits fallback
  ('Improvements and fixes') instead of the real changelog. Check the backend
  alongside the client on poller start, interval, and focus so its state is
  ready before the button is clicked.
- Order the status bar client-first, backend-second.
The poller starts at mount, before the gateway connects, so its initial
checkBackendUpdates() ran while mode was still unset and no-op'd via the
remote-mode guard — leaving the backend button empty until the user clicked it.
Subscribe to $connection and re-check the backend when mode resolves to remote.
@yoniebans
yoniebans force-pushed the feat/desktop-remote-update-skew branch from 1b25dae to 7c3b703 Compare June 7, 2026 08:17
yoniebans added 5 commits June 7, 2026 10:47
HERMES_DESKTOP_REMOTE_URL forces a remote connection but never writes
connection.json, so the gateway panel read mode/url from persisted config
and mislabelled an env-remote session as local with no url.
…tarts

The backend Install path set stage:'restart' and stopped — in remote mode no
boot-progress events arrive to carry the overlay to done, so it sat on the
restarting spinner until a manual reload while the backend had already come
back. Poll the backend until it answers again, then clear the overlay and
refresh the backend status. Target-aware applying copy explains the remote
restart + auto-reconnect instead of the local-updater-window wording.

Also switch the apply poll sleeps from window.setTimeout to globalThis.setTimeout
so the flow is exercisable off the renderer.
…o-return

Three rough edges in the remote backend apply flow:
- On success the overlay dropped to IDLE, briefly re-rendering the pre-install
  'update available' view and then the generic 'you're all set' before settling.
  Close the overlay outright once the backend is confirmed back instead of
  bouncing through the idle view.
- If the backend never came back (a failed restart), the flow still reported
  success. waitForBackendReturn now returns whether the backend answered;
  finishBackendApply surfaces an error when it didn't.
- The up-to-date copy said 'you're running the latest version', conflating
  client and backend. Backend target now reads 'the backend is running the
  latest version' — the client's own version is a separate pill.
…returns

The no-return error said 'Backend updated but did not come back online' — but
once the connection drops the client can't know the update's exit code, only
that it was started and the backend is unreachable. Reword to not overclaim:
the update may not have completed.
@yoniebans yoniebans changed the title feat(desktop): client + backend version buttons with per-target update overlay feat(desktop): client + backend version buttons and remote backend update flow Jun 8, 2026
yoniebans added 2 commits June 8, 2026 14:13
Two independent reviewers flagged that applyBackendUpdate's in-progress and
error messages were inline English while the rest of the update overlay is
i18n'd. Move them into updates.applyStatus (preparing/pulling/restarting/
notAvailable/failed/noReturn) across en, ja, zh, zh-hant + types.
@teknium1

teknium1 commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Merged via #42181 — your 14 commits were cherry-picked onto current main with your authorship preserved in git log (rebase merge).

The only change from your branch: corrected the deployment note. There's no templates/hermes-dashboard.service file in the repo, and the standard unit generated by hermes gateway service install is Restart=always + RestartForceExitStatus=75 — which already satisfies the restart-wait flow. The on-failure caveat applies only to the NixOS host unit (#35102), so the note now scopes it there.

Thanks!

@teknium1 teknium1 closed this Jun 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/cli CLI entry point, hermes_cli/, setup wizard 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