nativeRemovalInstructions told an 'external' Linux install with no AppImage path to delete an AppImage or app directory, which leaves the package registered with missing files.
The agent's terminal puts a sealed payload's launcher dir first on PATH even when the inherited PATH already lists it behind another install.
The macOS removal instructions name the running .app bundle and its folder instead of a fixed Hermes.app.
A manifest.json with a mistyped runtime, commands, venv or store reads as not a payload, and a missing venv or store is treated as unset, so startup no longer raises.
The existing stubs pass isFocused as an arrow function, so they cannot catch
a detached call that loses `this` (Electron then throws "Object has been
destroyed"). The new case uses a window whose isFocused reads `this`. The
fix itself is already on main.
Settings > About > Uninstall rendered nothing when the OS or a package
manager owns removal (MSIX, bundled, Nix). The summary IPC now adds the
canonical nativeRemovalInstructions text from the main process. The page
shows it, where user data lives, and on Windows an "Open Apps settings"
button backed by a fixed-URL IPC, because the generic openExternal bridge
admits web schemes only.
The uninstall strings move into en_uninstall_section.ts and
types_uninstall_section.ts, main's split i18n layout, which keeps en.ts and
types.ts under their line caps. main.ts offsets its two new lines by
deleting isActiveRuntimeUsable(), which nothing calls.
Inside a packaged desktop build, bare `hermes` in the terminal tool resolved
to <payload>/venv/Scripts/hermes.exe. That venv was built on CI, and its
Windows redirector reads pyvenv.cfg `home`, which still names the CI path,
so every call failed with "did not find executable at C:\a\hermes-agent\...".
The working launcher in <payload>/bin carries the channel name and was never
on the agent's PATH.
- activate_dependencies puts the payload's own launcher dir (from the
manifest's runtime.commands) first on PATH, and on Windows no longer adds
the shipped venv's Scripts dir: every redirector in it names the CI
interpreter.
- _resolve_hermes_bin_dir answers with that launcher dir in a sealed payload,
so terminal and cron children do not depend on PATH order.
- stageDesktopLaunchers copies the canonical launchers to their qualified
names instead of renaming them. Only the qualified names are published.
- The "plugin already exists" hint names the real CLI command.
- Every manifest-relative path (venv, store, hermes launcher) goes through one
resolve-and-contain check, and a manifest.json that is unreadable or not a
payload manifest reads as "not a payload" instead of raising, since every
install now asks.
Source and dev installs are unchanged: they have no payload manifest.
f6c15b0971's fixtures toggle all-profiles with an empty $profiles cache,
which 87e94805's effective-scope rule now treats as a hidden single-profile
preference. Seed two profiles like projects.test.ts so the suite exercises
the real all-profiles path.
One unavailable remote profile no longer holds the whole session sidebar
aggregate hostage until the 180s backend readiness deadline. Each remote's
session read now runs under its own bounded budget and settles allSettled-
style: healthy profiles' rows land immediately, the dead one contributes a
named error entry in the response payload instead of being swallowed to
null, and the underlying fetch keeps running so a slow-to-boot remote lands
on a later refresh. No sidebar visuals change — the renderer's existing
errors handling carries the failure.
Fixes#75712
Named-profile assign writes the global slot on every pick. After forge=dark then atlas=light, a never-themed profile inherits light. A focused test pins that hop. promoteUnanimousLegacy persists from stored. The write is idempotent and a no-op when records disagree.
Named-profile appearance picks now mirror into the global fallback, and a unanimous pre-existing per-profile mode promotes into an empty global slot. Bot Mode gateway hops onto never-themed bots inherit the user's mode instead of resolving system (#101216).
Co-authored-by: Cursor <cursoragent@cursor.com>
The right cluster reservation added 24px of pad before Settings.
That pad is a layout spacer with no app-region, so it ate window
drags while the empty tab strip still moved the window.
The 48px data-window-drag-handle is already the clearance; do not
mark the spacer drag (it sits under the z-70 cluster).
Fixes#131729
Replaces --ui-accent with --ui-text-secondary in model-catalog-menu,
model-visibility-dialog, and dropdown-menu. Accent color is reserved for
interactive elements; secondary text is semantically correct for static
provider-group section headers. Addresses AI code review feedback on #103493.
Replaces --ui-accent with --ui-text-secondary in model-catalog-menu,
model-visibility-dialog, and dropdown-menu. Accent color is reserved for
interactive elements; secondary text is semantically correct for static
provider-group section headers. Addresses AI code review feedback on #103493.
Replaces --ui-accent with --ui-text-secondary in model-catalog-menu,
model-visibility-dialog, and dropdown-menu. Accent color is reserved for
interactive elements; secondary text is semantically correct for static
provider-group section headers. Addresses AI code review feedback on #103493.
Voice input was record-then-upload on every surface: nothing showed while the user
talked, and the transcript arrived only after the whole take uploaded. With
`stt.streaming: true` the openai, xai and elevenlabs providers now stream microphone
PCM to their realtime endpoints while the user speaks. Partial text renders live and
the final transcript is ready at end of speech.
- tools/transcription_streaming.py: one session API (push_audio / end_audio /
finalize / cancel, an on_partial callback, a worker thread that owns the socket)
with three wires, all verified live: the OpenAI Realtime `type: transcription`
session (gpt-live-transcribe, 16->24 kHz resample since the endpoint refuses
< 24 kHz), xAI wss /v1/stt with interim results, and ElevenLabs Scribe v2
realtime. Plugin providers opt in through
TranscriptionProvider.streaming_capable / open_stream_session.
- tools/voice_mode.py: AudioRecorder taps its capture callback into the session and
parks it under the WAV path. transcribe_recording uses the live transcript and
falls back to the file on any failure or empty result, so CLI push-to-talk, the
continuous VAD loop and the TUI gateway gain streaming with no call-site changes.
- Classic CLI and Ink TUI show partial text in the input placeholder while recording
(new `voice.partial` gateway event, contract regenerated). Desktop dictation
streams PCM to the new /api/audio/transcribe-stream socket (provider keys stay on
the host, and the host resamples) and shows partial text in the dictation pill.
The recorded blob stays the fallback.
The plugin streaming contract (streaming_capable / open_stream_session /
TranscriptionStreamSession) and the transcribe-stream socket shape come from #103173.
Co-authored-by: Vinicius <timelord1025@gmail.com>
Re-check the active chat after the local-model wait and answer 'Tours only run in the session the user is looking at.' when it changed.
Move TourPreset to tools/tour_presets.py so importing the wire contracts no longer registers gui_tour.
A general look around the desktop app is now one tool call. Before, the
model ran a targets scan and wrote its own steps from raw DOM labels: two
round trips, English-only copy.
- gui_tour action='start' without steps runs the built-in tour on
surface='app' (refused on 'preview'). preset 'quick' shows four stops:
chats list, composer, new session, model picker. 'full' (the default)
adds what the interface mode puts on screen. A stop that is not on
screen is skipped. The model stop adds a line when this computer can
run a local model (the local-setup tip's fit check, capped at 2 s).
- TourPreset is defined once, in tools/tour_tool.py. The wire contract
types tour.preset with it, so the renderer uses the generated
TourPreset type. Contracts regenerated.
- The stop copy lives in i18n (appTour, en_app_tour.ts); other locales
fall back to English until the translations PR.
- engine.ts: one shared teardown for every driver exit, which now tells
the host. The app host clears a reactive $tourActive flag, so the
usage-stats consent dialog waits until no tour is on screen. A closed
one-off highlight also clears it.
- en.ts and types.ts are over the size cap, so the existing handoffTour
copy and type move unchanged into the same siblings to offset appTour.
test_start_validates_its_steps: a bare start is no longer an error; it is
the built-in tour, and start on surface='preview' without steps is.
An agent stores its session platform's gated toolsets in disabled_toolsets at load, so the tool_search listing, the bridge's search, MCP refreshes and delegate children drop manage_catalog too.
InstallPhase moves to the leaf hermes_cli/plugin_install_phase.py; the gateway contracts no longer import the installer.
The per-home enable lock moves from the plugins_cmd facade to plugins_cmd_install._enable_placed.
Reusing an installed catalog tree reads the install record through catalog_install_record, so older top-level catalog_name records match.
A failed row no longer redacts the Advanced choices (profile name, commit); only credentials are redacted.
The profiles.py role comment no longer says a role grants the setup toolset.
session-states imported connection-request, which reaches use-session-actions/utils, which imports session-states. The cycle broke the sidebar at load (vitest sidebar suites and the lineage/rename E2E). Kept install cards stay without a teardown: they are bounded by the installs a user approves in one window.
Catalog plugin installs pass the commit approved under Advanced to the clone; a disabled tree on disk is reused only when it is at that commit.
A settled Try again on a plugin row draws the tools, setup notes and enabled state from that retry's install result.
A manage_catalog search row that reuses an install's call id no longer draws the install card.
Connection cards kept for a session are cleared when its runtime is dropped and on a gateway re-home; turnHasReply moves to lib/chat-messages to keep session-states.ts under its size cap.
Toolsets that exist only for one kind of session now say so on their spec:
`platforms=frozenset({"desktop"})`. `TOOLSET_SESSION_PLATFORMS` is derived
from the specs that carry the key, `all` / `*` / no selection skip those
toolsets, and `toolsets.agent_tool_drops(agent)` removes their tools from any
agent whose own session platform is not listed. `agent_init._load_tools` and
`tools/mcp_tool_agent.py` (MCP refresh and prefix restore) call that one
helper. `tool_search._DIRECT_SURFACE_TOOLSETS` is derived from
`CLIENT_SURFACE_TOOLSETS` and `TOOLSET_SESSION_PLATFORMS`.
This replaces the profile-role gate: `profile_role_toolsets`, the role code in
`_with_session_toolsets` and the explicit-pin branch, the strip step in
`_select_tool_names`, and main's `setup` toolset (whose only tool moves out)
are deleted. `manage_catalog` moves into a new `catalog` toolset, which the
desktop client folds into every desktop chat like `project` and `desktop_ui`.
It is deferred behind tool_search by default (`config_defaults` defer list).
The shared-metrics schemas' toolset enum follows (`setup` out, `catalog` in).
The install card works outside the setup chat (#129650, with review fixes):
- Parallel installs: `_set_plugin_enabled` holds a per-home lock around the
read-then-admit section, so rows enabled at once no longer refuse each other.
- A catalog plugin already on disk and not enabled is enabled in place, so Try
again after a refused enable can finish.
- A failed catalog row counts as resolved (`contract.resolves`), so it no longer
holds the turn to the 300 s deadline. The snapshot carries `resolved`, and the
card reads it instead of a hand copy of the backend's rule.
- Each card draws its own operation by op id, because models reuse call ids.
- The row carries the approved non-secret choices (`approved`: force, enable,
ref) and the chosen profile, so Try again after the card settled repeats them.
- Install phases are ids (`InstallPhase`, wire `CatalogInstallPhase`) and the
installed-row notes are fields (`enabled`, `missing_env`, `server_errors`,
`already_installed`, `requires_hermes`); the card words them in English
strings (translations follow later). Failure reasons stay raw text, and
`detail` keeps the English notes for the model.
- Installs and search target the calling chat's own home, carried as a path;
the row's profile name is only a label, with no `'default'` fallback in
Python or TypeScript.
- `dashboard_install_plugin` is split into `_resolve_source`, `_place_tree` and
`_enable_placed`.
Desktop English strings for the card move to `en_catalog_install.ts` /
`types_catalog_install.ts`, which offsets the growth of `en.ts` and `types.ts`.
The electron project inherited vitest's 5s default while its tests spawn
real python3/pwsh processes over a 40+ case corpus. On loaded CI runners
pwsh startup alone takes several seconds, so
remote-update-marker-programs.test.ts timed out and turned JS & TS checks
red on unrelated PRs. The react project already carries 15s for the same
reason.
Two cleanup holes in TreeSplit's sash drag:
1. setPaneWidthOverride early-returns when the released px equals the
stored override, so a drag released back at its origin is a store
no-op with no re-render. cleanup relied on that re-render to rewrite
flex and clear the inline preview, so the flex-basis longhand stayed
stuck on the seam wrapper (frozen grow / stale basis) until some
unrelated render. cleanup now restores the captured style attribute
FIRST, then commits the store; when the store does notify, React
rewrites the same styles anyway.
2. endSashDrag() ran after applyShift with no try/finally, so a
throwing store commit left the sash-drag depth stuck above zero,
freezing every :root geometry-var write for the rest of the session,
and kept the window pointer listeners attached to a dead drag. The
whole teardown (endSashDrag, cursor/select restore, pointer capture,
listener removal, persistTree) now runs in finally.
STT_XAI_MODEL is read per call (a module constant froze the launch profile env),
the xAI client-direct form builder moves out of transcribeAudioClientDirect (CC 21),
the silencedetect probe catches only what subprocess raises, and the stt.xai
defaults comment folds into one line so config_defaults.py does not grow.
STT_MODEL_CATALOG moves to tools/transcription_common.py and becomes the one table
behind the `hermes tools` picker, the dashboard selects and the auto-correction
sets. Before this, the dashboard and Desktop offered models for only four providers,
Mistral and xAI had no select, DeepInfra had no model field, and Groq still listed
distil-whisper-large-v3-en, which Groq shut down on 2025-08-23. A config naming it
now remaps to whisper-large-v3-turbo.
Mistral Voxtral STT comes back to the picker and the dashboard provider list. It had
been hidden since the 2026-05-12 mistralai quarantine, and the pin is a verified-clean
2.4.8 again. DeepInfra's picker reads its live STT catalog.
xAI's /v1/stt now answers HTTP 400 "Field 'language' is required when
'format' is true", so every transcription with stt.language "" (auto-detect)
failed on both the server path and the Desktop client-direct path. Send the
inverse-text-normalization flag only when a language is resolved; the model
field is still always named. Comments updated: the server default moved from
grok-voice-transcribe-1.0 to 2.0 between Sep 17 and Sep 21, so the pin is
about determinism and a 1.0 rollback knob, not about escaping a 1.0 default.
Live: auto-detect on origin/main -> HTTP 400; with this change -> transcript
returned for 2.0 and pinned 1.0; language=en still sends format=true.
(cherry picked from commit 1ff4887819a12a5e37e9cbea7f31b5cc6b040857)
POST /v1/stt still defaults to grok-voice-transcribe-1.0 when model is
omitted. Hermes logged "grok-stt" and never sent the field, so voice notes
stayed on 1.0. Send stt.xai.model (default 2.0, overridable via STT_XAI_MODEL)
on the gateway and the desktop client-direct path.
(cherry picked from commit c869631946e9a9d84b1738bfdd278f926300da00)
A gateway flap clears the inference status on every gatewayState change, and
a readiness round that answers with no boolean is dropped on purpose because
a transport fallback is not proof that inference lost its credentials. Both
leave the status null, and the only callers of the readiness legs are three
seams the window may never cross again, so the chip can sit on "Checking
inference" while the backend is healthy and nothing ever retries it.
Let the periodic tick carry the readiness legs while the status is still
null. Once a round publishes an authoritative verdict the tick is status-only
again, so a healthy gateway pays no extra RPCs.
MoveToProjectItems read $projectTree without ever populating it itself.
In the default flat Sessions view, the tree is only warmed by a
background timer (PROJECT_TREE_WARM_MS ~2s) or a visit to grouped
view, so opening "Move to project" before either had run showed "No
other projects" even when named projects with folder roots existed.
Fixes#121193
#127048 detected both modern nvidia open kernel drivers AND propetiary
drivers as the proprietary one, disabling wayland.
this fix looks for the "open kernel module" string in
/proc/driver/nvidia/version, and lets Wayland rendering continue to be
used.
The fresh-install receipt is kept from before the consent answer and counted
after an opt-in, so "data from before you opt in stays on this machine" was
false for it on the CLI send explainer and Desktop's consentWindow line in all
9 locales; only the pre-chat offer named the exception. Both now carry the same
"apart from the fresh-install note (noted on this machine and counted only once
you opt in)" clause. No i18n key changes; the CLI lines stay under 80 columns.
CLI setup, the pre-chat consent offer and the Desktop collectedReliability line (all 9 locales,
no new key) now say update and install results carry a fixed-list reason and stage, and that a
fresh install is noted on this machine and counted only once the user opts in. The offer's
'data from before you opt in is never sent' sentence names that one exception.
The spawn fixtures run commands that never read stdin, so on a fast Linux
runner the shell can exit before shellExec's stdin write lands and the
write fails with EPIPE. The stream had no error listener, so vitest
reported it as an unhandled error and failed the electron project even
though every test passed. The close verdict is the result; the EPIPE is
noise.
A double that rejects any command over 4 KB - the shape of an OpenSSH mux
socket that cannot pass descriptors behind a large session request - must still
get a verdict, with the judge on stdin and out of argv. Red on base, where the
inlined program makes the gate raise "Could not prove that the remote Hermes
install is clear for SSH startup."
shellSshDouble forwards stdinData the way the real mux exec does, so the live
spawn and v2-marker fixtures now run `python3 -` against a real python3 and keep
proving the argument positions, lock behaviour and verdicts unchanged.
assertRemoteInstallUpdateClear inlined the ~7.5 KB marker gate program with
`python3 -c`, making the ssh command ~8.1 KB. ssh.exec rides the POSIX
ControlMaster mux socket, where ssh writes the session request and only then
hands our stdio descriptors over with sendmsg(SCM_RIGHTS); a request that still
fills the socket buffer - 8 KB for a unix-stream socket on macOS - leaves no
room for that descriptor message, so the pass fails with EMSGSIZE:
mm_send_fd: sendmsg(0): Message too long
mux_client_request_session: send fds failed
ssh exits 255 and the fail-closed gate reads it as "Could not prove that the
remote Hermes install is clear for SSH startup." on every attempt, so no remote
host connects while nothing is wrong on the remote. The window depends on the
client's socket buffer and the command's exact length, which is why a shorter
probe to the same host can still answer.
Hand the program to `python3 -` on stdin instead. Python compiles the whole
program before running it and the gate never reads stdin, so argv keeps the same
`<marker> [payload] [hermes...]` positions while the command stays a few dozen
bytes however the judge grows.
- desktop main.ts: the challenge IPC handler moves to
electron/challenge-window-ipc.ts (registerChallengeWindowIpc), and
resolvePortalBaseUrl moves next to the portal session that already
consumed it (portal-session.ts), so the facade ends below its main
line count; imports are sorted.
- desktop renderer: free_tier.challenge gets its own family handler
(gateway-event/free-tier.ts) in the dispatcher table instead of a
branch in handleLifecycleEvent.
- ui-tui: the challenge presenter and the existing billing step-up link
presenter move to app/gatewayBrowserLinks.ts, keeping the event
switch at or below its main size; the presenter test moves to its own
file.
No behaviour change.