Commit Graph

2477 Commits

Author SHA1 Message Date
ethernet 3bd844edf1 fix(desktop): make un-highlighted code readable while streaming in light theme
streaming code blocks in the light theme render near-white text on the
white code card until shiki's highlight lands, then snap to normal token
colors. the pale text is @tailwindcss/typography's pre foreground: its
prose theme styles pre as a dark slab (--tw-prose-pre-code = gray-200 on
a gray-800 bg). we strip the bg for our own code card but the near-white
foreground survives on the container. shiki's opaque per-token span
colors normally hide it — it shows through wherever text renders without
spans: the streaming delay window, the lazy-chunk suspense fallback, and
over-budget blocks that never highlight.

traced on the live renderer: computed color on the wrapper of mid-stream
code was oklch(0.928 0.006 264.531) (gray-200), supplied by the
.prose :where(pre) rule.

fix: prose-pre:text-foreground on the markdown container, so every
fenced path inherits the transcript foreground instead. the utility
layer is emitted after typography's base rule in the built css, so the
override wins by order at equal specificity.
2026-08-09 19:29:59 -04:00
hermes-seaeye[bot] e34c4c009a
fmt(js): `npm run fix` on merge (#82771)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-09 22:43:38 +00:00
Teknium aaf0fbfa95 test(desktop-ssh): cover wrapper preservation and explicit-path passthrough in locateHermes
Replaces the canonicalization test (which pinned the behavior #74425
removes) with wrapper-preservation coverage for auto-detection and an
explicit remoteHermesPath, both asserting no python3 -c parser call is
issued. Verified both fail against the pre-fix implementation.
2026-08-09 15:35:25 -07:00
webtecnica 8b752603c4 fix(desktop-ssh): stop resolving exec-wrappers to python in locateHermes (#74411)
Problem 1: resolveLauncher() read bash 'exec <python> <script>' wrappers
and returned ONLY the python interpreter path, discarding the script.
This made probeHermesVersion() run '<python> --version', which always
printed 'Python x.y.z' instead of the Hermes version. And
remoteSupportsSshOwnership() ran '<python> serve --help' which failed
entirely because no 'serve' module exists in the python stdlib.

Problem 2: When the user set remoteHermesPath (an explicit override),
resolveLauncher() resolved it to the python interpreter, replacing the
user's specified path. The override was effectively ignored for version
checking and capability probing.

Fix: resolveLauncher now returns the candidate path directly. The hermes
binary or wrapper script is already executable and handles argument
forwarding (e.g. 'exec <python> <script> "$@"') correctly on its own.
No additional remote SSH round-trip or python script needed.
2026-08-09 15:35:25 -07:00
ethernet c002b6fbe5 fix(desktop): send full tool args so expanded rows show the whole command
The gateway sent only an 80-char preview (context) for a tool call.
The desktop rebuilds the expanded tool row from the args of the part.
When the args were absent, the row showed the preview, and long
commands ended in '...' after the user expanded them.

Two paths had this fault:

- tool.start: the payload had no args until tool.complete, so the
  expanded row was truncated while the tool ran. Now tool.start ships
  the args, the same as tool.complete already does.
- _history_to_messages: the projection read the full arguments, then
  discarded them. Hydration from this projection (watch windows,
  compress, branch, seeded create) kept only the preview, so the
  truncation was permanent. Now tool rows carry the args. This
  projection is the display view of the transcript — each renderer
  decides what to paint, and the preview stays for collapsed titles.

The DB rows do not change: the args already persist in tool_calls.
2026-08-09 17:48:37 -04:00
Teknium 244d296646 fix(personality): single-owner personality state + one-time reset migration
Personality persistence used to be split per surface: the TUI/desktop wrote
the NAME to display.personality while the CLI/gateway wrote rendered TEXT
into agent.system_prompt (and their /personality none only blanked the
text, leaving the name behind). When #81946 made display.personality
authoritative everywhere, stale names written long ago resurrected
personalities users had turned off - kawaii defaulting on after updating.

- hermes_cli/personality.py: new single owner of personality state.
  Built-in personality definitions, neutral-name normalization, rendering,
  availability (built-ins overlaid by agent.personalities), overlay
  resolution, and the ONLY sanctioned persistence path
  (persist_personality -> display.personality; never agent.system_prompt).
- v34 config migration: one-time reset of display.personality to none
  (announcing which personality was cleared and how to re-enable), plus a
  scrub of agent.system_prompt when it verbatim-equals a known personality
  render (machine-written by the old CLI/gateway). Hand-written manual
  prompts are never touched.
- All surfaces rewired through the module: CLI /personality (incl. active
  marker in the list), gateway /personality, TUI config.set + slash path
  (which previously applied without persisting), TUI config.get (reports
  the EFFECTIVE personality), completer, hermes config display, and the
  tui_gateway health probe.
- cli.py/config duplicates removed: built-ins now defined once; the
  desktop mirrors them from one lib module (src/lib/personalities.ts).
- Docs updated: selection lives in display.personality, built-ins always
  available, one-time reset note.
2026-08-09 10:33:58 -07:00
hermes-seaeye[bot] 28e401c30b
fmt(js): `npm run fix` on merge (#82468)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-09 11:49:06 +00:00
brooklyn! a4970d07d5
Sidebar filter menu (#82463)
* feat(desktop): resolve a session's pull request

A session row can say whether its work is open, merged or closed, and link
to it. The join is the session's own repo + branch, asked of GitHub in one
batched GraphQL request per repo (branch aliases, not a `gh pr list` page
that a busy repo crowds ours out of), through the remote-aware git facade so
a desktop on a remote gateway asks the backend's `gh`.

Two ways a session's branch can't answer, both covered:

- It ran on trunk. Fork PRs share our branch namespace, so asking about
  `main` badges a stranger's PR onto it — trunk is never asked about, and
  cross-repository PRs are dropped server-side either way.
- It worked in a worktree, so the branch it recorded at start isn't where
  the PR came from. Creating a PR from the review pane binds the session to
  the branch it actually used, and for sessions that predate that, the PR is
  recovered from the transcript: `gh pr create` prints a bare PR url and
  nothing else, so a tool result whose whole output is one is a claim rather
  than a mention. Scanned read-only across profiles, once per session ever.

* refactor(desktop): one profile glyph

The rail, the profiles page and the session-row chip each drew the same
tinted initial square from scratch, so a row tag could disagree with the
rail about a profile's color. One component owns the square, its tint, and
the home icon the default profile gets instead of a letter.

* feat(desktop): sidebar filter menu

The sessions header's project/list toggle was one binary choice standing in
for a view. It becomes a menu: group by date, project or status; order by
updated, created, status, tokens or cost; show tokens, cost, PR, profile or
an always-visible timestamp per row; filter by status, pull request, project
or archived. Everything persists, and one reset puts it all back.

The pieces that make it read right:

- Status groups reuse the date dividers rather than inventing a second
  separator, and a magnitude sort (tokens, cost) drops the calendar
  entirely — "Today" above the priciest session you have ever had is a lie.
- Row metadata shares the trailing slot the kebab covers on hover, so only
  the last fact steps aside and the number you switched on stays readable.
- A filter deepens the loaded page to 300 rows and hands the window back
  when cleared, so "merged PRs" doesn't quietly answer for the last 50.
- Archived is a view of its own set, and dragging is still what picks a
  manual order — the menu only offers a way back out of one.
2026-08-09 06:41:05 -05:00
Brooklyn Nicholson 868d2161b7 test(desktop): stub repoStatusForCwd in the review store tests
Binding a new PR to its session reads the repo's live branch, which the
suite's coding-status mock didn't provide.
2026-08-09 06:34:04 -05:00
Brooklyn Nicholson 83b8ca1e12 feat(desktop): sidebar filter menu
The sessions header's project/list toggle was one binary choice standing in
for a view. It becomes a menu: group by date, project or status; order by
updated, created, status, tokens or cost; show tokens, cost, PR, profile or
an always-visible timestamp per row; filter by status, pull request, project
or archived. Everything persists, and one reset puts it all back.

The pieces that make it read right:

- Status groups reuse the date dividers rather than inventing a second
  separator, and a magnitude sort (tokens, cost) drops the calendar
  entirely — "Today" above the priciest session you have ever had is a lie.
- Row metadata shares the trailing slot the kebab covers on hover, so only
  the last fact steps aside and the number you switched on stays readable.
- A filter deepens the loaded page to 300 rows and hands the window back
  when cleared, so "merged PRs" doesn't quietly answer for the last 50.
- Archived is a view of its own set, and dragging is still what picks a
  manual order — the menu only offers a way back out of one.
2026-08-09 06:21:15 -05:00
Brooklyn Nicholson 9c73cbe66a refactor(desktop): one profile glyph
The rail, the profiles page and the session-row chip each drew the same
tinted initial square from scratch, so a row tag could disagree with the
rail about a profile's color. One component owns the square, its tint, and
the home icon the default profile gets instead of a letter.
2026-08-09 06:21:08 -05:00
Brooklyn Nicholson 21aaa8b4f8 feat(desktop): resolve a session's pull request
A session row can say whether its work is open, merged or closed, and link
to it. The join is the session's own repo + branch, asked of GitHub in one
batched GraphQL request per repo (branch aliases, not a `gh pr list` page
that a busy repo crowds ours out of), through the remote-aware git facade so
a desktop on a remote gateway asks the backend's `gh`.

Two ways a session's branch can't answer, both covered:

- It ran on trunk. Fork PRs share our branch namespace, so asking about
  `main` badges a stranger's PR onto it — trunk is never asked about, and
  cross-repository PRs are dropped server-side either way.
- It worked in a worktree, so the branch it recorded at start isn't where
  the PR came from. Creating a PR from the review pane binds the session to
  the branch it actually used, and for sessions that predate that, the PR is
  recovered from the transcript: `gh pr create` prints a bare PR url and
  nothing else, so a tool result whose whole output is one is a claim rather
  than a mention. Scanned read-only across profiles, once per session ever.
2026-08-09 06:21:04 -05:00
hermes-seaeye[bot] 8295d24736
fmt(js): `npm run fix` on merge (#82417)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-09 09:51:41 +00:00
brooklyn! c8318460e4
feat(desktop): read the window below through Hyprland's IPC (#82226)
`read_window_below` enumerates through get-windows, which on Linux reads
`_NET_CLIENT_LIST_STACKING` via xprop. That is an X11 protocol, and Wayland
deliberately refuses to tell one application about another's windows. Under
XWayland it is worse than nothing: it finds the few legacy X11 clients and
silently misses every native Wayland window, which on a Hyprland desktop is
most of them — so the HUD floats over an app it cannot name.

Hyprland answers the question directly. `j/clients` on its command socket
returns every window with class, title, position, size, pid and focus history.
Ask it first when HYPRLAND_INSTANCE_SIGNATURE is set, fall back to get-windows
everywhere else, and keep the picking logic shared and unchanged.

Three things the provider has to get right, all covered by tests: order comes
from focusHistoryID rather than the list; windows on other workspaces are
dropped, since they share coordinates with the visible ones and would win the
overlap test; and our own window is left out, because focus history is not
stacking order — the HUD floats on top while the user works underneath it, so
slicing after ourselves would skip past the very app we are trying to report.

One request per tool call, opened and closed immediately: Hyprland evaluates
this socket synchronously and freezes until a five-second timeout on a
connection left hanging.
2026-08-09 04:43:10 -05:00
brooklyn! 774e9d4d59
Merge pull request #82390 from NousResearch/bb/draft-status
fix(desktop,title): name and mark an unsent session, and the titler behind it
2026-08-09 04:42:30 -05:00
Brooklyn Nicholson c143ec7f0c feat(desktop): name a draft after what you have typed into it
Every unsent tab was called "New session", so a row of them said nothing
about which was which. Name each one from its composer, using the same
first-line, word-boundary rule the backend's derive_title applies a moment
after the draft is finally sent — so the name the tab already shows is the
name it keeps.

The title moves with the composer, which is far faster than a pane
contribution should be re-registered. Panes can now render a tab label
instead of declaring one, so the label subscribes to its own key and a
rename repaints one string rather than the panes area.
2026-08-09 04:33:58 -05:00
Brooklyn Nicholson 5b3a5ccae6 feat(desktop): mark an unsent session with its own status dot
A draft got no dot at all, so the one tab that has never done anything
looked identical to a settled session. Give it the faintest mark the app
has — a hollow outline, weakest claim in the dot's priority order, so the
first thing that actually happens speaks over it.

The row's own message_count is the tiebreaker for what counts as a draft:
a session RESUMING also holds an empty message list for a moment, and
calling that a draft flashes the wrong mark on a conversation with years
of history in it.
2026-08-09 04:20:31 -05:00
brooklyn! 11dc61b45e
fix(desktop): keep the HUD exit chip clickable when the composer has no focus (#82403)
The chip was pointer-events: none until [data-slot='composer-rich-input']
had :focus, which made the only visible way out of HUD mode conditional
on the thing most likely to be broken when someone wants out. When focus
never lands (#81893 on macOS) you can neither type nor click your way
out: the HUD is a transparent always-on-top rectangle over the desktop
with no in-app dismiss.

It is now always clickable and dim (0.45) at rest, brightening on hover,
focus-visible, and composer focus. That keeps the original intent — not
a loud chip over the app behind — without gating the escape hatch on the
failure mode it exists for.

Salvaged from #82317 by @Ne0teric. The centering half of that PR is
dropped: #82233 already fixed the dock offset, and its 'translate: none'
is the exact literal Lightning CSS folds into 'transform', which is the
bug #82233 fixed.

Co-authored-by: Ne0teric <Ne0teric@users.noreply.github.com>
2026-08-09 09:13:33 +00:00
Teknium 124aff0aa9
fix(desktop): open HUD mode on the focused conversation's profile (#82325)
* fix(desktop): open HUD mode on the focused conversation's profile

The HUD is a full app renderer that adopted the PRIMARY backend's
profile at boot, so toggling HUD mode from a conversation on any other
profile resolved the session id against the wrong backend — the lookup
missed and the HUD fell back to the default profile's last session
(#82285).

- openHud() resolves the target's owning profile (session's stamped
  owner, else the active gateway profile) and passes it through
  hermes:hud:open.
- hudUrl() carries the profile in the query string next to win=hud;
  the HUD renderer's gateway boot honors it as an override for both
  getConnection() and profile adoption, so the window dials and adopts
  the right backend from first paint.
- Retargeting a live HUD onto a session from a DIFFERENT profile
  respawns the window against that profile's backend (a renderer adopts
  its backend exactly once at boot; an in-place goto would repeat the
  wrong-backend lookup).

No profile in the URL means no override — ordinary windows and
single-profile users boot exactly as before.

* refactor(desktop): extract the HUD renderer URL so its contract is tested

hudUrl() built the query string inline in main.ts, where the part that
actually breaks — `?win=hud&profile=` must sit BEFORE the '#' or
HashRouter eats it as the route — had no coverage. Move it next to
buildSessionWindowUrl's split (pure piece out of the monolith, unit
tested) and pin the contract: flag order, profile encoding, trailing
slash on the dev server, empty profile omitted, packaged file URL.

Co-authored-by: rainbowgore <rainbowgore@users.noreply.github.com>

* refactor(desktop): resolve the HUD's target profile through the existing ladder

openHud() had its own copy of "stamped owner, else active gateway, else
default" — the same ladder rememberedSessionProfile() already owns for
the remembered-navigation key, down to sessionMatchesStoredId and the
default fallback. One resolver per policy, so the two can't drift.

---------

Co-authored-by: Brooklyn Nicholson <brooklyn.bb.nicholson@gmail.com>
Co-authored-by: rainbowgore <rainbowgore@users.noreply.github.com>
2026-08-09 09:10:59 +00:00
Ne0teric b105a3b057
fix(desktop): keep the HUD on the session it was opened for (#82360)
* fix(desktop): keep the HUD on the session it was opened for

The HUD is a full app renderer, so the main window's cold-start
'restore last session' logic ran inside it: opening HUD on a blank new
chat (#/) navigated it to the remembered session instead of the new one,
because a blank draft has no stored id and the HUD boots at the default
route. Guard the restore/remember effect with isHudWindow() — the HUD's
destination is always chosen explicitly at open time.

Also stops the HUD from clobbering the main window's remembered
navigation while it is up.

* fix(desktop): use type-only import for the windows-store mock in HUD restore test

consistent-type-imports forbids inline import() type annotations; use the
established import type * as pattern (same as session-row.test.tsx).
2026-08-09 04:08:50 -05:00
Teknium 085a9d332f
test(desktop): widen HUD composer containment regression coverage (#82319)
Extend the packaged-app HUD geometry test from horizontal-only to full
containment: both axes for the dock and the input, plus an explicit
assertion that no percentage translate survives on the composer dock.
The vertical clipping reported on Windows (#82203) and macOS (#82214)
is the same escape class on the other axis, and the computed-translate
probe makes a future optimizer regression fail with a diagnosis instead
of a bare coordinate mismatch.
2026-08-09 04:03:46 -05:00
brooklyn! f8bdbc540e
Merge pull request #82373 from NousResearch/bb/drag-title
Drag sidebar sessions and projects by the title
2026-08-09 03:36:07 -05:00
Teknium 6495ef82f7 fix(desktop): hand-off hardening - fail-closed gates, truthful completion, progress UI, result surfacing
Review feedback on the #82328/#82366 hand-off, all four points plus the
missing progress GUI:

1. FAIL CLOSED. Both preflight gates aborted-open: a Desktop still alive
   after 30s proceeded anyway, and a shim locked after 20s proceeded
   with --force - both mutate a potentially locked install (the exact
   Access-denied brick class). Now: desktop-alive -> exit 4, nothing
   changed; shim-locked -> exit 5, nothing changed. Both relaunch the
   Desktop so the user is never stranded.

2. TRUTHFUL COMPLETION. `hermes update` treats a Desktop GUI build
   failure as non-fatal (warns, exits 0) - correct for CLI use, a lie
   for a Desktop-driven update that then relaunches the OLD exe as
   "success". The script now detects the warning in the update output,
   retries the build once (`hermes desktop --force-build --build-only`),
   and exits 6 with an honest message when it still fails.

3. MARKER OWNERSHIP. Cleanup now removes the marker only while OUR pid
   still owns it - a handoff partner that rewrote the marker keeps its
   claim (same rule as UpdateLock.release).

4. RESULT SURFACING. The script writes .hermes-update-result.json on
   every exit path (ok, exit_code, message, branch, finished_at). New
   electron/handoff-result.ts consumes it exactly once at the boot
   update-gate: success logs, failure shows a real dialog pointing at
   desktop-update-handoff.log. Stale (>30min) and malformed results are
   consumed silently. Previously a failed detached update was
   indistinguishable from "nothing happened" - the exact live report
   that triggered this work.

5. PROGRESS UI. The old Tauri updater showed a window; the script ran
   in a hidden console with zero feedback. It now shows a WinForms
   progress window (marquee bar + streaming log) pumped via DoEvents
   during the update; -NoUi keeps tests/headless sessions clean, and a
   WinForms-unavailable session degrades to log-only.

Also: subprocess execution moved from Start-Process (ExitCode
unreliably $null under PS 5.1 even with the Handle workaround -
observed live: happy path reported "failed (exit )") to
System.Diagnostics.Process with synchronous stdout pumping, which
keeps the UI alive and the exit code real.

E2E on a real Windows box, sandbox HERMES_HOME + compiled fake
hermes.exe, all five paths:
- happy: exit 0, result {ok:true, "Update complete."}
- shim held open via O_RDWR: exit 5, nothing mutated, honest result
- desktop pid alive (60s ping child): exit 4 after the 30s gate
- update exits 0 printing "Desktop build failed" + rebuild fails:
  exit 6, result names the stale build and the retry command
- foreign-owned marker: overwritten by step-0 claim, removed as owner;
  ownership check verified in the cleanup path
vitest 18/18 (5 new handoff-result tests), typecheck 3 projects clean,
eslint clean, PS 5.1 parse + footguns + ASCII-only clean.

Remaining known gap (deliberate): the full click-to-relaunch lifecycle
through a REAL Desktop build still needs one live Windows verification
after this lands - tracked in the PR body.
2026-08-09 01:26:14 -07:00
Teknium 3b08a0f9b5 fix(desktop): give the update hand-off script its own console - a detached hidden powershell dies before -File runs
Live failure on the first real use of #82328 (2026-08-09): clicking
Update closed the Desktop with "an updater will happen", then nothing.
desktop.log showed `launched repo hand-off script`, but
desktop-update-handoff.log was never created - PowerShell exited 0
without executing a single line.

Root cause, isolated by spawning the exact production shape against a
sandbox HERMES_HOME: `spawn('powershell', [..., '-File', script],
{ detached: true, stdio: 'ignore', windowsHide: true })` kills
powershell.exe during console-subsystem init, before -File processing.
Variant matrix: plain pipes -> runs; hide only -> runs; detached only ->
runs; detached+hide -> exits 0, script never starts. Unit tests and
foreground invocations can't see this class of bug.

Fix: wrapHandoffForDetachedConsole() routes the invocation through
`cmd /d /s /c start "" /min powershell ...` - `start` allocates the
script its own minimized console and fully detaches it; the cmd wrapper
exits immediately. Verified the wrapped form survives the full
detached+hidden production spawn.

Knock-on: child.pid is now the short-lived wrapper, not the script, so
the Electron-side marker pre-write can't represent the script. The
script now claims the update marker itself as step 0 (its own $PID,
byte-exact "<pid>\n<ts>\n" via WriteAllText - Set-Content emits CRLF
and would break the three readers' framing). The Electron pre-write is
kept as a bridge for the spawn window: the script overwrites it, and if
the script never starts the wrapper's dead pid reads as stale and
self-deletes (no wedge). `hermes update` adopts the script's claim via
update_lock.py's process-ancestry rule, unchanged.

E2E in exact production shape (cmd start wrapper, detached, hidden,
parent exits 1.5s after spawn) against a sandbox HERMES_HOME with a
compiled fake hermes.exe: script ran, claimed marker with its own pid
(fake observed "<script-pid>|<ts>|" LF-framed DURING the update),
desktop-pid wait worked, update invoked with correct argv, marker
removed on completion. vitest 13/13 (new wrapper-shape test), 3-project
typecheck clean, eslint clean, PS 5.1 parse + windows-footguns clean.
2026-08-09 01:26:14 -07:00
Brooklyn Nicholson 35b82fdef3 feat(desktop): drag sidebar rows by the title, not just the grabber
The grabber in the lead column was the only way to reorder a session or a
project, and it only appears on hover — a 14px target for the whole gesture.
The row's title is the obvious thing to grab, so put the sortable listeners
on the row shell.

For a session that means two drags share one press, since the title already
starts the drag into the layout. They need no arbitration: each declines
outside its own region. Over the sidebar only the reorder has a target (the
session drop denies — side chrome hosts no main tile); over the tree only the
session drop does (no sortable row there). Whichever the release lands on is
the one that commits.

Rows exclude their own controls through one data-row-actions selector, now
owned by SidebarRowShell instead of restated per row.
2026-08-09 03:23:08 -05:00
Brooklyn Nicholson e128f1c131 fix(desktop): stop lighting drop zones a session can't land in
A dragged session outlined every zone in the tree, including standing side
chrome — the sidebar, files, terminal. None of them host a main tile, so the
drop was always refused; the outline just advertised a target that wasn't one.

Gate the overlay on the same isMainStripPane/isSessionStripPane test that
tileZoneHost resolves the drop with, so what lights up and what commits can
never disagree.
2026-08-09 03:23:00 -05:00
hermes-seaeye[bot] 35e562ebd0
fmt(js): `npm run fix` on merge (#82346)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-09 07:36:34 +00:00
Teknium 92be912d73 feat(desktop): repo-owned Windows update hand-off script - stop depending on the frozen hermes-setup binary
The Desktop's Update button hands off to the staged Tauri binary
(HERMES_HOME/hermes-setup.exe). That binary has no self-update path
(copy_self_to_hermes_home no-ops during --update), so every updater-side
fix only reaches users when a new installer is built, signed, and
published. In practice the published binary lags main by months and
users hit long-fixed bugs on every GUI update: the 2026-08-09 incident
chain was four distinct failures (stale install.ps1 cache resolver
pre-#67369, marker adoption pre-#74782, straggler teardown) all caused
by a June 4 binary running against an August repo.

This inverts ownership: scripts/desktop-update.ps1 lives in the repo
checkout, so every `hermes update` refreshes the code that drives the
NEXT update. Only PowerShell itself - an OS component - stays frozen.

Desktop side (apps/desktop/electron):
- resolveUpdateScriptHandoff() (updater-process.ts): returns the spawn
  recipe when scripts/desktop-update.ps1 exists in the checkout;
  Windows-only (POSIX updates in place via applyUpdatesPosixInApp);
  null on old checkouts -> caller falls back to the staged binary path
  completely unchanged.
- applyUpdates() prefers the script hand-off. The marker pre-write is
  ALWAYS safe on this path - no stagedUpdaterSupportsPrewrittenMarker()
  mtime heuristics - because hermes_cli/update_lock.py's UpdateLock
  adopts a live marker held by a process ANCESTOR, and the script is
  the `hermes update` child's parent. This closes the unguarded
  marker-gap window that pre-#74782 binaries force today (the 23:56
  failure in the incident: 'skipping marker pre-write: staged updater
  predates self-adopt' -> renderer respawned a backend into the gap ->
  update refused).
- CLI-installed users (no staged binary) now get the script hand-off
  too instead of the manual `hermes update` card, when the script
  exists.

Script (scripts/desktop-update.ps1): waits for the Desktop pid to exit
(bounded 30s), waits for the venv shim to unlock (mirrors the Rust
is_locked probe, bounded 20s), runs `hermes update --yes --gateway
--force --branch <ref>` from the CURRENT checkout with one retry for
the update-boundary class (skipped for exit 2), removes the marker on
every exit path, relaunches the Desktop. ASCII-only (the #67193
lesson), logs to logs/desktop-update-handoff.log.

Verification (real Windows box):
- apps/desktop: typecheck (3 projects) clean, eslint clean, vitest
  updater-process.test.ts 12/12 (3 new resolver tests).
- Script E2E against a sandbox HERMES_HOME with a compiled fake
  hermes.exe: correct argv (update --yes --gateway --force --branch
  main), stale marker removed, exit code propagated (0 and 1 paths),
  retry-once fires exactly once on failure, PS 5.1 parse + windows
  footguns check clean.
- Contract E2E with the real UpdateLock: ancestor-owned marker adopted
  (True), left in place on release, foreign live holder still refused.
2026-08-09 00:27:06 -07:00
brooklyn! 62431364e3
Merge pull request #82233 from kerpopule/fix/desktop-hud-composer-clipping 2026-08-09 01:24:08 -05:00
teknium1 bb8280b753 revert(desktop): roll Electron back to 40.10.2
Reverts the Electron portion of 7537de9e74 (40.10.2 -> 40.10.6,
GHSA-r4w5-6pfg-jxp5). The bump broke fresh Windows installs: 40.10.3+
swapped install.js's extraction to @electron-internal/extract-zip, an
MSVC-built native binding that ERR_DLOPEN_FAILEDs on machines without
the VC++ Redistributable (field report Aug 9, fresh VM, confirmed).

Security impact of reverting is nil for our code paths:
- GHSA-r4w5-6pfg-jxp5 (Moderate 5.9): requires the legacy
  ProtocolResponse.url API with session-partition isolation; desktop
  only uses the modern protocol.handle() and no cross-session
  isolation. Not exposed.
- GHSA-9f4c-93c8-jc8g (High 7.2): affects ALL of 40.x with no fixed
  40.x release (fix is 41.10.3+); 40.10.2 vs 40.10.6 is a wash. Real
  mitigation is hardening our setWindowOpenHandler (follow-up PR).

allowScripts version-keyed entry moved to electron@40.10.2 so the
postinstall (dist download) still runs; allow-scripts-sync vitest
suite passes. Electron 41.x major remains deferred/on hold.
2026-08-08 23:24:02 -07:00
Steve Darlow f2731da4a4 fix(desktop): keep HUD composer within window 2026-08-08 22:54:16 -05:00
hermes-seaeye[bot] 1792e756e4
fmt(js): `npm run fix` on merge (#82209)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-09 03:32:52 +00:00
Brooklyn Nicholson 04afc8d48e fix(desktop): say why read_window_below cannot see the windows
When enumeration was impossible the tool answered "could not determine the
window underneath (the desktop app did not answer, or window enumeration is
unavailable on this system)" — true, and a dead end. On Linux the two ways it
fails have opposite fixes and neither is guessable from that: a Wayland session
withholds window identity from applications outright, while an X11 session
needs xprop and xwininfo installed, because that is what the enumerator shells
out to.

Answer with the reason instead of nothing. A session with both WAYLAND_DISPLAY
and DISPLAY is XWayland, where xprop can still answer, so it gets the tooling
advice rather than being told to change session type.
2026-08-08 22:17:38 -05:00
Brooklyn Nicholson da933bf279 fix(desktop): keep the HUD clickable on Linux
Click-through decides whether to swallow the mouse by hit-testing the document
under the cursor, and it learns where the cursor is from mousemove. Those keep
arriving while the window ignores the mouse only because of
`setIgnoreMouseEvents(true, { forward: true })`, and `forward` is
`@platform darwin,win32`. On Linux the moves stop the instant the HUD turns
click-through, so it never sees the pointer return to the bar: the bar is
visible, and clicking it hits whatever is behind.

Main can still see the cursor, so on Linux it polls and pushes the position to
the renderer, which runs its usual hit test on it. The decision and its rules
stay in one place — only the courier for that one input changes — and off-window
is sent as null, which is already how the renderer hands the mouse back.
2026-08-08 22:15:24 -05:00
Brooklyn Nicholson d73bc7f17a fix(desktop): persist window geometry on Linux
`moved` and `resized` are macOS/Windows only — Electron tags them
`@platform darwin,win32` — so on Linux neither the main window nor the HUD
ever heard that it had been dragged or resized, and both reopened at their
default placement every launch. The main window had a `close` flush to fall
back on; the HUD had nothing, so its position was lost outright.

Bind `move`/`resize` instead. Those carry no platform tag and fire everywhere,
and the trailing debounce already collapses the mid-drag stream a settled event
would have saved us from.
2026-08-08 22:13:08 -05:00
Teknium da3a0a852f fix(update): make orphan-backend reap tree-aware + drain Desktop update trees without pre-signalling
Follow-up to #82179 addressing helix4u's review comment
(#82179 issuecomment-5229441571). Three parts:

1. Desktop teardown (salvaged from #77436, @4adwentures): the update
   hand-off's releaseBackendLock() sent SIGTERM to the primary backend
   BEFORE taskkill /T. If the launcher exits first, Windows can no longer
   enumerate its descendants and they survive holding the venv — the
   Electron path that creates the orphan #82179 then has to repair.
   New stopBackendTreesForUpdate() tree-kills the live root first, with
   the behavioral vitest from #77436. The scanner half of #77436 is
   deliberately NOT taken (superseded by #82158's full-cmdline scan).

2. Tree-aware orphan classification: _orphaned_desktop_backend_pids()
   previously refused the whole holder set when any holder had a live
   parent. But the scanner legitimately returns an orphaned serve root
   AND its descendants (the venv trampoline's uv-managed interpreter
   worker — which carries the same backend argv — plus .hermes-runtime
   children). Those have a live parent: the orphan root itself. Now
   holders inside an accepted orphan root's tree fold into that root
   (only roots are returned; taskkill /T reaps descendants), and
   live-parent backends defer to the ancestry check instead of refusing
   outright. Anything outside an orphan tree still refuses.

3. Tests for the mixed shapes: root+managed-runtime child,
   grandchild depth, non-descendant stray alongside an orphan root
   (still refuses), descendant exited mid-classify.

E2E on a real Windows box: spawned a detached backend-shaped orphan
that itself spawned children (3 python descendants); the scanner-shaped
mixed holder set classified to [root], taskkill /T reaped root and all
descendants. The live Desktop backend on the box still classified None
(refusal preserved). The first E2E attempt caught exactly the
trampoline/worker case the mocks missed — the live worker re-execs with
the same backend argv and a live parent — which is what part 2 fixes.

Co-Authored-By: 4adwentures <296413879+4adwentures@users.noreply.github.com>
2026-08-08 20:01:03 -07:00
Brooklyn Nicholson 2cd9e1777b fix(desktop): rebuild get-windows when its win32 binding is missing
Fixing the allowlist only helps a fresh install. npm will not re-run an
install script for a package already on disk, so every checkout that
installed while get-windows was blocked stays bricked: `hermes update`
pulls the fix, `npm install` skips the script, and the build fails on the
same missing binding.

Run `npm rebuild get-windows` from the staging step when the binding is
absent, and if that still yields nothing, print the two commands that
recover the checkout by hand instead of the previous advice to reinstall
dependencies, which is exactly what the user already tried. Gated to a
win32 host building for win32, since no other host can produce the binding.

Co-authored-by: JoaoMarcos44 <JoaoMarcos44@users.noreply.github.com>
2026-08-08 21:23:20 -05:00
brooklyn! 5f62b6c1ab
Merge pull request #82113 from NousResearch/bb/hud-unsticky
fix(desktop): keep tool rows and notices out of the HUD band
2026-08-08 19:39:55 -05:00
owner 3a8d95a93b fix(desktop): give the delivery-target group an accessible name
Field renders its visible label as <label htmlFor>, but that only
associates with labelable elements (input, select, ...), not the
div[role="group"] DeliverCheckboxes renders. The checkbox group had
no accessible name for assistive tech.

Field now stamps an id on its label (`${htmlFor}-label`) and
DeliverCheckboxes references it via aria-labelledby, so the group
picks up the same visible label text instead of duplicating it.

Addresses review feedback on PR #73886.
2026-08-08 17:33:22 -07:00
Carbon 67927808be feat(desktop): support multiple cron delivery targets 2026-08-08 17:33:22 -07:00
Brooklyn Nicholson d91f08a39a fix(desktop): keep tool rows and notices out of the HUD band
The band is a few lines of conversation floating over another app, not a
transcript. Tool blocks, file-diff panels, and background notices ("Self-
improvement review: patched ...") pushed the actual answer out of the
capped band and read as junk pinned over the window below.
2026-08-08 19:32:54 -05:00
brooklyn! 37bb73d2cf
Merge pull request #82101 from NousResearch/bb/hud-slash-popover
fix(desktop): make the slash list usable in HUD mode
2026-08-08 19:20:20 -05:00
Brooklyn Nicholson 05c5a337f6 fix(desktop): settle the HUD band's glanceable hold at 1.1s
Anything longer reads as the band waiting for something. Also drops the note
claiming a sub-second hold disappears into the fade — that was written when
losing window focus never started the hold at all, so what looked like too
short a stage was no stage.
2026-08-08 19:12:27 -05:00
hermes-seaeye[bot] 99d1f18377
fmt(js): `npm run fix` on merge (#82099)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-09 00:11:46 +00:00
Brooklyn Nicholson 1040cfe286 fix(desktop): shorten the HUD band's glanceable hold to 1.75s
2.5s was long enough to feel like the band was waiting for something.
2026-08-08 19:10:35 -05:00
Brooklyn Nicholson 3d5f173440 feat(desktop): lift the HUD bar off what it lies over
A hairline shadow under the bar's bottom edge. Without it the HUD reads as
pasted onto the other app rather than floating above it; kept tight and faint
so it never becomes a glow around a card.
2026-08-08 19:10:35 -05:00
Brooklyn Nicholson 051a7fb1e2 fix(desktop): open the HUD completion list where there is room for it
The composer's completion list — `/`, `@`, `:`, and the help hint — hangs off
the top of the bar. That is right everywhere the composer has a window above
it, and wrong in the one place the bar is parked against the screen's top edge:
the list rendered off screen, so `/` looked like it did nothing at all.

Flip it below the bar in that orientation and cap it to the room it actually
has, since the app's own cap assumes a full window.
2026-08-08 19:08:46 -05:00
Brooklyn Nicholson 5ff506896f feat(desktop): sit the HUD band back when a completion list opens
The list hangs over the band, and both were half-lit over a third thing — the
app underneath. No pair of opacities reads well in that stack.

So the list goes fully opaque and the band falls back behind it: dimmer,
fractionally smaller, slightly out of focus, scaled from the bar's edge so it
reads as depth rather than as the panel shrinking.
2026-08-08 19:08:45 -05:00
brooklyn! 34f3135879
Merge pull request #82077 from NousResearch/bb/hud-drag-and-chrome
fix(desktop): make the HUD grabbable, and stop it painting over dead space
2026-08-08 18:58:10 -05:00
Brooklyn Nicholson d2dc0ce707 fix(desktop): hold the HUD band when the window loses focus
Clicking away to another app is the commonest way the HUD gets let go of, and
it fires no focusout — the composer stays document.activeElement while the
window is inactive. Chrome stops matching `:focus` on an unfocused window all
the same, so the band lost its focus state with no hold running and snapped
shut instead of stepping down to the glanceable stage.

The sheet also goes heavier than the text in front of it. There is no blur to
separate the band from what it lies over, so it is the only thing keeping
half-opacity text off someone else's UI.
2026-08-08 18:49:19 -05:00