
登录页本身就是 Demo
GeekBye v1.7.5 给登录页加了一张产品截图,又在同一天把它删了——改用真实组件重新搭了一遍。另外:一个比 OCR 还快的用户,以及一天 25 个 style commits 究竟长什么样。
GeekBye v1.7.5 是一个有着双重人格的一周版本:37 commits,其中 28 个是登录页重设计——而这里面又有 25 个是 style(login) 微调,全部落在同一天。它同时也是修掉了代码库里最有教学意义的竞态之一的版本。两个故事都值得讲,而且它们汇合在同一个想法上:登录页是未登录用户看到的第一样东西,所以它最好就是产品本身,而不是产品的一张照片。
一张只活了大约五小时的截图
重设计的开头很常规。改成两栏布局,登录放左边,右边展示产品——用一张图。上午晚些时候的 commits 干的正是这件事:11:57,一张真正的预览截图进了仓库,一个 114 KB 的 PNG,六分钟后更新过一次。
到 17:04 它就没了。那个转向的 commit 把静态图片换成了 344 行真实组件——一个 LoginDemo 面板,由八个小部件拼装而成,复刻真实的悬浮窗 UI:带脉动 EQ 条和停止按钮的录音导航胶囊、带 Chat/Transcript 标签页的聊天面板、一条渲染出来的助手回答(恰如其分地,讲的是 React 的 useEffect)、"Tell me more / Simplify / Show example"(展开说说 / 简化 / 举个例子)快捷操作、实时提示行,以及 ⌘⏎ Assist 输入框。
"demo" 这个词很容易说过头,所以我们把话说准:按钮有真实的 hover 状态——辉光、流光、过渡动画——但没有任何东西接了点击事件,输入框也是只读的。它不是沙盒。它的本质,是把产品的真实 UI 词汇实时渲染出来:同一个 logo 资源,同样的渐变描边和背景模糊处理,和白标系统用的是同一个 productName import——所以这个 demo 会为从这套代码库构建出来的每个产品自动换牌。截图一样都做不到。截图在拍下的那天就过时了,在不匹配的显示器上是糊的,并且永远烙着你碰巧截下的那个产品的牌子。组件在任何 DPI 下都清晰、永远是最新的、对每个品牌都正确——因为它们不是设计系统的照片,它们就是设计系统。
顺便一提,那张被删掉的 PNG 在 release tag 那个时间点仍然留在仓库里——成了孤儿,没有任何东西引用它。每套代码库里都躺着几件落败方案留下的遗迹。
一天 25 个 style commits 长什么样
那一天的 commit log 是一本设计师的草稿本,以少见的诚实被保存了下来。栏宽比例先是 40/60,然后 45/55。平台 logo 上方的 "Works with"(支持平台)标签中午被加上、挪了位置、又被删掉——它活了大约半天。平台 logo 那一排本身(Google Meet、Teams、Zoom、Slack、Webex),上午 10:59 加入,当晚 22:08 被整排删除。而页脚的对齐经历了一场四种方案的长征:先是单个包裹容器,然后删掉居中类,然后 flexbox 居中,最后干脆把它往底部挪近一点了事。
把这读成瞎折腾很容易。我们把它读成设计工作本来的样子:靠渲染、观察、调整来收敛——而用 25 个小 commits 而不是一个被 squash 掉的 "redesign login page" 来做这件事,意味着探索过程本身被记录了下来。当有人问"我们是不是试过一排 logo?",答案就在历史里,带着时间戳。
比 OCR 还快的用户
这个版本的工程故事发生在五天前,一条三个 commits 的修复链里。从 v1.5.12 起,按下 Cmd+H 会以即时反馈的方式把截图入队:缩略图立刻出现在队列里,而文字提取——本地的 Apple Vision OCR,大约一到两秒——在后台运行。入队事件是真真切切带着 ocrText: '' 发出去的,旁边还有一条注释承诺稍后会补上。
这是个好优化,也造出了一个竞态。手快的用户按下 Cmd+H,紧接着就提交了问题。截图的文本字段还是空的。请求不带任何屏幕内容就发了出去,模型的回答——真实得让人火大——是 "I cannot see images"(我看不到图片)。这条天真的路径不是报错失败的;它是用一个"对错误问题的正确回答"失败的,这是软件所有失败方式里最让人摸不着头脑的一种。
修复是用优化推迟掉的那份异步工作给提交路径加了一道闸:只要队列里还有任何一张截图的 OCR 文本为空,请求就先停靠,UI 显示 "Reading screen..."(正在读取屏幕)指示;文本一落地,一个 useEffect 立刻触发,带上完整提取内容把请求发出去。diff 里有两个值得偷师的 React 细节:截图队列被镜像到一个在渲染期间同步的 ref 里,因为事件处理器的闭包链捕获的是过期状态;而停靠的那个请求是直接发送的,没走通用的排队请求 hook——原因同样是 stale closure。
commit message 里还如实记下了一个转折:为了追捕这个竞态加上的追踪日志,暴露了后端的第二个 bug——某条代码路径里,prompt 组装把截图文本整个弄丢了。一个症状,两个 bug,靠给整条链路加上观测、盯着文本在哪里消失才找到。这条链的结尾是一个产品决策,在竞态修复之后九分钟落地:Cmd+H 把截图入队用于批量分析,而 Cmd+Enter 永远对当前屏幕做一次全新捕获——两条清晰的工作流,取代一条模棱两可的。(Cmd+Enter 的 Assist 到底做什么,那是另一篇文章:对你正看着的任何东西的即时 AI 帮助。)
同一周还有个小尾巴,送给白标那一章的读者:Product/version (platform) 这个 User-Agent 得到了一个开发模式修复,因为在未打包的 Electron 里,app.getVersion() 返回的是 _Electron 的_版本,不是产品的。身份字符串,一如既往,总在管道里泄漏。
这个版本教会我们的三件事
- 把产品渲染出来,而不是给它拍照。 用真实组件搭的登录页 demo 会自动换牌,扛得住任何显示密度,也不可能过时。截图方案在一个工作日之内被尝试又被删除——便宜的实验,清楚的裁决。
- 每一次异步优化,都需要在使用点加一道闸。 "带着空 OCR 文本的即时反馈"是正确的 UX 决定,但提交路径必须学会等待优化推迟掉的那部分工作。如果你推迟了某件事,就要找到它结果的每一个消费者,教会它们区分"空"和"还没准备好"。
- 给整条链路加观测,而不是只盯嫌疑人。 追踪日志把截图文本从头跟到尾——从快捷键处理器到 IPC 再到 API 客户端——这就是为什么一次排查关掉了两个 bug,其中一个还在网络的另一端。
v1 故事的上一章,见终于又找得回来的会议(v1.7.3);整段弧线则见把软件打磨到完美地发布出去的解剖学。