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
The desktop provider panel previously rendered the curated declarations
from hermes_cli/memory_providers.py: five hindsight fields, and no panel
at all for undeclared providers like honcho (OAuth connect only). The
dashboard provider-switching rework re-pointed the shared config route
at raw plugin schemas, so the desktop began dumping every internal field
(35 for hindsight) and grew a bespoke honcho panel.
Serve both surfaces from the same route: ?surface=declared returns the
curated schema (empty for undeclared providers) with the original
config-file + env-store write semantics; the dashboard keeps the raw
plugin schema unchanged. The desktop client opts into declared.
alelpoan's fix (emit hermes:zoom:changed after restore) is correct, but the
bug's root is duplication: setAndPersistZoomLevel and restorePersistedZoomLevel
each independently did setZoomLevel + send, and restore forgot the send.
Collapse both (and the lifecycle re-assert) into a single applyZoomLevel()
helper in zoom.ts that always applies-then-notifies — the regression can't
recur by omitting a send. Replace the source-grep test (which broke on main:
the sibling source-assertion pet test it copied was refactored to a behavioral
one, dropping the fs/path imports it relied on) with behavioral coverage of the
funnel, matching zoom.ts's "unit-testable without booting a BrowserWindow"
convention.
Co-authored-by: alelpoan <alelpoan@proton.me>
classifyNativeBinary only checked big-endian Mach-O/Fat magic bytes
(feedfacf, feedface, cafebabe). Real Darwin .node files from node-pty
prebuilds are stored little-endian on disk (cffaedfe = MH_CIGAM_64),
so every Darwin prebuild classified as null, and validateStagedBinaries
threw a platform mismatch on macOS — breaking npm run check for every
macOS contributor. CI didn't catch it because runners are Linux (ELF
path was correct) and tests only planted big-endian fake headers.
Add recognition for all six Mach-O/Fat byte orderings:
- MH_CIGAM (cefaedfe) — LE 32-bit
- MH_CIGAM_64 (cffaedfe) — LE 64-bit [the one real prebuilds use]
- FAT_CIGAM (bebafeca) — LE universal
Update makeFakeNode to write LE CIGAM_64 bytes for the darwin fixture
(matching real on-disk format) and add regression tests for all new
magic forms.
stageNodePty received electron-builder's { platform, arch } but unconditionally
copied host build/Release, staging a host binary (e.g. macOS Mach-O) for a
foreign target (e.g. linux-arm64). The fallback rebuild also didn't pass the
target arch to electron-rebuild.
Now:
- build/Release is only staged when target platform+arch match the host
- cross-platform targets with no matching prebuild fail closed
- same-platform different-arch rebuild passes --arch to electron-rebuild
- post-staging validation reads .node magic bytes (ELF/Mach-O/PE) and rejects
any binary whose platform doesn't match the target
Adds stageNodePtyInto() (testable core) and classifyNativeBinary() (pure),
plus 11 regression tests covering the cross-target scenarios.
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).
Extracted zoomWiringForWindowKind() + ZOOM_WINDOW_CONFIG into zoom.ts so
the pet-overlay-
opts-out / chat-windows-keep-zoom contract is tested via the pure
config,
not by reading source. Callers in main.ts now use
zoomWiringForWindowKind()
instead of inline { zoom: false } / default { zoom: true }.
When neither a prebuild nor a compiled build/Release/*.node is found for
the target platform-arch, stage-native-deps.mjs now runs
electron-rebuild -f -w node-pty to compile one from source before
re-copying build/Release into the staged dist.
This makes the staging script self-sufficient — it always produces a
working native binary dir regardless of whether npm ci --ignore-scripts
skipped postinstall or whether node-pty publishes prebuilds for the
target (e.g. linux-x64 has no prebuild).
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.
oauth-session-request.test.ts regexed main.ts source text (extracting the
fetchJsonViaOauthSession function body and matching regexes against it) to
check Electron net.request doesn't set the forbidden Content-Length header
and does call request.write(body).
That behavior is already covered for real by oauth-net-request.test.ts,
which imports the actual serializeJsonBody/setJsonRequestHeaders helpers
from oauth-net-request.ts and asserts on a mock request object's setHeader
calls -- it only needed its relative import corrected to include the .ts
extension (Node's ESM loader doesn't resolve extensionless relative
specifiers). Removed the now-fully-superseded oauth-session-request.test.ts
and its dangling package.json wiring.
profile-delete-respawn.test.ts regexed main.ts source text to check that
prepareProfileDeleteRequest returns the torn-down profile name, and that
the hermes:api ipcMain handler captures that return value and routes to
the primary backend instead of respawning a pool backend for the just-
deleted profile.
Extract the pure decision logic into profile-delete-routing.ts (no
Electron import):
- profileNameFromDeleteRequest(request): parses a DELETE
/api/profiles/<name> path, moved verbatim (already pure).
- decideProfileDeleteAction(profile, deps): the branch decision (noop /
teardown-primary / teardown-pool) and the profile name to return,
parameterized over isDefaultProfile/isValidProfileName/primaryProfileKey.
- resolveRouteProfile(tornDownProfile, profile): the
routing ternary from the hermes:api
handler.
prepareProfileDeleteRequest in main.ts now calls decideProfileDeleteAction
for the decision and only performs the async side effects (teardown +
writeActiveDesktopProfile) the decision calls for.
profile-delete-routing.test.ts replaces profile-delete-respawn.test.ts
(which was never wired into test:desktop:platforms), importing the pure
functions directly and asserting real return values across every branch
-- no readFileSync, no regex-on-source. Wired the new test file into
test:desktop:platforms in package.json.
windows-hermes-resolution.test.ts regexed main.ts source text to check
three Windows resolution bugs that caused desktop reinstall loops:
1. findOnPath()'s PATHEXT extension order (must try real extensions before
the empty one, or an extensionless Git-Bash hermes shim shadows
hermes.cmd/.exe).
2. handOffWindowsBootstrapRecovery()'s --update vs --repair choice (must
gate on any real-install signal, not just the hermes.exe shim).
3. unwrapWindowsVenvHermesCommand()'s probe-before-trust behavior (must
canImportHermesCli() before returning a venv python, or a broken venv
gets re-selected forever).
Extract all three into pure, dependency-injected functions in
windows-hermes-path.ts (no Electron import): buildPathExtCandidates(),
chooseUpdaterArgs(), resolveVenvHermesCommand(). main.ts's
findOnPath/handOffWindowsBootstrapRecovery/unwrapWindowsVenvHermesCommand
now call these with their existing helpers (fileExists, canImportHermesCli,
getVenvPython, etc.) passed through as deps.
windows-hermes-path.test.ts replaces windows-hermes-resolution.test.ts,
importing the pure functions directly and asserting real return values
with fake/injected dependencies (fake venvs, fake probes) -- no readFileSync,
no regex-on-source.
windows-child-process.test.ts regexed main.ts and bootstrap-runner.ts
source text to check that spawn/execFileSync call sites wrapped their
options with hiddenWindowsChildOptions(), and that backend teardown
chose the right kill strategy.
Extract both into dependency-free sibling modules:
- windows-child-options.ts: hiddenWindowsChildOptions(options,
isWindows)
now takes isWindows as an injectable param (defaults to the real
platform check). main.ts and bootstrap-runner.ts both import the same
implementation instead of each defining their own copy.
- backend-child.ts: stopBackendChild(child, deps) with
forceKillProcessTree
and isWindows injected, so the SIGTERM-vs-tree-kill branching is
directly
testable with a fake child + a spy.
windows-child-options.test.ts replaces windows-child-process.test.ts,
calling the real functions with fake spawn/execFileSync-shaped objects
and asserting on the actual returned options / kill call.
Live deltas from session A were attaching to session B after New Session
when events arrived without session_id — fallback used the newly focused
activeSessionId (#47709).
Pin unscoped stream events to the session that received message.start
(#48281). Also reset RAF-pending view staging on new/resume/create so a
stale background flush cannot repaint over the switched chat (#47743).
Co-authored-by: Ray <rayjun0412@gmail.com>
Co-authored-by: zapabob <1920071390@campus.ouj.ac.jp>
When the base is an origin/… ref, fetch just that branch so the
local tracking ref is fresh before `git worktree add -b new origin/main`.
Fetch failures (offline / no remote) are silently ignored — git uses
whatever local ref exists, or raises a clear error if it's missing.
The sidebar "New worktree" button branched off whatever HEAD you were
on — now a filterable Popover+Command combobox lets you pick any local
or remote-tracking branch as the base, defaulting to origin/HEAD.
Backend listBaseBranches() queries refs/heads + refs/remotes via
for-each-ref, flags origin/HEAD (falling back to the local default for
no-remote repos). Mirrored on the Python REST API side
(base_branch_list + /api/git/base-branches route).
Add fallback only updates local editor state; complete pairs are filtered
before onChange. The post-#7b5ba205 resync effect then saw the unchanged
persisted chain and wiped the draft — button looked dead.
Ignore value updates that match the last chain we emitted; still resync
on real external changes (profile/config reload).
Co-authored-by: HexLab98 <liruixinch@outlook.com>
The #54527 context pin (7acaff5ef) snapshots the selected stored session
and route token at submit entry and aborts when either changes mid-flight.
But a NEW chat's create pipeline legitimately moves both: on success,
createBackendSessionForSend re-homes selection and navigates to the chat
it just minted. Judged against the pre-create draft baseline that read as
a user switch, so every first send of a new chat aborted before
prompt.submit — message dropped, no DB row persisted (row creation is
lazy, server-side in prompt.submit), and the window stranded on a route
whose REST reads 404 "Session not found" forever.
Fix: after a successful create, verify no one re-homed during create's
post-commit await via the active-session ref (a non-null return
guarantees create set it; every switch path retargets it synchronously),
then re-pin the drift baseline to the created chat. A mid-create switch
still aborts through create's own null return, or through the active-ref
check for the post-commit window. Re-pinning also restores the correct
stored-id association for the optimistic-message state updates, which the
pinned pre-create null had degraded.
Tests: red-first regression for the new-chat send, an abort case for a
switch landing in create's post-commit window, and the sleep/wake
new-chat stub made faithful to the real create (it sets the active ref
before returning — the inert stub is what let this ship green).
Asserts each {provider, model} entry renders as its own row (the bug
produced "[object Object]"), that removing a row emits the remaining
entries, that adding a blank row never persists a partial pair, and the
empty-state hint.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Settings → Model rendered `fallback_providers` (a list of `{provider,
model}` objects) through the generic `list` config field, which does
`value.join(', ')` and stringified each entry to `[object Object],
[object Object]`.
Add a dedicated provider+model row editor (add/remove), sourced from the
same `getGlobalModelOptions()` the composer picker uses, that reads and
writes the `{provider, model}` chain. Half-filled rows are kept in local
state so the config autosave never persists a partial entry, and an
out-of-catalog model stays selectable so existing custom entries render.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
MoA was internally inconsistent: preset-level ops (set default / add /
delete) persisted on click, but reference-model and aggregator slot edits
sat behind a manual Save button. Debounce-persist slot/aggregator edits
like the rest of settings and drop the redundant button, so MoA is
uniformly autosave.