Compare commits

..
Author SHA1 Message Date
Codeman maintainer 88bb98de43 chore: version packages 2026-08-22 14:45:08 +02:00
Codeman maintainer 687e9d7565 feat!: retire the sc tmux chooser in favour of codeman tui
scripts/tmux-chooser.sh is deleted. codeman tui replaces it and does the
job better: sc numbered its entries globally but only accepted a single
[1-9] keypress, so sessions 10+ were listed and unselectable, and it
inferred nothing about what an agent was doing. The tui carries the
server's real states, answers permission dialogs, and leaves an attach
with one key.

install.sh no longer creates the tmux-chooser symlink or the sc alias.
It now sweeps both up instead, on update AND uninstall, so an update
cannot leave a symlink pointing at a script this version stopped
shipping. The alias removal is marker-owned: it matches the exact line
the installer wrote, so someone's own 'alias sc=' for another tool is
never touched, and it rewrites through 'cat >' so the profile keeps its
mode and ownership. Verified against three profile shapes.

BREAKING CHANGE: the 'sc' command and the 'tmux-chooser' symlink are
gone. Use 'codeman tui' (and 'codeman tui --list' / 'codeman tui <n>').
2026-08-22 14:44:14 +02:00
Codeman maintainer 5d81cc01ca docs: point terminal users at codeman tui instead of the sc chooser
codeman tui supersedes the sc bash chooser: it reaches sessions 10+,
carries the server's real states instead of a static list, and leaves an
attach with one key. Every place that told a user to run sc now names the
tui equivalent, including the two wiki pages and install.sh's next-steps
banner. Both wiki pages also carried the wrong detach chord (Ctrl+A D;
the socket's prefix is C-b), which the tui makes moot.

Source comments that explained themselves as "the sc -l replacement" now
just say what they do. docs/tui-plan.md and CHANGELOG.md are historical
records and keep their references.
2026-08-22 14:36:23 +02:00
Ark0N f49249fb2f Merge pull request #312 from Ark0N/feat/tui
feat: codeman tui, a terminal dashboard with live agent states
2026-08-22 14:32:51 +02:00
16 changed files with 97 additions and 737 deletions
+35
View File
@@ -1,5 +1,40 @@
# aicodeman
## 1.21.0
### Minor Changes
- **`codeman tui`: a terminal dashboard for your sessions.** For the times you are in SSH or Termius instead of a browser. The web UI remains the primary surface and bare `codeman` still prints help, so the dashboard itself is strictly additive.
Sessions are grouped NEEDS YOU / WORKING / IDLE / RECENT in the same status language as the web tabs and the phone overview, and the states come from the server (hooks, idle confirmation, the approvals inbox) over the existing HTTP/SSE API rather than being screen-scraped. That is what lets the dashboard answer a permission dialog instead of only reporting one.
- `↑↓`/`j`/`k` select; `1`-`9`, `[`/`]` and `Tab` switch between sessions
- `Enter` attaches and hands the terminal to tmux; **`F1` comes back**, one key, no modifier. Inside the pane a bar across the top carries the session strip and `Alt+1`..`Alt+9` switch without returning to the dashboard first
- `Enter` on a RECENT row resumes that conversation; on a session whose pane has died it refuses and offers `r` to resume it in a fresh pane
- `y`/`n`/digits answer the selected session's pending permission or question card (the server re-captures the pane first, so a keystroke can never land in the composer)
- `p` sends a one-line prompt without attaching, `x` kills (`y` confirms), `n` starts a session and opens straight into it
- `/` cross-session search, `g` away digest, `?` help, live preview pane, plan-usage chip in the header, a terminal bell when a new approval arrives
- `codeman tui --list` and `codeman tui <n>` are scriptable fast paths; with no server running it lists panes straight from the instance's tmux socket, attach-only, and upgrades live when the server comes back
Narrow terminals (under 72 columns, a phone SSH client) drop the preview and get a single-column layout. `NO_COLOR`, non-UTF-8 glyph fallback and a non-TTY refusal are all handled. Zero new dependencies: hand-rolled ANSI over chalk and commander. User guide: `docs/tui.md`.
**Breaking: the `sc` tmux chooser is retired.** `scripts/tmux-chooser.sh` is deleted and `install.sh` no longer creates the `tmux-chooser` symlink or the `sc` alias; it sweeps both up instead, on update and on uninstall. `codeman tui` replaces it and does the job better: `sc` numbered its entries globally but only accepted a single `[1-9]` keypress, so sessions 10+ were listed and could not be selected, and it inferred nothing about what an agent was doing. The alias cleanup is marker-owned, matching the exact line the installer wrote, so a user's own `alias sc=` for another tool is untouched.
**CLI polish that came with it.**
- New shared style kit (`src/cli-style.ts`) used across the CLI: semantic palette, glyphs, width-aware table, spinner, confirm.
- `codeman doctor` is colorized and its table is measured, so the "Antigravity CLI" label no longer pushes its row out of column. `--json` output is unchanged.
- `codeman web` no longer prints its "running at" line twice, and the server's non-loopback security warning is painted like the CLI's (chalk degrades off a TTY, so journald and `web.log` stay free of escape codes).
- Spinners on the silent up-to-30s waits in `codeman web -d`, `codeman web --stop` and `codeman service install`.
- `codeman reset` asks a real y/N confirmation on a TTY; non-interactive callers keep the old `--force` refusal.
- `codeman list` and `codeman session list` share one renderer instead of drifting copies.
- `codeman attach` is described correctly in the README (it shows an attachment card for a local file).
- `test/cli-commands.test.ts` now derives its inventory from the real commander program instead of a hand-written fixture that had drifted.
**Internal.** New `tmux -L` callers resolve the socket through `resolveTmuxSocketName()`, now exported from `config/instance.ts`, so a second process can never point a beta instance at prod's panes. CLAUDE.md and `docs/architecture-invariants.md` both record the rule.
### Thanks
The TUI went through seven rounds of beta testing over PuTTY/SSH by **@Ark0N**, which is where the way out of an attach, the session strip, the preview repaint handling and the glyph set all came from.
## 1.20.1
### Patch Changes
+2 -2
View File
@@ -75,7 +75,7 @@ When user says "COM":
CI runs `npm run check:lockfile` on every push/PR, so lockfile drift fails the build even if the `version-packages` script is bypassed.
**Version**: 1.20.1 (must match `package.json`)
**Version**: 1.21.0 (must match `package.json`)
## Project Overview
@@ -87,7 +87,7 @@ Codeman is a Claude Code session manager with web interface and autonomous Ralph
**Requirements**: Node.js 22+, Claude CLI, tmux
**Git**: Main branch is `master`. SSH session chooser: `sc` (interactive), `sc 2` (quick attach), `sc -l` (list).
**Git**: Main branch is `master`. Terminal session dashboard: `codeman tui` (`--list` to list, `codeman tui <n>` to attach).
## Additional Commands
+1 -15
View File
@@ -285,7 +285,7 @@ Hit start — Codeman spawns the CLI via a real PTY and streams it to your brows
- **Phone/tablet** — the UI is fully touch-optimized; scan the desktop **QR code** to log in without typing a password.
- **Outside your network** — `./scripts/tunnel.sh start` opens a Cloudflare tunnel (set `CODEMAN_PASSWORD` first).
- **SSH** — the `sc` chooser attaches to any session from a terminal (`sc` interactive, `sc 2` quick-attach, `sc -l` list).
- **SSH** — `codeman tui` is a full-screen dashboard in the terminal (`codeman tui --list` to list, `codeman tui 2` to attach straight to one).
### 7. Operate & maintain
@@ -674,20 +674,6 @@ The web UI remains the primary surface; see **[docs/tui.md](docs/tui.md)** for t
---
## SSH Alternative (`sc`)
If you prefer SSH (Termius, Blink, etc.), the `sc` command is a thumb-friendly session chooser:
```bash
sc # Interactive chooser
sc 2 # Quick attach to session 2
sc -l # List sessions
```
Single-digit selection (1-9), color-coded status, token counts, auto-refresh. `sc` hands you to a plain `tmux attach`, so you leave the pane with tmux's own detach chord: `Ctrl+B`, release, then `d`. (`codeman tui` is the one that gives you a single `F1` instead, because its attach claims that key for the duration.)
---
## Keyboard Shortcuts
> Ctrl bindings also accept Cmd on macOS.
+1 -15
View File
@@ -253,7 +253,7 @@ codeman web -H 0.0.0.0 # 绑定局域网 —— 必须设置 CODEMAN_
- **手机/平板** —— UI 完全触控优化;扫描桌面上的**二维码**即可免密码登录。
- **网络之外** —— `./scripts/tunnel.sh start` 打开一条 Cloudflare 隧道(先设置 `CODEMAN_PASSWORD`)。
- **SSH** —— `sc` 选择器可从终端附着任意会话(`sc` 交互式,`sc 2` 快速附着,`sc -l` 列表)。
- **SSH** —— `codeman tui` 是终端里的全屏会话面板(`codeman tui --list` 列出,`codeman tui 2` 直接附着到某个会话)。
### 7. 运维与维护
@@ -615,20 +615,6 @@ Codeman 默认用 `--dangerously-skip-permissions` 启动会话,因此 Web UI
---
## SSH 替代方案(`sc`)
如果你更喜欢 SSH(Termius、Blink 等),`sc` 命令是一个便于拇指操作的会话选择器:
```bash
sc # 交互式选择器
sc 2 # 快速附着到会话 2
sc -l # 列出会话
```
单数字选择(1–9)、颜色编码的状态、token 计数、自动刷新。用 `Ctrl+A D` 分离。
---
## 键盘快捷键
> Ctrl 绑定在 macOS 上也接受 Cmd。
+1 -1
View File
@@ -139,7 +139,7 @@ set -g extended-keys-format csi-u
Codeman's browser input path sends `\r` for submit, so basic use works
unconfigured — what degrades is newline-in-editor, mostly when you attach to the
pane directly (`sc`).
pane directly (`codeman tui`).
⚠️ Upstream notes the setting may need a full `tmux kill-server` to take effect.
**Never run `tmux kill-server` on Codeman's socket** — it would kill every live
+3 -3
View File
@@ -20,7 +20,7 @@ codeman tui --list # print the numbered session list and exit
codeman tui 2 # attach straight to session 2 of that list
```
The two fast paths are the scriptable ones (they are the `sc -l` / `sc 2` shapes).
The two fast paths are the scriptable ones.
Neither sets up a screen, so both are as quick as the one API call they make, and
`--list` prints plain text when piped, so it composes with `grep`/`awk`.
@@ -228,8 +228,8 @@ The TUI is an ordinary terminal program with no local dependencies beyond tmux,
separate remote mode.
Below 72 columns (Termius, an iPhone in portrait) the preview pane is dropped and
rows take two lines each: the same constraint the `sc` chooser was built around,
now with a cursor, live states and the answer/prompt/kill verbs. The switch is
rows take two lines each, keeping the cursor, the live states and the
answer/prompt/kill verbs. The switch is
width-driven at draw time, so unfolding a foldable or resizing a window re-lays out
immediately; there is no mode flag to set.
+1 -1
View File
@@ -88,7 +88,7 @@ would. That indirection buys:
- **Real scrollback.** History is held by tmux, so reconnecting replays what happened while
you were gone instead of starting from blank.
- **Attach from anywhere else.** The same session is reachable from a terminal over SSH
with the `sc` chooser, or plain `tmux -L codeman attach`.
with `codeman tui`, or plain `tmux -L codeman attach`.
- **Secrets off the command line.** Environment overrides are injected with socket-scoped
`tmux setenv` rather than being visible in the spawn command.
+4 -2
View File
@@ -73,8 +73,10 @@ features are Claude-only; [Agent CLIs](Agent-CLIs) lists exactly which.
### Can I attach to a session from a terminal instead of the browser?
Yes. `sc` is an interactive chooser (`sc 2` attaches directly, `sc -l` lists), or use tmux
directly on the `codeman` socket. Detach with `Ctrl+A D`.
Yes. `codeman tui` is a full-screen dashboard of your sessions, with the same
NEEDS YOU / WORKING / IDLE grouping the web UI uses. `codeman tui --list` prints the
numbered list and exits, and `codeman tui 2` attaches straight to session 2. `Enter`
attaches, `F1` comes back. You can also use tmux directly on the `codeman` socket.
## Running unattended
+9 -6
View File
@@ -179,16 +179,19 @@ the same IP, which matters because all tunnel traffic arrives from one loopback
## Terminal alternatives
You do not have to use a browser. `sc` is a thumb-friendly session chooser for SSH clients
like Termius or Blink:
You do not have to use a browser. `codeman tui` is a full-screen session dashboard that
works well in SSH clients like Termius or Blink:
```bash
sc # interactive chooser
sc 2 # attach to session 2
sc -l # list
codeman tui # the dashboard
codeman tui 2 # attach straight to session 2
codeman tui --list # numbered list, then exit
```
Detach with `Ctrl+A D`. The sessions are the same ones the dashboard shows.
`Enter` attaches into the pane and `F1` comes back. Under 72 columns it drops the preview
and becomes a single-column list, so it stays usable on a phone. The sessions are the same
ones the dashboard shows. See [docs/tui.md](https://github.com/Ark0N/Codeman/blob/master/docs/tui.md)
for the full guide.
## Common problems
+29 -20
View File
@@ -1072,21 +1072,28 @@ add_to_path() {
success "Added to $profile"
}
setup_sc_alias() {
# The `sc` bash chooser was retired in favour of `codeman tui`, which reaches
# sessions 10+, carries the server's real states and leaves an attach with one
# key. Older installers wrote this alias, so take it back out.
#
# Marker-owned on purpose: it matches the exact line WE wrote, so a user's own
# `alias sc=` for something entirely different is never touched. The rewrite
# goes through `cat >` rather than `mv` so the profile keeps its own mode and
# ownership.
remove_sc_alias() {
local profile
profile=$(detect_shell_profile)
[[ -f "$profile" ]] || return 0
grep -qE "^alias sc='tmux-chooser'\$" "$profile" 2>/dev/null || return 0
# Check if alias already exists
if [[ -f "$profile" ]] && grep -qE "^alias sc=" "$profile" 2>/dev/null; then
info "Alias 'sc' already configured in $profile"
return 0
local tmp
tmp=$(mktemp 2>/dev/null) || return 0
if sed -e "/^alias sc='tmux-chooser'\$/d" \
-e '/^# Codeman tmux session shortcut$/d' "$profile" > "$tmp" 2>/dev/null; then
cat "$tmp" > "$profile"
info "Removed the retired 'sc' alias from $profile (use: codeman tui)"
fi
echo "" >> "$profile"
echo "# Codeman tmux session shortcut" >> "$profile"
echo "alias sc='tmux-chooser'" >> "$profile"
info "Added 'sc' alias for tmux-chooser"
rm -f "$tmp"
}
# ============================================================================
@@ -2243,13 +2250,14 @@ main() {
ln -sf "$INSTALL_DIR/dist/index.js" "$symlink_dir/codeman"
info "Created symlink: $symlink_dir/codeman"
# Install tmux-chooser as 'tmux-chooser' command
if [[ -f "$INSTALL_DIR/scripts/tmux-chooser.sh" ]]; then
ln -sf "$INSTALL_DIR/scripts/tmux-chooser.sh" "$symlink_dir/tmux-chooser"
info "Created symlink: $symlink_dir/tmux-chooser"
# Add 'sc' alias for quick access
setup_sc_alias
# tmux-chooser/`sc` is retired; `codeman tui` replaces it. Sweep up what
# an older installer left behind, so an update does not leave a symlink
# pointing at a script this version no longer ships.
if [[ -L "$symlink_dir/tmux-chooser" ]]; then
rm -f "$symlink_dir/tmux-chooser"
info "Removed the retired tmux-chooser symlink (use: codeman tui)"
fi
remove_sc_alias
# Add ~/.local/bin to PATH if not already there
if [[ ":$PATH:" != *":$symlink_dir:"* ]]; then
@@ -2450,9 +2458,9 @@ main() {
echo -e " ${BOLD}Mobile Access (Termius/SSH):${NC}"
echo ""
echo -e " ${CYAN}sc${NC} # Interactive tmux session chooser"
echo -e " ${CYAN}sc 2${NC} # Quick attach to session 2"
echo -e " ${CYAN}sc -h${NC} # Help"
echo -e " ${CYAN}codeman tui${NC} # Full-screen session dashboard"
echo -e " ${CYAN}codeman tui 2${NC} # Attach straight to session 2"
echo -e " ${CYAN}codeman tui -l${NC} # Numbered list, then exit"
echo ""
echo -e " ${BOLD}Documentation:${NC}"
@@ -2620,6 +2628,7 @@ uninstall() {
rm -f "$symlink_dir/tmux-chooser"
success "Removed symlink: $symlink_dir/tmux-chooser"
fi
remove_sc_alias
# Remove install directory
if [[ -d "$INSTALL_DIR" ]]; then
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "aicodeman",
"version": "1.20.1",
"version": "1.21.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "aicodeman",
"version": "1.20.1",
"version": "1.21.0",
"hasInstallScript": true,
"license": "MIT",
"workspaces": [
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "aicodeman",
"version": "1.20.1",
"version": "1.21.0",
"description": "Mission control for AI coding agents - run 20 autonomous agents with real-time monitoring and session persistence",
"type": "module",
"main": "dist/index.js",
-662
View File
@@ -1,662 +0,0 @@
#!/bin/bash
# ============================================================================
# Codeman Sessions - Mobile-friendly Tmux Session Chooser
# Optimized for iPhone/Termius (portrait ~45 chars, landscape ~95 chars)
# ============================================================================
#
# Design principles:
# - Single-digit selection (1-9) for fast thumb typing
# - Compact display, no wasted space
# - Color-coded status for quick scanning
# - Names pulled from Codeman state.json
# - Minimal keystrokes to attach
#
# Usage:
# tmux-chooser # Interactive chooser
# tmux-chooser 1 # Quick attach to session 1
# tmux-chooser -l # List only (non-interactive)
# tmux-chooser -h # Help
#
# Alias: alias sc='tmux-chooser'
# Then: sc (interactive)
# sc 2 (attach session 2)
#
# ============================================================================
set -e
# ============================================================================
# Configuration
# ============================================================================
CODEMAN_STATE="$HOME/.codeman/state.json"
CODEMAN_SESSIONS="$HOME/.codeman/mux-sessions.json"
# Dedicated tmux socket all Codeman sessions live on. MUST match
# DEFAULT_CODEMAN_TMUX_SOCKET / CODEMAN_TMUX_SOCKET in src/tmux-manager.ts —
# otherwise list-sessions would enumerate the user's default tmux server
# (missing the real Codeman sessions, surfacing unrelated ones).
CODEMAN_TMUX_SOCKET="${CODEMAN_TMUX_SOCKET:-codeman}"
TMUX_CMD=(tmux -L "$CODEMAN_TMUX_SOCKET")
# iPhone 17 Pro portrait width (conservative)
MAX_WIDTH=44
MAX_NAME_LEN=28
# Page size for pagination (leave room for header/footer)
PAGE_SIZE=7
# Auto-refresh timeout (seconds) - 0 to disable
AUTO_REFRESH=60
# ============================================================================
# Icon Detection (Nerd Fonts vs ASCII)
# ============================================================================
detect_icons() {
if [[ "$TERM_PROGRAM" == "iTerm"* ]] || \
[[ "$TERM" == "xterm-kitty" ]] || \
[[ -n "$WEZTERM_PANE" ]] || \
[[ "$LC_TERMINAL" == "iTerm2" ]]; then
ICON_SESSION="󰆍"
ICON_ATTACHED="●"
ICON_DETACHED="○"
ICON_UNKNOWN="◌"
else
ICON_SESSION="[T]"
ICON_ATTACHED="*"
ICON_DETACHED="-"
ICON_UNKNOWN="?"
fi
}
detect_icons
# ============================================================================
# Colors - ANSI 256 for better Termius compatibility
# ============================================================================
R='\033[0m' # Reset
B='\033[1m' # Bold
D='\033[2m' # Dim
GREEN='\033[38;5;82m'
YELLOW='\033[38;5;220m'
BLUE='\033[38;5;75m'
CYAN='\033[38;5;87m'
RED='\033[38;5;203m'
GRAY='\033[38;5;245m'
WHITE='\033[38;5;255m'
BG_SEL='\033[48;5;236m'
# ============================================================================
# Utilities
# ============================================================================
truncate() {
local str="$1"
local max="$2"
local len=${#str}
if [ "$len" -le "$max" ]; then
echo "$str"
return
fi
if [[ "$str" == *"/"* ]]; then
echo "..${str: -$((max-2))}"
else
echo "${str:0:$((max-1))}…"
fi
}
find_full_session_id() {
local short_id="$1"
if [ -f "$CODEMAN_STATE" ]; then
local full_id
full_id=$(jq -r --arg short "$short_id" '
.sessions | keys[] | select(startswith($short))
' "$CODEMAN_STATE" 2>/dev/null | head -1)
if [ -n "$full_id" ]; then
echo "$full_id"
return
fi
fi
if [ -f "$CODEMAN_SESSIONS" ]; then
local full_id
full_id=$(jq -r --arg short "$short_id" '
.[] | select(.sessionId | startswith($short)) | .sessionId
' "$CODEMAN_SESSIONS" 2>/dev/null | head -1)
if [ -n "$full_id" ]; then
echo "$full_id"
return
fi
fi
echo "$short_id"
}
get_session_name() {
local session_id="$1"
local name=""
local workdir=""
if [ -f "$CODEMAN_SESSIONS" ]; then
local result
result=$(jq -r --arg id "$session_id" '
.[] | select(.sessionId | startswith($id)) | "\(.name // "")\t\(.workingDir // "")"
' "$CODEMAN_SESSIONS" 2>/dev/null | head -1)
if [ -n "$result" ]; then
name="${result%% *}"
workdir="${result#* }"
fi
fi
if [ -z "$name" ] && [ -f "$CODEMAN_STATE" ]; then
local result
result=$(jq -r --arg id "$session_id" '
.sessions | to_entries[] | select(.key | startswith($id)) | "\(.value.name // "")\t\(.value.workingDir // "")"
' "$CODEMAN_STATE" 2>/dev/null | head -1)
if [ -n "$result" ]; then
name="${result%% *}"
[ -z "$workdir" ] && workdir="${result#* }"
fi
fi
if [ -n "$name" ]; then
echo "$name"
return
fi
if [ -n "$workdir" ]; then
echo "${workdir##*/}"
return
fi
echo "${session_id:0:8}"
}
get_working_dir() {
local session_id="$1"
if [ -f "$CODEMAN_SESSIONS" ]; then
local dir
dir=$(jq -r --arg id "$session_id" '
.[] | select(.sessionId | startswith($id)) | .workingDir // empty
' "$CODEMAN_SESSIONS" 2>/dev/null | head -1)
if [ -n "$dir" ] && [ "$dir" != "null" ]; then
echo "${dir/#$HOME/~}"
return
fi
fi
if [ -f "$CODEMAN_STATE" ]; then
local dir
dir=$(jq -r --arg id "$session_id" '
.sessions | to_entries[] | select(.key | startswith($id)) | .value.workingDir // empty
' "$CODEMAN_STATE" 2>/dev/null | head -1)
if [ -n "$dir" ] && [ "$dir" != "null" ]; then
echo "${dir/#$HOME/~}"
return
fi
fi
echo ""
}
get_tokens() {
local session_id="$1"
if [ -f "$CODEMAN_STATE" ]; then
local tokens
tokens=$(jq -r --arg id "$session_id" '
.sessions | to_entries[] | select(.key | startswith($id)) |
((.value.inputTokens // 0) + (.value.outputTokens // 0))
' "$CODEMAN_STATE" 2>/dev/null | head -1)
if [ -n "$tokens" ] && [ "$tokens" != "null" ] && [ "$tokens" -gt 0 ] 2>/dev/null; then
if [ "$tokens" -gt 1000 ]; then
echo "$((tokens / 1000))k"
else
echo "${tokens}"
fi
return
fi
fi
echo ""
}
get_respawn_status() {
local session_id="$1"
if [ -f "$CODEMAN_SESSIONS" ]; then
local respawn_enabled
respawn_enabled=$(jq -r --arg id "$session_id" '
.[] | select(.sessionId | startswith($id)) | .respawnConfig.enabled // false
' "$CODEMAN_SESSIONS" 2>/dev/null | head -1)
if [ "$respawn_enabled" = "true" ]; then
echo "R"
return
fi
fi
echo ""
}
check_deps() {
if ! command -v jq &>/dev/null; then
echo -e "${YELLOW}Note: Install jq for session names${R}"
echo ""
fi
}
# ============================================================================
# Tmux Session Parser
# ============================================================================
declare -a SESSION_PIDS
declare -a MUX_NAMES
declare -a SESSION_STATES
declare -a SESSION_IDS
declare -a DISPLAY_NAMES
declare -a WORKING_DIRS
declare -a TOKEN_COUNTS
declare -a RESPAWN_STATUS
parse_sessions() {
SESSION_PIDS=()
MUX_NAMES=()
SESSION_STATES=()
SESSION_IDS=()
DISPLAY_NAMES=()
WORKING_DIRS=()
TOKEN_COUNTS=()
RESPAWN_STATUS=()
local i=0
# Parse tmux list-sessions output
while IFS= read -r line; do
local session_name="${line%%:*}"
# Only show codeman sessions
if [[ "$session_name" != codeman-* ]]; then
continue
fi
# Check if attached
local state="Detached"
if [[ "$line" == *"(attached)"* ]]; then
state="Attached"
fi
# Get PID from tmux
local pid
pid=$("${TMUX_CMD[@]}" display-message -t "$session_name" -p '#{pane_pid}' 2>/dev/null || echo "0")
SESSION_PIDS+=("$pid")
MUX_NAMES+=("$session_name")
SESSION_STATES+=("$state")
# Extract session ID from codeman session name
local session_id=""
local cm_regex='^codeman-(.+)$'
if [[ "$session_name" =~ $cm_regex ]]; then
session_id="${BASH_REMATCH[1]}"
fi
SESSION_IDS+=("$session_id")
# Get display name and metadata
if [ -n "$session_id" ]; then
DISPLAY_NAMES+=("$(get_session_name "$session_id")")
WORKING_DIRS+=("$(get_working_dir "$session_id")")
TOKEN_COUNTS+=("$(get_tokens "$session_id")")
RESPAWN_STATUS+=("$(get_respawn_status "$session_id")")
else
DISPLAY_NAMES+=("$session_name")
WORKING_DIRS+=("")
TOKEN_COUNTS+=("")
RESPAWN_STATUS+=("")
fi
i=$((i + 1))
done < <("${TMUX_CMD[@]}" list-sessions 2>/dev/null || true)
}
# ============================================================================
# Display Functions
# ============================================================================
clear_screen() {
printf '\033[2J\033[H'
}
print_header() {
local count=${#SESSION_PIDS[@]}
echo -e "${B}${CYAN}Codeman Sessions${R} ${D}($count)${R}"
echo -e "${D}$(printf '%.0s─' {1..32})${R}"
}
print_entry() {
local idx="$1"
local num=$((idx + 1))
local name="${DISPLAY_NAMES[$idx]}"
local state="${SESSION_STATES[$idx]}"
local dir="${WORKING_DIRS[$idx]}"
local tokens="${TOKEN_COUNTS[$idx]}"
local respawn="${RESPAWN_STATUS[$idx]}"
local name_max=$MAX_NAME_LEN
[ -n "$respawn" ] && name_max=$((name_max - 2))
[ -n "$tokens" ] && name_max=$((name_max - 4))
name=$(truncate "$name" $name_max)
local status_icon status_color
if [[ "$state" == *"Attached"* ]]; then
status_icon="$ICON_ATTACHED"
status_color="$GREEN"
elif [[ "$state" == *"Detached"* ]]; then
status_icon="$ICON_DETACHED"
status_color="$GRAY"
else
status_icon="$ICON_UNKNOWN"
status_color="$YELLOW"
fi
local num_str="${B}${WHITE}${num})${R}"
local name_str="${B}${WHITE}${name}${R}"
local status_str="${status_color}${status_icon}${R}"
local respawn_str=""
if [ -n "$respawn" ]; then
respawn_str=" ${GREEN}${respawn}${R}"
fi
local token_str=""
if [ -n "$tokens" ]; then
token_str=" ${D}${tokens}${R}"
fi
echo -e " ${num_str} ${name_str} ${status_str}${respawn_str}${token_str}"
if [ -n "$dir" ]; then
dir=$(truncate "$dir" $((MAX_NAME_LEN - 2)))
echo -e " ${D}${dir}${R}"
fi
}
print_footer() {
local page="$1"
local total_pages="$2"
echo ""
echo -e "${D}────────────────────────────────${R}"
if [ "$total_pages" -gt 1 ]; then
echo -e " ${D}Page $((page+1))/$total_pages${R} ${GRAY}[${WHITE}n${GRAY}]ext [${WHITE}p${GRAY}]rev${R}"
fi
echo -e " ${GRAY}[${WHITE}1-9${GRAY}]attach [${WHITE}r${GRAY}]efresh [${WHITE}q${GRAY}]uit${R}"
}
print_no_sessions() {
clear_screen
echo -e "${B}${CYAN}Codeman Sessions${R}"
echo -e "${D}$(printf '%.0s─' {1..32})${R}"
echo ""
echo -e " ${YELLOW}No tmux sessions found${R}"
echo ""
echo -e " ${D}Start one with:${R}"
echo -e " ${WHITE}codeman web${R}"
echo ""
echo -e "${D}$(printf '%.0s─' {1..32})${R}"
echo -e " ${GRAY}[${WHITE}r${GRAY}]efresh [${WHITE}q${GRAY}]uit${R}"
}
# ============================================================================
# Main Display Loop
# ============================================================================
current_page=0
render() {
clear_screen
parse_sessions
local count=${#SESSION_PIDS[@]}
if [ "$count" -eq 0 ]; then
print_no_sessions
return
fi
local total_pages=$(( (count + PAGE_SIZE - 1) / PAGE_SIZE ))
if [ "$current_page" -ge "$total_pages" ]; then
current_page=$((total_pages - 1))
fi
if [ "$current_page" -lt 0 ]; then
current_page=0
fi
local start=$((current_page * PAGE_SIZE))
local end=$((start + PAGE_SIZE))
if [ "$end" -gt "$count" ]; then
end=$count
fi
print_header
echo ""
for ((i = start; i < end; i++)); do
print_entry $i
done
print_footer $current_page $total_pages
}
attach_session() {
local idx="$1"
local mux_name="${MUX_NAMES[$idx]}"
if [ -z "$mux_name" ]; then
return 1
fi
clear_screen
echo -e "${GREEN}Attaching to ${B}${DISPLAY_NAMES[$idx]}${R}${GREEN}...${R}"
echo -e "${D}(Ctrl+B D to detach)${R}"
sleep 0.3
"${TMUX_CMD[@]}" attach-session -t "$mux_name"
return 0
}
# ============================================================================
# Input Handler
# ============================================================================
handle_input() {
local key="$1"
local count=${#SESSION_PIDS[@]}
local total_pages=$(( (count + PAGE_SIZE - 1) / PAGE_SIZE ))
case "$key" in
[1-9])
local idx=$((key - 1))
if [ "$idx" -lt "$count" ]; then
attach_session "$idx"
return 0
fi
;;
$'\e')
read -rsn2 -t 0.1 seq 2>/dev/null || true
case "$seq" in
'[A'|'[D')
if [ "$total_pages" -gt 1 ]; then
current_page=$(( (current_page - 1 + total_pages) % total_pages ))
fi
;;
'[B'|'[C')
if [ "$total_pages" -gt 1 ]; then
current_page=$(( (current_page + 1) % total_pages ))
fi
;;
esac
;;
n|N|j|J)
if [ "$total_pages" -gt 1 ]; then
current_page=$(( (current_page + 1) % total_pages ))
fi
;;
p|P|k|K)
if [ "$total_pages" -gt 1 ]; then
current_page=$(( (current_page - 1 + total_pages) % total_pages ))
fi
;;
r|R)
;;
q|Q)
clear_screen
exit 0
;;
'')
if [ "$count" -eq 1 ]; then
attach_session 0
return 0
fi
;;
esac
return 0
}
# ============================================================================
# List Mode
# ============================================================================
list_mode() {
parse_sessions
local count=${#SESSION_PIDS[@]}
if [ "$count" -eq 0 ]; then
echo "No tmux sessions"
exit 0
fi
for ((i = 0; i < count; i++)); do
local num=$((i + 1))
local name="${DISPLAY_NAMES[$i]}"
local state="${SESSION_STATES[$i]}"
local respawn="${RESPAWN_STATUS[$i]}"
local indicator="-"
[[ "$state" == *"Attached"* ]] && indicator="*"
[ -n "$respawn" ] && indicator="${indicator}R"
echo "$num) $name [$indicator]"
done
}
# ============================================================================
# Quick Attach
# ============================================================================
quick_attach() {
local num="$1"
parse_sessions
local count=${#SESSION_PIDS[@]}
local idx=$((num - 1))
if [ "$idx" -lt 0 ] || [ "$idx" -ge "$count" ]; then
echo -e "${RED}Invalid session: $num${R}"
echo "Available: 1-$count"
exit 1
fi
attach_session "$idx"
}
# ============================================================================
# Help
# ============================================================================
show_help() {
cat << 'EOF'
Codeman Sessions - Mobile-friendly Tmux Session Chooser
USAGE:
sc Interactive chooser
sc <number> Quick attach to session N
sc -l List sessions (non-interactive)
sc -h Show this help
INTERACTIVE KEYS:
1-9 Attach to session
n/j/↓ Next page
p/k/↑ Previous page
r Refresh
q Quit
INDICATORS:
* / ● Attached (someone connected)
- / ○ Detached (available)
R Respawn enabled
45k Token count
TIPS:
- Detach from tmux: Ctrl+B D
- Session names from Codeman state
- Optimized for Termius/iPhone
EOF
}
# ============================================================================
# Main
# ============================================================================
main() {
case "${1:-}" in
-h|--help)
show_help
exit 0
;;
-l|--list)
list_mode
exit 0
;;
[1-9]|[1-9][0-9])
quick_attach "$1"
exit $?
;;
esac
check_deps
render
while true; do
local timeout_opt=""
if [ "$AUTO_REFRESH" -gt 0 ]; then
timeout_opt="-t $AUTO_REFRESH"
fi
if read -rsn1 $timeout_opt key 2>/dev/null; then
handle_input "$key"
fi
render
done
}
trap 'clear_screen; exit 0' INT
main "$@"
+5 -4
View File
@@ -2600,8 +2600,9 @@ export async function runTui(options: TuiRunOptions = {}): Promise<number> {
}
/**
* `codeman tui --list`: the `sc -l` replacement. Prints and exits, colored when
* the output is a terminal and plain when it is piped (chalk's call, not ours).
* `codeman tui --list`: prints and exits, colored when the output is a terminal
* and plain when it is piped (chalk's call, not ours), so it composes with
* grep/awk.
*/
export async function runTuiList(options: TuiRunOptions = {}): Promise<number> {
const stdout = options.stdout ?? process.stdout;
@@ -2638,8 +2639,8 @@ export async function runTuiList(options: TuiRunOptions = {}): Promise<number> {
}
/**
* `codeman tui <n>`: the `sc 2` replacement. No screen setup at all, so it is
* as fast as the API call it makes.
* `codeman tui <n>`: attach straight to the nth row of `--list`. No screen setup
* at all, so it is as fast as the API call it makes.
*/
export async function runTuiAttach(position: number, options: TuiRunOptions = {}): Promise<number> {
const stdin = options.stdin ?? process.stdin;
+2 -2
View File
@@ -213,8 +213,8 @@ export function glyphsFor(tier: TuiGlyphTier): TuiGlyphSet {
/**
* Glyph tier from the environment. IO-ish by nature (it reads env), so it takes
* the env as an argument and the app layer calls it once at startup. The
* known-capable list is the same gate `scripts/tmux-chooser.sh` uses, plus a
* UTF-8 locale check and an explicit override.
* known-capable list is a TERM allowlist, plus a UTF-8 locale check and an
* explicit override.
*/
export function detectGlyphTier(env: Record<string, string | undefined>): TuiGlyphTier {
const override = env.CODEMAN_TUI_GLYPHS;
+1 -1
View File
@@ -138,7 +138,7 @@ describe('registered options', () => {
expect(flagsOf(find(program, 'doctor')!)).toEqual(expect.arrayContaining(['--json', '--category']));
});
it('keeps the two `tui` fast paths, which stand in for `sc -l` and `sc 2`', () => {
it('keeps the two `tui` fast paths that scripts call', () => {
expect(flagsOf(find(program, 'tui')!)).toEqual(expect.arrayContaining(['-l', '--list']));
});