Skip to content

fix(ui): match tool result snippet limit to backend (200 -> 4000 chars) - #3117

Closed
mysoul12138 wants to merge 2 commits into
nesquena:masterfrom
mysoul12138:fix/snippet-limit
Closed

mysoul12138 wants to merge 2 commits into
nesquena:masterfrom
mysoul12138:fix/snippet-limit

Conversation

@mysoul12138

Copy link
Copy Markdown
Contributor

Summary

_cliToolResultSnippet truncated to 200 chars while the backend's _tool_result_snippet uses 4000. This caused tool card details to be more aggressively truncated after session reload than during live streaming.

Impact

  • Tool cards now show the same amount of detail after reload as during live streaming
  • 4000 chars is a 20x bump from 200 — may affect rendering on very long shell commands

Test plan

  • Run a terminal tool with long output → reload session → detail shows same content as during streaming
  • Verify long tool outputs don't break card layout

🤖 Generated with Claude Code

_cliToolResultSnippet truncated to 200 chars while the backend's
_tool_result_snippet uses 4000. This caused tool card details to be
more aggressively truncated after session reload than during live
streaming.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@nesquena-hermes

Copy link
Copy Markdown
Collaborator

Review

The diagnosis is correct. The backend's _tool_result_snippet in api/streaming.py:3003 and :3064 caps at 4000 chars, but the WebUI's reload-path snippet builder _cliToolResultSnippet in static/ui.js:6083-6086 was capping at 200. So live-stream tool cards (snippet comes from the SSE tool_complete event, populated by result_snippet = _tool_result_snippet(function_result) at api/streaming.py:4245) were getting 4000 chars, while reload-path tool cards (populated via _cliToolResultSnippet(m.content) at static/ui.js:6629 and :6641) were getting 200. That's a genuine asymmetry and matches the user-visible symptom.

Code reference

Origin/master static/ui.js:6083-6087:

function _cliToolResultSnippet(raw){
  const fullText=_cliToolResultText(raw);
  if(_cliLooksLikePatchDiff(fullText)) return _clipCliToolSnippet(fullText);
  return String(fullText||'').slice(0,200);
}

Backend pair at api/streaming.py:3003:

_TOOL_RESULT_SNIPPET_MAX = 4000

The PR aligns those two limits, which is the right symmetry.

Layout / perf check

The 20x bump is fine because buildToolCard at static/ui.js:6909-6920 already caps the visible preview at 800 chars (with a word-break cutoff) and stuffs the full snippet behind a "Show more" data attribute:

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;
}

So the rendered DOM stays the same size by default. The 4000-char tc.snippet only matters when the user clicks "Show more", which is exactly the desired behavior. No layout-breakage risk.

One small caveat: the data-full and data-short attributes on the more-button are HTML-escaped via .replace(/"/g,'&quot;') (see static/ui.js:6946). Going from 200 to 4000 chars stuffed into a data-full attribute is fine for HTML, but it does grow the per-card DOM string by ~3.8KB on reload of a session with many tool calls. For a session with, say, 50 tool calls, that's ~190KB of extra HTML in the initial render. Probably not material on modern browsers, but if reload-time renderMessages becomes sluggish on big sessions, this is a place to look.

Diff-path branch

_cliLooksLikePatchDiff at static/ui.js:6075-6082 is left alone, so patches/diffs still go through _clipCliToolSnippet (cap 20000 chars). The PR doesn't disturb that path. Good — diff snippets often need more room than text snippets.

Verdict

LGTM. The asymmetry was real (200 in WebUI vs 4000 in backend), the fix is the minimum delta, and the rendering path already gates the visible portion at 800 chars so this is a "Show more reveals more" change rather than a layout change. The test plan boxes are right; the easy verification is "reload a session whose live tool card showed 4000 chars and confirm the reloaded card's Show-more reveals the same content". One thing missing from the PR: a regression test asserting the JS constant matches the Python constant. Something like:

# tests/test_tool_snippet_limit_parity.py
def test_tool_snippet_limit_parity():
    py = (REPO / "api" / "streaming.py").read_text()
    js = (REPO / "static" / "ui.js").read_text()
    assert "_TOOL_RESULT_SNIPPET_MAX = 4000" in py
    assert ".slice(0,4000)" in js

…would prevent the limits from drifting apart again. Not blocking; ship as-is or with that two-assertion test added.

1 similar comment
@nesquena-hermes

Copy link
Copy Markdown
Collaborator

Review

The diagnosis is correct. The backend's _tool_result_snippet in api/streaming.py:3003 and :3064 caps at 4000 chars, but the WebUI's reload-path snippet builder _cliToolResultSnippet in static/ui.js:6083-6086 was capping at 200. So live-stream tool cards (snippet comes from the SSE tool_complete event, populated by result_snippet = _tool_result_snippet(function_result) at api/streaming.py:4245) were getting 4000 chars, while reload-path tool cards (populated via _cliToolResultSnippet(m.content) at static/ui.js:6629 and :6641) were getting 200. That's a genuine asymmetry and matches the user-visible symptom.

Code reference

Origin/master static/ui.js:6083-6087:

function _cliToolResultSnippet(raw){
  const fullText=_cliToolResultText(raw);
  if(_cliLooksLikePatchDiff(fullText)) return _clipCliToolSnippet(fullText);
  return String(fullText||'').slice(0,200);
}

Backend pair at api/streaming.py:3003:

_TOOL_RESULT_SNIPPET_MAX = 4000

The PR aligns those two limits, which is the right symmetry.

Layout / perf check

The 20x bump is fine because buildToolCard at static/ui.js:6909-6920 already caps the visible preview at 800 chars (with a word-break cutoff) and stuffs the full snippet behind a "Show more" data attribute:

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;
}

So the rendered DOM stays the same size by default. The 4000-char tc.snippet only matters when the user clicks "Show more", which is exactly the desired behavior. No layout-breakage risk.

One small caveat: the data-full and data-short attributes on the more-button are HTML-escaped via .replace(/"/g,'&quot;') (see static/ui.js:6946). Going from 200 to 4000 chars stuffed into a data-full attribute is fine for HTML, but it does grow the per-card DOM string by ~3.8KB on reload of a session with many tool calls. For a session with, say, 50 tool calls, that's ~190KB of extra HTML in the initial render. Probably not material on modern browsers, but if reload-time renderMessages becomes sluggish on big sessions, this is a place to look.

Diff-path branch

_cliLooksLikePatchDiff at static/ui.js:6075-6082 is left alone, so patches/diffs still go through _clipCliToolSnippet (cap 20000 chars). The PR doesn't disturb that path. Good — diff snippets often need more room than text snippets.

Verdict

LGTM. The asymmetry was real (200 in WebUI vs 4000 in backend), the fix is the minimum delta, and the rendering path already gates the visible portion at 800 chars so this is a "Show more reveals more" change rather than a layout change. The test plan boxes are right; the easy verification is "reload a session whose live tool card showed 4000 chars and confirm the reloaded card's Show-more reveals the same content". One thing missing from the PR: a regression test asserting the JS constant matches the Python constant. Something like:

# tests/test_tool_snippet_limit_parity.py
def test_tool_snippet_limit_parity():
    py = (REPO / "api" / "streaming.py").read_text()
    js = (REPO / "static" / "ui.js").read_text()
    assert "_TOOL_RESULT_SNIPPET_MAX = 4000" in py
    assert ".slice(0,4000)" in js

…would prevent the limits from drifting apart again. Not blocking; ship as-is or with that two-assertion test added.

Prevents the JS slice(0,N) and Python _TOOL_RESULT_SNIPPET_MAX from
drifting apart again.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
AJV20 pushed a commit to AJV20/hermes-webui that referenced this pull request May 29, 2026
AJV20 pushed a commit to AJV20/hermes-webui that referenced this pull request May 29, 2026
@nesquena-hermes

Copy link
Copy Markdown
Collaborator

Shipped in v0.51.159 (Release EE, stage-batch41) via release PR #3144 — thank you, @mysoul12138! 🎉

Your change (CLI tool-result snippet limit parity (200 → 4000)) is now on master. Your authorship is preserved via the --no-ff merge of your commits in the release branch.

Two PRs in this batch needed a small semantic merge against code that landed in v0.51.158:

GitHub didn't auto-close this PR because the release merged resolved/reparented commits rather than your branch's exact head SHA, so closing manually. The full diff and tests are verified present on master. Full sequential pytest passed (6810 tests). Closing as shipped.

SysAdminDoc pushed a commit to SysAdminDoc/hermes-webui that referenced this pull request Jun 26, 2026
SysAdminDoc pushed a commit to SysAdminDoc/hermes-webui that referenced this pull request Jun 26, 2026
bernyforce pushed a commit to bernyforce/hermes-webui that referenced this pull request Jul 29, 2026
bernyforce pushed a commit to bernyforce/hermes-webui that referenced this pull request Jul 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants