Compare commits
2 Commits
f53c6d2980
...
2b3b4e2783
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b3b4e2783 | ||
|
|
a9e425a29b |
@@ -14,6 +14,7 @@ Python script (`scripts/claude-sessions`, symlinked to `~/sbin/`). Manages Claud
|
||||
- **Location column.** Live and resumable rows show the session folder (`~` for `$HOME`). Live rows take it from the registry (`project_dir`), falling back to tmux `#{session_path}`.
|
||||
- **`n` New session.** Asks for a folder: Enter accepts the nearest `CLAUDE.md` folder, `b` opens the folder browser, or type a project name/path (same lookup as `claude-tmux`).
|
||||
- **Folder browser.** `zenity --file-selection --directory` when `DISPLAY`/`WAYLAND_DISPLAY` is set and zenity exists (has Create Folder). Otherwise a terminal browser: number = open, `u` up, `n` new folder, `/text` filter, `>` `<` page, `h` hidden, `~`/`/path` jump, Enter/`s` select, `q` cancel.
|
||||
- **Profile.** After the topic, a numbered picker lists every `~/.claude-[company]-[profile]` dir that has a `projects/` folder, shown as `Company (profile)` (e.g. `O'Reilly IT (anthropic)`). A dir with no profile part (`~/.claude-octopus`) is skipped; `~/.claude` shows `Personal (main)`. Company names come from `profiles.yaml` `display_name` (parenthetical stripped), else the Title-Cased segment. Enter accepts `oreillyit-anthropic`. The choice is passed as `--profile` to `claude-tmux`.
|
||||
- **Topic.** Becomes the tmux session name and the Remote Control name (passed as `--name` to `claude-tmux`). Defaults to the Title-Cased folder name. If that name is already a live tmux session, no default is offered and a unique topic is required. Topics containing `.` or `:` are rejected because tmux rewrites them to `_`.
|
||||
|
||||
## Headless spawn
|
||||
|
||||
@@ -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,61 @@ 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 and a profile part
|
||||
(~/.claude-[company] alone is skipped). 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("-")
|
||||
if not variant:
|
||||
continue
|
||||
label = f"{company_names.get(company) or title_case(company)} ({variant})"
|
||||
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 +939,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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user