Address helix4u's review:
- finish() now delivers the outcome BEFORE publishing it: mac bundle swap
and the linux relaunch gate run first, then the result file, marker
removal, and the shim event -- the app launch itself goes last so it
can't race the result write. A gated/skewed linux install (AppImage/
deb/rpm, broken sandbox helper) surfaces its message in the result file
AND holds the shim window open with it instead of closing on a false
'Opening Hermes...'.
- mac swap is transactional with a checked rollback; a failed install
restores the previous bundle and the result says so (exit 7 when even
rollback fails). Failed 'open' rewrites the result truthfully.
- linux gate is an exact port of the deleted update-relaunch.ts logic:
anchored path-segment match on <root>/apps/desktop/release/linux-unpacked,
chrome-sandbox absent = namespace build = fine, present = root+setuid
required, with the real opt-outs (ELECTRON_DISABLE_SANDBOX, --no-sandbox
among replayed args, or the Desktop vouching) instead of the invented
HERMES_DESKTOP_NO_SANDBOX. collectRelaunchArgs/sandboxFallbackFromEnv
live in updater-process.ts again; the Desktop passes filtered launch
args (after --) and --relaunch-cwd so a deep-link or --no-sandbox
launch survives the update.
- result/status JSON strings are escaped (git permits '"' in branch
names) and the result write is atomic (tmp + rename).
- coverage: resolvePosixScriptHandoff + ported helpers in
updater-process.test.ts (19 pass); repro.sh gate / npm run
update:repro:gate asserts the whole gate matrix and round-trips a
hostile branch name through the result JSON.
applyUpdatesPosixInApp is gone: mac/linux Update now quits into the
detached posix orchestrator, same shape as Windows. Deletes everything
the in-app path dragged into main.ts -- runStreamedUpdate, the rebuild
retry, the relaunch-outcome matrix (update-relaunch.ts/update-rebuild.ts
and tests), shellQuote, resolveHermesCliBinary -- and with the app dead
before the update starts, the HERMES_DESKTOP_CHILD_PID reaper-exclusion
dance (#37532) is structurally unnecessary on the desktop path.
scripts/desktop-update/posix.sh is the mac/linux twin of windows.ps1:
the Desktop spawns it detached and QUITS; it waits the app out, runs
plain hermes update (retry-once across the update boundary, truthful
desktop-rebuild completion), swaps/relaunches the .app bundle (mac) or
the release/*-unpacked binary when its sandbox helper is launchable
(linux), writes .hermes-update-result.json, and drives the same shim.
Repo-owned, so every update refreshes the code that drives the next one.
resolvePosixScriptHandoff mirrors the Windows resolver (with the
flat-path fallback covering the scripts/ reorg skew).
Reconcile the salvaged #81533 lifecycle helper with the renderer-log
console pipeline that landed in #83535 (the two PRs raced):
- window-renderer-lifecycle.ts no longer handles console-message —
renderer-log.ts is the single owner (per-window labels, boundary
reports). One owner means no double-logged errors on windows wearing
both, and OAuth/portal windows (lifecycle-wired for process events)
cannot spill third-party page console output into desktop.log.
- wake indicator window gets attachRendererConsoleCapture, keeping the
console coverage it previously got from the helper.
- HUD window (added after the PR branched) gets log-only lifecycle
coverage — it was the one renderer window the PR couldn't have known
about.
- Tests updated: lifecycle helper asserts it attaches NO console-message
listener; parser tests live in renderer-log.test.ts.
@spfcraze's triage review noted the PR description claimed "every
BrowserWindow" but the OAuth and portal sign-in windows were not wired:
a crashed sign-in renderer leaves the window's promise path never
settling, with no trace in desktop.log.
Wire both with the same log-only lifecycle diagnostics as the overlay
and quick windows — `kind: 'oauth'` and `kind: 'portal'` respectively.
Neither window gets crash-reload treatment (a sign-in window that
reloads itself mid-auth would be surprising); the lifecycle helper's
log-only callback is the exact contract needed here.
window-renderer-lifecycle.test.ts: 17/17 pass.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three-part class closure for the React #310 / lost-renderer-crash family
(#79428, follow-up to #80560 / #82763):
1. Diagnosability (#79428 defect B): error-boundary catches now persist to
desktop.log with their component stack via a new fire-and-forget
hermes:logs:renderer-error IPC (synchronous flush — the window may be
dying). Every renderer-content window (main, secondary session, instance,
HUD, quick entry, pet overlay) gets the error-level console capture that
previously only the main window had, labeled per window. 'Open logs' on
the crash dialog now reveals a file that actually contains the crash.
2. Recurrence guard: eslint no-restricted-syntax rule banning inline
render() calls in JSX — the mechanism behind #80560. The rule
immediately caught two live sites #82763's audit missed (floating
panes, narrow-overlay reveal), both hosting plugin panes.
3. Fix those two missed sites with the same ContribRender mount.
extracted console-capture/report formatting to electron/renderer-log.ts
with unit tests; renderer console lines now carry the window label.
An unclean desktop exit (crash / SIGKILL / update handoff) stranded every
`hermes serve` profile backend as an orphan (ppid=1) still serving, each
holding its MCP child subtree — 31 orphans / ~1.3 GiB RSS on one install.
Root causes + fixes:
- serve had no parent-death watchdog: add _start_parent_death_watchdog() in
web_server.py (mirrors slash_worker.py), gated on HERMES_PARENT_PID; os._exit
cascades to MCP watchdogs. No-op for standalone `hermes serve`.
- desktop passes HERMES_PARENT_PID in both serve spawn env blocks (main.ts).
- POSIX teardown now group-kills (process.kill(-pid, ...)) so MCP grandchildren
die too (backend-child.ts + waitForBackendExit SIGKILL fallback).
Windows path unchanged (forceKillProcessTree). Tests updated + passing.
The floating HUD inherited the default Hermes title from index.html.
Set it explicitly in main and the renderer so the OS window label
matches the mode.
Tahoe already aligns traffic lights without the optical translate. In
fullscreen, drop windowButtonPosition in the main process and clear the
right-cluster inset so traffic-light dodge chrome goes away on both sides.
Register CommandOrControl+Shift+G in main while HUD mode is open so the
floating bar can jump under the pointer from any app. Tap-to-snap only —
Electron globalShortcut has no keyup for hold-to-follow.
* fix(desktop): stop HUD window growing on drag; add corner resize handle
The HUD window is created frame:false + transparent:true + resizable:true.
On Windows, a transparent frameless window silently grows ~1px per
setPosition call (worse at >100% DPI scaling) — every drag of the composer
bar accumulated size drift, and the HUD could end up enormous (reported at
1385x1052 against a 620x320 default). Reading the size back mid-drag
compounds the drift because getSize() returns the already-drifted value.
Fix, mirroring the pet overlay's pattern:
- create the HUD window non-resizable (no system edge resize hot-zone)
- moveBy uses setBounds with a size snapshotted on the first move of each
drag, so the OS can never accumulate drift (verified: 500 moveBy calls
with zero size change on Electron 40 / Win11 / 175% DPI)
- add a bottom-right corner resize handle (resize-handle.ts) driving a new
hermes:hud:set-bounds IPC that flips resizable on for the call, restoring
the ability to resize a window that is otherwise non-resizable
* fix(desktop): pin HUD drag size in renderer, not main-process globals
The superseding pass drops hudDragWidth/hudDragHeight from main: composer
drag snapshots outerWidth/outerHeight when the hold arms (pet overlay
pattern) and passes them on every moveBy. Adds one test for that contract.
Supersedes #82455.
Co-authored-by: Ringo6107 <199014580+Ringo6107@users.noreply.github.com>
* fix(desktop): keep the HUD solid through a corner resize; drop dead handle state
The resize handle's `resizing` flag only fed a CSS rule that restated the
cursor it already had, so nothing pinned the window mid-gesture: click-through
hands the mouse away the moment the growing edge outruns the cursor. Raise the
composer drag's existing `data-hud-grabbing` instead — one flag for "a gesture
owns the window" — and cover it in click-through's tests.
Also drops the hook's always-true `enabled` param and routes teardown through a
`reset` callback, matching composer-drag.ts and clearing the atom-mirrored-ref
lint rule.
---------
Co-authored-by: Ringo6107 <199014580+Ringo6107@users.noreply.github.com>
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.
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.
* 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.
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.
`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.
* 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>
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.
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.
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.
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.
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.
`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.
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>
`-webkit-app-region: drag` and `useHudClickThrough` cannot share a window, and
every previous attempt at a HUD grab handle had them fighting. The window
manager takes a draggable region's mouse input whole, so the page never sees
the cursor arrive on the handle — and click-through, which decides whether the
window is solid from exactly those moves, has already handed the window to the
desktop by the time you press. The handle was unusable (the press fell through
to the app behind) and, having eaten the moves on the way out, it also left the
HUD solid over its own dead space so clicks meant for the app behind died in
empty window.
So no HUD surface declares a drag region any more, and dragging is a press and
hold on the bar: 140ms to arm, then the renderer moves the window through a new
`hud.moveBy`. Deltas are read in screen coordinates, because client coordinates
are relative to the window being moved and report zero once it keeps up with
the cursor. The pointer is captured and the window pinned solid for the
duration, so a fast drag cannot outrun the bar.
Removes the 2rem invisible drag strip along with it — dead window that only
ever swallowed clicks aimed at whatever was behind the HUD.
Session, instance, HUD, quick-entry and pet-overlay windows all open with
show: false and are revealed only by ready-to-show, so the Electron 40 bug
strands them exactly the way it stranded the primary window — and none of
them have the second-launch workaround that made the main-window case
recoverable.
Generalize the controller to any window and wire all six through one
wireWindowReveal helper. Callers pass their own reveal action (showInactive
for the pet overlay, show + focus for the HUD and quick entry) and their own
post-visible work, so whichever path wins runs them exactly once.
Quick entry now reveals the window the call created rather than whatever
`quickEntryWindow` points at when the event lands.
Desktop E2E is hard-disabled in ci.yml (#76627) because the mock-backend
window never reaches a usable state, so nothing can validate dropping the
TEST_WORKER_INDEX force-show right now — and the suite's lead symptom is
already a window-readiness failure. Restore it, routed through the reveal
controller so the bookkeeping in onRevealed still runs exactly once, and
leave the removal to whoever re-enables the suite.
The desktop_ui and post-hook ownership contract tests enumerate their tool
sets exactly — add read_window_below to both (plus the executor-path
parametrize case). Lint: sorted type import, explicit GetWindowsModule type
instead of an import() annotation, curly + blank-line style.
New electron/window-below.ts: pure z-order picker (walks past our own pid,
first other-process window whose bounds overlap ours) over get-windows'
front-to-back enumeration, with the Linux xprop stacking order reversed to
match (EWMH _NET_CLIENT_LIST_STACKING is bottom-to-top). Main answers the
hermes:window:readBelow IPC; on macOS other apps' titles pass through only
when Screen Recording is already granted — never prompted for.
The band is real macOS vibrancy now rather than backdrop-filter, which
reaches nothing in a transparent window — its backdrop root is the document,
and the desktop was never in it. Vibrancy composites below the web contents,
so it can see the desktop, and it can't be masked or clipped from the page.
That rules out the gradient the band used to carry and settles it as a flat
panel: uniform tint, uniform frost.
The fade does the work the gradient was doing. A landing turn brings the
transcript half way up to be glanced at, focus promotes it to properly
readable, and the hold takes it back down and then away — the panel sliding
behind the bar as the last of the text goes.
It only fades from an idle transcript. A running turn or a question waiting
on you holds it open, because a prompt that fades out is one you can neither
read nor answer, and the hold timer alone would expire through a long tool
call that prints nothing.
Asking for HUD mode from another tab used to just raise whatever the HUD
already had, so the conversation you were looking at never arrived. Main
now retargets the window and tells every renderer where it is pointed, so
the toggle keeps reading "switch" rather than "dismiss".
The remote interceptor rebuilt session/messages requests from pathname
only, silently dropping limit/offset/order. Against a paginating remote
backend, getAllSessionMessages would refetch the same default page until
the safe-load guard threw, breaking export/artifacts/branch for remote
sessions over one page.
A transparent, frameless, always-on-top window that renders the real chat
surface, so its composer is the app's composer rather than a lookalike that
drifts. Main owns the window, its remembered geometry, and click-through.
Leaving is a handoff, not a window close. The gateway binds a session's event
stream to exactly one socket, so a turn started in the HUD streams only there
and the app window hears nothing — no deltas, no turn-complete, no draft clear,
and nothing to poll for mid-turn. So the app re-resumes the session the HUD
ended on, which rebinds the transport, reconciles the transcript, and picks up
an in-flight turn. Main carries the session id across, since it is the only
party that outlives the HUD's renderer.