Commit Graph

1068 Commits

Author SHA1 Message Date
Dennis Soong cbb251b823 fix: add sidebar cancel for running sessions 2026-05-03 08:46:36 +08:00
nesquena-hermes 7fddc331ae Merge pull request #1490 from nesquena/stage-271
v0.50.271 — Composer voice buttons UX (#1488)
v0.50.271
2026-05-02 15:37:22 -07:00
Hermes Bot 63361ddb1c chore(release): stamp v0.50.271 — composer voice buttons UX (#1488) 2026-05-02 22:35:07 +00:00
Hermes Bot 6b68f14884 Stage 271: PR #1489 — composer voice buttons (icon + tooltips + opt-in pref) (#1488) 2026-05-02 22:26:18 +00:00
Hermes Bot 341b1ee6b6 fix(composer): distinct voice-mode icon, descriptive labels, opt-in pref (#1488)
Composer footer rendered two near-identical mic icons whose tooltips both
said "Voice input" — push-to-talk dictation and hands-free voice mode were
visually indistinguishable. Researched how ChatGPT/Claude/Gemini solve the
same problem and adopt the industry convention.

Changes:
- btnVoiceMode now uses Lucide audio-lines (6 vertical bars), the
  universal voice-conversation glyph. Also registered in LI_PATHS.
- Distinct localized tooltips: voice_dictate ("Dictate") and
  voice_mode_toggle ("Voice mode"), with active-state flips
  (voice_dictate_active "Stop dictation", voice_mode_toggle_active
  "Exit voice mode"). Legacy voice_toggle key removed (it resolved to
  "Voice input" in every locale and caused the duplicate-tooltip bug).
- Voice mode is opt-in via Settings -> Preferences ->
  "Hands-free voice mode button" (default off). Dictation mic stays
  visible by default, unchanged. localStorage-backed; panels.js onchange
  calls window._applyVoiceModePref() so the button appears/disappears
  immediately without reload.
- 17 regression tests pin: distinct titles, audio-lines glyph, all 4
  new keys in all 9 locales, removal of stale voice_toggle, English
  labels match convention, pref gating (no unconditional display=''
  left in boot.js), Settings checkbox + i18n, panels.js wiring,
  active-state tooltip flips.

Browser-verified on port 8789: default state shows 1 mic; enabling
the pref makes the audio-waveform button appear live; tooltips read
"Dictate" and "Voice mode" distinctly.

Closes #1488
2026-05-02 22:16:23 +00:00
nesquena-hermes 913c93ae85 Merge pull request #1487 from nesquena/stage-270
v0.50.270 — Bootstrap launcher import validation (#1315) + Opus follow-up
v0.50.270
2026-05-02 12:56:25 -07:00
Hermes Bot dc36d7c977 chore(release): stamp v0.50.270 — bootstrap launcher import validation (#1315)
- CHANGELOG.md: v0.50.270 entry detailing #1315 + maintainer follow-ups
- ROADMAP.md: bump to v0.50.270, 3849 tests collected
- TESTING.md: bump header + total to 3849
- bootstrap.py: Opus advisor optional-followup — PYTHONPATH prepend comment

#1315 by @ccqqlo (113 LOC): bootstrap.py validates launcher Python can
import both yaml and run_agent.AIAgent. Companion fix to v0.50.269's #1478
— addresses the start-healthy-then-cryptic-fail mode (different from #1478's
supervisor-respawn loop).

3849 tests pass. Opus advisor verdict: ship as-is. CI green on contributor
branch + on local stage. QA harness all green.
2026-05-02 19:54:21 +00:00
Hermes Bot 58571c9221 fix(bootstrap): validate WebUI launcher can import agent (#1315) 2026-05-02 19:47:22 +00:00
Hermes Bot 9049d4d6b3 test(bootstrap): skip venv.EnvBuilder.create() in fail-loud test
The test_ensure_python_fails_loudly_when_no_interpreter_can_import_agent
test was passing locally but failing on CI runners because:

1. CI runners don't have REPO_ROOT/.venv/bin/python on the filesystem
2. The function path on missing venv calls venv.EnvBuilder(with_pip=True).create()
3. That internally calls subprocess.check_output() — a different code path
   than the monkey-patched bootstrap.subprocess.run, which only stubs run().
4. CI fails with: AttributeError: NoneType has no attribute stdout

The behavior under test is "what happens when no interpreter can import
both WebUI deps and the agent" — NOT the venv-creation path. So we sidestep
EnvBuilder by setting REPO_ROOT to tmp_path with a pre-existing
.venv/bin/python file. The venv-existence check passes, EnvBuilder is
skipped, the stubbed _python_can_run_webui_and_agent returns False on the
final check, and the expected RuntimeError fires.

Co-authored-by: ccqqlo <ccqqlo@users.noreply.github.com>
2026-05-02 19:45:54 +00:00
Hermes Bot 0076f3d9ab test(bootstrap): widen ensure_python_has_webui_deps stub for rebase onto v0.50.269
The PR added an `agent_dir` parameter to ensure_python_has_webui_deps. The
test_bootstrap_foreground.py tests (added in #1478) had `lambda p: p` stubs
that were 1-arg only. Widened to `lambda *a, **kw: a[0]` so the stubs
accept the new signature on the rebased base.

Co-authored-by: ccqqlo <ccqqlo@users.noreply.github.com>
2026-05-02 19:35:42 +00:00
milo 634f90a807 fix: validate WebUI launcher can import agent 2026-05-02 19:32:21 +00:00
nesquena-hermes b8a346f421 Merge pull request #1483 from nesquena/stage-269
v0.50.269 — Bootstrap supervisor fix (#1478) + #1473 follow-ups (#1479, #1480)
v0.50.269
2026-05-02 11:14:23 -07:00
Hermes Bot e1708c4535 chore(release): stamp v0.50.269 — bootstrap supervisor fix + 2 v0.50.267 follow-ups
- CHANGELOG.md: v0.50.269 entry detailing #1478 #1479 #1480
- ROADMAP.md: bump to v0.50.269, 3847 tests collected
- TESTING.md: bump header + total to 3847

#1478: nesquena APPROVED self-built bootstrap.py --foreground mode
       (closes #1458 Bug #1, +Opus follow-ups: XPC noise filter, executability guard)
#1479: surgical follow-up to #1473 — Session.compact() now includes pending_user_message
#1480: bfcache pageshow restores active session via loadSession + checkInflightOnBoot

3847 tests pass (+47 net). Opus advisor on stage diff: no blockers.
2026-05-02 18:12:13 +00:00
Hermes Bot 715a80569d fix(bootstrap): --foreground mode for process supervisors (#1478) 2026-05-02 18:04:44 +00:00
Hermes Bot 6aa2190cc6 fix(boot): restore inflight session on bfcache pageshow (#1480) 2026-05-02 18:04:44 +00:00
Hermes Bot 26b332612d fix(api): add pending_user_message to Session.compact() (#1479) 2026-05-02 18:04:44 +00:00
nesquena-hermes 7d5c9bd76f Merge pull request #1482 from nesquena/stage-268
v0.50.268 — 4 contributor PRs (sessions URL sync, sidebar nesting, /api/session/duplicate, Android PWA) + Opus follow-ups
v0.50.268
2026-05-02 10:57:08 -07:00
Hermes Bot bcfd8b2eac chore(release): stamp v0.50.268 — 4-PR batch + Opus follow-ups (i18n + per-session fields + None title guard)
- CHANGELOG.md: v0.50.268 entry detailing #1395 #1450 #1462 #1476 + Opus SHOULD-FIX followups
- ROADMAP.md: bump to v0.50.268, 3800 tests collected
- TESTING.md: bump header + total to 3800

SF-1 i18n fix:
- static/i18n.js: session_meta_children key in all 10 locale blocks (en, ja, ru, es, de, zh, zh-Hant x2, pt, ko)
- static/sessions.js: 2 callsites use t(session_meta_children, childCount)

SF-2 #1462 per-session field carry-over:
- api/routes.py: duplicate now carries personality, enabled_toolsets, context_length, threshold_tokens

SF-3 #1462 None-title guard:
- api/routes.py: (session.title or "Untitled") + " (copy)"

Tests:
- tests/test_stage268_opus_followups.py: 6 regression tests pinning SF-1 + SF-2 + SF-3
- tests/test_session_duplicate.py: 2 brittle assertions widened to accept new forms

Follow-up issue filed: #1481 (PWA /sw.js whitelist vestige, Opus SF-4)
2026-05-02 17:54:58 +00:00
Dennis Soong 5e806f6fd8 fix: restore inflight session on bfcache pageshow 2026-05-03 01:53:01 +08:00
Hermes Bot 6a26e82c22 fix(bootstrap): address Opus pre-merge review feedback (#1478)
Three changes from the pre-merge Opus review:

**MUST-FIX** — XPC_SERVICE_NAME false-positive on macOS Terminal

macOS launchd sets `XPC_SERVICE_NAME` in EVERY Terminal-spawned shell, not
just real services. Typical noise values: `"0"` (truthy in Python!) and
`"application.com.apple.Terminal.<UUID>"`. A bare `os.environ.get(name)`
existence check would auto-promote interactive `./start.sh` runs to
foreground mode on every Mac dev machine — silently breaking the most
common installation path (no /health probe, no browser open, no log file,
hanging shell).

Fix: new `_is_real_supervisor_value()` helper that filters noise. For
`XPC_SERVICE_NAME` specifically, reject `"0"` and any `"application.*"`
prefix. Real launchd plists use reverse-DNS Label form (`com.<rdns>.<svc>`)
which still triggers correctly.

7 new tests in `TestXPCServiceNameNoiseFilter`:
- 4 noise values (`0`, Terminal.app, iTerm2, VSCode) → no detection
- 3 real Label forms → correct detection
- Mixed env with XPC noise + real INVOCATION_ID → falls through to systemd

**SHOULD-FIX 1** — Test env leakage

The original `clean_env` fixture stripped supervisor-detection env vars
but not the resolved bootstrap vars (HERMES_WEBUI_HOST/PORT/AGENT_DIR)
that `main()` mutates onto `os.environ`. After
`test_foreground_exports_resolved_env_vars` ran, later tests would import
bootstrap with polluted defaults (DEFAULT_HOST="0.0.0.0" instead of
"127.0.0.1"). Existing assertions still passed (tautological vs DEFAULT_*),
but it was a footgun for future tests.

Fix: extend `clean_env` to also `delenv` the three resolved vars before
each test.

**SHOULD-FIX 2** — Pre-execv executability guard

If `discover_launcher_python` returns a path that doesn't exist or isn't
executable, `os.execv` raises OSError → wrapper catches → SystemExit(1)
→ supervisor restarts → loop forever. That's exactly the failure mode
this PR is supposed to eliminate.

Fix: `os.access(python_exe, os.X_OK)` check before execv. Converts
infinite supervisor loop into a single visible RuntimeError.

1 new test in `TestForegroundExecutabilityGuard` pinning that the guard
fires before execv when the python path is non-executable.

**Docs** — supervisor.md updates

- New section explaining the XPC_SERVICE_NAME noise filter and what
  values trigger / don't trigger detection
- New section listing supervisors that are NOT auto-detected (runit,
  daemontools, PM2, Foreman/Honcho, custom shell-script supervisors)
  with explicit recommendation to set HERMES_WEBUI_FOREGROUND=1

Verification

- 3820 tests pass (+9 from this commit's new tests vs the original PR
  push of 3811)
- Filter manually verified end-to-end with the live os.environ:
  XPC=0 → None, XPC=application.* → None, XPC=com.example.foo → triggers
- run-browser-tests.sh ALL CHECKS PASSED on the worktree

Items deferred from the Opus review

- #4 chdir target may not exist: REPO_ROOT comes from __file__.resolve()
  so it's stable; not a real concern in practice
- #6 two startup messages in foreground mode: cosmetic, useful for
  diagnostics
- #7 stricter explicit-only mode: leaves user the override of just not
  passing --foreground (current behavior)
- #8 test stub return value: trivial, can fix later if regression surface
- #9 argparse positional-after-option ordering: test reads fine

These can be follow-up issues if anyone hits them.
2026-05-02 17:52:13 +00:00
youzhi b804b66238 Fix session list pending message payload 2026-05-03 01:44:38 +08:00
Hermes Bot 273888df48 fix(sidebar): nest child sessions under lineage roots (#1450) 2026-05-02 17:41:05 +00:00
Hermes Bot 7c1b53258a feat(api): /api/session/duplicate endpoint for session cloning (#1462) 2026-05-02 17:41:05 +00:00
Hermes Bot 02726b9123 feat(pwa): Android PWA app installation with manifest and icons (#1476) 2026-05-02 17:41:05 +00:00
Hermes Bot f0ed4aaa59 fix(sessions): sync URL after session id rotation (#1395) 2026-05-02 17:41:05 +00:00
Hermes Bot 6303a30a87 Address review feedback: deepcopy independence, persist on duplicate, reset pinned/archived, 404 status
Five fixes from the May 2 2026 maintainer review:

1. messages and tool_calls now use copy.deepcopy() — prior plain assignment
   shared list refs between source and duplicate, so appending a turn to one
   mutated the other.
2. copied_session.save() called explicitly — pre-fix, the duplicate was
   in-memory only until the user sent a turn. Refreshing mid-flow lost it.
3. pinned and archived reset to False — duplicating an archived conversation
   should produce a visible (un-archived) copy.
4. Missing-session error is now status=404 (was default 400).
5. Removed redundant `import uuid` / `import time` inside the handler — both
   are already at the top of routes.py.

Test updates:

- Two existing static-grep tests widened to accept the new
  `copy.deepcopy(session.messages)` form alongside the original
  `messages=session.messages`.
- Five new static-grep regression tests pin each of the five fixes so
  reverting any single one trips a test.

All 3775 tests pass.

Co-authored-by: Alexey Dsov <AlexeyDsov@users.noreply.github.com>
2026-05-02 17:39:55 +00:00
Hermes Bot f84b6a4e2f fix(bootstrap): add --foreground mode for process supervisors (#1458 Bug #1)
Issue #1458 reports persistent-host crashes (≥1/day) when running the WebUI
under launchd KeepAlive on macOS. Root cause: `bootstrap.py` calls
`subprocess.Popen([python, "server.py"], start_new_session=True)`, probes
/health, then exits 0. Under any process supervisor (launchd, systemd,
supervisord, runit, s6), the supervisor sees its tracked PID exit, marks
the program as "completed," and respawns it. The new bootstrap fails to
bind port 8787 (orphaned server still has it), exits non-zero, supervisor
respawns again — loop until the orphan crashes for some other reason and
the next respawn finds the port free.

This PR addresses Bug #1 of the three failure modes tracked in #1458:
the `bootstrap.py` double-fork breaking process supervisors. Bug #2
(state.db FD leak) and Bug #3 (HTTP-unhealthy wedge) remain open under
the same issue — they need diagnosis data before a fix can land.

Changes
-------

1. `bootstrap.py`:
   - New `--foreground` argparse flag with help text mentioning launchd /
     systemd / supervisord.
   - New `_detect_supervisor()` that returns the env var name for any
     supervisor it detects: `INVOCATION_ID` / `JOURNAL_STREAM` /
     `NOTIFY_SOCKET` (systemd, s6), `XPC_SERVICE_NAME` (launchd),
     `SUPERVISOR_ENABLED` (supervisord), or `HERMES_WEBUI_FOREGROUND` for
     the explicit user opt-in. Truthy values for the explicit opt-in:
     `1` / `true` / `yes` / `on` (case-insensitive).
   - `main()` branches on `args.foreground or _detect_supervisor()`:
     - **Foreground path:** chdir to `agent_dir or REPO_ROOT`, then
       `os.execv(python, [python, server_path])` to replace the bootstrap
       process image with the server. The supervisor sees the long-lived
       server as the original child. No `wait_for_health` probe — the
       supervisor's KeepAlive / Restart=on-failure handles liveness.
     - **Default path:** unchanged. Spawn server as detached child via
       `Popen + start_new_session=True`, probe /health, return 0. This
       still works for interactive `bash start.sh` invocations.
   - Resolved env vars (HOST/PORT/STATE_DIR/AGENT_DIR) are now mutated on
     `os.environ` directly instead of into a local `env` copy so they
     are inherited across `os.execv`.

2. `docs/supervisor.md` (new): runnable launchd plist, systemd .service,
   and supervisord conf examples + a diagnostic recipe (`lsof` + ppid
   chain) for catching the orphan-loop in production.

3. `.gitignore`: allowlist `docs/supervisor.md` (the directory uses an
   opt-in pattern; matches the existing `!docs/docker.md` precedent).

4. `tests/test_bootstrap_foreground.py` (new): 35 regression tests
   covering the argparse flag, `_detect_supervisor()` behavior across all
   five supervisor env vars, the explicit opt-in's truthy/falsy values,
   and `main()`'s execv-vs-Popen routing decision under each input
   combination. `os.execv` is monkeypatched in the routing tests — we
   pin the structural choice (which call is made, with which args, in
   which cwd, with which env) not the post-exec behavior.

Why this scope and no more
--------------------------

Bug #2 (state.db FD leak) lists 5 candidate paths and asks the reporter
for `lsof -p <pid> | sort | uniq -c | sort -rn | head -20` output to
disambiguate. Until that data lands, any "fix" would be speculative —
explicitly out of scope per the contributor-pickup comment on the issue.

Bug #3 (launchd-running, port-listening, HTTP-unhealthy) was added in
@stefanpieter's reply comment. Diagnosis is in flight; no concrete fix
shape yet. Also out of scope.

Running locally end-to-end verifies the behavior:

```
[bootstrap] Starting Hermes Web UI on http://127.0.0.1:8789 (foreground mode: --foreground)
$ pgrep -af 'server.py'
2997632 /home/.../python /tmp/wt-fix-1458/server.py
$ ps -o ppid -p 2997632
2997581   ← bash that ran bootstrap.py — same PID as the original bootstrap
$ ps -p 2997581 -o cmd
... bootstrap.py ...   ← but exec'd into server.py
```

The same PID that bash forked for `bootstrap.py` is now `server.py`.
A supervisor watching that PID would correctly observe the long-lived
server. No double-fork.

Verification
------------

- 3811 tests pass (`pytest tests/` — full suite, +51 from this PR plus
  master-merge-in)
- All 35 new bootstrap-foreground tests pass
- `bash scripts/run-browser-tests.sh` PASS (HTTP API checks against worktree)
- `bash scripts/webui_qa_agent.sh 8789` PASS (23/23 visual QA)
- Live verified: server starts cleanly under both `--foreground` and
  `HERMES_WEBUI_FOREGROUND=1`; PID lineage confirms no double-fork

Closes #1458 (Bug #1 only). Bugs #2 and #3 remain tracked under the
issue.
2026-05-02 17:37:54 +00:00
Jan 8e2fea6f5d feature: add manifest and icons to enable app install on android 2026-05-02 19:06:39 +02:00
nesquena-hermes 5650d1107a Merge pull request #1475 from nesquena/stage-267
v0.50.267 — 7 contributor PRs (model ID normalization, navigation, sessions, batch actions) + Opus follow-up
v0.50.267
2026-05-02 10:05:24 -07:00
Hermes Bot 3abae9aca7 chore(release): stamp v0.50.267 — 7 contributor PR batch + Opus follow-up
- CHANGELOG.md: v0.50.267 entry detailing #1454/#1474/#1461/#1465/#1467/#1460/#1473
  + Opus advisor SHOULD-FIX trailing-empty guard for _norm_model_id
- ROADMAP.md: bump to v0.50.267, 3776 tests collected
- TESTING.md: bump header + total to 3776
- api/config.py: trailing-empty fallback in _norm_model_id (parts[-1] or s)
- static/ui.js: mirror trailing-empty fallback in _normalizeConfiguredModelKey
- tests/test_norm_model_id_trailing_empty_guard.py: 5 regression tests
2026-05-02 17:03:25 +00:00
Hermes Bot c517339bce fix(sessions): batch session actions + in-flight reload recovery (#1473) 2026-05-02 16:49:55 +00:00
Hermes Bot 18f6fd14da fix(sessions): handle 401 redirect gracefully in loadSession (#1460) 2026-05-02 16:49:55 +00:00
Hermes Bot daa450a700 fix(sessions): reuse inflight session stream on switch-back (#1467) 2026-05-02 16:49:55 +00:00
Hermes Bot 99c515af52 fix(sessions): rename guard + ondblclick handler (#1465) 2026-05-02 16:49:55 +00:00
Hermes Bot 41b4ecb192 fix(nav): pushState instead of replaceState for chat navigation (#1461) 2026-05-02 16:49:55 +00:00
Hermes Bot 74641f47a2 fix(models): _normalizeConfiguredModelKey frontend parity (#1474) 2026-05-02 16:49:55 +00:00
Hermes Bot 9c893c8bc5 fix(models): _norm_model_id strips multi-segment provider prefixes (#1454) 2026-05-02 16:49:55 +00:00
joaompfp eafda3cebc fix(ui): model dropdown invisible on mobile — anchor fallback to mobile action when desktop chip hidden 2026-05-02 17:30:01 +01:00
happy5318 29a23115bc Fix _normalizeConfiguredModelKey in frontend to match backend behavior
The JavaScript _normalizeConfiguredModelKey function had the same bug as the
Python _norm_model_id function that was fixed in commit d6164cd. It used
substring(indexOf(':')+1) which only removes the first colon-separated segment,
leaving provider names in the normalized model ID.

For example, '@custom:jingdong:GLM-5' became 'jingdong:glm.5' instead of 'glm.5'.

This caused duplicate Primary badges to appear in the model dropdown when using
custom providers with @provider:model ID format.

Changes:
- Replace substring(indexOf(':')+1) with split(':').pop() to strip all colon prefixes
- Add provider name to badge label for clarity (e.g., 'Primary (jingdong)')
2026-05-02 23:13:15 +08:00
youzhi a90e38f033 Fix string i18n placeholder interpolation 2026-05-02 23:05:55 +08:00
youzhi 40d2563d51 Fix batch session actions and inflight reload 2026-05-02 22:45:49 +08:00
Dennis Soong 3aafe52985 test: tighten inflight stream reuse invariants 2026-05-02 22:29:14 +08:00
Dennis Soong 6f0c5d6e1a fix: reuse inflight session stream 2026-05-02 19:12:26 +08:00
AlexeyDsov 384f8fb3f2 Fix session renaming - add ondblclick handler and guard against loading sessions 2026-05-02 13:05:40 +03:00
joaompfp 22fce2fda1 fix(sessions): handle 401 redirect gracefully in loadSession flow
When the webui auth session expires (e.g., after a server restart),
api() returns undefined after redirecting to /login. Previously,
loadSession() and _ensureMessagesLoaded() would dereference the
undefined response and throw, surfacing a confusing 'Failed to load
session' toast while the browser was already navigating away.

Add guards after api() calls that may trigger 401 redirects:
- loadSession(): bail early if data is undefined
- _ensureMessagesLoaded(): return silently if data is missing
- _loadOlderMessages(): return silently if data is missing

This prevents the stuck loading state and unnecessary error toasts
when the user is already being redirected to re-authenticate.

Fixes #1391 (reported as 'Failed to load session' after restart)
2026-05-02 10:49:51 +01:00
AlexeyDsov 7c4c0142d5 feat(api): add /api/session/duplicate endpoint for session cloning\nNew endpoint creates independent session copies with all messages, model and workspace intact. Added 10 comprehensive regression tests for error handling and logic verification. 2026-05-02 11:59:45 +03:00
Josh f80537ad76 fix: use pushState instead of replaceState for chat navigation
Browser back/forward now correctly traverses through each visited chat.
2026-05-02 09:53:59 +01:00
happy5318 d6164cdadb Fix _norm_model_id to properly strip provider prefixes
The _norm_model_id function was using split(':', 1)[1] which only removed
the first colon-separated segment, leaving provider names in the normalized
model ID. For example, '@custom:jingdong:GLM-5' became 'jingdong:glm.5'
instead of 'glm.5'.

This caused the default model injection check to fail, resulting in a
duplicate 'Default' group being added to the model list even when the
model already existed with a provider prefix.

Changes:
- Use split(':')[-1] to get the last segment after all colons
- Use split('/')[-1] consistently for slash-separated paths
- Replace local _norm lambda with _norm_model_id function call

Fixes duplicate Default group appearing in model dropdown when using
custom providers with @provider:model ID format.
2026-05-02 13:40:38 +08:00
nesquena-hermes 4e0dce9a03 Merge pull request #1449 from nesquena/polish-v265-followups
v0.50.264 polish followups: i18n parity + assistant-output readability (closes #1442, #1443, #1446, #1447)
v0.50.266
2026-05-01 21:23:38 -07:00
nesquena-hermes 8f6b9d43dd docs(release): stamp v0.50.266 — CHANGELOG + ROADMAP + TESTING test counts 2026-05-02 04:20:44 +00:00