Skip to content

Desktop: ask install or connect on first launch - #62293

Open
Zeus-Deus wants to merge 3 commits into
NousResearch:mainfrom
Zeus-Deus:feat/desktop-first-run-choice
Open

Desktop: ask install or connect on first launch#62293
Zeus-Deus wants to merge 3 commits into
NousResearch:mainfrom
Zeus-Deus:feat/desktop-first-run-choice

Conversation

@Zeus-Deus

Copy link
Copy Markdown
Contributor

What this does

Right now, installing the desktop app on a fresh machine immediately downloads and installs the full agent runtime on that machine. There is no way to say that your agent already runs somewhere else and you just want to connect to it.

This PR adds a first launch screen that asks where your agent should run. You can install it on this computer like before, or you can connect to a Hermes gateway that is already running on another machine. I built this because I want to run the agent on my home server and use the desktop app on my laptop as a pure client. The code comments in the repo already hint at a future lite client, so I hope this fits the direction you have in mind.

choice screen with install selected

choice screen with connect selected

How it works

On a fresh machine the main process now waits before starting the local install. The renderer shows the setup screen with two options.

Install on this computer runs the exact same bootstrap as before. The choice is saved to disk, so an interrupted install resumes on the next launch without asking again, and the Windows recovery handoff keeps working.

Connect to a server reuses the remote gateway form from Settings, which I extracted into a shared hook and component so both screens stay in sync. It probes the server to detect token or OAuth auth, then applies the remote connection. The local install tree is never touched on this path.

connect form filled in with a reachable server

app running as a pure client against a remote gateway

install path handing off to the normal install overlay

Existing installs, dev checkouts, a hermes on PATH, and profile pool backends never reach this screen, so nothing changes for anyone who already has the app set up.

There is also a new dev flag, HERMES_DESKTOP_FORCE_BOOTSTRAP=1, which makes the fresh machine path testable from a source checkout.

Testing

Typecheck and lint are clean, and the new gate module has its own unit tests (10 cases) next to the existing electron test suites, which still pass.

I also tested the whole flow end to end. I ran a gateway in Docker to stand in for a home server, launched the app in a sandboxed dev environment simulating a fresh machine, and verified that the screen shows up before anything is installed, that connecting with a session token turns the app into a pure client with nothing written to the local install tree, that the install option hands off to the normal install overlay, that the choice persists across a relaunch, and that a normal dev launch never shows the screen.

One note. On Linux without a keyring service the app cannot save remote gateway tokens because safeStorage is unavailable. That behavior already exists today in the Settings gateway form and this PR simply inherits it. I am opening a separate issue for that.

@Zeus-Deus
Zeus-Deus requested a review from a team July 10, 2026 20:49
@alt-glitch alt-glitch added type/feature New feature or request comp/desktop Electron desktop app (apps/desktop/*) area/config Config system, migrations, profiles P3 Low — cosmetic, nice to have labels Jul 10, 2026
@Zeus-Deus
Zeus-Deus force-pushed the feat/desktop-first-run-choice branch from fb6c648 to 49beb87 Compare July 10, 2026 21:43
@teknium1

Copy link
Copy Markdown
Contributor

Thanks for the focused first-launch client-mode flow. The premise is confirmed on current main: apps/desktop/electron/main.ts:3527 immediately begins bootstrap after runtime resolution fails, while the remote form is only reachable in apps/desktop/src/app/settings/gateway-settings.tsx:97 after startup.

Problems

  • apps/desktop/electron/first-run-gate.test.ts:1 covers the pure latch only. It does not cover the production integration through ensureRuntime(), hermes:first-run:* IPC, and connection-config:apply, where an aborted parked boot must restart against the newly saved remote connection.
  • This needs a manual port rather than a clean cherry-pick: compared with the PR base, current main has changed these touched desktop surfaces by 1,339 deletions, including 545 lines in electron/main.ts and 596 lines in gateway-settings.tsx.

Suggested changes

  • Preserve the gate design, but add an integration-level test for install and remote-apply transitions while porting it to current main.

Automated hermes-sweeper review.

@teknium1 teknium1 added sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:blast-contained Sweeper blast radius: contained — one narrow path / opt-in / few users labels Jul 11, 2026
@Zeus-Deus
Zeus-Deus force-pushed the feat/desktop-first-run-choice branch from 49beb87 to 4f63bac Compare July 11, 2026 19:33
@Zeus-Deus

Copy link
Copy Markdown
Contributor Author

Thanks for the review. I pushed an update that addresses both points.

Rebase. The branch is now rebased onto current main and it came over clean. I could not reproduce the 1,339 deletion drift from the review. On my machine the diff between my old base and current main changes main.ts by 2 lines and does not touch gateway-settings.tsx at all, so I suspect the comparison ran against an older base. Either way the PR now sits on top of current main.

Integration tests. Fair point, first-run-gate.test.ts only covered the latch. main.ts has no exports, so I extracted the orchestration it wired inline around the gate into a new electron free module, first-run-flow.ts, following the same pattern as connection-config.ts. main.ts now calls those helpers with identical behavior. The new first-run-flow.test.ts wires the real gate, the real flow helpers, the real modeIsRemoteLike classifier and real on disk marker and config files together and drives both transitions:

  • install: the parked boot resolves on choose, the choice is persisted, bootstrap runs exactly once, and a relaunch with the marker resumes without asking again
  • remote apply: the config is written first, the teardown aborts the parked boot, and the retry resolves the freshly saved remote without parking again and without ever touching the local install tree
  • the bounded retry case: if the aborted boot finds no remote config, the retry re-parks the gate instead of looping or bootstrapping on its own
  • the orphan case: a teardown settles a parked run, so a single install click can never resume two concurrent installers

The wiring inside main.ts itself (gate before the Windows recovery handoff, primary vs pool call site, the non latching catch, gate abort at the top of the teardown, config write before the teardown) is pinned with the repo's source assertion pattern, same as windows-hermes-resolution.test.ts.

The full electron suite passes with the new file included (the single failure in windows-child-process.test.ts is already failing on a clean main checkout and is unrelated). Typecheck and lint are clean.

I also re-ran the whole flow end to end after the refactor to make sure the extraction changed nothing in practice: sandboxed fresh machine launch, real gateway running from this checkout. The choice screen shows before anything installs, connecting turns the app into a pure client with the desktop log showing the aborted boot re-dialing the saved remote and nothing written to the install tree, choosing install hands off to the normal install overlay, and killing the app mid install and relaunching resumes the install without asking again.

@Zeus-Deus

Copy link
Copy Markdown
Contributor Author

@teknium1 Bumping this since it keeps drifting out of date while it waits — I've resolved the conflicts with main again (twice now since your review) and the PR is currently mergeable.

Latest sync notes:

  • Merged current main into the branch. The only conflict this round was apps/desktop/package.json: main migrated the electron tests from node --test to vitest, so I dropped the old explicit test-list script in favor of main's vitest run --project electron and migrated the PR's two test files (first-run-gate.test.ts, first-run-flow.test.ts) to vitest following the same pattern as 955c5b7.
  • The integration coverage you asked for landed earlier in first-run-flow.test.ts (install, remote-apply, bounded retry, and orphaned-run transitions, plus source assertions pinning the main.ts wiring). All of it passes under the new vitest runner: 433 passed / 1 skipped across the electron project, typecheck and lint clean.
  • Verified the source assertions still pin the right code after main's recent main.ts refactors — no drift.

Happy to keep rebasing, but if there's anything else blocking a merge decision, let me know and I'll address it.

@Zeus-Deus
Zeus-Deus force-pushed the feat/desktop-first-run-choice branch from a574c08 to 389a9f7 Compare July 14, 2026 16:36
@Zeus-Deus

Copy link
Copy Markdown
Contributor Author

@teknium1 rebased over the vitest migration and the embeddable gateway panel work, so the PR is conflict free and mergeable again. The gateway-settings resolution keeps both sides intact: the embedded prop semantics from main (bare content, chrome and diagnostics hidden when embedded) and the shared RemoteConnectForm extraction from this PR, with the dev soft switch row staying removed. The two first-run test files are ported to the vitest idiom with their bodies unchanged, and the package.json test list edit is gone since vitest discovers them on its own now.

Everything passes under the new runner: the whole electron project is green (433 tests including the 20 first-run integration cases you asked for), the ui project matches the main baseline, and typecheck, lint and prettier are clean on every line this PR adds. Ready for another look whenever you have time.

@teknium1 teknium1 added the area/install-update Installer, updater, packaging, wheels, doctor label Jul 19, 2026
On a fresh machine the desktop app used to install the full local runtime
automatically before the user could tell it not to. Now the first launch asks
where the agent should run: install on this computer, or connect to a Hermes
gateway that is already running somewhere else (a home server for example).

The install path proceeds exactly as before once chosen, and the choice is
persisted so interrupted installs resume without asking again. The connect
path reuses the remote gateway form from Settings, now extracted into a shared
hook and component, probes the server for its auth mode, and applies the
remote connection without ever touching the local install tree.

Existing installs, dev checkouts, hermes on PATH, and pooled profile backends
never see the prompt. A new HERMES_DESKTOP_FORCE_BOOTSTRAP=1 dev flag makes
the fresh-machine path testable from a source checkout.
The gate unit tests only covered the pure latch. Review feedback asked for
coverage of the production integration: ensureRuntime parking on the gate,
the hermes:first-run IPC transitions, and the connection-config apply path
where an aborted parked boot has to restart against the freshly saved
remote connection.

main.ts has no exports, so the orchestration it wired inline around the
gate now lives in first-run-flow.ts, electron free and dependency injected,
same pattern as connection-config.ts. main.ts keeps the exact same behavior
and just calls the extracted helpers.

first-run-flow.test.ts drives the real gate, the real flow helpers, the
real modeIsRemoteLike classifier and real on disk marker and config files
through both transitions. Install: the parked boot resolves on choose, the
choice persists, bootstrap runs exactly once, and a relaunch resumes
without asking again. Remote apply: the config is written first, the
teardown aborts the parked boot, and the retry resolves the remote backend
without ever touching the local install tree. It also pins the bounded
retry (a non remote re-run re-parks the gate instead of looping) and the
orphaned run protection (a teardown settles a parked run so a single
install click can never resume two installers). The wiring inside main.ts
itself is pinned with the repo's source assertion pattern.
@Zeus-Deus
Zeus-Deus force-pushed the feat/desktop-first-run-choice branch from 8a2289e to 35b57dc Compare July 19, 2026 18:38
# Conflicts:
#	apps/desktop/electron/main.ts
#	apps/desktop/src/app/settings/gateway-settings.tsx
#	apps/desktop/src/app/settings/primitives.tsx
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/config Config system, migrations, profiles area/install-update Installer, updater, packaging, wheels, doctor comp/desktop Electron desktop app (apps/desktop/*) P3 Low — cosmetic, nice to have sweeper:blast-contained Sweeper blast radius: contained — one narrow path / opt-in / few users sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants