Skip to content

fix: stop transcript jumps while subagent cards stream - #14166

Merged
marius-kilocode merged 6 commits into
mainfrom
fix-subagent-reasoning-ui-flicker
Sep 15, 2026
Merged

marius-kilocode merged 6 commits into
mainfrom
fix-subagent-reasoning-ui-flicker

Conversation

@marius-kilocode

Copy link
Copy Markdown
Collaborator

What Problem This Solves

While subagents run, the chat transcript flickered up and down. The jump happened the moment the parent started its next assistant step (after subagents returned, or between tool rounds), and again when the turn completed.

Why This Change Was Made

Instrumenting a live three-subagent run in an isolated VS Code instance (sidebar and Agent Manager share the same MessageList and task-card renderer) with per-frame scrollTop, scrollHeight, and per-element height observers showed two deterministic layout collapses, both from rows remounting:

  1. Task cards collapsed on remount. partitionRows hands the previous assistant message from the live tail to Virtua when a new step starts. The cards remounted there, but their auto-open was a local setOpen(true), so initialOpen() found nothing stored and auto() was already false for the completed task. Three 260px cards came back collapsed: scroll height 1260px to 557px and scrollTop 703 to 0 in one frame. The transcript then regrew as the next message streamed.
  2. Every row of the turn remounted at completion. transcriptRows rebuilds row objects when turn meta changes (live flips, copy anchor). Virtua's <For> keys by object identity, so all rows remounted at the 260px estimate until measured: a 708px shrink followed by a 708px regrow.

A third, smaller transient remained after restoring the open state: BasicTool defer painted a remounted card at header height for one frame (28px to 260px) before its body landed, which still moved the pinned scroll position.

Changes

  • Persist the task card auto-open state (rememberOpen) so it survives a remount instead of being re-derived from the now-false auto().
  • Mount a known-open card body synchronously on remount (defer={!remount}), so the first painted frame is already at full height.
  • Key the virtualizer by stable row key and resolve the row reactively, matching the pattern the direct tail already used, so completion meta changes update rows in place instead of remounting them.

User Impact

The transcript keeps its scroll position while subagents run, and task cards stay expanded across assistant steps. No more up and down bouncing when the parent resumes after a subagent returns.

Evidence

Measured with a per-frame recorder in isolated VS Code, same prompt fanning out three explore subagents, before and after:

Event Before After
Handoff at next assistant step scrollHeight -703px, scrollTop 703 to 0 no scroll change
Turn completion scrollHeight -642px then +644px no scroll change
Second handoff scrollHeight -708px then +708px no scroll change
Task card heights at handoff 260px to 0, then remeasured stay at 260px

bun run typecheck, bun run lint, bun run knip, bun run check-kilocode-change, and bun run test:unit (5713 pass) in packages/kilo-vscode/.

Caveat

In one of four runs a single card body shrank for one frame when its child finished, as if the result markdown briefly replaced the child list. It did not recur with finer instrumentation and is left for follow-up.

Virtualizer handoff rebuilt every row of a turn when the turn completed,
remounting the rows at the 260px estimate before they were measured, and
task cards returned collapsed because their auto-open state was local to
the mount. Persist the card auto-open state, mount a known-open body
synchronously, and key the virtualizer by row key instead of row identity.

Measured on a three-subagent run: scroll height dropped 703px and
scrollTop snapped to 0 at the handoff, then regrew. The same run no
longer moves the scroll position at the handoff.

The transcript now keeps its position and the task cards stay expanded
while subagents run.
@kilo-code-bot

kilo-code-bot Bot commented Sep 15, 2026 •

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Incremental review of 5ceef03..ce6d778 (commit ce6d778). The only changed file is packages/kilo-vscode/tests/session-dock-stability.spec.ts. It drops the exact first-frame assertion (|frames[0].left - start| <= 1) and replaces it with assertions on the shape of the glide: total travel is non-trivial (glide > 1), the first sampled frame has covered less than half of it, and the last sampled frame is still short of the settled position. The removed assertion assumed the observer's first frame is the swap frame, which is what drifted between machines; the replacements still fail on a teleport while tolerating a frame or two of sampling drift. The six-frame sample (~100ms) sits well inside the 420ms --working-status-glide (chat-layout.css:256), and settled is measured after data-swap clears, so the comparisons are against the true resting position.

Files Reviewed (1 file)
  • packages/kilo-vscode/tests/session-dock-stability.spec.ts
Previous Review Summaries (4 snapshots, latest commit 5ceef03)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit 5ceef03)

Status: No Issues Found | Recommendation: Merge

Incremental review of b98ed5cf..5ceef03 (commit 5ceef03). The new commit extracts the remount open-state decision into taskStoredOpen(auto, background, touched) in task-tool-state.ts and calls it from the effect in TaskToolExpanded.tsx:128-133. The extracted helper is behaviorally identical to the inline branch it replaces (touched -> leave alone, auto -> persist open, background -> persist collapsed, otherwise leave alone), so the promoted-background collapse still persists false and the auto-open still persists true across a virtualizer remount.

The reworked hydration tests now drive the extracted production helper (taskStoredOpen) for both the auto-open and promoted-background scenarios and add a case covering the untouched/settled paths, so the decision logic under test is the real implementation rather than a restatement of the store contract. The persistence call itself is still exercised via writeToolOpen/readToolOpen; rendering TaskToolExpanded to assert rememberOpen is not practical in this unit suite.

The store writes remain bounded by the existing 2000-entry FIFO in tool-open-state.ts:24-27, so the extra false writes introduce no unbounded growth. No new findings on the changed lines.

Files Reviewed (3 files)
  • packages/kilo-vscode/tests/unit/task-tool-hydration.test.ts
  • packages/kilo-vscode/webview-ui/src/components/chat/TaskToolExpanded.tsx
  • packages/kilo-vscode/webview-ui/src/components/chat/task-tool-state.ts

Previous review (commit b98ed5c)

Status: No Issues Found | Recommendation: Merge

Incremental review of e4f66afa..b98ed5cf (commits 7fa6daba, b98ed5cf). All three findings from the previous review are addressed in current HEAD:

  • The promoted-background collapse now persists rememberOpen(..., false) alongside setOpen(false), so a remount reads false instead of the stale true (TaskToolExpanded.tsx:136-139).
  • The defer decision is now stored !== true, so a stored-closed card keeps its deferred body instead of eagerly building it (TaskToolExpanded.tsx:294).
  • The hydration test now drives rememberOpen rather than writeToolOpen (task-tool-hydration.test.ts:57).

The shared Row renderer is prop-for-prop equivalent to the two call sites it replaces; it resolves the row through the same virtual()/lookup() memos, so reactivity and remount behavior are unchanged.

Files Reviewed (3 files)
  • packages/kilo-vscode/tests/unit/task-tool-hydration.test.ts
  • packages/kilo-vscode/webview-ui/src/components/chat/MessageList.tsx
  • packages/kilo-vscode/webview-ui/src/components/chat/TaskToolExpanded.tsx

Previous review (commit e4f66af)

Status: 3 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 2
Issue Details (click to expand)

WARNING

File Line Issue
packages/kilo-vscode/webview-ui/src/components/chat/TaskToolExpanded.tsx 130 Auto-open is persisted but the background-task collapse is not, so a card promoted to background re-expands on remount, contradicting the "background cards stay collapsed" invariant.

SUGGESTION

File Line Issue
packages/kilo-vscode/webview-ui/src/components/chat/TaskToolExpanded.tsx 288 remount is true for any stored value, including a stored false, so defer={!remount} makes BasicTool eagerly build a collapsed body.
packages/kilo-vscode/tests/unit/task-tool-hydration.test.ts 57 The test calls writeToolOpen directly instead of rememberOpen, so it does not exercise the production code path it claims to cover.
Files Reviewed (6 files)
  • .changeset/steady-subagent-cards.md - no issues
  • packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/labs-tool-call-lab/search-previews-chromium-linux.png - no issues (generated LFS baseline only)
  • packages/kilo-ui/src/components/basic-tool.tsx - no issues
  • packages/kilo-vscode/tests/unit/task-tool-hydration.test.ts - 1 suggestion
  • packages/kilo-vscode/webview-ui/src/components/chat/MessageList.tsx - no issues
  • packages/kilo-vscode/webview-ui/src/components/chat/TaskToolExpanded.tsx - 1 warning, 1 suggestion

No new changed code since the previous review (b4df88d): the only incremental commit (e4f66afa) refreshes the visual-regression baseline, and the findings above were re-verified against current HEAD as still reproducible.

Fix these issues in Kilo Cloud

Previous review (commit b4df88d)

Status: 3 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 2
Issue Details (click to expand)

WARNING

File Line Issue
packages/kilo-vscode/webview-ui/src/components/chat/TaskToolExpanded.tsx 130 Auto-open is persisted but the background-task collapse is not, so a card promoted to background re-expands on remount, contradicting the "background cards stay collapsed" invariant.

SUGGESTION

File Line Issue
packages/kilo-vscode/webview-ui/src/components/chat/TaskToolExpanded.tsx 288 remount is true for any stored value, including a stored false, so defer={!remount} makes BasicTool eagerly build a collapsed body.
packages/kilo-vscode/tests/unit/task-tool-hydration.test.ts 57 The new test calls writeToolOpen directly instead of rememberOpen, so it does not exercise the production code path it claims to cover.
Files Reviewed (5 files)
  • .changeset/steady-subagent-cards.md - no issues
  • packages/kilo-ui/src/components/basic-tool.tsx - no issues
  • packages/kilo-vscode/tests/unit/task-tool-hydration.test.ts - 1 suggestion
  • packages/kilo-vscode/webview-ui/src/components/chat/MessageList.tsx - no issues
  • packages/kilo-vscode/webview-ui/src/components/chat/TaskToolExpanded.tsx - 1 warning, 1 suggestion

Notes: the virtualizer change (data={keys()} with a reactive virtual().get(key) lookup) matches the key pattern the direct tail already uses, and row keys are unique per transcript-rows.ts, so the non-null lookup is consistent with existing behavior. The new rememberOpen writes go to the existing bounded tool-open-state map (2000 entries, FIFO eviction), so no new memory leak is introduced.

Fix these issues in Kilo Cloud


Reviewed by deepseek-v4.1-flash · Input: 36.9K · Output: 9.2K · Cached: 249.3K

Review guidance: REVIEW.md from base branch main

kilo-maintainer Bot and others added 4 commits September 15, 2026 12:52
The virtualizer child and the live tail rendered the same TranscriptRowView
props, which the duplication check flagged once both read the row through a
key lookup. Extract a single Row component so the two paths cannot drift.
Record the collapse when a foreground card is promoted to a background
task, so a later remount does not read the earlier open value and expand a
card that must stay collapsed. Defer the body only when the stored state is
open, so a card the user collapsed still mounts its body lazily.
A kilo-vscode unit test cannot import the kilo-ui component module: that
file has no jsxImportSource pragma and relies on kilo-ui's tsconfig, so Bun
resolved react/jsx-dev-runtime for it and the test run failed on CI without
React installed.

Move the persist decision into a dependency-free helper in task-tool-state
and cover it there, along with the promoted background collapse.
The swap test required the spinner to still be at its starting position in
the first frame it could measure. That frame is not guaranteed to be the
frame of the swap: it drifts by a frame or two between machines, and each
frame of drift is one step of the width glide, so the test failed on CI with
a 7px move that is only the animation progressing.

Assert the shape of the motion instead. The glide covers a fraction of the
distance in the first observed frame and is still travelling when the
sampled frames end, while a teleport covers all of it at once. Verified that
the new assertions still fail when the glide is made near-instant.
@marius-kilocode
marius-kilocode merged commit 0333605 into main Sep 15, 2026
36 checks passed
@marius-kilocode
marius-kilocode deleted the fix-subagent-reasoning-ui-flicker branch September 15, 2026 14:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants