* fix(desktop): un-minimize zone when revealing a collapsed tool panel
`revealTreePane()` fronted the pane's tab but never un-minimized its zone
when the pane lived in a shared zone (terminal + logs, or a tool panel
stacked with the workspace). The shared-zone branch of `setPaneCollapsed`
routes opens through `revealTreePane` instead of `toggleTreeGroupMinimized`,
so the zone stayed collapsed and the terminal appeared to "close but not
open" on Ctrl+` — and tab clicks needed a double-click because the first
click's `restoreTreePane` → opener → listener → `revealTreePane` chain
didn't un-minimize either.
The fix: `revealTreePane` now restores a minimized zone before fronting
the pane, chaining the un-minimize + activate into a single `commit` so
the second op sees the updated tree.
* fix(desktop): side collapse in column-root layouts + restore after zone-menu minimize
Two follow-up fixes for the panel layout:
1. Ctrl+B / Ctrl+J sidebar toggles didn't work in the Terminal deck (and
Quad) layout. The side-collapse system (treeSideOfPane, paneRootSide,
layoutHasRootSide, and the renderer's semanticSides) only operated on the
ROOT split when it was a row — but Terminal deck's root is a column, so the
side columns (sessions/files) nested inside a child row were invisible to
the collapse system. Extracted a rootRow() helper that finds the row
containing main (the root itself for row layouts, or the row child of a
column root), and a rootRow prop propagated through TreeNode → TreeSplit so
semanticSides fires on the right split regardless of root orientation.
2. Clicking the "logs" tab in a minimized terminal/logs zone needed a
double-click when the zone was minimized via the zone menu (right-click →
minimize), not the toggle. restoreTreePane called the opener
($logsOpen.set(true)) and returned early — but when the store was already
true, nanostores don't fire the listener, so setPaneCollapsed/revealTreePane
never ran and the zone stayed minimized. Now restoreTreePane also
un-minimizes directly + calls revealTreePane when the zone is still
minimized after the opener runs.
Reasoning ("Thinking") rendered through MarkdownTextContent with a smooth
typewriter reveal. TextMessagePartProvider mints a fresh part object on every
text change, and useSmooth resets its reveal to empty whenever the part
identity changes — so each reasoning delta restarted the animation from the
first character (h / hell / hello wo ...). Token-streaming reasoners
(R1/Qwen/GLM/Claude thinking) fire a delta per token and flash hard; GPT-5's
coarse reasoning summary updates too rarely to notice, which is why it looked
fine.
Drop smooth on the reasoning path so it plain-appends, exactly like the
assistant answer already does. Removes the now-dead smooth plumbing too.
Generalize the clarify opt-out into an UNBOUNDABLE_TOOLS set so a run
containing image_generate also stays a plain, fully-visible stack instead
of collapsing into the bounded window, where the max-height + gradient mask
clipped the image the same way it clipped clarify forms.
Extract shared hermes_cli/input_sanitize.py (bracketed-paste wrapper stripping
and terminal ~[[e artifact suffix collapse) and wire it into prompt.submit
and the Desktop composer so corrupted user text is cleaned before
messages.content is persisted.
resetLayoutTree() claims to "restore everything" but never reopened collapsed
SIDES. A sidebar hidden before a reset survived it, so the next ⌘B toggled from
that stale-hidden state into a SHOW — and the user's hide never persisted across
reload. Reopen every bound side through its store so the toggles stay truthful.
Adds a store-level regression test (real modules, re-import = reload) covering
hide→reload and the reset→hide→reload repro.
Make every session tab speak the same drag language as a sidebar row, and
keep tab focus 1:1 across the sidebar, tiles, and main:
- Drag a session tile's OR the main workspace tab onto a composer to link the
chat (@session chip), or onto a zone/edge to stack/split — tabDrag now
returns whether it took the drag, so the workspace tab defers to the generic
pane move on a fresh draft (nothing to link).
- A lone tool panel (terminal/logs) dragged to its own zone keeps its header,
so it stays draggable/closable instead of stranding a dead, tab-less zone.
- The sidebar highlight follows the FOCUSED session (interacted tile, else the
main selection) rather than only the main one.
- Clicking an already-open session jumps to its tab — an open tile, or the
workspace tab when it's the main session and focus sits on a tile — instead
of a dead no-op reload.
- Secondary (single-chat pop-out) windows boot to the default tree with no
tiles and never persist their stripped-down layout back, so they can't
inherit or clobber the primary window's tabs/splits.
The pointer drag ghost is extracted to a shared lib/drag-ghost and the drop
affordances drop their now-unused labels.
@assistant-ui/react 0.14 makes respondToApproval required on
ToolCallMessagePartProps; the settledClarifyProps helper still lacked it
after the upgrade cherry-picks, so tsc failed on clarify-tool.test.tsx.
lock the four behaviors the built-in normalizeMathDelimiters/escapeCurrencyDollars introduce over the deleted custom helpers: $$<digit>$$ display math stays intact, double-backslash brackets and [/math]/[/inline] tag pairs rewrite to dollar delimiters, and currency dollars in prose are escaped. the existing preprocessMarkdown suite had no math cases.
the parseIncompleteMarkdown comment implied the reveal frontier is repaired; repair runs on the full accumulated text, so reword it to say that. drop the now-dead "multiple surfaces render the same content" clause from the block-cache comment (the smooth and defer wrappers that caused it were removed), and trim the math-preprocess comment to the load-bearing prose-only constraint.
@assistant-ui/store renamed its index-out-of-bounds throw from tapClientLookup/tapClientResource to useClientLookup in the 0.14 upgrade, so the boundary's /tapClient.../ filter stopped matching and re-threw the transient session-switch and reconnect race to root, blanking the app. broaden the regex to accept the new prefix (keeping the old one for older store versions) and point the test at the real message so it exercises the live path instead of the dead string.
delete lib/remend-tail.ts (108 lines) and lib/remend-tail.test.ts (105
lines). the tailBoundedRemend export from @assistant-ui/react-streamdown
0.3.4 is algorithmically identical — same findRemendWindowStart boundary
scan, same fence/math tracking, same slice-and-repair strategy. the only
differences are improvements: the built-in handles \r (CR) in line
endings for Windows compatibility, and accepts an optional RemendOptions
parameter passed through to remend.
the import in markdown-text.tsx moves from @/lib/remend-tail to
@assistant-ui/react-streamdown. the call site
(preprocessWithTailRepair) is unchanged.
verified: tsc 0 errors, eslint clean, vitest 0 new failures (15
pre-existing, 786 passing — 6 fewer than before because the deleted
remend-tail.test.ts had 6 cases), manual verification of incomplete
markdown repair during streaming.
delete SmoothStreamingText, DeferStreamingText, and useSmoothReveal
(~174 lines) from markdown-text.tsx. the built-in defer and smooth
props on StreamdownTextPrimitive now handle the same work:
- defer: routes streaming text through useDeferredValue so markdown
re-parsing runs at lower priority (typing/scrolling stay responsive)
- smooth: typewriter-style reveal via useSmooth with SmoothOptions
{ drainMs: 500, maxCharsPerFrame: 30, minCommitMs: 33 }, matching
the old useSmoothReveal constants exactly
MarkdownTextContent (reasoning text) gets both defer and smooth.
MarkdownText (assistant text) gets defer only, matching the previous
behavior where text messages had no typewriter effect.
the internal pipeline order changes from smooth → defer → preprocess
to preprocess → smooth → defer (the built-in primitive runs preprocess
first). this is functionally equivalent: the tail-bounded remend repair
runs once on the full text instead of per revealed prefix, and the
smooth reveal operates on already-repaired markdown. end result is
identical.
verified: tsc 0 errors, eslint clean, vitest 0 new failures (15
pre-existing, 792 passing), manual verification of 6 streaming
scenarios (defer, smooth reveal, typing-while-streaming, code blocks,
math, long text performance).
bumps @assistant-ui/react from ^0.12.28 to ^0.14.23 and
@assistant-ui/react-streamdown from ^0.1.11 to ^0.3.4. this crosses
two minor bumps on each package and unlocks the built-in defer, smooth,
and tail-bounded remend primitives for PR 2.
breaking change from core 0.2.x: MessageRepository.appendOptimisticMessage
was removed (assistant-ui#4162). inline the three steps it did (generateId
+ fromThreadMessageLike + addOrUpdateMessage) in
incremental-external-store-runtime.ts, and set metadata.isOptimistic so
the new off-branch eviction logic cleans up the placeholder correctly.
fromThreadMessageLike and generateId graduated to the public API in
0.14.22 (assistant-ui#4414), so they now import from @assistant-ui/react
instead of @assistant-ui/core/internal. ExportedMessageRepository in the
test file moves to the public import for the same reason. the remaining
internal imports (AssistantRuntimeImpl, BaseAssistantRuntimeCore,
ExternalStoreThreadListRuntimeCore, ExternalStoreThreadRuntimeCore,
hasUpcomingMessage) are runtime construction internals with no public
equivalent and stay on @assistant-ui/core/internal.
the @assistant-ui/store npm override is removed: all transitive ranges
now resolve to 0.2.18 without it.
verified: tsc --noEmit passes, vitest shows zero new failures (15
pre-existing, 792 passing, identical to baseline before the upgrade).
Terminal and logs now follow the IntelliJ/VS-Code tool-window model: their
toggle (⌃`, ⌘K) COLLAPSES the zone to a rail with the tab still showing instead
of hiding it outright, so "toggle" and "the tab bar" stop fighting. Restore
routes through the pane's store opener (rail click / chevron) so the shortcut and
titlebar toggle stay truthful; the tab's ✕ dismisses the panel (comes back via
its toggle), while a session tile's ✕ still closes the session. New store
primitives (setPaneCollapsed / restoreTreePane / collapseTreePane + a
collapse-pane registry) via a bindPaneCollapse in the controller.
The faint statue backdrop behind the transcript was only switchable via
the DEV-only leva panel. Add a persisted Appearance toggle (default on)
so users can hide it; the Backdrop simply skips rendering when off.
* fix(desktop): clear stale compaction status
Clear the compaction phase when a turn resumes with model or tool activity, and key response timers by session and turn so switching chats preserves elapsed time.\n\nSupersedes #48115 by porting its resumed-content approach to the current split stream hook and covering tool-first resumptions.\n\nCo-authored-by: liuhao1024 <sunsky.lau@gmail.com>
* fix(desktop): resume after thinking activity
* fix(desktop): clear turn timer on stop
Self-review nits on the Thinking-widget fix:
- type ReasoningTextPart as ReasoningMessagePartComponent and read the
typed useMessagePartReasoning() directly, dropping the ad-hoc cast
(the hook already returns text/status).
- remove useRef, now unused after deleting useSmoothReveal.
- trim the autopsy comments; the PR body carries the narrative.
The Thinking disclosure rendered blank for every reasoning-emitting model
(Fable, DeepSeek, GPT-5.5, ...). Two causes:
1. ReasoningTextPart read a `text` prop that assistant-ui never populates —
reasoning parts arrive via context, same as text parts — so it always got
an empty string. Read the text via useMessagePartReasoning() instead,
mirroring how MarkdownText uses useMessagePartText().
2. The reasoning-only SmoothStreamingText / useSmoothReveal layer stalled at
revealed="": the reasoning part stays isRunning for the whole message while
the answer streams and thrashes re-renders, so the char-reveal never
advanced past 0. Render reasoning through the same DeferStreamingText →
surface path the assistant answer uses, and drop the dead smoothing code.
Move setGatewayState + rerender calls inside act() blocks and make the
synchronous soft-switch test async so all state updates are wrapped.
Eliminates the last 4 act() warnings (44 → 0).
Add vitest.setup.ts with IS_REACT_ACT_ENVIRONMENT=true + auto-cleanup,
and wrap render()/fireEvent() calls in act() across 8 test files:
- provider-config-panel.test.tsx: wrap renderPanel + fireEvent in act
- providers-settings.test.tsx: wrap renderProvidersSettings + fireEvent
in act
- use-prompt-actions/index.test.tsx: add actRender helper, wrap all 38
render
calls, wrap Harness handle methods (submitText/cancelRun/steerPrompt/
restoreToMessage) in act at the onReady callback level
- attachments.test.tsx: make renderWithI18n async + wrap in act
- skills/index.test.tsx: make renderSkills async + wrap fireEvent in act
- messaging/index.test.tsx: make renderMessaging async + wrap fireEvent
in act
- gateway-connecting-overlay.test.tsx: wrap all render/rerender in act
- preview-pane.test.tsx: wrap render calls in act, make tests async
Reduces act warnings from 44 to 4 (remaining are fake-timer + pre-render
store mutation edge cases). All 146 test files / 1180 tests still pass.
Answered clarifies were collapsing into a generic tool row, hiding the
choice. Settle into a Q&A panel instead, and route freeform input through
the shared Textarea chrome.
Sign out of the dedicated OAuth partition before opening the login window so a
stale gateway/identity-provider cookie can't silently bounce an expired session
straight back into failure. Relabel the action "Sign out & sign in" and spell
out the sign-out step in the hint.
Co-authored-by: Tony Antunez <57689194+smtony@users.noreply.github.com>
Add isRemoteReauthError so an auth-shaped boot error counts as a remote-reauth
failure even when the session indicator still reads connected (a stale refresh
cookie / failed ws-ticket mint). Wire the boot error into the overlay's reauth
check so those sessions route to Sign in instead of the local-only recovery
buttons.
Co-authored-by: Tony Antunez <57689194+smtony@users.noreply.github.com>
A remote/VPS backend that failed to boot trapped the user on the recovery
screen — Retry/Repair/Use-local only target the local backend, so the only fix
was hand-editing connection.json. Add an in-place "Gateway settings" view (the
real GatewaySettings panel embedded via `embedded`, lazy-loaded) reached from
the recovery card, and shape the recovery actions by failure kind: Sign in for a
lapsed remote session, Gateway settings for any other remote failure (Retry
drops to secondary; Repair is dropped — it can't revive a remote), Retry for a
local backend. Use-local is scoped to remote failures.
Factor the "remote/cloud with a URL" check out of isRemoteReauthFailure into a
shared isRemoteConfig helper so the boot-failure overlay can tell any remote
failure apart from a local one.
A block-level label child (e.g. `flex`) collapses TooltipContent's inline
`box-decoration-clone` wrapper, so Radix measures a zero-size chip and parks
an empty black rectangle in the panel corner instead of by the trigger
(#62022). The terminal rail's hotkey labels and the preview row's two-line
label both hit this.
Harden the shared wrapper (`[&>*]:!inline-flex`) so any call site's direct
child renders inline-flex, add a reusable `TipHintLabel` for the common
text+hotkey label, and keep the preview row's label explicitly inline-flex.
Salvages #62139 (shared-component hardening + TipHintLabel) and #62073
(inline-flex call-site fixes + rail/preview coverage).
Co-authored-by: alelpoan <alelpoan@proton.me>
Co-authored-by: zapabob <1920071390@campus.ouj.ac.jp>
Bundle Fireworks AI as a first-class BYOK provider across the CLI, web/TUI,
and desktop onboarding.
- New model-provider plugin with attribution headers (HTTP-Referer / X-Title)
so Fireworks can attribute Hermes traffic; PAYG-safe default aux + fallback
models (accounts/fireworks/models/...), IDs tracking fw-ai/fireconnect.
- Registered in CANONICAL_PROVIDERS so it appears in the CLI/web/TUI pickers.
- Alias wiring (fireworks-ai, fw) into both CLI resolvers.
- First-class wiring: OPTIONAL_ENV_VARS, HERMES_OVERLAYS (FIREWORKS_BASE_URL
override), doctor env hints. Live catalog + model_metadata are auto-derived.
- doctor: treat Fireworks' native slash-form IDs (accounts/fireworks/...) as
valid, not aggregator vendor prefixes, so it no longer tells Fireworks users
to switch to openrouter or drop the prefix.
- picker: plugin providers with no static curated list now lead with their
profile fallback_models, so the default is an agentic chat model instead of
whatever the live catalog returns first (Fireworks listed an image model,
flux-*, ahead of its chat models).
- Desktop onboarding: Fireworks as a RECOMMENDED hero card with the official
Fireworks logomark and a brand-purple badge, routing to the BYOK key form;
i18n in en/ja/zh/zh-hant.
- Tests: profile contract, first-class wiring (both resolvers, overlay, config,
doctor incl. the slash-form regression, aux headers, credentials), discovery
spot-check, and a live smoke test driven through the Hermes runtime.
Fire Pass (fpk_) support is coming soon; the future wiring is kept as a
commented-out scaffold in the plugin.
Add a glyph-agnostic ParticleField (float-up + organic sway/bank + springy
pop-in), skinned as pink pixel hearts. Hearts play on the pet when one is out
(in-window or popped out) and celebrate alongside; otherwise they rise from the
composer. A generic $petReaction bus mirrors the burst to the pop-out overlay
window so it reacts even while the app is minimized.
Consume the core `reaction` event to fire hearts on affectionate messages. DEV
Shift+H previews a burst.
Radix's hoverable-content grace area can leave tips stuck over Electron drag regions; disable it and make tip content pointer-events-none so open state tracks the trigger only.
Switching connection mode (local / cloud agent / remote) no longer
full-window-reloads into the cold-boot CONNECTING screen. The primary
backend is torn down in place (no renderer reload); the shell + Settings
stay up while session lists are wiped so sidebar skeletons retrigger, then
the socket re-dials and config/sessions refresh. Cold-boot CONNECTING
latches off after the first successful boot; the intentional teardown
suppresses the backend-exit toast. Dev affordance: a "Preview soft switch"
button under Gateway diagnostics (Electron has no ?query= entry).
Gateway settings UI brought in line with the rest of Settings:
- Mode cards use the shared selectableCardClass on an equal-height
auto-rows-fr grid, stacking 1→3 (never an orphaned 2+1); titles wrap
instead of truncating.
- Remote gateway's auth detail moves into a ? tooltip in the title; drop
the redundant "connects to the one you choose" from the cloud card.
- textStrong buttons force px-0 so the underline sits flush with the label.
- Tooltip chip uses box-decoration-break: clone so the background hugs each
wrapped line (bg only on the text), capped at max-w-64.
Fully i18n'd (en + zh; ja/zh-hant inherit via defineLocale).
Adds a third "Hermes Cloud" gateway mode to the desktop app: one portal
sign-in auto-discovers the agents on your account and connects to any of
them with no second interactive prompt.
- Electron: widen connection mode to 'local' | 'remote' | 'cloud', routed
through a centralized modeIsRemoteLike() so every resolution site treats
cloud exactly like remote; portal discovery (GET /api/agents over the
OAuth partition), Privy-cookie liveness, multi-org picker (NAS 409), and a
silent per-agent /oauth cascade (load protected root, not /login).
- Persist a cloudOrg on the cloud block; unselect cloud on mode switch.
- Renderer: Hermes Cloud ModeCard + agent picker (signed-out/loading/empty/
list), org picker, Change-org, connected-highlight + Connected pill.
- i18n (en + zh full; ja/zh-hant inherit via defineLocale), Cloud icon.
- IPC: hermes☁️{status,login,logout,discover,agent-sign-in}.
Salvage of #55402 onto current main: the original branch predates the
desktop electron .cjs -> .ts migration (39d09453f), so the electron half
was re-authored against the .ts files. Authorship preserved.
cloud-auto-discovery Phases 3 + 4.