feat(assistant): auto-follow transcript, collapse tool spam, one-click continue#162
Conversation
…click continue Three chat UX problems surfaced while driving the assistant: The transcript never auto-scrolled, so a streaming response ran below the fold and the user had to scroll by hand to watch it. Add a bottom-stick auto-follow on the conversation container: it pins to the newest content as tokens stream (a pre-paint layout effect keyed to the streamed length), yet yields the moment the user scrolls up to read and re-arms when they return to the bottom or a new turn/thread starts. A multi-step turn floods the transcript with a card per tool call. Group a SETTLED run of 3+ consecutive tool calls into one "Worked through N steps" disclosure; while streaming nothing is grouped (live activity is what the user wants to watch), and text runs (the answer) always render in full. A turn that hit the per-turn step limit told the user to type "continue". Track the capped state and render a one-click Continue button (the thread keeps full context, so it resumes where it left off), and soften the status copy from "I reached the step limit" to a calmer "Paused after a lot of steps."
🤖 AI Code Review (ensemble)
SummaryThe PR adds auto-follow scrolling, tool-call collapsing for settled turns, and a Continue button for capped turns with good reducer test coverage. However, the tool-collapsing logic has a serious UX bug (hiding failed/actionable tool calls behind a generic summary) and both significant UI behaviors lack rendering/component tests. Given 4 P2s, the PR needs another pass before merge. Issues Found4 total — 0 P1 (blocking) · 4 P2 (should fix) · 0 P3 (nice to have)
|
…er scroll + collapse Addresses the review on the transcript changes: - A settled tool run is no longer collapsed when it contains a failed tool or a proposal awaiting approval, so a failure or a blocked turn can never hide behind a "Worked through N steps" summary and read as successful. - Extract the auto-follow logic into a `useStickToBottom` hook and unit-test the follow / yield-on-scroll-up / re-arm-on-new-turn / re-arm-on-thread-change / disabled contract (jsdom can't compute scroll geometry, so the hook test mocks the element's scrollHeight/clientHeight/scrollTop). - Add SegmentedBody rendering tests: <3 tools render inline, 3+ settled collapse, a streaming run stays inline, and a run with a failed or approval-pending tool is never collapsed. - Document why the Continue button sends the literal "continue": it's the automated form of the instruction the prior capped-turn copy gave the user, and resume works from the thread history the server replays each turn — no special backend handling.
🤖 AI Code Review (ensemble)
SummaryThe PR adds auto-follow transcript scrolling, collapsible tool-run grouping, and a one-click Continue button for capped turns. While the reducer changes and UI affordances are well-tested, the new auto-follow hook has a significant ordering bug that defeats its purpose on the common thread-switch path. Issues Found1 total — 0 P1 (blocking) · 1 P2 (should fix) · 0 P3 (nice to have)
|
…ad switch scrolls The re-arm ran in a passive effect that fires AFTER the scroll's layout effect, so switching threads while scrolled up — which updates threadId and loads the new thread's content in the same render — skipped the scroll (the layout effect saw the stale unstuck ref) and left the user at the old position. Fold the re-arm into the same layout effect, before the stuck check, so the re-arm and scroll are synchronous. Strengthen the turn/thread re-arm tests to change the id and content in one rerender (the real path), which fails on the old ordering.
🤖 AI Code Review (ensemble)
SummaryThe PR adds auto-follow transcript scrolling, collapsible tool call runs, and a one-click Continue button for capped turns. It is well-structured and includes comprehensive test coverage for the new logic. However, there are subtle UI regressions where tool-only transcript updates bypass the auto-follow mechanism, and the scroll hook incorrectly tracks the user's position while disabled, which directly undermines the feature. Issues Found4 total — 0 P1 (blocking) · 3 P2 (should fix) · 1 P3 (nice to have)
|
…'t collapse failed/stuck tools Addresses the re-review: - Auto-follow now reacts to tool-card updates: tool activity rides on separate `tool` messages, so the content signature includes each tool's status — a turn that streams tool cards before any answer text still scrolls. - `onScroll` is ignored while the hook is disabled, so scrolling the (shared) history-view container can't unstick the chat transcript. - `isImportantTool` now also treats a done tool with an `ok:false` outcome (matching ToolCallCard's own failure predicate) and a still-`running` tool in a settled turn (stuck / timed out) as non-collapsible, so neither hides behind the "Worked through N steps" summary. Tests cover both.
🤖 AI Code Review (ensemble)
SummaryThe PR introduces transcript auto-follow, tool spam collapsing, and a one-click Continue button for capped turns. The implementation is robust overall, with comprehensive tests for the new Issues Found3 total — 0 P1 (blocking) · 1 P2 (should fix) · 2 P3 (nice to have)
|
Addresses the re-review: the auto-follow content signature now includes pending proposal identities, so a newly-inserted approval card scrolls into view while pinned; and it guards `m.text?.length` so a malformed message from external thread history can't throw and blank the panel.
🤖 AI Code Review (ensemble)
SummaryThis PR adds auto-follow transcript scrolling, collapsing of consecutive settled tool calls, and a one-click Continue button for capped turns, backed by thorough tests. The implementation is solid overall, but there is a rendering stability issue in the tool collapse logic where historical messages depend on a global streaming flag rather than their own state. Issues Found2 total — 0 P1 (blocking) · 1 P2 (should fix) · 1 P3 (nice to have)
streaming flag causing rendering instability for historical messages |
tangletools
left a comment
There was a problem hiding this comment.
🟢 Value Audit — sound
| Verdict | sound |
| Concerns | 1 (1 low) |
| Heuristic | 0.0s |
| Duplication | 0.1s |
| Interrogation | 136.1s (2 bridge agents) |
| Total | 136.2s |
💰 Value — sound
Adds three coherent chat UX improvements—transcript auto-follow, settled tool-run collapse, and a one-click Continue for capped turns—built in the existing reducer-and-web-react component grain.
- What it does: 1) Pins the assistant transcript scroll to the bottom as content streams, yielding when the user scrolls up and re-arming on return-to-bottom or a new turn/thread. 2) Collapses settled runs of 3+ consecutive tool calls into a 'Worked through N steps' disclosure, while leaving live turns, text answers, failed/stuck tools, and approvals fully visible. 3) Tracks a
cappedstate when a turn hits the - Goals it achieves: Keeps streaming responses in view without manual scrolling, reduces visual spam from multi-step agent turns, and removes the friction of typing 'continue' to resume a capped turn.
- Assessment: Good change. The hook is extracted and unit-tested independent of the DOM, the reducer state change is minimal and tested, the collapse logic correctly excludes failures/running/approval states, and the pre-paint
useLayoutEffecttiming is deliberately chosen to handle thread switches correctly. - Better / existing approach: none — no existing auto-scroll, stick-to-bottom, or tool-collapse capability exists in the repo (
git grepfound only an unrelated reasoning-box scroll at src/web-react/index.tsx:956). The new hook is generic but correctly colocated with its only consumer; moving it to web-react would be speculative until another surface needs it. - Model: opencode/kimi-for-coding/k2p7
- Bridge attempts: 1
🎯 Usefulness — sound
Three coherent, correctly-wired UI polish changes (auto-follow, tool-spam collapse, one-click Continue) that each fill a real gap in the existing assistant transcript with no dead or competing surface.
- Integration: All three are reachable on every applicable render.
cappedis fully wired backend→client.ts:271→reducer.ts:326→AssistantPanel.tsx:525, and the Continue button reuses the composer'schat.sendpath (useAssistantChat.ts:59,275).useStickToBottomis instantiated on the livelogRefcontainer (AssistantPanel.tsx:245) withonScrollattached at :440. The collapse runs inside the existing `Segmen - Fit with existing patterns: Each change extends an established pattern rather than competing with one. The
cappedflag follows the exact shape of siblingAssistantStatefields (usage/error/reasoning). The collapse adds a conditional wrapper to the existing consecutive-tool grouping inSegmentedBody. Auto-follow targets a container (logRef) that had NO prior auto-scroll — confirmed by grep: the only other chat-path `s - Real-world viability: Edge cases are handled.
useStickToBottomuses a pre-paintuseLayoutEffectwith slack-based pin detection (48px), re-arms on new-turn/thread transitions, and ignores scrolls while disabled (history view) — all covered by jsdom tests. The collapse'sisImportantTool(web-react/index.tsx:784) keeps failed/error/running/approval-pending tools visible so a stuck or blocked turn never hides behind - Model: opencode/zai-coding-plan/glm-5.2
- Bridge attempts: 1
🔎 Heuristic Signals
🟡 Cruft: magic number added src/assistant/use-stick-to-bottom.test.ts
+function makeEl(scrollHeight = 1000, clientHeight = 400, scrollTop = 0): MockScrollEl {
What this audit checks
It judges the change on its merits — not whether it was tasked out in an issue. Unticketed, fast-moving work is fine; the question is whether the change is good and whether a better or existing approach should be used instead.
| Pass | What it asks |
|---|---|
| Heuristic | Vague title? Whitespace-only or cruft-bearing diff? (content signals only) |
| Duplication | Do added function/class names already exist elsewhere in the repo? |
| Value Audit | What does it do? What goal does it achieve? Is it good? Better architecture or already-exists? |
| Usefulness Audit | Does it integrate and fit? Will it hold up in real use and actually get used? |
Findings are concerns, not blocks — the human reviewer decides what to do with them.
✅ No Blockers —
|
| opencode-kimi | glm | deepseek | aggregate | |
|---|---|---|---|---|
| Readiness | 79 | 83 | 89 | 79 |
| Confidence | 70 | 70 | 70 | 70 |
| Correctness | 79 | 83 | 89 | 79 |
| Security | 79 | 83 | 89 | 79 |
| Testing | 79 | 83 | 89 | 79 |
| Architecture | 79 | 83 | 89 | 79 |
Reviewer score is advisory once the run is complete and the verdict has no blockers.
Full multi-shot audit completed 2/2 planned shots over 7 changed files. Global verifier still owns final merge decision. | Full multi-shot audit completed 2/2 planned shots over 7 changed files. Global verifier still owns final merge decision. | Full multi-shot audit completed 2/2 planned shots over 7 changed files. Global verifier still owns final merge decision.
🟠 MEDIUM Continue affordance is lost after history restore / page reload — src/assistant/reducer.ts
restore_historyrestoresmessagesandpendingProposalsbut never setscapped. BecauseinitialAssistantState()(used byswitch_threadandhydrate) initializescapped: false, a reloaded thread that ended with a capped turn still shows the status note in messages but no Continue button. Concrete: render a capped turn, reload →state.cappedis false → AssistantPanel line 525 condition is false. Fix: either includecappedin therestore_historypayload from the server, or derive it client-side by scanning the restored messages for the cap note.
🟡 LOW Continue button UI has no component test — src/assistant/AssistantPanel.tsx
The new Continue button (lines 525-553) is a user-facing affordance that gates on
state.capped,state.status === 'idle',!chat.restoring, andview === 'chat', and on click callschat.send('continue').AssistantPanel.test.tsxwas not updated to exercise this branch, so regressions in visibility logic or the send call are not caught. Fix: add a component test that rendersAssistantPanelwithcapped: true, asserts the button appears, clicks it, and verifieschat.sendwas called with 'continue'.
🟡 LOW contentSignature misses proposal retry-error updates — src/assistant/AssistantPanel.tsx
The signature includes
p.callIdfor pending proposals but notp.retryError. Whenproposal_retry_failedadds a retry error to an existing proposal, the card expands to show the error, butcontentSignaturedoes not move and the layout effect will not auto-scroll a pinned view to reveal the new content. Fix: appendp.retryError(or a hash of it) to the proposal segment of the signature.
🟡 LOW Cap note renders inside awaiting_confirm, where 'continue when you're ready' is unreachable — src/assistant/reducer.ts
When a turn is both capped and leaves a pending proposal (reachable: a tool_proposal emitted before the server applies the step limit),
doneappends the status note 'Paused after a lot of steps — continue when you're ready…' AND sets status toawaiting_confirm(reducer.ts:342) because pendingProposals is non-empty. The AssistantPanel Continue button is correctly hidden (it requires status==='idle'), but the note still shows in the transcript telling the user to 'continue' while the composer is disabled and the only valid actions are confirm/cancel. Fix: either suppress the cap note when proposals are pending, or reword it so it doesn't prescribe an action the current status blocks.
🟡 LOW Continue button absent after thread restore/reload — src/assistant/reducer.ts
The
cappedflag is set only by thedonestream event. When a thread is restored viarestore_historyorhydrate, no stream events replay, socappedstaysfalse. The capped-status note persists in messages (and renders), but the one-click Continue button won't appear after reload — the user must type 'continue' manually. This is the intentional session-only design, but the copy on the status note says 'continue when you're ready' which implies the button should be there. Consider either: (a) including acappedflag in therestore_historypayload so the button re-appears, or (b) adjusting the status-note copy on reload to not suggest a one-click path exists.
🟡 LOW capped flag and cap status note are session-only; reload drops the Continue affordance and the paused indicator — src/assistant/reducer.ts
cappedlives only in reducer state —loadThread(useAssistantChat.ts:82) persists/loads just threadId, andrestore_history(reducer.ts:484-515) never setscapped, so after a reload the Continue button never renders even if the last turn was capped. The cap status note (idcap-${turnId}, reducer.ts:331) is client-authored, not a server message, sorestore_historydoesn't bring it back either — meaning a reloaded capped turn shows the partial reply with zero indication it was paused, reading as complete (the exact misread the note exists to prevent). Not a regression vs. pre-PR (there was no Continue button before), but the feature's value is limited to the live session. Worth either persistingcapped/deriving it from server transcript metadata, or accepting the limitation expl
🟡 LOW Missing test: approved proposal within settled run should collapse — src/web-react/chat-messages-segments.test.tsx
The test for pending-approval-prevention (line 292) proves that queued_for_approval blocks collapse. The first collapse test (line 226) proves 3+ done generic tools collapse. But no test verifies that an approved/acted-upon proposal (status != 'queued_for_approval') within a 3+ settled run collapses as expected. The code path is correct — pendingApprovalOf returns null for non-pending statuses, so isImportantTool returns false — but covering this variant explicitly would strengthen the contract.
🟡 LOW No test for multiple independent collapsible runs in one message — src/web-react/chat-messages-segments.test.tsx
The grouping logic creates a separate group per maximal run of consecutive tools, and each group is independently collapse-eligible. No test exercises a message like [text, tool×3, text, tool×3] to confirm two independent disclosures render with correct counts. The grouping loop (lines 843-853) is straightforward and single-run cases are covered, so risk is low, but a multi-group test would lock in the per-group independence.
🟡 LOW Successful schedule_followup / render_ui cards collapse inside a 3+ generic-tool run — src/web-react/index.tsx
isImportantTool only flags error/running/ok:false/pendingApproval. A successfully-completed schedule_followup (renders as a distinct FollowupCard at line 661) or render_ui inside a 3+ run of generic tools will be folded behind 'Worked through N steps'. The user must expand the disclosure to discover a scheduled follow-up or rendered view. Defensible by design (these are completed actions), but a scheduled follow-up is arguably a user-actionable artifact worth surfacing. If the product wants these always-visible, add
|| blockKindOf(call) === 'followup'to isImportantTool. No correctness bug — content is accessible on expand.
tangletools · 2026-07-02T00:21:33Z · trace
tangletools
left a comment
There was a problem hiding this comment.
✅ Approved — 9 non-blocking findings — b5f02817
Full multi-shot audit completed 2/2 planned shots over 7 changed files. Global verifier still owns final merge decision. | Full multi-shot audit completed 2/2 planned shots over 7 changed files. Global verifier still owns final merge decision. | Full multi-shot audit completed 2/2 planned shots over 7 changed files. Global verifier still owns final merge decision.
Full immutable report for this review: trace
Summary comment for this run: full summary
tangletools · 2026-07-02T00:21:33Z · immutable trace
The chat-UI half of a batch of fixes found while testing workflows on the Tangle platform (the platform-side changes are in the
agent-dev-containerrepo). All three are in the assistant transcript.Auto-follow the transcript
There was no auto-scroll on the conversation container at all, so a streaming response ran below the fold and the user had to scroll by hand to watch it. Added a bottom-stick auto-follow: it pins to the newest content as tokens stream (a pre-paint
useLayoutEffectkeyed to the streamed length, so content never flashes above the fold), yet yields the moment the user scrolls up to read and re-arms when they return to the bottom or a new turn/thread starts.Collapse tool-call spam
A multi-step turn floods the transcript with a card per tool call. Now a settled run of 3+ consecutive tool calls collapses into one "Worked through N steps" disclosure. While a turn is streaming nothing is grouped (live activity is what the user wants to watch), and text runs (the answer) always render in full.
One-click Continue
A turn that hit the per-turn step limit told the user to type "continue" with no affordance. Now the capped state is tracked and a Continue button renders (the thread keeps full context, so it resumes where it left off), and the status copy is softened from "I reached the step limit for this turn" to a calmer "Paused after a lot of steps."
Tests
Updated reducer tests for the
cappedflag (set on a capped turn, cleared when the next turn starts) and the softened copy. Full assistant + web-react suites green (243 tests); changed files typecheck clean.Verified locally
Linked into the platform web app and driven with deepseek: the transcript followed the streaming response to the bottom and yielded correctly when scrolled up mid-stream; a multi-step authoring turn rendered cleanly.