Codex 更新 Computer Use 之后,让我感觉惊艳的是它的 onboarding:点「Enable Accessibility」,系统设置窗口弹出来,一张引导卡从按钮位置沿着抛物线飞出,贴在设置窗口旁边;卡上露出宿主 app 的图标,拖进去就完成了授权;落位时卡片和箭头各自越过再回来,用户移动设置窗口时,上面的蓝色箭头像被惯性拽着拉长。
macOS TCC 是一套对用户和开发者都不友好的系统。大部分 app 的做法是弹一个对话框,写一句「请去系统设置授权」,然后假装这件事已经尽力了。Codex 这段 onboarding 则是体现出了对 macOS 系统深刻的理解,通过引导用户的目光、动作,顺滑地完成授权的整个过程。
因为 Computer Use 刚发布几个小时,社区里还没有找到复刻的,所以我觉得自己做一个。(不过实现到一半的时候发现有人逆向了一个,但看了一下效果不够理想,很多细节没还原)。
一开始我想既然录了视频,就按照视频效果来实现吧。需要还原的主要是两块:飞行动画和拖动引导。细节层面要照顾的有这些:
按这份清单做完第一版,视觉直觉上就不对。两个最明显的问题:
想了想,我觉得还是得逆向找找思路。
Codex 的 macOS 插件产物就躺在本地缓存里,用 Hopper、IDA、Ghidra 各过了一遍,把符号、字符串常量、布局数值逐项核对。做完这些我们获得了如下的信息:
按这些参数再实现一次,差距缩小了,但还没到完全一致。原因有几个:反编译的符号只能告诉我用了哪些 API,缓动和弹簧阻尼这一层属于运行时组合,看不到;Codex 有一部分走的是自定义渲染,绕过了标准动画 API;我的显示器缩放、字体设置和 Codex 录屏机器不同,肉眼像素比对本来就有偏差。
考虑到继续反编译的话收效甚微,于是换了一个方向,在目前的这个基础上,把体验按照我们直观的感受进行细致的优化,重新设计几处交互。
Codex 飞行用的是两个控件做交叉淡入:一个承载原卡片样式,一个承载目标卡片样式,两个控件同步沿抛物线移动,靠透明度切换完成内容变换。
我们改成单一飞行卡片的方案:从头飞到尾只用一张卡片,源图和目标图在同一个窗口里做交叉淡入,圆角、尺寸、阴影都是同步插值。
这样做的几处改善:
模糊做成钟形曲线:起点和终点归零,抛物线顶点达到峰值。顶点位置的模糊和图像交叉淡入落在同一时间点,内容切换被最深的模糊遮住,观感是一次「模糊里完成了变身」。
Codex 原版和网上大部分复刻在起飞瞬间都有一小段真空:源行已经消失,但飞行控件还没到位。我们用两步处理这段衔接。
反向返回对称:在抛物线顶点那一帧触发回调,把行的占位态切到最终形态(已完成或者回到原按钮),飞行卡片正好在最高点做反向交叉淡入,行的变化被整张卡遮住,落地时不会闪。
Stage Manager 开启时,系统设置被唤起会把当前 app 从屏幕中央推到左侧 pile。如果不处理,飞行从一个被推走的源位置起飞,卡片直接落到屏幕外。
最早想过抢回焦点,让 app 始终在前台。实测和 Stage Manager 的调度逻辑冲突,体验反而更差:app 一会儿被推走一会儿被拉回,光标位置也跟着来回跳。
最终的顺序是:
顺序反过来会错:先动再等,源行会在 Stage Manager 挪动的中途被采样,飞行起点就对不上。
另一处容易踩的坑:飞行卡片和引导卡的窗口属性刻意不带 transient 标志,否则 Stage Manager 会把它们当成宿主 app 的一部分一起推走。
用户把系统设置最小化到 Dock 之后,再次点击权限按钮,系统会用 genie 动画把窗口从 Dock 恢复。如果直接读窗口位置,读到的是动画中间帧,飞行卡片会落在窗口飞出的半路。
判定条件改成两个,任一满足才返回:两次采样到的窗口位置像素相同,或者持续观察累计超过 1.2 秒。这个时间窗口足够覆盖 genie 动画的全部过程,飞行终点一定落在设置窗口真正停住的位置。
引导卡贴在系统设置窗口旁边,用户拖动系统设置时,引导卡 1:1 跟随。如果箭头也完全刚性,视觉上会「糊」在一起。我们给向上箭头加了一个纵向的惯性形变:
方向故意反过来:窗口向下移动时箭头顶部向上拉伸,观感像橡皮筋被拖拽出来的后坐力。刚性跟随会让箭头和窗口看起来是同一块,加了反向形变之后,箭头变成一个单独的物体,有自己的惯性。
卡片本身的落位也用欠阻尼弹簧:卡片和箭头会越过目标位置再回来。调到接近临界值看起来更稳重,但那一下弹的节奏正是 Codex 最有辨识度的手感,保留了这一点。
Codex Computer Use 的权限引导覆盖两项:Accessibility 和 Screen Recording。这两项是 macOS 上最常见、也是必须走「拖 app 进列表」的权限。AskForPermission 把覆盖面扩到六项:
| 权限 | PermissionKind |
|---|---|
| Accessibility(辅助功能) | .accessibility |
| Screen & System Audio Recording(屏幕与系统音频录制) | .screenRecording |
| Input Monitoring(输入监控) | .inputMonitoring |
| Full Disk Access(完全磁盘访问权限) | .fullDiskAccess |
| Developer Tools(开发者工具) | .developerTools |
| App Management(App 管理) | .appManagement |
其中「开发者工具」和「App 管理」没有公开的查询 API,我们在运行时从 TCC 私有框架里解析出对应的符号,不加编译期依赖。未来 macOS 如果移掉这些符号,调用退回到「未授权」状态,不会闪退。
不在覆盖范围内的三类权限各有原因:
用户把图标拖进系统设置的权限列表之后,macOS 会弹一个 TCC 确认弹窗。系统不会通知我们这个弹窗的结果,需要自己判断用户是点了允许还是取消。
我们同时监听四个信号,哪个先命中以哪个为准:
任何一路「弹窗消失类」信号命中后,会额外等 200ms 再读一次权限状态,因为 TCC 的更新偶尔比弹窗消失的 UI 慢半拍。整个判断不需要额外权限:窗口列表信息公开可读,app 前后台也是本地查询。
做完这一圈,我对 Codex 这段 onboarding 的评价比一开始更高。
AppKit 其实早就提供了这些能力:精准唤起系统设置对应页面、把宿主 app 塞进拖拽面板、让浮动窗口在 Stage Manager 下稳定跟随。这些机制散落在文档里,很少被串起来。Codex 的工程师知道每一层能干什么,也知道它们组合起来可以做成什么。
用户不需要在系统设置里艰难寻找开关的位置,整个流程轻松丝滑地完成了。
我们在这条路径上做的事情,是把几个容易被忽略的细节做干净:前后无缝切换的时机、Stage Manager 下的顺序、最小化复原时的窗口落位、单卡片的飞行路径、箭头的惯性方向,以及更大的权限覆盖面。
一个优秀的交互一旦被人们知晓,会提高大家审美的阈值,也会让更多产品在起步的时候更容易靠近「优雅」的标准。最后希望这个开源项目可以被更多产品用上,无论是引用或者是蒸馏,希望未来都有更多 macOS 的软件用上这个交互。
If you have anything you'd like to discuss, any ideas you want to bounce around, please send me a message.