6848 Commits

Author SHA1 Message Date
kshitijk4poor
5912ed81ed chore(desktop): name the bootstrap cache key and ref policy accurately
cachedScriptPath takes a cache key (often branch-main), not a commit, and
downloadInstallScript now receives the branch for existing checkouts too.
2026-09-28 12:40:15 +05:30
JoaoMarcos44
82cff2ed43 fix(desktop): keep bootstrap script aligned with checkout 2026-09-28 12:40:15 +05:30
hermes-seaeye[bot]
5458de9483 fmt(js): npm run fix on merge (#126047)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-09-28 06:27:20 +00:00
teknium1
989798cd5e fix(desktop): test runs stop leaking temp dirs into TMPDIR
Hermes points TMPDIR at ~/.hermes/cache/scratch, so every Desktop test
run's leftovers pile up there until the 24h idle pruner catches them:
one day on a dev host left 688 playwright-tracing-*, 152
playwright-artifacts-*, 75 hermes-e2e-mock-* sandboxes and ~600 small
vitest mkdtemp dirs.

- Playwright (main + core configs): e2e/run-tmp.ts gives the run one
  temp root that workers, Electron and the backend inherit via TMPDIR,
  and removes it when the runner exits. Covers the stack-trace dirs the
  Electron tracing patch orphans (2 per launch, even on green runs),
  sandboxes a failed/timed-out setup never cleans, and Playwright's own
  artifact dirs. A SIGKILLed run now leaves one pw-* dir for the pruner.
  The update suite keeps os.tmpdir(): its seeded install is reused
  across runs and bakes its path into AF_UNIX sockets.
- vitest (apps/desktop): globalSetup does the same for the run; one
  `--project electron` run left 54 dirs from tests that never remove
  what they mkdtemp.
2026-09-27 23:10:22 -07:00
ahisblessed
d4720b4ea8 fix(desktop): compare preview/tour gate on one session identity class (#122062)
drive_preview (and tour) were refused with "The in-app browser only takes
actions in the session the user is looking at." for a compressed-but-still-
selected conversation: the backend stamps preview.act.request with the
RUNTIME session id, which auto-compression rotates mid-conversation, while
the pane keeps the durable/lineage id it navigated to — so
`sessionId === activeSessionId` failed for the rest of the conversation
even though desktop_preview open/read kept working.

Both sides of the check now resolve to the same identity class before
comparing: the request's runtime id maps to its stored id through the
message stream's session state cache (rotation-aware), unknown ids pass
through unchanged (they may already be stored ids), and a final lineage
match lets a compression-rotated tip and its root read as one
conversation — the same one-row lineage test session.info matching uses.
Branch siblings that only share a root stay distinct, and a background
tile session is still refused by the gate.

The same matcher backs windowHostsSession, so the window hosting the
conversation claims the request instead of leaving it unanswered when the
ids differ only by rotation.

Fixes #122062
2026-09-28 01:16:05 -04:00
Gille
83597d9b71 fix(desktop): keep background roster auth from opening login (#125972) 2026-09-28 01:09:29 -04:00
teknium1
68fa7e9f84 fix(desktop): keep PM store dirs first on PATH for Hermes's own children
The login-shell PATH merge (shell-path.ts) writes the user's interactive
PATH into process.env ahead of the inherited entries. When Desktop is
started by `hermes desktop`, the inherited PATH is where the PM store's
node/npm/uv dirs sit, so every Hermes-toolchain child built from
process.env (the `hermes serve` backend, backend probes, the source/posix
update hand-offs via sourceUpdateEnvironment, the bootstrap installer)
resolved the user's nvm/Homebrew/~/.local/bin copies first.

buildDesktopBackendEnv and the bootstrap installer env now move PATH
entries under the store root (HERMES_RUNTIME_DIR, else <hermes home>/tools,
matching pm.environments.store_root) to the front, other entries in their
existing order. process.env itself is unchanged, so the embedded terminal
and external terminal keep the user's own PATH order.
2026-09-27 21:30:40 -07:00
dskwe
78999579c8 fix(desktop): bound NVIDIA EGL fallback to confirmed-broken driver series (#123213)
* fix(desktop): bound NVIDIA EGL fallback to confirmed-broken driver series

The 580+ check was open-ended: every future NVIDIA series (615.x, #123203)
was forced onto CPU SwiftShader rendering even though only 580.x has
confirmed EGL probe reports (#40077: 580.159.03, 580.173.02) and 570.x is
the recommended downgrade. Replace the >= floor with a closed set of
known-broken series majors so healthy newer drivers keep GPU rendering.
HERMES_DESKTOP_NVIDIA_SWIFTSHADER still overrides both ways.

* docs(desktop): document the NVIDIA SwiftShader override and scope its force-on claim

- add HERMES_DESKTOP_NVIDIA_SWIFTSHADER to environment-variables.md (both
  directions, with the recovery-hatch framing the closed-set design leans on)
- correct the module header: force-on does not apply where an earlier gate
  already returned (remote display, WSLg, DISABLE_GPU=0)

Review follow-ups on PR #123213.

* test(desktop): pin NVIDIA EGL detection to set membership across a driver sweep

The closed-set test only checked that 580 is a member, so it still passed
if a neighbouring series was added to the set or the check drifted back
to a floor. Sweep majors 500-700 and require enable === set.has(major);
this fails against a '>= 580' floor. Also clears the prettier arrow-paren
diff that js-autofix would have reverted on merge.

---------

Co-authored-by: Austin Pickett <austinpickett@users.noreply.github.com>
2026-09-28 04:26:16 +00:00
Austin Pickett
bfda74c71a fix(desktop): interrupt a deleted session's runtime so no prompt outlives it (#124859)
Some checks failed
Skills Index Freshness Check / check-freshness (push) Has been cancelled
auto-fix lint issues & formatting / Generate eslint --fix patch (push) Has been cancelled
auto-fix lint issues & formatting / Apply patch (push) Has been cancelled
* fix(desktop): interrupt a deleted session's runtime so no prompt outlives it

Deleting a session did not stop its in-flight turn unless that session was the
focused one. `removeSession` gated runtime teardown on selection:
`closingRuntimeId = wasSelected ? activeSessionIdRef.current : null`. A
NON-selected (sidebar/background) session therefore never sent `session.close`,
and even the selected path only called `session.close` — which tears the runtime
down but does not walk the interrupt path that releases approval / clarify /
sudo / secret waits (`tui_gateway/session_lifecycle.py`: `_finalize_session`
interrupts delegations but never calls `resolve_gateway_approval(..., 'deny')`;
only `_interrupt_session_turn` does, via `_clear_pending` →
`server_requests.cancel`). A blocked run could outlive its sidebar row and
surface a blocking prompt for a conversation that no longer existed.

Fix:

1. `removeSession` (`use-session-actions/index.ts`) resolves the doomed runtime
   from the selection OR `runtimeIdByStoredSessionIdRef`, marks it interrupted
   (`interrupted: true`, `needsInput: false` — rolled back if the interrupt RPC
   genuinely fails), sends `session.interrupt` before `session.close`, and
   flushes `clearAllPrompts` + `clearClarifyRequest` for that runtime. The RPCs
   route by the row's owner (`requestForSessionProfile`), matching the existing
   delete/close contract.
2. Blocking-input handouts no longer park a card for an interrupted runtime
   (`server-requests.ts`: new `sessionStopped` guard on approval / sudo / secret
   / vault.code / vault.save_login / vault.unlock_prompt; `input-requests.ts`:
   same guard on `connection.request`). This closes the window between the local
   interrupted flag and a prompt frame already queued on the transport.

Fixes #75587

* fix(desktop): answer, don't drop, blocking-input requests for a stopped session

The interrupted-session guard on approval / sudo / secret / vault requests
returned without touching the request, so the backend kept blocking on a frame
that would never come — until its own deadline, or until `request.cancel`
happened to cover it. The channel contract already says a JSON-RPC error reads
as "unanswered" on the Python side (the same result `server_requests.cancel`
produces), so fail the request and let the tool return now. The clarify
handler's interrupted branch already answered; this makes the family match.

Also drop the second `clearAllPrompts` / `clearClarifyRequest` flush after the
row delete — the flush right after `session.interrupt` already ran, and any
later frame is declined by the guard, so the repeat was defense in depth.

Tests: assert the guard fails the request (and a live session is untouched),
assert the interrupted flag is set before the first RPC with the right shape,
and cover the rollback when `session.interrupt` fails for a non-gone reason
(no close, no delete, live state restored, row survives).
2026-09-27 21:41:55 -04:00
Austin Pickett
e1fdfb32f1 fix(desktop): re-point the window route after a primary connection apply (#124816)
* fix(desktop): re-point the window route after a primary connection apply

sendConnectionApplied() notified the renderer with a bare
hermes:connection:applied event, so the renderer's soft switch re-dialed
profile-less and main answered it from the window's recorded route
(resolveDesktopConnectionRequest). That record names the source the user just
LEFT, so after an apply the renderer kept dialing the stale registry-scoped
gateway: the session list never re-scoped, the status bar kept the old
backend's version, the old socket stayed ESTAB, and only a full restart
recovered. A locally recorded route is unscoped and follows the new v1 config,
which is why local -> remote looked fine while remote -> local did not.

Re-point the applied window's recorded route at the newly applied primary in
the same step as the notify, so the profile-less re-dial targets the gateway
just applied (and a window launched from that route afterwards inherits it).
An applied registry source stays registry-scoped to that exact identity; a
This-device apply stays unscoped so the dial follows the freshly written v1
config. The route rewrite and the notify only run once the config/registry
write has committed, so a rolled-back apply cannot leave a record naming a
source that never landed.

Fixes #92352

* fix(desktop): re-point the window route only on a global primary apply

A profile-scoped apply (the v1 per-profile remote override, payload.profile
set) never reconciles the registry, so its primary is unchanged: re-pointing
the main window's route there would yank a window viewing another registry
source onto the primary for an apply that did not move it. Keep the bare
notify for scoped applies and re-point only when the registry primary was
actually reconciled.

Read the applied primary from the registry snapshot that was just written
(nextRegistry) instead of re-reading the file inside the notify.
2026-09-27 21:37:07 -04:00
Austin Pickett
2a27f12110 fix(desktop): never adopt a page from a backend that ignored order=latest (#124815)
* fix(desktop): never adopt a page from a backend that ignored order=latest

The desktop's transcript authority is GET .../messages?order=latest, but a
backend built before the `order` param silently drops it (FastAPI ignores
unknown query params), serves the OLDEST page, and still answers with a
`pagination` object. That page was adopted as the tail: the transcript
silently became its first N rows, and "Show earlier" then prepended rows
N..2N counted from the oldest end.

Only a page that echoes `pagination.order === 'latest'` may be adopted as
the tail. Anything else is read as the complete transcript instead.

Fixes #92508

* fix(desktop): keep the order-echo fallback passive and skip re-reads it does not need

The compatibility read for a backend that ignored `order=latest` dropped
the caller's `passive` flag, so a hidden tile's background refresh
(#103375) could hold or wake a backend through the follow-up paged read.
It also paged the whole transcript again for pages that already held
every row: a response with no `pagination` (the pre-paging generation,
which previously cost one request) and an orderless page that came back
short from offset 0.

Thread `passive` through `getAllSessionMessages`, and only page again
for a FULL orderless page; the other two are adopted as the complete
transcript with `pagination` stripped.
2026-09-27 21:37:01 -04:00
Austin Pickett
90c8598e38 fix(desktop): cron run rows carry their owning backend so the transcript opens
Desktop cron run entries (sidebar CRON JOBS peek and the Cron page's run
history) listed fine but clicking one never loaded the transcript: the row
highlighted and nothing else happened.

The open path was handed only the run's id. The clicked ROW is the identity
that carries the owning (connection, profile); an id-only resume resolves
against the ambient backend, which over SSH/remote has never seen the run.
Session-list rows are tagged with their serving registry connection
(stampRowsWithOwningConnection via /api/sessions and the sidebar slices), but
getCronJobRuns returned the backend rows untagged — so even the owner-aware
wiring door had no owner to pin.

Route both cron surfaces through the same owner-aware open the sessions
sidebar uses, and tag the run rows with the connection that served them.

Fixes #82527
2026-09-27 21:36:32 -04:00
Brooklyn Nicholson
952c941e74 feat(desktop): apply the reveal floor in the project tree listing
readProjectDir now filters with SHOW_IGNORED_EXCLUDED when the project
opted into showing gitignored files, so out/vendor/coverage listings
survive the toggle; the default ALWAYS_EXCLUDED pass and the gitignore
pass are unchanged for everyone else.

Fixes https://github.com/NousResearch/hermes-agent/issues/55169
2026-09-27 20:10:31 -05:00
Brooklyn Nicholson
b8885eb492 feat(desktop): let the show-ignored toggle reveal hygiene dirs the transports keep
ALWAYS_EXCLUDED dropped out/, vendor/, coverage/ and friends even for
projects whose owner flipped the per-project eye toggle on. Deny-by-default
.gitignore layouts rely on exactly those names for real build output, so the
toggle could never deliver what it promises there.

Split the set: SHOW_IGNORED_EXCLUDED is the floor the local Electron bridge
and the remote /api/fs/list transport already strip (VCS internals,
dependency/build/cache dirs), so the tree keeps its hygiene when opted out
while an opted-in project can browse the entries both transports return.

Fixes https://github.com/NousResearch/hermes-agent/issues/55169
2026-09-27 20:10:31 -05:00
hermes-seaeye[bot]
865b141a3d fmt(js): npm run fix on merge (#125870)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-09-28 01:05:02 +00:00
Brooklyn Nicholson
2ffa4977ba feat(desktop): add reasoning level up/down keybind actions
Two new Composer rows in the keyboard-shortcuts panel — Reasoning level
up / Reasoning level down — stepping the active session's effort through
the ladder from the previous commit. Ships unbound like dictation: the
chords users pick for a runtime dial (Alt+., Ctrl+Alt+↑, Numpad +/-) are
too personal to claim by default.

A new `editableTargetPolicy: 'modified'` on the action opts it into
firing while the composer is focused for any combo carrying a real
modifier — including Alt and Numpad chords without a primary modifier,
which the global mod/ctrl rule never covered. Bare and shift-only
rebinds stay with the input, so typing is never hijacked.

The handler writes optimistically with rollback and a monotonic request
sequence, so rapid presses can't have a slow earlier response revert a
newer one. Without a live session the draft pick still steps (it ships
on the next session.create) and no config.set is issued — without a
session the RPC falls back to persistent profile config.

Desktop only for now: the Ink TUI has no rebindable keybind registry to
register these actions in (its handlers are hardcoded useInput chains).

Builds on the approach of the closed-unmerged #46174 (handler shape,
editable-target policy, stale-response guard).

Fixes https://github.com/NousResearch/hermes-agent/issues/71627
2026-09-27 19:50:13 -05:00
Brooklyn Nicholson
ad799e37fb feat(desktop): step reasoning effort through a session-scoped ladder
Reasoning effort today is reachable only through the model menu or
/reasoning — both interrupt keyboard-first workflows when moving between
quick and hard tasks. This adds the pure stepping logic those surfaces
share: one notch through off → minimal → low → medium → high → xhigh,
clamped at both ends, with max/ultra kept off the ladder so a step never
crosses into the expensive tiers by accident.

The session write uses the same session-scoped config.set the model menu
issues and normalizes the gateway's echo so callers settle their
optimistic store on the reported truth.

Part of the #71627 keybind work.
2026-09-27 19:50:13 -05:00
Teknium
80ee8830db fix(tui_gateway): declare known_issues on the plugins.manage result contract; drop issue number from test filename 2026-09-27 17:06:55 -07:00
Brooklyn Nicholson
88039d73b3 feat(tui_gateway): inline_images=false switch on session.resume history reads
_history_dict_text has always had both image renderings, but
_coerce_message_text hard-coded image_urls=True at both call sites, so the
reference-form branch was unreachable from the API: a remote client re-read
the sum of every attachment the conversation ever carried (26.30 MiB on the
measured session.resume, paid again on every reconnect) with no way to ask
for less. session.resume now accepts inline_images (default true) and
threads it through every path that carries messages — cold/lazy/eager
resume, live reattach via _live_session_payload — routing to the existing
[image] branch when false. The REST twin lands in the follow-up commit.

Fixes https://github.com/NousResearch/hermes-agent/issues/116511
2026-09-27 19:06:26 -05:00
Brooklyn Nicholson
d3f9ba3cd5 feat(tui_gateway): add session.archive RPC for Desktop archive parity
Desktop archives sessions via PATCH /api/sessions/{id} -> set_session_archived,
but the TUI gateway had no equivalent JSON-RPC method, so TUI clients had to
keep every session visible or delete it permanently. Open PR #47184 added this
against the pre-split server.py layout and resolves only live runtime sessions;
this lands the method on current main next to session.set_hidden with the same
two-tier resolution: live runtime id first (unpersisted drafts defer via
pending_archived, applied by _ensure_session_db_row like pending_hidden), then
a stored id/key resolved through the profile db, covering the historical rows
the session.list picker shows. Contract declared in tui_gateway/contracts and
rendered into the generated TS/OpenRPC catalog.

Fixes https://github.com/NousResearch/hermes-agent/issues/47168
2026-09-27 19:06:26 -05:00
Brooklyn Nicholson
a0ec0d286f feat(cli): /s as a one-letter alias for /steer
/queue has /q; /steer forced users to type the full word mid-run just to
inject a line after the next tool call. Add the same one-letter alias in
the central registry (every surface derives from it) and mirror it in the
TUI's local slash registry, the way /q is mirrored — checked that no
TUI-local /s binding shadows it (the /q vs /quit collision, #31983).

/i for interrupt stays out: /interrupt is not a registered command yet
(#69954 holds that decision), and the issue gates the alias on its
existence.

Fixes https://github.com/NousResearch/hermes-agent/issues/119176
2026-09-27 19:04:20 -05:00
hermes-seaeye[bot]
cd3f453f3e fmt(js): npm run fix on merge (#125819)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-09-27 23:22:39 +00:00
Brooklyn Nicholson
26472756f1 feat(desktop): skip staging for local OS drops; keep inline @file refs
Since #43109 every OS drag-drop is unconditionally routed through the
file.attach staging pipeline, even when the gateway runs on this
machine and shares its filesystem. Local-mode users paid a duplicate
copy per dropped file and lost the original path semantics.

partitionDroppedFiles now takes the staging context (session remote vs
local via the connection store, backend cwd, terminal backend). When
the backend resolves this machine's paths as-is — a local connection
on a non-container backend — a non-image OS drop keeps its
original-path inline @file: ref, exactly like in-app project-tree
drags. Remote gateways, container backends (docker/ssh/...), Windows
host -> POSIX-backend crosses, images (vision needs the bytes queued
gateway-side), and path-less drops still go through the upload
pipeline. Applied across every drop surface: the conversation area,
the composer form, the contenteditable input, and the message-edit
composer.

Fixes https://github.com/NousResearch/hermes-agent/issues/52427
2026-09-27 18:04:40 -05:00
Brooklyn Nicholson
d294750932 refactor(desktop): extract attachmentPathNeedsUpload into lib
The byte-vs-path upload decision now lives in one pure helper both the
submit-time upload pipeline and drop-time routing read, so the two
sides can never drift. Behavior unchanged; next commit adds a caller.
2026-09-27 18:04:40 -05:00
hermes-seaeye[bot]
2e627917a6 fmt(js): npm run fix on merge (#125798)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-09-27 23:01:44 +00:00
Brooklyn Nicholson
511a6b1c16 fix(desktop): ⌘1…⌘9 switch the tab under the pointer again, and profiles when there is none
#92569 made profile.switch.N unconditional and shipped view.tabSlot.N
unbound, which threw away the hover → focused → workspace tab dispatch
from #74447: holding ⌘ still painted tab-number hints, but the chord
switched profiles. The reporter's real complaint was that the tab
dispatch was hardcoded inside the profile handler, so rebinding the
chord could not separate the two.

Give the keybind registry a `passthrough` action kind: the combo index
keeps every action bound to a chord in registration order, the
dispatcher runs the first and, when a passthrough handler returns
`false`, hands the chord to the next. view.tabSlot.N defaults to ⌘N
ahead of profile.switch.N and declines when no zone is a real tab
strip or the strip has no Nth tab, so ⌘N is "tab N" over a strip and
"profile N" anywhere else. Either action can be rebound on its own,
the panel does not flag the layered pair as a conflict, and the held-⌘
hints key off the tab action they describe.
2026-09-27 18:00:11 -05:00
Brooklyn Nicholson
e98be8a328 fix(desktop): label large pastes and fill file-only bubbles with their chips
A large-paste file chip reads "Pasted content" (path on hover) instead of
a long absolute path, and a turn that is only file chips renders them in
the bubble rather than an empty bubble above a detached row.
2026-09-27 17:45:06 -05:00
Brooklyn Nicholson
cac30a91cd fix(desktop): lift a stored turn's attachment refs into the chip row
A persisted user row carries the composer's leading @file:/@folder: block
inline, so reloaded, reconciled and in-flight bubbles opened with a raw
path and a blank line where the live bubble showed a chip under the
prompt. Hydration now moves that leading block into attachmentRefs, the
same shape the optimistic row uses, and the in-flight prompt projects
through the same conversion.
2026-09-27 17:45:06 -05:00
Brooklyn Nicholson
f9a57d5f91 fix(desktop): stop finished replies rendering twice after settle
Two settle paths could each paint a second bubble for one stored row:

1. use-message-stream: a rewritten final (response_previewed /
   response_transformed) shares no text prefix with the streamed interim, so
   the prefix heuristics missed and the volatile boundary flag could not
   speak for it once a chained message.start reset it. The terminal frame
   names the stored row it settled (persisted_turn.final_assistant_row_id);
   settle onto the display-only interim keyed on that durable receipt instead
   of appending a duplicate.

2. use-session-actions overlayConcurrentMessageChanges: the committed row
   and the settled live row capture the same reply at two moments while it
   kept streaming, so the strict text-equality guard rejected one as a
   duplicate of the other. Accept either as a forward text-extension of the
   other via isStrictAnswerTextExtension, the same trade
   removeRepresentedLocalLiveProjection already made.

Four new tests cover the rewritten-previewed, rewritten-transformed,
lagging-live-row, and ran-past-committed-row shapes.

Fixes https://github.com/NousResearch/hermes-agent/issues/123993
Fixes https://github.com/NousResearch/hermes-agent/issues/124128
Refs https://github.com/NousResearch/hermes-agent/issues/122167
2026-09-27 15:11:08 -05:00
Brooklyn Nicholson
bb09271d6a fix(desktop): show why a Kanban task is blocked and let it be unblocked 2026-09-27 15:09:50 -05:00
funky-xamarin
b911605dc5 fix(desktop): treat unknown version as absent in status labels 2026-09-27 15:08:15 -05:00
Brooklyn Nicholson
8859acea58 fix(desktop): keep archived sessions archived and renames consistent across session lists
Archiving a session could resurrect it in the sidebar: a sessions page read
before the archive RPC committed could land after the projects.tree refresh
pruned the removal tombstone, so the only remaining guard (tombstone
membership) had nothing to say and the stale row re-entered through the
keep set. Tombstones also only matched the row's tip + lineage root, so the
same conversation could return under an intermediate lineage segment id.

- session-removal: track the direction of the last removal edge per id;
  `sessionRemovalIntersected` answers "did this removal lifecycle move
  toward removal since this fetch started", surviving the tree prune.
  `tombstoneRowIds` names every id a row answers to (tip, root, and all
  lineage segments).
- use-session-list-actions: capture a tombstone-generation snapshot at the
  start of every fetch (recents, messaging refresh, platform load-more) and
  reject rows raced toward removal; filter after the merge so survivors are
  covered too; the cron slice gets the same guard.
- projects: fetchProjectSessions (drill-in) applies the same guard via
  `excludeProjectSessions`, keeping the project reference on no-match.

Renaming a session patched only the bare-id `$sessions` slice, leaving
project-scoped rows (overview previews, entered-project lanes), cron and
messaging rows on the stale title until an unrelated refetch.

- session: `applySessionTitle` patches every sidebar slice
  (recents/cron/messaging/unlisted), matching rows lineage-aware via
  sessionMatchesStoredId, reference-stable when nothing changes.
- projects: `applyRenamedSessionTitle` mirrors the rename into the cached
  project tree (lane rows + preview sessions, recomputing counts) and
  re-pulls the authoritative tree.
- Wire both into the rename dialog, the /title slash command, and the
  session.title gateway event.

Fixes https://github.com/NousResearch/hermes-agent/issues/123685
Fixes https://github.com/NousResearch/hermes-agent/issues/123337
2026-09-27 14:31:39 -05:00
teknium1
8860e1d78b fix(desktop): only Electron's singleton socket gets the short TMPDIR
The launcher replaced TMPDIR with /tmp for the whole Electron process tree, so
the Python backend inherited TMPDIR=/tmp while TMP/TEMP/HERMES_SCRATCH_DIR
still named the scratch dir; apply_scratch_tmp_env then treated /tmp as
user-set and the agent subtree wrote temp files to /tmp.

The launcher now passes the original in HERMES_DESKTOP_TMPDIR and Electron main
restores it right after requestSingleInstanceLock(), so every child gets the
profile scratch dir back.

The override threshold is Chromium's real budget instead of the shared 50-byte
socket constant: sun_path holds 107 bytes and Chromium appends
/scoped_dirXXXXXX/SingletonSocket (33 bytes, measured on Electron 40), so a
TMPDIR up to 74 bytes is kept.
2026-09-27 11:44:31 -07:00
liuhao1024
26f9fd1035 fix(desktop): compare transcript tips, not counts, in the stale-send guard
Retention (boundRetainedTranscript, #77311) releases the store's head down to the live window plus its budget, so a long tool-heavy chat keeps a store shorter than the latest 120-row page renders. The stale-transcript guard compared authored counts, so every send on such a chat saw 'remote ahead', installed the refreshed page, got re-trimmed on the next anchor move, and refused again — a permanent 'Chat out of date' loop with no convergence (#123909).

Compare tips first: the last durable rowId of the local view equal to the page's own last durable rowId means the view is current however much head was paged out, while a peer window's newer row still changes the tip (#65047 protection intact). Counts remain the fallback when tips cannot be established (no durable ids on either side).
2026-09-27 13:34:37 -05:00
Brooklyn Nicholson
7d7f712a78 fix(desktop): name web, browser and vision tool runs for what they did
Fixes #123085

Co-authored-by: Yuan Li <dskwelmcy@163.com>nCo-authored-by: Semir Kabir <semirkabir@users.noreply.github.com>
2026-09-27 13:23:46 -05:00
Brooklyn Nicholson
aa25f9e85f fix(desktop): Kanban board switcher outside the full-page layout
A Kanban board opened in a split route tile rendered no board switcher:
the board contributed it to WORKSPACE_PAGE_HEADER_AREA unconditionally,
and only the workspace pane paints that area. The tile's contribution
also leaked into another page's header and shared its id with the full
page's, so closing the tile removed the page's switcher.

Add WorkspacePageHeaderControl (exported via the plugin SDK). The
workspace pane's render provides a private host context; inside it the
control projects into the page header, anywhere else it renders inline.
The board mounts BoardSwitcher once, through it, in its own header row.

Fixes #123597

Originally authored by Justin Haynes (@jhaynes).
2026-09-27 13:18:46 -05:00
Brooklyn Nicholson
c04e9a1d0d fix(desktop): honour a user-set voice.silence_duration in the voice loop
The desktop voice conversation hardcoded its silence hold at 1,250 ms and
ignored the voice.silence_duration config key the CLI, TUI and gateway
capture paths already honour, so users who pause mid-thought were cut off
with no way to raise the threshold, and fast local STT/TTS stacks sat
through a rigid extra second of dead air.

Seed a $voiceSilenceMs atom from /api/config on every refresh (the same
useHermesConfig path as stop_phrases / barge-in sensitivity): a value the
user actually changed (differing from /api/config/defaults) overrides the
tuned desktop hold; an untouched install keeps 1.25 s, because /api/config
merges DEFAULT_CONFIG and would otherwise raise the hold to 3 s for
everyone. Malformed or non-positive values fall back like the gateway's
shape-safe lookup, and booleans never coerce to 1 s.

The barge-in utterance endpoint reads the same atom live per frame, so
barge-in capture and the voice loop stay matched across config refreshes.

Supersedes #83572 (live-atom read, no backend-default comparison, would
triple the hold to 3 s for untouched installs) and #85772 (same, plus
accepts booleans as 1 s).
Fixes https://github.com/NousResearch/hermes-agent/issues/83518
Fixes https://github.com/NousResearch/hermes-agent/issues/124760
2026-09-27 13:15:32 -05:00
Brooklyn Nicholson
ac9a850eb9 fix(desktop): don't offer Rename on canonical Bot Chat tabs
Fixes #124857
2026-09-27 13:11:35 -05:00
Adolan
9b17e80970 fix(desktop): keep single line breaks in assistant replies 2026-09-27 13:11:25 -05:00
Brooklyn Nicholson
8c180e3fa2 fix(desktop): paint OS-dropped images as thumbnails in sent messages
Fixes #123368

Co-authored-by: kokhlo <konstantin.khlopkov93@gmail.com>
2026-09-27 13:10:00 -05:00
Brooklyn Nicholson
a776efb06a fix(desktop): show saved key previews without the backend's redaction sentinel
Fixes #124378

Co-authored-by: Adolan <94890352+Adolanium@users.noreply.github.com>
2026-09-27 13:08:44 -05:00
PRATHAMESH75
b07a3b46ed style(desktop): satisfy curly + import-sort lint on personality folding
The new foldPersonalityName call sites and the @/lib/personalities import
tripped the curly and perfectionist/sort-imports rules, reddening the
desktop check:lint gate. Brace the single-statement ifs and move the
value import into its natural-sorted internal position.
2026-09-27 12:59:34 -05:00
PRATHAMESH75
b6b3708db5 fix(desktop): fold personality names like the runtime so only resolvable rows are offered
Both dropdown readers (personalityOptions, personalityNamesFromConfig) listed
config keys verbatim via Object.keys, but the runtime folds every key
(available_personalities: str(name).strip().lower(), skipping the neutral
spellings none/default/neutral). So a root/agent case clash (Catgirl vs catgirl),
a whitespace-padded name, or a neutral spelling surfaced a row the runtime never
resolves — the user could pick it and get a different (or no) definition than the
one shown, with the widened root-block read (#123297) making mixed case the likely
input.

Add a shared foldPersonalityName + NEUTRAL_PERSONALITY_NAMES in @/lib/personalities
mirroring hermes_cli/personality.py, fold+skip in both readers, and route
normalizePersonalityValue through it (which also folds the 'neutral' spelling it
previously missed). Reuse the existing isPlainObject guard in helpers. Pin the
behavior with case-variant + whitespace + neutral-name cases in both test files.

Reported by @Enough1122.
2026-09-27 12:59:34 -05:00
PRATHAMESH75
afb924d841 test(desktop): cover personalityOptions' root read and pin GUI ordering
Address review on #123378:
- Add enumOptionsFor('display.personality', …) tests with a root-level
  `personalities` block, deriving expected built-ins from
  BUILTIN_PERSONALITIES (change-detector rule). Reverting the new root
  read now fails these, closing the coverage gap the reviewer flagged.
- Replace the Set-based clash assertion in personalityNamesFromConfig
  with direct array equality so it pins membership, dedupe, and the
  root-before-agent ordering the CLI listing uses.
2026-09-27 12:59:34 -05:00
PRATHAMESH75
9992d70f0d fix(desktop): list root-level personalities in the Settings dropdown (#123297)
The Desktop Settings → Chat → Personality dropdown and the config-driven
personality name list read only `agent.personalities`, so a persona
registered under the root-level `personalities` block — which the Python
runtime, CLI `/personality`, and gateway all honour via
`available_personalities()` — never appeared in the GUI and could not be
selected.

Read both blocks in `personalityOptions` (settings/helpers.ts) and
`personalityNamesFromConfig` (lib/chat-runtime.ts), mirroring the runtime's
merge order (root `personalities`, then `agent.personalities`), so the two
surfaces list the same personas.
2026-09-27 12:59:34 -05:00
finn763
9fa23760bc fix(desktop): never report the 0.0.0 placeholder in About
The desktop package.json carries a 0.0.0 placeholder on main (real versions come from channel builds and the live backend), but two About paths repeated it verbatim: appVersionInfo fell back to packageVersion when no display version was baked, and the native panel was seeded - and could be refreshed - with an empty applicationVersion, which macOS renders as the bundle's 0.0.0. Skip placeholder values ('', 0.0.0, unknown) across every candidate, and map the native panel string through nativeAboutVersion so unknown becomes an explicit label (backend spelling git.<short>[.dirty]) instead of 0.0.0. The renderer keeps its ''-means-unavailable contract. Closes #124581.
2026-09-27 12:50:11 -05:00
Adolan
861893da2f fix(desktop): show provider retry and auto-recovery waits in the status row 2026-09-27 12:50:03 -05:00
Adolanium
d1167fdff7 fix(desktop): match custom:<key> providers in the settings and bot model pickers
model.info and saved profiles report a user-defined provider as custom:<key>, but the catalog row uses the bare key as its slug. Settings > Model and the Bot Mode picker compared the two with ===, so a saved custom provider never found its row. Settings showed a duplicate custom:<key> entry and a Set up provider button, and the bot editor fell back to the manual form.

Both now match rows with catalogProviderMatches, like the composer picker already does. Settings uses a small findCatalogProvider helper for every row lookup, including the aux and MoA slots and the endpoint passed on Set to main. catalogProviderMatches is now exported through the plugin SDK so the bot picker can use it.
2026-09-27 12:49:56 -05:00
Adolan
0559c367ef fix(desktop): offer New cron from the empty scheduled-jobs detail 2026-09-27 12:49:39 -05:00
Adolan
6b512010ec fix(desktop): show one error card when the agent fails to start 2026-09-27 12:49:32 -05:00