## 概要
- [[cmux]]から[[herdr]]に移行した
- planファイルを[[herdr]]の新しいペインで開きたい
- `plan` コマンドを作成することにした
## 前提
以下がインストールされている。
- [[herdr]]
- [[leaf]]
### 環境
| 対象 | バージョン |
| ----------- | ------ |
| [[macOS]] | 26.5.1 |
| [[Ghostty]] | 1.3.1 |
| [[herdr]] | 0.7.1 |
| [[leaf]] | 1.25.0 |
## プラン
> [!code]-
> ````markdown
> # planコマンドの実装計画
>
> ## Context
>
> herdr上で起動しているAIエージェントCLI (Claude Code / Codex CLI / GitHub Copilot CLI) から `!plan` で呼び出され、最新のplanファイルを右ペインにleafで表示するコマンドを `mnt/plan/` に新規作成する。
>
> ## 変更ファイル
>
> 1. **`mnt/plan/plan.sh`** (新規作成) - メインスクリプト
> 2. **`provision.sh`** (1行追加) - `~/bin/plan` へのシンボリックリンク
>
> ## `mnt/plan/plan.sh` の実装
>
> ### 全体フロー
>
> ```
> plan [<mdファイル>]
> ├─ 引数あり → そのファイルを使用
> └─ 引数なし → Agent自動判別
> ├─ $HERDR_PANE_ID + herdr pane list → agent名取得
> ├─ claude → ~/.claude/plans/ の最新.mdファイル (ls -t)
> ├─ copilot → ~/.copilot/session-state/*/plan.md の最新 (find + stat)
> └─ codex → ~/.codex/sessions/ のJSONLから<proposed_plan>抽出
> → 一時ファイルに書き出してleafで表示
> ```
>
> ### Agent判別方法
>
> ```bash
> agent=$(herdr pane list | jq -r --arg pid "$HERDR_PANE_ID" \
> '.result.panes[] | select(.pane_id == $pid) | .agent // empty')
> ```
>
> ### 各Agentのplanファイル取得
>
> - **Claude Code**: `ls -t ~/.claude/plans/*.md | head -1`
> - **GitHub Copilot CLI**: `find ~/.copilot/session-state -name "plan.md" -exec stat -f '%m %N' {} + | sort -rn | head -1 | cut -d' ' -f2-`
> - **Codex CLI**: `find ~/.codex/sessions -name "*.jsonl" | sort -r` で最新JONLから `<proposed_plan>...</proposed_plan>` タグを抽出。セッションの `session_meta.cwd == $PWD` でカレントディレクトリのセッションに限定。抽出結果を一時ファイル(`/tmp/codex-plan-XXXXX.md`)に書き出す。見つからなければエラー。
>
> ### ペイン分割 + leaf起動
>
> ```bash
> NEW_PANE=$(herdr pane split --direction right --focus | jq -r '.result.pane.pane_id')
> herdr pane run "$NEW_PANE" "leaf --watch --editor nvim $plan_file"
> ```
>
> ## `provision.sh` への追加
>
> 428行目 `ln -snf "$MNT"/otm/otm.sh ~/bin/otm` の後に追加:
>
> ```bash
> ln -snf "$MNT"/plan/plan.sh ~/bin/plan
> ```
>
> ## 検証方法
>
> 1. herdr上でClaude Codeを起動し、planモードでplanファイルを作成
> 2. Claude Codeから `!plan` を実行 → 右ペインにplanファイルが表示されることを確認
> 3. `!plan path/to/file.md` で指定ファイルが表示されることを確認
>
> ````
## 成果物
`plan.sh`
```bash
#!/bin/bash
set -euo pipefail
usage() {
echo "Usage: plan [<md file>]"
echo ""
echo "herdr上で起動中のAIエージェントの最新planファイルをleafで表示します。"
echo ""
echo "引数なし: 現在のペインのエージェントを自動判別し、最新のplanを開く"
echo "引数あり: 指定したMarkdownファイルを開く"
exit 1
}
case "${1:-}" in
-h | --help)
usage
;;
esac
find_claude_plan() {
local plans_dir="$HOME/.claude/plans"
if [[ ! -d "$plans_dir" ]]; then
echo "Error: Claude Codeのplansディレクトリが見つかりません: ${plans_dir}" >&2
return 1
fi
local plan_file
plan_file=$(ls -t "$plans_dir"/*.md 2>/dev/null | head -1)
if [[ -z "$plan_file" ]]; then
echo "Error: Claude Codeのplanファイルが見つかりません" >&2
return 1
fi
echo "$plan_file"
}
find_copilot_plan() {
local session_dir="$HOME/.copilot/session-state"
if [[ ! -d "$session_dir" ]]; then
echo "Error: GitHub Copilot CLIのsession-stateディレクトリが見つかりません: ${session_dir}" >&2
return 1
fi
local plan_file
plan_file=$(find "$session_dir" -name "plan.md" -exec stat -f '%m %N' {} + 2>/dev/null | sort -rn | head -1 | cut -d' ' -f2-)
if [[ -z "$plan_file" ]]; then
echo "Error: GitHub Copilot CLIのplanファイルが見つかりません" >&2
return 1
fi
echo "$plan_file"
}
find_codex_plan() {
local target_cwd="${1:-$PWD}"
local sessions_dir="$HOME/.codex/sessions"
if [[ ! -d "$sessions_dir" ]]; then
echo "Error: Codex CLIのsessionsディレクトリが見つかりません: ${sessions_dir}" >&2
return 1
fi
local plan_text=""
while IFS= read -r file; do
jq -e --arg cwd "$target_cwd" '
select(.type == "session_meta")
| .payload.cwd == $cwd
' "$file" >/dev/null 2>&1 || continue
local extracted
extracted=$(jq -rs -r '
[.[] | select(.type == "event_msg" and .payload.type == "item_completed")
| .payload.item
| select(.type == "Plan")
| .text] | last // empty
' "$file" 2>/dev/null)
if [[ -n "$extracted" ]]; then
plan_text="$extracted"
break
fi
done < <(find "$sessions_dir" -name "*.jsonl" 2>/dev/null | sort -r)
if [[ -z "$plan_text" ]]; then
echo "Error: Codex CLIのplanが見つかりません (cwd: $target_cwd)" >&2
return 1
fi
local tmp_file
tmp_file=$(mktemp /tmp/codex-plan-XXXXXX)
printf '%s\n' "$plan_text" >"$tmp_file"
echo "$tmp_file"
}
get_plan_file() {
if [[ $# -ge 1 ]]; then
local file="$1"
if [[ ! -f "$file" ]]; then
echo "Error: ファイルが見つかりません: ${file}" >&2
exit 1
fi
echo "$file"
return
fi
local pane_id="${HERDR_PANE_ID:-${HERDR_ACTIVE_PANE_ID:-}}"
if [[ -z "$pane_id" ]]; then
echo "Error: HERDR_PANE_ID/HERDR_ACTIVE_PANE_ID が設定されていません。herdr内で実行してください。" >&2
exit 1
fi
local pane_info
pane_info=$(herdr pane list | jq -r --arg pid "$pane_id" '.result.panes[] | select(.pane_id == $pid)')
local agent
agent=$(echo "$pane_info" | jq -r '.agent // empty')
if [[ -z "$agent" ]]; then
echo "Error: 現在のペインでエージェントが検出できません (pane_id: ${pane_id})" >&2
exit 1
fi
local pane_cwd
pane_cwd=$(echo "$pane_info" | jq -r '.cwd // empty')
case "$agent" in
claude)
find_claude_plan
;;
copilot)
find_copilot_plan
;;
codex)
find_codex_plan "$pane_cwd"
;;
*)
echo "Error: 未対応のエージェント: ${agent}" >&2
exit 1
;;
esac
}
plan_file=$(get_plan_file "$@")
if [[ -z "$plan_file" ]]; then
echo "Error: planファイルのパスが空です" >&2
exit 1
fi
NEW_PANE=$(herdr pane split --direction right --focus | jq -r '.result.pane.pane_id')
herdr pane run "$NEW_PANE" "leaf --watch --editor 'nvim +{\$line} +\"setlocal ft=markdown\" +\"normal! zz\"' $(printf '%q' "$plan_file")"
```
## 使い方
### 1. `plan` コマンドを実行できるようにする
1. `plan.sh` に実行権限をつける
2. `plan -> plan.sh` の[[シンボリックリンク]]を貼る
3. `plan` に PATHを通す
### 2. `config.toml` に設定を追加する
[[herdr]]の設定ファイルに追加。`key`は好きなものでOK。
`config.toml`
```toml
[[keys.command]]
key = "prefix+p"
type = "shell"
command = "plan"
```
### 3. [[AIコーディングエージェント]]から実行
`config.toml` で設定したkeyを押すと、右側に分割されたペインに[[leaf]]のプランが表示される。
## 機能/サポート対象
#### サポートしている[[AIコーディングエージェント]]
| エージェント | ホットリロード |
| ---------------------- | --------- |
| [[Claude Code]] | ✅ **対応** |
| [[Codex CLI]] | ❌ **未対応** |
| [[GitHub Copilot CLI]] | ✅ **対応** |
#### 機能
- [[herdr]]のペインを右側に分割して開き、[[leaf]]でプランの[[Markdown]]ファイルを表示する
- `<C-e>` で [[Neovim]]による編集モードに切り替わる
- スクロール位置も可能な限り保持して切り替わる
- ホットリロード
- 元の[[Markdown]]ファイルが変更されたら自動で内容が更新される
- ただし、一部の[[AIコーディングエージェント]]は未対応
## 関連コミット
- [feat(plan): planコマンドを追加](https://github.com/tadashi-aikawa/toki/commit/edca743d13e4e8d6e312b70125c93433e0dd9bd7)