アーキテクチャ
English version is architecture.md.
このページは Mytty がなぜ今の形になっているかを説明します。端末エミュレーションを libghostty に委譲し、その上にエージェント認識機能を重ねています。ワークスペースという抽象も、チャット形式のオーケストレーション UI も持ちません。
Mytty とは何で、何でないか
Mytty は macOS 専用のネイティブ端末で、AI 支援ワークフロー向けに作られています。端末エミュレーション、PTY 管理、Metal によるレンダリングを libghostty に委譲し、その上にエージェント状態を認識する Attention Inbox を追加しています。対応プロバイダーは Codex、Claude Code、OpenCode、Gemini(Antigravity)、Cursor です。
意図的に持たせなかったものが Mytty の性格を決めています。ワークスペースやプロジェクトという実体、チャットパネル、アプリがエージェントを代わりに実行するオーケストレーションエンジンはいずれも存在しません。エージェントはペインの中で動いている一つのフォアグラウンドプロセスにすぎず、イベント経由で観測される対象であり、端末そのものが主役であり続けます。この割り切りのおかげで、エージェントを一度も動かさないペインでもアプリは普通に役立ちますし、各プロバイダーがすでに持っている「エージェントセッションとは何か」というモデルと競合する二つ目のモデルを Mytty 側で維持する必要もなくなります。
ユーザーモデル
オブジェクトの階層は意図的に浅くしてあります。
Window
`-- Tab
`-- SplitNode
`-- TerminalSurface
`-- AgentRun
Window の上にワークスペースという実体はありません。カレントディレクトリ、Git ブランチ、フォアグラウンドプロセス、エージェント状態はすべて端末サーフェス一つひとつが持つプロパティであり、それらをまとめる大きなプロジェクトオブジェクトは存在しません。これは端末が実際に使われる様子をそのまま反映しています。人はいま取り組んでいる作業のためにペインを開くのであって、あらかじめ用意したプロジェクトのためにペインを開くわけではありません。縦タブがデフォルトで(横タブは設定で選べる)、複数ウィンドウとネストした分割もサポートしますが、TerminalSurface より上の層はそれらのペインが「なぜ一緒に存在するか」を覚えようとはしません。
UI の分担
AppKit と SwiftUI はそれぞれアプリの異なる領域を受け持ち、その境界線は「精度が最も重要になる場所はどこか」で引かれています。
- AppKit がウィンドウ、メニュー、フォーカス、キーボードルーティング、IME、端末ホスト自体を持ちます。キー入力のレイテンシと IME の正しさは SwiftUI の再描画サイクルに任せてよいものではありません。
- SwiftUI が Settings、タブサイドバー、Attention drawer を持ちます。ここでは生の制御力よりも宣言的なレイアウトのほうが価値があります。
- 設定変更は、明示的に再起動が必要なものを除いて即座に反映されます。ユーザーが設定画面をわざわざ後で見直しに来る必要がある場所にはしたくありません。
- Attention drawer は右側から開き、フォーカスを奪うことも自分から開くこともありません。バックグラウンドでエージェントが作業を終えたからといって、フォアグラウンドのペインでユーザーがやっていることを中断できてはなりません。
ターゲットの境界
SwiftPM パッケージ(Package.swift)はこのアーキテクチャを慣習ではなく実際のターゲット境界として強制しています。
MyTTYAppが macOS アプリ本体です。TerminalWindowControllerはウィンドウのハブで、関心事ごとのコラボレーター(AgentStatusPollingCoordinator、AgentUsagePollingCoordinator、RepositoryStatusCoordinator、PaneLayoutController、TerminalAutocompleteCoordinator、TerminalRecordingCoordinator、ScheduledInputCoordinator、RemotePaneBridge、TabDragController)を保持し、それらの出力をサーフェスとステータスバーに配線します。すべてを直接書き込む代わりにこの構成を取っています。AppDelegateもアプリ層で同じように委譲していて、メニュー構築はMainMenuBuilder、ウィンドウとセッションのライフサイクルはWindowSessionCoordinator、アップデート確認はApplicationUpdateCoordinator、リモートアクセスサーバーのデリゲートはRemoteAccessCoordinatorがそれぞれ受け持ちます。神クラス化したコントローラーを名前付きのコーディネーターに分けているからこそ、それぞれの関心事を単体でテストでき、たとえばエージェント使用量のポーリングを変更してもペインレイアウトに触れずに済みます。GhosttyAdapterは Ghostty の型に触れることが許された唯一のターゲットです。libghostty はソースリビジョンを固定した上で XCFramework としてビルドされ、バージョニングされていない C API を持つため、そこへの呼び出しはすべてこのアダプター境界の内側に隔離されています。キーボード入力、IME、リサイズ、レンダリングはグローバルな SwiftUI や監視可能なアプリ状態を経由してはなりません。これらの経路は libghostty 自体と同じくらい直接的で低レイテンシである必要があります。MyTTYCoreは Foundation のみに依存するプラットフォーム非依存のロジックを持ちます。タブ/セッションモデル(TabSession、SessionSnapshot)、エージェントイベントプロトコルとそのリデューサー、SQLite リポジトリ、プリファレンス、各プロバイダー固有の*SessionInspector/*UsageProbeの実装、そしてAgentSessionDatabaseがここに入ります。Foundation のみに絞っているからこそ、AppKit 環境なしでテストを走らせられます。MyTTYAgentHookはプロバイダーのフックが呼び出すヘルパーバイナリmytty-agent-hookです。後述するソケット経由でイベントをアプリに転送します。MyTTYClamshellHelperは蓋を閉じたままスリープを防ぐためにpmset disablesleepを実行する特権デーモンmytty-clamshell-helper(SMAppService/XPC) です。そのステートマシン(ClamshellHelperCore)はMyTTYCore側に置かれていて、特権ヘルパーを実際に動かさなくてもテストできます。MyTTYRemoteKitはリモートプロトコルと、クライアント側の実装一式(ペアリング、セキュアチャネル、メッセージコーデック、RemoteClientのセッション、Bonjour 探索、画面レンダラ)です。iOS 専用ではなく共有なのは、Mac もクライアントになるからです。リモートペインが別の Mac のターミナルを表示するとき、通っているのはまさにこのコードです。ここでは UIKit も AppKit も参照しません。そのおかげで、両プラットフォームがプロトコルの実装を 1 つだけ共有でき、2 つに分かれて食い違うことがありません。
リモートペインは設計上の選択として説明しておく価値があります。「変わったコマンドを実行しているターミナル」ではなく SplitNode の独立したケースにしてあります。ローカルのプロセスも作業ディレクトリも Ghostty サーフェスも持たず、表示しているものはすべてスタイル付きのランとしてネットワーク越しに届いたものだからです。別種にしてあることで、「ペイン = ローカルのターミナル」と仮定していた箇所 — agent ステータスのポーリング、リポジトリ状態、mytty-ctl のアドレッシング — をコンパイラが全部指摘してくれます。結果として、隣のペインの状態を黙って表示するのではなく、それぞれが意図的にスキップする形になります。接続はウィンドウ単位ではなくアプリ全体で RemotePaneConnectionCoordinator が持ちます。同じ Mac に対する 2 つのペインは 1 本のセッションを共有し、ウィンドウ間を移動しても表示は途切れません。
メニューコマンド、キーボードショートカット、CLI、mytty-ctl の制御ソケットといったすべてのエントリポイントは、それぞれ独自の状態変更経路を持つのではなく、最終的に TerminalWindowController と WindowSessionCoordinator の同じアプリケーションレベルのコマンドを呼び出します。この収束があるからこそ、「メニュー項目から開いたペイン」と「mytty-ctl split で開いたペイン」がずれることなく同じ振る舞いをします。
エージェントイベント: 画面をスクレイピングしない理由
エージェント状態は、明示的なフックか libghostty 自体の端末プロトコルサポートが配信する、バージョン管理された冪等なイベントから導出されます(ワイヤーフォーマットは docs/reference/agent-event-protocol.md を参照)。人間可読な端末出力を解析して状態を推測することは一切しません。これは「Waiting for approval」のような文字列をエージェントの CLI 出力から正規表現で拾うという、一見手軽な近道を最初から選択肢から外すということでもあります。そうした文字列はプロバイダーのバージョンやロケールによって予告なく変わりますし、読み取りを誤れば Attention Inbox を無駄なイベントで埋めるか、逆に本物の承認要求を静かに取りこぼすことになります。安定したスキーマバージョンを持てるのはイベントだけです。provider の hook 連携が未インストールでも、実行状態は unknown のまま放置されるわけではありません。Mytty はペインのポーリング済みフォアグラウンドプロセス、OSC 133 のコマンド終了通知、そして(Claude Code、Codex、Cursor については)その provider 自身の transcript や store から、実行のライフサイクルをネイティブに推定します。例外は attention 状態(waiting-input/waiting-approval)で、誤読すると Attention Inbox に偽の要対応項目が出かねないため、ネイティブ推定はこれをあえて一切合成しません。provider 本来の hook が入るまで、これらの状態だけは報告されないままになります。
unknown -> running
running -> waitingInput | waitingApproval | succeeded | failed
waitingInput | waitingApproval -> running
any -> disconnected
追記専用のイベントログが唯一の正となる情報源で、純粋なリデューサーがそこから現在の実行状態を導出し、Attention ポリシーがその状態から実際に対応すべきアイテムを導出します。リデューサーを副作用なし・隠れた状態なしの純粋な関数として保つからこそ、上記の状態遷移は実際に動くアプリの中でしか壊れないものではなく、入力と出力の対応表としてテストできます。
Attention Inbox
Inbox が拾うイベントは承認要求、入力要求、失敗または切断、長時間実行タスクの完了という 4 種類に限られます。ペインに関するそれ以外の情報、通常の出力や進捗ログなどは意図的に外に出しています。「まだ実行中です」まで報告するようになった inbox は、もう確認する価値のあるものではなくなるからです。
イベントはタブにバッジを付け、macOS 通知はそのタブが今見えていないときにだけ飛びます。ペインを実際に見ているユーザーが同じイベントについて通知でも中断される、という事態を避けるためです。Inbox 上のアクションは Focus と Acknowledge に限られます。プロンプトへの返信や要求への承認は端末そのものの中で行います。エージェントが実際に見ている画面とずれうる二つ目の操作面をわざわざ作りません。解決済みのアイテムは 24 時間だけ保持され、あとから軽く振り返ることはできつつも Inbox が恒久ログになることは避けています。
エージェント連携: フックだけに頼らない
Settings は各対応プロバイダー向けに、ワンクリックで元に戻せるフックのインストールと削除を提供します。書き込み先はそのプロバイダー自身のグローバル設定(~/.codex/hooks.json、~/.claude/settings.json など)で、インストーラーは無関係な既存設定を保持したままアトミックに書き込みます。ユーザーは自分自身のプロバイダー設定を持ち込んでいるので、インストーラーが無関係なキーを壊したり、クラッシュ時に中途半端なファイルを残したりする方が、イベントが届かないことよりはるかに悪い失敗モードになります。
フックはユーザー専用の Unix ソケット経由でイベントを送り、各ペインのフックはそのペイン一つだけに閉じたケイパビリティを受け取ります。入力の注入と画面キャプチャは意図的に別のケイパビリティとして分けてあり、イベント用フックには付与されません。そのため不正なフックスクリプトや誤動作するフックスクリプトも、状態を報告することはできても端末を操作することはできません。
フックだけではステータスバーが見せたいすべてを網羅できないため、TerminalWindowController の AgentStatusPollingCoordinator が 0.5 秒ごとに各ペインのフォアグラウンドプロセスをポーリングして補っています。実行ファイルと引数からプロバイダーを検出し(TerminalAgentProcessDetector)、そのプロバイダーの AgentProviderRuntime(プロバイダーごとに一つ実装され、AgentProviderRuntimeRegistry に登録されている)を解決し、そのプロバイダーの *SessionInspector を通じて使用中のモデルと残りコンテキストを読み取ります。Codex のトランスクリプトのファイルディスクリプタ、Claude Code のプロジェクトトランスクリプト、OpenCode と Cursor の SQLite データベース、Antigravity の設定ファイルと、プロバイダーごとに読み取り方は異なります。これがメインスレッドで動くため、毎ティック解析し直すのではなく、スロットリングとフィンガープリントキャッシュ(AgentSessionThrottleCache)を挟んでいます。AgentUsagePollingCoordinator も同じ構造で、NativeAgentUsageLoader と各プロバイダーの *UsageProbe に対応する AgentProviderUsageSource レジストリを通じてクォータやコストのメーターを読み込みます。
同じポーリングは NativeAgentRunEstimator にも使われます。検出したフォアグラウンドプロセスと OSC 133 のコマンド終了シグナルから、hook 連携が未インストールの provider について実行のライフサイクルを導出し、Claude Code、Codex、Cursor についてはさらに、上記の session inspector が既に読んでいるのと同じ transcript / store.db を読んで、プロンプトターンごとの run に分割します。これにより、hook を一度もインストールしていない状態でも実行状態を観測できます。仕組みの詳細はAgent event protocolの「ネイティブ推定による run 検出」を参照してください。
これらのプローブが共通で使う読み取り専用 SQLite ヘルパーが MyTTYCore の AgentSessionDatabase で、WAL データベースのサイドカーファイルがチェックポイントで消えている場合は immutable=1 接続にフォールバックします。プロバイダーの SQLite 状態を読む必要があるコードはこのヘルパーを再利用します。WAL のフォールバックはまさに一度ハマって二度目にまた同じ場所でデバッグしがちな類の落とし穴だからです。
設定と状態
ユーザー設定は UI から制御され、次の場所に保存されます。
~/.config/mytty/config.toml
~/.config/mytty/terminal.conf
~/.config/mytty/agents.toml
terminal.conf はそのまま libghostty に渡されます。既存の Ghostty 設定のインポートは明示的な一度限りの操作であり、Mytty がずっと同期を取り続けなければならない暗黙の二つ目の情報源にはしていません。
実行時の状態は設定とは別の場所にあります。
~/Library/Application Support/mytty/mytty.sqlite
~/Library/Logs/mytty/mytty.log
$TMPDIR/mytty.sock
ウィンドウ、タブ、分割レイアウト、カレントディレクトリは再起動をまたいで復元されます。スクロールバックの永続化はオプトインでデフォルトは無効、実行中だったプロセス自体は復元されません。デバッグビルドは Mytty Dev として動き、上記すべてを ~/.config/mytty-dev/ とそれに対応する Application Support / ログの配下に隔離しています。開発ビルドがインストール済みのリリース版のセッションやソケットに一切干渉しないようにするためです。
CLI と制御ソケット
同じアプリケーションコマンド群はローカル CLI とソケット API からも呼び出せます(mytty list --json、mytty tab new、mytty split --right、mytty focus <surface-id>、mytty event emit <event>、mytty config validate/reload など)。これは、一つのペインで動く AI が他のペインを目に見えて割り込み可能なチームとして操作できるようにする AI 向け制御 CLI、mytty-ctl の土台でもあります。ペアリングも暗号化も持たない単純なローカルソケットとして作られている理由は mytty-ctl-architecture_ja.md を参照してください。
当初の設計から変わったこと
Mytty の当初の設計文書は、埋め込みブラウザをワークスペースオーケストレーションや Inbox 内での返信と並んで、当初から「やらないこと」に挙げていました。ブラウザペイン(BrowserPaneView.swift)はその後実装されました。ローカルの HTML や Web コンテンツを端末と並べて見られる機能として十分に価値があると判断され、別の概念としてではなく端末サーフェスと同じペイン/分割モデルの中に実装されています。その他の「やらないこと」(ワークスペースオーケストレーション、Inbox 内での返信・承認、エージェント状態の出力ヒューリスティック推測、永続 PTY デーモン、クラウド同期)は執筆時点でも変わらず有効なままです。当初の設計が示していた段階的な提供計画、サーフェス単体の実証から始めてタブと永続化、エージェントイベント、UI とフックインストーラー、CLI とパッケージングへと進む流れはすでに一通り完了しています。上のセクション群はその計画がどこへ向かっているかではなく、どこに着地したかを説明したものです。