Skip to content

fix(vscode): stop clipping the working status label during a status swap - #14156

Merged
marius-kilocode merged 2 commits into
mainfrom
smooth-vscode-loading-indicator-overlay
Sep 15, 2026
Merged

marius-kilocode merged 2 commits into
mainfrom
smooth-vscode-loading-indicator-overlay

Conversation

@marius-kilocode

@marius-kilocode marius-kilocode commented Sep 15, 2026 •

Copy link
Copy Markdown
Collaborator

What Problem This Solves

When the working indicator swapped a longer status label for a shorter one, the incoming label was rendered with a hard cut and a faded tail. With "Thinking" this read as a cut-off "g" next to a faint fragment, and it stayed that way for the length of the transition.

Why This Change Was Made

StatusText animates a width lock: the box is pinned to the outgoing label's width, released to the incoming one, and the outgoing label stays mounted at zero opacity until that glide lands. Two things were wrong with that.

The truncation mask was driven by scrollWidth > clientWidth on the box. On a shrink the outgoing copy is wider than the box for the entire glide, so the mask switched on and faded the right edge of a label that already fit. It also stayed on for the whole 520ms lock, because releasing the lock does not change the box width once the glide has landed, so the ResizeObserver never rechecked.

The second issue is that the mounted copy kept painting. Its exit animation ends at zero opacity but the both fill held nothing that removed it from the paint, so its tail was still drawn past the shrunken box.

The mask now compares the live label's own width against the box, so a copy that only exists to hold the lock can never trigger it. That check also runs immediately after a swap is measured, since the box is locked at that moment and the observer would not fire. The exit keyframe now ends in visibility: hidden, held by the both fill, so the copy stops painting while it waits for the lock to release. When the indicator has no layout (the dock is display: none between turns) the swap is applied without the width lock or the exit copy, instead of pinning the box to 0px.

User Impact

A status change to a shorter label glides smoothly with the full text visible and no faded fragment beside it. A label that is genuinely wider than the row still fades at the cut while the box opens up for it.

Evidence

Frame-by-frame capture of a shrink swap ("Making edits" to "Thinking…") through the dock story that the visual regression suite already drives, sampled once per animation frame:

Frame sample Before After
data-clip during the 520ms lock on for every frame never set
Live label width vs box width label fits, mask still applied label fits, no mask
Outgoing copy paint visible until the lock released hidden once its exit landed
Box width at rest 59px 59px, identical

The dock story runs unchanged: bun run test:visual -- tests/session-dock-stability.spec.ts passes all 16 tests, including three new ones. Two of the new tests fail on the parent commit and pass here, so they guard the regression rather than restating the implementation. The grow path still enables the fade, and the reduced-motion path still cuts instead of animating.

The extension bundle was rebuilt and loaded in an isolated VS Code instance: the sidebar chat mounts the dock and the status label with no clip attribute and no new console errors.

Limitations

The swap was not reproduced against a live backend turn, because that needs a model request. The frame-level capture and the new tests drive the same component and stylesheets in a browser, and the isolated extension run confirms the built webview mounts them; the remaining gap is a real turn's label sequencing.

@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

Files Reviewed (4 files)
  • .changeset/working-status-swap-clip.md
  • packages/kilo-vscode/webview-ui/src/components/shared/StatusText.tsx
  • packages/kilo-vscode/webview-ui/src/styles/chat-layout.css
  • packages/kilo-vscode/tests/session-dock-stability.spec.ts

Incremental review of e2fb0b7 (test(vscode): cover the status swap fade and the outgoing label exit), which adds only two Playwright cases to session-dock-stability.spec.ts. Both hold up against the current implementation:

  • The clip test walks the story's real status cycle (Thinking… -> Searching the codebase -> Making edits -> Thinking…), so its grow and shrink samples match the widths the effect actually locks. It reads the live .working-status-line:not([data-old]) against clientWidth, which is exactly the comparison StatusText.check() now makes, so it guards the regression instead of restating the implementation. The live <= box + 1 assertion is a real invariant because the box starts at the outgoing width and only narrows toward the incoming one on a shrink.
  • The exit test relies on the working-status-out end keyframe's visibility: hidden (held by the both fill). The exit duration is 240ms * 0.75 = 180ms and the loop samples 24 frames, so the last mounted frame is past the animation end at the refresh rates the visual suite runs at.
  • check() measures the live label instead of box scrollWidth, so the still-mounted outgoing copy can no longer drive the mask during a shrink glide; the incoming label is still masked mid-glide when it is genuinely wider than the locked box.
  • The from === undefined || from === "0px" guard runs after settle(), so the no-layout path leaves no lock, outgoing copy, or data-swap state behind.
  • No new leaks in the touched code: the rAF, timer, and ResizeObserver are each released through settle()/onCleanup, and the added tests only read DOM state.

The production files were unchanged in this increment and were already verified at 1b62b5e.

Previous Review Summary (commit 1b62b5e)

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

Previous review (commit 1b62b5e)

Status: No Issues Found | Recommendation: Merge

Files Reviewed (3 files)
  • .changeset/working-status-swap-clip.md
  • packages/kilo-vscode/webview-ui/src/components/shared/StatusText.tsx
  • packages/kilo-vscode/webview-ui/src/styles/chat-layout.css

Verified the swap logic against the surrounding CSS and the existing session-dock-stability.spec.ts contract:

  • check() now measures the live .working-status-line width instead of box scrollWidth, so the still-mounted outgoing copy can no longer trigger the truncation mask during a shrink glide. The incoming label is still masked mid-glide when it is genuinely wider than the locked box, and the fade still engages in steady state when a label overflows the lane.
  • The from === undefined || from === "0px" guard runs after settle(), so the pending rAF/timer are already cancelled on the no-layout path and no lock, outgoing copy, or data-swap state is left dangling.
  • check() is safe to call from both the swap rAF and the ResizeObserver; it early-returns when refs are unset and the observer cleanup is unchanged. No new leaks (rAF, timer, and observer are all released).
  • visibility: hidden in the working-status-out end keyframe only takes effect at animation end (visibility interpolates as visible while both endpoints are not hidden), so the crossfade is preserved and the reduced-motion display: none path is unaffected. No inline padding on .working-status, so the clientWidth comparison is border-box consistent.

Reviewed by deepseek-v4.1-flash · Input: 45.7K · Output: 8.6K · Cached: 441.3K

Review guidance: REVIEW.md from base branch main

@marius-kilocode
marius-kilocode merged commit 037c108 into main Sep 15, 2026
25 checks passed
@marius-kilocode
marius-kilocode deleted the smooth-vscode-loading-indicator-overlay branch September 15, 2026 09:59
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