Commit graph

1482 commits

Author SHA1 Message Date
nesquena-hermes
ff81591e8e feat(workspace): add file upload + drag-drop with archive extraction (#3104, @antoniocarlos97ss) 2026-06-02 00:26:53 +00:00
nesquena-hermes
07a50ebf3b polish(#3220): clarify download button on dark images (border + backdrop blur)
Nathan feedback: the download icon read as 'oddly placed' on dark generated
images. Geometry was already correct (8px inset on the image corner) — the
issue was low contrast: a flat rgba(0,0,0,.55) button blends into a dark image.
Add a subtle 1px white border, soft shadow, and 3px backdrop-blur so the button
reads as a clear chip-on-image regardless of the underlying pixels (the standard
treatment for on-image controls). CSS-only.
2026-06-02 00:09:32 +00:00
nesquena-hermes
a66a008ad3 fix(#3220): shrink-wrap artifact-image wrapper so download button overlays the image
The .msg-artifact-image span was stretching wider than the image (inline-block
in a block context filled the line), so the absolutely-positioned download
button (right:8px of the wrapper) floated ~240px to the right of the image.
Add width:fit-content + max-width so the wrapper hugs the image; the button now
correctly overlays the image's top-right corner. line-height:0 removes inline
descender gap.
2026-06-01 23:49:10 +00:00
nesquena-hermes
e5ddb2ae1f iterate(stage-hi1 v2): #3220 larger generated images, #3223 compact icon+label menu
Nathan feedback round 2:
- #3220: generated images were rendering at the 120x90 upload-thumbnail size
  (too small for the subject of the message). Now render at natural aspect
  ratio up to 360px (max-height 360, responsive max-width), lightbox preserved.
- #3223: drop the per-item subtitle/description lines from the session action
  menu — show only icon + label (VS Code / browser / ChatGPT pattern). The
  description is preserved as a hover tooltip (title=). Makes the menu ~40%
  shorter, less crowded, and structurally less prone to viewport clipping.
2026-06-01 23:44:31 +00:00
nesquena-hermes
031a3ce2e9 polish(#3220): esc() download label + scrub orphaned media_open i18n keys
Opus iter2 SHOULD-FIX (both non-blocking, applied for cleanliness):
- esc() the media_download title/aria-label so a future translator's quote
  can't break out of the attribute (defense-in-depth).
- Remove the now-dead media_open key from all 11 locales + the ui.js fallback
  dict (the Open button was dropped in the clean-image redesign).
2026-06-01 23:03:48 +00:00
nesquena-hermes
8ce02caf11 iterate(stage-hi1): Nathan UX feedback — #3220 clean image+hover-download, #3337 uniform code bg, #3223 menu clip fix
#3220: redesign generated-image rendering from a permanent bordered card
(filename + Open/Download buttons) to a clean inline image with click-to-zoom
lightbox + a hover/focus-revealed Download overlay, matching ChatGPT/Claude/
Gemini. Drops redundant Open (lightbox already covers it). Tests updated.

#3337: fix two-tone code background — Prism's prism-tomorrow theme styled the
parent <pre> gray while the <code> was navy var(--code-bg), so dark theme showed
a gray frame around navy code. Override BOTH .preview-code[class*=language-] and
its <code> to var(--code-bg) (mirrors the chat code-block fix at .msg-body pre).

#3223: fix action-menu clipping — the new Regenerate-title row made the 9-item
menu tall enough to overflow the viewport bottom when opened on a top-anchored
row at short viewports (e.g. 1280x720). _positionSessionActionMenu now clamps
the menu within both viewport edges and caps max-height with scroll when the
menu is taller than the viewport.
2026-06-01 22:48:01 +00:00
nesquena-hermes
c1156b4c67 fix(#3337): prevent Prism highlight leaking across workspace files
Maintainer fix on stage (browser-test catch): Prism.highlightElement()
propagates the language-* class onto the parent <pre>, so previewing a
.css file then a .txt file rendered the plain text with CSS grammar.
Strip any stale language-* from #previewCode before each render and only
call highlightElement when a language was assigned. Adds regression test
(tests/test_issue3337_workspace_preview_highlight.py) and bumps the
_openSessionActionMenu scan window in test_1466 to cover the new
Regenerate-title action lines (#3223).
2026-06-01 21:56:26 +00:00
nesquena-hermes
546afe8374 feat: add manual session title regeneration (#3223, @AJV20)
Closes #3106. Adds /api/session/title/regenerate endpoint + session-action
menu item. Preserves chronology (touch_updated_at=False), guards read-only
and imported sessions, syncs to state.db when Insights sync enabled.

Maintainer refinement (Opus SHOULD-FIX): scope the is_imported guard to the
regenerate action only instead of broadening the shared _isReadOnlySession()
helper, which also gates rename/pin/archive/move/fork. Matches the backend
403 guard. Test updated to assert the scoped shape.
2026-06-01 21:39:51 +00:00
nesquena-hermes
949dc7aac8 feat: render generated media artifact cards (#3220, @AJV20)
- CHANGELOG entry moved to ### Added (feature, not fix)
- zh-Hant locale: use Traditional 開啟/下載 instead of simplified forms
2026-06-01 21:21:32 +00:00
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