# NewEditor Window Workspace Single-Source Refactor Plan Date: 2026-04-22 Status: In Progress ## Progress Completed on 2026-04-22: 1. `EditorWindowWorkspaceCoordinator::BuildWorkspaceMutationController()` no longer reads mutation input from `EditorWindowWorkspaceStore` mirror 2. mutation input is now built from live `EditorWindow` controllers at call time 3. Debug build of `XCUIEditorApp` passed 4. manual verification passed for: - detach - re-dock - open / close baseline behavior - detached title baseline behavior Next: 1. split detached title refresh from frame-time projection 2. then remove the remaining per-frame mirror commit path ## 1. Objective 这份计划只处理 `new_editor` 当前最严重的一处结构性冗余: 1. 每个 `EditorWindow` 已经各自持有一份 live `UIEditorWorkspaceController` 2. `EditorWindowWorkspaceStore` 又额外持有一份完整的 `UIEditorWindowWorkspaceSet` 3. 宿主层每帧把前者整份投影回后者 4. 跨窗口操作再从后者反向重建 controller 灌回窗口 本计划的目标不是“看起来更抽象”,而是把窗口 workspace 的单一事实源收回到 live window,自根源消除双写状态同步环,同时不破坏以下现有行为: 1. 分离窗口打开与关闭 2. Tab 跨窗口拖拽 3. 面板 detach / re-dock 4. detached tool window 标题与尺寸策略 5. 现有 `UIEditorWindowWorkspaceController` 上的跨窗口变换语义 ## 2. Confirmed Problem 当前重复链路已经形成完整闭环: 1. live state: - `EditorWindowRuntimeController::m_workspaceController` 2. mirrored state: - `EditorWindowWorkspaceStore::m_windowSet` 3. frame-time projection: - `EditorWindowHostRuntime::RenderAllWindows(...)` - `EditorWindowWorkspaceCoordinator::CommitWindowProjection(...)` 4. mutation-time reverse application: - `EditorWindowWorkspaceCoordinator::BuildWorkspaceMutationController()` - `EditorWindowWorkspaceCoordinator::SynchronizeWindowsFromWindowSet(...)` 这不是普通 cache,而是双向同步的第二套真相源。 ## 3. Root Cause 根因不在某一个函数,而在宿主层把“跨窗口协调”实现成了“跨窗口状态镜像”。 当前设计默认: 1. window 内的 live controller 负责真实交互与更新 2. store 内的 window set 负责跨窗口 mutation 的输入 3. 二者通过每帧投影保持近似一致 这导致: 1. 同一语义的 workspace/session 状态有两份 owner 2. 每帧发生整份复制和整套验证 3. mutation 逻辑被迫依赖 mirror,而不是直接面向 live windows 4. 标题刷新、窗口集合描述、关闭路径,也被顺带绑在 mirror 上 ## 4. Why This Is The Most Serious Redundancy 这处问题同时命中四个严重特征: 1. 同一职责重复实现 - live window controller 和 mirrored window set 都在表达同一份窗口 workspace 状态 2. 同一帧重复计算 - 每帧 `CommitWindowProjection(...)` 都会复制 `m_windowSet`、覆写某个窗口状态、再验证整套 window set 3. 结构扩散 - 打开窗口、关闭窗口、跨窗口拖拽、标题刷新都被迫围绕 mirror 组织 4. 没有独立 owner - `EditorWindowWorkspaceStore` 并没有独立业务语义,只是在宿主层替 live state 保存一份副本 ## 5. Red Lines 这次重构明确禁止以下做法: 1. 只删除 `CommitWindowProjection(...)`,但继续让 mutation 从旧 store 读数据 2. 先删 store,再临时把跨窗口逻辑散落回 `EditorWindowWorkspaceCoordinator` 3. 改写 `UIEditorWindowWorkspaceController` 的跨窗口变换语义 4. 修改 dock / transfer / extract 的业务规则,只为了配合结构调整 5. 以“后面再补验证”为理由跳过 detached window / global tab drag 的回归验证 其中第 1 条最危险。它一定会造成功能回归,因为当前 detached 窗口标题刷新和 mutation 输入都还绑在 projection 路径上。 ## 6. Target End State 目标终态必须满足: 1. live `EditorWindow` 持有的 `UIEditorWorkspaceController` 成为唯一可变真相源 2. 跨窗口 mutation 需要的 `UIEditorWindowWorkspaceSet` 改为按需从 live windows 现采样构建 3. `EditorWindowWorkspaceStore` 不再保存整份 `workspace/session` 4. detached 窗口标题刷新从 projection 逻辑中独立出来 5. primary / active / existing window 集合信息只保留真正被消费的最小元数据 允许两种落地形式: 1. 保留 `EditorWindowWorkspaceStore`,但只保留窗口元数据和注册信息,不再保存 workspace/session 2. 完全删除 `EditorWindowWorkspaceStore`,由 coordinator 直接从 host runtime 构建 snapshot ## 7. Refactor Strategy ### Phase A. Freeze semantics and add live snapshot builder 先新增从 live windows 构建 `UIEditorWindowWorkspaceSet` 的单一路径,例如: 1. `BuildLiveWindowWorkspaceSnapshot()` 2. 或 `BuildWorkspaceMutationControllerFromLiveWindows()` 要求: 1. snapshot 必须完整覆盖所有活跃窗口 2. 每个窗口的 workspace/session 直接来自 live controller 3. primary window id 必须来自 live host state 4. active window id 只保留当前真实需要的最小语义 这一步先增加,不删旧路径。 ### Phase B. Make mutation read from live snapshot instead of store mirror 把以下入口改为依赖 live snapshot: 1. `BuildWorkspaceMutationController()` 2. `TryStartGlobalTabDrag(...)` 3. `TryProcessDetachRequest(...)` 4. `TryProcessOpenDetachedPanelRequest(...)` 5. cross-window drop commit path 要求: 1. mutation 前只从 live windows 取状态 2. mutation 后仍复用 `SynchronizeWindowsFromWindowSet(...)` 回灌窗口 3. 在这一阶段仍允许 store 暂时存在,但不能再是 mutation 输入源 ### Phase C. Split title refresh from projection 当前 `RefreshWindowTitle(...)` 被挂在 `CommitWindowProjection(...)` 后面,这是一种错误耦合。 必须单独拆出标题同步路径: 1. detached 窗口标题直接基于 live controller 刷新 2. 标题刷新继续允许 frame-time 执行 3. 但标题刷新不能再隐含 workspace mirror commit 完成后,删除“为了标题而保留每帧 projection”的借口。 ### Phase D. Remove mirrored workspace/session from store 在 mutation 输入已经切换到 live snapshot 后: 1. 删除 `EditorWindowWorkspaceStore` 中的 `workspace/session` 镜像 2. 删除 `BuildWindowState(...)` 这类整份复制逻辑 3. 删除每帧 `CommitWindowProjection(...)` 4. 删除围绕 projection 的整套验证与回退路径 如果此时 store 还存在,它只能保存: 1. 已注册窗口 id 2. primary window id 3. 可能仍有意义的轻量元数据 ## 8. Functional Risk Analysis 这次重构不是零风险,但风险集中且可控。 ### 8.1 Real risk: title synchronization 如果直接删除每帧 projection,而不拆标题刷新: 1. detached 窗口标题可能停留在旧 active panel 2. 同一窗口内切 tab 后标题可能不再更新 因此标题刷新必须先和 projection 解耦,再删 projection。 ### 8.2 Real risk: mutation input staleness 如果 mutation 仍从 store 读状态,而 store 不再每帧同步: 1. detach 可能从旧 workspace 提取 panel 2. cross-window drop 可能基于过期 session 判断 panel owner 3. open detached panel 可能错误判断某 panel 已经打开或未打开 因此必须先引入 live snapshot builder,再切 mutation 输入。 ### 8.3 Medium risk: lifecycle and close path assumptions 当前 `IsPrimaryWindowId(...)`、`DescribeWindowSet()`、`RemoveWindowProjection(...)` 都依赖 store。 需要逐条确认: 1. 哪些只用于日志 2. 哪些只用于关闭路径 3. 哪些真影响行为 如果不区分,容易把“日志依赖”误当成“结构依赖”。 ### 8.4 Low risk: cross-window mutation algorithm itself `UIEditorWindowWorkspaceController` 和 `UIEditorWorkspaceTransfer` 负责的是跨窗口 panel 变换语义。 本计划不改: 1. extract 规则 2. insert 规则 3. dock 规则 4. detached workspace 生成规则 因此只要 mutation 输入快照正确,这一层不是主要风险源。 ## 9. Why This Refactor Is Actually Simplifying 它不是表面简化,而是实质上减少状态 owner。 完成后会得到: 1. 窗口 workspace 只在 live window 内持有一份 2. mutation 输入不再依赖长期镜像,而是按需快照 3. frame path 不再做整份 workspace/session 复制与整套 validate 4. coordinator 的职责变成: - 从 live windows 取快照 - 调用 mutation controller - 把 mutation 结果同步回 live windows 5. title refresh 变成一个独立的小职责,而不是夹带在 projection 里 也就是说,复杂度不是被藏起来,而是真的减少了一层。 ## 10. Why This Refactor Is Root-Cause-Oriented 它之所以是从根源出发,是因为它处理的是“谁拥有窗口 workspace 真相”。 如果不解决 owner 问题,只做局部删减,最终都会回到: 1. 某处还需要一份 mirror 2. 某帧还得再同步一次 3. 某条宿主功能又悄悄绑回 projection 而这份计划直接把真相源收回 live window,把跨窗口 controller 降级为临时 snapshot consumer,这才是对根因动刀。 ## 11. Validation Requirements 没有以下验证,不允许落地删除 projection: 1. detached window 内切换 active panel 后,窗口标题实时更新 2. primary window detach panel 正常创建新窗口 3. detached window 再次拖拽 panel 发起 global tab drag 正常 4. 跨窗口 drop 到 tab stack 和 relative dock 都正常 5. 已打开 tool window 的 panel 重复打开请求仍然正确复用 6. 关闭 detached window 后,其余窗口状态保持正确 7. 关闭 primary window 时,其他窗口生命周期不异常 8. `XCUIEditorApp` Debug 构建通过 ## 12. Completion Criteria 只有满足以下条件,这次重构才算真正完成: 1. `EditorWindowWorkspaceStore` 不再持有整份 `workspace/session` 镜像 2. `EditorWindowHostRuntime::RenderAllWindows(...)` 不再每帧调用 workspace projection commit 3. 所有跨窗口 mutation 都从 live snapshot 构建输入 4. detached 标题刷新与 projection 彻底解耦 5. 现有 detach / drag / re-dock / close 行为无回归 ## 13. Final Statement 这次重构不能靠“删几行同步代码”完成。 真正安全、真正彻底的路径只有一条: 1. 先建立 live snapshot 输入 2. 再把 mutation 输入切过去 3. 再拆标题刷新 4. 最后删除 mirror 只有按这个顺序,才能既不破坏功能,又从根上消灭这套双写宿主残片。