開発claude-code-tools

Windows Terminal 自動フォーカス実装ガイド

概要

Claude Code の notification フックが発火したとき、Windows Terminal を自動的に最前面に持ってきて、該当ペインにフォーカスを当てる仕組みを実装する。

ゴール

  • 別アプリ(Chrome等)を操作中でも、通知が来たらWindows Terminalが自動で最前面に来る
  • 4分割ペインのうち、通知を出したペインに自動フォーカスが当たる
  • あとはEnterを押すだけで操作を続行できる

前提環境

  • Windows Terminal(4分割ペイン: 左上・右上・左下・右下)
  • Claude Code セッションを各ペインで非同期実行
  • 既存の音声通知フックが動作済み
  • PowerShell 7.5+

アーキテクチャ

Claude Code (notification hook)
  ├── 既存: 音声通知スクリプト (そのまま)
  └── 新規: auto-focus.ps1
        ├── 環境変数 $env:PANE_POS で自分の位置を判定
        ├── Win32 API (SetForegroundWindow) でターミナルを最前面化
        └── SendKeys (Alt+矢印) で該当ペインにフォーカス移動

実装手順

Step 1: 自動フォーカススクリプトの作成

以下のファイルを作成する。

ファイルパス: ~/.claude/scripts/auto-focus.ps1

# auto-focus.ps1
# Claude Code notification フックから呼び出される
# Windows Terminal を最前面にし、該当ペインにフォーカスを当てる

# --- Win32 API 定義 ---
Add-Type @"
using System;
using System.Runtime.InteropServices;
public class Win32Focus {
    [DllImport("user32.dll")]
    public static extern bool SetForegroundWindow(IntPtr hWnd);

    [DllImport("user32.dll")]
    public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);

    [DllImport("user32.dll")]
    public static extern IntPtr GetForegroundWindow();

    [DllImport("user32.dll")]
    public static extern void keybd_event(byte bVk, byte bScan, uint dwFlags, UIntPtr dwExtraInfo);

    [DllImport("user32.dll")]
    public static extern bool IsIconic(IntPtr hWnd);
}
"@

# --- 定数 ---
$SW_RESTORE = 9
$KEYEVENTF_KEYUP = 0x0002
$VK_MENU = 0x12  # Alt キー

# --- Windows Terminal のウィンドウハンドルを取得 ---
$wtProcess = Get-Process -Name "WindowsTerminal" -ErrorAction SilentlyContinue |
    Select-Object -First 1

if (-not $wtProcess) {
    Write-Error "Windows Terminal が見つかりません"
    exit 1
}

$hwnd = $wtProcess.MainWindowHandle

if ($hwnd -eq [IntPtr]::Zero) {
    Write-Error "Windows Terminal のウィンドウハンドルを取得できません"
    exit 1
}

# --- 最小化されていたら復元 ---
if ([Win32Focus]::IsIconic($hwnd)) {
    [Win32Focus]::ShowWindow($hwnd, $SW_RESTORE)
}

# --- フォアグラウンドロック回避 ---
# バックグラウンドプロセスから SetForegroundWindow を呼ぶには
# Alt キーをシミュレートしてロックを解除する必要がある
[Win32Focus]::keybd_event($VK_MENU, 0, 0, [UIntPtr]::Zero)          # Alt down
[Win32Focus]::keybd_event($VK_MENU, 0, $KEYEVENTF_KEYUP, [UIntPtr]::Zero)  # Alt up

# --- Windows Terminal を最前面に ---
[Win32Focus]::SetForegroundWindow($hwnd)

# 少し待ってフォーカスが安定するのを待つ
Start-Sleep -Milliseconds 200

# --- ペインフォーカス移動 ---
# 環境変数 PANE_POS に基づいて対象ペインにフォーカスを移す
# 戦略: まず確実に左上に移動(←↑連打)してから目的のペインへ移動
$panePos = $env:PANE_POS
if (-not $panePos) {
    # PANE_POS 未設定の場合はウィンドウ最前面化のみで終了
    Write-Host "PANE_POS が未設定のため、ウィンドウ最前面化のみ実行しました"
    exit 0
}

# WScript.Shell の SendKeys を使って Alt+矢印 を送信
$wsh = New-Object -ComObject WScript.Shell

# まず左上にリセット(最大2回ずつ送れば確実に左上に到達する)
Start-Sleep -Milliseconds 100
$wsh.SendKeys("%{LEFT}")   # Alt+Left
Start-Sleep -Milliseconds 50
$wsh.SendKeys("%{LEFT}")   # Alt+Left
Start-Sleep -Milliseconds 50
$wsh.SendKeys("%{UP}")     # Alt+Up
Start-Sleep -Milliseconds 50
$wsh.SendKeys("%{UP}")     # Alt+Up
Start-Sleep -Milliseconds 50

# 目的のペインに移動
switch ($panePos) {
    "top-left" {
        # 既に左上にいるので何もしない
    }
    "top-right" {
        $wsh.SendKeys("%{RIGHT}")  # Alt+Right
    }
    "bottom-left" {
        $wsh.SendKeys("%{DOWN}")   # Alt+Down
    }
    "bottom-right" {
        $wsh.SendKeys("%{RIGHT}")  # Alt+Right
        Start-Sleep -Milliseconds 50
        $wsh.SendKeys("%{DOWN}")   # Alt+Down
    }
    default {
        Write-Warning "不明な PANE_POS: $panePos (top-left / top-right / bottom-left / bottom-right を指定してください)"
    }
}

Write-Host "フォーカス移動完了: $panePos"

Step 2: notification フックの設定

.claude/hooks.json (グローバル設定)に以下を追加する。既存の音声通知フックとは別エントリで追加すること。

既存の hooks.json に追記するパターン:

{
  "hooks": {
    "notification": [
      // ... 既存の音声通知フックはそのまま残す ...
      {
        "command": "pwsh -NoProfile -File \"$HOME/.claude/scripts/auto-focus.ps1\"",
        "description": "通知時にWindows Terminalを最前面にし、該当ペインにフォーカスを当てる"
      }
    ]
  }
}

注意: hooks.json の正確な書式は Claude Code のバージョンに依存する。/hooks コマンドで既存の設定を確認し、それに合わせて追記すること。

Step 3: 各ペインで環境変数を設定

各ペインの起動時に、ペイン位置を示す環境変数をセットする。

方法A: 手動設定(シンプル)

各ペインで Claude Code を起動する前に以下を実行:

# 左上ペイン
$env:PANE_POS = "top-left"

# 右上ペイン
$env:PANE_POS = "top-right"

# 左下ペイン
$env:PANE_POS = "bottom-left"

# 右下ペイン
$env:PANE_POS = "bottom-right"

方法B: 起動スクリプトで自動化

以下のスクリプトで4ペインを一括起動し、環境変数も自動セットする。

ファイルパス: ~/.claude/scripts/launch-4pane.ps1

# launch-4pane.ps1
# Windows Terminal で4分割ペインを立ち上げ、各ペインに PANE_POS を設定する
#
# 使い方: pwsh -File ~/.claude/scripts/launch-4pane.ps1
#
# 必要に応じてプロジェクトパスを変更すること

param(
    [string]$Project1 = ".",
    [string]$Project2 = ".",
    [string]$Project3 = ".",
    [string]$Project4 = "."
)

# Windows Terminal の `wt` コマンドで4ペイン起動
# セミコロン区切りで split-pane を連結する
wt new-tab --title "CC:top-left" `
    pwsh -NoExit -Command "`$env:PANE_POS='top-left'; cd '$Project1'" `; `
  split-pane --horizontal --title "CC:bottom-left" `
    pwsh -NoExit -Command "`$env:PANE_POS='bottom-left'; cd '$Project3'" `; `
  move-focus up `; `
  split-pane --vertical --title "CC:top-right" `
    pwsh -NoExit -Command "`$env:PANE_POS='top-right'; cd '$Project2'" `; `
  move-focus down `; `
  split-pane --vertical --title "CC:bottom-right" `
    pwsh -NoExit -Command "`$env:PANE_POS='bottom-right'; cd '$Project4'"

Write-Host "4ペイン起動完了。各ペインで claude を起動してください。"

使用例:

# 4つのプロジェクトを指定して起動
pwsh -File ~/.claude/scripts/launch-4pane.ps1 `
    -Project1 "C:\Users\numbe\Git_repo\mdx-playground" `
    -Project2 "C:\Users\numbe\Git_repo\chrome-extension-MF" `
    -Project3 "C:\Users\numbe\Git_repo\project3" `
    -Project4 "C:\Users\numbe\Git_repo\project4"

動作確認手順

1. スクリプト単体テスト

# 左上ペインにいると仮定してテスト
$env:PANE_POS = "top-left"

# Chrome など別ウィンドウにフォーカスを移してから実行
Start-Sleep -Seconds 3  # 3秒以内に別ウィンドウをクリック
pwsh -NoProfile -File "$HOME/.claude/scripts/auto-focus.ps1"
# → Windows Terminal が最前面に来て左上ペインにフォーカスが当たれば成功

2. フック経由テスト

# Claude Code セッション内で長時間かかるタスクを依頼し、
# 別ウィンドウに切り替えて待機する
# → タスク完了時に音声通知と同時にターミナルが最前面に来れば成功

トラブルシューティング

Windows Terminal が最前面に来ない

原因: Windows のフォアグラウンドロック制約。Alt キーシミュレートが効いていない可能性。

対策:

  • スクリプト内の Start-Sleep の値を増やす(200ms → 500ms)
  • PowerShell を管理者権限で実行してみる
  • それでもダメな場合は FlashWindowEx(タスクバー点滅)にフォールバックする:
# auto-focus.ps1 の SetForegroundWindow 後に追加
Add-Type @"
using System;
using System.Runtime.InteropServices;
public class FlashHelper {
    [StructLayout(LayoutKind.Sequential)]
    public struct FLASHWINFO {
        public uint cbSize;
        public IntPtr hwnd;
        public uint dwFlags;
        public uint uCount;
        public uint dwTimeout;
    }
    [DllImport("user32.dll")]
    public static extern bool FlashWindowEx(ref FLASHWINFO pwfi);
}
"@

$flash = New-Object FlashHelper+FLASHWINFO
$flash.cbSize = [System.Runtime.InteropServices.Marshal]::SizeOf($flash)
$flash.hwnd = $hwnd
$flash.dwFlags = 0x0003  # FLASHW_ALL (タスクバー + タイトルバー)
$flash.uCount = 5
$flash.dwTimeout = 0
[FlashHelper]::FlashWindowEx([ref]$flash)

ペインフォーカスが正しく移動しない

原因: Windows Terminal のペインレイアウトがスクリプトの想定と異なる。

対策:

  • Alt+矢印 のデフォルトキーバインドが変更されていないか確認
  • Windows Terminal の settings.json で以下が設定されているか確認:
{
    "actions": [
        { "command": { "action": "moveFocus", "direction": "left" }, "keys": "alt+left" },
        { "command": { "action": "moveFocus", "direction": "right" }, "keys": "alt+right" },
        { "command": { "action": "moveFocus", "direction": "up" }, "keys": "alt+up" },
        { "command": { "action": "moveFocus", "direction": "down" }, "keys": "alt+down" }
    ]
}

PANE_POS が引き継がれない

原因: Claude Code がサブシェルで起動し、環境変数が引き継がれていない。

対策:

  • claude コマンド起動前に同じシェルで $env:PANE_POS をセットしているか確認
  • 起動スクリプト(方法B)を使う場合は -NoExit フラグが付いているか確認

ファイル構成

~/.claude/
├── hooks.json              # フック設定(既存に追記)
└── scripts/
    ├── auto-focus.ps1      # 自動フォーカススクリプト(新規)
    └── launch-4pane.ps1    # 4ペイン一括起動スクリプト(任意)

補足: 今後の拡張案

  • Stream Deck 連携: 各ボタンに Alt+矢印 キーストロークを割り当て、手動でのペイン切り替えも高速化
  • トースト通知との併用: BurntToast PowerShell モジュールを使い、どのプロジェクトで完了したかをWindowsトースト通知で表示する(自動フォーカスが不要な場面用)
  • 完了ペインの視覚的ハイライト: Windows Terminal の SetTabColor 等で、完了したペインのタブ色を変更する