Commit Graph

31943 Commits

Author SHA1 Message Date
Teknium d3fdcef132 fix(goal): the re-paste pointer applies only to a near-whole re-paste, and on every surface
Independent review: substring matching lost the selected objective (after a
message offering API or UI work, '/goal ship the API' and '/goal ship the UI'
kicked identically), and the gateway and TUI /goal paths still re-sent the
full goal. The rule now lives once in hermes_cli.goals.goal_kick_prompt:
the pointer replaces the goal only when the goal is >= 400 chars, contained
in the last user message, and >= 80% of it. The CLI reads the last user
message from its history; the gateway and TUI read it from the SessionDB
(fail-safe: goal sent verbatim).

Tests: a fragment that selects one option is kicked verbatim; a long goal
that is a minority of a longer message is kicked verbatim; all three
surfaces call the shared function.
2026-09-05 06:36:52 -07:00
Teknium 7e2dbcfb33 fix(goal): /goal <text> kicks the loop with a short pointer when the user's last message already carries the goal
`/goal <text>` queues the goal text as the next user turn to start the loop.
When that text is what the user just pasted (a handoff note, a plan the agent
already has), the kickoff re-sends it verbatim: in the 1,393-agent run
`/goal <2,000-char handoff>` came 16 minutes after the same note was pasted
as a message, and the agent spent 11 API calls / 6 min deciding it was a
replay, with the note duplicated in context from then on.

_goal_kick_prompt() compares the normalized goal against the LAST user
message (string or block content); when contained, the kick is a one-line
'[Goal set] Continue with the goal you were just given; there is no need to
re-read it.' Otherwise the goal text is sent as before. Both /goal <text>
and /goal draft use it.

Tests (2): a just-pasted goal (plain and block content) kicks with the
pointer; a new goal, an unrelated last message, and a goal pasted in an
OLDER turn kick verbatim.
2026-09-05 02:03:50 -07:00
Teknium f58fcc8118 chore: map contributor email for salvaged #92934 (bluefateludi) 2026-09-05 00:24:40 -07:00
Teknium 41e6b60249 test(update): fold the #97332 plan-kind cases into one invariant test 2026-09-05 00:24:40 -07:00
Teknium 1cce7c6dd8 fix(update): unmapped gateway stops no longer fail the fleet check with "no rows"
A gateway with no profile mapping (or one whose relaunch could not be armed) is
SIGTERMed and listed under "Restart manually" — by design it has no successor and
publishes no fleet-matrix row. It still counted in ``killed_pids`` and the
pre-restart snapshot, so ``_fleet_probe_expected_runtimes`` demanded rows that
could not exist and a fully successful update exited 1 with "Fleet version check
returned no rows even though gateway runtimes were expected", leaving the
fleet_restart_pending marker behind and every later CLI start warning about it.

Track the unmapped stops on the restart outcome and subtract them from the
row-predicting signals (``fleet_probe_signals``); relaunched/systemd gateways
still predict rows exactly as before.
2026-09-05 00:24:40 -07:00
liuhao1024 b8e3c5c700 fix(update): scope fleet-probe runtime expectation to gateway-kind plan records
A dashboard-only runtime plan (gateway never started) made
_fleet_probe_expected_runtimes() return True from the unfiltered
'plan.runtimes is non-empty' check. collect_fleet_versions() reports
gateway identities only, so the probe waited for rows that cannot exist,
printed the incomplete-verification warning, and exited 1 after a
successful update (#97332).

Key the plan-derived expectation on kind == 'gateway' records — the same
row-capability rule already applied to the Windows resume token (#93406)
— and update the two tests that pinned the old object() placeholder so
they pin the runtime-kind distinction. Restart-phase, killed-PID, and
pre-restart-PID signals still fail closed unchanged.
2026-09-05 00:24:40 -07:00
Teknium e17dd8b428 fix(update): protect the whole hermes_cli.update_* family from the stale-module purge
Widen the salvaged receipt protection to every updater-owned module: the
pre-update plan's ``RuntimeRecord`` dataclass (``update_inventory``) has the same
failure — the plan is built before the purge and the post-restart reconciliation
filters with ``isinstance``, so an evicted module means a fresh class, zero matches,
and an empty runtime-outcome report. One prefix rule instead of a name list that
has to be re-fixed per symptom.
2026-09-05 00:24:40 -07:00
WolveBrown fada877519 fix(update): preserve active receipt across module purge 2026-09-05 00:24:40 -07:00
kshitijk4poor 5ac75e91e2 test(tools_config): assert display/runtime parity for disabled composite toolsets
The runtime-only assertion passed on unfixed main (it exercised
model_tools, not _get_platform_tools). Replace it with the contract the
fix exists for: a toolset is listed iff at least one of its tools survives
_select_tool_names.
2026-09-05 12:21:28 +05:30
kshitijk4poor 872be057e1 refactor(cli): class-level disabled_toolsets default instead of per-site getattr
Inspection surfaces run on partially built HermesCLI instances (tests use
HermesCLI.__new__), so the attribute needs a default; declare it once on
the class next to _seeded_first_message rather than getattr at five sites.
2026-09-05 12:21:28 +05:30
kshitijk4poor b1b8ea4f7d refactor(tools_config): single-pass pruning in _prune_toolsets_stripped_by_disabled
Resolve each remaining toolset once and state the keep-rule positively
(no static tools, or at least one tool survived the runtime subtraction)
instead of building a drop-set in a second resolve loop.
2026-09-05 12:21:28 +05:30
aydnOktay e2d738f89d fix(cli): guard disabled_toolsets on partial CLI instances
show_banner/_show_status call get_tool_definitions with disabled_toolsets,
but test and early-init paths build HermesCLI via __new__ without running
__init__, so self.disabled_toolsets was missing and CI failed with
AttributeError in test_cli_context_warning.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-05 12:21:28 +05:30
aydnOktay 2494eadb39 fix(tools): align inspection surfaces with disabled composite toolsets
hermes tools --summary and CLI /tools used name-level disabled_toolsets subtraction, so disabling debugging still showed terminal/web/file as enabled while runtime stripped those tools. Prune platform toolsets after tool-level subtraction and pass disabled_toolsets into CLI get_tool_definitions calls.

Fixes #97015

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-05 12:21:28 +05:30
kshitijk4poor ec1e6d50bc test(distributions): keep two invariant tests for grouped entries
Drop the literal-key assertion (change-detector) and the seeded 500-roll
loop (leaked global RNG state; vacuous if neither member rolls). Exercise
the fallback against a throwaway distribution so the contract no longer
depends on which browser_tasks entry has the highest probability.
2026-09-05 12:21:21 +05:30
kshitijk4poor d2664e9de1 docs(distributions): document compound entries once, at the module level
Move the grouped-roll rationale into the module docstring and shrink the
data-site comment; the mechanism is already explained on the sampler.
2026-09-05 12:21:21 +05:30
falkoro 6ba09c8cc3 fix(distributions): keep browser+web_search co-occurrence at 97% via grouped roll
Independent 97% rolls for browser and search co-occur only ~94% of the
time, versus 97% when web_search was bundled inside the browser toolset
(hermes-sweeper review). Grouped "+"-compound entries roll once and
select every member together; browser_tasks now uses "browser+search".
Deterministic tests cover the hit/miss/fallback/invalid-member contracts
and a 500-sample co-occurrence invariant.
2026-09-05 12:21:21 +05:30
falkoro decacbac35 fix(toolsets): stop disabled browser from stripping web_search
web_search belongs to the web/search toolsets, but the browser toolset
also listed it as a member. Because disabled_toolsets is a strict
end-of-pipeline subtraction (#17309), disabling browser — a natural
choice for headless/Docker deployments with no Chromium — stripped
web_search from every session even when web was enabled.

Remove web_search from the browser composite so its membership no longer
couples web search to browser availability. The browser_tasks RL
distribution kept web_search via browser, so add the search toolset there
to preserve its effective tool availability.

Fixes #64503
2026-09-05 12:21:21 +05:30
Teknium b51c055a12 fix(desktop): harden window-open deny and cover the link-title window
Follow-ups on the cherry-picked handler:
- a throwing observer can no longer change the decision; the handler returns
  an explicit deny regardless of logging failures
- the denied-URL log carries origin only, so query tokens / signed URLs from
  attacker-controlled content never reach the persisted desktop log
- the hidden link-title window (loads arbitrary user-linked pages on render,
  had no window-open handler at all) now denies too
- tests trimmed to two invariants (proven red against the pre-fix shape)
2026-09-04 23:19:11 -07:00
teknium1 77ca6a6d12 fix(desktop): deny window-open side-effect opens (GHSA-9f4c-93c8-jc8g)
setWindowOpenHandler opened details.url as a side effect before denying.
Per GHSA-9f4c-93c8-jc8g (CVE-2026-70608, High 7.2), a sandboxed iframe
with no allow-popups and no user gesture can reach this handler via the
OpenURL path -- and the desktop renders untrusted artifact HTML in
<iframe sandbox="allow-scripts">. A malicious artifact could therefore
force the OS browser to an attacker URL with zero interaction. Electron
ships no fixed 40.x release (fix is 41.10.3+/42.0.1), so we close it at
the seam, version-independently.

- electron/window-open-policy.ts: pure decideWindowOpen (always deny) +
  createWindowOpenHandler(onDenied) that denies and never opens a URL;
  the hook is logging-only.
- main.ts: wireCommonWindowHandlers uses it (covers primary + all
  secondary/quick windows); the deny is logged, no side-effect open.
- Trusted external links are unaffected: they already route through the
  audited hermes:openExternal IPC channel (openExternalUrl, http/https/
  mailto allowlist). Converted the one remaining bare window.open on the
  Electron path (env-var docs menu) to openExternalLink; other
  window.open sites are bridge-absent web fallbacks.
- tests-js/window-open-policy.test.ts: 4 tests pinning always-deny, the
  logging-only hook, and that a throwing hook never degrades to allow.
2026-09-04 23:19:11 -07:00
Sohom Sahaun 1e69c12b64 feat(slack): interactive Block Kit model picker for /model
Cherry-picked from PR #101542 by @sohomsahaun.

Two-step drill-down: provider static_select → model static_select,
with Back/Cancel buttons. Resolves through the same gateway callback
as Discord/Telegram, so persistence and failed-switch rollback
semantics come for free.

Changes:
- send_model_picker() posts Block Kit provider/model picker via chat_postMessage
- _handle_model_picker_action() dispatches provider/model/back/cancel actions
- _model_picker_state bounded dict (cap=100, workspace-scoped keys)
- Expired-state UX: missing state rewrites message to expiry notice
- Failure header: both exception and gateway error-prefix get 'Failed' header
- Index-based option values avoid Slack's 75-char value cap
- 21 tests covering send, action dispatch, gateway integration

Co-authored-by: Sohom Sahaun <ssahaun19@gmail.com>
2026-09-05 11:05:17 +05:30
kshitijk4poor f5832f81ed test(streaming): drive the Relay finalizer through its collector
The chat_completions finalizer now reads collector-observed chunks, not the
consumer loop, so the test feeds the captured on_chunk before calling it —
the same ordering Relay guarantees.
2026-09-05 10:54:19 +05:30
kshitijk4poor 74de0fd4fe fix(relay): build the chat-completions Relay response from collector-observed chunks
Relay invokes its finalizer as soon as the provider stream ends, concurrently with
Hermes' consumer thread, so any finalizer that reads the consumer loop's closures
races the last chunk. #103104 moved `usage` onto the collector; the same race still
truncated the final tool-call `arguments` delta, dropped the last content delta, and
downgraded `finish_reason` to "stop" in Relay's annotated response (reproduced ~50%
of runs). Every other Relay integration (Anthropic, Bedrock, Codex) already
rebuilds from collector-observed chunks; this makes chat_completions match via
`_RelayChatAccumulator` and removes the single-field nonlocal stopgap.

Test: the ordering harness is shared, and a second invariant test pins the
tool-call/finish_reason case (fails on the stopgap, passes here). Hermes' own
returned response was never affected.
2026-09-05 10:54:19 +05:30
mnajafian-nv 2cf5e68c5d fix(relay): retain usage from trailing chat completion chunks
Signed-off-by: mnajafian-nv <mnajafian@nvidia.com>
2026-09-05 10:54:19 +05:30
Austin Pickett 79445a496c fix(desktop): macOS parent-death watchdog treats ps: marker drift as inconclusive (#103172)
* fix(desktop): treat a macOS ps: parent-marker mismatch as inconclusive, not death

`ps -o lstart=` is a TZ/locale-rendered wall-clock string. Electron caches it
once per app lifetime; the backend re-renders it per spawn. A timezone change
(travel, or the DST boundary) while the app stays open makes the SAME instant
differ byte-for-byte, and _is_serve_orphaned() treated that as proof the parent
died -- os._exit(0) ~5ms after HERMES_BACKEND_READY, silently, on every respawn
until a full quit.

Route mismatches through _parent_start_marker_mismatch_is_conclusive(): linux:/
win:/winms: machine markers stay conclusive (PID-reuse defence intact); any ps:
side degrades to the PID-liveness check the legacy Desktop path already uses.
Log one warning before os._exit(0) so the exit is no longer traceless.

Fixes #95693
Fixes #93958

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

* fix(desktop): reject truncated ps: parent markers; blank marker env means absent

A ps: marker that got whitespace-split in env plumbing (`ps:Sat`) passed
_valid_parent_start_marker and armed a watchdog that could never match, so
the backend exited 0 right after HERMES_BACKEND_READY. Require a full
lstart value (>=4 tokens, a 4-digit year, a time). Treat empty
HERMES_PARENT_START_MARKER / HERMES_PARENT_NONCE as absent so a blank
inherited value degrades to PID-only tracking instead of disarming or
misfiring.

* chore: map contributor email for drewTuzson

* fix(desktop): log when the parent-death watchdog disarms on an unusable marker

Disarming is the fail-safe branch (the backend keeps serving) but it also
means this backend will never reap itself when the Desktop dies. Leave one
warning naming the rejected marker so that downgrade is not traceless.

* test(desktop): live macOS proof that a TZ-drifted ps: marker no longer kills the backend

Runs the real start_server in a subprocess against the host ps, with the
marker rendered under Europe/Paris and the backend under America/New_York.
Asserts READY + still alive after several watchdog polls, then that the
backend still exits once the stand-in parent is killed. Fails on main with
the reported symptom (exit 0, live parent); macos_only lane.

---------

Co-authored-by: ygd58 <buraysandro9@gmail.com>
Co-authored-by: Ahmett101 <Ahmett101@users.noreply.github.com>
Co-authored-by: Drew Tuzson <drew.tuzson@uqual.com>
2026-09-04 20:24:51 -04:00
Gille a99340c247 fix(dashboard): prevent PTY input from blocking event loop (#93565)
* fix(dashboard): prevent PTY input from blocking event loop

* fix(win-pty): don't terminate a healthy ConPTY on write cancellation; log leaked write workers

Review follow-up to the backpressure fix.

CancelledError on WinPtyBridge.write() ran the same path as a timeout and
force-terminated the ConPTY. Cancellation means the owning socket went away
mid-write, which is the keep-alive session's normal reattach case, not a
wedged child; killing the process there defeats the PTY-outlives-socket
design. Give the in-flight write the shutdown grace window and only
terminate if it never lands.

When terminate() fails to unblock pywinpty, the worker stays parked in the
default executor. That was swallowed by a bare except; log it so a slow
thread-pool starvation is diagnosable.

---------

Co-authored-by: Austin Pickett <pickett.austin@gmail.com>
2026-09-04 20:24:24 -04:00
Austin Pickett 04fd0172cd fix(dashboard): stop the embedded TUI repainting on every OS app-switch (#103165)
* feat(pty_bridge): mark the dashboard-spawned TUI with HERMES_PTY_HOST

Ink needs to know when its emulator is the dashboard's xterm.js rather
than a native terminal, so it can drop hidden-tab recovery work that only
makes sense for emulators that coalesce output.

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

* fix(tui): skip the focus-in erase+repaint under the dashboard PTY

Ink answers a DECSET 1004 focus-in with a full clear+repaint to heal rows
a native emulator may have dropped while the tab was hidden. xterm.js fed
by the dashboard WebSocket never drops frames, and it reports focus on
every OS window blur/focus, so under the dashboard that repaint was a
visible "session reloaded" flash on every alt-tab. Keep the mode
re-assert and keep delivering the focus report to TerminalFocusProvider
(the composer hides its cursor on blur); only the repaint is skipped.

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

* fix(web): restore terminal focus after an OS app-switch

Alt-tabbing away and back lands browser focus on <body>, so Ctrl+V never
reached the composer. Pull focus back into xterm on window focus under
the same ownership rule tab activation already uses, extracted into
shouldRestoreTerminalFocus so both paths share it.

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

---------

Co-authored-by: Raymond <supere989@users.noreply.github.com>
2026-09-04 20:24:02 -04:00
Teknium f159e581c7 feat(models): add GPT-6 Astra + Astra Pro with fast/flex speed tiers to Nous Portal and OpenRouter
Six slugs land in the nous and openrouter curated lists, above the gpt-5.6 line:
openai/gpt-6-astra{,-fast,-flex} and openai/gpt-6-astra-pro{,-fast,-flex}.

Nous Portal serves the tiers as distinct slugs (verified live: each echoes its id, service_tier
default/priority/flex, cost 1x/2x/0.5x). OpenRouter serves them as ENDPOINTS of the base model
(tags openai/fast, openai/flex) and silently routes an unknown suffix to the standard tier at
standard price, so the OpenRouter profile rewrites a tier slug to its base wire model and pins
provider.only to that tier's endpoints (OPENROUTER_ENDPOINT_PINS). The base slug is pinned to
openai/azure/azure-us so default routing never lands on a flex or fast endpoint.

Provider-agnostic metadata: one DEFAULT_CONTEXT_LENGTHS entry (gpt-6-astra: 1,050,000, live on
OpenRouter for both models; substring-matches -pro and the tier suffixes). Pricing is skipped:
both routes bill via official_models_api. Reasoning floor not added (no evidence of long thinks).
2026-09-04 15:58:59 -07:00
hermes-seaeye[bot] 71f8c60f6a fmt(js): npm run fix on merge (#103240)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-09-04 22:20:13 +00:00
Hao Zhe 64534b4dd4 fix(desktop): use shared tooltip for listing gallery 2026-09-04 15:14:49 -07:00
Brooklyn Nicholson b0ab2e163a test(desktop): pin that a multi-tab zone can still hide its strip
The resolver's own suite only ever asserted `never` against a LONE pane, so
the whole-stack stranding check passed every existing case while Hide tabs was
dead in the one zone users keep tabs in. Adds the missing count: an explicit
`never` is honored once a closeable tile has a neighbor to cycle to, and those
same stacks still paint their strip on auto (the rung resolves before `mode`,
so a stack it stops claiming has to reach the auto rule).

Verified as a tripwire — reverting the resolver fails the first case.
2026-09-04 15:01:10 -05:00
Brooklyn Nicholson 2c52945974 fix(desktop): Hide tabs works again in a zone holding more than one tab
The reachability rung that outranks an explicit `never` ran over the whole
stack: any closeable `placement: 'main'` pane pinned the strip visible at any
tab count. Session tabs and bot chats are exactly that, so one session tab in
main was enough to make Hide tabs a no-op — the zone menu row and ⌘⌥T both
wrote `tabStrip: 'never'` and the resolver overrode it on the next line.

Stranding is about the last handle, not about tabs existing. A stack of two or
more answers tab cycling and ⌘1…⌘9, so hiding its strip costs chrome and no
handle. The tile and tool-panel rungs now apply to a lone pane; hide-only
chrome (sessions / Bots) keeps firing at every count, since it has no close
verb and its Show/Hide rows live on the strip itself.
2026-09-04 15:01:10 -05:00
Brooklyn Nicholson 4b30b91756 Teach the desktop agent the listing fence
One clause in the desktop platform hint, covering the field shape and that
it applies to every property presented — including follow-ups and
re-rankings, so a shortlist stays comparable instead of decaying into prose
after the first answer.
2026-09-04 15:00:43 -05:00
Brooklyn Nicholson cb6cb74696 Keep renderer-owned fences out of artifact promotion
Artifact detection runs before the rich-fence registry, so a fence long
enough to look like a code document was promoted to a code card and never
reached its own renderer. A multi-listing set crosses that threshold
easily, which made the same content render two different ways depending on
how many properties it held.

`listing` joins the languages that are never artifacts, next to `mermaid`
for the same reason.
2026-09-04 15:00:43 -05:00
Brooklyn Nicholson 9b16719e17 Render property listings as browsable transcript cards
Adds a `listing` fence to the rich-embed registry: JSON in, a native card
out — photo mosaic, address and price, specs, and the catches worth
verifying before a tour.

The rental portals publish no embeddable surface (public APIs deprecated or
broker-gated, no oEmbed, X-Frame-Options on every listing page), so a URL
cannot be framed. The data the agent already gathered can be, and the
portals' image CDNs hotlink fine — so the card is authored rather than
fetched, alongside the existing mermaid and svg fences.

Parsing treats every field as untrusted model output: one object, a bare
array, or `{listings: []}` all work, beds accept `3` and `"3.5"`, non-http
URLs are rejected so a card cannot smuggle a javascript: href, and lists
are capped. Anything unparseable falls back to the plain code block.
2026-09-04 15:00:43 -05:00
abundantbeing 6b1e12c7f4 feat(desktop): drag to create sessions from New session, project + controls, and profile groups 2026-09-04 14:57:37 -05:00
Brooklyn Nicholson 96e1e3f921 feat(desktop): Message Bubble transparency slider in Appearance
One 0-100 lever beside Window Translucency: 0 keeps your bubble solid (the
default), 100 leaves only the outline. The store paints a single root var,
--user-bubble-keep, that --dt-user-bubble mixes in, so the read-only bubble
and the inline edit composer follow together and an untouched lever yields
byte-identical CSS. Reuses TranslucencySlider (with its overlay peek), has a
Cmd-K search entry, and copy in all six locales.
2026-09-04 14:40:54 -05:00
Brooklyn Nicholson 59302ece0d fix(desktop): transcript timestamps were painted at 30% ink in both modes
TimelineTimestamp layered text-muted-foreground/55 on a token that is already
a 54% mix of the base ink, so the stamp rendered near 30% alpha: 2.3:1 on a
dark chrome, 1.9:1 on a light one. Point it at --conversation-scaffold-meta,
the token the tool-row stamps already use, so every stamp in the transcript
reads from one place.
2026-09-04 14:40:54 -05:00
kshitijk4poor 5a0b1ba766 test(local-models): assert _assign_default reaches the real assignment body
Follow-up to the salvaged fix from #102994. The quickstart leg tests
stubbed web_deps.late itself, so the resolution path that regressed
(late() -> web_server facade -> getattr) was never exercised; the PR's
proposed tests pinned the module string instead, which fails on any
future move of the symbol even when the call site is updated.

Stub only the leaf (web_server_config._apply_model_assignment_sync) so
the real late() runs, and add one invariant test: _assign_default must
call it with ("main", "llamacpp", model_id, "", "", ""). Red on base
(AttributeError), green with the fix; not tied to where the symbol lives.
Drop the docstring parenthetical that described test mechanics.
2026-09-05 00:29:42 +05:30
Leanolf aef409343d fix(local-models): point _assign_default's late-bound model assignment at web_server_config
The whole-codebase refactor moved _apply_model_assignment_sync from the
web_server facade into web_server_config, but _assign_default in
web_routers/local_models.py still resolved it through the default late()
module (web_server), which no longer carries the symbol. A local-model
quickstart therefore failed its final 'making it your default' step with
AttributeError: module 'hermes_cli.web_server' has no attribute
'_apply_model_assignment_sync'.

Pass the defining module explicitly to web_deps.late() so the call
resolves against hermes_cli.web_server_config, matching how the sibling
models.py router imports the same helper. Update the two quickstart test
stubs to accept the module argument the real late(name, module=...) takes.
2026-09-05 00:29:42 +05:30
Nacho Avecilla a6e10e693f fix(gateway): stop dropping the first message after an Azure agent sleeps (#99736)
* feat(gateway): let NAS broker the scale-to-zero suspend where the guest has no lever

* fix(gateway): require the going_idle ack and outlast the broker before suspending

* fix(gateway): release the redial hold on every path that abandons the suspend

* fix(gateway): hold the re-dial only for the brokered lever, and require it

* fix(gateway): make the re-dial hold contract real and fence the in-guest suspend too

* refactor(gateway): share the watcher-iteration and descriptor helpers across the sleep tests

* refactor(gateway): move the sleep tests' repeated setup into their fixtures

* refactor(gateway): one docstring line per sleep test, and parametrise the abort and lever paths

* fix(gateway): fence the freeze gap, cool down aborts, and stop clobbering a shutdown drain

* fix(gateway): slice the Fly freeze fence on the wall clock and widen it for larger machines
2026-09-04 15:41:27 -03:00
hermes-seaeye[bot] 59d330fd15 fmt(js): npm run fix on merge (#103110)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-09-04 18:28:32 +00:00
Brooklyn Nicholson 1d1411df9c fix(desktop): keep the saved skin name until the registry can resolve it
normalizeSkin ran at boot, before the gateway seeded backend skins, and flattened an unresolvable name to the default for the whole session. The connect-time seed is apply: false by design, so nothing repainted. Keep the raw pick in provider state and normalize against the live registry instead.
2026-09-04 13:19:19 -05:00
Brooklyn Nicholson 413d5ab863 fix(desktop): cache backend skins so the boot paint can resolve the saved one
$backendThemes only lived in memory, so a relaunch on a skin from ~/.hermes/skins painted the default until the next skin.changed. Hydrate the registry from localStorage like user themes, and write it back on every change.
2026-09-04 13:19:19 -05:00
Brooklyn Nicholson 9403ad2656 refactor(desktop): share the theme shape check between registries 2026-09-04 13:19:19 -05:00
Brooklyn Nicholson 433f719676 fix(desktop): tab ✕ fade lands on the visible field under Glass
Under Window Translucency → Glass, the glass block sets the surface
tokens transparent so <body> is the sole painter. A tab's `--tab-face`
read those tokens, so the ✕ runway resolved to transparent→transparent
and the chip's `bg-(--tab-face)` painted nothing: hovering showed a bare
✕ over un-faded text, with no gradient at all.

Add a `data-glass-field` contract beside the block's existing
`data-glass-opaque` / `data-glass-raised`: a declarer receives
`--glass-field`, the same body mix glass already paints. The tab prefers
it and falls back to its own surface token (`--tab-surface`, which
TAB_ACTIVE / TAB_IDLE now set instead of `--tab-bg` so they feed the
chain rather than bypass it). Face, hover-darken, and the selection wash
all inherit the one base. Inert with glass off: the fallback resolves to
the exact opaque surface it did before.
2026-09-04 13:09:07 -05:00
Brooklyn Nicholson ff75ff2a18 fix(desktop): closeable tab labels clip so the ✕ fade has text to dissolve
The label span used `truncate`, whose ellipsis stops the text ~8px short
of the overlay. The hover gradient then faded over empty tab surface —
surface to the same surface, invisible by construction — and the ✕ read
as floating in a gap beside three dots.

A closeable tab publishes `data-closeable`; the label reads it via
`group-data-[closeable]/tab:text-clip` so overflow runs under the fade
and the gradient dissolves real letterforms. Vertical rails and
uncloseable tabs have no ✕, so they keep the ellipsis.
2026-09-04 13:09:07 -05:00
Brooklyn Nicholson 87e78d668b fix(desktop): pane tab ✕ costs a min-width floor, not 36px of dead padding
#96880 reserved `pr-9` on every closeable tab so the hover ✕ could never
sit over a label — 36px of empty runway at rest on every tab, nearly
doubling FILES (40→76px) and taxing tabs whose label already cleared the
overlay by a mile.

Restore the overlay as designed (✕ paints over the label's right edge,
gradient-blended, zero layout shift) and protect short labels with a
`min-w-13` floor instead. Sized from the chrome it has to survive: 8px
label inset + the ~19px opaque chip, so the shortest labels clear the
chip and pass only under the translucent fade. A floor bills only tabs
that need it; TERMINAL, BROWSER, NEW CHAT pay nothing.

The test that pinned `pr-9` by class string is rewritten as a set-diff
contract: adding `onClose` must add a `min-w-*` and never a `pr-*`.
2026-09-04 13:09:07 -05:00
Brooklyn Nicholson d353931dd9 test(desktop): assert tooltip labels stay in inline flow, not inline-flex
The preview-row and terminal-rail tests pinned the label's `inline-flex`
class — the very shape that broke multi-line tips. Assert the invariant
instead: no flex box under the decoration, and the preview row splits its
two lines with a hard break.
2026-09-04 12:51:08 -05:00
Brooklyn Nicholson a22b295dbd fix(desktop): paint every line of a multi-line tooltip, not just the first
Tip's chip is an inline span with `box-decoration-break: clone`, so its
background only paints the line boxes of inline flow. The #62022 hardening
forced every label child to `inline-flex`, which is an atomic inline: it sits
on the baseline and hangs any extra lines below the chip, dark-on-dark. The
preview status row's two-line label (target path + click hint) showed the
path lit and the hint invisible, and long paths wrapped into unreadable rows.

Force label children `inline` instead — still no block collapse, and wrapped
content stays in flow. TipHintLabel and the preview row drop their flex
boxes for plain inline text (a `<br />` for the hard break).
2026-09-04 12:51:08 -05:00
Teknium 13e72fb205 Merge pull request #102986 from NousResearch/fix/browser-exec-packaged-chromium
browser_exec local mode drives the packaged Chromium instead of the user's Chrome
2026-09-04 08:42:27 -07:00