Commit Graph
2704 Commits
Author SHA1 Message Date
a4a2f49e4d Release: folder-download subpath baseURI fix (#6227, @steezypunk) (#6237)
* fix(ui): resolve folder download URL against document.baseURI for subpath support

When Hermes WebUI is served behind a reverse proxy with a path prefix
(e.g. /hermes/), the right-click → "Download Folder" context menu option
navigates to a root-absolute URL (/api/folder/download?...), which resolves
to the server origin instead of the proxy mount point, causing a 404.

This matches the pattern already used by the workspace.js route helper
refactored in v0.52.41 (commit 1a64d7d3).

* CHANGELOG: folder-download subpath baseURI fix (#6227)

---------

Co-authored-by: Steezy <21984836+steezypunk@users.noreply.github.com>
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
2026-07-18 03:46:58 -07:00
92a7189e61 Release: durable run-journal recovery + full tool args (#6197, @franksong2702) (#6236)
* fix(streaming): prefer durable run journal recovery

* fix(streaming): reject stale recovery stream scenes

* test(streaming): pin todo recovery metadata guard

* fix(streaming): preserve recovery snapshot tool args

* test(streaming): align journal snapshot args contract

* fix(streaming): bound recovery snapshot tool args

* CHANGELOG: durable run-journal recovery + full tool args (#6197)

---------

Co-authored-by: Frank Song <franksong2702@gmail.com>
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
2026-07-18 03:18:20 -07:00
735ba1c4d3 Release: extension session-open handler + renderTranscript API (#5508, @ChonSong) (#6226)
* feat(core): add registerHermesSessionOpenHandler + renderTranscript extension hooks

- registerHermesSessionOpenHandler(fn): register a handler that fires on
  session open. Return {cancel:true} to prevent navigation.
- renderTranscript(container, messages, opts): render messages into any
  DOM container using core's renderMd pipeline. Skip tool messages.
- Wire _hermesNotifySessionOpen into loadSession: pre-load guard at top,
  post-load notification at end for extensions to hook into.
- Follows existing registerHermesTtsEngine extension registration pattern.

This gives extensions like chat-tiling a sanctioned API instead of DOM
hacking to intercept and render session transcripts.

* fix: address 3 gate-fail blockers from PR #5508 review

Fixes the three core issues identified by nesquena-hermes code review:

1. XSS sink in renderTranscript (boot.js)
   - Fallback to textContent when window.renderMd is unavailable
   - innerHTML exclusively for successful renderMd output

2. Stale _loadingSessionId reset in cancel branch (sessions.js)
   - Remove premature nulling of _loadingSessionId
   - Cancel path simply returns without touching loading-guard state

3. Pre-open veto bypasses profile/import side-effects (sessions.js)
   - Move cancellable preload hook to start of _openSidebarSession,
     before external session import and profile switching
   - Pass internal _preloadNotified flag to skip duplicate preload
     in loadSession while retaining post-load notification

Closes #5508 gate-fail items.

* fix: use module-level flag instead of call argument to avoid test regression

The _preloadNotified approach broke test_static_sessions_js_switches_profile
before_opening_all_profiles_row because it changed the loadSession call
signature from loadSession(sid, loadOpts) to loadSession(sid, Object.assign(...)).

Switch to a module-scoped boolean _hermesSessionOpenAlreadyFired set by
_openSidebarSession before calling loadSession, checked by loadSession's
pre-hook guard. The call signature stays unchanged.

Test 2 (test_load_session_rearms_stream_on_every_early_return) also passes.

* fix: compact pre-hook comment to keep loadSession within test window limits

* fix: replace global _hermesSessionOpenAlreadyFired flag with per-call opts._preloadNotified

The module-level boolean introduced in 381fa0ef had two problems:
1. ReferenceError on direct loadSession() calls — the flag was undeclared
   when called outside _openSidebarSession, breaking saved-session restore.
2. Never reset after first sidebar open — all subsequent direct calls
   silently skipped the cancellable preload handler.

Per the maintainer's review (PR #5508), replace the global with a per-call
option _preloadNotified passed by _openSidebarSession via Object.assign.
This keeps the call signature stable for existing tests and eliminates
the stale global state leak.

Test adjustments:
- test_issue1611: widened loadSession call literal assertion
- test_session_channel_option_x: body slice 14000→15000 to accommodate
  the slightly longer function body

* chore: remove unrelated files from commit

* fix: address 3 review issues — preload-only cancel, drop inner wrapper, pass _preloadNotified on retry

- Only honor {cancel:true} when opts.preload===true (boot.js)
- Drop .msg-body-inner wrapper, render directly into .msg-body (boot.js)
- Carry _preloadNotified:true through cross-profile 409 retry (sessions.js)

* recommit

* Update static/boot.js

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* fix(extensions): resolve canonical sid before preload hook + pass _preloadNotified on continuation retry

PR #5508 review follow-up (review 4690636760):
1. Move _resolveSessionIdFromSidebarLineage() before the preload hook so
   extensions always see the canonical sid, not the raw sidebar click id.
2. Pass _preloadNotified:true on the continuation-session retry path to
   prevent duplicate preload events to extensions.
3. Add functional test_extension_session_hooks.py — actually drives the
   new hook registration, preload-veto, transcript rendering, and
   _preloadNotified bridge in Node (13 tests, all green).

* Remove unused pytest import

Removed unused import of pytest from test file.

* fix: address 2 gate-blocking veto-ordering defects (PR #5508)

Blocker 1 (CORE): cross-profile retry now passes _preloadNotified:true
so the pre-hook doesn't re-fire after destructive side-effects already
ran (stream teardown, message clear, profile switch). A {cancel:true}
on that second fire was stranding the UI profile-switched with a
cleared transcript.

Blocker 2 (SILENT): closeMobileSidebar() was called synchronously
BEFORE _openSidebarSession()'s veto guard in three places (tap-to-open,
child-session, lineage-segment). A {cancel:true} still closed the
sidebar out from under it. Removed the three premature calls; moved
a single closeMobileSidebar() inside _openSidebarSession AFTER the
veto guard so it only runs when the open actually proceeds.

Added 3 regression tests asserting {cancel:true} leaves NO side-effect.

* CHANGELOG: extension session-open handler + renderTranscript API (#5508)

---------

Co-authored-by: Sean <seanos1a@gmail.com>
Co-authored-by: ChonSong <85378550+ChonSong@users.noreply.github.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
2026-07-18 00:53:17 -07:00
02aa91ba6e Release: stop false Compressing-context card (#6184, @carlotestor) (#6223)
* fix: stop false "Compressing context" on non-compress turns

Narrow the agent status → SSE compressing bridge to real Hermes
compaction start notices, and stop snapshot hydration inventing a
running compress divider from terminal/lifecycle rows without cues.

Brand-new low-token chats (and skip/cooldown notices) no longer paint
the live auto-compression worklog row.

* CHANGELOG: false compressing-context card fix (#6184)

---------

Co-authored-by: carlotestor <89560945+carlotestor@users.noreply.github.com>
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
2026-07-18 00:03:30 -07:00
52c962cf7d Release: Transparent Stream multi-segment prefix dedupe (#6189, @ai-ag2026) (#6217)
* fix(transparent-stream): drop stale final-answer prefix row in multi-segment settle (#5749 follow-up)

A turn with interim assistant messages (prose interleaved with tool calls)
could show the beginning of the final answer TWICE after watching it stream:
once as a settled anchor-scene prose row (the live-token accumulator's last
throttled snapshot) and once as the real assistant segment. The duplicate
persisted until reload.

Root cause: #5758 suppresses the accumulator row only when it sits "after the
last tool row", but _completeSettledAnchorSceneForTurn appends the settled
per-message tool rows AFTER the projected live rows — those re-list tools that
ran EARLIER in the turn, pushing the boundary past the final segment's
accumulator so the guard never fired. The stale prefix snapshot then survived
into the persisted scene and rendered above the settled answer.

Fix: judge final-segment eligibility against the LIVE projection's own
chronology — a live-prose row belongs to the final segment iff no PROJECTED
tool row follows it. Pre-tool narration that happens to prefix the final
answer stays protected (existing #5758 regression tests still pass), and a
new regression test pins the multi-segment shape.

Verified end-to-end by replaying the captured run journal of an affected
session through the real SSE live handlers in headless Chromium: duplicate
before (answer prefix visible twice after settle), gone after; fresh-reload
rendering unchanged.

Rollback: revert this commit; behavior returns to pre-fix (duplicate prefix
row after live-settle of multi-segment turns).

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

* CHANGELOG: transparent-stream multi-segment prefix dedupe (#6189, @ai-ag2026)

---------

Co-authored-by: ai-ag2026 <m.fuechtenkoetter@posteo.de>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
2026-07-17 21:51:22 -07:00
ab937ef3ec Release: msg_limit ceiling metadata decoupling (#6214, @webtecnica) (#6216)
* feat: expose msg_limit ceiling via /api/session metadata, frontend reads dynamically (#6177)

Backend exposes _MAX_MSG_LIMIT as _msg_limit_max in every /api/session
response. Frontend reads it dynamically, falling back to _MSG_LIMIT_MAX
for older servers. This removes the hand-mirrored coupling between the
two layers.

Removed test_msg_limit_ceiling_drift.py since the mirror pattern that
required the drift guard is replaced by dynamic metadata.

This is the standalone metadata-decoupling piece from #6206,
without the clamp/paging that already shipped in exp-v0.52.98 via #6152/#6154.

* fix(session): declare _msgLimitMax at module scope + CHANGELOG + gate fixes (#6214 follow-up)

The submitted PR used _msgLimitMax at two read sites (boundedReloadLimit in
_ensureMessagesLoaded, useBeforePaging in _loadOlderMessages) but never declared
it and read it before assignment -> undefined on cold load -> full-transcript
fetch every load (regression) + implicit global. Declared `let _msgLimitMax =
_MSG_LIMIT_MAX;` at module scope so the reload-width paths always read a defined
value (the static fallback) until the server's _msg_limit_max lands.

Also: defined the ceiling globals in the two inline node-harness tests that copy
_ensureMessagesLoaded's body (test_cross_session_message_load_isolation,
test_session_unread_dot_on_visit), updated the source-string assertion in
test_webui_external_refresh_frontend, and added 3 decoupling tests (backend field
present, module-scope declaration + fallback, both paths read the live ceiling)
replacing the deleted drift-guard.

Gate: Codex adversarial SAFE TO SHIP (executable probes: cold-load fallback,
mixed-version omission, live update, cross-session isolation, over-ceiling bare
refresh, msg_before row preservation). Full sharded suite green.

Co-authored-by: webtecnica <webtecnica@users.noreply.github.com>

---------

Co-authored-by: webtecnica <webtecnica@users.noreply.github.com>
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
2026-07-17 21:21:52 -07:00
b470793473 Release: msg_limit ceiling + row-loss-safe pagination (#6152 + #6154, @rh-id) (#6213)
* fix(routes): clamp GET /api/session msg_limit to a server-side ceiling

Memory: ?msg_limit= had no upper bound — a client could request
msg_limit=1000000 (or the frontend's msg_limit=9999 outline-jump path) and
force the server to assemble and serialize an unbounded message payload. The
frontend's real pagination grows windows by ~30 at a time (initial load 30,
load-older +30, msg_before paging 30), so only the outline-jump and
pathological/hostile cases ask for huge values.

Add _MAX_MSG_LIMIT (500) and clamp at the query-parse site
(max(1, min(int(_msg_limit), _MAX_MSG_LIMIT))). The ceiling is generous — far
above any legitimate visible-row window — so real pagination is unaffected. The
existing _messages_truncated signal already covers the clamped case: when the
request exceeds the ceiling, _message_window_for_display returns
_messages_offset > 0 and the client learns more rows exist (load-older still
works). The bare no-msg_limit path is intentionally unchanged — it is the
documented "give me everything" escape hatch used by branch/undo/jump-to-start
flows that need the full transcript for correct global indices.

Test: tests/test_session_msg_limit_ceiling.py — the ceiling constant is
reasonable (100..2000), an over-ceiling request is clamped and signals
truncation, and legit sizes (5/30/60/100) are unaltered. Existing window/tail
tests (19) unchanged.

Contract Routing
- Task type: memory-bound fix (unbounded msg_limit request payload).
- Touched areas: GET /api/session query parsing (api/routes.py).
- Relevant public docs: AGENTS.md, CONTRIBUTING.md, docs/CONTRACTS.md.
- State layer mutated: none (read-only endpoint response shaping).
- Contract change (additive): ?msg_limit=N is now clamped to _MAX_MSG_LIMIT=500.
  Compatibility: the only frontend caller above the ceiling is outline.js's
  msg_limit=9999, which now receives up to 500 rows + _messages_truncated=true
  (load-older still works). All other callers (30/60) are unaffected. The bare
  no-msg_limit path is unchanged.

Release note: GET /api/session now caps ?msg_limit at 500 rows server-side,
bounding the worst-case payload for oversized requests.

Model Used: builtin:zai-coding-plan/GLM-5.2 (ZCode agent).

* fix(frontend): route msg_limit-heavy paths around the server ceiling

PR #6152 clamps GET /api/session ?msg_limit= to a max of 500 server-side. Two
frontend paths relied on unbounded msg_limit and silently broke for sessions
> 500 visible rows:

1. outline.js _jumpToMessage sent msg_limit=9999 as a "give me everything" hack
   so it could scroll to any message by absolute index (msg-user-<rawIdx>).
   Clamped to 500, the target row's DOM id doesn't exist for early messages in
   long sessions, and the jump silently no-ops (line 119 `if (r)` is falsy).
   Fix: drop msg_limit=9999 — use the bare messages=1 path, which #6152 left
   unbounded precisely for callers that need the full transcript. This restores
   exact pre-#6152 behavior for outline jumps.

2. sessions.js _loadOlderMessages grows msg_limit by +_INITIAL_MSG_LIMIT each
   load (30 -> 60 -> ... -> 500 -> clamped). Past 500 the server returns the
   same clamped tail, olderMsgs shrinks to 0, and endless-scroll / "Load
   earlier" silently stalls — the head becomes unreachable. Fix: once the grown
   requestedLimit meets/exceeds the new _MSG_LIMIT_MAX mirror (500), switch the
   primary fetch to msg_before paging (a fixed _INITIAL_MSG_LIMIT backward page
   keyed off _oldestIdx) — the same bounded paging the existing race-fallback
   uses, so the head stays reachable for arbitrarily long transcripts. The
   response is handled by the existing prepend path (tailMatches is naturally
   false for a page, so the !tailMatches branch reuses the fetched data).

Added _MSG_LIMIT_MAX = 500 constant in sessions.js mirroring backend
_MAX_MSG_LIMIT, documented as kept-in-sync.

Verification: node --check passes on both files; the backend window-contract
tests (test_session_tail_payload, test_session_message_window_renderable_tail,
19 tests) still pass. No JS test harness exists in this repo (vanilla JS,
manual browser verification per TESTING.md) — manual verification needed: open a
> 500-message session, (a) click an outline entry for an early message and
confirm it scrolls+flashes, (b) scroll up past 500 loaded rows and confirm
"Load earlier" keeps producing older messages without stalling.

Contract Routing
- Task type: frontend follow-up to a backend memory fix (#6152).
- Touched areas: outline jump-to-message (static/outline.js), endless-scroll /
  load-older (static/sessions.js _loadOlderMessages).
- Relevant public docs: AGENTS.md, CONTRIBUTING.md, docs/CONTRACTS.md.
- State layer mutated: client-side S.messages (the active-session transcript),
  same as the existing load-older path. No backend state change.
- Scope boundaries: does not weaken the #6152 clamp. The bare messages=1 path
  (outline) was intentionally left unbounded by #6152; the msg_before paging
  path (load-older) is bounded by _INITIAL_MSG_LIMIT and never hits the ceiling.

Release note: fixes outline jump-to-message and endless scroll silently
breaking on sessions longer than 500 messages after the server-side msg_limit
ceiling (companion to #6152).

Model Used: builtin:zai-coding-plan/GLM-5.2 (ZCode agent).

* test(frontend): update static-source assertion for refactored _loadOlderMessages fetch

The static-source test test_session_message_loads_keep_explicit_longer_timeouts
asserted the exact multi-line shape of the _loadOlderMessages fetch strings.
The #6154 refactor wrapped them in a useBeforePaging ternary (tail-growth vs
msg_before paging), reindenting them — the contract the test guards (both
fetches keep timeoutMs:120000 for large state.db loads) is preserved, but the
literal source shape changed, so the assertion failed on shard 0.

Updated the assertions to match the new source shape while keeping the test's
intent: assert each fetch URL + its {timeoutMs:120000} survive in sessions.js.

Model Used: builtin:zai-coding-plan/GLM-5.2 (ZCode agent).

* test(msg-limit): stopgap drift guard for _MSG_LIMIT_MAX === _MAX_MSG_LIMIT

Reviewer guidance (#6154 round-2): the stopgap drift-test belongs in THIS PR
(#6152) because _MAX_MSG_LIMIT is defined here. The frontend _MSG_LIMIT_MAX
mirror lives on #6154 (not master yet), so the assertion SKIPS when the JS
constant is absent and activates automatically once both branches land on master.

The guard: extract _MAX_MSG_LIMIT from api/routes.py and _MSG_LIMIT_MAX from
static/sessions.js via regex; assert equality when both present. If they drift,
load-older silently stalls at the wrong value (the exact regression #6154 fixed).
Retired by #6177 (metadata-exposure), which removes the mirror entirely.

Verified: on this branch the cross-check SKIPS (_MSG_LIMIT_MAX absent, as
expected — #6154 not landed) and the backend-existence test PASSES. Extraction
logic confirmed correct against simulated match (500==500 passes) and drift
(999!=500 fails) cases.

Model Used: builtin:zai-coding-plan/GLM-5.2 (ZCode agent).

* fix(session): prevent silent row-loss in msg_limit ceiling paging (gate follow-up)

Adversarial review of #6152/#6154 reproduced two silent row-loss regressions in
static/sessions.js; both fixed here, plus a stale source-string assertion update
and the CHANGELOG entry:

1. _loadOlderMessages: gate the suffix-continuity heuristic on !useBeforePaging so
   a raw-row-heavy msg_before cursor page whose visible text repeats the current
   tail can't be misclassified as the cumulative tail and wholesale-replace it
   (previously dropped 470 of 940 rows). msg_before pages are always prepended.

2. loadSession same-session refresh: only apply the width-hint msg_limit when it
   is <= _MSG_LIMIT_MAX; above the ceiling the backend would clamp and silently
   shrink an already-loaded >500-row transcript to the last 500. Over-ceiling
   reloads now use the bare full-transcript path (no msg_limit / no expand_renderable).

3. tests/test_webui_external_refresh_frontend.py: update the reloadLimitParam
   source assertion to the boundedReloadLimit form.

Re-gate: Codex adversarial SAFE TO SHIP (940/940 rows retained on cursor paging,
1000/1000 on refresh, no seam duplication, small sessions still use the fast tail
window); targeted + sessions/pagination/regression suites green.

Co-authored-by: Ruby Hartono <rh-id@users.noreply.github.com>

* test: fix two node-harness tests broken by the boundedReloadLimit change

- test_issue3162_ensure_messages_loaded: replace the brittle fixed 4500-char
  window (which the boundedReloadLimit lines pushed the carry-forward past) with
  robust brace-balance function-body extraction.
- test_cross_session_message_load_isolation: define _MSG_LIMIT_MAX=500 in the node
  harness env; _ensureMessagesLoaded now references it, and an undefined value made
  boundedReloadLimit=null -> the fetch URL dropped msg_limit/expand_renderable and
  mismatched the ordered api() harness -> stall/timeout.

---------

Co-authored-by: Ruby Hartono <58564005+rh-id@users.noreply.github.com>
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
Co-authored-by: Ruby Hartono <rh-id@users.noreply.github.com>
2026-07-17 20:15:40 -07:00
Frank SongandPaperclip 6e3543de16 merge master into provider-aware model selection
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-07-16 23:44:01 +08:00
df0c204d73 docs(rfc): reconcile Live-to-Final assistant-reply contracts with shipped behavior (#6144) (#6147)
Docs-only reconciliation of the Stable Assistant Turn Anchors / Live-to-Final
RFCs + CONTRACTS.md + Phase 0 inventory from proposed/scaffold to
accepted/implemented framing. No runtime/CI change; one comment-header fix in
static/assistant_turn_anchors.js. Remaining hardening tracked under #3400.

Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com>
2026-07-16 08:19:43 -07:00
Frank Song be21deb966 fix(models): preserve provider-aware duplicate selections 2026-07-16 20:05:58 +08:00
nesquena-hermes a69bc3a264 fix(#6105): close the drawer-over-picker stacking class via z-index, not per-site guards
Codex found the body-reparented picker (z-200, later body child) could be revealed
UNDER a full-screen drawer/panel via multiple paths (swipe, breakpoint resize,
profile-switch). Rather than guard each open-site (whack-a-mole + my resize guard
over-fired on keyboard reflows), fix the class once: .model-dropdown--floating gets
z-index:190 — above the composer-mobile-config-panel (z-180) it opens from, below
the mobile sidebar + workspace panel (both z-200) — so any drawer/panel that opens
covers the picker, matching master's stacking intent. Reverted the two boot.js
guards. Co-authored-by webtecnica.
2026-07-16 01:15:11 +00:00
nesquena-hermes 7ed58510bc fix(#6105): also close floating picker on breakpoint-crossing resize that reveals a drawer
Codex found a sibling of the swipe-stacking edge: a direct landscape/tablet->phone
resize can re-reveal an already-open mobile sidebar or workspace panel under the
body-reparented picker (z-200 siblings, no click to trigger outside-close). The
window resize handler now guardedly closes+restores the picker before re-syncing
drawer state, gated on entering phone width AND a drawer/panel actually being open,
so ordinary keyboard/URL-bar reflows don't dismiss the picker mid-use.
2026-07-16 00:59:01 +00:00
nesquena-hermes 7091c4aa84 fix(#6105): close floating model picker before gesture-opening mobile sidebar
Codex re-gate found the body-reparented picker (z-index:200, body child) would
paint on top of the mobile sidebar drawer when a left-edge swipe opened the
sidebar while the picker was open. _openMobileSidebarFromGesture() now calls
guarded closeModelDropdown() first (which also restores the dropdown to the
footer), so the swipe reveals the session drawer cleanly. Co-authored-by webtecnica.
2026-07-16 00:48:27 +00:00
nesquena-hermesandwebtecnica fca5056589 fix: reparent mobile model picker to body so fixed positioning escapes footer (#6080)
The composer model dropdown (#composerModelDropdown) is a child of
.composer-footer, which sets container-type:inline-size (and, under the Geist
Contrast skin, a backdrop-filter). Both establish a fixed containing block, so
the phone position:fixed dropdown resolved against the FOOTER (pinned to the
bottom of the screen) instead of the visual viewport — dropping the menu below
the fold on all skins (the "tapping the model picker does nothing / opens
off-screen" symptom in #6080).

Fix (Fable UX gate recommendation on PR #6105): on the phone path
(<=640px) reparent the dropdown to <body> and switch it to position:fixed via a
.model-dropdown--floating class, exactly the working #profileDropdown idiom, so
fixed positioning is viewport-relative on every skin. The visualViewport math
from PR #6105 is preserved (viewport-relative coordinates, above/below flip
preferring the larger space, width = viewport - 16px, titlebar-bottom clamp) and
so are the rAF-coalesced visualViewport resize/scroll listeners.

Desktop (>640px) is unchanged: closeModelDropdown() and the desktop branch of
_positionModelDropdown() restore the element into its original .composer-footer
slot and clear the phone inline styles, so the desktop popover stays an
absolutely positioned footer child, byte-for-byte identical to master. Because
the menu now leaves the footer on phones, no per-skin backdrop-filter override
is needed.

Adds tests/test_issue6080_model_picker_reparent.py, including a node-executed
DOM-parentage guard that runs _positionModelDropdown()/closeModelDropdown()
against a stub DOM and asserts the open phone dropdown has NO .composer-footer
ancestor (escaping the container-type trap) and is restored into the footer on
close and on desktop.

Co-authored-by: webtecnica <75556242+webtecnica@users.noreply.github.com>
2026-07-16 00:27:47 +00:00
nesquena-hermes ee7ec5e5d7 Release exp batch: narrow-screen CSS fixes — approval/clarify popup min-width (#6104) + stream-timestamp foldable layout (#6106).
Scoped to the two stated fixes only: strips the #composerModelDropdown position:fixed
mobile override that #6104's branch had bundled in (that's the #6080 model-picker
mechanism, being reworked separately to reparent to body level — the fixed override
is trapped by .composer-footer's container-type so it doesn't belong here), and drops
the <=360px tool-name-hiding rules (header overflow-clip prevents the spill without
losing tool identity). Thanks @webtecnica.
2026-07-16 00:10:25 +00:00
nesquena-hermes 928646a57c Merge commit 'refs/pull/6106/head' of github.com:nesquena/hermes-webui into stage/css2-batch 2026-07-15 23:58:12 +00:00
nesquena-hermes 89e443674e Merge commit 'refs/pull/6104/head' of github.com:nesquena/hermes-webui into stage/css2-batch 2026-07-15 23:58:10 +00:00
silent-reader-cn 0403f00789 fix: address Greptile review and fix CI test mock for touch handlers
Three fixes in one commit:

1. **CI test mock** — Added addEventListener/removeEventListener to the
   Node.js makeElement mock so viewport.addEventListener() calls in the
   touch handler registration don't crash with 'not a function'.

2. **Two-finger pan during pinch** — Replaced _setScale() call with
   direct transform math. The new formula computes scale and position
   simultaneously:
     ratio = boundedScale / pinchStartScale
     x = currMidX - (startMidX - startPosX) * ratio
   This keeps the diagram under both fingers regardless of midpoint
   drift, enabling simultaneous zoom + pan.

3. **Post-pinch click suppression** — _onTouchEnd now sets dragged=true
   when exiting pinch mode so any synthesized click event is caught
   by the existing dragged guard in _openViewerOnClick instead of
   opening the lightbox.
2026-07-15 23:02:13 +00:00
silent-reader-cn 7419b1bb51 feat: add pinch-to-zoom support for Mermaid diagrams on touch devices
Adds touch event handlers (touchstart/touchmove/touchend/touchcancel) to
the Mermaid viewer viewport for two-finger pinch zoom, using the existing
_setScale() function with midpoint anchoring.

- Guards pointer handlers (down/move/click) during active pinch to prevent
  interference between touch and pointer event systems
- Uses {passive: false} on touchstart/touchmove to allow preventDefault()
- Falls back gracefully on non-touch devices — touch listeners are inert
  without touch hardware

Fixes: Mermaid diagrams in Hermes WebUI were not zoomable via multi-touch
pinch on mobile/tablet devices, frustrating users with complex diagrams.
2026-07-15 23:02:13 +00:00
webtecnica 9ac68feecb fix: stream event timestamps layout on narrow foldable screens (#6099) 2026-07-15 18:31:51 -03:00
webtecnica 2143563109 fix: ensure approval/clarify popups have min-width on narrow screens (#6094) 2026-07-15 18:30:54 -03:00
Frank Song 59c8f988ef fix(ui): reject invalid read range limits 2026-07-15 20:37:02 +00:00
Frank Song 5b46c54c92 feat(ui): show read ranges in collapsed tool labels 2026-07-15 20:37:02 +00:00
ai-ag2026andClaude Opus 4.8 b6733ccf6e fix(webui): give the onboarding password inputs a form owner too
Completes the previous commit. It gave the Settings password fields an explicit
<form>, but onboarding.js injects two more password inputs (API key, password)
with no form owner. While the wizard is mounted the document is back to having
unowned password fields — exactly the shape that makes Chromium synthesise one
document-wide credential form and autofill the saved account name into the
sidebar conversation filter.

Same treatment: a display:contents <form> wrapper (no layout impact) plus
autocomplete tokens. All five password inputs in the app are now form-owned, so
there is no unowned credential form left for Chromium to build from.

Verified no CSS depends on .onboarding-field / .onboarding-copy being children
or siblings of the wrapped nodes, and no JS traverses their parents.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 10:41:16 +02:00
ai-ag2026andClaude Opus 4.8 7707ea1d2a fix(webui): stop password managers autofilling the search box, and keep unsaved sessions startable
Two independent defects that chain into "new conversations cannot be started":
every POST /api/session/draft and /api/chat/start 404s a few seconds after
POST /api/session/new returned 200.

1. Search inputs are parsed as username fields.

index.html contains no <form> at all, but does contain password inputs (the
Settings "Access Password" panel). Chromium therefore groups the document's
unowned fields into one synthetic password form and picks the first text input
in the DOM — #sessionSearch, the sidebar conversation filter — as that form's
username field, then autofills the saved account name into it on load.
autocomplete="off" was already present; it is ignored for credential
heuristics.

The autofill fires oninput -> filterSessions() -> GET /api/sessions/search
?content=1&depth=5: a full content search the user never asked for, repeated
on every page load.

Fix: give the four search inputs type="search" plus the per-manager opt-outs
(data-1p-ignore / data-lpignore / data-bwignore / data-form-type), and give the
Settings password inputs a real <form> owner with proper autocomplete tokens so
Chromium scopes the credential form to them instead of to the whole document.
The form uses display:contents, so layout is unchanged. CSS suppresses WebKit's
native search clear button, since these fields ship their own
(#sessionSearchClear).

2. A never-persisted session is treated as evictable.

new_session() intentionally keeps a session in RAM until its first message
(#1171), so the SESSIONS cache is its only copy. _session_is_evictable()
(#4765) short-circuited on zero messages, reasoning that an empty shell "is
recreated on next access". It is not: get_session() has no recreate path and
raises KeyError, so both routes 404 and the session can never be started.

The content search from (1) pulls every hit through get_session(), which blows
past sessions_cache_max (default 300) and evicts the session being composed.
That is why this reproduces within seconds on an install with ~1700 sessions,
and why it grew worse as the session count rose.

Fix: require proof of persistence before evicting, for empty sessions too —
which is what the function's own docstring already promised ("Its full state is
already persisted to the JSON sidecar"). The zero-message branch was the single
path that bypassed it. #4765's memory bound is unaffected: sessions loaded from
disk remain evictable, and its existing tests still pass.

Trade-off: an unsaved empty session now stays resident for the process
lifetime (one per "New Conversation" click, an empty object each). That is a
deliberate application of the function's stated invariant — "slightly more RAM
for a session we are unsure about is strictly better than evicting an unsaved
session". Bounding it by age instead would also work if maintainers prefer.

Validation:
  pytest tests/test_issue4765_sessions_lru_eviction.py  ->  9 passed
  The added regression test fails on the unpatched predicate (verified by
  reverting api/models.py alone) and passes with the fix.

Note: (1) fixes what triggers the churn, (2) fixes what turns that churn into
an unstartable session. Either fix alone leaves a real defect in place, so
they are submitted together.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 10:06:41 +02:00
Frank Song e13ebd96c1 fix(chat): align reasoning fallback identity key 2026-07-15 07:10:02 +08:00
Frank Song 90e9f04dab fix(chat): key reasoning fallback to exact anchor row 2026-07-15 06:58:02 +08:00
Frank Song a89a6be075 fix(chat): avoid duplicate reasoning fallback owner 2026-07-15 06:58:02 +08:00
Frank Song e79f3cf53e fix(chat): recover reasoning after stale live turn 2026-07-15 06:58:02 +08:00
Frank Song 54f43b37fa fix(chat): isolate reasoning fallback by session 2026-07-15 06:58:02 +08:00
Frank Song 4f2190c500 fix(chat): preserve visible reasoning ownership 2026-07-15 06:58:02 +08:00
pxxD1998 73b6c5d604 fix(sessions): cascade delegated subagents on parent deletion 2026-07-14 18:10:03 +08:00
Rui Xue b2c7578653 fix(sessions): guard all auto-mint paths + strict config/body parsing (gate review)
Gate review follow-ups for the #6022 no-leak rule: every non-deliberate
newSession/session-new call site passes worktree:false; only deliberate
New Chat inherits the config default.

- Strict boolean config read: only YAML true enables the default;
  quoted strings/ints/lists/dicts/null fall to the safe no-worktree
  default instead of truthiness-coercing.
- Presence-based explicit detection: 'worktree' in body — an explicit
  worktree:null is an opt-out, never a fall-through to config.
- Guarded remaining auto-mint call sites with explicit worktree:false:
  profile switch (panels.js), promptWorkspacePath + switchToWorkspace
  blank-page binds (panels.js), promptNewFile + promptNewFolder
  (ui.js), terminal auto-session (commands.js).
- Tests: worktree:null vs config-true (factory must not be called),
  strict-boolean malformed-shape matrix, static regression gates for
  every auto-mint site + a bare-body sweep so future auto-mint paths
  can't silently omit the key; updated the cmdTerminal preflight
  assertion for the new call shape.
2026-07-14 04:35:06 +00:00
Rui Xue 5f638a1694 feat(sessions): honor the agent's worktree config default in /api/session/new
/api/session/new treated an absent 'worktree' key as false, so the
agent's config-level 'worktree: true' default was never consulted for
WebUI sessions even though the WebUI reuses the agent's own worktree
helper. Both callers should agree on same-agent/same-repo isolation.

Three-value model:
- body 'worktree' absent -> agent config default applies (profile-aware
  via get_config_for_profile_home, so a user with worktree:true in one
  profile but not another gets per-profile behavior)
- explicit true/false -> honored verbatim; explicit always wins

Non-git workspaces: a config-default request degrades to a plain
session and surfaces 'worktree_skipped' in the payload; an explicit
worktree:true keeps the hard 400.

Frontend (same change, load-bearing): newSession() forwards explicit
true/false and omits the key otherwise; the boot-time auto-bind
(_maybeBindFreshDefaultWorkspaceSession) and the onboarding session
send worktree:false explicitly so a config default can never mint a
worktree + branch from merely opening the UI. Deliberate 'New Chat'
sends nothing and inherits the default.

Tests: route matrix (config on/off x body absent/true/false), non-git
fallback vs explicit 400, explicit-false-beats-config-true, config
helper (ambient/named-profile/fail-soft), static UI assertions that
auto-bind and onboarding opt out and New Chat paths don't pin a value.

Fixes #6022
2026-07-14 04:35:06 +00:00
nesquena-hermes 503a369d0c refine(#2147): drop confusing Binding row, keep localization + Example
Maintainer revision of #5806: the localization refactor (all profile-concept
help text now goes through t() so it's translatable via the documented
English fallback) is the core value, and the concrete Example row genuinely
clarifies how profiles + workspaces work together. The Binding row restated
the Together row + added scheduled-task edge-case trivia that muddied a concept
explainer, so it's dropped (row + its 2 i18n keys + its test).

Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
2026-07-14 04:04:39 +00:00
nesquena-hermes 5d97c18242 fix(#2147): correct two inaccurate help-copy claims
- workspace 'scopes file access' -> 'keeps chat, terminal, and file browsing
  pointed at that project' (terminal/agent tools can access outside the workspace).
- scheduled tasks 'run under the profile that created them' -> 'run under the
  profile selected on the task, or the WebUI server default when none selected'.

Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
2026-07-14 03:40:21 +00:00
Rod Boev d2843d94e7 fix(#2147): restore i18n fallback for profile concept copy 2026-07-14 03:33:00 +00:00
Rod Boev 2f95ae7dbb fix(#2147): use stable label keys instead of splitting translated prose 2026-07-14 03:33:00 +00:00
Rod Boev 453b9696e3 fix(#2147): add profile_concept keys to all locale blocks and update existing test 2026-07-14 03:33:00 +00:00
Rod Boev e9b17b1a52 feat(#2147): localize profile concept help and add usage examples 2026-07-14 03:33:00 +00:00
nesquena-hermes 7b210baca4 fix(#5759): match composer-focus chord on '/' char not physical Slash code
The Cmd/Ctrl+/ predicate keyed off e.code==='Slash' + !e.shiftKey, which on
QWERTZ layouts (a) stole Ctrl/Cmd+- browser zoom-out (physical Slash there is
the minus/zoom key) and (b) missed the real shifted-'/' chord. Match e.key==='/'
instead — layout-correct on every keyboard. Tests updated to assert the physical
code isn't used and a layout-shifted '/' still matches.

Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
2026-07-14 01:20:12 +00:00
Rod Boev 37b51803e6 Keep composer focus shortcut easy to reach 2026-07-14 01:06:46 +00:00
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