【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 场景:桌面应用开了"悬停模式"——鼠标离开侧边栏时侧边栏自动隐藏。此时用户在侧边栏里点开"重命名项目"对话框开始输入,鼠标一移开,侧边栏自动收起,对话框跟着被关掉,输入内容丢失。 关键词:悬停模式、侧边栏自动隐藏、模态对话框、焦点与生命周期、输入防丢。

一、现象长什么样

复现非常稳定:

  1. 设置里开启"悬停模式"(hover mode),即鼠标移出侧边栏后它自动滑出隐藏;
  2. 在侧边栏里右键某项目 → 重命名,弹出输入框,用户开始打字;
  3. 为看正文或点别的,鼠标移出侧边栏;
  4. 侧边栏按设定自动隐藏,重命名输入框随之被销毁,已经打的字没了;
  5. 再打开,发现重命名没保存。

这不是崩溃,但体验很坏:用户在对话框里做"有状态输入",却被一个"无状态 UI 动画"(侧边栏收起)强行打断并清掉状态。核心矛盾是侧边栏的隐藏逻辑把"还开着对话框"这件事给忽略了

二、背景:为什么悬停模式会和对话框冲突

悬停模式的设计意图是:侧边栏是辅助区,鼠标不在那它就让位给主内容。实现上通常是监听鼠标的 mouseleave / 失焦事件,触发侧边栏 collapse()

问题在于:重命名输入框是一个模态/半模态对话框,它挂在侧边栏这个容器里。当侧边栏收起时,容器被隐藏或卸载,里面的对话框组件也被一起卸载,输入状态随之丢弃。对话框的"应该在用户确认/取消前一直存在"这条不变量,被侧边栏的"鼠标离开就收起"覆盖了。

正确的设计应当是:只要有一个活动对话框(尤其是正在编辑的),侧边栏的自动隐藏就必须被抑制,直到对话框关闭。

三、根因:隐藏逻辑不看"是否有活动对话框"

把问题抽象,根因有三:

  1. 隐藏条件单一:侧边栏只在"鼠标离开"时收起,没有检查"当前是否有未关闭的对话框"。
  2. 对话框挂在会卸载的容器里:重命名框是侧边栏的子组件,侧边栏一收它就死,而不是独立存活。
  3. 输入状态未暂存:即便要关,也应先把已输入文本暂存,重新打开时恢复,而不是直接丢弃。

下面用最小模型复现第 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("悬停模式对话框测试通过。")

九、排查清单("对话框被自动隐藏关掉"按顺序查)

  1. 隐藏条件:侧边栏收起逻辑是否检查"当前有活动对话框"?没有就会误关。
  2. 生命周期归属:对话框挂在哪个组件树?是否随侧边栏卸载而销毁?
  3. 模态保证:正在编辑的对话框是否应在用户确认/取消前一直存在?
  4. 输入暂存:意外关闭时草稿是否暂存?重开能否恢复?
  5. 焦点逻辑:鼠标离开 ≠ 用户想放弃编辑,隐藏逻辑是否把两者混淆?
  6. 事件顺序:mouseleave 与对话框 open 哪个后到?避免打开瞬间被旧事件关掉。
  7. 状态一致:对话框关闭后,dialog_open 等标志是否同步清除?避免幽灵状态。

十、小结

"悬停模式下重命名对话框被关掉"是自动隐藏逻辑忽略了活动对话框这一不变量导致的状态丢失。修复三层:

  • 抑制:有活动对话框时,侧边栏不自动收起;
  • 解耦:对话框由应用级状态持有,不随侧边栏卸载;
  • 暂存:任何非确认关闭都保留草稿,重开可恢复。

核心原则一句话:正在进行的、有状态的用户编辑,优先级高于无状态的 UI 动画。把这条写进隐藏/卸载的判断条件里,这类"输入莫名丢失"的问题就不会再发生。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐