Keep-awake lives only in Settings → Advanced now. Remove the statusbar
quick-toggle (+ its Sun icon, store toggle helper, and keepAwakeOn/Off
strings across locales). Since the statusbar was what eagerly loaded the
store at boot, move persistence to the main process (keep-awake.json,
re-applied on app ready — same pattern as translucency), so a cold launch
restores the blocker without the renderer opening Settings.
Blocking #1 — gateway-connecting-overlay.tsx reduced-motion regression:
the top `if (reduce) setPhase('gone')` fired unconditionally on mount
whenever reduce-motion was on, so every OS reduced-motion user lost the
CONNECTING overlay during cold boot entirely (jumped to 'gone' before the
gateway was even open). The intent was to skip the exit *choreography*,
not to skip showing the overlay. Removed the unconditional top block and
the redundant nested preview block; kept only the third branch
(`gatewayState === 'open' && shownRef.current` → `reduce ? 'gone' :
'text-out'`) which correctly gates the short-circuit on connect. Also
fixed `if(reduce)` missing-space, 6-space misindent, and the same 3-line
comment pasted three times.
Nit #1 — tsconfig excludes e2e, so specs were never typechecked in CI.
Added tsconfig.e2e.json (extends base, includes e2e/ + playwright.config.ts,
adds @playwright/test types) and wired it into the typecheck script. This
surfaced three latent type errors that are fixed in the same commit:
- fix-electron-tracing.ts: `app._context` and `electron._playwright` are
private APIs — added `as any` on the access before the existing cast.
- playwright.config.ts: `reducedMotion: 'reduce'` directly under `use:`
is not a valid UseOptions property in playwright 1.58; it's a
BrowserContextOption accessed via `contextOptions: { reducedMotion:
'reduce' }`. The old form was silently ignored at runtime, so
reduced-motion emulation wasn't actually active — screenshots could
catch overlays mid-fade (exactly what the comment warned about).
Nit #2 — fix-electron-tracing.ts reaches into Playwright internals
(_playwright, _allContexts, _context) with no public contract. Added a
header comment calling out the `@playwright/test` exact pin (=1.58.2) so a
future bump knows to re-verify the private symbols still exist.
Nit #3 — main.ts TEST_WORKER_INDEX block had stray 6-space indentation.
Verified: tsc -p . && tsconfig.electron && tsconfig.e2e → 0 errors;
vitest boot-failure-overlay (3/3) + boot-failure-reauth (21/21) pass;
npm run build clean; playwright e2e/boot-failure.spec.ts 2/2 pass.
Add a "keep computer awake" toggle (Claude-style) for long/overnight runs:
the renderer owns the device-local pref and mirrors it to the Electron main
process, which holds a single `powerSaveBlocker('prevent-app-suspension')` —
the same authority split as translucency. Surfaced as a statusbar quick-toggle
and a Settings row.
Introduce a dedicated System settings section (device-local machine prefs) and
de-crowd Appearance by moving Window Translucency + UI Scale into it (both are
main-process/window-owned, not visual theme). Give Haptic Feedback its first
Settings home there too (the titlebar quick-toggle stays). Relocated i18n copy
into `settings.system` across en/zh/zh-hant/ja; wired the `system` route into
the SettingsView union + allowlist + nav.
The boot-failure screenshot showed a progress bar because of two bugs:
1. waitForBootFailure matched on "Let's get you setup" (the onboarding
header that mounts from frame 1 during normal boot), so the screenshot
fired at ~86% progress while the Preparing component's progress bar was
still painted.
2. The Preparing component kept rendering the progress bar even after
boot.error was set — it just turned the bar red and appended the error
text below it.
Fixes:
- Preparing bails out (returns null) when boot.error is set, so
BootFailureOverlay (z-1400) owns the screen exclusively.
- applyDesktopBootProgress no longer clobbers a previously-set boot.error
when a late progress event arrives with error: null — failDesktopBoot is
terminal for the boot cycle.
- waitForBootFailure guards against progress bars being visible and matches
on actual failure signals (error toast, Retry/Repair buttons), not the
onboarding header.
- setupDeadBackend now accepts { fakeError: true } which injects
HERMES_DESKTOP_BOOT_FAKE_ERROR to trigger a real boot failure — the
previous dead-provider fixture never actually caused a boot failure
(hermes serve starts fine; the dead endpoint only matters at chat time).
- boot-failure.spec.ts updated to use { fakeError: true }.
Verified: e2e test passes with 0 progress bars in the DOM at screenshot
time (confirmed via DOM inspection), 16/16 vitest tests pass, typecheck
clean.
- Type gatewayState in session store
- Electron main.ts: force-show window for e2e test workers
- tsconfig: include e2e test types
- nix/devShell.nix: add cage for headless visual testing on tiling WMs
A `persist:` partition's cookie store hydrates lazily, so the first
cookies.get() on a fresh launch can return empty for a signed-in user.
That false-negative made hasLiveOauthSession() throw "not signed in",
which on the no-retry initial boot path surfaced as the transient
"Hermes couldn't start" OAuth overlay that always cleared on Retry.
hasLiveOauthSession now reads once (no added latency on the happy path);
only on an empty read does it warm the store (flushStorageData + a
throwaway get, memoized) and re-read with a bounded ~180ms backoff
before trusting the negative. Genuinely signed-out users still resolve
false quickly and get the overlay. Fixes the whole class: the same
function backs the reconnect path and the Settings connected indicator.
* fix(desktop): allow write-build-stamp from non-git checkouts
Stop hard-failing npm pack when neither GITHUB_SHA nor git HEAD is
available (ZIP installs / broken .git). Emit an explicit fallback stamp
instead so local Windows desktop builds can finish (#50823).
* fix(desktop): treat fallback stamps as unpinned; harden Windows install
Keep all-zero fallback commits out of -Commit/--commit pins and fetch
install.ps1 by branch instead. After bootstrap, pin the marker to the
checkout HEAD so isBootstrapComplete accepts it. On Windows, force ZIP
checkout, seed GITHUB_SHA (ASCII-only install.ps1), and avoid the pack
stamp failure.
* fix(install): pin core.autocrlf=false before ZIP-path checkout (#50823 review)
The ZIP-fallback path added in #67643 runs `git checkout -f FETCH_HEAD`
before core.autocrlf gets pinned (which only happened later, on the
shared clone-path config). On Git for Windows -- where core.autocrlf
defaults to true -- that renormalizes the repo's LF text files to CRLF in
the working tree during checkout, leaving the freshly-created managed
checkout dirty versus HEAD and aborting the next `hermes update`. That is
the exact "dirty tree the user never touched" failure the surrounding
code already guards against (install.ps1:1461-1469, 1750-1753).
Move the `config core.autocrlf false` pin to run immediately after
`git init`, before the fetch/checkout. The later idempotent pin on the
shared clone path is retained so git-clone installs are unaffected.
Addresses teknium1's review on #67643 and supersedes it, preserving the
original author's two commits.
Co-authored-by: HexLab98 <8422520+HexLab98@users.noreply.github.com>
* chore(contributors): map austinpickett commit email for attribution
The check-attribution CI gate flagged austinpickett@users.noreply.github.com
as an unmapped commit-author email (introduced by the autocrlf fix commit
on this PR). Add the per-email mapping file as the gate instructs (the
legacy AUTHOR_MAP in scripts/release.py is frozen).
---------
Co-authored-by: HexLab98 <liruixinch@outlook.com>
Co-authored-by: austinpickett <austinpickett@users.noreply.github.com>
Co-authored-by: HexLab98 <8422520+HexLab98@users.noreply.github.com>
simple-git's custom-binary validation rejects paths containing spaces, so
the default Windows Git install (C:\Program Files\Git\cmd\git.exe) made
every Review pane git call throw and the pane silently showed 'No diffs'.
The binary is resolved inside the Electron main process from known install
locations or PATH — never renderer/user input — so for spaced paths we opt
into simple-git's supported unsafe.allowUnsafeCustomBinary escape hatch
rather than falling back to PATH (often absent in GUI-launched apps).
Simplified from PR #64713 by @unsupportedpastels; supersedes the 8.3
short-path approaches in #55337/#60156.
Fixes#54888
The sidebar refresh fired three /api/profiles/sessions calls (recents, cron,
messaging), and each one reopened every selected profile's state.db and re-ran
list_sessions_rich + session_count — ~3N DB opens/counts per refresh, on every
turn/broadcast/reconnect.
Add GET /api/profiles/sessions/sidebar: one pass that opens each profile DB
once and runs the three source-scoped queries together (recents scoped to the
active profile; cron + messaging cross-profile), returning the three windows in
one payload. Same read-only projection, 300s active heuristic, and caller-
supplied source taxonomy (recents_exclude / messaging_exclude / source=cron) as
the per-slice endpoint.
Renderer refreshSessions now makes one listSidebarSessions call and distributes
recents/cron/messaging to their stores (cron *jobs* stay a separate getCronJobs
API). Electron splices remote profiles per slice via fetchProfilesSessionSlice
(reusing the proven per-slice merge) so remote correctness is preserved; the
no-remote common case gets the single-open fast path.
From the Desktop performance audit (P1: "Batch sidebar session slices").
Surgical reapply of the surviving half of PR #43517 by @jingsong-liu
(the branch predates the ts-ify migration; its other half — zoom restore
after reload/navigation — landed via #66989).
On US layouts Plus is physically Shift+=, so Cmd+Plus arrives with the
shift modifier set. The blanket 'input.shift' early-return in
installZoomShortcuts silently swallowed keyboard zoom-in on macOS: the
chord matched neither branch and fell through to nothing. Shift is now
evaluated per-chord: zoom-in accepts it, zoom-reset and zoom-out still
reject it (Ctrl/Cmd+Shift+0 and Shift+'-' are different chords).
Ground-truth reapply of PR #40414 by @liuhao1024. The original branch
predates the ts-ify migration and implemented the gesture by injecting
a DOM wheel listener via executeJavaScript + a new IPC channel. Current
Electron surfaces the modifier+wheel gesture natively as the main-process
webContents 'zoom-changed' event, so the salvage uses that instead:
no renderer injection, no new preload surface, no new IPC channel.
The handler routes through setAndPersistZoomLevel — the same
persist+notify funnel as the keyboard shortcuts — so wheel zoom uses
the same 0.1 half-step, persists to zoom-state.json across restarts,
and keeps the settings Scale control in sync. Session windows get the
gesture automatically via wireCommonWindowHandlers; the pet overlay
stays opted out via zoomWiringForWindowKind.
Surgical reapply of PR #46429 by @SongotenU (the branch predates the
ts-ify migration; main.cjs no longer exists so a direct cherry-pick
cannot apply).
Main already adopted half of the PR's fix when zoom restore moved into
wireCommonWindowHandlers (57dfebe3d) — session windows are covered. The
surviving delta is the listener lifetime: 'once' spends the listener on
the first did-finish-load, so any later full load in the same
webContents — the crash-recovery webContents.reload(), a manual reload,
or an in-place navigation that lands on a fresh per-host zoom entry —
came up at default zoom while the settings Scale control still showed
the persisted percentage. 'on' re-applies the persisted level after
every completed load; restorePersistedZoomLevel routes through the
applyZoomLevel funnel so the renderer stays in sync.
Surgical reapply of the surviving halves of PR #57414 by @Sahil-SS9
(the branch predates the ts-ify migration and the zoom apply/notify
funnel, so a direct cherry-pick no longer applies):
- Zoom persists to a main-process zoom-state.json as the primary store.
The old localStorage-only store lives under Electron's cache/storage
folders, which crash recovery can move or recreate — wiping zoom
exactly when the user recovers from a crash. localStorage stays as a
secondary mirror; pre-JSON installs migrate on first read.
- Window geometry persists at ready-to-show, so a crash before the
first resize/move/close still captures the restored bounds.
The third half of #57414 (one-shot --no-sandbox relaunch on Windows
renderer crash loops) was superseded by #66842, which ships the same
recovery gated on the 0x80000003 sandbox-crash signature.
Adapted to current main: restore/persist route through the
applyZoomLevel funnel (39230d173) so the settings UI Scale control
stays in sync, and JSON writes go through writeFileAtomic.
Follow-up to the salvaged #66803 (@HexLab98):
- Two-strike boot marker: a single mid-boot abort (task-manager kill,
power loss) no longer disables the sandbox — only a second consecutive
abort, or a signature-confirmed GPU/renderer STATUS_BREAKPOINT death,
engages --no-sandbox.
- Version-scoped stickiness: the fallback marker records the app version
and re-probes the sandbox once after an update (new Electron or
installer ACL repair may have fixed the host) instead of degrading
forever. A failed re-probe returns straight to fallback.
- Launch-time icacls repair now runs only when the marker shows a prior
aborted boot (icacls /T recurses the whole install tree — healthy
launches skip it; the installer grants the ACE at install time), and
targets the install dir only. The userData grant is dropped: granting
S-1-15-2-2 RX on userData would expose Hermes sessions/config to every
AppContainer app on the machine.
- Renderer crash-loop recovery (same class as #56726, credit @Sahil-SS9
in PR #57414): a Windows renderer crash loop bearing the breakpoint
exit code gets the same one-shot --no-sandbox relaunch instead of a
dead window; unrelated crash loops keep the sandbox.
- Manual --no-sandbox launches are honored but never made sticky.
Tests: 15/15 windows-sandbox-fallback vitest; full desktop electron
suite 432 passed / 1 skipped.
Hot-path pass following the switch-latency work. Four independent costs,
one theme — work that runs on every boot or every turn but only needed
to run on actual change:
- electron: start the Python backend in parallel with the renderer load
instead of on did-finish-load. The backend cold boot is the dominant
startup cost and was serialized behind Chromium's load; the connection
promise is shared, so the renderer's getConnection() joins the
in-flight boot, and its getBootProgress() pull on mount recovers any
progress events emitted before the renderer was listening.
- boot/soft-switch: after the socket connects, run the independent
post-connect fetches (cwd seed, config, session lists) concurrently
instead of serially — profile adoption still lands first because the
session fetch scopes by it.
- session.info: config refetch is now gated to the foreground context
and coalesced (one trailing fetch per event burst) — it used to fire
two REST calls per event, including background sessions' heartbeats.
model-options invalidation now requires a VALUE change vs the
session's cached runtime state; the backend stamps model/provider on
every event, so the presence-typed flags refetched the provider
catalog once or twice per turn for a model that never changed.
- turn complete: sidebar refreshes (recents + cron + messaging fan-out,
each scanning profile state.dbs server-side) coalesce across
near-simultaneous completions; $sessions and profile totals keep
their identity when a refresh returns content-identical rows (same
signature gate cron/messaging already use), and the loading flag no
longer flickers over a populated list.
node-pty's published npm tarball ships the POSIX `spawn-helper` with mode
0644 (no exec bit). node-pty `posix_spawnp`s that helper on macOS/Linux, so a
non-executable copy fails every embedded-terminal spawn with
`Error: posix_spawnp failed.`. Packaged builds are unaffected because
stage-native-deps.mjs chmods the staged copy, but the dev flow
(`npm run dev` -> `electron .`) resolves node-pty straight from node_modules,
which nothing chmods -- so the first terminal in dev always dies.
Restore the exec bit once, lazily, right before the first spawn, via a small
DI-testable helper. Idempotent: already-executable copies (packaged builds)
are left untouched, and stat/chmod failures are collected and logged rather
than thrown so terminal startup never breaks.
* fix(desktop): trust Windows system CAs for remote gateways
Load Windows-trusted roots into Node's default TLS context before Desktop probes remote backends, while preserving bundled and extra CAs.
* test(desktop): cover Windows system CA installation
Verify existing trust roots survive the merge and that unsupported or unavailable stores fail open without changing TLS defaults.
Reassert the persisted webContents zoom after BrowserWindow moved, covering Windows monitor transitions where Chromium recalculates display scaling and drops the user-selected zoom.
Rebased onto current main, where the electron test harness migrated from
node:test to vitest (test:desktop:platforms = `vitest run --project electron`,
auto-discovering electron/*.test.ts). Swap the node:test import for vitest and
drop the .ts-extension import hack; the obsolete package.json node --test list
edit is dropped in the cherry-pick resolution since vitest auto-discovers.
Co-authored-by: Gille <4317663+helix4u@users.noreply.github.com>
hermes update is a Python CLI writing to a pipe when the Tauri updater or
the desktop's in-app POSIX path spawns it, so CPython block-buffers stdout.
Long quiet steps stream nothing to the progress UI. Worst case is the
pre-update backup (updates.pre_update_backup: true): it can zip multi-GB
archives for minutes while the updater still shows the previous line
('waiting for Hermes to exit...'). Users read that as a hang, cancel a
healthy update, and the orphaned child keeps mutating the install.
Set PYTHONUNBUFFERED=1 in both spawn sites (update_child_env in the Tauri
updater, applyUpdatesPosixInApp in the desktop) so output streams line by
line.
Also make the lock-probe unit test pass on macOS: the packaged payload
lives under Contents/Resources there, and Path::ends_with is
case-sensitive, so the lowercase resources/app.asar assertion only ever
matched the Windows/Linux layouts.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.
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.
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).
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>
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 }.