Skip to content

fix(web-shell): constrain virtual scroll rows - #6362

Merged
wenshao merged 2 commits into
QwenLM:mainfrom
chiga0:codex/fix-web-shell-virtual-scroll-width
Jul 6, 2026
Merged

fix(web-shell): constrain virtual scroll rows#6362
wenshao merged 2 commits into
QwenLM:mainfrom
chiga0:codex/fix-web-shell-virtual-scroll-width

Conversation

@ytahdn

@ytahdn ytahdn commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

Constrain web-shell virtualized transcript rows to the same centered chat content width used by the non-virtualized transcript. The virtualizer's full-width measurement layer remains full width, while each rendered row now carries the chat content width cap.

Why it's needed

When long web-shell sessions crossed the virtual scrolling threshold, the transcript rows expanded across the whole pane instead of respecting the configured chat maximum width. This made the page look full-width only after virtualization kicked in.

Reviewer Test Plan

How to verify

Open a web-shell session with enough transcript rows to enable virtual scrolling. Confirm messages remain centered and capped to the normal chat content width before and after virtual scrolling is active. Locally, I verified the existing MessageList DOM behavior with cd packages/web-shell && npx vitest run client/components/MessageList.dom.test.tsx.

Evidence (Before & After)

Before: virtualized transcript rows filled the available page width once the virtual scroller activated. After: virtualized transcript rows stay centered and constrained to the normal chat content width while the virtualizer measurement layer remains full width.

Tested on

OS Status
🍏 macOS ✅ tested
🪟 Windows ⚠️ not tested
🐧 Linux ⚠️ not tested

Environment (optional)

Local vitest run in packages/web-shell.

Risk & Scope

  • Main risk or tradeoff: Low. The change is limited to the web-shell virtualized transcript layout and keeps the measurement container full width.
  • Not validated / out of scope: Manual browser verification on Windows and Linux.
  • Breaking changes / migration notes: None.

Linked Issues

N/A

中文说明

What this PR does

让 web-shell 虚拟化后的 transcript 行继续使用非虚拟列表相同的居中聊天内容宽度。虚拟滚动器的测量层仍保持全宽,每个实际渲染的行重新套用聊天内容最大宽度。

Why it's needed

长会话超过虚拟滚动阈值后,transcript 行会展开到整个面板宽度,不再遵守配置的聊天最大宽度。这样页面会在虚拟滚动触发后才突然变成满宽。

Reviewer Test Plan

How to verify

打开一个足够长、能够触发虚拟滚动的 web-shell 会话。确认虚拟滚动启用前后,消息仍保持居中并限制在正常聊天内容宽度内。本地已通过 cd packages/web-shell && npx vitest run client/components/MessageList.dom.test.tsx 验证现有 MessageList DOM 行为。

Evidence (Before & After)

Before: 虚拟滚动启用后,transcript 行会填满可用页面宽度。After: 虚拟滚动启用后,transcript 行仍保持居中并限制在正常聊天内容宽度内,同时虚拟滚动器测量层继续保持全宽。

Tested on

OS Status
🍏 macOS ✅ tested
🪟 Windows ⚠️ not tested
🐧 Linux ⚠️ not tested

Environment (optional)

packages/web-shell 中运行本地 vitest。

Risk & Scope

  • Main risk or tradeoff: 风险较低。改动仅限 web-shell 虚拟化 transcript 布局,并保留测量容器全宽。
  • Not validated / out of scope: 未在 Windows 和 Linux 上做手动浏览器验证。
  • Breaking changes / migration notes: 无。

Linked Issues

N/A

@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

Thanks for the PR! (re-triage after test addition)

Template looks good ✓

Problem: Structural CSS bug — when the virtualizer kicks in for long web-shell sessions, the .list > * width constraint applies to the virtualizer measurement container rather than individual rendered rows, causing rows to expand to full pane width. The mechanism is clear from the code; the structural cause is concrete and verifiable.

Direction: Aligned. Web-shell layout correctness is in scope, and this is a straightforward CSS fix. No product-direction concerns.

Approach: The scope is tight — 62 additions, 6 deletions across 3 files (CSS, TSX, and now a focused DOM test). The approach mirrors what I'd do independently: a .virtualSizer class that resets the .list > * constraint on the measurement container, and a .virtualRow class that re-applies the same --chat-content-width cap to each rendered row. Reuses existing CSS custom properties and the local joinClassNames utility. No drive-by refactors or scope creep.

Moving on to code review. 🔍

中文说明

感谢贡献!(添加测试后重新审查)

模板完整 ✓

问题:结构性 CSS bug——当 web-shell 长会话触发虚拟滚动后,.list > * 的宽度约束作用在了虚拟滚动测量容器上,而不是单个渲染行上,导致行扩展到整个面板宽度。通过阅读代码可以确认原因;结构性问题是具体且可验证的。

方向:对齐。Web-shell 布局正确性在项目范围内,这是一个直接的 CSS 修复。没有产品方向方面的顾虑。

方案:范围紧凑——3 个文件,62 行增加、6 行删除(CSS、TSX 以及新增的 DOM 测试)。方案与独立分析的结果一致:.virtualSizer 类重置测量容器上的 .list > * 约束,.virtualRow 类对每个渲染行重新应用 --chat-content-width 上限。复用了已有的 CSS 自定义属性和本地 joinClassNames 工具函数。没有夹带无关重构或范围蔓延。

进入代码审查 🔍

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

Code Review

The diff is clean and minimal. Two CSS classes do the work:

  • .virtualSizer — applied to the outer container (the virtualizer measurement layer). Resets the .list > * width constraint (max-width: none, margin: 0) so the measurement layer stays full-width, which the virtualizer needs for correct size calculations.

  • .virtualRow — applied to each rendered virtual row. Re-applies the same min(100%, var(--chat-content-width)) constraint and auto-centering margins that the non-virtual .list > * rule provides. This is the same pattern used elsewhere in the component — no new CSS variables or logic introduced.

In the TSX, inline width: '100%' on the container is replaced by the .virtualSizer class. Each virtual row gets .virtualRow composed with the existing getRowClassName() via the local joinClassNames utility (already defined at line 1651). The width: '100%' on rows becomes right: 0 (absolute positioning spanning the sizer), with the CSS class handling the visual width constraint.

No correctness bugs, no security concerns, no regressions spotted. The CSS correctly preserves the measurement container's full width while constraining visible rows.

New Test

The author added a focused DOM test ('renders virtual scroll rows with sizer and row width classes') that mocks @tanstack/react-virtual so jsdom doesn't need real layout measurement, renders 110 turns to cross the virtual-scroll threshold, and asserts both .virtualSizer and .virtualRow classes are present. This is a good, tight test for the change.

Test Results

Ran the MessageList DOM test suite on the PR code:

 ✓ components/MessageList.dom.test.tsx (34 tests) 829ms

 Test Files  1 passed (1)
      Tests  34 passed (34)
   Duration  1.59s

All 34 tests pass (33 existing + 1 new virtual scroll test). No regressions.

Real-Scenario Testing (tmux)

N/A — this is a CSS layout fix for the web-shell browser UI. The tmux workflows (qwen -p '...') drive the CLI terminal tool, not the web-shell. Virtual-scroll width constraints require a real browser rendering engine to verify. The author states macOS was tested locally; Windows and Linux are untested.

Browser verification by a reviewer (open a long web-shell session, confirm centered width is maintained across the virtual-scroll threshold) is the appropriate validation step here.

中文说明

代码审查

Diff 干净且最小化。两个 CSS 类完成了工作:

  • .virtualSizer — 应用于外层容器(虚拟滚动器测量层)。重置 .list > * 的宽度约束(max-width: nonemargin: 0),使测量层保持全宽,这是虚拟滚动器正确计算尺寸所需的。

  • .virtualRow — 应用于每个渲染的虚拟行。重新应用与非虚拟 .list > * 规则相同的 min(100%, var(--chat-content-width)) 约束和自动居中边距。这是组件中其他地方已有的模式——没有引入新的 CSS 变量或逻辑。

在 TSX 中,容器的内联 width: '100%'.virtualSizer 类替换。每个虚拟行通过本地 joinClassNames 工具函数(已在第 1651 行定义)将 .virtualRow 与现有的 getRowClassName() 组合。行上的 width: '100%' 变为 right: 0(绝对定位跨越 sizer),由 CSS 类处理视觉宽度约束。

没有发现正确性 bug、安全问题或回归。CSS 正确保留了测量容器的全宽,同时约束了可见行。

新增测试

作者新增了针对性的 DOM 测试('renders virtual scroll rows with sizer and row width classes'),mock 了 @tanstack/react-virtual 使 jsdom 不需要真实的布局测量,渲染 110 个 turn 以跨越虚拟滚动阈值,并断言 .virtualSizer.virtualRow 类均存在。这是一个好的、紧凑的测试。

测试结果

在 PR 代码上运行了 MessageList DOM 测试套件:

 ✓ components/MessageList.dom.test.tsx (34 tests) 829ms

 Test Files  1 passed (1)
      Tests  34 passed (34)
   Duration  1.59s

全部 34 个测试通过(33 个已有 + 1 个新增虚拟滚动测试)。没有回归。

真实场景测试 (tmux)

不适用——这是 web-shell 浏览器 UI 的 CSS 布局修复。 tmux 工作流 (qwen -p '...') 驱动的是 CLI 终端工具,而非 web-shell。虚拟滚动宽度约束需要真实的浏览器渲染引擎来验证。作者声明已在 macOS 上本地测试;Windows 和 Linux 未测试。

审查者的浏览器验证(打开一个长 web-shell 会话,确认在虚拟滚动阈值前后居中宽度保持一致)是此处合适的验证步骤。

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

This is a clean, well-scoped CSS fix. The structural bug is real: the .list > * width constraint targets the virtualizer measurement container rather than individual rows, causing a visible full-width jump when virtual scrolling activates. The fix is exactly what I'd do independently — two CSS classes that cleanly separate the measurement layer (full-width) from the rendered rows (chat-content-width constrained), reusing the same CSS custom properties already in use.

The author has now added a focused DOM test that mocks the virtualizer and verifies the correct CSS classes are applied, closing the test-coverage gap from the initial submission. 34/34 tests pass.

62 lines across 3 files, no drive-by changes, no new abstractions. If I had to maintain this in six months, I'd thank the author for keeping it simple.

The one gap remains browser screenshots — the structural analysis is convincing from the code, but a visual confirmation from a reviewer would close the loop. That's a reviewer task, not a blocker.

Approving. ✅

中文说明

这是一个干净、范围合理的 CSS 修复。结构性 bug 是真实存在的:.list > * 的宽度约束作用于虚拟滚动器测量容器而非单个行,导致虚拟滚动激活时出现可见的全宽跳变。修复方案与独立分析的结果完全一致——两个 CSS 类清晰地将测量层(全宽)和渲染行(chat-content-width 约束)分离,复用了已有的 CSS 自定义属性。

作者现已新增了一个针对性的 DOM 测试,mock 虚拟滚动器并验证正确的 CSS 类是否被应用,弥补了初次提交时的测试覆盖缺口。34/34 测试通过。

3 个文件 62 行改动,没有夹带无关改动,没有新增抽象。如果六个月后需要维护这段代码,我会感谢作者保持了简洁。

唯一的缺憾是缺少浏览器截图——从代码层面看结构性分析是有说服力的,但审查者的视觉确认可以闭环验证。这是审查者的任务,不是阻塞项。

批准 ✅

Qwen Code · qwen3.7-max

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, looks ready to ship. ✅

@ytahdn
ytahdn requested a review from wenshao July 6, 2026 05:34

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No review findings. Downgraded from Approve to Comment: CI still running.

— qwen3.7-max via Qwen Code /review

/>
{useVirtualScroll ? (
<div
className={styles.virtualSizer}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] No existing DOM test exercises the virtual scroll rendering path. The VIRTUAL_SCROLL_THRESHOLD is 200 messages, but all tests in MessageList.dom.test.tsx use fewer than 20 messages. Since this PR changes CSS classes and inline styles on the virtual path, a regression in this branch (e.g., a CSS specificity conflict with .list > *) would go undetected.

Consider adding a test that triggers the virtual scroll branch:

it('renders virtual scroll rows with sizer and row classes', () => {
  const messages = simpleTurns(110); // 220 messages, above threshold
  const c = mount(messages);
  expect(c.querySelector(`.${styles.virtualSizer}`)).not.toBeNull();
  expect(c.querySelectorAll(`.${styles.virtualRow}`).length).toBeGreaterThan(0);
});

— qwen3.7-max via Qwen Code /review

@ytahdn

ytahdn commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed review comment #6362 (comment).

Added a focused DOM test for the virtual branch in MessageList.dom.test.tsx. The test mocks @tanstack/react-virtual so jsdom does not need real layout measurement, then renders enough turns to cross the virtual scroll threshold and asserts both the sizer and virtual row width classes are present.

Verified locally:

cd packages/web-shell && npx vitest run client/components/MessageList.dom.test.tsx

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No review findings. Downgraded from Approve to Comment: CI still running.

Clean, minimal fix. The .virtualSizer / .virtualRow class split correctly separates the measurement layer (full width) from the rendered rows (content-width constrained), mirroring the existing .list > * centering behavior. The new DOM test covers the virtual scroll rendering path. LGTM ✅

— qwen3.7-max via Qwen Code /review

@wenshao

wenshao commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, looks ready to ship. ✅

@wenshao

wenshao commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

✅ Verification report — real-browser A/B (merge reference)

I reproduced the bug and verified the fix by rendering the real MessageList component + real MessageList.module.css in a real headless Chromium (Playwright), with 420 messages — past the VIRTUAL_SCROLL_THRESHOLD = 200, so the actual virtualized render branch that this PR touches is exercised (the real @tanstack/react-virtual activates: virtualized: true, 34 windowed rows of 420). Only the leaf row content is stubbed — the exact same modules this PR's own DOM test mocks (MessageItem, ParallelAgentsGroup, CompactModeContext), none of which affect row width.

A/B: identical harness + identical vite server, swapping only the 2 source files — base main @edc0555e vs PR head @01f7546. Environment: 🐧 Linux (the PR box was macOS-only), so this also adds the Linux coverage the PR marked not tested.

before / after

Measured DOM geometry (pane = 1440px, cap --chat-content-width: 1000px)

metric ✗ BEFORE (main) ✓ AFTER (this PR)
virtualized (real virtualizer active) true true
row width 1388px — full pane content 1000px — exactly the cap
row left / margin 50px / 0flush-left 244px / 194px each side — centered
row center vs pane center (744) overflows cap by ~194px per side 744 = exact pane center
sizer / measurement layer 1388px (full) 1388px (full) — unchanged
row DOM inline width:100%, no class .virtualRow class + right:0

The BEFORE rows blow straight through the green guide (the expected centered cap); the AFTER rows hug it. Crucially, the sizer stays full width in both — the fix only re-applies the centered content cap to each rendered row (.virtualRow) while keeping the virtualizer's full-width measurement container (.virtualSizer). This matches the non-virtualized path, where .list > * already caps direct children; virtual rows are grandchildren of .list, so they escaped that rule — which is the root cause this PR addresses.

Unit test

  • MessageList.dom.test.tsx on this PR: 34/34 pass (incl. the new renders virtual scroll rows with sizer and row width classes).
  • Non-vacuity check — reverting only the source to main while keeping the PR's test makes the new test fail: expected null not to be null at querySelector('.'+styles.virtualSizer) (main emits no .virtualSizer). So the added test genuinely guards the fix.

Verdict

LGTM — safe to merge. The reported symptom (virtualized rows expanding to full pane width) is reproduced on main and fully resolved by this PR; centering and the content-width cap now match the non-virtualized transcript, and the virtualizer's measurement layer is untouched. Scope is limited to the web-shell virtualized transcript layout; no behavioral change to the non-virtual path.

🇨🇳 中文版(点击展开)

✅ 验证报告 —— 真实浏览器 A/B(供 merge 参考)

我通过在真实的无头 Chromium(Playwright)中渲染真实的 MessageList 组件 + 真实的 MessageList.module.css 来复现问题并验证修复,喂入 420 条消息——超过 VIRTUAL_SCROLL_THRESHOLD = 200,因此本 PR 改动的虚拟化渲染分支被真实执行(真实的 @tanstack/react-virtual 生效:virtualized: true,420 行中窗口化渲染 34 行)。只有叶子行内容被打桩替换——与本 PR 自带 DOM 测试所 mock 的模块完全一致(MessageItemParallelAgentsGroupCompactModeContext),这些都不影响行宽。

A/B: 相同的测试壳 + 相同的 vite 服务,仅交换 2 个源文件——基线 main @edc0555e vs PR head @01f7546。环境:🐧 Linux(PR 作者仅在 macOS 测过),因此本次也补上了 PR 中标注"未测试"的 Linux 覆盖。

实测 DOM 几何(面板 = 1440px,上限 --chat-content-width: 1000px

指标 ✗ 修复前(main ✓ 修复后(本 PR)
virtualized(真实虚拟化生效) true true
行宽 1388px —— 撑满面板内容区 1000px —— 恰好等于上限
left / margin 50px / 0 —— 左对齐 244px / 两侧各 194px —— 居中
行中心 vs 面板中心 (744) 每侧超出上限约 194px 744 = 面板正中心
sizer / 测量层 1388px(全宽) 1388px(全宽)—— 未变
行 DOM 内联 width:100%,无 class .virtualRow class + right:0

修复前的行直接穿过绿色参考线(期望的居中上限);修复后的行紧贴参考线。关键点:两种情况下 sizer 都保持全宽——修复只是给每个实际渲染的行.virtualRow)重新套上居中内容上限,同时保留虚拟化器的全宽测量容器(.virtualSizer)。这与非虚拟化路径一致:.list > * 本就会给 .list 的直接子元素加上限,而虚拟行是 .list孙子元素,因此逃逸了该规则——这正是本 PR 修复的根因。

单元测试

  • 本 PR 上 MessageList.dom.test.tsx34/34 通过(含新增的 renders virtual scroll rows with sizer and row width classes)。
  • 非空验证 —— 仅把源码回退到 main、保留 PR 的测试,新测试即失败:在 querySelector('.'+styles.virtualSizer) 处报 expected null not to be null(main 不产生 .virtualSizer)。说明新增测试确实能守护该修复。

结论

LGTM —— 可以安全合并。 所报告的现象(虚拟化行撑满面板宽度)在 main 上可复现,本 PR 完全修复;居中与内容宽度上限现已与非虚拟化 transcript 一致,且虚拟化器测量层未受影响。改动范围仅限 web-shell 虚拟化 transcript 布局,未改变非虚拟路径行为。

Methodology: real component + real CSS module in headless Chromium; leaf children stubbed identically to the PR's own vitest DOM test (row width is governed purely by the CSS/DOM structure this PR changes, not by row content). Screenshots hosted on my fork's release.

@wenshao
wenshao added this pull request to the merge queue Jul 6, 2026
Merged via the queue into QwenLM:main with commit b726b7c Jul 6, 2026
35 checks passed
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.

3 participants