The outline FAB was position:fixed at z-index 9999 and could stack on top of the
scroll-to-bottom button. Anchor it inside the messages shell (position:absolute,
z-index 12) so the two controls don't overlap.
Co-authored-by: Mohamed Habib <Habib1001-m@users.noreply.github.com>
- Hamburger button: 32x32 -> 44x44 tap target (iOS HIG minimum), SVG icon
20 -> 24px. flex-shrink:0 preserved; spacer keeps the title centered.
- Edge-swipe-to-open-sidebar: edge zone 28 -> 80px, trigger 72 -> 64px, max
vertical 48 -> 56px; removes the _isPwaStandalone() gate in
_onPwaSidebarSwipeStart so the gesture works in a normal mobile browser, not
only the installed PWA. Still _isDesktopWidth()-blocked, touch/pen-only,
interactive-target-excluded, and requires horizontal intent (dx>=64 &&
dx>|dy|*1.5), so scroll/tap won't trigger it.
Known tradeoff (Nathan-approved ship-as-is): in mobile BROWSER the 80px left
edge zone overlaps iOS Safari's back-swipe; degrades gracefully (PWA, the
primary mobile target, has no back gesture).
Verified live at 390x844: hamburger renders 44x44 + tap opens sidebar; a
left-edge swipe opens the sidebar in a non-PWA context. 66 existing mobile/
swipe/titlebar tests pass.
Co-authored-by: kaishi00 <kaishi00@users.noreply.github.com>
"Reply with selection" no longer appends the selected text straight into
the composer. It now adds a named chip ("Context N") to a row above the
message box (composerSelectionChips). Chips can be renamed (double-click,
esc()-escaped) or removed (x); multiple stack. On send(),
_flushSelectionBlocksToComposer() materializes pending blocks into the
composer as **Name:** + markdown-quoted text, then the normal send path
runs unchanged on composer.value.
Regression-safety:
- _flushSelectionBlocksToComposer early-returns when no blocks are pending,
so send() is byte-identical for anyone who never uses the feature.
- loadSession() AND newSession() both clear pending blocks before replacing
S.session, so chips never leak across a sidebar switch OR a New Chat
(the New Chat clear + its regression test were added per Codex review).
- chip name/text rendered via esc(); XSS-safe.
Verified live (headless CDP drive): add/render/remove/rename, flush
produces correct markdown, empty-flush no-op, session-switch clears,
injected <img onerror=...> chip-name escaped (not executed).
Co-authored-by: Rod Boev <rod.boev@gmail.com>
Ships @rodboev's searchable-settings feature (#3850/#3936): a search box in
Settings that finds any field across all 5 sections with breadcrumbs +
click-to-navigate+highlight+clear. Rebased fresh onto master (post v0.51.470),
5 feature files byte-identical to PR head, re-stamped v0.51.471 (was v0.51.469,
collided with the shipped transcript-virtualization release).
Co-authored-by: Rod Boev <rod.boev@gmail.com>
Virtualization (#500/#4214) caused a scroll-up flicker on long sessions with
tall tool-call rows (variable-height anchor oscillation), distinct from the
stream-completion jump fixed in #4325. Per maintainer decision, flip the
'Virtualize long transcripts' preference to experimental/opt-IN (default OFF)
for everyone until the scroll behavior is reliable.
- api/config.py: virtualize_transcript default True->False; add
virtualize_transcript_optin marker (both bool-allowlisted); load_settings
force-off migration — a stored true WITHOUT the post-flip opt-in marker is
reset to off, so 100% of existing users land on off; an explicit opt-in
(marker present) is honored.
- boot.js / panels.js: default-off semantics (===true), persist the opt-in
marker on enable, checkbox not pre-checked.
- index.html / i18n.js (en): relabel experimental + flicker warning.
- Tests: rewrite the #4325 toggle assertions for the opt-in contract + 4 new
migration tests (unset/stale-true/explicit-optin/marker-without-true).
Phase B (root-cause scroll-stability) tracked separately. Thanks @b3nw.
Adds a Preferences setting 'Virtualize long transcripts' (default ON, opt-out).
When disabled, _currentMessageVirtualWindow returns a full non-virtualized window
so the entire transcript renders at once (also short-circuits the virtual
re-measure loop). Server-persisted (virtualize_transcript bool, default True),
hot-applies via renderMessages on toggle. i18n in all 13 locales.
Pairs with @rodboev's #4328 stream-end anchor scroll fix (cherry-picked into
this branch) to fully resolve#4325.
ai... rodboev's #4133, notification-feature ONLY (dropped the unrelated
'invalidate sidebar cache on WAL' commit — that's a separate good fix to split
out). Button now reflects live Notification.permission instead of silent no-op.
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
* feat(workspace): add Todos tab to workspace panel behind a setting (#2942)
* fix(workspace): use DOM attribute to check active tab across script scope
* Prefer the live todos snapshot in the workspace tab
* Fill the missing workspace Todos locale keys
* feat(workspace): add optional Todos tab to workspace panel (#3564)
Rebased rodboev's #3564 onto current master + CHANGELOG entry.
Co-authored-by: Rod Boev <rod.boev@gmail.com>
* fix(#3564): apply workspace Todos tab visibility on cold boot
Codex gate: boot.js set window._workspaceTodosTab from saved settings but
never called _applyWorkspaceTodosTabVisibility(), so a previously-enabled
Todos tab stayed hidden on a normal page reload (only re-appeared after
opening Settings). Call the visibility helper after assigning the flag in
both the settings-apply and the fallback/default paths.
Co-authored-by: Rod Boev <rod.boev@gmail.com>
* chore(release): stamp v0.51.422 (Release OI) for #3564
---------
Co-authored-by: Rod Boev <rod.boev@gmail.com>
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
Two scroll-state bugs cause the viewport to snap to the bottom after
streaming completes, even when the user has scrolled up to read:
1. _scrollAfterMessageRender (ui.js): when preserveScroll=true and
_scrollPinned=true (re-set by the 250px near-bottom hysteresis),
_followMessagesAfterDomReplace() calls scrollToBottom(), overriding
the user's position. Added !_messageUserUnpinned guard so explicitly
scrolled-up users get their snapshot restored instead.
2. _finishDone (messages.js): the explicit scrollToBottom() at L3302
fires when shouldFollowOnDone is true, but that flag only checks a
120px near-bottom threshold. Added _isMessagePaneNearBottom(250)
gate so users reading 250px+ above bottom keep their position.
Also adds a new 'auto_scroll_follow' setting (default: off) that gates
scrollIfPinned() and _shouldFollowMessagesOnDomReplace(). When disabled,
the viewport never auto-scrolls during streaming — the user controls
scroll position manually and uses the ↓ button to jump to bottom.
7-file setting-add pattern applied:
- api/config.py: _SETTINGS_DEFAULTS + _SETTINGS_BOOL_KEYS
- static/boot.js: both init paths
- static/ui.js: scrollIfPinned() + _shouldFollowMessagesOnDomReplace()
- static/index.html: checkbox in appearance settings
- static/i18n.js: en + zh-CN translations
- static/panels.js: payload, autosave, populate, full save
- messages.js: no additional changes (gated via _shouldFollowMessagesOnDomReplace)
Tests updated: test_tars_scroll_reset_regressions, test_issue1690,
test_issue3545. All 116 scroll-related tests pass.
Adds an opt-in, default-off, desktop-only floating panel that lists the user's
questions in a conversation as a numbered jump-list (click to scroll + flash the
message). Gated behind a Settings → Preferences toggle (show_conversation_outline)
wired through the existing show_* preference boot/load/autosave/save paths.
Review follow-ups applied on absorb:
- Outline is now strictly chat-only: leaving the chat view (Settings, Tasks,
Insights, …) hides the toggle button AND closes the panel; returning to chat
restores the toggle (panel stays closed until re-opened). Implemented by gating
_outlineAllowed() on the active panel and re-evaluating via a MutationObserver on
the <main> showing-<panel> class (switchPanel is a global fn declaration that
can't be reliably wrapped from this script).
- Fixed a latent visual bug: #outlinePanelWrapper{display:flex} (id selector)
outranked the UA [hidden]{display:none}, so wrapper.hidden=true never actually
hid the panel — the × close button and auto-close had no visual effect. Added
#outlinePanelWrapper[hidden]{display:none;}.
- Regression tests for both.
Closes#2124.
Co-authored-by: Rod Boev <rod.boev@gmail.com>
* Add PWA notification controls (#3229, #3196)
Rebased onto fresh master (#3229 was 281 commits behind). Routes browser
notifications through the service worker (reg.showNotification) with a direct
Notification fallback, threads the originating session deep link/tag into the
payload, and adds Enable/Send-test/permission-status controls in Settings.
notificationclick prefers an already-open target-session tab before opening a
new window. Added the 6 new i18n keys to the Polish (pl) locale block, which
the original PR missed.
Co-authored-by: AJV20 <AJV20@users.noreply.github.com>
* Apply Opus review fixes to #3229 PWA notifications
- MUST-FIX: _showPwaNotification used navigator.serviceWorker.ready, which
never settles when no SW registration ever activates (reverse-proxy MIME
mishap, SW disabled) — every notification was silently dropped, a regression
vs master. Switch to getRegistration() raced against a 2s timeout, falling
back to new Notification().
- SHOULD-FIX: notificationclick matched client.url === targetUrl exactly, but
_sessionUrlForSid copies the current query/hash into the deep link, so an
already-open session tab missed the match and spawned a duplicate window.
Compare pathnames instead.
- SHOULD-FIX: "Send test" with permission denied was a silent no-op — now
surfaces the notifications_denied toast on the force path.
- SHOULD-FIX: permission-status span went stale after granting via the button
— refresh it in requestNotificationPermission().then.
- Consistency: the request-permission grant branch now has the same
new Notification fallback as the granted branch.
- Update test_pwa_notification_controls.py to assert the pathname match.
* Stamp v0.51.346 — Release LJ (PWA notification controls #3229/#3196)
---------
Co-authored-by: nesquena-hermes <[email protected]>
Co-authored-by: AJV20 <AJV20@users.noreply.github.com>
Phase-1 low-risk batch, each rebased onto fresh master + gated fresh:
- #3883 (@rodboev, #3740): sidebar refreshes a stale message_count:0 index row
from its sidecar when user_message_count>0 + sidecar mtime newer than index,
self-healing the interrupted-stream stale-count case beyond compression lineage.
- #3878 (@rodboev, #3833): manual workspace refresh clears the dir cache and
re-fetches expanded descendants so background-written files become visible.
- #3880 (@koshikai): translate the 11 remaining English strings in the ja locale.
greptile flags evaluated: #3878 P1 relative-path + P2 stale-comment already fixed
in PR head; #3883 P2 missing-snapshot-test already covered by the PR's own
test_all_sessions_refreshes_stale_zero_count_snapshot_row_from_sidecar, P2
double-stat is a bounded cheap micro-opt (FOLD); #3880 'needs screenshots'
rejected (in-place translation of existing keys, no UI shape change).
Co-authored-by: nesquena-hermes <[email protected]>
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
Co-authored-by: koshikai <koshikai@users.noreply.github.com>
* feat(composer): add saved prompts library with per-profile storage (#2732)
* fix(composer): move saved-prompts popup out of .composer-left to preserve DOM test (#2732)
* fix(composer): correct ARIA roles, add server-side prompt limits (#2732)
* fix(composer): surface save-prompt errors instead of silent success toast (#2732)
* Release v0.51.338 — Release LB (saved prompts library, #3571)
Composer saved-prompts library (@rodboev): bookmark button → popup of saved
prompts; click to insert, save current input, delete. Persists to
$HERMES_HOME/webui/saved_prompts.json with server-side caps (8000 chars / 200).
Maintainer work (per Nathan): conditions were (a) verify it actually works and
(b) hide on mobile. Both met:
- Live-verified load/save/delete all persist through the UI.
- Added mobile-hide (#btnSavedPrompts,.saved-prompts-popup display:none in the
@media max-width:640px + 900px composer blocks). DOM-verified visible at 1280px,
vision-confirmed absent from the composer at 390px.
- Added missing Polish (pl) i18n for the 5 saved_prompts_* keys (PR had en+others
but not pl — failed locale-parity).
- Added tests/test_issue3571_saved_prompts.py (mobile-hide + caps + wiring guards).
Full suite green, ESLint/scope-undef CLEAN, Opus SHIP-safe (auth-gated, CSRF,
XSS-safe, sane caps), Codex SAFE-TO-SHIP.
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
---------
Co-authored-by: Rod Boev <rod.boev@gmail.com>
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
#3518 (@rodboev): in-app Help tab in Settings (docs + GitHub issues links), redesigned as polished icon-led cards. UX-approved by Nathan. Full suite 8242, CI 11/11. Co-authored-by: rodboev <rodboev@users.noreply.github.com>
#3801 (@leszek3737): translate settings/gateway-status labels across 13 locales. +placeholder-parity fix for gateway_session_count pluralization. Full suite 8235, 469 locale tests pass, CI 11/11. Co-authored-by: leszek3737 <leszek3737@users.noreply.github.com>
* Harden interrupted recovery control filtering
* Redesign live-to-final assistant replies
* Fix live activity anchor test fixture
* Fix CI lint issues for live reply tests
* Strengthen live progress prompt contract
* Recover PR #3401 refresh on origin/master
* Repair live-to-final refresh regressions
* Fix live worklog refresh regressions
* Show live footer timer on initial stream start
* Restore live stream shell after reload
* Preserve per-frame live SSE replay cursors
* Preserve reasoning as Worklog Thinking cards
* Quiet Worklog Thinking card styling
* Align Worklog Thinking card styling
* Scope live Worklog Thinking cards by segment
* Suppress exact duplicate settled Thinking
* Close#3401 merge review test gaps
* fix(#3401): resolve 4 deep-review regressions (inline-think, reconnect-dup, neon skin, busy-gate worklog)
Deep review (Codex diff-vs-master + live-browser drive) of the live-to-final refactor
surfaced 4 regressions vs master that the rewritten suite no longer guarded:
1. Inline <think>…</think>answer reasoning vanished — _assistantReasoningPayloadText
used $-anchored regexes so a leading think block + visible answer extracted nothing
and the Thinking card never rendered. Removed the 3 $ anchors to match the
(non-anchored) display stripper. Live: inline-think thinking-only turn now renders.
2. (CORE) reconnect/reload duplicated the live reply — _rememberRunJournalCursor advanced
a closure-local seq but never wrote INFLIGHT[activeSid].lastRunJournalSeq, so a reload
replayed the journal from after_seq=0 over restored lastAssistantText. Now mirrors the
cursor onto INFLIGHT + schedules a throttled persist.
3. Neon skin silently broke — PR deleted the :root[data-skin="neon"] CSS but left Neon in
the picker. Restored the neon CSS block from master.
4. Settled tool-worklog rebuild gated purely on !S.busy — dropped every prior settled
turn's worklog when renderMessages re-ran during an active stream (switch-back to an
in-progress session). Restored master's !S.busy || (S.toolCalls && S.toolCalls.length).
Live: busy re-render now preserves tool cards (4→4, was 4→0).
Live-verified all 4 + confirmed #3709/#3592 invariants still hold (1 thinking card, none
below footer; distinct siblings preserved). + tests/test_issue3401_deep_review_fixes.py (7).
* test(#3401): realign 3 stale source-shape assertions to the deep-review fixes
Fix commit changed two source literals that existing stage tests scanned for:
- test_live_activity_timeline.py (x2): split anchor 'if(!S.busy){' → the restored
'if(!S.busy || (S.toolCalls&&S.toolCalls.length)){' guard (fix 4).
- test_run_journal_frontend_static.py: 'after_seq=0' not in source — fix 2's comment
contained that literal; rephrased the comment to 'the zero floor (after_seq of 0)'.
Intent of all three assertions unchanged; only the matched string updated. No code
behavior change.
* docs(changelog): v0.51.294 — Release JJ (stage-3401, #3401 live-to-final redesign)
---------
Co-authored-by: Frank Song <franksong2702@gmail.com>
Co-authored-by: Nathan-Hermes <nesquena-hermes@users.noreply.github.com>
Co-authored-by: nesquena-hermes <[email protected]>
* feat(approval): make the approval card collapsible (#3515)
Adds a collapse toggle to the approval card header so users can shrink it
to a thin header strip and keep the tool-call rationale/transcript above
readable. Full ARIA (aria-expanded/controls/label), chevron swap, and
transcript reflow that preserves near-bottom scroll. Closes#3007.
Co-authored-by: Rod Boev <rod.boev@gmail.com>
* docs(changelog): v0.51.288 — Release JD (stage-r24)
* fix(approval): clear collapsed state for a distinct queued approval (#3515)
Codex regression-gate finding: showApprovalCard's sameApproval check didn't
include approval_id and didn't clear .collapsed in the !sameApproval branch, so
a NEW/parallel approval arriving while the card was already collapsed could
render collapsed with its command + action buttons hidden. Add approval_id to
the signature; clear .collapsed for a distinct approval before syncing. +2 regression tests.
---------
Co-authored-by: Rod Boev <rod.boev@gmail.com>
Co-authored-by: nesquena-hermes <[email protected]>
* feat(sidebar): add show_cron_sessions toggle to surface cron sessions (#3514, #2841)
Co-authored-by: Rod Boev <rod.boev@gmail.com>
* feat(sidebar): add manual session status labels (#3570)
Co-authored-by: Rod Boev <rod.boev@gmail.com>
* docs(changelog): v0.51.284 — Release IZ (stage-w4)
* fix(settings): persist show_cron_sessions in the explicit Save Settings path too (#3514)
Codex regression-gate follow-up: the autosave path (_preferencesPayloadFromUi)
included show_cron_sessions but the explicit saveSettings() button path read/saved
show_cli_sessions and dropped the cron checkbox — clicking Save Settings silently
omitted it. Read settingsShowCronSessions + add body.show_cron_sessions (gated on
CLI sessions, mirroring autosave).
* fix(settings): gate show_cron_sessions identically in BOTH save paths (#3514)
Codex round-2: my saveSettings() gate exposed that the autosave path
(_preferencesPayloadFromUi) posted the raw cron checkbox state ungated, so
show_cli_sessions=false + show_cron_sessions=true could persist via autosave.
Gate autosave on showCliCb too; update the regression test to assert both
paths gate on settingsShowCliSessions.
---------
Co-authored-by: nesquena-hermes <[email protected]>
Co-authored-by: Rod Boev <rod.boev@gmail.com>
Adds the Verdigris dark-only appearance skin (emerald/forest-green + bronze-gold),
renamed from the contributor's 'Hermes Agent' to a descriptive material name per
maintainer naming convention. Registered across all 5 skin sites (config allowlist,
boot.js swatch, index.html FOUC map, i18n in 12 locales, scoped CSS palette) + test.
Also fixes the zeus i18n test (zeus is no longer the trailing skin token).
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
## Release v0.51.258 — Release HZ (stage-r6)
Fresh-arrival low-risk pair (both @rodboev, landed in the last sweep window).
### Fixed
| Issue | Fix |
|-------|-----|
| #3597 | The "update available" banner now shows from **any panel** (Settings → System "Check now", etc.), not just the Chat view — it was positioned inside the chat surface so it only rendered there. |
| #3592 | Under Simplified Tool Calling, an assistant turn with **thinking but no tool calls** now renders that thinking inline on settlement instead of burying it in an empty collapsed activity group. |
### Review fix absorbed (Codex)
#3592's inline-render `continue` skipped the activity-group creation that carried the turn's `data-turn-duration`, but the footer still suppressed the "Done in …" duration for any `assistantThinking` turn → thinking-only turns silently lost their duration display. Fixed: footer duration is now suppressed **only** for turns that actually build an activity group (`toolCallAssistantIdxs.has(mi)`), so thinking-only inline turns keep "Done in …". + regression test.
### Gate
- Full pytest suite: **7631 passed, 0 failed**
- ESLint: CLEAN · browser-smoke: CLEAN
- Codex (regression): SHIP-ONLY-WITH-FIXES (duration-drop) → fixed → **SAFE TO SHIP**
### Sweep note
#3603 + #3604 (sidebar CLI-session classification, same author/area) were **not** included — they assert contradictory models for a sidecar-less `source='cli'` recovery row; flagged on both PRs for the author to reconcile.
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
## Release v0.51.249 — Release HQ (stage-q21)
Small opt-in feature. UX-approved (screenshot of the toggle in Settings → Preferences).
### Added
| Issue | Author | Feature |
|-------|--------|---------|
| #2974 | @rodboev | **"Auto-expand terminal on output"** preference (Settings → Preferences, **off by default**). When enabled, the collapsed embedded terminal panel expands automatically the first time a running command emits output. Fires once per stream (guarded on open && collapsed — not per chunk), and uses `expandComposerTerminal({focus:false})` so it doesn't steal focus from the composer. Backend-persisted boolean mirroring the `simplified_tool_calling` pattern; default-off = no behavior change on upgrade. |
### Gate
- Full pytest suite: **7557 passed, 0 failed**
- ESLint: CLEAN · ruff: CLEAN · browser-smoke: CLEAN · screenshot vision-verified (toggle renders cleanly under "Compact tool activity", unchecked by default)
- Codex (regression): **SAFE TO SHIP** (clean first pass) — no-arg `expandComposerTerminal` callers unchanged, default-off incl. settings-load-failure path, all 5 plumb sites mirror the existing pattern
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
## Release v0.51.244 — Release HL (stage-q16)
UX-approved direction via Telegram (workspace drag-drop polish you requested). All 4 drag-drop flows verified live in-browser.
### Added
| PR | Author | Feature |
|----|--------|---------|
| #3402 / #3424 | @pamnard | **Drop OS files/folders onto a specific workspace folder row or breadcrumb** to upload into that directory (not just the current dir). OS folder drops are traversed (`webkitGetAsEntry`/`readEntries`) preserving nested structure. Uploads via the existing `/api/workspace/upload` (no new backend). |
### Fixed
- **Composer drop-zone jank**: dragging a workspace file (or OS file) over the composer footer rendered a translucent overlay that let the textarea/chips/icons bleed through and collide with the hint text. Now a clean, fully-opaque box with a single centered **context-aware** label — *"Drop to insert workspace reference"* (workspace file → `@path` insert) vs *"Drop files to attach"* (OS file → message attach).
- **Drag-drop handler coexistence (CORE, caught in review)**: #3424's OS-upload binding assigned `el.ondrop` on folder rows, which **overwrote** the drag-to-move handler from #3422 (also `el.ondrop`) — silently breaking move-to-folder (the ws-path drop fell through to the composer as an `@path` insert). Fixed by binding the OS-upload handlers via `addEventListener` so they compose; each handler gates on its own drag type.
### Drag-drop matrix — all verified LIVE in-browser (real drag→drop, asserted on disk)
| Flow | Result |
|------|--------|
| OS image → composer footer | ✓ attaches |
| workspace file → composer footer | ✓ inserts `@path` |
| workspace file → workspace folder | ✓ moves on disk (report.md → docs/) |
| OS file → workspace folder | ✓ uploads into target folder |
### Scope note
#3424's PR branch carried the OLD pre-hardening `_handle_file_move`. Applied **frontend-only** — master's hardened move backend (v0.51.243, TOCTOU/symlink fixes) is untouched (Codex confirmed no `api/routes.py` diff).
### Gate
- Full pytest suite: **7542 passed, 9 skipped, 3 xpassed, 0 failed**
- ESLint: CLEAN · ruff: CLEAN · browser-smoke: CLEAN
- Codex (regression): CORE handler-clobber → fixed → **SAFE TO SHIP**
Co-authored-by: pamnard <pamnard@users.noreply.github.com>
## Release v0.51.242 — Release HJ (stage-q14)
UX-approved via Telegram (Nathan — dark/light/mobile screenshots).
### Added
| PR | Author | Feature |
|----|--------|---------|
| #3440 | @t3chn0pr13st | **Graphite appearance skin** — a quiet, neutral-gray "workbench" alternative to the default gold/cream. Selectable from Settings → Appearance and `/theme skin graphite`. Both light + dark palettes on the existing CSS-variable token system; tightened typography, shadows, active-sidebar spacing, code-block framing. |
### Why it's safe
- **Fully scoped + additive**: every new CSS rule (and every `!important`) is under `[data-skin="graphite"]` — Codex verified zero bleed into the default appearance or other skins. `api/config.py` keeps the default skin as `default` and only *adds* `graphite` to the allowed set. No i18n keys dropped (only the `/theme` help string gains `graphite`).
- Opt-in; a user has to select it. Default experience unchanged.
### Test-robustness fix (absorbed)
The new graphite scoped selectors (e.g. `:root[data-skin="graphite"] .session-item.active .session-time{…}`) appear in `style.css` *before* the canonical unscoped rules, which broke 3 naive first-occurrence CSS-contract tests (`test_issue677` scroll-btn-overlay, `test_issue856_pinned_indicator_layout`, `test_workspace_panel_session_list`). Fixed those 3 to anchor on the canonical **unscoped** rule (start-of-line regex) instead of the first `.selector` match — robust against this and future skins. Verified they still pass on clean master CSS (invariant preserved, not weakened).
### Gate
- Full pytest suite: **7517 passed, 9 skipped, 3 xpassed, 0 failed**
- ESLint runtime gate: CLEAN · ruff: CLEAN · browser-smoke: CLEAN
- Codex (regression): **SAFE TO SHIP** (verified all CSS scoped, default skin unchanged, no i18n key loss)
- Vision-verified dark + light + mobile; UX-approved by Nathan via Telegram
Co-authored-by: t3chn0pr13st <t3chn0pr13st@users.noreply.github.com>
* Release v0.51.228 (stage-p12): workspace tree-drop #3411 + large-markdown preview #3378#3411 (@pamnard): stopPropagation on workspace file-tree OS-file dragenter/dragover/drop
so a tree drop uploads to the workspace WITHOUT also attaching to the composer.
#3378 (@starGazerK): raise md rich-render ceiling 64KB/1500L->256KB/5000L + backend
file read 200KB->400KB, add 'Render as markdown anyway' force button (reuses cached
raw content, no extra fetch).
* fix(workspace): force-render uses fresh path-scoped cache, blocked while dirty (#3378 Codex follow-up)
Codex review of #3378 found the markdown force-render path had two SILENT bugs:
(1) saving a md file from the plain-text fallback didn't update _previewRawContent,
so a later force-render showed stale pre-save content; (2) the cache-reuse check
'path===_previewCurrentPath' was tautological (var just assigned), so a force-render
after a file switch could render the previous file's cached content. Fixes: track
_previewRawContentPath (set on fetch AND save), reuse cache only when it matches the
requested path, and block force-render while the editor is dirty/open. +3 regression
tests. (#3411 was cleared clean by Codex.)
---------
Co-authored-by: nesquena-hermes <[email protected]>
* feat(composer): replace mobile config-button sliders icon with context-usage ring (#3062, @NottheGuy007)
Squashed net diff of #3062 (the PR branch's tip commits were deletes of files not
present in our tree). Replaces the composerMobileCtxBadge text badge with an SVG
progress ring (ctx-arc + ctx-num) showing real-time context-window usage: ring
fill via stroke-dashoffset, centered percentage, color-coded green<=50%
orange<=85% red>85%. Ring resets to 0%/green on new session.
* feat(activity): add 'expand activity feed by default' appearance setting (#3080, @AJV20)
Squashed net diff of #3080. Adds a Settings -> Appearance checkbox
(activity_feed_expanded_default, default off) to expand new Activity disclosures
by default; preserves manual per-turn collapse/expand (explicit toggle still
wins); live 'Waiting on model' rows explain what the agent is doing before/after
tool calls. i18n keys for all locales.
* docs(changelog): v0.51.226 — context-usage ring (#3062) + activity-feed default-expand setting (#3080)
* test(mobile): update touch-target test for #3062 ring (badge removed)
#3062 replaced the composerMobileCtxBadge text badge with the SVG context-usage
ring (composerMobileCtxRing) but left 3 stale references in tests/test_mobile_layout.py.
The full suite caught test_mobile_composer_primary_controls_keep_touch_friendly_sizing
asserting the removed .composer-mobile-ctx-badge CSS rule + #composerMobileCtxBadge
element. Updated the assertion to the new ring: confirm composerMobileCtxRing exists,
the old badge is fully gone (not dangling), and the ring SVG is aria-hidden so it
stays a decorative overlay that doesn't steal the config button's 44px touch target
(which is still asserted via .composer-mobile-config-btn above). 56/56 mobile-layout
tests pass. Codex: no production JS dereferences the removed badge.
---------
Co-authored-by: nesquena-hermes <[email protected]>
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.
- 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
Cyberpunk-inspired dark-first skin with neon purple (#B347FF) accent
and cyan (#00DDFF) highlights. Full palette for both light and dark
modes with component-level overrides (glow effects, purple scrollbars,
tool cards, tree viewer, session badges, MCP status indicators).
- static/style.css: 37 CSS rules (light + dark palette + components)
- static/boot.js: Neon entry in _SKINS array
- static/index.html: neon:1 in skin allowlist
Follows the existing skin pattern (modeled after Nous).