Commit Graph

1720 Commits

Author SHA1 Message Date
nesquena-hermes 4ce113f324 Stage 327: PR #1965 — fix(kanban): header + button opens create-task modal (#1964) by @nesquena-hermes
# Conflicts:
#	CHANGELOG.md
2026-05-09 19:51:30 +00:00
nesquena-hermes 55623ef249 Stage 327: PR #1943 — feat: expand collapsed session lineage segments by @dso2ng 2026-05-09 19:50:50 +00:00
nesquena-hermes 10ea2a014f fix(kanban): header '+' button opens create-task modal
The Kanban sidebar panel's header '+' button (#kanbanNewTaskBtn) was
wired straight to createKanbanTask(), which reads the inline
#kanbanNewTaskTitle input and silently returns when empty. The inline
input lives below five rows of filters (search, assignee, tenant,
archived/mine toggles, stats, bulk-action bar) and is typically off-screen
on first panel open, so the header button looked dead — clicking it with
no title typed did nothing visible (no modal, no scroll, no focus shift,
no toast).

Now the header '+' opens #kanbanTaskModal — a centered overlay with the
same .kanban-modal-overlay shell the existing create-board modal uses,
so the two flows look and behave identically (centered card, dim
backdrop, ESC closes, click-on-backdrop closes). The modal exposes the
fields the backend already accepts at /api/kanban/tasks: Title, Description,
Status (Triage/Todo/Ready), Priority, Assignee (datalist suggestions from
the active board), Tenant (datalist).

UX details:
- Title is required; submit-with-empty shows a properly styled red error
- Title field auto-focuses on open
- ESC closes the modal; backdrop click closes; Enter on simple inputs
  submits, Enter in the description textarea inserts a newline
- Submit POSTs only the fields the user filled in (no forced empty strings)
  and auto-opens the new task's detail view
- Submit button disables while posting to prevent double-submit
- Inline quick-add (Enter on #kanbanNewTaskTitle) is preserved as a
  power-user shortcut

Side effect: .kanban-modal-error styling improved (proper red alert with
border + tinted background) so the existing create-board modal benefits
from the same polish for free.

i18n: 11 new keys added across all 8 supported locales (en, ja, ru, es,
de, zh, pt, ko).

Tests: tests/test_kanban_ui_static.py::test_kanban_new_task_header_button_opens_modal
covers the modal markup, button wiring, ESC/Enter handling, datalist
population, submit behavior, and inline-quick-add fallthrough.

Verified end-to-end in the browser on an isolated test env (port 8789):
created a board from scratch, opened the modal via header '+',
submitted with title/description/status/priority/assignee/tenant filled in,
moved the task through statuses (Triage → Todo → Ready → Blocked → Archived),
added a comment, verified Cancel + ESC + backdrop-click all close cleanly,
verified validation error rendering, verified inline quick-add still works.

Closes #1964
2026-05-09 19:33:07 +00:00
nesquena-hermes 9a1b68a955 Merge pull request #1969 from nesquena/fix/docker-env-readonly-vars
fix(docker): salvage operational hardening from #1686 — .env readonly-var parser + xz-utils/git apt deps + root re-exec
2026-05-09 12:25:57 -07:00
nesquena-hermes 1681ce567e fix(start.sh): NOPASSWD precheck on root re-exec — silent fall-through
Per Opus advisor on PR #1969: the original three-guard root re-exec
(EUID==0, hermeswebui exists, sudo on PATH) would exit non-zero with
`sudo: a password is required` on host machines where the developer's
hermeswebui user doesn't have NOPASSWD configured.

Better failure mode: silent fall-through to running as root (back to
pre-PR behavior). Adds a fourth guard `sudo -n -u hermeswebui true 2>/dev/null`
that pre-flights the sudo capability without producing visible output.

Also expands the comment to clarify which guard is load-bearing on the
canonical container path (the production image doesn't ship sudo at all,
so `command -v sudo` is the silent-no-op gate there; the entrypoint
docker_init.bash never invokes start.sh in any case).

No new tests needed — existing behavioral tests already cover the
non-root + non-sudo paths, which is what runs in CI and on host.
2026-05-09 19:23:54 +00:00
nesquena-hermes 57c71e89f3 fix(docker): salvage operational hardening from #1686 (env readonly + apt deps)
Three independent operational hardening fixes salvaged from PR #1686
(@binhpt310) after the parent PR was deferred over a separate sibling-repo
build-context concern unrelated to these fixes:

1. start.sh's .env loader now filters readonly bash vars (UID, GID, EUID,
   EGID, PPID) before `source`-ing.  docker-compose.yml's macOS instructions
   document `echo "UID=$(id -u)" >> .env` to set host UID/GID for bind-mount
   permission fixing — that .env was crashing start.sh with
   `UID: readonly variable` when `set -a; source ...; set +a` tried to
   assign to those names.  Replaced with
   `source <(grep -vE '^[[:space:]]*(export[[:space:]]+)?(UID|GID|EUID|EGID|PPID)=' "${REPO_ROOT}/.env")`.
   The bootstrap regression guard at tests/test_bootstrap_dotenv.py:181
   still passes — both `source` and `.env` are still on the modified line.

2. start.sh now defensively re-execs as the unprivileged hermeswebui user
   when invoked as root.  Fires only when EUID==0 AND a hermeswebui user
   actually exists AND sudo is on PATH — so it's a no-op on host machines
   without the container user setup.  The production image's entrypoint
   (docker_init.bash) already drops to hermeswebui before invoking start.sh,
   so this is a no-op on the canonical container path; it only matters for
   `sudo ./start.sh` or accidental root shells inside the container during
   interactive debugging.

3. Dockerfile installs xz-utils + git apt packages.  xz-utils is required
   to decompress .tar.xz archives (e.g. Node.js distribution tarballs);
   git is needed for `git describe` (powers WEBUI_VERSION resolution at
   api/updates.py:_detect_webui_version) and any clone-based agent install
   path.  Both are tiny apt packages on top of python:3.12-slim with no
   measurable image-size impact.

What's NOT in this commit (deferred from #1686):

- Pre-baking hermes-agent source into the image via
  `COPY hermes-agent-desktop/hermes-agent /opt/hermes/` plus a build-context
  flip to `..`.  Requires a sibling-repo layout that breaks the canonical
  `git clone hermes-webui && cd hermes-webui && docker compose build` flow.
  The right shape is a build arg gating the COPY behind
  --build-arg WITH_AGENT_SOURCE=1; left to a separate PR.
- Pre-installing Node.js 22 LTS system-wide.  Real motivation but worth
  evaluating the fix shape (full Node bake vs. opt-in vs. layer cache)
  separately from these three operational fixes.

Tests: tests/test_docker_env_readonly_vars.py — 11 tests (4 source-grep
on the start.sh filter pattern + 5 behavioral that actually run bash
against synthetic .env files containing readonly vars + 2 Dockerfile
package-presence tests).  All 11 pass.  Behavioral tests skip if bash
is not on PATH.

Full suite: 5028 → 5036 passing (+8 net new after pytest collection
counted some behavioral tests under skip), 0 regressions, 147.84s.

Closes the operational-hardening portion of #1686.

Co-authored-by: binhpt310 <binhpt310@users.noreply.github.com>
2026-05-09 19:17:34 +00:00
nesquena-hermes 8a653bac20 Merge pull request #1967 from nesquena/stage-326
release: v0.51.31 — Release H (12-PR contributor batch: image-mode + race fixes + composer drafts + locale parity)
v0.51.31
2026-05-09 11:55:08 -07:00
nesquena-hermes 1d7344c602 release: v0.51.31 — Release H (12-PR contributor batch)
CHANGELOG, ROADMAP, TESTING refresh for v0.51.31 stage release covering
12 contributor PRs:

Added (2 PRs):
- #1956 JKJameson — persistent composer draft (server-side, cross-client)
- #1957 hermes-gimmethebeans — configurable session TTL via env + settings

Fixed (10 PRs):
- #1939 ai-ag2026 — theme-color + sw cache regression coverage
- #1941 ai-ag2026 — preserve chat scroll across final render
- #1945 franksong2702 — localize session jump controls (#1938)
- #1947 happy5318 — show same model from different custom providers
  (Co-authored-by hacker1e7 for #1874 close)
- #1949 Sanjays2402 — close #1937 endless-scroll vs Start-jump race
  with generation-token + mutex
  (Co-authored-by franksong2702 + Michaelyklam)
- #1950 franksong2702 — mute stale stopped gateway heartbeat (#1944)
- #1951 amlyczz — gate goal hook on goal-related turns (#1932)
  (Co-authored-by franksong2702 for #1946 close)
- #1953 lucky-yonug — skip provider peel for custom host:port slugs
- #1960 Michaelyklam — translate hidden-files workspace label (#1841)
- #1961 sbe27 — respect image_input_mode (#1959)

Closed in favor of canonical: #1942, #1962, #1946, #1874, #1311.

Stage-326 hotfixes (per Opus advisor):
- CRITICAL #1951 PENDING_GOAL_CONTINUATION race fix (removed finally
  discard that race-erased the marker before consumer could read it)
- #1956 composer-draft input validation (50 KB text / 50 file clamp +
  type coercion to prevent unbounded session-JSON bloat)
- #1957 SESSION_TTL constant preserved as named fallback (existing
  regression tests pin it; #1957 originally deleted it)

Tests: 5006 → 5028 (+51 net new) — 0 regressions, 142.61s runtime.
2026-05-09 18:46:25 +00:00
nesquena-hermes 8782fd2675 fix(stage-326): apply Opus advisor critical + recommended fixes
CRITICAL: #1951 PENDING_GOAL_CONTINUATION race
  Removes `PENDING_GOAL_CONTINUATION.discard(session_id)` from the
  streaming worker's `finally` cleanup block. The marker is set inside
  the SAME function call (line ~3328 on `goal_continue`) and the discard
  in the `finally` (line ~3553) almost always raced ahead of the
  frontend's SSE-receive → POST /api/chat/start round-trip, erasing
  the marker before the consumer in routes.py could read it. The
  consumer (`_start_chat_stream_for_session` in routes.py:6522) already
  discards atomically when consuming, so removing the streaming-side
  discard preserves single-use semantics and unblocks the
  goal-continuation chain.

  Adds tests/test_stage326_pending_goal_continuation_race.py with 5
  regression guards:
  1. streaming.py's finally must NOT discard PENDING_GOAL_CONTINUATION
  2. routes.py consumer must check + set + discard atomically
  3. PENDING_GOAL_CONTINUATION must be a set (GIL-safe single-op)
  4. STREAM_GOAL_RELATED.pop must be keyed by stream_id, not session_id
  5. PENDING_GOAL_CONTINUATION.add must precede the goal_continue SSE
     emission in source ordering

HARDENING: #1956 composer-draft input validation
  Per Opus, the POST /api/session/draft handler accepted unbounded /
  arbitrary-typed text and files inputs. With the 400ms debounced
  auto-save firing on every keystroke, a misbehaving client could
  persist multi-MB strings into the session JSON. Adds:
  - text: coerced to str if not already; clamped to 50_000 chars
  - files: coerced to list if not already; clamped to 50 entries
  Validation runs BEFORE the session lock acquire / save.

  Adds tests/test_stage326_composer_draft_validation.py with 5 guards.

Verdict from Opus advisor on stage-326: SHIP-WITH-FIXES.
This commit applies the required + recommended fixes; #1957 hardening
fixed in a prior stage commit.
2026-05-09 18:36:01 +00:00
nesquena-hermes 404e24ac9d fix(stage-326): preserve SESSION_TTL constant + reconcile #1957 tests
PR #1957 deleted the SESSION_TTL = 86400 * 30 module-level constant in
favor of the new _resolve_session_ttl() helper. Two existing regression
tests pin the constant: test_auth_sessions.TestSessionPruning.test_session_ttl_is_24_hours
imports SESSION_TTL directly, and test_v050258_opus_followups.test_redirect_session_ttl_30_days
asserts the literal "SESSION_TTL = 86400 * 30" line is present in source
(guarding against the daily-kick-out regression from #1419).

Restore SESSION_TTL as the named fallback for _resolve_session_ttl(); the
new env-var/settings.json path is unchanged. Backwards-compatible.

Also fix the new TestSessionTtlResolution suite:
- Switch from pytest's `monkeypatch` fixture (incompatible with
  unittest.TestCase subclasses) to setUp/tearDown env snapshotting
- Reconcile clamp tests with actual implementation: out-of-range env
  values fall through to settings/default, not snap to bounds
- test_session_uses_dynamic_ttl now sets the env var so the dynamic
  resolved value (3600s) is exercised rather than expecting the default

Verified: tests/test_auth_sessions.py + tests/test_v050258_opus_followups.py
21/21 pass.
2026-05-09 18:33:28 +00:00
nesquena-hermes 7cf8dcff4c Stage 326: PR #1956 — feat: persistent composer draft — server-side, cross-client, survives refresh by @JKJameson 2026-05-09 18:17:51 +00:00
nesquena-hermes 07d39612ce Stage 326: PR #1949 — fix(#1937): close endless-scroll prefetch vs Start-jump race with generation-token + mutex by @Sanjays2402
# Conflicts:
#	CHANGELOG.md
2026-05-09 18:17:51 +00:00
nesquena-hermes 4751b5ace5 Stage 326: PR #1951 — fix: only evaluate goal hook on goal-related turns (#1932) by @amlyczz 2026-05-09 18:17:20 +00:00
nesquena-hermes a0a65ba0bc Stage 326: PR #1941 — fix: preserve chat scroll across final render by @ai-ag2026 2026-05-09 18:17:20 +00:00
nesquena-hermes f0ecd94e04 Stage 326: PR #1945 — Localize session jump controls by @franksong2702
# Conflicts:
#	CHANGELOG.md
2026-05-09 18:17:03 +00:00
nesquena-hermes 22ea145d49 Stage 326: PR #1950 — Mute stale stopped gateway heartbeat by @franksong2702 2026-05-09 18:16:16 +00:00
nesquena-hermes 979f30e46a Stage 326: PR #1960 — fix: translate hidden-files workspace label by @Michaelyklam 2026-05-09 18:16:16 +00:00
nesquena-hermes c2f0c6ccc0 Stage 326: PR #1961 — fix: WebUI respects image_input_mode — stop unconditionally embedding native images by @sbe27 2026-05-09 18:16:16 +00:00
nesquena-hermes 072ec41e0a Stage 326: PR #1947 — fix: show same model from different custom providers instead of deduplicating by @happy5318 2026-05-09 18:16:16 +00:00
nesquena-hermes 1c84da07fc Stage 326: PR #1953 — fix(config): skip #1776 provider peel for custom host:port slugs by @lucky-yonug 2026-05-09 18:16:16 +00:00
nesquena-hermes 9732795e9c Stage 326: PR #1957 — feat(auth): make session TTL configurable via env var and settings.json by @hermes-gimmethebeans 2026-05-09 18:16:16 +00:00
nesquena-hermes 7a0e4f1ee7 Stage 326: PR #1939 — test: cover theme-color media fallback by @ai-ag2026 2026-05-09 18:16:16 +00:00
nesquena-hermes 6f7479944c test(#1947): regression coverage for same-model-multiple-named-custom-providers
Adds tests/test_pr1947_same_model_multiple_custom_providers.py covering:

1. Two named custom providers exposing the same model id — both must
   surface in the rendered groups (one bare, one @custom:slug:model)
2. Three named providers all exposing the same model — none dropped
3. Distinct-model-per-provider sanity check (still grouped correctly)

Verified the regression-detecting tests (1 + 2) FAIL against master's
api/config.py (where _seen_custom_ids was seeded from auto_detected_models
and used as a global bare-id bucket — the second provider's entry was
silently dropped) and PASS against the contributor fix on this branch.

Test 3 (distinct-models sanity) passes either way as expected.

Co-authored-by: happy5318 <happy5318@users.noreply.github.com>
Co-authored-by: hacker1e7 <hacker1e7@users.noreply.github.com>
2026-05-09 18:15:50 +00:00
hermes-agent b443e8ea5a fix: WebUI respects image_input_mode — stop unconditionally embedding native images
_build_native_multimodal_message() unconditionally embedded images as
native image_url parts, bypassing the agent's image_input_mode config.

Add _resolve_image_input_mode(cfg) helper mirroring the agent's
decide_image_input_mode logic, and wire it into
_build_native_multimodal_message with a new cfg parameter.

When mode resolves to 'text' (explicit aux vision config, or
image_input_mode: text), returns plain string so the agent's
existing text-mode pipeline (vision_analyze) handles images.

Closes #1959
2026-05-09 19:39:50 +02:00
Michael Lam ce6685a27c fix: translate hidden-files workspace label 2026-05-09 10:36:30 -07:00
hermes-gimmethebeans 9d7c213971 feat(auth): make session TTL configurable via env var and settings.json
Add _resolve_session_ttl() with three-layer precedence:
  1. HERMES_WEBUI_SESSION_TTL env var (highest priority)
  2. session_ttl_seconds in settings.json
  3. Default: 86400 * 30 (30 days)

Clamped to [60s, 1 year] for safety. Settings changes take effect
immediately since the function is called dynamically at each login/cookie-write.

Closes #1954
2026-05-09 17:11:53 +00:00
Minimax 08c4ef8d88 feat: persistent composer draft — server-side, cross-client, survives refresh
- Session.composer_draft field: {text, files} stored in session JSON
- POST+GET /api/session/draft endpoint for save/load
- loadSession: save draft before switch, restore from S.session.composer_draft
- textarea input: debounced 400ms auto-save to server
- send(): clear draft after message is sent
- lockComposerForClarify(): save draft before card locks composer
- _restoreComposerDraft: clears textarea when target has no draft, guards
  against stale responses racing new session loads, exact text comparison
- Session.compact(): includes composer_draft in response
- Fix: use handler.command instead of parsed.method (ParseResult has no .method)

Co-authored-by: Minimax <noreply@minimax.io>
2026-05-09 13:47:57 +01:00
happy5318 a6599cd68e fix: show same model from different custom providers instead of deduplicating
When multiple custom providers expose the same model ID (e.g. baidu,
huoshan, and liantong all offering glm-5.1), only the first provider's
entry was shown in the model dropdown.

Root cause (backend):  used the bare model ID as the
dedup key, so the second and subsequent providers with the same model
were silently skipped.

Root cause (frontend):  stripped the @provider: prefix before
comparing, so @custom:baidu:glm-5.1 and @custom:huoshan:glm-5.1 were
treated as duplicates.

Fix:
- Backend: change _seen_custom_ids key to '{slug}:{model_id}' so each
  provider's models are tracked independently.
- Frontend: add _providerOf() helper and deduplicate on the composite
  (normId, provider) key instead of normId alone. Bare model IDs
  (without @provider: prefix) still deduplicate on normId for backward
  compatibility.
2026-05-09 16:17:23 +08:00
liyang1116 7532482393 fix: fix(config): skip #1776 provider peel for custom host:port slugs
model_with_provider_context can emit @custom:<host>:<port>:<model> when
model_provider is derived from an OpenAI base_url authority (e.g.
custom:10.8.0.1:8080). The colon-count heuristic meant for @custom:slug:model:free
mistook those extra colons for an over-split model ID and prepended the port
segment onto the bare model (8080:Qwen3-235B), breaking WebUI while CLI/curl
stayed correct.

Detect endpoint-style slugs (IPv4/localhost/hostname + numeric port) and skip
the peel in that case. Add regression tests for IPv4, dotted hostname,
localhost, and model_with_provider_context round-trip.
2026-05-09 16:16:32 +08:00
zqy 6fd07c2af4 fix: only evaluate goal hook on goal-related turns (#1932)
The goal evaluation hook was firing on every completed assistant turn
when a goal was active, even for unrelated messages like "what time is
it". This burned the goal budget, triggered continuation prompts that
interrupted unrelated conversations, and made /goal status numbers
misleading.

Add STREAM_GOAL_RELATED and PENDING_GOAL_CONTINUATION flags to gate
the evaluate_goal_after_turn() call in the streaming loop. Only streams
started from goal kickoff (/goal <text>) or goal continuation are
marked as goal-related. Normal user messages skip the hook entirely.
2026-05-09 15:08:13 +08:00
Frank Song b38cc2f1ea Mute stale stopped gateway heartbeat 2026-05-09 14:53:42 +08:00
Sanjay Santhanam fb822239ea fix(#1937): close endless-scroll prefetch vs Start-jump race with generation-token + mutex
The originally-proposed fix (gate _ensureAllMessagesLoaded on the existing
_loadingOlder flag) does not actually close the race. By the time the
prefetch reaches its post-await body, it has already cleared the entry-
gate that reads _loadingOlder, so a same-flag check inside the resolved
callback would be a no-op for an in-flight request.

The actual fix is two-pronged:

1. New module-scoped _messagesGeneration counter, bumped every time
   S.messages is wholesale-replaced. _loadOlderMessages snapshots it
   BEFORE its await and re-checks after — if it changed, the prepend
   is aborted. This is the canonical async-invalidation pattern.

2. _ensureAllMessagesLoaded now claims the _loadingOlder mutex around
   its body so a new prefetch cannot start mid-replace and concurrent
   ensure-all calls (rapid double-click on Start) serialize cleanly.
   It bumps the generation token before mutating S.messages, yields
   until any in-flight prefetch finishes, and resets _oldestIdx so a
   subsequent prefetch cannot request stale older messages.

Also adds the same-session / _loadingSessionId guards that the original
ensure-all body was missing post-await — if the user switched sessions
mid-flight, the old code would happily overwrite the new session's
messages with the previous session's full history.

12 new regression tests in tests/test_issue1937_endless_scroll_jumpstart_race.py
lock in: generation token declaration, bump-helper presence, snapshot-
before-await ordering, post-await-abort behaviour, mutex acquisition and
finally-release, yield-then-claim ordering when a prefetch is in flight,
generation bump during the wait phase, _oldestIdx reset, and the new
session-switch guard.

Closes #1937.
2026-05-08 21:14:22 -07:00
Dennis Soong 376727a6d1 fix: localize lineage segment row labels 2026-05-09 10:39:44 +08:00
Frank Song 3dfd692d75 Localize session jump controls 2026-05-09 10:03:27 +08:00
Dennis Soong a3ab46e345 fix: keep project-dot regression resilient 2026-05-09 09:53:38 +08:00
Dennis Soong 5b36232cbf feat: expand collapsed session lineage segments 2026-05-09 09:49:10 +08:00
ai-ag2026 d84eaea594 ci: retrigger flaky ctl test 2026-05-09 02:19:32 +02:00
ai-ag2026 1559c70a41 fix: preserve chat scroll across final render 2026-05-09 02:15:35 +02:00
ai-ag2026 5dcb4e9ade test: cover theme-color media fallback 2026-05-08 23:51:24 +02:00
nesquena-hermes 0b7e1e60e8 Release v0.51.30 — Release G (offline recovery + PWA hardening + opt-in session jump buttons + opt-in endless-scroll)
Merge stage-325 to master.
v0.51.30
2026-05-08 14:37:53 -07:00
nesquena-hermes bc4421a1b6 release: v0.51.30 — Release G (3-PR batch: offline recovery + PWA hardening + opt-in session jump buttons + opt-in endless-scroll)
Three-PR contributor batch (all from @ai-ag2026):
- PR #1891: Browser offline recovery + PWA cache hardening
- PR #1928: Opt-in session Start/End jump buttons
- PR #1929: Opt-in session endless-scroll (builds on shipped #1927)

Tests: 4960 → 4977 (+17 net new). Browser API harness all-green.
Manual browser verification on port 8789 passed.
Opus advisor: SHIP-WITH-FIXES (both fast-follows are non-blocking).
2026-05-08 21:31:41 +00:00
nesquena-hermes bec4433c2a Stage 325: PR #1929 — feat: add opt-in session endless scroll by @ai-ag2026
Conflict resolution: both #1928 (session jump buttons) and #1929 (endless
scroll) add their own settings/UI/i18n keys. Resolved by keeping both —
the features are independent opt-in toggles.
2026-05-08 21:23:34 +00:00
nesquena-hermes fba860da48 Stage 325: PR #1928 — feat: add opt-in session jump buttons by @ai-ag2026 2026-05-08 21:16:33 +00:00
nesquena-hermes 503d549cd2 Stage 325: PR #1891 — feat: add browser offline recovery and PWA cache hardening by @ai-ag2026 2026-05-08 21:16:33 +00:00
ai-ag2026 ea8aca2818 feat: add opt-in session endless scroll 2026-05-08 21:16:21 +00:00
ai-ag2026 df1ba9fde8 feat: add opt-in session jump buttons 2026-05-08 21:16:19 +00:00
ai-ag2026 8f58a8c94e feat: add browser offline recovery and PWA cache hardening 2026-05-08 21:16:17 +00:00
nesquena-hermes 596c6b314d Release v0.51.29 — Release F (Docker hardening + login persistence + scroll/lineage fixes + i18n cleanup)
Merge stage-324 to master.
v0.51.29
2026-05-08 14:01:17 -07:00
nesquena-hermes 351fbd3dd2 release: v0.51.29 — Release F (6-PR batch — Docker hardening + login persistence + scroll/lineage fixes + i18n cleanup)
Six-PR contributor batch:
- PR #1919 (franksong2702): Persist login rate limit attempts (closes #1910)
- PR #1920 (franksong2702): Remove dead Kanban start i18n key
- PR #1921 (Michaelyklam): Production Docker image hardening (closes #1908)
- PR #1926 (ai-ag2026): Prevent chat scroll resets after final render
- PR #1927 (ai-ag2026): Preserve viewport when loading older messages
- PR #1930 (ai-ag2026): Collapse stale compression sidebar segments

Tests: 4947 → 4960 (+13 net new). Browser API harness all-green.
Opus advisor: SHIP-READY. CHANGELOG conflict on #1919 auto-resolved
during stage rebase (CHANGELOG took ours strategy).
2026-05-08 20:58:56 +00:00
nesquena-hermes 383507f368 Stage 324: PR #1926 — fix: prevent chat scroll resets after final render by @ai-ag2026 2026-05-08 20:49:00 +00:00