mirror of
https://github.com/nesquena/hermes-webui.git
synced 2026-05-26 11:40:26 +00:00
f60db40133cf348232bea1024aaadca84dfbe97b
99 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
bff8cb2b58 |
fix: Nous Portal full live catalog + dropdown cache invalidation on provider remove
Closes #1538, #1539. Two related dropdown-staleness bugs reported by Deor (Discord, May 03 2026). #1538 — Nous Portal picker showed only 4 hardcoded models ========================================================= The Settings → Default Model picker, the composer model dropdown, the /model slash command, and the Settings → Providers card all showed only four Nous models (Claude Opus 4.6, Claude Sonnet 4.6, GPT-5.4 Mini, Gemini 3.1 Pro Preview) because `_PROVIDER_MODELS["nous"]` had four hardcoded entries and `_build_available_models_uncached()` fell through to the generic `pid in _PROVIDER_MODELS` branch. The actual Nous Portal catalog has 30 models live — Claude Opus 4.7, GPT-5.5, Kimi K2.6, MiniMax M2.7, Gemini 3.1 Pro/Flash, several Xiaomi/Tencent/StepFun entries, and more. Fix: - New `_format_nous_label()` helper in `api/config.py` — reuses the `_format_ollama_label()` token rules, drops the vendor namespace, and appends ` (via Nous)` so labels disambiguate from same-named direct- provider entries (e.g. "Claude Opus 4.7" via direct Anthropic). - New `elif pid == "nous":` branch in `_build_available_models_uncached()` mirroring the Ollama Cloud pattern: live-fetch through `hermes_cli.models.provider_model_ids("nous")`, prefix every id with `@nous:` (matches the existing routing convention from PR-era #854 and pinned in tests/test_nous_portal_routing.py), fall back to the curated 4-entry static list when hermes_cli is unavailable. - Same fix applied to `api/providers.py:get_providers()` — that's the separate code path that builds Settings → Providers card models, and it had the identical bug shape. #1539 — Removed provider lingered in dropdowns until restart ============================================================ After Settings → Providers → Remove, the provider's models still appeared in every model dropdown until the page was reloaded. The server-side TTL cache was correctly flushed (`set_provider_key()` calls `invalidate_models_cache()` on both add and remove) but JS-side caches were never dropped: - `_slashModelCache` / `_slashModelCachePromise` (commands.js) — feeds the `/model` slash-command suggestions. - `_dynamicModelLabels` / `window._configuredModelBadges` (ui.js) — populated by `populateModelDropdown()` on app boot and profile switch. Pre-fix, `_removeProviderKey()` only called `loadProvidersPanel()` which refreshed the providers card list but never asked any consumer to re-fetch /api/models. Fix: - `static/commands.js`: new `_invalidateSlashModelCache()` helper that nulls both cache slots, exposed on `window` (typeof-guarded so the module remains importable in headless vm contexts — needed by the existing tests/test_cli_only_slash_commands.py harness). - `static/panels.js`: new `_refreshModelDropdownsAfterProviderChange()` helper that calls the invalidator + `populateModelDropdown()`, wrapped in try/catch so the providers panel update never breaks if a downstream module hasn't loaded yet. Both `_saveProviderKey` and `_removeProviderKey` invoke it (defense-in-depth: same staleness shape applies to the add path too). Tests ----- - `tests/test_issue1538_nous_live_catalog.py` (12 tests): live-fetch surfaces ≥20 entries, every id starts with `@nous:`, every label ends with ` (via Nous)`, recent flagships (Opus 4.7, GPT-5.5, Kimi K2.6, Gemini 3.1 Pro, MiniMax M2.7) reach the dropdown, static fallback works when hermes_cli raises, label formatter unit tests (vendor namespace stripping, variant rendering, MiniMax mixed-case), the curated static list and its routing invariants are preserved. - `tests/test_issue1539_provider_removal_dropdown_invalidation.py` (11 tests): invalidator helper exists and clears both cache slots, exposed on window with typeof guard, both save and remove paths invoke the dropdown flush, helper calls both invalidator and populateModelDropdown, helper is resilient to missing modules, helper does not block panel refresh, server-side `set_provider_key → invalidate_models_cache` invariant pinned. Verified live on port 8789: `/api/models` Nous group returns 30 models (was 4); browser `document.getElementById('modelSelect')` exposes 30 options under the "Nous Portal" group; the dropdown-flush helper is callable from the browser and round-trip rebuild keeps the dropdown at 30 options. Test counts: - Full pytest: 4013 passed, 2 skipped, 3 xpassed, 0 failures (was 3990 → 4013, +23 from this PR). - QA harness pytest: 20 passed. - Browser API sanity: 11/11 passed. - Agent Browser CDP: 21/23 passed (the 2 SSE liveness failures reproduce on master and are unrelated to this PR). |
||
|
|
341b1ee6b6 |
fix(composer): distinct voice-mode icon, descriptive labels, opt-in pref (#1488)
Composer footer rendered two near-identical mic icons whose tooltips both
said "Voice input" — push-to-talk dictation and hands-free voice mode were
visually indistinguishable. Researched how ChatGPT/Claude/Gemini solve the
same problem and adopt the industry convention.
Changes:
- btnVoiceMode now uses Lucide audio-lines (6 vertical bars), the
universal voice-conversation glyph. Also registered in LI_PATHS.
- Distinct localized tooltips: voice_dictate ("Dictate") and
voice_mode_toggle ("Voice mode"), with active-state flips
(voice_dictate_active "Stop dictation", voice_mode_toggle_active
"Exit voice mode"). Legacy voice_toggle key removed (it resolved to
"Voice input" in every locale and caused the duplicate-tooltip bug).
- Voice mode is opt-in via Settings -> Preferences ->
"Hands-free voice mode button" (default off). Dictation mic stays
visible by default, unchanged. localStorage-backed; panels.js onchange
calls window._applyVoiceModePref() so the button appears/disappears
immediately without reload.
- 17 regression tests pin: distinct titles, audio-lines glyph, all 4
new keys in all 9 locales, removal of stale voice_toggle, English
labels match convention, pref gating (no unconditional display=''
left in boot.js), Settings checkbox + i18n, panels.js wiring,
active-state tooltip flips.
Browser-verified on port 8789: default state shows 1 mic; enabling
the pref makes the audio-waveform button appear live; tooltips read
"Dictate" and "Voice mode" distinctly.
Closes #1488
|
||
|
|
26d0f45791 |
fix: new-chat guard ignores in-flight streams (#1432) + profile form auto-capitalizes typed values (#1423)
Two unrelated UX bugs, both small surgical fixes with regression tests. Issue #1432 — "+" button doesn't open new chat during streaming ================================================================ Reported by @Olyno: clicking "+" after sending a first message keeps redirecting to the same chat instead of opening a new blank conversation, making parallel chats impossible until the first response finishes. Root cause: static/boot.js:691 (and the Cmd/Ctrl+K branch at :844) had an empty-session guard from #1171 that skipped newSession() when message_count===0: if(S.session && (S.session.message_count||0)===0){ $('msg').focus(); closeMobileSidebar(); return; } But during the first user turn of a brand-new session, message_count is still 0 server-side because the user message hasn't been merged into s.messages yet. The guard treated that as "empty" and silently dropped the click, blocking parallel chats for the entire stream duration. Fix: Tighten the predicate to also exclude in-flight state: if(S.session && (S.session.message_count||0)===0 && !S.busy && !S.session.active_stream_id && !S.session.pending_user_message){ $('msg').focus(); closeMobileSidebar(); return; } Same predicate applied to the Cmd/Ctrl+K handler at :844. The in-flight signal (active_stream_id || pending_user_message) is the same one _restoreSettledSession() in messages.js:1081 already uses to decide whether a session is "settled" — keeping both call sites aligned. Verified end-to-end: with S.busy=true and pending_user_message set, the old guard returned `block=true` (= the bug), the new guard returns `block=false` (= fixed). With a truly empty session (no busy, no pending), both old and new guards still block — preserving #1171 behavior. Issue #1423 — Profile name field auto-capitalizes typed values ============================================================== Self-reported (Mac app, May 1 2026): typing `hello` into the New Profile "Name" field shows `Hello` after blur/autofill, contradicting the "Lowercase letters, numbers, hyphens, underscores only" hint right next to it. The form lowercases on submit so stored data is correct, but the displayed value during typing is misleading. Root cause: static/panels.js:2532 had only autocomplete="off": <input type="text" id="profileFormName" placeholder="..." autocomplete="off" required> Missing three attributes that actually prevent the misbehavior: - autocapitalize="none" — mobile keyboards (iOS Safari, Android Chrome, WKWebView in the Mac app) auto-capitalize the first letter without it - autocorrect="off" — Safari runs autocorrect on blur, can rewrite hello→Hello - spellcheck="false" — desktop browsers may run spellcheck on blur Fix: Add the three attributes to profileFormName. Also added to profileFormBaseUrl since URLs are similarly bad targets for autocapitalize/autocorrect. profileFormApiKey is type="password" and already has correct browser behavior. Verified end-to-end against the live DOM: openProfileCreate() → getElementById('profileFormName').getAttribute(...) returns the new attributes correctly, with required preserved. Tests ----- 3648 passed, 2 skipped, 3 xpassed (was 3640 — added 8 new regression tests in test_1432_newchat_and_1423_profile_input.py). One pre-existing test had to be widened: tests/test_mobile_layout.py test_new_conversation_closes_mobile_sidebar grabbed only the first 500 chars of the btnNewChat handler block to scan for closeMobileSidebar. The new comment block pushed closeMobileSidebar past that window even though both calls are still present. Bumped the window to 1500 chars and the shortcut-block lines from 12 to 24 to match the multi-line guard. Closes #1432 Closes #1423 Reported by @Olyno (#1432, GitHub) |
||
|
|
bc17229a7d |
Merge PR #1402 from bergeouss: P2 improvements — cron history, toolsets per session, Codex OAuth
# Conflicts: # static/i18n.js |
||
|
|
fcba6fda1c |
Merge PR #1411 from nesquena-hermes: TTS toggle CSS specificity collision (#1409) + Ollama env var bleed (#1410)
# Conflicts: # CHANGELOG.md |
||
|
|
0e9bd651a4 |
fix: TTS toggle CSS specificity collision (#1409) + Ollama env var bleed (#1410)
Two unrelated UX/Settings bugs, both small surgical fixes with regression tests. Issue #1409 — TTS toggle has no effect ======================================= Reported via Discord: ticking Settings → Voice → "Text-to-Speech for responses" did nothing. The speaker icon never appeared on assistant messages despite the checkbox saving to localStorage correctly. Root cause (CSS specificity collision): static/panels.js _applyTtsEnabled() set btn.style.display = enabled ? '' : 'none' on every .msg-tts-btn. The '' branch removes the inline override, after which the .msg-tts-btn { display:none; } rule from style.css re-hides the button. Both branches left the icon hidden, so the toggle has been silently broken since #499 first shipped the TTS feature. Fix (body-class toggle, Option B from the issue): - panels.js: _applyTtsEnabled now toggles body.classList('tts-enabled') - style.css: new compound selector body.tts-enabled .msg-tts-btn { display:inline-flex; align-items:center; } - default-hidden rule (.msg-tts-btn{display:none;}) preserved so the icon stays hidden by default (CSS-only state) - boot.js paths that already call _applyTtsEnabled(localStorage…) work unchanged — the new function applies state at the body level instead of inline-styling individual buttons, so the rule survives renderMd() re-renders without re-querying every button Verified end-to-end against live server: getComputedStyle on a probe .msg-tts-btn returns display:flex when body has tts-enabled, display:none when it doesn't. Two regression tests in TestIssue1409TtsToggleBodyClass explicitly check for the body-class shape and forbid the broken inline-style pattern. Issue #1410 — Ollama (local) shows "API key configured" when only Ollama Cloud key is set ================================================================= Reported via Discord: configuring Ollama Cloud lit up the local Ollama card too. Both providers were mapped to OLLAMA_API_KEY in api/providers.py _PROVIDER_ENV_VAR. Root cause: api/providers.py:47-48 "ollama": "OLLAMA_API_KEY", "ollama-cloud": "OLLAMA_API_KEY", _provider_has_key("ollama") found the value the user set for Ollama Cloud and returned True. But the runtime code path in hermes_cli/runtime_provider.py only consumes OLLAMA_API_KEY when the base URL hostname is ollama.com (Ollama Cloud) — local Ollama is keyless by default and reaches a custom base URL with no auth. The WebUI was reporting "configured" for a key local Ollama doesn't even read. Fix (Option A from the issue body, preferred): - Drop bare "ollama" from _PROVIDER_ENV_VAR with an inline comment explaining why - _provider_has_key("ollama") falls through to the config.yaml branch, which already supports providers.ollama.api_key for local users who genuinely need to set a token - ollama-cloud retains its OLLAMA_API_KEY mapping unchanged Verified end-to-end against live server with OLLAMA_API_KEY=sk-cloud-key-test in env: GET /api/providers reports has_key=True only for ollama-cloud, and has_key=False for bare ollama. Two regression tests in TestIssue1410OllamaEnvVarBleed cover the bleed-prevention case AND the "local user with config.yaml api_key still reports configured" case to guard against over-correction. Tests ----- 3572 passed, 2 skipped, 3 xpassed (was 3567 — added 5 new regression tests). Closes #1409 Closes #1410 Reported by @AvidFuturist (Discord, May 1 2026) |
||
|
|
6ad7a4cc83 | Merge PR #1405 from bergeouss: P3 features (insights, rollback, voice mode, subagent tree, redact toggle) | ||
|
|
6f55b973e5 | Merge PR #1390 from starship-s: preserve session provider context | ||
|
|
ae40af03d7 |
feat: P3 improvements — insights panel, rollback UI, voice mode, subagent tree, api redact toggle
- #464 Insights panel: usage analytics dashboard with session/message/token stats, model breakdown, activity by day/hour charts, token breakdown (GET /api/insights) - #466 Rollback UI: checkpoint list, diff viewer, restore confirmation (api/rollback.py, GET /api/rollback/{list,diff}, POST /api/rollback/restore) - #1333 Voice mode: turn-based STT→send→TTS loop using Web Speech API, progressive enhancement with pulsing indicator and auto-resume - #494 Subagent session tree: parent→children grouping in sidebar with expand/collapse chevrons, child count badges, localStorage persistence - #1396 API redact toggle: Settings checkbox to disable forced redaction for self-hosted users (lazy check at call-time, default ON) - #1385 Closed: compact tool activity toggle already exists in Settings - #497 Commented: proposed shared-file bridge for cross-process gateway approvals - i18n: tab_insights added to all 8 locales, voice/checkpoint keys to EN+RU |
||
|
|
8ae198e88c |
feat: P2 improvements — cron history, toolsets per session, Codex OAuth
- #468: Cron run history — GET /api/crons/history (metadata listing) + GET /api/crons/run (full output), lazy-load on click in Tasks panel - #493: Per-session toolset override — Session.enabled_toolsets field, POST /api/session/toolsets endpoint, streaming handler override, composer chip UI with dropdown (matches reasoning chip pattern) - #1362: In-app Codex OAuth — device-code flow (stdlib only, no httpx), SSE polling endpoint, onboarding wizard login button - #1240: Design proposal comment for provider/model source-of-truth |
||
|
|
8439817c76 | fix: keep profile placeholder refresh in switch path | ||
|
|
1a76e8761e | Mobile composer layout: progressive-disclosure config panel + scoped titlebar safe-area (#1381) | ||
|
|
bdc328d034 |
fix: preserve webui model provider context
Persist session model_provider separately from model IDs so active/default provider selections like gpt-5.5 remain bare while routing through OpenAI Codex. Keep @provider:model for picker disambiguation and runtime bridging, and preserve explicit OpenRouter plus custom/proxy base_url routing. |
||
|
|
f8754ded70 |
fix(autosave): guard preferences-autosave dirty-clear when password/model pending (Opus SHOULD-FIX Q1)
Pre-release Opus review of v0.50.250 caught a UX regression in PR #1369: _autosavePreferencesSettings unconditionally cleared _settingsDirty=false and hid the unsaved-changes bar on every successful autosave. But password and model are still committed via the explicit 'Save Settings' button (password for security; model goes through /api/default-model). Race scenario: 1. User opens System pane, types a new password (sets _settingsDirty=true; bar appears on close) 2. User switches to Preferences, toggles any checkbox -> autosave fires -> _settingsDirty=false, bar permanently suppressed 3. User closes panel -> _closeSettingsPanel short-circuits because !_settingsDirty -> typed password silently discarded (loadSettingsPanel blanks pwField.value='' on next open) Same shape with model selector: pick a new default model, then toggle any preference -> autosave fires -> no warning on close -> model never persists. Fix: only clear _settingsDirty and hide settingsUnsavedBar when both the password field is empty AND the model selector matches its on-open snapshot. Pinned by an updated regression test asserting the conditional guard exists. |
||
|
|
645dfa25af |
fix: autosave preferences settings (#1369, fixes #1003 phase 2)
Phase 2 of #1003: extend the autosave pattern from the Appearance panel to the Preferences panel so all preference changes are saved automatically without requiring a manual 'Save Settings' click. Mirrors the Phase 1 (Appearance) pattern exactly: - 350ms debounce on field changes (500ms additional debounce on the bot_name text input — effective ~850ms latency for typing) - Inline status feedback (saving / saved / failed + retry button) - Clears dirty flag and hides unsaved-changes bar after successful save - Password field excluded — still requires explicit save (security) - Model selector excluded — still requires explicit save 13 fields now autosaving: send_key, language, show_token_usage, simplified_tool_calling, show_cli_sessions, sync_to_insights, check_for_updates, sound_enabled, notifications_enabled, sidebar_density, auto_title_refresh_every, busy_input_mode, bot_name. i18n keys (settings_autosave_saving/saved/failed/retry) already exist in all 8 locales from Phase 1. Co-authored-by: Feco Linhares <feco.linhares@gmail.com> |
||
|
|
4683a4a0d0 |
fix(models): default model rehydration when providers share slash-qualified IDs (#1313)
From PR #1326. Co-authored-by: hacker2005 <chen20057275@outlook.com> |
||
|
|
3f838fc31a |
release: v0.50.244 (#1308)
release: v0.50.244 Batch release of 4 PRs: - #1303 (@fecolinhares) — TTS playback of agent responses via Web Speech API. Per-message speaker button + auto-read toggle + voice/rate/pitch in Settings. localStorage-only state. Closes #499. - #1304 — Stale saved session 404 cleanup + structured api() errors. Salvaged from #1084. Independently approved on |
||
|
|
0ad95cb16a |
release: v0.50.241 (#1293)
release: v0.50.241
Batch release of 4 PRs:
- #1290 (@nickgiulioni1) — Inline audio/video media editor with playback
speed controls and HTTP byte-range streaming. PDF/media previews in
workspace file browser. Composer tray inline players for audio/video.
(Rebased from #1232.)
- #1287 (@renatomott) — Configured model badges (Primary / Fallback N) in
the model picker, carried through to the composer chip. Persists through
on-disk model cache.
- #1289 (@franksong2702) — Appearance autosave for theme/skin/font-size in
Settings; inline Saving / Saved / Failed status. Font size now persists
to config.yaml. Refs #1003.
- #1294 (@franksong2702) — Normalize agent session source metadata
(raw_source / session_source / source_label) through /api/sessions and
gateway watcher SSE snapshots. Existing source_tag / is_cli_session
fields preserved. Refs #1013.
Tests: 3254 passed, 2 skipped, 3 xpassed (was 3199 before this release).
Independently reviewed and approved by nesquena (commit
|
||
|
|
33a145a669 |
release: v0.50.240
## Release v0.50.240 Batch release of 13 PRs that passed full triage + code review + test suite (3199 tests, 0 failures). --- ### Added - **Compact tool activity mode** (`simplified_tool_calling`, default on) — groups tool calls and thinking traces into a single collapsed "Activity" disclosure card per assistant turn. Also adds a new **Calm Console** theme with earth/slate palette and serif prose. @Michaelyklam — #1282 - **PDF first-page preview** — `MEDIA:` `.pdf` files render a canvas thumbnail via PDF.js CDN (4 MB cap). **HTML sandbox iframe** — `.html`/`.htm` files render inline in a sandboxed `<iframe srcdoc>` (256 KB cap). 10 i18n keys × 7 locales. @bergeouss — #1280, closes #480 #482 - **Inline Excalidraw diagram preview** — `.excalidraw` files render as pure SVG (no external deps; rectangles, ellipses, diamonds, text, lines, arrows, freehand; 512 KB cap). @bergeouss — #1279, closes #479 - **Inline CSV table rendering** — fenced `csv` blocks and `MEDIA:` CSV files render as scrollable HTML tables with auto-separator detection. @bergeouss — #1277, closes #485 - **Inline SVG, audio, and video rendering** — SVG as `<img>`, audio as `<audio controls>`, video as `<video controls>`. @bergeouss — #1276, closes #481 - **Batch session select mode** — multi-select sessions for bulk Archive/Delete/Move. 11 i18n keys × 7 locales. @bergeouss — #1275, closes #568 - **Collapsible skill category headers** — click to collapse/expand without re-render; state persists across filter cycles. @bergeouss — #1281 - **`providers.only_configured` setting** — opt-in flag to restrict the model picker to explicitly configured providers. @KingBoyAndGirl — #1268 - **OpenCode Go model catalog** — adds Kimi K2.6, DeepSeek V4 Pro/Flash, MiMo V2.5/Pro, Qwen3.6/3.5 Plus. @nesquena-hermes — #1284, closes #1269 ### Fixed - **Profile `TERMINAL_CWD` TypeError** — `_build_agent_thread_env()` helper merges env before `_set_thread_env()` call. @hi-friday — #1266 - **Service worker subpath cache bypass** — regex now matches `/api/*` under any mount prefix. @Michaelyklam — #1278 - **SSE client disconnect leaks** — `TimeoutError`/`OSError` treated as clean disconnects; server backlog 64, threads daemonized; session list renders before saved-session restore. @KayZz69 — #1267 - **i18n locale corrections** — Korean MCP strings (23), Chinese MCP strings (23), zh-Hant missing keys (41), de missing keys (229). @bergeouss — #1274, closes #1273 --- ### Test results ``` 3199 passed, 2 skipped, 3 xpassed in 72.79s ``` ### PRs on hold (not included) #1265 (draft), #1271 (superseded by #1266), #1272 (skipped XSS tests), #1232 (partial test run), #1222 (review questions open), #1134 (live-server tests), #1132 (superseded by #1134), #1108 (negative UX review), #1084 (empty description) |
||
|
|
867f2a3f81 |
absorb: address Opus review findings (security + correctness)
B1: fix stored XSS in MCP delete button — replace inline onclick with
data-mcp-name attribute + event delegation (panels.js)
B2: fix zip/tar-slip via startswith prefix collision — use
is_relative_to(); track actual extracted bytes instead of trusting
member.file_size (upload.py)
B3: add NVIDIA NIM endpoint to _OPENAI_COMPAT_ENDPOINTS and
_SUPPORTED_PROVIDER_SETUPS so provider is reachable (routes.py,
onboarding.py)
H1: add terminalResizeHandle element to index.html and return it from
_terminalEls() so resize-by-drag works (index.html, terminal.js)
H2: fix dead get_terminal() branch — return None for dead terminals
instead of always returning term (terminal.py)
H3: replace os.environ.copy() with a safe allowlist in PTY shell env
so API keys are not exposed inside the terminal (terminal.py)
H5: make model dedup deterministic — sort groups by provider_id
alphabetically before first-occurrence assignment (config.py)
H7: add pid regex validation before OAuth probe; constrain key_source
to a closed set of safe values (providers.py)
M8: add double-run guard for cron run-now — reject if job is already
tracked as running (routes.py)
|
||
|
|
9806a42a26 |
fix: protect secrets from masked-value round-trip overwrite (#1237)
- Add _strip_masked_values() to skip masked placeholders in PUT endpoint, preserving the original stored secret values instead of overwriting them - Fix transport badge to gracefully handle unknown/future transport types with a fallback that shows the raw string - Add TestStripMaskedValues (5 tests) for the round-trip protection logic - Addresses reviewer feedback on secret masking semantics and transport badge |
||
|
|
b2771ebf69 |
feat: MCP server management UI (#538)
- Add GET /api/mcp/servers (list with masked secrets) - Add PUT /api/mcp/servers/<name> (add/update stdio and http servers) - Add DELETE /api/mcp/servers/<name> (remove server) - MCP section in System settings with server list, add/delete form - Auto-detect transport type (stdio vs http) from server config - Mask sensitive values (API keys, tokens, passwords) in list response - Uses showConfirmDialog for delete confirmation (no native confirm) - i18n: 21 keys across 7 locales - 21 tests (list, save, delete, mask_secrets, validation) |
||
|
|
acbd0c14f2 |
fix: sanitize err.message in workspace reorder error toast (#1233)
- Remove raw err.message from error toast to prevent leaking internal error details to the UI (Path Trust Boundary Rule) - Use i18n key workspace_reorder_failed for the sanitized message - Addresses reviewer concern about optimistic vs confirmed reorder: the reorder is confirmed (API-first), not optimistic |
||
|
|
103a9833d5 |
feat: workspace drag-to-reorder (#492)
- Add POST /api/workspaces/reorder endpoint to reorder workspace list - Implement HTML5 drag-and-drop in workspace panel (panels.js) - Add grip-vertical drag handle icon (icons.js) - Add drag visual states: dragging, drag-over, cursor styles (style.css) - Add i18n keys (workspace_drag_hint, workspace_reorder_failed) in all 7 locales - 11 tests: 7 backend (order, strip, preserve, dedup, unknown, validation) + 4 frontend Closes #492 |
||
|
|
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.
|
||
|
|
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 |
||
|
|
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})
|
||
|
|
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.
|
||
|
|
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 |
||
|
|
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> |
||
|
|
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> |
||
|
|
7d1aa2e261 |
v0.50.209: check-for-updates, workspace toggle, HTML preview, provider categories, queue flyout docs (#1042)
* feat: add manual 'Check for Updates' button in System settings (#785) Add a 'Check now' button next to the version badge in the System settings section, allowing users to manually trigger an update check at any time without waiting for the automatic periodic check. Changes: - index.html: add button with spinner and status text inline with version badge - panels.js: add checkUpdatesNow() calling /api/updates/check?force=1 with immediate feedback (checking... / up to date / X updates available) - style.css: style the button block and spinner - i18n.js: add 5 new keys (settings_check_now, settings_checking, settings_up_to_date, settings_updates_available, settings_updates_disabled) in all 6 locales (en, ru, es, de, zh, zh-Hant) * fix: sanitize error message in checkUpdatesNow to avoid exposing paths Review feedback: strip filesystem paths from error messages and cap length to prevent internal details leaking into the UI. * fix: fully sanitize error in update check — never expose raw e.message in UI Previous partial fix ( |
||
|
|
ad8e10304c |
v0.50.207: batch of 10 PRs — TPS stat, SSE guard, session polish, cron UX, folder create, model errors, session speed, title gen (#1031)
* fix: remove orphaned i18n keys from top-level LOCALES object Three Traditional Chinese translation keys (cmd_status, memory_saved, profile_delete_title) were placed outside any locale block between the en and ru blocks in static/i18n.js. They became top-level properties of the LOCALES object, causing them to appear as invalid language options in the Settings > Preferences dropdown. The correct translations already exist in the zh-Hant locale block. Fixes #1008 * fix: block stale SSE events from polluting new session's DOM - appendThinking(): guard with !S.session||!S.activeStreamId to drop events from a previous session's SSE stream during a session switch - appendLiveToolCard(): same guard for consistency - finalizeThinkingCard(): scroll thinking-card-body to top when scroll is pinned, so completed response is immediately visible - appendThinking(): auto-scroll thinking card body to bottom while streaming if user is watching (scroll pinned) * Fix empty agent sessions in sidebar * fix: resolve cron UI UX issues — icon ambiguity, toast overlap, running status Fixes #995 — three sub-issues in the Cron Jobs UI: 1. Dual play icons ambiguous: Resume button now shows a distinct play+bar icon (play triangle + vertical line) instead of the identical triangle used by Run now. 2. Toast notification overlapping header buttons: Added position:relative; z-index:10 to .main-view-header so it stacks above the fixed toast (z-index:100 within its layer). 3. No running status after trigger: After triggering a job, the status badge immediately shows 'running…' with a CSS spinner animation, and polls the cron list every 3s (up to 30s) to refresh when the job completes. - Added cron_status_running i18n key in all 5 locales (en, es, de, ru, zh, zh-Hant) - Added .detail-badge.running CSS class with spinner animation - New functions: _setCronDetailStatus(), _startCronRunningPoll() * fix(#1011): address review feedback — poll cleanup, badge persistence, 30s fallback - _clearCronDetail() now clears _cronRunningPoll interval on navigation - Poll re-applies 'running' badge after loadCrons() re-render (prevents flicker) - When poll ends (30s max), detail re-renders with actual status as fallback * feat: create folder and add space directly from UI (#782) - After creating a folder via the file tree New folder button, offer to add it as a space via confirm dialog - Add Create folder if it doesnt exist checkbox in the New Space form - Backend: support create flag in /api/workspaces/add to mkdir before validation - i18n: 4 new keys (folder_add_as_space_title/msg/btn, workspace_auto_create_folder) in all 6 locales * fix: validate workspace path before mkdir to prevent orphan directories Review feedback (critical): the previous code called mkdir() before validate_workspace_to_add(), which meant a rejected path (e.g. system dir) would leave an orphan directory on disk. New flow: 1. Resolve path and check against blocked system roots BEFORE any mutation 2. mkdir() only if path passes the blocklist check 3. Full validation (exists, is_dir) after mkdir Also imports _workspace_blocked_roots for the pre-mutation blocklist check. * fix(#1014): classify model-not-found errors with helpful message - Add model_not_found error type to streaming.py exception classifier - Detect 404, 'not found', 'does not exist', 'invalid model' patterns - Strip HTML tags from provider error messages (nginx 404 pages, etc.) - Add model_not_found branch to apperror handler in messages.js - Add i18n key model_not_found_label in all 6 locales - 15 tests covering detection, sanitization, frontend, and i18n * feat(ui): add live TPS stat to header Adds a TPS (Tokens Per Second) chip to the right of the header title bar that updates live while AI output is streaming. Metering (api/metering.py) - Tracks per-session output + reasoning tokens via GlobalMeter singleton - Per-session TPS = total_tokens / elapsed_time - Global TPS = average of active sessions' TPS values - HIGH/LOW are max/min of global_tps snapshots over a 60-minute rolling window (only recorded when > 0, so idle periods are excluded) - Thread-safe with a single lock Metering events emitted from streaming.py - Throttled at 100ms from token/reasoning/tool callbacks so the display updates rapidly during fast token streams - 1Hz ticker as fallback for slow streams (exits when no active sessions) - Final stats emitted on stream end Routes (api/routes.py) - Removed POST /api/metering/interval endpoint (dynamic interval via focus/blur was replaced with simple always-1s-when-active approach) UI (static/messages.js, index.html, style.css) - TPS chip in titlebar: shows 'N.N t/s . N.N high . N.N low' - Default: '0.0 t/s . 0.0 high' when idle - Display updates on every metering SSE event (throttled to 100ms) * feat: session restore speed + title gen reasoning hardening (#1025, #1026) PR #1025 (@franksong2702): Speed up large session restore paths - GET /api/session?messages=0 now parses only metadata before the messages array - Metadata-only loads no longer populate the full-session LRU cache - Frontend lazy fetch uses resolve_model=0 to avoid cold model-catalog lookup - Hard reload no longer waits for populateModelDropdown() before restoring session PR #1026 (@franksong2702): Harden auto title generation for reasoning models - Raises title-gen completion budget to 512 tokens (reasoning-safe) - Retries once with 1024 tokens on empty content / finish_reason:length - Applies retry to both auxiliary and active-agent fallback routes - Preserves underlying failure reason in title_status on local fallback Co-authored-by: Frank Song <franksong2702@gmail.com> * feat: session attention indicators in right slot + last_message_at timestamps (#1024) PR #1024 (@franksong2702): Polish session attention indicators - Streaming spinners and unread dots now reuse the right-side actions slot - Running/unread rows hide timestamps; idle/read rows keep right-aligned timestamps - Date group carets point down when expanded, right when collapsed - Pinned group no longer repeats pinned-star icon per row - Running indicators appear immediately after send (local busy state while /api/sessions catches up) - Sidebar sorting/grouping/timestamps now prefer last_message_at (derived from last real message) so metadata-only saves don't make old sessions appear under Today Co-authored-by: Frank Song <franksong2702@gmail.com> * docs: v0.50.207 release notes — 10 PRs, 2169 tests (+36) --------- Co-authored-by: bergeouss <bergeouss@users.noreply.github.com> Co-authored-by: Josh <josh@fyul.link> Co-authored-by: Frank Song <franksong2702@gmail.com> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com> |
||
|
|
970bc1d3fd |
refactor(ui): three-column layout with left rail + main-view migration (#899)
refactor(ui): three-column layout with left rail + main-view migration (#899) Unifies the shell into a three-column layout (rail + sidebar + main) matching the hermes-desktop reference, and migrates every per-item detail/edit surface into a shared main-view canvas with consistent headers, empty states, and action buttons. Changes: - New desktop-only left rail (48px) with 8 nav tabs (chat/tasks/skills/memory/workspaces/profiles/todos/settings) - Persistent app titlebar (replaces per-chat topbar), active conversation title shown - All panel detail/create/edit views migrated to #mainSkills, #mainTasks, #mainSettings, #mainWorkspaces, #mainProfiles, #mainMemory - Settings moved out of modal into main-view page; ESC closes it - YAML frontmatter rendered in collapsible <details> block in skill detail - Toasts repositioned from bottom-center to top-right with theme-aware success/error/warning/info variants - Composer workspace chip split into two-button group: files-icon toggles file panel, label opens workspace picker - .settings-menu → .side-menu / .side-menu-item (generalised, shared by memory and settings panels) - i18n: ~25 new keys across en/ru/es/de/zh/zh-Hant for all new form labels, placeholders, and empty states - Mobile: hamburger in titlebar, slide-in sidebar; box-shadow removed from sidebar - New regression test: tests/test_settings_navigation_and_detail_refresh.py (9 tests) Co-authored-by: Aron Prins <pwf.aron@gmail.com> |
||
|
|
07caaec6ef |
fix(mobile): adapt settings dialog and message controls for mobile screens (#919)
* fix(mobile): adapt settings dialog and message controls for mobile screens (#915) Co-authored-by: bsgdigital * fix(mobile): adapt settings dialog and message controls for mobile screens (v0.50.177, #915) Co-authored-by: bsgdigital --------- Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com> |
||
|
|
1175ee363f |
fix(models): duplicate dropdown entries, stale default model, lowercase injected label (#907 #908 #909) (#918)
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com> |
||
|
|
4089972b09 |
fix(models): preserve @nous: prefix in settings + fix cross-namespace 404 for Nous (#895 #894) (#901)
* fix(models): preserve @nous: prefix in settings + fix cross-namespace 404 for Nous (#895 #894) * fix(review): persist bare form for CLI compatibility + picker smart-match The PR persisted `@nous:anthropic/claude-opus-4.6` verbatim to config.yaml to make the Settings picker match its dropdown options (which carry the `@nous:` prefix after #885). That fixes the WebUI picker but introduces a cross-tool regression: hermes-agent's CLI reads `config.yaml -> model.default` directly and passes it to the provider API verbatim. For aggregator providers (Nous is one — see hermes_cli/model_normalize.py `_AGGREGATOR_PROVIDERS`), `normalize_model_for_provider` is skipped entirely (run_agent.py:887), so the literal `@nous:anthropic/...` string flows to the Nous API, which rejects it — breaking every user who runs `hermes` in the terminal right after saving via WebUI. Fix the tension at the picker rather than the persistence: the existing `_findModelInDropdown()` smart matcher already normalises both sides (lowercase, strip namespace prefix, dashes→dots) so a saved bare `anthropic/claude-opus-4.6` resolves to the `@nous:anthropic/claude-opus-4.6` option automatically. Applied this in panels.js via `_applyModelToDropdown()`. Changes: api/config.py revert the @-prefix preservation; persist the resolved bare/slash form (CLI-compatible) static/panels.js Settings picker uses _applyModelToDropdown() instead of raw `.value =` so saved bare forms still select the matching @nous: option tests test renamed + asserts bare persisted form; new test locks the smart-matcher contract This also improves behaviour for a dormant case not flagged in #895: a user who set their default via `hermes model X` and opens Settings for the first time used to see a blank picker (bare form vs prefixed options). Now the smart matcher finds the right option, so the "open Settings → save → bare form in config.yaml" round-trip is stable for both CLI- and WebUI-origin saves. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: update CHANGELOG v0.50.171 — bare-form persistence + picker smart-match --------- 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> |
||
|
|
498156a3e8 |
fix(settings): show live models in default model picker and apply to new chats (#872) (#900)
* fix(settings): show live models in default model picker and apply to new chats (#872) Two related bugs: 1. Settings > Preferences > Default Model dropdown only showed static models from /api/models — live-fetched models (e.g. @nous:anthropic/claude-opus-4.7) were missing. Now calls _fetchLiveModels() on the settings picker too. 2. New chats ignored the saved default model preference — they always used the chat-header dropdown value (which reflects the previous session's model). Now newSession() uses the saved default_model and syncs the dropdown. Extracted _addLiveModelsToSelect() from _fetchLiveModels() so cached live models can be applied to any <select> element (chat-header or settings picker). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(tests): update live-model prefix tests for _addLiveModelsToSelect extraction The tests searched for og.dataset.provider, _isPortalFetch, and openrouter exclusion patterns inside _fetchLiveModels(). These were extracted into _addLiveModelsToSelect() as part of the #872 fix. Updated regex targets to check _addLiveModelsToSelect first, falling back to _fetchLiveModels. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: add multi-tab note on window._defaultModel Clarifies that window._defaultModel is per-page-load and not synced across browser tabs, following maintainer feedback on #889. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: CHANGELOG for v0.50.170 * chore: trigger PR refresh after rebase --------- Co-authored-by: fr33m1nd <bergeouss@gmail.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com> |
||
|
|
62c56175b7 |
feat(workspaces): autocomplete trusted workspace paths — v0.50.162 (PR #880 by @franksong2702, closes #616)
Adds GET /api/workspaces/suggest endpoint and autocomplete dropdown in the Spaces panel. Suggestions limited to trusted roots (home, saved workspaces, boot default). Keyboard nav, Tab completion, hidden dir support. Symlink-escape and dotdot-escape invariants locked by regression tests. |
||
|
|
04b00065f9 |
feat: provider key management from Settings — v0.50.159 (PR #867 by @bergeouss, closes #586)
New Providers tab in Settings lets users add/update/remove API keys without editing .env. Six review fixes applied. 18 tests. |
||
|
|
f42f1c69ca |
fix: correct webui profile switching state — v0.50.150 (PR #849 by @migueltavares)
Three related profile-switching fixes: - Always persist hermes_profile=default cookie when switching back to default (was being cleared with max-age=0, causing fallback to process-global profile) - Replace undefined updateWorkspaceChip() with syncTopbar() in the sessionInProgress branch of switchToProfile() - Make sidebar/dropdown active-profile rendering prefer S.activeProfile client state when available, with safe fallback Tests: 1854 passing. |
||
|
|
11fd0d8412 |
feat(tasks): refresh button in cron panel + auto-refresh on job creation (closes #835)
* feat(tasks): refresh button in cron panel + hermes:cron_created event Add a ↺ refresh button to the Scheduled Jobs header so the job list can be reloaded without a full page refresh. Closes #835. - static/index.html: ↺ button with cronRefreshBtn id, calls loadCrons(true) - static/panels.js: loadCrons(animate) dims+disables the button while fetching, restores it in finally; hermes:cron_created window event auto-refreshes list when the agent creates a job from chat * test: add regression tests for cron refresh button + event listener The PR shipped without automated coverage (pure UI wiring). Filling that gap with 8 source-level tests: - Refresh button element exists with aria-label + title (icon-only a11y) - Button wires onclick to loadCrons(true) for the dim animation - Button sits in the same header row as "New job" - loadCrons() now accepts an animate parameter - loadCrons() restores the button's opacity/disabled in finally (so a throwing fetch doesn't leave the button stuck) - hermes:cron_created window listener is registered at module scope - Listener calls loadCrons() when dispatched Also rebased onto master (CHANGELOG conflict resolved — v0.50.143 → v0.50.142 since master's top is currently v0.50.141). Full suite: 1750 passed, 0 new failures. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.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> |
||
|
|
24fc9d4155 |
feat(appearance): font size setting with Small/Default/Large toggle (closes #833)
* feat(appearance): font size setting with Small/Default/Large toggle Add a font size preference to the Appearance settings pane. Three options (12px/14px/16px) follow the same three-button visual pattern as the Theme picker. Closes #833. - static/style.css: :root[data-font-size=small|large] CSS overrides - static/index.html: boot script applies from localStorage before CSS renders (no FOUC); fontSizePickerGrid HTML in Appearance pane - static/boot.js: _applyFontSize(), _pickFontSize(), _syncFontSizePicker() - static/panels.js: loadSettingsPanel syncs picker on open; _revertSettingsPreview restores on discard - static/i18n.js: settings_label_font_size + font_size_{small,default,large} keys in all 6 locales (en, ru, es, de, zh, zh-Hant) - tests/test_font_size_setting.py: 14 new tests * fix(ui): remove duplicate font-size picker + correct CHANGELOG issue ref Two small fixes on the font size feature: 1. Duplicate HTML IDs — the picker block was injected into BOTH settingsPaneAppearance (correct, next to Theme/Skin) AND settingsPanePreferences (accidental copy-paste). Duplicate IDs #fontSizePickerGrid and #settingsFontSize violate HTML spec and break the _syncFontSizePicker visual sync which reads via document.querySelectorAll('#fontSizePickerGrid .font-size-pick-btn') — only the first grid would update its highlight, leaving the second stale. $('settingsFontSize') via getElementById also always returns the first match, so the second hidden input never reflected the user's choice. Removed the Preferences-pane copy. The Appearance-pane copy is the one the PR description describes and is the correct home for it (next to Theme and Skin). 2. CHANGELOG trailer said `Closes #830.` but #830 is the session-search autocomplete PR — this feature closes #833. Fixed. Added two regression tests: - test_font_size_picker_not_duplicated: asserts each ID appears exactly once in index.html. - test_font_size_picker_lives_in_appearance_pane: asserts the picker sits inside settingsPaneAppearance and not any other pane. Full suite: 1754 passed, 0 failures. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.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> |