feat(web-shell): add bottom status items#6613
Conversation
|
Thanks for the PR! Template looks good ✓ Problem: this is a Direction: aligned. WebShell is an actively extended part of the product (CHANGELOG has 20+ web-shell features). The Size: not applicable — no core paths touched. 359 production lines + 88 test lines across 10 files, all in Approach: the status-items half (type, TodoPanel rendering, CSS, exports) is clean and minimal. The scroll-follow half (MessageList.tsx +172 lines) is where I'd push back. The PR bundles a substantial rework of the scroll-follow state machine — Moving on to code review. 🔍 中文说明感谢贡献! 模板完整 ✓ 问题:这是 方向:对齐。WebShell 是产品中持续扩展的部分(CHANGELOG 有 20+ 个 web-shell 特性)。 规模:不适用——未触及核心路径。10 个文件共 359 行生产代码 + 88 行测试代码,全部在 方案:状态项部分(类型、TodoPanel 渲染、CSS、导出)简洁合理。滚动跟随部分(MessageList.tsx +172 行)我想提出异议。PR 捆绑了对滚动跟随状态机的大规模改造—— 进入代码审查 🔍 — Qwen Code · qwen3.7-max |
Code ReviewIndependent proposal: I'd define a Comparison: the status-items half matches my proposal closely — clean type, clean conditional rendering in TodoPanel, clean CSS. The scroll-follow half goes well beyond what I'd have done: No correctness bugs found in the code. The new state transitions are internally consistent and the naming is clear. The concern is maintenance surface — scroll-follow behavior now has more moving parts, and timing-dependent interactions (1s userScrollIntent window, ResizeObserver callbacks, RAF chains) are hard to reason about from code alone. Reuse check: no duplicated logic or missed reuse opportunities. The Test ResultsAll 138 tests pass, build succeeds, no TypeScript errors. Tmux TestingNot applicable — this is a WebShell React component feature that requires a browser host application to pass 中文说明代码审查独立方案: 定义 对比: 状态项部分与我的方案接近——类型干净,TodoPanel 条件渲染干净,CSS 干净。滚动跟随部分远超我会做的范围: 代码中未发现正确性 bug。新的状态转换内部一致,命名清晰。担心的是维护面——滚动跟随行为现在有更多活动部件,时序相关的交互(1 秒 userScrollIntent 窗口、ResizeObserver 回调、RAF 链)很难仅从代码推断。 复用检查: 无重复逻辑或遗漏的复用机会。 测试结果138 项测试全部通过,构建成功,无 TypeScript 错误。 Tmux 测试不适用——这是 WebShell React 组件功能,需要浏览器宿主应用通过 API 传入 — Qwen Code · qwen3.7-max |
|
The core feature — The scroll-follow overhaul is the part I'd flag. ~172 lines of new scroll state machinery in MessageList — That said: no correctness bugs found, all tests pass, the build is clean, and the scroll changes are internally well-structured with clear naming. The author clearly understands the scroll system and made deliberate choices. The concern is maintenance cost, not correctness — and that's a tradeoff the maintainer can weigh. LGTM overall, with the scroll complexity noted for awareness. ✅ 中文说明核心功能—— 滚动跟随改造是我要指出的部分。MessageList 中约 172 行新的滚动状态机制—— 尽管如此:未发现正确性 bug,所有测试通过,构建干净,滚动变更内部结构良好、命名清晰。作者显然理解滚动系统并做出了深思熟虑的选择。担心的是维护成本而非正确性——这是维护者可以权衡的取舍。 总体 LGTM,滚动复杂度提请留意。✅ — Qwen Code · qwen3.7-max |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
| } else if (hasUserScrollIntent) { | ||
| followPausedByUserRef.current = true; | ||
| setShouldFollow(false); | ||
| } else if (!followPausedByUserRef.current) { |
There was a problem hiding this comment.
[Critical] followPausedByUserRef / shouldFollow divergence creates a gap state that triggers phantom auto-scroll.
This branch sets setShouldFollow(false) without setting followPausedByUserRef.current = true, creating a state where shouldFollow === false AND followPausedByUserRef === false. Three consumer effects then treat !followPausedByUserRef.current as permission to auto-scroll:
- Auto-scroll driver (line 3231):
shouldFollow.current || isNewUserMessage || !followPausedByUserRef.current - Execution-key effect (line 3057):
shouldFollow.current || !followPausedByUserRef.current - Container-resize guard (line 2923):
if (followPausedByUserRef.current) return;(no longer bails)
The || operator makes these gates almost-always-open: when the user hasn't explicitly paused (the common case), follow resumes regardless of shouldFollow. This contradicts the setShouldFollow(false) decision made here.
Trigger scenario: any upward scroll without a recent (within 1s) wheel/touch/pointer/keyboard event — e.g., browser scroll restoration, virtualizer internal scrollTop adjustments, or a container resize that clamps scrollTop downward.
| } else if (!followPausedByUserRef.current) { | |
| } else if (!followPausedByUserRef.current) { | |
| followPausedByUserRef.current = true; | |
| setShouldFollow(false); | |
| } |
Alternatively, if the || in consumer effects is intentional (auto-scroll should resume whenever the user hasn't explicitly paused), consider documenting the invariant or replacing the dual-ref system with a single authoritative flag.
— qwen3.7-max via Qwen Code /review
Suggestions — commit
|
| File | Issue | Suggested fix |
|---|---|---|
MessageList.tsx:2838 |
20px scrollbarEdge hit-test zone is 3× wider than the actual 6px scrollbar (scrollbar-width: thin), catching content-area clicks. The bottom-edge check (clientY >= rect.bottom - 20) is dead code since the container is overflow-y: auto only (no horizontal scrollbar). |
Extract scrollbarEdge to a named constant near the other *_PX constants. Consider using el.clientWidth comparison instead of a fixed pixel zone. Remove the bottom-edge check or document its intent. |
MessageList.tsx (multiple locations) |
All new scroll-follow state paths (followPausedByUserRef at 11 write sites, userScrollIntentUntil with 4 event listeners, pendingTranscriptBottomScroll lifecycle, bottomOverlayInset prop) have zero test coverage. Only 2 new TodoPanel tests were added. |
Add DOM tests covering: (1) user scroll-up without recent intent signal, (2) panel resize while user is scrolled up, (3) bottomOverlayInset change scroll behavior, (4) transcript load settle scroll. |
MessageList.tsx:3032 |
requestAnimationFrame in bottomOverlayInset effect doesn't store the frame ID or cancel on unmount, unlike the transcriptBottomScrollFrame pattern elsewhere in the file. |
Store the RAF ID in a ref and cancel in the effect cleanup, consistent with the existing pattern. |
App.module.css:630 |
.scrollToBottomLayerWithTodos class name is now applied when ANY bottom panel exists (todos OR status items), not just todos. |
Rename to .scrollToBottomLayerWithBottomPanel or .scrollToBottomLayerWithPanels. |
TodoPanel.module.css:80 |
.statusSegmentButton:focus-visible sets outline: none without providing a sufficient alternative focus indicator (only a color shift from --muted-foreground to --foreground). This may not meet WCAG focus-visible requirements. |
Add a visible focus indicator such as text-decoration: underline, box-shadow, or border-bottom on :focus-visible. |
— qwen3.7-max via Qwen Code /review
What this PR does
Adds a generic
bottomStatusItemsWebShell prop for rendering host-provided status items in the floating bottom panel next to the TODO summary. It also reserves transcript space for that floating panel and keeps bottom-follow behavior stable when the panel appears, resizes, or is used without TODO items.Why it's needed
WebShell embedders sometimes need to surface compact session-level status near the existing TODO progress indicator. A first-class generic slot avoids custom overlays that can cover the transcript or drift out of sync with the composer height, while preserving the default UI when no status items are provided.
Reviewer Test Plan
How to verify
Confirm that the default WebShell TODO panel still renders as before when
bottomStatusItemsis omitted. Then pass one or more status items and verify they appear in the same floating panel, separated from TODO progress by·, support optional click handlers, and can render without any TODO progress. Also verify that when the bottom panel appears or changes height, the transcript reserves enough bottom padding and continues following the bottom unless the user has intentionally scrolled away.Evidence (Before & After)
N/A. This PR adds a WebShell extension point and layout behavior covered by DOM tests; default visible UI is unchanged when the new prop is not used.
Tested on
Environment (optional)
Local macOS validation passed:
npx prettier --check packages/web-shell/client/App.tsx packages/web-shell/client/App.module.css packages/web-shell/client/components/MessageList.tsx packages/web-shell/client/components/MessageList.module.css packages/web-shell/client/components/panels/TodoPanel.tsx packages/web-shell/client/components/panels/TodoPanel.module.css packages/web-shell/client/components/panels/TodoPanel.test.tsx packages/web-shell/client/customization.tsx packages/web-shell/client/index.ts packages/web-shell/client/index.tsx;npx eslint packages/web-shell/client/App.tsx packages/web-shell/client/customization.tsx packages/web-shell/client/index.ts packages/web-shell/client/index.tsx packages/web-shell/client/components/MessageList.tsx packages/web-shell/client/components/panels/TodoPanel.tsx packages/web-shell/client/components/panels/TodoPanel.test.tsx;npm run test --workspace=packages/web-shell -- MessageList.dom.test.tsx MessageList.test.ts TodoPanel.test.tsx;npm run build --workspace=packages/web-shell. Fullnpm run preflightwas also run on this branch after removing the unrelated nested local worktree from the checkout; it completed clean/install/format/lint/build/typecheck, then failed duringpackages/clitest:cion CLI-side assertions outside this WebShell change (src/commands/extensions/list.test.tslocalization output andsrc/ui/auth/AuthDialog.test.tsxselection/navigation expectations). Later workspaces continued and passed.Risk & Scope
npm run preflightdoes not currently pass in this checkout because of CLI-sidetest:cifailures outside this WebShell change.bottomStatusItems, and the default TODO-only panel remains supported.Linked Issues
N/A
中文说明
What this PR does
为 WebShell 增加通用的
bottomStatusItemsprop,用于在浮动底部面板中、TODO 摘要旁边渲染宿主传入的状态项。同时为这个浮动面板预留 transcript 底部空间,并在面板出现、尺寸变化、或没有 TODO 项但有状态项时保持贴底滚动行为稳定。Why it's needed
WebShell 集成方有时需要在现有 TODO 进度提示附近展示紧凑的会话级状态。提供一个通用的一等 slot,可以避免宿主自建 overlay 遮挡消息或与 composer 高度不同步;当不传状态项时,默认 UI 保持不变。
Reviewer Test Plan
How to verify
确认省略
bottomStatusItems时默认 WebShell TODO 面板仍按原样渲染。然后传入一个或多个状态项,确认它们出现在同一个浮动面板中,与 TODO 进度之间用·分隔,支持可选点击事件,并且可以在没有 TODO 进度时单独渲染。还需要确认底部面板出现或高度变化时,transcript 会预留足够底部 padding,并且在用户没有主动滚离底部时继续贴底。Evidence (Before & After)
N/A。这个 PR 增加的是 WebShell 扩展点和对应布局行为,并通过 DOM 测试覆盖;不使用新 prop 时默认可见 UI 不变。
Tested on
Environment (optional)
本地 macOS 验证已通过:
npx prettier --check packages/web-shell/client/App.tsx packages/web-shell/client/App.module.css packages/web-shell/client/components/MessageList.tsx packages/web-shell/client/components/MessageList.module.css packages/web-shell/client/components/panels/TodoPanel.tsx packages/web-shell/client/components/panels/TodoPanel.module.css packages/web-shell/client/components/panels/TodoPanel.test.tsx packages/web-shell/client/customization.tsx packages/web-shell/client/index.ts packages/web-shell/client/index.tsx;npx eslint packages/web-shell/client/App.tsx packages/web-shell/client/customization.tsx packages/web-shell/client/index.ts packages/web-shell/client/index.tsx packages/web-shell/client/components/MessageList.tsx packages/web-shell/client/components/panels/TodoPanel.tsx packages/web-shell/client/components/panels/TodoPanel.test.tsx;npm run test --workspace=packages/web-shell -- MessageList.dom.test.tsx MessageList.test.ts TodoPanel.test.tsx;npm run build --workspace=packages/web-shell。移除 checkout 中无关的本地嵌套 worktree 后,本分支也已运行完整npm run preflight;该命令完成了 clean/install/format/lint/build/typecheck,随后在packages/cli的test:ci阶段失败,失败点是本次 WebShell 改动范围外的 CLI 侧断言(src/commands/extensions/list.test.ts本地化输出,以及src/ui/auth/AuthDialog.test.tsx选择/导航期望)。后续 workspace 继续执行并通过。Risk & Scope
npm run preflight因本次 WebShell 改动范围外的 CLI 侧test:ci失败而未通过。bottomStatusItems,默认 TODO-only 面板仍然支持。Linked Issues
N/A