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).
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.
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.
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>
`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>
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.
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.
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.
- 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.
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.
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.
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>
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).
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>
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)
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.
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.
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).
- 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.
- 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.
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>
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)
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)
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)
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>
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.
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.
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.
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.
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.
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.
Off macOS, ctrl folds to mod, so a Ctrl+B voice default is the sidebar
chord. Ship Ctrl+Alt+V instead, and let a binding be cleared to an empty
combo so sidebar mod+b can be unbound. Point the Voice settings hint at
the voice conversation action, not dictation.
Revealing the terminal restored the layout but left the composer holding
the keyboard. Terminals stay mounted while hidden, so the activation
focus effect never re-runs. Ctrl+backtick, the palette row, and the
statusbar pill now claim focus on the reveal direction and re-assert if
the composer steals it. Hiding does not. No second focus chord.