#!/usr/bin/env bash
# cc-tmux: 全フック共通エントリ(T007/T012/T013/T014/T017/T020/T022)。
# 参照: contracts/hooks.md
#
# Claude Code の各フックから呼ばれる。stdin の JSON を jq で解析し、
# hook_event_name で分岐する。tmux 外($TMUX 無し)は即 no-op。常に exit 0。
#
# 登録イベント: SessionStart / UserPromptSubmit / PreToolUse / PreCompact / Stop / SessionEnd
set -u
# このスクリプトの位置から cc-tmux ルートを解決。
CC_DIR=$(cd "$(dirname "$0")/.." && pwd)
# ライブラリ読み込み。
# shellcheck source=../lib/state.sh
. "$CC_DIR/lib/state.sh"
# shellcheck source=../lib/tmux.sh
. "$CC_DIR/lib/tmux.sh"
# name.sh は US3 用(存在すれば名前連動を有効化)。
if [ -f "$CC_DIR/lib/name.sh" ]; then
# shellcheck source=../lib/name.sh
. "$CC_DIR/lib/name.sh"
fi
# 失敗してもターンを妨げない: 何が起きても最後は exit 0。
log_err() { printf 'cc-tmux-hook: %s\n' "$*" >&2; }
main() {
# tmux 外なら US1/US3 は無効(FR-010/042)。stdin は読み捨てる。
if ! cc_in_tmux; then
cat >/dev/null 2>&1 || true
return 0
fi
# jq が無ければ何もできない(依存不足)。安全に no-op。
command -v jq >/dev/null 2>&1 || { cat >/dev/null 2>&1 || true; return 0; }
# stdin JSON を一括取得。
_input=$(cat 2>/dev/null) || _input=""
[ -n "$_input" ] || return 0
# 主要フィールド抽出(欠落は空文字に)。
_event=$(printf '%s' "$_input" | jq -r '.hook_event_name // empty' 2>/dev/null)
_sid=$(printf '%s' "$_input" | jq -r '.session_id // empty' 2>/dev/null)
_cwd=$(printf '%s' "$_input" | jq -r '.cwd // empty' 2>/dev/null)
_transcript=$(printf '%s' "$_input" | jq -r '.transcript_path // empty' 2>/dev/null)
# SessionStart の種別(startup|resume|clear|compact)。
_source=$(printf '%s' "$_input" | jq -r '.source // .matcher // empty' 2>/dev/null)
# PreCompact の契機(manual=/compact コマンド | auto=自動コンパクション)。
_trigger=$(printf '%s' "$_input" | jq -r '.trigger // empty' 2>/dev/null)
[ -n "$_event" ] || return 0
[ -n "$_sid" ] || return 0
# window_id を解決(できなければ表示系は無効)。
_win=$(cc_tmux_window_id "${TMUX_PANE:-}") || _win=""
[ -n "$_win" ] || return 0
case "$_event" in
SessionStart)
handle_session_start ;;
UserPromptSubmit|PreToolUse)
handle_busy ;;
PreCompact)
handle_precompact ;;
Stop)
handle_idle ;;
SessionEnd)
handle_session_end ;;
*)
: ;; # 未知イベントは無視
esac
return 0
}
# busy へ遷移し、スピナーを起動(FR-001/002/003/011)。冪等。
handle_busy() {
ensure_display
state_init "$_sid"
state_meta_set "$_sid" tmux_pane "${TMUX_PANE:-}"
state_meta_set "$_sid" window_id "$_win"
[ -n "$_cwd" ] && state_meta_set "$_sid" cwd "$_cwd"
state_set "$_sid" busy
cc_tmux_set_state "$_win" busy
start_spinner
start_namewatch # 落ちていたら再起動(冪等, T044)
cc_tmux_refresh
}
# PreCompact: busy 表示にしつつ契機(manual|auto)を記録する。
# 手動 /compact はターン外で走り完了後に Stop が来ないため、
# compact 明けの SessionStart(source=compact) で idle へ戻す判断材料にする。
handle_precompact() {
handle_busy
[ -n "$_trigger" ] && state_meta_set "$_sid" compact_trigger "$_trigger"
}
# idle へ遷移し、スピナーを停止・@cc_spin クリア(FR-004/008/009)。
handle_idle() {
state_set "$_sid" idle
cc_tmux_set_state "$_win" idle
stop_spinner
cc_tmux_clear_spin "$_win"
# US3: /rename ベストエフォート追従(アイドル中に現在名を再確認, FR-041)。
# 即時反映はウォッチャ(cc-tmux-namewatch)が担い、ここは取りこぼし補完。
follow_name
cc_tmux_refresh
}
# US3: /rename・セッション名追従。transcript の customTitle(ユーザーの明示名)を
# 読めた場合、変化時に反映する。明示名は session 名が正(FR-043)なので
# ロック(automatic-rename off)を貫通して適用する。自動導出名(derive 側)は
# 従来どおりロックを尊重する。読めない/形式不一致は静かに諦める(FR-041)。
follow_name() {
command -v name_follow >/dev/null 2>&1 || return 0
_cur=$(name_follow "$_sid" "$_transcript" 2>/dev/null) || return 0
[ -n "$_cur" ] || return 0
_prev=$(state_meta "$_sid" name 2>/dev/null) || _prev=""
if [ "$_cur" != "$_prev" ]; then
state_meta_set "$_sid" name "$_cur"
cc_tmux_rename_force "$_win" "$_cur"
fi
}
# tmux 表示フォーマットを遅延適用(プラグイン単体で完結させる)。
# tmux サーバごとに一度だけ(@cc_tmux_fmt センチネルでガード)。
# tmux.conf を編集していなくてもプラグイン導入だけで色+スピナー表示が有効になる。
ensure_display() {
cc_in_tmux || return 0
[ "$(tmux show-options -gv @cc_tmux_fmt 2>/dev/null)" = "1" ] && return 0
if [ -f "$CC_DIR/tmux/cc-tmux.tmux" ]; then
tmux source-file "$CC_DIR/tmux/cc-tmux.tmux" 2>/dev/null
fi
tmux set-option -g @cc_tmux_fmt 1 2>/dev/null
}
# SessionStart: 初期化と名前導出。compact は busy 維持(FR-011 取りこぼし防止)。
handle_session_start() {
ensure_display
state_init "$_sid"
state_meta_set "$_sid" tmux_pane "${TMUX_PANE:-}"
state_meta_set "$_sid" window_id "$_win"
[ -n "$_cwd" ] && state_meta_set "$_sid" cwd "$_cwd"
[ -n "$_transcript" ] && state_meta_set "$_sid" transcript "$_transcript"
# 同一 pane の孤児(クラッシュ後)を掃除。
state_cleanup_stale_pane "${TMUX_PANE:-}" "$_sid"
if [ "$_source" = "compact" ]; then
# コンパクション明け: 契機で分岐する。
# manual(/compact コマンド): この後 Stop が来ないため idle へ戻す。
# auto(ターン中の自動圧縮): busy を維持(ターン末の Stop で戻る, FR-011)。
_ct=$(state_meta "$_sid" compact_trigger 2>/dev/null) || _ct=""
state_meta_set "$_sid" compact_trigger ""
if [ "$_ct" = "manual" ]; then
state_set "$_sid" idle
cc_tmux_set_state "$_win" idle
stop_spinner
cc_tmux_clear_spin "$_win"
elif [ "$(state_get "$_sid" 2>/dev/null)" = "busy" ]; then
cc_tmux_set_state "$_win" busy
start_spinner
fi
cc_tmux_refresh
return 0
fi
# 通常起動(startup/resume/clear): idle で初期化。
state_set "$_sid" idle
cc_tmux_set_state "$_win" idle
cc_tmux_clear_spin "$_win"
# US3: 名前導出(name.sh があれば)。
derive_and_apply_name
# US3: /rename 即時追従ウォッチャを起動(T044, FR-041 強化)。
start_namewatch
cc_tmux_refresh
}
# SessionEnd: 表示クリア・状態破棄(FR-060 復元の一部)。
handle_session_end() {
stop_spinner
stop_namewatch
cc_tmux_clear_all "$_win"
state_cleanup "$_sid"
cc_tmux_refresh
}
# スピナー起動(多重防止は spinner 側 pidfile でも担保)。
start_spinner() {
_spin_bin="$CC_DIR/bin/cc-tmux-spinner"
[ -x "$_spin_bin" ] || return 0
_pidf="$(cc_state_dir "$_sid")/spinner.pid"
if [ -f "$_pidf" ]; then
_p=$(cat "$_pidf" 2>/dev/null)
if [ -n "$_p" ] && kill -0 "$_p" 2>/dev/null; then
return 0 # 既に稼働中
fi
fi
# フックより長生きさせるため完全にデタッチ(フック終了の SIGHUP で死なせない)。
if command -v setsid >/dev/null 2>&1; then
setsid "$_spin_bin" "$_sid" "$_win" >/dev/null 2>&1 </dev/null &
else
nohup "$_spin_bin" "$_sid" "$_win" >/dev/null 2>&1 </dev/null &
fi
# bash の場合はジョブ管理から外す(SIGHUP 連鎖防止)。
disown 2>/dev/null || true
return 0
}
# スピナー停止。
stop_spinner() {
_pidf="$(cc_state_dir "$_sid")/spinner.pid"
if [ -f "$_pidf" ]; then
_p=$(cat "$_pidf" 2>/dev/null)
[ -n "$_p" ] && kill "$_p" 2>/dev/null
rm -f "$_pidf" 2>/dev/null
fi
}
# /rename 即時追従ウォッチャ起動(T044)。スピナーと同じデタッチ+pidfile 方式。冪等。
start_namewatch() {
[ -n "$_transcript" ] || return 0
_nw_bin="$CC_DIR/bin/cc-tmux-namewatch"
[ -x "$_nw_bin" ] || return 0
_nw_pidf="$(cc_state_dir "$_sid")/namewatch.pid"
if [ -f "$_nw_pidf" ]; then
_p=$(cat "$_nw_pidf" 2>/dev/null)
if [ -n "$_p" ] && kill -0 "$_p" 2>/dev/null; then
return 0 # 既に稼働中
fi
fi
if command -v setsid >/dev/null 2>&1; then
setsid "$_nw_bin" "$_sid" "$_win" "$_transcript" >/dev/null 2>&1 </dev/null &
else
nohup "$_nw_bin" "$_sid" "$_win" "$_transcript" >/dev/null 2>&1 </dev/null &
fi
disown 2>/dev/null || true
return 0
}
# ウォッチャ停止。
stop_namewatch() {
_nw_pidf="$(cc_state_dir "$_sid")/namewatch.pid"
if [ -f "$_nw_pidf" ]; then
_p=$(cat "$_nw_pidf" 2>/dev/null)
[ -n "$_p" ] && kill "$_p" 2>/dev/null
rm -f "$_nw_pidf" 2>/dev/null
fi
}
# US3: 名前導出して未ロック時のみ適用(name.sh が無ければ no-op)。
derive_and_apply_name() {
command -v name_derive >/dev/null 2>&1 || return 0
# 既にロック(ユーザー設定名)なら何もしない(FR-044)。
if cc_tmux_name_locked "$_win"; then
state_meta_set "$_sid" name_locked 1
return 0
fi
state_meta_set "$_sid" name_locked 0
_name=$(name_derive "$_sid" "$_cwd" "$_transcript" 2>/dev/null) || _name=""
if [ -n "$_name" ]; then
state_meta_set "$_sid" name "$_name"
cc_tmux_rename "$_win" "$_name"
fi
}
main
exit 0