Panda Noir

JavaScript の限界を究めるブログでした。最近はいろんな分野を幅広めに書いてます。

tmuxで起動しているclaudeを横断して監視する

./claude-ps で起動。

#!/usr/bin/env bash
set -euo pipefail

# claude-ps: Monitor Claude Code sessions running in tmux panes

HOME_DIR="$HOME"
WATCH_INTERVAL=1

# Colors
BOLD='\033[1m'
GREEN='\033[32m'
YELLOW='\033[33m'
DIM='\033[2m'
RESET='\033[0m'

usage() {
  echo "Usage: claude-ps [-w|--watch [INTERVAL]]"
  echo "  -w, --watch [SEC]  Watch mode (default: ${WATCH_INTERVAL}s interval)"
  exit 0
}

check_deps() {
  if ! command -v tmux &>/dev/null; then
    echo "error: tmux is not installed" >&2
    exit 1
  fi
  if ! tmux list-sessions &>/dev/null 2>&1; then
    echo "error: no tmux sessions found" >&2
    exit 1
  fi
}

parse_args() {
  watch_mode=false
  while [[ $# -gt 0 ]]; do
    case "$1" in
      -w|--watch)
        watch_mode=true
        if [[ "${2:-}" =~ ^[0-9]+$ ]]; then
          WATCH_INTERVAL="$2"
          shift
        fi
        shift
        ;;
      -h|--help)
        usage
        ;;
      *)
        echo "Unknown option: $1" >&2
        usage
        ;;
    esac
  done
}

is_claude_pane() {
  local cmd="$1" title="$2"
  [[ "$cmd" =~ ^[0-9]+\.[0-9]+\.[0-9]+ ]] || [[ "$title" == *"Claude Code"* ]]
}

# Detect status from pane_title icon
# ✳ (e29cb3) / ✻ (e29cbb) = idle, Braille spinner (e2a080-e2a3bf) = running
detect_claude_status() {
  local title="$1"
  local icon_hex
  icon_hex=$(echo -n "$title" | head -c 3 | xxd -p)

  if [[ "$icon_hex" == "e29cb3" || "$icon_hex" == "e29cbb" ]]; then
    echo "idle"
  elif [[ "$icon_hex" == e2a0* || "$icon_hex" == e2a1* || "$icon_hex" == e2a2* || "$icon_hex" == e2a3* ]]; then
    echo "running"
  else
    echo "unknown"
  fi
}

extract_task() {
  local title="$1"
  local task
  task=$(echo "$title" | sed $'s/^[\xe2\xa0\x80-\xe2\xa3\xbf\xe2\x9c\xb3\xe2\x9c\xbb\xe2\x8f\xb5\xe2\x8f\xb6\xe2\x8f\xb7\xe2\x8f\xb8] *//')
  task="${task#Claude Code}"
  task="${task# - }"
  if [[ ${#task} -gt 45 ]]; then
    task="${task:0:42}..."
  fi
  echo "$task"
}

# List Claude panes as pipe-delimited lines: dir|status|task
get_list_claude_panes() {
  while IFS='|' read -r _pane cmd dir title; do
    if ! is_claude_pane "$cmd" "$title"; then
      continue
    fi
    local status short_dir task
    status=$(detect_claude_status "$title")
    short_dir="$(basename "$dir")"
    task=$(extract_task "$title")
    echo "${status}|${task}|${short_dir}"
  done < <(tmux list-panes -a -F "#{session_name}:#{window_index}.#{pane_index}|#{pane_current_command}|#{pane_current_path}|#{pane_title}")
}

render() {
  # header
  printf "${BOLD}  %-45s %s${RESET}\n" "TASK" "DIR"
  printf "%s\n" "$(printf '%.0s─' {1..60})"

  # panes
  local found=0
  while IFS='|' read -r status task short_dir; do
    found=$((found + 1))
    local status_icon status_color
    case "$status" in
      running) status_icon=""; status_color="$GREEN" ;;
      idle)    status_icon=""; status_color="$YELLOW" ;;
      *)       status_icon="?"; status_color="$DIM" ;;
    esac
    printf "${status_color}%s${RESET}  %-45s ${DIM}%s${RESET}\n" \
      "$status_icon" "$task" "$short_dir"
  done < <(get_list_claude_panes)
  if [[ $found -eq 0 ]]; then
    echo "No Claude Code sessions found in tmux."
  fi

  # footer
  printf "\n${DIM}Updated: $(date '+%H:%M:%S')${RESET}"
}

watch_loop() {
  trap 'tput cnorm; tput clear; exit 0' INT TERM
  tput civis
  tput clear
  while true; do
    local buf
    buf=$(render)
    tput home
    printf "${DIM}claude-ps  (every ${WATCH_INTERVAL}s, Ctrl-C to stop)${RESET}\n\n"
    printf '%s' "$buf"
    tput ed
    sleep "$WATCH_INTERVAL"
  done
}

# --- Main ---

check_deps
parse_args "$@"

if $watch_mode; then
  watch_loop
else
  render
  echo
fi

ステート更新を伴わないなら useTransition を使うべきではない

※React公式の見解ではありません。公式ドキュメントの文言などをもとに推察した記事になります。

「useTransitionはどう使うべきものなのか?どう使ってはいけないのか?」を考察したのでまとめる。

useTransition は「ステートの遷移」を表現するべき

useTransitionは(複数の) ステートの遷移を扱うフック だ。

const [isPending, startTransition] = useTransition();

startTransition(() => {
  setSomeState(newValue);
});

someState が newValue へ遷移している途中なら isPending は true になる。

ステートの遷移 = 画面遷移

Reactは UI=f(state) という原則を持つ。なので、ステートの遷移=画面遷移 とも言い換えられる。stateが変化した場合、UIも(基本的に)変化するからだ。

これは逆に言えば、画面変化を伴わないなら useTransition のユースケース外 、ということでもある。

単なるローディング処理に使うべきでない

上の定義に従うと、例えばファイルダウンロードのような、最終的に画面が変化しないユースケースは トランジションに該当しない

// ↓こういう使い方はトランジションに該当しないため推奨されない
const [isDownloading, startTransition] = useTransition();

const download = () => startTransition(async() => {
  await downloadFile(); // state変化を伴っていない
});

この操作は「ある画面から別の画面に段階的に遷移」に該当していない。ダウンロードが完了したら元の状態に戻るからだ。

こういう時は単純に isLoading ステートを定義すれば十分 だ。

const [isDownloading, setIsDownloading] = useState(false);

const download = async() => {
  setIsDownloading(true);
  await downloadFile();
  setIsDownloading(false);
};

Q. コレって公式見解なの?

大事なことだがが、上に書いたのは 公式見解ではない。が、ドキュメントを読む限りは それを想定していると思われる

例えば useTransition – React の引数の項目にはこういう記述がある。

action: 1 つ以上の set 関数を呼び出して state を更新する関数。

また、React v18.0 - 新機能:トランジションにはこのように書かれている。

トランジションとは React における新たな概念であり、緊急性の高い更新 と高くない更新 を区別するためのものです。

  • トランジションによる更新は UI をある画面から別の画面に段階的に遷移させるものです。

ココからも、画面遷移を伴わなければトランジションと呼べないと思われる。

ただ、現時点ではlinterによる制約などもなく、画面遷移を含まない(set関数を呼び出さない)"トランジション"も書ける。しかし、今後のReactのアップデートによってset関数を1つ以上含んでいることを前提とした最適化が行われる可能性は全然ある。なので、今のうちにset関数を含まない場合はuseTransitionを使わないようにしておくほうがよさそうだ。

おわり

過去には単に「async関数の実行中ステート」のためにuseTransitionを使っていたが、改めてドキュメントを読んで整理してみて、今後は控えていこうと思った。みんなも気をつけよう。いや、気をつけなくても良いかもしれない。それは自分で決めよう。

claudeをこき使う、本当に

claude codeを使い始めて1ヶ月。こいつのポテンシャルの高さはまざまざと理解した。

  • コードベースで検索して的確に情報を集める
  • 一定水準のコーディング力 (人間にはまだ及ばない)
  • 適切な観点でのレビュー

つまり、1人のエンジニアを代替する程度のことは既にできてしまう。が、逆に言えば まだ1人のエンジニアを代替する程度の能力しかない のだ。単一のインスタンスだと。

claudeは既存のコーディングの延長で考えてはいけない。 つまり、我々が今すべきなのはclaudeのマルチ管理だ。

数人のclaudeをしばく

claudeは何人でも起動できる。なら遊ばせておくなんてもったいない。1タスクに2人claudeを投下したってよい のだ。1人には懸念点の洗い出しをやらせつつ実装も進めてレビューもしていくみたいなことだってできる。

こっちはひたすら次の指示を考え、それを打っている間に別のclaudeがタスクを完了し、指示を出したらその完了したclaudeに次の指示を出す。そういうゲームだ。

既存のコーディングの延長では戦えない

今までは自分でコーディングする必要があったので1つのタスクにかかりきりという感じだったが、claude codeによって完全に変わった。 我々は次々と指示を出し続けるのが仕事なのだ。 今までと同じ感覚で1つのタスクにclaudeをアサインしたから作業おしまい、なんてしてる暇はない。どんどんとclaudeを立ち上げ、次のインスタンスに指示を出し続ける必要がある。

最近だとgit worktreeも便利だ。「これをレビューしたいからworktreeを切ってそっちで作業して」みたいな指示を出せば作業用のディレクトリを汚さずに同時並行で作業ができてしまう。

ミーティング中に指示を出せないのがもどかしい

弊社の上長で「ミーティング中に指示を出せないとソワソワする」と言っていた人がいたが、完全に気持ちがわかる。 今の俺に課せられてるのは指示出しだから 。指示は一度出せば勝手に回ってくれる。なので遊ばせておく理由がないのだ。

完全自律駆動もさせられる

zenn.dev

今朝、こちらの記事を参考にしながらセットアップをした。この記事は 確認ダイアログなしでclaudeを自走させ続けるための設定方法 が書かれている。もちろん安全性には気をつけていて、サンドボックスを介してclaudeがアクセスできるファイルを制限している。

これを設定したことで、ますます俺の役割が指示出しへと傾いた。指示を出したらノンストップで成果物が上がってくる。確認フォームで止まることがなくなった。ますます加速していく一方だ。指示を出し、確認し、別のインスタンスに指示を出す。これをひたすら繰り返すだけの仕事。

終わり

今回話したことに近い機能として、エージェントチーム という機能もあるらしい。課長AIと作業員AIに分かれて、俺は課長AIに指示を出して課長AIが作業員に指示を出すという構成らしい。明日試してみる予定。

どんどんとAIは進化していくが、プログラマの仕事は当分なくならなさそうだ。

ブログに書くことなんもない

久しぶりに先月は全くのゼロ件だった。原因ははっきりしている。2月の頭からclaude codeを使い始めたから。

今までのブログネタは仕事中に見つけることが多かった

今まで、ブログのネタは仕事で手を動かしているときに見つけることが多かった。例えばこの辺。

こういうのは手を動かして調査してるときに「そう言えばどうなってるんだ?ちゃんと調べておくか…」と思って書くことが多い。が、claude codeなどを使うと基本的にそういう悩みにぶち当たることが少なくなった。さらっとわかりやすく解説してくれるし。

そもそも情報をまとめるモチベが消えた

今までは「情報源はあるけどわかりづらすぎるわ!!だったら俺がまとめ直してやる」というモチベが結構あった。でも今はAIが良い感じに整理してくれる。claude codeならコードベースを漁って情報を整理するみたいな芸当すらできる。 調査タスクはほぼ消えたと言っても過言でない。

自分用にチューニングしたまとめ記事が欲しい欲はブログの大きい動力源だったので、それがなくなったのは痛い。

アーキテクチャ論なども大幅な変更が迫られている

今までいくつかアーキテクチャ論とかそういう話を書いたりもしてきた。でも、それもモチベが薄れつつある。なぜなら AIによって今まで実質不可能だった開発手法が取れるようになってきたから。

例えばテストをしっかり拡充しておけば、現状の挙動を維持しつつ中身を丸ごとすり替えるってことだってできる。当然ながら人間がこれをやろうと思ったら工数が高くてとても現実的ではない。が、AIならできてしまう。つまり、リアーキテクチャをするコストが体感として以前の1/10程度にまで下がっている。

また、そもそも今までのアーキテクチャ論は人間のスペックを前提としたものだった。しかしAIの登場でその前提が崩れた。今はアーキテクチャ論自体が大きく変化を迫られており、新規のブログネタを悠長に探すようなフェーズでない。

AI関連の記事はそんなに書きたくない

AI関連の記事を書けば良くない?という話もあると思うが、単純につまらんのでしたくない。

まとめ

けっっっっっっっっっっっっこうやばい変化だなぁと思っている。今後もしばらくずっと同じ状況なんじゃなかろうか。ブログネタを仕事中に発見するのも難しい、情報をまとめるモチベも薄れた、アーキテクチャ論も書けない。でもどうしようもねえよなぁ…

which-key.nvimで覚えづらいキーバインドにアイコンを設定 & グルーピングする

アイコンを付け、操作内容でグルーピングするようにした。個人的にはこれ入れてから結構コマンドを打ちやすくなった。

実際の画面

<c-w>

z

g

設定

return {
  {
    'folke/which-key.nvim',
    opts = {
      sort = {
        -- descの先頭についている[xxx]を元にソート
        function(item)
          local group = item.desc:match('^%[(.*)%]')
          return group and '0:' .. group or '1:'
        end,
        'local', 'order', 'group', 'alphanum', 'mod',
      },
      spec = {
        -- ウィンドウ関連のキーマップ
        { '<c-w>h', desc = '[focus] Go to the left window', icon = '󰜱', },
        { '<c-w>j', desc = '[focus] Go to the down window', icon = '󰜮', },
        { '<c-w>k', desc = '[focus] Go to the up window', icon = '󰜷', },
        { '<c-w>l', desc = '[focus] Go to the right window', icon = '󰜴', },
        { '<c-w>w', desc = '[focus] Switch windows', icon = '', },
        { '<c-w>H', desc = '[move] Move window to far left', icon = { icon = '', color = 'green' }, },
        { '<c-w>J', desc = '[move] Move window to far bottom', icon = { icon = '', color = 'green' }, },
        { '<c-w>K', desc = '[move] Move window to far top', icon = { icon = '', color = 'green' }, },
        { '<c-w>L', desc = '[move] Move window to far right', icon = { icon = '', color = 'green' }, },
        { '<c-w>s', desc = '[split] Split window', icon = { icon = '󰤻', color = 'yellow' }, },
        { '<c-w>v', desc = '[split] Split window vertically', icon = { icon = '󰤼', color = 'yellow' }, },
        { '<c-w>q', desc = '[close] Quit a window', icon = { icon = '󰅖', color = 'red' }, },
        { '<c-w>o', desc = '[close] Close all other windows', icon = { icon = '󰅗', color = 'red' }, },
        { '<c-w>+', desc = '[size-h] Increase height', icon = { icon = '󰡏', color = 'purple' }, },
        { '<c-w>-', desc = '[size-h] Decrease height', icon = { icon = '󰡍', color = 'purple' }, },
        { '<c-w>_', desc = '[size-h] Max out the height', icon = { icon = '', color = 'purple' }, },
        { '<c-w>>', desc = '[size-w] Increase width', icon = { icon = '󰡎', color = 'purple' }, },
        { '<c-w><', desc = '[size-w] Decrease width', icon = { icon = '󰡌', color = 'purple' }, },
        { '<c-w>|', desc = '[size-w] Max out the width', icon = { icon = '', color = 'purple' }, },
        { '<c-w>T', desc = 'Break out into a new tab', icon = '󰓩', },
        { '<c-w>x', desc = 'Swap current with next', icon = '󰓡', },
        { '<c-w>d', desc = 'Show diagnostics under the cursor', icon = '', },
        { '<c-w>=', desc = 'Equally high and wide', icon = '', },

        -- z系
        { 'za', desc = '[fold] Toggle fold under cursor', icon = { icon = '', color = 'yellow' } },
        { 'zA', desc = '[fold] Toggle all folds under cursor', icon = { icon = '', color = 'yellow' } },
        { 'zi', desc = '[fold] Toggle folding', icon = { icon = '', color = 'yellow' } },
        { 'zc', desc = '[fold] Close fold under cursor', icon = { icon = '󰘕', color = 'red' } },
        { 'zo', desc = '[fold] Open fold under cursor', icon = { icon = '󰘖', color = 'cyan' } },
        { 'zd', desc = '[fold] Delete fold under cursor', icon = { icon = '󰁮', color = 'gray' } },
        { 'zf', desc = '[fold] Create fold', icon = { icon = '󰐕', color = 'gray' } },
        { 'zC', desc = '[fold] Close all folds under cursor', icon = { icon = '󰘕', color = 'red' } },
        { 'zO', desc = '[fold] Open all folds under cursor', icon = { icon = '󰘖', color = 'cyan' } },
        { 'zD', desc = '[fold] Delete all folds under cursor', icon = { icon = '󰁮', color = 'gray' } },
        { 'zE', desc = '[fold] Delete all folds in file', icon = { icon = '󰁮', color = 'gray' } },
        { 'zR', desc = '[foldlevel] Open all folds (set foldlevel=max)', icon = { icon = '󰘖', color = 'cyan' } },
        { 'zM', desc = '[foldlevel] Close all folds (set foldlevel=0)', icon = { icon = '󰘕', color = 'red' } },
        { 'zr', desc = '[foldlevel] Fold less (foldlevel +1)', icon = { icon = '󰝡', color = 'cyan' } },
        { 'zm', desc = '[foldlevel] Fold more (foldlevel -1)', icon = { icon = '󰝠', color = 'red' } },
        { 'zx', desc = '[foldlevel] Reapply folds (reset to foldlevel)', icon = { icon = '󰑐', color = 'yellow' } },
        { 'zb', desc = '[cursor] Bottom this line', icon = { icon = '󰝓', color = 'purple' } },
        { 'zt', desc = '[cursor] Top this line', icon = { icon = '󰝕', color = 'purple' } },
        { 'zz', desc = '[cursor] Center this line', icon = { icon = '󰝔', color = 'purple' } },

        -- g系
        { 'ga', icon = '󰊄', group = '[case] converting text case' },
        { 'gd', icon = '󰅬', desc = '[lsp] go to definition' },
        { 'gg', icon = '󰘀', desc = 'First line' },
        { 'gO', icon = '󰅬', desc = '[lsp] Lists all symbols in current buffer in location-list' },
        { 'gp', icon = '󰅬', desc = '[lsp] peek definition' },
        { 'gr', icon = '󰅬', group = '[lsp] LSP' },
        { 'gt', icon = '󰅬', desc = '[lsp] go to type definition' },
        { 'gu', icon = '󰊄', desc = '[case] Lowercase' },
        { 'gU', icon = '󰊄', desc = '[case] Uppercase' },
        { 'gv', icon = '󰒉', desc = 'Last visual selection' },
      },
    },
  },
}