mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-01 04:59:41 +02:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
88bb98de43 | ||
|
|
687e9d7565 | ||
|
|
5d81cc01ca | ||
|
|
f49249fb2f |
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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。
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Generated
+2
-2
@@ -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
@@ -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",
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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']));
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user