Commit graph

1044 commits

Author SHA1 Message Date
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
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
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)
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
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
nesquena-hermes
0ed6103f1e
Merge pull request #1448 from nesquena/stage-265
release: v0.50.265 — opt-in WebUI extension hooks
2026-05-01 20:53:08 -07:00
nesquena-hermes
4ee9368464 Opus pre-release follow-ups for PR #1445
REQUIRED:
- _fully_unquote_path range(3) -> range(10) — defense-in-depth so quadruple-
  encoded .. is rejected by validator instead of slipping through (not
  exploitable but contract violation)
- docs/EXTENSIONS.md trust-model callout moved to top of file with explicit
  'don't enable in untrusted env / don't point at user-writable dir' guidance

NICE-TO-HAVE (taken since Nathan asked for all fixes big and small):
- URL list cap at _MAX_URL_LIST=32 to avoid pathological rendering
- One-shot WARNING log for rejected URLs (silent drop now visible to admin)
- One-shot WARNING log for URL list truncation
- MIME map: ttf (font/ttf), otf (font/otf), wasm (application/wasm)

5 regression tests in tests/test_pr1445_opus_followups.py pin all invariants.
2026-05-02 03:49:40 +00:00
nesquena-hermes
73cb3c1948 stage-265: test fix + CHANGELOG for v0.50.265 2026-05-02 03:42:58 +00:00
nesquena-hermes
3de70c52fb Merge PR #1445: feat: add opt-in WebUI extension hooks 2026-05-02 03:42:01 +00:00
Ryan Jones
9de61a0b9a feat: add opt-in webui extension hooks 2026-05-02 03:36:54 +00:00
nesquena-hermes
fb66ba5e10
Merge pull request #1444 from nesquena/stage-264
release: v0.50.264 — ja locale, IME Safari fix, fence regex anchoring
2026-05-01 20:11:08 -07: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
241bdafd28 test: bump locale-count assertions for new ja locale (8 -> >=8/9) 2026-05-02 02:50:40 +00:00
nesquena-hermes
7027c6a50b docs: v0.50.264 release notes 2026-05-02 02:46:16 +00:00
nesquena-hermes
71cf06cd1c test: pr1441 IME helper guards + pr1439 ja locale parity
- Loosen test_ime_composition._ime_guarded_enter_pattern to accept the
  new _isImeEnter(e) helper (PR #1441 widened guard for Safari + 229 keyCode
  + manual _imeComposing flag). Original e.isComposing-only pattern still
  matches via alternation.
- Add test_pr1441_ime_safari_guard.py (6 tests): pin the 3-guard helper,
  compositionstart sets manual flag, compositionend defers reset to next
  tick (Safari race), null-guard $('msg') for non-chat pages, send-Enter
  uses helper, dropdown-Enter uses helper.
- Add test_japanese_locale.py (8 tests): mirror Chinese/Korean templates,
  block exists, representative translations, full key parity with English,
  no extra keys, duplicates mirror en exactly, placeholders preserved,
  arrow-function values mirrored, _label uses Japanese script.
2026-05-02 02:44:59 +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
nesquena-hermes
e6ee89d3d9 Merge PR #1440: fix(renderer): line-anchor fence regex (#1438) 2026-05-02 02:42:42 +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
9d0d86be5f
Merge pull request #1437 from nesquena/fix/issue-1436-context-indicator-load-path
fix: context-window indicator broken on older sessions (#1436)
2026-05-01 18:54:13 -07:00
nesquena-hermes
51552e849a docs: v0.50.263 release notes and version bump 2026-05-02 01:52:49 +00: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
c8f2daa990
Merge pull request #1435 from nesquena/fix/profile-autocapitalize-and-newchat-guard
fix: new-chat guard ignores in-flight streams (#1432) + profile form auto-capitalizes (#1423)
2026-05-01 18:04:09 -07:00
nesquena-hermes
2ec15a4345 docs: v0.50.262 release notes and version bump
- CHANGELOG: stamp [Unreleased] -> [v0.50.262] dated 2026-05-02
- ROADMAP: bump 'Last updated' to v0.50.262 / 3648 tests
- TESTING: bump test count 3309 -> 3648 in header and footer + date
2026-05-02 01:02:23 +00:00