本文へスキップ

tako開発日記 #1: AIエージェントが画面を操作するターミナル

12 分で読めます

Claude Codeを日常的に使うようになってから、ターミナルの前でやっている作業のうち、実際にコードを考えている時間より「画面を整える時間」のほうが長いことに気づいた。エージェントを立てて、隣にdevサーバーのタブを開いて、差分を見るために別ウィンドウを出して、成果物のMarkdownをエディタで開き直す。エージェントは賢くなったのに、その作業場を組み立てるのはずっと人間の仕事のままだった。

そこで作り始めたのがtakoAIエージェントがタブとペインをプログラマブルに操作できるGUIターミナルで、Rust + GPUI製のOSS(GPL-3.0-or-later)。2026年6月11日に最初のコミットを打って、68日で822コミット、現在はv0.7.3が動いている。この記事では、何を解決するために作ったのか、どう作ったのか、いま何ができるのかをまとめる。

takoの実際の画面。左にファイルツリー、中央にClaude Codeのペインとgit log、右にREADMEのMarkdownプレビュー 実際の画面。中央でClaude Codeが動いていて、右のREADMEプレビューも下のgit logペインも、エージェント自身がMCP経由で開いたもの

既存ターミナルで何に困っていたのか

エージェントを使う開発では、1つの作業が自然に複数プロセスへ分裂する。エージェント本体、それが起動した子エージェント、devサーバー、ログ監視。これらが別タブ・別ウィンドウに散らばると、「いまどのエージェントが何をしていて、どれが自分の返事を待っているのか」の把握コストが作業の主要なボトルネックになる。tmuxで束ねてもいいが、今度はプレビューや差分の確認が全部テキストの中に閉じ込められる。

もう1つ、もっと本質的な問題があった。エージェントに「画面を操作する手」がないことだ。エージェントは「この変更のdiffを確認してください」と文章で言ってくるが、diffを開くのは人間。「devサーバーを起動しました」と言ってくるが、その出力を見にいくのも人間。エージェントに画面の操作権を渡せれば、この往復はまるごと消える。

takoの一行定義はこうなる。1グループ = 1タブで作業を集約し、その画面をエージェント自身が組み立てられるターミナル

takoの画面構成: 1グループ=1タブと、エージェントが生やすペイン

エージェントに渡している「手」の中身

エージェントからの操作は3層で受けている。

takoがエージェントに渡す3層と、tako-controlを経てGPUIの画面に届くまで

主役はLayer 2の内蔵MCPサーバーで、現在134ツールを公開している(crates/tako-control/testdata/mcp_tools_full_snapshot.jsonのエントリ数)。ペインの分割・入力送信・画面読み取りといった基本操作から、ファイルを開く、Markdownをプレビューする、gitのdiffを見せる、レイアウトを整える、tmuxセッションを取り込む、といったところまで揃っている。

tako内のClaude Codeが、takoのMCPツール群を自分で説明しているペイン

tako内で動くClaude Codeのペイン。ここに「プレビューを開いて」と頼むと、返事の代わりに画面が動く

導入は1コマンドで終わる。

tako setup-mcp

これは内部でclaude mcp add --scope userを呼んでユーザー設定に登録するだけなので、以後どのプロジェクトでも設定ゼロで使える。手で書くならこう。

{
  "mcpServers": {
    "tako": {
      "type": "stdio",
      "command": "/Applications/tako.app/Contents/MacOS/tako",
      "args": ["mcp", "serve"],
      "env": {}
    }
  }
}

「設定ゼロ」を成立させているのは環境変数の注入だ。tako内で起動したシェルには、接続先とそのペイン自身のIDが最初から入っている。

# tako 内のペインで実行(値はマスクしている)
$ env | grep '^TAKO_'
TAKO_PANE_ID=3
TAKO_TAB_ID=1
TAKO_SOCKET=<データディレクトリ>/tako.sock
TAKO_TOKEN=<セッションごとのトークン>

エージェントが「ペインを分割して」と頼まれたとき、どのペインの隣に生やすかを指定しなくていいのはこれが理由で、TAKO_PANE_IDから呼び出し元を自動特定する。逆にtakoの外でMCPブリッジを起動した場合は公開ツール0個になり、邪魔をしない。

実際の動きはこうなる。

ペイン分割からレイアウト調整までをエージェントがMCP経由で完結させる流れ

Layer 3のパッシブ検知だけは性格が違う。シェル統合(OSC 7/133)でcwdとコマンドの実行状態を拾い、ペイン配下のプロセスがlistenしているTCPポートを検知して「プレビューを開く?」の提案チップを出す。ここは勝手に分割しない方針を守っている。自動化が気持ちよく効くのは明示的に頼んだときだけで、勝手に画面が変わるターミナルは単純に使いにくい。

技術スタック

# Cargo.toml(抜粋)
[workspace.dependencies]
alacritty_terminal = "0.26"
# GPUI は zed リポの git rev 固定(自動更新しない。追従は意識的なタスク)
gpui = { git = "https://github.com/zed-industries/zed", rev = "cafbf4b5df7fedb67fc0f248850a5654efcec5d9" }
# 内蔵 MCP サーバーの HTTP トランスポート。
# 公式 SDK の rmcp は tokio 必須のため不採用(「tokio を持ち込まない」方針)
tiny_http = "0.12"

ターミナルのコアはalacritty_terminal、描画はGPUI(Zedのフレームワーク)。Electronを使わない選択は最初から固定にした。エージェントの出力は「1行ずつ、長時間流れ続ける」形で来るので、1行あたりの取り込みコストがそのまま体感に出る。

非同期ランタイムはGPUIのexecutorとfutures channelだけで組み、tokioを持ち込んでいない。MCPのHTTPトランスポートに公式SDKのrmcpではなくtiny_httpを選んだのもこの方針のためで、依存の重さとビルド時間を素のまま保ちたかった。

ワークスペースは4クレートに分かれている。

クレート 責務
tako-core ターミナル・タブ・ペインのモデル、tmuxバックエンド
tako-control IPCサーバー、CLI/MCPの一元ディスパッチャ、MCPカタログ
tako-app GPUIバイナリ(画面)
tako-cli takoコマンド

依存方向はapp → control → coreの一方向で、coreとcontrolをGPUI非依存に保つのを不変条件にしている。GPUIをgit revで固定している以上、追従時に破壊的変更を踏むのは前提で、そのとき壊れる範囲をUIレイヤに閉じ込めるための防波堤になっている。

ここまでの転換点

1. 仕様を先に書き切ってから、初日にPoCまで通した

最初のコミットは実装ではなくAGENTS.mdと.agent/の仕様書一式だった。その日のうちにGPUIの最小ウィンドウ、alacritty_terminal + PTYの最小ターミナルまで通して、スタックを確定させている。

これは「エージェントに開発させる」前提で逆算した順序だった。仕様書が先にあれば、以後どのセッションでもエージェントが同じ前提から始められる。実際、いまでも仕様(requirements.md)とアーキテクチャ(architecture.md)は実装より先に更新するルールで回している。

2. MCPツールが40個から134個に増え、公開契約をテストで固定した

オーケストレーター機能を足した2026年6月18日時点でMCPツールは40個だった。それが2か月で134個になった。増えること自体は想定どおりだったが、途中で問題が出た。ツールの説明文や入力スキーマはエージェントから見た公開APIなのに、リファクタで説明文が変わっても誰も気づけない。エージェントの挙動は説明文で変わるので、これは静かに壊れる種類のバグになる。

対策として、カタログ全体(ツール名・説明文・inputSchema・公開順)のスナップショットテストを入れた。

#[test]
fn mcp公開カタログが完全スナップショットと一致する() {
    let actual = rendered_catalog();
    assert_eq!(
        actual, SNAPSHOT,
        "MCP 公開カタログが変化した。挙動変更でないことを確認し、意図した変更の場合だけ\
         `TAKO_UPDATE_MCP_SNAPSHOT=1 cargo test -p tako-control \
         --test mcp_catalog_snapshot` で更新する"
    );
}

「意図した変更のときだけ環境変数を立てて更新する」形にしておくと、レビューで説明文の差分が必ず目に入る。AIに公開するAPIは人間向けAPIよりも文章の比重が高いぶん、テストで押さえる価値も高かった。

3. 「速い」を名乗るために、描画を作り直した

自分にとって一番学びが大きかったのはここ。GPUIを使っているのだから速いはずだと思っていたが、実測すると全然そうではなかった。

原因は構成にあった。takoは単一のGPUI entity(TakoAppがルートビュー)で全体を描いていたので、どのペインの出力でもcx.notify()がアプリ全体を描き直していた。裏タブのペインが出力しているだけで、見た目が1ピクセルも変わらないのに60fps超で全面再描画が回る。

施策 条件 before after
可視性ゲート(#782) 隔離環境・200行/秒・裏タブ2ペイン 22.3% CPU / 再描画1596回 2.6〜2.7% / 11〜12回
イベント配送の削減(#816) 隔離・交互3反復の中央値・instructions・裏タブ + 1行ずつ 1569.4M 686.7M(-56.2%)
セル単位変換の削減(#801) grid-bench・300フレーム・同一バイナリA/B・空画面119x27 3.587M 2.197M(-39%)

#816で分かったのが特に意外だった。取り込み経路のコストを層別に計装すると、支配項はエスケープシーケンスのパース(31.4%)ではなくイベント配送(35.8%)で、しかも配送コストは行数ではなくPTY readの回数に比例していた。同じ6000行でも「20行バースト」が353M instructionsなのに対し「1行ずつ」は1565Mで4.4倍。エージェントの出力は1行ずつ来るので、ここが直撃していた。

踏み抜いた罠も書いておく。ペインをAnyView::cachedで包んでビュー単位のキャッシュにしたとき、ビューを親のrenderの中でcx.newしてしまうと、初回prepaintのaccessed_entities差分からそのidが漏れ、次フレームでtracked_entitiesから外れて二度と描き直されなくなる。プレビューペインが開いた直後の1フレームで固まる、という形で表に出た。cx.notify()が再描画に化けるのは、そのentityが「このウィンドウでアクセスされた」と記録されている間だけ、というGPUIの前提を読み違えていた。

なお、いまも目標には届いていない。#786の実測時点でZedの1フレームが0.16M instructionsなのに対し、takoは固定5.1M + 0.39M×行数だった。#801で空画面2.197Mまで削ったが、目標の1M未満は未達で、残りはペインヘッダをルート側へ持ち上げないと取れないことまで実測で確定している。

4. 開発そのものをAIエージェントに任せた

822コミットのうち696コミット(約85%)はClaudeとの共著になっている。人間がやっているのは方針決定・受け入れ判断・「これは違う」と言うことで、実装とテストと検証はエージェントが回している。

そのために、takoにはオーケストレーター機能が内蔵されている。masterエージェントが複数プロジェクトの作業をworkerへ委任し、workerのペインを生やして監視する。外部スクリプトへの依存はゼロで、workerのCLIはclaude / codex / agyから選べる。

tako master          # オーケストレーター(worker へ委任する)
tako solo            # 1 対 1。worker spawn を禁止して自分で手を動かす

さらに夜間のパッチリリースはlaunchdジョブで自動化してあり、これまでに25回の自動リリースが出ている(タグは全47本、最新はv0.7.3)。tako自身がtakoの上で開発されているので、遅い・使いにくいところは開発中に必ず自分に返ってくる。#816の「1行ずつ来る出力が重い」に気づけたのも、自分がエージェントworkerを並べて動かしていたからだった。

いまできること、できないこと

v0.7.3時点で動いているものを正直に並べる。

  • 動く: ターミナル基盤(タブ・ペイン分割・IME・truecolor)、tako CLI、内蔵MCPサーバー、cwd連動のファイルツリー、コード / Markdown / 画像 / PDFのライブプレビュー、gitグラフ、tmuxバックエンドによる再起動復元、オーケストレーター、Tailscale経由のリモートアクセス(既定OFF)
  • macOS向け。配布しているバイナリはmacOS arm64のみ
  • Windowsは道半ば。CIではプロジェクト初日からmacOS / Windows両ランナーでbuild + testを回し続けていて、テスター向けのプレビュービルドも3本出した(v0.5.13-win.1.3)。ただしConPTY・named pipe・PowerShellのシェル統合を仕上げるフェーズはこれからで、ロードマップ上はPhase 6になっている
  • エディタの置き換えは目指していない。成果物をその場で確認して軽く直すまでが守備範囲

READMEのライブプレビューペイン。目次ジャンプと実行ボタンつき

Markdownプレビューはペインとして開く。エージェントが書いたドキュメントをターミナルから出ずに確認できる

テストはcargo test --workspaceで2,062件(2026-08-15時点のコミット記録)。加えてアプリを実際に起動して項目を機械検証する隔離セルフテストと、描画のvisual-test回帰網がある。GUIアプリをエージェントに書かせる以上、「ビルドが通った」を完了条件にできないので、ここは厚めに積んでいる。

使ってみるには

takoのアプリアイコン

macOSなら次の1行で入る。

brew install --cask takushio2525/tako/tako

zipを直接取るならReleasesから。まだDeveloper ID署名を付けていないので、初回起動時はGatekeeperの警告が出る(システム設定 → プライバシーとセキュリティ →「このまま開く」)。

インストール後、Claude Code連携は先に書いたtako setup-mcpの1回だけ。あとはtako内でClaude Codeを起動して「差分を見せて」「devサーバーを立てて隣に置いて」と頼めば、画面のほうが動く。

ソースはtakushio2525/tako。ライセンスはGPL-3.0-or-later(依存クレートのzlog / ztracingがGPL-3.0のため)。

学び

2か月ちょっと作ってみて、一般化できそうなことが3つある。

1つ目。AI向けのAPIは、文章がAPIの一部になる。MCPツールの説明文を変えるとエージェントの振る舞いが変わるので、説明文まで含めてスナップショットテストで固定する価値がある。人間向けのAPIならdocコメントの変更はレビューの些事だが、AI向けだとそれが挙動変更になる。

2つ目。性能は構成で決まる。「GPUIを使っているから速い」は成り立たなくて、単一entityで全部を描く構成にした時点で、フレームワークが何であっても全面再描画は避けられなかった。可視性ゲートとビュー単位キャッシュで直したが、これはZedが最初からやっている素朴な方式に追いついただけで、要は再描画の単位をどう切るかという設計の問題だった。

3つ目。AIに開発させるなら、検証の自動化にコストを寄せる。エージェントは「たぶん動く」を平気で報告してくるので、隔離セルフテストと実測ハーネスを先に用意しておかないと、速度低下も表示崩れも人間が全部手で見つけることになる。逆にそこさえ積んでおけば、夜のうちにパッチリリースが1本出ている状態が普通に成立する。

このブログではtakoの開発で見つけた実装知見を書いていくつもりなので、GPUIやエージェント運用まわりの話が続くと思う。