Commit Graph

818 Commits

Author SHA1 Message Date
Frank Song 8f077d37f7 Fix German profile_skill_count interpolation 2026-05-10 14:25:08 +08:00
nesquena-hermes c67336e4e3 Stage 328: PR #1981 — feat(kanban): edit task button, real Run dispatcher, assignee dropdown by @nesquena-hermes
# Conflicts:
#	CHANGELOG.md
2026-05-09 21:02:27 +00:00
Nathan Esquenazi 8e0eedd163 fix(kanban-edit): preserve real status when editing non-{triage,todo,ready} tasks
PR #1981's edit-task modal silently demotes tasks whose real status is
running/blocked/done/archived. The dropdown only offers triage/todo/ready,
so `_kanbanEditableStatusFor()` maps any other status to 'triage' for
display. If the user just edits the title and saves, the dropdown's
displayed 'triage' lands in the PATCH payload — and `_patch_task` calls
`_set_status_direct` which:
  - ends any active run with outcome='reclaimed' (worker yanked back)
  - nulls claim_lock / claim_expires / worker_pid
  - moves the task to triage

So editing a 'running' task's title would reclaim the running worker.
Editing a 'done' task would un-done it. Editing an 'archived' task would
un-archive it. All silent, no warning.

Reproducer (Node):
  Original: {status: 'running'}
  Modal display: 'triage' (mapped)
  User leaves dropdown alone → submit
  Payload: {title: 'X', status: 'triage'}  ← destructive

Fix: track the modal's initial displayed status in
_kanbanTaskModalInitialDisplayedStatus on edit-mode open. In submit's
edit branch, only include `status` in the PATCH payload when the user
actually picked a different value than what the dropdown opened with.
Create-mode resets the tracker to null so create payloads always include
status.

Verified end-to-end via Node harness:
  - edit running, untouched → no status sent ✓ (server keeps running)
  - edit running, picked ready → status:ready sent ✓ (worker reclaimed
    intentionally)
  - edit triage, untouched → no status sent ✓ (idempotent)
  - edit triage, picked ready → status:ready sent ✓
  - create new → status always sent ✓
  - edit done, untouched → no status sent ✓ (no un-done)

Adds test_kanban_edit_mode_preserves_status_when_dropdown_untouched
pinning the tracker variable, openKanbanEdit captures, submit-skip
condition, and create/close reset paths. Verified to fail pre-fix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 13:57:31 -07:00
nesquena-hermes c71312b2e8 feat(kanban): edit task button, real Run dispatcher, assignee dropdown
Three connected gaps in the Kanban UX, fixed together because they're
load-bearing for the actual work-queue lifecycle:

1. Edit task — the detail view had only status-transition buttons (Triage/
   Todo/Ready/Blocked/Done/Archived) plus Block/Unblock and Add comment.
   No way to edit title, body, assignee, tenant, or priority once the task
   was created. Backend already supported it via PATCH /api/kanban/tasks/<id>
   (api/kanban_bridge.py::_patch_task) — purely a UI gap.

   Now: an Edit button on the task-detail header opens the existing modal
   pre-filled with current values, switches the modal title to 'Edit task'
   and the submit button to 'Save', PATCHes instead of POSTing on submit.

2. Run dispatcher — the existing 'Preview dispatcher' button always passed
   ?dry_run=1 (nudgeKanbanDispatcher), so it was preview-only. There was
   literally no UI button anywhere in the WebUI that actually ran the
   dispatcher to claim Ready tasks and spawn workers. Users had to drop
   to the CLI.

   Now: new runKanbanDispatcher() entry point hits /api/kanban/dispatch
   without dry_run=1, after a showConfirmDialog confirmation because it
   spawns subprocess workers. Two UI surfaces: a lightning-bolt button in
   the board header (visually distinct from the dry-run preview ▶), and
   a primary 'Run dispatcher' button in the sidebar bulk bar next to a
   relabeled 'Preview' button. Toast result shows concrete numbers from
   dispatch_once(): 'Dispatched: 1 spawned, 2 skipped (no assignee)' —
   not just a generic 'OK'.

3. Assignee dropdown — the previous create modal accepted free-text
   assignee with no validation. The dispatcher (kanban_db.py:3567) only
   spawns workers when row['assignee'] is a real Hermes profile name; any
   typo or blank value made the task sit in Ready forever.

   Now: <select> populated from /api/profiles (Hermes profile names) with
   historical board assignees grouped under 'Other (CLI lanes / removed
   profiles)', plus an explicit '— Unassigned (won't auto-run) —' option.
   Default selection is the first profile, not Unassigned. Custom SVG
   chevron so the field reads visually as a dropdown. Helper text under
   the field explains the dispatcher claim contract. Soft warning if user
   explicitly picks Unassigned + Ready ('You picked Unassigned + Ready.
   The dispatcher will skip this task. Submit again to confirm, or pick
   a profile.'); proceeds on second submit.

Side effect: default new-task status changed from triage to ready, since
'ready' is what users want for tasks they intend to actually run. Triage
is still in the dropdown for tasks that need staging review.

i18n: 19 new keys translated across all 8 supported locales.

Tests: 3 new regression tests in tests/test_kanban_ui_static.py:
- test_kanban_task_detail_has_edit_button_and_modal_supports_edit_mode
- test_kanban_assignee_dropdown_uses_select_not_freetext
- test_kanban_run_dispatcher_button_exists_and_is_distinct_from_preview

Verified end-to-end in browser: created board → opened modal with profile
dropdown → created task with assignee=archivist → clicked Edit → changed
all 5 fields → saved → verified persistence → clicked Run dispatcher →
confirm dialog → confirmed → toast 'Dispatched: 1 spawned' → task moved
Ready → Running.

Test suite: 5042 passed, 11 skipped, 3 xpassed, 0 regressions in 151s.
2026-05-09 20:48:28 +00:00
nesquena-hermes ed776ee1a1 Merge pull request #1976 from nesquena/fix/mcp-profile-discovery
fix(profile/mcp): discover MCP tools after per-session HERMES_HOME mutation (#1968)
2026-05-09 13:29:06 -07:00
nesquena-hermes a3af4a3c8f fix(profile/mcp): discover MCP tools after per-session HERMES_HOME mutation
Issue #1968: switching to a non-default profile in the WebUI dropdown
had no effect on which MCP servers were available. Every chat session,
regardless of profile, only saw the default profile's mcp_servers from
~/.hermes/config.yaml. Non-default profile MCP servers (postgres, custom
stdio servers, anything in <profile>/config.yaml) never registered.

Root cause: api/streaming.py:1922 called discover_mcp_tools() at the
TOP of _run_agent_streaming(), about 100 lines BEFORE the per-session
'os.environ["HERMES_HOME"] = _profile_home' mutation at line 2053.
discover_mcp_tools() reads ~/.hermes/config.yaml via get_hermes_home(),
which uses os.environ['HERMES_HOME']. So at the call site, HERMES_HOME
was still whatever the WebUI server process had at startup — the default
profile, every time.

Fix: relocate the discover_mcp_tools() call past the _ENV_LOCK block so
get_hermes_home() resolves to the session's actual profile home. Same
try/except wrapping is preserved; same idempotency semantics on
already-connected servers; same lazy-import pattern.

Caveat (out of scope, agent-side): _servers in tools/mcp_tool.py is a
process-global Dict[str, MCPServerTask] keyed only by server name. So
once profile A registers a server named e.g. 'postgres', profile B's
discovery sees 'postgres' as already connected and skips it — even if
B's config points at a different binary or DB. Concurrent multi-profile
WebUI processes will still hit 'first profile wins per server name'.
Fully fixing that requires keying _servers by (profile_home, name)
upstream in hermes-agent. This PR ships layer 1 only — fixes the
single-non-default-profile case (the headline symptom).

Tests: tests/test_issue1968_mcp_profile_discovery.py — 4 static tests
pinning the lexical ordering invariants. Verified mutation-safety: a
proof-of-concept revert (re-adding a discover call before the
HERMES_HOME mutation) makes the 'only called once' test fail.

Test suite: 5047 passed, 4 skipped, 3 xpassed, 0 regressions.

Closes #1968
2026-05-09 20:08:16 +00:00
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 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 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 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 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 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
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
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 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 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
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 383507f368 Stage 324: PR #1926 — fix: prevent chat scroll resets after final render by @ai-ag2026 2026-05-08 20:49:00 +00:00
nesquena-hermes 1f8e641e27 Stage 324: PR #1927 — fix: preserve viewport when loading older messages by @ai-ag2026 2026-05-08 20:49:00 +00:00
nesquena-hermes 89b8914704 Stage 324: PR #1930 — fix: collapse stale compression sidebar segments by @ai-ag2026 2026-05-08 20:49:00 +00:00
nesquena-hermes 55fdf48db4 Stage 324: PR #1921 — security: harden production Docker image by @Michaelyklam 2026-05-08 20:49:00 +00:00
nesquena-hermes afb5edff1a Stage 324: PR #1919 — Persist login rate limit attempts by @franksong2702 2026-05-08 20:49:00 +00:00
ai-ag2026 447b4e6c0f fix: collapse stale compression sidebar segments 2026-05-08 20:48:47 +00:00
ai-ag2026 018d491570 fix: preserve viewport when loading older messages 2026-05-08 20:48:44 +00:00
ai-ag2026 c65ae46983 fix: prevent chat scroll resets after final render
Keep explicit bottom pins stable across late layout growth and make clicking the already-active sidebar session a no-op before loadSession mutates state. Update scroll regression tests for the delayed settle path.
2026-05-08 20:48:43 +00:00
Frank Song e8fd8dac5d Persist login rate limit attempts 2026-05-08 20:48:41 +00:00
Michael Lam b1b0cedbe9 security: harden production Docker image 2026-05-08 20:48:39 +00:00
Frank Song 431705e498 Remove dead Kanban start i18n key 2026-05-08 20:48:37 +00:00