Commit Graph

882 Commits

Author SHA1 Message Date
nesquena-hermes 2dbee503c2 feat(ux): collapse sidebar by clicking the active rail icon (fuses #1884 + #1924)
Lets desktop users collapse the session-list sidebar to maximise the chat
area, without adding any visible UI affordance. Default appearance is
identical to master — only users who actively try to toggle (or know the
keyboard shortcut) ever see a difference.

## Behaviour (desktop only, ≥641px)

| State                              | Action                | Result                                  |
|------------------------------------|-----------------------|-----------------------------------------|
| Sidebar open, click active rail    | Toggle                | Sidebar collapses to width:0            |
| Sidebar open, click different rail | Normal switch         | **Sidebar stays open** (no surprise)    |
| Sidebar collapsed, click any rail  | Expand + switch       | Sidebar expands, then panel switches    |
| Anywhere, Cmd/Ctrl+B               | Toggle                | Same as same-active-rail click          |
| Mobile (<641px), any of the above  | No-op                 | Mobile overlay behaviour unchanged       |

Two discoverability paths, both opt-in. **No new visible buttons.** Users
who never click the active rail icon see zero UI change vs. master.

## Surface-minimal design

The behaviour is contained behind one extra arg on the rail/sidebar-nav
onclick: `switchPanel('chat',{fromRailClick:true})`. Without that flag the
function preserves master's behaviour exactly — every programmatic
`switchPanel(name)` callsite (commands, deeplinks, internal state changes)
is unaffected. The guard chain inside `switchPanel`:

  opts.fromRailClick && _isDesktopWidth() && (
      _isSidebarCollapsed() ? expandSidebar() :
      prevPanel === nextPanel ? (toggleSidebar(true); return false))

is the ONLY new code path that can cause a collapse. Cross-panel clicks
fall through to the existing switch logic untouched.

## Polish from both source PRs

- **Click-active gesture** as the primary toggle (#1884 @jasonjcwu — the
  genuine UX innovation; no extra button needed)
- **Cmd/Ctrl+B keyboard shortcut** (#1924 @spektro33; VS Code convention).
  Guarded against firing when typing in INPUT / TEXTAREA / contenteditable
  so the shortcut never steals from in-progress text editing.
- **Inline flash-prevention `<script>`** in `<head>` (#1924) sets
  `data-sidebar-collapsed='1'` on `<html>` BEFORE the stylesheet loads,
  so cold loads with a persisted-collapsed state paint correctly from
  frame 0 with no flicker. Cleared by JS once the class system takes over.
- **Smooth slide animation** via `.24s cubic-bezier(.22,1,.36,1)`
  (#1924, mirrors the existing workspace-panel collapse on the right)
- **`aria-expanded` mirrored** on the active rail button (#1884) so
  screen readers announce open/collapsed transitions.
- **`body.resizing` transition-suppression** (#1884) keeps the drag-resize
  cursor instant — no animation during a width-resize gesture.
- **bfcache `pageshow` re-sync** (#1884) — if another tab toggled the
  sidebar while this page was frozen, bring it in line on restore.

## Drops vs. #1924

- No persistent rail "toggle sidebar" button (Nathan: keep the UI stealth)
- No close-X button in chat panel head (same reason)
- No i18n keys for the dropped buttons

## What did NOT change

- 22 rail/sidebar-nav `onclick` handlers gained the `{fromRailClick:true}`
  arg — function-call shape, invisible to users
- 1 inline `<script>` in `<head>` (flash prevention) — invisible
- 5 lines of CSS — invisible unless someone collapses

That's the entire visible-UI delta. **23 ins / 22 del on `index.html`,
all string-replace.**

## Verification

- 5,151 pytest passing including a new 34-test structural suite covering
  every contract (CSS rules, JS functions, fromRailClick guard, legacy
  proxy forwarding, flash-prevention `<script>` ordering, mobile
  exclusion via :not(.mobile-open) selector, aria-expanded sync).

- Live browser walkthrough at 1280px verified:
  - Default boot state identical to master (sidebar open, width 300px)
  - Click active rail → collapse (width 1, opacity 0, translateX -14px,
    localStorage='1', aria-expanded=false). Panel unchanged.
  - Click active rail again → expand back to width 300, aria=true
  - Click DIFFERENT rail → normal switch, sidebar stays open (legacy-
    preserving case, verified explicitly)
  - Click rail while collapsed → expand + switch in one gesture
  - Cmd+B toggles correctly
  - Cmd+B inside `<textarea>` → suppressed (defaultPrevented=false)
  - Reload with collapsed state persisted → restores without flash
  - Mobile simulation (matchMedia returns false for min-width:641px):
    same-active-rail click is no-op, Cmd+B is no-op, sidebar stays at 300px

Co-authored-by: jasonjcwu <jasonjcwu@users.noreply.github.com>
Co-authored-by: spektro33 <spektro33@users.noreply.github.com>
Closes #1884
Closes #1924
2026-05-11 04:49:18 +00:00
nesquena-hermes 0c26ab3425 test(conftest): strip HERMES_WEBUI_SKIP_ONBOARDING env globally; rfcs: note discussion-first for contributor RFCs
Two follow-ups from Opus pre-release review of stage-336:

1. tests/conftest.py — autouse session fixture that removes
   HERMES_WEBUI_SKIP_ONBOARDING from os.environ for the whole pytest run, and
   restores it after. Hosting providers and isolated harnesses set this var
   to short-circuit the onboarding wizard, but it leaked into pytest and
   caused tests that exercise apply_onboarding_setup() to fail with cryptic
   FileNotFoundError. Tests that specifically validate the short-circuit
   behavior can opt back in with monkeypatch.setenv. Surgical per-test
   delenv calls remain as defense-in-depth but are now redundant.

2. docs/rfcs/README.md — one-line note that first-time contributor RFCs
   should be discussed in an issue before opening a PR. Gates drive-by
   design-doc PRs without us having to decline them on contribution.

Verified: 96 onboarding-related tests pass with HERMES_WEBUI_SKIP_ONBOARDING=1
exported in the test runner env (would have failed before this fixture).
2026-05-11 03:02:01 +00:00
nesquena-hermes 8c803c0a07 fix(tests): clear two test failures (one pre-existing, one bumped by #2044)
1. test_issue1362_codex_oauth_onboarding.py::test_anthropic_onboarding_setup_allows_linked_oauth_without_api_key
   Pre-existing env-collision bug, surfaced when HERMES_WEBUI_SKIP_ONBOARDING=1
   is in the test runner env (set by hosting providers and by isolated test
   harnesses). `apply_onboarding_setup()` short-circuits without writing the
   config file when SKIP_ONBOARDING is set, but the test asserts the file was
   written, so it fails with FileNotFoundError on read_text().
   Fix: `monkeypatch.delenv("HERMES_WEBUI_SKIP_ONBOARDING", raising=False)` —
   matches the convention already used in test_issue1499_keyless_onboarding.py
   and test_issue1500_lmstudio_env_var_alignment.py.

2. test_issue1800_file_html_interactions.py::test_media_html_inline_keeps_csp_sandbox
   Slicing-based source-string assertion (4000-char window after `def _handle_media`)
   broke because PR #2044's MEDIA_ALLOWED_ROOTS parsing was inserted earlier in
   the function and pushed the CSP block to offset 4211. Widened window to 5000.
   Assertion content is structural (CSP sandbox string present), not positional.
2026-05-11 02:55:50 +00:00
Chris Watson 8566462b72 feat: add MEDIA_ALLOWED_ROOTS env var for configurable /api/media whitelist
The /api/media endpoint only serves files from ~/.hermes, /tmp, and the
active workspace. Power users with media in custom directories (models,
Downloads, Pictures, ComfyUI outputs) have no way to serve those files
inline without copying or symlinking.

Add MEDIA_ALLOWED_ROOTS env var — a colon-separated list of absolute
paths — that extends the allowed roots at runtime. Each entry is resolved
and validated as an existing directory before being appended. Non-existent
or invalid paths are silently skipped.

This is purely additive: the built-in security whitelist is unchanged,
and if MEDIA_ALLOWED_ROOTS is unset, behavior is identical to before.
2026-05-11 02:45:46 +00:00
nesquena-hermes 9f3f8ea902 fix(recovery): close concurrency hazards in state.db sidecar reconciliation
Two concrete data-corruption vectors flagged in Opus review of PR #2041,
both fixed atomically so the new repair-safe endpoint is safe for production:

1. Shared tmp filename under concurrent calls
   `tmp = target.with_suffix('.json.reconcile.tmp')` produced a fixed path
   per session ID. Two simultaneous repair-safe POSTs would interleave bytes
   in the same tmp file, then both rename → corrupted JSON. Now matches the
   `Session.save()` convention at api/models.py:484 with a pid+tid suffix.

2. TOCTOU between target.exists() check and tmp.replace(target)
   `os.replace()` overwrites unconditionally. If a concurrent Session.save()
   for the same SID materialized the live sidecar in the microsecond window
   between the existence check and the rename, the reconciliation would
   silently overwrite a live sidecar with a (lossier) state.db reconstruction.
   Switched to `os.link()` + `unlink(tmp)` which is atomic create-or-fail —
   on FileExistsError we record `skipped: sidecar_appeared_during_reconcile`
   and keep the live sidecar untouched.

Plus a round-trip schema-parity test: materialize a sidecar from state.db,
then load it back through `Session.load()` and assert the messages survive.
Catches future schema drift between `_state_db_row_to_sidecar()` and
`Session.__init__()`. Also adds a guard test confirming the .reconcile.tmp
suffix includes pid+tid (regression guard for hazard #1).

Tests: 23 passing across the recovery suite (was 21; +2 new in this commit).

Co-authored-by: ai-ag2026 <261867348+ai-ag2026@users.noreply.github.com>
2026-05-11 02:44:38 +00:00
ai-ag2026 a34ded8e99 feat: reconcile missing WebUI sidecars from state db 2026-05-11 02:43:00 +00:00
ai-ag2026 90c3611732 feat: expose session recovery audit and safe repair endpoints 2026-05-11 02:43:00 +00:00
nesquena-hermes db32b70771 Merge PR #2038 into stage-335
# Conflicts:
#	CHANGELOG.md
2026-05-11 00:25:35 +00:00
nesquena-hermes 97b283c5a4 Merge PR #2039 into stage-335 2026-05-11 00:25:07 +00:00
nesquena-hermes 91f1e3df94 Merge PR #2036 into stage-335 2026-05-11 00:25:06 +00:00
ai-ag2026 2ead7daa2f fix: expose active run lifecycle in health 2026-05-11 02:15:00 +02:00
Frank Song 642249747f Fix session message identity dedup 2026-05-11 08:14:50 +08:00
ai-ag2026 7b6d91d490 feat: add read-only session recovery audit 2026-05-11 02:06:43 +02:00
ai-ag2026 663817570c fix: recover orphaned session backups on startup 2026-05-11 02:03:37 +02:00
nesquena-hermes 9c471be4a3 Merge PR #2034 into stage-334
# Conflicts:
#	CHANGELOG.md
2026-05-10 23:38:05 +00:00
nesquena-hermes c3d40ad51f Merge PR #2033 into stage-334 2026-05-10 23:37:39 +00:00
nesquena-hermes 02506eadb5 Merge PR #2032 into stage-334 2026-05-10 23:37:39 +00:00
Frank Song 128e734df4 Fix Xiaomi API key env detection 2026-05-11 07:33:52 +08:00
Frank Song a27f1bf7db Clarify one-shot cron schedules 2026-05-11 07:03:17 +08:00
Michael Lam d620f4394a fix: prewarm skill imports outside env lock 2026-05-10 15:51:49 -07:00
Michael Lam cb3284b73f fix: harden quota probe subprocess handling 2026-05-10 12:18:02 -07:00
nesquena-hermes 2377216860 Stage 333: PR #2009 — feat(context): live status tracking during streaming by @dobby-d-elf 2026-05-10 18:16:59 +00:00
nesquena-hermes 8824f3c88d Stage 333: PR #2022 — fix(resolver): prefer active provider for default model overlap by @Michaelyklam 2026-05-10 18:16:59 +00:00
nesquena-hermes 83bce07d29 Stage 333: PR #2018 — fix(stop): refresh button after chat/start stream id by @rhelmer 2026-05-10 18:16:59 +00:00
nesquena-hermes fe922d83b0 Merge remote-tracking branch 'origin/master' into stage-332
# Conflicts:
#	CHANGELOG.md
2026-05-10 18:07:50 +00:00
nesquena-hermes 22991fa820 Merge remote-tracking branch 'origin/master' into stage-331
# Conflicts:
#	CHANGELOG.md
2026-05-10 18:03:55 +00:00
Michael Lam ed183784d4 fix: prefer active provider for default model overlap 2026-05-10 10:49:12 -07:00
nesquena-hermes c9d4100218 Merge remote-tracking branch 'origin/master' into stage-332
# Conflicts:
#	CHANGELOG.md
2026-05-10 17:46:34 +00:00
nesquena-hermes 16535e1f66 Merge remote-tracking branch 'origin/master' into stage-331
# Conflicts:
#	CHANGELOG.md
2026-05-10 17:46:10 +00:00
nesquena-hermes 4f900d0763 Merge remote-tracking branch 'origin/master' into stage-330
# Conflicts:
#	CHANGELOG.md
#	static/i18n.js
2026-05-10 17:45:29 +00:00
Robert Helmer ce27499762 Fix Stop button not refreshing after chat/start stream id
Call updateSendBtn after S.activeStreamId is cleared for a new turn and
again after the server returns streamId, since setBusy(true) already
refreshed the button while activeStreamId was still null.

Add regression tests in test_1062_busy_input_modes (TestBusySendButton).
2026-05-10 10:15:16 -07:00
nesquena-hermes 71aaad6e7f fix(stage-330): broaden chinese-locale test to accept both \uXXXX and literal CJK forms (PR #2002 source-form refresh) 2026-05-10 17:13:52 +00:00
nesquena-hermes cb27ab0142 Stage 332: PR #2013 — fix(sessions): avoid sidebar jumps when active session is visible by @ai-ag2026 2026-05-10 17:09:44 +00:00
nesquena-hermes 2fb29e508c Stage 332: PR #2008 — fix(diff): CLI session patch diff rendering by @franksong2702 2026-05-10 17:09:44 +00:00
nesquena-hermes f5a8a6f9ef Stage 332: PR #2007 — fix(mobile): wrap markdown code blocks on mobile by @insecurejezza 2026-05-10 17:09:44 +00:00
nesquena-hermes c624770c63 Stage 331: PR #2015 — fix(sessions): stitch continued session transcripts by @Jellypowered 2026-05-10 17:09:21 +00:00
nesquena-hermes 44dc7d05e8 Stage 331: PR #2014 — fix(sessions): keep explicit fork sessions out of compression lineage by @ai-ag2026 2026-05-10 17:09:21 +00:00
nesquena-hermes b68d7c62e7 Stage 331: PR #2012 — feat(sessions): read-only session lineage report endpoint by @dso2ng 2026-05-10 17:09:21 +00:00
nesquena-hermes f4d3e9eed4 Stage 331: PR #2011 — fix(sessions): prefer latest compressed segment by @ai-ag2026 2026-05-10 17:09:21 +00:00
nesquena-hermes 9060bdb344 Stage 330: PR #2001 — fix(clarify): honor clarify.timeout config by @franksong2702 2026-05-10 17:07:37 +00:00
nesquena-hermes 9242305a81 fix(stage-329): zh-Hant locale parity for kanban_status_original_hint + extend locale parity test (Opus advisor SHIP-WITH-CAVEATS follow-up) 2026-05-10 17:06:10 +00:00
nesquena-hermes f44ead1404 Stage 329: PR #1996 — test(kanban): modal locale parity regression by @franksong2702 2026-05-10 16:48:44 +00:00
nesquena-hermes b01df72727 Stage 329: PR #1995 — feat(kanban): trap focus in kanban modals + status hint by @franksong2702 2026-05-10 16:48:41 +00:00
nesquena-hermes 7ce48de817 Stage 329: PR #1993 — fix(kanban): invalidate profile cache for assignee select by @franksong2702 2026-05-10 16:48:15 +00:00
nesquena-hermes bf98ffec9b Stage 329: PR #1991 — fix(i18n): correct German profile_skill_count pluralization by @franksong2702 2026-05-10 16:48:15 +00:00
dobby-d-elf fecfc5f6db fix: reanchor live context usage updates 2026-05-10 10:31:14 -06:00
Jellypowered 8aed650b4c Stitch continued session transcripts in WebUI 2026-05-10 11:10:54 -05:00
ai-ag2026 017a631b6c fix: keep explicit fork sessions out of compression lineage 2026-05-10 18:03:21 +02:00
ai-ag2026 8226328cba fix: avoid sidebar jumps when active session is visible 2026-05-10 18:00:10 +02:00
Dennis Soong c3cf8b10e9 feat: add read-only session lineage report 2026-05-10 23:28:14 +08:00