Commit graph

1473 commits

Author SHA1 Message Date
nesquena-hermes
2d1b4642e2 feat: syntax highlighting in workspace file preview (#3337, @mysoul12138) 2026-06-01 21:20:42 +00:00
nesquena-hermes
c5d4806a64 fix(#3331): gate findings — empty-session profile retag, root-alias project filter, create-profile validation
Codex+Opus gate findings on the profile-scoping PR:
1. panels.js: retag S.session.profile on ANY profile switch (was inside the
   if(data.default_model) block, so model-less profile switches left a stale chip).
2. sessions.js: project-picker filter now mirrors the server's root-alias
   tolerance (default <-> renamed-root) so a server-approved 'default' project
   isn't hidden for a renamed-root session.
3. routes.py /api/projects/create: validate the optional client-supplied profile
   via _PROFILE_ID_RE before stamping (was trusting raw client input -> could
   create hidden cross-profile rows). Updated the PR's string-assertion test.
2026-06-01 21:05:32 +00:00
nesquena-hermes
3cf4d15dc6 fix: align project/session operations with session profile instead of global active profile (#3331)
Co-authored-by: PINKIIILQWQ <PINKIIILQWQ@users.noreply.github.com>
2026-06-01 20:57:40 +00:00
nesquena-hermes
d09f2e4efb feat: sticky manual unpin for streaming chat scroll (#3343)
Supersedes the v0.51.199 proximity-re-pin (#3330) and the #3250 upward-intent
timeout with a sticky-unpin model (ChatGPT/Claude/Codex behavior): scroll up =
stay put until you return to the bottom or click the scroll-to-bottom control.
Reconciled against the shipped #3330 code: removed the now-dead
_recentMessageUpwardIntent reference from the #3319 rAF retry, kept the
load-time -Infinity intent-init fix, kept the pinned-only >500 catch-up.

Co-authored-by: pamnard <pamnard@users.noreply.github.com>
2026-06-01 20:34:06 +00:00
nesquena-hermes
7d2be7f52c fix(#3321): drop provider_details_label recovery-control heuristic (over-filtered genuine interruptions)
Codex+Opus regression-gate finding: _isRecoveryControlMessage /
_streamRecoveryControlMessage fell back to matching
provider_details_label==='interruption details'. But a GENUINE 'Response
interrupted' card (Stop button, real provider crash) carries that exact label,
so the filter would drop a real user-facing interruption from the transcript on
the next render/restore — the inverse of the #3300 data-loss class. Require the
explicit server-set recovery_control marker; keep only the two fully-anchored
synthetic-text matches for pre-marker backward-compat. Adds a node-driven
regression test (revert-verified) asserting a label-only interruption card and a
real user turn stay visible while marker + strict text are filtered.
2026-06-01 20:14:29 +00:00
nesquena-hermes
f50b4fc2fa fix: filter interrupted recovery control text from visible transcript (#3321)
Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com>
2026-06-01 20:07:16 +00:00
nesquena-hermes
0b7f32f5d9 feat: color diff lines in tool card snippets (#3336)
Co-authored-by: mysoul12138 <mysoul12138@users.noreply.github.com>
2026-06-01 19:53:40 +00:00
nesquena-hermes
0cae7c644c fix: preserve ephemeral turn fields when loadSession force-reloads (#3313)
Co-authored-by: Sanjays2402 <Sanjays2402@users.noreply.github.com>
2026-06-01 19:38:00 +00:00
nesquena-hermes
5d4ce75041 fix(#3330): init scroll-intent timestamps to -Infinity so load-time isn't read as intent
Codex follow-up finding: _lastMessageUpwardIntentMs/_lastNonMessageScrollIntentMs
initialized to 0, so _recentMessageUpwardIntent() returned true for the first 2s
after load (performance.now() < MESSAGE_UPWARD_INTENT_MS=2000) even with no user
scroll — which would disable the new #3319 retry guard during initial load. Sentinel
-Infinity makes 'no event recorded yet' read as no-intent. Also tightens the
pre-existing scrollIfPinned/settle callers that read the same helpers.
2026-06-01 19:19:08 +00:00
nesquena-hermes
fd3c4696e7 fix(#3330): guard the pinned-scroll rAF retry against user scroll-up during streaming
Codex regression-gate finding: the new requestAnimationFrame retry in
_setMessageScrollToBottom re-asserted scrollTop=scrollHeight + _scrollPinned=true
on the next layout frame unconditionally, so a user who scrolled up in that ~16ms
window during streaming would be snapped back and re-pinned, bypassing the
scrollIfPinned early-return guards. Re-check _messageUserUnpinned / upward-intent /
non-message-scroll-intent / !_scrollPinned inside the retry; on hit, only release
the programmatic-scroll latch and bail.
2026-06-01 19:14:14 +00:00
nesquena-hermes
04ce7599e3 stage-batch11: #3330 pinned-scroll + #3311 inline-math currency
#3330 Fix pinned chat scroll after message rebuild
Co-authored-by: jianongHe <jianongHe@users.noreply.github.com>

#3311 fix: reject inline math when $ is followed by a digit (currency)
Co-authored-by: toanalien <toanalien@users.noreply.github.com>
2026-06-01 19:08:30 +00:00
nesquena-hermes
de22c607bc stage-batch10: #3327 model-id normalize + #3334 RFC slice doc + #3341 profile skill counts
#3327 fix(reasoning): normalize custom-provider model ids for fallback heuristics
Co-authored-by: Carry00 <Carry00@users.noreply.github.com>

#3334 docs(rfc): mark run-adapter Slice 4f shipped, define Slice 4g gate
Co-authored-by: Michaelyklam <Michaelyklam@users.noreply.github.com>

#3341 fix(profiles): show enabled vs compatible skill counts
Co-authored-by: b3nw <b3nw@users.noreply.github.com>
2026-06-01 18:24:05 +00:00
nesquena-hermes
1c4365ce2a fix(artifacts): guard malformed tool_calls entries in collectSessionArtifacts (#3329)
Codex regression-gate finding: the OpenAI tool_calls loop dereferenced
tc.function with no null/type guard, so a persisted message.tool_calls
array containing a null or non-object entry would throw and abort artifact
collection. Mirror the existing tool_use-block guard. Adds a node-driven
regression test.

Co-authored-by: mysoul12138 <mysoul12138@users.noreply.github.com>
2026-06-01 17:27:02 +00:00
mysoul12138
fae5ada40d fix: Artifacts tab cannot open files when messages carry structured tool metadata
Two bugs prevented clicking Artifacts entries from opening files:

1. collectSessionArtifacts() only read S.toolCalls, but
   _syncToolCallsForLoadedMessages clears it when messages carry
   their own tool_calls/tool_use metadata. Fix: also scan messages'
   structured tool data (OpenAI tool_calls array + Anthropic
   tool_use content blocks).

2. openArtifactPath() only stripped ~/ and ./ prefixes. When artifact
   paths were absolute (e.g. /mnt/.../workspace/file.js), /api/list
   received the full absolute path and returned 404. Fix: strip the
   session workspace prefix before calling _workspacePathExists.

Bonus: renderSessionArtifacts() now displays workspace-relative paths
instead of cluttered absolute paths in the artifact list.
2026-06-01 17:19:48 +00:00
xz-dev
2303aa1023 Fix workspace panel edge arrow direction 2026-06-01 17:19:48 +00:00
AJV20
ec704356ac fix: hide attachment path markers in chat UI 2026-06-01 05:31:53 +00:00
xz-dev
239f913485 Fix workspace open in browser inline sandbox 2026-06-01 04:42:05 +00:00
Pamnard
f24d633189 Fix skills detail markdown styling with preview-md wrapper
Skill detail and linked markdown files now use the same preview-md
pipeline as Memory/Notes, with code highlighting and KaTeX enhancement.
2026-05-31 23:02:23 +00:00
nesquena-hermes
1aed605fb6 fix(#3267): harden collapsed tool-preview secret filter (Codex gate MUST-FIX)
Codex regression gate found the exact-name hidden-key set leaked secret-shaped
args (apiKey/access_token/clientSecret/Authorization/cookie/...) into the
always-visible collapsed tool-card header. Replace with a normalized
case-insensitive _toolArgPreviewKeyIsHidden() predicate matching secret-bearing
substrings + camelCase variants. Adds 22 parametrized regression tests pinning
the secret-key denial + a legit-key-still-shown guard. Co-authored-by preserved.
2026-05-31 19:21:15 +00:00
ai-ag2026
69072ac34d fix: keep collapsed tool previews quiet 2026-05-31 19:12:31 +00:00
ai-ag2026
e34c632236 fix: localize WebUI tooltip quick wins 2026-05-31 18:26:31 +00:00
PINKIIILQWQ
aeda6add2b fix: suppress phantom sidebar refresh on gateway SSE reconnect
Shift from backend mtime-based detection to frontend SSE deduplication.

Backend: Revert gateway_watcher.py to original pure hash-based polling.
Remove _get_db_mtime, _detect_gateway_restart, and mtime tracking.
This is a no-op in behavior — the original was already hash-only.

Frontend: Add deduplication at the SSE event handler level.
- _gatewaySessionSnapshotKey(sessions): deterministic key from
  session_id + updated_at + message_count (same fields as backend hash)
- _isGatewaySessionForSnapshot(session): classify non-webui sessions
- _isDuplicateGatewaySessionSnapshot(sessions): compare SSE payload
  against current _allSessions, filtered to gateway subset
- SSE sessions_changed handler wraps renderSessionList() in dedupe:
  identical data → skip refresh

This directly addresses the real root cause: the SSE reconnect snapshot
(routes.py:7735) unconditionally pushes an initial snapshot, and the
frontend always re-renders. After this fix, a reconnect with unchanged
session data is correctly detected and the redundant redraw is skipped.

Previously submitted as #3259 (backend mtime approach, now closed per
maintainer review).
2026-05-31 18:26:30 +00:00
nesquena-hermes
d46d3a1411 fix: canonicalize ./ and ~/ prefixes in _normalizeArtifactPath (#3262)
Pre-release Codex regression gate caught that _normalizeArtifactPath()
did not strip ./ or ~/ prefixes, so a tool arg recorded as ./foo.md did
not match a file-tree-opened foo.md in _turnMutatedPreviewPaths — the
open preview was left stale after an agent edit via a ./-prefixed path.
Strip ~/ and leading ./ before ignore/membership checks. Node-driven
regression test pins foo.md == ./foo.md == ~/foo.md and confirms the
existing ignore-dir / URL / empty rejections still hold.

Co-authored-by: Pamnard <pamnard@users.noreply.github.com>
2026-05-31 17:03:07 +00:00
Pamnard
ee414144d3 Reload open workspace preview when agent mutates that file
Track write/edit tool paths per turn, refresh the open preview on
tool_complete and after preservePreview loadDir on stream done, without
closing preview for unrelated responses or wiping unsaved local edits.
2026-05-31 16:54:22 +00:00
Pamnard
9365f2d219 Fix workspace preview closing on chat stream done
Background file-tree refresh after a response must not call clearPreview();
preserve the open preview while still reloading the directory listing.
2026-05-31 16:53:57 +00:00
emanon312
e24ca105d7 fix: extend upward scroll intent timeout to prevent streaming scroll snap-back
Increase MESSAGE_UPWARD_INTENT_MS from 450ms to 2000ms to fix a race
condition where the user scrolls up during streaming, pauses to read
for >450ms, and then gets snapped back to the bottom.

The root cause: after the 450ms upward-intent window expires, DOM layout
changes from the streaming markdown parser (smd), tool card insertions,
or code re-highlighting can trigger scroll events that the handler no
longer recognizes as user-initiated. When the resulting position lands
inside the 250px near-bottom zone for two consecutive samples, the
hysteresis counter re-pins (_scrollPinned=true) and the next streaming
token's scrollIfPinned() call forces scrollTop to the bottom.

With a 2-second window, the user's upward intent persists through typical
streaming DOM churn. Downward scrolling and the scroll-to-bottom button
are unaffected — movedUp requires top < _lastScrollTop-2 which is false
for downward movement regardless of the intent timeout.

Refs: #1360 (macOS momentum protection), #1731 (direction-aware unpin)
2026-05-31 16:53:57 +00:00
nesquena-hermes
86a1ddc3a7 Merge PR #3247 into stage-batchE 2026-05-31 06:50:10 +00:00
nesquena-hermes
584a3f0bf1 Merge PR #3245 into stage-batchE 2026-05-31 06:50:10 +00:00
mysoul12138
f9ff6df883 fix: prevent browser autofill on clarify input (#clarify-autofill)
Chrome's password manager aggressively autofills the clarify card's
input field with saved credentials (e.g. provider base URLs) despite
autocomplete='off'. This causes two bugs:

1. 'Clarification closed. Your draft was kept in the composer.' appears
   on every session completion because _stashClarifyDraft reads the
   autofilled value and treats it as a user draft.

2. The autofilled URL gets injected into the main composer, confusing
   the user.

Fix: add readonly attribute to the clarify input element so Chrome's
autofill ignores it. When showClarifyCard() makes the card visible,
readonly is removed programmatically so the user can type normally.

Both the static HTML (index.html) and the dynamic DOM creation
(_ensureClarifyCardDom in messages.js) are patched.
2026-05-31 13:54:02 +08:00
Andy Kang
18e9a6b9c9 fix: distinguish identical clarify prompts by id 2026-05-31 14:12:15 +09:00
nesquena-hermes
0cceb3bebf fix(#3169): pin capture backend at mic start (_activeCaptureMode) — toggle-mid-record safety (Codex review #2)
Codex round-2: _stopMic and mediaRecorder.onstop read the CURRENT _rawAudioMode
to choose backend/dispatch, but the recording was started on the OLD mode — so
toggling Settings→Sound mid-recording could stop the wrong backend (orphaning
the other) or dispatch raw-vs-transcribe wrongly. Pin _activeCaptureMode
(speech | media-raw | media-transcribe) at start; _stopMic + onstop use it.
Adds front-end source-invariant regression tests.

Co-authored-by: lucasrc <lrclucas@gmail.com>
2026-05-31 04:54:56 +00:00
nesquena-hermes
c74a019a8a fix(#3169): honor explicit Send-click (_micPendingSend) in raw-audio mode (Codex review)
Codex pre-release gate: clicking Send while raw-audio recording with text in the
composer attached the audio but never sent — btnSend sets _micPendingSend=true
before _stopMic, but _sendRawAudio only called send() when the textarea was empty.
Mirror the transcribe path: if _micPendingSend is set, clear it and send()
regardless of composer text; otherwise keep the empty-composer auto-send + toast.

Co-authored-by: lucasrc <lrclucas@gmail.com>
2026-05-31 04:47:04 +00:00
Lucas Coutinho
038bf9924f fix: _stopMic skips recognition.stop() when raw audio mode is active
Bug: when raw audio mode is enabled, SpeechRecognition is
skipped and MediaRecorder is used instead. But _stopMic()
checked for a truthy 'recognition' reference (which exists
even when unused) and called recognition.stop() — a no-op
since speech recognition was never started — never reaching
the mediaRecorder.stop() branch. Recording could never be
stopped by the mic button.

Fix: add !_rawAudioMode guard to the recognition.stop() branch.
2026-05-31 04:34:17 +00:00
Lucas Coutinho
5ebf98479d fix: rename _updateMicBadge → _updateMicTooltip, use monkeypatch in tests
- 'Badge' naming was misleading since the visual badge was removed
- Use monkeypatch.setitem with pytest fixture instead of manual
  import sys + del sys.modules (safer cleanup, no inline import)
- All 9 tests pass
2026-05-31 04:33:52 +00:00
Lucas Coutinho
6258afbaef fix: tooltip conflict with RAW badge — use span instead of ::after
The .has-tooltip CSS uses ::after to show the tooltip text.
Using ::after for the RAW badge overwrote the tooltip content.
Fixed by adding a <span class="mic-raw-badge"> child element via JS.

Also: use _setButtonTooltipAndKey for consistent i18n tooltip management.
2026-05-31 04:33:52 +00:00
Lucas Coutinho
f6b658c7a2 fix: use _setButtonTooltipAndKey for mic tooltip to update data-i18n-title
The global _setButtonTooltip only sets data-tooltip, not data-i18n-title.
The i18n system reads data-i18n-title for locale-aware tooltip updates.
Switched to _setButtonTooltipAndKey which manages both attributes.
2026-05-31 04:33:52 +00:00
Lucas Coutinho
41a8b1deb0 feat: add raw audio upload mode — send audio as attachment instead of transcribing
- Settings toggle in Sound section (after voice mode button)
- Mic button shows 'RAW' badge when raw audio mode is active
- Raw mode: record → pending file → send() (auto-send if textarea empty)
- Dictation mode: unchanged (transcribe → textarea)
- 12 locales with full i18n keys and translations
- 4 backend tests for upload + transcribe regression

PR: Thinking Path
- Hermes WebUI has dictation via Web Speech API / MediaRecorder → /api/transcribe
- Telegram sends raw audio as attachment, agent decides what to do
- Raw audio mode enables external STT, emotion/noise analysis, multimodal models

What Changed
- static/index.html: settingsRawAudio checkbox in Sound section
- static/boot.js: raw audio preference, send raw blob as pending file
- static/style.css: badge RAW on mic button
- static/i18n.js: 12 locales with translations
- tests/test_raw_audio_upload.py: 4 tests for upload + regressions

AI Usage
- Provider: opencode-go
- Model: deepseek-v4-flash
2026-05-31 04:33:52 +00:00
Andy Kang
6091428791 fix: preserve scroll on CLI import refresh 2026-05-31 04:33:12 +00:00
nesquena-hermes
4fa051ea14 fix(security/#3234 + #3219): unify case-folded path checks, fix workspace over-block, protect raw <pre> (Codex review #6)
1. routes.py: hoist a single case-folded path helper (_norm/_within_ci/_equal_ci)
   used for ALL deny + carve-out comparisons (consistent macOS/Windows safety).
2. routes.py: split the deny into (a) dir-based denies that ALWAYS fire (even
   inside the active workspace — so a workspace overlapping a state dir cannot
   expose sessions/memories), and (b) filename denies relaxed only by the
   carve-out. Fix the over-block: a workspace that is a proper DESCENDANT of a
   Hermes root (e.g. STATE_DIR/workspace) is a legit project workspace and keeps
   the carve-out; only a root-itself / ancestor / $HOME / profiles / state-subdir
   workspace disables it.
3. ui.js: move the bare file:// media-stash pass after the raw-<pre> stash too,
   so file:// inside a raw <pre> block stays literal (not just fenced/inline code).
2026-05-31 03:24:35 +00:00
nesquena-hermes
5758d4a827 fix(security/#3234 + #3219): case-fold deny checks + make bare file:// rewrite code-fence-aware (Codex review #5)
1. api/routes.py: case-fold /api/media deny filename + dir containment checks
   (os.path.normcase + casefold) so STATE.DB / Sessions/ cannot bypass the
   state/secret deny on case-insensitive filesystems (macOS/Windows).
2. static/ui.js: move the bare file:// media-stash pass to run AFTER fenced-block
   and inline-code stashing, so a file:// inside a code block / backtick span
   stays literal text instead of becoming an auto-loaded <img>. The MEDIA: stash
   keeps its first-position precedent.
Adds behavioral renderer tests (real renderMd via node) for fenced + inline code
file:// staying literal, bare file:// becoming media, and anchors keeping the
link path. Closes the last Codex review items for #3234.
2026-05-31 03:17:10 +00:00
AJV20
aae3b418f0 fix: render file artifact links as media 2026-05-31 02:24:55 +00:00
nesquena-hermes
5134d896ba absorb #3201: set _streamFinalized immediately in SSE done handler (closes #3195)
Keeps the messages.js fix from @mysoul12138 (set _streamFinalized=true right
after the early-return guard, before the fade window, so a stream_end arriving
mid-fade can't call _restoreSettledSession and overwrite live messages with a
stale server snapshot). Drops the contributor's dead empty `else {}` block in
ui.js (no behavior). Adds a regression test pinning the immediate-finalize order.

Co-authored-by: mysoul12138 <mysoul12138@users.noreply.github.com>
2026-05-31 00:07:39 +00:00
nesquena-hermes
d6d74b5354 polish(#3203): preserve localized 'Forked from' base in fork tooltip
Opus advisor SHOULD-FIX: the tooltip rework dropped t('forked_from') in favor
of a hardcoded English string, a real i18n regression (the catalog key exists
in all locales). Restore the localized base while keeping the clearer
'<base>: <parent>' format. Adds a regression test.

The new lineage/child explanatory suffixes (additive English) and the
read-only title hint are deferred to a small follow-up — they need new locale
keys and the read-only state is still surfaced by the existing meta chip.

Co-authored-by: ai-ag2026 <ai-ag2026@users.noreply.github.com>
2026-05-30 21:51:17 +00:00
nesquena-hermes
cceeca89d0 polish(#3203): keep localized attention tooltip precedence over generic state tooltip
The merged hunk set state.title=_sessionStateTooltip(...) unconditionally,
two lines after assigning the localized attention.title (pending approval/
clarify, from #3190). That clobbered the attention tooltip and, for a
needs-attention session not currently streaming, blanked it to '' since
_sessionStateTooltip returns '' when neither streaming nor unread.

Make the attention title win, and apply the state tooltip only when non-empty.
Adds a regression test pinning the precedence.

Co-authored-by: ai-ag2026 <ai-ag2026@users.noreply.github.com>
2026-05-30 21:46:01 +00:00
nesquena-hermes
5cd2aef0d5 Merge #3203: clarify sidebar session tooltips
# Conflicts:
#	CHANGELOG.md
2026-05-30 21:43:03 +00:00
nesquena-hermes
4ef544cfa4 Merge branch 'pr-3190' into release/stage-batch58
# Conflicts:
#	CHANGELOG.md
2026-05-30 19:56:01 +00:00
Hermes Agent
01c1239fc9 refactor(sessions): simplify attention indicator to a color-coded dot
Per maintainer UX direction: drop the text badge pill ("APPROVAL" /
"N QUESTIONS") in favor of color-coding the existing right-side status dot
plus the colored left rail. Red dot (--error) for pending approvals, amber
dot (--warning) for pending clarifies; theme-driven, visible even when the
session is not streaming/unread. Approval dot gently pulses (disabled under
prefers-reduced-motion). Quieter and consistent with the existing pin/unread
dot family — the row no longer needs to truncate its title to fit a pill.

Updated test_session_attention_badges to assert the dot classes
(is-attention-approval/clarify) instead of the removed badge element/styles.

Co-authored-by: ai-ag2026 <261867348+ai-ag2026@users.noreply.github.com>
2026-05-30 19:54:51 +00:00
ai-ag2026
00e6d953c2 feat: add distinct attention request sound
Adds a distinct two-tone attention sound (880->660Hz) for approval and
clarify prompts so they are not confused with the existing completion sound,
plus sidebar attention badges + colored rails driven by `attention` metadata
on /api/sessions. Includes lock-safety note: the in-lock
publish_session_list_changed() calls in clarify.py are safe because publish()
only takes the leaf _SESSION_EVENTS_LOCK and never re-acquires clarify._lock
(verified by Opus advisor review).

Co-authored-by: ai-ag2026 <261867348+ai-ag2026@users.noreply.github.com>
2026-05-30 19:40:40 +00:00
ai-ag2026
d2aa066862 feat: surface session attention badges 2026-05-30 19:18:13 +00:00
ai-ag2026
8e44dc8609 feat: resolve session links in conversation filter 2026-05-30 19:13:49 +00:00