Skip to content

fix(web-shell): dedupe restored images and harden the sidebar shortcut handler - #7169

Merged
wenshao merged 1 commit into
QwenLM:mainfrom
zjunothing:fix/7134-followup-image-dedup
Jul 18, 2026
Merged

fix(web-shell): dedupe restored images and harden the sidebar shortcut handler#7169
wenshao merged 1 commit into
QwenLM:mainfrom
zjunothing:fix/7134-followup-image-dedup

Conversation

@zjunothing

Copy link
Copy Markdown
Collaborator

What this PR does

Applies the four post-merge automated-review follow-ups left on #7134 and #7135. Restored prompt images are now deduplicated together with the text: restoreImages appends to the pasted-image list, so a second restore of the same prompt (reconnect/retry) previously doubled the attachments while the text correctly stayed single. The Cmd+B/Ctrl+B sidebar shortcut is hardened three ways: it no longer fires while a non-composer editable element is focused (sidebar search, session rename, settings inputs — the composer intentionally keeps the editor-convention behavior and still toggles); all state updates are dispatched sequentially outside React updater functions (purity contract, mirroring the existing hamburger handler), with the effect re-binding on the involved state so the listener closure stays fresh; and a drawer forced open on a wide viewport now takes the drawer path, closing the overlay instead of collapsing the rail invisibly underneath it.

Why it's needed

Both PRs merged with non-blocking inline suggestions from the automated review that are real correctness gaps: doubled image attachments on duplicate restores produce an inconsistent editor state exactly in the failure-recovery flow #7128 was about; a shortcut firing inside the sidebar's own search or rename inputs yanks the sidebar away mid-typing; side effects inside updater functions violate React's purity contract and can misbehave under strict/concurrent double-invocation; and the forced-drawer branch made the shortcut look like a no-op on wide viewports.

Reviewer Test Plan

How to verify

  1. Fail a prompt submission that carries pasted images, let the restore run twice (reconnect): the composer holds one copy of the text and one set of the images.
  2. Focus the sidebar search input (or a session rename field) and press Ctrl+B: the sidebar does not move. Focus the composer and press Ctrl+B: it still toggles (editor convention preserved).
  3. On a wide viewport, open the session drawer via the hamburger, then press Ctrl+B: the drawer closes; the rail is not collapsed underneath the overlay.
  4. Toggle repeatedly: the collapse preference still persists across reloads.

Automated coverage: the guard branch semantics are pinned by the existing mergeRestoredPromptText suite (6 tests) and matcher suite (5 tests) — 11/11 pass; the image call now lives inside the already-tested text-change branch. npm run typecheck, the web-shell package build, eslint and prettier are clean.

Evidence (Before & After)

Real-browser E2E (Playwright Chromium against a live qwen serve daemon built from this branch):

composer-focused Ctrl+B: 260 -> 56   (still toggles — editor convention kept ✓)
search-input Ctrl+B:     width 260   (guarded, no toggle ✓)

Before this change the search-input case toggled the sidebar mid-typing; the composer behavior is unchanged from #7135's original verification.

Tested on

OS Status
🍏 macOS
🪟 Windows ⚠️
🐧 Linux ⚠️

Environment (optional)

macOS (Darwin 24.6), Node v22.23.1; Playwright Chromium against a live qwen serve daemon, plus vitest unit tests.

Risk & Scope

  • Main risk or tradeoff: the shortcut-handler effect now re-binds its window listener when drawer/collapse state changes — a single listener add/remove per state change, negligible. Images are only restored alongside a text change; the edge where the editor already contains identical manually-typed text while a first-time restore carries images will skip the images — acceptable and conservative for a failure-recovery path.
  • Not validated / out of scope: no direct unit test drives the image-restore branch (it lives behind the editor-ref boundary); its guard branch is covered by the mergeRestoredPromptText suite and the behavior was reasoned through the same dedup semantics. The forced-drawer scenario was implemented per the review finding and manually reasoned; automation covers the desktop paths.
  • Breaking changes / migration notes: none.

Linked Issues

Refs #7102, #7128 (follow-ups to the reviews on #7134 and #7135)

中文说明

本 PR 做了什么

落实 #7134#7135 合并后自动评审留下的四条 follow-up。恢复失败 prompt 时图片随文本一起去重:restoreImages 是向粘贴图片列表追加,同一 prompt 的第二次恢复(重连/重试)此前会让附件翻倍而文本保持单份。Cmd+B/Ctrl+B 侧栏快捷键三处加固:非 composer 的可编辑元素聚焦时不再触发(侧栏搜索、会话重命名、设置输入框——composer 有意保留编辑器惯例仍可切换);全部状态更新移出 React updater 函数(纯度契约,对齐既有汉堡按钮 handler),effect 依赖相关状态重绑定保证闭包新鲜;宽视口下被强制打开的抽屉走抽屉路径,快捷键关闭覆盖层而不是在其下面不可见地折叠侧栏。

为什么需要

两个 PR 合并时带着自动评审的非阻塞行内建议,但都是真实的正确性缺口:重复恢复时图片翻倍恰好发生在 #7128 关注的失败恢复流程里;快捷键在侧栏自己的搜索/重命名输入框里触发会在输入途中拽走侧栏;updater 内副作用违反 React 纯度契约、strict/并发双调用下可能不一致;强制抽屉分支让快捷键在宽视口看起来像无操作。

审阅测试计划

如何验证

  1. 让带粘贴图片的 prompt 提交失败并触发两次恢复(重连):composer 中文本一份、图片一组;
  2. 聚焦侧栏搜索框(或会话重命名框)按 Ctrl+B:侧栏不动;聚焦 composer 按 Ctrl+B:仍切换(编辑器惯例保留);
  3. 宽视口用汉堡按钮打开会话抽屉后按 Ctrl+B:抽屉关闭,侧栏不会在覆盖层下被折叠;
  4. 反复切换:折叠偏好仍跨刷新持久化。

自动化覆盖:守卫分支语义由既有 mergeRestoredPromptText(6 例)与匹配器(5 例)套件固定——11/11 通过;图片调用现位于已被测试的文本变更分支内。typecheck / web-shell 构建 / eslint / prettier 全绿。

证据(Before & After)

真实浏览器 E2E(Playwright Chromium 对本分支构建的真实 qwen serve):composer 聚焦 Ctrl+B 仍切换(260→56);搜索框聚焦 Ctrl+B 被守卫(保持 260)。改动前搜索框场景会在输入途中切换侧栏;composer 行为与 #7135 原验证一致。

测试平台

macOS 已本地验证(✅);Windows / Linux 依赖 CI(⚠️)。

环境

macOS(Darwin 24.6)、Node v22.23.1;Playwright Chromium + 真实 daemon + vitest 单测。

风险与范围

  • 主要风险/权衡:快捷键 effect 随抽屉/折叠状态重绑定 window listener——每次状态变化一次增删,开销可忽略。图片仅随文本变更恢复;「编辑器已含相同手输文本且首次恢复带图」的边缘场景会跳过图片——对失败恢复路径而言保守可接受。
  • 未验证/超出范围:图片恢复分支无直接单测(位于 editor-ref 边界之后);其守卫分支由 mergeRestoredPromptText 套件覆盖。强制抽屉场景按 review 发现实现并人工推演;自动化覆盖桌面路径。
  • 破坏性变更/迁移说明:无。

关联 Issue

Refs #7102, #7128#7134#7135 评审的 follow-up)

🤖 Generated with Claude Code

…t handler

Post-merge review follow-ups from QwenLM#7134 and QwenLM#7135:

- restoreImages now runs only alongside an actual text change: it
  appends to the pasted-image list, so a deduplicated restore of the
  same prompt (reconnect/retry) previously doubled the attachments
  while the text correctly stayed single.

- The Cmd+B/Ctrl+B handler no longer toggles the sidebar while a
  non-composer editable element is focused (sidebar search, session
  rename, settings inputs) — matching the codebase's isEditableTarget
  convention; the composer keeps the editor-convention behavior and
  still toggles, verified in a real browser.

- All state updates moved out of React updater functions (purity
  contract, mirroring the hamburger handler); the effect re-binds on
  the involved state so the listener closure stays fresh.

- A drawer forced open on a wide viewport now takes the drawer path:
  the shortcut closes the overlay instead of collapsing the rail
  invisibly underneath it.

Refs QwenLM#7102 QwenLM#7128

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

🖼️ web-shell visual preview

Rendered against a mock daemon (no real backend): the PR base vs this PR head e641e9c. Only screenshots that changed are shown (flows below, if any, are head-only) — refreshes on every push.

Screenshots · before / after

No screenshot changes against the PR base.

Full-resolution recordings (.webm) are attached to the workflow run.

Qwen Code · web-shell visuals

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR!

Template looks good ✓ — all required sections present, bilingual, with Playwright evidence in the "Before & After" slot.

Problem: All four issues are observable directly from the code, not theoretical:

  • restoreImages appends to the pasted-image list, so calling it on a deduplicated restore (text unchanged, images re-appended) doubles attachments — the append semantics are self-evident.
  • The Ctrl+B handler uses window.addEventListener('keydown', ...) with no editable-target guard, so it fires inside sidebar search / session rename inputs.
  • setForceMobileDrawer(false) inside the setMobileDrawerOpen updater is a React purity violation.
  • The old mobile-drawer branch checked only viewport width, not forceMobileDrawer, so a forced drawer on a wide viewport would incorrectly take the rail-collapse path.

No live reproduction needed — the bugs are legible from the code structure.

Direction: Aligned. These are follow-ups to already-merged PRs (#7134, #7135) addressing automated-review suggestions that were left as non-blocking but represent real correctness gaps. Closing the loop on merged-PR review debt is good hygiene.

Size: 46 additions + 14 deletions across 2 web-shell files. No core modules touched — not applicable.

Approach: Scope feels right — four small, focused fixes in two files, no unrelated churn. The dedup fix (moving restoreImages inside the text-change branch) is the minimal correct change. The editable-target guard with composer exception mirrors the VS Code convention the codebase already follows. Moving state updates outside the updater mirrors the existing hamburger handler pattern. The effect re-binding on state changes is an acceptable tradeoff for keeping the closure fresh without refs.

Moving on to code review. 🔍

中文说明

感谢贡献!

模板完整 ✓ — 所有必填章节齐全,双语,"Before & After" 中有 Playwright 证据。

问题: 四个问题均可从代码直接观测,非理论性:

  • restoreImages 向列表追加,重复恢复时文本已去重但图片翻倍——追加语义自明。
  • Ctrl+B handler 使用 window.addEventListener 无编辑目标守卫,在侧栏搜索/重命名输入框中也会触发。
  • setForceMobileDrawer(false)setMobileDrawerOpen updater 内调用,违反 React 纯度契约。
  • 旧移动抽屉分支仅检查视口宽度,不检查 forceMobileDrawer,宽视口强制抽屉会错误走侧栏折叠路径。

无需实时复现——bug 从代码结构即可看清。

方向: 对齐。这是对已合并 PR(#7134#7135)自动评审遗留建议的跟进,属于真实正确性缺口。关闭已合并 PR 的评审债是良好习惯。

规模: 46 行新增 + 14 行删除,2 个 web-shell 文件,未触及核心模块——不适用。

方案: 范围合理——两个文件中四个小而聚焦的修复,无无关改动。去重修复(将 restoreImages 移入文本变更分支)是最小正确改动。编辑目标守卫配合 composer 例外,对齐代码库已有的 VS Code 惯例。状态更新移出 updater 对齐既有汉堡按钮 handler 模式。effect 随状态变化重绑定是为了保持闭包新鲜的合理权衡。

进入代码审查 🔍

Qwen Code · qwen3.7-max

Reviewed at e641e9c655bef8434d05f3875e6e49b27ba48cb6 · re-run with @qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Code review

Independent proposal before reading the diff: move restoreImages inside the text-change branch for dedup, add isEditableTarget guard with composer exception for the shortcut, move state updates outside updaters for React purity, and check forceMobileDrawer before viewport width for the drawer path.

The PR matches this proposal exactly. Two files, 60 lines — nothing extraneous.

No critical issues found. Specifically:

  • isEditableTarget is already imported at line 154 of App.tsx, and data-web-shell-composer-editor is a real attribute on the composer (ChatEditor.tsx:1998). The guard's closest-check correctly carves out the composer.
  • The old setMobileDrawerOpen((open) => { if (open) setForceMobileDrawer(false); return !open }) pattern had a side effect inside the updater — the new sequential-dispatch approach mirrors the hamburger handler at lines 2268–2269.
  • The forceMobileDrawer || matchMedia branch correctly routes a forced drawer on a wide viewport to the drawer path (close drawer + clear force) rather than the rail-collapse path. When the drawer is already open, both forceMobileDrawer and mobileDrawerOpen are cleared — that's the right behavior.
  • The effect dependency expansion (mobileDrawerOpen, forceMobileDrawer, sidebarCollapsed) causes one listener remove/add per state change. Negligible cost, and it keeps the closure fresh without refs.
  • Image dedup: restoreImages now runs only when next !== current — conservative, since an unchanged editor that already has the correct text won't get images re-appended. The edge case the PR describes (manually typed identical text + first-time restore with images) is unlikely in practice and the failure-recovery path is appropriately conservative.

Reuse check: No new utilities or abstractions added — the PR reuses isEditableTarget, mergeRestoredPromptText, writeSidebarCollapsed, and the existing data-web-shell-composer-editor attribute. Clean.

Testing

Unit tests (verified locally)

$ cd packages/web-shell && vitest run client/hooks/useQueuedPrompts.test.ts client/components/sidebar/sidebarToggleShortcut.test.ts

 ✓ hooks/useQueuedPrompts.test.ts > mergeRestoredPromptText > fills an empty editor with the restored text
 ✓ hooks/useQueuedPrompts.test.ts > mergeRestoredPromptText > prepends above a different draft the user is typing
 ✓ hooks/useQueuedPrompts.test.ts > mergeRestoredPromptText > is a no-op when the same text was already restored
 ✓ hooks/useQueuedPrompts.test.ts > mergeRestoredPromptText > is a no-op when the text already sits at the top of the editor
 ✓ hooks/useQueuedPrompts.test.ts > mergeRestoredPromptText > stays idempotent across repeated restores of the same prompt
 ✓ hooks/useQueuedPrompts.test.ts > mergeRestoredPromptText > does not treat a same-prefix but different first line as a duplicate

 ✓ components/sidebar/sidebarToggleShortcut.test.ts (5 tests)

 Test Files  2 passed (2)
      Tests  11 passed (11)

Browser E2E (author-provided, Playwright Chromium)

composer-focused Ctrl+B: 260 -> 56   (still toggles — editor convention kept ✓)
search-input Ctrl+B:     width 260   (guarded, no toggle ✓)

tmux note

This is a web-shell (browser-rendered React) change, not a CLI TUI change — tmux capture-pane would show a terminal prompt, not the sidebar UI. The Playwright evidence above is the appropriate verification path for browser UI. The unit test suites pin the dedup semantics and shortcut matcher logic.

中文说明

代码审查

读 diff 前的独立方案:将 restoreImages 移入文本变更分支以去重,为快捷键加 isEditableTarget 守卫并排除 composer,将状态更新移出 updater 以保证 React 纯度,在视口宽度检查前先检查 forceMobileDrawer 以正确路由抽屉路径。

PR 与方案完全一致。两个文件,60 行——无多余内容。

未发现关键问题:

  • isEditableTarget 已在 App.tsx 154 行导入,data-web-shell-composer-editor 是 composer 上的真实属性(ChatEditor.tsx:1998)。守卫的 closest 检查正确排除了 composer。
  • 旧的 setMobileDrawerOpen updater 内含副作用——新的顺序派发方式对齐 2268–2269 行的汉堡按钮 handler。
  • forceMobileDrawer || matchMedia 分支正确将宽视口强制抽屉路由到抽屉路径(关闭抽屉 + 清除强制),而非侧栏折叠路径。
  • effect 依赖扩展(mobileDrawerOpenforceMobileDrawersidebarCollapsed)每次状态变化一次 listener 增删,开销可忽略。
  • 图片去重:restoreImages 仅在 next !== current 时执行——保守策略,编辑器未变时不追加图片。PR 描述的边缘场景(手输相同文本 + 首次恢复带图)实际罕见,失败恢复路径保守合理。

复用检查: 未新增工具函数或抽象——复用 isEditableTargetmergeRestoredPromptTextwriteSidebarCollapsed 及既有 data-web-shell-composer-editor 属性。整洁。

测试

单元测试(本地验证)

✓ hooks/useQueuedPrompts.test.ts (6 tests) — mergeRestoredPromptText 全套
✓ components/sidebar/sidebarToggleShortcut.test.ts (5 tests)

Test Files  2 passed (2)
     Tests  11 passed (11)

浏览器 E2E(作者提供,Playwright Chromium)

composer 聚焦 Ctrl+B: 260 -> 56   (仍切换——编辑器惯例保留 ✓)
搜索框聚焦 Ctrl+B:    width 260   (已守卫,未切换 ✓)

tmux 说明

此为 web-shell(浏览器渲染 React)变更,非 CLI TUI 变更——tmux capture-pane 只能显示终端提示符,无法显示侧栏 UI。上述 Playwright 证据是浏览器 UI 的合适验证路径。单元测试套件固定了去重语义和快捷键匹配逻辑。

Qwen Code · qwen3.7-max

Reviewed at e641e9c655bef8434d05f3875e6e49b27ba48cb6 · re-run with @qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Confidence: 4/5 — Clean, focused follow-up PR that fixes four observable correctness gaps in two files; minor reservation is that the image-restore branch lacks direct unit test coverage (acknowledged by the author).

My independent proposal before reading the diff was identical to the PR's approach — dedup inside the text-change branch, editable-target guard with a composer carve-out, sequential state dispatch outside updaters, and forceMobileDrawer check before viewport width. The PR doesn't miss a simpler path.

What I liked: the scope is tight — four fixes, two files, no drive-by refactors or scope creep. Each change reuses an existing utility or attribute (isEditableTarget, data-web-shell-composer-editor, the hamburger handler pattern) rather than inventing something new. The comments in the diff explain why without over-narrating. Unit tests (11/11) pin the dedup semantics and the shortcut matcher, and the Playwright evidence shows the guard working in a real browser.

Minor reservation: the image-restore branch inside restoreTextToEditor has no direct unit test — it's behind the editor-ref boundary. The guard branch is covered by mergeRestoredPromptText, but the actual restoreImages call in that branch is untested. Acceptable for a failure-recovery path, and the author flagged this honestly in the "Risk & Scope" section.

Ship it. ✅

中文说明

信心度:4/5 — 简洁、聚焦的跟进 PR,在两个文件中修复了四个可观测的正确性缺口;小保留意见是图片恢复分支缺乏直接单测覆盖(作者已承认)。

读 diff 前的独立方案与 PR 方案一致——文本变更分支内去重、编辑目标守卫配合 composer 例外、updater 外顺序状态派发、视口宽度检查前先检查 forceMobileDrawer。PR 没有遗漏更简路径。

优点:范围紧凑——四个修复,两个文件,无顺手重构或范围蔓延。每个改动复用既有工具或属性(isEditableTargetdata-web-shell-composer-editor、汉堡按钮 handler 模式),未引入新抽象。diff 中的注释说明为什么而非赘述。单元测试(11/11)固定了去重语义和快捷键匹配器,Playwright 证据显示守卫在真实浏览器中生效。

小保留意见:restoreTextToEditor 中的图片恢复分支无直接单测——位于 editor-ref 边界之后。守卫分支由 mergeRestoredPromptText 覆盖,但该分支中的 restoreImages 调用未被测试。对失败恢复路径而言可接受,作者在"风险与范围"中也坦诚标注了这一点。

可以合并 ✅

Qwen Code · qwen3.7-max

Reviewed at e641e9c655bef8434d05f3875e6e49b27ba48cb6 · re-run with @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. ✅

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

Reviewed. Suggestions are inline.

— qwen3.7-max via Qwen Code /review

Comment on lines +1183 to +1185
} else {
setMobileDrawerOpen(true);
}

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] The keyboard shortcut's drawer-open branch does not clear forceMobileDrawer, while the hamburger handler (line 6434) always calls setForceMobileDrawer(false) before toggling. — Concrete cost: today the state forceMobileDrawer === true && mobileDrawerOpen === false is unreachable, but a future change that independently closes the drawer without clearing forceMobileDrawer would leave the keyboard shortcut stuck in forced-drawer mode on wide viewports. The hamburger would handle that state correctly; the keyboard shortcut would not.

Suggested change
} else {
setMobileDrawerOpen(true);
}
} else {
setForceMobileDrawer(false);
setMobileDrawerOpen(true);
}

— qwen3.7-max via Qwen Code /review

Comment on lines +312 to +314
if (images && images.length > 0) {
editorRef.current?.restoreImages(images);
}

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 test verifies that restoreTextToEditor skips restoreImages when the text was already restored (the deduplicated path). — Failure scenario: a future refactor moves restoreImages back outside the next !== current guard. Because restoreImages appends (setPastedImages((prev) => [...prev, ...images])), a prompt with images restored twice across a reconnect would double the image attachments while the text correctly stays deduplicated — reintroducing the #7134 bug for images.

Suggested fix: add a test that calls restoreTextToEditor twice with the same text+images and asserts the mock editor's restoreImages is called only once.

— qwen3.7-max via Qwen Code /review

Comment on lines +1162 to +1167
if (
isEditableTarget(target) &&
!target?.closest('[data-web-shell-composer-editor]')
) {
return;
}

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] The Cmd+B keyboard shortcut handler has no test coverage for its three new branches — editable-target exemption, forced-drawer close on wide viewport, and direct state reads replacing updater functions. — Failure scenario: a future change to the effect's dependency array or closure structure could regress the editable-target guard (Cmd+B toggles the sidebar while the user is typing in the sidebar search field) or the forced-drawer path without any test catching it.

Suggested fix: add a test that fires a Cmd+B keydown on window and asserts: (a) the sidebar does not toggle when focus is in an <input>, (b) a forced drawer on a wide viewport closes, and (c) the drawer opens/closes on a narrow viewport.

— qwen3.7-max via Qwen Code /review

@wenshao

wenshao commented Jul 18, 2026

Copy link
Copy Markdown
Collaborator

✅ Local verification — real end-to-end tests (maintainer)

Built and exercised this PR locally at head e641e9c6 (on base af6a9b64, which already carries #7134 + #7135). Both follow-ups were driven through the real production code paths — not just the extracted helpers — and each fix was confirmed load-bearing with a controlled experiment that reverts only the changed line and watches the test fail.

1) Restored images dedupe with the text — useQueuedPrompts.ts

Real-hook E2E (jsdom + createRoot/act) drives the actual useQueuedPrompts hook with a stateful mock editor. A failed submit runs the exact restoreTextToEditor(text, images) path this PR hardens.

Scenario restoreImages calls Editor text
Same prompt restored twice (reconnect/retry) 1 single copy ✓
Two different prompts restored 2 ✓ prepended, both kept ✓
  • Fix confirmed load-bearing: moving the restoreImages call back outside the text-change guard (the pre-fix(web-shell): dedupe restored images and harden the sidebar shortcut handler #7169 position) makes the same test fail with 2 calls on the duplicate restore — i.e. the doubled attachments the PR describes. Restoring the guard → green again.
  • The "two different prompts" case proves this is a dedup, not a blanket suppression: images still flow whenever the text genuinely changes.

2) Sidebar Cmd/Ctrl+B hardening — App.tsx

Playwright (Chromium) against the real App (dark, sidebar enabled, StrictMode) on a wide 1280×860 viewport with a live mock daemon. Sidebar rail = 260px expanded / 56px collapsed; forced drawer = [data-sidebar-shell][role="dialog"].

Scenario Expected Result
Nothing focused → Ctrl+B toggles 260 ↔ 56
Composer focused → Ctrl+B still toggles 260 → 56 (editor convention kept), draft untouched
Sidebar search focused → Ctrl+B guarded — stays 260, query preserved
Forced drawer (wide) → Ctrl+B drawer closes, rail stays expanded (no silent collapse)

Load-bearing controlled experiments (each isolates one changed line):

  • Neuter the isEditableTarget guard → the search-input case collapses to 56 (regresses). ✅ guard is doing the work.
  • Drop forceMobileDrawer || from the drawer condition → Ctrl+B leaves the drawer open while collapsing the rail underneath — the pre-fix visual no-op. ✅ branch is doing the work.

guard

drawer

3) Regression gates

  • Cited suites: mergeRestoredPromptText (6) + sidebarToggleShortcut (5) = 11/11
  • Full web-shell unit suite: 117/117 files, 1889 tests green (built the lib bundle so build-artifact.test.ts reads a real artifact; its only failures without a dist/ are ENOENT, PR-independent).
  • Builds: vite build (app) + vite build (lib) both clean.
  • ESLint clean on both changed files; Prettier clean on both changed files.
  • tsc --noEmit: 0 errors in the changed files. The 23 pre-existing errors are all in unrelated files (GitBranchIndicator, GitDiffDialog, GoalsDialog) — SDK/webui dist drift in my symlinked verification worktree, untouched by this PR.

Verdict

LGTM — ready to merge. All four review follow-ups behave as described, each backed by a real-flow test and a load-bearing revert; no regressions in the package's unit suite, build, lint, format, or typecheck.

Env: macOS (Darwin 24.6), Node v22.23.1, Playwright Chromium. Screenshots are Playwright captures of the real app in these scenarios.

中文版本(点击展开)

✅ 本地验证 —— 真实端到端测试(维护者)

在 head e641e9c6(基线 af6a9b64,已含 #7134 + #7135)本地构建并验证。两条 follow-up 都通过真实的生产代码路径驱动(而非仅测提取出的辅助函数),且每处修复都用「只回退被改的那一行、观察测试变红」的对照实验确认其确实起作用

1)恢复图片随文本一起去重 —— useQueuedPrompts.ts

真实 hook 级 E2E(jsdom + createRoot/act)驱动真正的 useQueuedPrompts,配一个有状态的 mock 编辑器;提交失败会走本 PR 加固的 restoreTextToEditor(text, images) 路径。

场景 restoreImages 调用次数 编辑器文本
同一 prompt 恢复两次(重连/重试) 1 单份 ✓
两个不同 prompt 恢复 2 ✓ 前置追加,均保留 ✓

2)侧栏 Cmd/Ctrl+B 加固 —— App.tsx

Playwright(Chromium)对真实 App(暗色、启用侧栏、StrictMode),宽视口 1280×860,配真实 mock daemon。侧栏轨道展开 260px / 折叠 56px;强制抽屉 = [data-sidebar-shell][role="dialog"]

场景 期望 结果
无聚焦 → Ctrl+B 260 ↔ 56 切换
composer 聚焦 → Ctrl+B 仍切换 260 → 56(保留编辑器惯例),草稿不动
侧栏搜索框聚焦 → Ctrl+B 被守卫 —— 保持 260,查询词保留
强制抽屉(宽视口)→ Ctrl+B 抽屉关闭,轨道保持展开(不在其下悄悄折叠)

对照实验(每个只隔离一处改动):

  • 使 isEditableTarget 守卫失效 → 搜索框场景折叠到 56(回归)。✅ 守卫确实生效。
  • 从抽屉条件里去掉 forceMobileDrawer || → Ctrl+B 让抽屉保持打开、轨道在其下折叠 —— 即修复前的视觉无操作。✅ 该分支确实生效。

(截图见上方英文版。)

3)回归门禁

  • PR 引用的套件: mergeRestoredPromptText(6)+ sidebarToggleShortcut(5)= 11/11
  • web-shell 全量单测: 117/117 文件、1889 测试通过(先构建 lib bundle,使 build-artifact.test.ts 读到真实产物;无 dist/ 时它仅报 ENOENT,与本 PR 无关)。
  • 构建: vite build(app)+ vite build(lib)均通过。
  • ESLint 对两个改动文件干净;Prettier 对两个改动文件干净。
  • tsc --noEmit 改动文件 0 错误。那 23 个既有错误全在无关文件(GitBranchIndicatorGitDiffDialogGoalsDialog)—— 我这套 symlink 验证工作树里的 SDK/webui dist 漂移,本 PR 未触及。

结论

LGTM —— 可合并。 四条评审 follow-up 行为均符合描述,各有真实流程测试 + 载荷回退佐证;单测、构建、lint、格式化、类型检查均无回归。

环境:macOS(Darwin 24.6)、Node v22.23.1、Playwright Chromium。截图为真实应用在各场景下的 Playwright 抓图。

@wenshao
wenshao added this pull request to the merge queue Jul 18, 2026
Merged via the queue into QwenLM:main with commit 4505706 Jul 18, 2026
80 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