Commit Graph

563 Commits

Author SHA1 Message Date
Frank Song f32989d5bb fix: voice-mode pref toggle-off now stops the recognizer (#1491)
When a user disables 'Hands-free voice mode' in Settings while voice
mode is active, the button hides but the SpeechRecognition keeps
running — the user can't stop it because the button is invisible.

Fix: _applyVoiceModePref() now checks if voice mode is active and
calls _deactivate() when the pref is toggled off. Move
_voiceModeActive declaration above the function to avoid TDZ.

Also removes a duplicate window._applyVoiceModePref assignment.
2026-05-03 15:03:17 +08:00
nesquena-hermes 4fea813adc fix(sw-cache): version style.css link so old SW cannot return stale CSS (#1507)
Container restart / in-place upgrade left the previous service worker still
controlling open tabs. Its fetch handler intercepted 'static/style.css',
matched the unversioned URL exactly against its old shell cache, and returned
the OLD CSS — while the JS files (which already carry ?v=__WEBUI_VERSION__)
hit the cache as misses and loaded fresh from network. New JS + old CSS
broke the layout until a force refresh bypassed the SW.

Fix is a 1-line attribute change plus aligning the SW pre-cache list:

* static/index.html: add ?v=__WEBUI_VERSION__ to the style.css link, matching
  the pattern already in use for every JS file in the page.
* static/sw.js: add the same ?v=__CACHE_VERSION__ suffix to every versioned
  entry in SHELL_ASSETS so that pre-cache URLs match what the page actually
  requests. Unversioned entries (root, manifest, favicons) stay unversioned.

Tests:

* New regression test_index_versions_stylesheet (lock the href) and
  test_sw_shell_assets_match_versioned_asset_urls in test_pwa_manifest_sw.py.
* test_workspace_panel_preload_marker_restored_in_head in test_sprint37.py
  loosened to match the css link prefix (preserves the ordering invariant).

Verified live on port 8789: served HTML carries
'static/style.css?v=v0.50.275-dirty' and SW SHELL_ASSETS receive the
matching VQ at request time.

Closes #1507.
2026-05-03 06:09:47 +00:00
Hermes Bot 8f4692b8cf fix(onboarding): allow keyless setup for self-hosted providers (#1499 third sub-bug)
Pre-fix, the wizard rejected an empty api_key for every provider in
_SUPPORTED_PROVIDER_SETUPS — including lmstudio, ollama, and custom,
which run keyless on the vast majority of local installs. The agent's
LMSTUDIO_NOAUTH_PLACEHOLDER substitution at chat-time was the workaround
for the no-auth case, but the wizard side rejected the empty input first.
Users had to type random gibberish into the API key field to clear the
form — the third sub-bug from #1420 that the prior commit's PR description
explicitly punted to a follow-up.

Surfaced by Nathan during PR review: "I think it's too weird for users
to have to type a string into the API key field, right?"  Yes — and the
probe (#1499) makes the cleanest fix strictly better: we accept empty
keys, and the probe gives instant feedback ("Connected. 2 model(s)
available." for keyless servers, "401" for auth-required servers).

Backend changes
---------------

* `api/onboarding.py` — `_SUPPORTED_PROVIDER_SETUPS` gains
  `key_optional: True` for `lmstudio`, `ollama`, `custom`. Cloud
  providers (openrouter, anthropic, openai, gemini, deepseek, …)
  remain key_required.

* `apply_onboarding_setup` skips the "{env_var} is required" check
  when `key_optional` is set AND no key is supplied. No write to .env
  for the empty-key case (no `LM_API_KEY=*** placeholder lying in the
  user's .env`).

* `_status_from_runtime` reports `provider_ready=True` for key_optional
  providers based on `requires_base_url` alone, so the wizard doesn't
  refire on the next page load just because there's no api_key. Cloud
  providers still need a key for provider_ready=True.

* `_build_setup_catalog` exposes the `key_optional` flag to the frontend.

Frontend changes
----------------

* `static/onboarding.js` — new `_renderOnboardingApiKeyField()` helper.
  For key_optional providers:
    - Label: "API key (optional)"
    - Placeholder: "Leave blank for keyless servers"
    - Inline italic muted help: "Most LM Studio / Ollama / vLLM installs
      run keyless — leave this blank if your server doesn't require
      authentication. Use the Test connection button to verify."
  For cloud providers: unchanged (label "API key", standard placeholder,
  no help block).

* The api-key input also now triggers `_scheduleOnboardingProbe()` on
  oninput, so changing the key re-runs the probe — handles "the server
  rejected my empty key with 401, let me add one and retry."

* `static/i18n.js` — 3 new keys × 9 locales (canonical English in `en`,
  English fallback with `// TODO: translate` markers in the other 8).

* `static/style.css` — `.onboarding-api-key-help` rule for the muted
  italic helper paragraph.

Verified end-to-end on port 8789
--------------------------------

Spun up an isolated test server + a mock LM Studio at
`127.0.0.1:11234/v1/models`. Stepped through the wizard:

* Picked LM Studio → field label flipped to "API key (optional)",
  placeholder showed "Leave blank for keyless servers", help text
  rendered in italic muted gray below.
* Switched to Anthropic → label reverted to "API key", help text
  disappeared. Visual hierarchy correct.
* Left api_key blank, set base_url to the mock, clicked Test connection
  → green "Connected. 2 model(s) available." banner. Probe-discovered
  models populated the workspace-step dropdown.
* Continued through to the finish step. config.yaml written with
  provider/model/base_url. **`.env` does NOT exist** — no placeholder
  string written. `chat_ready: true`, `state: ready`.
* Vision tool confirmed the visual hierarchy: subtle italic help
  reads as documentation, prominent green banner pops as status.

Tests
-----

`tests/test_issue1499_keyless_onboarding.py` — 16 tests in 3 classes:

  TestKeyOptionalProviderSchema (5)
    - lmstudio / ollama / custom declare key_optional=True
    - openrouter / anthropic / openai do NOT (regression defense)
    - setup catalog exposes the flag

  TestKeylessOnboarding (6)
    - lmstudio / ollama / custom: empty api_key accepted, no .env write
    - openrouter / anthropic: empty api_key still rejected
    - lmstudio with explicit key still writes .env (regression defense)

  TestKeylessChatReady (5)
    - lmstudio / ollama: provider_ready=True with no key
    - custom: provider_ready=True with key+base_url, False without base_url
    - openrouter: provider_ready=False with no key (regression defense)
    - End-to-end get_onboarding_status reports chat_ready=True

Full suite: 3901 → 3917 passing (+16 from this commit; +22 cumulative
from the PR's earlier commit). 0 failures.

Closes #1499 (all three sub-bugs from #1420 now addressed)
2026-05-03 03:07:07 +00:00
Hermes Bot 8616033605 fix(onboarding,providers): probe LM Studio /models + align env var with agent CLI (#1499 #1500)
Addresses both #1499 (onboarding wizard never probes the configured base URL)
and #1500 (cross-tool env-var name divergence between webui and agent CLI).
Surfaced together because they're both LM-Studio onboarding bugs that pile
on top of each other — fixing only one leaves the broken UX.

#1499 — Onboarding wizard probes <base_url>/models before persisting

Pre-fix, `apply_onboarding_setup` accepted whatever `base_url` the user typed
without ever fetching `<base_url>/models`. @chwps's log timeline in #1420
showed the wizard finishing in 239ms with zero outbound HTTP — onboarding
silently persisted unreachable URLs and left users with empty model
dropdowns they had to populate by hand-editing config.yaml.

Backend:
* New `probe_provider_endpoint(provider, base_url, api_key, timeout=5.0)`
  in `api/onboarding.py`. Stdlib-only (urllib + socket — no httpx dep).
  Returns `{ok, models}` on success; `{ok: False, error: <code>, detail}`
  on failure with stable error codes the frontend can switch on:
  invalid_url, dns, connect_refused, timeout, http_4xx, http_5xx, parse,
  unreachable. 256 KB response cap and 5s timeout keep a hostile or mis-
  pointed endpoint from blocking the wizard.
* New `POST /api/onboarding/probe` route — thin JSON wrapper around the
  function above. Same local-network gate as `/api/onboarding/setup`
  because the body carries an `api_key` the user typed.
* The probe response is NEVER persisted. Only the user's typed selection
  ends up in config.yaml; the probed model list just populates the
  wizard's dropdown.
* SSRF: deliberately does NOT block private-IP ranges. The wizard is
  gated behind WebUI auth and the legitimate target IS a local LM Studio
  / Ollama / vLLM server. A "block private IPs" SSRF defense would make
  the feature useless for its primary use case.

Frontend:
* `static/onboarding.js`:
  - New `ONBOARDING.probe` state ({status, error, detail, models, probedKey}).
  - `_runOnboardingProbe()` — POSTs to /api/onboarding/probe, idempotent
    & cached on (provider, baseUrl, apiKey).
  - Debounced (400ms) on `oninput` of the base URL field.
  - Explicit "Test connection" button.
  - `nextOnboardingStep` blocks Continue at the setup step for any
    provider with `requires_base_url=True` until the probe succeeds.
    Same localized error renders inline.
* `static/i18n.js`: 13 new keys × 9 locales (canonical English in `en`,
  English fallback with `// TODO: translate` markers in the other 8 —
  same convention as v0.50.271 #1488 voice-buttons).
* `static/style.css`: probe banner + Test button styling (red-tinted
  error variant, green-tinted success variant, neutral probing state).

Verified via manual repro on port 8789:
* connect_refused → red banner, helpful "from Docker, try the host IP"
  hint, blocks Continue.
* DNS failure → red banner, "could not resolve host '...'", blocks Continue.
* Success against a mock /v1/models server → green banner, model dropdown
  populates from the probed list, Continue advances normally.

#1500 — webui env var aligned with agent CLI (LM_API_KEY)

The webui has long used `LMSTUDIO_API_KEY` for LM Studio's API key in
both onboarding and Settings detection. The agent CLI runtime
(hermes_cli/auth.py:177-183) reads `LM_API_KEY`. So a user who configured
auth on their LM Studio instance got Settings → Providers reporting
has_key=True (because webui saw its own LMSTUDIO_API_KEY) but the agent
runtime ignored the key and fell back to LMSTUDIO_NOAUTH_PLACEHOLDER →
401 against the auth-enabled LM Studio server. Masked in practice for
the no-auth majority.

Picked Option B from the issue (defer to the agent — single source of
truth) but mitigated the migration cliff by reading the legacy name as
a fallback:

* `api/onboarding.py:_SUPPORTED_PROVIDER_SETUPS["lmstudio"]`:
  - `env_var: "LM_API_KEY"` (canonical, what onboarding writes going forward).
  - `env_var_aliases: ["LMSTUDIO_API_KEY"]` (read-only fallback for
    pre-#1500 users so detection keeps working without forcing an
    .env rewrite).
* `api/onboarding.py:_provider_api_key_present` reads aliases too.
* `api/providers.py:_PROVIDER_ENV_VAR["lmstudio"] = "LM_API_KEY"`.
* `api/providers.py:_PROVIDER_ENV_VAR_ALIASES["lmstudio"] = ("LMSTUDIO_API_KEY",)`
  — new dict, used by `_provider_has_key` and `get_providers`'s
  key_source resolution. Drops in cleanly when other providers later
  rename their env vars too.

Verified:

```
before fix:  webui writes LMSTUDIO_API_KEY → agent ignores it → 401 on chat
 after fix:  webui writes LM_API_KEY → agent picks it up → chat works
             pre-#1500 .env with LMSTUDIO_API_KEY → still has_key=True in Settings
                                                  → key_source='env_file'
```

Tests

* `tests/test_issue1499_onboarding_probe.py` — 17 tests:
  3 invalid_url variants, dns, connect_refused, success (OpenAI shape),
  success (bare-list shape), http_4xx, http_5xx, parse non-JSON, parse
  wrong-shape, api_key authorization header passthrough, "probe must
  not write to config.yaml or .env", PROBE_ERROR_CODES contract pin,
  3 end-to-end route-level smoke tests against the live server fixture.
* `tests/test_issue1500_lmstudio_env_var_alignment.py` — 5 tests:
  onboarding declares LM_API_KEY canonical with LMSTUDIO_API_KEY alias,
  onboarding writes ONLY the canonical name, legacy env var still
  detected post-migration, canonical takes precedence when both are
  set, _provider_api_key_present reads aliases.
* `tests/test_issue1420_lmstudio_provider_env_var.py` — updated:
  the original 5-test #1420 suite now pins LM_API_KEY as canonical
  and LMSTUDIO_API_KEY as alias.

Full suite: 3879 → 3901 passing (+22), 0 failures.

Out of scope (explicitly NOT addressed here)

The third LM Studio onboarding sub-bug from #1420's thread — that
`apply_onboarding_setup` requires a non-empty api_key for lmstudio
even though most LM Studio installs run keyless — remains. The agent's
`LMSTUDIO_NOAUTH_PLACEHOLDER` substitution kicks in at runtime, but
the onboarding wizard rejects the empty-key case at submit. Fixing
this requires a UX decision (auto-write a sentinel? loosen the
required-key check for self-hosted providers?) and is left as a
separate follow-up.

Closes #1499
Closes #1500

Co-authored-by: chwps <106549456+chwps@users.noreply.github.com>
Co-authored-by: AdoneyGalvan <25235323+AdoneyGalvan@users.noreply.github.com>
2026-05-03 02:46:24 +00:00
Hermes Bot c4ea9643f9 Stage 272: PR #1492 — P0 bugfixes (tool-card args + CLI rename + scroll pinning + sw.js relative-path regression test) 2026-05-03 01:34:10 +00:00
bergeouss 6d17e55688 fix: revert sw.js to relative path + add regression test
- Revert '/sw.js' back to relative 'sw.js' in serviceWorker.register()
  (static/index.html:50). The dynamic <base href> script resolves
  relative paths correctly for both root and subpath mounts.
  Absolute path breaks reverse-proxy installs at e.g. /hermes/.

- Add regression test test_index_sw_registration_uses_relative_path
  to prevent future absolute-path rewrites from silently breaking
  subpath-mount installs.

Addresses reviewer feedback on PR #1492 (review by @nesquena).
2026-05-03 01:29:41 +00:00
Dennis Soong cbb251b823 fix: add sidebar cancel for running sessions 2026-05-03 08:46:36 +08:00
bergeouss 24a5457471 fix: P0 bugfixes — tool-card args, sw.js path, CLI rename, scroll pinning
- #1481: Use absolute path for service worker registration to avoid
  <base> tag resolution on session pages causing JSON 404
- #1484: Fix tool-card expanded args readability — replace
  word-break:break-all with pre-wrap+break-word, add display:block
  so newlines and indentation are preserved
- #1486: Prefer WebUI JSON title over state.db title for CLI sessions,
  fixing rename-not-persisting after compression chain extension
- #1469/#1360: Add _programmaticScroll guard to distinguish
  programmatic scrolls from user scrolls, preventing the race
  condition where scrollIfPinned() re-pins after user scrolls up
2026-05-02 23:39:52 +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
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
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
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
Jan 8e2fea6f5d feature: add manifest and icons to enable app install on android 2026-05-02 19:06:39 +02: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
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
nesquena-hermes c73f2ff387 v0.50.264 polish followups: i18n parity + assistant-output readability
Closes #1442 (server-side _LOGIN_LOCALE missing ja/pt/ko)
Closes #1443 (promote _isImeEnter helper to 6 other Safari Enter guards)
Closes #1446 (glued-bold-heading lift for LLM thinking-block output)
Closes #1447 (markdown heading visual hierarchy in chat messages)

All four issues were filed by the Opus pre-release advisor on the v0.50.264 batch
or by Cygnus via Discord (relayed by @AvidFuturist, May 1 2026). They share a
common shape — narrow, well-scoped, independent of each other, all adding
regression tests.

== #1442: _LOGIN_LOCALE parity (api/routes.py + static/i18n.js) ==

Added entries for ja/pt/ko to the server-side _LOGIN_LOCALE dict that renders
the localized login page BEFORE the JS i18n bundle loads. With v0.50.264
shipping Japanese as the 8th built-in locale, ja/pt/ko users were seeing the
English login page even with their language preference set.

While auditing static/i18n.js for English leakage, also fixed:
  - ko: 10 user-facing login/sign-out/password keys still in English
  - es: 3 sign-out/auth-disabled keys still in English

Tests: tests/test_login_locale_parity.py (20 tests) — pins both invariants:
  (a) every locale in i18n.js LOCALES has a matching _LOGIN_LOCALE entry
  (b) every locale's login-flow keys (13 of them) are translated, not English

== #1443: window._isImeEnter promotion ==

PR #1441 fixed the Safari IME-composition Enter race in the chat composer
(`#msg`) by widening the guard from `e.isComposing` to a `_isImeEnter(e)`
helper that combines three signals (isComposing || keyCode===229 ||
_imeComposing flag). Six other Enter-input handlers were left on the original
narrow guard and would still drop IME composition Enters on Safari for
Japanese/Chinese/Korean users.

Promoted the helper to `window._isImeEnter` (defined in static/boot.js) and
replaced the `e.isComposing` guards at all six sites:

  - static/sessions.js: session rename, project create, project rename
  - static/ui.js: app dialog (confirm/prompt), message edit, workspace rename

The state-free part of the helper (`isComposing || keyCode===229`) handles
Safari's race for any focused input without needing per-input composition
listeners — only `#msg` keeps the local `_imeComposing` flag.

Tests:
  - tests/test_issue1443_ime_helper_promotion.py (9 tests) — pins each site
    + verifies no raw `e.isComposing` Enter-guards remain in sessions.js/ui.js
  - tests/test_ime_composition.py — alternation regex extended to accept
    the windowed helper form (loosen-test-on-shape-change pattern from
    v0.50.264 reflection notes)

== #1446: glued-bold-heading lift (static/ui.js renderMd + Python mirror) ==

LLMs in thinking/reasoning mode emit "section headers" glued to the end of the
previous paragraph with no whitespace:

    Para 1 text.**Heading to Para 2**

    Para 2 text.**Heading to Para 3**

The renderer correctly produces inline `<strong>` per CommonMark, but it looks
like trailing emphasis on the body text rather than a section break. Cygnus
reported this as "Markdown feedback 2 of 3."

Added a single regex pre-pass in renderMd():

    s.replace(/([.!?])\*\*([^*\n]{1,80})\*\*\n\n/g, '$1\n\n**$2**\n\n')

Constraints chosen to avoid false positives:
  - Trigger only on `[.!?]` IMMEDIATELY before `**` (no space) — almost always
    an LLM-glued heading, not intentional emphasis
  - Inner text ≤80 chars, no `*` or newline (single-line only)
  - Trailing `\n\n` required — preserves "this is **important** to know."
    mid-paragraph emphasis untouched
  - Position: after rawPreStash restore, before fence_stash restore — fenced
    code blocks stay protected (their content is `\x00P` / `\x00F` tokens
    when the lift runs)

Mirrored in tests/test_sprint16.py render_md() so both stay in sync.

Tests: tests/test_issue1446_glued_heading_lift.py (17 tests, 5 of which drive
the actual ui.js renderMd via node) — covers all 3 trigger forms (.!?), all 4
preserve-emphasis cases the issue spec'd, fenced/inline code protection,
chained glued headings, source-level position pin, regex shape pin.

== #1447: markdown heading visual hierarchy (static/style.css) ==

Pre-fix sizes in `.msg-body`:
  h1 18px, h2 16px, h3 14px (= body), h4 13px, h5 12px, h6 11px

So h3 was indistinguishable from body and h4/h5/h6 were SMALLER than body.
Cygnus's report: "Markdown feedback 3 of 3 — Headings seem to be missing
across the board in Hermes. They're there, but all plaintext."

New sizes:
  h1 24px (border-bottom)  h2 20px (border-bottom)  h3 17px  h4 15px
  h5 14px (uppercase, tracked)  h6 13px (uppercase, tracked, muted)

All headings now `font-weight:700` + `color:var(--strong)` for stronger ink.
h5/h6 use uppercase + letter-spacing for "label-style" affordance instead
of being smaller-than-body.

Synced .preview-md (file preview pane) to match exactly so a markdown file
preview and a chat message render identically. Added missing h4/h5/h6 rules
to .preview-md (it only had h1-h3 before).

Updated data-font-size="small"/"large" h1-h6 overrides to scale
proportionally with the new defaults. Hierarchy preserved at all three
font-size settings.

Tests: tests/test_issue1447_heading_hierarchy.py (9 tests) — pins the size
hierarchy, the bottom borders on h1/h2, the uppercase affordance on h5/h6,
the .preview-md sync, and the small/large override scaling.

== Verification ==

  pytest tests/ -q                                  → 3748 passed (+56 new)
  bash ~/WebUI/scripts/run-browser-tests.sh         → 20 + 11 PASS
  bash ~/WebUI/scripts/webui_qa_agent.sh 8789       → 23/23 PASS

Visual confirmation in browser at port 8789:
  - Heading hierarchy clearly visible at all 6 levels
  - Glued-bold lift produces separate paragraphs as designed
  - window._isImeEnter accessible from any module after boot.js
  - Login page renders ja/pt/ko strings correctly (curl -s /login)
2026-05-02 04:19:28 +00:00
Dennis Soong 082f3d45b7 fix: nest child sessions under lineage roots 2026-05-02 12:09:36 +08:00
nesquena-hermes e6e9868625 Opus pre-release follow-up: blur resets _imeComposing flag
Opus advisor caught a recoverable footgun in PR #1441's manual flag: if
focus is lost mid-composition (window blur or older Safari WebKit IME
quirk), compositionend may never fire and _imeComposing stays true
until the next full composition cycle. Result: Enter-to-send is
silently broken until page reload — an unrecoverable stuck state for
something that's supposed to be transient.

Add a blur listener that also resets the flag. Cheap belt-and-suspenders
against the stuck state. Adds 1 regression test pinning the listener.

(other Opus findings logged in /tmp/stage-264-brief.md as follow-up
issues: _LOGIN_LOCALE parity for ja/pt/ko, promote _isImeEnter to the
6 other Safari-affected Enter guards in sessions.js + ui.js)
2026-05-02 02:56:48 +00:00
nesquena-hermes cad2d1c0aa Merge PR #1439: feat: add Japanese (ja) locale 2026-05-02 02:42:56 +00:00
nesquena-hermes 641da8b9cc Merge PR #1441: Fix IME composition Enter (East Asian input) 2026-05-02 02:42:49 +00:00
Dennis Soong 9e894a2555 fix: sync URL after session id rotation 2026-05-02 10:35:40 +08:00
nesquena-hermes 584974c9d2 fix(renderer): line-anchor fence regex to prevent mid-line ``` corruption (#1438)
The markdown fence regex /```([\s\S]*?)```/g had no line anchoring. A literal
triple backtick inside code block content (e.g. a regex with ``` in a lookbehind,
or a script that documents fences) terminated the outer fence at the wrong place.
The leaked tail then went through bold/italic/inline-code passes, eating `*`
characters as italic markers and emitting literal </strong> tags into the
rendered output.

CommonMark §4.5 requires that an opening code fence be the first non-whitespace
content of a line (up to 3 spaces of indent allowed) and that the closing fence
also start a line. This patch updates 3 sites + the Python mirror to use that
invariant:

  static/ui.js:1559  renderMd() fenced-block stash (assistant messages)
  static/ui.js:66    _renderUserFencedBlocks() (user messages)
  static/ui.js:2599  _stripForTTS() (TTS speech pre-strip)
  tests/test_sprint16.py  Python mirror

Pattern: (^|\n)[ ]{0,3}```(?:([\s\S]*?)\n)?[ ]{0,3}```(?=\n|$)

The non-capturing (?:...\n)? group keeps empty fences (```\n```) working;
without it, a body+\n is required and the closing fence on the very next line
no longer matches. The lead group (^|\n) is prefixed back to the stash token
so paragraphs above don't bleed into the <pre> block.

20 regression tests in tests/test_issue1438_fence_anchoring.py cover:
- Cygnus's exact repro from Discord (May 1 2026)
- Inline ``` mid-paragraph (must not open fence)
- Partial/streaming fence with no close (must not eat content)
- Empty fences with and without language tag
- 3-space indented fences (allowed) vs 4-space (not a fence)
- Multiple adjacent blocks
- Bold/italic/inline-code surviving after a fence
- Source-level guards on all 3 patched sites + lead-prefix invariant

Empirical browser verification (live JS, on bug repro):
  Before fix:  </code></pre>[^\n]<em>|%%[ \t]</em>...   ← truncated, italic leak
  After fix:   <pre><code>...```[^\n]*|%%...</code></pre>  ← intact, regex preserved

Tests: 3678 passed (+20 from new test file, was 3658), 0 failures.

Reported-By: Cygnus (Discord)
Relayed-By: @AvidFuturist
Closes #1438
2026-05-02 02:30:20 +00:00
snuffxxx 14da297cd6 feat: add Japanese (ja) locale to i18n.js
Adds a ja locale entry (828 keys) under static/i18n.js LOCALES,
inserted between en and ru. All existing keys translated to natural
concise Japanese suitable for UI labels, with placeholders ({0}, etc.)
and template literals preserved verbatim.

- _lang: 'ja', _label: '日本語', _speech: 'ja-JP'
- 828 keys (matches en, including the documented duplicate keys
  whose JS last-wins semantics are preserved)
- syntax verified with `node -c static/i18n.js`

Tested live on a self-hosted instance; Settings → Language → 日本語
selects the new locale and switches the UI text.
2026-05-02 11:21:20 +09:00
RZ 39c99b015a Fix IME composition Enter sending message prematurely
East Asian IMEs (Japanese/Chinese/Korean) use Enter to commit composition.
The existing isComposing guard misses Safari, where the committing keydown
fires after compositionend with isComposing=false. Also track composition
manually and check keyCode===229 for broader coverage.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 11:12:14 +09:00
nesquena-hermes 081e600b33 fix: context-window indicator broken on older sessions (#1436)
Fix two-layer bug where `/api/session` returned `context_length=0` for
sessions that pre-date #1318, then the frontend silently fell back to
cumulative `input_tokens` and the 128K JS default, producing nonsense
indicators like "100" capped from "890% used (context exceeded), 1.2M
/ 131.1k tokens used".

Empirical impact: 23 of 75 sessions on dev server rendered >100% before
this fix. #1356 fixed the same symptom on the live SSE path but missed
the GET /api/session load path that older sessions go through.

Two-layer fix:
  1. Backend (api/routes.py:1295-1313) — resolve context_length via
     agent.model_metadata.get_model_context_length() when the persisted
     value is 0. Mirrors api/streaming.py:2333-2342.
  2. Frontend (static/ui.js:1269) — drop the cumulative `input_tokens`
     fallback. When last_prompt_tokens is missing, render "·" + "tokens
     used" (existing !hasPromptTok branch) instead of computing a
     percentage from the cumulative total.

10 regression tests in tests/test_issue1436_context_indicator_load_path.py
covering both layers + the empty-model edge case (avoids the 256K
default-for-unknown-model trap that get_model_context_length('') returns).

Verified live: claude-opus-4-7 session with input_tokens=5,226,479 now
renders "·" + "5.3M tokens used" instead of "100" + "3987% used".

Reported by @AvidFuturist.
Closes #1436.
2026-05-02 01:43:00 +00:00
nesquena-hermes 26d0f45791 fix: new-chat guard ignores in-flight streams (#1432) + profile form auto-capitalizes typed values (#1423)
Two unrelated UX bugs, both small surgical fixes with regression tests.

Issue #1432 — "+" button doesn't open new chat during streaming
================================================================
Reported by @Olyno: clicking "+" after sending a first message keeps
redirecting to the same chat instead of opening a new blank conversation,
making parallel chats impossible until the first response finishes.

Root cause:
  static/boot.js:691 (and the Cmd/Ctrl+K branch at :844) had an empty-session
  guard from #1171 that skipped newSession() when message_count===0:

    if(S.session && (S.session.message_count||0)===0){
      $('msg').focus(); closeMobileSidebar(); return;
    }

  But during the first user turn of a brand-new session, message_count is
  still 0 server-side because the user message hasn't been merged into
  s.messages yet. The guard treated that as "empty" and silently dropped
  the click, blocking parallel chats for the entire stream duration.

Fix:
  Tighten the predicate to also exclude in-flight state:

    if(S.session
       && (S.session.message_count||0)===0
       && !S.busy
       && !S.session.active_stream_id
       && !S.session.pending_user_message){
      $('msg').focus(); closeMobileSidebar(); return;
    }

  Same predicate applied to the Cmd/Ctrl+K handler at :844. The in-flight
  signal (active_stream_id || pending_user_message) is the same one
  _restoreSettledSession() in messages.js:1081 already uses to decide
  whether a session is "settled" — keeping both call sites aligned.

  Verified end-to-end: with S.busy=true and pending_user_message set, the
  old guard returned `block=true` (= the bug), the new guard returns
  `block=false` (= fixed). With a truly empty session (no busy, no pending),
  both old and new guards still block — preserving #1171 behavior.

Issue #1423 — Profile name field auto-capitalizes typed values
==============================================================
Self-reported (Mac app, May 1 2026): typing `hello` into the New Profile
"Name" field shows `Hello` after blur/autofill, contradicting the
"Lowercase letters, numbers, hyphens, underscores only" hint right next
to it. The form lowercases on submit so stored data is correct, but the
displayed value during typing is misleading.

Root cause:
  static/panels.js:2532 had only autocomplete="off":

    <input type="text" id="profileFormName"
           placeholder="..." autocomplete="off" required>

  Missing three attributes that actually prevent the misbehavior:
  - autocapitalize="none" — mobile keyboards (iOS Safari, Android Chrome,
    WKWebView in the Mac app) auto-capitalize the first letter without it
  - autocorrect="off" — Safari runs autocorrect on blur, can rewrite hello→Hello
  - spellcheck="false" — desktop browsers may run spellcheck on blur

Fix:
  Add the three attributes to profileFormName. Also added to
  profileFormBaseUrl since URLs are similarly bad targets for
  autocapitalize/autocorrect. profileFormApiKey is type="password" and
  already has correct browser behavior.

  Verified end-to-end against the live DOM: openProfileCreate() →
  getElementById('profileFormName').getAttribute(...) returns the new
  attributes correctly, with required preserved.

Tests
-----
3648 passed, 2 skipped, 3 xpassed (was 3640 — added 8 new regression tests
in test_1432_newchat_and_1423_profile_input.py).

One pre-existing test had to be widened: tests/test_mobile_layout.py
test_new_conversation_closes_mobile_sidebar grabbed only the first 500
chars of the btnNewChat handler block to scan for closeMobileSidebar.
The new comment block pushed closeMobileSidebar past that window even
though both calls are still present. Bumped the window to 1500 chars
and the shortcut-block lines from 12 to 24 to match the multi-line guard.

Closes #1432
Closes #1423

Reported by @Olyno (#1432, GitHub)
2026-05-02 00:52:41 +00:00
nesquena-hermes 8ceeef3716 Apply Opus pre-release fixes: dropdown resize guard + display:block
Three fixes from Opus advisor review of stage-261:

1. CRITICAL: dropdown-survives-resize bug. The composerToolsetsDropdown is a
   DOM sibling of composerToolsetsWrap, not a child, so CSS hiding the wrap
   does not cascade-hide an open dropdown. If a user opens the dropdown at
   composer-footer >= 1100px and then opens the workspace panel (or resizes
   the window), the dropdown would stay open without a visible anchor.

   Fixed in three places (defense-in-depth):
   - resize listener: closes dropdown when chip.offsetParent === null
   - _positionToolsetsDropdown: closes if chip hidden (defense-in-depth)
   - toggleToolsetsDropdown: early-returns if chip hidden (defense against
     future #1431 redesign code that might invoke from elsewhere)

2. MEDIUM: display:flex changed to display:block to match sibling wraps
   (.composer-profile-wrap, .composer-model-wrap, .composer-reasoning-wrap
   all use the natural block display).

3. Added 3 new regression tests to pin all three guards.

Refs #1431, #1433.
2026-05-02 00:21:15 +00:00
nesquena-hermes a6884ca40f Make composer-footer toolsets chip responsive instead of always-hidden
Replaces PR #1433 unconditional JS display:none with a CSS @container query
that shows the chip only at composer-footer widths >= 1100px. JS now clears
inline style instead of setting display:none, so the CSS responsive cascade
is the single source of truth. Also removed inline style=\"display:none\" from
index.html so the CSS base rule provides the default-hidden state.

10 regression tests pin the base hide, wide-container show, narrow-container
hide (520px container query), mobile viewport hide (640px @media), JS does
not force display:none, JS clears inline style, /api/session/toolsets and
the dropdown machinery (toggleToolsetsDropdown, _populateToolsetsDropdown)
are preserved.

Refs #1431, #1433.
2026-05-02 00:04:12 +00:00
Hermes Agent 4f50cb2511 Reference correct issue number (#1431) in comment + CHANGELOG 2026-05-01 23:47:46 +00:00
Hermes Agent 4adbb5ebee Hide composer-footer toolsets chip (cramped layout)
The session-toolsets restriction chip (#493) was making the composer
footer too cramped on narrower widths once it was sharing space with
model, reasoning effort, profile, and context-usage indicators.

Surgical fix: `_applyToolsetsChip()` now sets the wrap to display:none
unconditionally. Underlying state and the /api/session/toolsets endpoint
still work, so any cron job or scripted client that relies on
`enabled_toolsets` continues unaffected. To be revisited when the
footer layout is redesigned (#1430).
2026-05-01 23:47:13 +00:00