Commit Graph

323 Commits

Author SHA1 Message Date
Brooklyn Nicholson 80c86c4949 fix(desktop): make the Windows app icon match native icon size
The shipped artwork bakes in the macOS-style ~10% transparent margin
(content covered only ~80% of the canvas), so the taskbar/titlebar icon
rendered visibly smaller than neighboring Windows apps, which draw
full-bleed.

- Regenerate assets/icon.ico full-bleed (~95% coverage) from the same
  art, with the standard 16-256px frames. This feeds both the exe stamp
  (set-exe-identity via rcedit) and the installer.
- On Windows, resolve the BrowserWindow icon from the full-bleed ico
  (resources/icon.ico, shipped via extraResources) before falling back
  to the padded apple-touch PNG.

macOS is untouched: the dock icon and icon.icns keep the padded art,
which is correct there.
2026-08-01 01:26:46 -05:00
Kewe63 16e66e721f fix(desktop): break renderer-led reinstall loop on transient backend stalls
Issue #74874. The renderer's 'Repair' button treated every transient
backend GIL stall (event loop stalled ... ws ready frame send failed)
as a fatal backend fault, asking the bootstrap to force-reinstall +
restart, which then stalled again for the same reason — looping the
user through 30+ minutes of reinstall cycles.

Distinguish 'venv is genuinely broken' from 'backend is just transiently
stalled' before honouring a repair request. Probe the live backend
process (exitCode === null && signalCode === null) and an in-flight
repair-attempt counter:

  attempt <= 3 AND primary alive   → soft restart (skip installer)
  attempt <= 3 AND primary dead    → soft restart (verify before reinstall)
  attempt >  3                    → hard reinstall (escalate)

Counter resets on a clean backend.ready so a later, unrelated failure
episode starts at attempt 1. The guard is a pure helper (decideBootstrap
Repair in electron/bootstrap-repair-guard.ts) so the decision logic is
unit-tested in isolation; main.ts only wires the existing flag and
counters to it.

Refs #74874
2026-07-31 22:33:53 -07:00
zakhounet 0ee9723b52 fix(desktop): make the non-Windows updater bypass an explicit policy
resolveUpdaterBinary() picked up a staged hermes-setup on every platform, so a
macOS binary predating the update hand-off protocol took over the update, held
the marker, and had its `hermes update` child refuse its own parent. The in-app
Update button then failed for good, with no route -- update, re-download or
reinstall -- back to a capable binary (#74836).

Move the decision into a pure resolveStagedUpdaterBinary() helper in
updater-process.ts and return null off Windows. The installer self-copies into
HERMES_HOME on every platform (paths::installer_dest,
bootstrap::copy_self_to_hermes_home), so finding that binary on macOS or Linux
is expected rather than leftover junk: declining to hand it an update is a
policy decision, and the comments now say so instead of describing the binary
as Windows-specific.

Cover the resolver in updater-process.test.ts: Windows accepts a staged
hermes-setup.exe, macOS/Linux return null even when hermes-setup exists, and
Windows returns null when nothing is staged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 22:33:53 -07:00
chenbin d649973751 fix(desktop): skip hermes-setup binary on macOS/Linux
resolveUpdaterBinary() returned a path on macOS if hermes-setup existed
in HERMES_HOME, routing macOS into the Windows-style quit→hand-off→rebuild
update dance. A stale hermes-setup (e.g. from 2026-06-08, predating the
applyUpdatesPosixInApp path) permanently breaks the in-app Update button.

The Tauri hermes-setup binary is a Windows-specific mechanism; macOS and
Linux use applyUpdatesPosixInApp instead. Always return null on non-Windows
so the native drag-and-drop updater is used on those platforms.

Fixes #74836
2026-07-31 22:33:53 -07:00
teknium1 087b2230c4 fix(desktop): accept scheme-less host:port in the remote gateway URL field
Users pasting a Tailscale IP or LAN host as 'host:port' (no http://) hit
either a hard 'URL is not valid' error in the main process or, worse, a
silent dead probe in the renderer: the ^https?:// gates in the settings
and first-run forms never fired, so the field sat idle with no feedback.

- normalizeRemoteBaseUrl() (electron/connection-config.ts) now prepends
  http:// when the input has no scheme:// prefix; explicit non-http
  schemes (ws://, ftp://) still reach the protocol check and get a clear
  rejection.
- New renderer twin coerceRemoteUrlScheme() (src/lib/remote-url.ts),
  wired into both probe gates (gateway-settings.tsx and
  first-run-remote-form.tsx) so the debounced /api/status probe, sign-in,
  test, and save all see the coerced URL.
- Tests for both sides (electron/connection-config.test.ts,
  src/lib/remote-url.test.ts).
2026-07-31 22:28:43 -07:00
hermes-seaeye[bot] 17e5f7244a fmt(js): npm run fix on merge (#75582)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-31 18:54:27 +00:00
hermes-seaeye[bot] daa1befaf6 fmt(js): npm run fix on merge (#75517)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-31 16:22:58 +00:00
Doud-FR eb08467a7a fix(desktop): redact gateway credentials from token logs 2026-07-31 10:16:21 -04:00
Doud-FR 3553c1b313 fix(desktop): reject empty encrypted token payloads 2026-07-31 10:16:21 -04:00
Doud-FR 6cb459af9e fix(desktop): harden native token store handling 2026-07-31 10:16:21 -04:00
Doud-FR 61d8be5cb0 test(desktop): cover native OAuth persistence path 2026-07-31 10:16:21 -04:00
Doud-FR f15c4db4c8 fix(desktop): log native token decryption failures 2026-07-31 10:16:21 -04:00
Doud-FR a3618c2b22 fix(desktop): preserve non-Error OAuth load failures 2026-07-31 10:16:21 -04:00
Doud-FR ca7659a86c test(desktop): guard native OAuth parser boundary 2026-07-31 10:16:21 -04:00
Doud-FR df1f825ce7 fix(desktop): restore native OAuth tokens after restart 2026-07-31 10:16:21 -04:00
Brooklyn Nicholson 044800e358 perf(desktop): stretch backstop polls while on battery
powerMonitor's AC/battery state is mirrored to the renderers
(store/power.ts) and visiblePoll quadruples its cadence on battery. Only
the safety-net refreshes slow down — event-driven refreshes and live
streaming are untouched.
2026-07-31 00:38:11 -05:00
Brooklyn Nicholson be7c4b8fe7 perf(desktop): let the hidden link-title window throttle
It loads arbitrary user-linked pages offscreen; unthrottled, a heavy page
burns full CPU for the window's whole lifetime. Title resolution rides
load events and main-process timers, which throttling doesn't touch.
2026-07-31 00:38:11 -05:00
Brooklyn Nicholson 8ccb4c2cee perf(desktop): scope background-throttling opt-out to live streaming
The process-wide disable-background-timer-throttling /
disable-backgrounding-occluded-windows switches plus a static
backgroundThrottling: false on every chat window pinned each renderer's
document.visibilityState to 'visible' for the life of the window. Every
visibility-gated backstop poll and clock tick in the renderer became an
always-on timer: an idle, minimized Hermes burned ~20% CPU around the
clock, on battery too.

Throttling is now a runtime dial. A small controller (stream-throttle.ts)
rides the merged hermes:active-work reports the quit guard already
receives: while any turn is in flight every chat window gets
setBackgroundThrottling(false) — a live answer keeps painting while
blurred, occluded, or minimized, exactly as before — and once all turns
settle (plus a 5s trailing window so the final flush lands at full
cadence) Chromium's default throttling returns and hidden windows go
quiet.

disable-renderer-backgrounding stays: process priority only, no timer
semantics, and it keeps hidden streaming fast.
2026-07-31 00:38:11 -05:00
Brooklyn Nicholson dba7bef5ce fix(desktop): stop right-click showing a lone Select All on bare surfaces 2026-07-30 05:55:30 -05:00
Brooklyn Nicholson ba194c1d19 feat(desktop): File > Open Folder… menu item
No accelerator (⌘O stays a rebindable renderer keybind, matching New
Window's rationale); clicking routes hermes:open-folder-requested through
the preload bridge to the same openFolderAsProject flow.
2026-07-30 01:15:01 -05:00
kshitijk4poor 472658d014 fix(desktop): pass shell to serve probe; bound Windows discovery probes
Two residual gaps from the #72707/#72632 probe-hardening series:

- backendSupportsServe never forwarded backend.shell to the serve
  --help probe. A .cmd/.bat shim backend (which carries shell: true in
  its step-4 descriptor) makes execFileSync throw EINVAL on modern
  Node; the bare catch then caches supported=false for the process
  lifetime, permanently routing that backend through the legacy
  dashboard form. Forward the flag.

- The Windows python-discovery probes ran with no timeout at all:
  reg query (registry read) and py.exe -c 'import sys;...' (bare
  interpreter startup) are both synchronous execs on the boot path;
  a wedged reg.exe or python.exe would hang the resolver forever.
  Bound reg query at 5s and the py.exe probe at the shared
  PROBE_TIMEOUT_MS budget.
2026-07-29 17:09:25 +05:30
hermes-seaeye[bot] 022a175e0a fmt(js): npm run fix on merge (#74057)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-29 11:32:21 +00:00
kshitijk4poor 74f8e59877 fix(desktop): widen probe-budget fix to sibling boot-path probes
Follow-up to #73907 (probe timeout 15s + env override + timeout-only
retry), widening the same fix to the two sibling sites it missed:

- backendSupportsServe's serve --help probe kept a bare execFileSync
  with its own 15s literal: same cold-Windows Python-startup class
  (#72632 measured ~10.5s for --version cold), no retry, and a false
  negative is cached for the process lifetime - silently routing a
  modern runtime through the legacy dashboard form. Route it through
  execProbeSync with the shared PROBE_TIMEOUT_MS (honours
  HERMES_PROBE_TIMEOUT_MS) and the timeout-only retry.

- resolveHermesBackend step 4 called unwrapWindowsVenvHermesCommand
  twice; the second call re-ran the same un-memoized import probe,
  costing up to another full probe timeout on a hung interpreter for
  an answer the first call already gave. Drop the redundant re-probe.

Part of the #72707 bug class (transient disconnect must not strand a
healthy install).
2026-07-29 16:52:53 +05:30
hermes-seaeye[bot] 015718066a fmt(js): npm run fix on merge (#73999)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-29 08:37:16 +00:00
kshitijk4poor f199c4c92e test: update boot handshake test for synchronous warmup (#73083)
The old test asserted _warm_gateway_module was fire-and-forget (startup
completes in << SLOW_SECONDS). PR #73291 intentionally reversed this:
the import now runs synchronously before the lifespan yield because
run_in_executor didn't release the GIL on Windows + Python 3.11.
Updated the test to assert startup blocks for >= SLOW_SECONDS.
2026-07-29 13:59:11 +05:30
Bartok9 a7925bdd49 fix(desktop): raise backend probe timeout and retry on timeout
Closes #61764

Root cause: PROBE_TIMEOUT_MS=5000 false-negatives healthy Windows cold
starts (AV/disk), so launcher runs hermes-setup --update forever.

Fix:
- Default timeout 15s (HERMES_PROBE_TIMEOUT_MS override, max 120s)
- One automatic retry on timeout only
- Tests for default + env resolution

Salvage of closed #61781/#61956 with env override + timeout retry.

Verification: npx vitest run electron/backend-probes.test.ts (12 passed)
2026-07-29 13:59:11 +05:30
hermes-seaeye[bot] 7d3075d0d5 fmt(js): npm run fix on merge (#73960)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-29 07:13:20 +00:00
Teknium d60a2eb3b4 fix(desktop): gate backend respawns on updateInFlight, not just the update marker (#73822)
On Windows, applyUpdates kills its own backend (releaseBackendLock)
BEFORE the venv-blocker preflight but only writes the on-disk update
marker AFTER the scan. Killing the backend drops the renderer's
WebSocket; the renderer reconnects within ~1s and the marker-only
waitForUpdateToFinish gate happily spawns a fresh 'hermes serve' inside
the update's own critical section. scanVenvBlockers then finds that
brand-new process and aborts with 'another Hermes process is using this
installation' — a different PID on every attempt, so Desktop self-update
can never succeed.

Fix: extract the gate into update-gate.ts (pure, DI-testable) and make
it consult BOTH signals — the on-disk marker AND the in-process
updateInFlight flag. The success path writes the marker before the flag
clears in applyUpdates' finally, so there is no instant where both are
false and a waiter can slip through. Also gate spawnPoolBackend, which
previously had no waitForLocalStart at all — a background profile window
could respawn a pool backend during the same window with the identical
abort.

Tests: update-gate.test.ts covers the open gate, the flag-only window
(the #73822 shape), the flag→marker handoff with no gap, and timeout.
2026-07-28 23:53:25 -07:00
Teknium eaecca4a71 fix(desktop): widen local plugin-root fix — profile-aware root + dir-watch path
Follow-ups on top of #66911's salvaged commit:
- hermes:fs:desktopPluginsRoot now resolves the ACTIVE desktop profile
  (readActiveDesktopProfile) so named profiles keep their own
  profiles/<name>/desktop-plugins root instead of sharing the global one
  (profile-scope concern raised on the PR thread).
- startDirWatch in runtime-loader.ts was a third sibling site still
  deriving the watch path from the backend's hermes_home (added by the
  later fs-watch commit); routed through the same Electron-local
  resolver, with regression coverage.
2026-07-28 23:53:20 -07:00
PRATHAMESH75 e614876c63 fix(desktop): resolve local plugin root independent of remote backend
The Settings "Open plugins folder" action and the runtime disk-plugin
scanner both derived the plugin directory from getStatus().hermes_home.
Against a remote backend that value is a path on the REMOTE box (or
undefined), producing `undefined/desktop-plugins` — the folder action
errors ("Could not open the plugins folder undefined") and disk-plugin
discovery silently finds nothing, even with a valid local plugin.js.

Add an Electron-owned IPC resolver (hermes:fs:desktopPluginsRoot) that
returns <HERMES_HOME>/desktop-plugins computed from the main-process
HERMES_HOME — the local Electron path, valid in every connection mode —
creating it on demand. Route both the Settings folder action and the
runtime scanner through it, so a remote backend never determines the
local filesystem location used for Desktop runtime plugins.

Fixes #66899
2026-07-28 23:53:20 -07:00
Teknium 7e7f7d3059 Merge pull request #70509 from NousResearch/hermes/hermes-29661bf6
feat(voice): on-device wake words with open-vocabulary phrases and multi-profile voice routing
2026-07-28 17:58:33 -07:00
brooklyn! 392f1b9477 Merge pull request #73705 from NousResearch/bb/terminal-clipboard
feat(desktop): copy and paste in the GUI terminal
2026-07-28 19:58:31 -05:00
Brooklyn Nicholson 254aeda122 fix(desktop): dedicated reader for large remote attachments
Keep Settings-configurable preview/image loads on readFileDataUrl, and
route remote non-image attach through a 256 MiB IPC so uploads are not
stuck on the 16 MiB default after #73221.

Co-authored-by: Börje <borje@dqsverige.se>
2026-07-28 19:20:14 -05:00
Brooklyn Nicholson 807dc0c45f feat(desktop): copy and paste in the GUI terminal
xterm paints to a canvas, so its selection is not a DOM selection: the Edit
menu's Copy and the right-click Copy both call webContents.copy(), find
nothing, and copy nothing. On macOS the menu also swallows the Cmd+C
accelerator before the renderer sees it. That left Ctrl+C as the only key that
did anything, and xterm correctly forwards it to the PTY as SIGINT.

Mirror the selection into xterm's hidden helper textarea (the mechanism xterm
already uses for Linux middle-click paste) so the OS sees a real selection and
every platform copy path works, and add explicit chords on top: Cmd+C/Cmd+V on
macOS, Ctrl+Shift+C/V elsewhere, matching VS Code. Plain Ctrl+C copies only
when text is selected -- the behavior Windows Terminal and Tabby ship -- and
stays SIGINT otherwise, so interrupting a process never breaks.

Reads go through a new hermes:readClipboard IPC handler for the same reason
writes already do: the renderer's clipboard API throws whenever the document
isn't focused.
2026-07-28 19:15:04 -05:00
Brooklyn Nicholson c14ae3796e feat(desktop): fs-watch the plugin dir + demote the status snapshot
- watchDirectory IPC (same registry/channel as the preview file watchers)
  replaces the disk-plugin door's 5s readdir poll; older shells without
  the capability keep the poll, which self-upgrades to the watch once the
  plugins dir exists.
- Status snapshot: 15s → 60s, skips round-trips while hidden, and
  refreshes immediately on visibilitychange so re-focus never shows stale
  health.

Part of #73618.
2026-07-28 17:21:58 -05:00
hermes-seaeye[bot] 66e20786d0 fmt(js): npm run fix on merge (#73666)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-28 21:55:38 +00:00
Teknium 03af12c5c9 style(desktop): blank line between node: and vitest import groups
perfectionist/sort-imports requires spacing between builtin and
external import groups.
2026-07-28 14:47:36 -07:00
Teknium 20e0b26c4e test(desktop): run venv-blocker-scan tests under vitest
CI's electron vitest project collects electron/**/*.test.ts; the
node:test-based suite reported 'No test suite found'. Import
describe/it from vitest like sibling electron tests.
2026-07-28 14:47:36 -07:00
Teknium 1dd1c449bb fix(desktop): run venv-blocker scan async off the main-process event loop
The preflight's execFileSync froze the Electron UI event loop for up to
15s while psutil scanned the full process table. Convert
scanVenvBlockers to async execFile and await it in applyUpdates; tests
updated to async DI stubs. Also adds trailing newlines.
2026-07-28 14:47:36 -07:00
atakan g d210500b54 fix(desktop): surface external venv update blockers 2026-07-28 14:47:36 -07:00
Teknium f5ff8fa4f8 Merge remote-tracking branch 'origin/main' into wake-toggle-config 2026-07-28 13:24:23 -07:00
brooklyn! c8cdeb435f fix(desktop): allow renderer camera capture (#73558)
* fix(desktop): allow camera capture through the permission handlers

The session permission hooks were written for the voice composer and denied
video outright, so any renderer getUserMedia({video}) failed with
NotAllowedError before the OS was ever consulted.

Rename isAudioCapturePermission to isMediaCapturePermission and accept video
alongside audio in both the request and check handlers. The OS capture
permission still applies, so the user keeps a real allow/deny.

* build(desktop): declare camera usage for signed macOS builds

A hardened-runtime build needs the camera entitlement and an
NSCameraUsageDescription string, or the packaged app is killed on first
camera access instead of prompting.
2026-07-28 14:50:52 -05:00
Teknium 0cf58de85e Merge remote-tracking branch 'origin/main' into wake-toggle-config
# Conflicts:
#	tests/test_tui_gateway_server.py
#	tui_gateway/server.py
2026-07-28 12:37:35 -07:00
atakan g d4221b2736 fix(desktop): validate WebSocket token before backend ready 2026-07-28 12:35:57 -07:00
Teknium f9d7be82fb fix(desktop): speak the first reply of a wake-started voice session
Diagnosed from a Windows desktop's logs: the backend TTS was generating
and saving the reply mp3 every turn (provider: openai), so synthesis was
fine — the DESKTOP wasn't playing the first reply. Root cause is Chromium's
autoplay policy: audio (HTMLAudioElement.play() and AudioContext) is
suspended until the frame sees a user gesture. A voice conversation started
by the 'Hey Hermes' wake word has no preceding click, so the first reply's
play() was rejected with NotAllowedError (silently swallowed) and only
turn 2+ spoke. Manual voice-start worked because the button click WAS the
gesture — exactly the 'first message in a new voice session is silent,
manual start is fine' report.

Fix (defense in depth):
- chatWindowWebPreferences sets autoplayPolicy: 'no-user-gesture-required'
  on every chat window (primary + secondary), so a deliberately-launched
  native app never gates audio on a gesture. Primary fix.
- voice-playback.ts resumes a suspended AudioContext on stream 'start' and
  retries HTMLAudioElement.play() once via an unlock context after a
  NotAllowedError — covers the dashboard-embedded surface that doesn't get
  the Electron policy.

Tests: 16 electron session-window tests (incl. new autoplay assertion),
tsc + eslint clean.
2026-07-28 12:01:27 -07:00
Brooklyn Nicholson eaf30a4de7 build(desktop): declare camera usage for signed macOS builds
A hardened-runtime build needs the camera entitlement and an
NSCameraUsageDescription string, or the packaged app is killed on first
camera access instead of prompting.
2026-07-28 13:40:44 -05:00
Brooklyn Nicholson 47cb4ea1fe fix(desktop): allow camera capture through the permission handlers
The session permission hooks were written for the voice composer and denied
video outright, so any renderer getUserMedia({video}) failed with
NotAllowedError before the OS was ever consulted.

Rename isAudioCapturePermission to isMediaCapturePermission and accept video
alongside audio in both the request and check handlers. The OS capture
permission still applies, so the user keeps a real allow/deny.
2026-07-28 13:40:44 -05:00
hermes-seaeye[bot] 2faac36866 fmt(js): npm run fix on merge (#73552)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-28 18:30:19 +00:00
Adolanium b5dc471152 feat(desktop): make attachment data-URL size limit configurable (#73221)
Hard 16 MB cap on readFileDataUrl blocked larger local attaches with no way to raise it. Settings -> Chat now has a free-form MB field. Main process owns the persisted value and clamps only absurd inputs.
2026-07-28 09:10:21 -04:00
brooklyn! 7ddf3f1500 Merge pull request #73161 from NousResearch/bb/default-zoom-out
feat(desktop): default UI zoom to Appearance 90% preset
2026-07-28 04:08:15 -05:00