Skip to content

feat: 🎉新增多窗口功能#159

Merged
LarryZhu-dev merged 1 commit intoAuto-Plugin:mainfrom
wxfengg:main
Feb 27, 2026
Merged

feat: 🎉新增多窗口功能#159
LarryZhu-dev merged 1 commit intoAuto-Plugin:mainfrom
wxfengg:main

Conversation

@wxfengg
Copy link
Contributor

@wxfengg wxfengg commented Feb 27, 2026

🎉新增多窗口功能,可以让 Tab 独立新的窗口,也可以合并 Tab 窗口

Copilot AI review requested due to automatic review settings February 27, 2026 07:09
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

本 PR 为编辑器引入“多窗口”能力:支持将 Tab 拖拽分离为独立窗口,并支持拖拽到其他窗口进行合并(含悬停预览),同时增加跨窗口文件去重与多窗口下的主进程窗口管理。

Changes:

  • 新增主进程 windowManager,集中管理编辑器窗口、tear-off 创建/跟随、合并预览与落点合并逻辑
  • 渲染进程增加 Tab 拖拽出窗/合并预览的 UI 与状态管理,并支持新窗口从主进程注入初始 Tab 数据
  • 增加跨窗口“同文件已打开则聚焦”的去重逻辑,并补齐对应 preload / global typings

Reviewed changes

Copilot reviewed 11 out of 12 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
src/types/tab.ts 为 Tab 增加合并预览标记字段 isMergePreview
src/shared/types/tearoff.ts 新增跨窗口传输的 Tab 数据结构与 FileTraitsDTO
src/renderer/hooks/useTab.ts 新窗口初始化接收 tear-off 数据;新增 tear-off/合并预览/单 Tab 窗口拖拽相关方法与事件监听
src/renderer/hooks/useFile.ts 打开文件时增加跨窗口去重聚焦逻辑
src/renderer/global.d.ts 补充 tear-off 相关 electronAPI typings,并改为从 shared types 导入 DTO
src/renderer/components/workspace/TabBar.vue 增加拖拽出窗检测、单 Tab 窗口拖拽、Sortable fallback 样式与合并预览样式
src/preload.ts 暴露 tear-off、跨窗口聚焦、窗口拖拽相关 IPC API
src/main/windowManager.ts 新增:窗口追踪、拖拽跟随窗口、合并预览/合并、跨窗口文件索引等主进程能力
src/main/ipcBridge.ts 新增/调整 IPC:tear-off、跨窗口聚焦、窗口边界获取、窗口拖拽、广播文件变化;并将部分逻辑改为基于 sender 路由
src/main/index.ts 主窗口纳入 windowManager 追踪
lang/index.json 新增一条 i18n 文案(当前看起来像测试内容)
lang/index.js 注释文案更新
Comments suppressed due to low confidence (1)

src/main/ipcBridge.ts:782

  • The file watcher diffing uses the current window’s filePaths against a single global watchedFiles set. In a multi-window app, when another window reports its list, this will incorrectly unwatch files still open in other windows. Track watched files per-window (or ref-count), then watch the union across all windows.
    // 先差异对比
    const newFiles = filePaths.filter((filePath) => !watchedFiles.has(filePath));
    const removedFiles = Array.from(watchedFiles).filter(
      (filePath) => !filePaths.includes(filePath)
    );

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.


// ── 窗口初始化逻辑 ─────────────────────────────────────────
// 如果是由 Tab 拖拽分离创建的新窗口,使用传入的 Tab 数据替换默认 Tab
let _tearOffInitPromise: Promise<void> | null = null;
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

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

_tearOffInitPromise is assigned but never read. With noUnusedLocals: true, this will fail TypeScript compilation. Consider removing the variable entirely, or actually use it (e.g., to prevent duplicate init calls / allow awaiting initialization).

Suggested change
let _tearOffInitPromise: Promise<void> | null = null;

Copilot uses AI. Check for mistakes.
Comment on lines +51 to +55
// 所有 on 类型监听 —— 使用 event.sender 自动路由到正确窗口
export function registerIpcOnHandlers(win: Electron.BrowserWindow) {
ipcMain.on("set-title", (_event, filePath: string | null) => {
ipcMain.on("set-title", (event, filePath: string | null) => {
const targetWin = BrowserWindow.fromWebContents(event.sender);
if (!targetWin) return;
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

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

registerIpcOnHandlers registers multiple ipcMain.on(...) listeners every time it’s called, but they’re global and never removed. If createWindow() runs more than once (macOS activate / reopening), handlers will be duplicated and fire multiple times. Add a one-time registration guard or move these listeners into a single global registration function.

Copilot uses AI. Check for mistakes.
Comment on lines +158 to +160
const clientX = event.originalEvent.clientX ?? event.originalEvent.touches?.[0]?.clientX ?? 0;
const clientY = event.originalEvent.clientY ?? event.originalEvent.touches?.[0]?.clientY ?? 0;
if (clientX && clientY) {
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

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

Offset calculation skips when either clientX or clientY is 0 because of the truthy check. This can leave initialOffsetX/Y as 0 even when the user clicked within the tab, causing the tear-off window to jump. Use an explicit !== undefined / Number.isFinite check instead of truthiness.

Suggested change
const clientX = event.originalEvent.clientX ?? event.originalEvent.touches?.[0]?.clientX ?? 0;
const clientY = event.originalEvent.clientY ?? event.originalEvent.touches?.[0]?.clientY ?? 0;
if (clientX && clientY) {
const clientX = event.originalEvent.clientX ?? event.originalEvent.touches?.[0]?.clientX;
const clientY = event.originalEvent.clientY ?? event.originalEvent.touches?.[0]?.clientY;
if (typeof clientX === "number" && typeof clientY === "number") {

Copilot uses AI. Check for mistakes.
try {
const crossResult = await window.electronAPI.focusFileIfOpen(result.filePath);
if (crossResult.found) return;
} catch {}
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

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

The empty catch {} swallows failures from focusFileIfOpen completely, which makes cross-window open issues hard to diagnose. Consider at least logging at debug level, or add a short comment explaining why ignoring the error is safe.

Suggested change
} catch {}
} catch (error) {
// Focusing the file in another window is a best-effort operation; if it fails,
// we fall back to opening the file in the current window.
console.debug("focusFileIfOpen failed, opening file in current window instead:", error);
}

Copilot uses AI. Check for mistakes.
Comment on lines +177 to +179
window.electronAPI.getWindowBounds().then((bounds) => {
cachedBounds = bounds;
});
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

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

getWindowBounds() is invoked without a .catch(...). If the IPC handler is unavailable or errors, this can produce an unhandled promise rejection and break subsequent drag logic. Please handle failures (e.g., set cachedBounds = null and proceed without tear-off detection).

Suggested change
window.electronAPI.getWindowBounds().then((bounds) => {
cachedBounds = bounds;
});
window.electronAPI
.getWindowBounds()
.then((bounds) => {
cachedBounds = bounds;
})
.catch(() => {
// 若 IPC 调用失败,则禁用本次 tear-off 检测
cachedBounds = null;
});

Copilot uses AI. Check for mistakes.
Comment on lines +3450 to +3454
"gl01kl7e": {
"zh-cn": "\n<br />\n\n# MilkUp\n\n<br />\n\n> 这是一段测试文字\n\n<br />\n\n***\n\n<br />\n\n* item1\n* item2\n* item3\n\n代码:\n\n```JavaScript\nconst text = \"Hello Word!\"\nconsole.log(text)\n```\n\n<br />\n\n| 1 | 2 | 3 |\n| :- | :- | :- |\n| 1 | 2 | 3 |\n| 1 | 2 | 3 |\n\n\n",
"ja": "",
"ko": "",
"ru": "",
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

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

This new i18n entry looks like test/demo content (e.g., “这是一段测试文字”) and the key doesn’t appear to be referenced elsewhere. Since it’s unrelated to the multi-window feature, please remove it or document where it’s used to avoid shipping stray content in production builds.

Copilot uses AI. Check for mistakes.
"fr": ""
},
"gl01kl7e": {
"zh-cn": "\n<br />\n\n# MilkUp\n\n<br />\n\n> 这是一段测试文字\n\n<br />\n\n***\n\n<br />\n\n* item1\n* item2\n* item3\n\n代码:\n\n```JavaScript\nconst text = \"Hello Word!\"\nconsole.log(text)\n```\n\n<br />\n\n| 1 | 2 | 3 |\n| :- | :- | :- |\n| 1 | 2 | 3 |\n| 1 | 2 | 3 |\n\n\n",
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

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

Typo in the sample code string: “Hello Word!” should be “Hello World!”.

Suggested change
"zh-cn": "\n<br />\n\n# MilkUp\n\n<br />\n\n> 这是一段测试文字\n\n<br />\n\n***\n\n<br />\n\n* item1\n* item2\n* item3\n\n代码:\n\n```JavaScript\nconst text = \"Hello Word!\"\nconsole.log(text)\n```\n\n<br />\n\n| 1 | 2 | 3 |\n| :- | :- | :- |\n| 1 | 2 | 3 |\n| 1 | 2 | 3 |\n\n\n",
"zh-cn": "\n<br />\n\n# MilkUp\n\n<br />\n\n> 这是一段测试文字\n\n<br />\n\n***\n\n<br />\n\n* item1\n* item2\n* item3\n\n代码:\n\n```JavaScript\nconst text = \"Hello World!\"\nconsole.log(text)\n```\n\n<br />\n\n| 1 | 2 | 3 |\n| :- | :- | :- |\n| 1 | 2 | 3 |\n| 1 | 2 | 3 |\n\n\n",

Copilot uses AI. Check for mistakes.
Comment on lines +87 to +89
tearOffTabStart: (
tabData: any,
screenX: number,
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

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

tabData is typed as any here, which defeats the point of adding TearOffTabData/FileTraitsDTO types elsewhere. Consider typing these parameters as TearOffTabData to keep IPC payloads type-safe end-to-end.

Copilot uses AI. Check for mistakes.
@LarryZhu-dev LarryZhu-dev merged commit 5a598b5 into Auto-Plugin:main Feb 27, 2026
3 of 4 checks passed
@github-actions
Copy link

🎉 Thank you for your contribution! If you have not yet joined our community group, please feel free to join us (when joining, please provide the link to this PR).

🎉 感谢您的贡献!如果您对此项目非常感兴趣,请扫描下方二维码加入我们(加群时请提供此 PR 链接)。

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