Desktop had no way to read or change the shared-metrics opt-ins. The new
profile-scoped RPCs read/write telemetry.shared_metrics.{enabled,send} in the
focused profile's config.yaml through the one config writer, keep the setup
wizard's invariant (send is forced off unless collection is on) and reconcile
the consent windows on every change via the wizard's own helper. first_run
answers also record the desktop setup-completed metric (lazy import until the
events module lands).
Issue #63125 asks for the cached vs uncached read rate at a glance in
the model picker. The backend already ships it: _apply_pricing fills
ModelPricing.cache from input_cache_read, and the CLI picker renders it
as a third column — the catalog menu was the only surface dropping it.
- ModelPrice appends the cached rate (dim, `·$0.16`) next to the
uncached input/output pair; its hover title names it, and the row's
tooltip gains the cached-read line. Uncached read stays the inline
`input` rate, so the cached-vs-uncached comparison reads at a glance.
- The hardcoded "free" label and English-only tooltip move to
shell.modelMenu copy (free / cacheRead / priceTitle) so the row
localizes; fr/de/es are completeness-checked, so all locales that
carry the modelMenu block get the new keys.
- A collapsed `-fast` family now falls back to the fast sibling's price
when the base id is unpriced (the sharing the comment already
claimed — review point on #84831).
Refs #63125
Review follow-ups on #84831:
- Treat null input/output as unknown: render nothing rather than
"null/null" when a provider reports a non-free model without rates
(_apply_pricing ships "" for unknown, but the wire type is
string | null, so guard both).
- Only show the sale tag for a positive discount_percent, so a negative
value can't render as a double-minus.
- Fallback title uses "—" for a missing half instead of "undefined".
- New test: no price span at all when the provider carries no pricing
for the model.
The model.options payload has carried per-model ModelPricing for providers
that support live pricing (Nous Portal included) since the picker requested
it, but the catalog menu never displayed it. Add a compact price label to
each model row: input/output $/Mtok, a green "free" label for free-tier
models, and a sale tag when the portal reports a discounted list price.
Verified against a live payload probe: the nous provider ships 35 pricing
entries (e.g. anthropic/claude-sonnet-5 -> $1.60/$8.00, 20% discount).
fr/de/es overlays are completeness-checked against the en key set, so the
new updates.copyFullLog key must ship there or catalog-completeness fails;
ar/ru carry the neighbouring moreChanges key, so they translate too.
Swap the hand-rolled copied-state button for the shared CopyButton
primitive (haptics, aria-label, error path, unmount cleanup included).
The update overlay previously showed at most 6 commit subjects from the
changelog, with no way to access the full list or copy it for translation.
Changes:
- Increase git log limit from 40 to all commits (remove -n 40)
- Add 'Copy full changelog' button that copies all commits as formatted
text (type(scope): subject - author) to clipboard
- Use parseCommitHeader to reconstruct full convention commit format
including scope and author
- Add i18n keys: en 'Copy full changelog', zh '复制完整更新日志'
Closes#48654
The first-pick bug was the Select's controlled value already being the
Custom sentinel, which the value swap fixes on its own; the Input's
autoFocus handles focus once it mounts, so the rAF refocus effect is not
needed. Extend the regression test to type into the field and to check
that clearing it and blurring returns to the dropdown.
Live settle addresses a folded tool turn by its final row, hydration by its
first, so after a tool-using turn the tips never matched and the first send
bounced with "Chat out of date" in a single window.
* refactor(plugins): remove the Sep 2026 decomposition compat layer on schedule
The PLUGIN-COMPAT layer (2776813df3 + d63e380324 + 0a5164cebe) kept pre-#102117 import paths
alive for external plugins until 2026-09-14. That window closed two weeks ago; since then the loader
has already been skipping plugins that use the old paths. This removes the layer itself:
- 328 appended `PLUGIN-COMPAT` blocks (lazy `__getattr__` pointer tables, re-exported third-party
names, restored dead definitions) and the three re-export stub modules
(gateway/startup_watchdog, hermes_cli/observability/relay_runtime, tools/environments/modal_utils)
- COMPAT_MANIFEST.md, compat_manifest.json, scripts/check_compat_pointers.py and its lint step
- the reporting surfaces: CLI banner notice, `hermes plugins compat`, the `hermes doctor` section,
the post-update notice, the Desktop one-time dialog, the loader's pre-import skip and the
`plugins.allow_deprecated_imports` escape hatch
An external plugin that still imports an old path now fails to load with its ImportError as the
reason in `hermes plugins list`, the same path as any broken plugin.
hermes_cli/plugin_compat.py stays as three inert stubs (compat_report, removal_in_effect,
summary_lines): an already-running pre-removal `hermes update` lazy-imports them after the checkout
swap (tests/compat/old_updater_surface.json).
In-tree fallout, both already dead: hermes_cli/setup.py::_check_espeak_ng (no callers; its
`shutil` came from a compat block) and gateway/config.py::SessionResetPolicy ("retained solely for
the scheduled plugin-compat window"). Two test_run_agent patches targeted the removed
`run_agent.handle_function_call` pointer; they now patch `model_tools.handle_function_call`, the
seam production reads, like every sibling test in that file.
* chore: retrigger CI (zero-job startup_failure phantom)
* test: drop resolution allowlist rows for the two deleted which() sites
hermes_cli/setup.py::_check_espeak_ng (dead) and tools/skillevaluator_scan.py::scanner_available
(a restored definition inside a PLUGIN-COMPAT block) no longer exist; the stale-row gate requires
their allowlist entries go with them.
The banner was text-xs amber-200 — easy to miss in dark mode and near
illegible in light mode. It now uses the app's warn-badge contrast
pair (text-amber-600 dark:text-amber-300) at text-sm with a size-4
icon, stronger border/background, and medium weight, so it reads at
a glance in both themes. The persistent variant gains a dismiss button
that records the fingerprint acknowledgement; the post-switch notice
stays undismissable because it announces a change that just happened.
Co-authored-by: kyssta-exe <25470058+kyssta-exe@users.noreply.github.com>
Co-authored-by: Konstantin Khlopkov <47825603+kokhlo@users.noreply.github.com>
A dismissal is bound to the exact pin configuration it acknowledged —
main provider plus every slot's task/provider/model/base_url — stored
per profile. Any slot edit, main switch, or endpoint repoint changes
the fingerprint and re-arms the banner, so the silent-credit-burn
protection is never lost. base_url is normalized (trimmed, trailing
slashes stripped) so equivalent endpoints share a fingerprint, and
StaleAuxAssignment grows an optional base_url the persistent banner's
mapping now carries (switch echoes still omit it).
Co-authored-by: Konstantin Khlopkov <47825603+kokhlo@users.noreply.github.com>
The queue panel showed each queued prompt as a single truncated line;
reading a long entry required loading it back into the composer. Previews
now wrap across two lines, and long or multiline entries get an in-place
expand/collapse toggle (aria-expanded chevron) that reveals a scrollable
full-text view. Edit/send/delete actions are unchanged.
Adds a QueuePanel test covering toggle presence (long/multiline only) and
the expand/collapse round-trip. Copy added in every locale that carries
the neighbouring queue keys.
Original work by YonganZhang in
https://github.com/NousResearch/hermes-agent/pull/46007, rebased onto the
current TypeScript sources (Codicon + iconSize tokens, steer/resume
actions preserved).
Co-authored-by: Yongan Zhang <374456248@qq.com>
Fixes https://github.com/NousResearch/hermes-agent/issues/45664
The composer model pill capped its width at max-w-40 (160px), so long
model names with session-state suffixes ("DeepSeek V4 Flash · Med")
truncated with an ellipsis even when the row had room. Drop the cap: the
pill sizes to its label, and the existing shrink + min-w-0 + truncate
squeeze path still yields width between collapse stages, so long names
only clip when the row is genuinely out of space.
Extends the model-pill test to lock the unbounded-width behaviour.
Original idea by @Aio777 in
https://github.com/NousResearch/hermes-agent/pull/49341 (raised the cap;
this removes it as requested by the issue).
Co-authored-by: aryan <alokesh1@sheffield.ac.uk>
Fixes https://github.com/NousResearch/hermes-agent/issues/49340
LogTail gains in-place search: matches are highlighted, the active one is
scrolled into view, and a "3/6" counter with up/down (Enter, Shift+Enter,
Cmd+G) steps through them, reusing the find bar's key handling and labels.
The Command Center system log and the MCP log pane share it, so there is one
log component instead of a Command Center fork.
Clearing a search re-engages follow-the-tail instead of leaving the pane
parked where the last match was.
WARNING/ERROR/CRITICAL lines get a thin gutter mark, read from the level
column of the log format, so an INFO line mentioning ERROR is not flagged.
Command Center sections use the same breadcrumb header as Settings.
Co-authored-by: Sonnenwerk <a.majuskel@gmail.com>
The reasoning chips were a raw span re-rolling Badge, the add-custom row a
raw button, the refresh icon an animate-spin class, and Reset to defaults
re-set its own color.
A status filter sits beside the Messaging search, with a tab only for tones
some platform is in (errors, needs attention, connected, inactive). Picking a
tab moves the detail pane into the filter; a platform that leaves the filter
because you just toggled it stays open.
Messaging and the gateway menu now read platform state through one
classifier, so the same bot can no longer show two different tones.
Co-authored-by: Sonnenwerk <a.majuskel@gmail.com>
A message.interim redelivered after the first copy sealed the stream took the
standalone-append path and painted the same commentary twice. Refresh the
newest same-text assistant row of the current turn in place instead,
comparing whitespace-normalized text.
Supersedes #120145 and #93962.
Co-authored-by: Brammiebram <64777864+Brammiebram@users.noreply.github.com>
'data-drop-hover' is a generic token any component could style; rename to
'data-session-drop-hover' to match the session-drag module that owns it
(same attr constant in session-drag.ts) so no foreign CSS can hit-test it.
Moves to the dropped project row, and stays a no-op over Home and the
session's own current project — the same exclusions the row's existing
"Move to project" menu applies.
Extends the existing pointer-based session drag (session-drag.ts) with a
new resolvable target: a sidebar project row (tagged data-sessions-project
by ProjectOverviewRow). Dropping a session there calls the same
moveSessionToProject RPC the row's own "Move to project" context-menu item
already uses, so this is a faster path onto an existing capability rather
than a new one. Home and the session's current project are excluded as
targets, matching the context-menu's own filtering. The hovered project
row is highlighted imperatively (a data-drop-hover attribute + an
outline-sidebar-ring style) to keep the sidebar out of React re-render
churn during pointer travel.
2ffa4977ba made combo.ts import keybindActionAllowedInEditableTarget
from actions.ts, while actions.ts already imported IS_MAC from
combo.ts. When actions.ts is evaluated first, its KEYBIND_ACTIONS
table reads IS_MAC before combo.ts has initialized it. The dev
renderer throws "Cannot access 'IS_MAC' before initialization" and
#root stays empty.
actions.ts now reads the platform from the leaf module
@/lib/platform (isMacPlatform), which chords.ts and other keybind
code already use. actions.ts no longer imports combo.ts, so the
cycle is gone. The three defaults resolve at module load exactly as
before.
`hermes logs --since` and `--level` passed every line that had no leading
timestamp. A traceback's frames are written without one, so an old error
printed its frames without the header that was filtered out, and
`--component` dropped the frames of a matching record. `hermes logs` now
reads each unstamped line as part of the record above it: the line gets
that record's verdict for every filter, in the tail read and in `-f`.
Lines before the first stamp in the read window have an unknown time and
level, so they are dropped when `--since` or `--level` is set.
mcp-stderr.log had no parseable stamp at all: the banner started with
`=====` and server output was copied raw, so `hermes logs mcp --since`
printed the whole file. The stderr tee already reads each server's
stderr in a thread, so it now writes one line at a time, each prefixed
with the asctime-shaped local stamp the Python logs use. The banner
starts with the same stamp. The stamp comes from new public
`timestamp()`/`stamp_line()` in hermes_cli/stderr_timestamp.py, which
stays stdlib-only. The desktop MCP log view accepts both banner shapes.
A test now requires a real writer's sample line for every LOG_FILES
entry to parse with `_parse_line_timestamp`. The docs no longer say the
`timezone` key changes log timestamps; log lines use the machine's
local time.
desktop.log stamped each line with an ISO-8601 UTC time in brackets
(`[2026-09-28T07:48:19.466Z]`), while agent.log, gui.log and errors.log use
Python's local-time asctime (`2026-09-28 13:18:46,062`). Reading one
incident across logs/ meant converting clocks by hand, and
`hermes logs desktop --since 1h` returned every desktop line, because its
timestamp regex expects the Python shape at the start of the line and
lines it cannot parse pass the filter.
formatLogStamp() now emits the Python shape in local time, and
formatDesktopLogLine() puts it first: `2026-09-28 13:18:46,062 [hermes] ...`.
The #84405 comment already said the stamp matched the Python convention;
now it does.
Keep the (connectionId, profile) tile keying and its two invariant tests;
drop the post-switch remembered-route re-navigation (separate scope, hook
latch juggling) and the two extra delete-path tests.