【Bug已解决】Codex Desktop: project rename dialog closes when sidebar auto-hides in hover mode 解决方案
【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(commit=True) 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 动画。把这条写进隐藏/卸载的判断条件里,这类"输入莫名丢失"的问题就不会再发生。
