Skip to content

[Fix] 显示器变化后自动恢复屏幕外窗口 - #300

Merged
qiyinxi merged 1 commit into
AUTO-MAS-Project:dev_v2from
qiyinxi:agent/restore-offscreen-window
Jul 15, 2026
Merged

[Fix] 显示器变化后自动恢复屏幕外窗口#300
qiyinxi merged 1 commit into
AUTO-MAS-Project:dev_v2from
qiyinxi:agent/restore-offscreen-window

Conversation

@qiyinxi

@qiyinxi qiyinxi commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

修改内容

  • 启动时校验标题栏左侧固定的 64px 恢复拖拽区是否仍完整位于任一显示器工作区内。
  • 保存坐标失效时,将窗口收缩并居中恢复到主显示器,同时持久化修正后的 bounds。
  • 监听显示器移除和指标变化,先恢复窗口位置,再按最终落点屏幕的 DPI 与工作区重算最小尺寸。
  • 保留合法的负坐标与多显示器布局,不再把 x = 0y = 0 误判为缺失配置。
  • 为标题栏左侧定义稳定的恢复拖拽区,并保留更新提示的点击行为。
  • 恢复时保留 normal、maximized、maximized → minimized 以及隐藏到托盘等窗口状态;隐藏窗口不会因显示器变化而意外弹出。

根因

原实现通过 screen.getDisplayNearestPoint() 选择缩放和工作区,但创建 BrowserWindow 时仍原样使用保存的 cfgX/cfgY。后续逻辑只调整宽高,不校验可拖拽区域是否可见,也没有处理 display-removed,因此断开副屏后窗口可以完全位于所有现存屏幕之外。

验证

  • yarn exec tsc -p tsconfig.electron.json --noEmit:通过。
  • yarn exec eslint electron/main.ts src/components/TitleBar.vue:通过。
  • git diff --check upstream/dev_v2...HEAD:通过。
  • 完整代码审查后无剩余 P1/P2 问题。

Closes #298

Summary by Sourcery

确保在显示器变更时主窗口保持可见且尺寸正确,并持久化调整后的边界和状态。

Bug Fixes:

  • 防止在移除显示器或显示参数变化后,主窗口完全移出屏幕之外。
  • 在显示器变化和应用重启之间,正确保留窗口状态(正常、最大化、最小化、隐藏到托盘)。
  • 避免将合法的负坐标或零坐标误判为缺失的配置值。

Enhancements:

  • 基于每个显示器的工作区域和 DPI,对窗口边界和最小尺寸进行稳健处理,包括居中和限制在可用区域内。
  • 优化窗口尺寸与位置的配置解析逻辑,提供合理默认值,并持久化修正后的边界。
  • 稳定标题栏中的专用可拖拽区域,同时保留可点击的更新提示行为。
Original summary in English

Summary by Sourcery

Ensure the main window remains visible and correctly sized when displays change, and persist adjusted bounds and state.

Bug Fixes:

  • Prevent the main window from ending up completely off-screen after monitors are removed or display metrics change.
  • Correctly preserve window state (normal, maximized, minimized, hidden to tray) across display changes and app restarts.
  • Avoid misinterpreting valid negative or zero coordinates as missing configuration values.

Enhancements:

  • Add robust handling for window bounds and minimum size based on per-display work areas and DPI, including centering and clamping within usable areas.
  • Refine configuration parsing for window size and position with sensible defaults and persistence of corrected bounds.
  • Stabilize a dedicated draggable area in the title bar while preserving clickable update hint behavior.

@sourcery-ai

sourcery-ai Bot commented Jul 15, 2026

Copy link
Copy Markdown

Reviewer's Guide

此 PR 通过围绕稳定的 64px 标题栏拖拽区域校验和调整已保存的窗口边界、按显示器/DPI 重新计算最小尺寸,并在保留负坐标和托盘/隐藏行为的前提下正确持久化/恢复窗口状态,从而增强了 Electron 窗口在显示配置变化时的定位稳健性;同时在 Vue 标题栏中标记了一个稳定的拖拽区域,并保持更新提示仍可点击。

显示器变化时窗口恢复的时序图

sequenceDiagram
  participant Screen
  participant MainProcess
  participant BrowserWindow as win
  participant ensureWindowIsVisible
  participant recomputeMinSize
  participant Config as ConfigStorage

  Screen->>MainProcess: display-metrics-changed
  activate MainProcess
  MainProcess->>MainProcess: handleDisplayConfigurationChanged()
  MainProcess->>ensureWindowIsVisible: ensureWindowIsVisible()
  activate ensureWindowIsVisible
  ensureWindowIsVisible->>win: isDestroyed()
  ensureWindowIsVisible->>win: isMinimized()
  ensureWindowIsVisible->>win: isVisible()
  ensureWindowIsVisible->>win: isMaximized()
  ensureWindowIsVisible->>win: getNormalBounds()/getBounds()
  ensureWindowIsVisible->>MainProcess: findDisplayWithUsableTitleBar(bounds)
  alt [title bar not visible on any display]
    ensureWindowIsVisible->>MainProcess: centerBoundsInWorkArea(bounds, primary.workArea)
    ensureWindowIsVisible->>win: unmaximize()
    ensureWindowIsVisible->>win: setBounds(safeBounds)
    ensureWindowIsVisible->>ConfigStorage: loadConfig()
    ensureWindowIsVisible->>ConfigStorage: saveConfig(updatedConfig)
  end
  deactivate ensureWindowIsVisible

  MainProcess->>recomputeMinSize: recomputeMinSize()
  activate recomputeMinSize
  recomputeMinSize->>win: isMinimized()
  recomputeMinSize->>win: getNormalBounds()/getBounds()
  recomputeMinSize->>MainProcess: screen.getDisplayMatching(bounds)
  recomputeMinSize->>win: getMinimumSize()
  recomputeMinSize->>win: setMinimumSize(w, h)
  recomputeMinSize->>win: setBounds(newBounds)
  deactivate recomputeMinSize
  deactivate MainProcess
Loading

文件级变更

Change Details Files
在启动时校验并规范化已保存的窗口边界,确保标题栏拖拽区域完全位于某个显示器的工作区域内,如不满足则回退到在主显示器居中,并持久化修正后的边界。
  • 引入标题栏高度和固定宽度恢复拖拽句柄的常量,并使用它们来检查可拖拽区域的可见性。
  • 添加辅助函数,用于定位一个工作区能完全包含窗口边界左上角拖拽句柄区域的显示器。
  • 更改配置解析逻辑,使用健壮的整数解析与默认值,允许 0 和负位置,同时清理非数字或非正的尺寸。
  • 从保存的配置计算候选初始边界,在可用显示器上进行校验;若无效,则将其居中并限制在主显示器的工作区域内。
  • 当保存的边界被调整时,立即用修正后的尺寸和位置更新并保存配置,并记录警告日志。
frontend/electron/main.ts
让窗口大小和可见性适应动态显示器/DPI 变化,在显示配置变更时将屏幕外窗口恢复到可见位置,并保持最小尺寸位于当前工作区域内。
  • 更新最小尺寸计算,将逻辑最小宽/高限制在目标显示器的工作区域内,并在窗口最小化时使用 getNormalBounds() 处理。
  • 扩展 recomputeMinSize,在窗口未最大化或最小化时,也会将窗口边界限制在当前显示器工作区域内。
  • 新增 ensureWindowIsVisible 辅助函数,如果窗口的标题栏拖拽区域在所有显示器之外,则将窗口重新居中到主显示器,同时谨慎保留最小化/最大化/隐藏状态,并更新保存的边界和最大化标记。
  • 引入共享的显示配置变更处理函数,先确保可见性再重新计算最小尺寸,并将其绑定到 display-metrics-changed 和 display-removed 事件上,在窗口关闭时正确清理。
  • 调整保存配置的逻辑,在窗口最小化时使用 normal bounds,并在窗口隐藏或最小化时通过独立的 restoreToMaximized 状态推导最大化标志。
frontend/electron/main.ts
跟踪并在最小化、托盘隐藏和重新显示周期中正确恢复预期的最大化状态。
  • 添加从 config.UI.maximized 派生的 restoreToMaximized 布尔值,在 maximize/unmaximize 事件中更新,并在恢复时用来在合适情况下重新最大化。
  • 确保在窗口 show 时,如果 restoreToMaximized 为 true 且窗口既未最大化也未最小化,则重新最大化窗口。
  • 在关闭或 before-quit 时持久化配置时,如果窗口不可见或最小化,则基于 restoreToMaximized 计算 isMaximized,从而保留预期的恢复状态。
frontend/electron/main.ts
在标题栏左侧定义一个稳定的可拖拽区域,同时保留更新提示的点击行为。
  • 在标题栏左侧区域设置 min-width 和 -webkit-app-region: drag,以创建一个与 main.ts 中恢复逻辑匹配的永久拖拽句柄。
  • 将可点击的更新提示标记为 -webkit-app-region: no-drag,使其保持可交互且不会参与拖拽。
frontend/src/components/TitleBar.vue

与关联 issue 的对照评估

Issue Objective Addressed Explanation
#298 启动时,根据当前显示器配置校验保存的窗口边界,确保窗口具有可用的可见区域;如果不满足,则将其移动并调整大小到主显示器工作区域内,并持久化修正后的边界。
#298 当显示器拓扑发生变化(显示器被移除或指标改变)时,重新校验现有窗口的位置/大小,将屏幕外窗口恢复到有效显示器上的可见区域,并在保留有效负坐标的前提下保持窗口大小在目标工作区域内。
#298 在恢复过程中正确处理最大化/最小化/隐藏状态,先修正 normal bounds,再恢复之前的最大化状态且不造成窗口意外显示,并持久化更新后的边界和最大化状态。

可能关联的 issue


Tips and commands

Interacting with Sourcery

  • Trigger a new review: 在 pull request 上评论 @sourcery-ai review
  • Continue discussions: 直接回复 Sourcery 的审查评论。
  • Generate a GitHub issue from a review comment: 通过回复审查评论,请求 Sourcery 从该评论创建一个 issue。你也可以在审查评论中回复 @sourcery-ai issue 来从中创建 issue。
  • Generate a pull request title: 在 pull request 标题的任意位置写上 @sourcery-ai 来在任意时间生成标题。你也可以在 pull request 中评论 @sourcery-ai title 来(重新)生成标题。
  • Generate a pull request summary: 在 pull request 正文任意位置写上 @sourcery-ai summary,即可在你希望的位置生成 PR 摘要。也可以在 pull request 中评论 @sourcery-ai summary 来在任意时间(重新)生成摘要。
  • Generate reviewer's guide: 在 pull request 上评论 @sourcery-ai guide,即可在任意时间(重新)生成审查者指南。
  • Resolve all Sourcery comments: 在 pull request 上评论 @sourcery-ai resolve,即可将所有 Sourcery 评论标记为已解决。适用于你已经处理完所有评论且不再希望看到它们的情况。
  • Dismiss all Sourcery reviews: 在 pull request 上评论 @sourcery-ai dismiss,即可关闭所有现有的 Sourcery 审查。特别适用于你想从一次全新的审查开始 —— 记得再评论 @sourcery-ai review 以触发新的审查!

Customizing Your Experience

访问你的 dashboard 以:

  • 启用或禁用审查特性,例如 Sourcery 自动生成的 pull request 摘要、审查者指南等。
  • 更改审查语言。
  • 添加、移除或编辑自定义审查指令。
  • 调整其他审查设置。

Getting Help

Original review guide in English

Reviewer's Guide

This PR hardens Electron window positioning against display configuration changes by validating and adjusting saved window bounds around a stable 64px title-bar drag area, recomputing min sizes per-display/DPI, and correctly persisting/restoring window state while preserving negative coordinates and tray/hidden behavior; it also marks a stable drag region in the Vue title bar while keeping the update-hint clickable.

Sequence diagram for window recovery on display change

sequenceDiagram
  participant Screen
  participant MainProcess
  participant BrowserWindow as win
  participant ensureWindowIsVisible
  participant recomputeMinSize
  participant Config as ConfigStorage

  Screen->>MainProcess: display-metrics-changed
  activate MainProcess
  MainProcess->>MainProcess: handleDisplayConfigurationChanged()
  MainProcess->>ensureWindowIsVisible: ensureWindowIsVisible()
  activate ensureWindowIsVisible
  ensureWindowIsVisible->>win: isDestroyed()
  ensureWindowIsVisible->>win: isMinimized()
  ensureWindowIsVisible->>win: isVisible()
  ensureWindowIsVisible->>win: isMaximized()
  ensureWindowIsVisible->>win: getNormalBounds()/getBounds()
  ensureWindowIsVisible->>MainProcess: findDisplayWithUsableTitleBar(bounds)
  alt [title bar not visible on any display]
    ensureWindowIsVisible->>MainProcess: centerBoundsInWorkArea(bounds, primary.workArea)
    ensureWindowIsVisible->>win: unmaximize()
    ensureWindowIsVisible->>win: setBounds(safeBounds)
    ensureWindowIsVisible->>ConfigStorage: loadConfig()
    ensureWindowIsVisible->>ConfigStorage: saveConfig(updatedConfig)
  end
  deactivate ensureWindowIsVisible

  MainProcess->>recomputeMinSize: recomputeMinSize()
  activate recomputeMinSize
  recomputeMinSize->>win: isMinimized()
  recomputeMinSize->>win: getNormalBounds()/getBounds()
  recomputeMinSize->>MainProcess: screen.getDisplayMatching(bounds)
  recomputeMinSize->>win: getMinimumSize()
  recomputeMinSize->>win: setMinimumSize(w, h)
  recomputeMinSize->>win: setBounds(newBounds)
  deactivate recomputeMinSize
  deactivate MainProcess
Loading

File-Level Changes

Change Details Files
Validate and normalize saved window bounds at startup to ensure the title-bar drag area is fully within a display work area, falling back to centering on the primary display and persisting corrected bounds.
  • Introduced constants for title bar height and a fixed-width recovery drag handle and used them to check visibility of the draggable area.
  • Added helper to locate a display whose work area fully contains the top-left drag handle region of the window bounds.
  • Changed config parsing to use robust integer parsing with defaults, allowing 0 and negative positions while sanitizing non-numeric or non-positive sizes.
  • Computed candidate initial bounds from saved config, validated them against available displays, and if invalid, centered and clamped them into the primary display’s work area.
  • When saved bounds are adjusted, immediately updated and saved the config with the corrected size and location, logging a warning.
frontend/electron/main.ts
Adapt window sizing and visibility to dynamic display/DPI changes, restoring off-screen windows to a visible position and keeping min size within the current work area.
  • Updated min-size computation to clamp logical min width/height to the target display’s work area and to handle minimized windows using getNormalBounds().
  • Extended recomputeMinSize to also clamp window bounds within the current display’s work area when not maximized or minimized.
  • Added an ensureWindowIsVisible helper that re-centers the window to the primary display if its title-bar drag area is off all displays, carefully preserving minimized/maximized/hidden states and updating persisted bounds and maximized flag.
  • Introduced a shared display-configuration change handler that first ensures visibility then recomputes min size, and wired it to display-metrics-changed and display-removed events with proper cleanup on window close.
  • Adjusted config-saving paths to use normal bounds when minimized and to derive the maximized flag from a dedicated restoreToMaximized state when the window is hidden or minimized.
frontend/electron/main.ts
Track and correctly restore the intended maximized state across minimize, tray-hide, and show cycles.
  • Added a restoreToMaximized boolean derived from config.UI.maximized, updated on maximize/unmaximize events, and used on restore to re-maximize when appropriate.
  • Ensured that on show, the window re-maximizes if restoreToMaximized is true and the window is neither maximized nor minimized.
  • When persisting config on close or before-quit, computed isMaximized using restoreToMaximized when the window is invisible or minimized so the intended restore state is preserved.
frontend/electron/main.ts
Define a stable draggable area on the left side of the title bar while preserving click behavior on the update hint.
  • Set a min-width and -webkit-app-region: drag on the title bar left section to create a permanent drag handle that matches the recovery logic in main.ts.
  • Marked the clickable update hint as -webkit-app-region: no-drag so it remains interactive and does not participate in dragging.
frontend/src/components/TitleBar.vue

Assessment against linked issues

Issue Objective Addressed Explanation
#298 On startup, validate saved window bounds against current displays, ensuring the window has a usable visible area; if not, move and resize it into the primary display work area and persist the corrected bounds.
#298 When display topology changes (display removed or metrics changed), re-validate the existing window position/size, recover off-screen windows to a visible area on a valid display, and keep window size within the target work area while preserving valid negative coordinates.
#298 Correctly handle maximized/minimized/hidden states during recovery so that normal bounds are fixed first and the prior maximized state is restored without spurious window showing, and persist the updated bounds and maximized state.

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@qiyinxi
qiyinxi force-pushed the agent/restore-offscreen-window branch from 9e35bb2 to 6dd3c05 Compare July 15, 2026 13:58
@qiyinxi
qiyinxi changed the base branch from main to dev_v2 July 15, 2026 14:00
@qiyinxi
qiyinxi marked this pull request as ready for review July 15, 2026 14:18
@qiyinxi
qiyinxi merged commit ab22c0f into AUTO-MAS-Project:dev_v2 Jul 15, 2026
2 of 3 checks passed

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - 我在这里给出一些高层次的反馈:

  • 新的窗口边界逻辑在不同地方混用了 workAreaworkAreaSize;建议在所有涉及宽度/高度的逻辑中统一使用 workArea,以避免在不同平台上出现微妙的不一致。
  • ensureWindowIsVisible 和配置保存处理函数中,loadConfig() 在短时间内被多次调用;可以考虑在 createWindow() 内缓存当前配置,或者将其作为参数传入辅助函数,以减少重复的 I/O 并简化状态推理。
  • restoreToMaximized 状态由多个事件处理函数更新(maximizeunmaximizerestoreshow、配置保存),并在不同分支中使用;可以添加一个小的辅助函数来封装最大化/恢复的决策,从而让窗口状态转换更易理解,也更不容易出错。
给 AI 智能体的提示
Please address the comments from this code review:

## Overall Comments
- The new window bounds logic mixes `workArea` and `workAreaSize` in different places; consider standardizing on `workArea` for width/height everywhere to avoid subtle inconsistencies across platforms.
- In `ensureWindowIsVisible` and the config save handlers, `loadConfig()` is called multiple times in quick succession; caching the current config within `createWindow()` or passing it into helpers would reduce repeated I/O and simplify state reasoning.
- The `restoreToMaximized` state is updated from several event handlers (`maximize`, `unmaximize`, `restore`, `show`, config save) and used in different branches; adding a small helper to encapsulate the maximize/restore decision could make the window state transitions easier to follow and less error-prone.

Sourcery 对开源项目是免费的——如果你觉得我们的评审有帮助,欢迎分享 ✨
帮我变得更有用!请在每条评论上点 👍 或 👎,我会根据你的反馈改进后续的评审。
Original comment in English

Hey - I've left some high level feedback:

  • The new window bounds logic mixes workArea and workAreaSize in different places; consider standardizing on workArea for width/height everywhere to avoid subtle inconsistencies across platforms.
  • In ensureWindowIsVisible and the config save handlers, loadConfig() is called multiple times in quick succession; caching the current config within createWindow() or passing it into helpers would reduce repeated I/O and simplify state reasoning.
  • The restoreToMaximized state is updated from several event handlers (maximize, unmaximize, restore, show, config save) and used in different branches; adding a small helper to encapsulate the maximize/restore decision could make the window state transitions easier to follow and less error-prone.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The new window bounds logic mixes `workArea` and `workAreaSize` in different places; consider standardizing on `workArea` for width/height everywhere to avoid subtle inconsistencies across platforms.
- In `ensureWindowIsVisible` and the config save handlers, `loadConfig()` is called multiple times in quick succession; caching the current config within `createWindow()` or passing it into helpers would reduce repeated I/O and simplify state reasoning.
- The `restoreToMaximized` state is updated from several event handlers (`maximize`, `unmaximize`, `restore`, `show`, config save) and used in different branches; adding a small helper to encapsulate the maximize/restore decision could make the window state transitions easier to follow and less error-prone.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

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.

[Bug] 多显示器变更后,窗口可能启动在屏幕外且无法恢复

1 participant