Vibing a Non-Trivial Ghostty Feature

Mitchell Hashimoto

用 Vibe Coding 打造一個不簡單的 Ghostty 功能

我最近上線了一個不簡單的 Ghostty 功能(低干擾的 macOS 自動更新),這個功能很大程度上是由 AI 開發的。

我經常被問到如何以具體且具一定複雜度的實際案例來使用 AI 與 agentic coding(代理式程式設計)工具,而這次正是一個絕佳的機會,讓我能透過一個範圍明確、具複雜度且已實際上線的功能1,完整呈現我的流程。

這篇文章將完整、未經編輯地分享我在開發並上線這個功能的過程中所進行的每一場 agentic coding 對話。同時,我也會提供關於流程與思考脈絡的額外說明。當然,對於好奇 token 成本的人,我也會一併公開。

重要說明:當中也有大量的人工撰寫程式碼。我幾乎總是會在 AI 完成工作後,親自再反覆修改一段時間。與其在每個環節都重複說明,我在這裡先一次說清楚。因此,你可能會發現 AI 產出的內容與最終程式碼之間存在一些差異。這是刻意為之,我認為優秀的 AI 駕馭者本身就是其領域的專家,他們將 AI 視為助手,而非替代品。


功能介紹

這篇部落格文章要介紹的完成品,是 macOS 上低干擾的更新通知功能。這個功能會在終端機視窗內顯示更新狀態,而不會透過建立新視窗、搶走焦點等方式中斷使用者的工作。

讓我們先回顧一下催生這個功能的背景(容我玩個文字遊戲,稍後你就會明白)。在一場備受矚目的 OpenAI 主題演講中,一場示範被 Ghostty 的更新提示粗魯地打斷了:

OpenAI 示範畫面

我想要確保這種情況不再發生2。我決定採取的做法,是讓更新通知變得低干擾。應用程式不會彈出視窗,而是會在某個不會打斷使用者的位置,顯示一個小型的、非強制回應(non-modal)的圖形介面元件。


AI 之前的規劃

於是我拿出了我的 AI 工具。 完全不是。我一開始先大致規劃了我希望這個功能如何運作。Ghostty 使用Sparkle,這是一個極為熱門的 macOS 更新框架。我翻了一下它的文件,發現它支援custom UI through Obj-C protocols。你必須從頭重新實作一大堆東西,但這是可行的。

好,所以我對後端有了大致的想法。至於前端,我其實不太確定(而且這也不是我的專長)。我有個非常模糊的想法,覺得應該是在標題列中嵌入一個小按鈕,而且我知道 macOS 透過titlebar accessory controllers支援在標題列中加入自訂 UI,但除此之外,我對於它應該長什麼樣子、帶來什麼樣的體驗,並沒有太具體的概念。

不過,這樣就足夠開始了。AI 是非常好的原型設計工具,所以即使知道自己不知道什麼,也足以讓你起步。我對整體樣貌已經有了足夠清晰的掌握。


第一場對話:打造 UI 原型

以下是我的第一場 agentic coding 對話,起始提示如下:

我想透過自訂 SPUUserDriver 來啟用自訂的、低干擾的更新通知與安裝流程。我們先從規劃所需的自訂 UI 開始。我們只處理 UI。請為建立能顯示 SPUUserDriver 所需各種狀態的 SwiftUI 視圖擬定一份計畫。我認為最適合顯示這些狀態的位置是 macOS 視窗標題列的右上角。請擬定一份將其放在那裡的計畫。請諮詢 oracle。

常見問題:「什麼是 oracle?」它是Amp 專屬的唯讀子代理(subagent),使用速度較慢、成本較高,但通常更擅長思考的模型。我在所有規劃階段都會諮詢 oracle。

一開始,我決定先做出 UI 的原型。

請注意,我並沒有叫代理人直接把整個功能做完。這有幾個原因。首先,也是最重要的,我自己都還不清楚想要什麼樣的 UI/UX,自然不可能期待 AI 能在其他變更中一併幫我釐清。其次,較小的工作單位更容易審查、理解與反覆修改。

另一個值得注意的地方是,我只要求它建立一份計畫,而不是直接寫程式碼。由於這個請求本身相當模糊,在讓它投入大量工作(並耗費大量 token)之前,先讓我審視計畫是非常重要的。

提示:與代理人互動、共同制定一份完整的計畫,對於任何具一定複雜度的任務來說,都是非常重要的第一步。我通常也會請代理人將其儲存成類似spec.md的檔案,這樣在未來的對話中,我就可以說「請參考 @spec.md 並處理某項任務」。

代理人提出了一份還算可以接受的計畫,於是我讓它繼續實作。你可以在後續的對話紀錄中看到我如何反覆迭代。

它做出來的 UI 在大方向上非常好。雖然有一大堆細節需要打磨(間距、顏色等等),但看到這個 UI 讓我靈光一現,清楚知道自己想要什麼了。

提示:我非常常利用 AI 來尋找靈感。在這個例子中,我保留了它產生的大部分(但不是全部)UI 程式碼,但我也常常會讓代理人先做一次,然後把所有東西丟掉,再親自(手動!)重做一遍。我覺得從零到一的創作階段非常困難且耗時,而 AI 非常適合作為我的靈感來源。

撞牆了

你可以在第 11 到 14 則對話中看到,我們正進入slop zone(品質下滑區)。代理人產生的程式碼有一個嚴重的錯誤,而且它完全無法修復。而我也不知道該怎麼修。

我經常會這樣做幾次最後一搏的嘗試來修錯誤。如果代理人能找出解法,我就能從中學習。如果不行,我的損失也很小。如果代理人找到了解法但我看不懂,我就會把它退回。我不會上線我看不懂的程式碼。在它嘗試失敗的同時,我也在另一個分頁中搜尋問題、嘗試自己找出解法。

到這個時候,我知道我需要退一步,檢視它做了什麼,並擬定自己的計畫。是時候讓自己好好學習、批判性思考了。AI 不再是解方,而是負擔。


清理階段

接下來的幾場對話,我引導代理人清理程式碼。

第二場對話專注於將一些方法移到我認為更合適的位置:

請將 pill 背景、前景與徽章的函式從 @macos/Sources/Features/Update/UpdateAccessoryView.swift 移至 @macos/Sources/Features/Update/UpdateViewModel.swift,並讓它們更通用(background、foreground、badge)

第三場對話為程式碼加上了文件:

請更新 @UpdateBadge.swift 的文件

提示:加上文件是非常重要的一步,因為這不僅有助於鞏固你自己對程式碼的理解,也能讓未來讀取並修改這些程式碼的代理人更容易理解。我發現當代理人同時擁有自然語言的說明與程式碼本身時,表現會好得多。

第四場對話將 view model 移至應用程式的全域位置,因為原本的實作將它放在視窗層級,而更新資訊其實是應用程式層級的。

請將 update view model 的資料移至 AppDelegate,因為更新資訊將會是 app-global(應用程式全域)的。

在這些過程中,我也經常會穿插進行一些小型的手動修改。

清理步驟非常重要。要有效地進行清理,你必須對程式碼有相當深入的理解,因此這會迫使我不能盲目接受 AI 寫的程式碼。隨後,結構更良好、文件更完整的程式碼,也能讓未來的 agentic coding 對話表現得更好。

我有時會半開玩笑地稱之為「反 slop 對話」。


面對「那個錯誤」

是時候回頭處理在最初那場對話中發現的錯誤了。我又花了幾場對話試圖讓代理人解決這個問題。我從模糊的描述開始,慢慢給出越來越具體、我會採取的做法。

首先是模糊的描述

對於標準、原生的分頁,update accessory view 無法顯示。它應該要保持在視窗標題列中可見。

失敗。接著,我試著更具體一點

我們需要更新 @macos/Sources/Features/Terminal/Window Styles/TerminalTabsTitlebarTahoe.swift 中 tab bar 的約束條件,將 tab bar 的右緣對齊 update accessory view 的左緣,讓它保持可見。

失敗。接著,我嘗試另一種具體的做法

如果我們把 @macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift 中的 tab bar 改為頂部的 accessory view,而不是底部的,讓我們的分頁進入標題列,會怎麼樣?

失敗。還有最後一次嘗試

@macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift 中的「right accessory view」與版面配置,和 @macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift 中 update accessory view 的設定有所衝突。我們能不能將 tab bar 約束為永遠顯示在更新通知的左側?

失敗。

這段期間,我也花了時間透過手動研究與人力努力來嘗試自行解決。我的提示之所以越來越具體,是基於我在這個過程中學到的東西。但整體來說,顯然還是行不通。

我覺得光靠自己可能無法解決這個問題,所以我決定轉換方向。我決定對於這些有問題的標題列樣式,改為將更新通知放在視窗右下角,疊在內容視圖(content view)之上,而不是放在標題列中。

無論如何都需要支援這種模式,因為 Ghostty 有一個可以完全隱藏標題列的設定。所以,即使我之後解決了標題列樣式的問題,仍然需要支援這種替代模式。

我的下一場對話就以非常具體的提示,朝這個計畫推進:

請擴充 @macos/Sources/Features/Update 系統,使其也能在 @macos/Sources/Features/Terminal/TerminalView.swift 中支援 overlay(覆蓋)模式。更新通知應該顯示在視窗底部。它應該覆蓋在文字之上(所以不會重新調整終端機視圖的大小)。所有點擊行為在其他方面都應該與 accessory view 相同。

它在這方面做得非常好。我之後做了大量手動的美化(搬移程式碼、重新命名等等),但核心的工作是紮實的。

以下是在這場對話後不久的功能影片,展示了對於特定標題列樣式或當標題列被隱藏時,更新通知如何顯示在視窗的右下角:


開始處理後端

UI 已經感覺夠好了。我記下了許多之後想處理的細節問題,但我想先轉向後端的工作,主要是想看看是否會發現任何意料之外的未知狀況,打亂我的計畫。

我手動建立了一個包含未完成函式骨架與各種TODO註解的檔案。然後我開啟了一場對話來幫我完成這些工作

請完成 @macos/Sources/Features/Update/UpdateDriver.swift。請視需要閱讀 Sparkle 文件以了解功能。https://sparkle-project.org/documentation/api-reference/Protocols.html

提示:AI 非常擅長填空或「畫出貓頭鷹的其餘部分」。我這種建立帶有描述性函式名稱、參數、todo 註解等骨架的模式,是我非常常用的做法,而且效果非常好。

但它在這裡其實表現得非常糟,最後我把這些程式碼全部丟掉了。它產生的程式碼可以運作,但顯然是錯誤的做法。它把許多不同的關注點混在一起,而且它在 driver 中儲存狀態的方式明顯是錯的。

當我研究它的成果時,我意識到這是因為 view model 的結構不夠理想,所以我轉而進入清理模式,以便為 AI(以及如果我選擇自己寫的話,也為人類)提供更好的框架來進行後續工作。


再次大規模清理

經驗告訴我,UI 前端與商業邏輯後端的整潔程度,往往取決於中間 view model 的品質。因此我花了一些時間手動重構了 view model。這包括改用 tagged union(標記聯合)而非帶有一堆 optional 的 struct。我重新命名了一些型別,也搬移了一些東西。

憑經驗我知道,在中間進行這樣一小段手動工作,將會為未來無論是前端還是後端的代理人對話奠定成功的基礎。完成後,我接著進行了一系列密集的清理對話。

完成重構後,我做的第一件事是請代理人再次完成貓頭鷹的其餘部分,這次是檢視我的變更,並將相依的程式碼更新為新的風格,同時移除舊的程式碼:

請將 @macos/Sources/Features/Update/UpdateViewModel.swift 更新為專門使用新的UpdateState。將state2重新命名為state(移除舊的 state)。

接著我請它移除額外的無用程式碼

我覺得我們可以把 UpdateUIActions 拿掉了。自從我們的 UpdateState 有了 callbacks 之後,它們就不再被使用了。

然後我在清理一些東西時自己把建置弄壞了。我剛好要去開會,所以決定讓代理人在我忙的時候幫我修好

請執行建置並修復錯誤

提示:「我把一堆東西弄壞了,請幫我收拾殘局。」也是我經常交給代理人的一種使用情境。我想,這大致上也屬於前面提到的填空模式。

之後,我又開始對一些視圖進行重構

請將 @macos/Sources/Features/Update/UpdatePopoverView.swift 中的每個 case 轉為一個獨立的 fileprivate Swift 視圖,該視圖以具型別的值作為參數,這樣我們就可以移除那些 guards。

更多清理

請將 @macos/Sources/Features/Update/UpdateViewModel.swift 中的iconName改為 optional,若為空白則回傳 nil。並更新使用處。


模擬

在我的第一場 UI 對話中,我讓代理人建立了一些示範程式碼,這樣我就能在不進行真實更新檢查的情況下看到 UI 的實際運作。但更新流程有許多種情境,而到目前為止我只測試了快樂路徑(happy path)。

在我的下一場對話中,我將模擬程式碼抽取到一個獨立的檔案中,並請代理人建立更多的情境:

請將 @macos/Sources/App/macOS/AppDelegate.swift 中的更新模擬程式碼抽取到 @macos/Sources/Features/Update 中的一個獨立檔案。這個檔案應該包含多種模擬情境(快樂路徑、找不到更新、錯誤等等),讓我們可以輕鬆嘗試不同的示範。

提示:代理人非常擅長產生測試與模擬程式碼。它在這裡產生的模擬程式碼老實說寫得相當粗糙,但它能運作,而且不會包含在正式發行的執行檔中,所以品質對我來說並不重要。我除了在對話中看到的基本清理之外,完全沒有再多做整理。

接著我執行了各種模擬,並發現了許多可以改善 UX 的地方。


最後一哩路

到這個階段,我已經有了可運作的前端與後端,接下來需要將它們全部串起來。

我的下一場對話請代理人做了這件事:

請建立一個UpdateController類別,其做法與https://github.com/sparkle-project/Sparkle/blob/2.x/Sparkle/SPUStandardUpdaterController.m相同,但適用於我們的 updater 型別。

這需要一些來回的溝通與手動的美化,但最終還是完成了。

接著我做了一些小幅的改進

對於我們有 appcast 的「有可用更新」狀態,請查看https://sparkle-project.org/documentation/api-reference/Classes/SUAppcastItem.html,如果有設定其他相關的詮釋資料,也一併顯示出來。例如,用 content length 來顯示大小。


還有其他嗎?

最後一次給代理人的提示,總是問它是否還有什麼我可能遺漏的地方。無論程式碼是我自己手寫的還是 AI 寫的,我都會這麼做。

你覺得 @macos/Sources/Features/Update 功能還有什麼可以改進的地方嗎?不要寫任何程式碼。請諮詢 oracle。請考量那些也可以加上更多單元測試的程式碼部分。

這指出了一些真正的問題,所以我接著請它直接全部實作。我發現與其請代理人做特定事項,不如直接告訴它「好,全部都做」,因為我之後總是可以透過選擇性的提交(commit)輕鬆地清理。

在這場對話中,有趣的是代理人開始陷入一個非常瘋狂的死胡同,所以我介入阻止了它:

停停停。把所有 main actor 的東西都復原。

我也注意到它在某個地方做得相當糟糕,其實有更好的做法:

對於錯誤訊息,與其截斷它,難道沒有 SwiftUI 標準的做法嗎?我們應該新增一個額外的 UI 元件,讓他們可以用來查看完整的訊息。


成本與時間

這項工作總共進行了16 場獨立的對話,在 Amp 上的 token 花費總計為$15.983。我不打算評論這究竟算貴還是便宜,但對我個人而言,光是在這兩天開發此功能的期間,我在咖啡店的花費就超過這個數字了。

我估計在這個功能上花費的總「實際時間」約為 8 小時。我每天大約只有 4 小時可以使用電腦,而第一筆與最後一筆提交之間橫跨了 3 天。但我也並非把所有時間都花在這個功能上。舉例來說,我在進行這項功能的同時,也在可供「用電腦工作」的時段內發布了一次 Ghostty 更新、在ThePrimeagen上擔任了一小時的來賓,以及在 Zoo 的年度全員大會上進行了一場客座演講4。所以,我認為 8 小時的估計算是寬鬆的了。

網路上許多人爭論 AI 是否能讓你工作得更快。就這個例子而言,我認為我比完全靠自己動手做更快地完成了上線,特別是因為對我個人來說,反覆調整 SwiftUI 的細微樣式是非常繁瑣且耗時的工作,而 AI 在這方面做得非常好。

對我個人而言,快慢之爭忽略了我最喜歡的一點:AI 可以在我暫時離開去做其他事情時,繼續為我工作。以下是我在為家人做早餐時,進行其中一場清理對話的照片:

一邊 Vibe 一邊做早餐

對此有各種各樣的否定說法,例如「我不想在煮飯時還在寫程式」或「專心一點」之類的。如果那就是你想要的生活方式,那也沒問題。就我這個具體例子而言,我是家裡最早起床的人,我在其他人還在睡覺時準備早餐。我並不是每個清醒的時刻都在這樣做。

總之,這對我來說是可行的。我真的、真的不是在試圖說服你。我與任何 AI 公司都沒有財務上的關聯。但作為一個在使用 AI 工具上有很多成功經驗、也很喜歡分享的人,大家經常請我分享實際的例子。這篇文章就是在做這件事。


結語

我覺得這個功能既美觀又運作良好,在經過最終的人工審查後,我就將它合併了5。對於使用 tip 版本的 Ghostty 使用者來說,現在已經可以使用了。對於使用已標記發行版本的使用者,這個功能將會在 Ghostty 1.3 中提供。

我一直大力倡導公開分享 agentic coding 對話的重要性6,原因之一是這是一種非常有力的方式,可以教導他人如何有效地使用這些工具。希望這篇文章能有所幫助,展示這一點。

註腳

  1. 目前僅向 nightly 測試版(「tip」測試者)提供,但這群體已有數千人。程式碼已合併,將會包含在下一個 Ghostty 穩定版中。

  2. 是的,這是很好的行銷效果。不,這不是故意的。而且,不,我並不是在為此慶祝,因為你不會希望使用者擔心工具會趁機利用他們。你會希望相信它是來幫忙的。我希望簡報者(或任何人,真的)是想要使用 Ghostty 的,而關心這類事情正是其中的一部分。

  3. 最有詩意的做法本來應該是用 OpenAI Codex 來做這件事。我相信它也會表現得很好!這篇文章並不是在為 Amp 打廣告,它只是剛好是我目前最常使用的 agentic coding 工具。

  4. 我家裡有個幼兒,所以我的「用電腦時間」非常固定且非常有限。😁

  5. 「最終人工審查」是超級無敵非常重要的。這或許不該只是個註腳,但我找不到更好的地方來強調這一點。請永遠不要在未經徹底人工審查的情況下就上線 AI 寫的程式碼。

  6. 我這樣做的理由本身就可以寫成另一篇部落格文章,所以我在這裡就不進一步解釋了,除了我已經說過的部分。

原文由 Mitchell Hashimoto 發布

本文章由 muse-spark-1.2-contributor 進行翻譯