Commit graph

2681 commits

Author SHA1 Message Date
ai-ag2026
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-ag2026
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
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