Commit Graph

6401 Commits

Author SHA1 Message Date
ethernet
dec282c658 style(desktop): eslint --fix on the split local-models and remote-setup files 2026-09-24 14:12:16 -04:00
ethernet
de89d9c95b fix: update ui double string 2026-09-24 14:11:17 -04:00
ethernet
427ab1d58d test(desktop): require HERMES_PYTHON for mac-sign digest tests
The fixture imports pm and scripts.bundles, which only the prepared
runtime provides. Falling back to py/python3 on PATH failed later with
an unrelated ImportError; fail up front with a clear message instead.
2026-09-24 14:08:22 -04:00
ethernet
787c739a2a refactor(desktop): split the local models pane into scoped sections
ScopedLocalModelsSettings was one ~760-line component that threaded
`owner` into every child. The pane now provides its owner through
LocalModelsOwnerProvider (still keyed by owner, so each connection/profile
remounts), and renders sibling components that read it from context: the
quickstart hero, runtime, hardware, models (catalog and sideloaded rows)
and browse sections. Request actions live in local-models-actions.ts and
keep the current-owner toast fence.

Two intentional differences: the delete spinner is per row instead of one
shared id, and the runtime install/update buttons pass the pane's owner
and QueryClient explicitly (the unused handleInstallRuntime is gone).
2026-09-24 14:08:22 -04:00
ethernet
fd19d439cb refactor(desktop): split remote setup into probe, test and OAuth hooks
useRemoteSetup covered the credential form, the connection test and the
OAuth login in one hook. The test/feedback leg now lives in
useRemoteConnectionTest and the login/logout leg in useRemoteOAuth, both
fenced by the probe's target generation as before. useRemoteSetup keeps
the form state and composes the three legs; its public shape is unchanged.
2026-09-24 14:08:22 -04:00
ethernet
372979c3de Merge remote-tracking branch 'origin/main' into ethie/pm-clean 2026-09-24 13:40:17 -04:00
samuelpatro
130b8f2c5d fix(desktop): run cloud agent silent sign-in at boot
A Hermes Cloud agent authenticates through the silent per-agent cascade
(cloudAgentSilentSignIn), which was only ever invoked from the settings
"Use gateway" button. Boot went straight to waitForHermes, so once the
agent's session cookie expired the ticket mint answered 401, the app
reported "not signed in" and latched reauth, even though the portal
session it needed to recover was still live. Every relaunch needed a
manual click.

Boot now runs the cascade once and retries once, only for remoteKind
cloud + authMode oauth on the terminal reauth error and only with a live
portal session. Everything else surfaces unchanged.
2026-09-24 13:23:37 -04:00
Austin Pickett
4dc7a23690 fix(desktop): adopt an unmarked copy of a package's own desktop half
Folders already installed by shipped builds have no marker, and reconcile
refused them on every pass, so the stuck "copying…" row never recovered.

Reconcile now stamps such a folder in place when its `plugin.js` is byte
for byte the package's own half — that can only be our pre-marker copy,
and adopting it writes no files. Anything that differs is a standalone
plugin the user installed and is still never touched (#112450).

Co-authored-by: xxxigm <tuancanhnguyen706@gmail.com>
2026-09-24 13:17:12 -04:00
Austin Pickett
0d914a0652 fix(desktop): stamp the package marker when installing a unified plugin from git
`installDesktopPluginFromGit` published the desktop half with no
`.hermes-package.json`, unlike every other publisher. Without that marker
the Plugins page has no evidence the copy belongs to the agent package:
the agent row sat on "copying…" forever beside a second, standalone row,
and the half loaded default-enabled instead of opt-in.

A repo carrying both halves now lands under the AGENT package name and is
stamped as part of the same staged publication, so this path and
`reconcileUnifiedDesktopHalves` converge on one folder (#100412) instead
of racing to two. A desktop-only repo is unchanged: git-derived folder
name, no marker, standalone.

The Plugins-page test moves with it — pairing is the marker's job, so the
page keeps an unmarked copy as its own row rather than guessing from the
folder name.

Co-authored-by: xxxigm <tuancanhnguyen706@gmail.com>
2026-09-24 13:17:12 -04:00
xxxigm
b372ffa859 test(desktop): pin pairing for a marker-less desktop plugin copy
A catalog install can leave desktop-plugins/<name>/plugin.js with no
package marker. The Plugins page must show that copy on the agent row
instead of a second row stuck on copying.
2026-09-24 13:17:12 -04:00
ethernet
05a78671a7 Merge remote-tracking branch 'origin/main' into ethie/pm-clean 2026-09-24 13:13:35 -04:00
kshitijk4poor
d5785bb525 fix(desktop): stop the sentence-fallback poll on unmount and session drop (#79314)
The incremental Edge/non-streaming TTS fallback re-polls the pending reply every 150 ms until the response finishes. Nothing cancelled that timer when the hook unmounted or the speech session was dropped, so a tick could fire after teardown (CI: 'ReferenceError: window is not defined' at use-voice-conversation.ts poll, from the rearm test file). The poll's cancel is now held in a ref, cleared by dropSpeechSession() and by an unmount effect.
2026-09-24 22:42:17 +05:30
kshitijk4poor
dbc4780a08 test(desktop): poll for 'listening' after re-arm in voice rearm tests
status flips to 'listening' only after handle.start() resolves, so the
synchronous assertion right after waitFor(start x2) raced (2/9 flakes on
'speaks completed fallback sentences before the response finishes').
Wrap it in waitFor; same assertion, no longer timing-dependent.
2026-09-24 22:42:17 +05:30
kshitijk4poor
feb503d68f fix(desktop): edge-TTS fallback flushes sealed interims and skips stream re-probes
- poll: flush the sentence buffer when the interim bubble is sealed but a
  tool is still running (mirrors feedSpeechSession's session.flush), so its
  trimmed last sentence isn't held for the whole tool run.
- playSpeechText gains a syncOnly option; the per-sentence fallback uses it
  so each sentence no longer re-runs directTtsConfig/resolveSpeakStreamUrl
  and a speak-stream WebSocket that already answered fallback.
- single ownedSequence baseline per sentence.
2026-09-24 22:42:17 +05:30
kshitijk4poor
299fc88f6e fix(desktop): make the edge-TTS sentence buffer a pure cutSentences accumulator
Drop the ad-hoc <think> handling (narrower than markdown-preprocess's
REASONING_TAGS; prose mentioning '<thinking' held the rest of the reply
until finish) and the unused minSentenceChars parameter. The fallback is
fed text-parts-only reply text, and the streaming session's ingest does no
think stripping either, so this restores parity.
2026-09-24 22:42:17 +05:30
kshitijk4poor
e71cdf328f style(desktop): prettier on voice-playback import after re-export removal 2026-09-24 22:42:17 +05:30
kshitijk4poor
e84313f65b test(desktop): trim edge-TTS sentence-queue tests to two invariants
Keep: speaks completed sentences before the reply finishes; Stop halts the
queue without re-arming. Buffer unit tests dropped — the splitter is
cutSentences(), already covered in speech-text.test.ts.
2026-09-24 22:42:17 +05:30
kshitijk4poor
d2fec0ea99 refactor(desktop): sentence-queue fallback reuses cutSentences()
Salvage follow-up for #79314 / #82774: one sentence splitter for both the
client-direct streaming session and the sync (edge) fallback queue. Moves
cutSentences into the pure speech-text module (importers and its tests
now import it from speech-text directly; no re-export shim) and makes
IncrementalSpeechSentenceBuffer a thin think-block-aware wrapper over it. Also passes ownerRef scope to per-sentence
playback (grafted during conflict resolution).

Co-authored-by: XtremXpert <5651439+XtremXpert@users.noreply.github.com>
2026-09-24 22:42:17 +05:30
embwl0x
e39900cc91 fix(desktop): stream sync TTS by sentence
(cherry picked from commit 16bb8c320d9dae99713099584469b5871f9928ea)
2026-09-24 22:42:17 +05:30
ethernet
0f65d698d0 Merge remote-tracking branch 'origin/main' into ethie/pm-clean 2026-09-24 13:10:14 -04:00
kshitijk4poor
32385a905b fix(desktop): own busy-stale context breakdown rule in useContextBreakdown (#70871)
The hook's effect clears the snapshot only after commit, so the first busy
render still returned the pre-turn breakdown; the gauge re-checked busy but
ContextUsagePanel did not and briefly painted stale categories. Gate the
hook's return on !busy so gauge and panel share one owner, and inline the
gauge merge back into useMemo (drops the exported resolveContextGaugeUsage
helper and its #87903 narration).
2026-09-24 22:34:18 +05:30
kshitijk4poor
bfef939643 test(desktop): trim context-gauge tests to two lifecycle invariants (#70871)
Drop the resolveContextGaugeUsage unit file and three lifecycle cases; keep
the end-to-end contracts: streamed usage drives the gauge mid-turn and at
turn end until a fresh idle breakdown lands, and a failed idle refresh never
restores the pre-turn snapshot.

Co-authored-by: konsisumer <11262660+konsisumer@users.noreply.github.com>
Co-authored-by: webtecnica <75556242+webtecnica@users.noreply.github.com>
2026-09-24 22:34:18 +05:30
Xipong
a3d98f49ad fix(desktop): invalidate idle context snapshots across turns
(cherry picked from commit c3701c1ddd77f921121689fa055072ec1b605167)
2026-09-24 22:34:18 +05:30
Albert.Zhou
c4bb76a132 fix(desktop): show live streamed context gauge during turns (frozen-gauge fix)
While a turn is in flight the status-bar context meter is painted from the
pre-turn `context_breakdown` value because `useStatusbarItems` overlays it
over the streamed usage whenever the breakdown is non-null — and
`useContextBreakdown` skips refetching while busy, so the bar stays frozen
at the turn-start figure and jumps when the turn ends. The backend already
streams live `session.usage` snapshots every second
(`_start_usage_ticker`, tui_gateway/server.py); the overlay defeats it.

Extract `resolveContextGaugeUsage`: while busy, the streamed usage always
wins; only when idle may the breakdown override (it answers for resumed
sessions whose streamed store has no context fields). Adds four combination
tests. Shows the first figure as soon as the first provider response
arrives, no longer only after the turn completes.

Refs #70871 (frozen-gauge half, mechanism named in the #87903 discussion),

(cherry picked from commit 79f66cfa4dfaf76b00f9dfb022c8e441289bbddb)
(cherry picked from commit 3c93db06090371ce3aceaa92318c113918cfc69e)
2026-09-24 22:34:18 +05:30
kshitijk4poor
6cb1aa025c fix(desktop): gate transformed-final settle on the interim boundary (#96465)
A transformed final shares no prefix with the streamed text, the same shape
as a responsePreviewed rewrite, so it takes the same interimBoundaryPending
gate: a late transformed completion after the turn settled appends instead
of overwriting the settled reply.
2026-09-24 22:33:58 +05:30
kshitijk4poor
55c6fd01ac fix(tui_gateway): declare response_transformed on the message.complete contract (#96465)
MessageCompletePayload is extra="forbid", so the forwarded flag raised
ContractViolation under test isolation and logged a wire-contract violation
on every transformed turn. Regenerate the shared TS/OpenRPC contract and
type the desktop field from the generated MessageCompletePayload.
2026-09-24 22:33:58 +05:30
orcunulutas
caa8ee61c7 fix(desktop): settle transform_llm_output final onto the streamed bubble (#96465)
Renderer half of #96467 (7eeb0d4b03), hand-applied onto main's
use-message-stream: completeAssistantMessage takes response_transformed
(appended after persistedTurn) and settles a transformed final in place even
with no prefix relationship, gated on sawAssistantPayload. Contributor vitest
kept (trimmed to the core invariant).
2026-09-24 22:33:58 +05:30
kshitijk4poor
42457cc233 fix(desktop): fold only a partial committed during this turn; carry rowId
- turnPartiallyCommitted now requires both timestamps and committedAt >=
  turn_started_at, shared with turnAlreadyCommitted via committedDuringTurn():
  on an older runtime the tail may be the previous turn's answer, and
  splicing over it drops a real history row.
- The fold carries rowId with reactions through carryRowIdentity(), the same
  helper the overlay and withAuthoritativeTurnState now use, so a reaction
  toggle on the folded row still reaches the persisted row.
- Drop the always-true committedPartialAt >= 0 / && committedPartial guards.
2026-09-24 22:32:12 +05:30
kshitijk4poor
8c3146c489 fix(desktop): hold the stream-growth pin during inline edit; share resize metrics
- Skip the #118482 resize pin while the viewport carries data-editing
  (beginEditHold), so an open edit bubble is never snapped to the bottom.
- One readThreadScrollResizeMetrics() + COMPOSER_CLEARANCE_SLOT for both RO
  legs; the clearance spacer is read through a ref instead of a per-scroll
  querySelector walk over the whole transcript.
- Bottom test via threadScrollStateFromMetrics; isRunning read through a ref
  and the unused sessionKey dep dropped so a turn boundary doesn't rebuild
  the listener and observer.
2026-09-24 22:32:12 +05:30
kshitijk4poor
79d1d513e8 style(desktop): prettier on live-projection dedupe salvage 2026-09-24 22:32:12 +05:30
kshitijk4poor
5268ae3bd4 test(desktop): trim #121122 switch-back regressions to two invariants
Keep the fold-into-committed-partial and the advanced-text strip cases;
drop the diverged-tail guard, the end-to-end pipeline and the lagging-text
mirror, which exercise the same branches.

Co-authored-by: Jony <13896935+zyz619963502zyz@users.noreply.github.com>
2026-09-24 22:32:12 +05:30
chelsealong
2494b95929 fix(desktop): dedupe live stream row by text-extension, not exact match
removeRepresentedLocalLiveProjection compared the local optimistic
stream row's text against the session.activate inflight snapshot with
strict equality. The two are captured at different times while a turn
keeps streaming in the background (switch away, keep streaming,
switch back), so the texts routinely differ by whatever tokens
streamed in between even though they represent the same running
reply. The equality check then fails, the stale local row survives
the strip, and the transcript ends up showing it alongside the freshly
activated row for the same turn - the duplicate frozen/live copy
reported in #121122.

Accept either row as a forward text-extension of the other (the same
isStrictAnswerTextExtension idiom already used elsewhere in this file
for streaming text), in addition to exact equality.

(cherry picked from commit 48b300c90626fbf73da11e8693c43b31f680347b)
2026-09-24 22:32:12 +05:30
finn763
e3e51c831b fix(desktop): fold still-streaming dump into same-turn committed partial on session switch
Switching away mid-turn and back painted the in-flight assistant turn
twice: REST already holds the turn's partial row (text + tool blocks
committed as the turn progressed) while inflight still streams the fuller
dump, and appendLiveSessionProjection appended the dump beside it - the
frozen partial with its action bar plus the live copy repeating it.

Mirror the turnAlreadyCommitted guard in the live direction: when the
persisted tail assistant is the same turn's partial (inflight user already
persisted, turn still streaming, dump extends-or-equals the tail text),
splice the live row into the tail slot with committed structure carried
over instead of appending a second row. Diverged tails still append.

Closes #121122

(cherry picked from commit 99b5730450fec93297e50293998c435dd524a7c4)
2026-09-24 22:32:12 +05:30
Halldrix
e0a58b3848 test(desktop): render the composer-clearance case in the stream-follow regression
The composer-only resize test early-returned before asserting: the harness
never passed clampToComposer, so aui_composer-clearance never rendered and
the test passed vacuously on every tree — including the branches it is
meant to discriminate. Opt the harness in, and grow the stubbed clearance
by the same +168 as scrollHeight so transcript-only height is invariant
(a true composer-only resize). With this the test is RED on #118522's
raw-scrollHeight predicate (scrollTop 4400 -> 4568, no transcript growth)
and GREEN here.

(cherry picked from commit eabf4c6b51995c59ac5f8344398dbc890d669148)
2026-09-24 22:32:12 +05:30
Halldrix
cc269a7d0a fix(desktop): pin the transcript while streamed content grows (#118482)
(cherry picked from commit e975eec42d8ae5a0554351caaa29a5df65d272d5)
2026-09-24 22:32:12 +05:30
kshitijk4poor
66b31fc781 fix(desktop): keep turn-activity live region mounted across idle gaps (#46225)
TurnActivityIndicator returned null whenever the turn went quiet, so each
working/idle flip remounted a role=status aria-live node and screen readers
re-announced it. Latch once shown; while idle keep the row as an empty,
sr-only live region (still in the a11y tree) instead of unmounting, and only
mount the pulse/timer while active.

Co-authored-by: LINGJIAKUN <200388706+LINGJIAKUN@users.noreply.github.com>
2026-09-24 22:24:52 +05:30
Kevin Yin
c14933bf45 fix(desktop): silence live-region elapsed timers
Signed-off-by: Kevin Yin <182213728+yinkev@users.noreply.github.com>
(cherry picked from commit 27bc6e46a54b69d74baac8a0ec82ffb6cf69d902)
2026-09-24 22:24:52 +05:30
ethernet
cff2cfd41d fix(desktop): put the checkout on the source backend's PYTHONPATH
A developer/E2E interpreter override need not have the checkout installed,
and the backend runs in the user's workspace cwd, so `-m hermes_cli.main`
only resolved from an editable .venv. The Desktop core E2E on the PM test
environment died with "No module named hermes_cli" on every local boot.
Name this checkout explicitly; the scrub of an inherited value stays.
2026-09-24 12:48:04 -04:00
ethernet
ed6e37f9c1 Merge remote-tracking branch 'origin/main' into ethie/pm-clean 2026-09-24 11:58:45 -04:00
ethernet
56d1414daf fix(desktop): drop the HERMES_DESKTOP_FEED_BASE_URL fallback
The desktop feed base came from config.yaml's
updates.desktop_feed_base_url and then an undocumented env var. Config
already wins and is the documented override; a second env source only
lets a stray variable redirect where updates are fetched from. Remove
the fallback from resolveFeedBaseUrl, its caller, its test cases and
the Windows bundled E2E that blanked it.
2026-09-24 11:50:30 -04:00
Mohamad Kanso
ca67828504 fix(desktop): load local skin before gateway connects 2026-09-24 10:46:45 -05:00
brooklyn!
421f9592b3 fix(desktop): coalesce zone-editor split preview to one frame
Repeated pointer events were writing the split preview on every move, and
Shift did not flip the line until the next move. Paint at most once per
frame, skip an unchanged preview, and recompute orientation on Shift while
the pointer is still inside. The grid is not written until the click commits.
2026-09-24 10:42:36 -05:00
ethernet
46840bdf82 Merge remote-tracking branch 'origin/main' into ethie/pm-clean
# Conflicts:
#	apps/desktop/src/components/boot-failure-overlay.tsx
2026-09-24 11:17:13 -04:00
Austin Pickett
645da6561c fix(desktop): let the cloud ladder own the reauth logout
The boot overlay dropped this gateway's cookies before branching, and
reestablishCloudAgentSession drops them again as its first rung, so the
cloud path fired oauthLogoutConnectionConfig twice per sign-in. Move the
logout into the native-OAuth branch that still needs it; the overlay's
cloud test now pins exactly one call.
2026-09-24 11:13:26 -04:00
Carl Taylor
eb9ee9cacc fix(desktop): reject missing Cloud dashboard URL 2026-09-24 11:13:26 -04:00
Carl Taylor
0def3f162f fix(desktop): re-auth a lapsed saved Cloud gateway from Settings
A saved Hermes Cloud connection on a local-primary device had no
sign-in surface when its gateway session lapsed. The dial rejects
with a reauth-shaped error whose copy says 'Open Settings → Gateway
and sign in again', but the Settings OAuth row only renders in
remote mode, and both 'Use gateway' paths called selectConnection
only — the silent portal cascade ran solely on first connect, team
change, or boot-primary failure (overlay). Portal liveness also
read 'Signed in' while the agent session was dead.

One recovery ladder now exists in lib/cloud-agent-session.ts
(reestablishCloudAgentSession: drop lapsed cookies → ensure portal
session → silent per-agent cascade) and is shared by Settings
(retry the switch once after re-establishing) and the boot overlay
(same sequence, deduplicated, so the two cannot drift).

Tests: three invariants in gateway-settings.test.tsx — cascade +
retry on reauth rejection, no cascade for non-reauth failures,
interactive portal login when that session lapsed too. Proven red
on base for the two cascade cases.
2026-09-24 11:13:26 -04:00
ethernet
8d8edf4cda test(desktop-e2e): drop the packaged smoke's now-unused repo-root import 2026-09-24 11:12:27 -04:00
ethernet
890e9b73d4 test(desktop-e2e): run the core Desktop E2E on the PM toolchain
Upstream's e2e-desktop-core job provisioned setup-node + uv sync and pointed the
packaged smoke at a checkout .venv. Provision through setup-pm, hand its selected
interpreter to Desktop via HERMES_DESKTOP_PYTHON, and read the packager contract
from electron-builder.config.cjs, where the build config now lives.
2026-09-24 11:12:27 -04:00
ethernet
427d0883be Merge remote-tracking branch 'origin/main' into ethie/pm-clean 2026-09-24 09:33:58 -04:00
ethernet
e8a5e0978c chore(desktop): require prepared build Python and refresh release comment 2026-09-24 09:20:50 -04:00