diff --git a/CHANGELOG.md b/CHANGELOG.md index 2bdc1228..d45652ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,12 @@ ## [Unreleased] +## [v0.51.574] — 2026-06-22 — Release UG (transparent-stream collapsed tool previews) + +### Fixed + +- **Collapsed tool rows in the Transparent Stream view show their one-line summary again.** A regression left collapsed tool cards showing only the bare tool name (`read_file`, `terminal`, `search_files`, `skill_view`) with no hint of what each call did — you had to expand every row. Each collapsed row now shows a quiet target-based summary (file path, command, search query, skill name) next to the tool name, without dumping the raw result into the row. Affects both live streaming and reloaded/old sessions (they share the render path). Fixes #4658. + ## [v0.51.573] — 2026-06-22 — Release UF (reasoning request-storm fix) ### Fixed diff --git a/static/ui.js b/static/ui.js index cd306184..3d52866b 100644 --- a/static/ui.js +++ b/static/ui.js @@ -8216,6 +8216,31 @@ function _transparentToolStatus(tc, settled){ if(tc&&tc.done===false) return settled?'Interrupted':'Running'; return 'Completed'; } +// Quiet one-line summary for a collapsed transparent tool row (#4658). +// The transparent view overrides the row name to the bare tool name +// (_toolShortName, e.g. "read_file"/"terminal"), so — unlike the worklog view — +// it has no action-label carrying the target, and buildToolCard's collapsed +// preview is blanked for the common arg/shell case (the #4411 suppression that +// assumes the name carries the target). That left transparent rows showing only +// the bare tool name with no hint of what each call did. Rebuild a summary from +// the call's TARGET (path/command/query/skill/...) — NOT the raw result JSON — +// so it stays consistent with the "keep collapsed previews quiet" intent +// (test_tool_card_preview_summary.py) while restoring "understand the call +// without expanding it". +function _transparentToolSummary(tc){ + if(!tc||typeof tc!=='object') return ''; + // Explicit progress text (e.g. subagent_progress) wins while still running. + const explicit=String(tc.preview||'').trim(); + if(tc.done===false&&explicit) return _shortToolLabel(explicit,160); + // Target-based summary only (path/command/query/skill). Deliberately NO generic + // arg-preview fallback: a call with args but no real target (e.g. `terminal` + // with only {workdir} or an unknown tool with {mode:"dry-run"}) must yield an + // EMPTY collapsed preview rather than dumping a raw arg snippet — that keeps the + // collapsed row quiet and consistent with the no-args case (#4658 review). + const target=typeof _toolVisibleTargetLabel==='function'?_toolVisibleTargetLabel(tc,{limit:160}):''; + if(target) return target; + return ''; +} function _copyEventToClipboard(row){ if(!row) return; const type=row.getAttribute('data-event-type'); @@ -8477,6 +8502,22 @@ function _decorateTransparentEventRow(row, opts){ // also prefix the name with "Running:" — that's the same redundancy class // V6 removed from the detail body. (Trifecta r2 #4.) if(nameEl) nameEl.textContent=_toolShortName(name); + // #4658: restore the collapsed-row inline summary. buildToolCard emits a + // `.tool-card-preview` span but blanks its text for the common case, and + // the bare _toolShortName above drops the target the worklog view carries + // in its action-label name. Populate the preview from a quiet, + // target-based summary so each collapsed row says what it did without + // expanding. Idempotent across re-decoration (status updates re-run this). + const previewEl=header.querySelector('.tool-card-preview'); + if(previewEl){ + const summary=_transparentToolSummary(tc); + if(summary){ + previewEl.textContent=summary; + previewEl.removeAttribute('hidden'); + }else{ + previewEl.textContent=''; + } + } let statusEl=header.querySelector('.transparent-event-status'); if(!statusEl){ statusEl=document.createElement('span'); diff --git a/tests/test_issue4658_transparent_collapsed_preview.py b/tests/test_issue4658_transparent_collapsed_preview.py new file mode 100644 index 00000000..c0aa224c --- /dev/null +++ b/tests/test_issue4658_transparent_collapsed_preview.py @@ -0,0 +1,226 @@ +"""Regression test for issue #4658: collapsed tool-card rows in the +transparent-stream view lost their inline summary/preview. + +Symptom: collapsed transparent tool rows showed only the bare tool name +(`read_file`, `terminal`, `search_files`) with no hint of what the call did; +expanding still worked. Caused by the interaction of two changes: + * 69072ac34 moved buildToolCard's collapsed preview to an arg summary and + #4411 (6951d10a) then blanked that preview for the common arg/shell case + (it assumes the row NAME carries the target — true for the worklog view's + action-label name), and + * the transparent view (_decorateTransparentEventRow) overrides the row name + back to the BARE tool name (_toolShortName), so it had neither the + target-carrying label nor a preview. + +Fix: _decorateTransparentEventRow now populates the `.tool-card-preview` span +from a quiet, TARGET-based summary (_transparentToolSummary) — path/command/ +query/skill, never the raw result JSON — so collapsed rows are self-describing +again while honoring the "keep collapsed previews quiet" intent +(test_tool_card_preview_summary.py). + +This drives the ACTUAL functions from static/ui.js via node + jsdom-free DOM +shims, and runs the same render path live streaming and persisted reload share +(both go through _decorateTransparentEventRow(buildToolCard(tc))). +""" +import json +import shutil +import subprocess +from pathlib import Path + +import pytest + +REPO_ROOT = Path(__file__).parent.parent.resolve() +UI_JS_PATH = REPO_ROOT / "static" / "ui.js" + +NODE = shutil.which("node") +pytestmark = pytest.mark.skipif(NODE is None, reason="node not on PATH") + + +_DRIVER_SRC = r""" +const fs = require('fs'); +const src = fs.readFileSync(process.argv[2], 'utf8'); + +// ── Minimal DOM shims (enough for buildToolCard + _decorateTransparentEventRow) +function makeEl(tag){ + const el = { + tagName: (tag||'div').toUpperCase(), + _attrs: {}, _classes: new Set(), children: [], style: {}, + dataset: {}, _html: '', textContent: '', parentNode: null, + firstChild: null, + classList: { + add(c){el._classes.add(c)}, remove(c){el._classes.delete(c)}, + toggle(c,on){const w=on===undefined?!el._classes.has(c):!!on; if(w)el._classes.add(c); else el._classes.delete(c);}, + contains(c){return el._classes.has(c)}, + }, + setAttribute(k,v){el._attrs[k]=String(v);}, getAttribute(k){return k in el._attrs?el._attrs[k]:null;}, + removeAttribute(k){delete el._attrs[k];}, hasAttribute(k){return k in el._attrs;}, + appendChild(c){c.parentNode=el; el.children.push(c); el.firstChild=el.children[0]; return c;}, + insertBefore(c,ref){c.parentNode=el; const i=el.children.indexOf(ref); if(i<0)el.children.push(c); else el.children.splice(i,0,c); el.firstChild=el.children[0]; return c;}, + insertAdjacentHTML(){/* detail body not needed for collapsed-preview assertions */}, + closest(){return null}, focus(){}, + get innerHTML(){return el._html;}, + set innerHTML(v){ + el._html=String(v); + // Parse just the spans we assert on out of the template buildToolCard emits. + el.children=[]; + const mk=(cls,text)=>{const c=makeEl('span'); c._classes=new Set(cls.split(' ').filter(Boolean)); c.textContent=text; c.parentNode=el; el.children.push(c);}; + const grab=(cls)=>{const re=new RegExp('([\\s\\S]*?)<\\/span>'); const m=el._html.match(re); return m?m[1]:null;}; + const nameLabel=grab('tool-card-name-label'); + if(nameLabel!==null){ const nameSpan=makeEl('span'); nameSpan._classes=new Set(['tool-card-name']); nameSpan.textContent=nameLabel; nameSpan.parentNode=el; el.children.push(nameSpan); } + const preview=grab('tool-card-preview'); + if(preview!==null) mk('tool-card-preview', preview); + const header=makeEl('div'); header._classes=new Set(['tool-card-header']); + // re-home the parsed spans under a header element so querySelector('.tool-card-header') works + header.children=el.children.slice(); header.children.forEach(c=>c.parentNode=header); + const card=makeEl('div'); card._classes=new Set(['tool-card']); card.appendChild(header); + el.children=[card]; el.firstChild=card; + }, + querySelector(sel){ return el._find(sel, false)[0]||null; }, + querySelectorAll(sel){ return el._find(sel, true); }, + _find(sel, all){ + const want=sel.split(',').map(s=>s.trim().replace(/^\./,'')); + const out=[]; + const walk=(node)=>{ for(const c of (node.children||[])){ if(want.some(w=>c._classes&&c._classes.has(w))) out.push(c); walk(c);} }; + walk(el); return out; + }, + }; + return el; +} + +global.document = { createElement: (t)=>makeEl(t), querySelectorAll:()=>[], querySelector:()=>null }; +global.window = {}; +global.CSS = { escape: s=>s }; +global.t = undefined; +global.li = () => ''; +global.esc = s => String(s==null?'':s).replace(/&/g,'&').replace(//g,'>'); +global.toolIcon = () => ''; +global._snippetLooksLikeDiff = () => false; +global._colorDiffLines = s => s; +global._attachCopyButton = () => {}; +global._attachProgressBar = () => {}; +global._wireTransparentHeaderToggle = () => {}; +global._transparentToolDetailHtml = () => '
'; +// _toolDisclosureIdentity pulls in worklog hashing helpers irrelevant to the +// collapsed-preview assertion; stub it (only feeds a data- attribute). +global._toolDisclosureIdentity = () => ''; + +function extractFunc(name){ + const re=new RegExp('function\\s+'+name+'\\s*\\('); + const start=src.search(re); if(start<0) throw new Error(name+' not found'); + let i=src.indexOf('{',start); let depth=1; i++; + while(depth>0&&i