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.
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.
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.
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.
* 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.
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.
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.
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.
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.
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.
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>
* 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>
* 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).
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.
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 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.
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.
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.
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.
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>
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>
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.
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.
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.
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.
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.
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.
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.