mirror of
https://github.com/nesquena/hermes-webui.git
synced 2026-06-07 17:30:21 +00:00
fix(chat): keep visible interim progress in timeline
This commit is contained in:
@@ -3,6 +3,10 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Compact tool activity now keeps visible interim assistant progress in the live Session timeline instead of making that progress effectively collapsed-only inside Activity details. The interim assistant stream path creates and flushes a visible assistant segment before resetting for later tool/compression activity.
|
||||
|
||||
## [v0.51.137] — 2026-05-25 — Release DI (stage-batch19 — 6-PR medium-risk batch)
|
||||
|
||||
### Added
|
||||
|
||||
@@ -74,6 +74,11 @@ terse, for example `Activity: 4 tools`, and should not duplicate the thinking
|
||||
area, list every tool name in the summary, or add redundant trailing count
|
||||
badges.
|
||||
|
||||
Visible interim assistant progress is part of the live conversation timeline,
|
||||
not raw debug detail. Compact Activity may collapse tool arguments, long tool
|
||||
results, and low-level reasoning detail, but it must not make concise
|
||||
user-visible progress text available only inside a collapsed disclosure.
|
||||
|
||||
The existing two-stage proposal in `docs/ui-ux/two-stage-proposal.html` records a
|
||||
compatible direction for long turns: live work can be grouped as a worklog, then
|
||||
settled history can collapse while the final answer reads as the calm
|
||||
|
||||
@@ -82,6 +82,9 @@ while WebUI still has multiple overlapping state stores.
|
||||
browser-facing timeline renderer as live SSE events so recovery does not
|
||||
downgrade a structured Thinking / progress / tool / compression turn into a
|
||||
separate flattened presentation.
|
||||
Visible interim assistant progress must remain visible timeline content; a
|
||||
compact Activity disclosure may summarize adjacent tool/debug detail, but it
|
||||
must not be the only place where the user can see emitted progress text.
|
||||
6. **Compression is not current intent.** Automatic compression summaries and
|
||||
reference cards are recovery/handoff material. They must not be treated as a
|
||||
new user request, active-turn content, or the default visible explanation for
|
||||
|
||||
+6
-4
@@ -1352,9 +1352,10 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
|
||||
};
|
||||
step();
|
||||
}
|
||||
function _flushPendingSegmentRender(){
|
||||
if(!assistantBody||!_renderPending) return;
|
||||
_cancelAnimationFramePendingStreamRender();
|
||||
function _flushPendingSegmentRender(options={}){
|
||||
const force=!!(options&&options.force);
|
||||
if(!assistantBody||(!force&&!_renderPending)) return;
|
||||
if(_renderPending) _cancelAnimationFramePendingStreamRender();
|
||||
const displayText=segmentStart===0
|
||||
? _parseStreamState().displayText
|
||||
: _stripXmlToolCalls(assistantText.slice(segmentStart));
|
||||
@@ -1512,8 +1513,9 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
|
||||
if(typeof updateThinking==='function') updateThinking(_liveThinkingText());
|
||||
else appendThinking(_liveThinkingText());
|
||||
}
|
||||
_flushPendingSegmentRender();
|
||||
ensureAssistantRow(true);
|
||||
_flushPendingSegmentRender({force:true});
|
||||
if(typeof closeCurrentLiveActivityGroup==='function') closeCurrentLiveActivityGroup();
|
||||
_resetAssistantSegment();
|
||||
_scheduleRender();
|
||||
});
|
||||
|
||||
@@ -25,14 +25,14 @@ class TestFlushHelperExists:
|
||||
|
||||
def test_flush_helper_declared(self):
|
||||
src = read("static/messages.js")
|
||||
assert "function _flushPendingSegmentRender()" in src, (
|
||||
assert "function _flushPendingSegmentRender(options={})" in src, (
|
||||
"_flushPendingSegmentRender helper must be declared in messages.js"
|
||||
)
|
||||
|
||||
def test_flush_helper_guards_on_assistant_body(self):
|
||||
src = read("static/messages.js")
|
||||
m = re.search(
|
||||
r"function _flushPendingSegmentRender\(\)\{.*?\n \}",
|
||||
r"function _flushPendingSegmentRender\(options=\{\}\)\{.*?\n \}",
|
||||
src,
|
||||
re.DOTALL,
|
||||
)
|
||||
@@ -45,7 +45,7 @@ class TestFlushHelperExists:
|
||||
def test_flush_helper_guards_on_render_pending(self):
|
||||
src = read("static/messages.js")
|
||||
m = re.search(
|
||||
r"function _flushPendingSegmentRender\(\)\{.*?\n \}",
|
||||
r"function _flushPendingSegmentRender\(options=\{\}\)\{.*?\n \}",
|
||||
src,
|
||||
re.DOTALL,
|
||||
)
|
||||
@@ -58,7 +58,7 @@ class TestFlushHelperExists:
|
||||
def test_flush_helper_cancels_pending_raf(self):
|
||||
src = read("static/messages.js")
|
||||
m = re.search(
|
||||
r"function _flushPendingSegmentRender\(\)\{.*?\n \}",
|
||||
r"function _flushPendingSegmentRender\(options=\{\}\)\{.*?\n \}",
|
||||
src,
|
||||
re.DOTALL,
|
||||
)
|
||||
@@ -71,7 +71,7 @@ class TestFlushHelperExists:
|
||||
def test_flush_helper_uses_smd_write(self):
|
||||
src = read("static/messages.js")
|
||||
m = re.search(
|
||||
r"function _flushPendingSegmentRender\(\)\{.*?\n \}",
|
||||
r"function _flushPendingSegmentRender\(options=\{\}\)\{.*?\n \}",
|
||||
src,
|
||||
re.DOTALL,
|
||||
)
|
||||
@@ -84,7 +84,7 @@ class TestFlushHelperExists:
|
||||
def test_flush_helper_has_render_md_fallback(self):
|
||||
src = read("static/messages.js")
|
||||
m = re.search(
|
||||
r"function _flushPendingSegmentRender\(\)\{.*?\n \}",
|
||||
r"function _flushPendingSegmentRender\(options=\{\}\)\{.*?\n \}",
|
||||
src,
|
||||
re.DOTALL,
|
||||
)
|
||||
@@ -97,7 +97,7 @@ class TestFlushHelperExists:
|
||||
def test_flush_helper_has_esc_fallback(self):
|
||||
src = read("static/messages.js")
|
||||
m = re.search(
|
||||
r"function _flushPendingSegmentRender\(\)\{.*?\n \}",
|
||||
r"function _flushPendingSegmentRender\(options=\{\}\)\{.*?\n \}",
|
||||
src,
|
||||
re.DOTALL,
|
||||
)
|
||||
@@ -159,7 +159,7 @@ class TestInterimAssistantHandlerFlush:
|
||||
def test_interim_handler_calls_flush(self):
|
||||
src = read("static/messages.js")
|
||||
fn = _extract_handler(src, "interim_assistant")
|
||||
assert "_flushPendingSegmentRender()" in fn, (
|
||||
assert "_flushPendingSegmentRender({force:true})" in fn, (
|
||||
"interim_assistant handler must call _flushPendingSegmentRender() "
|
||||
"before _resetAssistantSegment()"
|
||||
)
|
||||
@@ -169,10 +169,32 @@ class TestInterimAssistantHandlerFlush:
|
||||
the segment for new content (not the early alreadyStreamed branch)."""
|
||||
src = read("static/messages.js")
|
||||
fn = _extract_handler(src, "interim_assistant")
|
||||
flush_pos = fn.index("_flushPendingSegmentRender()")
|
||||
flush_pos = fn.index("_flushPendingSegmentRender({force:true})")
|
||||
# Find the _resetAssistantSegment call that comes AFTER the flush
|
||||
reset_pos = fn.index("_resetAssistantSegment()", flush_pos)
|
||||
assert flush_pos < reset_pos, (
|
||||
"_flushPendingSegmentRender must be called BEFORE the final "
|
||||
"_resetAssistantSegment in the interim_assistant handler"
|
||||
)
|
||||
|
||||
def test_interim_handler_creates_visible_segment_before_forced_flush(self):
|
||||
src = read("static/messages.js")
|
||||
fn = _extract_handler(src, "interim_assistant")
|
||||
ensure_pos = fn.index("ensureAssistantRow(true)")
|
||||
flush_pos = fn.index("_flushPendingSegmentRender({force:true})")
|
||||
reset_pos = fn.index("_resetAssistantSegment()", flush_pos)
|
||||
assert ensure_pos < flush_pos < reset_pos, (
|
||||
"visible interim assistant progress must create a live assistant "
|
||||
"segment, synchronously flush it, then reset for the next segment"
|
||||
)
|
||||
|
||||
def test_interim_handler_closes_activity_after_visible_progress_boundary(self):
|
||||
src = read("static/messages.js")
|
||||
fn = _extract_handler(src, "interim_assistant")
|
||||
flush_pos = fn.index("_flushPendingSegmentRender({force:true})")
|
||||
close_pos = fn.index("closeCurrentLiveActivityGroup()", flush_pos)
|
||||
reset_pos = fn.index("_resetAssistantSegment()", close_pos)
|
||||
assert flush_pos < close_pos < reset_pos, (
|
||||
"visible interim assistant progress is timeline content; it must "
|
||||
"close the current live Activity burst before later tools append"
|
||||
)
|
||||
|
||||
@@ -285,7 +285,7 @@ class TestToolCallGroupingStatic:
|
||||
"The non-simplified path should preserve standalone settled thinking cards."
|
||||
)
|
||||
|
||||
def test_live_visible_interim_text_keeps_single_activity_group(self):
|
||||
def test_live_visible_interim_text_preserves_timeline_boundary(self):
|
||||
live_thinking_fn = _function_body(UI_JS, "appendThinking")
|
||||
live_tool_fn = _function_body(UI_JS, "appendLiveToolCard")
|
||||
helper = _function_body(UI_JS, "ensureActivityGroup")
|
||||
@@ -318,12 +318,18 @@ class TestToolCallGroupingStatic:
|
||||
"Compact live thinking should reactivate the latest existing Thinking card instead of stacking a new card after every tool boundary."
|
||||
)
|
||||
reset_fn = _function_body(MESSAGES_JS, "_resetAssistantSegment")
|
||||
assert "_closeCurrentLiveActivityGroup" not in MESSAGES_JS and "closeActivity" not in reset_fn, (
|
||||
"Assistant text resets should not carry a dead Activity-splitting path."
|
||||
assert "function closeCurrentLiveActivityGroup()" in UI_JS, (
|
||||
"Visible interim assistant progress needs a shared helper to close the current Activity burst."
|
||||
)
|
||||
interim_match = re.search(r"source\.addEventListener\('interim_assistant',e=>\{(.*?)\n\s*\}\);", MESSAGES_JS, re.S)
|
||||
assert interim_match and "_resetAssistantSegment({closeActivity:true});" not in interim_match.group(1), (
|
||||
"Visible interim assistant text should not split Compact tool activity into multiple Activity rows."
|
||||
assert interim_match and "closeCurrentLiveActivityGroup()" in interim_match.group(1), (
|
||||
"Visible interim assistant progress is timeline content and must split the current Activity burst."
|
||||
)
|
||||
assert interim_match and "ensureAssistantRow(true)" in interim_match.group(1), (
|
||||
"Visible interim assistant progress must create a visible assistant timeline segment."
|
||||
)
|
||||
assert interim_match and "_flushPendingSegmentRender({force:true})" in interim_match.group(1), (
|
||||
"Visible interim assistant progress must be synchronously rendered before the segment reset."
|
||||
)
|
||||
tool_start_segment = MESSAGES_JS.split("source.addEventListener('tool',e=>{", 1)[1].split("source.addEventListener('tool_complete'", 1)[0]
|
||||
assert "_resetAssistantSegment();" in tool_start_segment, (
|
||||
|
||||
Reference in New Issue
Block a user