chore: import existing iTerm2 Claude panel code
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
0d2380d36d
1
.claude/code-paths
Normal file
1
.claude/code-paths
Normal file
@ -0,0 +1 @@
|
||||
files/
|
||||
54
README.md
Normal file
54
README.md
Normal file
@ -0,0 +1,54 @@
|
||||
# Claude — панель для iTerm2 Toolbelt
|
||||
|
||||
Сверху — дерево активных сессий Claude Code и их субагентов (модель, контекст, worktree),
|
||||
снизу — лимиты подписки и баланс extra usage.
|
||||
|
||||
## Установка
|
||||
|
||||
```bash
|
||||
tar xzf claude-panel.tar.gz
|
||||
cd claude-panel
|
||||
./install.sh
|
||||
```
|
||||
|
||||
Затем в iTerm2: **View → Toolbelt → Show Toolbelt**, **Toolbelt → Claude**, и перезапустите
|
||||
открытые сессии `claude`. Повторный запуск `install.sh` обновляет установленное.
|
||||
|
||||
Нужно: macOS, iTerm2, python3, jq (`brew install jq`, в macOS 15+ уже есть).
|
||||
|
||||
## Что ставится
|
||||
|
||||
| Что | Куда |
|
||||
|---|---|
|
||||
| скрипт панели | `~/Library/Application Support/iTerm2/Scripts/AutoLaunch/claude_agents.py` |
|
||||
| хук Claude Code | `~/.claude/hooks/agents_panel_hook.sh` + записи в `~/.claude/settings.json` |
|
||||
| пример настроек | `~/.config/claude-agents/config.example.json` |
|
||||
|
||||
Перед изменением `settings.json` и заменой файлов старые копии кладутся в
|
||||
`~/.claude-panel-backup/<дата>/`. Ваши остальные хуки и настройки не трогаются.
|
||||
|
||||
## Работа с панелью
|
||||
|
||||
- клик по сессии или агенту — последние сообщения (копятся, пока окно открыто);
|
||||
- ⌘-клик по сессии — перейти в её вкладку;
|
||||
- клик по `⎇ папка` — открыть в ней `lf` в соседней панели;
|
||||
- цвет точки: зелёный — работает, жёлтый — ждёт вас, серый — свободен;
|
||||
- сессии внутри tmux (и `tmux -CC`) тоже видны: клик по `⎇` делит панель tmux, ⌘-клик переключает на неё;
|
||||
- цвет токенов: жёлтый > 500k, красный > 700k.
|
||||
|
||||
## Настройки
|
||||
|
||||
Скопируйте `config.example.json` в `~/.config/claude-agents/config.json` и оставьте только
|
||||
нужные ключи. Меняется на лету, кроме `port`.
|
||||
|
||||
## Если что-то не так
|
||||
|
||||
`http://127.0.0.1:8766/raw` — что панель видит: логи хука, причины скрытия сессий,
|
||||
ошибки лимитов и баланса.
|
||||
|
||||
## Удаление
|
||||
|
||||
```bash
|
||||
./uninstall.sh # настройки и логи остаются
|
||||
./uninstall.sh --purge # удалить всё
|
||||
```
|
||||
61
files/agents_panel_hook.sh
Executable file
61
files/agents_panel_hook.sh
Executable file
@ -0,0 +1,61 @@
|
||||
#!/bin/sh
|
||||
# Хук Claude Code для панели «Claude» в iTerm2.
|
||||
# Пишет события сессий и субагентов в ~/.cache/claude-agents/<session_id>.jsonl
|
||||
# вместе с тем, где запущена сессия: PID процесса claude, вкладка iTerm2 или панель tmux.
|
||||
# Нужен jq (в macOS 15+ встроен, иначе: brew install jq).
|
||||
# Никогда не мешает Claude Code: ничего не выводит и всегда выходит с кодом 0.
|
||||
|
||||
dir="$HOME/.cache/claude-agents"
|
||||
input=$(cat)
|
||||
|
||||
sid=$(printf '%s' "$input" | jq -r '.session_id // empty' 2>/dev/null | tr -cd 'A-Za-z0-9_-')
|
||||
[ -n "$sid" ] || exit 0
|
||||
mkdir -p "$dir" 2>/dev/null || exit 0
|
||||
|
||||
# PID самого claude: идём вверх по родителям, пропуская оболочки, которыми запущен хук
|
||||
pid=$PPID
|
||||
comm=""
|
||||
i=0
|
||||
while [ "$i" -lt 4 ] && [ "${pid:-1}" -gt 1 ]; do
|
||||
comm=$(ps -o comm= -p "$pid" 2>/dev/null | sed 's#.*/##; s/^-//')
|
||||
case "$comm" in
|
||||
sh|bash|zsh|dash|fish) pid=$(ps -o ppid= -p "$pid" 2>/dev/null | tr -d ' ') ;;
|
||||
*) break ;;
|
||||
esac
|
||||
i=$((i + 1))
|
||||
done
|
||||
|
||||
# Внутри tmux ITERM_SESSION_ID унаследован от сервера tmux и указывает не туда — не пишем его
|
||||
iterm="${ITERM_SESSION_ID:-}"
|
||||
tmux_socket=""
|
||||
tmux_pane=""
|
||||
if [ -n "${TMUX:-}" ] && [ -n "${TMUX_PANE:-}" ]; then
|
||||
iterm=""
|
||||
tmux_socket="${TMUX%%,*}"
|
||||
tmux_pane="$TMUX_PANE"
|
||||
fi
|
||||
|
||||
printf '%s' "$input" | jq -c --arg iterm "$iterm" --arg pid "${pid:-}" --arg pid_comm "$comm" \
|
||||
--arg tmux_socket "$tmux_socket" --arg tmux_pane "$tmux_pane" '
|
||||
def cut: if type == "string" then .[0:300] else . end;
|
||||
{
|
||||
hook_event_name, session_id, transcript_path, cwd, agent_id, agent_type,
|
||||
model, session_title, source, from_model, to_model, tool_name, tool_use_id,
|
||||
notification_type, agent_transcript_path, reason,
|
||||
message: (.message | cut),
|
||||
description: (.description | cut),
|
||||
last_assistant_message: (.last_assistant_message | cut),
|
||||
tool_input: (if (.tool_input | type) == "object"
|
||||
then .tool_input | {subagent_type, description, model, name}
|
||||
| map_values(cut) | with_entries(select(.value != null))
|
||||
else null end),
|
||||
t: now,
|
||||
iterm: $iterm,
|
||||
pid: ($pid | tonumber? // null),
|
||||
pid_comm: (if $pid_comm == "" then null else $pid_comm end),
|
||||
tmux_socket: (if $tmux_socket == "" then null else $tmux_socket end),
|
||||
tmux_pane: (if $tmux_pane == "" then null else $tmux_pane end)
|
||||
} | with_entries(select(.value != null))
|
||||
' >> "$dir/$sid.jsonl" 2>/dev/null
|
||||
|
||||
exit 0
|
||||
1506
files/claude_agents.py
Normal file
1506
files/claude_agents.py
Normal file
File diff suppressed because it is too large
Load Diff
18
files/config.example.json
Normal file
18
files/config.example.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"file_manager": "lf",
|
||||
"agent_dir": "changed",
|
||||
"done_linger_sec": 20,
|
||||
"agent_idle_min": 15,
|
||||
"poll_sec": 1.5,
|
||||
"keep_logs_days": 2,
|
||||
"use_profile_theme": true,
|
||||
"port": 8766,
|
||||
"context_warn": 500000,
|
||||
"context_crit": 700000,
|
||||
"history_max_chars": 30000,
|
||||
"usage_refresh_sec": 120,
|
||||
"balance_refresh_sec": 600,
|
||||
"show_breakdown": true,
|
||||
"show_balance": true,
|
||||
"theme_variant": "auto"
|
||||
}
|
||||
78
files/settings_hooks.py
Executable file
78
files/settings_hooks.py
Executable file
@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Добавляет или убирает хуки панели в ~/.claude/settings.json, не трогая остальное.
|
||||
|
||||
settings_hooks.py add <settings.json>
|
||||
settings_hooks.py remove <settings.json>
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
MARK = "agents_panel_hook"
|
||||
COMMAND = 'sh "$HOME/.claude/hooks/agents_panel_hook.sh"'
|
||||
EVENTS = { # событие -> matcher (None — без фильтра)
|
||||
"SessionStart": None, "SessionEnd": None, "UserPromptSubmit": None, "Stop": None,
|
||||
"Notification": None, "SubagentStart": None, "SubagentStop": None,
|
||||
"PostModelSwitch": None, "PreToolUse": "Agent|Task",
|
||||
}
|
||||
|
||||
|
||||
def ours(hook):
|
||||
return isinstance(hook, dict) and MARK in str(hook.get("command", ""))
|
||||
|
||||
|
||||
def add(cfg):
|
||||
hooks = cfg.setdefault("hooks", {})
|
||||
added = []
|
||||
for event, matcher in EVENTS.items():
|
||||
groups = hooks.setdefault(event, [])
|
||||
if any(ours(h) for g in groups for h in (g.get("hooks") or [])):
|
||||
continue # уже стоит — повторная установка ничего не дублирует
|
||||
group = {"hooks": [{"type": "command", "command": COMMAND}]}
|
||||
if matcher:
|
||||
group = {"matcher": matcher, **group}
|
||||
groups.append(group)
|
||||
added.append(event)
|
||||
return added
|
||||
|
||||
|
||||
def remove(cfg):
|
||||
hooks = cfg.get("hooks") or {}
|
||||
removed = []
|
||||
for event in list(hooks):
|
||||
groups = []
|
||||
for g in hooks[event]:
|
||||
kept = [h for h in (g.get("hooks") or []) if not ours(h)]
|
||||
if len(kept) != len(g.get("hooks") or []):
|
||||
removed.append(event)
|
||||
if kept:
|
||||
groups.append({**g, "hooks": kept})
|
||||
if groups:
|
||||
hooks[event] = groups
|
||||
else:
|
||||
del hooks[event]
|
||||
if "hooks" in cfg and not cfg["hooks"]:
|
||||
del cfg["hooks"]
|
||||
return sorted(set(removed))
|
||||
|
||||
|
||||
def main():
|
||||
action, path = sys.argv[1], os.path.expanduser(sys.argv[2])
|
||||
cfg = {}
|
||||
if os.path.exists(path):
|
||||
with open(path, encoding="utf-8") as f:
|
||||
text = f.read().strip()
|
||||
cfg = json.loads(text) if text else {}
|
||||
changed = add(cfg) if action == "add" else remove(cfg)
|
||||
if changed:
|
||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||
tmp = path + ".tmp"
|
||||
with open(tmp, "w", encoding="utf-8") as f:
|
||||
json.dump(cfg, f, ensure_ascii=False, indent=2)
|
||||
f.write("\n")
|
||||
os.replace(tmp, path)
|
||||
print(", ".join(changed) if changed else "—")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
101
install.sh
Executable file
101
install.sh
Executable file
@ -0,0 +1,101 @@
|
||||
#!/bin/bash
|
||||
# Установка панели «Claude» для iTerm2 Toolbelt: дерево сессий и агентов Claude Code,
|
||||
# лимиты подписки и баланс. Повторный запуск безопасен — обновляет установленное.
|
||||
set -euo pipefail
|
||||
|
||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||
FILES="$HERE/files"
|
||||
AUTOLAUNCH="$HOME/Library/Application Support/iTerm2/Scripts/AutoLaunch"
|
||||
HOOKS_DIR="$HOME/.claude/hooks"
|
||||
SETTINGS="$HOME/.claude/settings.json"
|
||||
CONFIG_DIR="$HOME/.config/claude-agents"
|
||||
BACKUP_DIR="$HOME/.claude-panel-backup/$(date +%Y%m%d-%H%M%S)-$$"
|
||||
PORT=8766
|
||||
|
||||
say() { printf '\033[1m==>\033[0m %s\n' "$*"; }
|
||||
ok() { printf ' \033[32m✓\033[0m %s\n' "$*"; }
|
||||
warn() { printf ' \033[33m!\033[0m %s\n' "$*"; }
|
||||
die() { printf '\033[31mОшибка:\033[0m %s\n' "$*" >&2; exit 1; }
|
||||
backup() { mkdir -p "$BACKUP_DIR"; cp -p "$1" "$BACKUP_DIR/"; }
|
||||
|
||||
# ---------- проверки ----------
|
||||
say "Проверяю окружение"
|
||||
[ "$(uname)" = "Darwin" ] || [ "${CLAUDE_PANEL_ANY_OS:-}" = 1 ] || die "нужен macOS с iTerm2"
|
||||
python3 -c 'import sys; assert sys.version_info >= (3, 8)' 2>/dev/null \
|
||||
|| die "нужен python3 ≥ 3.8 (xcode-select --install или brew install python)"
|
||||
ok "python3: $(python3 -V 2>&1)"
|
||||
if command -v jq >/dev/null; then ok "jq: $(command -v jq)"
|
||||
else warn "нет jq — хук без него молча ничего не пишет. Установите: brew install jq"; fi
|
||||
if [ -d "/Applications/iTerm.app" ] || [ -d "$HOME/Applications/iTerm.app" ]; then ok "iTerm2 найден"
|
||||
else warn "iTerm2 не найден в /Applications — панель будет работать только в нём"; fi
|
||||
|
||||
# ---------- хук Claude Code ----------
|
||||
say "Хук Claude Code"
|
||||
mkdir -p "$HOOKS_DIR"
|
||||
install -m 755 "$FILES/agents_panel_hook.sh" "$HOOKS_DIR/agents_panel_hook.sh"
|
||||
ok "$HOOKS_DIR/agents_panel_hook.sh"
|
||||
[ -f "$SETTINGS" ] && backup "$SETTINGS"
|
||||
added=$(python3 "$FILES/settings_hooks.py" add "$SETTINGS") \
|
||||
|| die "не смог обновить $SETTINGS (битый JSON?) — копия в $BACKUP_DIR"
|
||||
if [ "$added" = "—" ]; then ok "settings.json: хуки уже на месте"
|
||||
else ok "settings.json: добавлены хуки $added (копия старого — в $BACKUP_DIR)"; fi
|
||||
|
||||
# ---------- скрипт iTerm2 ----------
|
||||
say "Скрипт iTerm2"
|
||||
mkdir -p "$AUTOLAUNCH"
|
||||
if [ -f "$AUTOLAUNCH/claude_usage.py" ]; then # прежняя отдельная панель лимитов
|
||||
backup "$AUTOLAUNCH/claude_usage.py" && rm "$AUTOLAUNCH/claude_usage.py"
|
||||
ok "старый claude_usage.py убран из AutoLaunch (копия в $BACKUP_DIR)"
|
||||
fi
|
||||
[ -f "$AUTOLAUNCH/claude_agents.py" ] && backup "$AUTOLAUNCH/claude_agents.py"
|
||||
install -m 644 "$FILES/claude_agents.py" "$AUTOLAUNCH/claude_agents.py"
|
||||
ok "$AUTOLAUNCH/claude_agents.py"
|
||||
|
||||
# ---------- настройки панели ----------
|
||||
mkdir -p "$CONFIG_DIR"
|
||||
install -m 644 "$FILES/config.example.json" "$CONFIG_DIR/config.example.json"
|
||||
if [ -f "$CONFIG_DIR/config.json" ]; then ok "ваш config.json не тронут"
|
||||
else ok "пример настроек: $CONFIG_DIR/config.example.json (скопируйте в config.json, чтобы менять)"; fi
|
||||
|
||||
# ---------- Python API в iTerm2 ----------
|
||||
say "iTerm2"
|
||||
iterm_running=0
|
||||
pgrep -xq iTerm2 2>/dev/null && iterm_running=1
|
||||
api=$(defaults read com.googlecode.iterm2 EnableAPIServer 2>/dev/null || echo 0)
|
||||
if [ "$api" != 1 ]; then
|
||||
if [ $iterm_running = 0 ] && command -v defaults >/dev/null; then
|
||||
defaults write com.googlecode.iterm2 EnableAPIServer -bool true
|
||||
ok "Python API включён"
|
||||
else
|
||||
warn "включите Python API: iTerm2 → Settings → General → Magic → Enable Python API"
|
||||
[ $iterm_running = 1 ] && warn "(пока iTerm2 запущен, установщик не меняет его настройки — он бы их перезаписал)"
|
||||
fi
|
||||
else
|
||||
ok "Python API уже включён"
|
||||
fi
|
||||
|
||||
# старая копия панели держит порт — останавливаем, чтобы запустилась новая
|
||||
if command -v lsof >/dev/null; then
|
||||
pids=$(lsof -ti "tcp:$PORT" -sTCP:LISTEN 2>/dev/null || true)
|
||||
if [ -n "$pids" ]; then kill $pids 2>/dev/null || true; sleep 1; ok "остановлена прежняя версия панели"; fi
|
||||
fi
|
||||
|
||||
if [ $iterm_running = 1 ] && [ "$api" = 1 ]; then
|
||||
if osascript -e 'tell application "iTerm2" to launch API script named "claude_agents"' >/dev/null 2>&1; then
|
||||
ok "панель запущена"
|
||||
else
|
||||
warn "не смог запустить сам — Scripts → AutoLaunch → claude_agents.py"
|
||||
fi
|
||||
else
|
||||
warn "панель запустится сама при следующем запуске iTerm2"
|
||||
fi
|
||||
|
||||
cat <<EOF
|
||||
|
||||
Готово. Осталось:
|
||||
1. В iTerm2: View → Toolbelt → Show Toolbelt, затем Toolbelt → Claude.
|
||||
2. Перезапустите открытые сессии claude — хуки подхватываются при старте сессии.
|
||||
|
||||
Проверка, что всё работает: http://127.0.0.1:$PORT/raw
|
||||
Удаление: $HERE/uninstall.sh
|
||||
EOF
|
||||
33
uninstall.sh
Executable file
33
uninstall.sh
Executable file
@ -0,0 +1,33 @@
|
||||
#!/bin/bash
|
||||
# Удаление панели «Claude». С --purge удаляет ещё настройки и логи хука.
|
||||
set -euo pipefail
|
||||
|
||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||
AUTOLAUNCH="$HOME/Library/Application Support/iTerm2/Scripts/AutoLaunch"
|
||||
SETTINGS="$HOME/.claude/settings.json"
|
||||
BACKUP_DIR="$HOME/.claude-panel-backup/$(date +%Y%m%d-%H%M%S)-$$-uninstall"
|
||||
PORT=8766
|
||||
|
||||
ok() { printf ' \033[32m✓\033[0m %s\n' "$*"; }
|
||||
|
||||
if command -v lsof >/dev/null; then
|
||||
pids=$(lsof -ti "tcp:$PORT" -sTCP:LISTEN 2>/dev/null || true)
|
||||
[ -n "$pids" ] && kill $pids 2>/dev/null && ok "панель остановлена"
|
||||
fi
|
||||
|
||||
rm -f "$AUTOLAUNCH/claude_agents.py" && ok "скрипт убран из AutoLaunch"
|
||||
|
||||
if [ -f "$SETTINGS" ]; then
|
||||
mkdir -p "$BACKUP_DIR" && cp -p "$SETTINGS" "$BACKUP_DIR/"
|
||||
removed=$(python3 "$HERE/files/settings_hooks.py" remove "$SETTINGS")
|
||||
ok "settings.json: убраны хуки ${removed} (копия — в $BACKUP_DIR)"
|
||||
fi
|
||||
rm -f "$HOME/.claude/hooks/agents_panel_hook.sh" && ok "хук удалён"
|
||||
|
||||
if [ "${1:-}" = "--purge" ]; then
|
||||
rm -rf "$HOME/.config/claude-agents" "$HOME/.cache/claude-agents"
|
||||
ok "настройки и логи удалены"
|
||||
else
|
||||
echo " Настройки (~/.config/claude-agents) и логи (~/.cache/claude-agents) оставлены; --purge удалит и их."
|
||||
fi
|
||||
echo "Панель «Claude» исчезнет из меню Toolbelt после перезапуска iTerm2."
|
||||
Loading…
x
Reference in New Issue
Block a user