mirror of
https://github.com/nesquena/hermes-webui.git
synced 2026-05-25 03:00:23 +00:00
9fabd12e41b17eb6282ccf5713643bf55ffb0c5e
416 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
9fabd12e41 | fix: preserve clarify drafts on timeout | ||
|
|
4dbc9ac3e1 | fix(i18n): add cron_status_running to de locale and fix es fallback | ||
|
|
d734efa8af |
fix: stop cron watch when clearing cron panel detail view
_stopCronWatch() was only called when switching between cron job details but not when the panel was cleared entirely (_clearCronDetail). This could leave orphaned polling intervals if the user navigated away or the panel was dismissed while a job was running. |
||
|
|
98ed2d804b |
feat: cron run status tracking and watch mode (#526)
Backend:
- Track running cron jobs in thread-safe dict (job_id → start_time)
- Wrapper _run_cron_tracked() marks done on completion
- New GET /api/crons/status?job_id=... returns {running, elapsed}
- New GET /api/crons/status returns all running jobs
Frontend:
- After 'Run Now', enters watch mode with 3s polling
- Shows running indicator (spinner + elapsed timer) in detail card
- Auto-detects running jobs when opening detail view
- Stops watch and refreshes output on job completion
- Cleanup on detail view switch
Note: True SSE streaming is not possible because the hermes-agent
scheduler writes output files only on completion. This polling
approach provides real-time status feedback within that constraint.
|
||
|
|
8c24b24dcd |
feat: upload and extract zip/tar archives into workspace (#525)
- Add extract_archive() with zip-slip and tar-slip protection - New /api/upload/extract endpoint for archive uploads - Auto-detect archive files (.zip, .tar.gz, .tgz, .bz2, .xz) - Archives extracted into named subfolder (avoids overwrites) - Workspace file tree auto-refreshes after extraction - Archive extensions added to file picker accept list - i18n: archive_extracted key in all 7 locales Security: path traversal blocked via resolve() prefix check, matching existing safe_resolve_ws() sandbox pattern. |
||
|
|
d08d96f864 |
fix: deduplicate clone name + explicit enabled:false for duplicates
- Name dedup: 'Job (copy)', 'Job (copy 2)', 'Job (copy 3)' etc. - Duplicates explicitly pass enabled:false to backend - Normal cron create is unaffected (no enabled field sent) Addresses reviewer feedback on #1225 (points 1 + 4). |
||
|
|
8c63324ff7 |
feat: duplicate cron job with form pre-fill (#528)
- Add duplicate button in cron detail header - Pre-fills create form with original job settings - New job created as paused copy with '(copy)' suffix - i18n keys in all 7 locales |
||
|
|
9c57d36156 |
fix: update expanded dirs cache on double-click directory rename
The inline rename via double-click (nameEl.ondblclick) was not updating the _expandedDirs and _dirCache when renaming a directory, unlike the context-menu rename path (_inlineRenameFileItem) which already had this logic. This could cause the tree view to show stale expand state after a directory was renamed via double-click. |
||
|
|
38df294af9 |
feat(#1104): workspace directory CRUD — delete, rename, context menu
The file tree already supported file rename (double-click), file delete (button), and create file/folder. This adds the missing directory operations: Backend: - _handle_file_delete now supports directories when recursive=true (uses shutil.rmtree instead of blocking with an error) Frontend: - Right-click context menu on all file/directory items with Rename and Delete options (follows the project context menu pattern) - Directory delete button (x) with confirmation dialog - _inlineRenameFileItem() for renaming dirs via context menu prompt - Expanded-dir cache is updated on rename/delete to stay consistent - Context menu auto-positions within viewport bounds i18n: delete_dir_confirm, rename_title, rename_prompt in all 7 locales Closes #1104 |
||
|
|
03b7714f65 | docs: note Lucide source for composer icons | ||
|
|
62650e6a0d | fix: add missing commas after approval_skip_all_title in all locales | ||
|
|
9d5480565f |
fix: remove deprecated btnCancel; localise composer tooltips with disabled reason branching
- Drop btnCancel element and all JS show/hide call sites across boot.js, messages.js, sessions.js, ui.js (superseded by single primary action button) - Remove .cancel-btn CSS rules including mobile media-query override - Route updateSendBtn() title/aria-label through t() with English fallbacks; add composer_send/queue/interrupt/steer/stop keys to all 7 locales (en, ru, es, de, zh, zh-Hant, ko) - Branch disabled-state tooltip on reason: clarify lock, compression running, or idle-empty, each with its own i18n key - Update test_sprint10 / test_sprint36 to reflect single-button model: assert btnSend present and id="btnCancel" absent; replace test_hides_cancel_button with test_clears_composer_status |
||
|
|
b57134bf2b | ui: reflect explicit busy slash command in send button | ||
|
|
be291498cf |
ui: swap composer action icons to Lucide (ISC-licensed)
- queue: list-end (append to queue) - interrupt: skip-forward (jump ahead) - steer: compass (course correction) |
||
|
|
8eb3d8bdbc | chore: strip remaining btnCancel inline-flex references (superseded by single-button model) | ||
|
|
96182e5f51 | fix: keep busy-input send available on mobile | ||
|
|
26f51b7190 |
fix: address review feedback — restore V3 as legacy, fix zai base_url
- Restore deepseek-chat-v3-0324 and deepseek-reasoner with '(legacy)' labels; these are deprecated 2026-07-24 but still live until then - Fix zai (Z.AI/GLM) default_base_url: use /api/paas/v4 instead of /api/coding/paas/v4; the coding plan path is for the glmcode custom provider, not the general API - Update test assertions to match |
||
|
|
25958139da |
feat: show model names in provider cards + scan custom_providers
Provider card improvements:
- Show model name tags when a provider card is expanded (panels.js)
- Add .provider-card-model-tag styling (style.css)
Custom providers in providers panel:
- Scan config.yaml custom_providers (e.g. glmcode, timicc) and list
them as providers with their configured models (api/providers.py)
- Detect API key status from env var references (${ENV_VAR})
|
||
|
|
568a913615 |
chore: remove deprecated DeepSeek V3/R1 models, keep only V4
- Remove deepseek-chat-v3-0324 (DeepSeek V3) and deepseek-reasoner (R1) from _MODEL_LIST, _PROVIDER_MODELS, static/index.html, and static/ui.js - Keep only deepseek-v4-flash and deepseek-v4-pro - These old model IDs are deprecated since 2026-07-24 |
||
|
|
9df01c6167 |
feat: add DeepSeek V4 Flash and V4 Pro models
Add deepseek-v4-flash and deepseek-v4-pro model entries to: - api/config.py (_MODEL_LIST and _PROVIDER_MODELS) - static/index.html (model dropdown) - static/ui.js (static label map) These are the latest DeepSeek models with 1M context window, replacing the legacy deepseek-chat/deepseek-reasoner (deprecated 2026-07-24). |
||
|
|
f384368ee2 | fix(sessions): preserve unread dots after compression | ||
|
|
248cfd1248 | Track cache-rendered streaming sessions for unread dots | ||
|
|
6b04ae0254 | Fix unread markers for local inflight completions | ||
|
|
b488a0d1b0 | Fix unread markers for background completions | ||
|
|
5d16ff7522 | Fix background completion unread markers | ||
|
|
a8101d98f7 |
fix(models): deduplicate model IDs across provider groups (#1228)
When multiple providers expose the same bare model ID (e.g. two custom providers both listing gpt-5.4), the model picker cannot distinguish them — both rows appear active and clicking the other provider's copy is a no-op. Fix: - Add _deduplicate_model_ids() post-process in api/config.py that detects duplicate bare model IDs across groups and prefixes collisions with @provider_id: so each entry is globally unique - Update norm() regex in static/ui.js to strip @provider: prefixes for fuzzy matching, so existing sessions with bare model IDs still restore correctly - First occurrence stays bare for backward compatibility with sessions that already store the bare model name - Update test_model_resolver to be dedup-aware Closes #1228 |
||
|
|
ae2ed1a4e7 |
Fix #1214: refresh workspace on profile switch when session is empty
Add loadDir('.') call in switchToProfile() Case B so the workspace file
tree panel reflects the new profile's workspace instead of showing stale
files from the previous profile.
Fix #1212: detect OAuth providers not in hardcoded set
Expand _OAUTH_PROVIDERS with copilot-acp and qwen-oauth.
Add fallback in get_providers() that checks hermes auth live status
for providers that have no API key and are not in the hardcoded set
(e.g. Anthropic connected via OAuth), so the Providers tab shows
them as configured.
|
||
|
|
f7f8fc6496 |
fix: _loadOlderMessages scrolls to bottom instead of preserving position
When _loadOlderMessages prepends older messages, the viewport snaps
to the bottom instead of staying where the user was.
Two bugs compounding:
1. Wrong scrollable container. Code used `$("msgInner")` for scrollHeight
and scrollTop, but #msgInner has no overflow-y — it is a flex column.
The actual scrollable container is #messages (`.messages{overflow-y:auto}`).
Setting msgInner.scrollTop was silently ignored.
2. renderMessages calls scrollToBottom at the end (ui.js:2552),
which unconditionally scrolls #messages to the bottom and sets
_scrollPinned=true. Since bug #1 made the scroll-restore a no-op,
the page landed at the bottom every time.
Fix:
- Changed scroll restore target from `$("msgInner")` to `$("messages")`.
- Reset _scrollPinned = false after restoring the user position,
so scrollToBottom does not re-fire on next tick.
|
||
|
|
24b1e6f3fc |
fix+feat: batch v0.50.236 — OAuth providers fix, profile switch UX, YOLO mode (#1211)
fix+feat: batch v0.50.236 — OAuth providers fix, profile switch UX, YOLO mode (#1211) Merges PRs #1208, #1209, #1210 (#1152 rebased): - fix(providers): OAuth provider cards show correct Configured status in Settings. get_providers() was discarding has_key=True from _provider_has_key() for OAuth providers, hiding config.yaml tokens. Also fixed filter excluding all OAuth providers from the Settings panel. Surfaces auth_error string. (closes #1202) - ux(profiles): profile chip shows spinner and new name immediately on switch. Optimistic name update + .switching CSS class + chip disabled + finally cleanup. populateModelDropdown() and loadWorkspaceList() now parallelized via Promise.all. - feat: YOLO mode toggle — skip all approvals per session. /yolo slash command, "Skip all this session" button on approval cards, amber ⚡ pill indicator in composer footer. Session-scoped, in-memory. Full i18n: en, ru, es, de, zh, ko, zh-Hant. (closes #467) Original author: @bergeouss (PR #1152) Tests: 2837 passed (+50 new tests vs previous release) QA harness: 20/20 passed + all browser API checks passed |
||
|
|
7189416969 |
fix: batch v0.50.234-235 — XSS hardening, workspace validation, profile switch fixes (#1206)
fix: batch v0.50.234-235 — XSS hardening, workspace validation, profile switch fixes v0.50.235 (#1203 — profile switch workspace/model/chip, 3 bugs + flaky test): - switch_profile now reads target profile's workspace directly (thread-local bypass) - invalidate_models_cache() after profile switch (model dropdown staleness) - syncTopbar() updates chip before early-return (no-session path) v0.50.234 (#1201/#1205 — XSS hardening + workspace security): - renderMd() full HTML attribute sanitizer replacing tag-name-only allowlist - Delegated image lightbox (removes all inline onclick) - macOS /etc → /private/etc symlink bypass fixed - /System /Library added to blocked workspace roots - Legacy /api/chat workspace trust gap closed Both PRs independently reviewed. 2787/2787 tests. QA harness 20/20 + 11/11 API checks. Co-authored-by: Brendan Schmid <bschmidy10@Wilson.bschmidy10> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> |
||
|
|
3780df9428 |
fix: batch v0.50.232 — fuzzy match, codex detection, workspace reload, timestamp sync (#1198)
Batch release v0.50.232 — 4 fixes. ## PRs included | PR | Author | Fix | |---|---|---| | #1192 | @nesquena-hermes | Model chip fuzzy-match false positive (#1188) | | #1193 | @nesquena-hermes | openai-codex not detected in model picker (#1189) | | #1196 | @nesquena-hermes | Workspace files blank after second empty-session reload | | #1197 | @bergeouss | Session timestamps wrong with server/client clock drift (#1144) | All four PRs independently reviewed and approved by @nesquena. ## Integration fixes applied **#1193:** Updated misleading comment — `OPENAI_API_KEY` does NOT authenticate the default Codex OAuth endpoint (that uses `chatgpt.com/backend-api/codex` and requires a separate OAuth flow). The comment now accurately states the known limitation. Also replaced a fragile 400-char source-scan test with an isolation-safe unit test. Note: OAuth-authenticated users already get detected via `hermes_cli.auth` — this fix only addresses the env-var fallback path. ## Test results **2764 passed, 2 skipped** (macOS-only workspace tests). Browser QA: **21/21**. `/api/sessions` confirmed returning `server_time` and `server_tz` fields. |
||
|
|
e61a405add |
fix: batch v0.50.231 — macOS symlink bypass, workspace panel, fenced code leak (#1194)
Batch release v0.50.231 — 3 fixes. ## PRs included | PR | Author | Fix | |---|---|---| | #1186 | @nesquena (Claude Code) | macOS `/etc` symlink bypass in workspace blocked-roots | | #1187 | @nesquena-hermes | Workspace panel stuck closed after empty-session reload | | #1190 | @bergeouss | Fenced code content leaking into markdown passes (#1154) | All three PRs were independently reviewed and approved by @nesquena. ## Test results **2729 passed, 2 skipped** (2 macOS-only tests correctly skipped on Linux). Browser QA: **21/21**. ## Key fix notes **#1186:** `_workspace_blocked_roots()` now returns both literal and `Path.resolve()` forms of each blocked root. macOS symlinks (`/etc → /private/etc`) previously let a resolved candidate slip past the literal check. New `_is_blocked_system_path()` helper with `/var/folders` and `/var/tmp` carve-outs for pytest temp dirs. **#1187:** Regression from #1182 — `syncWorkspacePanelState()` force-closed on any no-session state. Now only closes in `'preview'` mode. Both boot paths restore localStorage panel pref before sync. **#1190:** Fenced code blocks are now stashed as `\x00P<n>\x00` tokens through ALL markdown passes (list/heading/table regexes), restored at the very end. Previously, diff hunks and markdown headings inside code blocks triggered those regexes, injecting `<ul>/<li>/<h>` tags that broke `</pre>` closure. |
||
|
|
a091be6a8e |
fix: batch v0.50.229 — session perf, ephemeral sessions, iOS zoom (#1183)
Merged as v0.50.229. 2678 tests passing. Browser QA 21/21. All three PRs were independently reviewed and approved by @nesquena with reviewer commits pulled in: - #1181 (#1158): `d974388` (stale-response race in _loadOlderMessages) - #1182: `7e20006` (full-scan fallback path consistency) - #1180: `a5ad154` (regression test for iOS zoom threshold) Thanks @jasonjcwu (#1158)! |
||
|
|
ef26d19549 |
fix: batch v0.50.228 — renderer, model race, tool card, empty session, .env (#1179)
Merged as v0.50.228. 2644 tests passing. Browser QA 21/21 (desktop 1440×900 + mobile iPhone 14). All 5 fix invariants verified live in browser. **Fix verifications:** - #1172 (`renderMd` pre-stash): `rawPreStash` present in function, `<pre>` blocks pass through without content rewrite ✅ - #1174 (model race guard): `syncTopbar()` contains `liveStillPending` guard ✅ - #1175 (tool card): `.tool-card-result pre` max-height=360px, `.tool-card.open .tool-card-detail` overflow=auto, cap=600px ✅ - #1176 (empty session guard): double-click New Conversation on empty session → stays on same session, composer focused ✅ - #1178 (`.env` atomic write): `tempfile.mkstemp + os.replace` in `providers.py`, 9/9 env tests pass ✅ Thanks @bsgdigital (#1150) and @bergeouss (#1178)! |
||
|
|
8b8ff3328a |
fix: batch triage — 12 contributor PRs (v0.50.227) (#1168)
Merged as v0.50.227. 2634 tests passing, browser QA 21/21 (desktop + mobile). Full attribution below. Thanks to all 12 contributors: @jundev0001 (#1138), @franksong2702 (#1142, #1157, #1162), @dso2ng (#1143), @bergeouss (#1145, #1146, #1156, #1159), @jasonjcwu (#1149), @ccqqlo (#1161), @frap129 (#1165) Two fixes applied during integration and two more by the independent reviewer (@nesquena): - messages.js: per-turn cost delta capture order (#1159) - workspace.py: symlink target blocked-roots check + HOME sanity guard (#1149, #1165) - panels.js: cron unread counter bookkeeping (in-loop increment bug) - tests/test_symlink_cycle_detection.py: register workspace before session/new |
||
|
|
dca8624454 |
fix(ui): restore rail-era app titlebar state (v0.50.226) (#1163)
Merged as v0.50.226. Integration branch absorbed @aronprins's original PR #1141 with one reviewer fix from @nesquena (`1d11646`: queue hide tooltip updated to reference the queue pill, not the removed titlebar badge). **Full gate results:** - 2595 tests passing ✅ - Browser QA 21/21 (desktop 1440×900 + mobile iPhone 14) ✅ - Independent review: APPROVED by @nesquena ✅ Thank you @aronprins for the clean PR — the titlebar is properly restored. |
||
|
|
5192ca5de5 |
v0.50.225: cron attention, image lightbox, pytest isolation (#1137)
* feat: attention state for broken cron jobs + Korean i18n (#1133, @franksong2702) * fix: pytest state isolation for direct session saves (#1136, @franksong2702) * fix(#1095): image thumbnails in composer + lightbox in chat (#1135) * fix(css): restore cron attention + detail-alert rules overwritten by style.css merge (absorb) * docs: v0.50.225 release notes and version bump --------- Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com> |
||
|
|
fc0152b2fc |
v0.50.223: model picker, idle retry, drag-drop, CSP, clipboard copy (#1127)
* fix(#604): model picker shows all configured providers Two fixes to ensure the model picker surface every provider a user has configured: 1. Added env var detection for XAI_API_KEY (→ x-ai) and MISTRAL_API_KEY (→ mistralai). Previously these providers were only detectable via hermes auth or credential pool, not via environment variables. 2. Added config.yaml providers section scanning. Users who configure providers in config.yaml (e.g. providers.anthropic.api_key) without setting the corresponding env var will now see those providers in the model picker. Only providers with known model catalogs are added. - Added 12 regression tests * fix(#1112): allow Google Fonts in CSP style-src and font-src Mermaid themes inject @import for fonts.googleapis.com at render time. CSP style-src blocked these requests, causing console violations. - Add https://fonts.googleapis.com to style-src (CSS stylesheets) - Add https://fonts.gstatic.com to font-src (WOFF2/WOFF font files) - Add 3 regression tests + verify existing CSP tests still pass * fix(#1118): retry api() calls on network errors after long idle After a long idle period, the browser's TCP keep-alive connection to the server can become stale. The next fetch() throws a TypeError (network failure), causing 'Failed to load session' instead of transparently reconnecting. - Added retry loop in api() (workspace.js): up to 3 attempts - Only retries on TypeError (network failures), NOT on HTTP errors (4xx/5xx) - 401 redirects still fire immediately - Added 6 regression tests * feat(#1116): composer placeholder reflects active profile name When a named profile is active (not 'default'), the composer placeholder and title bar show the profile name (capitalised) instead of the global bot_name. Falls back to bot_name/'Hermes' for the default profile. - boot.js: applyBotName() checks S.activeProfile before _botName - panels.js: switchToProfile() calls applyBotName() after switch - Added 5 regression tests * feat(#1097): drag and drop workspace files into chat composer Files and folders in the workspace file tree are now draggable. Dropping them into the composer inserts @path reference at cursor position. OS file drag-and-drop (attach files) still works. - ui.js: _renderTreeItems sets draggable + dragstart with ws-path - panels.js: drop handler checks for application/ws-path first, inserts @path with smart spacing and cursor positioning - Added 9 regression tests * fix(#1096): copy buttons work — add clipboard-write Permissions-Policy Copy buttons on messages and code blocks were silently failing because the Permissions-Policy header did not include clipboard-write=(self). Firefox blocks navigator.clipboard.writeText() without explicit permission. - api/helpers.py: add clipboard-write=(self) to Permissions-Policy - ui.js: _copyText now catches clipboard API errors and falls back to execCommand('copy'). _fallbackCopy extracted as separate function with proper focus() call and visible-but-hidden positioning (not -9999px) - Added 8 regression tests * chore: CHANGELOG for v0.50.223 --------- Co-authored-by: bergeouss <bergeouss@users.noreply.github.com> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com> |
||
|
|
4528c6c848 |
v0.50.222: Korean locale, provider fixes, reasoning chip boot, Prism SRI (#1119)
* feat: add Korean locale support (#1093, @jundev0001) — 615 keys, copy_failed added * fix(#1094): provider deletion + false positive API key + threading deadlock (#1102, @bergeouss) * fix(#1103): show reasoning chip on page load not only after session load (#1114, @bergeouss) * fix(#1100): remove Prism CSS SRI integrity to fix intermittent blocking (#1115, @bergeouss) * fix(tests): update copy_failed locale count for 7 locales (Korean added) * fix: drop unused _cfg_cache import; update locale count comment --------- Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com> |
||
|
|
27b17a8fc8 |
v0.50.221: copy HTTP fix, inline images, mobile tap, custom providers x2 (#1117)
* fix(#1096): copy buttons fall back to execCommand on HTTP contexts - Add _copyText() helper: tries navigator.clipboard first, falls back to document.execCommand('copy') with hidden textarea when not in secure context - Update copyMsg() and addCopyButtons() to use helper instead of direct navigator.clipboard.writeText() - Code block copy button now has .catch() handler (was silently failing) - Error messages use t('copy_failed') for i18n instead of hardcoded string - Add copy_failed key to all 6 locale blocks (en, ru, es, de, zh, zh-Hant) - Add 10 regression tests * fix(#1095): render pasted/dragged images as inline preview instead of paperclip badge - User message attachments with image extensions now render as <img> via api/media endpoint, with click-to-fullscreen support - Non-image attachments still show paperclip + filename badge - Extracts filename from full path for display - Add 5 regression tests * fix: hoist _IMAGE_EXTS to module scope, add avif (absorb fix) * fix: improve mobile touch responsiveness for session list items iPad Safari has known issues with the click/dblclick pattern on touch: - :hover-triggered padding-right layout shift causes the first tap click to target the wrong element (actions button that just appeared) - No touch-action:manipulation means iOS still delays taps for double-tap zoom detection - The old onclick+ondblclick pattern is designed for mouse, not touch Changes: - CSS: Remove :hover from padding-right rule to prevent layout shift - CSS: Add touch-action:manipulation and -webkit-tap-highlight-color to .session-item for immediate tap response - JS: Replace onclick/ondblclick with onpointerup + manual 350ms double-tap detection — works consistently on mouse and touch * fix(#1106): iterate custom_providers[].models dict keys for dropdown population - After reading singular 'model' field, also iterate 'models' dict keys - Deduplicate: model field value not repeated if also in models dict - Skip non-string keys gracefully - Works for both named and unnamed custom_providers entries - Add 7 regression tests * fix(#1105): allow custom_providers hostnames through SSRF check - Build trusted hostname set from custom_providers[].base_url in config.yaml - These are user-explicitly configured endpoints — not SSRF risks - Hardcoded allowlist (ollama, localhost, 127.0.0.1, lmstudio) still active - Unknown private IPs still blocked - Add 7 tests (5 source analysis + 2 functional with mocked socket) * fix(tests): update hover padding assertions for #1110 touch fix (absorb) * fix(css): restore hover padding via @media (hover:hover) for mouse devices (absorb) * fix: filter right/middle-click from pointerup handler (absorb) * docs: v0.50.221 release notes and version bump --------- Co-authored-by: bergeouss <bergeouss@users.noreply.github.com> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com> Co-authored-by: sheng <378978764@qq.com> |
||
|
|
d67036db24 |
v0.50.220: workspace panel collapse + project color dot fix (#1090)
* fix(ui): workspace panel collapse priority + visible project color dot
Two related sidebar UI bugs from project-ui-bugs.md:
1. Workspace panel header had no collapse priority. As the right panel
narrowed, all three header children (Workspace label, git badge,
icon buttons) compressed at the same rate because `.panel-header`
used `justify-content:space-between` with no flex-shrink ratios.
The icon buttons -- the actual primary controls -- could disappear
before the git badge (which is least-essential metadata).
Fix: declare `.rightpanel` as a `container-type:inline-size` container.
Replace `justify-content:space-between` with `gap:6px` plus
`margin-left:auto` on `.panel-actions`. Set flex-shrink:0 on
`.panel-actions` (icons never shrink), flex-shrink:2 on the label,
flex-shrink:3 on `.git-badge` (shrinks fastest), and
`min-width:0;text-overflow:ellipsis` for graceful intermediate
shrink. Add @container queries that crisply set `display:none` on
the git badge below 220px and on the label below 160px.
2. Project color dot was appended INSIDE the `.session-title` span,
which is `overflow:hidden;text-overflow:ellipsis`. Long titles
clipped the dot off entirely -- hiding the project marker exactly
when it was most needed. The timestamp was also `position:absolute`,
so the title's `flex:1` ran underneath it and there was nowhere
coherent to anchor the dot.
Fix: in sessions.js, append the dot to `titleRow` between title and
timestamp (a flex sibling, not inside the truncating title span).
In style.css, move `.session-time` from absolute positioning to
`margin-left:auto` in the flex row. Drop the
`margin-left:4px/vertical-align:middle` from
`.session-project-dot` (gap:6px on the row handles spacing).
Reduce `.session-item` padding-right at rest from 86px (which was
reserving space for the absolute timestamp) to 8px; expand to 40px
on hover/streaming/unread/menu-open/focus-within so the absolute
action button + attention indicator still have room.
Tests:
- tests/test_workspace_panel_session_list.py (14 new tests)
- tests/test_issue856_pinned_indicator_layout.py updated to reflect
the new flex-flow timestamp + reduced rest-padding
Full suite: 2433 passed, 47 skipped, 0 PR-related failures.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ui): remove duplicate margin-left:auto from .git-badge
With .panel-actions already carrying margin-left:auto, both .git-badge
and .panel-actions having auto margins split the free space equally,
centering the badge instead of keeping it adjacent to the label.
Remove margin-left:auto and margin-right:4px from .git-badge. The
panel-header gap:6px handles label→badge spacing; panel-actions
margin-left:auto owns the right-push. Layout: [label][badge][→][actions].
* fix(ui): mobile session-item padding 86px → 40px + git-badge margin fix
Two fixes from Opus independent review of #1089:
1. Mobile padding regression: .session-item mobile override had
padding:10px 86px 10px 12px — the 86px was reserving space for the
old position:absolute timestamp. Since the timestamp now lives in the
flex flow of .session-title-row (margin-left:auto), that 86px
reservation is wasted and pushes the timestamp ~76px from the right
edge, leaving dead space between it and the always-visible action
button. Fixed: 86px → 40px (matching desktop hover/attention rule,
only enough for the absolute action button at right:6px + 26px wide).
2. Duplicate margin-left:auto on .git-badge: the old rule from master
had margin-left:auto on .git-badge (for the old space-between layout).
With .panel-actions also having margin-left:auto, the two auto margins
split free space equally, floating the badge to the middle of the header
instead of keeping it flush against the label. Removed margin-left:auto
and margin-right:4px from .git-badge; gap:6px on .panel-header handles
label→badge spacing; .panel-actions margin-left:auto owns the right-push.
Updated tests:
- test_workspace_panel_session_list.py: assert 40px mobile padding
- test_issue856_pinned_indicator_layout.py: assert 40px mobile padding
Verified by Playwright visual QA:
- Desktop 250px: badge hidden, Workspace label visible, icons visible ✓
- Desktop 150px: badge hidden, label hidden, icons only ✓
- Project dots visible on long-title sessions (outside truncating title span) ✓
- Mobile: padding-right=40px, no layout overflow ✓
* docs: v0.50.220 release notes, test count 2481, roadmap
---------
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
|
||
|
|
d625bac6d4 |
v0.50.219: project chip context menu + input auto-sizing (#1087)
* fix(projects): opaque context menu + auto-sizing rename/create input Two project chip UI bugs reported in project-ui-bugs.md: 1. Right-click context menu was transparent and the session list bled through it. Root cause: _showProjectContextMenu set background: var(--panel), but --panel is not defined anywhere in style.css, so the menu fell back to transparent. Fix: use var(--surface) -- the same opaque variable used by .session-action-menu and other floating popovers. 2. The rename and new-project input field was hard-coded to 100px regardless of the project name being edited (a 3-letter name got the same field size as a 20-letter name). Fix: drop width:100px from .project-create-input, replace with min-width:40px / max-width:180px / width:auto. Add a _resizeProjectInput() helper that measures the current value with a hidden span and sets pixel width inside those bounds. Wired into both _startProjectRename (called once on focus, again on every input event) and _startProjectCreate (same pattern). Tests: 9 new static-source tests in tests/test_project_chip_ui.py that pin (a) var(--panel) is undefined in style.css so the fallback trap doesn't return; (b) menu uses var(--surface); (c) the fixed width:100px is gone and min/max bounds are present; (d) the _resizeProjectInput helper is defined and called from both flows. Full suite: 2419 passed, 47 skipped, 0 PR-related failures. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(projects): use getComputedStyle in _resizeProjectInput sizer span Switch the hidden sizer span from hardcoded font-size:10px / font-family:inherit to reading the live values from getComputedStyle(inp). This keeps the sizer calibrated if the CSS rule ever changes, rather than silently drifting. Also update test_resize_helper_uses_hidden_span to assert getComputedStyle is used rather than the old literal font-size check. Suggested by Opus independent review of #1086. * docs: v0.50.219 release notes, test count 2467, roadmap update --------- Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com> |
||
|
|
498b51bfc6 |
v0.50.218: chat bubble overflow, project color picker, blockquote renderer (#1085)
* fix(css): add overflow-wrap:anywhere to chat bubbles — prevents long URL overflow (#1080) * fix(projects): rename now works via dblclick timer guard + right-click color picker (#1078) * fix(renderer): block-level constructs inside blockquotes now render Fenced code blocks, headings, horizontal rules, and ordered lists inside blockquotes now render correctly. Six related bugs documented in blockquote-rendering-bugs.md were collapsed into one architectural fix in renderMd(). Bugs fixed (all 6): 1. Fenced code blocks inside blockquotes -- > prefixes leaked into the <pre> body and the blockquote got fragmented around the rendered code, sometimes leaving raw <pre>/<div class="pre-header"> as visible text. 2. Blank > continuation lines fragmented multi-paragraph blockquotes into separate <blockquote> elements with literal > between them. 3. ## headings inside blockquotes rendered as literal "##" text. 4. Numbered lists inside blockquotes rendered as plain prose. 5. Complex blockquote (mixed headings + code + list + inline code) collapsed into a monospace blob with raw markdown syntax leaking everywhere. 6. Horizontal rules (---) inside blockquotes rendered as literal text. Root cause: The per-line passes for fenced code, headings, hr, ordered lists all ran BEFORE the blockquote handler and could not match lines that started with >, so by the time blockquote stripping ran those constructs had already been mishandled. Fix: A new blockquote pre-pass at the top of renderMd(): - Walks lines fence-aware so > -prefixed lines inside non-blockquote code fences (e.g. shell prompts in bash code blocks) are not miscaptured as a blockquote. - Groups consecutive > -prefixed lines, strips the > prefix, and recursively calls renderMd() on the stripped content. The recursive call handles all block-level constructs (fenced code, headings, hr, ordered/unordered lists, nested blockquotes) using the same pipeline. - Wraps the rendered HTML in <blockquote> and stashes it with a \x00Q token. Restored at the very end of renderMd() so no later pass can mangle the inner HTML. The old _applyBlockquotes regex-replace is removed entirely along with its limited inline branches for nested blockquotes and unordered lists. Behaviour change: Blockquotes now produce CommonMark-compliant <p> wrapping for text content (was: bare text directly inside <blockquote>). The visual output is the same in browsers but the HTML structure is now standard. Tests: - 14 new behavioural tests in tests/test_renderer_js_behaviour.py drive the actual renderMd() via node and lock all 6 bug fixes. - .local-review/test_blockquote_bugs.js -- node harness covering the same scenarios, runnable manually for fast iteration. - 2407/2408 tests pass (1 pre-existing macOS-only failure deselected). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(renderer): entity decode before blockquote pre-pass + CSS margin fix - Move the >/</& entity-decode to run at the very top of renderMd(), before the blockquote pre-pass. Previously decode() ran at line 756 (after the pre-pass at line 697), so LLM output containing >-encoded blockquotes was never matched by the pre-pass. - Add .msg-body blockquote p{margin:0} and .preview-md blockquote p{margin:0} so the new CommonMark-compliant <p> wrapping inside blockquotes doesn't add extra vertical spacing. Prior shape (bare text) had no default p-margins. - Add Node-driven tests: TestBlockquoteEntityEncodedInput covers > prefix and >-encoded fenced code inside blockquotes. - Add struct test: TestBlockquotePrePassOrdering::test_entity_decode_runs_before_blockquote_pre_pass locks decode < _bq_stash ordering in ui.js. Fixes found during Opus independent review of #1083. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs: v0.50.218 release notes, test count 2458, roadmap update --------- Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
62adc0c00d |
v0.50.217: /queue /interrupt /steer send normally when agent is idle (#1077)
* fix(commands): /queue /interrupt /steer send normally when agent is idle When the agent is not running, these three commands now fall through to a direct send() call (setting the input value and invoking send()) instead of showing an error toast. This matches CLI behaviour — the commands are mode-sensitive: they operate as queue/interrupt/steer when busy, and as normal sends when idle. Before: /queue hello → "No active task — just send normally" (toast, nothing sent) /steer hello → "No active task to stop." (misleading + nothing sent) /interrupt hi → "No active task to stop." (nothing sent) After: /queue hello → message sent immediately (same as typing and pressing Enter) /steer hello → message sent immediately /interrupt hi → message sent immediately Note: /stop when idle still shows "No active task" — that one is correct since stopping nothing is always an error. 15 new tests in test_cmd_idle_fallback.py covering the idle path for all three commands and verifying the active-session paths are unchanged. * test(commands): update stale test doc — /queue idle now sends, not rejects test_cmd_queue_requires_busy was written before the idle-send fallback existed. Its docstring said "/queue while not busy is a usage error" and the assertion message said "reject if idle" — both accurate for the old toast-and-return behaviour but wrong after this PR. The test assertion itself (`"if(!S.busy)" in body`) still passes because the idle guard still exists; it just routes to send() instead of a toast. Updating the name and copy to accurately describe what the code now does, so the test reads as documentation rather than as a contradiction. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: v0.50.217 release notes and version bump --------- Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
58ad315dca |
v0.50.216: compression chains, renderer fixes, HTML preview, approval z-index, /steer fix, reasoning chip (#1075)
* fix(workspace): add .html/.htm to MIME_MAP so HTML preview renders correctly
MIME_MAP was missing entries for .html and .htm. The server fell back to
Content-Type: application/octet-stream, which browsers refuse to render as
HTML in an iframe — causing a blank white preview.
The rest of the pipeline was already correct: the iframe exists in
static/index.html, openFile() in static/workspace.js routes .html to
showPreview('html'), and _handle_file_raw() in api/routes.py sets the
correct CSP sandbox header when ?inline=1 is present. The only missing
piece was the MIME type.
* test(workspace): lock in MIME_MAP entry for .html/.htm
PR #1070 added .html/.htm → text/html to MIME_MAP in api/config.py
to fix the blank workspace HTML preview iframe. Without a direct
assertion on the MIME_MAP entries, the fix could silently regress
(the existing test_779_html_preview.py tests cover the iframe wiring,
the inline=1 query handling, and the CSP sandbox header — but none of
them touch MIME_MAP itself).
Add a single regression test that asserts MIME_MAP['.html'] and
MIME_MAP['.htm'] are both 'text/html' so any future removal of those
entries fails CI immediately.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(composer): raise .approval-card.visible z-index above .queue-card
.queue-card has z-index:2. .approval-card.visible had no z-index, so the
queue flyout would render on top of the approval card when both were visible
simultaneously — obscuring the Allow/Deny buttons.
Fix: add z-index:3 to .approval-card.visible so approvals always render
above the queue flyout. Approval is a blocking, security-relevant interaction
and must never be obscured by passive UI elements.
* test(composer): pin approval-card z-index > queue-card invariant
PR #1071 raises .approval-card.visible to z-index:3 so the security-
relevant Allow / Deny buttons stay clickable when the queue flyout is
also open. Without a regression test, a future CSS edit could silently
drop the z-index back below queue-card (z-index:2) and reintroduce the
bug — there is no automated UI test covering this stacking interaction.
Add a focused regex check that pins the invariant:
.approval-card.visible z-index must be strictly greater than
.queue-card z-index.
Modeled on the existing CSS-regex regression style in
tests/test_mobile_layout.py (test_profile_dropdown_not_clipped_by_overflow).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: intercept /steer /interrupt /queue before busy-mode routing in send()
Root cause: slash commands entered while the agent is busy never reached
the command dispatcher. send() enters the busy block and returns early at
line ~50, so the slash-command intercept (~line 56) is never reached.
The text was queued as a plain message. When it drained after the turn
ended, cmdSteer / cmdInterrupt ran on an idle session, saw no active stream,
and showed "No active task to stop."
Fix: at the top of the busy block, before checking busyMode, check if the
text starts with / and is one of the three control commands. If so, dispatch
the handler immediately and return. This lets the user type /steer, /interrupt,
or /queue at any time — including while the agent is mid-stream — and have
them execute against the live session.
Two new regression tests added:
- test_slash_commands_intercepted_before_busymode_routing: verifies the
intercept appears before the busyMode routing in the busy block
- test_steer_intercept_calls_handler_directly: verifies the intercept calls
_bc.fn(_pc.args) and returns, not queues
* test(busy-intercept): pin sync input-clear before await in slash intercept
PR #1072's intercept clears the msg input before awaiting the handler.
Order matters: if the await happens first (or if the clear is moved
inside the handler), the input still shows '/steer foo' for the duration
of the await. A reflexive second Enter press during that window — common
while waiting for the toast — re-runs send(): either re-fires the
handler (double-steer) or, if the turn just ended, falls through to the
non-busy slash dispatcher and drops a confusing "No active task to stop."
Add test_steer_intercept_clears_input_before_await pinning the order so
this UX invariant cannot silently regress.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: update steer i18n and settings copy — steer no longer interrupts
With the real /steer implementation (agent.steer() via /api/chat/steer),
steer injects a correction mid-turn WITHOUT interrupting the current stream.
The previous copy said "falls back to interrupt", "Steer (interrupt + send)",
etc. — accurate only for the old placeholder, not the real implementation.
Changes across all 6 locales (en/ru/es/de/zh/zh-Hant):
cmd_steer: "falls back to interrupt" removed
settings_busy_input_mode_steer: "interrupt + send" → "mid-turn correction"
cmd_steer_fallback: "interrupted" → "queued for next turn"
busy_steer_fallback: "interrupted instead" → "queued for next turn"
settings_desc_busy_input_mode: "currently falls back to interrupt" removed
Also:
static/index.html: inline fallback text updated to match
static/commands.js: internal comment clarified (fallback = queue+cancel,
not "interrupt mode" which implies the primary action)
* fix(renderer): group consecutive blockquote lines into single element
Root cause: the old rule `s.replace(/^> (.+)$/gm, ...)` had three bugs:
1. `.+` required at least one character — bare `>` lines (blank
continuation lines) did not match and passed through as literal `>`
2. Each matching line became its own `<blockquote>` element — a 10-line
blockquote produced 10 stacked `<blockquote>` tags with no grouping
3. When a fenced code block sat inside a blockquote, the fence-stash
pass consumed the code content and left orphaned `>` lines that the
old `.+` pattern could not match
Fix: replace the single-line regex with a group-based approach that matches
one or more consecutive `>` lines as a single block, strips the `>` prefix
from each line, passes each non-empty line through inlineMd(), turns blank
`>` lines into `<br>`, and wraps the entire group in one `<blockquote>`.
14 regression tests added covering:
- Single-line blockquotes (regression)
- Multi-line grouping (2 and 10 lines)
- Two separate blockquotes staying separate
- Bare `>` and `>text` (no space) edge cases
- Blank continuation lines → <br>
- Bold / italic / inline-code inside blockquotes
- Blockquote followed by normal paragraph
* fix(renderer): drop empty trailing line from blockquote match
The new group-based blockquote rule introduced in this PR captures the
trailing newline in its (?:\n|$) clause. After block.split('\n') that
trailing newline produces an empty final element. The original filter
only dropped lone bare '>' artifacts on the last line, so the empty
final element survived, and the .map(blank → '<br>') step turned it
into a phantom <br> immediately before </blockquote>.
Visible symptom: any blockquote whose source ends with \n (the common
case — a quote followed by another paragraph or end-of-message) renders
with an extra blank line at the bottom of the quote.
Reproducer:
'> Hello\n\nThe rest of the message.'
→ '<blockquote>Hello\n<br></blockquote>\nThe rest of the message.'
^^^ phantom <br>
Fix: replace the single-line filter with a while-loop that pops trailing
lines while they are either empty OR a bare '>'. This matches the
intent the Python test mirror in tests/test_blockquote_rendering.py
already had (the mirror was correct; the JS was not — that's why
the original tests passed despite the bug).
Also add four new regression tests in TestNoPhantomTrailingBr that pin
the no-trailing-<br> invariant for the common shapes:
- input ending with \n
- quote followed by paragraph (the real-world case)
- multi-line quote ending with \n
- quote with blank continuation + trailing \n (internal <br> stays,
trailing <br> does not)
Verified end-to-end with node against the actual JS regex.
244 renderer-adjacent tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(renderer): comprehensive markdown fixes — strikethrough, task lists, CRLF, nested blockquotes
Five additional fixes on top of the blockquote grouping from the initial commit:
1. CRLF normalisation: strip \r\n → \n at start of renderMd so Windows
line endings do not produce stray \r characters in rendered output
2. Strikethrough: ~~text~~ → <del>text</del> in both inlineMd() (for use
inside blockquotes/lists) and the outer pass (for plain paragraphs).
Added <del> to SAFE_TAGS and SAFE_INLINE so it is not HTML-escaped.
3. Task lists: - [x] / - [ ] items in unordered lists render as ✅/☐
via task-done/task-todo span wrappers. Checks [X] (uppercase) too.
4. Nested blockquotes: >> / >>> etc. now recurse so each level gets its
own <blockquote> element rather than passing through as literal >.
Implemented by extracting the blockquote rule into _applyBlockquotes()
which calls itself recursively on the stripped inner content.
5. Lists inside blockquotes: > - item now renders <ul><li> inside the
blockquote instead of a literal "- item" string. Task list items work
inside blockquotes too (> - [x] done → ✅ inside <blockquote><ul>).
Also fixed test_issue342.py search window (5000→10000 chars) — the CRLF
strip at the top of renderMd pushed the autolink regex past the old limit.
68 new tests in test_renderer_comprehensive.py + test_blockquote_rendering.py
covering all constructs, edge cases, and combinations.
* fix(renderer): restore space in blockquote prefix-strip regex
Commit
|
||
|
|
3d96dc1498 |
v0.50.215: real /steer via agent.steer() — mid-turn correction without interrupt (#1069)
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com> Co-authored-by: nesquena <nesquena@users.noreply.github.com> |
||
|
|
520034c071 |
v0.50.214: busy input modes + queue/interrupt/steer slash commands (#1067)
* feat: busy input modes with queue/interrupt/steer slash commands - Add busy_input_mode setting (queue/interrupt/steer) to config defaults - Add /queue, /interrupt, /steer slash commands with handlers - Modify send() to respect busy_input_mode (interrupt cancels and resends, steer falls back to interrupt with toast, queue preserves existing behavior) - Add settings dropdown in settings panel with load/save/apply wiring - Initialize window._busyInputMode at boot and on settings save - Add 17 i18n keys across all 6 locale blocks (en/ru/es/de/zh/zh-Hant) Addresses #720 * test: 17 regression tests for busy_input_mode + slash commands PR description noted manual testing only. Added structural tests matching the pattern used by recent contributor PRs (#1010, #1011, #1018, #1022, #1058) so future refactors don't silently regress the wiring: Backend (api/config.py): - default 'queue' is set in _DEFAULT_SETTINGS - enum validator restricts to {queue, interrupt, steer} Slash commands (static/commands.js): - /queue, /interrupt, /steer all registered with correct fns - /interrupt and /steer set noEcho:true (the queued payload becomes the visible turn, not the slash invocation) - cmdQueue requires S.busy - cmdInterrupt + cmdSteer call queueSessionMessage before cancelStream (otherwise the drain has nothing to pick up) send() busy branch (static/messages.js): - reads window._busyInputMode - calls cancelStream on interrupt/steer - queues before cancelling (ordering invariant) Boot init + panels.js wiring (static/boot.js, static/panels.js): - both success and fallback paths set window._busyInputMode - load/save/apply path threads busy_input_mode through i18n (static/i18n.js): - all 17 new keys present in each of the 6 locale blocks Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: add noEcho:true to /queue; clear pendingFiles in all three slash handlers 1. /queue was missing noEcho:true — the dispatcher would echo the raw slash text as a user bubble, then the drain would send the queued message, causing a double-bubble in the conversation (#840 pattern). 2. cmdQueue, cmdInterrupt, and cmdSteer all captured S.pendingFiles into the queue payload but never cleared S.pendingFiles or called renderTray(). Staged files would remain in the tray and be re-attached on the next send(), duplicating attachments. Fix: add S.pendingFiles=[];renderTray() after updateQueueBadge(). 3. test_all_three_busy_commands_are_no_echo: expanded to cover /queue (was only interrupt + steer), now documents that all three must set noEcho:true. 4. test_slash_commands_clear_pending_files: new test that all three handlers clear S.pendingFiles and call renderTray() after enqueuing. Co-authored-by: bergeouss <bergeouss@users.noreply.github.com> * docs: v0.50.214 release notes and version bump --------- Co-authored-by: bergeouss <bergeouss@users.noreply.github.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com> |
||
|
|
360463dd8e |
v0.50.212: model cache perf (~30s→~1ms), session switch UX, cache isolation fix (#1063)
* fix(models): disk cache now used on restart, cold path locked, 24h TTL
Root causes fixed:
- reload_config() was deleting disk cache on every server start (cfg_mtime 0.0 vs real mtime).
Now saves old mtime before update and skips cache deletion on first-ever load.
- Cold path was running outside the lock causing thundering herd on startup.
Now extracted to _build_available_models_uncached() helper running inside RLock.
- Disk cache was never being checked before lock acquisition.
Now loads from disk BEFORE acquiring lock; cache hit returns without lock contention.
- Credential pool load_pool() was called per-provider per-request (~10s for zai).
Now cached in _CREDENTIAL_POOL_CACHE with 24h TTL.
Result: /api/models returns in ~1ms on restart instead of ~30s.
* fix(ui): block stale SSE events, cancel old stream on switch, clear pending files after send, focus textarea after switch, instant click for inactive sessions, rename session via titlebar dblclick
Key UX improvements:
- Block stale SSE responses from old sessions reaching new session DOM after switch
- Cancel in-flight streaming when switching sessions
- Clear pending files after send (prevents ghost attachments in tray)
- Auto-focus message textarea after session switch
- Instant click for inactive sessions (no loading spinner blocking)
- Double-click app titlebar to rename active session
- Persist/restore composer draft across session switches
* style: add user-select:none to session titles to prevent accidental text selection
* fix(models): prevent concurrent cold path runs with _cache_build_in_progress guard
Thread 2 was re-entering the cold path (via RLock) while Thread 1 was
still inside it, causing duplicate 10s zai load_pool() calls. The RLock
allows re-entry from the same thread, defeating the 'only one cold path'
guarantee. Now threads wait on _cache_build_cv instead of re-entering.
* fix(models): add missing global declarations, move mtime check to outer scope for test
* fix(models): attach _cache_build_cv to the RLock so notify_all() is safe
* fix(models): evict _CREDENTIAL_POOL_CACHE entries when provider cache is invalidated
Without this, invalidate_provider_models_cache(provider_id) cleared the
models cache but left stale CredentialPool objects in _CREDENTIAL_POOL_CACHE
for up to 24h. The next get_available_models() cold path would re-use the
stale pool instead of re-loading, meaning new credentials added by the user
wouldn't show up until the pool TTL expired.
Now evicts both provider_id and its canonical alias from the pool cache
so the next cold path re-loads from disk.
* fix(merge): restore #1024/#1025 work in static/sessions.js after rebase
The merge of master (commit
|
||
|
|
01404ac062 |
v0.50.211: compact timestamps, adaptive title refresh, settings picker fix (#1061)
* Shorten session sidebar relative time labels * feat: adaptive session title refresh based on conversation evolution Addresses #869 — the 'Optional' part: adapt session names to current conversation context instead of only generating once from the first exchange. Backend (api/streaming.py): - Add _latest_exchange_snippets() to extract last user+assistant pair - Add _count_exchanges() to count user messages - Add _get_title_refresh_interval() to read the setting - Add _run_background_title_refresh() — refreshes title from latest exchange with LLM, skips if title is unchanged or user manually renamed - Add _maybe_schedule_title_refresh() — checks exchange count and schedules refresh after stream_end (non-blocking) Config (api/config.py): - Add auto_title_refresh_every setting (default '0' = off) - Enum validation: {'0', '5', '10', '20'} Frontend: - Settings UI dropdown (static/index.html) - Wire up load/save in panels.js - i18n keys for all 6 locales (en/ru/es/de/zh/zh-Hant) Default: off. Opt-in via Settings > Conversation > Adaptive title refresh. * test: add 37 tests for adaptive title refresh helpers Covers all five new functions introduced in this PR: _count_exchanges, _latest_exchange_snippets, _get_title_refresh_interval, _run_background_title_refresh, _maybe_schedule_title_refresh Co-authored-by: bergeouss <bergeouss@users.noreply.github.com> * fix(settings): show selected state on theme/skin/font-size picker cards The CSS rule `#mainSettings .theme-pick-btn { border-color: var(--border) !important }` was overriding the inline `style.borderColor = "var(--accent)"` set by `_syncThemePicker()` and siblings — `!important` beats inline styles. Active cards showed no visual highlight. Fix: move to `.active` CSS class with `border-color:var(--accent)!important` so the active rule wins over the base rule, and clear the stale inline borderColor/boxShadow from the sync functions. 5 regression tests added. Closes #1057 * fix: rename test file to match PR number, fix stale issue reference * docs: v0.50.211 release notes and version bump Compact sidebar timestamps, adaptive title refresh (opt-in), settings picker fix. * docs(changelog): correct settings tab for adaptive title refresh The v0.50.211 entry for #1058 said "Settings → Appearance" but the toggle is actually rendered inside settingsPanePreferences (the Preferences tab) per static/index.html:604+. The commit message also had the wrong tab ("Conversation"). Updated CHANGELOG to match the actual UI surface so users can find the toggle. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: create state dir before writing settings file save_settings() called SETTINGS_FILE.write_text() without ensuring the parent directory exists. In fresh environments (CI, first run without HERMES_WEBUI_STATE_DIR set) this raised FileNotFoundError. Add mkdir(parents=True, exist_ok=True) before the write. --------- Co-authored-by: Pavol Biely <biely@webtec.sk> Co-authored-by: bergeouss <bergeouss@users.noreply.github.com> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
6c343aff84 |
v0.50.210: gpt-5.5, cron titles, agent cache, bfcache fix, onboarding fix, mermaid CSP, PWA auth (#1056)
* feat(models): add gpt-5.5 to openai, openai-codex, copilot catalogs Adds GPT-5.5 and GPT-5.5 Mini entries to the static _PROVIDER_MODELS catalog so they appear in the model picker for the openai, openai-codex, and copilot providers. Signed-off-by: Pix (PiClaw, claude-opus-4-7) via Hermes Agent * fix(models): add gpt-5.5-mini to copilot provider catalog * fix(renderer): suppress Mermaid Google Fonts CSP violation via fontFamily inherit (#1044) Mermaid's built-in 'dark' and 'default' themes inject an @import for fonts.googleapis.com/Manrope into every generated SVG. The CSP style-src only allows cdn.jsdelivr.net, so this request is blocked on every diagram render, filling the console with CSP errors. Fix: pass fontFamily:'inherit' (and fontSize:'14px') in the themeVariables block of mermaid.initialize() in renderMermaidBlocks(). This suppresses Mermaid's external font import and uses the page's existing font stack. Avoids adding fonts.googleapis.com to the CSP — no new external dependency, no font FOUT, consistent with the rest of the UI typography. 3 regression tests added in tests/test_1044_mermaid_csp_font.py. 2215/2215 tests passing. * fix(onboarding): non-standard provider/path cluster (#1029) * fix(bfcache): restore full layout on tab/session restore — rail, topbar, panels (#1045) The pageshow handler added for #822 only cleared the session search filter and re-rendered the session list. This left the rest of the layout chrome (topbar, rail icons, workspace panel, resize handles, gateway SSE) in the stale bfcache DOM state, causing a broken layout (oversized search icon, uninitialized rail) that required a hard refresh to fix. Fix: extend the pageshow handler to re-run the full set of layout sync calls that the boot IIFE runs on a fresh page load: syncTopbar() — restores model chip, title, topbar state syncWorkspacePanelState() — restores workspace panel open/closed _initResizePanels() — reattaches panel resize drag listeners startGatewaySSE() — reconnects the gateway SSE watcher (bfcache-persisted connections are dead) All four calls are typeof-guarded for safe degradation if a helper is not yet defined. The existing #822 fixes (sessionSearch clear + renderSessionListFromCache) are preserved unchanged. loadSession() is intentionally NOT re-called — it would cause message flicker; the sync calls above are sufficient to restore visual state. 7 regression tests added in tests/test_1045_bfcache_layout_restore.py. 2219/2219 tests passing. * fix(bfcache): also close open dropdowns on bfcache restore (#1045) Additional symptom noted in issue #1045: bfcache freezes the DOM including any open dropdown/popover state. The thinking-level selector (and other composer dropdowns) left open when navigating away would appear open without user interaction on tab restore. Extend the pageshow handler to call all four named close functions before the layout sync: closeModelDropdown() — composer model selector closeReasoningDropdown() — thinking/reasoning effort selector closeWsDropdown() — workspace chip dropdown closeProfileDropdown() — profile switcher dropdown All calls are typeof-guarded, matching the style of the layout sync calls already in the handler. 2 new tests (9 total in test_1045_bfcache_layout_restore.py): - pageshow closes all four named dropdowns - dropdown closes appear before layout sync calls (clean state first) 2221/2221 tests passing. * fix(bfcache): remove _initResizePanels() — bfcache preserves listeners * fix(bfcache): remove _initResizePanels from pageshow — bfcache preserves listeners; update test * fix(sessions): use cron job name as session title when available (#1032) * fix(test): add id column to messages table in cron title test fixture * fix(merge): inject cron title lookup into read_importable loop, remove stale sqlite3 block * fix(pwa): redirect to /login client-side on 401 — fixes iOS PWA auth expiry trap (#1038) When an auth session expires, the server returns a 302→/login for page requests. In a normal browser this works fine, but in an iOS PWA running in standalone mode the redirect navigates out of the PWA shell into Safari, leaving the app permanently stuck on 'Authentication required' with no recovery path. Fix: intercept 401 responses client-side before surfacing any error. - workspace.js api(): check res.status===401 first; call window.location.href='/login' and return immediately (no throw) - ui.js: add _redirectIfUnauth() helper; wire into all direct fetch() calls that bypass api() — api/models, api/models/live, api/upload All fetch paths that could receive a 401 now redirect cleanly within the PWA frame rather than opening Safari. 6 regression tests added in tests/test_1038_pwa_auth_redirect.py. 2175/2175 tests passing. * fix(pwa): preserve current URL in ?next= param on 401 redirect * fix(test): update 401-redirect assertion to accept ?next= URL format * feat(pwa): add _safeNextPath() to login.js so ?next= param is honored after re-login Addresses reviewer suggestion: the ?next= URL set on 401 redirect was ignored by the login success handler (always redirected to ./). _safeNextPath() validates and returns the ?next= param with open-redirect guards: rejects non-path-absolute inputs, // protocol-relative URLs, backslash variants, and control characters. 4 new regression tests added. * Implement session agent cache for AIAgent reuse Added session agent cache to reuse AIAgent across messages. * Implement agent caching for session management * Implement session agent eviction on session deletion Added session agent eviction to prevent turn count leakage in recycled sessions. * docs: v0.50.210 release notes — 7 PRs, 2239 tests (+27) * docs(changelog): drop stale [Unreleased] entries duplicated by v0.50.210 Three entries in the [Unreleased] section are duplicates of items now listed under v0.50.210: - Mermaid CSP font fix (#1044) → v0.50.210 / Mermaid Google Fonts CSP - bfcache layout restore (#1045) → v0.50.210 / bfcache layout and dropdown restore - iOS PWA auth redirect (#1038) → v0.50.210 / Login redirects back to original URL The original drafts landed in [Unreleased] when individual PRs (#1047, #1048, #1043) were approved; the v0.50.210 release-notes commit then added the same items under the version section without removing the [Unreleased] copies. Drop the duplicates so users reading the CHANGELOG don't see the same fix listed twice. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Signed-off-by: Pix (PiClaw, claude-opus-4-7) via Hermes Agent Co-authored-by: Pix (Hermes) <aliceisjustplaying@users.noreply.github.com> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com> Co-authored-by: qxxaa <mrhanoi@outlook.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |