feat(web-shell): show Settings and Daemon Status as an in-place panel#6341
Conversation
The Settings and Daemon Status buttons opened centered modal overlays that dimmed the whole app. Render them as a full-height panel that replaces the chat surface instead — a Back button or Escape returns to the chat — with the content left-aligned and filling the chat pane width rather than centered in a narrow column. On the Daemon Status page, give the time-series charts a taller plot and the overview cards a wider track so a wide window is actually used; the tab grouping (overview / metrics / diagnostics) is kept.
|
Re-run after Problem: Real UX issue — Settings and Daemon Status opened as centered modal dialogs that dimmed the whole app and wasted horizontal space. The before/after screenshots demonstrate the improvement clearly. Direction: Aligned. Converting modal dialogs to in-place views is a well-established pattern. Web Shell only, no core/auth/sandbox/model/telemetry implications. Scope is focused on two components + CSS adjustments. Approach: The scope is right for the stated goal. The chat view stays mounted ( Moving on to code review. 🔍 中文说明
问题: 真实的 UX 问题——设置和 Daemon 状态以居中模态弹窗打开,遮暗整个界面并浪费横向空间。前后对比截图清楚展示了改善效果。 方向: 对齐。将模态弹窗转换为就地视图是成熟的设计模式。仅 Web Shell,无核心/认证/沙箱/模型/遥测影响。 方案: 范围与目标匹配。最新提交 进入代码审查 🔍 — Qwen Code · qwen3.7-max |
Code ReviewRe-run after fix rounds ( State management: Replacing two boolean states ( Mutual exclusivity (new in Draft preservation: The chat view stays mounted ( Approval auto-close: When an actionable approval arrives while a panel is open, an effect closes the panel AND dismisses stacked sub-dialogs (model/approval pickers), so the approval overlay surfaces correctly. Focus is routed to the approval overlay wrapper ( Focus management: The panel focus effect correctly handles: Back button focus on panel open, composer focus restore on panel close (no pending approval), composer focus restore after approval resolves post-auto-close, and the Escape key layering: DialogShell uses a capture-phase listener with CSS changes: Chart height Sub-dialog behavior: Settings panel stays open under model/approval sub-dialogs — closing the sub-dialog returns to settings (not the chat). Intentional and better UX for a panel-based layout. Build & Test ResultsReal-Scenario TestingThis is a browser-only UI change (Web Shell). tmux cannot capture visual panel layout, modal absence, or CSS grid adjustments — the meaningful evidence is in-browser DOM assertions and screenshots. Author's verification (from PR body, Playwright + real Chrome):
What a reviewer should verify manually:
中文说明代码审查修复多轮后的重新评估(含 互斥逻辑( 状态管理: 用单个 草稿保留: 面板显示时聊天视图保持挂载( 审批自动关闭: 当可操作审批到来时,effect 关闭面板并关闭堆叠的子弹窗,使审批浮层正确显示。 焦点管理: 面板焦点 effect 正确处理所有场景。 CSS 变更: 图表高度改为可覆盖的自定义属性。防溢出网格。专用的宽指标图表网格。 构建与测试结果
真实场景测试纯浏览器 UI 变更。作者已用 Playwright + 真实 Chrome 验证。建议复核者手动验证上述 8 个步骤。 — Qwen Code · qwen3.7-max |
|
This PR has matured through nine fix rounds and a final mutual-exclusivity patch ( What stands out is the iterative depth. Each review round surfaced real issues — composer draft loss, approval overlay hidden behind the panel, stacked sub-dialog dismissal, focus management edge cases, and now panel↔Scheduled Tasks z-index collision — and the author addressed each one thoroughly with mutation-checked tests. The latest commit adds The approval auto-close path remains the most complex part: when an actionable approval arrives while a panel (possibly with a stacked sub-dialog) is open, the effect closes the panel, dismisses sub-dialogs, routes focus to the approval overlay, and the The chat view JSX duplication in App.tsx (~200 lines) is a known trade-off — keeping the chat view mounted preserves composer state (draft, attachments). Extracting a CI green across all platforms. Screenshots show a clear before/after improvement. All changes are web-shell only — no core module impact. Ships the feature cleanly. ✅ 中文说明这个 PR 经过九轮修复和最终的互斥补丁( 最突出的是迭代深度。每轮审查都发现了真实问题——输入框草稿丢失、审批浮层被面板遮挡、堆叠子弹窗关闭、焦点管理的边界情况,以及现在的面板↔Scheduled Tasks z-index 冲突——作者用变异检验的测试彻底解决了每一个。最新提交添加了 审批自动关闭路径仍是最复杂的部分:设计合理、测试充分。聊天视图 JSX 重复是已知的权衡——以后可以重构。 CI 全平台通过。截图展示了清晰的前后对比改进。所有变更仅 web-shell——无核心模块影响。 可以合入。✅ — Qwen Code · qwen3.7-max |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
wenshao
left a comment
There was a problem hiding this comment.
Downgraded from Request changes to Comment: self-PR; CI still running. Inline comments contain the review findings.
— GPT-5 via Qwen Code /review
wenshao
left a comment
There was a problem hiding this comment.
Reviewed — no blockers. Suggestion-level recommendations are in the Suggestion summary comment below.
Suggestions — commit
|
| File | Issue | Suggested fix |
|---|---|---|
packages/web-shell/client/App.tsx:3745 |
handleFastModelSelect has entirely new logic (closePanel(), sendPrompt, reloadWorkspaceSettings) with zero test coverage. If closePanel() is accidentally removed, the model-change response piles up behind the hidden chat view. |
Add a test that opens Settings, triggers fast-model selection, asserts the panel closed and reloadWorkspaceSettings was called. |
packages/web-shell/client/App.tsx:4241 |
Touch-device text selection regression: the panel uses role="region", but the user-select: text rule in standalone.css only matches [role='dialog'] and [data-user-selectable]. Users on touch devices cannot select/copy text in Settings or Daemon Status panels. |
Add data-user-selectable="" to the panel <section>. |
packages/web-shell/client/App.tsx:2355 |
createNewSession calls closePanel() but no test verifies the panel closes when a new session is created. |
Add a test: open panel → trigger onNewSession → assert [data-testid="inline-panel"] is gone. |
packages/web-shell/client/App.tsx:2375 |
loadSidebarSession calls closePanel() but no test covers the sidebar-click path (only /resume is tested). |
Expose onLoadSession from the sidebar mock, open a panel, invoke it, and assert the panel closes. |
— qwen3.7-max via Qwen Code /review
…-settings-status-panel # Conflicts: # packages/web-shell/client/App.tsx
Address review feedback on the in-place Settings / Daemon Status panel: - Keep the chat view (message list + composer) mounted and just hidden while a panel is shown, so typing a prompt, opening Settings/Status, then going Back no longer discards the unsent draft and attachments (the composer subtree was being unmounted and remounted empty). - Focus the Back button when a panel opens and restore focus to the composer when it closes, replacing the focus management DialogShell used to provide. - Reload workspace settings after the fast-model command resolves so the still- mounted Settings panel doesn't keep showing the previous value. - Don't close the panel when Escape is handled inside the sidebar (its search input clears on Escape without stopping the event). - Guard the overview card grid with min(100%, 340px) so a 340px track can't overflow a narrow panel.
Add key={activePanel} on the panel body so switching directly between Settings
and Daemon Status (activePanel goes 'settings' -> 'status' without passing
through null) remounts the scroll container and opens at the top instead of
inheriting the previous panel's scrollTop.
|
Thanks for the detailed review — pushed fixes for all 8 threads and rebased onto latest Fixes (
All verified with 中文感谢细致的 review——已针对全部 8 条线程推送修复,并 rebase 到最新 修复(
全部用 |
The chatViewWrap added to keep the composer mounted while a panel is shown was
filling the pane, which cancelled the empty (new-chat) state's vertical
centering (welcome header + composer stuck to the top instead of centered).
Shrink the wrapper to its content in the empty state so
`.appChatEmpty .chatPane { justify-content: center }` centers it again, matching
how `.content` behaves there.
wenshao
left a comment
There was a problem hiding this comment.
Downgraded from Approve to Comment: self-PR; CI failing: Post Coverage Comment, review-pr, delay-automatic-review, Test (ubuntu-latest, Node 22.x).
No issues found. LGTM! ✅
— qwen3.7-max via Qwen Code /review
My earlier merge of origin/main 3-way-combined WebShellSidebar.test.tsx
incorrectly, dropping the `{ }` destructuring from the 7 session-organization
tests. renderSidebar returns `{ container, rerender }` (not the element), so
`const container = renderSidebar(...)` made `container.querySelector` throw.
Restore the file to origin/main so the tests pass again.
…-settings-status-panel
doudouOUC
left a comment
There was a problem hiding this comment.
Review Summary
Overall the approach is sound — converting modal dialogs to in-place panels for Settings and Daemon Status is the right UX direction. The implementation is clean with good focus management and keyboard handling. A few concerns below.
Positive
- Good focus management: auto-focus Back button on open, return to composer on close
- Correct
key={activePanel}on panelBody to force remount between views - Thoughtful Escape handling that scopes to outside the sidebar drawer
- Proper deps array updates for
closePanelincreateNewSession/loadSidebarSession reloadWorkspaceSettings()after fast model switch to keep panel in sync
The in-place Settings/Daemon Status panel hides the chat footer with display:none, and the ToolApproval / AskUserQuestion overlays live in that footer. A gated tool call that arrived while a panel was open rendered the approval into a hidden container, so the turn hung with no visible prompt (reported [Critical]). Close the panel when an actionable approval is pending so it surfaces. Only actionable approvals count (pendingToolApproval / pendingAskUserApproval already gate on canActOnPendingApproval), so a non-owner in a shared session isn't yanked out of Settings by someone else's prompt. Leave focus for the overlay rather than the composer on this path: ToolApproval uses a window-level key handler that ignores editable targets, so focusing the composer would swallow its shortcuts. Also refocus the Back button on panel->panel switches (not just on open), so focus no longer relies on the Back button being the same DOM node across the keyed panel body, and expose SvgLineChart's plot height as --chart-height (fallback 140px) so a constrained caller can shrink it. Adds App-level tests: the panel auto-closes on a pending tool approval, and stays open when the only block is a resolved (non-actionable) one.
|
Thanks both — addressed this round in Fixed
Explained / confirmed (no change needed)
Open for your call
Verification: 中文已在本轮 已修
已解释/确认(无需改动)
留待你定
验证: |
wenshao
left a comment
There was a problem hiding this comment.
— qwen3.7-max via Qwen Code /review
Follow-up to the approval auto-close fix, addressing review suggestions:
- Add data-testid="inline-panel" to the panel <section> and query by it in
App.test instead of querySelector('section'), which would false-positive if
any future component renders a <section>.
- Add a companion test for the pendingAskUserApproval branch of the auto-close
effect. The ask-user block carries toolCall.input.questions so
isAskUserPermission() classifies it as an AskUserQuestion; mutation-checked
(restricting the effect to pendingToolApproval fails only this test).
- Document why handleFastModelSelect's post-sendPrompt reload is not racy:
sendPrompt awaits waitForAcceptedPromptCompletion, so the /model --fast turn
has already applied when reloadWorkspaceSettings() runs. Also note why the
command path needs the explicit reload that the setWorkspaceSetting pickers
(vision/voice) get for free via the settingsVersion signal.
The /resume <id> command and the ResumeDialog onSelect both call loadSession() without closePanel(), unlike createNewSession and loadSidebarSession. Loading a session means the user wants that chat, so leaving a Settings/Daemon Status panel open would hide it. Add closePanel() to both paths for consistency (a no-op when no panel is open). Currently reachable only in theory — the composer that submits /resume is display:none while a panel is shown — but the guard keeps the invariant if a non-composer entry point (sidebar/shortcut) is ever added. Adds a mutation-checked test (removing closePanel from the /resume handler fails it) and gives the ChatEditor test mock a focus() method, since the panel-close focus effect now runs editorRef.focus() on this no-approval path.
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
No issues found. LGTM! ✅
— qwen3.7-max via Qwen Code /review
wenshao
left a comment
There was a problem hiding this comment.
— GPT-5 via Qwen Code /review
Follow-up to the approval auto-close fix. When an approval arrives while a Settings/Status panel is open, the auto-close clears activePanel, so interactionBlocked flips false and useComposerCore's dialogOpen effect refocuses the still-mounted composer. ToolApproval ignores approval shortcuts from editable targets, so the now-visible approval stops responding to Enter/Escape/number keys until focus moves away. Key the ChatEditor dialogOpen prop off the pending approval too, so the composer stays blurred while an approval owns the keyboard. Consolidate the "an approval overlay is active" condition — previously duplicated in the auto-close effect and the panel focus guard — into a single approvalOverlayActive value so the three consumers can't drift. Uses the actionable-approval condition (pendingToolApproval / pendingAskUserApproval) rather than raw pendingApproval, matching the auto-close gating and avoiding a blurred composer when a non-actionable approval renders no overlay. Adds a mutation-checked test asserting dialogOpen is true while an approval is pending with no panel open.
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
No review findings — the implementation is clean and well-tested. Downgraded from Approve to Comment: CI still running.
Low-confidence note (terminal only): The activePanel === 'status' branch (rendering DaemonStatusDialog) has no unit test coverage — all 5 new tests open the panel via /settings. The gap is narrow (simple ternary, lifecycle tested regardless of panel type) but a regression in the 'status' literal or import would go undetected.
— qwen3.7-max via Qwen Code /review
A review note observed that all five panel tests open via /settings, so the
activePanel === 'status' branch (DaemonStatusDialog) had no coverage — a
regression in the 'status' literal would go undetected. Add a test that
opens Daemon Status through the sidebar and asserts the panel opens and
auto-closes on a pending approval, exercising that branch and confirming the
auto-close is panel-type-agnostic.
Gives the WebShellSidebar test mock an onOpenDaemonStatus button, since
there is no slash command to open the status panel. Mutation-checked:
neutering setActivePanel('status') fails only this test.
|
Re: the low-confidence note on the Added 中文关于 新增 |
Addresses a review round on the inline Settings/Status panel. [Critical] When an approval arrives while a DialogShell sub-dialog (model picker / approval-mode picker) is open over the panel, the auto-close removed the panel but left the sub-dialog backdrop covering the footer approval overlay, so the turn hung. Worse, the approval-mode picker stayed usable — selecting "yolo" auto-approved (handleSetMode) a tool call the user never saw. Dismiss the panel AND both sub-dialogs when an actionable approval is pending. [Critical] After the panel closes for an approval, focus fell to <body> (the Back button unmounted; ToolApproval has no autofocus). Move focus onto the ToolApproval overlay wrapper (tabindex=-1, so its window-listener shortcuts keep working and Enter doesn't confirm early) once it is visible. AskUserQuestion keeps managing its own focus. Suggestions: - Guard reloadWorkspaceSettings() rejection (was unhandled). - aria-hidden the display:none chat view so AT can't wander into it. - Close the panel before /model --fast so its response shows in the chat in context instead of piling up behind the hidden panel. - Remove the dead .panelBodyInner wrapper (redundant width:100%). - Rename data-mobile-drawer -> data-sidebar-shell (it wraps the desktop sidebar for all viewports) across App.tsx + standalone.css. Tests (+5, mutation-checked): sub-dialog dismissal on approval, overlay focus, Escape closes panel / sidebar-Escape does not, dialogOpen while a panel replaces the chat. Adds an observable DialogShell test mock.
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
No blocking issues. Three suggestion-level findings on test coverage and a dead CSS rule below.
— qwen3.7-max via Qwen Code /review
wenshao
left a comment
There was a problem hiding this comment.
[Critical] The panel focus effect consumed prevActivePanelRef to null on the approval auto-close (correctly skipping editor focus then). When the approval later resolved with no panel to return to, neither branch fired and focus was left on <body> — the visible composer took no keyboard input. Track the approvalOverlayActive transition too and restore composer focus on approval-resolve-after-panel-close. (useComposerCore's dialogOpen effect also covers this; the extra branch makes the panel effect self-contained.) Suggestions: - Log the reloadWorkspaceSettings() rejection instead of swallowing it. - Remove the dead :global([data-dialog-fullscreen]) .grid rule (the allowFullscreen source was removed when Daemon Status became a panel). Tests (+4, focus-restore mutation-checked): focus restored after an approval resolves post-auto-close; Back-button closes the panel and restores focus; fast-model pick closes the panel, sends /model --fast, and reloads settings; chat view is aria-hidden while a panel is shown. Adds interactive SettingsMessage / ModelDialog mocks and stable editor-focus / settings-reload spies.
wenshao
left a comment
There was a problem hiding this comment.
— qwen3.7-max via Qwen Code /review
Suggestions — commit
|
doudouOUC
left a comment
There was a problem hiding this comment.
Re-review (v2 — post 98aea1f)
Significant improvement since the first pass. All critical issues from prior rounds have been addressed:
Previously raised — now resolved
| Issue | Resolution |
|---|---|
display:none + virtual scroll concern |
Author confirmed: browsers retain scrollTop on display:none nodes; ResizeObserver re-measures on un-hide. No action needed. |
| Escape priority during streaming | Correctly scoped: interactionBlocked via activePanel !== null already suppresses cancel; the panel Escape handler is consistent. |
onSubDialog not closing panel |
handleFastModelSelect now calls closePanel() + reloadWorkspaceSettings() — the fast-model turn is visible to the user. |
| Hardcoded 140px chart height | Now uses var(--chart-height, 140px) — callers can override. |
| Approval overlay hidden behind panel | useEffect auto-closes panel + sub-dialogs on approvalOverlayActive, with focus management and tests. |
New observations (non-blocking)
-
Focus effect complexity is high but correct. The three-branch
useEffect(panel open/close, approval-resolve) handles all transitions correctly. Considered suggesting extraction into a custom hook for readability, but the inline comments document each branch well enough. -
approvalOverlayRefshared between two conditional elements —ToolApprovalandAskUserQuestionboth use the same ref. This is safe because they're mutually exclusive (classified byisAskUserPermission), but a future refactor adding a third overlay type would need to account for this. -
Test coverage is thorough. 15+ new test cases covering: panel open/close via
/settingsand sidebar button, Escape from panel vs sidebar, approval auto-close for both tool and ask-user variants, focus transitions, sub-dialog dismissal,/resumeclosing panel,aria-hiddenon chatViewWrap, fast-model settings reload, and composer dormancy. -
data-mobile-drawer→data-sidebar-shellrename — Good semantic improvement; consistently updated instandalone.css, the Escape handler, and the touch scroll prevention.
Verdict
The implementation is solid. Approval interactions are safe, keyboard/AT focus is well-managed, tests verify the critical paths. No blocking issues remaining. LGTM.
|
Thanks for the thorough re-review @doudouOUC — much appreciated. Resolved the virtual-scroll thread now that it's confirmed no-action. On observation #2 ( 中文感谢细致的复审 @doudouOUC。已 resolve 虚拟滚动那条线程(既已确认无需改动)。关于观察 #2( |
|
@qwen-code /triage |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
No review findings. Downgraded from Approve to Comment: CI still running.
— qwen3.7-max via Qwen Code /review
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
Resolved App.tsx conflicts between this branch's inline Settings/Daemon
Status panel and main's new inline Scheduled Tasks page — both are
full-pane views that replace/cover the chat:
- Kept `activePanel` (Settings/Status; it replaced the old
`showDaemonStatusDialog` boolean) and adopted main's `mainView`
('chat' | 'scheduledTasks').
- `interactionBlocked = dialogOpen || mainView !== 'chat'` — `dialogOpen`
keeps `activePanel !== null`, plus main's full-pane guard.
- Render both full-pane blocks before the (still-mounted) chat view.
Also extended the approval auto-close: the Scheduled Tasks `fullPage`
overlay covers the chat footer where the approval renders, so an approval
now dismisses it too (like the panel/sub-dialogs) and the overlay-focus is
gated on `mainView === 'chat'`. Added a mutation-checked test.
Rebuilt @qwen-code/sdk and @qwen-code/webui so web-shell typechecks against
main's new SDK exports (DaemonScheduledTask, listScheduledTasks, session
color). Verified: web-shell 1040 tests pass, tsc + vite build clean.
…ly exclusive Opening Scheduled Tasks (mainView — a position:absolute z-index overlay) and then Daemon Status (activePanel) rendered the Daemon Status panel *behind* the Scheduled Tasks overlay, so the button looked like it did nothing. These are mutually-exclusive full-pane views, so opening one must close the other. Centralize into openPanel() / openScheduledTasks() helpers (last-opened wins) and route all six open sites (the /settings and /schedule commands, the three sidebar handlers, and the StatusBar) through them. Mutation-checked tests: opening Daemon Status closes the Scheduled Tasks page (the reported repro), and opening Scheduled Tasks closes an open panel.
doudouOUC
left a comment
There was a problem hiding this comment.
LGTM. All prior concerns addressed — approval overlay surfacing, focus management, CSS variable for chart height, comprehensive tests. No blocking issues.
|
@qwen-code /triage |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
What this PR does
In the Web Shell, the Settings and Daemon Status buttons opened centered modal dialogs that dimmed the whole app. This renders them as a full-height panel that takes over the chat area (message list + composer) in place instead — the sidebar stays visible and a ← Back button (or Escape) returns to the chat. The panel content is left-aligned and fills the chat pane width rather than being centered in a narrow column. On the Daemon Status page the time-series charts get a taller plot and the overview cards a wider track, so a wide window is actually used; the overview / metrics / diagnostics tab grouping is kept.
Why it's needed
The modal overlays wasted horizontal space (centered, narrow) and dimmed the app for what are really full "views", not transient dialogs. Rendering them in place uses the full width, keeps the sidebar usable, and — on the Status dashboard — makes the trend charts legible instead of squeezed into a 52px sliver that only widened in a fullscreen mode the panel layout can't reach.
Reviewer Test Plan
How to verify
cd packages/web-shell && npx vite build, then runqwen serve --weband open the printed loopback URL.Verified locally with
qwen serve --web+ Playwright (real Chrome). DOM assertions: with a panel open it renders inside the chat pane (not adocument.bodyportal), there is no[role=dialog][aria-modal]backdrop, and the composer is replaced; Back and Escape both restore the composer; 0 console errors.tsc -p tsconfig.lib.jsonandeslintpass; the 3 affected component test files (67 tests) pass.Evidence (Before & After)
Before: Settings / Daemon Status opened as a centered modal dialog with a dimmed backdrop over the chat.
After:
Settings — in-place, full-width, left-aligned:
Daemon Status → Metrics — taller charts (52px → 140px plot):
Daemon Status → Overview — ~3 wide columns instead of 4 cramped ones:
Tested on
Environment (optional)
qwen serve --web(loopback) + Playwright real-Chrome;packages/web-shellvite build.Risk & Scope
aria-modallike the old dialogs); Escape returns to the chat and is intercepted before the chat's own Esc handling. Sub-dialogs (model / approval mode) remain modals on top of the panel.Linked Issues
N/A
中文说明
这个 PR 做了什么
在 Web Shell 里,设置和 Daemon 状态按钮原本打开居中的模态弹窗、遮暗整个界面。本 PR 改为在聊天区就地渲染一个占满高度的面板(替换消息列表 + 输入框)——侧栏保持可见,通过 ← 返回按钮(或 Escape)回到聊天。面板内容左对齐、占满聊天区宽度,而不是居中在窄栏里。Daemon 状态页的时序图表加高、概览卡片加宽,让宽窗口真正被利用;概览 / 指标 / 诊断的 tab 分组保留。
为什么需要
模态弹窗浪费横向空间(居中、窄),而且为本质是完整"视图"的内容遮暗界面。就地渲染用满宽度、保持侧栏可用,并在状态面板让趋势图清晰,而不是挤在 52px 的细条里(那个只有面板布局触发不到的全屏模式才会加宽)。
复核测试计划
如何验证
cd packages/web-shell && npx vite build,再qwen serve --web,打开输出的 loopback URL。已在本地用
qwen serve --web+ Playwright(真实 Chrome)验证。DOM 断言:面板打开时渲染在聊天区内部(非 body portal)、无模态蒙层、输入框被替换;返回和 Escape 都恢复输入框;0 console 错误。tsc -p tsconfig.lib.json和eslint通过;3 个相关组件测试文件(67 个用例)通过。证据(前后对比)
改动前: 设置 / Daemon 状态是居中模态弹窗 + 遮暗蒙层。
改动后: 见上方英文部分的三张截图——设置面板就地全宽左对齐、状态指标图表加高(52px → 140px)、状态概览约 3 列宽卡片。
测试平台
macOS 已测;Windows / Linux 交给 CI。
风险与范围
aria-modal);Escape 返回聊天,且在聊天自身 Esc 逻辑之前拦截。二级弹窗(模型 / 审批模式)仍是浮在面板上的模态。