24 Commits
Author SHA1 Message Date
n 27bed49ee4 test(renderer): pin master-parity for malformed trailing-text table rows
Round-3 gate finding: the downstream table regex lacks an end-of-line anchor, so
it accepts a prefix that the physical-line guard rejects (e.g.
'| h<br>x | n |\n|---|---| trailing').

Measured against MASTER rather than against the intermediate commit: master does
not render that input as a table either, so the stage is at parity, not
regressed. Across the edge-case matrix (indented, CRLF, trailing-space, padded
separator, blank-line delimited, mid-document) the stage is equal to master on
the two malformed inputs and strictly better on six genuine tables.

Pinned as a regression so any future change to the table guard has to make a
deliberate decision about this input instead of changing it silently.
2026-09-18 22:41:56 +00:00
nandJayaMegelar 49d4aab4e2 fix(renderer): match the table-row guard to the real table grammar
Third gate finding: the table-row predicate accepted ANY pipe-wrapped line, but
the downstream table parser only accepts a run of pipe-lines whose SECOND line
is a separator. So pipe-delimited prose such as '| note<br># heading |' was
treated as a table row and kept its literal <br>, silently losing heading/list
rendering that master renders correctly.

The guard now classifies complete blocks with the parser's own grammar
(row regex + separator check) and converts <br> on every unclassified line.
Verified: all non-table cases now match master byte-for-byte, while genuine
multi-row tables keep <br> in every data cell.

Co-authored-by: JayaMegelar <JayaMegelar@users.noreply.github.com>
2026-09-18 22:33:38 +00:00
nandJayaMegelar 75d51d5ee2 fix(renderer): repair two gate-found defects in the #7618 fix
The release gate (Codex, adversarial pass) reproduced two SILENT regressions
introduced by the original implementation. Both are fixed here with
revert-sensitive regressions.

1. Collidable sentinel. Table-row <br> was stashed as a fixed \x00BR\x00 token
   and that token was unconditionally rewritten back to <br> afterwards, so
   user-supplied text containing the literal token was corrupted -- in prose it
   became a <br>, and inside a link URL it broke the anchor apart. Replaced with
   conditional conversion: <br> becomes a newline only on non-table-row lines,
   so no placeholder is ever introduced and nothing user-suppliable is rewritten.

2. Lost HTML emphasis. The stricter italic regex (which correctly stops
   'a * b * c' from italicising) also rejected the '* x *' produced by the
   <em>/<i> pre-pass, so '<em> italic </em>' degraded to literal asterisks -- and
   to a bullet list at line start. Boundary whitespace is now kept OUTSIDE the
   generated delimiters.

Verified against the real renderMd(): output for both cases now matches master,
while the table-cell <br> and spaced-asterisk fixes still hold. A revert probe
confirms all five new regressions fail against the defective implementation.

Co-authored-by: JayaMegelar <JayaMegelar@users.noreply.github.com>
2026-09-18 22:24:45 +00:00
n 4e70791757 chore: trim trailing blank line at EOF (pre-release whitespace gate) 2026-09-18 22:12:03 +00:00
Jaya Megelar Cakrawarty 38ab9f51fa fix(renderer): preserve <br> in table cells and fix bullet list italic collision 2026-09-17 11:59:46 +08:00
webtecnica 2c0b4902d0 fix(renderer): emit nested lists iteratively to avoid stack overflow (#6700) 2026-09-08 01:16:01 -03:00
webtecnica b810e5466f fix(renderer): preserve ordered flag on list items (#6700)
openItem() received ordered but dropped it from the item constructor, so
it.ordered was always undefined and every item looked unordered at the
task-list check. An ordered item whose text begins with [x] or [ ] (e.g.
'1. [x] shipped') was silently converted to a task icon and lost its
literal prefix. Task-list rendering now applies only to unordered items.
2026-09-08 01:16:00 -03:00
webtecnica 13dd97d60d fix(renderer): preserve nested mixed ul/ol list hierarchy (#6700) 2026-09-08 01:16:00 -03:00
silent-reader-cn 6950540ed5 fix(webui): render MEDIA: tokens inline during smd streaming
The full renderMd() pipeline maps MEDIA:<path-or-url> tokens to inline
images / video / audio / SVG / PDF / etc. via stash + restore passes.
The streaming smd renderer used by the live assistant body never went
through that pass, so any MEDIA token emitted mid-turn showed up as the
raw path string in the chat window and only became a real <img> once
the turn settled and the full render fired.

Fix:
- Extract the URL/media → HTML mapping in renderMd() into a shared
  helper _inlineMediaHtmlForRef(ref, sessionId) so the streaming path
  can reuse the same DOM the settled message will eventually render.
- Add _smdMediaAwareAddText in messages.js that wraps smd's add_text
  to detect MEDIA tokens in any incoming text chunk and splice the
  resulting <img>/<video>/<a>/<div data-path=...> node directly into
  the smd parent via a DOMParser-created fragment. Plain prose still
  delegates to the renderer it intercepted (so the fade renderer keeps
  its word-by-word animation, the safe renderer keeps its O(n) plain
  text-node insertion).
- Route _safeSmdRenderer.add_text and _streamFadeRenderer.add_text
  through the interceptor. Each fast-paths the no-MEDIA case to the
  original baseAddText so we can't recurse into our own wrapper.

Coverage:
- New test_smd_media_in_stream.py locks in the architectural invariants
  (helper exists, both renderers delegate to it, recursion guard).
- test_renderer_js_behaviour.py driver now provides a minimal
  _inlineMediaHtmlForRef stand-in so renderMd can be invoked standalone
  under node without the rest of ui.js in scope (matches the patterns
  renderMd already relied on for _IMAGE_EXTS / _SVG_EXTS / _AUDIO_EXTS).
- test_svg_audio_video_rendering.py static-count assertions relax from
  ">=2" to ">=1" since audio/video/SVG markup now lives in a single
  shared function rather than being copy-pasted inside renderMd's
  stash-restore closure.
2026-07-13 07:41:12 +00:00
luandnh dba6340661 test: add regression test for pipe inside inline code in renderMd tables 2026-06-25 08:25:58 +00:00
a30348b2c4 stage #4669 (TomBanksAU): wrap markdown source previews — AUTHORITATIVE head 0cee92c9 (#4679)
Re-staged from the contributor's rebased head (0cee92c9) which independently converged on
the same 5-test-file class-tolerance fixups I'd made — shipping the contributor's exact
version, not my private variant. Tags md/markdown/mdx fenced blocks with md-source-block
class in both renderers; CSS wraps them while code/diff/csv keep horizontal-scroll. 90
renderer-family tests pass.

Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
Co-authored-by: TomBanksAU <TomBanksAU@users.noreply.github.com>
2026-06-22 00:45:20 -07:00
nesquena-hermes 7c21fe4a93 fix(sessions): invalidate session-list cache on CLI-visibility toggle + de-flake node test timeouts
ROOT FIX (product bug): POST /api/settings never invalidated the session-list
cache, so toggling show_cli_sessions/show_cron_sessions could serve a stale
sidebar for up to the cache TTL when the settings-file mtime stamp collided with
a cached entry. The handler now clears the cache directly on those toggles. This
is also the root cause of the recurring test_gateway_sync CI flake (session
absent from /api/sessions right after the visibility toggle).

DEFENSE-IN-DEPTH: cleanup_test_sessions fixture now resets show_cli_sessions to
default BEFORE each test too, not only in teardown.

FLAKE FIX (test infra): bump 31 node-subprocess correctness-harness timeouts from
10s -> 30s across 23 test files. Node spawn + V8 warmup under parallel CI shard
load can exceed 10s, causing TimeoutExpired flakes (e.g. test_1325 on #4445).
Only real node subprocess.run() sites changed — mock signatures, HTTP urlopen,
and deliberate timeout-behavior tests (test_api_timeout, timeout=0/1/2) untouched.
2026-06-18 22:21:03 +00:00
nesquena-hermesandnesquena-hermes e8d71a2fb8 Release v0.51.347 — Release LK (streaming & render reliability cluster #3892 #3898 #3885 #3882 #3868) (#3902)
* stage v0.51.347: render/stream cluster (#3892 #3898 #3885 #3882 #3868) + 2 Opus SHOULD-FIX

* stage v0.51.347: trim #3885 error-guard comment to fit diagnostic-test window

* Stamp v0.51.347 — Release LK (streaming & render reliability cluster)

* Remove stray uv.lock accidentally staged (not part of any cluster PR)

---------

Co-authored-by: nesquena-hermes <[email protected]>
2026-06-09 17:02:35 -07:00
nesquena-hermes 5758d4a827 fix(security/#3234 + #3219): case-fold deny checks + make bare file:// rewrite code-fence-aware (Codex review #5)
1. api/routes.py: case-fold /api/media deny filename + dir containment checks
   (os.path.normcase + casefold) so STATE.DB / Sessions/ cannot bypass the
   state/secret deny on case-insensitive filesystems (macOS/Windows).
2. static/ui.js: move the bare file:// media-stash pass to run AFTER fenced-block
   and inline-code stashing, so a file:// inside a code block / backtick span
   stays literal text instead of becoming an auto-loaded <img>. The MEDIA: stash
   keeps its first-position precedent.
Adds behavioral renderer tests (real renderMd via node) for fenced + inline code
file:// staying literal, bare file:// becoming media, and anchors keeping the
link path. Closes the last Codex review items for #3234.
2026-05-31 03:17:10 +00:00
ai-ag2026 ce4026660e test: harden session link sanitization 2026-05-30 19:13:11 +00:00
Michael Lam 3cbe206832 fix: keep markdown tables block-level 2026-05-16 02:12:52 -07:00
Michael Lam f76921d322 fix: honor markdown fence lengths 2026-05-05 08:36:17 -07:00
nesquena-hermesandDennis Soong fbe84d26e6 fix(ui+pwa): avoid stale Mermaid render errors and bust cached static asset URLs on every release
From PR #1337.

Co-authored-by: Dennis Soong <dso2ng@gmail.com>
2026-04-30 16:18:01 +00:00
nesquena-hermes 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)
2026-04-29 17:42:32 -07:00
Brian f65f488635 fix(renderer): render h4-h6 markdown headings (####, #####, ######)
The post-stream renderMd() in static/ui.js only handled #, ##, ### — lines starting with #### through ###### fell through and emitted as literal text after streaming finalized.

  Extend the heading replacer chain to cover h4-h6, ordered longest-first, so ###### cannot be partially captured by the shorter ### rule. Add the matching .msg-body h4/h5/h6 CSS rules (and data-font-size variants) so the new tags inherit the same visual rhythm as h1-h3.

  Adds 3 node-driven tests in test_renderer_js_behaviour.py pinning all six heading levels and the longest-first replacer order.

Closes #1258
2026-04-29 23:15:59 +08:00
7189416969 fix: batch v0.50.234-235 — XSS hardening, workspace validation, profile switch fixes (#1206)
fix: batch v0.50.234-235 — XSS hardening, workspace validation, profile switch fixes

v0.50.235 (#1203 — profile switch workspace/model/chip, 3 bugs + flaky test):
- switch_profile now reads target profile's workspace directly (thread-local bypass)
- invalidate_models_cache() after profile switch (model dropdown staleness)
- syncTopbar() updates chip before early-return (no-session path)

v0.50.234 (#1201/#1205 — XSS hardening + workspace security):
- renderMd() full HTML attribute sanitizer replacing tag-name-only allowlist
- Delegated image lightbox (removes all inline onclick)
- macOS /etc → /private/etc symlink bypass fixed
- /System /Library added to blocked workspace roots
- Legacy /api/chat workspace trust gap closed

Both PRs independently reviewed. 2787/2787 tests. QA harness 20/20 + 11/11 API checks.

Co-authored-by: Brendan Schmid <bschmidy10@Wilson.bschmidy10>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-27 21:39:30 -07:00
nesquena-hermes ef26d19549 fix: batch v0.50.228 — renderer, model race, tool card, empty session, .env (#1179)
Merged as v0.50.228. 2644 tests passing. Browser QA 21/21 (desktop 1440×900 + mobile iPhone 14). All 5 fix invariants verified live in browser.

**Fix verifications:**
- #1172 (`renderMd` pre-stash): `rawPreStash` present in function, `<pre>` blocks pass through without content rewrite ✅
- #1174 (model race guard): `syncTopbar()` contains `liveStillPending` guard ✅
- #1175 (tool card): `.tool-card-result pre` max-height=360px, `.tool-card.open .tool-card-detail` overflow=auto, cap=600px ✅  
- #1176 (empty session guard): double-click New Conversation on empty session → stays on same session, composer focused ✅
- #1178 (`.env` atomic write): `tempfile.mkstemp + os.replace` in `providers.py`, 9/9 env tests pass ✅

Thanks @bsgdigital (#1150) and @bergeouss (#1178)!
2026-04-27 15:28:19 -07:00
498b51bfc6 v0.50.218: chat bubble overflow, project color picker, blockquote renderer (#1085)
* fix(css): add overflow-wrap:anywhere to chat bubbles — prevents long URL overflow (#1080)

* fix(projects): rename now works via dblclick timer guard + right-click color picker (#1078)

* fix(renderer): block-level constructs inside blockquotes now render

Fenced code blocks, headings, horizontal rules, and ordered lists inside
blockquotes now render correctly. Six related bugs documented in
blockquote-rendering-bugs.md were collapsed into one architectural fix
in renderMd().

Bugs fixed (all 6):

1. Fenced code blocks inside blockquotes -- > prefixes leaked into the
   <pre> body and the blockquote got fragmented around the rendered
   code, sometimes leaving raw <pre>/<div class="pre-header"> as
   visible text.
2. Blank > continuation lines fragmented multi-paragraph blockquotes
   into separate <blockquote> elements with literal > between them.
3. ## headings inside blockquotes rendered as literal "##" text.
4. Numbered lists inside blockquotes rendered as plain prose.
5. Complex blockquote (mixed headings + code + list + inline code)
   collapsed into a monospace blob with raw markdown syntax leaking
   everywhere.
6. Horizontal rules (---) inside blockquotes rendered as literal text.

Root cause:

The per-line passes for fenced code, headings, hr, ordered lists all ran
BEFORE the blockquote handler and could not match lines that started
with >, so by the time blockquote stripping ran those constructs had
already been mishandled.

Fix:

A new blockquote pre-pass at the top of renderMd():

- Walks lines fence-aware so > -prefixed lines inside non-blockquote
  code fences (e.g. shell prompts in bash code blocks) are not
  miscaptured as a blockquote.
- Groups consecutive > -prefixed lines, strips the > prefix, and
  recursively calls renderMd() on the stripped content. The recursive
  call handles all block-level constructs (fenced code, headings, hr,
  ordered/unordered lists, nested blockquotes) using the same pipeline.
- Wraps the rendered HTML in <blockquote> and stashes it with a \x00Q
  token. Restored at the very end of renderMd() so no later pass can
  mangle the inner HTML.

The old _applyBlockquotes regex-replace is removed entirely along with
its limited inline branches for nested blockquotes and unordered lists.

Behaviour change:

Blockquotes now produce CommonMark-compliant <p> wrapping for text
content (was: bare text directly inside <blockquote>). The visual
output is the same in browsers but the HTML structure is now standard.

Tests:

- 14 new behavioural tests in tests/test_renderer_js_behaviour.py
  drive the actual renderMd() via node and lock all 6 bug fixes.
- .local-review/test_blockquote_bugs.js -- node harness covering the
  same scenarios, runnable manually for fast iteration.
- 2407/2408 tests pass (1 pre-existing macOS-only failure deselected).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(renderer): entity decode before blockquote pre-pass + CSS margin fix

- Move the &gt;/&lt;/&amp; entity-decode to run at the very top of
  renderMd(), before the blockquote pre-pass. Previously decode() ran
  at line 756 (after the pre-pass at line 697), so LLM output containing
  &gt;-encoded blockquotes was never matched by the pre-pass.

- Add .msg-body blockquote p{margin:0} and .preview-md blockquote p{margin:0}
  so the new CommonMark-compliant <p> wrapping inside blockquotes doesn't
  add extra vertical spacing. Prior shape (bare text) had no default p-margins.

- Add Node-driven tests: TestBlockquoteEntityEncodedInput covers &gt; prefix
  and &gt;-encoded fenced code inside blockquotes.

- Add struct test: TestBlockquotePrePassOrdering::test_entity_decode_runs_before_blockquote_pre_pass
  locks decode < _bq_stash ordering in ui.js.

Fixes found during Opus independent review of #1083.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* docs: v0.50.218 release notes, test count 2458, roadmap update

---------

Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 23:08:59 -07:00
58ad315dca v0.50.216: compression chains, renderer fixes, HTML preview, approval z-index, /steer fix, reasoning chip (#1075)
* fix(workspace): add .html/.htm to MIME_MAP so HTML preview renders correctly

MIME_MAP was missing entries for .html and .htm. The server fell back to
Content-Type: application/octet-stream, which browsers refuse to render as
HTML in an iframe — causing a blank white preview.

The rest of the pipeline was already correct: the iframe exists in
static/index.html, openFile() in static/workspace.js routes .html to
showPreview('html'), and _handle_file_raw() in api/routes.py sets the
correct CSP sandbox header when ?inline=1 is present. The only missing
piece was the MIME type.

* test(workspace): lock in MIME_MAP entry for .html/.htm

PR #1070 added .html/.htm → text/html to MIME_MAP in api/config.py
to fix the blank workspace HTML preview iframe. Without a direct
assertion on the MIME_MAP entries, the fix could silently regress
(the existing test_779_html_preview.py tests cover the iframe wiring,
the inline=1 query handling, and the CSP sandbox header — but none of
them touch MIME_MAP itself).

Add a single regression test that asserts MIME_MAP['.html'] and
MIME_MAP['.htm'] are both 'text/html' so any future removal of those
entries fails CI immediately.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(composer): raise .approval-card.visible z-index above .queue-card

.queue-card has z-index:2. .approval-card.visible had no z-index, so the
queue flyout would render on top of the approval card when both were visible
simultaneously — obscuring the Allow/Deny buttons.

Fix: add z-index:3 to .approval-card.visible so approvals always render
above the queue flyout. Approval is a blocking, security-relevant interaction
and must never be obscured by passive UI elements.

* test(composer): pin approval-card z-index > queue-card invariant

PR #1071 raises .approval-card.visible to z-index:3 so the security-
relevant Allow / Deny buttons stay clickable when the queue flyout is
also open. Without a regression test, a future CSS edit could silently
drop the z-index back below queue-card (z-index:2) and reintroduce the
bug — there is no automated UI test covering this stacking interaction.

Add a focused regex check that pins the invariant:
.approval-card.visible z-index must be strictly greater than
.queue-card z-index.

Modeled on the existing CSS-regex regression style in
tests/test_mobile_layout.py (test_profile_dropdown_not_clipped_by_overflow).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: intercept /steer /interrupt /queue before busy-mode routing in send()

Root cause: slash commands entered while the agent is busy never reached
the command dispatcher. send() enters the busy block and returns early at
line ~50, so the slash-command intercept (~line 56) is never reached.
The text was queued as a plain message. When it drained after the turn
ended, cmdSteer / cmdInterrupt ran on an idle session, saw no active stream,
and showed "No active task to stop."

Fix: at the top of the busy block, before checking busyMode, check if the
text starts with / and is one of the three control commands. If so, dispatch
the handler immediately and return. This lets the user type /steer, /interrupt,
or /queue at any time — including while the agent is mid-stream — and have
them execute against the live session.

Two new regression tests added:
- test_slash_commands_intercepted_before_busymode_routing: verifies the
  intercept appears before the busyMode routing in the busy block
- test_steer_intercept_calls_handler_directly: verifies the intercept calls
  _bc.fn(_pc.args) and returns, not queues

* test(busy-intercept): pin sync input-clear before await in slash intercept

PR #1072's intercept clears the msg input before awaiting the handler.
Order matters: if the await happens first (or if the clear is moved
inside the handler), the input still shows '/steer foo' for the duration
of the await. A reflexive second Enter press during that window — common
while waiting for the toast — re-runs send(): either re-fires the
handler (double-steer) or, if the turn just ended, falls through to the
non-busy slash dispatcher and drops a confusing "No active task to stop."

Add test_steer_intercept_clears_input_before_await pinning the order so
this UX invariant cannot silently regress.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: update steer i18n and settings copy — steer no longer interrupts

With the real /steer implementation (agent.steer() via /api/chat/steer),
steer injects a correction mid-turn WITHOUT interrupting the current stream.
The previous copy said "falls back to interrupt", "Steer (interrupt + send)",
etc. — accurate only for the old placeholder, not the real implementation.

Changes across all 6 locales (en/ru/es/de/zh/zh-Hant):
  cmd_steer:                  "falls back to interrupt" removed
  settings_busy_input_mode_steer: "interrupt + send" → "mid-turn correction"
  cmd_steer_fallback:         "interrupted" → "queued for next turn"
  busy_steer_fallback:        "interrupted instead" → "queued for next turn"
  settings_desc_busy_input_mode: "currently falls back to interrupt" removed

Also:
  static/index.html: inline fallback text updated to match
  static/commands.js: internal comment clarified (fallback = queue+cancel,
                      not "interrupt mode" which implies the primary action)

* fix(renderer): group consecutive blockquote lines into single element

Root cause: the old rule `s.replace(/^> (.+)$/gm, ...)` had three bugs:
  1. `.+` required at least one character — bare `>` lines (blank
     continuation lines) did not match and passed through as literal `>`
  2. Each matching line became its own `<blockquote>` element — a 10-line
     blockquote produced 10 stacked `<blockquote>` tags with no grouping
  3. When a fenced code block sat inside a blockquote, the fence-stash
     pass consumed the code content and left orphaned `>` lines that the
     old `.+` pattern could not match

Fix: replace the single-line regex with a group-based approach that matches
one or more consecutive `>` lines as a single block, strips the `>` prefix
from each line, passes each non-empty line through inlineMd(), turns blank
`>` lines into `<br>`, and wraps the entire group in one `<blockquote>`.

14 regression tests added covering:
- Single-line blockquotes (regression)
- Multi-line grouping (2 and 10 lines)
- Two separate blockquotes staying separate
- Bare `>` and `>text` (no space) edge cases
- Blank continuation lines → <br>
- Bold / italic / inline-code inside blockquotes
- Blockquote followed by normal paragraph

* fix(renderer): drop empty trailing line from blockquote match

The new group-based blockquote rule introduced in this PR captures the
trailing newline in its (?:\n|$) clause. After block.split('\n') that
trailing newline produces an empty final element. The original filter
only dropped lone bare '>' artifacts on the last line, so the empty
final element survived, and the .map(blank → '<br>') step turned it
into a phantom <br> immediately before </blockquote>.

Visible symptom: any blockquote whose source ends with \n (the common
case — a quote followed by another paragraph or end-of-message) renders
with an extra blank line at the bottom of the quote.

Reproducer:
  '> Hello\n\nThe rest of the message.'
    → '<blockquote>Hello\n<br></blockquote>\nThe rest of the message.'
                          ^^^ phantom <br>

Fix: replace the single-line filter with a while-loop that pops trailing
lines while they are either empty OR a bare '>'. This matches the
intent the Python test mirror in tests/test_blockquote_rendering.py
already had (the mirror was correct; the JS was not — that's why
the original tests passed despite the bug).

Also add four new regression tests in TestNoPhantomTrailingBr that pin
the no-trailing-<br> invariant for the common shapes:
  - input ending with \n
  - quote followed by paragraph (the real-world case)
  - multi-line quote ending with \n
  - quote with blank continuation + trailing \n (internal <br> stays,
    trailing <br> does not)

Verified end-to-end with node against the actual JS regex.
244 renderer-adjacent tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(renderer): comprehensive markdown fixes — strikethrough, task lists, CRLF, nested blockquotes

Five additional fixes on top of the blockquote grouping from the initial commit:

1. CRLF normalisation: strip \r\n → \n at start of renderMd so Windows
   line endings do not produce stray \r characters in rendered output

2. Strikethrough: ~~text~~ → <del>text</del> in both inlineMd() (for use
   inside blockquotes/lists) and the outer pass (for plain paragraphs).
   Added <del> to SAFE_TAGS and SAFE_INLINE so it is not HTML-escaped.

3. Task lists: - [x] / - [ ] items in unordered lists render as ✅/☐
   via task-done/task-todo span wrappers. Checks [X] (uppercase) too.

4. Nested blockquotes: >> / >>> etc. now recurse so each level gets its
   own <blockquote> element rather than passing through as literal >.
   Implemented by extracting the blockquote rule into _applyBlockquotes()
   which calls itself recursively on the stripped inner content.

5. Lists inside blockquotes: > - item now renders <ul><li> inside the
   blockquote instead of a literal "- item" string. Task list items work
   inside blockquotes too (> - [x] done → ✅ inside <blockquote><ul>).

Also fixed test_issue342.py search window (5000→10000 chars) — the CRLF
strip at the top of renderMd pushed the autolink regex past the old limit.

68 new tests in test_renderer_comprehensive.py + test_blockquote_rendering.py
covering all constructs, edge cases, and combinations.

* fix(renderer): restore space in blockquote prefix-strip regex

Commit 04e7b53 changed the blockquote prefix-strip regex from
  /^>[ \t]?/   (consume "> ", "\t>", or just ">")
to
  /^>[\t]?/    (only consume "\t>" or just ">")

The space character was dropped from the character class. Since
practically every blockquote an LLM produces is "> " (greater-than
followed by a space), this leaves a leading space artifact on every
stripped blockquote line. Worse, the leading space breaks the
list-detection regex `^(?:  )?[-*+] ` inside the new `_applyBlockquotes`
helper — that regex requires either zero or two leading spaces, never
one — so the new "list inside blockquote" feature never fired for
the canonical input shape `> - item`.

Reproducer (against the actual ui.js via node, before the fix):
  > Hello world         → <blockquote> Hello world</blockquote>
                                       ^ phantom leading space
  > Steps:              → <blockquote>Steps:
  > - one                  - one
  > - two                  - two</blockquote>
                          ^ literal text, NOT a <ul>; lists-in-quote feature broken
  > - [x] done          → blockquote with literal "[x] done", no checkbox span

Tests passed despite the bug because tests/test_blockquote_rendering.py
and tests/test_renderer_comprehensive.py validate against a Python
mirror (`_apply_blockquotes`) whose strip regex is `^>[ \t]?` — i.e.
the mirror is correct, the JS is not, and the static-mirror tests
can't catch the divergence. Same shape of bug as commit 94d63d0
(phantom <br> in trailing line) where the mirror was right and the JS
was wrong.

Fix: restore the space character in the strip regex's character class.

Add tests/test_renderer_js_behaviour.py — 11 tests that drive the
ACTUAL renderMd via node and assert on rendered output for the most
common LLM shapes (single-line quote, multi-line quote, list inside
quote, task list inside quote, nested >>>, strikethrough inside and
outside quote, top-level task list, quote followed by heading,
multi-paragraph quote with list, CRLF normalisation).

Verified: the buggy regex makes 6 of those 11 tests fail; the corrected
regex makes all 11 pass.

Suite: 2354 passed, 0 new failures.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Collapse agent session compression chains

* Restore upstream changelog entries

* fix(agent_sessions): bubble active compression chains to top by tip last_activity

The original PR merge kept the chain head's id/title/started_at and overrode
id/model/message_count/ended_at/end_reason from the tip — but did NOT override
last_activity. Since the projected list is sorted by last_activity DESC and
the WebUI sidebar surfaces updated_at = last_activity, an actively-used
compression chain whose tip is being edited NOW would sort by the ROOT's
old last_activity and fall below recently touched standalone sessions.

Reproducer (with the harness against actual code, before the fix):
  - root: started 30 days ago, last msg 30 days ago
  - tip:  started 28 days ago (parent_session_id=root), last msg 5 seconds ago
  - standalone: last msg 2 days ago

  Sidebar order with original PR:
    [0] standalone  (48h ago)
    [1] active_tip  (last_activity=root's 720h ago)  ← wrong

  Sidebar order after fix:
    [0] active_tip  (last_activity=tip's 0h ago)     ← correct
    [1] standalone  (48h ago)

This matches Hermes Agent's own list_sessions_rich projection at
hermes_state.py:903-909, which overrides "last_active" from the tip
exactly so that the agent CLI's session list orders the same way.

Add ``last_activity`` to the merge-from-tip key list, update the existing
test_compression_chain_collapses_to_latest_tip_in_sidebar assertion to
expect tip-derived updated_at, and add
test_compression_chain_bubbles_to_top_by_tip_activity locking in the
bubble-to-top invariant — without this regression test the previous
behaviour passed CI because no test exercised the sort order against a
mixed set of chains and standalone sessions.

The chain head's started_at (created_at) and title remain preserved, so
users can still find the conversation by its original date and name.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: v0.50.216 release notes and version bump

Compression chains, renderer fixes, HTML preview, approval z-index, /steer fix.

* chore: gitignore local-only review harness directory

Adds .local-review/ to .gitignore so renderer drivers, sample inputs,
fixture builders, and other reviewer scratch files do not accidentally
get committed. Nothing under that path is ever shared in the repo;
keeping the entry tracked makes the boundary explicit for any future
contributor who creates the directory locally.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Keep reasoning chip visible for None effort

* test(reasoning): pin chip render output via node, not just source regex

The PR's static checks in test_reasoning_chip_btw_fixes.py validate the
shape of _applyReasoningChip (no display='none' literal, the right
classList.toggle call exists, the right label literals are in the
function body) but pass even if the runtime detail is wrong — for
example if `inactive` were inverted, _normalizeReasoningEffort
mishandled whitespace, or _formatReasoningEffortLabel returned the
wrong literal for an unknown input.

Add tests/test_reasoning_chip_js_behaviour.py — 11 tests that drive
the actual _applyReasoningChip() via node and assert on the rendered
DOM state for each effort value:

  TestChipAlwaysVisible
    - empty / null  -> "Default" label, inactive=true
    - "none"        -> "None" label, inactive=true
    - "low"/"high"  -> verbatim label, inactive=false
  TestNormalizationEdgeCases
    - "NONE"        -> normalises to "None"
    - "  none  "    -> trims and normalises
    - unknown junk  -> falls through visible, never hidden
  TestTitleAttributeAccessibility
    - title attribute carries the human-readable label for tooltip /
      screen-reader use

Sanity-checked against master's pre-fix ui.js: 11/11 fail (bug caught).
Against this PR's ui.js: 11/11 pass.

This pattern (drive the actual JS via node) caught two regex-only
regressions in PR #1073 where the Python mirror was correct while the
JS was broken. Same protection added here so the chip-visibility
contract can't silently break in a future refactor.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: add #1074 to v0.50.216 changelog, bump test count to 2428

* fix(i18n): restore broken Unicode in Russian and Spanish steer strings

Commit 56c7a14 (fix: update steer i18n and settings copy) accidentally
stripped the `\u` prefix from Unicode escape sequences in two locales,
producing garbled literal hex strings visible to users:

  Spanish (es):
    - cmd_steer:                   correcci00f3n  → corrección
    - cmd_steer_fallback:          2014 en cola   → — en cola
    - busy_steer_fallback:         2014 en cola   → — en cola
    - settings_desc_busy_input_mode: qu00e9, est00e1, correcci00f3n → qué, está, corrección
    - settings_busy_input_mode_steer: correcci00f3n  → corrección

  Russian (ru):
    - settings_desc_busy_input_mode: the entire Cyrillic string was
      replaced with raw 4-hex-char code-points without the \u prefix
      (041e043f... instead of actual Cyrillic). Decoded:
      "Определяет поведение при отправке сообщения во время работы
      агента. Очередь ждёт; Прерывание отменяет и начинает заново;
      Steer внедряет коррекцию без прерывания."

Fix: write the correct characters directly (UTF-8 is the file encoding
so embedding them literally is cleaner than \u escapes for long text).

All other locales (en, de, zh, zh-Hant) were not affected — confirmed
by grepping for bare hex run-ons in the updated file.

Verified: node --check static/i18n.js passes; full pytest suite green
(2365 passed, 47 skipped).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: remove duplicate compression chain entry from [Unreleased]

---------

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>
Co-authored-by: Frank Song <franksong2702@gmail.com>
2026-04-25 21:06:31 -07:00