Original PR: #2676 by @lucasrc
Adds POST /api/skills/toggle endpoint that flips skills.disabled in
config.yaml, and a UI toggle in the Skills panel that shows all skills
(including disabled ones) with a per-skill on/off control.
- Backend: new endpoint validates skill exists in filesystem before
toggling. Read-modify-write wrapped in _cfg_lock for thread safety.
Writes through to platform_disabled.webui when present.
- Frontend: each skill-item now has a toggle switch; disabled skills
appear muted but still listed (previously they were filtered out).
- i18n: new toggle keys translated across all 9 non-English locales.
- Tests: round-trip test for disabled list normalization + toggle
endpoint behavior.
Squash-merged from contributor's branch (19 commits + 1 merge commit)
onto current master via the cherry-pick-stale-contributor-prs procedure.
- Replace text 'Collapse'/'Expand' button labels with Lucide chevron SVG
icons (chevron-down expanded → click to collapse, chevron-up collapsed
→ click to expand). Matches the iconographic design language of the
rest of the chrome (composer buttons, sidebar controls).
ARIA label + title attributes carry the same semantics for assistive
tech, so no accessibility regression vs. the text labels.
- Fix collapsed-card edge clipping at viewport bottom. Original
.clarify-card { bottom: -24px } was sized for the expanded card
(300-420px tall); adding a 72px collapsed variant pushed the header
below the parent's visible region. Override bottom to 8px and reduce
inner padding for the collapsed state so the entire header sits cleanly
inside the viewport at both desktop and mobile sizes (verified card
fits with ~115px margin desktop / ~125px margin mobile).
Per Nathan's 2026-05-22 UX feedback on the screenshot package.
Without --force, git fetch origin --tags refuses to overwrite divergent
local tags and returns 'would clobber existing tag', jamming the entire
WebUI update path indefinitely. The WebUI is a release-tracking consumer
that never pushes tags, so it should always defer to whatever the remote
says a release tag points to. Add --force to all three fetch-tag call
sites:
- _check_repo (the 'Check now' button + periodic check)
- apply_force_update (force-reset to remote HEAD)
- apply_update (stash + pull --ff-only)
Tests:
- Updated 3 existing tests in test_updates.py whose fake_git mocks
asserted the exact ['fetch', 'origin', '--tags'] args list.
- Updated 1 existing test in test_update_banner_fixes.py that asserted
the same shape for apply_update.
- Added 4 new regression tests:
- test_check_repo_fetches_tags_with_force
- test_apply_force_update_fetches_tags_with_force
- test_apply_update_fetches_tags_with_force
- test_check_repo_recovers_from_remote_retag (end-to-end,
proves the bare --tags fetch shape is no longer used)
Closes#2756.
When display.personality is set in config.yaml (e.g. personality: taleb),
new sessions now inherit it automatically instead of starting with
personality=None and requiring an explicit /personality command.
This makes the selected personality sticky across new conversations rather
than requiring per-session activation.
Behavior:
- display.personality values 'none', 'default', 'neutral', '' are treated
as no personality (personality=None), matching TUI gateway semantics.
- Config read is wrapped in try/except — if it fails, personality falls
back to None (no crash, no regression).
- Case-insensitive: 'Taleb' normalizes to 'taleb'.
The /personality slash command still works for per-session overrides as
before; this change only affects the initial default.
Two functions on the /api/session/handoff-summary hot path were opening
sqlite3.connect(...) inside a bare `with` statement, which commits the
transaction at scope exit but does NOT close the connection. Per-turn
invocations accumulated state.db / state.db-wal file descriptors and
CPython heap pages on long-lived worker threads, surfacing as the
multi-GB VmRSS and 6x duplicated state.db fds observed on the live
instance (D0 pre-restart baseline: VmRSS 1,334,248 kB, 55 fds; cold
baseline after restart: VmRSS 136,668 kB, 10 fds).
Wrap both call sites with contextlib.closing(...) (already imported and
used at seven other sites in the same files) so the connection is
closed deterministically:
- api/models.py :: count_conversation_rounds
- api/routes.py :: _persist_handoff_summary_to_state_db
Regression test:
tests/test_issue2233_sqlite_connection_leak.py loops both functions
20 times against a tmp state.db and asserts /proc/<pid>/fd count
does not grow more than 2. Linux-only via sys.platform skip.
D1 live soak against a freshly-built worktree server (port 8799,
isolated HERMES_HOME / HERMES_WEBUI_STATE_DIR) hitting
/api/session/handoff-summary 20 times:
fd_before = 5
fd_after = 5 (growth 0, threshold < 5)
vmrss_before = 52636 kB
vmrss_after = 52636 kB (growth 0 kB, threshold < 30 MB)
The patched fix curve trends below the leak curve.
Rollback: single git revert <this-sha> reverts both file edits.
Refs #2233.