Panda Noir

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

vim.packを使ってlazy.nvimをインストールする

lazy.nvim自体のインストール処理は地味にめんどうです。

lazy.nvim自体をインストールする設定

15行もあるうえ、そこまでわかりやすいとも言えません。

これが、neovim0.12で導入された標準パッケージマネージャーのvim.packを使うとだいぶ簡潔になります。

vim.packでlazy.nvimをインストールする

vim.pack.add({
  { src = 'https://github.com/folke/lazy.nvim.git', version = 'stable' },
}, { load = true })

これだけ。設定の意図も明快ですっきり分かりやすいです。

…まあ、細かい部分で差異がある可能性はあるので、やる場合は自己責任で。今のところはちゃんと動いていそう。

neovim 0.12からはネイティブの補完で十分そう

neovim 0.11でLSP補完が有効になった。が、枠線がなかったりプレビューが不足していたり、実用するにはあと一歩足りてない感 があった。

0.11の補完画面

が、0.12でその辺に 強化が入った。

0.12の補完画面

見た目的にも結構変化がわかりやすい。

0.12で入った強化

neovim docsのNews-0.12に書いてあるこの辺りがポイント。

  • 補完アイテムのプレビュー
  • popup menu にボーダー追加

個人的に大きいのは1つ目。プレビュー(枠線部)があることで 圧倒的に使いやすくなった

2つ目も気分的にはだいぶアガる。枠線が何もないと見づらくて使いづらかったので、けっこう嬉しい。

(他にも色が表示されるようになったという細かい変更もあるが、個人的にはふーんって感じだ)

設定方法

基本はこれでOK。

vim.o.pumborder = 'rounded' -- ポップアップメニューに罫線を追加
vim.opt.completeopt = { 'menu', 'menuone', 'noselect', 'fuzzy', 'popup' } -- popupを入れると候補の説明がプレビューされる

-- LSPの補完を自動で有効化
vim.api.nvim_create_autocmd('LspAttach', {
  callback = function(ev)
    local client = vim.lsp.get_client_by_id(ev.data.client_id)
    if client and client:supports_method('textDocument/completion') then
      vim.lsp.completion.enable(true, client.id, ev.buf, { autotrigger = true })
    end
  end,
})

ただ、これだけだと候補説明ウィンドウにはボーダーがつかない。今はオプションが未整備なので、ハックを使って無理やり罫線をつける↓

-- HACK: ドキュメントポップアップに無理やりボーダーを付ける
-- 現状 winborder や completeopt=popup だけではドキュメントfloatのボーダーを制御できない
-- https://github.com/neovim/neovim/issues/38248
-- 将来的に completepopup オプション等が実装されればこのワークアラウンドは不要になる
local orig_complete_set = vim.api.nvim__complete_set
vim.api.nvim__complete_set = function(...)
  local result = orig_complete_set(...)
  if result and result.winid then
    pcall(vim.api.nvim_win_set_config, result.winid, { border = 'rounded' })
  end
  return result
end

(この手法は deathbeam/autocomplete.nvim や neovim/neovim#29225 でも使われているので、そこまでダーティではない)

実際の設定

まとめ: もうネイティブの補完でいいかも

0.11で使えるようになったLSP補完が0.12でいよいよ実用レベルになったといえる。特段こだわりがなければもうこれで十分そうだ。

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は進化していくが、プログラマの仕事は当分なくならなさそうだ。