Commit graph

179 commits

Author SHA1 Message Date
github-actions[bot]
75f45a0692
fmt(js): npm run fix on merge (#65229)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-15 21:40:56 +00:00
ethernet
09f8a8268c test: port workspace-level JS tests to a new js-tests workspace package 2026-07-15 17:24:12 -04:00
ethernet
2f3007ff51 test: port JS/package.json invariant tests from Python to vitest
The CI change classifier routes package.json / package-lock.json /
.ts/.tsx changes to the frontend lane, not the Python lane. Four
Python tests asserted about these JS-side artifacts, so a PR touching
only those files would skip the Python suite — the regression goes
green on the PR and red on main (where the classifier fails open).

Ported all four to vitest so they run in the correct CI lane:

  tests/test_package_json_lazy_deps.py
    → apps/desktop/electron/package-json-lazy-deps.test.ts
    (camofox is lazy, agent-browser is eager, lockfile clean)

  tests/test_desktop_electron_pin.py
    → apps/desktop/electron/desktop-electron-pin.test.ts
    (electron dep is exact, matches build.electronVersion, lockfile agrees)

  tests/test_assistant_ui_tap_compat.py
    → apps/desktop/electron/assistant-ui-tap-compat.test.ts
    (@assistant-ui cluster shares one tap version + semver helper)

  tests/test_dashboard_sidecar_close_on_disconnect.py
    → web/src/lib/chat-sidebar-session-params.test.ts
    (sidecar session.create opts into close_on_disconnect + profile)

The ChatSidebar test was regex-matching .tsx source text (the
source-reading anti-pattern). Extracted sidecarSessionCreateParams()
from the component's effect into an exported pure function so the
test calls real code instead of pattern-matching a string.

Verified: 8 electron tests + 76 web tests pass; both typecheck clean.
2026-07-15 17:24:12 -04:00
Brooklyn Nicholson
5c03e27ce2 Merge remote-tracking branch 'origin/main' into bb/contrib-areas
# Conflicts:
#	apps/desktop/src/app/session/hooks/use-prompt-actions/index.ts
2026-07-15 13:22:08 -04:00
ethernet
1d48863b85 fix(desktop): inline git identity in worktree tracking test for CI
CI runners have no global git identity. The remote repo seed commit
needs inline -c user.email/user.name flags, matching the pattern
already used by ensureGitRepo.
2026-07-15 11:46:29 -04:00
ethernet
678b86df1c fix(desktop): worktree from origin/main should not set up upstream tracking
When "new worktree" branches off a remote-tracking ref like origin/main,
`git worktree add -b <branch> <dir> origin/main` auto-sets upstream
tracking (branch → origin/main), producing `branch:origin/main` in branch
listings. The user wants a standalone local branch — like `git checkout
origin/main && git checkout -b branch` — not one silently wired to the
remote. Add `--no-track` when the base is an `origin/` ref. Local branch
bases are unaffected (they never triggered tracking).
2026-07-15 11:46:29 -04:00
Brooklyn Nicholson
10fbade64b feat(desktop): electron — openDir IPC + ⌘W menu bridge (tabs, not windows) 2026-07-13 17:57:54 -04:00
Brooklyn Nicholson
39230d1738 refactor(desktop): funnel zoom apply+notify so restore can't desync
alelpoan's fix (emit hermes:zoom:changed after restore) is correct, but the
bug's root is duplication: setAndPersistZoomLevel and restorePersistedZoomLevel
each independently did setZoomLevel + send, and restore forgot the send.

Collapse both (and the lifecycle re-assert) into a single applyZoomLevel()
helper in zoom.ts that always applies-then-notifies — the regression can't
recur by omitting a send. Replace the source-grep test (which broke on main:
the sibling source-assertion pet test it copied was refactored to a behavioral
one, dropping the fs/path imports it relied on) with behavioral coverage of the
funnel, matching zoom.ts's "unit-testable without booting a BrowserWindow"
convention.

Co-authored-by: alelpoan <alelpoan@proton.me>
2026-07-13 17:39:03 -04:00
alelpoan
9f7a3cb1f6 test(desktop): cover restorePersistedZoomLevel renderer notification 2026-07-13 17:32:17 -04:00
alelpoan
a1a4c8ce1f fix(desktop): sync UI Scale setting after zoom restore on window load 2026-07-13 17:32:17 -04:00
ethernet
b034fa7026 test(desktop): convert zoom source-regex test to behavior test
Extracted zoomWiringForWindowKind() + ZOOM_WINDOW_CONFIG into zoom.ts so
the pet-overlay-
opts-out / chat-windows-keep-zoom contract is tested via the pure
config,
not by reading source. Callers in main.ts now use
zoomWiringForWindowKind()
instead of inline { zoom: false } / default { zoom: true }.
2026-07-13 17:22:17 -04:00
ethernet
727025a2f3 test(desktop): replace windows-child-process.test.ts regex with real tests 2026-07-13 17:22:17 -04:00
ethernet
955c5b73c5 test(desktop): move node tests to vitest as well 2026-07-13 17:22:17 -04:00
ethernet
56bec611e5 cleanup(desktop): note that ts imports don't need extension in one comment 2026-07-13 17:22:17 -04:00
ethernet
2762555038 cleanup(desktop): remove 'use strict' in ts & mjs
it's implied by ts and mjs files already
2026-07-13 17:22:17 -04:00
ethernet
7c98c65163 cleanup(desktop): lint&fmt all 2026-07-13 17:22:17 -04:00
ethernet
bffc098d09 test(desktop): fix relative import in oauth-net-request.test.ts, drop dead source-regex test
oauth-session-request.test.ts regexed main.ts source text (extracting the
fetchJsonViaOauthSession function body and matching regexes against it) to
check Electron net.request doesn't set the forbidden Content-Length header
and does call request.write(body).

That behavior is already covered for real by oauth-net-request.test.ts,
which imports the actual serializeJsonBody/setJsonRequestHeaders helpers
from oauth-net-request.ts and asserts on a mock request object's setHeader
calls -- it only needed its relative import corrected to include the .ts
extension (Node's ESM loader doesn't resolve extensionless relative
specifiers). Removed the now-fully-superseded oauth-session-request.test.ts
and its dangling package.json wiring.
2026-07-13 17:22:17 -04:00
ethernet
1b8f1504b3 test(desktop): extract profile-delete routing decision for real unit tests
profile-delete-respawn.test.ts regexed main.ts source text to check that
prepareProfileDeleteRequest returns the torn-down profile name, and that
the hermes:api ipcMain handler captures that return value and routes to
the primary backend instead of respawning a pool backend for the just-
deleted profile.

Extract the pure decision logic into profile-delete-routing.ts (no
Electron import):
- profileNameFromDeleteRequest(request): parses a DELETE
  /api/profiles/<name> path, moved verbatim (already pure).
- decideProfileDeleteAction(profile, deps): the branch decision (noop /
  teardown-primary / teardown-pool) and the profile name to return,
  parameterized over isDefaultProfile/isValidProfileName/primaryProfileKey.
- resolveRouteProfile(tornDownProfile, profile): the
   routing ternary from the hermes:api
  handler.

prepareProfileDeleteRequest in main.ts now calls decideProfileDeleteAction
for the decision and only performs the async side effects (teardown +
writeActiveDesktopProfile) the decision calls for.

profile-delete-routing.test.ts replaces profile-delete-respawn.test.ts
(which was never wired into test:desktop:platforms), importing the pure
functions directly and asserting real return values across every branch
-- no readFileSync, no regex-on-source. Wired the new test file into
test:desktop:platforms in package.json.
2026-07-13 17:22:17 -04:00
ethernet
ec2cb3ab47 test(desktop): extract Windows hermes-resolution helpers for real unit tests
windows-hermes-resolution.test.ts regexed main.ts source text to check
three Windows resolution bugs that caused desktop reinstall loops:
1. findOnPath()'s PATHEXT extension order (must try real extensions before
   the empty one, or an extensionless Git-Bash hermes shim shadows
   hermes.cmd/.exe).
2. handOffWindowsBootstrapRecovery()'s --update vs --repair choice (must
   gate on any real-install signal, not just the hermes.exe shim).
3. unwrapWindowsVenvHermesCommand()'s probe-before-trust behavior (must
   canImportHermesCli() before returning a venv python, or a broken venv
   gets re-selected forever).

Extract all three into pure, dependency-injected functions in
windows-hermes-path.ts (no Electron import): buildPathExtCandidates(),
chooseUpdaterArgs(), resolveVenvHermesCommand(). main.ts's
findOnPath/handOffWindowsBootstrapRecovery/unwrapWindowsVenvHermesCommand
now call these with their existing helpers (fileExists, canImportHermesCli,
getVenvPython, etc.) passed through as deps.

windows-hermes-path.test.ts replaces windows-hermes-resolution.test.ts,
importing the pure functions directly and asserting real return values
with fake/injected dependencies (fake venvs, fake probes) -- no readFileSync,
no regex-on-source.
2026-07-13 17:22:17 -04:00
ethernet
4527943e91 test(desktop): extract hiddenWindowsChildOptions + stopBackendChild for real unit tests
windows-child-process.test.ts regexed main.ts and bootstrap-runner.ts
source text to check that spawn/execFileSync call sites wrapped their
options with hiddenWindowsChildOptions(), and that backend teardown
chose the right kill strategy.

Extract both into dependency-free sibling modules:
- windows-child-options.ts: hiddenWindowsChildOptions(options,
isWindows)
  now takes isWindows as an injectable param (defaults to the real
  platform check). main.ts and bootstrap-runner.ts both import the same
  implementation instead of each defining their own copy.
- backend-child.ts: stopBackendChild(child, deps) with
forceKillProcessTree
  and isWindows injected, so the SIGTERM-vs-tree-kill branching is
directly
  testable with a fake child + a spy.

windows-child-options.test.ts replaces windows-child-process.test.ts,
calling the real functions with fake spawn/execFileSync-shaped objects
and asserting on the actual returned options / kill call.
2026-07-13 17:22:17 -04:00
ethernet
f6d1fd511c feat(desktop): auto-fetch remote base branch before worktree add
When the base is an origin/… ref, fetch just that branch so the
local tracking ref is fresh before `git worktree add -b new origin/main`.
Fetch failures (offline / no remote) are silently ignored — git uses
whatever local ref exists, or raises a clear error if it's missing.
2026-07-13 14:53:34 -04:00
ethernet
6f7ee72be5 feat(desktop): base-branch picker for new worktree dialog
The sidebar "New worktree" button branched off whatever HEAD you were
on — now a filterable Popover+Command combobox lets you pick any local
or remote-tracking branch as the base, defaulting to origin/HEAD.

Backend listBaseBranches() queries refs/heads + refs/remotes via
for-each-ref, flags origin/HEAD (falling back to the local default for
no-remote repos). Mirrored on the Python REST API side
(base_branch_list + /api/git/base-branches route).
2026-07-13 14:53:34 -04:00
Brooklyn Nicholson
88fbc8825c feat(desktop): bridge WSL paths for a Windows host + WSL backend
When the desktop UI runs on Windows and the gateway runs in WSL, a WSL/POSIX
cwd isn't openable/readable from the Windows host. Add wsl-path-bridge.ts to
translate the Windows-side direction only:
- native folder dialog defaultPath: `/home/...` → `\\wsl.localhost\<distro>\...`
- fs read path: WSL cwd → its UNC / `C:\` drive form

Distro detection reads `wsl.exe -l -q` with `WSL_UTF8=1` and strips stray NUL
bytes, since older wsl.exe emits UTF-16LE (microsoft/WSL#4607) — the original
utf8 read returned a garbled distro name. UNC uses `\\wsl.localhost\` with a
`\\wsl$\` fallback for older Windows. The reverse (any path → POSIX) is handled
once gateway-side, so the picker result needs no desktop translation.

Co-authored-by: Rage Lopez <VrtxOmega@pm.me>
2026-07-12 05:32:54 -04:00
Brooklyn Nicholson
1a3b220651 fix(desktop-terminal): reopen terminal tabs in the last-used directory
A reopened tab restarted the shell in its original launch dir, so the fresh
prompt showed the wrong folder after a prior `cd` (the issue's "separate
thing" note). Track the shell's working directory and restart the PTY there.

Two independent signals feed a persisted per-tab restoreCwd:
- a main-side PTY cwd probe (shell-agnostic; /proc on Linux, lsof on macOS;
  Windows has no cheap per-process query so it falls back to the launch dir)
- cwd-reporting OSC sequences parsed in the renderer (OSC 7 file URIs, OSC 9;9
  ConEmu/Windows-Terminal paths) for shells configured to emit them

On relaunch the fresh shell boots in restoreCwd, falling back to the launch
cwd (then home) when it no longer exists.
2026-07-12 02:49:26 -04:00
ethernet
5e849942c3 feat(dev): add isolated sandbox script for local dev
scripts/desktop-sandbox.sh runs a Hermes desktop instance in an isolated
sandbox — separate HERMES_HOME, separate Electron userData, and a
distinct
app name (HERMES_DESKTOP_APP_NAME) so it doesn't compete with the main
desktop instance's single-instance lock.

Two modes:
- Ephemeral (default): temp dir, cleaned up on exit
- --persistent: stored under .hermes-sandbox/ in the worktree git root,
  survives restarts for repeat testing

In the Nix devShell the script is available as 'sandbox'.

Also makes APP_NAME overridable via HERMES_DESKTOP_APP_NAME in main.ts —
app.setName() runs before requestSingleInstanceLock(), so the overridden
name changes the lock key. collectRelaunchEnv already preserves
HERMES_DESKTOP_* vars through self-update relaunches; test updated to
cover the new env var.
2026-07-10 18:51:49 -04:00
brooklyn!
a9f3f08700
Merge pull request #61916 from NousResearch/bb/desktop-gateway-switch-ux
feat(desktop): soft gateway switch + gateway-settings polish
2026-07-10 03:16:09 -05:00
brooklyn!
79a104d037
Merge pull request #61912 from NousResearch/bb/salvage-55402-desktop-cloud-mode
feat(desktop): Hermes Cloud connection mode (salvage of #55402)
2026-07-10 03:15:59 -05:00
Brooklyn Nicholson
b3bde1fbee feat(desktop): soft gateway switch + gateway-settings polish
Switching connection mode (local / cloud agent / remote) no longer
full-window-reloads into the cold-boot CONNECTING screen. The primary
backend is torn down in place (no renderer reload); the shell + Settings
stay up while session lists are wiped so sidebar skeletons retrigger, then
the socket re-dials and config/sessions refresh. Cold-boot CONNECTING
latches off after the first successful boot; the intentional teardown
suppresses the backend-exit toast. Dev affordance: a "Preview soft switch"
button under Gateway diagnostics (Electron has no ?query= entry).

Gateway settings UI brought in line with the rest of Settings:
- Mode cards use the shared selectableCardClass on an equal-height
  auto-rows-fr grid, stacking 1→3 (never an orphaned 2+1); titles wrap
  instead of truncating.
- Remote gateway's auth detail moves into a ? tooltip in the title; drop
  the redundant "connects to the one you choose" from the cloud card.
- textStrong buttons force px-0 so the underline sits flush with the label.
- Tooltip chip uses box-decoration-break: clone so the background hugs each
  wrapped line (bg only on the text), capped at max-w-64.

Fully i18n'd (en + zh; ja/zh-hant inherit via defineLocale).
2026-07-10 02:57:02 -05:00
Brooklyn Nicholson
6207d68948 fix(desktop): prevent bootstrap stale commit repin on existing checkouts
Old packaged Desktop apps re-entering bootstrap against an existing
~/.hermes/hermes-agent were still passing the baked-in --commit pin, which
detached the managed checkout back to the app stamp (e.g. 0.15.1) after
hermes update had already moved it forward.

Skip the packaged commit pin when activeRoot already has git metadata;
keep branch args and fresh-install commit pinning unchanged. Port of
#59902 onto the post-ts-ify bootstrap-runner.ts.

Co-authored-by: helix4u <4317663+helix4u@users.noreply.github.com>
2026-07-10 02:29:58 -05:00
Brooklyn Nicholson
57dfebe3db fix(desktop): re-apply UI zoom on show/restore, scoped to chat windows
Windows drops webContents zoom on minimize/restore, so the UI snapped back
to 100% while Settings still read 125%. Zoom was only reasserted on the main
window's did-finish-load, never on show/restore and never for session windows.

Reassert the persisted level on show/restore + first load, wired once in
wireCommonWindowHandlers so the main window and secondary session windows
share it. The pet overlay opts out (zoom:false): it sizes its own OS window
to fit the sprite in unzoomed CSS px and has its own Alt+wheel scale, so
inheriting the global zoom would render the mascot larger than its window and
crop it (and it shares the renderer origin's zoom localStorage key).

Salvages #61245; keeps its pure-helper tests and adds a scope assertion.

Co-authored-by: HexLab98 <liruixinch@outlook.com>
2026-07-10 02:13:10 -05:00
Brooklyn Nicholson
0ff097439e refactor(desktop): DRY the cloud helpers
Tighten the salvaged Hermes Cloud code with no behavior change:
- main: one `trimCloudOrg` projection reused by the success-echo and the 409
  org list (drop the duplicated map), and a `cloudLoginError()` factory for the
  three needsCloudLogin throw sites.
- renderer: a `cloudLoginLapsed()` predicate for the duplicated
  needsCloudLogin→signed-out check.
2026-07-10 01:47:46 -05:00
Ben
c101207b99 feat(desktop): Hermes Cloud connection mode — one sign-in, agent discovery, silent connect
Adds a third "Hermes Cloud" gateway mode to the desktop app: one portal
sign-in auto-discovers the agents on your account and connects to any of
them with no second interactive prompt.

- Electron: widen connection mode to 'local' | 'remote' | 'cloud', routed
  through a centralized modeIsRemoteLike() so every resolution site treats
  cloud exactly like remote; portal discovery (GET /api/agents over the
  OAuth partition), Privy-cookie liveness, multi-org picker (NAS 409), and a
  silent per-agent /oauth cascade (load protected root, not /login).
- Persist a cloudOrg on the cloud block; unselect cloud on mode switch.
- Renderer: Hermes Cloud ModeCard + agent picker (signed-out/loading/empty/
  list), org picker, Change-org, connected-highlight + Connected pill.
- i18n (en + zh full; ja/zh-hant inherit via defineLocale), Cloud icon.
- IPC: hermes☁️{status,login,logout,discover,agent-sign-in}.

Salvage of #55402 onto current main: the original branch predates the
desktop electron .cjs -> .ts migration (39d09453f), so the electron half
was re-authored against the .ts files. Authorship preserved.

cloud-auto-discovery Phases 3 + 4.
2026-07-10 01:37:43 -05:00
Brooklyn Nicholson
bf913abc2e fix(desktop): stop using tsx to boot Electron main in dev
Electron 40 ships Node 24.15, where tsx's ESM load hook returns null and
crashes with ERR_INVALID_RETURN_PROPERTY_VALUE. Bundle main+preload via
esbuild for `npm run dev` and always load the JS preload from dist/.
2026-07-08 22:40:51 -05:00
ethernet
56a8e81d33 cleanup(desktop): npm run fix for fmtting
we should run this as part of merges at some point :)
2026-07-08 16:24:16 -07:00
ethernet
39d09453f9 feat(desktop): ts-ify everything 2026-07-08 16:24:16 -07:00
Adolanium
8ce3c2f991 feat(desktop): add UI scale setting to appearance settings 2026-07-07 13:04:32 -07:00
brooklyn!
91c68bf834
Merge pull request #55923 from NousResearch/bb/serve-headless-no-web-build
feat(cli): make hermes serve a real headless backend (no web UI build/mount, neutral ready sentinel)
2026-07-06 13:18:00 -05:00
teknium1
077419b220 test(desktop): regression-guard fetchJsonViaOauthSession headers (#40069)
Closes #40069.

Salvaged from #40242; re-verified on main, tightened, tested.

Co-authored-by: maxpetrusenkoagent <maxpetrusenkoagent@users.noreply.github.com>
2026-07-06 11:12:15 -07:00
Brooklyn Nicholson
409560a7d9 Merge remote-tracking branch 'origin/main' into bb/serve-headless-no-web-build 2026-07-06 12:51:15 -05:00
teknium1
713236dcd0 fix(desktop): normalize CRLF back to LF in update-marker files
The salvaged commit rewrote update-marker.cjs and its test with CRLF
line endings (Windows editor artifact); restore LF so the diff shows
only the substantive change.
2026-07-05 21:36:23 -07:00
derek2000139
d00c7193c1 fix(desktop/windows): pre-write update marker before quit dwell to prevent backend respawn 2026-07-05 21:36:23 -07:00
Teknium
3c2f628f5b
fix(desktop): probe venv python in unwrapWindowsVenvHermesCommand so Repair can escape a broken venv (#59204)
A Windows venv broken mid-update (e.g. python-dotenv missing after a partial
pip install) still has python.exe + Scripts\hermes.exe on disk.
unwrapWindowsVenvHermesCommand() returned that interpreter with no probe --
bypassing even the caller's --version smoke test -- so every recovery action
(Retry, Repair install, Use local gateway) re-resolved the same dead backend:
ModuleNotFoundError: No module named 'dotenv', same overlay, forever.

- unwrapWindowsVenvHermesCommand now runs canImportHermesCli() on the venv
  python (checkout on PYTHONPATH, mirroring isActiveRuntimeUsable) and
  returns null on failure so the resolver falls through to the bootstrap
  installer, which actually repairs the venv.
- hermesRuntimeImportProbe() adds 'import dotenv' -- the first third-party
  import on the CLI boot path (hermes_cli/env_loader.py) -- so a venv missing
  python-dotenv fails the probe everywhere it's used (isActiveRuntimeUsable,
  system-python rung, and the new unwrap gate).
- Regression tests: probe content + source assertion that the unwrap path
  probes and falls through.
2026-07-05 19:13:33 -07:00
Brooklyn Nicholson
65415b1a12 Merge remote-tracking branch 'origin/main' into bb/skills-renovate 2026-07-03 13:59:26 -05:00
Brooklyn Nicholson
715aa3de85 refactor(desktop): adopt shared utils + app-wide cleanups
Route the app off its hand-rolled helpers onto lib/{text,time,format,json-format}
and the new primitives, plus assorted small tidy-ups:
- compactNumber for counts/tokens; normalize/capitalize/asText at the many
  filter/label sites; shared Intl date/time formatters; row-hover + framed
  editor adoption; scrollbar-gutter + padding parity on list surfaces.
- Messaging/Artifacts/Cron search hints + narrow-viewport tab dropdown;
  floating-pet adopts useOnProfileSwitch; number formatting in statusbar,
  command-center, agents.
- Electron: native overlay width + backend spawn tidy.
- Settings > Keys: credential fields read as plain subtext (all-unset) until
  the group is focused or expanded, then take full input chrome with no
  horizontal/vertical shift; inline Remove (trash) + Save mirror SearchField's
  trailing-clear pattern instead of a floating hint that overlapped the card;
  Esc still cancels. Drops the now-dead or/escToCancel i18n keys.
- Shared TabDropdown/ResponsiveTabs (components/ui): PageSearchShell and the
  Command Center log file/level filters reuse the one narrow-width collapse.
- OverlayNav: data-driven pane nav — persistent rail on wide, a single dropdown
  riding the titlebar strip on narrow; Settings and Command Center adopt it, and
  the mobile dropdown carries the same section icons as the rail. Fixes narrow
  vertical centering, redundant mobile section titles, gateway-status wrap, and
  Panel master/detail stacking.
- OverlayIconButton is now the titlebar ghost button, matching the close X at
  every size. Settings sub-view nav opens section + sub-view in one navigate so
  API-keys/accounts actually open on narrow.
- Settings > Model: cube icon (was the {} namespace glyph) and a DOM-shaped
  skeleton in place of the centered spinner.
- Command palette / session switcher clear the macOS traffic lights on small
  screens.
- Prettier/eslint sweep across the touched files.
2026-07-03 13:48:44 -05:00
Teknium
b14d75f8af
fix(update): prevent and self-heal half-updated venvs on Windows (#57659)
Root-causes the July 2026 Windows incident chain (locked _brotlicffi.pyd /
_sodium.pyd during install, then 'No module named annotated_doc' with
'hermes update' insisting 'Already up to date!'):

- hermes update: probe venv core imports even when the checkout is current;
  a half-updated venv (dep sync killed mid-flight by a locked .pyd) is now
  detected and repaired instead of being reported as up to date
- hermes update (Windows): after pausing gateways, refuse to mutate the venv
  while other processes run from the venv interpreter (the Desktop backend
  runs as python.exe so the hermes.exe shim guard never saw it); --force
  keeps the old behavior
- install.ps1 venv stage: disarm gateway autostart Scheduled Tasks before
  the kill sweep (they respawn the gateway inside the kill->delete window),
  make the sweep a bounded loop requiring 3 clean passes, and rename-then-
  delete the old venv (a rename succeeds even with mapped DLLs) with stale-
  dir cleanup on the next run
- desktop updater: 'venv shim still locked after 15s' now ABORTS the update
  hand-off (restarting our backend, surfacing the holder to the user)
  instead of 'proceeding anyway (force)' into guaranteed venv corruption;
  the unlock wait also re-kills respawned backends each poll tick
2026-07-03 03:24:08 -07:00
Brooklyn Nicholson
359518beac fix(desktop): guard link-title readTitle against destroyed windows
Grace and timeout timers in runRenderTitleJob can call getTitle after
finish() tears down the hidden BrowserWindow, throwing in the main
process when the Artifacts page resolves many link titles concurrently.
2026-07-03 05:13:23 -05:00
Brooklyn Nicholson
42bc07d107 fix(desktop): cancel downloads triggered by link-title fetch window
The hidden BrowserWindow used by fetchLinkTitle to scrape page titles
had no will-download handler on its session.  When a link artifact URL
responds with Content-Disposition: attachment, Electron fires will-download
and the file is saved for real — explaining the spurious download on the
Artifacts page.

Add guardLinkTitleSession() (parallel to the existing audio-mute guard for
#49505) that installs a will-download handler which immediately cancels
every download item on the hermes:link-titles session.  Call it from
getLinkTitleSession() right after the request-type blocklist is wired up.
2026-07-03 01:46:27 -05:00
Brooklyn Nicholson
e40175f069 fix(desktop): stop macOS Tahoe misplacing the traffic lights
On macOS Tahoe (Darwin 25+), a nonzero titleBarOverlay height makes
setWindowButtonPosition() miscalculate the native traffic-light position
(electron#49183), shoving the lights into the left titlebar tools. Pass
height 0 there so the lights land at the configured inset; the renderer
paints its own drag strips, so nothing is lost. Pre-Tahoe is unchanged.

Gate on the truthful Darwin kernel major (25 = Tahoe) rather than the
product version, which macOS reports as 16 or 26 depending on build SDK.
2026-07-02 21:14:28 -05:00
liuhao1024
c5e8a60b0a fix(desktop): skip ensureBackend after profile-delete teardown to prevent respawn loop
When the renderer sends a DELETE /api/profiles/{name} request, the IPC
handler tears down the profile's pool backend (or primary backend) via
prepareProfileDeleteRequest.  However, the very next line calls
ensureBackend(profile), which spawns a fresh pool backend for the just-
deleted profile.  The new backend's startup path calls ensure_hermes_home(),
which recreates the profile directory — defeating the deletion and leaving
the process as a zombie.

On the next Desktop restart the cycle repeats: the profile directory exists,
the Desktop spawns a backend, the backend recreates the directory after
deletion, and PIDs accumulate indefinitely.

Fix: make prepareProfileDeleteRequest return the torn-down profile name.
The IPC handler uses this to route the DELETE to the primary backend
instead of spawning a new pool backend for the deleted profile.

Fixes #52279
2026-07-02 15:18:49 -05:00
Brooklyn Nicholson
f0f8c84d1b feat(cli): make hermes serve a real headless backend
`serve` (added in #54568) reused cmd_dashboard wholesale, so it still
behaved like a dashboard: it ran a full vite build every launch, mounted
and served the SPA whenever a stray web_dist/ existed, printed
"Hermes Web UI →", and announced HERMES_DASHBOARD_READY. It's the headless
JSON-RPC/WS backend the desktop app and remote clients run — pure socket
clients that never load the browser SPA.

Mark serve with headless_backend=True (resolved once in cmd_dashboard) and:

- skip _build_web_ui entirely on the serve path
- export HERMES_SERVE_HEADLESS=1 so mount_spa() disables the SPA even when a
  dist is present — only the JSON-RPC/WS/API surface is reachable
- announce the bind ("Hermes backend listening on host:port") instead of a
  browser/auth-gated URL
- print a neutral HERMES_BACKEND_READY sentinel; dashboard keeps the legacy
  one and the desktop port-discovery regex matches either
- preserve serve across the named-profile re-exec so it can't rebuild as
  dashboard

`hermes dashboard` is unchanged (builds + serves the browser UI). Backward
compatible: old apps only ever spawn dashboard (legacy token + UI intact)
and never invoke serve; the ready-file side channel is name-agnostic. The
one behavior change is that a remote `hermes serve` no longer serves the
browser dashboard as a side effect — that's `hermes dashboard`'s job.

Tests: serve headless_backend contract, SPA-disabled-with-dist, the
HERMES_BACKEND_READY desktop parse (17/17 node), and the existing
serve/dashboard/web_server suites. AGENTS.md documents the behavior.
2026-06-30 17:58:52 -05:00