git worktree ごとに Claude Code / tmux セッションを張り、スマホブラウザからチャット操作できる開発コンパニオンツールのアーキテクチャ設計書。
このドキュメントは CommandMate の技術アーキテクチャを定義します。
- どのようなプロセス/コンポーネントで構成されるか
- どのように Claude CLI / tmux / Web UI が連携するか
- どのレイヤにどの責務を持たせるか
を、実装時の指針としてまとめています。
- 対象:
CommandMateアプリケーション全体- Next.js (App Router) ベースの Web UI
- Node.js API / WebSocket サーバ
- tmux + Claude CLI セッション管理
- SQLite / ローカル FS での永続化
- 対象外:
- Claude 側のモデル挙動
- 他ツール(mySwiftAgent / myAgentDesk など)との具体的な統合実装
命名・用語:
- worktree: git worktree として管理されるブランチディレクトリ
- worktreeId: URL セーフな識別子(例:
feature-foo) - tmux session:
cw_{worktreeId}という名前の tmux セッション - Stop フック: Claude CLI の
CLAUDE_HOOKS_STOPに設定する完了フック
- git worktree ごとに 独立した Claude CLI セッションを管理する
- スマホや PC のブラウザから、ブランチ別のチャット UIとして利用できる
- Stop フックを活用し、ポーリング無しのイベント駆動で最新の応答を反映する
- 対話の履歴/詳細ログをローカルに安全に保存する
- マルチユーザー SaaS としての動作(あくまでローカル開発者向け)
- セキュリティ境界を超えたネット越しの公開・マルチテナント運用
- CLI ツールのサポート (Issue #4で実装完了、Issue #368/#379/#545 で拡張)
- Claude Code, Codex CLI, Gemini CLI, Vibe-Local (Ollama), OpenCode, GitHub Copilot の 6 ツール対応
- Strategy パターンによる拡張可能な設計
- worktree毎に2〜4エージェントを選択可能(
selected_agentsカラム) - Vibe-LocalはOllamaモデルを指定可能(
vibe_local_modelカラム)
- 開発者のローカルマシン上で動作
- macOS / Linux を想定(tmux / Claude CLI が利用可能)
- Claude CLI がインストール済みであり、
claudeコマンドで起動できるCLAUDE_HOOKS_STOPを設定できるバージョンである
- git worktree により、複数のブランチディレクトリが管理されている
- ローカルモード(デフォルト)
CM_BIND = 127.0.0.1- 接続元は同一マシンのみ
- LAN アクセスモード(任意)
CM_BIND = 0.0.0.0- リバースプロキシでの認証を推奨(詳細:
docs/security-guide.md) - 同一ネットワーク上のスマホからアクセス可能
graph TD
subgraph Browser["スマホ/PCブラウザ"]
UI_A[画面A: Worktree一覧]
UI_B[画面B: チャット画面]
UI_C[画面C: ログビューア]
UI_WS[WebSocketクライアント]
end
subgraph Next["Next.js / Node.js Application"]
App["Next.js UI (App Router)"]
API_Send[/POST /api/worktrees/:id/send/]
API_WT[/GET /api/worktrees/ .../]
API_Hook[/POST /api/hooks/claude-done/]
API_Logs[/GET /api/worktrees/:id/logs/ .../]
WS_Server[WebSocket サーバ]
DB[(SQLite db.sqlite)]
end
subgraph TmuxClaude["tmux / Claude CLI"]
T[(tmux server)]
S1[tmux session: cw_main]
S2[tmux session: cw_feature-foo]
CL1[Claude CLI プロセス]
end
subgraph FS["Local Filesystem"]
Logs[".claude_logs/ (Markdownログ)"]
end
UI_A --> App
UI_B --> App
UI_C --> App
UI_WS <---> WS_Server
App --> API_WT
App --> API_Send
App --> API_Logs
API_Send --> T
API_Hook --> T
API_WT --> DB
API_Logs --> Logs
API_Hook --> DB
API_Hook --> Logs
API_Hook --> WS_Server
CL1 -->|"Stopフック"| API_Hook
• Next.js / Node.js プロセス
• ポート: CM_PORT(例: 3000)
• バインド: CM_BIND(127.0.0.1 or 0.0.0.0)
• 機能:
• HTTP サーバ(UI, API Routes)
• WebSocket サーバ
• SQLite への接続
• tmux server
• システム上に既存の tmux サーバを利用
• cw_{worktreeId} というセッション名で CLI ツールを起動
• CLI ツールプロセス(Claude Code / Codex CLI)
• 各 tmux セッション内で選択された CLI ツールが起動
• CLAUDE_HOOKS_STOP に設定されたコマンドで完了通知(Claude Code の場合)
⸻
interface Worktree {
id: string; // URLセーフなID ("main", "feature-foo" など)
name: string; // 表示名 ("main", "feature/foo" など)
path: string; // 絶対パス "/path/to/root/feature/foo"
lastMessageSummary?: string; // 最後のメッセージ要約
updatedAt?: Date; // 最終メッセージの timestamp
}
- id
- URL パラメータに使用可能な形式へ正規化した識別子
- 例: feature/foo → feature-foo
- path
- CM_ROOT_DIR を起点とした絶対パス
- updatedAt
- Worktree 一覧を最終更新日時順に並べるために利用
type ChatRole = "user" | "claude";
interface ChatMessage {
id: string; // UUID
worktreeId: string; // Worktree.id
role: ChatRole;
content: string; // UI表示用の全文
summary?: string; // 任意の短い要約
timestamp: Date;
logFileName?: string; // 対応する Markdown ログファイル名
requestId?: string; // 将来の拡張用: 1送信=1 UUID
}
- content: チャット UI にそのまま表示するテキスト(プロンプト/応答)。
- summary: Worktree 一覧の lastMessageSummary に反映するための要約(任意)。
- logFileName: .claude_logs/ 内の詳細ログファイルへのリンク。
interface WorktreeSessionState {
worktreeId: string;
lastCapturedLine: number; // tmux capture-pane で最後に取得した行数
}
- Stop フック時に tmux capture-pane で scrollback 全体を取得し、
- lastCapturedLine 以降を「今回の新規出力」として扱う差分抽出方式を前提とします。
⸻
- アプリ起動時、CM_ROOT_DIR を基準として git worktree をスキャン。
- 取得した worktree 情報を基に Worktree レコードを DB に格納/更新。
- 既存の cw_{worktreeId} tmux セッションがあれば、Worktree と突き合わせて整合性をとる(任意)。
概要
sequenceDiagram
participant UI as UI (画面B)
participant API as API_Send
participant T as tmux
participant CL as Claude CLI
UI->>API: POST /api/worktrees/:id/send { message }
API->>T: セッション存在チェック (cw_{worktreeId})
alt セッション無し
API->>T: tmux new-session (cw_{worktreeId})
API->>T: export CLAUDE_HOOKS_STOP=...
API->>T: claude 起動
end
API->>T: tmux send-keys "message"
API-->>UI: 202 Accepted (送信完了)
Note over UI: 「送信中…」バブルを表示
詳細ステップ
- UI(画面B)が POST /api/worktrees/:id/send に { message } を送信。
- API:
- worktreeId から Worktree を取得し、path を参照。
- tmux has-session -t cw_{worktreeId} でセッション存在チェック。
- なければ UC-2(遅延起動フロー)を実行(後述)。
- tmux send-keys -t cw_{worktreeId} "<message>" C-m で Claude CLI へ送信。
- API は 202 (Accepted) などで UI に即時応答。UI では自分のメッセージ+「送信中…」バブルを表示。
sequenceDiagram
participant CL as Claude CLI
participant Hook as CLAUDE_HOOKS_STOP
participant API as API_Hook
participant T as tmux
participant DB as SQLite
participant FS as .claude_logs
participant WS as WebSocket
CL-->>Hook: 完了時フック呼び出し
Hook->>API: POST /api/hooks/claude-done { worktreeId }
API->>T: tmux capture-pane (全scrollback取得)
API->>API: 差分抽出 (lastCapturedLine 以降)
API->>FS: Markdownログとして保存
API->>DB: ChatMessage と Worktree.updatedAt 更新
API->>WS: 新しい ChatMessage を publish
詳細ステップ
- Claude CLI が処理完了時、CLAUDE_HOOKS_STOP に設定されたコマンドを実行。
- 例:
HOOK_COMMAND="curl -X POST http://localhost:3000/api/hooks/claude-done \
-H 'Content-Type: application/json' \
-d '{\"worktreeId\":\"{worktreeId}\"}'"
export CLAUDE_HOOKS_STOP="${HOOK_COMMAND}"
- POST /api/hooks/claude-done を受け取った API_Hook は、
- WorktreeSessionState.lastCapturedLine を読み込む(デフォルト 0)。
- tmux capture-pane -p -S -10000 -t cw_{worktreeId} などで scrollback を取得。
- 取得したテキストの行数をカウントし、
- lastCapturedLine 以降の行だけを「新規出力」として抽出。
- 将来的にマーカー (### REQUEST {id} START/END) が導入される場合は、その範囲のみ抽出する拡張も可能。
- 抽出した差分を Markdown として .claude_logs/ 以下に保存。
- ChatMessage のレコードを作成(role = "claude")し、logFileName を紐付け。
- Worktree の lastMessageSummary, updatedAt を更新。
- 新しい ChatMessage を WebSocket 経由で publish し、該当 worktree を閲覧中のクライアントすべてに配信。
- クライアントは接続時に worktreeId を指定して subscribe。
- サーバ側では worktreeId ごとに room / channel を管理。
- 新しい ChatMessage が保存されると、
- 該当 worktreeId を購読中のクライアントにブロードキャスト。
メッセージ形式の例:
{
"type": "chat_message_created",
"worktreeId": "feature-foo",
"message": {
"id": "uuid",
"role": "claude",
"content": "Claude の応答...",
"timestamp": "2025-11-16T03:00:00.000Z",
"logFileName": "20251116-030000-feature-foo-bd2f8c3d.md"
}
}
Stop フックが届かない場合
- UI 側:
- 「送信中…」が一定時間(例: 120 秒)経過しても差し替わらない場合、警告バブルを表示。
- サーバ側:
- Hook API 側でタイムアウトログを残す(実装依存)。
- 手動で .claude_logs/ や tmux の状態を確認することを想定。
tmux セッション / Claude プロセスが落ちた場合
- API_Send 実行時に tmux has-session/プロセス状態をチェック。
- セッションが無い/Claude がいないと判定された場合は UC-2 で再起動。
- Claude CLI の再起動時にも CLAUDE_HOOKS_STOP を再セットする。
⸻
- セッション名:
mcbd-{cliToolId}-{worktreeId} - 例:
- Claude:
mcbd-claude-feature-foo
- Claude:
- 1 worktree に対して 1 セッションを維持する。
- ※ 旧命名規則
cw_{worktreeId}からの移行: Issue #4で実装
設計パターン:
- Strategy パターンによるCLI tool の抽象化
BaseCLITool抽象クラスが共通インターフェースを定義:abstract class BaseCLITool { abstract id: CLIToolType; abstract name: string; abstract command: string; abstract isInstalled(): Promise<boolean>; abstract isRunning(worktreeId: string): Promise<boolean>; abstract startSession(worktreeId: string, worktreePath: string): Promise<void>; abstract sendMessage(worktreeId: string, message: string): Promise<void>; abstract killSession(worktreeId: string): Promise<boolean>; getSessionName(worktreeId: string): string; }
実装クラス:
ClaudeTool- Claude Code CLICodexTool- Codex CLIGeminiTool- Gemini CLI (Issue #368)VibeLocalTool- Vibe-Local / Ollama (Issue #368)OpenCodeTool- OpenCode (Issue #379)CopilotTool- GitHub Copilot CLI (Issue #545)
管理:
CLIToolManagerシングルトンクラスで各ツールインスタンスを管理- ワークツリーの
cliToolIdに基づいて適切なツールを取得
利点:
- 新しいCLI toolの追加が容易(
BaseCLIToolを継承するだけ) - API層は抽象インターフェースのみに依存、具体的な実装に依存しない
- 各ツール固有のロジックをカプセル化
# {sessionName} = cw_{worktreeId}
# {worktreePath} = /path/to/root/feature/foo
tmux new-session -d -s "{sessionName}" -c "{worktreePath}"
HOOK_COMMAND="curl -X POST http://localhost:3000/api/hooks/claude-done \
-H 'Content-Type: application/json' \
-d '{\"worktreeId\":\"{worktreeId}\"}'"
tmux send-keys -t "{sessionName}" "export CLAUDE_HOOKS_STOP='${HOOK_COMMAND}'" C-m
tmux send-keys -t "{sessionName}" "claude" C-m
- API から直接 tmux コマンドを叩くラッパーは src/lib/tmux/tmux.ts にまとめる。
- シンプルな実装では「セッションの存在」のみチェック。
- より堅牢にする場合:
- tmux list-panes -F "#{pane_pid}" → ps で該当 PID が claude か確認。
- プロンプト状態(claude が終了)になっていた場合、CLAUDE_HOOKS_STOP 再設定 + claude 再起動。
⸻
- 保存場所の例: ~/.mycodebranchdesk/db.sqlite
- テーブル(イメージ):
- worktrees:
- id, name, path, last_message_summary, updated_at
- cli_tool_id (追加: Issue #4、#379/#545 で拡張) - 使用するCLI tool ('claude' | 'codex' | 'gemini' | 'vibe-local' | 'opencode' | 'copilot')
- selected_agents (追加: Issue #368) - 選択中の2〜4エージェント (JSON配列, 例: '["claude","vibe-local"]')
- vibe_local_model (追加: Issue #368) - Vibe-Local用Ollamaモデル名 (nullable)
- repository_path, repository_name, description
- last_user_message, last_user_message_at
- favorite, status, link
- chat_messages:
- id, worktree_id, role, content, summary, timestamp, log_file_name, request_id
- message_type, prompt_data
- session_states:
- worktree_id, last_captured_line
- 各 worktree ディレクトリ直下に .claude_logs/ ディレクトリを作成。
- ファイル命名規則:
{YYYYMMDD}-{HHmmss}-{worktreeId}-{uuid}.md
- 中身イメージ:
# CommandMate Log
## Worktree
feature/foo
## Timestamp
2025-11-16T03:00:00Z
## User
<ユーザーのプロンプト>
## Claude
<Claudeの応答全文>
- 削除ポリシー:
- v1 では自動削除は行わず、将来 CLI などでのクリーンアップを検討。
⸻
- CM_ROOT_DIR (必須)
- git worktree 管理のルートディレクトリ
- CM_PORT (任意, デフォルト: 3000)
- CM_BIND (任意, デフォルト: 127.0.0.1)
- 将来的に追加される可能性のあるもの:
- CM_DB_PATH(db.sqlite の保存先)
- CM_LOG_LEVEL(ログ出力レベル)
レガシー fallback について: 旧プレフィックス
MCBD_*(MCBD_ROOT_DIR/MCBD_PORT/MCBD_BIND/MCBD_DB_PATH/MCBD_LOG_LEVEL等)は後方互換のため引き続き参照される。CM_*が未設定の場合のみ対応するMCBD_*の値に fallback し、その際は deprecation 警告を出力する(実装:src/lib/env.tsのENV_MAPPING/getEnvWithFallback)。新規設定ではCM_*を使用すること。
.env.example としてリポジトリに同梱することを想定。
⸻
- デフォルトは CM_BIND=127.0.0.1
- 同一マシンからのアクセスに限定。
- セキュリティリスクはほぼ OS ユーザーに依存。
- CM_BIND=0.0.0.0 の場合、同一 LAN 上の任意端末からのアクセスが可能になる
- リバースプロキシ(Nginx / Caddy 等)での認証を推奨(詳細:
docs/security-guide.md) - HTTPS / TLS 終端はリバースプロキシに委譲する
⸻
- 将来的に、Stop フックの payload に requestId も含める設計余地を残す:
-d '{\"worktreeId\":\"{worktreeId}\", \"requestId\":\"{requestId}\"}'
- ChatMessage.requestId とログ名に埋め込むことで、「どのリクエストの応答か」をより厳密にトレース可能。
実装内容:
- Claude Code / Codex / Gemini / Vibe-Local / OpenCode / GitHub Copilot の 6 ツールに対応
- ワークツリーごとに
cliToolIdフィールドで使用するCLIツールを管理 - Strategy パターンによる抽象化:
BaseCLITool抽象クラスClaudeTool/CodexTool/GeminiTool/VibeLocalTool/OpenCodeTool/CopilotTool実装クラスCLIToolManagerシングルトンでツールインスタンスを管理
- データベーススキーマ:
worktrees.cli_tool_idカラム(デフォルト: 'claude') - 対応API:
POST /api/worktrees/:id/send- メッセージ送信POST /api/worktrees/:id/respond- プロンプト応答POST /api/worktrees/:id/kill-session- セッション終了GET /api/worktrees- ワークツリー一覧(セッション状態含む)
将来拡張:
- その他のCLIを追加する場合は、
BaseCLIToolを継承して実装 - Stop フックの仕様が異なる場合は各ツールクラス内で対応
実装内容:
- ターミナル出力を直接解析してCLIツールの状態をリアルタイム検出
- サイドバーに視覚的なステータスインジケーターを表示
- 2秒間隔でポーリング更新
ステータス種別:
| ステータス | 表示 | 検出条件 |
|---|---|---|
idle |
グレー● | セッション未起動 |
ready |
緑● | 入力プロンプト ❯ 表示中 |
running |
青スピナー | 思考インジケータ検出 |
waiting |
黄● | インタラクティブプロンプト検出 |
検出ロジック:
captureSessionOutput()でターミナル出力を取得- ANSIエスケープコードを除去
- 空行をフィルタリングして最後15行を抽出
- 優先順位に従ってパターンマッチング:
- インタラクティブプロンプト →
waiting - 思考インジケータ (
✻ Thinking…) →running - 入力プロンプト (
❯) →ready - それ以外 →
running(処理中と推定)
- インタラクティブプロンプト →
関連ファイル:
src/config/status-colors.ts- ステータス色の一元管理src/lib/detection/cli-patterns.ts- CLIツール別パターン定義src/lib/detection/prompt-detector.ts- プロンプト検出src/types/sidebar.ts- ステータス判定ロジック
詳細は ステータスインジケーター を参照。
実装内容:
- 登録済みリポジトリを削除するAPIエンドポイントとUI
- 関連するworktree、セッション、ポーリングを一括クリーンアップ
- Facadeパターンによるセッション停止処理の一元化
対応API:
DELETE /api/repositories- リポジトリ削除(リクエストボディでrepositoryPathを指定)
リクエスト/レスポンス:
// Request
{ "repositoryPath": "/path/to/repository" }
// Response (200 OK)
{
"success": true,
"deletedWorktreeCount": 3,
"warnings": []
}エラーレスポンス:
| ステータス | 条件 |
|---|---|
| 400 | repositoryPath 未指定 |
| 404 | リポジトリが存在しない |
削除処理フロー:
- リポジトリに属する全worktree IDを取得
- 各worktreeのtmuxセッションをkill(失敗してもwarningsに記録して続行)
- response-pollerを停止
- WebSocket購読状態をクリーンアップ
- DBからworktreeレコードをCASCADE削除
repository_deletedイベントをbroadcast
段階的エラーハンドリング:
- セッションkill失敗時もDB削除は継続
- 失敗情報は
warnings配列で返却
UI実装:
- リポジトリフィルターチップにホバー時削除ボタン(×)表示
- 確認ダイアログで「delete」入力必須
WORKTREE_REPOS環境変数設定リポジトリに警告アイコン(⚠️ )表示
関連ファイル:
src/lib/session-cleanup.ts- セッション/ポーラー停止のFacadesrc/app/api/repositories/route.ts- DELETEエンドポイントsrc/lib/db.ts-getWorktreeIdsByRepository(),deleteRepositoryWorktrees()src/lib/ws-server.ts-cleanupRooms()
- 将来的に:
- 応答時間(latency)の計測
- エラー率
- セッションごとの履歴メトリクス
- などを可視化するため、ChatMessage や Worktree にメタ情報フィールドを拡張する余地を残す。
| パス | 画面名 | 説明 |
|---|---|---|
/ |
Home | ダッシュボード。セッションサマリー・ショートカットカード |
/sessions |
Sessions | Worktree一覧。フィルタ・ソート・グループ化 |
/repositories |
Repositories | リポジトリ管理。追加・同期・削除 |
/review |
Review | レビュー待ちWorktree一覧。Done/Approval/Stalledフィルタ |
/more |
More | 設定・外部アプリ・バージョン情報 |
/worktrees/[id] |
Worktree Detail | Worktree詳細。ターミナル・履歴・ファイル・メモ |
/worktrees/[id]?pane=<DeepLinkPane> |
Worktree Detail (deep link) | 特定ペインを直接表示 |
?pane= クエリパラメータで指定可能な値:
| pane値 | デスクトップ左ペイン | モバイルタブ | 説明 |
|---|---|---|---|
terminal |
history | Terminal | ターミナル表示(デフォルト) |
history |
history | History | 会話履歴 |
git |
history | History | Gitコミット履歴 |
files |
files | Files | ファイルツリー・エディタ |
notes |
memo | CMATE | メモ・ノート |
logs |
memo | CMATE | 実行ログ |
agent |
memo | CMATE | エージェント設定 |
timer |
memo | CMATE | タイマー |
info |
history | Info | Worktree情報 |
Home (/)
|-- Sessions (/sessions) --> Worktree Detail (/worktrees/[id])
|-- Repositories (/repositories)
|-- Review (/review) --> Worktree Detail (/worktrees/[id]?pane=terminal)
|-- More (/more)
Worktree Detail (/worktrees/[id])
|-- ?pane=terminal (default)
|-- ?pane=history
|-- ?pane=git
|-- ?pane=files
|-- ?pane=notes / logs / agent / timer
|-- ?pane=info
PC(デスクトップ):
- Header: 5画面ナビゲーションリンク(Home / Sessions / Repositories / Review / More)
- Sidebar: Worktree一覧(ソート・グループ化・検索)
- メインエリア: 選択中のページコンテンツ
モバイル:
- GlobalMobileNav: 下部固定5タブ(Home / Sessions / Review / Repositories / More)
- Worktree Detail内: MobileTabBar(Terminal / History / Files / CMATE / Info)
- MobileTabBarは
onSearchParamsChangeコールバックで?pane=を更新
- MobileTabBarは
useWorktreesCache() ── 唯一のworktree一覧取得元
|
+-- WorktreesCacheProvider
| |
| +-- WorktreeSelectionProvider (externalWorktrees)
| |-- 選択中worktree ID管理
| |-- 詳細取得 (GET /api/worktrees/:id)
| |-- markAsViewed
|
+-- useWorktreeList() ── ソート・フィルタ・グループ化
|-- Sidebar
|-- Sessions画面
⸻
注: 既存 §11.2 DeepLinkPane 表・§11.4 ナビゲーション構造は #600 時点の旧デスクトップ左ペインモデル(history / files / memo, MobileTabBar)を前提とした記述であり、現行 PC UI は本章の ActivityBar モデルへ置き換わっている(旧 §11.2 / §11.4 の整理は本章のスコープ外)。
本章は PC 版 UI 最上位レイアウトの 責務境界・状態所有・データフロー(なぜその構造か / 状態の所有者は誰か)を扱う。視覚的なレイアウト・操作(キーボード / Tooltip)・コンポーネントカタログの詳細は docs/UI_UX_GUIDE.md を正(SSOT)とし、本章では再掲しない。
- ActivityBar (
src/components/worktree/ActivityBar.tsx): Activity 選択状態を所有しない完全な制御コンポーネント。active/onToggleを props で受け取り、@/config/activity-bar-configのACTIVITIES(Files / Git / Notes / Schedules / Agent / Timer の 6 Activity)を描画する。最上部のサイドバートグル (Issue #747) のみ例外で、useSidebarContext()から{ isOpen, toggle }を直接読み取り、role="tablist"の外側に配置されてタブのキーボードナビゲーションに干渉しない。 - ActivityPane (
ActivityPane.tsx): 状態を持たない table-driven のディスパッチャ。親が構築したActivityContentMap(Partial<Record<ActivityId, ReactNode>>)からactivities[active]を 1 つだけ選びErrorBoundary経由で描画する。単一のactiveキーで 1 ノードのみを描画するため、多 Activity 同時表示は構造的に不可能。 - TerminalContainer (
TerminalContainer.tsx, Issue #730):terminalスロット(常時)とhistoryスロット(任意)を内包する。Issue #744 以降、PC では最上位 History カラムを描画せず、History は各ターミナル split 内へ移管された(後述 §13)。historyprop とuseHistoryPaneState配線は #730 単一カラムレイアウトとの後方互換として残置している。 - WorktreeDesktopLayout (
WorktreeDesktopLayout.tsx, Issue #730): ActivityPane と Right ペインの 2 カラム構成。Issue #730 で 4 カラムから 2 カラムへ簡素化し(History は Right 内の TerminalContainer へ、ActivityBar は親側で全高描画へ移動)、activityPane === nullのとき Activity カラムと境界の PaneResizer を共に隠す。 - PaneResizer (
PaneResizer.tsx): 一過性のドラッグ状態のみを持つ汎用リサイザ。ピクセル差分をonResize(delta)で親へ通知するのみで、ペイン幅の確定は親が行う(WorktreeDesktopLayout の Activity↔Right 境界、TerminalContainer の History↔Terminal 境界の両方で再利用)。
- useActivityBarState (
src/hooks/useActivityBarState.ts):active状態の単一情報源。commandmate.worktree.activeActivityに永続化するが、閉じた状態(null)は永続化しない(最後に選択した Activity のみ保存して再訪時に復元)。不正値・未設定時はDEFAULT_ACTIVITY = 'files'にフォールバックし、初期描画は決定的にDEFAULT_ACTIVITY・localStorage 読み取りはマウント後のuseEffectで行う(hydration mismatch 回避)。 - SidebarContext (
src/contexts/SidebarContext.tsx): サイドバーisOpenをuseReducerで所有。ActivityBar のハンバーガー (Issue #747) は本 context を直接参照し、Activity 選択(props 経由)とは別系統のデータフローを成す。
オーケストレーター (WorktreeDetailRefactored.tsx) が useActivityBarState を呼んで ActivityContentMap を構築し、ActivityBar / ActivityPane / WorktreeDesktopLayout へ配線する。
graph TD
Parent["WorktreeDetailRefactored<br/>(オーケストレーター)"]
ActState["useActivityBarState<br/>(active を所有 / localStorage)"]
SideCtx["SidebarContext<br/>(isOpen を所有)"]
Bar["ActivityBar<br/>(状態を持たない)"]
Pane["ActivityPane<br/>(table-driven dispatch)"]
Layout["WorktreeDesktopLayout<br/>(2 カラム + PaneResizer)"]
Term["TerminalContainer<br/>(terminal + 任意 history)"]
ActState -->|active| Parent
Parent -->|onToggle| ActState
Parent -->|active / onToggle| Bar
Bar -->|"サイドバートグル (Issue #747)"| SideCtx
Parent -->|"activityPane (null 可)"| Layout
Parent -->|"ActivityContentMap"| Pane
Pane --> Layout
Layout -->|rightPane| Term
forward-reference: per-agent(CLI 単位)の session status indicator は Issue #743 / #749 / #751 で実装済み(per-split header の status・DesktopHeader の agent status row)。本章のスコープ外であり、詳細は将来追記する(任意)。
⸻
本章は PC 版ターミナルの 1〜3 分割と、分割ごとに独立した per-(worktreeId, cliToolId) の fan-out ポーリング戦略を扱う。
- 状態の所有者は
src/hooks/useTerminalSplits.ts(Issue #728)。VS Code 風レイアウト用 reducer(activityBar / historyPane 等)には意図的に統合せず、action の肥大化を避けて分離している。 - 定数(
src/config/terminal-split-config.ts):MIN_SPLITS = 1、MAX_SPLITS = 3、DEFAULT_SPLIT_CONFIG = { splits: [{ cliToolId: 'claude' }], widths: [1] }。 - 永続化は worktree 単位(
getTerminalSplitsStorageKey(worktreeId)をキーに splits + widths を localStorage へ保存)。worktreeId 変更時に再読込しfocusedSplitIndexを 0 リセットする。 - 読込時に
isValidSplitConfigで検証し(splits.length ∈ [1, 3]、widths.length === splits.length、各 width は有限かつ正)、不合格ならDEFAULT_SPLIT_CONFIGにフォールバックする(外部編集・stale データ防御)。 - 同一 CLI 複数選択禁止ガード:
setSplitCliToolは他 split が同じ CLI を使用中なら変更を拒否し、availableCliTools(idx)は使用中 CLI を除外したリストを返す(UI の<select>で該当 option を無効化)。CLI 種別数(6)がMAX_SPLITS(3)を上回るため、addSplit時の未使用 CLI 割り当ては枯渇しない。
- 各
TerminalSplitPaneContentがsrc/hooks/useTerminalPanePolling.tsを 1 インスタンス所有し、GET /api/worktrees/{worktreeId}/current-output?cliTool={cliToolId}を独立に叩く(split 0 = Claude、split 1 = Codex がそれぞれポーリング)。 - 所有する状態:
output/isRunning/isThinking/isSelectionListActive/attaching(初回の成功 fetch まで true)/autoScroll(ペイン単位)/ prompt。Auto-Yes 状態と History メッセージは activeCliTab スコープのグローバルのため意図的に所有しない。 - stale 応答破棄(2 軸): fetch 毎にインクリメントする
requestIdと、毎レンダで現在の CLI を記録するinFlightCliToolRefを応答時に照合し、いずれかが一致しない応答(順序逆転・CLI 切替跨ぎ)を破棄する。サーバ応答に含まれるcliToolIdが要求 CLI と異なる場合も破棄する。 - ポーリング周期は
isRunningで切り替える(ACTIVE_POLLING_INTERVAL_MS = 2000/IDLE_POLLING_INTERVAL_MS = 5000)。document.visibilityState === 'hidden'の tick はスキップし、可視復帰時に 1 回だけ再 fetch する(バックグラウンド時のポーリング停止)。 compositeKey = {worktreeId}::{cliToolId}の変化時にrequestIdを bump し、output / prompt をクリアしてattaching状態へ戻す(fresh attach として扱う)。
- useSplitMessages (
src/hooks/useSplitMessages.ts, Issue #744): 各 split がGET /api/worktrees/{worktreeId}/messages?cliTool={cliToolId}を独立 fetch する(SPLIT_MESSAGES_POLL_INTERVAL_MS = 5000)。親のstate.messagesは activeCliTab に絞り込み済みのため、split A=Claude と split B=Codex の同時表示には流用できない。stale-guard は useTerminalPanePolling と同型。 - makeHistoryNamespace (
src/lib/terminal-highlight.ts, Issue #744):CSS.highlightsは name をキーとするグローバル Map であり、複数の HistoryPane が同一 namespace(HISTORY_SEARCH_NAMESPACE)を使うと検索ハイライトが相互に上書きされる。makeHistoryNamespace(splitIndex)が split index サフィックス付きの namespace(history-search-{idx}等)を返し、各 split が別レジストリキーでハイライトを保持して衝突を回避する。applyHistoryHighlights等は namespace 引数を省略すると従来の共有 namespace を採用するため、Mobile / 単一ペインの呼び出しは無変更(後方互換)。
- Mobile は
MobileTerminalTabがuseTerminalPanePollingを単一インスタンスのみ所有し、terminal タブ表示時にだけマウントする(activeCliTab に対応する 1 CLI のみポーリング)。 - Issue #736 で terminal reducer slice を撤去し、PC / Mobile とも
useTerminalPanePollingから output を取得するようになった。PC は split 数(最大 3)ぶんを並列起動する点が Mobile との差分である。
graph TD
Splits["useTerminalSplits<br/>(splits / widths / focusedSplitIndex を所有)"]
Container["TerminalSplitContainer<br/>(renderPane で各 split を委譲)"]
P0["TerminalSplitPaneContent[0]<br/>cliTool=claude"]
P1["TerminalSplitPaneContent[1]<br/>cliTool=codex"]
Poll0["useTerminalPanePolling"]
Msg0["useSplitMessages"]
Poll1["useTerminalPanePolling"]
API0["/current-output?cliTool=claude"]
API0b["/messages?cliTool=claude"]
API1["/current-output?cliTool=codex"]
Splits --> Container
Container --> P0
Container --> P1
P0 --> Poll0 --> API0
P0 --> Msg0 --> API0b
P1 --> Poll1 --> API1
補足: History ペインの表示 / 幅は MVP では全 split 共通の
useHistoryPaneStateを参照する(per-split 独立の開閉・幅は将来の拡張余地)。
⸻