Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 39 additions & 3 deletions static/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -1735,8 +1735,8 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
function _streamFadePauseAfter(text, paragraphBreakIndex){
if(paragraphBreakIndex>=0) return 90;
const trimmed=String(text||'').trimEnd();
if(/[.!?]["')\]]*$/.test(trimmed)) return 45;
if(/[:;]["')\]]*$/.test(trimmed)) return 30;
if(/[.!?]["\x27)\]]*$/.test(trimmed)) return 45;
if(/[:;]["\x27)\]]*$/.test(trimmed)) return 30;
return 0;
}
function _streamFadeNextText(targetText){
Expand Down Expand Up @@ -2195,7 +2195,7 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
// Throttling to 66ms intervals prevents this pileup without noticeable
// visual degradation — streaming text updates still feel immediate.
// performance.now() is monotonic so tab suspend/resume and NTP adjustments
// can't produce negative or enormous deltas.
// cannot produce negative or enormous deltas.
const sinceLastMs=performance.now()-_lastRenderMs;
const _doRender=()=>{
_pendingRafHandle=null;
Expand Down Expand Up @@ -2336,9 +2336,45 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
}
_completeAutomaticCompressionOnLiveProgress(activeSid);
ensureAssistantRow(true);
if(assistantRow) assistantRow.setAttribute('data-interim','1');
Comment thread
rodboev marked this conversation as resolved.
_flushPendingSegmentRender({force:true});
if(typeof finalizeThinkingCard==='function') finalizeThinkingCard();
if(typeof closeCurrentLiveActivityGroup==='function') closeCurrentLiveActivityGroup();
// Collapse old interim notes once more than INTERIM_COLLAPSE_THRESHOLD accumulate.
const INTERIM_COLLAPSE_THRESHOLD=3;
Comment thread
rodboev marked this conversation as resolved.
if(visibleInterimSnippets.length>INTERIM_COLLAPSE_THRESHOLD&&assistantRow){
const blocks=assistantRow.parentElement;
if(blocks){
const allInterim=Array.from(blocks.querySelectorAll('[data-interim="1"]'));
const toHide=allInterim.slice(0,allInterim.length-INTERIM_COLLAPSE_THRESHOLD);
let toggle=blocks.querySelector('.interim-collapse-toggle');
if(!toggle){
toggle=document.createElement('span');
toggle.className='interim-collapse-toggle';
toggle.addEventListener('click',()=>{
const hidden=blocks.querySelectorAll('.interim-collapsed');
if(hidden.length){
hidden.forEach(el=>el.classList.remove('interim-collapsed'));
toggle.dataset.expanded='1';
toggle.textContent='Collapse';
} else {
const all=Array.from(blocks.querySelectorAll('[data-interim="1"]'));
const rehide=all.slice(0,all.length-INTERIM_COLLAPSE_THRESHOLD);
rehide.forEach(el=>el.classList.add('interim-collapsed'));
toggle.dataset.expanded='';
toggle.textContent='Show '+rehide.length+' earlier update'+(rehide.length===1?'':'s');
}
});
if(toHide.length) toHide[0].before(toggle);
Comment thread
rodboev marked this conversation as resolved.
}
// Skip re-collapse when the user expanded manually; always update the stored count.
if(!toggle.dataset.expanded){
toHide.forEach(el=>el.classList.add('interim-collapsed'));
}
const stillHidden=blocks.querySelectorAll('[data-interim="1"].interim-collapsed').length;
if(stillHidden) toggle.textContent='Show '+stillHidden+' earlier update'+(stillHidden===1?'':'s');
}
}
recordActivityBoundary();
_resetAssistantSegment();
_scheduleRender();
Expand Down
11 changes: 11 additions & 0 deletions static/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -5274,3 +5274,14 @@ main.main.showing-logs > #mainLogs{display:flex;}
}

#composerMobileCtxBadge { display: none !important; }

/* Interim progress note collapse (#2403) */
.interim-collapsed { display: none; }
.interim-collapse-toggle {
cursor: pointer;
color: var(--accent-color, #4a9eff);
font-size: 0.85em;
margin: 4px 0;
display: block;
}
.interim-collapse-toggle:hover { text-decoration: underline; }
151 changes: 151 additions & 0 deletions tests/test_issue2403_interim_collapse.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
"""Static-analysis tests for #2403 — collapse old interim progress notes.

When more than INTERIM_COLLAPSE_THRESHOLD interim_assistant events arrive in
one turn, earlier rendered blocks are hidden behind a toggle so the viewport
stays focused on the latest progress note.

These tests pin the structural invariants without a live browser, using the
same static-analysis pattern as test_issue2713_streaming_segment_flush.py.
"""
import pathlib
import re

REPO = pathlib.Path(__file__).parent.parent


def read(rel):
return (REPO / rel).read_text(encoding="utf-8")


def _extract_interim_handler(src):
"""Return the full interim_assistant SSE handler body."""
start_pattern = "source.addEventListener('interim_assistant'"
start = src.index(start_pattern)
end_marker = "\n });"
pos = start
while True:
idx = src.index(end_marker, pos + 1)
if idx > start + len(start_pattern) + 20:
return src[start : idx + len(end_marker)]
pos = idx


class TestInterimCollapseHandlerStructure:
"""The interim_assistant handler must contain the collapse threshold and logic."""

def test_collapse_threshold_constant_present(self):
src = read("static/messages.js")
fn = _extract_interim_handler(src)
assert "INTERIM_COLLAPSE_THRESHOLD" in fn, (
"interim_assistant handler must define INTERIM_COLLAPSE_THRESHOLD "
"to avoid scattered magic numbers"
)

def test_threshold_is_three(self):
src = read("static/messages.js")
fn = _extract_interim_handler(src)
# Constant must be assigned to 3
assert re.search(r"INTERIM_COLLAPSE_THRESHOLD\s*=\s*3\b", fn), (
"INTERIM_COLLAPSE_THRESHOLD must be set to 3"
)

def test_visibleInterimSnippets_length_comparison(self):
src = read("static/messages.js")
fn = _extract_interim_handler(src)
assert "visibleInterimSnippets.length" in fn, (
"collapse guard must compare visibleInterimSnippets.length"
)
assert "INTERIM_COLLAPSE_THRESHOLD" in fn, (
"collapse guard must reference INTERIM_COLLAPSE_THRESHOLD, not a magic number"
)

def test_interim_data_attribute_set(self):
src = read("static/messages.js")
fn = _extract_interim_handler(src)
assert "data-interim" in fn, (
"interim_assistant handler must mark each segment with data-interim "
"so collapse logic can query them"
)

def test_interim_collapsed_class_applied(self):
src = read("static/messages.js")
fn = _extract_interim_handler(src)
assert "interim-collapsed" in fn, (
"collapse logic must apply the interim-collapsed CSS class to hide old blocks"
)

def test_collapse_toggle_element_created(self):
src = read("static/messages.js")
fn = _extract_interim_handler(src)
assert "interim-collapse-toggle" in fn, (
"collapse logic must create an .interim-collapse-toggle element"
)

def test_toggle_text_references_count(self):
src = read("static/messages.js")
fn = _extract_interim_handler(src)
# Toggle label must be dynamic: "Show N earlier update(s)"
assert "earlier update" in fn, (
"collapse toggle text must reference 'earlier update' so the count is visible"
)

def test_attribute_set_before_flush(self):
src = read("static/messages.js")
fn = _extract_interim_handler(src)
attr_pos = fn.index("setAttribute('data-interim','1')")
flush_pos = fn.rindex("_flushPendingSegmentRender({force:true})")
assert attr_pos < flush_pos, (
"data-interim attribute must be set before _flushPendingSegmentRender "
"so the segment is marked before it is sealed"
)

def test_collapse_after_flush_before_reset(self):
src = read("static/messages.js")
fn = _extract_interim_handler(src)
flush_pos = fn.index("_flushPendingSegmentRender({force:true})")
collapse_pos = fn.index("INTERIM_COLLAPSE_THRESHOLD")
reset_pos = fn.index("_resetAssistantSegment()", collapse_pos)
assert flush_pos < collapse_pos < reset_pos, (
"collapse logic must run after flush but before _resetAssistantSegment"
)


class TestInterimCollapseCSS:
"""CSS must define both .interim-collapsed and .interim-collapse-toggle."""

def test_interim_collapsed_rule_present(self):
css = read("static/style.css")
assert ".interim-collapsed" in css, (
"style.css must define .interim-collapsed to hide collapsed blocks"
)

def test_interim_collapsed_uses_display_none(self):
css = read("static/style.css")
m = re.search(r"\.interim-collapsed\s*\{[^}]*\}", css)
assert m, ".interim-collapsed rule not found in style.css"
rule = m.group(0)
assert "display" in rule and "none" in rule, (
".interim-collapsed must set display:none"
)

def test_collapse_toggle_rule_present(self):
css = read("static/style.css")
assert ".interim-collapse-toggle" in css, (
"style.css must define .interim-collapse-toggle"
)

def test_collapse_toggle_has_cursor_pointer(self):
css = read("static/style.css")
# Extract the first .interim-collapse-toggle rule block
m = re.search(r"\.interim-collapse-toggle\s*\{[^}]*\}", css)
assert m, ".interim-collapse-toggle rule not found"
rule = m.group(0)
assert "cursor" in rule and "pointer" in rule, (
".interim-collapse-toggle must set cursor:pointer"
)

def test_collapse_toggle_hover_rule_present(self):
css = read("static/style.css")
assert ".interim-collapse-toggle:hover" in css, (
"style.css must define a :hover rule for .interim-collapse-toggle"
)
Loading