Commit Graph

4089 Commits

Author SHA1 Message Date
Thomas Hudspith-Tatham 6cbe5a35b6 fix(desktop): support multi-select clarifications 2026-08-14 20:24:32 -07:00
Richard Howes 34d76a1df0 [verified] fix(desktop): reveal active clarify prompts
Reveal a blocking clarify card by re-arming the existing thread bottom-scroll bridge after the request row is hydrated. Keep background-session prompts isolated to their needs-input indicator.

Refs #53666.
2026-08-14 20:24:32 -07:00
konsisumer 38b9005b95 fix(desktop): replay pending approvals after reconnect 2026-08-14 20:24:32 -07:00
VooDoo Pixels f703e70618 fix: make desktop approval routing reliable
Correlate approval requests, reject stale responses, replay pending approvals after reconnect or session resume, and preserve fail-closed timeout behavior.
2026-08-14 20:24:32 -07:00
Teknium 898cc87125 fix(desktop): salvage session-race sequencing cluster — widen Stop cooldown to tile interrupts, prove submit ownership both ways
Follow-ups on top of the two salvaged commits:

- widen the #83855 recently-interrupted cooldown to the session-tile
  interrupt path (use-session-tile-delegate.interruptSession) — same race
  class, sibling call site: a tile Stop also clears busy before the
  gateway settles, so a quick tile edit/resend raced 4009 session busy.
  The recovered runtime id is marked too.
- regression test for the tile cooldown.
- refresh three #65328 ownership-proof assertions to tolerate the
  omit_messages flag main now sends on session.resume (toMatchObject).
- eslint import-order fix in utils.test.ts.
2026-08-14 20:24:15 -07:00
Simplicio, Wesley (ext) 33b39f3f4b fix(desktop): prove submit target belongs to selected session from both directions (#65328)
Fail closed on missing ownership cache entries and prove runtime ownership
forward+reverse against runtimeIdByStoredSessionIdRef before prompt.submit.
Thread the ownership cache through main wiring and session-tile submit.
Adds regression tests for forward mismatch, reverse-only proof, positive
map control, and cache-miss resume.

Closes #65328.
2026-08-14 20:24:15 -07:00
Olympusbuildz 801fd0b3d8 fix(desktop): interrupt-first after Stop so edit/resend avoids session-busy
Stop clears frontend busy immediately while the gateway may still wind
down. Edit/restore then passed interruptFirst=false and raced 4009
session busy. Keep a short per-session cooldown after cancel so rewind
still interrupt-first, and expire the submit-in-flight lock so a hung
submit cannot block the session forever.

Fixes #83855

Co-authored-by: Olympusbuildz <Olympus.roots@outlook.com>
Signed-off-by: Olympusbuildz <Olympus.roots@outlook.com>
2026-08-14 20:24:15 -07:00
Teknium 1c80085f04 fix(desktop): widen terminal-status handling to sibling sites
Two sibling sites still treated only 'completed'/'failed' as terminal:

- delegate-model.ts settled result rows as 'completed' for ANY status other
  than 'failed', so a delegate result row with status 'timeout' or 'error'
  (the statuses tools/delegate_tool.py actually emits on child timeout or
  crash) rendered behind a green check. Settled rows now map ok/completed
  to completed and everything else to failed.
- subagents.ts asStatus accepted a literal 'queued' payload status even on
  a subagent.complete event, leaving the row active forever. The fail-closed
  branch now runs before the queued fallback, so completion events always
  settle.
2026-08-14 20:23:57 -07:00
Michael Gannotti e3c3d0895d test(desktop): late progress events must not revive a timed-out subagent row
Folded-in coverage from PR #80045 (gannotti, #80018): after a terminal
subagent.complete, a stray late 'running' progress event must not restart
the spinner — the upsert guard keeps the settled failed status.
2026-08-14 20:23:57 -07:00
Sebastian Mause a01c7bb43b test(desktop): completion events with still-active payload statuses settle as failed
Folded-in coverage from PR #85995 (smause): a subagent.complete event whose
payload still says 'running' or 'queued' must settle the row as failed —
the completion event itself is the source of truth that the child is done.
2026-08-14 20:23:57 -07:00
David Metcalfe d3fee89993 fix(desktop): prefer synthesized timeout summary over stale progress text
Review feedback: prev?.summary could shadow the 'Timed out after Xs'
reason when a live event had populated it. timeoutSummary() now wins for
raw timeout status; add coverage for the missing-duration placeholder.
2026-08-14 20:23:57 -07:00
David Metcalfe 09b1726a1d fix(desktop): fail closed on unrecognized subagent.complete statuses; surface timeout reason
Follow-up to the #73728 normalization fix (supersedes the event-agnostic
fallback the maintainers flagged as incomplete):

- subagent.complete is terminal by definition — an unrecognized status on
  it now renders as 'failed' instead of falling through to 'running',
  which would recreate the immortal false-active row for any future
  backend status (the keep_open request on #73859).
- Live events keep the lenient 'running' fallback.
- Synthesize a 'Timed out after Xs' summary from duration_seconds when
  the backend completes with status 'timeout' and no summary, so the
  failed row explains itself.
- Tests: timeout reason synthesis + pruning, event-aware fail-closed vs
  lenient live fallback (13 total).
2026-08-14 20:23:57 -07:00
RelaxJonh a351c17d4a fix(desktop): normalise timeout/error subagent statuses to terminal (#73728)
The backend emits terminal statuses including 'timeout' and 'error' in
subagent.complete payloads, but asStatus() only recognised 'completed',
'failed', 'interrupted', and 'queued'. Unrecognised values fell through
to 'running', making timed-out subagents immortal in the active status
stack.

Fix: map timeout/error to 'failed', cancelled/canceled to 'interrupted'.
Nonterminal unknown statuses still default to 'running' for forward
compatibility.

Fixes #73728
2026-08-14 20:23:57 -07:00
KBANTH f9f5c14f9a fix(desktop): keep live gateway across profile switches 2026-08-14 20:23:47 -07:00
Trevor Nash-Keller 077c04755e fix(desktop): sync active profile after reconnect 2026-08-14 20:23:47 -07:00
A9 8edb4626ca fix(desktop): refresh sessions on profile switch
Re-run the foreground session-list refresh whenever the active gateway profile changes, preventing rows from the previously selected profile from persisting in the sidebar.
2026-08-14 20:23:47 -07:00
Jakub Wolniewicz 21b57c61f4 fix(desktop): page remote profile session reads 2026-08-14 20:23:47 -07:00
Guilherme Aguiar 01e542b764 fix(desktop): address transcript refresh review feedback 2026-08-14 20:23:18 -07:00
Guilherme Aguiar 1a2b0ca8cb fix(desktop): refresh active transcript on session changes 2026-08-14 20:23:18 -07:00
chelsealong f0748b451c fix(desktop): stop empty REST transcript refresh from wiping a warm resume
session.activate's persisted-transcript refresh reconciled unconditionally
against getLatestSessionMessages, so a transient empty REST page (e.g. a
backend respawn racing its own state.db read after a wake/reconnect) wiped
a transcript the activate response had just restored. Guard it the same way
the activate payload itself already is guarded a few lines above: an empty
authoritative page never overrides a non-empty cached transcript.
2026-08-14 20:23:18 -07:00
spfcraze 9cc428cff8 fix(desktop): keep responsePreviewed settle gated on the boundary flag
Sweeper review on #76583: dropping interimBoundaryPending from the
previewed settle path let a previewed final arriving after a
message.start reset OVERWRITE a distinct interim instead of appending
(interim('old') → message.start → complete({response_previewed: true,
text: 'new'}) destroyed 'old'). responsePreviewed may rewrite the final
with no prefix guarantee, so it must stay flag-gated; only
finalContinuesInterim (prefix-either-way continuity, which can only hold
for the same message) settles flag-free. New test: distinct previewed
final after a reset appends its own bubble. Production ordering cited in
the test: compaction-resume events exclude message.start
(gateway-event.ts), and the TUI gateway emits message.complete before
goal-followup starts (tui_gateway/server.py).

74/74 use-message-stream tests pass.
2026-08-14 20:23:18 -07:00
spfcraze 71a98f69ee fix(desktop): settle final reply onto interim even after message.start reset the boundary flag
completeAssistantMessage merged a turn's final text onto its sealed interim
bubble only when the session's volatile interimBoundaryPending flag was still
true. A subsequent message.start (chained turn, follow-up, or mid-turn
compaction) resets that flag to false; when it landed between the same turn's
message.interim and message.complete, the flag-based gate fell through and the
UI appended a duplicate bubble.

Key the merge on the message's OWN durable interim state instead of the
session flag. finalContinuesInterim already requires existing.interim plus
prefix continuity, so distinct replies (which don't continue the interim) are
still appended as their own bubble.

Adds a regression test: interim + message.start + completing final that
continues the interim must yield one bubble, not two.
2026-08-14 20:23:18 -07:00
李灵航 cf63b79467 fix(desktop): drop pending stream rows whose reply the transcript already carries
A still-pending assistant stream row (id `assistant-stream-*`) whose reply
the authoritative transcript already committed used to fall through to
`preserved.push` when ordinal pairing missed it — the commit shifted the
row's ordinal under compaction/history rewrites, so `nextByRoleOrdinal`
returned nothing and the local copy was appended to the tail, rendering the
same answer twice (reported as A B C D E C D tail duplication).

The #70209 guard only covers SETTLED local rows (`pending !== true`);
pending rows were unprotected. Match pending rows against SETTLED
authoritative rows before appending:

- identical answer text            -> authoritative already carries it
- authoritative extends local text -> authoritative is the settled final
  version of the still-streaming local copy
- local extends authoritative text -> replace the committed row with the
  richer local body instead of appending

Live projection shells (still-pending candidates) never match, so the
traces-only local row keeps replacing the empty shell.
2026-08-14 20:23:18 -07:00
Aleks Clark 62eefff697 perf(desktop): bound long-running app resource use
Persist backend ownership for reliable cleanup, park inactive panes, and
evict unreferenced transcripts so Desktop stays responsive over long sessions.

💘 Generated with Crush

Assisted-by: Crush:gpt-5.6
2026-08-14 20:23:07 -07:00
AlexDev_ 4712721033 perf(desktop): pause decorative animations when unfocused 2026-08-14 20:23:07 -07:00
LemonSchneid d7e95315f7 fix(desktop): expand HUD transcript when resized
Use the available HUD window height for non-empty transcript scrollback instead of a fixed glance-band cap. This makes the corner resize affordance reveal additional conversation content while preserving the compact empty HUD state.
2026-08-14 20:22:56 -07:00
Jakub Wolniewicz b55677077f fix(desktop): keep completion selection visible 2026-08-14 20:22:56 -07:00
nanami7777777 957a7c20cd fix(desktop): keep text navigation keys in composer 2026-08-14 20:22:56 -07:00
LemonSchneid 0611f9f856 fix(desktop): retain HUD composer focus
Keep the native HUD window mouse-solid while focus is inside the rich composer. On Windows this prevents click-through from reactivating the app beneath the HUD, stealing the caret, and collapsing the transcript.
2026-08-14 20:22:56 -07:00
Jakub Wolniewicz 23954e3e31 fix(desktop): prevent navigation from stealing focus 2026-08-14 20:22:56 -07:00
hermes-seaeye[bot] b9fa46b5a5 fmt(js): npm run fix on merge (#86559)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-15 00:39:34 +00:00
Teknium b6726d57e4 feat(desktop): per-profile scope selector in the Capabilities view (#86548)
* feat(desktop): per-profile scope selector in the Capabilities view

Adds a 'Configuring:' profile selector above the Tools and MCP tabs in the
Capabilities (Skills) view, so a user can configure ANY profile's toolsets
and MCP servers without switching the whole app into that profile.

- hermes.ts: every capability fetcher (getToolsets, setToolsetEnabled,
  getToolsetConfig/Models, selectToolsetModel/Provider, runToolsetPostSetup,
  getMcpCatalog, installMcpCatalogEntry, testMcpServer, saveMcpServers,
  auth/oauth flow, setEnvVar/deleteEnvVar/revealEnvVar, startOAuthLogin/
  pollOAuthSession, getActionStatus, getHermesConfigRecord) takes an optional
  trailing profile? that forwards to profileScoped(profile). Omitting it
  preserves exact app-wide behavior (profileScoped(undefined) → _apiProfile).
- use-config-record.ts: hermesConfigKey(profile)/useHermesConfigRecord(profile)/
  hermesConfigCacheWriter(profile) — per-profile RQ keys (scope-in-key).
- toolset-config-panel.tsx + mcp-tab.tsx: thread profile through every fetch
  and their nested children (EnvVarField, PostSetupRunner, ModelCatalogPicker),
  keyed/remounted per selected profile so switching never shows stale state.
- skills/index.tsx: the selector (seeded from profiles.list, default→'Hermes',
  shown only with >1 profile), defaulting to ; toolsets
  query + toggles keyed and scoped to the selection; McpTab/ToolsetDetail
  remounted per scope.
- i18n: skills.configuringProfile (en + zh; others fall back).

When the selected profile equals the active one (the default), behavior is
identical to before — the selector is a pure override layered on top.

Tests: index.test.tsx — new case asserts picking a non-active profile in the
selector refetches toolsets scoped to that profile; existing single-profile
cases still pass (selector hidden with one profile). 5/5. Full-project tsc clean.

* Fix CI: command-palette getHermesConfigRecord call, panel test mock, lint

- command-palette/index.tsx: getHermesConfigRecord now takes an optional
  profile; passing the bare fn as queryFn fed it react-query's context object
  (TS2769 + mcp_servers on {}). Wrap in an arrow.
- toolset-config-panel.test.tsx: use-config-record now imports normalizeProfileKey
  from @/store/profile, which calls setApiRequestProfile at module-init; the
  full-replacement @/hermes mock must provide it (+ getApiRequestProfile).
- index.test.tsx selector test: stub Element.prototype.scrollIntoView (Radix
  Select calls it on open; jsdom lacks it).
- toolset-config-panel.tsx: PostSetupRunner useCallback missing 'profile' dep
  (stale-closure correctness); jsx-prop sort order.

Verified in a full-dep checkout: tsc 0 errors, eslint clean, panel 28/28 + skills 5/5.

---------

Co-authored-by: Teknium <teknium1@users.noreply.github.com>
2026-08-14 17:33:49 -07:00
hermes-seaeye[bot] bab9a85b67 fmt(js): npm run fix on merge (#86533)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-15 00:07:02 +00:00
Brooklyn Nicholson c83061ba3d test(desktop): cover the context gauge fetching before a turn runs 2026-08-14 19:01:15 -05:00
Brooklyn Nicholson 25d1c8d740 fix(desktop): context gauge reads the session it is on, not the last turn
The statusbar gauge painted only what the backend reported as measured
occupancy, which a session has none of until a turn runs in this process.
Turning the gauge on mid-conversation, or resuming a chat, therefore showed
nothing until the next message.

Fetch session.context_breakdown as soon as the gauge is on screen instead of
when its popover opens. It is the same read-only estimate the popover already
used (chars/4 over the live prompt, tools and transcript — no provider call),
and it reports the measured figure once the backend has one. The popover
becomes presentational and reads the gauge s merged usage, so the bar and the
panel cannot disagree.
2026-08-14 19:01:15 -05:00
hermes-seaeye[bot] 8e509c729e fmt(js): npm run fix on merge (#86430)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-14 22:48:47 +00:00
Teknium 7d96537bc8 fix(desktop): the main agent's model pick persists as the profile default (#86414)
* fix(desktop): the main agent's model pick persists as the profile default

Reported: the default bot switches to the OpenAI API account instead of
the user's subscription, and doesn't retain the previous selection.

Root cause: the composer model picker always sent the switch as
--session scope, even for the PRIMARY profile's main agent. So the pick
never wrote config.yaml model.provider — and with model.provider unset,
resolve_provider('auto') falls through to a leftover OPENAI_API_KEY env
var and picks OpenAI/OpenRouter. The subscription the user selected was
only ever a per-session override that evaporated on the next session.

Fix: when the pick targets the primary profile's main agent
(touchesPrimary), send --global so it persists to config.yaml
(model.default + model.provider) via the existing model-switch persist
path. A SET model.provider already outranks the OPENAI_API_KEY env var
in resolve_provider (tier 2 vs tier 3), so the main agent now keeps the
chosen provider across restarts. Secondary chat tiles stay --session so
picking a model in one chat never rewrites the profile default (the
cross-session-contamination guard the old comment protected).

No change to resolve_provider's priority chain, so #29285 (an explicit
env key beating a STALE oauth login) is untouched — we simply make the
user's explicit main-agent selection the config default it always
should have been.

* MoA presets stay session-scoped; update tests for primary-persist intent

Fix CI (ui shard 3of3): the primary main-agent pick now persists via
--global, but MoA (mixture-of-agents) presets must NOT — a transient
orchestration choice can't become the global gateway default. Exclude
provider==='moa' from the persist path (stays --session). Update the
primary-picker test to assert --global (the new intent) and keep the
MoA + secondary-tile tests asserting --session (the guards that prove
the narrowing). 19/19 green locally.
2026-08-14 15:40:53 -07:00
hermes-seaeye[bot] 31e571acf6 fmt(js): npm run fix on merge (#86407)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-14 22:16:25 +00:00
Gille a373a1b749 fix(desktop): tolerate omitted get-windows on ARM64 2026-08-14 16:51:00 -05:00
fangliquanflq 689a4be6f2 fix(desktop): degrade get-windows on Windows ARM64
(cherry picked from commit 6997b5a97e72aa1ffc98533aaa100e84857b1fbe)
2026-08-14 16:51:00 -05:00
webtecnica 58e4bb04d6 fix(desktop): make get-windows optional dep so Linux build doesn't break (#85377)
(cherry picked from commit f25e3467e6a28911067b416bf6647c8c0b3254a6)
2026-08-14 16:51:00 -05:00
QLYF 253d4aa996 fix(desktop): add focus to ZOOM_REASSERT_WINDOW_EVENTS for high-DPI displays
On Windows with high-DPI displays (150%+ scaling), Chromium
re-evaluates zoom on focus change (alt-tab). The persisted zoom
level was only re-asserted on show/restore/resize/move events.

Add 'focus' to zoomReassertWindowEvents() so the zoom level is
restored when the window regains keyboard focus, covering both
main and secondary session windows through the existing
installZoomReassertOnWindowEvents() abstraction.

Extend zoom.test.ts to exercise the registered focus handler.

Fixes #50837
2026-08-14 14:50:45 -07:00
Brooklyn Nicholson b66b4a50cf test(desktop): cover the torn-renderer-bundle detector
renderer-bundle.ts shipped in #85887 as pure/injectable but had no unit
coverage. Lock the contract that heals a torn self-update: the boot loader
must prefer a complete generation and only report a repair when every copy
is torn.

parseModuleAssetRefs: module scripts + modulepreload only (not stylesheets
or classic scripts), CDN/absolute refs dropped, ./ and query/hash stripped.
missingRendererAssets: intact -> [], torn -> the dangling chunk(s),
per-copy dir-relative existence (the split app.asar vs app.asar.unpacked
case), and an unreadable/module-free index is not treated as torn.
2026-08-14 16:48:02 -05:00
hermes-seaeye[bot] e1098cbb18 fmt(js): npm run fix on merge (#86379)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-14 21:44:50 +00:00
Brooklyn Nicholson 3d34b1916d feat(desktop): add 'Open in terminal' to the session menu
Sits under 'New window' in the Open group and resumes the chat in the user's
own terminal, starting in the session's workspace and pinned to its profile.
Hidden on a remote connection, where the emulator is local but the session
isn't.
2026-08-14 16:27:01 -05:00
Brooklyn Nicholson 867eb1a381 feat(desktop): expose openSessionInTerminal over the capability bridge
Resolves the same backend the app launches (usually a venv python running
-m hermes_cli.main), writes the launcher script, and spawns the terminal
detached so it outlives the app. Resolution only — never ensureRuntime, which
would start a first-run install from a menu click.
2026-08-14 16:27:01 -05:00
Brooklyn Nicholson 237abc1e9e feat(desktop): resolve the user's terminal emulator and TUI resume argv
Pure helpers for handing a session to an external terminal: the
`--tui --resume <id>` argv, a launcher script that carries the resolved
runtime's command and PYTHONPATH, and per-platform emulator resolution.

macOS opens the .command with no -a so LaunchServices routes it to whichever
app the user bound to shell scripts; Linux leads with Debian's
x-terminal-emulator alternative before the concrete emulators; Windows prefers
Windows Terminal over a cmd console.
2026-08-14 16:27:01 -05:00
Brooklyn Nicholson 4d6f4a6fe7 fix(desktop): local & remote profiles reuse the default socket after 0.20.1
sharedPrimaryRoute() inferred "served by the shared primary backend" from
the mere presence of connection.profile. But pooled backends (a local named
profile, or a per-profile remote override) also carry `profile` so their
WebSocket URL mints against the right backend. Both descriptors looked the
same, so ensureGatewayForProfile() took the shared-primary branch for a
pooled profile and never dialed its socket — Desktop stayed on the default
profile's socket even though the sidebar (REST) listed the right sessions.

Regression from #85665 (d16e236). Tag only the true shared-primary
descriptor with an explicit `sharedPrimary: true` marker in ensureBackend()
and check that marker instead of `profile`. Covers both the local-pool and
remote-override routes — the whole bug class, not one path.

Test asserts both sides of the invariant: a { profile, sharedPrimary: true }
descriptor activates the primary socket without dialing, and a pooled
descriptor carrying { profile } dials its own exact WebSocket URL.

Supersedes #85750, #85778, #85932
Fixes #85777

Co-authored-by: Tigrannnnnnn <122704900+Tigrannnnnnn@users.noreply.github.com>
Co-authored-by: Don Tuttle <11698271+wdon@users.noreply.github.com>
Co-authored-by: plcunha <145560011+plcunha@users.noreply.github.com>
2026-08-14 16:25:16 -05:00
Teknium 3f39f80355 fix(update-check): never flag local-ahead checkouts as updates (desktop SSH sibling)
Same local-ahead blind spot as the CLI SSH fast path, on the desktop's
passive SSH-official check: tips differ but ahead_by == 0 means the remote
tip is reachable from HEAD (carried local commit). Treat that as up to date
instead of 'update available' — the nudge toward hermes update is exactly
what wipes carried work.

Widens andyst-dev's #84860 to the desktop sibling site.
2026-08-14 13:46:33 -07:00
Teknium 29760fe6b1 perf(desktop): reuse the chip preview bytes at image submit
The composer read the same image off disk twice: once in attachImagePath
for the chip thumbnail (previewUrl — the FULL file as a base64 data URL),
and again inside uploadComposerAttachment at submit for the upload bytes.

readImageForRemoteAttach now accepts the attachment's previewUrl and
reuses its bytes when it is a base64 data URL, skipping the second disk
read + IPC round-trip. Anything else (e.g. a gateway media URL) falls
through to the disk read unchanged.

Flagged in #86302's renderer bench as the one real renderer-side
inefficiency on the attach path.
2026-08-14 13:44:02 -07:00