fix(#6467): show final assistant report with tool-limit status card

This commit is contained in:
Rod Boev authored and nesquena-hermes committed 2026-07-29 20:54:45 +00:00
1 parent 480de19710
commit 1e591aef13
3 files changed
+237 -1

No files matched your search

@@ -0,0 +1,7 @@
| surface | command run | observed result | base/head |
| --- | --- | --- | --- |
| ordinary assistant `_statusCard` plus non-empty final report | `C:\Apps\hermes\hermes-agent\venv\Scripts\python.exe -m pytest tests/test_status_command_card.py -v --timeout=60` | `tests/test_status_command_card.py::test_render_messages_keeps_final_report_when_status_card_present PASSED` and `12 passed in 4.50s` | base: simulated legacy ordinary-branch probe emitted only `<status-card>limit</status-card>` and dropped `<div class="msg-body"><p>Final report</p></div>`; head: extracted current ordinary branch emitted the status card before `<div class="msg-body"><p>Final report</p></div>` in the same assistant segment |
| status-only assistant `_statusCard` | `C:\Apps\hermes\hermes-agent\venv\Scripts\python.exe -m pytest tests/test_status_command_card.py -v --timeout=60` | boundary `tests/test_status_command_card.py::test_render_messages_keeps_status_only_card_boundary PASSED` | base: boundary unchanged by the bug contract; head: extracted current branch returned only `<status-card>limit</status-card>` |
| assistant files plus `_statusCard` with empty text body | `C:\Apps\hermes\hermes-agent\venv\Scripts\python.exe -m pytest tests/test_status_command_card.py -v --timeout=60` | boundary `tests/test_status_command_card.py::test_render_messages_keeps_files_when_status_card_present PASSED` | base: the legacy ordinary branch returned only `<status-card>limit</status-card>` when the assistant segment carried files without text. head: extracted current branch kept `<div class="msg-files">artifact.txt</div>` after the status card in the same assistant segment |
| special status-first branch | `C:\Apps\hermes\hermes-agent\venv\Scripts\python.exe -m pytest tests/test_status_command_card.py -v --timeout=60` | boundary `tests/test_status_command_card.py::test_ordered_transparent_branch_keeps_single_status_card_boundary PASSED` | base: boundary unchanged by this ordinary-branch fix; head: extracted ordered-transparent branch emitted one status card and one body block |
| ordinary assistant without `_statusCard` | `C:\Apps\hermes\hermes-agent\venv\Scripts\python.exe -m pytest tests/test_status_command_card.py -v --timeout=60` | boundary `tests/test_status_command_card.py::test_render_messages_keeps_body_only_boundary_without_status_card PASSED` | base: boundary unchanged by the bug contract; head: extracted current branch returned only `<div class="msg-body"><p>Final report</p></div><footer>meta</footer>` |
+2 -1
View File
@@ -16408,9 +16408,10 @@ function renderMessages(options){
if((isCompactWorklogMode()||isTransparentStream())&&_assistantThinkingBelongsInWorklog(m, rawIdx, toolCallAssistantIdxs)) assistantThinking.set(rawIdx, thinkingText);
else if(window._showThinking!==false) seg.insertAdjacentHTML('beforeend', _thinkingCardHtml(thinkingText));
}
const hasVisibleBody=!!(String(content||'').trim()||filesHtml||statusHtml||recoveryHtml);
const hasVisibleBody=!!(String(content||'').trim()||filesHtml||recoveryHtml);
if(statusHtml){
seg.insertAdjacentHTML('beforeend', statusHtml);
if(hasVisibleBody) seg.insertAdjacentHTML('beforeend', `${filesHtml}<div class="msg-body">${bodyHtml}</div>${footHtml}`);
}else if(hasVisibleBody){
seg.insertAdjacentHTML('beforeend', `${filesHtml}<div class="msg-body">${bodyHtml}</div>${footHtml}`);
}else if(!(thinkingText&&window._showThinking!==false&&!isSimplifiedToolCalling())){
+228
View File
@@ -5,7 +5,13 @@ ephemeral assistant-style card from already-loaded session/profile/model data.
It must not round-trip through the agent or a status endpoint just to draw the
card.
"""
import json
import pathlib
import shutil
import subprocess
import tempfile
import pytest
REPO_ROOT = pathlib.Path(__file__).parent.parent
@@ -14,6 +20,7 @@ UI_JS = (REPO_ROOT / "static" / "ui.js").read_text(encoding="utf-8")
STYLE_CSS = (REPO_ROOT / "static" / "style.css").read_text(encoding="utf-8")
I18N_JS = (REPO_ROOT / "static" / "i18n.js").read_text(encoding="utf-8")
MESSAGES_JS = (REPO_ROOT / "static" / "messages.js").read_text(encoding="utf-8")
NODE = shutil.which("node")
def _function_body(src: str, name: str) -> str:
@@ -31,6 +38,190 @@ def _function_body(src: str, name: str) -> str:
raise AssertionError(f"Could not extract {name}()")
def _run_node(source: str) -> str:
if NODE is None:
pytest.skip("node not on PATH")
with tempfile.NamedTemporaryFile(
"w", suffix=".cjs", encoding="utf-8", dir=REPO_ROOT, delete=False
) as script:
script.write(source)
script_path = pathlib.Path(script.name)
try:
result = subprocess.run(
[NODE, str(script_path)],
cwd=str(REPO_ROOT),
capture_output=True,
text=True,
timeout=30,
)
finally:
script_path.unlink(missing_ok=True)
if result.returncode != 0:
raise RuntimeError(result.stderr)
return result.stdout.strip()
def _status_render_probe() -> dict:
render_body = _function_body(UI_JS, "renderMessages")
return json.loads(
_run_node(
f"""
const renderBody = {render_body!r};
function extractBlock(startMarker, endMarker) {{
const start = renderBody.indexOf(startMarker);
if (start === -1) throw new Error(`missing start marker: ${{startMarker}}`);
const end = renderBody.indexOf(endMarker, start);
if (end === -1) throw new Error(`missing end marker: ${{endMarker}}`);
return renderBody.slice(start, end);
}}
function makeSeg() {{
return {{
html: '',
classList: {{
added: [],
add(value) {{ this.added.push(value); }},
}},
insertAdjacentHTML(_where, html) {{ this.html += String(html || ''); }},
}};
}}
const ordinaryBlock = extractBlock(
"const hasVisibleBody=!!(String(content||'').trim()||filesHtml||recoveryHtml);",
"_assistantTurnBlocks(currentAssistantTurn).appendChild(seg);"
);
const orderedBlock = extractBlock(
"if(isLastTextPart&&statusHtml){{",
"blocks.appendChild(orderedSeg);"
);
function runBaseOrdinary(opts) {{
const seg = makeSeg();
const content = opts.content || '';
const filesHtml = opts.filesHtml || '';
const statusHtml = opts.statusHtml || '';
const recoveryHtml = opts.recoveryHtml || '';
const bodyHtml = opts.bodyHtml || '';
const footHtml = opts.footHtml || '';
const thinkingText = opts.thinkingText || '';
const window = {{ _showThinking: opts.showThinking !== false }};
function isSimplifiedToolCalling() {{ return !!opts.simplified; }}
const hasVisibleBody = !!(String(content || '').trim() || filesHtml || statusHtml || recoveryHtml);
if (statusHtml) {{
seg.insertAdjacentHTML('beforeend', statusHtml);
}} else if (hasVisibleBody) {{
seg.insertAdjacentHTML('beforeend', `${{filesHtml}}<div class="msg-body">${{bodyHtml}}</div>${{footHtml}}`);
}} else if (!(thinkingText && window._showThinking !== false && !isSimplifiedToolCalling())) {{
seg.classList.add('assistant-segment-anchor');
}}
return seg;
}}
function runHeadOrdinary(opts) {{
const seg = makeSeg();
const content = opts.content || '';
const filesHtml = opts.filesHtml || '';
const statusHtml = opts.statusHtml || '';
const recoveryHtml = opts.recoveryHtml || '';
const bodyHtml = opts.bodyHtml || '';
const footHtml = opts.footHtml || '';
const thinkingText = opts.thinkingText || '';
const window = {{ _showThinking: opts.showThinking !== false }};
function isSimplifiedToolCalling() {{ return !!opts.simplified; }}
eval(ordinaryBlock);
return seg;
}}
function runOrderedSpecial(opts) {{
const orderedSeg = makeSeg();
const isLastTextPart = opts.isLastTextPart ?? true;
const statusHtml = opts.statusHtml || '';
const filesHtml = opts.filesHtml || '';
const footHtml = opts.footHtml || '';
const partBodyHtml = opts.partBodyHtml || '';
eval(orderedBlock);
return orderedSeg;
}}
const ordinaryInput = {{
content: 'Final report',
bodyHtml: '<p>Final report</p>',
statusHtml: '<status-card>limit</status-card>',
filesHtml: '',
footHtml: '<footer>meta</footer>',
recoveryHtml: '',
}};
const ordinaryBase = runBaseOrdinary(ordinaryInput);
const ordinaryHead = runHeadOrdinary(ordinaryInput);
const statusOnly = runHeadOrdinary({{
statusHtml: '<status-card>limit</status-card>',
content: '',
bodyHtml: '',
filesHtml: '',
footHtml: '<footer>meta</footer>',
recoveryHtml: '',
}});
const ordinaryNoStatus = runHeadOrdinary({{
statusHtml: '',
content: 'Final report',
bodyHtml: '<p>Final report</p>',
filesHtml: '',
footHtml: '<footer>meta</footer>',
recoveryHtml: '',
}});
const filesOnlyWithStatus = runHeadOrdinary({{
statusHtml: '<status-card>limit</status-card>',
content: '',
bodyHtml: '',
filesHtml: '<div class="msg-files">artifact.txt</div>',
footHtml: '',
recoveryHtml: '',
}});
const orderedSpecial = runOrderedSpecial({{
statusHtml: '<status-card>limit</status-card>',
filesHtml: '',
footHtml: '<footer>meta</footer>',
partBodyHtml: '<p>Final report</p>',
}});
console.log(JSON.stringify({{
ordinary: {{
baseHtml: ordinaryBase.html,
headHtml: ordinaryHead.html,
headHasBody: ordinaryHead.html.includes('<div class="msg-body"><p>Final report</p></div>'),
headStatusBeforeBody:
ordinaryHead.html.indexOf('<status-card>limit</status-card>') <
ordinaryHead.html.indexOf('<div class="msg-body"><p>Final report</p></div>'),
baseDropsBody: !ordinaryBase.html.includes('<div class="msg-body"><p>Final report</p></div>'),
}},
statusOnly: {{
html: statusOnly.html,
isCardOnly: statusOnly.html === '<status-card>limit</status-card>',
}},
ordinaryNoStatus: {{
html: ordinaryNoStatus.html,
isBodyOnly:
ordinaryNoStatus.html === '<div class="msg-body"><p>Final report</p></div><footer>meta</footer>',
}},
filesOnlyWithStatus: {{
html: filesOnlyWithStatus.html,
hasFiles: filesOnlyWithStatus.html.includes('<div class="msg-files">artifact.txt</div>'),
statusBeforeFiles:
filesOnlyWithStatus.html.indexOf('<status-card>limit</status-card>') <
filesOnlyWithStatus.html.indexOf('<div class="msg-files">artifact.txt</div>'),
}},
orderedSpecial: {{
html: orderedSpecial.html,
statusCount: (orderedSpecial.html.match(/<status-card>/g) || []).length,
bodyCount: (orderedSpecial.html.match(/<div class="msg-body">/g) || []).length,
}},
}}));
"""
)
)
def test_status_command_is_registered_with_help_text():
assert "{name:'status'" in COMMANDS_JS
assert "desc:t('cmd_status')" in COMMANDS_JS
@@ -81,6 +272,43 @@ def test_render_messages_treats_status_card_as_visible_assistant_content():
assert "statusHtml" in render_body
def test_render_messages_keeps_final_report_when_status_card_present():
probe = _status_render_probe()
ordinary = probe["ordinary"]
assert ordinary["baseDropsBody"] is True
assert ordinary["headHasBody"] is True
assert ordinary["headStatusBeforeBody"] is True
def test_render_messages_keeps_status_only_card_boundary():
probe = _status_render_probe()
status_only = probe["statusOnly"]
assert status_only["isCardOnly"] is True
assert status_only["html"] == "<status-card>limit</status-card>"
def test_render_messages_keeps_body_only_boundary_without_status_card():
probe = _status_render_probe()
ordinary_no_status = probe["ordinaryNoStatus"]
assert ordinary_no_status["isBodyOnly"] is True
assert ordinary_no_status["html"] == "<div class=\"msg-body\"><p>Final report</p></div><footer>meta</footer>"
def test_render_messages_keeps_files_when_status_card_present():
probe = _status_render_probe()
files_only = probe["filesOnlyWithStatus"]
assert files_only["hasFiles"] is True
assert files_only["statusBeforeFiles"] is True
def test_ordered_transparent_branch_keeps_single_status_card_boundary():
probe = _status_render_probe()
ordered = probe["orderedSpecial"]
assert ordered["statusCount"] == 1
assert ordered["bodyCount"] == 1
assert ordered["html"] == "<status-card>limit</status-card><div class=\"msg-body\"><p>Final report</p></div><footer>meta</footer>"
def test_status_card_styles_exist():
assert ".status-card" in STYLE_CSS
assert ".status-card-grid" in STYLE_CSS