Skip to content
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
78 commits
Select commit Hold shift + click to select a range
35b7051
fix(ui): bottom-align short VP content so blank space is at top (#9300)
qwen-code-dev-bot Aug 17, 2026
e4ab668
fix(ui): align VP design comment and tests with bottom-aligned layout…
qwen-code-dev-bot Aug 17, 2026
53c0178
Merge branch 'main' into fix/vp-bottom-align-9300
qwen-code-dev-bot Aug 18, 2026
c26ef42
Merge branch 'main' into fix/vp-bottom-align-9300
qwen-code-dev-bot Aug 19, 2026
a5403de
fix(ui): stop re-stick from bottom-aligning top-anchored VP lists (#9…
qwen-code-dev-bot Aug 20, 2026
23988d6
fix(ui): stop scroll and shrink paths from re-engaging VP sticking (#…
qwen-code-dev-bot Aug 20, 2026
d3abb95
Merge branch 'main' into fix/vp-bottom-align-9300
qwen-code-dev-bot Aug 20, 2026
dd0c773
Merge remote-tracking branch 'origin/main' into fix/vp-bottom-align-9300
qwen-code-dev-bot Aug 20, 2026
05c073f
fix(ui): keep clamp-parked VP positions from re-engaging sticking (#9…
qwen-code-dev-bot Aug 20, 2026
5e38caf
fix(ui): install the VP clamp mark only when the clamp moved the anch…
qwen-code-dev-bot Aug 20, 2026
047021a
fix(ui): let VP auto-follow re-engage after scrolled-away fitting-ban…
qwen-code-dev-bot Aug 20, 2026
c68e20b
Merge branch 'main' into fix/vp-bottom-align-9300
qwen-code-dev-bot Aug 20, 2026
fa7f5c9
fix(ui): close resize edges that defeat the VP sticking drop and clam…
qwen-code-dev-bot Aug 20, 2026
9cacb01
Merge branch 'main' into fix/vp-bottom-align-9300
qwen-code-dev-bot Aug 21, 2026
c7e46dc
fix(ui): re-engage VP follow on any fit-boundary crossing, not just l…
qwen-code-dev-bot Aug 21, 2026
754d17b
Merge branch 'main' into fix/vp-bottom-align-9300
qwen-code-dev-bot Aug 21, 2026
ee6f77e
Merge branch 'main' into fix/vp-bottom-align-9300
qwen-code-dev-bot Aug 22, 2026
839a873
fix(ui): keep VP follow alive across dataset swaps and banner-only ov…
qwen-code-dev-bot Aug 22, 2026
6c1ada4
fix(ui): reset VP scroll state on session swap, heal park-mark follow…
qwen-code-dev-bot Aug 22, 2026
9b1afb2
Merge branch 'main' into fix/vp-bottom-align-9300
qwen-code-dev-bot Aug 22, 2026
cf04b3f
Merge branch 'main' into fix/vp-bottom-align-9300
qwen-code-dev-bot Aug 23, 2026
d530b3d
Merge branch 'main' into fix/vp-bottom-align-9300
qwen-code-dev-bot Aug 24, 2026
be2358f
Merge branch 'main' into fix/vp-bottom-align-9300
qwen-code-dev-bot Aug 24, 2026
9361c28
Merge branch 'main' into fix/vp-bottom-align-9300
qwen-code-dev-bot Aug 25, 2026
4960a7e
Merge branch 'main' into fix/vp-bottom-align-9300
qwen-code-dev-bot Aug 25, 2026
d0ce0bb
Merge branch 'main' into fix/vp-bottom-align-9300
qwen-code-dev-bot Aug 26, 2026
d21e8f5
Merge branch 'main' into fix/vp-bottom-align-9300
qwen-code-dev-bot Aug 27, 2026
a95525e
Merge branch 'main' into fix/vp-bottom-align-9300
qwen-code-dev-bot Aug 28, 2026
0e5a87e
Merge branch 'main' into fix/vp-bottom-align-9300
qwen-code-dev-bot Aug 29, 2026
efd5ca1
Merge branch 'main' into fix/vp-bottom-align-9300
qwen-code-dev-bot Aug 29, 2026
5599fb9
Merge branch 'main' into fix/vp-bottom-align-9300
qwen-code-dev-bot Aug 30, 2026
bc395c4
Merge branch 'main' into fix/vp-bottom-align-9300
qwen-code-dev-bot Aug 31, 2026
88d4d10
Merge branch 'main' into fix/vp-bottom-align-9300
qwen-code-dev-bot Sep 1, 2026
3047e02
Merge branch 'main' into fix/vp-bottom-align-9300
qwen-code-dev-bot Sep 1, 2026
0bc876f
Merge branch 'main' into fix/vp-bottom-align-9300
qwen-code-dev-bot Sep 2, 2026
185e3b3
Merge branch 'main' into fix/vp-bottom-align-9300
qwen-code-dev-bot Sep 2, 2026
cf43c2c
Merge branch 'main' into fix/vp-bottom-align-9300
qwen-code-dev-bot Sep 3, 2026
98b7ddd
Merge branch 'main' into fix/vp-bottom-align-9300
qwen-code-dev-bot Sep 3, 2026
3904c6d
Merge branch 'main' into fix/vp-bottom-align-9300
qwen-code-dev-bot Sep 4, 2026
57afd05
Merge branch 'main' into fix/vp-bottom-align-9300
qwen-code-dev-bot Sep 5, 2026
6705926
Merge branch 'main' into fix/vp-bottom-align-9300
qwen-code-dev-bot Sep 6, 2026
a03f6ca
Merge branch 'main' into fix/vp-bottom-align-9300
qwen-code-dev-bot Sep 6, 2026
45f59b6
Merge branch 'main' into fix/vp-bottom-align-9300
qwen-code-dev-bot Sep 7, 2026
829564b
Merge branch 'main' into fix/vp-bottom-align-9300
qwen-code-dev-bot Sep 8, 2026
a1e6801
Merge branch 'main' into fix/vp-bottom-align-9300
qwen-code-dev-bot Sep 9, 2026
e5dd22d
Merge branch 'main' into fix/vp-bottom-align-9300
qwen-code-dev-bot Sep 9, 2026
31fcd02
Merge branch 'main' into fix/vp-bottom-align-9300
qwen-code-dev-bot Sep 10, 2026
6796923
Merge branch 'main' into fix/vp-bottom-align-9300
qwen-code-dev-bot Sep 10, 2026
38559cb
Merge branch 'main' into fix/vp-bottom-align-9300
qwen-code-dev-bot Sep 11, 2026
a6cf115
Merge branch 'main' into fix/vp-bottom-align-9300
qwen-code-dev-bot Sep 11, 2026
ff8da3b
Merge branch 'main' into fix/vp-bottom-align-9300
qwen-code-dev-bot Sep 12, 2026
f648122
Merge branch 'main' into fix/vp-bottom-align-9300
qwen-code-dev-bot Sep 13, 2026
633924d
Merge branch 'main' into fix/vp-bottom-align-9300
qwen-code-dev-bot Sep 14, 2026
c402ce2
Merge branch 'main' into fix/vp-bottom-align-9300
qwen-code-dev-bot Sep 15, 2026
969ff98
Merge branch 'main' into fix/vp-bottom-align-9300
qwen-code-dev-bot Sep 16, 2026
59d87dc
Merge branch 'main' into fix/vp-bottom-align-9300
qwen-code-dev-bot Sep 17, 2026
cfd844b
Merge branch 'main' into fix/vp-bottom-align-9300
qwen-code-dev-bot Sep 18, 2026
ff0b7b8
Merge branch 'main' into fix/vp-bottom-align-9300
qwen-code-dev-bot Sep 18, 2026
b9d383c
Merge branch 'main' into fix/vp-bottom-align-9300
qwen-code-dev-bot Sep 19, 2026
901dd7f
Merge branch 'main' into fix/vp-bottom-align-9300
qwen-code-dev-bot Sep 20, 2026
9d42fcb
Merge branch 'main' into fix/vp-bottom-align-9300
qwen-code-dev-bot Sep 21, 2026
8937043
Merge branch 'main' into fix/vp-bottom-align-9300
yiliang114 Sep 21, 2026
d3ee441
Merge branch 'main' into fix/vp-bottom-align-9300
qwen-code-dev-bot Sep 22, 2026
37b64ba
Merge branch 'main' into fix/vp-bottom-align-9300
qwen-code-dev-bot Sep 22, 2026
bc08734
Merge branch 'main' into fix/vp-bottom-align-9300
qwen-code-dev-bot Sep 23, 2026
075ad5a
Merge branch 'main' into fix/vp-bottom-align-9300
yiliang114 Sep 23, 2026
f551746
Merge branch 'main' into fix/vp-bottom-align-9300
yiliang114 Sep 23, 2026
6d99a5a
Merge branch 'main' into fix/vp-bottom-align-9300
qwen-code-dev-bot Sep 24, 2026
f8eab70
Merge branch 'main' into fix/vp-bottom-align-9300
qwen-code-dev-bot Sep 24, 2026
927df02
Merge branch 'main' into fix/vp-bottom-align-9300
qwen-code-dev-bot Sep 25, 2026
baa9484
Merge branch 'main' into fix/vp-bottom-align-9300
qwen-code-dev-bot Sep 26, 2026
4246def
Merge branch 'main' into fix/vp-bottom-align-9300
qwen-code-dev-bot Sep 26, 2026
0dcf344
Merge branch 'main' into fix/vp-bottom-align-9300
qwen-code-dev-bot Sep 27, 2026
175108e
Merge branch 'main' into fix/vp-bottom-align-9300
qwen-code-dev-bot Sep 28, 2026
3fcc6b5
Merge branch 'main' into fix/vp-bottom-align-9300
qwen-code-dev-bot Sep 28, 2026
17e3f30
Merge branch 'main' into fix/vp-bottom-align-9300
qwen-code-dev-bot Sep 29, 2026
8dffe4a
Merge branch 'main' into fix/vp-bottom-align-9300
qwen-code-dev-bot Sep 30, 2026
1f83404
Merge branch 'main' into fix/vp-bottom-align-9300
qwen-code-dev-bot Oct 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 25 additions & 6 deletions packages/cli/src/ui/components/shared/VirtualizedList.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ describe('<VirtualizedList />', () => {
expect(frame.endsWith('item-19')).toBe(true);
});

it('collapses a short bottom-stuck list below the container height', async () => {
it('bottom-aligns a short bottom-stuck list within the container height', async () => {
const { lastFrame, rerender } = render(
<VirtualizedList<Item>
data={makeItems(5)}
Expand Down Expand Up @@ -174,7 +174,12 @@ describe('<VirtualizedList />', () => {
await act(async () => {});

const frame = lastFrame() ?? '';
expect(frame.split('\n')).toEqual([
// Short bottom-stuck content is bottom-aligned (#9300): blank rows at the
// top, the five items pinned to the bottom of the 20-row container.
const lines = frame.split('\n');
expect(lines.length).toBe(20);
expect(lines.slice(0, 15).every((l) => l.trim() === '')).toBe(true);
expect(lines.slice(15)).toEqual([
'item-0',
'item-1',
'item-2',
Expand Down Expand Up @@ -268,7 +273,7 @@ describe('<VirtualizedList />', () => {
expect(lastFrame() ?? '').toContain('thinking line 29');
});

it('collapses after measuring changed content at full viewport height', async () => {
it('bottom-aligns changed content after measuring it at full viewport height', async () => {
const liveItems = [{ id: -1, label: 'live' }];

const { frames, lastFrame, rerender } = render(
Expand Down Expand Up @@ -301,7 +306,9 @@ describe('<VirtualizedList />', () => {
/>,
);
await act(async () => {});
expect(lastFrame()?.split('\n')).toEqual(['confirm']);
// Bottom-aligned (#9300): the single confirmation sits at the container
// bottom with blank rows above.
expect(lastFrame()?.split('\n')).toEqual(['', '', '', '', 'confirm']);

const longConfirmationItems = [
{ id: -1, label: ['confirm', 'line 2', 'line 3'].join('\n') },
Expand All @@ -326,7 +333,14 @@ describe('<VirtualizedList />', () => {
expect(
frames.slice(frameCountBeforeLongContent).map((frame) => frame.trimEnd()),
).not.toContain('confirm');
expect(lastFrame()?.split('\n')).toEqual(['confirm', 'line 2', 'line 3']);
// Bottom-aligned (#9300): 3-line content pinned to the 5-row bottom.
expect(lastFrame()?.split('\n')).toEqual([
'',
'',
'confirm',
'line 2',
'line 3',
]);
});

it('targetScrollIndex anchors to that index on first usable render', () => {
Expand Down Expand Up @@ -1132,10 +1146,15 @@ describe('<VirtualizedList /> VP collapsed thought groups', () => {
const lines = (harness.lastFrame() ?? '').split('\n');
expect(lines.some((l) => l.includes('c1 thought line 0'))).toBe(false);
expect(lines.filter((l) => l.trim() !== '').length).toBeLessThanOrEqual(6);
expect(lines.length).toBeLessThanOrEqual(8);
// Collapsed state must still render the head summary, not an empty
// window (the assertions above also hold for a blank frame).
expect(lines.some((l) => l.includes('Thought for 1m 41s'))).toBe(true);
// Bottom-aligned (#9300): the released height becomes blank space at the
// TOP, and the collapsed summary sits at the bottom of the full 40-row
// container (last row is content, not a gap between content and the
// composer).
expect(lines.length).toBe(40);
expect(lines[lines.length - 1]!.trim()).not.toBe('');
});

it('does not lock the render window when a tall thought collapses off-screen', async () => {
Expand Down
35 changes: 25 additions & 10 deletions packages/cli/src/ui/components/shared/VirtualizedList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,17 @@ function VirtualizedList<T>(
maxScroll,
);

// Bottom-align short content while stuck to the bottom (#9300): when the
// whole conversation fits in the viewport, push it down so the latest
// message sits directly above the composer and any blank space is at the
// TOP (standard chat-TUI behavior), instead of top-aligning and leaving a
// gap between the last message and the composer. Zero whenever content
// overflows (maxScroll > 0) or the user has scrolled away from the bottom.
const bottomAlignGap =
isStickingToBottom && maxScroll === 0
? Math.max(0, scrollableContainerHeight - totalHeight)
: 0;

// The render window must cover what the viewport actually paints, so
// it is computed from clampedScrollTop, not the anchor-based
// actualScrollTop. While bottom-stuck the viewport pins to maxScroll,
Expand Down Expand Up @@ -940,20 +951,23 @@ function VirtualizedList<T>(
]);

// The host passes `containerHeight` as the *maximum* viewport height (the
// room available between the header and the composer). Pinning the root box
// to that height unconditionally left a tall empty gap below short content
// and pushed the composer far down the screen — the legacy <Static> path
// instead grows with its content. Collapse to `totalHeight` whenever the
// content fits so the composer sits right beneath the conversation. A
// caller can request one full-height measurement pass while content changes
// shape under a stable item key; otherwise a stale cached total can clip
// the new content before it is measured. The root collapses again after the
// measurement so short content does not leave a gap above the composer.
// room available between the header and the composer). While the list is
// not bottom-aligned, collapse to `totalHeight` whenever the content fits:
// pinning the root box to the full height unconditionally would leave a
// tall empty gap below short content and push the composer far down the
// screen, while the legacy <Static> path grows with its content. While a
// bottom-stuck conversation has room to spare (#9300), keep the full
// `containerHeight` instead so `bottomAlignGap` can push it down: the
// blank rows render ABOVE the content and the latest message sits right
// above the composer. A caller can request one full-height measurement
// pass while content changes shape under a stable item key; otherwise a
// stale cached total can clip the new content before it is measured. The
// collapse/bottom-align rule above applies again after the measurement.
// `scrollableContainerHeight` (the scroll math) still uses the full
// `containerHeight`, so scrolling is unaffected.
const rootHeight =
props.containerHeight !== undefined
? fullHeightMeasurementPending
? fullHeightMeasurementPending || bottomAlignGap > 0
? props.containerHeight
: Math.min(props.containerHeight, totalHeight)
: '100%';
Expand All @@ -973,6 +987,7 @@ function VirtualizedList<T>(
flexDirection="column"
marginTop={-clampedScrollTop}
>
<Box height={bottomAlignGap} flexShrink={0} />
<Box height={topSpacerHeight} flexShrink={0} />
{renderedItems}
<Box height={bottomSpacerHeight} flexShrink={0} />
Expand Down
Loading