Skip to content

test(computer-use): cover native Windows and macOS registry dispatch - #109505

Closed
TheSmokeDev wants to merge 55 commits into
NousResearch:hermes/hermes-b802e898from
TheSmokeDev:fix/bot-screen-native-dispatch-ci
Closed

TheSmokeDev wants to merge 55 commits into
NousResearch:hermes/hermes-b802e898from
TheSmokeDev:fix/bot-screen-native-dispatch-ci

Conversation

@TheSmokeDev

Copy link
Copy Markdown

What does this PR do?

Adds native Windows/macOS registry-dispatch coverage for Bot Screen's computer-use integration. This is a test-only follow-up to #108914, targeting hermes/hermes-b802e898 at d65af42305c4; it does not reimplement the fcntl fix already integrated in f89e71e051.

The existing missing-fcntl subprocess test verifies lease operations and imports. These additional cases exercise capture, click, typing and window discovery through the real registered computer_use handler on the actual OS, using the existing harmless noop backend.

Related Issue

Complements the fix for the Windows regression reported by @helix4u and corroborated by other reviewers. #109446 was also checked for overlapping coverage.

Type of Change

  • Tests

Changes Made

One new file: tests/tools/test_computer_use_native_platform.py.

Cases carry windows_only and macos_only markers, so the existing OS workflow's file scanner and marker selector discover them. No production code, configuration or workflow changes.

How to Test

scripts/run_tests.sh tests/tools/test_computer_use_native_platform.py tests/tools/test_bot_desktop_lease.py -k 'native_computer_use or lease_works_without_fcntl' -j 2 --file-retries 0

On native Windows at d65af42305c4: 5 passed, 0 failed, 4 macOS cases skipped. This includes all four new registry cases plus upstream's missing-fcntl test. Both OS file-scanner selections include the new file. Changed-file Ruff and diff checks pass.

Earlier native Windows registry probes reproduced the original fcntl crash at d947fc83a461. The final coverage now passes against the integrated fix. macOS execution belongs to its CI lane; no live screen/driver or full-suite result is claimed.

SmokeDev

teknium1 and others added 30 commits September 12, 2026 18:15
…with human take-over

A bot running on a headless Linux gateway now gets its own desktop (TigerVNC
Xvnc + a minimal Xfce session, one per profile) that Hermes Desktop streams
live. The user can watch the bot work, take over to type a login / 2FA code /
CAPTCHA, and hand control back; the bot refuses every computer_use action
(screenshots included) while a human holds the screen, then resumes with the
session cookies the human just created.

Why this shape:
- The screen lives on the machine Hermes runs on, not in a vendor cloud browser,
  so it works for any app the bot drives and keeps the session on the user's host.
- Xfce components are launched individually (xfwm4, xfce4-panel, xfdesktop,
  xfsettingsd) under a private dbus session rather than xfce4-session/the
  metapackage: no screensaver, power manager or polkit agent to lock or prompt
  a headless desktop.
- Transport is raw RFB over a WebSocket beside /api/ws, authenticated with a
  one-shot ticket minted through the already-authenticated RPC channel; noVNC
  runs in the Electron renderer. The bridge parses the RFB client stream and
  drops keyboard/pointer/clipboard (incl. QEMU Extended KeyEvent, which noVNC
  switches to once Xvnc advertises it) from any viewer that does not hold the
  lease, so viewOnly is enforced server-side, not by the client.
- One lease per profile (agent | human viewer) is the single truth for the RFB
  bridge, the computer_use tool and the Desktop UI; taking control evicts other
  viewers' input with close code 4000 control-taken.
- Auto-start happens only at the computer_use tool boundary (headless host,
  packages present, bot_desktop.auto_start=true); env builders stay pure so
  status probes and tests never spawn X servers. tests/tools/conftest.py pins
  the binaries to "missing" for the same reason the browser-use fixture does.

Surfaces: Desktop (Bots → right-click → Open Screen; Take over / Hand back),
CLI (`hermes computer-use screen status|start|stop|install-deps`), tool
(`computer_use` actions request_handoff / wait_for_human), gateway RPCs
(display.status/start/stop/observe/lease.acquire/lease.release + display.lease
event), docs page user-guide/features/bot-screen.
After a server-side eviction (4000 control-taken) or stream loss noVNC has already
torn the client down; detach() then called disconnect() on it and noVNC logged
"Tried changing state of a disconnected RFB object". Clear the ref in the
disconnect listener so teardown only touches a live client.
The declaration lived in src/types/novnc.d.ts, which .gitignore drops
(apps/desktop/src/**/*.d.ts is ignored except for an allowlist), so the
pushed tree failed typecheck with TS7016 while the local worktree passed.
An ambient 'declare module' needs a script-scoped declaration file, so it
joins vite-env.d.ts rather than the module-scoped global.d.ts.
…kage install from Desktop

Three ways in, one install path, no shell required.

- Screen portal box: a compact card ("Screen · Live / Stopped / Not installed on
  host", who holds control) rendered at the top of a bot's Scheduled Jobs pane
  above the routines, and under each gateway/profile group header in the
  Sessions sidebar (new `sidebar.gatewayGroup.header` contribution area, so
  the plugin owns the box and core only exposes the slot). Clicking it opens
  the Screen pane; it reads the same display.status/lease events as the pane.
- Install from Desktop: `display.install` runs the distro package command on
  the gateway host (apt/dnf/pacman) as a supervised child, streams
  `display.install.log`, and finishes with `display.install.done`. When sudo
  needs a password the host raises `display.install.sudo.request` on the
  caller's own WebSocket; the renderer shows the existing masked SudoDialog
  (pointed at `display.install.sudo.respond`), so the password never touches
  the renderer store as plaintext beyond the field, is redacted from the
  gateway trace like `sudo.respond`, and an empty answer cancels without
  spawning the package manager. One install per profile at a time. The pane's
  "packages missing" state is now an install card with the command shown for
  the shell-inclined.
- Opt-in stays intact: nothing installs on `hermes update`; the only triggers
  are the Desktop button and `hermes computer-use screen install`.

Why the SudoDialog fallback to the app-level card: the install belongs to the
connection, not to a chat turn, so the request has no session id; the focused
chat's dialog now also shows the session-less card instead of it dying unseen.

Live (headless Electron via CDP, `hermes serve` with the packages hidden):
portal in Scheduled Jobs → click → Screen pane → Install on host → sudo card →
Cancel → "Install cancelled" and the card returns; with the packages present:
portal → Start → live stream, portal flips to "Live · bot in control"; the
same portal renders under the `default` profile in the Sessions sidebar.
…ed Jobs pane

The bot's computer is now the first thing in its pane: a big 16:10 box above the
title that shows an actual picture of the desktop, refreshed every 4 s while the
hero is on screen (paused when scrolled away or the window is hidden). Caption
carries who holds control; the chip reads Open live / Start / Install. Clicking
the picture expands into the live Screen pane where the user can take over.

- `display.thumbnail` RPC: one JPEG grab of the profile's Xvnc display via
  Pillow's ImageGrab (XAUTHORITY from the launcher's published env), scaled to
  <=960x600, returned as a data URL. Read-only: it never touches the lease, so a
  human in control is not disturbed and the bot is not blocked.
- `ScreenHero` replaces the small portal row in the routines pane; the compact
  `ScreenPortal` stays for the Sessions sidebar group header where a 16:10 box
  would crowd the list.

Live: hero "Screen is off" → click → pane → Start → hero shows the Xfce desktop
with "Live · bot in control"; an xmessage window opened on the bot's display
appeared in the hero on the next refresh; clicking the hero opened the live pane
(canvas + Take over).
… theme, curated dock

The vendor Xfce panel layout (copied only to silence the first-run dialog) put a
light grey bar with dead launchers on every bot screen: File Manager and Text
Editor pointed at Thunar/Mousepad we deliberately do not install, Web Browser
opened exo's "pick a browser" dialog. It read as an unconfigured VM, and so did
the thumbnail in Desktop.

- Wallpaper: `tools/bot_desktop/wallpaper.png` (the Nous gradient), seeded via
  xfconf as a zoomed backdrop over the existing colour fallback.
- Dark theme: first dark GTK theme the host ships (Adwaita-dark, Breeze-Dark,
  Greybird-dark, Arc-Dark, else Adwaita), dark icon theme likewise, xfwm4 Default
  decorations, DejaVu fonts; both panels dark with slight translucency.
- Own panel layout: top bar = menu · task list · tray · clock; bottom dock =
  only launchers whose program exists on this host, the browser pinned to the
  one the bot drives (chrome → chromium → firefox) so a human who takes over
  lands in the bot's own browser profile. Anything the user installs still
  appears in the Applications menu; the dock is the only curated part.
- `launcher.sh` and the wallpaper declared as package data (the launcher was
  already missing from sealed wheels).
- `HERMES_BD_SEED_ONLY=1` stops the launcher after seeding so the config tree is
  testable on a fake PATH without an X server.

Live: fresh screen shows the gradient, dark panels, two dock icons; XTEST clicks
on the dock opened xfce4-terminal and Chrome, both dark-themed and listed in the
task bar; the Desktop hero and Screen pane show the same picture.
…ight actions, sudo reply pinned to origin, dock Browser is the bot's browser, safe display reuse, install keeps profile scope

Independent review of the PR head found the control boundary only held inside
one process and several claims the code did not back. Each item below was
reproduced, fixed, covered by an invariant test proven red without the fix, and
re-verified live on a real Xvnc/Xfce screen.

- Lease authority on disk. `lease.json` under an fcntl lock in the profile's
  bot-desktop dir; every read goes to the file. `hermes serve` (viewer bridge),
  the messaging gateway, a CLI turn and isolated workers now agree. Live: a
  takeover in process A made `computer_use capture` in process B return
  human_has_control; release in C made B work again.
- Takeover fences admitted actions. `handle_computer_use` re-checks the lease
  under the dispatch lock and discards a result produced after the lease epoch
  changed, so an action admitted before a takeover cannot picture what the human
  typed during approval / backend start-up waits.
- Sudo reply pinned to its origin. `SudoRequest.origin` records the
  (connection, profile) the card came from; SudoDialog answers through
  `requestGatewayForAgent` on that socket, never the foreground gateway. A
  password typed for host A can no longer reach host B. `sudo.expire` and
  `display.install.sudo.expire` now tear the card down (the Desktop never
  handled sudo.expire).
- Dock Browser IS the bot's browser. `tools/bot_desktop/browser.py` resolves one
  identity — the Chromium agent-browser drives + a persistent per-profile
  user-data-dir (`bot-desktop/browser-profile`) — and both sides use it: the
  agent env gets AGENT_BROWSER_EXECUTABLE_PATH / AGENT_BROWSER_PROFILE, the
  dock launcher gets the same exe + --user-data-dir. Live: the bot wrote
  localStorage on http://127.0.0.1:8765 through agent-browser; a dock click and
  a typed URL on the screen showed BOT-WROTE-THIS in Chrome for Testing.
- Safe display reuse. Allocation under a host-wide lock; a recorded number is
  reused only when no live server holds it; the launcher never unlinks a lock
  whose pid is alive. Live: A stopped, B took :20, A restarted on :21, B kept
  running.
- Install worker keeps the caller's profile scope (copy_context carries the
  HERMES_HOME override and the transport); the done event carries the requested
  profile's status.
- Honest scope: `bot_desktop.auto_start` defaults to false (opt-in; Start lives
  in the Screen pane); request_handoff no longer claims a Telegram/Discord
  message was sent — the model relays the ask in its reply; docs match.
…ames marked, lease fails closed

Second review round (@Julientalbot):

- Desktop `display.*` listeners (lease, install log/done) match on (connectionId, profile_key),
  not the profile path alone: two hosts with the same ~/.hermes path no longer repaint each
  other's screen pane or install card. One predicate, `isEventForBotScreen`.
- Hero thumbnail: three consecutive failed refreshes dim the last frame and caption it
  "Last seen — screen unreachable"; a dead gateway never keeps looking live.
- Lease file present but unparsable reads as HUMAN holds (fail closed); only a missing file is
  a fresh agent-held profile. The next successful write repairs it.
- Taking over after `request_handoff` keeps the agent's reason on the lease and the pane shows
  it while the human acts, not only before they clicked Take over.
computer_use imports tools.computer_use.handoff (and the lease) on every
non-empty action, so the module-level `import fcntl` made every desktop
action raise ModuleNotFoundError on native Windows / fcntl-less hosts.
The import is now optional; the lease file semantics are unchanged and
only the cross-process lock degrades to a no-op where no multi-process
Bot Desktop exists.

(cherry picked from commit 82ca07dda5d73741a5d6ac60f7b596dd156ec20c)
…ib modules

fcntl/pwd/grp/termios/resource/pty/tty fail at import time on Windows and
take every importer down with them; the lease regression slipped through
because no rule covered this class. Honours the existing
`# windows-footgun: ok` marker; scoped to unindented imports so lazy and
try/except ImportError forms pass.

(cherry picked from commit aa625e7d2649dbe4e4357da712e492cb97f66acc)
…cribed

The Event was set before the import and on_change() ran, so a failure
there left the flag set and no listener ever installed: every later
lease transition would go unbroadcast for the life of the process.

(cherry picked from commit c5d43c620fc9ea1428fa546b079ceeca6dbc7ef3)
Real-profile local sessions attach over a loopback cdp_url, yet that
Chrome is launched with the Bot Desktop DISPLAY, so it is the very
browser a human who took over is typing into; keying the fence on "no
cdp_url" let every command through. Fence whenever the session carries
the `local` feature and exempt only remote/cloud/user-supplied CDP. Also
fence while a human holds the lease even when the published DISPLAY is
gone (dead Xvnc), matching computer_use instead of silently unfencing.

(cherry picked from commit 7b8825bf32a67229666f1f848d3ac171ff3fbc93)
…ent to aux vision

The epoch check ran only after _dispatch() returned, by which point the
capture path had already written the PNG to the media cache, spilled the
element tree to disk and routed the frame through auxiliary vision — the
human's screen had left the process before being "discarded". A fence
callable is threaded into _dispatch; capture and capture_after call it
the moment backend.capture() returns, and the post-dispatch check stays
for every other action.

(cherry picked from commit 70c09e5fad89d2817f13aea772bd845ab7cc4c8a)
…e wrong shape

`[]`, `null` or `5` parsed fine and then raised AttributeError outside
the except tuple; `{}` or an unknown holder read as "agent holds". Any
of these is a torn or tampered file, and an untrusted lease must never
let the agent act on a screen a human may be using.

(cherry picked from commit dd77806ab619fcd55ce45698c5f3d739fdd63fa9)
… lease

The Desktop polls thumbnails on a timer, so every connected client kept
receiving frames of the screen a human had taken over — the same frames
computer_use refuses to capture. Answer {data_url: null, suppressed:
'human_has_control'} without touching the framebuffer.

(cherry picked from commit 9bb968026fe6563f793c4fb6aa67ce0577bc881e)
…ks another viewer's lease

lease.release(None) skips the holder check, so a client that lost its
viewer id (or any bare RPC) could take control away from whoever held
it. Refuse with code viewer_mismatch unless params.force is set;
display.stop and the CLI keep their unconditional release.

(cherry picked from commit ae86da0a8d83ee60b538efb9f545d36213a3228c)
…wers the handoff

wait_for_release polled until the full timeout (10 min by default) even
when the holder never left AGENT, so an unseen request_handoff blocked the
turn instead of letting the model chase the user in chat. After `grace`
seconds (param, default 60, capped at the timeout) with the handoff still
pending and no human holding, return {ok: false, code: no_takeover}. Once
a human holds the screen the full timeout still applies to the hand-back.

(cherry picked from commit 1402af036044a7191ec885fc8fa5c24206880909)
/api/ws consumed any ticket and stamped its identity; display.observe mints
provider "bot-desktop" tickets for viewers who may only be watching a screen,
so one of those redeemed on /api/ws became a full authenticated session.
Refuse bot-desktop tickets in _ws_auth_reason (reported as ticket_invalid, same
as the display route refuses gateway tickets).

(cherry picked from commit 753c3c35975fe1efab4a5bbc8bc6bbc9532b8521)
Reject oversized positive and extended clipboard lengths at the header. Process coalesced WebSocket data in bounded slices without rejecting valid multi-message frames. Includes fragmented-header and boundary regressions.

Addresses the clipboard finding reported by carlotestor and corroborated by helix4u and other reviewers on NousResearch#108914.

(cherry picked from commit b1fbe363266e6d6fa3d73d2605fe1ca383607859)
The header check alone removes the unbounded buffering: every other client
message type is fixed-size or bounded by a 16-bit count / one byte, so once
ClientCutText is capped nothing can grow _buf past ~256 KiB + a partial frame.
The feed()/_feed() slicing wrapper was a second layer over the same fact and
its coalesced-frame test exercised behaviour the base parser already had.
Rename the test file into the rfb_filter family.

(cherry picked from commit b9bfa7de4c7e968347923d109c3ae68a66ceb37d)
…akeover

_bridge remembered "this viewer held control at some point" and never forgot
it, so after a hand-back to the agent a takeover by another human closed the
ex-holder's socket with 4000 control-taken although they were a plain watcher
by then. The eviction rule is now _should_evict(held, lease, viewer_id): a
lease held by the agent clears the memory; only a takeover while this viewer
still held evicts.

(cherry picked from commit 178abd0f87f980e603717b1eca39e4344816f7aa)
noVNC's code-less socket.close() and some proxies both surface as 1005 on the
server, so the bridge cannot tell a deliberate pane close from a drop and must
keep the lease. The Desktop closes with an explicit 1000 on unmount; this row
pins the server side of that contract.

(cherry picked from commit a62a7429f2b925d93d2122b7fa7f235144b8de44)
RfbClientFilter consulted lease.viewer_may_send_input per client message,
which stats and reads lease.json on the event loop for every pointer move.
The bridge now keeps one boolean, refreshed from the in-process on_change
listener (same-process takeovers apply to the very next message) and by a
file re-read at most every 250 ms (a takeover written by another process is
seen within one interval). Test: a foreign takeover stops input in < 0.5 s
(fails with the interval set to 5 s).

(cherry picked from commit dcafcb2448c9668c80f2c449a4cda5ad95f82bab)
thumbnail_data_url swaps the process-wide os.environ["XAUTHORITY"] around
ImageGrab.grab; a multiplexed gateway thumbnails several profiles from worker
threads at once, so one grab could run with another profile's cookie (Xlib
auth failure or the wrong screen) and the restore left the wrong value
behind. A module lock serialises swap+grab+restore.

(cherry picked from commit be789c85fdbae869a31d63f30a3434f03d350a50)
…sh, not the id

display.observe took viewer_id from the client and display.status handed every
client the holder's viewer_id, while the lease authorised input and release on
string equality: any authenticated client could read the holder's id, mint an
observe ticket with it and co-drive or release their lease.

observe now mints viewer_id = secrets.token_urlsafe(16) and returns it; a
requested id is honoured only when this same connection minted it earlier (a
reconnecting pane keeps its lease), tracked per transport in a weak dict.
Every lease payload leaving the gateway (display.status/start/stop/observe
snapshots and the display.lease broadcast) goes through _lease_view, which
replaces viewer_id with null plus viewer_hash = sha256(id)[:12] so the Desktop
can still tell whether it is the holder.

(cherry picked from commit 52ff67ab409ad547c05e8a4244eb6c24513adf65)
Match the existing gateway stop contract on supported hosts, including when the desktop has already exited. Keep the Linux lease import off unsupported hosts. Native Linux test exercises the CLI parser and real persisted lease.

Addresses the CLI recovery finding from MrD1az and subsequent reviewers on NousResearch#108914.

(cherry picked from commit 0b361bf11b45ea9667369214b93919fad4c1b0f5)
…ary->package map

Fedora split xorg-x11-server-utils and xorg-x11-utils into per-binary
packages (F35) and retired the umbrellas; dnf5 refuses the whole
transaction on one unknown name, so `hermes computer-use screen install`
was a no-op on Fedora. The dnf list now names xsetroot/xset/xdpyinfo/
xprop/setxkbmap directly. xprop (the launcher's WM-ready wait) joins
REQUIRED_BINARIES and every distro list; apt gains x11-xkb-utils
(setxkbmap) and pacman gains dbus (dbus-run-session), both previously
reached only via transitive deps. BINARY_PACKAGES documents which package
ships each binary per manager so a test can hold the lists to it.

(cherry picked from commit dc5c4d70f7a0fd355fe21c96071513d8b5ae3744)
…t a pid

pid_exists alone trusts a recycled pid: after a reboot or a long-lived
gateway, an unrelated process can own the recorded number and status()
reports the screen running while stop() SIGTERMs that stranger's whole
process group. The pid file now stores "<pid> <psutil create_time>" and
_launcher_pid() requires both to match; the pre-identity single-number
format, a dead pid and an out-of-range digit string all read as not
running (the safe direction: worst case is a spurious start()).

(cherry picked from commit 2f9e0e40a764f5a6bb38c02194081dc34c4c8f0b)
whyyagswhy and others added 15 commits September 12, 2026 18:15
Send close code 1000 before noVNC can send its statusless close. Keep reconnect teardown statusless so it does not release the human lease. Cover both lifecycle paths with component tests; verify the ordering against real noVNC and Xvnc separately.

Addresses the close-code finding discussed by MrD1az, Xipong, and other reviewers on NousResearch#108914.

(cherry picked from commit 6112341f0c79c710387de66db8c309a39e02eaf0)
Use the connection/profile event predicate in the portal, never match a legacy group to a remote namesake, and reset thumbnail state on owner changes. Component tests cover cross-host events, click routing, pending/rejected thumbnails, and stale responses.

Addresses findings from Julientalbot, erosika, and BearHuddleston on NousResearch#108914.

(cherry picked from commit 65155c92a808ce5bb94d783bb0f42d6a4fc7d94f)
…the lease hash

`display.observe` now returns the viewer id this attach is known by, and lease
payloads name the holder by `viewer_hash` (sha256(viewer_id)[:12]) instead of
the raw id. The pane stores the minted id per attach ($screenState.viewer),
passes it to display.lease.acquire/release, and derives iHold (pane) and the
'human' portal tone from `leaseHeldBy` — hash compare with a raw-id fallback for
backends that still broadcast viewer_id.

Why: the client-generated VIEWER_ID constant let a reloaded window mint a new
identity while the lease still named the old one, and a raw id on the wire was
a usable credential for anyone listening on the profile's event stream.

(cherry picked from commit ad985457d185a99f23807c282a9a2eae7b496ecc)
…lds control

`display.thumbnail` answers `{data_url: null, suppressed: 'human_has_control'}`
while a human holds the lease. The hero captions that as "Hidden while someone
has control" (screen.heroSuppressed, all four locales) and treats it as a
successful refresh, so the miss counter never ages a withheld frame into
"Last seen — screen unreachable".

(cherry picked from commit 7da75f086fe30f0c7e329bba70f32b2dca283334)
… attach

The SDK disposes an INACTIVE registry-routed bot's secondary socket as soon as
its request count returns to zero, so `display.install.log/done` and the
pane's `display.lease` events had no socket to arrive on — the install card
sat at "Installing…" forever and the pane never learned about a take-over.

`retainBotScreen` feature-detects `host.retainProfile` (like group-turns).
The install card acquires it before `display.install` and releases on
done/failed/unmount; the pane holds one for the attach lifetime, released by
detach (reconnect swaps it, unmount drops it).

(cherry picked from commit 1eed9c82e27d24a63abeda835f3349a9db66ebfe)
… close code

noVNC 1.7's `disconnect` detail carries only `{clean}`; the pane was searching
a `reason` that never arrives, so the "Another viewer took control" overlay was
unreachable and a take-over looked like a clean stop. The pane now listens on
the WebSocket it hands RFB (installed before RFB's own `onclose`) and treats
close code 4000 as control-taken.

(cherry picked from commit c0ed9a4cdb0d616cc727af26fd804f7e15a9f330)
…hecking forever

A bot whose Hermes predates the display RPCs answered `display.status` with
JSON-RPC -32601; the portal swallowed it and stayed on "Checking the screen…"
for good, and the pane retried on every mount. `isDisplayUnavailable` (same
shape the session-control store recognises) marks the bot `unavailable` in
$screenState: the sidebar portal and hero render nothing, the routines-page
portal and the pane show "Update the bot's Hermes to use Screen"
(screen.portalUnavailable / screen.unavailableTitle, all locales), and no
further status probes are issued.

(cherry picked from commit 9ff358422b60e735ce2846f1e30427e15ae767fd)
…k a newer lease

Lease events and status replies race on the wire; the cache took whichever
landed last, so a status reply describing the pre-take-over lease could flip
the pane back to "Bot is in control" while a human held it. DisplayLease keeps
the backend's monotonic `epoch`; both setScreenStatus and setScreenLease drop
a lease whose epoch is below the stored one. Payloads without an epoch (older
backends) are applied as before.

(cherry picked from commit 37f8b91067f8af8f11127249f6f689547238d8af)
…er owns

After a Desktop reload the pane attaches under a fresh server-minted viewer id
while the old one still holds the lease, so the docs' "click Hand back" had no
button to click — only Take over. When holder is human and this window is not
the holder, the pane now also offers "Hand back (force)", which calls
display.lease.release with {force: true} (the server refuses a plain release
from a non-holder).

i18n cleanup: drop the unused screen.recheck key, move the hardcoded
"Update Hermes Desktop…" notice and the tab title suffix in screen-open.tsx
into the bundle (screen.openNeedsUpdate / screen.title). All four locales.

(cherry picked from commit 5cf98770f72da261a5471467170993833d15118b)
…outer

The canvas host borrows `data-terminal` so type-to-focus / bare-key shortcuts
leave the remote screen's keystrokes alone — but `closeActiveTab` reads the
same marker as "a local terminal tab is focused" and closes the user's
terminal on Ctrl/⌘+W. The keybind listener sits on `window` in the capture
phase (and on macOS the chord arrives via Electron's before-input-event IPC),
so the pane cannot intercept it locally; it now carries a distinct
`data-remote-screen` marker for the close-tab router to exclude
(`isFocusWithin('[data-terminal]') && !isFocusWithin('[data-remote-screen]')`
in app/chat/close-tab.ts — a follow-up outside this plugin's files).

(cherry picked from commit e97b5923864b03c75a0502dd3b04fc5ce2dac34c)
…re-renders

ProfileGroupScreenPortal rebuilt its synthesized RosterRow on every render, and
the portal's effects (status probe, display.lease subscription) key on that row
— so every sidebar paint re-subscribed the lease listener and re-ran the probe
guard. The row is now memoised on (connectionId, profile, roster).

The other half of the remount lives outside this plugin: gateway-groups.tsx
hands ContribRender an inline `() => render(route)` closure, and ContribRender
mounts its argument AS a component type, so the whole contribution remounts on
every group-header render. That needs a stable component per (item, route)
in ProfileGroupHeaderSlot (useMemo/useCallback), not a plugin-side change.

(cherry picked from commit c8a606f76d8fb69c21773de7024c698cfbe22732)
… lease views everywhere, backend rebind on display change, footgun annotations, grace in schema
Signed-off-by: SmokeDev <degensmoke@gmail.com>

@gaoanze888 gaoanze888 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified exact head d13f3425bb75acc9f99a1e8c2e40e8a42b98ee3a. The tests use the repository's native OS markers, dispatch through the real registered computer_use handler and lease/import path, and select the harmless noop backend rather than controlling the real desktop. Assertions cover both successful result shape and exactly one expected backend call.

On native macOS the file reports 4 passed / 4 Windows-skipped; Ruff and diff checks are clean. The OS test-file scanner recognizes both markers. This is complementary to the existing direct-handler coverage and I found no blocker.

@alt-glitch alt-glitch added type/test Test coverage or test infrastructure P3 Low — cosmetic, nice to have comp/tools Tool registry, model_tools, toolsets platform/windows Native Windows-specific behavior or breakage labels Sep 13, 2026
@teknium1
teknium1 force-pushed the hermes/hermes-b802e898 branch from d65af42 to bc36ddb Compare September 13, 2026 02:00
@teknium1

Copy link
Copy Markdown
Collaborator

Cherry-picked into #108914 with your authorship (commit c66f8d0391e1, rebased into bc36ddb5f969). Leaving this open until #108914 lands, then closing with the landed SHA.

teknium1 added a commit that referenced this pull request Sep 13, 2026
…contract; native dispatch test stubs the approver

Main gained a single typed GatewayEventMap sourced from the tui_gateway emitters
(test_gateway_event_contract), so the six display.* notifications this PR emits must be listed
there or the contract test and the desktop typecheck both fail. The native Windows/macOS
dispatch test (#109505) ran on a runner with no approver, where the approval gate blocks click/type
before dispatch; it now stubs `_request_approval` exactly like the capture-fence tests.
@teknium1
teknium1 deleted the branch NousResearch:hermes/hermes-b802e898 September 23, 2026 11:03
@teknium1 teknium1 closed this Sep 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/tools Tool registry, model_tools, toolsets P3 Low — cosmetic, nice to have platform/windows Native Windows-specific behavior or breakage type/test Test coverage or test infrastructure

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants