Vibing a Non-Trivial Ghostty Feature

Mitchell Hashimoto

バイブコーディングで作る骨太なGhostty機能

最近、AIを大いに活用して開発した、骨太なGhosttyの機能(控えめなmacOS自動アップデート)をリリースしました

AIやエージェント型コーディングツールをどう使っているのか、骨太な事例を共有してほしいとよく頼まれます。今回のようにスコープが明確で、本格的かつ実際にリリースされた機能は、そのプロセスを詳しく紹介する絶好の機会だと感じました1

この記事では、この機能をリリースするまでに実施したエージェントとのコーディングセッションを、すべて無編集で余すところなく公開します。あわせて、プロセスや考え方の背景についても補足していきます。もちろん、気になる方のためにトークンのコストについてもお伝えします。

重要:人間によるコーディングもたくさん含まれています。AIが作業した後は、ほぼ毎回自分で手を入れてしばらく改善を続けています。これをいちいち繰り返す代わりに、ここで一度だけお伝えしておきます。そのため、AIが生成した内容と最終的なコードに差異がある箇所が見られるかもしれません。これは意図的なもので、優れたAIの使い手は自身の領域の専門家であり、AIを代替ではなくアシスタントとして活用していると私は考えています。


機能

今回紹介する完成した機能は、macOS向けの控えめなアップデート通知機能です。この機能は、ウィンドウを新たに表示したりフォーカスを奪ったりすることなく、ターミナルウィンドウ内にアップデート状況を表示し、作業を邪魔しません。

この機能が生まれた経緯を説明しましょう(この表現は、すぐに分かる通り意図的な言葉遊びです)。注目を集めたOpenAIの基調講演で、デモがGhosttyのアップデート通知によって無粋にも中断されてしまったのです。

OpenAIのデモ

二度とこんなことが起きないようにしたいと思いました2。そこで私が選んだのは、アップデート通知を控えめにすることでした。ウィンドウをポップアップさせる代わりに、ユーザーの邪魔にならない場所に、小さくモーダルではないGUI要素を表示するようにしたのです。


AI前の計画

そこで私はAIツールを取り出しました。 とんでもありません。まずは、この機能をどう実現したいか大まかな計画を立てるところから始めました。Ghosttyは、Sparkleという、macOSで非常に人気のあるアップデートフレームワークを使っています。ドキュメントを調べてみると、Obj-CプロトコルによるカスタムUIをサポートしていることが分かりました。多くの部分を一から再実装する必要がありますが、実現は可能です。

これでバックエンドについてはおおよその見当がつきました。フロントエンドについては、正直よく分かっていませんでした(そもそも私の専門外です)。タイトルバーに埋め込む小さなボタンのようなものにしたいという漠然としたイメージはあったのですが、macOSではtitlebar accessory controllersを使ってタイトルバーにカスタムUIを配置できることは知っていても、それが具体的にどう見え、どう感じられるべきかについては、あまり明確なイメージを持っていませんでした。

しかし、始めるにはそれで十分でした。AIはプロトタイピングが非常に得意なので、自分が何を分かっていないかを把握しているだけでも、着手するには十分なのです。全体像については、しっかりと掴めている感覚がありました。


最初のセッション:UIのプロトタイピング

こちらが最初のエージェントコーディングセッションです。開始時のプロンプトは次の通りでした。

SPUUserDriverをカスタマイズして、カスタムで控えめなアップデート通知とインストールを実現したいと考えています。まずは必要なカスタムUIの計画から始めましょう。今回はUIだけに取り組みます。SPUUserDriverが必要とする様々な状態を表示できるSwiftUIビューを作成する計画を立ててください。これらはmacOSウィンドウのタイトルバーの右上に表示するのが最適だと思います。そこに配置するための計画を作成してください。oracleに相談してください。

よく聞かれるのが「oracleとは何か?」という質問です。これはAmp特有の読み取り専用サブエージェントで、より低速でコストの高いモデルを使用しており、物事を深く考えるのが得意です。私は計画を立てる際には必ずoracleに相談しています。

まずはUIのプロトタイプから始めることにしました。

ここで、私がエージェントに機能全体の構築を丸投げしていない点に注目してください。理由はいくつかあります。第一に、私自身まだUI/UXをどうしたいのか分かっていないので、他の変更と合わせてAIに一貫した形でやらせることは到底期待できません。第二に、作業を小さく分割した方が、レビューや理解、改善がしやすくなるからです。

もう一つ注目してほしいのは、コードを書かずに計画だけを作成するよう依頼している点です。今回は比較的曖昧な依頼なので、エージェントが大量の作業(そして大量のトークン消費)を始める前に、計画を私が確認することが重要です。

ヒント:エージェントと対話しながら包括的な計画を作成することは、骨太な作業において非常に重要な最初のステップです。私は通常、その計画を(エージェント経由で)spec.mdのようなファイルに保存しておき、以降のセッションでは「@spec.mdを参照して、〇〇のタスクに取り組んで」と指示できるようにしています。

エージェントは十分に納得のいく計画を立ててくれたので、そのまま実装を進めてもらいました。そこから私が改善を重ねていく過程の会話は、残りのログからご覧いただけます。

エージェントが作ったUIは、方向性としては非常に優れていました。細かな磨き込みの点では問題が山積みでしたが(余白、色など)、そのUIを見たことで、自分が本当に求めているものへのインスピレーションが湧いてきました。

ヒント:私はインスピレーションを得るためにAIをよく使います。今回はエージェントが作ったUIコードの多く(すべてではありませんが)を残しましたが、エージェントに指示を出して作らせたものをすべて破棄し、自分で一から(手作業で!)やり直すことも非常によくあります。「0から1」を生み出す段階はとても難しく時間がかかるのですが、AIは私にとって優れたミューズになってくれます。

壁にぶつかる

チャット11から14を見ると、私たちがスラッジゾーンに入りつつあることが分かります。エージェントが作成したコードには致命的なバグがあり、それをまったく修正できていません。そして、私自身もどう直せばいいのか皆目見当がつかない状況でした。

私はバグ修正のために、このように何度か一か八かの試みをすることがよくあります。もしエージェントが解決できれば、それを学んで自分の知識にできます。できなくても、失うものはほとんどありません。たとえエージェントが解決できても自分が理解できなければ、その変更は取り消します。理解していないコードをリリースすることはありません。エージェントが失敗している間、私は別のタブで問題について検索し、自分でも解決策を探しています。

この時点で、一歩引いてエージェントが何をしたのかをレビューし、自分自身で計画を立てる必要があると悟りました。自分を教育し、批判的に考えるべき時です。ここから先、AIは解決策ではなく、むしろ負債になります。


クリーンアップのセッション

次の数回のセッションでは、エージェントにコードのクリーンアップを指示しました。

2回目のセッションでは、いくつかのメソッドを私がより適切だと考える場所へ移動させることに集中しました。

@macos/Sources/Features/Update/UpdateAccessoryView.swiftにあるpill background、foreground、badge関数を@macos/Sources/Features/Update/UpdateViewModel.swiftに移動し、より汎用的(background, foreground, badge)にしてください。

3回目のセッションでは、コードにドキュメントを追加しました。

@UpdateBadge.swiftのドキュメントを更新してください。

ヒント:ドキュメントを追加することは非常に重要なステップです。自分自身のコードへの理解を再確認できるだけでなく、今後このコードを読んだり修正したりするエージェントにとっても役立つからです。自然言語による説明とコードの両方があると、エージェントははるかによく機能すると感じています。

4回目のセッションでは、元の作業ではウィンドウスコープに置かれていたview modelを、アップデート情報はアプリスコープであるため、アプリ全体で共有される場所に移動しました。

update view modelのデータをAppDelegateに移動してください。アップデート情報はアプリ全体で共有されるためです。

これらのセッションを通じて、私自身も細かな手作業の修正を随時加えています。

クリーンアップのステップは本当に重要です。効果的にクリーンアップするにはコードをかなりよく理解している必要があるため、AIが書いたコードを盲目的に受け入れることを防いでくれます。さらに、整理され文書化されたコードは、今後のエージェントセッションのパフォーマンス向上にもつながります。

私はこれを冗談めかして「アンチ・スラッジ・セッション」と呼ぶことがあります。


「バグ」との対峙

そろそろ、最初のセッションで見つかったあのバグに再び取り組む時が来ました。私は再び数回のセッションを費やして、エージェントに解決させようとしました。最初は曖昧に、そして徐々に自分ならどうアプローチするかを具体的にしていきます。

まずは曖昧なセッションです。

標準のネイティブタブでは、update accessory viewが表示されません。ウィンドウのタイトルバーに表示されたままになるべきです。

失敗です。次に、より具体的に指示しました。

@macos/Sources/Features/Terminal/Window Styles/TerminalTabsTitlebarTahoe.swiftのタブバーの制約を更新し、タブバーの右端をupdate accessory viewの左端に揃えることで、表示されたままになるようにする必要があります。

失敗です。次に、別の具体的なアプローチを試しました。

@macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swiftを変更して、タブバーをbottom accessory viewではなくtop accessory viewにしたらどうでしょう。そうすればタブがタイトルバーに入るようになります。

失敗です。最後の試みです。

@macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swiftの「right accessory view」とレイアウトが、@macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swiftで設定されているupdate accessory viewと競合しています。タブバーが常にアップデート通知の左側に表示されるように制約できませんか?

失敗です。

この間ずっと、私自身も手作業での調査や試行錯誤を通じて、この問題を自分で解決しようと時間を費やしていました。より具体的なプロンプトは、その過程で学んだことに基づいています。しかし、全体として明らかにうまくいっていませんでした。

自力では解決できそうにないと思い、方針を転換することにしました。問題のあるタイトルバースタイルについては、タイトルバーではなく、ウィンドウの右下にコンテンツビューの上に重ねてアップデート通知を表示することにしたのです。

いずれにせよ、この対応は必要でした。Ghosttyにはタイトルバーを完全に非表示にする設定があるからです。たとえ後でタイトルバーのスタイル問題を解決できたとしても、この別のモードには対応しなければなりません。

次のセッションでは、この計画を非常に具体的なプロンプトで進めました。

@macos/Sources/Features/Updateシステムを拡張して、@macos/Sources/Features/Terminal/TerminalView.swiftでもオーバーレイ方式をサポートするようにしてください。アップデート通知はウィンドウの下部に表示されるようにします。テキストの上に重なるようにしてください(ターミナルビューがリサイズされないように)。それ以外のクリック挙動はすべてaccessory viewと同じにしてください。

エージェントはこの作業を非常によくやってくれました。後で多くの手作業による磨き込み(移動やリネームなど)は行いましたが、中核となる作業はしっかりしていました。

このセッションの直後の機能の動画がこちらです。特定のタイトルバースタイルやタイトルバーを非表示にしている場合に、アップデート通知がウィンドウの右下に表示される様子が分かります。


バックエンドの着手

UIは十分に良い仕上がりになってきました。後で取り組みたい磨き込みの課題もたくさんメモしていましたが、まずはバックエンドの作業に移り、計画を狂わせるような未知の未知が見つかるかどうかを確認したかったのです。

私は手作業で、未完成の関数や様々なTODOコメントを含むスケルトンファイルを作成しました。そして、その作業を完了させるためのセッションを開始しました

@macos/Sources/Features/Update/UpdateDriver.swiftを完成させてください。機能を理解するために必要に応じてSparkleのドキュメントを読んでください。https://sparkle-project.org/documentation/api-reference/Protocols.html

ヒント:AIは穴埋めや「フクロウの残りを描く」ことが非常に得意です。説明的な関数名やパラメータ、TODOコメントなどを用いて足場を作るという私のパターンは、非常によく使うもので、とてもうまく機能します。

実はここでエージェントはひどい出来で、結局このコードはすべて破棄しました。生成されたコードは動作はしたのですが、明らかに間違ったアプローチでした。多くの異なる関心事が混在しており、ドライバ内での状態の持ち方も明らかに間違っていました。

その内容を詳しく調べてみると、原因はview modelの構造が最適でなかったことにあると気づきました。そこで方向転換してクリーンアップモードに入り、AI(あるいは自分で書く場合の人間)にとってより良い土台を用意することにしました。


再び大規模なクリーンアップ

経験から、UIフロントエンドとビジネスロジックのバックエンドの綺麗さは、その間にあるview modelの質によって決まることが多いと学びました。そこで、view modelを手作業で再構築するのに時間をかけました。具体的には、多数のOptionalを持つstructではなく、tagged unionに切り替えました。いくつかの型の名前を変更し、配置を整理しました。

経験上、この中央部分でのちょっとした手作業が、フロントエンドとバックエンドの両方において今後のエージェントセッションを成功に導くことを知っていました。これを終えた後、一連のマラソンのようなクリーンアップセッションを続けました。

再構築を終えて最初に行ったのは、再びエージェントにフクロウの残りを完成させるよう依頼することでした。今回は私の変更内容を精査し、依存するコードを新しいスタイルに更新して古いものを削除してもらいました。

@macos/Sources/Features/Update/UpdateViewModel.swiftを更新して、新しいUpdateStateのみを使うようにしてください。state2stateにリネームしてください(古いstateは削除します)。

次に、さらに不要なコードを削除するよう依頼しました

UpdateUIActionsは削除できると思います。私たちのUpdateStateがコールバックを持つようになったので、もう使われていません。

その後、自分でいくつかクリーンアップしている際にビルドを壊してしまいました。参加しなければならない会議があったので、忙しい間にエージェントに修正してもらうことにしました

ビルドを実行してエラーを修正してください。

ヒント:「いろいろ壊してしまったので、直してください」というのも、私がエージェントをよく使うパターンの一つです。これも概ね、先ほどの穴埋めパターンと同じだと言えます。

その後、再びいくつかのビューでリファクタリングを始めました

@macos/Sources/Features/Update/UpdatePopoverView.swiftの各caseを、型付けされた値をパラメータとして受け取る専用のfileprivateなSwiftビューにしてください。そうすることでguardを削除できます。

さらにクリーンアップを続けました

@macos/Sources/Features/Update/UpdateViewModel.swiftのiconNameをOptionalに変更し、空白の場合はnilを返すようにしてください。使い方も更新してください。


シミュレーション

最初のUIセッションでは、実際のアップデートチェックなしでUIの動きを確認できるよう、エージェントにデモコードを作成してもらいました。しかし、アップデートフローには多くのシナリオがあり、ここまではハッピーパスしかテストしていませんでした。

次のセッションでは、シミュレーションコードを専用のファイルに抽出し、エージェントにより多くのシナリオを作成してもらいました。

@macos/Sources/App/macOS/AppDelegate.swiftにあるアップデートシミュレーションコードを、@macos/Sources/Features/Update内の専用ファイルに抽出してください。様々なデモを簡単に試せるよう、複数のシミュレーションシナリオ(ハッピーパス、見つからない場合、エラーなど)を含めるようにしてください。

ヒント:エージェントはテストやシミュレーションの生成が得意です。今回エージェントが生成したシミュレーションコード自体は、正直かなり雑な出来ですが、動作はしますし、リリースバイナリに含まれるわけでもないので、品質は気にしていません。セッションでご覧いただけるような基本的なクリーンアップ以外は、手を加えていません。

その後、様々なシミュレーションを実行し、多くのUX改善点を見つけました。


ラストスパート

この時点で、動作するバックエンドとフロントエンドが揃い、あとはそれらを接続する必要がありました。

次のセッションでは、エージェントに次のように依頼しました。

https://github.com/sparkle-project/Sparkle/blob/2.x/Sparkle/SPUStandardUpdaterController.mと同じようなUpdateControllerクラスを作成してください。ただし、私たちのupdaterタイプ用にしてください。

これには何度かのやり取りと手作業での磨き込みが必要でしたが、何とか完成しました。

その後、いくつかの細かな改善を加えました。

appcastを伴うアップデート利用可能状態について、https://sparkle-project.org/documentation/api-reference/Classes/SUAppcastItem.htmlを参照し、設定されている場合は他の関連メタデータも表示してください。例えば、サイズを示すcontent lengthなどです。


他に何か?

エージェントへの最後のプロンプトは、常に「他に見落としていることはないか」と尋ねることです。これは、コードを自分で手書きしたかどうかに関わらず、必ず行っています。

@macos/Sources/Features/Update機能について、他に改善できる点はありますか?コードは書かないでください。oracleに相談してください。ユニットテストを追加できるコードの部分も考慮してください。

これによりいくつかの実際の問題点が浮き彫りになったので、そのまま実装してもらうことにしました。個別に具体的な指示を出すよりも、エージェントに「じゃあ全部やって」と伝える方が楽だと感じています。後で選択的なコミットで簡単に整理できますから。

このセッションで面白かったのは、エージェントがかなり突飛な方向に突き進み始めたので、私が止めに入ったことです。

ストップ、ストップ、ストップ。main actor関連の変更をすべて元に戻してください。

また、もっと良い方法があるのに、あまり良くないやり方をしていたことにも気づきました。

エラーメッセージについて、切り詰めるのではなく、SwiftUIの標準的な方法があるのではないでしょうか?全文を確認できる追加のUI要素を設けるべきです。


コストと時間

作業は合計16回のセッションにわたり、Ampでのトークン消費は合計15.98ドルでした3。これが一般的に高いか安いかを論じるつもりはありませんが、個人的には、この機能に費やした2日間でカフェで使ったお金の方が多かったと言っておきます。

この機能に費やした合計の実時間は、およそ8時間と見積もっています。私は1日にコンピュータに向かうのは4時間程度で、最初のコミットから最後のコミットまでは3日間にまたがっています。しかも、その時間のすべてをこの機能に費やしたわけではありません。例えば、この機能に取り組んでいたのと同じ日の「コンピュータ作業」に充てられる時間の中で、Ghosttyのアップデートをリリースし、ThePrimeagenに1時間ゲスト出演し、Zooの全社総会でゲストプレゼンテーションも行いました4。ですから、8時間という見積もりはかなり多めだと思います。

インターネット上では、AIによって作業が速くなるかどうかについて多くの議論があります。今回のケースでは、すべてを自分でやるよりも速くリリースできたと思います。特に、私個人にとって細かなSwiftUIのスタイリングを繰り返し調整する作業は非常に面倒で時間がかかるのですが、AIはそれをとてもうまくこなしてくれるからです。

個人的には、速いか遅いかという議論は、私が最も気に入っている点を見落としていると思います。それは、自分が離れて他のことをしている間にAIが作業を続けてくれるということです。家族の朝食を作っている間に実行していたクリーンアップセッションの写真がこちらです。

バイブコーディングしながら料理

これについては「料理中にコーディングなんてしたくない」とか「もっと今に集中すべきだ」とか、いろいろな否定的な意見があります。そうありたいと思うなら、それで構いません。私の場合、この具体的な例で言えば、家族の中で一番早く起きて、みんながまだ寝ている間に朝食の準備をしているのです。起きている時間ずっとこんなことをしているわけではありません。

要するに、これは私には合っているということです。決してあなたを説得しようとしているわけではありません。私はどのAI企業とも金銭的な関係はありません。ただ、AIツールを活用して多くの成果を上げ、それについて話すのが好きな人間として、よく事例の共有を求められるのです。ここでやっているのは、まさにそれです。


おわりに

この機能は美しく、素晴らしく動作すると思います。そして最終的な手作業でのレビューを経て、マージしました5。tipリリースを使っているGhosttyユーザーの皆さんには、すでに利用可能です。タグ付きリリースを使っている方には、この機能はGhostty 1.3で利用できるようになります。

私は、エージェントとのコーディングセッションを公開することの重要性を声高に主張しています6。その理由の一つは、これがこれらのツールを効果的に使う方法を他の人に教える、非常に強力な手段だからです。この記事が、その一助となれば幸いです。

脚注

  1. 現在はnightlyのベータテスター(「tip」テスター)のみに提供されていますが、それでも数千人規模のグループです。すでにマージされており、次の安定版Ghosttyリリースに含まれます。

  2. はい、良いマーケティングにはなりました。いいえ、わざとではありません。そして、決してそれを喜んでいるわけではありません。ツールが自分を利用するのではないかとユーザーに恐れを抱かせたくないからです。ツールは助けてくれるものだと信頼してほしいのです。プレゼンター(というか、誰にでも)Ghosttyを使いたいと思ってほしいし、こうした細やかな配慮もその一部なのです。

  3. もっとも詩的だったのは、この作業にOpenAI Codexを使うことだったかもしれません。きっと素晴らしい仕事をしてくれたでしょう!この記事はAmpの広告ではありません。たまたま今私が最もよく使っているエージェント型コーディングツールがAmpだったというだけです。

  4. 家には幼児がいるので、私の「コンピュータ時間」はきっちりと決められていて、とても限られています。😁

  5. 「最終的な手作業でのレビュー」はとてつもなく、とてつもなく重要です。本来は脚注にすべきではないかもしれませんが、強調するのにこれ以上良い場所が見つかりませんでした。AIが書いたコードを徹底的な手作業レビューなしに決してリリースしないでください。

  6. この理由については、それだけで一つのブログ記事になるほどなので、ここではすでに述べた以上の説明は控えます。

原文は Mitchell Hashimoto により に公開されました。

この記事は「muse-spark-1.2-contributor」を使用して翻訳されました。