Vibing a Non-Trivial Ghostty Feature

Mitchell Hashimoto

バイブコーディングで作る本格的なGhostty機能

原文は Mitchell Hashimoto により に公開されました。 このブログを購読する

最近、ほぼAIによって開発された本格的なGhosttyの新機能(目立たないmacOS自動アップデート)をリリースしました。

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

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

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


機能について

この記事で取り上げる完成した機能は、macOS向けの目立たないアップデート通知機能です。この機能は、ウィンドウを新たに表示したりフォーカスを奪ったりすることなく、ターミナルウィンドウ内にアップデート状況を表示し、作業の邪魔をしません。

この機能が生まれた背景を説明しましょう(すぐに分かる洒落を込めて)。注目を集めたOpenAIの基調講演で、デモがGhosttyのアップデートプロンプトによって無粋に中断されてしまったのです。

OpenAIのデモ

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


AI前のプランニング

さっそくAIツールを取り出しました。 とんでもない。まずは、この機能をどう動かしたいか大まかな計画を立てるところから始めました。Ghosttyは、macOSで非常に人気のあるアップデートフレームワークであるSparkleを使っています。ドキュメントをあれこれ調べて、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回目のセッションでは、いくつかのメソッドを私がより適切だと考える場所へ移動させることに焦点を当てました。

pill background、foreground、badgeの各関数を@macos/Sources/Features/Update/UpdateAccessoryView.swiftから@macos/Sources/Features/Update/UpdateViewModel.swiftに移動して、より汎用的にして(background、foreground、badge)

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

@UpdateBadge.swiftのドキュメントを更新して

ヒント:ドキュメントを追加することは非常に重要なステップです。自分自身のコードへの理解を再確認できるだけでなく、このコードを読んで変更する可能性のある将来のエージェントを教育することにもなるからです。自然言語による説明とコード自体の両方があると、エージェントはずっとうまく機能すると感じています。

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

アップデート情報はアプリ全体で共有されるので、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のセットアップと競合しています。タブバーが常にupdate noticeの左側に表示されるように制約できませんか?

失敗。

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

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

いずれにせよ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コメントなどで足場を組むという私のこのパターンは、非常によく使うもので、とてもうまくいきます。

実際、ここではかなりひどい仕事をし、結局このコードはすべて捨てることになりました。生成されたコードは動作はしましたが、明らかに間違ったアプローチでした。多くの異なる関心事を混同しており、ドライバーでの状態の持ち方も明らかに間違っていました。

何が起きたのかを詳しく調べてみると、ビューモデルの構造が最適でないことが原因だと気づいたので、AI(そして自分で書くことにした場合の人間)により良い土台を提供するために、クリーンアップモードへ切り替えました。


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

経験上、UIフロントエンドとビジネスロジックバックエンドの綺麗さは、しばしばその間にあるビューモデルの品質によって決まることを学んできました。そこで私は少し時間をかけて手作業でビューモデルを再構築しました。これには、多数のオプショナルを持つstructではなくタグ付きユニオンに切り替えることが含まれました。いくつかの型の名前を変更し、あちこち移動させました。

この中央部分での小さな手作業が、フロントエンドとバックエンドの両方について、将来のセッションでエージェントが成功するための土台になると経験から分かっていました。これを終えた後、マラソンのようなクリーンアップセッションを続けました。

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

@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をオプショナルに変更し、空の場合はnilを返すようにして。使用箇所も更新して。


シミュレーション

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

次のセッションでは、シミュレーションコードを専用ファイルに切り出し、エージェントにより多くのシナリオを作らせるように頼みました。

@macos/Sources/App/macOS/AppDelegate.swiftにあるupdate simulationコードを、@macos/Sources/Features/Update内の専用ファイルに抽出して。これは複数のシミュレーションシナリオ(ハッピーパス、見つからない場合、エラーなど)を含むべきで、異なるデモを簡単に試せるようにして。

ヒント:エージェントはテストやシミュレーションの生成が得意です。ここで生成されたシミュレーションコード自体は、正直かなりひどい出来ですが、動作するしリリースバイナリの一部でもないので、品質は私にとって重要ではありません。セッションでご覧いただける基本的な部分以外は、わざわざ綺麗にすることもしませんでした。

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


ラストスパート

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

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

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

これには何度かのやり取りと手作業での磨き上げが必要でしたが、なんとか形になりました。

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

appcastを伴うupdate available状態について、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. 現在はナイトリーのベータテスター(「tip」テスター)にのみ提供されていますが、それでも数千人規模のグループです。すでにマージされており、次のGhostty安定版リリースに含まれます。

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

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

  4. 家には幼児がいるので、私の「コンピュータ時間」は厳密にスケジュールされていて、とても限られています。😁

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

  6. この理由については、それだけで一つのブログ記事になり得るので、ここではすでに述べた以上の説明は避けることにします。

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

コメント