fix(ui): tool card output truncated at 220 chars and cannot scroll#1175
fix(ui): tool card output truncated at 220 chars and cannot scroll#1175nesquena-hermes wants to merge 1 commit intomasterfrom
Conversation
…1170) Two separate bugs prevented users from reading tool output: 1. JS truncation at 220 chars was too aggressive — stack traces and file paths are regularly cut mid-line. Raised to 800 chars so most outputs display in full without needing 'Show more'. 2. .tool-card-detail had overflow:hidden in both closed and open states. While required for the max-height CSS transition animation when closed, overflow:hidden on the parent clips the inner <pre>'s scroll — the pre had overflow-y:auto but could never actually scroll. Fixed by adding overflow:auto on .tool-card.open .tool-card-detail. Also raised the inner pre max-height from 180px → 360px and the card cap from 520px → 600px to give long tool outputs more room. Test updated to match the new max-height values and assert overflow:auto on the open card state. Closes #1170
nesquena
left a comment
There was a problem hiding this comment.
Review — end-to-end ✅ (clean approve)
What this ships
Two-issue fix for #1170 — tool card output truncated and unscrollable:
- JS truncation 220 → 800 chars (static/ui.js:2617-2620) — most realistic tool outputs (stack traces, file paths, JSON) now render in full without "Show more".
- CSS
overflow:autoon open card (static/style.css:1236) — the inner<pre>hadoverflow-y:autobut the parent.tool-card-detailhadoverflow:hidden(required for the closed-state max-height collapse animation). Addingoverflow:autoonly on the open state fixes the scroll without breaking the animation. - Heights bumped — outer card cap 520→600px, inner pre 180→360px (≈20 lines).
Traced against upstream hermes-agent
Pure WebUI presentation change. Tool snippets come from the agent's stream (text), and buildToolCard renders them; the truncation cap is purely visual. No upstream coupling.
End-to-end trace
CSS state-machine integrity
Closed state (style.css:1235):
.tool-card-detail{display:block;max-height:0;opacity:0;overflow:hidden;...}Open state (style.css:1236):
.tool-card.open .tool-card-detail{max-height:600px;opacity:1;padding:8px 12px;...;overflow:auto;}Critical: the closed overflow:hidden is required for the max-height collapse animation — without it, content would jump out during the transition. The fix correctly preserves it for the closed state and only adds overflow:auto to the open state. The CSS cascade applies the open-state overflow:auto only when .tool-card.open is set, so the animation still works on collapse.
Inner pre scroll path
.tool-card-result pre{...max-height:360px;overflow-y:auto;...}Now functional because the parent's overflow:auto no longer clips the inner scroll bar. Long content (>360px) will get an inner scrollbar within the pre.
Truncation logic
if(s.length<=800){displaySnippet=s;}
else{
const cutoff=s.slice(0,800);
const lastBreak=Math.max(cutoff.lastIndexOf('. '),cutoff.lastIndexOf('\n'),cutoff.lastIndexOf('; '));
displaySnippet=lastBreak>80?s.slice(0,lastBreak+1):cutoff;
}Same algorithm, just larger threshold. The lastBreak>80 heuristic still avoids cutting awkwardly close to the start. ✅
Edge-case trace
| Scenario | Expected | Actual |
|---|---|---|
| Tool result < 800 chars | full content, no truncation | ✅ |
| Tool result > 800 chars | truncated at sentence/line break | ✅ same algorithm |
| Tool result ≤ 360px tall in card | renders without scroll | ✅ pre never overflows max-height |
| Tool result > 360px tall in card | inner pre scrolls | ✅ fix's main goal |
| Tool result > 600px tall (huge) | outer card scrolls | ✅ open-state overflow:auto |
| Card collapse animation | smooth 0→600px transition | ✅ closed state still overflow:hidden |
| Card re-open after expand | doesn't get stuck mid-transition | ✅ standard CSS state machine |
Tests
- PR's targeted test (
test_tool_card_detail_uses_transitionable_collapsed_state): updated to assertmax-height: 600pxANDoverflow: autoon open state. Passes. - All 5
test_ui_card_animationtests: pass. - Local full suite: 2586 passed, 47 skipped, 1 PR-unrelated pre-existing failure (macOS-only).
- CI on PR: ✅ test (3.11), ✅ test (3.12), ✅ test (3.13).
Minor observations (non-blocking)
- The 800-char threshold is based on intuition ("most realistic outputs"). If users still hit "Show more" frequently, easy to bump again — no architectural decision baked in.
- The two
max-heightconstants (600px outer, 360px inner) leave 240px for header chrome and padding. If chrome shrinks, the inner pre could be the bottleneck before the outer card. Acceptable for the typical case.
Recommendation
Approved. Surgical 4-line behavioural fix. The CSS state-machine reasoning is correct (preserve overflow:hidden on closed, add overflow:auto only on open), the truncation bump is empirical but reasonable, the test was correctly updated to lock both new invariants. Parked at approval — ready for the release agent's merge/tag pipeline.
|
Good fix for a frustrating tool readability issue — the combination of the 220-char hard truncation and Issue A (truncation threshold): Raising from 220 to 800 characters is the right call — 220 characters is often the middle of a path, stack trace line, or JSON value. 800 characters gives a meaningful preview while still avoiding UI blowout for very long outputs. Issue B (scroll fix): Changing One thing worth verifying: the Flagging for maintainer review — closes #1170. |
…, .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)!
|
Merged as v0.50.228 via #1179. Thank you nesquena-hermes! |
…, .env (nesquena#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:** - nesquena#1172 (`renderMd` pre-stash): `rawPreStash` present in function, `<pre>` blocks pass through without content rewrite ✅ - nesquena#1174 (model race guard): `syncTopbar()` contains `liveStillPending` guard ✅ - nesquena#1175 (tool card): `.tool-card-result pre` max-height=360px, `.tool-card.open .tool-card-detail` overflow=auto, cap=600px ✅ - nesquena#1176 (empty session guard): double-click New Conversation on empty session → stays on same session, composer focused ✅ - nesquena#1178 (`.env` atomic write): `tempfile.mkstemp + os.replace` in `providers.py`, 9/9 env tests pass ✅ Thanks @bsgdigital (nesquena#1150) and @bergeouss (nesquena#1178)!
Summary
Fixes two issues with tool card output readability.
Issue A: 220-char JS truncation was too aggressive
Tool call results were cut off at 220 characters — often in the middle of a stack trace, file path, or JSON value. Raised the threshold to 800 characters so most realistic tool outputs display in full without needing to click "Show more".
Issue B:
overflow:hiddenon the parent blocked inner scroll.tool-card-detailhadoverflow:hiddenin both closed and open states. This is correct for the CSSmax-heightcollapse animation (closed state), but in the open state it clips all children — including the<pre>inside.tool-card-result, which hadoverflow-y:autobut could never actually render a scrollbar.Fix: add
overflow:autoto.tool-card.open .tool-card-detail. The hidden state is preserved for the collapse animation; only the open state gains a scroll container.Also raised:
<pre>max-height:180px → 360px(~20 lines, enough for most tool outputs without scrolling)520px → 600pxTesting
overflow:autoon open state).read_fileorterminalresult → content is scrollable.Closes #1170