Commit Graph

5454 Commits

Author SHA1 Message Date
Forkbert
a839530cd8 fix(models): clean up DeepSeek picker catalog
(cherry picked from commit f0b854bd067573e33f6ca8e1c5f68b184f4fef90)
2026-09-20 20:41:54 -07:00
teknium1
7756d04e40 fix(desktop): export Switch from the four remaining plugin-sdk test mocks
group-chat-view now renders the hold-detection Switch, so every sibling test that wholesale-mocks
@hermes/plugin-sdk must return it; only the render test's mock had been updated.
2026-09-20 20:41:43 -07:00
fangliquanflq
49c27fa50e test(desktop): keep group view SDK mock complete 2026-09-20 20:41:43 -07:00
fangliquanflq
d633255a0c fix(desktop): preserve explicit group stop cancellation 2026-09-20 20:41:43 -07:00
fangliquanflq
a273c2c031 fix(desktop): preserve messages skipped by group holds 2026-09-20 20:41:43 -07:00
teknium1
b6a87db515 fix(desktop): onboarding and model pill share one provider label table
The salvaged providerDisplayName duplicated onboarding PROVIDER_DISPLAY titles;
keep one table in the lib and let onboarding keep only its featured order.
2026-09-20 20:25:34 -07:00
Forkbert
3235206c5e fix(desktop): label xAI OAuth model selections
(cherry picked from commit c3f708e93b4777f1808fe189a0331aae7148712f)
2026-09-20 20:25:34 -07:00
teknium1
56af2bdf7e fix(desktop): idle renderer stops ticking once a decode placeholder resolves
The DecodeText scramble primitive replayed forever by default, so every
quiet-surface placeholder that uses it (the empty pane zone's "HERMES"
mark, the contrib LOGS/HERMES panes) kept a 45 ms setInterval + setState
alive for as long as it was on screen. Measured with the perf harness on a
seeded 200-message session with no turn running, that ticker alone held the
idle renderer at ~9.6 React commits/s (DecodeText: 16 renders/s); with it
resolved once, the same workspace idles at ~0.7 commits/s.

`loop` now defaults to false; the boot "CONNECTING" overlay — the one
progress surface that should replay while it waits — asks for it explicitly.
The vitest drives the real component under fake timers and is red on base
(the tail keeps scrambling after the hold).

Part of #98394
2026-09-20 20:14:20 -07:00
teknium1
28ff18740b test(desktop): pin the suppressed-rotation aftermath and trim the close-out scenario
Review follow-ups on the rotation-foreground gate:

- Assert the residual a suppressed rotation leaves behind. Nothing
  re-points the primary, so its selection and route keep the
  PRE-rotation stored id; that is benign only because the lineage row
  still resolves it to the tip, which is now asserted with the same
  cachedSessionRow() lookup every sidebar/route resume goes through.
- Note in the suppression cases that the "hash route" shape IS the
  pop-out/secondary-window shape: isSecondaryWindow() starts that
  renderer with $sessionTiles empty and $layoutTree null, so
  $focusedStoredSessionId collapses to the selection and the route is
  the only voice left. A separate pop-out case would be a byte-identical
  store state.
- Trim wrong-session-closeout.test.tsx to the one invariant this fix
  owns: the real producer wired to the real route-follow consumer must
  not navigate, re-select, or move focus. The queued-drain fence and the
  attachment recovery it also composed are pinned by their own suites
  (use-background-queue-drain, use-prompt-actions), so coupling this
  fix's red signal to them only misattributes failures.
2026-09-20 20:14:10 -07:00
teknium1
806a86ae7d test(desktop): pin the wrong-session close-out scenario across the mapped producers
Composes the producers mapped on #86106 into one regression against the real
cache, session-actions and prompt-actions hooks plus the production
owner-routing dispatcher (gateway edge mocked, no injected bindings):

  A active -> B queued send -> user focuses tile C -> B runtime reaped ->
  A's delayed stored-id rotation -> B drain (text + image) -> B recovers

On origin/main the rotation consumer navigates the primary to A-next while
the user is on tile C (route/focus steal); with the #86359 gate the user stays
on C, B's attachment and prompt reach connection-B on B's recovered runtime,
and every stored id maps to exactly its own runtime.
2026-09-20 20:14:10 -07:00
teknium1
c2b94ca273 test(desktop): consolidate the rotation-foreground suite into two invariants
Fold the six #86359 cases into two it.each tables: one negative over the
three surfaces that can name the on-screen session (stored selection,
HashRouter route, focused tile) and one positive over the shapes that still
belong to the rotating lineage. Same coverage, one assertion body per
invariant, so a future change to the foreground rule has one place to read.
2026-09-20 20:14:10 -07:00
Christopher
9aab00844c test(desktop): satisfy import ordering in session state cache test 2026-09-20 20:14:10 -07:00
Christopher
6d7a8005d0 fix(desktop): gate rotation follow on focused surface and hash route 2026-09-20 20:14:10 -07:00
Christopher
32bb86ff72 fix(desktop): cover route-based foreground and unselected A-next rotation
isSessionInForeground now treats a missing route and store selection as
the on-screen chat, so a fresh session can still emit A -> A-next.
Tests drive the primary-route branch via history.pushState and pin the
no-route/no-selection path.
2026-09-20 20:14:10 -07:00
Christopher
ced5f36b2a fix(desktop): prevent background session rotation from stealing foreground route (#86106)
When a session stored id is rotated (e.g. by auto-compression), the
rotation event was emitted whenever the active runtime id matched,
ignoring whether the user had already switched focus to a different
session. This let a fast A -> B switch followed by A delayed rotation
pull the foreground route back to A new tip.

Add isSessionInForeground() in session-states.ts that checks the
current route/selected stored id against the rotating session lineage.
Apply the guard both in handleTransition() and in the no-op-updater
path inside useSessionStateCache.ensureSessionState() so all rotation
emission sites re-validate the user current focus.

Include a regression test that sets selectedStoredSessionId to an
unrelated session while the active runtime rotates, expecting no
activeSessionStoredIdRotation event.
2026-09-20 20:14:10 -07:00
xxxigm
4ea57fa61b fix(auth): count profile .env keys in the explicit provider gate
os.getenv saw only the launch profile, so a DeepSeek key pasted in
another profile never appeared in Settings → Model until Refresh
models ran against that Bot's own backend.
2026-09-20 22:46:49 -04:00
xxxigm
050ea53aba fix(desktop): honor Applies-to on Custom Endpoints
The page only showed a read-only active-profile note, so endpoint
saves followed the left-rail Bot instead of the Settings chips
Accounts and API keys already share.
2026-09-20 22:46:49 -04:00
teknium1
60dd9778c6 fix(desktop): large text pastes attach from HERMES_HOME/composer-pastes when the chat cwd is elsewhere
Desktop persisted a large paste under Electron's userData dir and attached
it as `@file:<abs path>`; the tui_gateway prompt path expands that reference
with `allowed_root=cwd`, so `_resolve_path` refused it with "path is outside
the allowed workspace" whenever the chat's cwd was not an ancestor of the
paste dir (always on Linux/macOS, and on Windows for any project cwd).

Electron now writes pastes under `<HERMES_HOME>/composer-pastes`, and
`agent/context_references.py::_resolve_path` admits exactly that anchored
directory (active profile home and global root, via file_safety._hermes_dirs)
as the one root besides `allowed_root`. A sibling directory that merely
contains the substring stays refused; the credential deny-list still runs
on the admitted path.

Supersedes the substring whitelist proposed in #117150.

Fixes #117149
2026-09-20 19:45:30 -07:00
teknium1
3d0bdce9f9 fix(desktop): prove the sign-in wall from curl's arrival URL and title too
Port the reporter's (@tianyu-liu) url_effective half: curl reports where it
landed via --write-out on a tail buffer that survives the body budget, and
isAuthWall decides on arrival URL -> sign-in title -> markup, so the Apps
Script ServiceLogin shape (no markup id) is a wall too and a wall's own title
is never used as the link label. Tests trimmed to two invariants.

Co-authored-by: tianyu-liu <tianyu-liu@users.noreply.github.com>
2026-09-20 19:37:57 -07:00
Finn763
1c81ad3485 fix(desktop): never load a sign-in wall in the hidden link-title renderer
The cookieless title partition answers a Google Workspace link with Googles

(cherry picked from commit 31e7dd19fafb13b8350bf3271d91036f243079b3)
2026-09-20 19:37:57 -07:00
liuhao1024
04845f5f3e fix(desktop): skip the local gateway restart on update when the Desktop is remote-served
A Desktop whose active connection is remote (SSH/remote/cloud, including the
registry primary) owns no local messaging gateway, yet the update hand-off
always ran `hermes update --gateway`. On hosts where launchd/service recovery
fails, the updater falls back to a detached local `gateway run --replace`;
with the same Telegram bot token as the remote VPS gateway, the two processes
compete for getUpdates and Telegram rejects one consumer, taking the
production bot offline (#117529).

Pass the ownership down the hand-off: globalRemoteActive() now adds
--no-gateway (posix) / -NoGateway (windows) when the Desktop is remote-served,
and both orchestrators drop --gateway from every update invocation (initial +
retry). The local-ownership default keeps --gateway exactly as before.
2026-09-20 19:30:16 -07:00
Chukuwebuka-2003
338b31226d fix(desktop): surface an externally-terminated renderer with a recovery page (#116472)
The OS/Chromium can SIGKILL a renderer while the window is live (memory reclaim, an external kill). Hermes never does this itself and never reloads it (a killed-after-close window must not pop back up), so the window sat silent with only a desktop.log line.

- window-renderer-lifecycle.ts: optional onRendererTerminated(details) callback, invoked for an unrecoverable render-process-gone on a live (not destroyed) window; recoverable crashes still reload; expected teardown still does nothing.
- renderer-load-error-page.ts: optional escaped title for the recovery page.
- main.ts: wire the primary window's callback to a visible recovery page (reason + Reload), guarded against intentional quit/handoff (isQuittingForHandoff || backendShutdown.hasStarted()).
- tests: three-arm lifecycle invariant + custom-title test.

Electron-only slice harvested from #116592 (commit 9bb16bd6ae) per the split advised on #117084; the agent half of #116472 lives in #117084.

(cherry picked from commit 5bdfe7ed00c4b3f764d4044fdfa527f978140cf3)
2026-09-20 19:21:46 -07:00
teknium1
c4f5dd0dad test(desktop): widen the roster-enumeration scan window past the ssh install-id branch
enumerateRegistryAgentSources grew when the ssh branch started carrying the
install id learned by the inventory probe, so the 3_700-char slice in
backend-dial-claim.test.ts stopped before getJsonForBackend('/api/profiles')
and CI went red. Same fix the sibling update-all case already applied.
2026-09-20 19:13:13 -07:00
John Paul Soliva
82e147ffb8 fix(desktop): an ssh connection learns its backend install id, so two addresses for one machine collapse
#88828 gave every connection a stable backend identity so one physical install is one roster row
per profile — but wired the probe into the non-ssh branch only. `probeConnectionInstallId` has a
single caller, in the enumerator's `else`; the other writer, `rememberConnectionInstallId`, sits
in `hermes:connections:test` after its ssh branch has already returned. So an ssh connection's
`installId` was always undefined, `buildAgentRoster` always fell back to the per-connection key,
and a Mac registered twice over ssh (alias + address, LAN + WAN) showed every bot twice with
`@name-label` handles, pushed two rows per bot into every gateway's relay roster
(`write_remote_roster` dedups by connection_id + profile), and made a bare-handle DM to it
`ambiguous` — asking the sender to disambiguate between two ids naming one machine.

The id is a plain file at the install root, and the ssh inventory probe already reads the remote
home over its own session without spawning a dashboard, so `readRemoteInstallId` reads it there:
from the INSTALL root (a connection pinned at `<root>/profiles/<name>` must report the same
backend as one pointed at the root), read-only — a missing file stays missing, since minting
identity is the install's job and not a visiting client's — and only a well-formed id counts, so
an older backend keeps today's no-id behaviour.

Fixes #117226

(cherry picked from commit 3d596f9b8fc898c74f96a378588a0b4601bfe39d)
2026-09-20 19:13:13 -07:00
John Paul Soliva
07888c880f fix(desktop): removing or re-pointing a connection forgets what was cached about it
Three main-process caches are keyed by connection id — the ssh profile list, its retry stamp and
the backend install id — and neither lifecycle event that invalidates them evicted anything.
`hermes:connections:remove` stopped pooled backends and told renderers to dispose their sockets;
`saveRegistryConnection`'s dial-material branch recycled sockets for exactly this reason, its own
comment noting they "point at the OLD target". The caches were not in scope for either.

Ids are recycled label slugs (`connectionIdForLabel` suffixes only against CURRENTLY taken ids),
and `shouldRetrySshInventory` never retries a cached success — "Cached successes never retry" —
so a connection re-added under the same label, or simply pointed at another host, kept serving the
previous machine's profiles for the rest of the app session. Clicking one of those bots dialled
the new box and failed, the relay pushed the phantom names to every peer gateway, and a peer bot
DMing one got 4092 `no profile '<name>' on this gateway` while the new machine's real bots stayed
invisible. Only a restart or a manual Test recovered.

The three maps move into `connection-caches.ts`, where the invariant they share can be stated and
tested — each is valid only while its id names the same machine — and `evictConnectionCaches()`
is called from both handlers. Every existing reference in main.ts is unchanged: the module
exports the same Map objects.

Fixes #117227

(cherry picked from commit 03847f66e8eae50f1972511ec89421b0298dbd22)
2026-09-20 19:04:11 -07:00
teknium1
16b749f61f fix(desktop): keep the chat composer mounted through transient session loaders
The chat bar was gated on `!loadingSession`, so any one-render flip of the
routed session into its loading state (periodic sidebar / live-status
refresh dropping the routed row, a hydrate swapping the transcript through
an empty frame) unmounted the composer: draft, attachments, caret and focus
vanished and came back every ~30s. Once a route has rendered with its
composer, a later transient loader for the same route now keeps it mounted;
a route change, the exhausted state and watch windows still hide it.

The unmount also fired `placeCaretEnd` / `placeCaretAtOffset` on the
detached editor from the draft repaint, throwing
`addRange(): The given range isn't in document` inside React's commit and
looping the reconciler (the visible flicker). Both caret helpers now bail
when the editor or the computed range is no longer connected.

Fixes #117375
Fixes #117285
2026-09-20 18:53:37 -07:00
xielevi
5d9af50aa0 fix(desktop): gate the plain-text token warning on secure storage being unavailable
The Settings -> Gateway "Token stored in plain text" banner predates the
opt-in keychain policy: it was written when a plain token could only exist
on keyring-less Linux, and it fires off the token encoding alone. Since
keychain encryption became opt-in (default off), every saved token is
plain, so the banner shows for every default user, while
probeSecureTokenStorage deliberately reports availability in that mode
"so no plain-text warning banners fire: storing plaintext is the user's
chosen (default) mode, not a degraded state."

- Compute the renderer-facing signal in a new
  hardening.resolveRemoteTokenPlainText helper: true only when the token
  is plain AND secure storage is genuinely unavailable (the keyring-less
  opt-in path the banner was written for), and never under the
  HERMES_DESKTOP_REMOTE_URL env override.
- Behavioral tests for the truth table plus a source assertion pinning
  the call site; both proven red against the ungated logic.
- Scope the Linux-only keyring names in the plain-text copy (banner and
  save-time confirm, en/zh/zh-hant/ja/ru) to Linux, since both can render
  on any platform.

Fixes #117269
2026-09-20 18:47:20 -07:00
Finn763
f532b8adf5 fix(desktop): keep the edit composer available after a timeline rail jump
A rail jump selects a bounded around-page, which drove `onEdit: undefined`
on the runtime. assistant-ui's `useActionBarEdit` only checks
`composer.isEditing`, so the edit button stayed enabled while `beginEdit`
threw "Runtime does not support editing" — the inline composer was dead
for every message after any far jump, infectious downward, and healed
only by the floating jump button's returnToLatest.

`isDisabled` still keeps the page static (no submit/reload/branch) and
`editMessage` resolves its target against the live session store, so the
edit rewinds the real transcript and drops the page.

(cherry picked from commit 326b245ad7cc19a1d4db2a06da7a01cd07043117)
2026-09-20 18:31:49 -07:00
teknium1
b6d96ccc74 test(desktop): give the profiles-page profile mock the $showAllProfiles atom layout.ts reads
Scoping the right rail per profile made session-states import preview.ts,
which imports layout.ts, which derives its grouping from $showAllProfiles.
The Manage Profiles page test mocks @/store/profile without that export, so
vitest rejected the module graph and the whole suite failed to load.
2026-09-20 18:30:20 -07:00
teknium1
765cb40bb5 fix(desktop): trim right-rail scope salvage to the renderer store
Drop the per-profile Electron session partition (a second mechanism that
would reset every secondary-profile window's persisted layout once) and keep
the rail-per-owner store change only; trim the scope tests to two
invariants and sort imports for the repo lint.
2026-09-20 18:30:20 -07:00
Cody
c996d1c088 fix(desktop): scope the right rail to the chat on screen, not the gateway socket
The right rail's tabs were persisted under ONE global localStorage key, so a
preview opened in one agent's chat appeared in every other agent's chat —
Tess's 3D model showed up in VEXA's rail and vice versa.

apps/desktop/AGENTS.md states the rule this violates: "Persisted state must
declare its scope in its own key: is this global, or does it belong to a
connection, a profile, a stored session, a project, or a window? Getting the
scope wrong is how one profile's setting bleeds into another."

The scope is the CHAT ON SCREEN, deliberately not the window's gateway socket:
a focused tab does not swap the socket, and every Bot Mode chat is served by one
pooled backend, so a socket-keyed rail shows one agent's previews in every
agent's chat. bot-row.tsx documents the same trap for the roster highlight
("Highlight follows the chat on screen (focused session's owner), not the
gateway socket's home — a focused tab doesn't swap the socket").

The rail therefore resolves its scope from the focused session's owner via the
existing knownOwnerForSession ladder, the same value the roster highlight
trusts, so the two cannot disagree about whose chat is on screen.
session-states.ts owns that resolver and already imports preview.ts, so it
pushes the scope in (setPreviewScope) rather than the rail reaching back, which
would be an import cycle.

The bucket map mirrors tilesByProfile, including a "view key" that follows a
rename (without it the persist subscriber re-creates the bucket the rename just
deleted) and lazy adoption of a legacy global array into the first scope that
arrives, so tabs the user can see survive the migration. Registered in both
dropTilesForProfile and migrateTilesForProfile, as the guide requires of any
profile-keyed localStorage family.

Also gives each non-primary profile its own Electron session partition on the
session pop-out and instance windows, so a per-agent window keeps its own rail
even across partitions.

Tests: preview-tabs-scope.test.ts holds the contract (no cross-agent bleed, per
agent buckets, rename moves rather than strands, and a socket change does NOT
re-home the rail — the regression that made the first cut look correct).

(cherry picked from commit dd40bd75fe7e27b9164af67b352884fc28242fef)
2026-09-20 18:30:20 -07:00
fangliquan
46e1e49d30 fix(cron): preserve custom provider model choices
(cherry picked from commit c784dabdc3adb3573c8870d10b2a6db70e789e0f)
2026-09-20 18:27:54 -07:00
fangliquan
4de06d1dbf fix(desktop): honour updates.pre_update_backup=off in the Desktop updater preflight
The Electron updater copied state.db to state.db.pre-update-emergency-*.bak on every
update hand-off regardless of updates.pre_update_backup, pinning ~3x state.db on disk.
preflightStateDb now asks the backend (`hermes config get updates.pre_update_backup --json`)
and skips the emergency copy when the mode is off/false/none/disabled; any probe failure
fails open to taking the backup.

Squashed from PR #116755 (4 commits) for a clean apply onto origin/main.
2026-09-20 16:20:08 -07:00
teknium1
9c12b9808c fix(desktop): keep agent tips out of the persisted seen ledger
Agent tips are unbounded in number; only the ✕ ($retiredTips) needs to persist, so showTip no
longer writes agent: ids into $tipShownAt. Folds the duplicate store test into the first one.
2026-09-20 15:59:52 -07:00
teknium1
767ce1a5df test(desktop): tip.show bridge refuses a retired agent tip
Drives handleDesktopBridgeEvent (the production entry point) so a dropped
agentTipId wire at the call site goes red, not only the store helper.
2026-09-20 15:59:52 -07:00
wangtao
7a15dd1943 fix(desktop): hash persisted agent tip identities 2026-09-20 15:59:52 -07:00
wangtao
c1f96e6c65 fix(desktop): remember dismissed agent tips 2026-09-20 15:59:52 -07:00
teknium1
4f0ced3e5f fix(desktop): mask typographic double quotes too and document quoted stop words
macOS smart-quote substitution turns the straight quotes a user types into “ ” in the composer,
so the quoted-span mask covers both; the user-visible rule (stop words inside code, quotes or
blockquotes never hold) now has its sentence in bot-mode.md alongside the behaviour.
2026-09-20 15:58:58 -07:00
kokhlo
c5b27c4c8c fix(desktop): quoted or pasted stop words no longer hold a group-chat member (#117040)
stopWordPlacement() tokenized the raw message string, so a stop/halt/pause
token inside a fenced code block, inline code span, straight-quoted span or
blockquote line still landed within the two-token window of an @token and
held the resolved member. Reporting or pasting a log that contains
"stop @impl" held the bot again, repeatedly — all three repro cases from
the report hold on main.

maskQuotedAndCodeSpans() collapses each such span to the @tokens it
contains (mentions resolve from raw text and must keep their place in the
proximity window, so quoting AT a bot still releases a held member) or to
one neutral filler word, so masking never manufactures adjacency:
"stop `service` @impl" still holds. An unclosed fence — what a cut-short
paste produces — swallows the rest of the message; blockquote lines are
masked line-wise.

Mention resolution, bare-mention release and the @all wake semantics stay
untouched: the plain directive forms, the German-prose and distant-stop
suites are green, with new invariant tests for the quoted/pasted cases.

Fixes #117040
2026-09-20 15:58:58 -07:00
teknium1
c7f2e76c50 fix(desktop): drop the unwired owner parameter from fetchVoiceLiveStatus and trim the routing tests
refreshVoiceLiveStatus is the only caller and probes the active profile (engine selection is a
window-level capability), so the owner overload had no production reach; its direct-call test
pinned a surface that never shipped. Ownerless routing is now asserted inside the owner test.
2026-09-20 15:49:38 -07:00
Konstantin Khlopkov
ae1530745a fix(desktop): route GPT-Live sessions through the Bot owner's profile 2026-09-20 15:49:38 -07:00
tobenwarrior
5171ea18dc fix(desktop): scope plugin specifier scanning to code, not strings/comments
The runtime plugin loader's specifier regex is unanchored (from\s*['\"]), so a
plugin whose own source contains a string ending in "from" — e.g. a 'Copy
keys from' UI label — was rejected as an "unsupported import", and a mapped
specifier quoted inside a string (documenting the import form) was rewritten
into a shim blob URL, corrupting the string at load.

Scan the source once for code ranges (string, template-literal and comment
text excluded; template ${…} interpolations stay code) and honour specifier
matches only inside them. Both failure modes are real: a plugin's own label
made the entire plugin fail to load in the desktop app.

Tests: 3 new invariants proven red on base (label/comment must not
load-block, in-string specifiers must stay verbatim), 2 guards stay green
(real unmapped import still rejected, real mapped import still rewritten).

(cherry picked from commit 3ac1d440cb1e21f8931a75bc07074363fccaa0a7)
2026-09-20 15:46:26 -07:00
teknium1
522e121e90 fix(desktop): pin the update-check proxy deps exactly and document the proxy variables
The salvaged commit added https-proxy-agent, proxy-from-env and
@types/proxy-from-env as caret ranges; the repo pins every dependency to an
exact version so `npm ci` resolves the same tree everywhere. Pins are the
versions the lockfile already resolved (7.0.6 / 2.1.0 / 1.0.4), regenerated
with `npm install --package-lock-only`.

Documents that the Desktop update check now follows HTTPS_PROXY / HTTP_PROXY /
NO_PROXY in the environment-variables reference.
2026-09-20 15:08:40 -07:00
Jony
7473088b4d fix(desktop): honor proxy env for update API checks 2026-09-20 15:08:40 -07:00
teknium1
13fe9c7171 feat(providers): external-process provider support for standalone model-provider plugins (from #105863)
The provider-agnostic half of PR #105863, so a CLI-driven subscription provider can ship as a
standalone `kind: model-provider` plugin instead of a bundled one:

- ProviderProfile: `native_reasoning_details_type`, `model_aliases`, `get_model_context_length`,
  `get_usage_cost`, `setup_status`, `discover_models` hooks (all default None / no-op).
- Chat Completions transport: provider-native `reasoning_details` carriers follow only their
  declaring profile; standard records still replay on OpenRouter-style routes, strict routes
  drop the field wholesale (#70233). Relay/stream accumulate `delta.reasoning_details` verbatim.
- `hermes model`: the generic plugin flow gates an external-process row on the CLI's own login
  status (inline `login_command` on a TTY), offers `discover_models()` rows with per-row notes,
  and never writes config when the executable is missing.
- `/model` and the pickers: process providers list their live catalog merged with the pinned
  one, declared aliases/ids resolve inside the provider, and validation accepts a listed id
  without probing `process://`.
- Delegation keeps the selected external-process provider and protocol for the child.
- Model metadata / usage pricing consult the profile's bound and cost hooks first.
- Desktop: `[1m]` renders as a "1M" tag and hyphenated Anthropic versions read "Haiku 4.5".

The bespoke `_model_flow_external_process` and hard-coded `hermes_cli/main.py` paths from the
PR were dropped in favour of main's `_model_flow_plugin_provider`.

Co-authored-by: unsupportedpastels <unsupportedpastels@users.noreply.github.com>
2026-09-20 14:29:39 -07:00
teknium1
00c0ea6cbe fix(desktop): "Open containing folder" is offered only for a session on this computer, and says so when a path is not here
The statusbar workspace menu built its reveal item unconditionally while
the sidebar menus (file-actions.tsx, review/file-tree.tsx) already hid it
on a remote backend, and `hermes:fs:reveal` returned true after
`shell.showItemInFolder`, which silently no-ops on a missing item — so a
remote bot's workspace path gave a click that did nothing and reported
success.

- electron/fs-ipc.ts: `hermes:fs:reveal` answers false when nothing at the
  (tilde-expanded) path exists on this computer.
- lib/desktop-fs.ts: revealDesktopPath surfaces that false as an error the
  existing revealFile toast shows (new i18n key fileMenu.revealUnavailable;
  locales fall back to English through defineLocale).
- store/file-actions.ts: shouldOfferLocalReveal() — the focused row's
  Connections tag decides (a row tagged with another gateway is never
  local, even under a local primary); an untagged row follows the window's
  primary mode, the rule the sidebar already applies.
- use-statusbar-items.tsx: the reveal item is gated on it.

Slimmer redo of #115168 by @jonpol01 (19 files): same mechanism, without
the project-menu/workspace-header rewiring and the per-locale translations.

Fixes #115167
Co-authored-by: John Paul Soliva <soliva.johnpaul@icloud.com>
2026-09-20 14:29:15 -07:00
teknium1
ead1a80a33 test(desktop): trim the in-flight marker suite to two invariants
Keep the two tests that pin the fix: the marker is written at submit and
cleared with the reply (production entry runGroupChatMemberTurn), and a
marker left by a previous Desktop process is harvested at the next
boundary. The live-poll-owns-marker and 4007-drop cases are implied by
the first (seen.live === true while the poll runs) and by the harvest
code path; salvage bar is two invariant tests per fix.
2026-09-20 14:22:28 -07:00
teknium1
aae3c737ad feat(desktop): hermes://plugin/install?catalog=<name> opens the reviewed catalog install dialog
A `catalog=<name>` deep link now resolves the name against the live plugin
catalog feed (the same `/docs/api/plugins.json` the Capabilities → Plugins
picker renders) and opens the Install Plugin dialog in its reviewed/pinned
catalog mode — identical to an in-app catalog pick, via one shared
`openCatalogPluginInstall` helper the Plugins tab now uses too.

The `catalog` param claims the link outright: an unknown, invalid, or
unresolvable name is a clear error toast and nothing else. It is never
reinterpreted as a git identifier, so a link cannot smuggle an unreviewed
repo behind a familiar-looking name (a `repo=` riding along is ignored).
2026-09-20 13:56:30 -07:00
teknium1
2c78b9b39e feat(process_registry): stamp exited_at and expose it on process.list
The live-work docks retire a finished background process ~60 s after it
ends; the registry only knew started_at, so the age of an exit was not
observable. _move_to_finished is the single choke point every exit path
(reader loop, reconcile, kill) passes through, so the stamp lives there.
completion_reason rides along so a killed process can read "killed"
instead of "exit -15".
2026-09-20 13:55:03 -07:00
teknium1
099d19a512 fix(desktop/bots): outside writes into member sessions surface on room open (#93813)
Review follow-ups on the external-write mirror:

- Idle trigger: `openGroupChat` now runs `sweepExternalGroupWrites`, one
  `session.resume` per stored member session whose thread the room still
  shows, then the existing mirror. A Bot posting into its own room session
  between rounds (the reporter's scenario) is posted the moment the user
  opens the room, not only once the room next drives that member and it
  happens to be a responder. No polling; a room mid-round is left to the
  round, which sweeps its responders itself.
- Classifier: the single `[System:` skip becomes the canonical synthetic
  user-row set (mirrors `agent/context_compressor.py::
  _SYNTHETIC_USER_ROW_PREFIXES`, comment links both) plus the gateway's
  `display_kind` on typed scaffolding rows. A compaction handoff, cron
  delivery, delegation result or steer marker is never mirrored as member
  speech, and the assistant row reacting to it closes the exchange instead
  of inheriting the previous writer's origin.
- Stranded harvest picks the FIRST substantive assistant row after the
  header-prefixed prompt and stops at the next outside user row, so a CLI
  answer written after the late reply is no longer posted as the turn reply
  and then mirrored again.
- Cursor edges (documented in the module header): first sight of a session
  seeds the cursor at the transcript's current length — a room hydrated from
  the gateway mirror (which carries no cursors) on a second Desktop does not
  re-post its history; "late, never lost" holds from that moment on, at the
  cost of no history replay. A cursor past the end after compaction is
  reset to the end. A missing snapshot leaves the cursor alone.

Tests stay at 2 in group-external-writes.test.ts: the negative control now
also opens the room without a drive and asserts the peer exchange arrives
while the compaction/cron/auto-continue rows and their answers do not. Red
with the `openGroupChat` call removed and with the prefix set reverted.
2026-09-20 13:44:30 -07:00