_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.
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.
- Add cumulative extraction size limit (_MAX_EXTRACTED_BYTES = 200 MB)
that tracks uncompressed file sizes during extraction to guard against
zip/tar bombs (small compressed archives that expand to huge sizes).
- On any extraction failure (disk full, corrupted member, size limit),
clean up the partially-extracted destination directory to avoid
leaving orphaned folders in the workspace.
- 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).
- 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
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.
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
- 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
- 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
- Test custom_providers entries (glmcode, deepseek) appear in get_providers()
- Test env var reference detection (${VAR_NAME} pattern)
- Test bare API key, missing key, empty/malformed entries
- Assert DeepSeek V4 models present, V3 deprecated models removed
- Assert GLM model series in _PROVIDER_MODELS and onboarding setup
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})
- 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
- Add zai (Z.AI / GLM / 智谱) to onboarding _SUPPORTED_PROVIDER_SETUPS
with default model glm-5.1
- Add GLM models (glm-5.1, glm-5, glm-5-turbo, glm-4.x) to _MODEL_LIST
for display in model dropdowns
- Update DeepSeek default_model from deepseek-chat-v3-0324 to deepseek-v4-flash
- Update DeepSeek default_base_url from /v1 to bare domain (API docs change)
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).
Address reviewer questions:
- Document that first-occurrence ordering is not stable across
config changes, but removing a provider causes re-dedup on next
cache rebuild, so sessions still match the new bare entry
- Confirm @provider_id: format is consistent with existing
_apply_provider_prefix() and resolved by resolve_model_provider()
(splits on first ':')
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
Avoids unnecessary latency on the Settings page by restricting the
OAuth auth-status fallback to providers that are not in _PROVIDER_ENV_VAR.
Review feedback (PR #1221): the get_auth_status() call in the else branch
was firing for every unconfigured API-key provider (openai, anthropic, etc.),
adding a network round-trip per provider. Now it only runs for providers
that are not known API-key providers (custom/OAuth-capable providers).
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.
- Add nvidia to _PROVIDER_DISPLAY, _PROVIDER_MODELS, and _PROVIDER_ALIASES
- Add nvidia to _PORTAL_PROVIDERS to preserve full model paths (e.g. qwen/qwen3-next-80b-a3b-instruct)
- Add NVIDIA_API_KEY to _PROVIDER_ENV_VAR for API key management
- Fixes 404 errors when using nvidia provider with models from multiple namespaces
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.
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
fix(workspace): Allow /var/home workspaces (#1199)
Carries code from @frap129's PR #1199. On systemd-homed (Fedora/RHEL),
home lives under /var/home/<user> — blocked by _is_blocked_system_path
because /var is in the blocked roots list. Fix: trust any path under
Path.home() as long as home != /. Also adds symmetric early-return
in validate_workspace_to_add.
2764 tests pass.
Co-authored-by: Joe Maples <joe@maples.dev>
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.
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.
Merged as v0.50.230. 2685 tests passing. Browser QA 21/21.
Closes the orphan-files leg of #1171. `new_session()` no longer writes an empty session to disk — the first disk write is deferred until the session has real state. Verified live: `POST /api/session/new` creates no `.json` file; session is findable by GET from in-memory SESSIONS dict.
Attribution: original PR #1184 by @nesquena (Claude Code).
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.
* 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>