claude-sessions: profile picker when creating a new session

This commit is contained in:
Paul O'Reilly
2026-10-27 16:56:53 +13:00
parent f53c6d2980
commit a9e425a29b
2 changed files with 60 additions and 2 deletions

View File

@@ -3,7 +3,7 @@
claude-sessions — Session lifecycle manager for Claude Code tmux sessions.
Commands:
(no args) Interactive menu (new session: folder browser + unique topic)
(no args) Interactive menu (new session: folder browser, unique topic, profile)
list Show live Claude tmux sessions and resumable registry entries
recover [--days N] Detect sessions active before a restart
spawn <project> Spawn a new session (wraps claude-tmux); --headless for scripted use
@@ -47,6 +47,8 @@ PATH_SCAN_ROOTS = [
(HOME, 1),
]
DEFAULT_PROFILE_NAME = "oreillyit-anthropic" # same default as claude-tmux
DEFAULT_PROFILES = {
"oreillyit-anthropic": {
"display_name": "O'Reilly IT",
@@ -346,6 +348,58 @@ def prompt_topic(folder: Path, live: set[str]) -> Optional[str]:
return topic
def discover_profile_choices(profiles_cfg: dict) -> list[tuple[str, str]]:
"""Profiles from ~/.claude-[company]-[profile] dirs as (name, "Company (profile)").
A dir counts as a profile if it has a projects/ folder. Company display names
come from profiles.yaml display_name (parenthetical stripped) when known,
otherwise the Title-Cased directory segment. ~/.claude is the "default" profile.
"""
import re
company_names: dict[str, str] = {}
for name, cfg in profiles_cfg.items():
company = name.split("-", 1)[0]
disp = re.sub(r"\s*\(.*?\)\s*$", "", cfg.get("display_name", "")).strip()
if disp:
company_names.setdefault(company, disp)
choices = []
for d in sorted(HOME.iterdir()):
if not (d.is_dir() and d.name.startswith(".claude-") and (d / "projects").is_dir()):
continue
name = d.name[len(".claude-"):]
company, _, variant = name.partition("-")
label = f"{company_names.get(company) or title_case(company)} ({variant or 'base'})"
choices.append((name, label))
if (HOME / ".claude" / "projects").is_dir():
choices.append(("default", "Personal (main)"))
return choices
def prompt_profile(profiles_cfg: dict) -> Optional[str]:
"""Numbered profile picker. Enter accepts the default. Returns the profile name, or None to cancel."""
choices = discover_profile_choices(profiles_cfg)
if not choices:
return DEFAULT_PROFILE_NAME
names = [n for n, _ in choices]
default = DEFAULT_PROFILE_NAME if DEFAULT_PROFILE_NAME in names else names[0]
print(f"\n {BOLD}Profile:{NC}")
for i, (n, label) in enumerate(choices, 1):
mark = f" {DIM}(default){NC}" if n == default else ""
print(f" {CYAN}{i:>2}){NC} {label}{mark}")
while True:
try:
raw = input(f" Profile [{BOLD}{names.index(default) + 1}{NC}]: ").strip()
except (KeyboardInterrupt, EOFError):
print()
return None
if not raw:
return default
if raw.isdigit() and 1 <= int(raw) <= len(names):
return names[int(raw) - 1]
print(f" {YELLOW}Enter a number from 1 to {len(names)}.{NC}")
# ── Session file parsing ─────────────────────────────────────────────────────
def read_jsonl_header(path: Path, max_lines: int = 30) -> dict:
@@ -882,8 +936,11 @@ def cmd_menu(_args=None) -> None:
topic = prompt_topic(folder, list_tmux_sessions())
if topic is None:
continue
profile = prompt_profile(profiles_cfg)
if profile is None:
continue
import types
a = types.SimpleNamespace(project=str(folder), mode=None, profile=None, name=topic, attach=True)
a = types.SimpleNamespace(project=str(folder), mode=None, profile=profile, name=topic, attach=True)
print()
cmd_spawn(a) # exec's claude-tmux --attach; replaces this process