Three fixes from live testing the org persist/restore flow:
1. Org not persisting (stale closure). discoverCloud() resolves the org
asynchronously from the NAS response and setCloudOrg() is a React state
update, but connectCloudAgent read the cloudOrg value captured in its render
closure — often still null when the user clicked Connect in the same tick, so
no org was saved. Mirror the org into a ref (cloudOrgRef) updated
synchronously alongside state; connect reads cloudOrgRef.current.
2. Cloud connection lingered after switching away. coerceDesktopConnectionConfig
inherits existingBlock.url across mode switches (correct for remote↔local),
so switching cloud→local/remote kept the cloud instance URL in the remote
block — re-selecting Cloud then looked 'already connected' with no way to
re-pick. Added a leavingCloud rule: when the saved block was cloud and the new
mode isn't cloud, start from an empty block (drop the cloud url/org/token),
cleanly unselecting the cloud gateway. remote↔local toggles still preserve a
real remote URL.
3. Change-org button vanished after restore-open. It was gated on
cloudOrgs.length > 1, but the restore path discovers straight into the saved
org and never populates cloudOrgs. Gate on cloudOrg being set instead, via a
new changeCloudOrg() that clears the org + agent list and re-discovers with no
org arg (multi-org → NAS 409 picker; single-org → auto-resolve back).
Depends on NAS #550 (echo resolved org), merged + live on prod (0dc86d0b).
tsc + eslint clean; 57 node --test + 16 vitest pass; all three verified live on
Ben's host (org persists + restores, cloud unselects on switch, Change-org shows
after reopen). The benign 'Session not found' 404 on backend switch is left as-is
(already handled by isSessionGoneError → fresh draft; dev-log noise only).
cloud-auto-discovery Phase 3/4 follow-up.
The Hermes Cloud ModeCard in Settings → Gateway now only appears when the BETA
env var is truthy (1/true/yes/on, case-insensitive); absent/empty/false/0 hides
it. While the feature is in beta, non-beta users see only Local + Remote.
- main.cjs: betaFeaturesEnabled() reads process.env.BETA; exposed via new IPC
hermes☁️beta-enabled (the sandboxed renderer can't read process.env, and
runtime IPC means the same build honors BETA per-launch with no rebuild).
- preload.cjs / global.d.ts: cloud.betaEnabled() bridge + type.
- gateway-settings.tsx: fetch the flag on mount (default false so it never
flashes in for non-beta users), conditionally render the Cloud ModeCard, and
flip the grid sm:grid-cols-3 → sm:grid-cols-2 when hidden.
Gates the SELECTOR only — an already-saved cloud connection keeps working if
BETA is later turned off; only newly selecting cloud is hidden.
tsc + eslint clean; 57 node --test + 16 vitest pass; env parsing unit-checked
across 9 cases; gate verified in the packaged bundle.
cloud-auto-discovery beta gating.
Settings → Gateway remembered 'cloud' mode but not WHICH org/instance, so
reopening dropped multi-org users back to the org picker, hiding the connected
agent (reported live).
- Persist a cloudOrg on the saved cloud connection (rides the remote block:
coerce reads input.cloudOrg / inherits saved; buildRemoteBlock + profile
sanitizer carry it; sanitize echoes it back as config.cloudOrg). Only for
mode:'cloud'; plain remote is unchanged. The instance was already persisted as
remoteUrl (the dashboardUrl).
- discoverCloudAgents now returns the org NAS echoes in the response
(trimCloudOrg), and the renderer records cloudOrg AUTHORITATIVELY from
result.org — so it's set even on single-membership auto-resolve where no
picker ran (the exact case that left the org unpersisted). Requires NAS #550
(echo resolved org in /api/agents); before that deploys, falls back to the
requested org.
- On open, the cloud-status effect seeds cloudOrg from the persisted
config.cloudOrg and discovers scoped to it, so Settings reopens straight into
that org's agent list instead of the picker.
- connectCloudAgent passes cloudOrg when saving so the choice sticks.
- The connected instance is highlighted (primary tint + ring) and shows a
'Connected' pill instead of a Connect button (compares saved remoteUrl to each
agent's dashboardUrl, normalized).
tsc + eslint clean; 57 node --test + 16 vitest pass. Connected-pill verified live;
org-restore pending NAS #550 deploy for the authoritative echo.
cloud-auto-discovery Phase 3/4 follow-up.
When Hermes Cloud discovery returns zero agents, the empty-state message now
renders 'Nous portal' as a hyperlink to https://portal.nousresearch.com/agents
(opened via the app's ExternalLink → shell.openExternal), so the user can jump
straight to creating an agent instead of finding the portal manually.
The cloudNoAgents i18n string becomes { before, linkText, after } (en + zh) so
each locale controls link placement; ja/zh-hant fall back to en via defineLocale.
No external-link icon on this inline link to keep the sentence clean.
tsc + eslint clean; link verified present in the packaged renderer bundle.
The silent per-agent sign-in (decisions.md Q5) was prompting a SECOND interactive
login after portal sign-in → org → dashboard selection (Ben's screencast). Root
cause: cloudAgentSilentSignIn → openOauthLoginWindow loaded the agent gateway's
/login, but /login is a PUBLIC route (dashboard-auth middleware allowlist), so the
gate's _auto_sso_response never runs there — it only fires on an unauthenticated
load of a PROTECTED page. The window therefore rendered the interactive
'Log in with X' chooser every time, instead of the silent 302 cascade. (Auto-SSO
is correctly configured on hosted agents: exactly one 'nous' session provider,
client_id agent:{id}, so it would have fired silently if triggered.)
Fix: openOauthLoginWindow(baseUrl, { silent }). The cascade passes silent:true,
which loads the PROTECTED root '/' instead of '/login'. The gate then runs
auto-SSO — single provider + a live partition portal session → 302 through
/auth/login → portal /oauth/authorize (auto-approves org members) → /auth/callback
sets the gateway session cookie with NO prompt. In silent mode the window also
starts HIDDEN and only reveals after 2.5s if the cascade hasn't completed
(graceful fallback to interactive, e.g. the portal session lapsed). The
interactive remote-gateway login (settings UI) keeps silent:false → /login
chooser, behavior unchanged.
Verified live end-to-end on Ben's host: portal sign-in → org picker → select agent
→ Connect now completes with no second login prompt.
cloud-auto-discovery Phase 3 follow-up (decisions.md Q9).
Two fixes surfaced by the first live end-to-end test of cloud sign-in (both
would have shipped broken — green units + code review did not catch them).
1. Portal session is PRIVY, not Hermes-gateway cookies (Q7). Phase 3 polled for
hermes_session_at/rt on the portal host, but the Nous portal (NAS) is a
Privy-authed Next.js app — it sets privy-token (which NAS auth() and the
/api/agents cookie path both read). The sign-in window therefore never
detected success and hung. Fix: cookiesHavePrivySession (privy-token + __Host/
__Secure/legacy privy-session variants) in connection-config.cjs, and
hasLivePortalSession now checks the Privy cookie on the portal host. The
per-agent silent cascade still uses the gateway-cookie check (each agent IS a
Hermes gateway).
2. Multi-org discovery needs an org picker (Q8). A portal session carries no org
pin, so a user in >1 org got a dead-end 403. Paired with NAS #545 (merged):
/api/agents now returns 409 org_selection_required + the user's org list, and
accepts a membership-validated ?org=. discoverCloudAgents(org) appends ?org=,
and on 409 returns { needsOrgSelection, orgs } instead of throwing; the cloud
panel shows a 'Choose an organization' picker, then re-runs discovery scoped
to the chosen org (with a 'Change org' affordance for multi-org users).
Also reverts the ERR_NETWORK_CHANGED retry helper from the prior commit: the
IPv6-churn aborts on Ben's Arch host are a host/network-layer issue, and a
client reload can't safely drive Privy's single-use-code redirect chain
(disable IPv6 for the session is the workaround). Kept out of this feature PR.
Tests: connection-config.test.cjs (57, +5 Privy-cookie cases, proven to fail
without the helper); boot-failure-reauth (16). tsc + eslint clean. Verified live
end-to-end against prod portal: sign-in → org picker → scoped agent list →
silent per-agent connect.
cloud-auto-discovery Phases 3+4 follow-up (decisions.md Q7, Q8).
Phase 4 of cloud-auto-discovery — the UI on top of the Phase 3 cloud plumbing.
Adds a third 'Hermes Cloud' ModeCard alongside Local/Remote in gateway-settings.
Selecting it reveals the cloud panel instead of the URL/token form:
- signed-out → 'Sign in to Hermes Cloud' (one portal login in the OAuth partition)
- signed-in → a discovered-agent picker (loading / empty / list states) with a
Refresh control. Selecting an agent drives the silent per-agent cascade
(cloud.agentSignIn) then applies a mode:'cloud' connection pointed at its
dashboardUrl — no second sign-in prompt.
Cloud auto-discovers on entering the mode when a portal session already exists.
Test/Save bottom-row actions are hidden in cloud mode (selection applies the
connection); the remote URL/token form is now gated to remote mode only.
Wires the renderer to the Phase 3 IPC (window.hermesDesktop.cloud.*). i18n
strings added to en + zh (full) and the Translations type; ja/zh-hant inherit via
defineLocale fallback. New 'Cloud' icon (IconCloud) exported from lib/icons.
Validated: tsc clean, eslint clean, vite renderer build succeeds, 52 electron +
16 vitest tests pass.
cloud-auto-discovery Phase 4.
Phase 3 (non-UI) of cloud-auto-discovery. Adds the 'cloud' connection mode and
the IPC plumbing for a single portal login that powers both agent discovery and
silent per-agent sign-in. The Phase 4 UI (cloud ModeCard + instance picker)
sits on top of these IPC methods.
Mode widening (Model A, decisions.md Q6): DesktopConnectionConfig.mode and
DesktopConnectionConfigInput.mode widen to 'local'|'remote'|'cloud'. A cloud
entry is a remote-shaped block (remoteUrl = the selected agent's dashboardUrl,
remoteAuthMode 'oauth') tagged mode 'cloud' so settings reopens into the cloud
picker. Every RESOLUTION site treats cloud as remote via the new
modeIsRemoteLike() helper (centralized in connection-config.cjs): readDesktop-
ConnectionConfig, sanitizeConnectionProfiles, sanitizeDesktopConnectionConfig,
coerceDesktopConnectionConfig, profileRemoteOverride, resolveRemoteBackend,
globalRemoteActive, testDesktopConnectionConfig, and isRemoteReauthFailure. The
live resolved HermesConnection.mode stays 'local'|'remote' — cloud never reaches
the boot path or the renderer remote-gating sites.
Cloud mechanics (main.cjs): one portal session in the persist:hermes-remote-oauth
partition does double duty — discoverCloudAgents() GETs {portal}/api/agents over
the partition-bound net (cookie-authed; NAS #542 accepts the cookie), and
cloudAgentSilentSignIn() opens a selected agent's /login in the same partition so
the portal's silent auto-approve 302s back with that agent's session cookie, no
second prompt. Portal base URL resolves via DEFAULT_NOUS_PORTAL_URL +
HERMES_PORTAL_BASE_URL/NOUS_PORTAL_BASE_URL overrides, mirroring the CLI.
IPC: hermes☁️{status,login,logout,discover,agent-sign-in} in main.cjs +
preload.cjs, typed in global.d.ts (DesktopCloudStatus/Agent/DiscoverResult/
AgentSignInResult).
Tests: modeIsRemoteLike + cloud profileRemoteOverride (node --test, 52 pass);
cloud reauth-failure cases (vitest, 16 pass). tsc clean; eslint clean. New tests
verified to fail without the source changes.
cloud-auto-discovery Phase 3 (non-discovery half + discovery/cascade plumbing).
Drop the duplicate mermaid-block.tsx (own mermaid.initialize + render path,
theme frozen at first load) and wire preview-file.tsx's MarkdownCode through
the existing RichCodeBlock registry from #52935 instead. One mermaid init
path, theme-flip re-init, Zoomable + copy-as-PNG, RichBoundary error
fallback — and the preview pane gets svg fences for free. Shiki block stays
as the fallback for all other languages.
Salvaged from #40531; surgically reapplied onto current main (i18n'd
preview-file.tsx). mermaid dep already present on main.
Co-authored-by: liuhao1024 <liuhao1024@users.noreply.github.com>
The salvaged commit rewrote update-marker.cjs and its test with CRLF
line endings (Windows editor artifact); restore LF so the diff shows
only the substantive change.
A Windows venv broken mid-update (e.g. python-dotenv missing after a partial
pip install) still has python.exe + Scripts\hermes.exe on disk.
unwrapWindowsVenvHermesCommand() returned that interpreter with no probe --
bypassing even the caller's --version smoke test -- so every recovery action
(Retry, Repair install, Use local gateway) re-resolved the same dead backend:
ModuleNotFoundError: No module named 'dotenv', same overlay, forever.
- unwrapWindowsVenvHermesCommand now runs canImportHermesCli() on the venv
python (checkout on PYTHONPATH, mirroring isActiveRuntimeUsable) and
returns null on failure so the resolver falls through to the bootstrap
installer, which actually repairs the venv.
- hermesRuntimeImportProbe() adds 'import dotenv' -- the first third-party
import on the CLI boot path (hermes_cli/env_loader.py) -- so a venv missing
python-dotenv fails the probe everywhere it's used (isActiveRuntimeUsable,
system-python rung, and the new unwrap gate).
- Regression tests: probe content + source assertion that the unwrap path
probes and falls through.
The Capabilities/MCP/Hub/Skills UX has settled, so lift every
`// TODO(i18n): literal until the UX settles` hardcoded English string into
the typed i18n catalog and drop the comments.
- New keys under `common` (expand, tryHint), `settings.mcp` (capability
summary, status line, all-servers, auth flow, tool chip titles, log empty
label), and `skills` (provenance, sort/bulk labels, empty states, editor
actions). Full translations in en + zh; ja + zh-hant overrides added.
- Module-level pure fns that had no `t` in scope now take the mcp translations
(`capabilitySummary`/`statusLine`) or an `emptyLabel` prop (`McpLogs`); the
archive toast takes `t`.
- Shared `common.tryHint(term)` dedupes the "Try “…”" search hint across
skills/messaging/cron/artifacts.
No behavior or styling change — string lookups only. Zero TODO(i18n) remain.
Post-merge follow-ups + several review rounds + a hub-search rework, folded together.
Merge-scuff restores (a stale-base refactor had reverted two live-on-main fixes):
- gateway: SessionStore compression-tip healing + its regression test.
- desktop: messaging session/transcript polling in desktop-controller
(MESSAGING_POLL / ACTIVE_MESSAGING_SESSION_POLL, refreshMessagingSessions,
refreshActiveMessagingTranscript, the richer sameCronSignature) so inbound
platform traffic updates live again instead of freezing until manual refresh.
Profile-switch isolation (epoch/close/guard on every profile-scoped async):
- Hub store clears + in-flight runHubAction bails (and swallows the post-switch
404 instead of a phantom toast); hub preview/scan/search/sources profile-scoped.
- MCP: probe/auth epoch guards, dirty-draft reset, sidebar mutations blocked
until config resettles AND every persist re-checks the epoch post-await;
profilePending clears on config settle incl. error; logs re-key on profile.
- Model settings reload on switch and epoch-guard setModelAssignment /
saveMoaModels / API-key activation.
- Config draft resets + cancels its autosave on switch; skill editor/archive and
star-map node dialogs close on switch; openSkillEditor / star-map openEdit
discard stale fetches; tool-usage analytics loads are profile-guarded/keyed.
Correctness + UX:
- Unique per-skill action names for hub install AND uninstall; hub/catalog rows
flip only on a clean exit_code; catalog install polls the background bootstrap
to completion, reconciles the mcp.json draft (no dropped server), and fails
loudly on non-zero exit; MCP catalog query keyed by profile.
- /test reports needs-auth for anonymous auth:oauth servers; /auth snapshots +
restores tokens on a failed re-auth and clears the full 300s callback window.
- config-settings shows a retry on load failure; CodeEditor/JsonDocumentEditor
go read-only while saving so edits typed mid-save aren't dropped.
- Deep-link highlighter deletes its param only after a successful scroll.
- Restored the PageSearchShell trailing slot → Artifacts refresh button/spinner.
- /settings?tab=mcp redirect keeps server=.
Progressive hub search: fan out one query per backend-searchable source
(index-covered API sources stay unsearchable → no ~70-call GitHub re-hammer),
merge/dedupe by trust as each lands, per-source spinner overlaid on the dimmed
chip — results stream in without blocking on the slowest, no layout shift.
test(web): /api/skills list carries usage + provenance (CI contract).
A back-to-back run of 3+ adjacent tool calls now collapses into a
fixed-height window that pins the newest call to the bottom and fades
older ones up under a top gradient, so a long run no longer shoves the
reply off screen. Shorter runs are byte-identical to before, and the DOM
shape is the same in both modes (only classes flip) so crossing the
threshold mid-stream never remounts a row. Expanding any row breaks the
window out to full height via a `:has([data-tool-open])` rule.
Route the app off its hand-rolled helpers onto lib/{text,time,format,json-format}
and the new primitives, plus assorted small tidy-ups:
- compactNumber for counts/tokens; normalize/capitalize/asText at the many
filter/label sites; shared Intl date/time formatters; row-hover + framed
editor adoption; scrollbar-gutter + padding parity on list surfaces.
- Messaging/Artifacts/Cron search hints + narrow-viewport tab dropdown;
floating-pet adopts useOnProfileSwitch; number formatting in statusbar,
command-center, agents.
- Electron: native overlay width + backend spawn tidy.
- Settings > Keys: credential fields read as plain subtext (all-unset) until
the group is focused or expanded, then take full input chrome with no
horizontal/vertical shift; inline Remove (trash) + Save mirror SearchField's
trailing-clear pattern instead of a floating hint that overlapped the card;
Esc still cancels. Drops the now-dead or/escToCancel i18n keys.
- Shared TabDropdown/ResponsiveTabs (components/ui): PageSearchShell and the
Command Center log file/level filters reuse the one narrow-width collapse.
- OverlayNav: data-driven pane nav — persistent rail on wide, a single dropdown
riding the titlebar strip on narrow; Settings and Command Center adopt it, and
the mobile dropdown carries the same section icons as the rail. Fixes narrow
vertical centering, redundant mobile section titles, gateway-status wrap, and
Panel master/detail stacking.
- OverlayIconButton is now the titlebar ghost button, matching the close X at
every size. Settings sub-view nav opens section + sub-view in one navigate so
API-keys/accounts actually open on narrow.
- Settings > Model: cube icon (was the {} namespace glyph) and a DOM-shaped
skeleton in place of the centered spinner.
- Command palette / session switcher clear the macOS traffic lights on small
screens.
- Prettier/eslint sweep across the touched files.
Five follow-ups to #57659 from post-merge review:
1. install.ps1: gateway scheduled-task re-enable now runs in a finally
(a thrown Remove-Item/uv venv failure previously stranded the user's
gateway autostart disabled), and tasks that were already disabled
before the install are no longer blindly re-enabled.
2. The venv-python holder guard is no longer bypassed by plain --force
(which the desktop bootstrap passes on every update while its lock
probe only checks hermes.exe/app.asar). New explicit --force-venv is
the escape hatch; --force keeps bypassing only the hermes.exe shim
guard.
3. _detect_venv_python_processes now also catches uv/base-interpreter
trampolines whose exe is outside the venv, via cmdline (venv path or
'-m hermes_cli.main' tied to this install root) and cwd.
4. Missing venv python is now UNHEALTHY on managed installs
(.hermes-bootstrap-complete / .update-incomplete markers) so the
repair lane runs instead of 'Already up to date!'; the repair branch
recreates the venv first when it's gone entirely. Dev checkouts keep
reporting healthy.
5. install.ps1 comment no longer claims a Startup-folder disarm the
code doesn't perform (logon-only, not a mid-install respawner).
Root-causes the July 2026 Windows incident chain (locked _brotlicffi.pyd /
_sodium.pyd during install, then 'No module named annotated_doc' with
'hermes update' insisting 'Already up to date!'):
- hermes update: probe venv core imports even when the checkout is current;
a half-updated venv (dep sync killed mid-flight by a locked .pyd) is now
detected and repaired instead of being reported as up to date
- hermes update (Windows): after pausing gateways, refuse to mutate the venv
while other processes run from the venv interpreter (the Desktop backend
runs as python.exe so the hermes.exe shim guard never saw it); --force
keeps the old behavior
- install.ps1 venv stage: disarm gateway autostart Scheduled Tasks before
the kill sweep (they respawn the gateway inside the kill->delete window),
make the sweep a bounded loop requiring 3 clean passes, and rename-then-
delete the old venv (a rename succeeds even with mapped DLLs) with stale-
dir cleanup on the next run
- desktop updater: 'venv shim still locked after 15s' now ABORTS the update
hand-off (restarting our backend, surfacing the holder to the user)
instead of 'proceeding anyway (force)' into guaranteed venv corruption;
the unlock wait also re-kills respawned backends each poll tick
Grace and timeout timers in runRenderTitleJob can call getTitle after
finish() tears down the hidden BrowserWindow, throwing in the main
process when the Artifacts page resolves many link titles concurrently.
Fold the skill hub (search/preview/scan/install, from #57441) into Capabilities
as a fourth "Browse Hub" tab, rebuilt on our stack:
- sources + debounced term search + preview are useQuery-driven (RQ dedupes/
caches per term, cancels stale terms — no hand-rolled sequence guard).
- Each result is a self-contained HubSkillRow that installs/uninstalls ITSELF,
reading its own status from a nanostore (store/hub-actions). Concurrent
installs never desync; an optimistic installed-override flips a row the instant
its own action resolves instead of racing the sources refetch.
- Action log bubbles through a $hubActiveLog atom into the shared LogTail in a
collapsed-by-default, persistent bottom DetailPane (ANSI stripped).
A Cursor-style MCP manager inside Capabilities, plus the backend it needs.
- Server list with brand/favicon avatars + live status dot and a capability
summary (N tools, M prompts, K resources); Servers | Catalog views.
- Catalog: one-click install of Nous-approved servers with required-env prompts.
- GUI OAuth: Authenticate opens the system browser from the TTY-less backend and
verifies a token actually lands; header/API-key servers are never pushed down
OAuth; a dirty mcp.json can't drop a freshly-persisted auth field.
- Full-width mcp.json editor (ecosystem document format) + pinned stdio/agent
LogTail; probes cached 5m and keyed by (profile, config) so revisiting never
respawns the fleet or shows a stale probe.
- Whole-map persistence (PUT /api/mcp/servers) so deletes/toggles actually stick
(the generic /api/config deep-merge could not remove keys).
- perf: MCP probe/auth no longer hold the global skills lock, so a slow stdio
spawn can't stall every other request into a 15s timeout.
- per-tool include/exclude gating (lib/mcp-tool-filter) mirroring the CLI loader.
Merge the old Skills + Toolsets tabs and pull MCP out of Settings into one
master-detail "Capabilities" hub (Skills / Tools / MCP / Browse Hub).
- Skills: usage-sorted from real per-skill activity, provenance badges
(learned / built-in / hub), edit + archive for learned skills, per-tab bulk
toggle; full-bleed empty states.
- Tools: usage-aware, container-queried rows (no early two-column collapse).
- Settings panels move onto the shared config-record query cache; the deleted
Settings MCP page redirects (/settings?tab=mcp → /skills?tab=mcp).
- Lazy, profile-scoped, TTL-cached usage analytics so the heavy 365-day scan
never blocks the Skills/MCP tabs. Full en/zh strings (ja/zh-hant inherit).
The active-transcript poll armed a 5 s timer for every selected session
and no-op'd inside the tick for local chats (already live over the
websocket). Derive activeIsMessaging and gate the effect on it so local
chats never spin an idle timer.
Inbound Telegram/WeChat/Discord messages are written by the background
gateway, not the desktop websocket that drives local chats. Without
explicit polling the messaging sidebar and the open transcript stay
frozen until the user manually refreshes.
Desktop:
- MESSAGING_POLL_INTERVAL_MS (10 s): interval poll of the messaging
session list so new platform sessions surface automatically.
- ACTIVE_MESSAGING_SESSION_POLL_INTERVAL_MS (5 s): poll the currently-
viewed messaging transcript and re-hydrate the chat state when the
FNV-1a signature changes (hash covers role + timestamp + content).
- sameCronSignature now compares lineage_root_id / source / profile /
preview / message_count / last_active / ended_at so stale previews
and activity times are no longer silently ignored.
- sessionMatchesStoredId helper de-dups the id / _lineage_root_id check.
- refreshMessagingSessions exposed from useSessionListActions so the
controller can use it in the poll effect.
Gateway:
- SessionStore._compression_tip_for_session_id: look up the latest
compression continuation for a session id.
- SessionStore._heal_compression_tip_locked: rewrite a stale entry to
the compression child before returning it, so a restart or failed send
no longer leaves the store pinned to the compressed parent.
Co-authored-by: lawyer112 <lawyer112@users.noreply.github.com>
* feat(desktop): CLI/dashboard parity — skills hub browser, MCP test/toggle/catalog, maintenance ops, log filters
Brings desktop GUI to parity with hermes skills/mcp/doctor/backup/debug-share/
curator/memory CLI commands and the dashboard's System + Skills-hub pages:
- Skills page: new Browse Hub tab (search official/GitHub/community sources,
preview SKILL.md, security scan verdicts, install/update with live action log)
- MCP settings: connection test (tool listing), per-server enable/disable
toggle, and a Catalog tab installing Nous-approved MCP servers with env prompts
- Command Center: new Maintenance section (doctor, security audit, backup,
debug share links, curator status/pause/run, memory file status + reset)
- Command Center system logs: file (agent/errors/gateway/desktop), level, and
substring filters instead of a fixed agent.log tail
- hermes.ts API client + types for all the above; en/zh locale strings (ja and
zh-hant inherit via defineLocale)
* feat(desktop): backend model catalogs in toolset config — hermes tools parity
Completes the `hermes tools` parity gap: after picking an image/video
generation backend the CLI runs a model picker (e.g. FAL's multi-model
catalog with speed/strengths/price); the desktop toolset drawer now has the
same flow as a radio-card list.
- web_server: GET /api/tools/toolsets/{name}/models (catalog + current +
default for the active or named provider row) and PUT .../model
(validated write to image_gen.model / video_gen.model), reusing the CLI's
plugin catalog helpers so GUI and `hermes tools` stay in lockstep
- desktop: ModelCatalogPicker in ToolsetConfigPanel — per-model cards with
speed/strengths/price, in-use + default badges, disabled until the
backend is the active one; provider selection now mirrors is_active
locally so the catalog unlocks without a refetch
- tests: 3 backend endpoint tests (catalog shape invariants, persist +
validation), 2 component tests, 2 API-contract tests; en/zh strings
The hidden BrowserWindow used by fetchLinkTitle to scrape page titles
had no will-download handler on its session. When a link artifact URL
responds with Content-Disposition: attachment, Electron fires will-download
and the file is saved for real — explaining the spurious download on the
Artifacts page.
Add guardLinkTitleSession() (parallel to the existing audio-mute guard for
#49505) that installs a will-download handler which immediately cancels
every download item on the hermes:link-titles session. Call it from
getLinkTitleSession() right after the request-type blocklist is wired up.
A turn that ends without a final `todo` update left the composer "Tasks N/M"
panel pinned with its last item stuck pending/in_progress, and it survived
restarts because the panel is read back from stored session history.
Two coupled fixes (the first alone is undone by the second path):
- Turn end: clear a still-active todo list on `message.complete` and on a
terminal `error` (new `clearActiveSessionTodos` — active lists only; a
finished list keeps its short linger so the last checkmark still lands).
- Rehydration: `hydrateFromStoredSession` runs *after* a turn completes, so an
"active" stored list is stale, not in-flight. It now restores only a
*finished* list (via new `todosForHydration`) and drops anything still
active — otherwise it re-pinned the panel right after the turn-end clear and
resurrected it on every restart.
Salvages #52996 (@0disoft): the fix shape (clearActiveSessionTodos on turn
completion, preserving the finished-list linger) is carried forward and ported
onto the current use-message-stream/ folder split (gateway-event.ts), then
extended to the rehydration path per review.
Co-authored-by: 0disoft <rodisoft1@gmail.com>
On macOS Tahoe (Darwin 25+), a nonzero titleBarOverlay height makes
setWindowButtonPosition() miscalculate the native traffic-light position
(electron#49183), shoving the lights into the left titlebar tools. Pass
height 0 there so the lights land at the configured inset; the renderer
paints its own drag strips, so nothing is lost. Pre-Tahoe is unchanged.
Gate on the truthful Darwin kernel major (25 = Tahoe) rather than the
product version, which macOS reports as 16 or 26 depending on build SDK.
Add a shared PAGE_MAX_W (1200px) and center OverlayMain within its pane
so settings and command center bodies stay readable instead of sprawling
on wide/ultrawide displays.
Broadens Tranquil-Flow's profile-startup timeout fix (#48518) from getProfiles
+ refreshActiveProfile to the rest of the calls the desktop fires during
connect: /api/config, /api/config/defaults, /api/model/info, /api/model/options,
/api/cron/jobs. On a profile-heavy or remote install any of these can exceed
the 15s DEFAULT_FETCH_TIMEOUT_MS while the backend is alive-but-busy (e.g.
list_profiles walks the skill tree per profile), surfacing as the spurious
"Timed out connecting to Hermes backend after 15000ms" that hangs the UI
(#48504).
Uses the surgical per-call mechanism (renamed STARTUP_PROFILE_REQUEST_TIMEOUT_MS
→ STARTUP_REQUEST_TIMEOUT_MS) rather than raising the global default (the
alternative in #48526): the liveness poll /api/status and all interactive/
runtime calls keep the short default, so a genuinely-dead backend is still
detected fast and the boot readiness probe (waitForHermes) is untouched.
Supersedes #48518 (carried as the base commit) and #48526 (global-default
raise). Fixes#48504.
Co-authored-by: YapBi <129007007+HeLLGURD@users.noreply.github.com>
Co-authored-by: Tranquil-Flow <66773372+Tranquil-Flow@users.noreply.github.com>
A Z.ai desktop user reported thinking reverting to medium after one turn,
burning ~200% of a week's credits in 4 days despite reasoning_effort: false
in config.yaml. Four compounding bugs:
- _session_info reported reasoning_effort "" for disabled reasoning,
indistinguishable from unset — the desktop adopted it after the first
turn, wiping its sticky "thinking off" pick so every later chat
reverted to the default effort.
- config.set key=reasoning always wrote agent.reasoning_effort to global
config.yaml, so every desktop model-menu selection (preset.effort ??
'medium') clobbered the user's configured value. Now session-scoped
like the messaging gateway's /reasoning, landing on
create_reasoning_override so lazily-built sessions keep it too.
- YAML `reasoning_effort: false`/`off`/`no` (boolean False) was coerced
to "" by every loader's `str(x or "")`, silently re-enabling thinking.
parse_reasoning_effort now treats False/"false"/"disabled" as
{"enabled": False}; loaders (tui gateway, gateway, cli, cron,
delegate) pass the raw value through. The desktop config reader also
crashed on the boolean (false.trim()), aborting voice/STT settings.
- The zai provider profile never sent thinking on the wire, and GLM-4.5+
defaults to thinking ON server-side — so disabling reasoning was a
silent no-op on direct Z.ai, the actual token burner. The profile now
emits extra_body.thinking {"type": "enabled"|"disabled"} for
thinking-capable GLM models, mirroring the DeepSeek profile.
Also: /new (session reset) now carries reasoning_config across the
rebuild like model_override; config.get reasoning prefers the session's
live value and maps a config False to "none"; Settings shows "Off"
instead of a blank select for hand-written false.
When the renderer sends a DELETE /api/profiles/{name} request, the IPC
handler tears down the profile's pool backend (or primary backend) via
prepareProfileDeleteRequest. However, the very next line calls
ensureBackend(profile), which spawns a fresh pool backend for the just-
deleted profile. The new backend's startup path calls ensure_hermes_home(),
which recreates the profile directory — defeating the deletion and leaving
the process as a zombie.
On the next Desktop restart the cycle repeats: the profile directory exists,
the Desktop spawns a backend, the backend recreates the directory after
deletion, and PIDs accumulate indefinitely.
Fix: make prepareProfileDeleteRequest return the torn-down profile name.
The IPC handler uses this to route the DELETE to the primary backend
instead of spawning a new pool backend for the deleted profile.
Fixes#52279
Replace the loopback/PKCE-callback server and manual-paste fallback with
the RFC 8628 device-code flow as the only xAI Grok OAuth login path. The
flow works in headless/SSH/container sessions with no 127.0.0.1 listener,
shrinking the local attack surface.
- Poll the token endpoint with server-provided interval, honoring
slow_down and expires_in; store tokens with auth_mode
oauth_device_code.
- Adaptive proactive refresh skew for short-lived device-code JWTs;
rotated tokens sync back to auth.json, the global root store, and the
credential pool (no refresh-token replay).
- Clear source suppression on successful re-login (CLI + dashboard) and
drop the duplicate dashboard pool entry so exactly one seeded
device_code entry exists.
- Use the shared device_code source name for consistency with the
nous/codex device-code providers.
- Desktop: remove the loopback OAuth flow states and dead type variants;
pkce providers' sign-in URL selection is unchanged.
- Docs (EN + zh-Hans) rewritten for device-code login; drop the deleted
--manual-paste flag from documented commands.