|
1542 | 1542 | const files = (list) => list.length > 2 ? (list.length + ' files') : list.map(short).join(', '); |
1543 | 1543 | const of = (kind) => uniq(steps.filter((s) => s.kind === kind && s.path).map((s) => s.path)); |
1544 | 1544 | const cmds = steps.filter((s) => s.kind === 'cmd').length; |
1545 | | - const searches = steps.filter((s) => s.kind === 'search' || s.kind === 'list').length; |
| 1545 | + const searches = steps.filter((s) => s.kind === 'search').length; |
| 1546 | + const lists = steps.filter((s) => s.kind === 'list').length; // listing files ≠ searching content |
1546 | 1547 | const reads = of('read'), edits = of('edit'), creates = of('create'), dels = of('delete'); |
1547 | 1548 | // File work leads, commands trail ("Read and edited extension.js, ran a command") — the reference |
1548 | 1549 | // ordering: what changed matters more than how many shells it took. |
|
1558 | 1559 | if (creates.length){ parts.push('created ' + files(creates)); } |
1559 | 1560 | if (dels.length){ parts.push('deleted ' + files(dels)); } |
1560 | 1561 | if (searches){ parts.push('searched the workspace'); } |
| 1562 | + if (lists){ parts.push('listed files'); } |
1561 | 1563 | if (cmds){ parts.push('ran ' + (cmds === 1 ? 'a command' : cmds + ' commands')); } |
1562 | 1564 | if (steps.some((s) => s.kind === 'verify')){ parts.push('verified the edits'); } |
1563 | 1565 | if (!parts.length){ return steps.length === 1 ? '1 step' : steps.length + ' steps'; } |
|
1634 | 1636 | function groupAppend(el, step){ |
1635 | 1637 | if (!groupsOn){ log.appendChild(el); return; } |
1636 | 1638 | const g = curGroup || openGroup(); |
1637 | | - collapseMember(el); |
| 1639 | + // A RUNNING member stays expanded so its live output is readable — that is the whole point of |
| 1640 | + // the fixed-footprint header. Finished/instantaneous members (chips, an already-exited command) |
| 1641 | + // fold to a one-line row now; a running one folds later, in groupStepDone, once it completes. |
| 1642 | + if (!step || step.status !== 'running'){ collapseMember(el); } |
1638 | 1643 | g.body.appendChild(el); |
1639 | 1644 | if (step){ step.group = g; g.steps.push(step); refreshGroupHead(); } |
1640 | 1645 | } |
|
1665 | 1670 | g.el.remove(); return; |
1666 | 1671 | } |
1667 | 1672 | g.closed = true; |
| 1673 | + // A member may still be running when narration closes the group (a background command). Don't |
| 1674 | + // claim the group is finished or hide the Stop control — keep the live chrome and let that |
| 1675 | + // member's own groupStepDone re-enter here to complete the group once it actually exits. |
| 1676 | + if (g.steps.some((s) => s.status === 'running')){ |
| 1677 | + g.el.classList.remove('collapsed'); // never hide a command that is still going |
| 1678 | + refreshGroupHead(); |
| 1679 | + return; |
| 1680 | + } |
1668 | 1681 | g.el.classList.remove('running'); |
1669 | 1682 | g.el.classList.add(g.failed ? 'gfailed' : 'gok'); // recolours the rail node — see .tl-group.gok |
1670 | 1683 | g.label.textContent = groupAggregate(g.steps); |
|
1696 | 1709 | // if the user had collapsed it — a hidden failure is the one thing this UI must never do. |
1697 | 1710 | if (step.card && step.card.classList){ step.card.classList.remove('collapsed'); } |
1698 | 1711 | if (g.el.isConnected){ g.el.classList.remove('collapsed'); } |
| 1712 | + } else { |
| 1713 | + // Success: the step is done, so it folds to a one-line row (it stayed expanded while running). |
| 1714 | + collapseMember(step.card); |
1699 | 1715 | } |
1700 | 1716 | if (g.closed){ finalizeGroup(g); } else if (g === curGroup){ refreshGroupHead(); } |
1701 | 1717 | } |
|
1715 | 1731 | // tool text stays as the tooltip so the underlying path/query is never lost. |
1716 | 1732 | const shown = (groupsOn && st.base) ? st.base : String(text || ''); |
1717 | 1733 | d.innerHTML = '<div class="tl-rail"><span class="tl-node">' + (IC[icon] ? codicon(icon) : esc(icon || '•')) + '</span></div>' |
1718 | | - + '<div class="tl-body"><span class="toolt" title="' + esc(text || '') + '">' + esc(shown) + '</span></div>'; |
| 1734 | + + '<div class="tl-body"><span class="toolt" title="' + escAttr(text || '') + '">' + esc(shown) + '</span></div>'; |
1719 | 1735 | groupAppend(d, st); |
1720 | 1736 | scrollIfStuck(); |
1721 | 1737 | } |
|
1901 | 1917 | scrollIfStuck(); |
1902 | 1918 | } |
1903 | 1919 | function termExitFinish(m){ |
1904 | | - const card = termCards[m.id]; if (!card) { return; } |
1905 | | - const st = card.querySelector('.termstatus'); if (!st) { return; } |
| 1920 | + // Even if the card is gone (checkpoint restore / prune), still settle the group step and drop |
| 1921 | + // the step map entry — otherwise termSteps leaks and holds a stale group reference all session. |
| 1922 | + const card = termCards[m.id]; |
| 1923 | + if (!card || !card.querySelector('.termstatus')){ |
| 1924 | + if (termSteps[m.id]){ groupStepDone(termSteps[m.id], m.how === 'timeout' || (m.how !== 'stopped' && m.code !== 0)); delete termSteps[m.id]; } |
| 1925 | + delete termCards[m.id]; |
| 1926 | + return; |
| 1927 | + } |
| 1928 | + const st = card.querySelector('.termstatus'); |
1906 | 1929 | const dur = (typeof m.ms === 'number') ? (m.ms >= 1000 ? (m.ms / 1000).toFixed(1) + 's' : m.ms + 'ms') : ''; |
1907 | 1930 | let icon, text, cls; |
1908 | 1931 | if (m.how === 'stopped'){ icon = 'circle-slash'; text = 'stopped'; cls = 'bad'; } |
|
2113 | 2136 | function addEditCard(m){ |
2114 | 2137 | clearEmpty(); clearStatus(); agentBubble = null; |
2115 | 2138 | let card = editCards[m.id]; |
2116 | | - let wasOpen = true; |
| 2139 | + // A NEW edit card inside a group starts as a one-line row (diff closed); ungrouped it keeps |
| 2140 | + // today's open-diff default. (collapseMember can't close the diff at groupAppend time because the |
| 2141 | + // card's innerHTML isn't built yet, so the default has to be right here.) A re-render preserves |
| 2142 | + // whatever open/closed state the user last left it in. |
| 2143 | + let wasOpen = !groupsOn; |
2117 | 2144 | if (!card){ |
2118 | 2145 | card = document.createElement('div'); card.className = 'editcard'; |
2119 | 2146 | editCards[m.id] = card; |
|
2174 | 2201 | const det = card.querySelector('details.diffwrap'); if (det) { det.open = false; } // collapse so resolved cards don't stack tall |
2175 | 2202 | card.classList.add('resolved'); |
2176 | 2203 | delete editCards[m.id]; |
| 2204 | + delete editSteps[m.id]; // release the group step ref alongside the card (no unbounded growth) |
2177 | 2205 | } |
2178 | 2206 | // ---- context-window usage: always-visible footer stat + a warning bar when it fills ---- |
2179 | 2207 | const ctxBar = document.getElementById('ctxBar'); |
|
2826 | 2854 | if (typeof m.contextLimit === 'number'){ renderContext({ limit: m.contextLimit }); } |
2827 | 2855 | lastProvider = m.provider || ''; |
2828 | 2856 | lastModelId = m.modelId || m.model || ''; // gateway sends modelId (the real id); BYOK model IS the id |
2829 | | - if (typeof m.groupActivity === 'boolean'){ groupsOn = m.groupActivity; } // calm-transcript toggle |
| 2857 | + // calm-transcript toggle. Turning it OFF mid-run closes any open group first, so the timeline |
| 2858 | + // returns to fully flat immediately instead of leaving one stray group behind. |
| 2859 | + if (typeof m.groupActivity === 'boolean'){ if (!m.groupActivity){ closeGroup(); } groupsOn = m.groupActivity; } |
2830 | 2860 | renderRouting(); |
2831 | 2861 | } |
2832 | 2862 | else if (m.type === 'assistantStart'){ shown = ''; pending = ''; doneSignaled = false; flushAll = false; current = makeStream(add('assistant', '')); setStreaming(true); } |
|
2885 | 2915 | else if (m.type === 'reset'){ |
2886 | 2916 | log.innerHTML = '<div id="empty"><div class="big">New chat</div>Ask about your code, or describe what to build.</div>'; |
2887 | 2917 | selLabel = null; ctxFiles = []; renderChips(); |
2888 | | - for (const k in editCards) { delete editCards[k]; } for (const k in termCards) { delete termCards[k]; } for (const k in verifyCards) { delete verifyCards[k]; } for (const k in bgTasks) { delete bgTasks[k]; } renderBgTasks(); updateReviewBar(0); renderPlan([]); renderContext({ input: 0 }); |
| 2918 | + for (const k in editCards) { delete editCards[k]; } for (const k in termCards) { delete termCards[k]; } for (const k in verifyCards) { delete verifyCards[k]; } for (const k in bgTasks) { delete bgTasks[k]; } |
| 2919 | + for (const k in editSteps) { delete editSteps[k]; } for (const k in termSteps) { delete termSteps[k]; } for (const k in verifySteps) { delete verifySteps[k]; } // step maps ride with their cards |
| 2920 | + renderBgTasks(); updateReviewBar(0); renderPlan([]); renderContext({ input: 0 }); |
2889 | 2921 | pending = ''; shown = ''; doneSignaled = false; flushAll = false; current = null; setStreaming(false); |
2890 | 2922 | forceStick(); |
2891 | 2923 | } |
|
0 commit comments