@jx0/jmux 0.3.8 → 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 +48 -30
- package/config/new-session.sh +141 -19
- package/package.json +1 -1
- package/src/main.ts +28 -17
- package/src/sidebar.ts +5 -6
- package/src/types.ts +1 -0
package/README.md
CHANGED
|
@@ -4,12 +4,12 @@
|
|
|
4
4
|
|
|
5
5
|
**The terminal workspace for agentic development.**
|
|
6
6
|
|
|
7
|
-
|
|
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
|
[](https://www.npmjs.com/package/@jx0/jmux)
|
|
10
10
|
[](LICENSE)
|
|
11
11
|
|
|
12
|
-

|
|
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
|
-
|
|
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
|
|
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 `▎`
|
|
49
|
+
- Green `▎` marker + highlighted background on the active session
|
|
40
50
|
- Green `●` dot for sessions with new output
|
|
41
|
-
- Orange `!` flag for attention (
|
|
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
|
-
###
|
|
65
|
+
### Bring Your Own Everything
|
|
52
66
|
|
|
53
|
-
|
|
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
|
-
|
|
69
|
+
Use any editor. Any Git tool. Any AI agent. Any shell. jmux doesn't replace your tools — it organizes them.
|
|
56
70
|
|
|
57
|
-
###
|
|
71
|
+
### Works Great With
|
|
58
72
|
|
|
59
|
-
|
|
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
|
-
###
|
|
78
|
+
### Agent Integration
|
|
62
79
|
|
|
63
|
-
Built for
|
|
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
|
-
|
|
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-
|
|
101
|
-
| `Ctrl-a
|
|
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
|
|
119
|
-
~/.tmux.conf
|
|
120
|
-
config/core.conf
|
|
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
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
~
|
|
159
|
+
~1800 lines of TypeScript. No opinions about what you run inside tmux.
|
|
142
160
|
|
|
143
161
|
---
|
|
144
162
|
|
package/config/new-session.sh
CHANGED
|
@@ -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
|
-
#
|
|
44
|
-
DEFAULT_NAME=$(basename "$WORK_DIR")
|
|
43
|
+
# ─── Step 1.5: Detect wtm bare repo ──────────────────────────────────
|
|
45
44
|
|
|
46
|
-
#
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
--
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
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.
|
|
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 =
|
|
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();
|
|
@@ -179,19 +179,19 @@ function switchByOffset(offset: number): void {
|
|
|
179
179
|
async function fetchSessions(): Promise<void> {
|
|
180
180
|
try {
|
|
181
181
|
const lines = await control.sendCommand(
|
|
182
|
-
"list-sessions -F '#{session_id}:#{session_name}:#{session_activity}:#{session_attached}:#{session_windows}'",
|
|
182
|
+
"list-sessions -F '#{session_id}:#{session_name}:#{session_activity}:#{session_attached}:#{session_windows}:#{@jmux-attention}'",
|
|
183
183
|
);
|
|
184
184
|
const sessions: SessionInfo[] = lines
|
|
185
185
|
.filter((l) => l.length > 0)
|
|
186
186
|
.map((line) => {
|
|
187
|
-
const [id, name, activity, attached, windows] = line.split(":");
|
|
187
|
+
const [id, name, activity, attached, windows, attn] = line.split(":");
|
|
188
188
|
const cached = sessionDetailsCache.get(id);
|
|
189
189
|
return {
|
|
190
190
|
id,
|
|
191
191
|
name,
|
|
192
192
|
activity: parseInt(activity, 10) || 0,
|
|
193
193
|
attached: attached === "1",
|
|
194
|
-
attention:
|
|
194
|
+
attention: attn === "1",
|
|
195
195
|
windowCount: parseInt(windows, 10) || 1,
|
|
196
196
|
directory: cached?.directory,
|
|
197
197
|
gitBranch: cached?.gitBranch,
|
|
@@ -421,16 +421,6 @@ control.onEvent((event: ControlEvent) => {
|
|
|
421
421
|
break;
|
|
422
422
|
case "subscription-changed":
|
|
423
423
|
if (event.name === "attention") {
|
|
424
|
-
const pairs = event.value.trim().split(/\s+/);
|
|
425
|
-
for (const pair of pairs) {
|
|
426
|
-
const eqIdx = pair.indexOf("=");
|
|
427
|
-
if (eqIdx === -1) continue;
|
|
428
|
-
const id = pair.slice(0, eqIdx);
|
|
429
|
-
const val = pair.slice(eqIdx + 1);
|
|
430
|
-
if (val === "1") {
|
|
431
|
-
sidebar.setActivity(id, false);
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
424
|
fetchSessions();
|
|
435
425
|
}
|
|
436
426
|
break;
|
|
@@ -456,10 +446,31 @@ async function lookupSessionDetails(sessions: SessionInfo[]): Promise<void> {
|
|
|
456
446
|
.catch(() => "");
|
|
457
447
|
const gitBranch = branch.trim() || undefined;
|
|
458
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
|
+
|
|
459
469
|
// Write to persistent cache
|
|
460
|
-
sessionDetailsCache.set(session.id, { directory, gitBranch });
|
|
470
|
+
sessionDetailsCache.set(session.id, { directory, gitBranch, project });
|
|
461
471
|
session.directory = directory;
|
|
462
472
|
session.gitBranch = gitBranch;
|
|
473
|
+
session.project = project;
|
|
463
474
|
} catch {
|
|
464
475
|
// Session may not exist or no git repo
|
|
465
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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;
|