Commit Graph
2662 Commits
Author SHA1 Message Date
Rod Boev ca5f02bbff feat(#5759): add composer focus shortcut 2026-07-14 01:06:46 +00:00
sheldon-im d7c2cc2d5d Restore focus from non-button session action menus 2026-07-13 19:24:09 +00:00
sheldon-im bc85711af2 Improve conversation actions menu keyboard access 2026-07-13 19:24:09 +00:00
nesquena-hermes 4016130c24 Merge branch 'rebase-6024' into stage/r2-batch 2026-07-13 19:04:13 +00:00
Loukky d1aa5945b6 Update i18n.js 2026-07-13 18:57:49 +00:00
Loukky 6ccf9168f2 [locale]Localization for Simplified Chinese
Translate all untranslated English strings into Simplified Chinese.
2026-07-13 18:57:49 +00:00
Rod Boev 3ada146d48 fix(#5989): keep colon-suffixed proxy models distinct 2026-07-13 18:57:15 +00:00
Rod Boev ff89f2ebd6 fix(#5989): keep cross-provider live rows selectable 2026-07-13 18:57:15 +00:00
Rod Boev 58725d5de9 fix(#5989): separate picker dedup from restore matching 2026-07-13 18:57:15 +00:00
Rod Boev 052effcfc4 fix(#5989): deduplicate proxy and catalog model rows in the picker 2026-07-13 18:57:15 +00:00
silent-reader-cn 946f7dbc2b fix(media): preserve scanner prose ownership 2026-07-13 07:41:13 +00:00
silent-reader-cn e13a75b7b1 test(media): cover fade MEDIA streaming splits 2026-07-13 07:41:13 +00:00
silent-reader-cn 3b19660235 fix(media): handle fade MEDIA prefix splits 2026-07-13 07:41:13 +00:00
silent-reader-cn a17370ad4a fix(media): harden streamed MEDIA rendering 2026-07-13 07:41:13 +00:00
silent-reader-cn 76940e821d fix: flush smd media tail on stream end 2026-07-13 07:41:13 +00:00
silent-reader-cn 547c218ddf fix: clear fallback smd media tail 2026-07-13 07:41:13 +00:00
silent-reader-cn ca3a1ac04d fix: accept extensionless streaming media urls 2026-07-13 07:41:13 +00:00
silent-reader-cn 13b7080bf0 fix: align smd media boundary extensions 2026-07-13 07:41:13 +00:00
silent-reader-cn 5359b70c6a fix: harden smd media split-tail streaming 2026-07-13 07:41:12 +00:00
silent-reader-cn d2a696d09a fix(webui): resolve Greptile #3 — WeakMap string key crash + unused _smdMediaTailClear
Greptile re-review (3/5) flagged two issues:

1. parserFor fallback ('__default__') string key → WeakMap.set throws
   TypeError (WeakMap accepts only object keys). Fixed by introducing a
   module-level sentinel object __SMD_PARSER_FALLBACK = {} and using it
   in both parserFor definitions instead of the string literal.

2. _smdMediaTailClear was defined but never called despite the comment
   claiming it was. Fixed by adding calls in:
   - _smdEndParser → clears the ongoing live-stream parser's tail
   - _clearAnchorProseIncrementalNode → clears each anchor-scene
     parser's tail before the cache is dropped

All 222 tests remain green.
2026-07-13 07:41:12 +00:00
silent-reader-cn 0b14ab54e3 fix(webui): split prose/MEDIA paths in streaming interceptor + per-parser tail buffer
Code-level fixes for the two issues Greptile + owner review surfaced:

1. SAFETY (Greptile #1 / owner security regression): the original
   _smdMediaAwareAddText concatenated prose slices + MEDIA HTML into
   one string and handed it to DOMParser. Prose containing HTML
   metacharacters (e.g. <img onerror=...>) would be parsed as live
   elements. Fixed by routing each prose run through baseAddText
   (which calls createTextNode) and only feeding each single MEDIA
   token's trusted _inlineMediaHtmlForRef output through DOMParser.

2. CROSS-CHUNK SPLITS (Greptile #2): smd may deliver a MEDIA: token
   across two flushes (e.g. "MEDIA:C:\...\" then "foo.png"). The
   original implementation would treat each half as separate text
   because neither half terminated cleanly. Fixed with a per-parser
   tail buffer (WeakMap keyed by parser instance) that holds trailing
   bytes ending in "MEDIA:" or MEDIA:<partial-ref> until the next call.

   The tail-buffer boundary detection was also improved: the previous
   `/[\s\)\]]/.test(rest)` incorrectly rejected "before MEDIA:" (the
   whitespace is before MEDIA, not after a completed ref). Replaced
   with `rest.endsWith('MEDIA:')` and `/MEDIA:...$/.exec(rest)`.

Architecture: _smdAppendMediaNode is a new single-token DOM insertion
helper that creates a DocumentFragment from one _inlineMediaHtmlForRef
output. _smdMediaTailSet/_smdMediaTailClear manage the per-parser
buffer. Both _safeSmdRenderer and _streamFadeRenderer now route through
the updated interceptor with tail map and parser identity.

Coverage: test_smd_media_in_stream.py tests the architectural invariants
(helper exists, both renderers delegate, recursion guard, entity-safety,
cross-chunk tail). 222 tests pass across all renderer/behaviour suites.
2026-07-13 07:41:12 +00:00
silent-reader-cn 6950540ed5 fix(webui): render MEDIA: tokens inline during smd streaming
The full renderMd() pipeline maps MEDIA:<path-or-url> tokens to inline
images / video / audio / SVG / PDF / etc. via stash + restore passes.
The streaming smd renderer used by the live assistant body never went
through that pass, so any MEDIA token emitted mid-turn showed up as the
raw path string in the chat window and only became a real <img> once
the turn settled and the full render fired.

Fix:
- Extract the URL/media → HTML mapping in renderMd() into a shared
  helper _inlineMediaHtmlForRef(ref, sessionId) so the streaming path
  can reuse the same DOM the settled message will eventually render.
- Add _smdMediaAwareAddText in messages.js that wraps smd's add_text
  to detect MEDIA tokens in any incoming text chunk and splice the
  resulting <img>/<video>/<a>/<div data-path=...> node directly into
  the smd parent via a DOMParser-created fragment. Plain prose still
  delegates to the renderer it intercepted (so the fade renderer keeps
  its word-by-word animation, the safe renderer keeps its O(n) plain
  text-node insertion).
- Route _safeSmdRenderer.add_text and _streamFadeRenderer.add_text
  through the interceptor. Each fast-paths the no-MEDIA case to the
  original baseAddText so we can't recurse into our own wrapper.

Coverage:
- New test_smd_media_in_stream.py locks in the architectural invariants
  (helper exists, both renderers delegate to it, recursion guard).
- test_renderer_js_behaviour.py driver now provides a minimal
  _inlineMediaHtmlForRef stand-in so renderMd can be invoked standalone
  under node without the rest of ui.js in scope (matches the patterns
  renderMd already relied on for _IMAGE_EXTS / _SVG_EXTS / _AUDIO_EXTS).
- test_svg_audio_video_rendering.py static-count assertions relax from
  ">=2" to ">=1" since audio/video/SVG markup now lives in a single
  shared function rather than being copy-pasted inside renderMd's
  stash-restore closure.
2026-07-13 07:41:12 +00:00
kertisalex 90ee9484e0 fix: also block fork on pending user message during streaming
Extends the live-message guard to reject both _live assistant rows and
_pending user rows that haven't been committed server-side yet. Adds a
belt-and-suspenders check: when S.busy is true, the last message in
S.messages is also blocked regardless of flags, closing a timing window
where _live might be momentarily absent on the streaming row.

The user's optimistically-pushed message (messages.js:1603) now carries
_pending:true so forkFromMessage can distinguish it from committed
messages.
2026-07-13 02:25:30 +00:00
kertisalex 5023053ebf fix: allow fork from here during active streaming
Removes the blanket S.busy guard from forkFromMessage() that silently
swallowed all fork clicks while the agent was generating a response.
Instead, only blocks fork when the clicked message itself is the one
currently being streamed (msg._live), showing a clear toast.

Skips _ensureAllMessagesLoaded() during streaming to avoid visual
flicker — absoluteKeepCount is captured before any async work so the
fork data is unaffected.
2026-07-13 02:25:30 +00:00
Jaret Bottoms 30e32cb11e fix(cron): restrict reverse root-alias match to proven root names
The round-3 equivalence shortcut accepted any origin when the active
profile was literal 'default', so another profile's cron marker was
treated as active-owned and never cleared on switch (reverse
under-clear). Match now requires the origin name itself to resolve to
the root profile: the literal 'default' alias, or a roster entry
flagged is_default. Unknown names fall back to exact-name semantics.

Adds profile-a -> default regressions covering tagged and
legacy-untagged old-profile markers (cleared) alongside current-root,
renamed-root-alias, and non-cron markers (kept), plus a no-roster
fail-closed case.
2026-07-12 19:20:53 +00:00
Jaret Bottoms 617c1f451d fix(cron): drop stale pre-switch session-list completion marks
Greptile #5975 P1: capture _cronPollGeneration before /api/sessions await and
re-check in _applySessionListPayload before completion-unread marking so a
delayed profile-A response cannot remount cron dots after switch to B.
Also skip inactive-profile cron marks in single-profile mode. Tests cover the
race, positive path, and collateral call-shape contracts.
2026-07-12 19:20:53 +00:00
Jaret Bottoms 4c5a21cef3 fix(cron): close untagged persisted unread leak + root-alias over-clear
Address hermes-webui #5975 re-gate: tag session-list completion markers for
cron rows, resolve/migrate legacy untagged markers from sidebar metadata, and
use default/renamed-root profile equivalence when clearing inactive-profile
cron dots so current-root markers stay unread.
2026-07-12 19:20:53 +00:00
Jaret Bottoms 991b884ed2 fix(cron): clear persisted unread markers on profile switch
Gate review on #5975: sticky all-profile sidebars still showed old-profile
cron dots because hermes-session-completion-unread was not profile-scoped.
Tag cron markers with source+profile, drop inactive-profile cron markers in
the shared switch reset (both paths), and leave ordinary chat unread alone.
2026-07-12 19:20:53 +00:00
Jaret Bottoms a8ef99b830 no-mistakes(review): Reset cron unread state on alternate profile switches 🔧🟠 2026-07-12 19:20:53 +00:00
Jaret Bottoms 67e527fd74 fix(cron): guard profile-switch polling races 2026-07-12 19:20:53 +00:00
Jaret Bottoms 1a64fc68dd fix(cron): scope unread badges to active profile 2026-07-12 19:20:53 +00:00
f8546915ab Release exp-v0.52.55: localize manual Docker update guidance (#5973, #5959) (#5985)
* fix(i18n): localize manual Docker update guidance

* Release exp-v0.52.55: localize manual Docker update guidance (#5973, #5959)

---------

Co-authored-by: KaliShodan <25535214+jbbottoms@users.noreply.github.com>
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
2026-07-11 23:53:02 -07:00
nesquena-hermesandnesquena-hermes 7d94b1c890 Release exp-v0.52.52: keep transcript pinned across composer auto-resize (#5962, #5514) (#5983)
* fix(chat): keep transcript pinned across composer auto-resize (#5514)

Reported by @JeffArcLogic.

autoResize() sets the textarea to height:'auto' before measuring scrollHeight
and restoring the height. The transient collapse grows the flex:1 #messages
viewport and the browser clamps a bottom-anchored scrollTop down by the collapse
delta, stranding the reader a few px above the bottom on every keystroke while
the composer is multi-row (delta proportional to composer height) — and the
clamp's async scroll event sticky-unpins the reader, dead-ending the #5516
grow-path re-pin and stream auto-follow. The prior #5516 fix only re-pinned on
NET growth, so a steady-state keystroke was missed.

Fix: snapshot #messages.scrollTop before the height round-trip and restore it
after, undoing the transient clamp within the same synchronous task. Protects
pinned AND near-bottom scrolled-up readers, takes no _programmaticScroll latch,
inert to iOS toolbar reflows. The grow-gated re-pin + ResizeObserver remain for
genuine net growth.

* test(#5514): make near-bottom-unpinned case non-vacuous (Codex gate nit)

The companion unpinned case used scrollTop=7900, below the transient clamp max
(7903), so it never actually clamped and passed on master too. Move it to 7990
(inside the clamp zone: 7903 < 7990 < settled-bottom 8023) so it genuinely
exercises the transient-clamp yank the fix prevents.

* Release exp-v0.52.52: keep transcript pinned across composer auto-resize (#5962, #5514)

---------

Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
2026-07-11 22:37:23 -07:00
3722f2a743 Release exp-v0.52.51: refresh reasoning chip after empty boot (#5967, #5954) (#5982)
* fix(#5954): refresh the reasoning chip after empty boot hydration

* test(#5954): resolve the boot harness path from the test file

* Release exp-v0.52.51: refresh reasoning chip after empty boot hydration (#5967, #5954)

---------

Co-authored-by: Rod Boev <rod.boev@gmail.com>
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
2026-07-11 22:17:11 -07:00
9643cdd5c1 Release exp-v0.52.50: transparent-stream lazy settled DOM + per-turn cap (#5974, #5966) (#5981)
* fix(transparent-stream): defer settled tool-detail DOM + cap per-turn rows (#5966)

Reported by @curtisszmania-cytocync.

Transparent Stream built a DOM node for every activity event of every settled
turn at load, and eagerly materialized each tool row's full detail body — a
15+ prompt reasoning-heavy history produced thousands of subtrees and tipped the
tab into a multi-GB freeze. The #5860/#5839 lazy-worklog fix did not cover
Transparent Stream (it never collapses).

Two-part surgical fix in static/ui.js, mirroring #5860's proven machinery:
- Row-detail deferral: a settled+collapsed transparent tool row renders
  header-only; its heavy .tool-card-detail body builds on first expand
  (_materializeTransparentToolDetail), recovering the tool call from the scene
  after an innerHTML cache round-trip. Same anchor-suppressed post-processing as
  the eager path, so an expanded row is byte-identical.
- Per-turn row cap: a settled turn with >CAP+SLACK (30+10) rows renders the last
  30 behind a clean 'Show earlier steps (N)' affordance that materializes the
  prefix in place with viewport compensation. Just-settled turn (keep-open token)
  and already-revealed turns are exempt; true Trace tool-count is stashed so the
  label stays honest; Expand-all reveals capped rows; cache-restore re-wires the
  affordance. Live rendering is untouched.

Server-side #5858 (scene serialize-after-messages) already covers the container
RSS half on the experimental channel; this is the browser-DOM half.

* style(#5966): compact 'Show earlier steps' pill (hug content, left-aligned)

Constrain the affordance to fit-content width + align-self:flex-start so it
reads as a light inline control on the activity rail, not a full-width bar.

* fix(#5966): address Codex gate findings + Fable UX fast-follows

Codex SHIP-ONLY-WITH-FIXES (3 SILENT defects), all fixed + browser-verified:
- F1: buildToolCard PRE-BUILDS .tool-card-detail when a tool has args/output, so
  the old `!detail` defer guard skipped exactly the heavy rows. Defer now fires on
  settled+collapsed+has-detail and STRIPS any prebuilt body (verified: 400 shell
  tool rows -> 0 detail bodies mounted; expand still materializes byte-identically).
- F2: stamp owner rawIdx on rows + affordance; dataset recovery and cache-restore
  rebind resolve the scene-owning message (multi-segment turns own it on a later
  segment, not the first) with a scene-owning-segment fallback.
- F3: persist revealed-turn state in a session/owner-keyed Set + invalidate the
  session HTML cache on reveal, so a rebuild/switch-away never re-caps a turn the
  user expanded (verified: reveal 30->400, full re-render stays 400).

Fable SHIP-UX fast-follows: t()-with-fallback label (+ en locale keys), 640px
touch breakpoint, stale comment fixed.

Tests: 21 pass (added has-detail deferral+strip, detail-less no-defer, owner-idx
recovery, persistent-reveal, i18n).

* fix(#5966): address Codex re-gate findings (r2)

Codex re-gate SHIP-ONLY-WITH-FIXES — 2 more SILENT defects, both fixed + browser-verified:
- F1(r2): _materializeTransparentToolDetail rebuilt via the thinner
  _transparentToolDetailHtml(), dropping buildToolCard's richer detail (diff
  coloring / show-more / canonical shell-command lead). Now transplants
  buildToolCard(tc)'s own .tool-card-detail node so an expanded deferred row is
  byte-identical to the eager path (verified: canonical structure + 595-char body).
- F2(r2): after an in-session renderMessages() rebuild (next send) an already-open
  settled tool row got re-deferred, and disclosure-restore only toggled .open →
  open-but-empty card. _setWorklogDetailDisclosureOpen now materializes a deferred
  transparent row BEFORE toggling open (verified: expand → rebuild → still open
  WITH content). Likely the same defect the RegressionGate caught intermittently.

Tests: 22 pass (added canonical-detail + disclosure-restore-materialize).

* fix(review): add show_earlier_steps i18n keys to all locales (#5966)

The transparent-stream row-cap "Show N earlier steps" pill added two new
i18n keys (show_earlier_steps, show_earlier_step_one) to the English block
only, so the strict locale-parity tests (zh/ko/ru/zh-Hant/...) failed with
missing-key assertions and CI shard 0 went red across 3.11/3.12/3.13.

Add both keys to all 14 non-English locale blocks with the English string as
fallback (the repo's established add-with-English-fallback convention for new
UI keys). Inserted after each block's collapse_all anchor. All 197 locale
tests + the #5966 suite green; node --check clean.

* fix(review): extract _transparentToolRowHasDetail into the #5700 harness

#5966 added a _transparentToolRowHasDetail() call into the settled transparent
render (_anchorSceneTransparentNodeForRow) to decide whether to defer a
collapsed tool row's detail body. The pre-existing #5700 timestamp test extracts
that render fn into a node harness but didn't extract the new helper, so the
extracted fn hit a ReferenceError → node exited non-zero → 3 #5700 tests failed
(RED across CI shards; the PR updated its own #5966 harness but missed this one).

Add `eval(extractFunc('_transparentToolRowHasDetail'))` to the harness. The
helper self-guards its _toolActionKind/_toolCardAllowsDetail deps (returns true
when absent), and the #5700 timestamp lives on the row HEADER (rendered even when
detail is deferred), so all 20 #5700 assertions pass unchanged — confirming this
was a harness gap, not a timestamp regression.

* fix(#5974): hide Show-earlier-steps pill when a capped transparent turn is collapsed (Fable UX gate)

Fable UX gate SHIP-WITH-UX-FIXES: the .transparent-earlier-steps pill wasn't in the
.assistant-turn[data-transparent-turn-collapsed=1] hide rule, so collapsing a capped
turn via the name-tag toggle left an orphan pill. Added it to the hide rule.
(i18n finding was already resolved — both pill keys present in all 15 locales w/
English fallback + show_earlier_step_one is live at ui.js:12918, not dead.)

* Release exp-v0.52.50: transparent-stream lazy settled DOM + per-turn cap (#5974, #5966)

---------

Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-07-11 21:56:20 -07:00
6361d920ff Release exp-v0.52.49: dictation preserves composer text (#5895) (#5978)
* fix(dictation): preserve composer text when server STT commits

Dictation via the server STT path (MediaRecorder → POST /api/transcribe)
replaced any text already in the composer with the recognized transcript.
Browser SpeechRecognition worked correctly; only the async path was broken.

Root cause: the MediaRecorder.onstop handler calls _setRecording(false)
before awaiting _transcribeBlob's fetch. _setRecording(false) clears the
module-level _prefix. By the time the fetch resolves and _commitTranscript
runs, _prefix is '' — so the existing append logic collapses to
'just the transcript' and overwrites the textarea.

Browser SR dodged this because sr.onend builds the merged string into a
local variable before invoking _setRecording(false). Server STT is async
and could not use that pattern without help.

Fix:
- _commitTranscript accepts an optional prefixOverride parameter.
- _transcribeBlob snapshots _prefix into a local before the await.
- Pass the snapshot through to _commitTranscript.

Add a Settings → Voice → 'Append dictation to composer' toggle (default
ON) so users can opt into replace mode if they prefer the old buggy
behavior. The bug fix changes the default to append, matching what the
browser SR path always did.

i18n: new keys dictation_append_label / dictation_append_desc added to
all 14 locale blocks in static/i18n.js (English fallback where no native
translation exists).

Files: static/boot.js (+43/-7), static/index.html (+7), static/i18n.js (+30).

* fix(dictation): snapshot _prefix in onstop, not inside _transcribeBlob

First attempt at the append-mode fix took the snapshot inside
_transcribeBlob via 'const prefixSnapshot = _prefix'. By the time
_transcribeBlob runs (awaited from recorder.onstop), the sync block
above has already called _setRecording(false), which clears _prefix
inside _setRecording's !on branch. So the snapshot was always '' and
the append path fell through to the replace branch.

Move the snapshot into recorder.onstop, before _setRecording(false)
runs. Pass it as a parameter into _transcribeBlob so the call site
doesn't need to re-read the (now-cleared) module-level _prefix.

No behavior change for the browser-SR path — sr.onend still builds
the merged string into a local variable before invoking
_setRecording(false), and was already correct.

* fix(dictation): trim comments to fit test_590_recording_stops_before_transcribe

test_bugbatch_apr2026.py::test_590_recording_stops_before_transcribe slices
700 chars from 'recorder.onstop' and asserts that _setRecording(false)
comes before _sendRawAudio(/_transcribeBlob( inside that window. With
the verbose 6-line comment block from the previous fix, the actual
call sites landed at offsets 834/890 — past the 700-char window — so
the test's .find() returned -1 and the assertion failed mechanically.

Also: the verbose comment in _transcribeBlob contained the substring
'recorder.onstop', which made the test's first .find() land in the
comment instead of the actual handler, returning a wrong onstop_start.

Trim both comment blocks to single lines that fit within the test's
window and don't contain the literal 'recorder.onstop' substring.
Functional behavior unchanged from the previous fix; this is purely
a comment-shape adjustment to satisfy the brittle test.

* fix(tests): anchor test_590 on handler definition + widen window per review

The reviewer's fix-spec for test_590_recording_stops_before_transcribe
was more thorough than my single-shot comment trim:

  (1) Reword the _transcribeBlob comment so it doesn't contain the
      literal 'recorder.onstop' substring.
  (2) Make the test robust to the handler growing: anchor on
      'recorder.onstop=async' instead of 'recorder.onstop', and
      widen the fixed 700-char window to 1200.

Applied both. With these changes the test passes regardless of how
much comment text gets added in or around the handler in the future.
Also restored the verbose explanatory comments in both locations
(the previous a7f84027 had trimmed them to fit the brittle 700-char
window — that workaround is no longer needed).

Backward-compat check: the new test logic also passes against
origin/master (verified locally), so this change doesn't regress
the existing green-master invariant.

* fix(dictation): preserve user's mid-transcription edits in async server STT

Greptile flagged this as a 4/5 concern on the append-mode branch:
the async server-STT path appended the dictated text to the stale
snapshot (`_prefix` captured at recording start), which clobbered
any keystrokes the user made during the ~1s transcription round-trip.

Before this fix, with append mode ON:
  User typed before mic:    'Hello, '
  User typed during STT:    'Hello, from K'  (typed while waiting)
  Dictated text:            'world'
  Result:                   'Hello, world'   (lost 'from K')

After this fix:
  User typed before mic:    'Hello, '
  User typed during STT:    'Hello, from K'
  Dictated text:            'world'
  Result:                   'Hello, from K world'  (live state preserved)

The fix prefers live `ta.value` as the append base when the textarea
is non-empty, falling back to the `prefixOverride` snapshot only
when the textarea is genuinely empty (the original empty-composer path
where nothing to race with).

Adds a regression test in test_raw_audio_upload.py that asserts the
new `ta.value ||` fallback structure on `_commitTranscript`.

* fix(dictation): don't resurrect cleared text on async transcription resolve

Greptile P1 (re-reviewed at 20:07 UTC): the previous race fix
(`ta.value || (prefixOverride ?? _prefix)`) treats an empty
`ta.value` as falsy and falls back to the recording-start snapshot.
If the user explicitly clears the textarea while server transcription
is in flight, the cleared text reappears with the dictated text
appended — the user's intent is violated.

Fix: when prefixOverride IS passed (server-STT path), trust live
ta.value UNCONDITIONALLY — even when empty. The fallback to _prefix
only fires when no prefixOverride is given AND ta.value is empty,
which is purely defensive (no current caller hits this branch — the
browser-SR path commits inline in sr.onend without calling this
function).

Concrete repro:
  User typed before mic:    'Hello, '
  User clears textarea during STT
  Dictated text:            'world'
  OLD (0239bb4e) result:    'Hello, world'  (cleared text resurrected)
  NEW result:               'world'        (user intent respected)

Greptile's suggested one-liner:
  const base = prefixOverride !== undefined ? ta.value : (ta.value || _prefix);
Applied with expanded inline comment documenting the three concerns
this function balances (race, clear-during-STT, defensive fallback).

* Release exp-v0.52.49: preserve composer text on server STT commit (#5895)

---------

Co-authored-by: Konstantin M <kopamedve@gmail.com>
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
2026-07-11 20:47:54 -07:00
b2ae71e01a Release exp-v0.52.48: post-compression context metering (#5872, #4685) (#5977)
* fix(#4685): meter post-compression model context

* Release exp-v0.52.48: meter post-compression model context (#5872, #4685)

---------

Co-authored-by: Rod Boev <rod.boev@gmail.com>
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
2026-07-11 20:41:43 -07:00
99ca1d2d73 Release exp-v0.52.46: fix public-share locale value swap (#5955) (#5968)
* Fix public share locale values

* Release exp-v0.52.46: fix public-share locale value swap (#5955, regression from #5778)

---------

Co-authored-by: DrMaks22 <DrMaks22@users.noreply.github.com>
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
2026-07-11 18:19:52 -07:00
0037061cfd Release exp-v0.52.45: errored-turn response visible (#5941) + recovery honors model pick (#5924) (#5964)
* fix(streaming): keep errored-turn assistant response visible (#5941)

An errored turn that produced assistant content (tool calls + reasoning)
folded that content into a collapsed worklog above the error card, so the
user read a lone error bubble as "nothing came back". The settled-scene
renderer collapsed the worklog unconditionally, never consulting the
turn terminal_state. Now an errored/failure terminal_state keeps the
worklog expanded by default (unless the user explicitly collapsed it),
while completed turns and genuinely-empty errored turns are unchanged.

Reported by @b3nw.

* fix(review): update #4970 brittle collapsed-expr assertion for #5941

test_issue4970_stream_done_shrink_regression asserted the literal
`collapsed:!keepSettledWorklogOpen` in _renderSettledAnchorSceneForMessage.
#5941 OR'd an errored-turn keep-open term into that expression
(`collapsed:!(keepSettledWorklogOpen||erroredWorklogKeepOpen)`), so the
substring no longer matched and the test failed in the full suite (the PR
updated its own touched tests but missed this pre-existing one).

Re-anchor to `collapsed:!(keepSettledWorklogOpen` — still guards #4970's
invariant (keepSettledWorklogOpen negates into the settled-render collapsed
flag) without pinning the exact term list. #4970 + #5941 suites green.

* fix(chat): honor explicit model pick on post-failure recovery send (#5924)

The onchange explicit-pick marker is single-shot: send() consumes it once.
submitEdit() and cmdRetry() truncated and called send() directly without
re-arming it, so the recovery send went out with explicit_model_pick=false and
the server's compatible-model resolution re-reverted a freshly-picked
cross-family model back to the failed/stale value (Facet 1). Facet 4 is the
same loop keeping the persisted model_provider pinned across fork/refresh.

Re-arm the pending explicit-pick marker from the current selector state
immediately before await send() in both recovery paths. Survives a second
consecutive recovery send. Normal send path and the #3737/#5731 server repair
guard are unchanged.

Reported by @b3nw.

* fix(#5924): gate recovery re-arm on genuine pick + session-race guards

Codex gate found 4 defects on the recovery send path; fixed all:
- /retry + edit-resubmit re-armed the explicit-pick marker UNCONDITIONALLY, forcing
  explicit_model_pick even with no fresh pick (suppressed server compatible-model
  resolution). Now gated on _recoveryPick (selector model differs from session's own
  stored model), captured pre-await.
- session-switch races: added active-session guards after the retry GET await and the
  edit truncate await so session A's recovery intent can't apply to session B.
- 3 new regression tests (gated re-arm, pre-await capture, post-await re-guard).

* fix(#5924) round-2: derive recovery pick from non-default session model, not inference

Codex round-2 CORE: the state-comparison predicate false-negatived an already-applied
pick (consumed marker → looks unchanged) and false-positived on provider inference
(@removed:mistral-large + null stored provider → inferred 'removed' → fake change).
Replace with _deliberateSessionModelPick(sid): reports {model,provider} only when the
session's OWN model is genuinely non-default vs window._defaultModel/_activeProvider —
inference-free + survives marker consumption. Both recovery paths + tests updated.

* fix(#5924) round-3: require known-default+owned-provider evidence + fire-time re-arm guard

Codex round-3: (1) _deliberateSessionModelPick still false-positived when the
profile default was unknown or the provider was only inferred — now requires a
session-OWNED provider AND a known window._defaultModel/_activeProvider, else
fails closed. (2) new same-session race: a model change DURING the recovery awaits
made the pre-await pick stale and clobbered a newer marker — new _reArmRecoveryPick
helper re-arms only if the current session model/provider still equals the captured
pick AND no different pending marker exists. Both recovery paths route through it.

* Release exp-v0.52.45: errored-turn response stays visible (#5950/#5941) + recovery honors model pick (#5949/#5924)

* fix: keep absoluteKeepCount capture before _recoveryPick in submitEdit

The #5924 _recoveryPick comment contained the word 'await', tripping the
pre-existing test_issue_edit_regenerate_absolute_keep_count regex (first \bawait\b
must come after the absoluteKeepCount capture). Reorder both synchronous pre-network
captures (absoluteKeepCount first) + reword the comment. No behavior change.

---------

Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-07-11 18:07:28 -07:00
3a8f980e2b Release exp-v0.52.44: Docker/no-git image update notices (#5733, #2729) (#5958)
* fix(#2729): show Docker image update notices

* Release exp-v0.52.44: show Docker/no-git image update notices (#5733, #2729)

---------

Co-authored-by: Rod Boev <rod.boev@gmail.com>
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
2026-07-11 16:46:13 -07:00
b7b6b3a4c8 Release exp-v0.52.43: trusted-header auth / reverse-proxy SSO (#5568, #3351) (#5956)
* feat(#3351): add trusted-header auth core

* fix(#3351): reconcile trusted sessions per request

* fix(#3351): preserve valid trusted group mappings

* fix(#3351): defer trusted logout reconciliation until after csrf

* fix(auth): clear _pending_set_cookies in reset_trusted_auth_request_state (keep-alive cookie cross-request leak)

Codex gate CORE finding: a Set-Cookie queued in request N but not flushed
survived onto the reused HTTP/1.1 handler and could be emitted by request N+1
— after trusted-identity rotation on logout this could overwrite a valid login
cookie and 401 the user. Reset the queue at the per-request boundary + add a
keep-alive regression test.

* docs(.env.example): document trusted-header auth / reverse-proxy SSO env vars

HERMES_WEBUI_TRUSTED_AUTH_HEADER + TRUSTED_PROXY_CIDRS (with exact-proxy-IP
security warning) + optional groups-header/JSON group-profile-map/logout-url.

* Release exp-v0.52.43: trusted-header auth / reverse-proxy SSO (#5568, #3351) + keep-alive cookie fix + docs

---------

Co-authored-by: Rod Boev <rod.boev@gmail.com>
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
2026-07-11 16:34:14 -07:00
18793a1ba4 Release exp-v0.52.42: ?profile= deep links (#5730) + Russian locale refresh (#5778) (#5953)
* Refresh Russian localization bundle

* feat(#5682): honor profile query during boot

* fix(profile): refresh deep-link reasoning state (#5682)

* test(ci): initialize reasoning transition harness state

* test(ci): accept reasoning fetch context override

* fix(#5682): preserve retryable profile query intent

* test(#5682): accept profile switch result contract

* fix(#5682): bind reasoning fetches to profile transitions

* Release exp-v0.52.42: ?profile= deep links (#5730) + Russian locale refresh (#5778)

---------

Co-authored-by: DrMaks22 <DrMaks22@users.noreply.github.com>
Co-authored-by: Rod Boev <rod.boev@gmail.com>
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
2026-07-11 15:58:47 -07:00
9a1acbff2a Release exp-v0.52.41: dedupe pending prompts across compaction boundary (#5920) (#5952)
* fix: dedupe pending prompt across compaction

* Release exp-v0.52.41: dedupe pending prompts across compaction boundary (#5920, starship-s)

---------

Co-authored-by: Hermes Agent <hermes-agent@users.noreply.github.com>
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
2026-07-11 15:26:05 -07:00
20d258d552 Release exp-v0.52.40: ACP sessions in sidebar (#5939) + date-stamped Claude 3.0 reasoning fix (#5934) (#5951)
* fix(reasoning): stop date-stamped Claude 3.0 ids from reading reasoning-capable

_candidate_supports_reasoning captured a trailing date stamp as the minor
version so bare date-stamped Claude 3.0 ids like claude-3-opus-20240229
satisfied the major==3 and minor>=7 gate and wrongly surfaced reasoning-effort
controls. Cap the minor group to 1-2 digits with a (?!\d) guard so the date
stamp is not read as a minor version -- the same date-stamp defense
_is_pre_adaptive_anthropic already uses. Claude 3.0/3.5 now correctly report
no reasoning support; 3.7+ and 4.x (incl. date-stamped builds) are unaffected.

* test(reasoning): cover date-stamped Claude 3.0 not reasoning-capable

Locks the heuristic so claude-3-opus-20240229 (and sibling date-stamped 3.0
ids) report no reasoning support while 3.7+/4.x stay capable.

* fix(session): classify ACP adapter sessions into the CLI sidebar family

Gateway ACP adapter sessions (source='acp' in state.db — Zed, external
device bridges like a Rabbit R1) normalised to session_source 'other',
which fell through BOTH sidebar buckets: sidebar_source=webui skips the
state.db projection entirely (and ACP rows have no WebUI sidecar), while
sidebar_source=cli keeps only CLI-classified rows. Result: ACP
conversations were invisible and could never be clicked/imported.

Classify 'acp' as a CLI-family source, mirroring 'tui' (a local
interactive agent client): normalize_agent_session_source maps it to
session_source 'cli' with label 'ACP', is_cli_session_row recognises the
raw source, is_cli_session_row_visible keeps ended/untitled ACP rows
visible (they are always user-driven), and frontend _isCliSession
accepts raw === 'acp'. Zero-message ACP connect/reconnect stubs stay
hidden via the existing message-count guard. Claim policy is untouched:
ACP stays claimable like CLI/TUI per the documented 'future local agent
sources' allowance in _is_claimable_cli_source.

Rollback: revert this commit (pre-change file copies also in
~/.hermes/backups/acp-sidebar-fix-20260711/), then restart WebUI.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(session): require a user turn before surfacing ACP rows

An ended ACP connection can record only assistant/tool/system messages
(replayed or aborted turn), which the blanket TUI-style visibility
exception would surface despite not being user-driven. Gate the ACP
branch on _count_user_turns(row) > 0, per Greptile review on #5939.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Release exp-v0.52.40: ACP sessions in sidebar (#5939) + date-stamped Claude 3.0 reasoning-control fix (#5934)

---------

Co-authored-by: 黄云龙 <76432572+nankingjing@users.noreply.github.com>
Co-authored-by: ai-ag2026 <261867348+ai-ag2026@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
2026-07-11 15:20:41 -07:00
nesquena-hermesandnesquena-hermes 29f67b4289 Release exp-v0.52.39: transparent-stream mode-precedence fix (#5942/#5943) + hermetic OpenRouter test (#1426) (#5948)
* fix(transparent-stream): stop compact worklog from hijacking transparent live render (#5942, #5943)

Transparent Stream turns with tool calls alternated between the grouped compact
worklog summary ("Processed Ns") and the individual per-event row list (#5942), and
rebuilt every live row element each tick causing flicker (#5943, regression of #5367).

Root cause (bisected to the #5739 merge as a MERGE INTERACTION; #5700/#5739 timestamp
code is NOT causal): #5746 (final-answer-only mode) reworked renderLiveAnchorActivityScene
so a caller-supplied `requestedMode` (opts.mode) OVERRODE the user's active display mode.
Several live callers (appendLiveToolCard, ensureLiveWorklogShell, appendLiveCompressionCard,
appendLiveThinking, session restore in sessions.js) hardcode {mode:'compact_worklog'}.
Before #5746 the dispatch checked isTransparentStream() FIRST and ignored that hint; after,
those callers forced the compact grouped render onto a transparent turn. The compact pass
deletes the keyed transparent rows, so the next transparent tick's preserveByKey reuse
(the #5367 element-reuse fix) misses -> it rebuilds a grouped frame then tears it down and
recreates every row = alternating + flicker.

Fix (two layers, defence in depth):
1. renderLiveAnchorActivityScene: the USER's active chatActivityMode() is authoritative;
   opts.mode is only a fallback when there is no usable active mode. A compact_worklog hint
   can no longer override an active transparent_stream (restores the good-build precedence).
2. Removed the now-dead hardcoded {mode:'compact_worklog'} argument from all 8 current-view
   live/restore call sites (ui.js x5, sessions.js x3) so intent is explicit and the helpers
   resolve the active mode via chatActivityMode().

Compact_worklog folding, hide_all_activity suppression, the #5700/#5739 timestamp feature,
and #5860 lazy settled-worklog materialization are all unaffected.

Verified with the crown-jewel stream regression gate (private workspace): transparent_stream
goes rowElementSwaps 1105->0 and groupedFramesInTransparent 42->0; hide_all_activity clean.
Independently root-caused + fix-shape-confirmed by Codex adversarial review.

* fix(transparent-stream): also make projection mode active-authoritative in _renderLiveAnchorActivitySceneForStream (Fable advisory — kill latent requestedMode-wins drift)

* test(#5942/#5943): update brittle source-string assertions for removed {mode:'compact_worklog'} arg

test_live_to_final_anchor_visible_order.py asserted the literal presence of the now-removed
hardcoded compact-mode arg (in ensureLiveWorklogShell + the sessions.js restore path). The fix
correctly drops that arg so the helpers resolve the active display mode. Updated both to assert
the mode-free call / active-mode behavior instead of the removed literal. 63/63 in-file pass.

* revert wrapper projection-mode change (preserve #3820 explicit-mode contract)

The follow-up 8f654f0c0 made _renderLiveAnchorActivitySceneForStream projection-mode
active-authoritative, but that broke the #3820 contract (test_issue3820: explicit hide_all
honored when active=compact; hide_all-active always wins). Fable flagged this only as an
advisory/latent cleanup, NOT required — the real fix is the central renderLiveAnchorActivityScene
dispatch + call-site arg removal, both proven by the gate. Reverting the wrapper restores #3820
(109/109 targeted tests pass) with the bug fix fully intact.

* test(#1426): make OpenRouter live-fetch test hermetic by mocking curated fetch

test_openrouter_group_uses_live_fetch_when_available failed deterministically
in any environment where the hermes_cli agent package is installed AND the
network is reachable, e.g.:

    pytest "tests/test_issue1426_openrouter_free_tier_live_fetch.py::\
test_openrouter_group_uses_live_fetch_when_available" -p no:xdist

    assert 'minimax/minimax-m2.5:free' in
        ['anthropic/claude-sonnet-4.6', 'anthropic/claude-fable-5', ...]

Root cause — a gap in the test's mock, not a production regression:

get_available_models() populates the OpenRouter group from TWO live fetches:

  (1) hermes_cli.models.fetch_openrouter_models() — the curated catalog.
      This routes through hermes_cli's open_credentialed_url(), which builds
      its own opener and calls .open(); it does NOT go through the
      module-level urllib.request.urlopen symbol the test monkeypatches.
  (2) a direct urllib.request.urlopen(".../v1/models") free-tier augment —
      this one IS intercepted by the monkeypatch.

The test only patched urllib.request.urlopen, so fetch (1) hit the real
network and returned ~36 live curated models. Those filled the visible picker
cap (15), pushing the mocked free-tier entries (minimax/minimax-m2.5:free,
openrouter/elephant-alpha) into the group's extra_models bucket. The
assertion only inspected the visible `models` bucket, so it failed. In CI
(hermes_cli absent) fetch (1) raised ImportError, the curated base was empty,
the 2-3 mocked entries fit the visible cap, and the test passed — hence the
environment-dependent failure.

Production is correct: free-tier models DO surface in the picker (verified
they land in extra_models). The fix keeps the coverage intent intact:

  - Mock fetch (1) too (as sibling tests already do) so the test is genuinely
    hermetic — matching this module's docstring promise of "without depending
    on real network access" — with a small curated base so live data, not the
    fallback list, is exercised.
  - Assert the free-tier IDs surface across BOTH picker surfaces (`models` +
    `extra_models`), matching the sibling fail-closed and cap tests, so a
    large curated base overflowing the visible cap no longer flips the result.

No production code changed.

* Release exp-v0.52.39: transparent-stream mode-precedence fix (#5946/#5942/#5943) + hermetic OpenRouter live-fetch test (#5947/#1426)

---------

Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
2026-07-11 14:40:59 -07:00
akay64 834ff49816 fix: reset context selection counter on clear and session switch 2026-07-11 11:34:26 +04:00
nesquena-hermes 45fe6b4f10 Merge #5917: clear stale sidebar unread dot on session visit 2026-07-11 06:20:50 +00:00
nesquena-hermes cb94cecc1a fix(sessions): reset busy/stream flags before the visit-ack repaint (gate)
Gate finding: the idle-session S.busy/S.activeStreamId reset ran AFTER
_acknowledgeSessionVisit(), so the ack's sidebar repaint could inherit the
PREVIOUS session's busy state and manufacture a phantom unread/streaming
indicator on the freshly-loaded idle session. Moved the activeStreamId calc +
idle reset BEFORE the acknowledge call. Co-authored-by: neaucode-bot.
2026-07-11 05:52:56 +00:00
nesquena-hermesandneaucode-bot 05f6e8c7f4 fix(sessions): guard viewed-count clear in message-load + derive stream state from target session (gate findings)
Co-authored-by: neaucode-bot <neaucode-bot@users.noreply.github.com>
2026-07-11 05:39:19 +00:00
nesquena-hermes b33c008de2 Merge salvage/5294-mobile-dictation 2026-07-11 05:27:11 +00:00