@jx0/jmux 0.3.9 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,12 +4,12 @@
4
4
 
5
5
  **The terminal workspace for agentic development.**
6
6
 
7
- A tmux environment built for running coding agents in parallel with a persistent sidebar that shows every session, what's running, and what needs your attention.
7
+ Run coding agents in parallel from your terminal. A persistent sidebar shows every session, what's running, and what needs your attention — without leaving the tools you already use.
8
8
 
9
9
  [![npm](https://img.shields.io/npm/v/@jx0/jmux)](https://www.npmjs.com/package/@jx0/jmux)
10
10
  [![license](https://img.shields.io/github/license/jarredkenny/jmux)](LICENSE)
11
11
 
12
- ![jmux sidebar with grouped sessions alongside vim and server logs](docs/screenshots/hero.png)
12
+ ![jmux sidebar with grouped sessions alongside vim and Claude Code](docs/screenshots/hero.png)
13
13
 
14
14
  </div>
15
15
 
@@ -26,47 +26,64 @@ Requires [Bun](https://bun.sh) 1.2+, [tmux](https://github.com/tmux/tmux) 3.2+,
26
26
 
27
27
  ## Why
28
28
 
29
- tmux sessions are invisible. You have 30 of them, but the status bar shows one name. To switch, you `prefix-s`, scan a wall of text, and hope you remember what's where.
29
+ GUI agent orchestrators are 100MB+ Electron apps that lock you into their editor, their diff viewer, their Git workflow. They work on one platform. They'll charge you eventually.
30
30
 
31
- jmux fixes this with a persistent sidebar that shows every session, all the time.
31
+ jmux takes the opposite approach: it's a thin orchestration layer over tmux — the tool you already know. Your editor, your Git workflow, your shell, your tools. jmux just makes them visible and navigable when you're running 10+ agents in parallel.
32
+
33
+ | | jmux | GUI orchestrators |
34
+ |---|---|---|
35
+ | **Size** | ~0.3 MB | ~100+ MB |
36
+ | **Platform** | Anywhere tmux runs (macOS, Linux, SSH, containers) | macOS only |
37
+ | **Editor** | Yours (vim, emacs, VS Code, whatever) | Built-in (take it or leave it) |
38
+ | **Git** | `git`, `gh`, lazygit, [wtm](https://github.com/jarredkenny/worktree-manager), your workflow | Built-in GUI (their workflow) |
39
+ | **Agents** | Any (Claude Code, Codex, aider, custom) | Bundled subset |
40
+ | **Lock-in** | None — it's tmux underneath | Proprietary workspace format |
41
+ | **Cost** | Free, open source | Free today, VC-funded |
32
42
 
33
43
  ## Features
34
44
 
35
45
  ### Session Sidebar
36
46
 
37
- Every session visible at a glance — name, window count, git branch. Sessions sharing a parent directory are automatically grouped under a header.
47
+ Every session visible at a glance — name, window count, git branch. Sessions sharing a parent directory are automatically grouped under a header. Mouse wheel scrolling when sessions overflow.
38
48
 
39
- - Green `▎` left marker on the active session
49
+ - Green `▎` marker + highlighted background on the active session
40
50
  - Green `●` dot for sessions with new output
41
- - Orange `!` flag for attention (set programmatically)
51
+ - Orange `!` flag for attention (e.g., an agent finished and needs review)
52
+
53
+ ### Smart Window & Pane Titles
54
+
55
+ Window tabs auto-name to the working directory. Pane borders show the running command with automatic detection for tools like Claude Code. No more tabs full of `zsh` or garbled version strings.
42
56
 
43
57
  ### Instant Switching
44
58
 
45
- `Ctrl-Shift-Up/Down` moves between sessions with zero delay. No prefix key, no menu, no mode to enter. Or just click a session in the sidebar.
59
+ `Ctrl-Shift-Up/Down` moves between sessions with zero delay. No prefix key, no menu, no mode to enter. Or just click a session in the sidebar. Indicators only clear when you actually interact with a session — not when you're cycling through.
46
60
 
47
61
  ### New Session Modal
48
62
 
49
63
  `Ctrl-a n` opens a two-step fzf flow: fuzzy-search your git repos for a directory, then name the session. Pre-filled with the directory basename.
50
64
 
51
- ### Window Picker
65
+ ### Bring Your Own Everything
52
66
 
53
- `Ctrl-a j` opens a full-height fzf popup with every window in the current session. Type to filter, Enter to switch.
67
+ jmux works with your existing `~/.tmux.conf`. Your plugins, theme, prefix key, and custom bindings carry over. jmux applies its defaults first, then your config overrides them. Only a small set of core settings are enforced.
54
68
 
55
- ![fzf window picker popup](docs/screenshots/window-picker.png)
69
+ Use any editor. Any Git tool. Any AI agent. Any shell. jmux doesn't replace your tools — it organizes them.
56
70
 
57
- ### Bring Your Own Config
71
+ ### Works Great With
58
72
 
59
- jmux works with your existing `~/.tmux.conf`. Your plugins, theme, prefix key, and custom bindings carry over — jmux applies its defaults first, then your config overrides them. Only a small set of core settings the sidebar needs are enforced.
73
+ - **[wtm](https://github.com/jarredkenny/worktree-manager)** Git worktree manager. Create isolated worktrees for each agent, one session per branch. `wtm create feature-auth --from main` + jmux = parallel agents on parallel branches.
74
+ - **[Claude Code](https://docs.anthropic.com/en/docs/claude-code)** — AI coding agent with built-in attention flag support.
75
+ - **[lazygit](https://github.com/jesseduffield/lazygit)** — Terminal Git UI. Run it in a jmux pane alongside your agent.
76
+ - **[gh](https://cli.github.com/)** / **[glab](https://gitlab.com/gitlab-org/cli)** — GitHub and GitLab CLIs for PRs, issues, and reviews without leaving the terminal.
60
77
 
61
- ### Claude Code Integration
78
+ ### Agent Integration
62
79
 
63
- Built for agentic workflows. Run Claude Code in multiple sessions and get notified when each one finishes.
80
+ Built for running multiple coding agents in parallel. One command sets up attention notifications:
64
81
 
65
82
  ```bash
66
83
  jmux --install-agent-hooks
67
84
  ```
68
85
 
69
- One command adds a hook to `~/.claude/settings.json`. When Claude finishes a response, the orange `!` appears on that session. Switch to it, review the work, move on. See [docs/claude-code-integration.md](docs/claude-code-integration.md) for details.
86
+ When Claude Code finishes a response, the orange `!` appears on that session in the sidebar. Switch to it, review the work, move on. Works with any agent that can run a shell command on completion. See [docs/claude-code-integration.md](docs/claude-code-integration.md) for details.
70
87
 
71
88
  ---
72
89
 
@@ -81,6 +98,7 @@ One command adds a hook to `~/.claude/settings.json`. When Claude finishes a res
81
98
  | `Ctrl-a r` | Rename session |
82
99
  | `Ctrl-a m` | Move window to another session |
83
100
  | Click sidebar | Switch to session |
101
+ | Scroll wheel (sidebar) | Scroll session list |
84
102
 
85
103
  ### Windows
86
104
 
@@ -97,8 +115,9 @@ One command adds a hook to `~/.claude/settings.json`. When Claude finishes a res
97
115
  |-----|--------|
98
116
  | `Ctrl-a \|` | Split horizontal |
99
117
  | `Ctrl-a -` | Split vertical |
100
- | `Shift-arrows` | Navigate panes |
101
- | `Ctrl-a arrows` | Resize panes |
118
+ | `Shift-Left/Right/Up/Down` | Navigate panes (vim-aware) |
119
+ | `Ctrl-a Left/Right/Up/Down` | Resize panes |
120
+ | `Ctrl-a P` | Toggle pane border titles |
102
121
 
103
122
  ### Utilities
104
123
 
@@ -106,7 +125,6 @@ One command adds a hook to `~/.claude/settings.json`. When Claude finishes a res
106
125
  |-----|--------|
107
126
  | `Ctrl-a k` | Clear pane + scrollback |
108
127
  | `Ctrl-a y` | Copy pane to clipboard |
109
- | `Ctrl-a P` | Toggle pane border titles |
110
128
 
111
129
  ---
112
130
 
@@ -115,12 +133,12 @@ One command adds a hook to `~/.claude/settings.json`. When Claude finishes a res
115
133
  Config loads in three layers:
116
134
 
117
135
  ```
118
- config/defaults.conf jmux defaults (baseline)
119
- ~/.tmux.conf your config (overrides defaults)
120
- config/core.conf jmux core (always wins)
136
+ config/defaults.conf <- jmux defaults (baseline)
137
+ ~/.tmux.conf <- your config (overrides defaults)
138
+ config/core.conf <- jmux core (always wins)
121
139
  ```
122
140
 
123
- Override any default in your `~/.tmux.conf` — prefix key, colors, keybindings, plugins. Only four core settings are enforced: `detach-on-destroy off`, `mouse on`, `prefix + n` binding, and empty `status-left`.
141
+ Override any default in your `~/.tmux.conf` — prefix key, colors, keybindings, plugins. Only core settings the sidebar depends on are enforced (`mouse on`, `detach-on-destroy off`, window naming, `status-left`).
124
142
 
125
143
  See [docs/configuration.md](docs/configuration.md) for the full guide.
126
144
 
@@ -130,15 +148,15 @@ See [docs/configuration.md](docs/configuration.md) for the full guide.
130
148
 
131
149
  ```
132
150
  Terminal (Ghostty, iTerm, etc.)
133
- └── jmux (owns the terminal surface)
134
- ├── Sidebar (24 cols) ── session groups, indicators
135
- ├── Border (1 col)
136
- └── tmux PTY (remaining cols)
137
- ├── PTY client ──── @xterm/headless for VT emulation
138
- └── Control client tmux -C for real-time metadata
151
+ +-- jmux (owns the terminal surface)
152
+ +-- Sidebar (26 cols) -- session groups, indicators
153
+ +-- Border (1 col)
154
+ +-- tmux PTY (remaining cols)
155
+ +-- PTY client ---- @xterm/headless for VT emulation
156
+ +-- Control client - tmux -C for real-time metadata
139
157
  ```
140
158
 
141
- ~1500 lines of TypeScript. No opinions about what you run inside tmux.
159
+ ~1800 lines of TypeScript. No opinions about what you run inside tmux.
142
160
 
143
161
  ---
144
162
 
@@ -1,5 +1,5 @@
1
1
  #!/bin/bash
2
- # jmux new session — name + directory picker
2
+ # jmux new session — name + directory picker with wtm worktree support
3
3
  # Called via: display-popup -E "new-session.sh"
4
4
 
5
5
  FZF_COLORS="border:#4f565d,header:#b5bcc9,prompt:#9fe8c3,label:#9fe8c3,pointer:#9fe8c3,fg:#6b7280,fg+:#b5bcc9,hl:#fbd4b8,hl+:#fbd4b8"
@@ -40,26 +40,148 @@ SELECTED_DIR=$(echo "$DISPLAY_DIRS" | fzf \
40
40
  # Expand ~ back to $HOME
41
41
  WORK_DIR="${SELECTED_DIR/#\~/$HOME}"
42
42
 
43
- # Default session name to directory basename
44
- DEFAULT_NAME=$(basename "$WORK_DIR")
43
+ # ─── Step 1.5: Detect wtm bare repo ──────────────────────────────────
45
44
 
46
- # ─── Step 2: Session name ─────────────────────────────────────────────
45
+ # Check if this is a bare repo (wtm-managed) and wtm is available
46
+ IS_BARE=false
47
+ if command -v wtm &>/dev/null && [ -f "$WORK_DIR/.git/config" ]; then
48
+ if git --git-dir="$WORK_DIR/.git" config --get core.bare 2>/dev/null | grep -q "true"; then
49
+ IS_BARE=true
50
+ fi
51
+ fi
47
52
 
48
- SESSION_NAME=$(echo "" | fzf --print-query \
49
- --height=100% \
50
- --layout=reverse \
51
- --border=rounded \
52
- --border-label=" New Session Name " \
53
- --header="Directory: $SELECTED_DIR" \
54
- --header-first \
55
- --prompt="Name: " \
56
- --query="$DEFAULT_NAME" \
57
- --pointer="" \
58
- --no-info \
59
- --color="$FZF_COLORS" \
60
- | head -1)
61
-
62
- [ -z "$SESSION_NAME" ] && exit 0
53
+ if [ "$IS_BARE" = true ]; then
54
+ # ─── wtm flow: create worktree or pick existing ───────────────
55
+
56
+ # List existing worktrees (non-bare)
57
+ EXISTING=$(git --git-dir="$WORK_DIR/.git" worktree list --porcelain 2>/dev/null \
58
+ | grep "^branch " \
59
+ | sed 's|branch refs/heads/||' \
60
+ | sort)
61
+
62
+ # Detect default branch
63
+ DEFAULT_BRANCH=""
64
+ for b in main master develop; do
65
+ if git --git-dir="$WORK_DIR/.git" rev-parse --verify "refs/remotes/origin/$b" &>/dev/null; then
66
+ DEFAULT_BRANCH="$b"
67
+ break
68
+ fi
69
+ done
70
+
71
+ # Build options: existing worktrees + "new worktree" option
72
+ OPTIONS=""
73
+ if [ -n "$EXISTING" ]; then
74
+ OPTIONS=$(echo "$EXISTING" | sed 's/^/ /')
75
+ fi
76
+ OPTIONS=$(printf "+ new worktree\n%s" "$OPTIONS" | grep -v '^$')
77
+
78
+ PROJECT_NAME=$(basename "$WORK_DIR")
79
+ CHOICE=$(echo "$OPTIONS" | fzf \
80
+ --height=100% \
81
+ --layout=reverse \
82
+ --border=rounded \
83
+ --border-label=" $PROJECT_NAME — Worktree " \
84
+ --header="Pick a worktree or create a new one" \
85
+ --header-first \
86
+ --prompt="Branch: " \
87
+ --pointer="▸" \
88
+ --color="$FZF_COLORS")
89
+
90
+ [ -z "$CHOICE" ] && exit 0
91
+
92
+ if [ "$CHOICE" = "+ new worktree" ]; then
93
+ # ─── New worktree: pick base branch, then name ────────────
94
+
95
+ # List remote branches for base selection
96
+ REMOTE_BRANCHES=$(git --git-dir="$WORK_DIR/.git" for-each-ref \
97
+ --format='%(refname:short)' refs/remotes/origin/ 2>/dev/null \
98
+ | sed 's|^origin/||' \
99
+ | grep -v '^HEAD$' \
100
+ | sort)
101
+
102
+ BASE_BRANCH=$(echo "$REMOTE_BRANCHES" | fzf \
103
+ --height=100% \
104
+ --layout=reverse \
105
+ --border=rounded \
106
+ --border-label=" $PROJECT_NAME — Base Branch " \
107
+ --header="Branch to create worktree from" \
108
+ --header-first \
109
+ --prompt="From: " \
110
+ --pointer="▸" \
111
+ --query="$DEFAULT_BRANCH" \
112
+ --color="$FZF_COLORS")
113
+
114
+ [ -z "$BASE_BRANCH" ] && exit 0
115
+
116
+ # Prompt for worktree/branch name
117
+ WORKTREE_NAME=$(echo "" | fzf --print-query \
118
+ --height=100% \
119
+ --layout=reverse \
120
+ --border=rounded \
121
+ --border-label=" $PROJECT_NAME — Branch Name " \
122
+ --header="From: $BASE_BRANCH" \
123
+ --header-first \
124
+ --prompt="Name: " \
125
+ --pointer="" \
126
+ --no-info \
127
+ --color="$FZF_COLORS" \
128
+ | head -1)
129
+
130
+ [ -z "$WORKTREE_NAME" ] && exit 0
131
+
132
+ # Create session in bare repo dir, split into two panes
133
+ WORKTREE_PATH="$WORK_DIR/$WORKTREE_NAME"
134
+ PARENT_CLIENT=$(tmux display-message -p '#{client_name}' 2>/dev/null)
135
+ # Left pane: run wtm create (fetch, hooks visible here)
136
+ tmux new-session -d -s "$WORKTREE_NAME" -c "$WORK_DIR" \
137
+ "wtm create $WORKTREE_NAME --from $BASE_BRANCH --no-shell; cd $WORKTREE_NAME; exec \$SHELL"
138
+ # Right pane: wait for worktree, then open shell
139
+ tmux split-window -h -d -t "$WORKTREE_NAME" -c "$WORK_DIR" \
140
+ "while [ ! -d '$WORKTREE_PATH' ]; do sleep 0.2; done; cd '$WORKTREE_PATH' && exec \$SHELL"
141
+ # Switch to the session (focus on left pane)
142
+ tmux select-pane -t "$WORKTREE_NAME.0"
143
+ tmux switch-client -c "$PARENT_CLIENT" -t "$WORKTREE_NAME"
144
+ exit 0
145
+ else
146
+ # Existing worktree selected — find its path
147
+ BRANCH_NAME=$(echo "$CHOICE" | sed 's/^ //')
148
+ WORKTREE_PATH=$(git --git-dir="$WORK_DIR/.git" worktree list --porcelain 2>/dev/null \
149
+ | awk -v branch="$BRANCH_NAME" '
150
+ /^worktree / { path = substr($0, 10) }
151
+ /^branch / { b = $0; sub(/branch refs\/heads\//, "", b); if (b == branch) print path }
152
+ ')
153
+
154
+ if [ -z "$WORKTREE_PATH" ]; then
155
+ echo "Could not find worktree path for $BRANCH_NAME"
156
+ sleep 2
157
+ exit 1
158
+ fi
159
+
160
+ WORK_DIR="$WORKTREE_PATH"
161
+ SESSION_NAME="$BRANCH_NAME"
162
+ fi
163
+ else
164
+ # ─── Standard flow: just pick a name ──────────────────────────
165
+
166
+ # Default session name to directory basename
167
+ DEFAULT_NAME=$(basename "$WORK_DIR")
168
+
169
+ SESSION_NAME=$(echo "" | fzf --print-query \
170
+ --height=100% \
171
+ --layout=reverse \
172
+ --border=rounded \
173
+ --border-label=" New Session — Name " \
174
+ --header="Directory: $SELECTED_DIR" \
175
+ --header-first \
176
+ --prompt="Name: " \
177
+ --query="$DEFAULT_NAME" \
178
+ --pointer="" \
179
+ --no-info \
180
+ --color="$FZF_COLORS" \
181
+ | head -1)
182
+
183
+ [ -z "$SESSION_NAME" ] && exit 0
184
+ fi
63
185
 
64
186
  # ─── Create session ───────────────────────────────────────────────────
65
187
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jx0/jmux",
3
- "version": "0.3.9",
3
+ "version": "0.4.0",
4
4
  "description": "The terminal workspace for agentic development",
5
5
  "type": "module",
6
6
  "bin": {
package/src/main.ts CHANGED
@@ -12,7 +12,7 @@ import { homedir } from "os";
12
12
 
13
13
  // --- CLI commands (run and exit before TUI) ---
14
14
 
15
- const VERSION = "0.3.9";
15
+ const VERSION = "0.4.0";
16
16
 
17
17
  const HELP = `jmux — a persistent session sidebar for tmux
18
18
 
@@ -108,7 +108,7 @@ function installAgentHooks(): void {
108
108
 
109
109
  // --- TUI startup ---
110
110
 
111
- const SIDEBAR_WIDTH = 24;
111
+ const SIDEBAR_WIDTH = 26;
112
112
  const BORDER_WIDTH = 1;
113
113
  const SIDEBAR_TOTAL = SIDEBAR_WIDTH + BORDER_WIDTH;
114
114
 
@@ -163,7 +163,7 @@ let ptyClientName: string | null = null;
163
163
  let sidebarShown = sidebarVisible;
164
164
  let currentSessions: SessionInfo[] = [];
165
165
  const lastViewedTimestamps = new Map<string, number>();
166
- const sessionDetailsCache = new Map<string, { directory?: string; gitBranch?: string }>();
166
+ const sessionDetailsCache = new Map<string, { directory?: string; gitBranch?: string; project?: string }>();
167
167
 
168
168
  function switchByOffset(offset: number): void {
169
169
  const ids = sidebar.getDisplayOrderIds();
@@ -446,10 +446,31 @@ async function lookupSessionDetails(sessions: SessionInfo[]): Promise<void> {
446
446
  .catch(() => "");
447
447
  const gitBranch = branch.trim() || undefined;
448
448
 
449
+ // Detect wtm worktree — .git is a file pointing to a bare repo
450
+ let project: string | undefined;
451
+ try {
452
+ const commonDir = await $`git -C ${cwd} rev-parse --git-common-dir`
453
+ .text()
454
+ .catch(() => "");
455
+ const gitDir = await $`git -C ${cwd} rev-parse --git-dir`
456
+ .text()
457
+ .catch(() => "");
458
+ if (commonDir.trim() && gitDir.trim() && commonDir.trim() !== gitDir.trim()) {
459
+ // In a worktree — commonDir points to the bare repo's .git
460
+ // Bare repo structure: /path/to/project/.git → project name is parent dir basename
461
+ const resolved = resolve(cwd, commonDir.trim());
462
+ const bareRoot = dirname(resolved);
463
+ project = bareRoot.split("/").pop();
464
+ }
465
+ } catch {
466
+ // Not a worktree
467
+ }
468
+
449
469
  // Write to persistent cache
450
- sessionDetailsCache.set(session.id, { directory, gitBranch });
470
+ sessionDetailsCache.set(session.id, { directory, gitBranch, project });
451
471
  session.directory = directory;
452
472
  session.gitBranch = gitBranch;
473
+ session.project = project;
453
474
  } catch {
454
475
  // Session may not exist or no git repo
455
476
  }
package/src/sidebar.ts CHANGED
@@ -98,12 +98,11 @@ function buildRenderPlan(sessions: SessionInfo[]): {
98
98
  const ungrouped: number[] = [];
99
99
 
100
100
  for (let i = 0; i < sessions.length; i++) {
101
- const dir = sessions[i].directory;
102
- if (!dir) {
103
- ungrouped.push(i);
104
- continue;
105
- }
106
- const label = getGroupLabel(dir);
101
+ // Prefer project name (wtm) over directory-based grouping
102
+ const label = sessions[i].project ?? (() => {
103
+ const dir = sessions[i].directory;
104
+ return dir ? getGroupLabel(dir) : null;
105
+ })();
107
106
  if (!label) {
108
107
  ungrouped.push(i);
109
108
  continue;
package/src/types.ts CHANGED
@@ -36,4 +36,5 @@ export interface SessionInfo {
36
36
  attention: boolean;
37
37
  windowCount: number;
38
38
  directory?: string;
39
+ project?: string; // wtm project name (bare repo basename)
39
40
  }