Commit graph

1983 commits

Author SHA1 Message Date
Brooklyn Nicholson
d063684b3f style(desktop): sort the confirm-dialog import (eslint perfectionist) 2026-07-29 18:43:49 -05:00
Brooklyn Nicholson
cd8ea6f0f4 feat(desktop): approve pairing requests from the messaging page
Desktop had no pairing surface at all. Someone DMs the bot, gets a code,
and lands in pending — where only the dashboard or `hermes pairing approve`
could let them in. That gap is why the dashboard's broken approve button
went unnoticed for so long: desktop users never saw the flow.

This puts it where the decision already lives. The messaging page is
already master-detail by platform, already polls every 6s, and already owns
"who can talk to this bot" — the allowlist env vars in the same detail pane
are what an approval writes into. A pending block sits above the credential
fields (approving is the hot path); a count badge on the platform row is the
discovery mechanism, so you see "Telegram 2" whenever you open Messaging for
any reason. Neither renders when nobody is waiting: approvals are rare, and
a permanent empty state would be chrome on a page that is otherwise about
credentials.

Approval sends the row's request_id and never a code — the code is the
requester's proof that the channel is theirs and is never returned by the
API. Rows paint optimistically from a snapshot and roll back visibly on
failure, and a 429 gets its own message since the code path's lockout is a
condition the operator can only wait out.

Pairing rides the existing platform refresh via allSettled, so a backend
without the endpoint yields no rows instead of blanking the page.
2026-07-29 18:43:49 -05:00
brooklyn!
5c07ba2f3a
Merge pull request #74422 from NousResearch/bb/platforms-changed
Desktop: platforms.changed broadcast retires the Messaging page's 6s status poll
2026-07-29 18:37:35 -05:00
brooklyn!
1d95a227f8
Merge pull request #74447 from NousResearch/bb/hover-tab-slots
Tab verbs follow the pane under the pointer
2026-07-29 18:35:59 -05:00
brooklyn!
5010b9496a
Merge pull request #74445 from NousResearch/bb/cmdk-session-tab
Stop ⌘K and notification clicks stealing the main tab
2026-07-29 18:25:46 -05:00
Brooklyn Nicholson
15a55cbff1 feat(desktop): tab verbs follow the pane under the pointer
⌘1…⌘9, ⌃Tab, and the ⌘W / ⌘T family all resolved the last-interacted
zone, so switching tabs in a second pane meant clicking into it first.
They now resolve the HOVERED zone when the pointer is in one, falling
back to the focused zone otherwise — hover pane 1, ⌘2; hover pane 2, ⌘2,
and each lands in its own strip.

tabTargetGroupId() is the single resolver, keeping the number keys and
the tab verbs from disagreeing about which zone is "the" zone the way
they already couldn't. pointerover fires per boundary crossing rather
than per mouse move, and leaving the document clears the override so a
parked pointer never strands the keys on a stale zone.
2026-07-29 18:19:54 -05:00
Brooklyn Nicholson
93e54139c4 fix(desktop): stop ⌘K and notification clicks stealing the main tab
Both surfaces passed the sidebar's in-place intent, which means "load it
into main when it isn't already on screen" — right for a row you clicked
in a list you were looking at, wrong for a chat opened from outside the
workspace. Neither had a surface of its own, so they took the one you
were using.

Add a stack intent for that case. It focuses the session when it's
already open, spends an unused draft tab when there is one, and only
falls back to main while main is itself a blank draft. Modifiers still
force a tab or window.
2026-07-29 18:16:40 -05:00
Brooklyn Nicholson
272d3be6c8 feat(desktop): find and reuse an open blank "New session" tab
An unused draft tab is the one a user would have typed into, so give the
tile store a way to name it (blankDraftTile) and hand it to another
session in place (reuseBlankDraftTile). A blank-but-busy tab has its
first turn in flight and an unbound tile is unknown rather than empty, so
neither is a candidate.
2026-07-29 18:16:21 -05:00
Brooklyn Nicholson
fb120f850d refactor(desktop): move the main-is-occupied check into the session door
newSessionOpensTab answers a question that isn't specific to the sidebar
"+" — is there a conversation on main that must not be discarded — and the
palette needs the same answer. Fold it into open-session as
mainChatOccupied so both callers share one definition.
2026-07-29 18:16:14 -05:00
brooklyn!
4b7f709843
Merge pull request #74436 from NousResearch/bb/update-mutex
fix(update): one updater at a time, and never roll an install backwards
2026-07-29 18:15:17 -05:00
Brooklyn Nicholson
800c8a0458 fix(desktop): stop an unowned publisher poisoning the thread clearance at :root
The thread's bottom clearance is composer + status-stack + 2rem, and both
inputs are measured by JS onto the owning [data-chat-surface]. The surface-var
helpers fell back to document.documentElement when they couldn't resolve one —
but :root is where every surface's DEFAULTS live, so a single stale write there
becomes a global floor under every thread's clearance until reload.

An unowned publisher has nowhere to publish to, so it now publishes nowhere.
2026-07-29 17:57:49 -05:00
Brooklyn Nicholson
6a444ebec7 fix(installer): release the update marker on success, not just on drop
The updater keeps a Tauri/Cocoa event loop alive while it relaunches the
desktop, and that loop can outlive app.exit(0). Relying on Drop alone
left a *successful* update looking active -- a live pid holding a fresh
marker -- which blocked desktop startup and, now that the marker is also
the cross-process update lock, every subsequent updater until the age
ceiling expired.

Release explicitly once all install-tree mutations are done, before the
relaunch. complete() is idempotent so Drop still covers the failure and
panic paths. Arms a process-exit fallback so a wedged event loop cannot
leave a finished updater lingering as a live pid.

Co-authored-by: nateEc <nateEc@users.noreply.github.com>
2026-07-29 17:48:09 -05:00
Brooklyn Nicholson
38d5f44df1 fix(installer): refuse a second updater instead of clobbering the marker
UpdateMarkerGuard::acquire overwrote the in-progress marker
unconditionally, so a Tauri update launched while a dashboard-spawned
"hermes update" was mid-flight simply took the marker and ran a second
updater over the same checkout. That is the race behind the reported
Windows failure: install-mode bootstrap rewound the tree while the
dashboard's updater was still running npm install against it.

acquire now returns Result and refuses when a live foreign owner holds
the marker, and Drop no longer deletes a marker this process does not
own. Liveness matches the Python and Electron readers of the same file:
dead pid or past the shared age ceiling means stale and reclaimable, so
a crashed updater cannot wedge future updates.

Adds a cfg(unix) libc dependency for the signal-0 liveness probe; the
Windows path uses OpenProcess/GetExitCodeProcess.
2026-07-29 17:46:00 -05:00
Brooklyn Nicholson
f8e07a332e feat(desktop): platforms.changed broadcast retires the Messaging page's 6s status poll
The change watcher (#73673) missed one always-on-while-mounted timer: the
Messaging page polled /api/messaging/platforms every 6s for connection
status. The gateway already persists platform connect/disconnect/health to
gateway_state.json, so watch that file's mtime and broadcast
platforms.changed (floored to 5s — the gateway also rewrites the file for
in-flight-count bookkeeping), route it through live-sync like its
siblings, and refresh the page on the tick. Older backends keep the
legacy visible-tab poll verbatim.

Finishes the always-on poll sweep for #73618.
2026-07-29 17:33:35 -05:00
brooklyn!
a4973c3f11
Merge pull request #74363 from helix4u/fix/windows-wake-input-device
fix(wake): keep desktop ownership and select input devices
2026-07-29 15:44:13 -05:00
hermes-seaeye[bot]
e23d158f48
fmt(js): npm run fix on merge (#74360)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-29 20:30:47 +00:00
Gille
94d1dff50d fix(wake): route desktop control and select input devices 2026-07-29 14:04:21 -06:00
Teknium
e0233f8fc5 fix(desktop): full-duplex voice barge-in — interrupt during generation AND playback
The voice-interruption fix (5081551f0) covered the Python surfaces (CLI +
TUI gateway) but the desktop app has its own mic path in voice-barge-in.ts,
which still had both bugs on Windows:

- HALF-DUPLEX GAP: the barge monitor only opened when TTS playback started;
  during LLM generation no mic was listening at all.
- PLAYBACK DEAFNESS: the monitor calibrated its noise floor while the
  speakers were already playing TTS, baking bleed into the floor. On
  Windows, Chromium echoCancellation does not reliably cancel same-app
  output (measured live: quiet floor ~35-50 RMS vs playback bleed
  600-1700 RMS), making the trigger unreachable.

Changes mirror tools/voice_mode.full_duplex_listen:

- voice-barge-in.ts: phase-aware full-duplex monitor — quiet-only
  calibration (floor held through playback, never recalibrated against
  bleed), playback min-trigger clamp + ceiling, 500ms grace on playback
  onset only, windowed-majority detection.
- use-voice-conversation.ts: monitor arms at turn submit and spans
  generation + playback (ensureBargeMonitor, idempotent). Mid-generation
  speech fires the new onInterrupt callback; spoken stop-word during a
  barge ends the conversation; submit waits for the interrupt to settle.
- use-composer-voice.ts + composer/index.tsx: plumb onInterrupt: haltRun
  (same seam as the Stop button).

Tests: use-voice-conversation.test.tsx (6 tests) covering monitor lifecycle
across generation + playback, mid-generation interrupt, stop-word handling.
npm run typecheck + eslint clean; remaining desktop vitest failures
reproduce on a clean tree (pre-existing Windows env failures).
2026-07-29 12:52:42 -07:00
brooklyn!
3bb239750d
Merge pull request #74286 from NousResearch/bb/tool-ticker-clip
fix(desktop): let an opened tool row escape the live run's one-line window
2026-07-29 14:01:51 -05:00
Brooklyn Nicholson
53e4f87017 fix(desktop): let an opened tool row escape the live run's one-line window
A live tool run renders its rows inside a ticker — a window exactly one
line tall that offsets to the newest row. Expanding a row inside it left
the row's output clipped to that line, and the next call ticked it away
entirely.

Opening a row is a request to read it, so the run gives up the window
until it settles: the ticker unmounts and the rows render at full
height, the same escape an approval already had.
2026-07-29 13:30:06 -05:00
Brooklyn Nicholson
c4212b9453 fix(desktop): scope the remembered route per profile
`hermes.desktop.lastSessionId` is keyed by owning profile (143942d49), but
`hermes.desktop.lastRoute` stayed global -- and cold-start restore prefers the
route over the id. A session route embeds a session id in its path, so
relaunching under profile B navigated straight into a session owned by profile
A, bypassing the id scoping entirely (#67603 family).

Key the remembered route by the same owner the id already uses
(rememberedSessionProfile), read it back for the active profile on restore, and
keep the default profile on the original unsuffixed key so existing installs'
remembered route survives the upgrade.

Salvaged from the profile-hint work in #49619, which threaded an explicit
profile through every route/IPC/window surface to reach the same end. The server
now stamps session ownership unconditionally (#74033), so the resolver already
gets the right answer and only this persisted key was left crossing profiles.

Co-authored-by: d31tcjg <d31tcjg@users.noreply.github.com>
2026-07-29 13:02:12 -05:00
brooklyn!
cfa43f520a
Merge pull request #74245 from NousResearch/bb/pinned-messaging-index
Pinning a messaging session removes it from the sidebar
2026-07-29 12:41:16 -05:00
brooklyn!
f9f3811105
Merge pull request #74247 from NousResearch/bb/session-render-jank
fix(desktop): anchor the transcript through a session load
2026-07-29 12:39:50 -05:00
brooklyn!
c8f911112c
Merge pull request #74033 from NousResearch/jb/session-detail-profile-stamp
fix(sessions): always stamp owning profile so cross-profile open works toward default
2026-07-29 12:39:09 -05:00
Brooklyn Nicholson
a7cc924204 perf(desktop): add a session-load scenario to the harness
`submit` measures the scroll jump when a turn is appended; nothing measured
the jump when a session is opened, which is the prepend/settle path. Clicks
sidebar rows and tracks how far the bottom turn moves after first paint.
2026-07-29 12:13:36 -05:00
Brooklyn Nicholson
8c92983fde fix(desktop): anchor the transcript through a session load
Opening a session painted a small first budget, then prepended the rest of
the turns above the viewport with nothing holding the scroll position, so the
view was stranded near the top of the transcript until use-stick-to-bottom's
ResizeObserver caught up a frame or two later. The settle loop couldn't cover
for it either: keyed on sessionKey alone it measured an EMPTY viewport on a
cold load, saw a stable height, and declared the load settled hundreds of ms
before the messages arrived.

Anchor before the backfill prepends (the mechanism "Show earlier" already
uses) and re-arm the settle loop when the transcript actually lands.

Measured over 12 real sidebar-click loads: 12,179 -> 544 px of post-paint
movement per load, worst single jump 13,369 -> 2,296 px. Render churn is
unchanged (fewer commits and total renders, same wasted count).
2026-07-29 12:13:36 -05:00
Brooklyn Nicholson
46b55c2c91 refactor(desktop): extract the pin index and cover the messaging slice
Pebrd's fix is right but the `useMemo` it edits didn't list
`messagingSessions` as a dependency, so the index would go stale as the
messaging slice refreshed. Rather than regex around logic buried in a
1500-line component, lift it into `buildSessionByAnyId` where the
contract can be tested directly — the extraction LeonSGP43 proposed in
sibling PR #49896.

The test asserts the invariant that actually matters: a pin resolves
from every slice the sidebar fetches. Reverting the function to its
two-slice form fails it on the messaging and lineage-root cases.

Co-authored-by: LeonSGP43 <LeonSGP43@users.noreply.github.com>
2026-07-29 12:12:55 -05:00
Pebrd
ab694586b2 fix: resolve pinned Telegram sessions into sidebar Pinned section
The sessionByAnyId map only indexed cronSessions and visibleSessions
(local CLI chats), so a pinned gateway session (Telegram, etc.) stored
its pinId in localStorage but could never be resolved back to a
SessionInfo object — the Pinned section rendered empty for those.

Include messagingSessions in the lookup so pinned gateway chats appear
correctly at the top of the sidebar.
2026-07-29 12:09:19 -05:00
Brooklyn Nicholson
8ce8b70dca feat(desktop): pins sync between apps sharing a gateway
The pin bridge only ever pushed: localStorage to the backend, never
back. Two apps on the same gateway each kept their own localStorage, so
a pin made on the Mac never appeared on the Windows app.

Now that a pinned row is guaranteed to be in the page, its absence says
nothing about its pin state — which makes the server row authoritative.
`pullRemotePins` adopts pins this app hasn't seen and drops local pins
the server says are gone, keying on the durable lineage root so a pin
survives compression tip rotation. Adopted pins are recorded as already
mirrored rather than echoed back as a redundant write.

A list request in flight when we PATCH still carries the old value, so
honouring it would silently undo the pin the user just made. Writes are
guarded for the lifetime of their own request — no timers, no wall-clock
windows. A runtime predating the flag sends no `pinned` at all; that's
treated as no opinion and leaves the local set alone.

Co-authored-by: webtecnica <75556242+webtecnica@users.noreply.github.com>
Co-authored-by: Kyzcreig <9063726+Kyzcreig@users.noreply.github.com>
Co-authored-by: aman-merchant <274313970+aman-merchant@users.noreply.github.com>
Co-authored-by: rerdi92 <76791321+rerdi92@users.noreply.github.com>
2026-07-29 12:00:41 -05:00
Teknium
df093bf33c feat(voice): calm ambient "thinking" sound while the agent works in voice chat
Long thinking/tool stretches in a voice conversation are dead air — the
user cannot tell whether the agent is alive. New: quiet, repeating soft
bubble blips while the agent works and no speech audio is flowing.

- tools/voice_mode.py: numpy-synthesized blips (no binary assets) — two
  alternating low pitches (G4/E4) with pitch glide + smooth attack/decay
  envelopes, ~0.8-1.2s randomized spacing, volume = voice.beep_volume * 0.5.
  start_thinking_sound(should_play=...) / stop_thinking_sound() daemon-loop
  lifecycle; macOS-TCC-safe (sounddevice output gated there → silent skip,
  no per-second afplay churn). New mark_audio_output_active()/
  is_audio_output_active() ref-count wraps play_audio_file and the
  streaming OutputStream sentence writes so "audio is flowing" is accurate.
- Config: voice.thinking_sound (default true) off-switch.
- cli.py: starts when a voice-mode turn begins, per-blip gate skips while
  TTS speaks / mic records / barge capture owns the mic; stopped in the
  chat() finally on every exit path.
- tui_gateway/server.py: same lifecycle around _run_prompt_submit turns
  (voice mode on), gated on is_audio_output_active + continuous capture.
- Desktop: renderer owns voice-conversation audio, so a matching WebAudio
  implementation (src/lib/thinking-sound.ts, same envelope/pitches) runs
  while conversation status === "thinking"; honors voice.thinking_sound
  (via config store) and the shared sound-mute toggle; stops instantly on
  speaking/listening/end.
2026-07-29 08:24:00 -07:00
Teknium
6fdfdc1597 feat(voice): "Say <stop-phrase> to end the voice chat" notice on voice-mode start (CLI/TUI/desktop, i18n)
One owner for the wording: voice_stop_hint() in tools/voice_mode.py —
sources the phrase from voice.stop_phrases (first entry) so a custom
phrase renders correctly, and returns "" when the feature is disabled
(stop_phrases: []) so no surface shows a hint.

- CLI: printed in /voice on output (style-matched dim notice).
- TUI: voice.toggle action=on now carries stop_hint; the Ink client
  renders it in the "Voice mode enabled" block (older gateways omit
  the field — no hint, no crash).
- Desktop: the renderer voice loop never touches tools/voice_mode.py,
  so the phrase is read from config (voice.stop_phrases → $voiceStopPhrase
  store, seeded in use-hermes-config) and shown as an info toast when a
  voice conversation starts. i18n: en/ja/zh/zh-hant/ar.
2026-07-29 08:24:00 -07:00
yoniebans
a8ec50f681 fix(sessions): always stamp owning profile so cross-profile open works toward default
Session rows served without ?profile= carried no profile field, so in
multi-profile desktops the default profile's sessions circulated unowned:
resolveStoredSession cached them profile-less, resolveSessionProfile returned
undefined, and session.resume targeted whichever gateway was active -- opening
a default-profile session from a non-default window failed with
"session can't be found" while the reverse direction worked (#67603 family).

Server: GET /api/sessions/{id} and GET /api/sessions now stamp profile/
is_default_profile unconditionally -- the serving profile is always known
(_cron_default_profile() when the request is unscoped).

Renderer: resolveStoredSession treats a profile-less $sessions cache hit as
unresolved when >1 profile exists (falls through to the stamped by-id ladder)
and back-fills the active profile on bare by-id hits from older backends, so
unowned rows are never re-cached.

Verified via CDP against a live 4-profile renderer: bare by-id GET returned
hasProfileField:false and the stale cache rows matched; with the fix both
lookups return the owning profile and the resume routes to the right backend.
2026-07-29 13:54:12 +02:00
kshitijk4poor
472658d014 fix(desktop): pass shell to serve probe; bound Windows discovery probes
Two residual gaps from the #72707/#72632 probe-hardening series:

- backendSupportsServe never forwarded backend.shell to the serve
  --help probe. A .cmd/.bat shim backend (which carries shell: true in
  its step-4 descriptor) makes execFileSync throw EINVAL on modern
  Node; the bare catch then caches supported=false for the process
  lifetime, permanently routing that backend through the legacy
  dashboard form. Forward the flag.

- The Windows python-discovery probes ran with no timeout at all:
  reg query (registry read) and py.exe -c 'import sys;...' (bare
  interpreter startup) are both synchronous execs on the boot path;
  a wedged reg.exe or python.exe would hang the resolver forever.
  Bound reg query at 5s and the py.exe probe at the shared
  PROBE_TIMEOUT_MS budget.
2026-07-29 17:09:25 +05:30
hermes-seaeye[bot]
022a175e0a
fmt(js): npm run fix on merge (#74057)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-29 11:32:21 +00:00
kshitijk4poor
74f8e59877 fix(desktop): widen probe-budget fix to sibling boot-path probes
Follow-up to #73907 (probe timeout 15s + env override + timeout-only
retry), widening the same fix to the two sibling sites it missed:

- backendSupportsServe's serve --help probe kept a bare execFileSync
  with its own 15s literal: same cold-Windows Python-startup class
  (#72632 measured ~10.5s for --version cold), no retry, and a false
  negative is cached for the process lifetime - silently routing a
  modern runtime through the legacy dashboard form. Route it through
  execProbeSync with the shared PROBE_TIMEOUT_MS (honours
  HERMES_PROBE_TIMEOUT_MS) and the timeout-only retry.

- resolveHermesBackend step 4 called unwrapWindowsVenvHermesCommand
  twice; the second call re-ran the same un-memoized import probe,
  costing up to another full probe timeout on a hung interpreter for
  an answer the first call already gave. Drop the redundant re-probe.

Part of the #72707 bug class (transient disconnect must not strand a
healthy install).
2026-07-29 16:52:53 +05:30
hermes-seaeye[bot]
015718066a
fmt(js): npm run fix on merge (#73999)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-29 08:37:16 +00:00
kshitijk4poor
f199c4c92e test: update boot handshake test for synchronous warmup (#73083)
The old test asserted _warm_gateway_module was fire-and-forget (startup
completes in << SLOW_SECONDS). PR #73291 intentionally reversed this:
the import now runs synchronously before the lifespan yield because
run_in_executor didn't release the GIL on Windows + Python 3.11.
Updated the test to assert startup blocks for >= SLOW_SECONDS.
2026-07-29 13:59:11 +05:30
Bartok9
a7925bdd49 fix(desktop): raise backend probe timeout and retry on timeout
Closes #61764

Root cause: PROBE_TIMEOUT_MS=5000 false-negatives healthy Windows cold
starts (AV/disk), so launcher runs hermes-setup --update forever.

Fix:
- Default timeout 15s (HERMES_PROBE_TIMEOUT_MS override, max 120s)
- One automatic retry on timeout only
- Tests for default + env resolution

Salvage of closed #61781/#61956 with env override + timeout retry.

Verification: npx vitest run electron/backend-probes.test.ts (12 passed)
2026-07-29 13:59:11 +05:30
hermes-seaeye[bot]
7d3075d0d5
fmt(js): npm run fix on merge (#73960)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-29 07:13:20 +00:00
Teknium
ba13132298 fix(voice): bare stop phrase ends the voice chat on every surface, spoken or typed
Saying OR typing a configured stop phrase (voice.stop_phrases, default
"stop") now ends the voice chat everywhere, not just classic CLI PTT:

- hermes_cli/voice.py: new explicit on_stop_phrase callback through
  start_continuous/stop_continuous. The force-transcribe path previously
  DISCARDED the stop phrase silently — with auto_restart=False the client
  re-arms the next capture, so the conversation never ended. Both halt
  paths now fire on_stop_phrase (fallback: on_silent_limit for legacy
  callers) as user intent, distinct from the no-speech timeout.
- tui_gateway/server.py: voice.record wires on_stop_phrase and emits
  voice.transcript {stop_phrase: true} after flipping HERMES_VOICE(_TTS)
  off and stopping streaming TTS — same teardown as /voice off. The TTS
  barge-in monitor stop-checks its transcript too. prompt.submit consumes
  a TYPED bare stop phrase at the server-side choke point when voice mode
  is on (returns {voice_stopped: true}, no turn starts).
- ui-tui: voice.transcript {stop_phrase} ends voice mode with a clear
  'voice chat ended' notice (distinct from the no-speech-limit message);
  submitPrompt releases the busy latch on a consumed voice_stopped reply.
- cli.py: _typed_voice_stop in process_loop — typing a bare stop phrase
  while voice mode/continuous is active ends voice mode instead of
  sending 'stop' to the agent; typed 'stop' outside voice mode is
  unchanged. Voice transcripts skip the check (already stop-checked).
- desktop: interceptsTypedVoiceStop — the composer's onSubmit ends the
  live voice conversation (same path as clicking end on the pill) when a
  bare stop command is typed with no attachments; renderer-owned loop, so
  handled client-side like the existing spoken isVoiceStopCommand.
- tools/voice_mode.py: transcribe_recording never lets the Whisper
  hallucination filter swallow a configured stop phrase (e.g. 'bye'
  configured as a stop phrase is both a hallucination-blocklist entry and
  a stop phrase — stop-phrase check now wins).

Tests: continuous-loop signal (sabotage-verified), force-transcribe stop
signal + legacy fallback, hallucination-filter ordering, typed-stop CLI
unit tests (voice on/off/longer text), prompt.submit typed-stop gateway
tests, TUI vitest for stop_phrase event handling, desktop vitest for the
typed-stop interceptor.
2026-07-29 00:01:06 -07:00
brooklyn!
738725d18b
Merge pull request #73881 from NousResearch/bb/composer-type-to-focus-main
fix(desktop): heal type-to-focus onto the visible chat surface
2026-07-29 01:57:30 -05:00
Teknium
d60a2eb3b4 fix(desktop): gate backend respawns on updateInFlight, not just the update marker (#73822)
On Windows, applyUpdates kills its own backend (releaseBackendLock)
BEFORE the venv-blocker preflight but only writes the on-disk update
marker AFTER the scan. Killing the backend drops the renderer's
WebSocket; the renderer reconnects within ~1s and the marker-only
waitForUpdateToFinish gate happily spawns a fresh 'hermes serve' inside
the update's own critical section. scanVenvBlockers then finds that
brand-new process and aborts with 'another Hermes process is using this
installation' — a different PID on every attempt, so Desktop self-update
can never succeed.

Fix: extract the gate into update-gate.ts (pure, DI-testable) and make
it consult BOTH signals — the on-disk marker AND the in-process
updateInFlight flag. The success path writes the marker before the flag
clears in applyUpdates' finally, so there is no instant where both are
false and a waiter can slip through. Also gate spawnPoolBackend, which
previously had no waitForLocalStart at all — a background profile window
could respawn a pool backend during the same window with the identical
abort.

Tests: update-gate.test.ts covers the open gate, the flag-only window
(the #73822 shape), the flag→marker handoff with no gap, and timeout.
2026-07-28 23:53:25 -07:00
Teknium
eaecca4a71 fix(desktop): widen local plugin-root fix — profile-aware root + dir-watch path
Follow-ups on top of #66911's salvaged commit:
- hermes:fs:desktopPluginsRoot now resolves the ACTIVE desktop profile
  (readActiveDesktopProfile) so named profiles keep their own
  profiles/<name>/desktop-plugins root instead of sharing the global one
  (profile-scope concern raised on the PR thread).
- startDirWatch in runtime-loader.ts was a third sibling site still
  deriving the watch path from the backend's hermes_home (added by the
  later fs-watch commit); routed through the same Electron-local
  resolver, with regression coverage.
2026-07-28 23:53:20 -07:00
PRATHAMESH75
e614876c63 fix(desktop): resolve local plugin root independent of remote backend
The Settings "Open plugins folder" action and the runtime disk-plugin
scanner both derived the plugin directory from getStatus().hermes_home.
Against a remote backend that value is a path on the REMOTE box (or
undefined), producing `undefined/desktop-plugins` — the folder action
errors ("Could not open the plugins folder undefined") and disk-plugin
discovery silently finds nothing, even with a valid local plugin.js.

Add an Electron-owned IPC resolver (hermes:fs:desktopPluginsRoot) that
returns <HERMES_HOME>/desktop-plugins computed from the main-process
HERMES_HOME — the local Electron path, valid in every connection mode —
creating it on demand. Route both the Settings folder action and the
runtime scanner through it, so a remote backend never determines the
local filesystem location used for Desktop runtime plugins.

Fixes #66899
2026-07-28 23:53:20 -07:00
Teknium
2319dbb014 fix(desktop): honest browser-backend readiness + explicit backend activation + full OpenAI TTS voice/model options
Three GUI Capabilities-tab defects reported on Windows:

1. Browser rows stuck on 'Setup required' after a successful setup run.
   Root causes, all in the readiness probe (not the installer):
   - _has_agent_browser() never searched the Hermes-managed Node dir
     (%LOCALAPPDATA%/hermes/node / $HERMES_HOME/node/bin) where the
     Windows install lands, and probed node_modules/.bin/agent-browser
     as the extensionless POSIX shim, which fails exec on Windows
     (WinError 193) — now resolved via PATHEXT-aware shutil.which
     against both rungs, mirroring _find_agent_browser().
   - Cloud rows (Nous Subscription Browser Use, Browserbase, Browser
     Use, Firecrawl) declared post_setup: agent_browser, whose
     readiness gate requires a LOCAL Chromium build the cloud never
     uses — switched to the cloud-scoped 'browserbase' hook (CLI-only).
   - _agent_browser_installed() could read browser_tool's stale cached
     'Chromium missing' result from before the install ran in the
     spawned post-setup process — cache now dropped before probing so
     the pill flips to Ready right after a successful run.

2. No way to tell which backend is active, and clicking a row to read
   its details silently rewrote config. Row click now only
   expands/collapses; activation is an explicit 'Use this backend'
   button, the active row carries an 'Active' pill, and the expanded
   active row says 'This is your active backend'.

3. OpenAI TTS showed one model and one voice. The options were always
   defined but rendered through a native <datalist>, which filters by
   the field's current value — a field already set to a valid option
   suggested only itself. Replaced with a real combobox (Input +
   dropdown) that lists every option, and voice suggestions now track
   the selected model per the OpenAI TTS docs: tts-1/tts-1-hd = 9
   voices, gpt-4o-mini-tts = 13 (adds ballad, verse, marin, cedar).
2026-07-28 23:52:49 -07:00
Brooklyn Nicholson
f12e6526a6 fix(desktop): heal type-to-focus onto the visible chat surface
Type-to-focus routes through requestComposerFocus('active'), which resolved
to a module-level activeTarget claim. Inactive tabs stay mounted under
data-pane-hidden, so typing in a session tile then clicking the main tab
left activeTarget on the buried tile: use-keybinds preventDefaults the
keystroke, the buried composer ignores the request (or is filtered out),
and the main composer never sees it. Same class of bug after the inline
edit composer unmounts with activeTarget still 'edit'.

Heal 'active' against the visible data-composer-target stamp (the same
visibility policy as every other document-wide surface lookup), release
the claim on real unmounts (useComposerDraft + user-edit-composer), and
keep getActiveComposer honest so Esc / soft / / voice agree with the
keyboard path.

The unmount release is salvaged from #72625 (@briandevans); this PR adds
the keep-alive tab heal his unmount-only fix couldn't cover.

Co-authored-by: briandevans <252620095+briandevans@users.noreply.github.com>
2026-07-29 01:49:06 -05:00
brooklyn!
9bf1f7376f
Merge pull request #73875 from NousResearch/bb/review-69049
fix(desktop): keep queued drains out of the foreground session on session switch
2026-07-29 01:42:01 -05:00
brooklyn!
8b2d810956
Merge pull request #73884 from NousResearch/bb/cmdk-update
fix(desktop): make the ⌘K Update Hermes command actually update
2026-07-29 01:41:16 -05:00
Teknium
805c1c340c feat(stt): support OpenAI gpt-transcribe transcription model
Adds gpt-transcribe (OpenAI's new file-transcription model, $0.0045/min)
to the OpenAI STT provider:

- OPENAI_MODELS set: gpt-transcribe is recognized so provider
  auto-correction keeps it on OpenAI and rejects it on Groq
- Language hint wiring: gpt-transcribe replaces the singular
  'language' field with a 'languages' list; the API rejects the legacy
  field, so the hint is sent via extra_body {languages: [..]}
- Config comment (DEFAULT_CONFIG), cli-config.yaml.example, desktop
  settings enum, and docs (en + zh-Hans) updated
- Tests: model pass-through, languages-list hint shape, legacy singular
  hint preserved for gpt-4o-transcribe, Groq auto-correction

gpt-live-transcribe (realtime WebSocket, $0.017/min) is NOT wired here:
the file-based STT pipeline has no realtime session path; it belongs in
a future realtime/voice-mode integration.
2026-07-28 22:40:45 -07:00
Brooklyn Nicholson
20a0bfc9a6 feat(desktop): show the running version on the Update Hermes row
The command palette named the action but not the install it acts on. Carry the
version and its commit diff on the row, resolved from the shared resolver so it
reads identically to the statusbar.
2026-07-29 00:24:50 -05:00