Commit Graph

6908 Commits

Author SHA1 Message Date
Brooklyn Nicholson
6007b3ce4f refactor(desktop): export DialogCloseButton for the boot-failure overlay
The overlay carried a byte-for-byte copy of DialogContent's close button.
2026-09-28 11:50:16 -05:00
Brooklyn Nicholson
1b8569f41f feat(desktop): filter Messaging platforms by status
A status filter sits beside the Messaging search, with a tab only for tones
some platform is in (errors, needs attention, connected, inactive). Picking a
tab moves the detail pane into the filter; a platform that leaves the filter
because you just toggled it stays open.

Messaging and the gateway menu now read platform state through one
classifier, so the same bot can no longer show two different tones.

Co-authored-by: Sonnenwerk <a.majuskel@gmail.com>
2026-09-28 11:49:02 -05:00
finn763
1bb55780ab fix(desktop): collapse a redelivered interim into its bubble (#120104, #93926)
A message.interim redelivered after the first copy sealed the stream took the
standalone-append path and painted the same commentary twice. Refresh the
newest same-text assistant row of the current turn in place instead,
comparing whitespace-normalized text.

Supersedes #120145 and #93962.

Co-authored-by: Brammiebram <64777864+Brammiebram@users.noreply.github.com>
2026-09-28 11:31:46 -05:00
Abarajithan
675f29da03 fix(desktop): settle high-overlap sealed replies 2026-09-28 11:31:46 -05:00
Brooklyn Nicholson
34e09bff78 fix(desktop): namespace the project-row drop-hover attribute
Some checks failed
Deploy Site / deploy-vercel (push) Has been cancelled
Deploy Site / deploy-docs (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
Skills Index Freshness Check / check-freshness (push) Has been cancelled
Build Skills Index / build-index (push) Has been cancelled
Build Skills Index / trigger-deploy (push) Has been cancelled
'data-drop-hover' is a generic token any component could style; rename to
'data-session-drop-hover' to match the session-drag module that owns it
(same attr constant in session-drag.ts) so no foreign CSS can hit-test it.
2026-09-28 10:56:57 -05:00
Hermes Agent
fbfe3e29f7 test(desktop): cover the project-row drag-drop target
Moves to the dropped project row, and stays a no-op over Home and the
session's own current project — the same exclusions the row's existing
"Move to project" menu applies.
2026-09-28 10:56:57 -05:00
Hermes Agent
c4d14871e1 feat(desktop): drag a sidebar session onto a project row to move it
Extends the existing pointer-based session drag (session-drag.ts) with a
new resolvable target: a sidebar project row (tagged data-sessions-project
by ProjectOverviewRow). Dropping a session there calls the same
moveSessionToProject RPC the row's own "Move to project" context-menu item
already uses, so this is a faster path onto an existing capability rather
than a new one. Home and the session's current project are excluded as
targets, matching the context-menu's own filtering. The hovered project
row is highlighted imperatively (a data-drop-hover attribute + an
outline-sidebar-ring style) to keep the sidebar out of React re-render
churn during pointer travel.
2026-09-28 10:56:57 -05:00
Siddharth Balyan
e408d36339 fix(desktop): break the combo.ts <-> actions.ts import cycle (#126361)
2ffa4977ba made combo.ts import keybindActionAllowedInEditableTarget
from actions.ts, while actions.ts already imported IS_MAC from
combo.ts. When actions.ts is evaluated first, its KEYBIND_ACTIONS
table reads IS_MAC before combo.ts has initialized it. The dev
renderer throws "Cannot access 'IS_MAC' before initialization" and
#root stays empty.

actions.ts now reads the platform from the leaf module
@/lib/platform (isMacPlatform), which chords.ts and other keybind
code already use. actions.ts no longer imports combo.ts, so the
cycle is gone. The three defaults resolve at module load exactly as
before.
2026-09-28 15:00:41 +00:00
Siddharth Balyan
8f21606116 Hiding reasoning no longer hides tool rows, and tool_progress: off no longer hides reasoning (salvage #125948) (#126479) 2026-09-28 14:18:37 +00:00
Brooklyn Nicholson
7dbbb0f4a6 fix(desktop): report the current commit in updater status 2026-09-28 08:58:55 -05:00
alt-glitch
fc4dbe32df fix(logs): hermes logs --since/--level handle unstamped lines and MCP output
`hermes logs --since` and `--level` passed every line that had no leading
timestamp. A traceback's frames are written without one, so an old error
printed its frames without the header that was filtered out, and
`--component` dropped the frames of a matching record. `hermes logs` now
reads each unstamped line as part of the record above it: the line gets
that record's verdict for every filter, in the tail read and in `-f`.
Lines before the first stamp in the read window have an unknown time and
level, so they are dropped when `--since` or `--level` is set.

mcp-stderr.log had no parseable stamp at all: the banner started with
`=====` and server output was copied raw, so `hermes logs mcp --since`
printed the whole file. The stderr tee already reads each server's
stderr in a thread, so it now writes one line at a time, each prefixed
with the asctime-shaped local stamp the Python logs use. The banner
starts with the same stamp. The stamp comes from new public
`timestamp()`/`stamp_line()` in hermes_cli/stderr_timestamp.py, which
stays stdlib-only. The desktop MCP log view accepts both banner shapes.

A test now requires a real writer's sample line for every LOG_FILES
entry to parse with `_parse_line_timestamp`. The docs no longer say the
`timezone` key changes log timestamps; log lines use the machine's
local time.
2026-09-28 19:02:20 +05:30
alt-glitch
2d9d4abbef fix(desktop): desktop.log stamps lines in local time, like the Python logs
desktop.log stamped each line with an ISO-8601 UTC time in brackets
(`[2026-09-28T07:48:19.466Z]`), while agent.log, gui.log and errors.log use
Python's local-time asctime (`2026-09-28 13:18:46,062`). Reading one
incident across logs/ meant converting clocks by hand, and
`hermes logs desktop --since 1h` returned every desktop line, because its
timestamp regex expects the Python shape at the start of the line and
lines it cannot parse pass the filter.

formatLogStamp() now emits the Python shape in local time, and
formatDesktopLogLine() puts it first: `2026-09-28 13:18:46,062 [hermes] ...`.
The #84405 comment already said the stamp matched the Python convention;
now it does.
2026-09-28 19:02:20 +05:30
Brooklyn Nicholson
71cb7d60f8 fix(desktop): add model visibility recovery action
Edit Models gains a "Reset to defaults" action: it clears the persisted
visible/known model snapshots so models frozen hidden by the old
allowlist adoption come back, while custom models stay stored and shown.
Copy added in all supported locales.

Original work by @atomic-pancake-49 in
https://github.com/NousResearch/hermes-agent/pull/122055.

Fixes https://github.com/NousResearch/hermes-agent/issues/122053
2026-09-28 08:23:14 -05:00
teknium1
3df15fd727 fix(desktop): trim connection-scoped tile salvage to the tab-persistence invariants (#120106, salvage #121983)
Keep the (connectionId, profile) tile keying and its two invariant tests;
drop the post-switch remembered-route re-navigation (separate scope, hook
latch juggling) and the two extra delete-path tests.
2026-09-28 06:06:17 -07:00
Teo | Nexcore
70f869464b fix(desktop): delete tabs in the active connection scope 2026-09-28 06:06:17 -07:00
Teo | Nexcore
68f724b3b8 fix(desktop): restore connection-scoped tabs and chat on backend switch 2026-09-28 06:06:17 -07:00
teknium1
28365a5bec fix(tui_gateway): session.set_hidden requires an explicit hidden flag (#122190, salvage #122568)
Trim the salvaged handler/test prose to the salvage bar and make the
contract match the handler: SessionSetHiddenParams.hidden is required
(no default=True), so the generated TS/OpenRPC types stop advertising an
optional flag whose absence used to hide the whole compression lineage.
Regenerated apps/shared/src/gateway-contract.*.
2026-09-28 06:06:17 -07:00
teknium1
fc51ca12a2 fix(desktop): trim dead-tile retirement to the salvage bar (#125678, salvage #125706)
Same outcome as the salvaged commits — a restored tab whose id every
profile answered 404 for in calm conditions is dropped instead of being
re-probed on every launch — with less machinery:

- one set of scope listeners per backfill run instead of six per tile;
  retired ids are tracked by storedSessionId, so a tile array rebuilt by
  the backend-identity guard between start and gateway-open cannot make
  the retire silently never fire.
- the "profile inventory known" gate lives in probeStoredSession, where
  the ladder decides what its evidence proves, instead of in the caller.
- the nine-case test file is folded into session-tile.test.ts as two
  invariants: retire on all-profile 404 (red on origin/main), keep on
  5xx / network failure / switch in flight / profile A→B→A / connection
  switch / stashed draft / unknown inventory.
2026-09-28 06:06:17 -07:00
funky-xamarin
aa492fb76e fix(desktop): keep dead tiles out of reopen history 2026-09-28 06:06:17 -07:00
funky-xamarin
93ef2e2d09 fix(desktop): release cancelled tile backfill subscriptions 2026-09-28 06:06:17 -07:00
funky-xamarin
7a086ff53b fix(desktop): retire empty restored tabs after conclusive absence 2026-09-28 06:06:17 -07:00
teknium1
dd7f6ea1d3 fix(desktop): trim cold-resume preview salvage to the transient paint (#125336, salvage #125356)
Drop the second layer of #125356 (an `orphanedModelPick` reseed in
use-model-controls.ts): with the preview painted through the transient
setters the empty-provider row can no longer be written, and a fallback
that heals an unreachable state is defense-in-depth the salvage bar
excludes. The invariant test (preview never persists) stays.
2026-09-28 06:06:17 -07:00
Konstantin Khlopkov
7d033d61e4 fix(desktop): paint the cold-resume preview through the transient composer setters
An abandoned cold resume left the composer holding a manual model with an
empty provider in localStorage: the preview paint persisted both values,
and only a completed session.resume repaired them. Every new chat then
shipped the model alone, the gateway paired it with the profile provider,
and the coherence gate rejected the pair - the chat died on arrival and
the poisoned row survived restarts.

Paint the preview with the transient setters (the session.info heartbeat
path already does), and reseed a manual pick whose provider is empty from
the profile default so installs already carrying the stranded row recover
on the next launch.
2026-09-28 06:06:17 -07:00
hermes-seaeye[bot]
365259378d fmt(js): npm run fix on merge (#126363)
Some checks failed
Deploy Site / deploy-vercel (push) Has been cancelled
Deploy Site / deploy-docs (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
Skills Index Freshness Check / check-freshness (push) Has been cancelled
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-09-28 12:14:07 +00:00
teknium1
ad4e349df7 fix(review): supersede displaced PTY viewer, route project tile by active source
Review findings on #126144 (independent review, all minor):

- pty_session.close_other_sessions closed a sibling tab's PTY under it without
  closing its websocket: two tabs sharing one attach token on different
  profiles left tab A silent until its next keystroke returned 1013. Close the
  displaced viewer with the documented 4409 before closing the session. Still
  closes attached siblings (skipping them would reopen the #125287 lease hole
  via the old handler's finally: detach).
- openNewSessionTile paired projectProfile() with $newChatConnectionId — the
  source captured with a stale new-chat pin — so a pin made on another
  connection produced the right profile on the wrong host. Build the route
  from the active source (resolveActiveSourceOwnerRoute) for the
  project-derived profile.
- windows.test.ts: restore the no-parentSessionId case asserting the
  viewed-profile fallback alongside the parent-routed case.
2026-09-28 04:59:38 -07:00
teknium1
df05d0a8e2 fix(desktop): explain the stale-open gateway reconnect guard (#121680, salvage #121771)
Trim-follow-up to the salvaged PR: document why ensureGatewayOpen asks the
socket itself instead of trusting the effect-mirrored gatewayStateRef, and
why the registry backs a not-yet-populated ref. No behaviour change.
2026-09-28 04:59:38 -07:00
Joel
c39ecff00f test(desktop): cover stale-open closed socket recovery 2026-09-28 04:59:38 -07:00
Joel
150f28a8bb fix(desktop): verify gateway socket before skipping reconnect 2026-09-28 04:59:38 -07:00
Joel
341766ebb8 fix(desktop): verify gateway socket before skipping reconnect 2026-09-28 04:59:38 -07:00
Joel
4185428c2d test(desktop): cover stale gateway ref recovery 2026-09-28 04:59:38 -07:00
Joel
e56ec05fb8 fix(desktop): recover from stale gateway ref 2026-09-28 04:59:38 -07:00
teknium1
3361ac4a82 fix(desktop): trim #120899 salvage — owner route only, one window per chat (#120213, salvage #120899)
Keep the substantive fix from #120899 (renderer carries the session owner's
connectionId, buildSessionWindowUrl serializes it, boot honors it for
win=secondary windows, child watch windows route via their parent) and trim
to the salvage bar:

- windows.ts: drop the fail-closed assertSessionOwnerResolved rung; an owner
  that resolves nowhere keeps the documented active-profile fallback.
- main.ts: keep the session-window registry keyed by sessionId (one window per
  chat) instead of (connectionId, profile, sessionId).
- windows.test.ts: fold the connectionId invariant into the existing owner
  test instead of adding a second one.
2026-09-28 04:59:38 -07:00
itsflownium
13919e52a8 style: separate secondary window URL assertions 2026-09-28 04:59:38 -07:00
itsflownium
25bc086d3f fix: preserve connectionId for secondary chat windows (Fixes #120213) 2026-09-28 04:59:38 -07:00
teknium1
f8deae40fd fix(desktop): own a project-cwd tile by the project's profile (#124265, salvage #124296)
The #79005 pin (projectProfile + pinNewChatProfile) lives in
startWorkspaceSession, which the sidebar project "+" only reaches while no
chat is loaded. With a chat occupied, startSessionInWorkspace stacks a tile
via openNewSessionTile({ cwd, listed: false }) and the owner fell through to
$newChatProfile || $activeGatewayProfile — a stale new-chat pin routed
session.create to the wrong (default) profile with the project's cwd.

Fix it once at the tile create: a tile anchored at a project path defaults
its profile to projectProfile() unless the caller named one. That also
covers the other project-cwd tile paths (project-row "+" drags via
onNewSessionSplit, the "new project" drop via $newProjectSessionRequest).
All-profiles view has no owner and keeps the old fallback.

Direction credit: @kokhlo (#124296) identified the skipped pin; this lands
the profile on the tile call instead of adding a DI'd door module.
2026-09-28 04:59:38 -07:00
teknium1
e55c1cb879 fix(desktop): pin /messages/around reads to the session owner and trim the pin seam (#125372, salvage #125414)
Follow-up trim of the salvaged #125414 commits:
- fetchHistoryWindow (GET /api/sessions/{id}/messages/around) is the same
  session-scoped read class and now carries sessionReadOwnerPin too.
- client.ts: drop the unused mode field and the sessionOwnerForRead wrapper,
  shorten the seam comment.
- sessions.test.ts: keep two invariant tests (owner pin applied; explicit
  caller pin authoritative).
- use-timeline-history.test.tsx: stub sessionReadOwnerPin in its client mock
  so the hook test compiles against the new import.
2026-09-28 04:59:38 -07:00
Yuan Li
36d59a2ca3 test(desktop): stub the session-owner pin in the timeline-index client mock 2026-09-28 04:59:38 -07:00
Yuan Li
af9d7a6318 fix(desktop): pin session REST reads to the session's owner connection
A session-scoped read (detail / messages / timeline) dispatched on the
window's ambient connection scope: with a registered remote that exposes
a same-named profile, both directions answered 404 "Session not found"
for rows that verifiably exist on the other machine (#125372).

The renderer already persists the owner (tile route -> owner hint ->
connection-tagged row, resolved by knownOwnerForSession); the read path
never consulted it. Wire that resolver into api/ through a setter seam
(same pattern as setApiRequestProfile, no store->api cycle) and pin the
resolved connection on the three read helpers. An explicit (connection,
profile) caller scope stays authoritative; a bare-profile scope and the
ambient dial gain the owner's connection pin; reads with no resolvable
owner keep today's ambient behavior.
2026-09-28 04:59:38 -07:00
teknium1
a767190e84 fix(desktop): trust a remote HERMES_HOME only when it exists on the remote (#118988, salvage #119022)
probeWindowsRemote took `$env:HERMES_HOME` at face value. A stale
User-scope value on the remote (install.ps1 persisted one before
47f4ab3a17 stopped doing so; nothing removes it) or a client path
leaked over the SSH channel then flowed straight into
assertSafeRemoteHome and every Windows remote failed with
"Unsafe remote Hermes home" even though the remote install was fine.

Only honour HERMES_HOME when it names an existing directory on the
remote; otherwise fall back to the remote's default
%LOCALAPPDATA%\hermes, exactly as an unset variable does. The
installer/updater half of the report is already fixed on main:
scripts/install.ps1 no longer calls
[Environment]::SetEnvironmentVariable("HERMES_HOME", ..., "User")
(47f4ab3a17), and the updater's "Refreshed Windows gateway launcher
scripts" step only rewrites gateway.cmd/.vbs with a process-scoped
`set HERMES_HOME=`; it never persisted anything.

Slim redo of #119022 by @MohamadKanso, which reached for the User/Machine
registry scopes instead of a remote existence check.
2026-09-28 04:44:29 -07:00
hermes-seaeye[bot]
b23b8bcf7f fmt(js): npm run fix on merge (#126332)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-09-28 11:22:53 +00:00
teknium1
614b9b3f3c test(desktop): type the gateway mock forwarder in tutorial-lifetime.test.tsx (TS2554) 2026-09-28 04:08:33 -07:00
teknium1
dc357a51b1 test(desktop): teach the tutorial-lifetime gateway mock the profile-routed config.set
display-toggles now writes through requestGatewayForProfile (review fix for the #125969 class);
the tips test's @/store/gateway mock only exposed activeGateway, so the retire path threw on
the missing export. The mock forwards (method, params) so the existing assertions stand.
2026-09-28 04:08:33 -07:00
teknium1
96ae4ee44a fix(review): route Desktop voice/display config writes and the approval-mode RPCs by profile; remount the terminal-backend panel per scope
Independent-review findings on #126138 (all minor, all fixed):

- toolset-detail.tsx: `TerminalBackendPanel` now carries `key={profileScopeKey(profile)}`
  like its sibling `ToolsetConfigPanel`, so a late `getTerminalBackends(A)` response
  cannot repaint A's active backend under B after a profile switch (#96542 class).
- voice-live.ts `setVoiceChatMode` and display-toggles.ts `push`: both sent `config.set`
  on the bare active socket with no `profile`, so under the shared-primary route
  `voice.voice_chat_mode` and `display.*` landed in the launch profile's config.yaml
  (#125969 class). Both now go through `requestGatewayForProfile(activeGatewayProfileKey(), …)`,
  which adds `profile` exactly when the route shares the primary socket.
- use-statusbar-items.tsx: the approval-mode menu's now-scoped `config.get`/`config.set`
  went through the session-routed ambient dispatcher, whose `routeParams` re-scopes
  `params.profile` to the FOCUSED session's owner. The menu now dials
  `requestGatewayForProfile(activeGatewayProfile, …)` directly.

Tests: display-toggles.test.ts asserts the routed profile (red on the PR head);
voice-live.test.ts is new (one assertion, red on the PR head).
2026-09-28 04:08:33 -07:00
teknium1
f3107339c3 test(desktop): expect the approval-mode statusbar write to name its profile
The statusbar menu test renders `useApprovalModeStatusbarItem('work', …)` and
asserted the pre-#125969 unscoped shape `config.set {key, value}`. Since
setApprovalModeForProfile now scopes reads/writes to the viewed profile when
its name is non-blank, a write made while viewing "work" must carry
`profile: 'work'` — the old assertion pinned exactly the bug being fixed (an
unscoped write silently edits the launch profile).

Changed existing assertion (apps/desktop/src/app/shell/approval-mode-menu.test.tsx):
- `toHaveBeenCalledWith('config.set', { key: 'approvals.mode', value: 'manual' })`
  -> same call with `profile: 'work'` added. The blank-profile (launch profile)
  case, which must stay unscoped, is still covered by
  src/store/approval-mode.test.ts.
2026-09-28 04:08:33 -07:00
teknium1
64ea12456a style(desktop): blank lines around the scoped approval-mode read (eslint padding warnings) 2026-09-28 04:08:33 -07:00
teknium1
7b54f710bf fix(desktop): drop a profile's late messaging response after the scope switched (#96542)
Second half of the cross-profile token bleed: after A→B, A's in-flight
getMessagingPlatforms/getPairing could resolve and repaint A's platform/env
snapshot (redacted Telegram token included) under B until B's response won.
Responses whose scope is no longer the rendered one are now dropped. Also
restores the `useRef` import the salvaged commit removed — main's
refreshPlatformsRef still needs it.
2026-09-28 04:08:33 -07:00
liuhao1024
2a4595b1c6 fix(desktop): reset messaging credential fields during render on scope switch
Blanking the stale platforms state in a passive useEffect let the
'new scope + previous profile's data' frame paint first: for ~1s the new
profile's Messaging panel showed the previous profile's redacted
Telegram token as the field placeholder (password inputs render
placeholder text as plain text), then cleared once the per-profile fetch
landed (#96542). Passive effects fire after paint, so the effect-based
reset can never prevent that first stale frame.

Move the reset into render via the React-documented 'adjust state when
a prop changes' pattern (prev-scope comparison + setState during
render), which makes React discard the stale frame before it reaches
the screen. Same four states are cleared as before.

Fixes #96542
2026-09-28 04:08:33 -07:00
teknium1
1442fd44d4 fix(desktop): show the "Applies to" profile scope on the Saved credentials page (#121966)
The vault page already routes every RPC through the (connection, profile)
owner from $settingsScopeProfile (de551dcd3a), but unlike Browser/Memory it
rendered no scope chip, so a non-default selection read as "the default vault
with my entries missing" and the user could not tell which vault Add/delete
targeted. Render the shared SettingsProfileScope selector like every other
profile-scoped settings page.
2026-09-28 04:08:33 -07:00
1052326311
885a37658a fix(desktop): scope terminal backend picker to profile 2026-09-28 04:08:33 -07:00
PRATHAMESH75
ef6039cc77 fix(desktop): scope approval-mode reads/writes to the viewed profile (#125969)
The desktop approval-mode store keyed its per-profile cache by name but sent
`config.get` / `config.set` without a `profile` param. The gateway's config
RPCs are `@_profile_scoped`, so an unscoped call resolves to the backend's
*launch* profile: the menu showed one profile's mode for every profile, and a
write made while viewing another profile silently edited the launch profile.

Name the profile each read/write is for (a blank name is the launch profile, so
omit the param and keep the backend's os.environ/session precedence) — the same
rule `api/client.ts`'s `profileScoped()` already uses for REST settings.
2026-09-28 04:08:33 -07:00