Skip to content

feat(web-shell): add bottom status items#6613

Merged
dreamWB merged 1 commit into
QwenLM:mainfrom
dreamWB:codex/webshell-bottom-status-items
Jul 9, 2026
Merged

feat(web-shell): add bottom status items#6613
dreamWB merged 1 commit into
QwenLM:mainfrom
dreamWB:codex/webshell-bottom-status-items

Conversation

@dreamWB

@dreamWB dreamWB commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

Adds a generic bottomStatusItems WebShell 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 bottomStatusItems is 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

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

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. Full npm run preflight was 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 during packages/cli test:ci on CLI-side assertions outside this WebShell change (src/commands/extensions/list.test.ts localization output and src/ui/auth/AuthDialog.test.tsx selection/navigation expectations). Later workspaces continued and passed.

Risk & Scope

  • Main risk or tradeoff: The message-list follow logic now distinguishes user-initiated scrolling from layout-induced scroll changes so the bottom panel does not cover new content, while still preserving the scroll-to-bottom affordance when the viewport is away from the tail.
  • Not validated / out of scope: Windows and Linux local validation are not run; full npm run preflight does not currently pass in this checkout because of CLI-side test:ci failures outside this WebShell change.
  • Breaking changes / migration notes: None. Existing consumers do not need to pass bottomStatusItems, and the default TODO-only panel remains supported.

Linked Issues

N/A

中文说明

What this PR does

为 WebShell 增加通用的 bottomStatusItems prop,用于在浮动底部面板中、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

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

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.tsxnpx 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.tsxnpm run test --workspace=packages/web-shell -- MessageList.dom.test.tsx MessageList.test.ts TodoPanel.test.tsxnpm run build --workspace=packages/web-shell。移除 checkout 中无关的本地嵌套 worktree 后,本分支也已运行完整 npm run preflight;该命令完成了 clean/install/format/lint/build/typecheck,随后在 packages/clitest:ci 阶段失败,失败点是本次 WebShell 改动范围外的 CLI 侧断言(src/commands/extensions/list.test.ts 本地化输出,以及 src/ui/auth/AuthDialog.test.tsx 选择/导航期望)。后续 workspace 继续执行并通过。

Risk & Scope

  • Main risk or tradeoff: MessageList 的贴底逻辑现在会区分用户主动滚动和布局导致的滚动变化,避免底部面板遮挡新内容,同时仍保留视口离开底部时的回到底部按钮。
  • Not validated / out of scope: 未在 Windows 和 Linux 本地验证;当前 checkout 中完整 npm run preflight 因本次 WebShell 改动范围外的 CLI 侧 test:ci 失败而未通过。
  • Breaking changes / migration notes: 无。现有使用方不需要传入 bottomStatusItems,默认 TODO-only 面板仍然支持。

Linked Issues

N/A

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR!

Template looks good ✓

Problem: this is a feat adding a new extension point (bottomStatusItems) for WebShell embedders — not a bug fix, so no reproduction is expected. The gap is real: embedders currently have no first-class way to surface compact status alongside the TODO panel.

Direction: aligned. WebShell is an actively extended part of the product (CHANGELOG has 20+ web-shell features). The bottomStatusItems prop follows the existing customization pattern (renderComposerToolbarRight, renderFooter, etc.).

Size: not applicable — no core paths touched. 359 production lines + 88 test lines across 10 files, all in packages/web-shell/client/.

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 — followPausedByUserRef set in 10+ locations, a userScrollIntentUntil timestamp with four event listeners (wheel, touch, pointer, keyboard), and a new pendingTranscriptBottomScroll lifecycle. All of this is to prevent the follow-toggle from fighting the bottom panel's layout changes, but it's a lot of new state to get right. Have you considered a simpler guard — e.g. only re-evaluating follow state when the panel height is stable (debounced), or skipping the shouldFollow toggle inside the ResizeObserver callback when the trigger is a panel resize rather than new content? That might cover most of the use case without the scroll-intent detection machinery.

Moving on to code review. 🔍

中文说明

感谢贡献!

模板完整 ✓

问题:这是 feat,为 WebShell 增加新的扩展点(bottomStatusItems),不是 bug 修复,无需复现。缺口真实存在:集成方目前没有一等的方式在 TODO 面板旁边展示紧凑状态。

方向:对齐。WebShell 是产品中持续扩展的部分(CHANGELOG 有 20+ 个 web-shell 特性)。bottomStatusItems prop 遵循已有的自定义模式(renderComposerToolbarRightrenderFooter 等)。

规模:不适用——未触及核心路径。10 个文件共 359 行生产代码 + 88 行测试代码,全部在 packages/web-shell/client/

方案:状态项部分(类型、TodoPanel 渲染、CSS、导出)简洁合理。滚动跟随部分(MessageList.tsx +172 行)我想提出异议。PR 捆绑了对滚动跟随状态机的大规模改造——followPausedByUserRef 在 10+ 处设置,userScrollIntentUntil 时间戳配合四个事件监听器(wheel、touch、pointer、keyboard),以及新的 pendingTranscriptBottomScroll 生命周期。这些都是为了防止面板布局变化时 follow-toggle 产生冲突,但需要正确维护的新状态太多了。是否考虑过更简单的保护方式——例如仅在面板高度稳定后(debounce)才重新评估 follow 状态,或在 ResizeObserver 回调中区分面板 resize 和新内容?这样可能不需要滚动意图检测机制就能覆盖大部分场景。

进入代码审查 🔍

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Code Review

Independent proposal: I'd define a WebShellBottomStatusItem type, add it as a prop, render items in TodoPanel with a separator, and use CSS bottom positioning to keep the floating panel above the composer. For scroll follow, I'd leave the existing state machine alone and just ensure the panel height is reflected in a CSS variable for padding.

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: followPausedByUserRef (mutated in 10+ locations), userScrollIntentUntil with 4 event listeners, and a new pendingTranscriptBottomScroll lifecycle. This is a substantial rework of the scroll state machine bundled alongside a relatively small extension-point feature.

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 WebShellBottomStatusItem type is unique to this feature and the CSS follows existing patterns.

Test Results

$ cd packages/web-shell && npx vitest run components/panels/TodoPanel.test.tsx

 RUN  v3.2.4 web-shell/client

 ✓ components/panels/TodoPanel.test.tsx (2 tests) 26ms

 Test Files  1 passed (1)
      Tests  2 passed (2)
   Duration  653ms
$ cd packages/web-shell && npx vitest run components/MessageList

 RUN  v3.2.4 web-shell/client

 ✓ components/MessageList.test.ts (93 tests) 18ms
 ✓ components/MessageList.dom.test.tsx (43 tests) 1227ms

 Test Files  2 passed (2)
      Tests  136 passed (136)
   Duration  1.99s
$ npm run build --workspace=packages/web-shell

✓ built in 8.22s
vite v5.4.21 building for production...
✓ 200 modules transformed.
dist/index.js  1,505.45 kB │ gzip: 356.36 kB
✓ built in 1.36s

All 138 tests pass, build succeeds, no TypeScript errors.

Tmux Testing

Not applicable — this is a WebShell React component feature that requires a browser host application to pass bottomStatusItems via the API. It can't be exercised through the CLI or terminal. The behavior is covered by the 136 existing MessageList tests (including 43 DOM tests that exercise scroll-follow logic) and the 2 new TodoPanel tests.

中文说明

代码审查

独立方案: 定义 WebShellBottomStatusItem 类型,作为 prop 添加,在 TodoPanel 中用分隔符渲染,使用 CSS bottom 定位保持浮动面板在 composer 上方。对于滚动跟随,保持现有状态机不变,只确保面板高度通过 CSS 变量反映为底部 padding。

对比: 状态项部分与我的方案接近——类型干净,TodoPanel 条件渲染干净,CSS 干净。滚动跟随部分远超我会做的范围:followPausedByUserRef(在 10+ 处修改),userScrollIntentUntil 配合 4 个事件监听器,以及新的 pendingTranscriptBottomScroll 生命周期。这是对滚动状态机的大规模改造,与一个相对较小的扩展点功能捆绑在一起。

代码中未发现正确性 bug。新的状态转换内部一致,命名清晰。担心的是维护面——滚动跟随行为现在有更多活动部件,时序相关的交互(1 秒 userScrollIntent 窗口、ResizeObserver 回调、RAF 链)很难仅从代码推断。

复用检查: 无重复逻辑或遗漏的复用机会。

测试结果

138 项测试全部通过,构建成功,无 TypeScript 错误。

Tmux 测试

不适用——这是 WebShell React 组件功能,需要浏览器宿主应用通过 API 传入 bottomStatusItems,无法通过 CLI 或终端测试。行为已被 136 个现有 MessageList 测试(包括 43 个测试滚动跟随逻辑的 DOM 测试)和 2 个新 TodoPanel 测试覆盖。

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

The core feature — bottomStatusItems prop, TodoPanel rendering, type definition, exports — is clean, minimal, and well-tested. It fills a real gap for WebShell embedders and follows the established customization prop pattern. The 2 new TodoPanel tests cover both the combined (todos + status) and status-only rendering paths. All 136 existing MessageList tests still pass.

The scroll-follow overhaul is the part I'd flag. ~172 lines of new scroll state machinery in MessageList — followPausedByUserRef, userScrollIntentUntil with 4 event listeners, pendingTranscriptBottomScroll lifecycle — is a lot of complexity for what started as a "add status items" feature. My independent proposal would have left the scroll state machine alone and just adjusted CSS padding. The author's approach is more thorough (it prevents the follow-toggle from fighting panel layout changes), but it's also harder to reason about long-term. The 1-second userScrollIntentUntil window in particular feels like it could produce subtle timing-dependent behavior.

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. ✅

中文说明

核心功能——bottomStatusItems prop、TodoPanel 渲染、类型定义、导出——干净、最小化、测试充分。它填补了 WebShell 集成方的真实缺口,遵循了已有的自定义 prop 模式。2 个新 TodoPanel 测试覆盖了组合(todos + status)和纯 status 渲染路径。全部 136 个现有 MessageList 测试仍通过。

滚动跟随改造是我要指出的部分。MessageList 中约 172 行新的滚动状态机制——followPausedByUserRefuserScrollIntentUntil 配合 4 个事件监听器、pendingTranscriptBottomScroll 生命周期——对于一个"添加状态项"功能来说复杂度较高。我的独立方案会保持滚动状态机不变,仅调整 CSS padding。作者的方式更彻底(防止 follow-toggle 与面板布局变化冲突),但也更难长期推理。特别是 1 秒的 userScrollIntentUntil 窗口可能产生微妙的时序依赖行为。

尽管如此:未发现正确性 bug,所有测试通过,构建干净,滚动变更内部结构良好、命名清晰。作者显然理解滚动系统并做出了深思熟虑的选择。担心的是维护成本而非正确性——这是维护者可以权衡的取舍。

总体 LGTM,滚动复杂度提请留意。✅

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. ✅

@dreamWB
dreamWB added this pull request to the merge queue Jul 9, 2026
Merged via the queue into QwenLM:main with commit c412d62 Jul 9, 2026
38 of 41 checks passed
} else if (hasUserScrollIntent) {
followPausedByUserRef.current = true;
setShouldFollow(false);
} else if (!followPausedByUserRef.current) {

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.

[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.

Suggested change
} 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

@wenshao

wenshao commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

Suggestions — commit 42ee94df7

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

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.

4 participants