Commit Graph

3639 Commits

Author SHA1 Message Date
686f6c61
961635c19c fix(desktop): copy unsafe RPC rejections instead of mutating name
asRpcError now always wraps a non-string name in a fresh Error. In-place
assignment was a silent no-op on sealed objects in sloppy mode. Catch
only host.request / requestProfile so routing TypeErrors keep their stack.
2026-08-25 16:25:41 -07:00
686f6c61
a837c7aaa2 fix(desktop): coerce bot RPC rejections for React 19 error formatting
JSON-RPC/IPC can reject with a plain object whose name is a number.
React 19 then crashes on (error.name || '').trim, which takes down the
Routines pane instead of showing the cron.manage failure. requestForBot
now wraps those values in an Error with a string name, including
cross-realm Error-like objects from the plugin test vm.
2026-08-25 16:25:41 -07:00
Finn763
936a6ea8f7 fix(desktop): scope Cronjobs pane to the roster-clicked bot when the focused session has no owner (#94516)
The SDK's focusedSessionOwner store fails closed to null whenever the
focused session has no unique bot owner (a normal chat, ambiguous owner
hints) - the common case while the user browses the Bots pane.
resolveRoutineOwner treated that null as an error and returned null
before consulting the roster selection, so the Routines pane pinned
every agent on 'Cronjobs are unavailable until this agent appears in
the roster.'

Drop the fail-closed null gate and fall through to the existing
selection ladder (focusedBot || selectedBot || ...). An authoritative
focused owner still wins through its exact roster row and still fails
closed when that row is absent; a null owner with no matching selection
also still fails closed. Regression tests prove red pre-fix, green
post-fix.
2026-08-25 16:24:54 -07:00
Brooklyn Nicholson
30b042e137 fix(desktop): clear the JS/TS check failures on the Browser pop-out
Import order, an unused test import, and the session-menu windows mock now that session-states reads isBrowserWindow.
2026-08-25 18:23:43 -05:00
Brooklyn Nicholson
e8df401738 feat(desktop): hide the docked Browser tab while it is popped out
The live page stays in one window. The pop-out renderer skips shared layout/tile/unread writes so it cannot wipe the primary.
2026-08-25 18:23:43 -05:00
Brooklyn Nicholson
a608799689 feat(desktop): give the in-app Browser its own OS window
A ?win=browser renderer (query before hash) so HashRouter cannot swallow the flag, and so one window per tab can focus instead of cloning.
2026-08-25 18:23:43 -05:00
Brooklyn Nicholson
26777a4178 feat(desktop): open a Browser tab in the default browser from its context menu
Right-click the tab to hand its current page to the OS browser. about:blank
and other non-pages keep the row but disable it.
2026-08-25 16:43:54 -05:00
Brooklyn Nicholson
b9513f2e1d feat(desktop): let a pane prefix the zone tab menu
A Browser tab needs one extra verb without replacing Reload / Close. Give
the pane a tabMenuPrefix slot the strip menu renders when it opens.
2026-08-25 16:43:54 -05:00
Teknium
a87e8831f7 style(desktop): sort secret-storage-policy import per perfectionist lint 2026-08-25 14:10:19 -07:00
Teknium
6a6e16fa5d feat(desktop): OS-keychain encryption for stored secrets is now opt-in — no more macOS Keychain password prompt on every launch
Electron safeStorage parks a per-app key ('Hermes Key') in the macOS login
keychain; on machines with a locked/missing/corrupted default keychain that
turned every Hermes Desktop launch into a blocking 'Keychain Not Found' /
password dialog. Keychain-backed encryption is now an explicit opt-in:

- electron/secret-storage-policy.ts: standalone policy seam (default OFF,
  strict === true coercion, one-shot migration flag) + unit tests
- default path never calls any safeStorage API (including
  isEncryptionAvailable, which itself touches the keychain)
- one-shot legacy migration decrypts existing safeStorage blobs to plain
  0600 files at first launch; undecryptable blobs are kept but read as
  absent afterward (classify 'drop') so a dead keychain prompts at most once
- Settings -> Gateway toggle (all 5 locales) re-encodes every stored secret
  store in place when flipped (v1 connection.json, v2 connections.json,
  native-oauth-tokens.json)
- e2e: at-rest spec now covers both postures (opted-in unchanged contract,
  default saves without secure storage, owner-only bits, restart round-trip)
- docs: multi-connection-desktop + desktop-native-signin updated
2026-08-25 14:10:19 -07:00
Brooklyn Nicholson
02c7ae956e fix(desktop): route session list REST through the active profile
Sidebar and legacy session-list helpers tagged the registry connection but
not the active profile, so Electron routed those reads to the wrong backend
after a profile or remote switch.

Keep hermesApi connection-only: stamp profileScoped on the list helpers
instead of every REST call.

Co-authored-by: noah <loahnisk@gmail.com>
2026-08-25 12:07:00 -05:00
Brooklyn Nicholson
90ee4460cb fix(desktop): translate sidebar recents_profile through SSH aliases
Managed SSH maps a Desktop profile label onto a different remote name.
The sidebar filter lives in recents_profile, so rewriting only ?profile=
left those reads on the remote default and the Sessions list came back empty.

Co-authored-by: noah <loahnisk@gmail.com>
2026-08-25 12:07:00 -05:00
Teknium
76e306c458 refactor(tools): remove expired BFL FLUX 3 promo core tools (migration v39); FLUX 3 stays via video_gen/FAL for subscribers (#94599)
* refactor(tools): remove expired bfl_flux3_* promo tools; FLUX 3 rides the video_gen provider surface

* test: relay-cutover migration asserts >= v38, not the version literal
2026-08-25 02:45:10 -07:00
Teknium
c1b295d003 test(desktop): stop the syntax-diff mock factory from leaking unhandled rejections (#94415)
The diff-lines error-boundary test mocked './syntax-diff' with a factory
that THREW. vitest hoists the factory and registers its module promise in
the mocker registry; a throwing factory leaves rejected promises there,
and under CI load one escapes as "Vitest caught 1 unhandled error during
the test run" attributed to whichever sibling file the worker is running
(user-message-edit.test.tsx in run 32803716726) — an intermittent js-tests
red on green code.

Rework: the factory now resolves to a component that throws the fetch
error during render — the same way React surfaces a rejected lazy payload
— so no rejected promise ever sits in the registry.

Guard proof (sabotage A/B): with the local syntax-diff ErrorBoundary
removed from diff-lines.tsx, the reworked test still fails (workspace
fallback renders), so the #93479 regression pin is intact. Full ui
project: 596 files / 5729 tests green, no unhandled errors in 3 full-run
repetitions.
2026-08-24 21:45:44 -07:00
hermes-seaeye[bot]
7c97343950 fmt(js): npm run fix on merge (#94410)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-25 03:17:26 +00:00
Teknium
beb7941236 fix(tui-gateway): make WS reconnect replay actually deliver events (follow-up to #94219)
The #94219 replay was a production no-op: the server returned full
JSON-RPC envelopes from session.events.since while the client's replay
loop dispatches only elements with a top-level 'type' — every replayed
event was silently skipped. Each side's tests validated its own
assumption, so both suites stayed green.

- server: events_since() now returns bare event objects (the frame's
  params), the exact shape the live dispatch path consumes; ring stores
  params directly; cross-language contract test added on both sides.
- client: live frames racing an in-flight replay are parked and flushed
  seq-gated afterward — no double dispatch of deltas, no gap-skip from
  a watermark advanced past the replay window.
- restart poisoning: seq counters are in-process, so a backend restart
  reset them while clients kept high watermarks (replay forever empty,
  truncated=false). New replay_epoch advertised in gateway.ready and
  echoed by session.events.since; the client clears watermarks on epoch
  change.
- methods_session no longer reaches into event_replay privates
  (is_truncated() accessor).

Live repro: pre-fix, 3 stamped frames -> 0 dispatchable by the client
gate; post-fix 3/3. Tests: 16 py (replay+ws), 8 vitest, tsc clean, ruff
clean.
2026-08-24 20:11:30 -07:00
Teknium
a75ea37dc5 feat: browser snapshots drop LLM summarization — truncate-and-store like web_extract; auxiliary.web_extract slot removed
web_extract stopped using an auxiliary LLM long ago (deterministic
truncate-and-store), but browser snapshots still routed oversized
accessibility trees through the auxiliary web_extract model, keeping a
dead-looking aux slot alive across every config/picker surface.

- tools/browser_tool.py: remove _extract_relevant_content and
  _get_extraction_model; oversized snapshots always truncate at line
  boundaries, store the full tree to cache/web, and append a read_file
  pointer (element refs beyond the cut live in the file)
- tools/browser_camofox.py: same — no LLM path
- Remove auxiliary.web_extract slot: config_defaults (removal note, same
  pattern as session_search/PR #27590), cli.py defaults + env bridge,
  gateway/run.py bridged keys, hermes config display, hermes model picker,
  dashboard REST slots, desktop + web AUX_TASKS, i18n labels (en/zh/
  zh-hant/ja/ar)
- Docs: env-vars, configuration, fallback-providers, browser + zh-Hans
  mirrors (web-search zh-Hans was stale on the old LLM pipeline — synced
  to truncate-and-store truth)
- Tests updated: aux bridge uses approval slot, browser tests assert the
  LLM path is gone and stored files are secret-redacted
2026-08-24 20:11:18 -07:00
Brooklyn Nicholson
f8b52e4d80 test(desktop): cover UI scale across recordless hash routes
Drives the reported path rather than the helper: set a non-default
scale, then navigate to routes Chromium holds no zoom record for, which
is what opening a new session looks like to the per-URL store. Keeps the
Cmd/Ctrl+N case alongside it.

Co-authored-by: Clark Vines <38430798+clarkvines@users.noreply.github.com>
2026-08-24 22:01:47 -05:00
Clark Vines
b637ee0fc6 fix(desktop): keep UI scale across in-page route navigation
Desktop is a HashRouter over one file:// document, so every route is a
distinct URL to Chromium's per-URL zoom store. A route the user never
zoomed on has no record at all and resolves to the host default (100%) —
that is every fresh session and every never-visited settings tab.

In-page navigation fires neither did-finish-load nor any window event,
so nothing re-asserted the persisted level. The window dropped to 100%
while the Appearance control kept reading the chosen scale, because the
renderer only learns of zoom changes through 'hermes:zoom:changed',
which never fired. Touching the setting sent a fresh apply, which is
why it appeared to fix itself.

Re-assert the persisted level on main-frame did-navigate-in-page.
Verified on real Electron 40.10.2 / Chromium 144 (win32): a recordless
hash route reports 100% at the event, so the existing drift-guard sees
the drop and re-applies, and still no-ops when the route's record
already matches.

Fixes #48658
Fixes #38854
Fixes #79863

Co-authored-by: Brooklyn Nicholson <brooklyn.bb.nicholson@gmail.com>
2026-08-24 22:01:47 -05:00
Chen Jin
5400fb88e5 fix(desktop): stop gating edit-menu Paste on the clipboard probe (#91553)
The dom context menu disabled Paste unless a renderer-side
readClipboard() probe reported text when the menu opened. The items
action never consumes that probe: editableCommand("paste") dispatches
webContents.paste() in main - the same Chromium path Ctrl+V takes, which
resolves the system clipboard itself. On Windows the Win32
clipboard.readText() bridge can return empty while that path succeeds,
so Paste stayed grayed out even though pasting would have worked; probe
errors were swallowed the same way (.catch(() => undefined)).

Fail open instead: drop the gate and the now-unused clipboardHasText
fact from the dom menu shape, so opening an editable menu no longer
makes the IPC round-trip at all. Pasting with an empty clipboard is a
harmless no-op, matching Chromiums own menu, which keeps Paste enabled
for editables. The terminal paste item keeps its gate - its action
inserts the readClipboard() text into the PTY directly, so there the
probe and the action share one mechanism and the gate stays honest.

Fixes #91553
2026-08-24 19:59:20 -07:00
Gille
e3b5512b7b fix(desktop): keep modal context menus inside dialogs 2026-08-24 19:46:04 -07:00
hermes-seaeye[bot]
c86612ef82 fmt(js): npm run fix on merge (#94346)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-25 01:58:24 +00:00
Teknium
b85032fc7d test(desktop): pin vibe-hearts toggle across the pet-overlay forward path
The overlay window's playVibeHearts() only fires on a reaction forwarded by
burstVibeHearts, so the single gate covers it — these tests pin that so a
future direct caller shows up as a red test.
2026-08-24 18:52:11 -07:00
Adolanium
93acc22a9f feat(desktop): add Settings toggle for vibe hearts
Floating affection hearts were always on with no off switch. Message
Reactions in Appearance looks related but only gates message-row
tapbacks. Add a separate Vibe Hearts preference (default on) next to it.
2026-08-24 18:52:11 -07:00
Brooklyn Nicholson
5ef1409f50 fix(desktop): say why window enumeration failed instead of swallowing it
`read_window_below` answers "could not enumerate windows on this system" on
macOS and Windows whatever went wrong, and the three failure paths behind it
discarded their errors — so a report where the HUD could see nothing had no
way to distinguish the module failing to load, the helper failing to spawn,
and the OS answering with nothing. Three different fixes, one sentence.

Enumeration now returns the reason, the tool's error carries it, and the HUD's
game-overlay watch logs it once before it gives up (it retries twice and then
goes quiet forever, which is the other half of why the log said nothing).
Linux keeps its environment-derived advice, which is more actionable than the
raw exception.
2026-08-24 20:15:03 -05:00
Brooklyn Nicholson
321d5c76bb fix(desktop): stop the HUD frosting the window while a turn runs
The frost is the whole window rectangle and the `[data-hud-glass]` scrim is
what makes it readable, but the two ran on different gates: the scrim is
focus-only, while the caller widened the frost to "recent or held" — i.e. for
the whole of a turn. Thinking with the composer unfocused therefore raised a
bare native material with no scrim over it, which on a light theme is a white
slab under the band's unconditionally white ink.

Put the frost back on the scrim's gate, and re-run it on the window's own
focus changes: clicking away to another app fires no focusout, so the scrim
would go while the frost stayed behind.
2026-08-24 20:15:03 -05:00
hermes-seaeye[bot]
03b87d666d fmt(js): npm run fix on merge (#94230)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-24 21:18:54 +00:00
kshitijk4poor
c7577403f8 test: drop unused afterEach import (CI eslint) 2026-08-25 02:38:56 +05:30
kshitijk4poor
87631bd8ae feat(tui-gateway): seq-stamped event replay for lossless desktop reconnect
Server: per-session monotonic seq on every routed event frame, bounded
512-frame replay ring (64 sessions, FIFO eviction), plus two new RPCs —
session.events.since (replay newer-than-watermark, reports latest_seq +
truncated so clients detect gaps) and session.events.stats (telemetry).

Client: per-session seq watermarks recorded from live frames; after any
successful reconnect a fire-and-forget fetchReplay() drains missed events
through the normal dispatch path (recordSeq ignores non-increasing seqs,
so stale replay can never regress a watermark); focus-triggered reconnect
nudge in use-gateway-boot for the Electron unfocused case where macOS wake
skips visibilitychange.

Replay failures are swallowed by design: lossless resume is an upgrade
over the previous lossy reconnect, never a new failure mode.
2026-08-25 02:31:19 +05:30
Brooklyn Nicholson
a99001c3b3 style(desktop): space sibling restore import for eslint 2026-08-24 15:09:19 -05:00
Brooklyn Nicholson
53c4693004 fix(desktop): restore pending_clarify snapshots on activate and resume
Replay single-question and batch snapshots from session.activate/resume,
including locked answers, and extract the helper so the session-actions
god-file is not the only owner of that wire shape.

Co-authored-by: ClintonEmok <54935030+ClintonEmok@users.noreply.github.com>
Co-authored-by: frendo <frendo.wu@gmail.com>
2026-08-24 15:09:19 -05:00
Brooklyn Nicholson
dc998a2d59 fix(desktop): re-arm pending clarify cards in place
A hydrated Ask/clarify row stays complete after session or bot switch, so
the live card never mounts. Re-arm the existing transcript row and keep
the provider tool id instead of appending a duplicate at the tail.

Co-authored-by: frendo <frendo.wu@gmail.com>
2026-08-24 15:09:19 -05:00
Brooklyn Nicholson
e4dac8415f fix(desktop): demote unanswered clarify cards on Stop
Latch the pending card on submit, not on seeing a request, so Stop still
collapses an unanswered question instead of leaving a disabled panel.
2026-08-24 15:06:17 -05:00
Brooklyn Nicholson
ead9d8e3d4 fix(desktop): stop transcript jumps when a turn settles
Clarify remounted as a tool row once session.info flipped running=false, thinking previews collapsed their body, and the duration line grew the footer.
2026-08-24 15:06:17 -05:00
hermes-seaeye[bot]
b9eb37e5eb fmt(js): npm run fix on merge (#94176)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-24 20:04:10 +00:00
xxxigm
85a55e2b30 test(desktop): lock Home new-session detach against stale project cwd
Cover the null-path draft path, Home-scope createBackendSessionForSend,
and openNewSessionTile({ cwd: null }) so the last project folder cannot
leak back into a Home chat.
2026-08-24 14:57:12 -05:00
xxxigm
f3e0cf098b fix(desktop): keep Home new sessions detached from the last project
Home's "+" passes path/cwd null on purpose, but null was falsy and fell
through into resolveNewSessionCwd(), so "New session in Home" (especially
the openTab path while main chat is occupied) still created under the
previous project folder and showed its branch.
2026-08-24 14:57:12 -05:00
Brooklyn Nicholson
cd297653fa refactor(desktop): mint browser tab ids at random rather than by slot
Browser tabs took the lowest free slot, so an id was reused once its tab
closed. That is only safe while every store keyed by the id is wiped on
close — true today, but a discipline rather than a guarantee, and stale
state would resurface under an unrelated tab the day it lapses.

Mint like a terminal does instead: no id is ever handed out twice.
2026-08-24 14:50:10 -05:00
Brooklyn Nicholson
84758ed19d fix(desktop): hold the typed address in the browser bar until the page moves
Committing an address dropped the field back to the url of the page you
were leaving, so typing baby.com over google.com flashed google.com back
before baby.com arrived — and nothing said a load was underway.

The address you asked for now stays in the field until the page actually
lands somewhere (a redirect supersedes it, as it should), and progress
spins inside the field beside it. The pane owns that loading state from
the moment it accepts the address, because the reach probe it runs first
delays did-start-loading.
2026-08-24 14:50:10 -05:00
Brooklyn Nicholson
8a8f74e789 feat(desktop): let the in-app browser hold more than one tab
A URL tab used to be a singleton — every link navigated the one Browser,
so there was no way to keep a page open beside another and the strip's
"+" never appeared next to it.

A Browser tab is now a vessel with its own id: links still land in the
browser you are looking at (an agent opening five pages must not leave
five tabs behind), while the "+" mints another one on request. Tabs name
themselves after the page they are showing, since three tabs reading
"Browser" name nothing.

The "+" itself is now a pane capability rather than a session-only
button, so any pane kind that can make more of itself contributes one.
2026-08-24 14:50:10 -05:00
hermes-seaeye[bot]
a251e87d82 fmt(js): npm run fix on merge (#94140)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-24 19:18:19 +00:00
Brooklyn Nicholson
652f5d740c fix(desktop): sort HUD windowing imports for eslint 2026-08-24 14:11:49 -05:00
Brooklyn Nicholson
d7202d64ef refactor(desktop): derive HUD OS behavior from one windowing profile
Move, ignore-mouse, placement, resize edges, snap, cursor feed, and
overlay promote all read the same Ozone-normalized capabilities instead
of re-deriving linux/Wayland/X11 at each call site.
2026-08-24 14:11:49 -05:00
ethernet
28b758d5ca feat(desktop): make Cmd/Ctrl+L focus the composer from anywhere
The chord previously only acted when a terminal or preview selection
existed. A new bubble-phase window fallback now moves focus to the
composer on an unclaimed press, like the address-bar chord in a browser.

Existing owners keep priority: selection handlers claim the press on the
capture phase, a user-rebound action marks the event handled, and a
focused terminal with no selection keeps Ctrl+L as clear-screen via
composerFocusBlockedBySurface().

The chord matcher moves from the terminal feature to
src/lib/keybinds/chords.ts as isComposerChord: it now has three
consumers and the old name (isAddSelectionShortcut) was wrong at the
composer call site. The fixed panel row view.terminalSelection becomes
view.selectionToComposer because it covers preview selections too.
2026-08-24 12:04:03 -07:00
hermes-seaeye[bot]
74ad422d50 fmt(js): npm run fix on merge (#94046)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-24 17:03:13 +00:00
Brooklyn Nicholson
7e500d2ead fix(desktop): float and pin the HUD on Hyprland
Omarchy tiles the HUD like any other toplevel, so always-on-top and
xdg_toplevel.move never apply. Ask Hyprland to float+pin after map,
trying classic dispatch then Lua for 0.55+ configs.
2026-08-24 11:57:38 -05:00
Teknium
d08f9e14f6 fix(desktop): never kill a healthy backend on a claim probe failure; surface real stderr (#93608)
A start-marker probe failure (Get-Process timing out on a PowerShell 5.1
cold start, #87169) in claimBackendChild used to stop the freshly spawned
backend and rethrow — killing a healthy backend, triggering the renderer's
repair respawn, and looping. And because stderr piping only attached after
the claim, every before-ready failure surfaced as a bare exit code.

- extract probe + claim policy into electron/backend-claim.ts:
  processStartMarker/execText (moved verbatim from main.ts), probeStartMarker,
  and a pure claimDecision(childAlive, probe) a Windows CI lane can drive
  with real PowerShell
- probe failure + LIVE child now degrades to PID-only identity
  (pid-only:<pid> marker, WARNING logged), matching the existing
  createParentStartMarkerResolver degrade pattern; processIdentityMatches
  verifies degraded identities by PID liveness (command check still layers
  on top in backendIdentityMatches)
- probe failure + DEAD child keeps the fail-closed throw, now carrying the
  child's buffered stderr/stdout tail
- ring-buffered ~8KB output tail attached at spawn time in BOTH spawn paths
  (pool + primary); tail appended to claim errors, before-ready exit
  messages, and backend-ready's exited-before-port-announcement errors so
  the real exit reason reaches desktop.log and the boot UI
- tests: claimDecision matrix (degrade test fails against the old
  stop+throw behavior), real processStartMarker probe, ring-buffer caps,
  and output-tail suffixes on backend-ready exit errors
2026-08-24 09:55:25 -07:00
howdeploy
81baae6bc3 fix(desktop): polish HUD movement and resizing on X11 2026-08-24 06:48:10 -05:00
Brooklyn Nicholson
d467da9100 fix(desktop): add a HUD layout reset control
A persisted tall/narrow size has no way out on Linux. Put a reset next
to Exit HUD so the default size (and position, where the compositor
allows it) is one click away.

Co-authored-by: Shawn Wang <32839114+enwaiax@users.noreply.github.com>
2026-08-24 06:48:10 -05:00
Brooklyn Nicholson
b595fcd5e1 fix(desktop): keep the Linux HUD clickable and recoverable
X11 cannot restore a window that has ignored the mouse, so stay solid
there. Native Wayland keeps click-through via the cursor poll. Add
desktop.ozone_platform_hint so COSMIC users can opt into XWayland for
always-on-top, and a layout reset that restores the default size.

Co-authored-by: Codex Metatron <47930664+BlakeB254@users.noreply.github.com>
Co-authored-by: DeseretSaint <202557515+DeseretSaint@users.noreply.github.com>
Co-authored-by: Shawn Wang <32839114+enwaiax@users.noreply.github.com>
2026-08-24 06:48:10 -05:00