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.
When the desktop UI runs on Windows and the gateway runs in WSL, a WSL/POSIX
cwd isn't openable/readable from the Windows host. Add wsl-path-bridge.ts to
translate the Windows-side direction only:
- native folder dialog defaultPath: `/home/...` → `\\wsl.localhost\<distro>\...`
- fs read path: WSL cwd → its UNC / `C:\` drive form
Distro detection reads `wsl.exe -l -q` with `WSL_UTF8=1` and strips stray NUL
bytes, since older wsl.exe emits UTF-16LE (microsoft/WSL#4607) — the original
utf8 read returned a garbled distro name. UNC uses `\\wsl.localhost\` with a
`\\wsl$\` fallback for older Windows. The reverse (any path → POSIX) is handled
once gateway-side, so the picker result needs no desktop translation.
Co-authored-by: Rage Lopez <VrtxOmega@pm.me>
Capture durable Desktop engineering principles from recent sessions —
state by authority, workspace-switch shapes, resolver ladders, optimistic
UI — and point root AGENTS.md at the scoped guide with current filenames.
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.
Align the workspace status-bar dropdown with the rest of the status bar: drop
the per-item icons (they mixed lucide size-4 with a Codicon 1rem glyph and were
the only status-bar menu carrying item icons), leaving text-only items on the
shared DropdownMenuItem primitive with default typography. The status-bar
trigger keeps its FolderOpen glyph, consistent with sibling items.
Also map true@supersynergy.de → Supersynergy in AUTHOR_MAP.
Squashed salvage of #45744 (@harjothkhara), rebased onto current main and
resolved against #58241 (which swapped the new-session cwd fallback to the
project-aware resolveNewSessionCwd).
An explicitly clicked sidebar workspace stays authoritative until session.create:
a one-shot $newChatWorkspaceTarget (null → detached, string → that folder) plus a
generation counter so a stale async `config.get project` normalization can't
overwrite a newer draft target. The start-workspace-session action is extracted
out of desktop-controller.tsx into a testable workspace-session-target module.
Integrated with #58241: the no-explicit-target branch now falls through to the
project-aware resolveNewSessionCwd() instead of the old workspaceCwdForNewSession.
Co-authored-by: harjoth <harjoth.khara@gmail.com>
Addresses @teknium1's review of #61950:
- The desktop live overlay (workspace-groups.ts) matched cwd membership
case-sensitively, so a fresh mixed-case/separator Windows session missed
its explicit/auto project until the next backend tree refresh. Mirror the
backend identity (isWindowsPath/comparisonSegments/pathKey) in isPathUnder,
liveSessionProjectId, and overlayRepoLanes lane matching. Comparison-only —
emitted ids/labels keep their spelling. POSIX stays case-sensitive.
- Backend _is_windows_path missed root-relative `\wsl.localhost\...` (single
leading backslash), leaving that historical spelling case-sensitive. Classify
any backslash-rooted path as Windows.
Tests: WSL-spelling collapse + explicit-project precedence (project_tree),
Windows/WSL live-overlay membership + POSIX case-sensitivity (workspace-groups).
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.
Add an `embedded` flag to GatewaySettings (and a `bare` variant to
SettingsContent) that drops the page title/intro, Diagnostics row,
"Save for next restart", and the page gutters — so the same panel can be reused
inside a tighter surface without a second connection form to maintain. No change
to the standalone Settings → Gateway page (defaults off).
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.
Remove the DEV-gated "Dev · soft switch" ListRow and its previewGatewaySwitch
helper. It was a temporary review affordance for exercising the soft-switch
reconnect; dead-stripped from production, but it doesn't belong in the tree.
wipeSessionListsForGatewaySwitch (the real path) and $gatewaySwitching stay.
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>
A reopened tab restarted the shell in its original launch dir, so the fresh
prompt showed the wrong folder after a prior `cd` (the issue's "separate
thing" note). Track the shell's working directory and restart the PTY there.
Two independent signals feed a persisted per-tab restoreCwd:
- a main-side PTY cwd probe (shell-agnostic; /proc on Linux, lsof on macOS;
Windows has no cheap per-process query so it falls back to the launch dir)
- cwd-reporting OSC sequences parsed in the renderer (OSC 7 file URIs, OSC 9;9
ConEmu/Windows-Terminal paths) for shells configured to emit them
On relaunch the fresh shell boots in restoreCwd, falling back to the launch
cwd (then home) when it no longer exists.
cleanReviveSnapshot only dropped the trailing prompt when a blank separator
sat above it (starship add_newline), so shells that print the prompt with no
preceding blank line — default PowerShell (PS C:\..>), bash user@host:~$ —
kept the idle prompt in the saved buffer and showed a duplicate under the
fresh boot prompt on every relaunch of an *active* session.
An interactive shell always reprints its prompt after a command, so the tail
of an idle buffer is the prompt, never history. Drop the short block after a
blank separator when present, otherwise drop the trailing single-line prompt.
Command output is preserved; the fresh shell reprints the live prompt on boot.
An idle terminal tab (no command ever typed) grew one extra copy of the
shell's boot prompt on every close/reopen: persistSnapshot re-serialized a
buffer that was just the replayed old prompt plus the fresh shell's new
prompt, and cleanReviveSnapshot's blank-line trim can't strip prompts on
shells like default PowerShell that print no separator line.
Track real user input (keystrokes/paste, drag-and-drop paths, injected
commands) and, when a session had none, skip re-serializing. If the buffer
we loaded carried no real scrollback (empty or only a repeated prompt),
clear it so the next launch shows a single fresh prompt and any existing
accumulation heals; otherwise leave the prior snapshot untouched so real
history from an earlier active session survives an idle reopen.
Salvages #61584 (activity tracking) and #61577 (clearing content-free idle
buffers) into one path: it also heals already-polluted buffers, counts
drag-and-drop and injected input as activity, and never discards genuine
short command history (only empty/all-identical buffers are cleared).
Co-authored-by: alelpoan <alelpoan@proton.me>
Co-authored-by: liuhao1024 <sunsky.lau@gmail.com>
Desktop active-session picker calls already pass a session_id, but the gateway's model switch persistence is controlled by parsed model flags. Add --session so the shared parser keeps live-session selections, including MoA virtual provider presets, out of profile config.yaml.
Constraint: config.set model values are parsed by hermes_cli.model_switch before persistence is decided.
Rejected: backend special-case for desktop session_id | it would duplicate existing --session semantics and widen the gateway surface.
Confidence: high
Scope-risk: narrow
Directive: Keep desktop model picker active-session switches explicit with --session; do not rely on session_id alone for persistence.
Tested: npm run test:ui -- src/app/session/hooks/use-model-controls.test.tsx src/app/shell/model-menu-panel.test.tsx
Tested: npm run typecheck
Tested: git diff --check
Not-tested: full pytest suite; change is desktop TypeScript/UI routing only.
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.
Snapshot the selected stored session and route token for the full async submit
pipeline so a mid-flight session switch cannot resume the wrong chat or
misroute the user's text. Includes regression tests.
Fixes#54527 — a message typed into one TUI session could be silently
misrouted into (or overwritten by) another concurrently-open session.
Root cause: activeQueueSessionKeyRef is written on every render, but the
debounced draft-persist timer, the pagehide flush, and dispatchSubmit's
reject-restore path all read it lazily at async-resolve time instead of
capturing the scope that was active when the operation started. A session
switch landing between capture and resolve relabels one session's text
under the other session's key. A large paste widens the window (slower
synchronous render), which matches the original report.
Fix: introduce draftScopeRef, written only by the draft-swap effect (so it
always reflects the session whose text is actually loaded in the editor)
and read it instead of the render-time ref at both async write sites.
dispatchSubmit's restore() now uses the submittedScope already captured at
dispatch instead of re-reading the live ref.
Also adds isPendingDraftPersistCurrent as defense-in-depth: before the
debounce timer commits a write, it verifies its captured {scope, text}
pair is still the one on file. This is a no-op under the fix above (a
session swap or a newer keystroke already clears/replaces the pending
entry via clearTimeout), but turns any future regression that reintroduces
a stale/live-ref read at this call site into a dropped write instead of a
silent cross-session misroute.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
scripts/desktop-sandbox.sh runs a Hermes desktop instance in an isolated
sandbox — separate HERMES_HOME, separate Electron userData, and a
distinct
app name (HERMES_DESKTOP_APP_NAME) so it doesn't compete with the main
desktop instance's single-instance lock.
Two modes:
- Ephemeral (default): temp dir, cleaned up on exit
- --persistent: stored under .hermes-sandbox/ in the worktree git root,
survives restarts for repeat testing
In the Nix devShell the script is available as 'sandbox'.
Also makes APP_NAME overridable via HERMES_DESKTOP_APP_NAME in main.ts —
app.setName() runs before requestSingleInstanceLock(), so the overridden
name changes the lock key. collectRelaunchEnv already preserves
HERMES_DESKTOP_* vars through self-update relaunches; test updated to
cover the new env var.
we built .ts into .js for a minute there and dumped em in src, and those
old .js files are getting resolved over top of the ts updated ones so
desktop clients don't update.
just --clean the old files so there's never a conflict :3
The SLASH_TRIGGER_RE regex used (?:^|[\s]) as its left anchor, so typing
a / anywhere in the message (e.g. "hello /") opened the slash command
popover — even though slash commands only execute at the beginning of a
message. Anchor the regex strictly at position 0 (^) so the popover only
appears when / is the first character, matching the actual execution
semantics. The @-mention trigger is left untouched since those work
anywhere in the text.
removing tsc -b from the build script (previous commit) also removed
the only step that type-checked the electron/ directory — the CI
typecheck job runs tsc -p . --noEmit, which uses tsconfig.json whose
include is only ["src", "../shared/src"], so electron/ was silently
uncovered. extend the typecheck script to also run against
tsconfig.electron.json so electron/ stays type-checked in CI.
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).
Old packaged Desktop apps re-entering bootstrap against an existing
~/.hermes/hermes-agent were still passing the baked-in --commit pin, which
detached the managed checkout back to the app stamp (e.g. 0.15.1) after
hermes update had already moved it forward.
Skip the packaged commit pin when activeRoot already has git metadata;
keep branch args and fresh-install commit pinning unchanged. Port of
#59902 onto the post-ts-ify bootstrap-runner.ts.
Co-authored-by: helix4u <4317663+helix4u@users.noreply.github.com>