【Bug已解决】Codex Desktop: project rename dialog closes when sidebar auto-hides in hover mode 解决方案原始报错Codex Desktop: project rename dialog closes when sidebar auto-hides in hover mode 场景桌面应用开了悬停模式——鼠标离开侧边栏时侧边栏自动隐藏。此时用户在侧边栏里点开重命名项目对话框开始输入鼠标一移开侧边栏自动收起对话框跟着被关掉输入内容丢失。 关键词悬停模式、侧边栏自动隐藏、模态对话框、焦点与生命周期、输入防丢。一、现象长什么样复现非常稳定设置里开启悬停模式hover mode即鼠标移出侧边栏后它自动滑出隐藏在侧边栏里右键某项目 → 重命名弹出输入框用户开始打字为看正文或点别的鼠标移出侧边栏侧边栏按设定自动隐藏重命名输入框随之被销毁已经打的字没了再打开发现重命名没保存。这不是崩溃但体验很坏用户在对话框里做有状态输入却被一个无状态 UI 动画侧边栏收起强行打断并清掉状态。核心矛盾是侧边栏的隐藏逻辑把还开着对话框这件事给忽略了。二、背景为什么悬停模式会和对话框冲突悬停模式的设计意图是侧边栏是辅助区鼠标不在那它就让位给主内容。实现上通常是监听鼠标的mouseleave/ 失焦事件触发侧边栏collapse()。问题在于重命名输入框是一个模态/半模态对话框它挂在侧边栏这个容器里。当侧边栏收起时容器被隐藏或卸载里面的对话框组件也被一起卸载输入状态随之丢弃。对话框的应该在用户确认/取消前一直存在这条不变量被侧边栏的鼠标离开就收起覆盖了。正确的设计应当是只要有一个活动对话框尤其是正在编辑的侧边栏的自动隐藏就必须被抑制直到对话框关闭。三、根因隐藏逻辑不看是否有活动对话框把问题抽象根因有三隐藏条件单一侧边栏只在鼠标离开时收起没有检查当前是否有未关闭的对话框。对话框挂在会卸载的容器里重命名框是侧边栏的子组件侧边栏一收它就死而不是独立存活。输入状态未暂存即便要关也应先把已输入文本暂存重新打开时恢复而不是直接丢弃。下面用最小模型复现第 1 类最典型再给修复。四、最小可运行复现用一个简化的侧边栏 对话框状态机模拟。错误写法隐藏只看鼠标是否离开。class Sidebar: def __init__(self): self.visible True self.dialog_open False self.draft def on_mouse_leave(self): # 错误鼠标一离开就收起不管有没有开着的对话框 self.visible False def open_rename(self): self.dialog_open True self.visible True def type_char(self, ch: str): if self.dialog_open: self.draft ch def close_rename(self, commit: bool): self.dialog_open False self.draft if not commit else self.draft if __name__ __main__: sb Sidebar() sb.open_rename() sb.type_char(新) sb.type_char(名) sb.on_mouse_leave() # 鼠标移开 - 侧边栏收起 # 此时对话框其实已被收起逻辑连带关闭但代码没清 dialog_open print(侧边栏可见:, sb.visible) # False print(对话框仍标记打开:, sb.dialog_open) # True状态不一致 print(已输入:, sb.draft) # 新名但实际 UI 已丢这里对话框状态和侧边栏可见性出现了不一致UI 上框没了但dialog_open还是 True输入也没保存——这就是输入丢失且不提示的来源。五、方案隐藏前检查是否有活动对话框第一层修复侧边栏是否收起必须考虑当前是否有活动对话框。有则抑制自动隐藏。class SidebarFixed: def __init__(self): self.visible True self.dialog_open False self.draft def on_mouse_leave(self): # 关键有活动对话框时不收起 if self.dialog_open: return self.visible False def open_rename(self): self.dialog_open True self.visible True def type_char(self, ch: str): if self.dialog_open: self.draft ch def close_rename(self, commit: bool): if commit: self.saved self.draft self.dialog_open False self.draft # 关闭后可恢复自动隐藏行为 self.visible True if __name__ __main__: sb SidebarFixed() sb.open_rename() sb.type_char(新) sb.type_char(名) sb.on_mouse_leave() # 有对话框 - 不收起 print(侧边栏可见:, sb.visible) # True被抑制 print(对话框打开:, sb.dialog_open) # True sb.close_rename(commitTrue) print(保存结果:, sb.saved) # 新名现在鼠标移开不会打断正在进行的重命名。六、方案对话框独立存活不被容器卸载第二层修复从架构上让重命名对话框不依附于会卸载的侧边栏。一种做法是把它渲染到应用级覆盖层overlay / portal由全局状态管理其生命周期侧边栏收起不影响它from typing import Optional class AppRoot: def __init__(self): self.sidebar_visible True self.dialog: Optional[dict] None # 全局持有不挂在侧边栏 def set_sidebar_visible(self, v: bool): # 侧边栏隐藏绝不能动 dialog self.sidebar_visible v def on_sidebar_mouse_leave(self): # 即便收起侧边栏dialog 由 AppRoot 持有不受影响 if self.dialog is not None: return self.sidebar_visible False def open_rename(self, current: str): self.dialog {draft: current, committed: None} def type_char(self, ch: str): if self.dialog: self.dialog[draft] ch def confirm(self): if self.dialog: self.dialog[committed] self.dialog[draft] self.dialog None def cancel(self): self.dialog None if __name__ __main__: app AppRoot() app.open_rename(旧名) app.type_char(改) app.on_sidebar_mouse_leave() # 侧边栏想收起但 dialog 在 AppRoot 上 print(侧边栏可见:, app.sidebar_visible) # False print(对话框仍在:, app.dialog is not None) # True独立存活 app.confirm() print(结果:, app.dialog) # None已提交清空对话框活在AppRoot而不是侧边栏子树里侧边栏生灭不再波及它。七、方案输入暂存避免任何意外丢字第三层修复即便对话框因某种原因必须关如切换窗口、异常也要先暂存草稿重开时恢复class RenameController: def __init__(self): self.draft self.saved_draft # 暂存用于意外关闭后恢复 def open(self, current: str): # 优先恢复上次暂存 self.draft self.saved_draft or current self.saved_draft def type_char(self, ch: str): self.draft ch self.saved_draft self.draft # 每次输入都暂存 def force_close(self): # 任何非用户确认的关闭都保留草稿 self.saved_draft self.draft def commit(self): result self.draft self.draft self.saved_draft return result if __name__ __main__: rc RenameController() rc.open(项目A) rc.type_char(新) rc.force_close() # 意外关闭如侧边栏收起 rc.open(项目A) # 重开恢复草稿 print(恢复出的草稿:, rc.draft) # 新没丢三层合起来不主动关、关了也独立存活、真丢了也能恢复。八、验证把对话框打开时侧边栏不收起锁进测试def test_sidebar_does_not_collapse_while_dialog_open(): sb SidebarFixed() sb.open_rename() sb.type_char(x) sb.on_mouse_leave() assert sb.visible is True assert sb.dialog_open is True def test_draft_survives_unexpected_close(): rc RenameController() rc.open(A) rc.type_char(y) rc.force_close() rc.open(A) assert rc.draft y if __name__ __main__: test_sidebar_does_not_collapse_while_dialog_open() test_draft_survives_unexpected_close() print(悬停模式对话框测试通过。)九、排查清单对话框被自动隐藏关掉按顺序查隐藏条件侧边栏收起逻辑是否检查当前有活动对话框没有就会误关。生命周期归属对话框挂在哪个组件树是否随侧边栏卸载而销毁模态保证正在编辑的对话框是否应在用户确认/取消前一直存在输入暂存意外关闭时草稿是否暂存重开能否恢复焦点逻辑鼠标离开 ≠ 用户想放弃编辑隐藏逻辑是否把两者混淆事件顺序mouseleave与对话框open哪个后到避免打开瞬间被旧事件关掉。状态一致对话框关闭后dialog_open等标志是否同步清除避免幽灵状态。十、小结悬停模式下重命名对话框被关掉是自动隐藏逻辑忽略了活动对话框这一不变量导致的状态丢失。修复三层抑制有活动对话框时侧边栏不自动收起解耦对话框由应用级状态持有不随侧边栏卸载暂存任何非确认关闭都保留草稿重开可恢复。核心原则一句话正在进行的、有状态的用户编辑优先级高于无状态的 UI 动画。把这条写进隐藏/卸载的判断条件里这类输入莫名丢失的问题就不会再发生。