Skip to content

fix(web-shell): inset docked right panel below the macOS titlebar drag region - #12876

Merged
yiliang114 merged 2 commits into
mainfrom
fix/issue-12874-right-panel-toggle
Sep 28, 2026
Merged

yiliang114 merged 2 commits into
mainfrom
fix/issue-12874-right-panel-toggle

Conversation

@yiliang114

@yiliang114 yiliang114 commented Sep 27, 2026 •

Copy link
Copy Markdown
Collaborator

What this PR does

On the macOS Desktop shell, the docked right panel (file changes / side tasks / web preview / trajectory / terminal) could not be closed once opened: the header toggle unmounts on open by design, the panel's own toolbar button takes over the close action — and that button sat under the fixed 38px Tauri titlebar drag region, so clicks turned into window drags. This PR gives .artifactPanelDock the same --web-shell-desktop-titlebar-height top inset that .contextShell and the sidebar already have, and adds an e2e regression spec for both the plain-browser hand-over path and the macOS occlusion path.

Why it's needed

The dock portals into a display: contents slot, so it is a direct .appShell flex child — a sibling of .contextShell, not a descendant — and never received the 38px titlebar inset. The panel toolbar (min-height: 52px, 30px vertically-centred toggle button, y≈11–41) renders at the very top of the window, underneath the drag region (position: fixed, z-index: 40, y 0–38). With the header toggle unmounted by design, Esc only wired for the fullscreen variant, and the resize divider clamping at MIN_ARTIFACT_PANEL_WIDTH, no close path remained — and because panel state persists across restarts, the panel came back open after relaunch too. App.test.tsx already documents this failure mode: "If the views ever move out of the padded shell, the desktop drag strip overlaps their header controls."

Reviewer Test Plan

How to verify

Reproduced and verified in a real browser against the web-shell dev server (packages/web-shell, mock daemon harness), 1440x900 viewport (docked path):

  1. Without the flag: open the panel from the chat header "Toggle right panel" button → the header button unmounts (by design, pinned in App.test.tsx) and the panel toolbar's own toggle (same label, aria-pressed="true") closes the panel. Already passing before this PR.
  2. With window.__QWEN_CODE_MACOS_TITLEBAR__ = true injected (what the Tauri shell sets on macOS): before this PR, document.elementFromPoint at the panel toggle's center returns .qwen-code-macos-titlebar-drag-region and a real mouse click there never reaches the button — panel stays open. After this PR, the panel toolbar renders below the strip, the click lands, and the panel closes.

Automated: npx playwright test --config playwright.config.ts web-shell.right-panel-macos-titlebar --project=chromium in packages/web-shell (2 passed; the macOS test fails on main at the occlusion assertion), plus npx vitest run client/App.test.tsx -t "right panel" (10 passed, confirms the pinned hand-over behavior is unchanged) and npm run typecheck in packages/web-shell (clean).

Evidence (Before & After)

Before (macOS flag on): panel toolbar sits at the very top of the window, under the drag region — the toggle button's click point belongs to the strip, clicking does not close the panel.

before: panel open, toolbar under drag region

After (macOS flag on): panel toolbar is inset 38px, level with the chat header; clicking the toggle closes the panel.

after: panel open, toolbar below drag region

after: click closes the panel, chat pane back to full width

Tested on

OS Status
🍏 macOS ⚠️ not tested (defective layer is the shared web-shell; flag injected in Chromium reproduces the exact occlusion)
🪟 Windows N/A (no overlay titlebar; padding-top resolves to 0)
🐧 Linux ✅ tested (browser harness, flag on + flag off)

Environment (optional)

Vite dev server + Playwright Chromium with the repo's mock-daemon e2e harness.

Risk & Scope

  • Main risk or tradeoff: the dock loses 38px of vertical room on macOS Desktop only — same tradeoff the chat pane and sidebar already make. The resize divider's top 38px is no longer draggable on that platform; that region was a window-drag zone anyway.
  • Not validated / out of scope: the fullscreen panel variant (Esc works there, and its z-index/DOM order keeps it above the strip) and the narrow-screen floating drawer (Radix dismiss layer) — neither is part of the reported symptom.
  • Breaking changes / migration notes: none. Without the desktop titlebar flag the CSS variable is unset and padding-top resolves to 0, so browser/Linux/Windows rendering is byte-identical.
  • CI lane for the new spec — nightly / manual dispatch only, by decision: neither test carries @smoke, and PR CI runs only --grep @smoke (ci.yml:1552, in a job additionally gated on ci_profile == 'full'). The spec actually runs in the full lane at e2e.yml:787, gated to schedule || workflow_dispatch (e2e.yml:733), i.e. the 0 4 * * * nightly. So it guards against someone silently undoing the inset; it does not gate a PR. That is deliberate, because the smoke job's own comment (ci.yml:1354-1356) records 184 tests plus ~7.5 min of setup already finishing "right at 20 min" of a 30-min timeout, with sharding as the real fix.

Linked Issues

Fixes #12874

中文说明

这个 PR 做了什么

在 macOS Desktop 壳里,停靠态右侧面板(文件更改/侧边任务/网页预览/轨迹/终端)一旦展开就无法关闭:头部 toggle 按钮在面板展开后按设计卸载,面板自身工具栏上的按钮接管关闭动作——而那个按钮正好被顶部固定的 38px Tauri 标题栏拖拽条压住,点击变成了窗口拖动。本 PR 给 .artifactPanelDock 补上 .contextShell 和侧边栏已有的 --web-shell-desktop-titlebar-height 顶部内缩,并新增一条 e2e 回归测试,同时覆盖普通浏览器的接管路径和 macOS 遮挡路径。

为什么需要

dock 通过 display: contents 的 slot portal 渲染,因此它是 .appShell 的直接 flex 子元素——是 .contextShell 的兄弟节点而非后代——从未获得 38px 标题栏内缩。面板工具栏(min-height: 52px,30px 高的 toggle 按钮垂直居中,约 y≈11–41)从窗口最顶端开始渲染,正好落在拖拽条(position: fixed、z-index: 40、y 0–38)之下。头部 toggle 按设计卸载、Esc 只接全屏变体、分隔线拖拽在 MIN_ARTIFACT_PANEL_WIDTH 处收敛——所有关闭路径都不存在;且面板状态跨重启持久化,重启后面板依然展开。App.test.tsx 已记录过这个失效模式:"If the views ever move out of the padded shell, the desktop drag strip overlaps their header controls."

验证方式

在真实浏览器中对 web-shell dev server(packages/web-shell,mock daemon harness)以 1440x900 视口(停靠路径)复现并验证:不注入 flag 时接管路径正常;注入 window.__QWEN_CODE_MACOS_TITLEBAR__ = true 后,修复前 document.elementFromPoint 在按钮中心命中拖拽条、真实鼠标点击无法关闭面板,修复后工具栏下移 38px、点击生效、面板关闭。自动化:packages/web-shell 下 npx playwright test --config playwright.config.ts web-shell.right-panel-macos-titlebar --project=chromium(2 passed,其中 macOS 用例在 main 上会红)、npx vitest run client/App.test.tsx -t "right panel"(10 passed,钉住的接管行为不变)、npm run typecheck(干净)。

风险与范围

  • 主要代价:macOS Desktop 上 dock 少 38px 纵向空间——与聊天区、侧边栏已有的取舍一致;分隔线顶部 38px 在该平台不再可拖,但那里本来就是窗口拖拽区。
  • 未覆盖:全屏变体(Esc 可用,z-index/DOM 顺序在拖拽条之上)与窄屏浮动 drawer(Radix dismiss 层),均不在报告症状内。
  • 无破坏性变更:无桌面标题栏 flag 时 CSS 变量未定义,padding-top 为 0,浏览器/Linux/Windows 渲染完全不变。
  • 新增 spec 的 CI lane——有意只在夜间/手动触发时跑:两条测试都没有 @smoke,而 PR CI 只跑 --grep @smoke(ci.yml:1552,且该 job 还额外受 ci_profile == 'full' 限制)。spec 实际由 e2e.yml:787 的完整 lane 执行,在 e2e.yml:733 被限制为 schedule || workflow_dispatch,即 0 4 * * * 夜间任务。所以它防的是有人悄悄撤掉这段内缩,并不拦 PR。这是刻意的:smoke job 自己在 ci.yml:1354-1356 的注释记录了 184 条测试加约 7.5 分钟准备已经「正好卡在 20 分钟」(超时上限 30 分钟),真正的修法是分片。

修复 #12874。

yiliang114 and others added 2 commits September 28, 2026 04:16
…g region

The docked right panel portals into a display:contents slot, so it is a
direct .appShell child and never received the 38px
--web-shell-desktop-titlebar-height inset that .contextShell and the
sidebar get. On the macOS Desktop shell the fixed titlebar drag region
(z-index 40, covering y 0-38px) sat on top of the panel toolbar's toggle
button, so once the panel was open and the header toggle had handed over
(by design), the replacement button could not be clicked and the panel
could not be closed.

Apply the same padding-top inset to .artifactPanelDock and add an e2e
regression spec covering both the plain-browser hand-over path and the
macOS-flag occlusion path.

Fixes #12874

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-issue-patrol/jmuk6uemf26
The Lint & Static gate failed on PR #12876 because the newly added
e2e spec was not Prettier-formatted. No behavior change.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-issue-patrol/jmukb4q6028
@yiliang114

Copy link
Copy Markdown
Collaborator Author

Answering the @smoke question deliberately, as asked — this spec stays untagged on purpose. It is a nightly / manual-dispatch asset, not a PR gate.

I verified the lane facts rather than taking the description's word for it:

  • test:e2e:smoke is playwright test --config playwright.config.ts --grep @smoke (packages/web-shell/package.json), run by the web_shell_e2e_smoke job at ci.yml:1552. Neither new test carries @smoke, so PR CI does not run them.
  • That job is additionally gated on needs.test.outputs.ci_profile == 'full' (ci.yml:1343-1350), so it does not run on every PR even for tagged specs.
  • The full lane that does run them is e2e.yml:787 (npm run test:e2e --workspace=packages/web-shell), gated at e2e.yml:733 to schedule || workflow_dispatch, i.e. the 0 4 * * * nightly (e2e.yml:16).

Why not just tag it:

  1. The smoke job's own comment at ci.yml:1354-1356 says "184 smoke tests plus ~7.5 min of setup now finish right at 20 min, so passing runs get cancelled in cleanup. Sharding is the real fix." Adding tests to a job whose recorded fix is sharding pushes it the wrong way, and the failure mode is not a red gate — it is passing runs being cancelled in cleanup, which is worse than no coverage because it looks like flake.
  2. That is a budget owned by whoever owns the smoke job, and you were right not to have me take it unilaterally.

So the honest statement of what this PR contributes: the four CSS lines are the fix, and the spec is a nightly guard against someone silently undoing the inset — it has teeth when it runs (the macOS occlusion assertion fails on main, per the test plan above), it just does not gate a PR. Both tests are mock-daemon Chromium tests, so tagging them later needs no macOS runner; if the smoke budget owner wants them in smoke once sharding lands, it is a two-word change and I will make it on request.

The description has been updated to say this plainly instead of implying PR-level gating.

The Prettier blocker was fixed in d6ef63e08f (style(web-shell): apply Prettier to right-panel macOS titlebar spec), which is the current head; Lint & Static is green on it.

中文说明

按你的要求给一个明确的决定,而不是默认放过:这条 spec 有意不打 @smoke,它是夜间/手动触发的资产,不是 PR 门禁。

我自己核过链路事实,没有照抄描述:

  • test:e2e:smoke 就是 playwright test --config playwright.config.ts --grep @smoke(packages/web-shell/package.json),由 ci.yml:1552 的 web_shell_e2e_smoke 执行。两条新测试都没有 @smoke,所以 PR CI 不会跑它们。
  • 该 job 还额外受 needs.test.outputs.ci_profile == 'full' 限制(ci.yml:1343-1350),即使打了标签也不是每个 PR 都跑。
  • 真正会跑它们的是 e2e.yml:787 的完整 lane,在 e2e.yml:733 被限制为 schedule || workflow_dispatch,也就是 0 4 * * * 的夜间任务(e2e.yml:16)。

为什么不直接打标签:

  1. smoke job 自己在 ci.yml:1354-1356 的注释写着「184 条 smoke 测试加约 7.5 分钟准备现在已经正好卡在 20 分钟,通过的运行会在 cleanup 里被取消。真正的修法是分片」。往一个已记录「修法是分片」的 job 里加测试是反方向,而且它的失效形态不是变红——是通过的运行被 cleanup 取消,那比没有覆盖更糟,因为看起来像 flake。
  2. 这个预算归 smoke job 的归属者,你不让我擅自拿是对的。

所以本 PR 贡献的诚实表述是:那四行 CSS 是修复,spec 是夜间防止有人悄悄撤掉内缩的保护——它跑起来是有牙的(macOS 遮挡断言在 main 上会失败,见上面的测试计划),只是不拦 PR。两条测试都是 mock daemon 的 Chromium 测试,以后要打标签不需要 macOS runner;等分片落地后如果 smoke 预算归属者想要,这是两个词的改动,我可以随时加。

描述已更新,明确写出这一点,而不再暗示 PR 级门禁。

Prettier 阻塞项已在 d6ef63e08f(style(web-shell): apply Prettier to right-panel macOS titlebar spec)修掉,它就是当前 head;Lint & Static 在其上为绿。

@yiliang114

Copy link
Copy Markdown
Collaborator Author

@qwen-code /triage

@yiliang114

Copy link
Copy Markdown
Collaborator Author

@qwen-code /triage

@yiliang114
yiliang114 requested review from chiga0 and qqqys September 28, 2026 03:43

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

Critical-only review at d6ef63e0 — approving

Base 3f5ae3ff. Two files, +96/-0 and nothing deleted: four lines of CSS in packages/web-shell/client/App.module.css and a new 92-line Playwright spec. I read the changed rule, the variable it consumes, and the surrounding flex context.

Historical blocking issue — resolved

The one blocker ever filed was mechanical: Lint & Static failed at the Prettier step on the spec file this PR adds, so it could not be pre-existing noise. At this head Lint & Static passes, along with Test, web-shell E2E Smoke, Capture web-shell visuals and Integration Tests (no-AK, No Sandbox) — twelve green, zero failures, nothing pending. The bot has also replaced its CHANGES_REQUESTED with an approval at this head, and there are no review threads at all.

My scan

The inset cannot reach a non-macOS context. The declaration is padding-top: var(--web-shell-desktop-titlebar-height, 0), and that custom property is defined in exactly one place:

.app:global(.qwen-code-macos-titlebar) {
  --web-shell-desktop-titlebar-height: 38px;
}

So it resolves to 38px only when the root .app element also carries the global class the Tauri shell sets on macOS, and falls back to 0 everywhere else — a plain browser, and the Windows and Linux desktop shells. The :global() compound lands on the same element as .app, which is the thing that makes the scoping actually hold. This is also not a new mechanism: .contextShell at :91 consumes the identical expression, so the dock now matches the shell and the sidebar rather than introducing a second way to express the inset.

No conflicting or duplicated declaration. .artifactPanelDock had no padding-top before this change; the rule reads flex, width, min-width, display, the new declaration, then animation.

The box model and the horizontal layout are untouched. The dock is a horizontal flex item (flex: 0 0 var(--artifact-panel-dock-width) with an explicit width), and the new padding is on the vertical axis only, so the panel's width — and with it the resize divider's MIN_ARTIFACT_PANEL_WIDTH clamping — behaves exactly as before. .artifactPanelClip inside is flex: 1 1 auto with overflow: hidden, so it absorbs the reduced content height instead of overflowing. Under the package's universal border-box the padding cannot inflate the element's outer size, and the open animation keyframes only flex-basis and width, so the inset does not participate in the transition.

The fullscreen variant is correctly out of scope. It renders through a portal host at z-index: 1000 against the drag strip's 40, so it was never occluded and this rule does not apply to it.

No Critical found. The change adds a padding declaration gated on a macOS-only custom property, plus a test.

Non-blocking, recorded rather than requested

The bot's open question is a real one and worth a deliberate answer: the new spec carries no @smoke tag, and PR CI runs the e2e lane with --grep @smoke, while the full suite is post-merge and nightly. So this regression guard will not gate a pull request today — a future change that moves the panel back out of the padded shell would be caught after merge rather than before. Adding it to smoke is a budget decision for whoever owns that job's runtime, not something I would push for in this PR.

CI

Twelve checks pass at this head with zero failures and nothing pending. Capture web-shell visuals passing is useful corroboration here specifically, since it is the lane that would show an unintended inset leaking into another layout.

@yiliang114
yiliang114 added this pull request to the merge queue Sep 28, 2026

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

The CSS one-liner mirrors the existing .contextShell pattern exactly: the variable is set to 38px on .app.qwen-code-macos-titlebar and falls back to 0 everywhere else, so non-macOS and web-only deployments are unaffected.

What was checked

  • Stated intent vs. code (Class 10): The patch comment explains the portal mechanics correctly — .artifactPanelDock portals into a display:contents slot so it becomes a direct .appShell flex child and misses the .contextShell padding-top. Adding the same variable to .artifactPanelDock is the minimal, correct fix.
  • Variable definition: --web-shell-desktop-titlebar-height: 38px is declared on .app:global(.qwen-code-macos-titlebar) in the full CSS file; the 0 fallback keeps non-macOS paths unaffected.
  • Sibling entrances: .artifactResizeHandle is also a direct .appShell child but is a narrow 8 px drag rail with no interactive UI near its top edge — not a blocker, worth a follow-up only if that grip ever gains a hit area near the titlebar.
  • E2E test validity (Class 5): Both tests are well-formed. The occlusion test uses document.elementFromPoint at the button center plus an actual panelToggle.click() — Playwright's hit-target retry loop would keep spinning while the drag region intercepted pointer events, so the click robustly regresses the bug beyond the point-query alone.
  • No AI review ban found in CONTRIBUTING.md.
  • Idempotency: No prior chiga0 review on this PR.

No blockers found.

Reviewed with AI assistance.

Merged via the queue into main with commit 8d9543a Sep 28, 2026
90 of 91 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.

右側擴展區面板開啟後無法關閉(toggle 按鈕失效)

3 participants