このドキュメントでは、coding-human プロジェクトの内部アーキテクチャ、モジュール構成、および開発ワークフローについて説明します。
coding-human/
├── Cargo.toml # ワークスペースルート(メンバー: cli, worker)
├── Cargo.lock
├── .env # ローカル環境変数上書き用(SERVER_URL)
├── flake.nix # Nix dev-shell(任意)
│
├── cli/ # ネイティブバイナリ — coding-human
│ ├── Cargo.toml
│ └── src/
│ ├── main.rs # CLI エントリポイント(clap サブコマンド: client / coder)
│ ├── protocol.rs # WebSocket メッセージ型定義(WsMessage enum)
│ ├── tui.rs # ratatui による本格的な TUI(ピッカー、チャット、入力ロックなど)
│ ├── client.rs # クライアントモード: コーダー選択、Q&Aループ
│ └── coder.rs # コーダーモード: キュー登録、回答ループ
│
└── worker/ # Cloudflare Worker — リレーサーバー(Rust → WASM)
├── Cargo.toml
├── wrangler.jsonc
└── src/
└── lib.rs # QueueDO + RoomSession + fetch ハンドラー
| ツール | 用途 |
|---|---|
rustup + stable ツールチェーン |
CLI のビルド |
cargo |
ビルド / チェック / テスト |
Node.js + npx |
wrangler dev(ワーカー)の実行 |
wrangler(npx 経由) |
Cloudflare Worker 開発用ローカルサーバー |
cd worker
npx wrangler dev # http://localhost:8787 でリッスン# ターミナル 1 — コーダー
cargo run --bin coding-human -- coder "Alice"
# ターミナル 2 — クライアント
cargo run --bin coding-human -- client "Bob"ワークスペースルートの .env ファイルは dotenvy により自動で読み込まれます。
SERVER_URL=http://localhost:8787
すべてのメッセージは type フィールドを持つ JSON としてシリアライズされます(serde(tag = "type", rename_all = "snake_case"))。
| バリアント | 方向 | 目的 |
|---|---|---|
Matched { client_name } |
Client → Coder | 接続時のハンドシェイク |
Question { from, text } |
Client → Coder | 質問テキスト |
File { path, content } |
Client → Coder | @path で添付されたファイルデータ |
Cmd { command } |
Coder → Client | クライアントへのコマンド実行要求 |
CmdResult { command, output } |
Client → Coder | コマンドの実行結果 |
Diff { path, diff } |
Coder → Client | 適用を提案する unified diff |
DiffResponse { accepted } |
Client → Coder | diff が適用されたかどうかの返答 |
Done |
Coder → Client | 現在の回答ストリームの終了合図(Ctrl+D) |
WsMessage としてパースできないメッセージは、すべて raw な回答テキストとして扱われます。
ratatui と crossterm で構築されています。主要な型は以下の通りです。
pub enum ChatMsg {
Msg { role: ChatRole, text: String },
SetWaiting(bool), // 入力ボックスのロック(Wait状態)切り替え
OpenEditor(String), // TUIを一時停止して $EDITOR を開き、終了後に復元
}pub enum ChatEvent {
Line(String), // ユーザーが Enter を押して入力確定
Eof, // Ctrl+D
Quit, // Ctrl+C
EditorClosed, // OpenEditor で開いたエディタプロセスが終了した
}┌──────────────────────────────┐ log_tx (ChatMsg) ┌──────────────────┐
│ net_task (tokio::spawn) │ ──────────────────► │ run_chat │
│ │ │ (メインタスク) │
│ WebSocket I/O │ ◄────────────────── │ │
│ コマンド実行 │ event_tx (ChatEvent)│ TUI描画ループ │
│ パッチ適用 │ │ キーイベント │
└──────────────────────────────┘ └──────────────────┘
run_chat は Terminal<CrosstermBackend<Stdout>> の所有権を持っています。スクロールバーの位置をレンダリング行単位で正確に合わせるため、毎フレーム Vec<Line<'static>> に全メッセージを事前レンダリングしています。
scroll_offset はメッセージのインデックスではなくレンダリングされた実際の行数単位で記録されます。at_bottom = true の状態では、常にログの末尾に追従するように total_rendered_lines - inner_height から表示位置が動的に計算されます。
コーダーが @src/main.rs と入力した場合、ネットワークタスクは直接エディタのプロセスを起動するのではなく、TUI タスクに向けて ChatMsg::OpenEditor(path) を送信します。run_chat はこれを受け取ると:
leave(&mut term)— raw モードと alternate screen をクリーンに解除しますstd::process::Command::new(editor).arg(path).status()— クリーンなターミナル画面でエディタを実行します- 再度 raw モードと alternate screen に入り、
term.clear()を呼び出して画面を復元します ChatEvent::EditorClosedを送信して、待機していたネットワークタスクのブロックを解除します
この仕組みにより、ratatui がターミナルの制御を握ったまま外部プロセスが標準出力に書き込んで画面表示が崩れるバグを完全に防いでいます。
protocol.rsのWsMessageに新しいバリアントを追加します。- 送信側(
coder.rsまたはclient.rs)と受信側の両方で処理を実装します。 - 必要に応じて、
log_tx.send(ChatMsg::sys(...))を使って TUI ログにステータスを表示させます。
Cloudflare Worker は workers-rs を通じて WASM にコンパイルされます。
# 最初の一回のみ worker-build をインストール
cargo install worker-build
cd worker
npx wrangler deploy| メソッド | パス | 説明 |
|---|---|---|
GET |
/queue |
待機中のコーダー一覧 { roomId: label } を取得 |
POST |
/queue |
コーダーを登録 { label } → { roomId } |
DELETE |
/queue/:roomId |
コーダー登録の解除 |
GET (WS) |
/rooms/:id/coder |
コーダー用 WebSocket エンドポイント |
GET (WS) |
/rooms/:id/client |
クライアント用 WebSocket エンドポイント |
| オブジェクト | インスタンス数 | 役割 |
|---|---|---|
QueueDO |
シングルトン | 待機中コーダーのキューを KV ストレージに永続化 |
RoomSession |
ルームごとに 1 つ | ハイバネータブル WebSocket を活用したコーダーとクライアント間のメッセージリレー |
- エラーハンドリング: 失敗する可能性のある関数はすべて
anyhow::Resultを返します。?演算子を積極的に使い、最終的にmainでキャッチしてエラー終了(コード 1)させます。 - 非同期ランタイム:
full機能付きのtokioを使用します。ネットワーク I/O は専用のtokio::spawnタスクで、TUI の描画は呼び出し元のタスクで実行します。 - チャンネル: TUI ↔ 非同期タスクの連携には
mpsc::unbounded_channelを使用します。TUI ループはメッセージ生成よりも遥かに速くキューを消化するため、bounded なチャンネルによるバックプレッシャーは不要です。 - フォーマット: 標準の
rustfmtスタイルに準拠します。コミット前にcargo fmtを実行してください。 - リント:
cargo clippy -- -D warningsで警告が出ないようにしてください。