Commit Graph

504 Commits

Author SHA1 Message Date
Austin Pickett
9d7a4ab718 fix(dashboard): hide AuthWidget on 401 via ApiError status
fetchJSON now throws ApiError with a humanized message, so the
widget's legacy `msg.startsWith("401:")` check never matched and a
401 from /api/auth/me rendered "auth status unavailable" instead of
nothing. Read the status off the error object.

Part of GH issue 66223 (loopback /api/auth/me).
2026-10-07 17:13:07 -04:00
Tranquil-Flow
0f56e56a24 fix(dashboard): repair loopback auth identity frontend integration (#66295) 2026-10-07 17:13:07 -04:00
ethernet
2943ee6f19 fix(ts): npm run fix 2026-10-07 13:42:42 -04:00
Austin Pickett
ef0c635f33 refactor(web): move session source config to SessionsPage_sources sibling; satisfy code-health ratchet 2026-10-07 11:18:50 -04:00
Austin Pickett
d199cc2465 test(web): behaviour coverage for structured reasoning transcript rendering
RED on main: the expanded session transcript renders assistant
reasoning with literal <thinking>/<action>/<result> wrapper tags.
Keeps the user-role invariant (literal tags stay on the Markdown path).
2026-10-07 11:18:50 -04:00
giggling-ginger
85d7a896d0 fix(web): scope structured reasoning UI to assistant messages
Only assistant content with known reasoning tags uses StructuredReasoning.
User and tool messages with literal <action>/<result> tags stay on Markdown.
2026-10-07 11:18:50 -04:00
giggling-ginger
cdda67f83f Fix reasoning markup rendering in dashboard 2026-10-07 11:18:50 -04:00
Teknium
6cc1efd8f9 feat(voice): auxiliary.voice_chat routes spoken voice turns to their own model
Voice replies had to come from the session's main model, so a heavy model made every spoken
answer slow (#83894). A new auxiliary slot, voice_chat, picks the model for voice turns:

- CLI/TUI voice mode, the Desktop voice conversation (prompt.submit voice_turn) and messaging
  voice notes mark the turn; agent/voice_turn_route.py binds the route after the prompt, row and
  preflight compaction were settled against the main model and undoes it before memory sync and
  background review, so the next typed turn and the review run on the main model.
- A turn larger than the voice model's compression threshold stays on the main model with a
  one-time notice: switching to voice never compacts the conversation.
- Usage lands in session_model_usage under task voice_chat; it never rewrites the session's
  recorded model/route, so resume keeps the model the user picked.
- Slot appears in Desktop Settings -> Models, the dashboard, and `hermes model`; GPT-Live
  delegations keep using the session model.
2026-10-07 01:35:11 -07:00
alt-glitch
5bff65203e fix(dashboard): web typecheck checks the app project; ChatSessionList test types its mocks
web/tsconfig.json is a solution file (files: [], references), so 'tsc -p . --noEmit' checked nothing and CI's web check passed while the build's 'tsc -b' failed on four errors in ChatSessionList.test.tsx (#134100). The typecheck script now runs 'tsc -b . --noEmit' like hermes-ink, and the test types the act-environment global and the Button mock's extra props.
2026-10-06 17:59:06 -04:00
andyst-dev
1ac3f97fbe fix(dashboard): enable touch scrollback in chat 2026-10-06 17:39:35 -04:00
Teknium
eb6a3886f1 fix(dashboard): ChatSessionList test type-checks under tsc -b
#134100 added a test that assigns globalThis.IS_REACT_ACT_ENVIRONMENT directly
(TS7017) and destructures ghost/outlined/size from a plain ButtonHTMLAttributes
prop type (TS2339). Vitest strips types, so the test passes, but `tsc -b` in the
web build fails, which breaks every Docker image build and the Desktop update
E2E seed on main. Cast globalThis like the sibling tests do and widen the mock
prop type with the three props it consumes.
2026-10-06 14:22:13 -07:00
Austin Pickett
5e836abd90 refactor(dashboard): move the copy-last button into a chat-copy-last-button sibling
Offset ChatPage.tsx growth (the isActive prop for ChatSessionList) per the
code health ratchet: the floating copy-last response button moves verbatim
into web/src/lib/chat-copy-last-button.tsx. No behaviour change.
2026-10-06 15:31:39 -04:00
Austin Pickett
da1bf112a2 fix(dashboard): use react-router MemoryRouter in ChatSessionList test 2026-10-06 15:31:39 -04:00
debaditya
d81d4ac33f fix(dashboard): pause session refresh off chat route 2026-10-06 15:31:39 -04:00
debaditya
39ac8f2b25 fix(dashboard): refresh chat session switcher 2026-10-06 15:31:39 -04:00
Austin Pickett
35f0b4b569 fix(dashboard): route native text paste through term.paste to stop last-char duplication
A native/DOM paste on the chat terminal (context-menu paste, middle-click,
or any Ctrl+V route that bypasses the keydown interception) fell through to
xterm's own paste listeners. xterm clears the hidden textarea during the
paste dispatch, but the browser's default action — inserting the pasted
text into that textarea — runs after dispatch, so a stale value survives
and the next typed character is duplicated (#52471: "when" → "whenn").

The host capture-phase paste handler now cancels the native paste for
plain text and delivers it via term.paste() exactly once — the same clean
route the Ctrl/Cmd+V keydown interception already uses. Image pastes keep
their upload path and are never routed as text.

Supersedes #52477 (credit: @liuhao1024 — same interception, extended with
the image route preserved and regression coverage).

Fixes #52471
2026-10-06 13:00:43 -04:00
Austin Pickett
3225f357a5 fix(dashboard): show broken links as non-actionable entries in Files page
Backend listings now emit broken_link: true placeholder entries with
nullable mtime for dangling symlinks. Update ManagedFileEntry to match,
render broken links with an Unlink icon and a destructive badge, disable
download for them, and keep delete enabled so users can remove the dead
link from the UI.
2026-10-06 12:30:01 -04:00
Austin Pickett
0620913eac fix(dashboard): gate the chat WebGL renderer and fall back to DOM
The chat terminal loaded @xterm/addon-webgl for every wide host with
nothing but a constructor try/catch. Three failure classes escaped it:

- Safari's WebGL atlas garbles Unicode box-drawing glyphs (#18773)
- software rasterizers (llvmpipe/SwiftShader) advertise WebGL but regl
  throws '(regl) webgl not supported' from inside the addon (#45520)
- the per-glyph atlas cannot shape complex scripts, so Bengali
  conjuncts render broken (#58685)

Gate the addon behind a pre-flight probe (real context + non-software
unmasked renderer, released immediately) and a Safari UA check, both in
a new pure module; when shaped-script text arrives over the PTY, drop
the live addon so xterm re-renders through the DOM renderer, whose text
shaping handles conjuncts. Context loss already disposed the addon.
2026-10-06 12:26:53 -04:00
Austin Pickett
aa30e02d86 fix(dashboard): badge follows the PTY runtime model, not config, during fallback (#54509) 2026-10-06 11:44:38 -04:00
Austin Pickett
dca25fe74d refactor(web): extract terminal appearance helpers; satisfy code-health ratchet
ChatPage.tsx crossed its FILE_LINES cap and the ChatPage function its
FUNC_LINES cap once the #36784 accessibility lines landed. Move the
module-level theme/font-tier helpers to web/src/lib/terminal-appearance.ts
(the ratchet's sanctioned remedy — moved code keeps its cap) and tighten
the new comments. No behaviour change: same helpers, same values.
2026-10-06 11:44:32 -04:00
Austin Pickett
fe6eb7f976 test(web): assert composer screen-reader mode behaviourally (#36784)
Replace the static source-text assertions with behavioural tests that
render ChatPage against a capturing FakeTerminal and observe what
assistive technology observes: the Terminal is constructed with
screenReaderMode: true, the host div is a labelled role=region, and the
single registered key handler lets Tab escape (WCAG 2.1.2) while other
keys still reach the terminal. Proven RED on base, GREEN with the fix.
2026-10-06 11:44:32 -04:00
David Metcalfe
a29eb0d8e0 fix(web): fold Tab escape into existing xterm key handler (#59091 review)
Teknium's review of the original PR flagged that the new
`term.attachCustomKeyEventHandler` at line 540 silently overwrote
the clipboard handler at line 464 — xterm.js stores a single
`_customKeyEventHandler` per Terminal
(node_modules/@xterm/xterm/src/browser/CoreBrowserTerminal.ts:914,
`this._customKeyEventHandler = customKeyEventHandler;`) and assigns
overwrites later registrations, so the Tab escape branch never
executed.

The rework folds the Tab branch into the existing handler at the
top (right after the type guard) so only one handler is registered.
The ARIA region and screenReaderMode additions remain untouched.

Adds web/src/pages/ChatPage.a11y.test.ts — regression test asserting
that `screenReaderMode: true` is set, the host div carries
`role="region"` + `aria-label=`, exactly one
`term.attachCustomKeyEventHandler(` call exists, and the single
handler contains the Tab-escape branch + keydown guard. Verified the
test fails on the original PR diff (catches the regression) and
passes on the rework.

Closes inline review thread at #59091.
2026-10-06 11:44:32 -04:00
David Metcalfe
cc26049c52 fix(web): enable screen-reader mode for chat composer (#36784)
The web UI chat composer uses xterm.js, which renders to a canvas
invisible to VoiceOver/NVDA. Add screenReaderMode: true to create
the offscreen textarea screen readers need, a Tab escape handler to
prevent keyboard traps (WCAG 2.1.2), and an ARIA region label on
the host div.
2026-10-06 11:44:32 -04:00
liuhao1024
aff64c886a fix(dashboard): include BASE path in logout redirect to /login
The logout handler used a hardcoded `/login` path for the post-logout
redirect. When the dashboard is served under a URL prefix (via
X-Forwarded-Prefix), this navigates to the wrong path, breaking the
logout flow for reverse-proxied deployments.

Use `${BASE}/login` to match the prefix-aware pattern already used
by the API calls in the same file.
2026-10-06 11:17:11 -04:00
Austin Pickett
f16cbcde55 fix(web): the events feed's reconnect ladder also waits for a stable open
The sidecar now refills its redial budget only after a connection has
stayed open for the grace window; the events feed on the same component
still reset `attempt = 0` on every `open`. A /api/events socket that opens
and dies within a second therefore never reached
EVENTS_MAX_RECONNECT_ATTEMPTS — the same unbounded churn, minus the
agent boot. One shared HEALTHY_OPEN_GRACE_MS now gates both ladders; the
banner still clears on open, since a flap re-surfaces it on the next drop.

Tests: a brief open keeps the ladder on its next rung (2s, not 1s); forty
open-then-close cycles still hit the cap and surface the gave-up banner;
the existing "resets after reconnect" case now waits out the grace window.
2026-10-06 09:37:03 -04:00
Yuan Li
4f2a473244 test(web): type sidecar state unsubscribe as void 2026-10-06 09:37:03 -04:00
mario841859784
2efb8ac164 fix(web): clear the pending sidecar redial timer on open (#129393)
Stacked on #129397. Clearing the pending redialTimer on open prevents the backoff timer from closing a just-established connection. The 3 regression cases align with the 10s grace period from #129397. Tests: ChatSidebar 24/24 + ChatPage 15/15.

(cherry picked from commit ee043461a307a68ed0cbe3a5a5ffde7adfb10e7b)
2026-10-06 09:37:03 -04:00
Yuan Li
e36889194a fix(web): bound sidecar redial across flapping sockets 2026-10-06 09:37:03 -04:00
Yuan Li
8bd23cddee test(web): cover sidebar state replay 2026-10-06 09:37:03 -04:00
Yuan Li
d3c3a1ea2d fix(web): ignore replayed sidebar connection state 2026-10-06 09:37:03 -04:00
Teknium
88ebf908dd fix: dashboard update toast names a partial run's owed work, only from its own receipt
For a partial run (committed, exit 1 after record_user_action) the toast said
only "Action failed (exit 1)" and dropped the receipt's recovery instruction;
on exit 0 it named follow-ups from whatever receipt the status route attached,
which is the latest one when this action has none.

Keep the update POST's action_id and read the debt through the shared
updateDebt (committed success|partial, matching action_id only). The toast
composes it with every verdict: follow-ups keep the rerun remedy, a user
action shows the producer's own instruction verbatim (never "re-run hermes
update", which does not restore a parked stash).
2026-10-06 03:44:49 -07:00
Teknium
5d8654fecb fix: dashboard owed-update toast string exists in every web locale
`actionFinishedOwed` was added to the English catalog and the shared type only, so `tsc -b`
(what the Docker image and nix builds run) failed on the 15 locales typed against the full
shape, and Arabic fell back to English. Adds the translated string to all 16 catalogs.
2026-10-06 03:44:49 -07:00
Teknium
a9485fde6f fix: dashboard, Desktop and managed SSH clients report a committed update's owed steps
Under C3 a run whose code committed is a success (exit 0) even while a
post-commit step is owed, but the dashboard status summary and the managed SSH
observer dropped the receipt's followups/user_action, so every client outside
the local shell hand-off reported plain success. Both summary protocols now
carry followups and user_action; the Desktop backend apply and managed SSH
result keep their C3 success/restore status but name each owed step and its
remedy, and the web dashboard toast does the same. The Desktop receipt
fallback also requires the receipt's action_id to match the started action
(when the backend reports one), so another action's run cannot certify it.
2026-10-06 03:44:49 -07:00
Teknium
0808ed8ec0 refactor(desktop): shared-metrics copy moves to per-locale siblings
Offsets the code-health ratchet: the oversized locale files and types.ts
may only shrink, and the re-ask line would have grown eight of them. Same
shape as the model-menu move; web api.ts keeps its line count by
collapsing one signature.
2026-10-06 00:40:30 -07:00
Teknium
b5025017d5 feat(telemetry): ask once more where a "No thanks" may never have been seen
Before the type-ahead fix, an Enter pressed while `hermes chat` started
could save "No thanks" before the offer was on screen, and every surface
then treated the profile as answered. Every answer now also writes
`telemetry.shared_metrics.offer_version`. An "off" answer without it reads
as `reask`: undecided, so the CLI, Desktop strip and dashboard banner offer
it once more with a one-line reason. Any response settles it, including
Esc in the terminal and the banner close, which keep "No thanks", so
nobody is asked a third time. Opt-in answers were always deliberate picks
and are never re-asked.
2026-10-06 00:40:30 -07:00
Teknium
a928a959e3 feat(plugins): let plugins register Automation Blueprints via ctx
Plugins can now call ctx.register_automation_blueprint(key, *, title,
description, schedule_template, prompt_template, category, slots,
deliver_default, skills, tags). The fields mirror the built-in
AutomationBlueprint, and slots are BlueprintSlot field dicts, so the call
also crosses the plugin-host boundary.

- One lookup, cron.blueprint_catalog.list_blueprints() / get_blueprint(),
  merges the built-ins with the active profile's plugin blueprints at call
  time. /blueprint (CLI/TUI/messengers), /api/cron/blueprints and the
  instantiate route all use it. Each dashboard route resolves inside the
  ?profile= scope, off the event loop.
- Keys are namespaced <plugin>:<key>, so a plugin cannot shadow a built-in.
  A ':' in the key, a duplicate key, an unknown placeholder, a bad slot type
  or a schedule that does not parse is warned and ignored at registration.
- Catalog entries carry source/plugin. Desktop fetches the catalog for the
  profile it creates jobs in, keyed per profile, and shows the plugin name
  on the blueprint row and in the "Start from" select. The dashboard shows
  a plugin badge.
- A created job is an ordinary cron job with the prompt and schedule already
  filled in, so it keeps running if the plugin is later disabled.
- The ctx content registrars (register_skill, now joined by
  register_automation_blueprint) move to hermes_cli/plugins_content.py.
  The dashboard's blueprint types move to web/src/lib/automation-blueprints.ts.

Closes #132373. Requested by @enckequity.
2026-10-05 17:34:36 -07:00
Teknium
68420e1912 refactor: move aux-slot growth into topical siblings (code-health caps)
No behavior change. Keeps every touched file and function at or below
its value on main:

- agent/auxiliary_task_config.py: read-time `auxiliary.<task>`
  resolution (plugin defaults, inherit_from, whole-route pin); still
  re-exported as agent.auxiliary_client._get_auxiliary_task_config.
- hermes_cli/plugins_aux_tasks.py: register_auxiliary_task validation
  and entry shape.
- web/src/lib/api-aux.ts: /api/model/auxiliary wire types.
- Desktop: aux row list, label resolver, deep-link highlight, route
  summary, Follow button and the set-to-main route in
  settings/aux-task-rows.tsx; one assignAuxiliary callback replaces
  setAuxiliaryToMain. Built-in aux slot labels move from each locale
  into <locale>_aux_tasks.ts (types_aux_tasks.ts holds the shape).
2026-10-05 17:24:40 -07:00
Teknium
4013fa57d5 feat(dashboard,desktop): show which slot a plugin aux task inherits
Plugin aux rows (#40880) now carry `inherit_from` and, for inheriting
tasks, an `effective` {provider, model, base_url} resolved inside the
request's profile scope. Desktop Settings → Models and the dashboard
Auxiliary Tasks modal render an unpinned inheriting row as
"inherits Compression · <provider · model>" and offer "Follow <base>"
on a pinned one (writes provider auto, which resumes inheritance).

The dashboard test now writes plugin manifests/config as JSON (no yaml
dependency in the test env) and covers profile A/B isolation of the
same plugin slot: listing, effective route, pin persistence, follow and
base-change propagation each stay in their own profile.
2026-10-05 17:24:40 -07:00
thanhan-a17
56afae600e fix(dashboard,desktop): surface plugin-registered auxiliary tasks in Models settings
`PluginContext.register_auxiliary_task` already lists a plugin task in the `hermes model`
picker and layers its defaults into `auxiliary.<key>`, but every REST/UI surface behind the
Models page enumerated only the built-in `_AUX_TASK_SLOTS`:

- `GET /api/model/auxiliary` never listed the task
- `POST /api/model/set` (scope=auxiliary) rejected it: `400 unknown auxiliary task`
- `__reset__` and the empty-task broadcast skipped it; `_stale_aux_pins` ignored it
- Desktop Settings → Models and the dashboard ModelsPage hardcoded the built-in list

So a plugin's side model could only be pinned by hand-editing config.yaml per profile.

Fix, at the existing seams:
- `web_server_config._aux_task_slots()` = built-ins + plugin keys from
  `get_plugin_auxiliary_tasks()`, fail-soft; every consumer in that module uses it.
- `GET /api/model/auxiliary` reads plugin tasks INSIDE `_profile_scope`, so a request for
  profile B enumerates B's plugins (the plugin manager keys on the same context-local home) —
  the profile-scope gap flagged on #40922. Built-in rows are byte-identical; plugin rows are
  appended with `label`/`hint`/`plugin` from the registration.
- Desktop `model-settings.tsx` and dashboard `ModelsPage.tsx` render served plugin rows after
  the built-ins using the server label/hint (pure `auxTaskRows`; no new i18n strings, no-op on
  older backends).

Tests: real plugin loaded from a temp HERMES_HOME through PluginManager discovery, two
profiles in one process (listing scoped per profile; foreign task still 400), assignment /
broadcast / reset / stale-pin over a plugin slot, discovery failure leaves built-ins intact —
all red on main. Vitest covers the pure row merge and the rendered plugin row.

Fixes #40880. Salvages #40922.

Co-authored-by: Tranquil-Flow <tranquil_flow@protonmail.com>
2026-10-05 17:24:40 -07:00
teknium1
344a23bce5 feat(messaging): edit allowed users one ID per box on Desktop and dashboard
Allowlists (*_ALLOWED_USERS, LINE groups/rooms, SimpleX groups) rendered as a
single empty input with a redacted "«redacted:…»" placeholder, so a saved list
looked like a hidden secret and could only be replaced wholesale.

- Backend: messaging env rows carry is_list, and list rows carry their plain
  value (IDs are not secrets; password fields still only leave redacted).
- Desktop + dashboard: one visible text box per entry, x to remove, "+ Add
  another" (Enter also adds); a pasted comma list splits into boxes. Saved as
  the same comma-separated value the gateway already parses; removing every
  entry and saving clears the key.
- Desktop help copy for the allowlist fields says "one per box" in all locales.
2026-10-03 23:40:33 -07:00
xxxigm
9eaad2183a test(dashboard): pin auxiliary picker current against the main model 2026-10-02 12:26:54 -04:00
xxxigm
5146a01877 fix(dashboard): show the slot model as current in aux/MoA pickers
The shared model picker always loaded /api/model/options, which is the
main chat model. Opening Set Auxiliary: Vision therefore showed
current: glm-5.3 even after Vision was already pinned to Qwen.
2026-10-02 12:26:54 -04:00
ethernet
c2b69de66d feat(icons): drop the brand ring everywhere and frost the mono icon
Next to macOS 26's own icons the flat plate + girl reads better than the
ringed tile, so the ring is off in every output: the tiles' inward stroke,
the layered icon's border layer and its targets. BORDER_ENABLED keeps the
ring code one flag away.

Without a ring the girl's lowest nodes are dragged past the tile edge
(EDGE_OVERSHOOT) so the outline crops her instead of leaving a gap.

Clear light/dark and Tinted light/dark now come from one grayscale mono
layer with Liquid Glass on: the parts of the girl that are black in light
mode become a near-black frosted ink (0.13 @ 90%), her white parts stay
white, each pixel painted exactly once so materials never stack. The art
layer hides under "tinted" and the mono layer shows only there.

Mac girl at 1.06x (between main's 1.12x and the layers' 1.0x), for the icns
and the layered icon alike. Chosen from a six-appearance matrix of real
IconServices renders next to Finder.
2026-10-02 10:33:44 -04:00
StanleyStetson
cf6a457e2d fix(desktop): preserve profile scope during session rename
When renaming a session from the ChatHeader in Desktop, the session's
owning profile was omitted from SessionActionsMenu, causing rename calls
to omit profile parameters and fall back to the default profile database.
On non-default profiles (e.g. personal), this caused a 404 from the server
while the desktop UI optimistically displayed the new name locally.

Pass the active session's profile from ChatHeader to SessionActionsMenu,
resolve the session profile from the $sessions store when omitted in
renameSessionPreferringRpc, ensure REST renameSession falls back to
profileScoped, and propagate profile in web search and sessions page rename.

Fixes #103903
2026-10-01 00:19:39 -05:00
teknium1
7a06cb50d7 feat(telemetry): shared-metrics opt-in offer on every surface
One answer per profile, asked once, with the Desktop strip's three equal
choices (Send to Nous / Local only / No thanks) now offered by:

- `hermes setup`: at the end of every flow (Quick, Full, Blank Slate,
  Portal, --quick), via the setup-completed record they all funnel into.
- `hermes` / `hermes --tui`: once before an interactive chat on a profile
  that never answered. Skipped for -q, piped/JSON output, spawned actions
  (HERMES_NONINTERACTIVE), Desktop-hosted panes and managed installs.
- Web dashboard: a banner for the profile being managed, backed by
  GET/PUT /api/shared-metrics/consent.

"No thanks" is the terminal default so Enter never opts anyone in; Esc and
the banner's X leave the question open.

Also fixes the answer that could not stick: a CLI "no" equals the shipped
defaults, so save_config stripped it and every surface (Desktop strip
included) kept asking. The answer is now written to config.yaml directly,
and `decided` has one definition shared by the RPC, the CLI and the router.
2026-09-30 21:15:08 -07:00
zhengpeng
3cf2eb1c94 test(web): run the dashboard test project in development mode
React 19 only ships `act` in its development build. The jsdom suites in
`web/` drive components through `act()`, so when vitest inherits an
ambient `NODE_ENV=production` — the shell that launched the app, a
packaged CI step, an editor terminal — `React.act` resolves to undefined
and every `createRoot` test in the project fails with
`TypeError: act is not a function`.

Pin the test worker to `NODE_ENV=development` via the project's
`test.env` so the suite no longer depends on the ambient environment.
App runtime and production builds are unaffected; this only applies to
the vitest worker.

Same class of failure as #77409 (and the same fix shape as #77421), one
workspace over: that issue and PR cover `apps/desktop/vitest.config.ts`,
while `web/vitest.config.ts` has no equivalent guard.

Before (clean main, NODE_ENV=production):
  Test Files  3 failed | 33 passed (36)
  Tests      43 failed | 232 passed (275)

After:
  Test Files  36 passed (36)
  Tests      275 passed (275)
2026-09-28 21:54:03 -05:00
liuhao1024
5e1d5f2f4e fix(web): drop the test block accidentally nested inside the stalled-URL test
The gave-up-banner test was pasted twice when the budget-survival commit
(a5e0c9e5c0) was assembled: one copy at describe scope, and a second copy
inside the body of 'times out a stalled URL request and retries'. Vitest
rejects calling it() inside a test ('Calling the test function inside
another test function is not allowed'), which is exactly what failed the
JS & TS checks run on a5e0c9e5c0. Remove the nested copy; the describe-
scope copy and the stalled-URL test are unchanged and pass 20/20.
2026-09-28 21:29:02 -05:00
liuhao1024
121e557ed8 fix(web): make the sidecar redial budget survive client rebuilds (#95951)
The attempt counter lived in the [gw] effect closure, so every redial —
which bumps version and re-runs the effect — reset it to zero and the
five-attempt budget never exhausted. The counter and the gave-up flag
now live in component refs (reset on a successful open and on scope
switches), and exhausting the budget surfaces a one-time banner
('gateway sidecar disconnected — gave up after 5 attempts, use
Reconnect'), mirroring the events feed's give-up contract; the banner
clears on the next successful open.
2026-09-28 21:29:02 -05:00
liuhao1024
d04c351dff fix(web): redial after clean 1012 service-restart closes (#95951)
A dashboard service restart closes both web sockets with 1012 (Service
Restart) — a CLEAN close. The PTY pane's reconnect guard only retried
dirty closes and 1001/1006, so 1012 fell through to the '[session
ended]' path and the pane stranded until a manual relaunch (server logs
in the issue show a 23-minute dead window). The JSON-RPC sidecar had no
redial owner at all: GatewayClient deliberately delegates reconnect
policy upward, but ChatSidebar only dialed once.

- ChatPage: the transient-drop guard now also retries 1012 and 1013 —
  both mean 'the server is coming right back', so the same ?attach=
  token reattaches to the still-living PTY.
- ChatSidebar: the sidecar gets a redial owner with the same bounded
  backoff the PTY pane uses (250ms * 2^n capped at 3s, 5 attempts,
  reset on open, cancelled on unmount/scope switch; the manual
  Reconnect affordance stays the fallback after the cap).

Reported recovery goes from a manual app relaunch (~23 min stranded in
the log sample) to an automatic redial ~6.5s after the service returns.
2026-09-28 21:29:02 -05:00
ipriyaaanshu
064d16981b fix(dashboard): a named-profile dashboard keeps its own profile instead of the sticky active one (#96712, salvage #96736)
shouldAdoptActiveProfile() aligned the switcher to the machine-global active_profile
whenever it differed from the serving profile, so a dashboard hosted by a named profile
(hermes -p demo serve) silently retargeted its banner, chat TUI env and model to another
agent on every load. Only the default-hosted machine dashboard aligns now; "custom"
(unnamed HERMES_HOME) never does.

Co-authored-by: ipriyaaanshu <ipriyaaanshu@users.noreply.github.com>
2026-09-28 12:18:58 -07:00