Add a complete Turkish locale to the WebUI and login page so users can
select Türkçe in Settings, with speech recognition via tr-TR.
Co-authored-by: Cursor <cursoragent@cursor.com>
Add `PATCH /api/mcp/servers/{name}` endpoint that accepts `{"enabled": bool}`,
updates `mcp_servers.<name>.enabled` in config.yaml, and calls `reload_config()`.
Mirrors the existing DELETE pattern.
Also wire the previously-defined-but-unrouted `_handle_mcp_server_delete` into
`handle_delete`, and `_handle_mcp_server_update` into a new `handle_put` +
`do_PUT` in server.py — fixing a pre-existing bug where those handlers existed
but were never reachable over HTTP.
UI: add a toggle button in each MCP server row in the system settings panel
(panels.js). Clicking it calls PATCH and reloads the list. Toggle button is
styled with `.mcp-toggle-enabled` / `.mcp-toggle-disabled` CSS classes. The
`toggle_supported` flag in the list response is now `True`.
i18n: add 5 new keys (`mcp_enable_server`, `mcp_disable_server`,
`mcp_enabled_toast`, `mcp_disabled_toast`, `mcp_toggle_failed`) to all 9
non-English locales (English values as placeholder translations).
Tests: add `TestMcpToggle` class with 7 tests covering disable, enable,
404-not-found, empty name, missing field, response payload, and URL-encoded name.
Update `test_empty_config` and visibility panel assertions to reflect
`toggle_supported: True` and the new toggle button in panels.js.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fixes#2846. After PR #2758 (the #2653 fix) the update check correctly
falls through to the branch comparison when HEAD has moved past the
latest `v*` tag — so the banner reports the real commit count against
`origin/<branch>`. But `_select_apply_compare_ref` was never updated to
mirror that decision: as long as any `v*` tag exists, it returns
`tags[0]`, even when HEAD is far past it.
Result for everyone running hermes-agent past `v2026.5.16` (i.e. anyone
on agent master between tagged releases):
1. Banner: `Agent (origin/main): 254 updates available` ← correct
2. User clicks Update Now
3. `_select_apply_compare_ref` picks `v2026.5.16` because tags exist
4. `git pull --ff-only origin v2026.5.16` — no-op (HEAD is already past it)
5. `_schedule_restart()` fires anyway, server bounces
6. Next check still reports 254 behind — banner reappears unchanged
`apply_force_update` had the same bug, except worse: `git reset --hard
v2026.5.16` would have actively rewound the user's checkout 254 commits.
The root cause is the same bug class as #2653 — two parallel paths
(`_check_repo_release` and `_select_apply_compare_ref`) that should make
the same decision but didn't. Pre-fix, the "is HEAD past the latest
tag?" predicate lived inline inside `_check_repo_release` only.
Fix
---
Extract `_head_is_past_latest_tag(path, current_tag)` and have both
paths consult it. When HEAD is past the latest tag:
- check path: release check returns None → branch check runs (#2653,
unchanged behaviour, just refactored)
- apply path: falls through to upstream / `origin/<branch>`, never the
stale tag (#2846, new behaviour)
Tests
-----
- `test_select_apply_compare_ref_uses_tag_when_head_is_on_tag` —
unchanged behaviour pinned: HEAD exactly on tag → advance to tag.
- `test_select_apply_compare_ref_falls_through_when_head_is_past_tag` —
the #2846 repro: HEAD = v2026.5.16 + 608 commits → advance to
`origin/main`, not the tag.
- `test_select_apply_compare_ref_no_tags_uses_upstream` — unchanged.
- `test_select_apply_compare_ref_no_tags_no_upstream_uses_default_branch`
— unchanged.
- `test_check_and_apply_paths_agree_when_head_is_past_tag` — symmetry
test, ensures the two paths can't drift apart again.
All 21 tests in `tests/test_updates.py` pass locally (16 existing + 5
new).
Refs #2846, #2653.
Fixes#2853. The `_terminal_shell_preexec_fn` added in `71d8a8fb` called
`prctl(PR_SET_PDEATHSIG, SIGTERM)` so orphaned PTY shells would die when
the WebUI process crashed. But that signal is **per-thread**, not
per-process, and WebUI runs `ThreadingHTTPServer`: every HTTP request is
handled in its own short-lived worker thread.
Flow that broke every Linux user:
1. User clicks the terminal toggle → frontend hits `POST /api/terminal/start`.
2. ThreadingHTTPServer spins up a worker thread to handle that one request.
3. The worker thread calls `subprocess.Popen(..., preexec_fn=...)`.
4. The shell calls `prctl(PR_SET_PDEATHSIG, SIGTERM)` in its preexec_fn.
Its registered "parent" is now the WebUI worker thread that called Popen.
5. The handler returns its JSON response and the worker thread exits.
6. The kernel sees the pdeathsig-parent thread has died and sends SIGTERM
to the PTY shell. The shell dies within ~10 ms of being created.
7. The reader loop sees EIO on the master FD, emits `terminal_closed`, and
the frontend writes `[terminal closed]`.
macOS users were unaffected because `libc.prctl` doesn't exist there —
`ctypes.CDLL(None)` returns a libc handle, `libc.prctl` raises
`AttributeError`, the bare-`except` swallows it, and the shell starts
with no pdeathsig configured.
Empirical verification on this Linux host (real PTY + `subprocess.Popen`
inside a `threading.Thread` that joins immediately):
with preexec_fn → proc.poll() == -15 (SIGTERM), master FD returns EIO
without preexec_fn → proc.poll() == None (alive), master FD returns "HELLO\\r\\n"
Same shell, same PTY, same threading topology as WebUI.
Fix
---
Drop the `preexec_fn` entirely. The orphan-shell-on-crash case the original
PR was navigating is rare for self-hosted single-user installs, and the
existing `atexit.register(close_all_terminals)` + explicit `close_terminal`
paths cover graceful shutdown. A future fix (option B in the issue) can
re-introduce pdeathsig pinned to a long-lived supervisor thread, but that
is a follow-up — this PR is the smallest unbricks-Linux-today change.
Tests
-----
- Invert `test_terminal_shell_uses_parent_death_signal_preexec` →
`test_terminal_shell_does_not_use_pdeathsig_preexec`: asserts
`preexec_fn` is NOT in the Popen kwargs.
- Add `test_pty_shell_survives_when_spawning_thread_exits`: spawns a
real PTY shell via `start_terminal` from a worker thread, waits for
the worker to join, asserts the shell is still alive after a half-second
grace window. This is the contract the original tests never exercised.
- Update `test_terminal_module_registers_graceful_shutdown_reaper` to
refuse re-introduction of the preexec_fn or the `libc.prctl(1, SIGTERM)`
call (treats either as a regression).
All 27 terminal-related tests pass locally.
Refs #2853
Three small fixes from Copilot's review:
1. static/style.css:1354 - removed spaces inside `clamp(...)` args to
match the file's existing compact style (no spaces after commas in
neighboring declarations like `transition:border-color .2s,box-shadow .2s`).
2. CHANGELOG.md - wrapped the long single-line entry across multiple lines
with standard Markdown continuation indentation for cleaner diffs.
3. CHANGELOG.md - normalized `~1300 px` to `~1300px` for unit-formatting
consistency.
No behavior change. Same one-line CSS rule, just tightened formatting.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`.composer-box` had a hardcoded `max-width: 780px` since the early
v0.50.x layout pass. On wide displays (1440p+, 2880px ultrawides)
this leaves significant unused horizontal space AND squeezes the
composer-footer chips (workspace, model, reasoning, context %)
against each other inside the 780px box.
When the context-percentage ring appears (active token usage), the
workspace chip truncates to "Fou..." instead of showing the full
workspace name. Model + reasoning chips also lose room. The chip
strip horizontally-scrolls inside .composer-left, so the rightmost
chips effectively hide behind context %.
The constraint isn't "Reading flow looks better at 780px" — the
textarea is min-height:64px, max-height:200px and wraps naturally,
so users on wide displays get the SAME readable text wrap regardless
of box width. Only the footer chips suffer.
Fix: clamp(780px, 60vw, 1100px). Preserves the 780px floor (no
regression on viewports < 780px since clamp's first arg is the
minimum) while letting wider viewports use up to 1100px (60% of
viewport width, capped). 1100px gives ~40% more horizontal room for
the footer chips without filling the entire screen at extreme widths.
Per-viewport behavior:
<= 780 px → 780 px (hard floor) — zero change vs current
1280 px → 60vw = 768 → floored to 780 — zero change
1440 px → 60vw = 864 — +84 px room
1920 px → 60vw = 1152 → capped at 1100 — +320 px room
2880 px → 60vw = 1728 → capped at 1100 — +320 px room
One line in static/style.css. CHANGELOG entry. No JS. No new deps.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The path-discovery step succeeds on the first run, but the cleanup
step exits non-zero because `taskkill /PID 5560 /T /F` returns 128
("process not found") when server.py has already exited on the mock
hermes_cli stub. That's the expected steady state for this mock-only
workflow, not a failure.
Two-line fix: reset `$global:LASTEXITCODE = 0` after the taskkill
call, and explicit `exit 0` at the end of the step so any other
external-command exit codes don't bubble up. The try/catch wrapper
didn't help because taskkill writes its diagnostic to stderr without
raising a PowerShell exception — `catch` never fired.
Run 26352805510 on this branch shows the failure shape: "OK: start.ps1
path discovery - all guards passed." in the verify step, then
"ERROR: The process '5560' not found." in the cleanup step. Path
discovery is what this workflow exists to validate; cleanup just has
to not fail the job.
Per @nesquena-hermes review on #2811: hermes-agent isn't published to
PyPI, so `pip install hermes-agent` finds nothing and start.ps1's
hermes_cli guard correctly bails out — leaving the previous workflow
unable to self-validate against release/stage-batch6.
This rework adopts option 1 from the review: drop the pip install,
stub a hermes_cli/ directory with a minimal __init__.py next to the
sibling hermes-agent/ folder, then run start.ps1 for 8 seconds and
assert that none of its own Write-Error guards (no Python, no agent
dir, bad port, missing hermes_cli, missing server.py) appeared in
stderr. /health is no longer probed — the server cannot boot on a
stub, and full-boot regressions stay covered by the Linux jobs and
docker-smoke.yml.
Scope intentionally narrower than the original: this workflow
validates start.ps1's PowerShell syntax + path discovery only. The
exact bug class PR #2805 caught (WOW64 ProgramFiles redirect) would
now light up red here pre-merge, which is the reason this gate exists.
Paths filter trimmed to `start.ps1` + the workflow itself; the broader
list (requirements.txt / bootstrap.py / server.py) was inherited from
the original full-boot scoping and isn't relevant for a path-discovery-
only run.
Verification: workflow runs on this PR via its own pull_request trigger.
The first CI run on this branch IS the verification.
CHANGELOG updated under [Unreleased] with a single bullet sized to the
surrounding density.
Squashed from 2 author commits:
- d2237e23 feat: surface live activity timeline
- eee57ec0 fix: satisfy activity timeline CI guards
Frontend-only telemetry from existing stream events. Replaces empty
Thinking… placeholder with observable run status (Waiting on model /
Waiting on tool result / Working for …). New CSS, new test file.
- Rewrote _kanbanRenderMarkdown() from basic paragraph wrapper to a
line-by-line block processor supporting headings, code blocks, lists,
task lists, tables, blockquotes, horizontal rules, and strikethrough.
- Added CSS for all new elements (table borders, code blocks, checkboxes,
blockquote accent, heading sizing, etc.).
- Dropped white-space: pre-wrap from .kanban-task-preview-body and
.kanban-detail-row-main since markdown now handles layout.
- Applied _kanbanRenderMarkdown() to task description (was esc()) and
comment body (was esc()) in the task detail view.
Squashed from 2 author commits:
- a1017d02 initial fix: flex:0 0 auto on all 5 chip wraps
- bf54ba50 Copilot review fix-up: consolidate into single rule
Closes#2740. CSS-only, no JS changes. Default-width layout unchanged,
only affects narrow-viewport overflow regime via composer-left's existing
overflow-x:auto.
After the user responds to a clarify prompt, insert a synthetic user
message into the conversation showing their choice. This makes the
clarify interaction visible in the chat history, which was previously
only shown in the transient clarify dialog card.
The message is marked with _clarify_response: true so downstream
consumers can distinguish it from regular user messages if needed.
When tool approval or clarification cards appear during streaming,
they unconditionally call focus() on their input elements via setTimeout,
stealing focus from the composer (#msg) if the user is actively typing.
This silently drops keystrokes mid-type.
Add a guard: only move focus to the card if the composer textarea does
not already have focus. The document.activeElement check matches the
pattern already used upstream in other focus-sensitive components.
Fixes: #
Cherry-picked PRs (all by @Koraji95-coder):
- #2805 — expand hermes-agent candidate paths for Windows installers
- #2806 — clarify native Windows venv path; remove WSL2-venv-portability claim
- #2807 — TryParse HERMES_WEBUI_PORT + exit AFTER try/finally cleanup
- #2811 — native-Windows startup E2E CI workflow
All 4 PRs were branched off #2783 (now shipped in v0.51.121). Squash-merged
each PR's unique changes onto current master with conflict resolution.
Authorship preserved on every commit. Zero impact on Linux/macOS runtime —
file scope is start.ps1, README.md (Windows section), and a new Windows-CI
workflow that only runs on PRs touching start.ps1/requirements.txt/etc.
Squashed from 2 author commits onto current master (3 base commits from
already-shipped #2783 were filtered out by the squash):
- f53b9308 fix(start.ps1): TryParse HERMES_WEBUI_PORT + exit AFTER try/finally cleanup
- 7b6e0722 fix(start.ps1): drop non-functional @args splat under [CmdletBinding()]
Authorship preserved. CHANGELOG entry merged into batch stamp commit.
Squashed from 3 author commits onto current master (3 base commits from
already-shipped #2783 were filtered out by the squash). #2805's expanded
candidate-path discovery + PathType Container check preserved from prior
stage commit.
Authorship preserved. CHANGELOG entry merged into batch stamp commit.
Squashed from 3 author commits onto current master (the 3 base commits from
already-shipped #2783 were filtered out by the squash):
- 6822cbbb feat: expand hermes-agent candidate paths
- 6f423538 Copilot review: PathType+null-guard+changelog
- dbebbedd handle WOW64 ProgramFiles redirection
Authorship preserved. CHANGELOG entry merged into batch stamp commit.
Right-click any workspace file, folder, or root now shows
'Open in VS Code' alongside the existing Reveal in File Manager action.
- POST /api/file/open-vscode: resolves path via safe_resolve, finds VS
Code via shutil.which() with fallbacks for macOS (/usr/local/bin/code,
app bundle CLI), Linux (/usr/bin/code, /snap/bin/code), and Windows
(%LOCALAPPDATA% and %PROGRAMFILES% user/system installs). Returns a
descriptive error if not found rather than a bare OS error.
- Optional vscode block in config.yaml: command (default: code),
host_path_prefix + container_path_prefix for Docker path mapping.
- i18n: open_in_vscode and open_in_vscode_failed translated in all 10
locales (it, ja, ru, es, de, zh-CN, zh-TW, pt, ko).
- 26 tests in tests/test_2735_open_in_vscode.py covering source wiring,
command resolution, i18n completeness, and live endpoint error paths.
Two confirmed bugs in the thinking/reasoning display:
1. reasoningText was initialized once when the SSE stream opened and never
reset between turns. On the done event, the last assistant message
received the union of every turn's reasoning. Now reset at both turn
boundaries: tool (alongside existing liveReasoningText reset) and
interim_assistant (the other turn boundary where prior reasoning closes).
2. ui.js renderMessages preferred m.reasoning (which could be corrupted by
bug 1) over m.reasoning_content (the clean per-turn value from the
backend). The fallback now reads m.reasoning_content || m.reasoning.
Both fixes are needed: bug 2 alone cannot cover providers that stream
reasoning events without populating reasoning_content on the final API
message.
Updated test_streaming_race_fix.py to scope its reconnect-accumulator
guard to the _wireSSE preamble only, since turn-boundary resets inside
event listeners are intentional and correct.
9 new regression tests in test_issue2565_reasoning_accumulation.py.
Fixes#2713 — live assistant text can truncate at tool-call segment
boundaries during streaming.
Before _resetAssistantSegment() in the tool and interim_assistant SSE
handlers, synchronously flush any pending rAF render work so tokens that
arrived during the 66ms throttle window are written to the DOM before
assistantBody is cleared. Without this flush, the pending _doRender
callback fires after assistantBody is null and skips the write silently,
causing the tail of the pre-tool segment to disappear from the live view.
Implementation:
- Extract _flushPendingSegmentRender() helper (guarded by assistantBody
&& _renderPending) that cancels the pending rAF and synchronously
writes via smd/renderMd/esc — same cascade as _doRender.
- Call the helper from both the tool and interim_assistant handlers
before their respective _resetAssistantSegment() calls.
- Normal cases where the rAF has already fired are unaffected (guard
skips immediately).
Completed transcripts were never affected (renderMessages rebuilds from
the full assistantText accumulator on done).
Adds tests/test_issue2713_streaming_segment_flush.py with 11 static
analysis regression tests pinning the helper shape and call-site
ordering.
Cherry-pick of PR #2796 by @ai-ag2026, squashed from 5 author commits onto current master:
- dcee0563 fix: drop stale optimistic sidebar rows
- 3a73400d fix: clear stale busy state before send
- 46c3b902 fix: preserve server idle rows during optimistic merge
- de51d271 fix: let chat start survive pre-start UI errors
- d2f5c906 fix: hide nonfatal pre-start send warnings
Authorship preserved via --author. Code-only squash (no CHANGELOG).