記事一覧に戻る
技術ノート6分で読めます

【Rust】ただのTODOアプリからAIオーケストレーターへ。CLIとTUIの融合で見えた「今」必要な開発スタイル

Qiitaに投稿したMarkdownを、このサイトにも同じ内容で載せています。Qiitaで読む

はじめに

前回の記事では、AIエージェントが自律的にタスクを消化するためのインターフェースとして、Rust製のローカルIssue管理ツール「Lissue」のCLI版を紹介しました。

その後、自身の開発プロジェクトでLissueのドッグフーディングを続ける中で、CLIのみのインターフェースには課題があることが分かりました。 AIは与えられたコンテキストから単一のタスクを高速に処理することに長けていますが、人間はプロジェクトの全体像(依存関係や進捗)を視覚的に俯瞰し、エディタ操作を中断することなく思考を整理する仕組みを必要とします。

この人間側の認知負荷を下げ、AIとのコンテキストスイッチを最小化するため、LissueにフルスクラッチのTUI(Terminal User Interface)を実装しました。 本記事では、TUI実装における技術的なポイントと、今後のアプリケーションの展望についてまとめます。

tui_demo.gif

TUI実装における4つの技術的アプローチ

LissueのTUIは、AIエージェントの動作を阻害せず、かつ人間の操作性を最大化するために以下の要件で設計しています。

1. 単一バイナリによるゼロコンフィグ(CLIとTUIの同居)

サードパーティ製のTUIツールを別途インストールさせる構成は、環境構築の手間を増やします。Lissueでは、実行時の引数の有無だけでCLIとTUIをルーティングする設計にしました。 lazygitk9sなどと同じですね。

// src/main.rs
fn main() -> Result<()> {
    let args: Vec<String> = env::args().collect();
    let root_dir = env::current_dir()?;

    if args.len() == 1 {
        // 引数なしの場合はTUIモードを起動
        let usecase = TodoUsecase::new(root_dir)?;
        let mut guard = lissue::presentation::tui::TerminalGuard::new()?;
        let mut app = lissue::presentation::tui::TuiApp::new(usecase)?;
        app.run(guard.terminal())?;
        return Ok(());
    }

    // 引数がある場合は従来のCLIとしてルーティング
    let cli = Cli::parse();
    // ...
}

これにより、ターミナルで lissue と打てば人間用のTUIが立ち上がり、AIエージェントやスクリプトは従来通り lissue add などのコマンドをシームレスに実行できます。

2. SQLite WALモードと定期ポーリングによる状態同期

CLI(AI側)とTUI(人間側)は、同じSQLiteデータベースを共有しています。 AIがバックグラウンドの別ペインでタスクを完了(lissue close)させた際、その変更をTUIに反映させる必要があります。イベント駆動型の複雑なアーキテクチャを避けるため、TUIのメインループ内にシンプルな時間ベースのポーリングを実装しました。

// src/presentation/tui/mod.rs (一部抜粋)
if self.last_refresh.elapsed() > Duration::from_secs(3) {
    let _ = self.refresh_tasks();
}

SQLiteをWAL(Write-Ahead Logging)モードで動作させているため、TUI側が定期的にReadを行っていても、AI側からのWriteがブロックされることはほぼありません。結果として、AIがタスクを消化すると、数秒以内にTUI上のステータス表示が更新されるリアクティブな挙動を実現しています。

3. Vim-likeなキーバインドによる操作性の向上

TUIの操作体系には、Vimライクなキーバインドを全面的に採用しました。 リストの上下移動に j/k、ステータスタブ(Open/InProgress等)の切り替えに h/l、検索の開始に / を割り当てています。

これは単なる開発者の好みではなく(好みでもありますが)、Lissueのメインターゲットである「ターミナルとエディタ(Vim/Neovim)を行き来するエンジニア」の認知負荷を下げるための合理的な選択です。TUIでのタスク管理と、エディタでのコーディングにおける指の動き(メンタルモデル)を統一することで、コンテキストスイッチによる摩擦を最小限に抑えています。

4. Fuzzy Searchを用いたインクリメンタルなコンテキスト構築

AIに精度の高いコード修正を行わせるには、適切なファイル群をプロンプトとして渡す(コンテキストを絞る)必要があります。 TUI上では、キーボードから手を離さずにこの「コンテキスト構築」を行えるよう、fuzzy-matcher クレートを導入しました。

タスクを選択した状態で Shift-A を押すとプロジェクト内のファイル一覧が開き、/ キーによるインクリメンタルサーチで目的のファイルを特定、Space キーでタスクに紐付けることができます。人間がTUIで紐付けたファイル群は、AIが lissue context コマンドを実行した際に結合されて標準出力にダンプされます。

今後の展望と技術的課題

現在のアーキテクチャは個人開発レベルでのMVP(Minimum Viable Product)としては機能していますが、ツールとしてのスケーラビリティを高めるため、以下のアップデートを検討しています。

マージ戦略の見直し(LWWからより堅牢な競合解決へ)

現在、Git経由での複数環境の同期には Last-Write-Wins(LWW:最終更新日時が新しいものを優先する)を採用しています。実装がシンプルで自動化しやすい反面、オフライン状態で複数人が同一タスクのDescriptionを編集した場合、古い変更がサイレントに上書きされるリスクがあります。 今後は、タスクの各フィールドに対して CRDT(Conflict-free Replicated Data Type)の概念を取り入れるか、Git-bugのように意図的なコンフリクト解決ステップを挟むようなアーキテクチャへの移行が必要になると推測しています。

タスクの「優先度(Priority)」の導入

現在、AIエージェントに自律稼働させるための lissue next コマンドは「未割当で最も古いタスク」を返します。 しかし、実際のプロジェクトでは着手順序のコントロールが不可欠です。データモデルにPriority(1~10)の概念を導入し、lissue next の取得ロジックに重み付けを加えることで、人間の意図に沿った順序でAIにタスクを消化させることが可能になります。 また、人間が意図的に付けた優先度だけではなく、AIがどのタスクに高い優先度を割り当てたのかを監視することも可能になります。

おわりに:AIと人間の協働作業の今後

Lissueの開発を通じて明確になったのは、これからの開発ツールに求められる要件の変化です。

元々は単なるTODOアプリを作ろうと思って始めた開発でした。しかし、その開発にAIを使っているうちに、「コレ、AIのためのアプリにした方が今後のためになるんじゃない?」と思ったのが開発のきっかけです。

今はAIエージェントがコードを書き、テストを回す時代です。人間がコードを直接編集する時間は相対的に減少しています。代わりに増加するのは「抽象的な要件をタスクに分解する作業」と「AIに渡すコンテキストの境界を定義する作業」です。 つまり、AIにはパースしやすいAPI(CLI)が、人間には思考を可視化・整理しやすいインターフェース(TUI)が同時に必要になります。

機械のためのインターフェース(CLI)とTUIの融合というアプローチが数年後にどう評価されているかはわかりません。しかし、機械のためのインターフェース(CLI)と人間のためのインターフェース(TUI)をシームレスにつなぐこの構成は、少なくとも「今」必要とされている実用的なアプローチであると考えています。

今後、Lissueのように「AI」と「人間」をシームレスに繋ぐ思想を持ったツールが、多様なアプローチで登場してくると予想しています。なんならすでにたくさん生まれてきています。 開発スタイルの移り変わりが激しい現在ですが、今を確実に見つめられるようにしましょう!


Repository: https://github.com/Morishita-mm/Lissue Crates.io: https://crates.io/crates/lissue Install: cargo install lissue