开发 Ghostty 的一个非平凡功能

Mitchell Hashimoto 产品

摘要

Mitchell Hashimoto 详细介绍了如何使用智能 AI 编码工具为 Ghostty 开发一个不显眼的 macOS 自动更新功能,并逐步讲解了他的每次 AI 会话和迭代过程。

暂无内容
查看原文
查看缓存全文

缓存时间: 2026/05/16 03:38

# 我最近交付了一个非常规的 Ghostty 功能(macOS 自动更新,且不打扰用户),这个功能**很大程度上**是用 AI 开发的。 我经常被问到如何将 AI 和代理式编码工具用于非平凡场景,而这次正是一个绝佳机会,可以结合一个范围明确、非平凡、真实世界且已交付的功能,来完整展示我的流程¹。 这篇文章将完整、未经编辑地分享我实现该功能过程中所经历的每一次代理式编码会话。同时,我还会提供一些与流程和思考相关的额外背景。对了,如果你对 token 成本感兴趣,我也会一并说明。 **重要提示:这里面也有大量的人工编码。** 我几乎总会在 AI 完成工作后亲自介入,再进行一段时间的迭代。我不会每次都重复这句话,但在这里一次性说明。因此,你可能会看到 AI 产生的代码与最终代码之间存在一些差异。这是有意为之,我认为优秀的 AI 使用者必须是自己领域的专家,将 AI 作为助手而非替代品。 --- ## 功能概述 本文所介绍的功能是 macOS 上的“不打扰式更新通知”。该功能在终端窗口内显示更新状态,而不会通过创建窗口、夺取焦点等方式中断当前工作。 我们先来铺垫一下该功能诞生的背景(双关语,你马上就会明白)。在一次高调的 OpenAI 主题演讲中,演示被一个 Ghostty 更新提示粗暴地打断了: OpenAI 演示 我希望确保这种事不再发生²。我选择的方案是让更新通知变得 **不打扰**:不再弹出一个窗口,而是在应用的某个位置展示一个小的、非模态的 GUI 元素,从而不中断用户操作。 --- ## 前期规划 于是,我掏出了我的 AI 工具。**千万别这么做。** 我首先大致规划了功能如何工作。Ghostty 使用了极为流行的 macOS 更新框架 Sparkle。我查阅了相关文档,发现它们通过 Obj-C 协议支持自定义 UI。你需要从零开始重写大量内容,但这是可行的。 好了,我对 **后端** 有了大致想法。至于 **前端**,我并不太确定(而且这也不是我的专长)。我有一个非常模糊的想法:应该是一个嵌入标题栏的小按钮。我知道 macOS 通过 `titlebar accessory controllers` 支持在标题栏中放置自定义 UI,但除此之外,我对它的外观和交互方式并没有太多概念。 但这足以开始行动。AI 是一个极好的原型工具,即使你只知道自己不知道什么,也足以启动。我对整体方向已有足够把握。 --- ## 第一次会话:原型 UI 这是我的第一次代理式编码会话,初始提示如下: > 我想通过自定义 SPUUserDriver 来启用自定义、不打扰的更新通知与安装。我们先计划所需的自定义 UI。我们将 **只** 处理 UI。创建一个计划,用于创建满足 SPUUserDriver 所需各种状态的 SwiftUI 视图。我认为这些视图的最佳展示位置是 macOS 窗口标题栏的右上角。制定一个将其放在那里的计划。咨询预言者。 常见问题:“什么是预言者?” 它是 Amp 特有的只读子代理,使用较慢、成本更高的模型,通常更适合思考问题。所有计划我都会咨询预言者。 首先,我决定先原型 UI。 注意,我并没有让代理直接去 **构建完整功能**。原因有两个:首先,我甚至还不清楚自己想要的 UI/UX 是什么样子,所以我不可能合理期望 AI 能在一堆其他改动中帮我实现;其次,更小的工作块更容易审查、理解和迭代。 另一个需要注意的点是:我要求它只制定计划,不写任何代码。由于这是一个相对模糊的需求,我必须先审核计划,再让它开始大量工作(并大量消耗 token)。 **提示:** 对于任何非平凡任务,与代理交互共创一份全面的计划是至关重要的第一步。我通常还会(通过代理)将其保存为 `spec.md` 之类的文件,并在后续会话中说“查阅 @spec.md,然后处理 *某个任务*”。 代理提出了一个 **足够合理** 的计划,我让它继续实现了。你可以在后续对话中看到我的迭代过程。 **它生成的 UI 方向性非常好。** 虽然有很多细节问题(间距、颜色等),但看到 UI 后,我受到了启发,终于知道自己想要什么了。 **提示:** 我经常用 AI 来获取灵感。在这个案例中,我最终保留了它生成的大量(不是全部)UI 代码,但我也经常会让代理生成内容,然后全部丢弃,再自己(手动!)重做。我发现“从零到一”的创作阶段非常困难且耗时,而 AI 是极好的灵感缪斯。 ### 遇到瓶颈 你可以在聊天记录 11 到 14 中看到,我们正在进入 **混乱区**。代理创建的代码有一个严重 bug,它完全无法修复。而我也束手无策。 面对这种 bug,我通常会做几次“孤注一掷”的尝试。如果代理能解决,我可以研究它并学习。如果解决不了,我付出的成本也很低。如果代理解决了但我无法理解,我会把它撤掉。我不会交付自己不理解的代码。在它失败的同时,我也会切换到其他标签页搜索问题,并尝试自己解决。 到了这个节点,我知道自己需要退后一步,审查它做了什么,然后制定自己的计划。是时候自我学习并批判性思考了。AI 不再是解决方案,而是负担。 --- ## 清理会话 接下来的几次会话,我引导代理清理代码。 第二次会话 重点是将一些方法移动到更合适的位置: > 将 @macos/Sources/Features/Update/UpdateAccessoryView.swift 中的药丸背景、前景和徽章函数移动到 @macos/Sources/Features/Update/UpdateViewModel.swift 中,并使它们更加通用(background、foreground、badge)。 第三次会话 为代码添加了文档: > 更新 @UpdateBadge.swift 的文档。 **提示:** 添加文档是非常重要的一步,因为它能巩固你自己对代码的理解,同时也能帮助未来可能阅读或修改这段代码的代理。我发现当代理拥有自然语言描述和代码本身时,表现会好得多。 第四次会话 将视图模型移到应用全局位置,因为最初的工作把它放在了窗口范围内,而更新信息是应用范围的。 > 将更新视图模型数据移到 AppDelegate,因为更新信息是应用全局的。 在这些过程中,我通常也会手动做一些小的改动。 清理步骤非常重要。要有效地进行清理,你必须对代码有相当好的理解,这迫使我不能盲目接受 AI 编写的代码。随后,更好组织和文档化的代码也有助于未来的代理会话取得更好效果。 我有时会半开玩笑地称此为“反混乱会话”。 --- ## 直面“那个 Bug” 是时候回到初始会话中发现的那个 bug 了。我又花了几次会话试图让代理解决它。一开始我描述得很模糊,然后逐渐具体化,说明我会如何处理它。 首先是模糊会话: > 对于标准的原生标签页,更新附件视图不可见。它应该在窗口的标题栏中保持可见。 失败。然后我变得更加具体: > 我们需要更新 @macos/Sources/Features/Terminal/Window Styles/TerminalTabsTitlebarTahoe.swift 的标签栏约束,使其右边缘与更新附件视图的左边缘对齐,从而保持可见。 失败。接着,我尝试了另一种具体方法: > 如果我们修改 @macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift,将标签栏改为顶部附件视图而非底部附件视图,让我们的标签进入标题栏,会怎样? 失败。最后一次尝试: > @macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift 中的“右附件视图”和布局与 @macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift 中设置的更新附件视图存在冲突。我们能否约束标签栏,使其始终位于更新通知的左侧? 失败。 这段时间,我自己也一直在通过手动研究和人工努力尝试解决这个问题。我更具体的提示正是基于我在这过程中学到的东西。但总的来说,显然行不通。 我认为自己无法独自解决这个问题,于是决定转变方向。我决定对于那些有问题的标题栏样式,将更新通知放在窗口左下角覆盖在内容视图上,而不是标题栏中。 我 **无论如何** 都需要支持这种方式,因为 Ghostty 有完全隐藏标题栏的配置。所以,即使以后能解决标题栏样式的问题,我仍然需要支持另一种模式。 我的下一次会话 就按照这个计划推进,使用了非常具体的提示: > 增强 @macos/Sources/Features/Update 系统,使其也支持 @macos/Sources/Features/Terminal/TerminalView.swift 中的覆盖方式。更新通知应出现在窗口底部,覆盖在文字之上(因此不会调整终端视图大小)。所有点击行为应与附件视图相同。 它在这项任务上做得非常好。之后我做了大量的手动打磨(移动内容、重命名等),但核心工作很扎实。 下面是这次会话后不久功能展示的视频,展示了更新通知如何出现在窗口右下角(针对某些标题栏样式或隐藏标题栏时): --- ## 开始后端工作 UI 感觉 **足够好了**。我记下了很多后期要处理的打磨问题,但我想先开始后端工作,主要是看看是否会遇到未知的未知,从而打乱我的计划。 我手动创建了一个文件,其中包含了不完整的函数框架和许多 `TODO` 注释。然后我启动了一次会话 来完成这项工作: > 完成 @macos/Sources/Features/Update/UpdateDriver.swift。如有必要,查阅 Sparkle 文档以理解功能。https://sparkle-project.org/documentation/api-reference/Protocols.html **提示:** AI 非常擅长填空或“画出猫头鹰的剩余部分”。我这种创建脚手架的方式(使用描述性函数名称、参数、TODO 注释等)对我来说非常常见,而且效果很好。 实际上,它这次做得很 **糟糕**,我最后把所有这些代码都扔掉了。它生成的代码 **能用**,但方向明显错误。它将许多不同关注点混为一谈,并且在驱动器中存储状态的方式明显是错误的。 当我研究它的输出时,我意识到这是因为视图模型的结构不合理,于是我切换到清理模式,为 AI(以及如果我选择手动编写代码的人类)提供更好的框架。 --- ## 再次大规模清理 经验告诉我,前端 UI 和后端业务逻辑的整洁程度往往取决于中间视图模型的质量。因此,我花了一些时间手动重构了视图模型。这包括切换到标记联合体( tagged union ),而不是带有一堆可选值的结构体。我重命名了一些类型,移动了一些内容。 根据经验,我知道中间这点手动工作将为后续前端和后端的代理会话奠定成功基础。完成后,我继续进行了马拉松式的清理会话。 重构之后,我首先让代理再次完成猫头鹰的剩余部分,这次是检查我的更改并将依赖代码更新为新风格,同时移除旧代码: > 将 @macos/Sources/Features/Update/UpdateViewModel.swift 更新为仅使用新的 `UpdateState`。将 `state2` 重命名为 `state`(移除旧的 state)。 然后我让它删除额外的死代码: > 我认为我们可以删除 UpdateUIActions。它们不再被使用,因为我们的 UpdateState 有回调。 之后,我手动清理了一些东西,破坏了构建。因为要开会,我决定让代理在我忙的时候修复它: > 运行构建并修复错误。 **提示:**“我搞砸了一堆事情,请帮我收拾残局”是我使用代理的另一个常见场景。我认为这基本上与之前的填空模式属于同一类。 后来,我又开始对某些视图进行重构: > 将 @macos/Sources/Features/Update/UpdatePopoverView.swift 中的每个 case 转化成一个专用的 fileprivate Swift 视图,以键入的值作为参数,这样我们就可以移除那些 guard。 更多清理: > 将 @macos/Sources/Features/Update/UpdateViewModel.swift 中的 `iconName` 改为可选,如果为空则返回 nil。更新使用方式。 --- ## 模拟 在我第一次 UI 会话中,我让代理创建了一些演示代码,这样我就可以在不用真正检查更新的情况下看到 UI 效果。但更新流程有许多场景,而到目前为止我只测试了快乐路径。 在我的下一次会话中,我将模拟代码提取到一个专用文件中,并让代理创建更多场景: > 将 @macos/Sources/App/macOS/AppDelegate.swift 中的更新模拟代码提取到 @macos/Sources/Features/Update 的一个专用文件中。该文件应包含多个模拟场景(快乐路径、未找到、错误等),以便我们轻松尝试不同的演示。 **提示:** 代理非常擅长生成测试和模拟。它生成的模拟代码说实话相当粗糙,但能工作,而且它不是发布二进制文件的一部分,所以质量不太重要。

相似文章

加拉帕戈斯群岛上的智能体编程笔记

Hacker News Top

Dan Luu 反思了他使用 AI 编程代理的经验,指出虽然 AI 可能会编造看似可信但实际错误的复现步骤,但在调试和测试等领域仍能带来生产力提升。他讨论了现代软件开发中信任、自动化和质量之间的平衡。

氛围编码与智能工程正变得比我预想中更接近

Simon Willison's Blog

# 氛围编码与智能工程正变得比我预想中更接近 来源:[https://simonwillison.net/2026/May/6/vibe-coding-and-agentic-engineering/](https://simonwillison.net/2026/May/6/vibe-coding-and-agentic-engineering/) 2026年5月6日 我最近与 Joseph Ruscio 在 Heavybit 的 High Leverage 播客中讨论了 AI 编程工具: [Ep. #9, 与 Simon Willison 探讨 AI 编程范式转变](https://www.heavybit.com/library/podcasts/high-leverage/ep-9-the-ai-coding-paradigm-shift-with-simon

Ghostty: 反思1.0版本的发布

Mitchell Hashimoto

Mitchell Hashimoto 反思了他用 Zig 构建的终端模拟器 Ghostty 达到1.0版本的过程,讨论了项目的起源、成功但富有争议的内测版,以及他对这款终端的愿景。