sessionview 0.3.2__tar.gz → 0.3.3__tar.gz
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.
- {sessionview-0.3.2 → sessionview-0.3.3}/PKG-INFO +16 -10
- {sessionview-0.3.2 → sessionview-0.3.3}/README.md +15 -9
- {sessionview-0.3.2 → sessionview-0.3.3}/bin/sessionview-frame +17 -6
- {sessionview-0.3.2 → sessionview-0.3.3}/bin/sessionview-install +9 -5
- {sessionview-0.3.2 → sessionview-0.3.3}/bin/sessionview-messages +17 -7
- {sessionview-0.3.2 → sessionview-0.3.3}/bin/tmux-send-agent +4 -3
- {sessionview-0.3.2 → sessionview-0.3.3}/bin/tmux-sidebar +147 -30
- {sessionview-0.3.2 → sessionview-0.3.3}/config/README.md +4 -3
- {sessionview-0.3.2 → sessionview-0.3.3}/config/config.json +2 -2
- {sessionview-0.3.2 → sessionview-0.3.3}/docs/configuration.md +10 -4
- {sessionview-0.3.2 → sessionview-0.3.3}/docs/settings.md +1 -1
- sessionview-0.3.3/lib/pane-menu.sh +56 -0
- {sessionview-0.3.2 → sessionview-0.3.3}/macos/SessionView.applescript +27 -1
- {sessionview-0.3.2 → sessionview-0.3.3}/pyproject.toml +1 -1
- {sessionview-0.3.2 → sessionview-0.3.3}/src/sessionview/__init__.py +1 -1
- sessionview-0.3.3/tmux/agent-menu.conf +95 -0
- sessionview-0.3.2/tmux/agent-menu.conf +0 -33
- {sessionview-0.3.2 → sessionview-0.3.3}/.gitignore +0 -0
- {sessionview-0.3.2 → sessionview-0.3.3}/LICENSE +0 -0
- {sessionview-0.3.2 → sessionview-0.3.3}/bin/sessionview +0 -0
- {sessionview-0.3.2 → sessionview-0.3.3}/bin/sessionview-app-install +0 -0
- {sessionview-0.3.2 → sessionview-0.3.3}/bin/sessionview-autostart +0 -0
- {sessionview-0.3.2 → sessionview-0.3.3}/bin/sessionview-config +0 -0
- {sessionview-0.3.2 → sessionview-0.3.3}/bin/sessionview-from +0 -0
- {sessionview-0.3.2 → sessionview-0.3.3}/bin/sessionview-newtab +0 -0
- {sessionview-0.3.2 → sessionview-0.3.3}/bin/sessionview-send +0 -0
- {sessionview-0.3.2 → sessionview-0.3.3}/bin/sessionview-session +0 -0
- {sessionview-0.3.2 → sessionview-0.3.3}/bin/sessionview-sidebar-toggle +0 -0
- {sessionview-0.3.2 → sessionview-0.3.3}/bin/sessionview-terminal +0 -0
- {sessionview-0.3.2 → sessionview-0.3.3}/bin/sessionview-usermail +0 -0
- {sessionview-0.3.2 → sessionview-0.3.3}/changelog.d/README.md +0 -0
- {sessionview-0.3.2 → sessionview-0.3.3}/config/keys.md +0 -0
- {sessionview-0.3.2 → sessionview-0.3.3}/config/neomuttrc +0 -0
- {sessionview-0.3.2 → sessionview-0.3.3}/docs/decisions/state-and-log-home.md +0 -0
- {sessionview-0.3.2 → sessionview-0.3.3}/docs/dependencies.md +0 -0
- {sessionview-0.3.2 → sessionview-0.3.3}/docs/sessionview.md +0 -0
- {sessionview-0.3.2 → sessionview-0.3.3}/lib/client.sh +0 -0
- {sessionview-0.3.2 → sessionview-0.3.3}/lib/landing.sh +0 -0
- {sessionview-0.3.2 → sessionview-0.3.3}/lib/python.sh +0 -0
- {sessionview-0.3.2 → sessionview-0.3.3}/lib/sessionview-config-screen +0 -0
- {sessionview-0.3.2 → sessionview-0.3.3}/lib/state.sh +0 -0
- {sessionview-0.3.2 → sessionview-0.3.3}/macos/dreamcatcher.icns +0 -0
- {sessionview-0.3.2 → sessionview-0.3.3}/plugins/session/plugin.toml +0 -0
- {sessionview-0.3.2 → sessionview-0.3.3}/plugins/session/start-agent +0 -0
- {sessionview-0.3.2 → sessionview-0.3.3}/plugins/taskmaster/plugin.toml +0 -0
- {sessionview-0.3.2 → sessionview-0.3.3}/plugins/taskmaster/tasks-tab +0 -0
- {sessionview-0.3.2 → sessionview-0.3.3}/plugins/voicemode/plugin.toml +0 -0
- {sessionview-0.3.2 → sessionview-0.3.3}/plugins/voicemode/speak +0 -0
- {sessionview-0.3.2 → sessionview-0.3.3}/skills/tmux/SKILL.md +0 -0
- {sessionview-0.3.2 → sessionview-0.3.3}/skills/tmux/scripts/tmux-peek +0 -0
- {sessionview-0.3.2 → sessionview-0.3.3}/skills/tmux/scripts/tmux-show +0 -0
- {sessionview-0.3.2 → sessionview-0.3.3}/skills/tmux/scripts/tmux-type +0 -0
- {sessionview-0.3.2 → sessionview-0.3.3}/skills/tmux/scripts/tmux-where +0 -0
- {sessionview-0.3.2 → sessionview-0.3.3}/tmux/frame.conf +0 -0
- {sessionview-0.3.2 → sessionview-0.3.3}/tmux/tabs-top.conf +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: sessionview
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.3
|
|
4
4
|
Summary: A sidebar for tmux: your projects (sessions) and your agents, here and on remote hosts.
|
|
5
5
|
Project-URL: Homepage, https://github.com/sessionkit/sessionview
|
|
6
6
|
Project-URL: Repository, https://github.com/sessionkit/sessionview
|
|
@@ -285,7 +285,9 @@ beside it. Each line says what puts it there and what takes it away.
|
|
|
285
285
|
- **Services (launchd agents):** none. sessionview runs no daemon and
|
|
286
286
|
writes nothing in `~/Library/LaunchAgents/`. While a frame is open it
|
|
287
287
|
runs a tmux server of its own, `sessionview-frame`, beside yours; the
|
|
288
|
-
server exits when the last frame closes.
|
|
288
|
+
server exits when the last frame closes. With `pane_menu` on, an
|
|
289
|
+
Option + right-click on that server's pane that holds your tmux goes
|
|
290
|
+
through to yours.
|
|
289
291
|
- **Plugin and skills, optional:** `claude plugin marketplace add` and
|
|
290
292
|
`claude plugin install` ([Install](#install)). Claude Code names the
|
|
291
293
|
marketplace and the plugin in `~/.claude/settings.json`
|
|
@@ -310,9 +312,10 @@ beside it. Each line says what puts it there and what takes it away.
|
|
|
310
312
|
mouse bindings on the status line (`MouseDown1Status`,
|
|
311
313
|
`MouseDown1StatusRight`, `MouseDrag1Status`). It gives each session it
|
|
312
314
|
opens the sessionview look, with options set on that session only.
|
|
313
|
-
With `pane_menu` on, it also binds
|
|
314
|
-
(`MouseDown3Pane`
|
|
315
|
-
|
|
315
|
+
With `pane_menu` on, it also binds both right-clicks in a pane
|
|
316
|
+
(`MouseDown3Pane`, and `M-MouseDown3Pane` for Option + right-click),
|
|
317
|
+
in place of yours, and sets two global `@sessionview_agent_menu_`
|
|
318
|
+
options. Nothing is written to
|
|
316
319
|
`~/.tmux.conf`: these last until that tmux server exits.
|
|
317
320
|
- **Outside these folders, only on a verb:** with `fullscreen` on, the
|
|
318
321
|
Dock app on Terminal.app needs the Accessibility permission you grant
|
|
@@ -372,17 +375,20 @@ keep theirs beside it in `~/.sessionkit/`.
|
|
|
372
375
|
picker (type to filter, ↑↓, enter); `+` at the bottom → start an
|
|
373
376
|
agent; the store name → that store's session and store view, its `⋯`
|
|
374
377
|
(or `s`) → Switch store, right-click → Kick; `all · grouped` on the agents
|
|
375
|
-
heading are toggles; drag the border to resize; `q` reloads, `Q`
|
|
378
|
+
heading are toggles (the first cycles all / store / project); drag the border to resize; `q` reloads, `Q`
|
|
376
379
|
quits, esc backs out of any view
|
|
377
380
|
- tab bar: `◧` at the far left hides/shows the sidebar; click selects,
|
|
378
381
|
drag reorders, `+` new tab (the picker); a close `×`, on by default;
|
|
379
382
|
`tmux set -g @sessionview_tab_close 0` turns it off
|
|
380
383
|
- menu: ↑↓ / j k move, the letter or enter picks, esc or a click
|
|
381
384
|
anywhere else closes
|
|
382
|
-
- agent panes (opt-in): right-click an agent's
|
|
383
|
-
Read Clipboard, Park or Succeed; each mails
|
|
384
|
-
|
|
385
|
-
|
|
385
|
+
- agent panes (opt-in): right-click or Option + right-click an agent's
|
|
386
|
+
pane for Speak, Continue, Read Clipboard, Park or Succeed; each mails
|
|
387
|
+
that agent. It opens even on an agent whose program uses the mouse, as
|
|
388
|
+
Claude Code does. An agent's pane is one whose title is
|
|
389
|
+
`<glyph> <address>`. Turn it on with
|
|
390
|
+
`sessionview-config set pane_menu true`. It replaces your own tmux
|
|
391
|
+
right-click menus, which is why it is off by default. Using tmux without session view? Add
|
|
386
392
|
`source-file -q /path/to/sessionview/tmux/agent-menu.conf` to
|
|
387
393
|
`~/.tmux.conf` and reload.
|
|
388
394
|
|
|
@@ -268,7 +268,9 @@ beside it. Each line says what puts it there and what takes it away.
|
|
|
268
268
|
- **Services (launchd agents):** none. sessionview runs no daemon and
|
|
269
269
|
writes nothing in `~/Library/LaunchAgents/`. While a frame is open it
|
|
270
270
|
runs a tmux server of its own, `sessionview-frame`, beside yours; the
|
|
271
|
-
server exits when the last frame closes.
|
|
271
|
+
server exits when the last frame closes. With `pane_menu` on, an
|
|
272
|
+
Option + right-click on that server's pane that holds your tmux goes
|
|
273
|
+
through to yours.
|
|
272
274
|
- **Plugin and skills, optional:** `claude plugin marketplace add` and
|
|
273
275
|
`claude plugin install` ([Install](#install)). Claude Code names the
|
|
274
276
|
marketplace and the plugin in `~/.claude/settings.json`
|
|
@@ -293,9 +295,10 @@ beside it. Each line says what puts it there and what takes it away.
|
|
|
293
295
|
mouse bindings on the status line (`MouseDown1Status`,
|
|
294
296
|
`MouseDown1StatusRight`, `MouseDrag1Status`). It gives each session it
|
|
295
297
|
opens the sessionview look, with options set on that session only.
|
|
296
|
-
With `pane_menu` on, it also binds
|
|
297
|
-
(`MouseDown3Pane`
|
|
298
|
-
|
|
298
|
+
With `pane_menu` on, it also binds both right-clicks in a pane
|
|
299
|
+
(`MouseDown3Pane`, and `M-MouseDown3Pane` for Option + right-click),
|
|
300
|
+
in place of yours, and sets two global `@sessionview_agent_menu_`
|
|
301
|
+
options. Nothing is written to
|
|
299
302
|
`~/.tmux.conf`: these last until that tmux server exits.
|
|
300
303
|
- **Outside these folders, only on a verb:** with `fullscreen` on, the
|
|
301
304
|
Dock app on Terminal.app needs the Accessibility permission you grant
|
|
@@ -355,17 +358,20 @@ keep theirs beside it in `~/.sessionkit/`.
|
|
|
355
358
|
picker (type to filter, ↑↓, enter); `+` at the bottom → start an
|
|
356
359
|
agent; the store name → that store's session and store view, its `⋯`
|
|
357
360
|
(or `s`) → Switch store, right-click → Kick; `all · grouped` on the agents
|
|
358
|
-
heading are toggles; drag the border to resize; `q` reloads, `Q`
|
|
361
|
+
heading are toggles (the first cycles all / store / project); drag the border to resize; `q` reloads, `Q`
|
|
359
362
|
quits, esc backs out of any view
|
|
360
363
|
- tab bar: `◧` at the far left hides/shows the sidebar; click selects,
|
|
361
364
|
drag reorders, `+` new tab (the picker); a close `×`, on by default;
|
|
362
365
|
`tmux set -g @sessionview_tab_close 0` turns it off
|
|
363
366
|
- menu: ↑↓ / j k move, the letter or enter picks, esc or a click
|
|
364
367
|
anywhere else closes
|
|
365
|
-
- agent panes (opt-in): right-click an agent's
|
|
366
|
-
Read Clipboard, Park or Succeed; each mails
|
|
367
|
-
|
|
368
|
-
|
|
368
|
+
- agent panes (opt-in): right-click or Option + right-click an agent's
|
|
369
|
+
pane for Speak, Continue, Read Clipboard, Park or Succeed; each mails
|
|
370
|
+
that agent. It opens even on an agent whose program uses the mouse, as
|
|
371
|
+
Claude Code does. An agent's pane is one whose title is
|
|
372
|
+
`<glyph> <address>`. Turn it on with
|
|
373
|
+
`sessionview-config set pane_menu true`. It replaces your own tmux
|
|
374
|
+
right-click menus, which is why it is off by default. Using tmux without session view? Add
|
|
369
375
|
`source-file -q /path/to/sessionview/tmux/agent-menu.conf` to
|
|
370
376
|
`~/.tmux.conf` and reload.
|
|
371
377
|
|
|
@@ -119,12 +119,16 @@ FRESH_WIN=""
|
|
|
119
119
|
# global config is untouched (a normal tmux session keeps its bar at the
|
|
120
120
|
# bottom).
|
|
121
121
|
sh -c 'S="$1"; L="$3"; tmux() { command tmux -L "$L" "$@"; }; . "$2"' sh "$SESSION" "$SESSIONVIEW_ROOT/tmux/frame.conf" "$SOCK"
|
|
122
|
-
# the agent pane menu (right-click an agent's pane:
|
|
123
|
-
# the user turned it on. Opt-in:
|
|
124
|
-
#
|
|
125
|
-
#
|
|
126
|
-
#
|
|
127
|
-
|
|
122
|
+
# the agent pane menu (right-click or Option + right-click an agent's pane:
|
|
123
|
+
# Speak / Continue / ...), IF the user turned it on. Opt-in: its two bind-keys
|
|
124
|
+
# are global to the server and replace both of the user's own right-click
|
|
125
|
+
# bindings (MouseDown3Pane, M-MouseDown3Pane). Sourced into the running
|
|
126
|
+
# server, so it works whether or not tmux was already up. Fails open, like the
|
|
127
|
+
# rest of the frame's setup. See tmux/agent-menu.conf. Read once: the frame's
|
|
128
|
+
# own server acts on the same answer below (the Option + right-click
|
|
129
|
+
# pass-through).
|
|
130
|
+
PANE_MENU="$("$SESSIONVIEW_ROOT/bin/sessionview-config" get pane_menu 2>/dev/null || true)"
|
|
131
|
+
if [ "$PANE_MENU" = true ]; then
|
|
128
132
|
"$TMUX_BIN" -L "$SOCK" source-file "$SESSIONVIEW_ROOT/tmux/agent-menu.conf" 2>/dev/null || true
|
|
129
133
|
fi
|
|
130
134
|
# the chosen agent, if the user has one - asked once, remembered, started in
|
|
@@ -194,6 +198,13 @@ o set -w -t "=$F:0" pane-active-border-style "fg=#3a3f4d,bg=#21252e"
|
|
|
194
198
|
o set -w -t "=$F:0" pane-border-lines single
|
|
195
199
|
o bind-key -T root MouseDown1Border "resize-pane -M"
|
|
196
200
|
o bind-key -T root MouseDrag1Border "resize-pane -M"
|
|
201
|
+
# with pane_menu on, an Option + right-click on the client pane goes through to
|
|
202
|
+
# your tmux and its agent menu; the frame's other panes keep the binding this
|
|
203
|
+
# server had. Here, after new-session: before it this server may not exist.
|
|
204
|
+
# Off, nothing is touched. Fails open. See lib/pane-menu.sh.
|
|
205
|
+
if [ "$PANE_MENU" = true ]; then
|
|
206
|
+
{ . "$SESSIONVIEW_ROOT/lib/pane-menu.sh" && sv_frame_option_forward "$FRAME_SOCK"; } >/dev/null 2>&1 || true
|
|
207
|
+
fi
|
|
197
208
|
MAIN="$(o split-window -h -P -F '#{pane_id}' -t "$SIDE" -e "TMUX=" \
|
|
198
209
|
"exec \"$TMUX_BIN\" -L \"$SOCK\" attach -t \"=$SESSION\"")"
|
|
199
210
|
# the frame publishes its own panes, so the sidebar process (which cannot see
|
|
@@ -168,13 +168,17 @@ else
|
|
|
168
168
|
say WARN "neomutt not on PATH - the messages tab, which sessionview makes only when sessionmail is installed, cannot open (brew install neomutt)"
|
|
169
169
|
fi
|
|
170
170
|
# notmuch - the messages tab's sidebar queries Type: and Project:; unindexed, the Asks
|
|
171
|
-
# rows are every mail to NAME+ask, with no error
|
|
171
|
+
# rows are every mail to NAME+ask, with no error. Asked of sessionmail's notmuch profile
|
|
172
|
+
# when `message path --notmuch-config` names one (sessionmail 0.3.3), as the tab does;
|
|
173
|
+
# no message, an older one or no profile: notmuch's own default config.
|
|
172
174
|
if command -v notmuch >/dev/null 2>&1; then
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
175
|
+
NMCFG="$(message path --notmuch-config 2>/dev/null)" || NMCFG=""
|
|
176
|
+
NM="notmuch${NMCFG:+ --config=$NMCFG}"
|
|
177
|
+
if [ "$(notmuch ${NMCFG:+"--config=$NMCFG"} config get index.header.Type 2>/dev/null)" = Type ] &&
|
|
178
|
+
[ "$(notmuch ${NMCFG:+"--config=$NMCFG"} config get index.header.Project 2>/dev/null)" = Project ]; then
|
|
179
|
+
say PASS "notmuch indexes Type and Project${NMCFG:+ ($NMCFG)}"
|
|
176
180
|
else
|
|
177
|
-
say WARN "notmuch does not index Type and Project - the Asks rows show every mail to NAME+ask:
|
|
181
|
+
say WARN "notmuch${NMCFG:+ ($NMCFG)} does not index Type and Project - the Asks rows show every mail to NAME+ask: $NM config set index.header.Type Type; $NM config set index.header.Project Project; $NM reindex '*'"
|
|
178
182
|
fi
|
|
179
183
|
else say WARN "notmuch not on PATH - the messages tab has no sidebar (brew install notmuch)"; fi
|
|
180
184
|
# optional
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
# named `messages` in both store and project sessions. Three facts, each
|
|
9
9
|
# measured:
|
|
10
10
|
# the folder `message path [SLUG/]PROJECT` (sessionmail 2454f6f) prints
|
|
11
|
-
# the project's maildir,
|
|
11
|
+
# the project's maildir, wherever sessionmail keeps it;
|
|
12
12
|
# exit 2 when there is none.
|
|
13
13
|
# the viewer `neomutt -R -F RC -f FOLDER`. -R (read-only) is not optional:
|
|
14
14
|
# without it, reading a message moves it new/ -> cur/ and the
|
|
@@ -41,6 +41,9 @@
|
|
|
41
41
|
# the rows are every mail to PROJECT+ask, replies too, and the rc says so;
|
|
42
42
|
# without notmuch, or a folder notmuch does not hold, the tab is the one
|
|
43
43
|
# folder as before and the sidebar is hidden. --print names the reason.
|
|
44
|
+
# notmuch here, and neomutt's (nm_config_file), read sessionmail's notmuch
|
|
45
|
+
# profile when `message path --notmuch-config` names one (sessionmail 0.3.3);
|
|
46
|
+
# an older sessionmail, or no profile (exit 1 or 2), is notmuch's own default.
|
|
44
47
|
# Ctrl-o (sidebar-open) is the macOS tty's `discard` char and never reaches
|
|
45
48
|
# neomutt; `stty discard undef` runs below, in the shell that owns the tty
|
|
46
49
|
# (measured by the first user of this layout: a backtick inside
|
|
@@ -92,19 +95,26 @@ case "$FOLDER" in *[\"\`\\\$]*|*[[:cntrl:]]*)
|
|
|
92
95
|
[ -d "$FOLDER" ] || { echo "sessionview-messages: $BOX: $FOLDER is not a directory" >&2; exit 2; }
|
|
93
96
|
# The sidebar's rows: per project, so written here, not in config/neomuttrc.
|
|
94
97
|
SIDEBAR=""; SIDEBAR_WHY=""
|
|
95
|
-
|
|
96
|
-
|
|
98
|
+
NMCFG="$(message path --notmuch-config 2>/dev/null)" || NMCFG=""
|
|
99
|
+
nm() { notmuch ${NMCFG:+"--config=$NMCFG"} "$@"; }
|
|
100
|
+
NM="notmuch${NMCFG:+ --config=$NMCFG}" # the words in a fix line
|
|
101
|
+
case "$NMCFG" in *[\"\`\\\$]*|*[[:cntrl:]]*)
|
|
102
|
+
SIDEBAR_WHY="sessionmail's notmuch config refused: its path holds a \" \` \\ \$ or control character, which the rc would run or break on";; esac
|
|
103
|
+
if [ -n "$SIDEBAR_WHY" ]; then :
|
|
104
|
+
elif command -v notmuch >/dev/null 2>&1; then
|
|
105
|
+
DB="$(nm config get database.path 2>/dev/null)"
|
|
97
106
|
if [ -z "$DB" ]; then SIDEBAR_WHY="notmuch has no database.path"
|
|
98
107
|
else case "$FOLDER/" in
|
|
99
108
|
"$DB"/*)
|
|
100
|
-
if [ "$(
|
|
101
|
-
[ "$(
|
|
109
|
+
if [ "$(nm config get index.header.Type 2>/dev/null)" = Type ] &&
|
|
110
|
+
[ "$(nm config get index.header.Project 2>/dev/null)" = Project ]; then
|
|
102
111
|
Q="Type:ask and (to:\\\"$NAME+ask\\\" or Project:\\\"$NAME\\\")"
|
|
103
112
|
else
|
|
104
113
|
Q="to:\\\"$NAME+ask\\\""
|
|
105
|
-
SIDEBAR_WHY="notmuch does not index Type and Project, so Asks is every mail to $NAME+ask, replies too -
|
|
114
|
+
SIDEBAR_WHY="notmuch does not index Type and Project, so Asks is every mail to $NAME+ask, replies too - $NM config set index.header.Type Type; $NM config set index.header.Project Project; $NM reindex '*'"
|
|
106
115
|
fi
|
|
107
|
-
SIDEBAR="$(printf 'set nm_default_url="notmuch://%s"\nset nm_query_type=messages\nset virtual_spoolfile=no\nnamed-mailboxes "%s" "%s"\nvirtual-mailboxes "Asks" "notmuch://?query=%s"\nvirtual-mailboxes "Asks · threads" "notmuch://?type=threads&query=%s"\n' "$DB" "$NAME" "$FOLDER" "$Q" "$Q")"
|
|
116
|
+
SIDEBAR="$(printf 'set nm_default_url="notmuch://%s"\nset nm_query_type=messages\nset virtual_spoolfile=no\nnamed-mailboxes "%s" "%s"\nvirtual-mailboxes "Asks" "notmuch://?query=%s"\nvirtual-mailboxes "Asks · threads" "notmuch://?type=threads&query=%s"\n' "$DB" "$NAME" "$FOLDER" "$Q" "$Q")"
|
|
117
|
+
[ -z "$NMCFG" ] || SIDEBAR="set nm_config_file=\"$NMCFG\""$'\n'"$SIDEBAR";;
|
|
108
118
|
*) SIDEBAR_WHY="$FOLDER is not under notmuch database.path $DB";;
|
|
109
119
|
esac; fi
|
|
110
120
|
else SIDEBAR_WHY="no notmuch on PATH (brew install notmuch)"; fi
|
|
@@ -51,9 +51,10 @@ to=""
|
|
|
51
51
|
# the session box: the last +field, :field or .field that is 8 hex
|
|
52
52
|
for cand in "${title##*+}" "${title##*:}" "${title##*.}"; do
|
|
53
53
|
if printf '%s' "$cand" | grep -qE '^[0-9a-f]{8}$'; then
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
# sessionmail names the box: the first `session ID` row of `message box
|
|
55
|
+
# list`. Where its mail root is, is sessionmail's to know, never ours to
|
|
56
|
+
# spell. A refusal or no row leaves `to` empty: the seat address below.
|
|
57
|
+
to="$("$message_bin" box list "$cand*" --all 2>/dev/null | awk '$1=="session"{print $2; exit}' || true)"
|
|
57
58
|
break
|
|
58
59
|
fi
|
|
59
60
|
done
|
|
@@ -477,16 +477,50 @@ def paint(text, selected):
|
|
|
477
477
|
WIDTH = int(CFG["width"])
|
|
478
478
|
REFRESH = float(CFG["refresh"])
|
|
479
479
|
GLYPH = {"busy": fg("#f9e2af") + "◐" + NOFG, "idle": fg("#a6e3a1") + "●" + NOFG,
|
|
480
|
-
"
|
|
481
|
-
|
|
482
|
-
|
|
480
|
+
"blocked": fg("#f38ba8") + "■" + NOFG, "waiting": fg("#f38ba8") + "■" + NOFG}
|
|
481
|
+
RANK = {"blocked": 0, "waiting": 0, "busy": 1, "idle": 2}
|
|
482
|
+
# A seat's condition, in the sidebar's words: blocked, waiting, busy or idle.
|
|
483
|
+
# Every glyph, sort and rollup asks seat_word(); nothing else reads `status`
|
|
484
|
+
# for that. `state` is the session tools' word (busy | idle with children |
|
|
485
|
+
# idle) and decides busy against idle, but it cannot say "a prompt blocks the
|
|
486
|
+
# turn" (`waiting` reads idle there), so the attention words come from the
|
|
487
|
+
# raw `status` first. A feed with no `state` (an older session tools) has its
|
|
488
|
+
# `status` mapped instead. Claude Code writes `shell`, not `idle`, for a seat
|
|
489
|
+
# at its prompt whenever a background shell lives (a listener left running is
|
|
490
|
+
# one), so most idle seats say `shell`: that is idle.
|
|
491
|
+
ATTENTION = ("blocked", "waiting")
|
|
492
|
+
STATE_WORD = {"busy": "busy", "idle": "idle", "idle with children": "idle"}
|
|
493
|
+
STATUS_WORD = {"busy": "busy", "idle": "idle", "shell": "idle"}
|
|
494
|
+
|
|
495
|
+
def seat_word(a):
|
|
496
|
+
status = a.get("status")
|
|
497
|
+
if status in ATTENTION:
|
|
498
|
+
return status
|
|
499
|
+
state = a.get("state")
|
|
500
|
+
if state:
|
|
501
|
+
return STATE_WORD.get(state, state)
|
|
502
|
+
return STATUS_WORD.get(status, status)
|
|
503
|
+
|
|
483
504
|
MOUSE_RE = re.compile(r"\x1b\[<(\d+);(\d+);(\d+)([mM])")
|
|
484
505
|
ANSI_RE = re.compile(r"\x1b\[[0-9;]*m")
|
|
485
506
|
|
|
486
507
|
POPUP = "--popup" in sys.argv # inside `tmux display-popup -E`: a jump closes us
|
|
487
508
|
PICKER = None # the ONE type-and-filter box (new session / new tab / start an agent); see open_picker
|
|
488
509
|
GROUPED = CFG.get("group_by", "none") != "none"
|
|
489
|
-
|
|
510
|
+
# The agents list's scopes, in the order a click on the heading word cycles
|
|
511
|
+
# them: widest to narrowest. scope_filter() is what each one keeps.
|
|
512
|
+
SCOPES = ("all", "store", "project")
|
|
513
|
+
# The scope the sidebar starts in: "all" | "store" | "project". An OPEN
|
|
514
|
+
# choice, not a decision: "all" is today's behaviour, kept until the
|
|
515
|
+
# owner chooses. A one-word edit; every option is already tested.
|
|
516
|
+
FILTER_DEFAULT = "all"
|
|
517
|
+
# What the `store` scope shows when the selected project is in no store
|
|
518
|
+
# (no store owns the viewed server, or the selected session is not one of
|
|
519
|
+
# its projects): "project" | "pinned" | "nostore" | "all". An OPEN choice
|
|
520
|
+
# too, and PROVISIONAL: "all" is the one option that cannot hide a seat.
|
|
521
|
+
# A one-word edit; scope_filter() implements and tests all four.
|
|
522
|
+
STORE_SCOPE_NO_STORE = "all"
|
|
523
|
+
FILTER = FILTER_DEFAULT # all | store | project; see scope_filter
|
|
490
524
|
MENU = None # the project menu, drawn IN the sidebar (see project_menu)
|
|
491
525
|
PROMPT = None # an inline text/confirm line, drawn IN the sidebar
|
|
492
526
|
# The row a menu opens beside, so a menu appears close to the row that
|
|
@@ -996,6 +1030,30 @@ def row_store():
|
|
|
996
1030
|
on = [s for s in stores() if (s.get("socket") or "default") == SOCK]
|
|
997
1031
|
return next((s for s in on if s.get("active")), None) or (on[0] if on else {})
|
|
998
1032
|
|
|
1033
|
+
_STORE_PROJ = {}
|
|
1034
|
+
def store_projects(rec):
|
|
1035
|
+
"""The set of tm project names in store REC, by `tm project list
|
|
1036
|
+
--store PATH`, cached 60s per store path (projects_recent's lifetime).
|
|
1037
|
+
The empty set when REC has no path, tm is absent, or its answer is not
|
|
1038
|
+
JSON. Only the `store` scope asks: `all` and `project` cost nothing.
|
|
1039
|
+
Not projects_recent(): that mixes in checkouts on disk
|
|
1040
|
+
(repo_projects), and a checkout is not a store project."""
|
|
1041
|
+
root = (rec or {}).get("path") or ""
|
|
1042
|
+
if not root:
|
|
1043
|
+
return set()
|
|
1044
|
+
root = os.path.realpath(root)
|
|
1045
|
+
now = time.time()
|
|
1046
|
+
hit = _STORE_PROJ.get(root)
|
|
1047
|
+
if hit and now - hit[0] < 60:
|
|
1048
|
+
return set(hit[1])
|
|
1049
|
+
try:
|
|
1050
|
+
rows = json.loads(sh(["tm", "project", "list", "-f", "json", "--store", root]) or "[]")
|
|
1051
|
+
names = {p.get("name", "") for p in rows if isinstance(p, dict)} - {""}
|
|
1052
|
+
except (json.JSONDecodeError, TypeError, AttributeError):
|
|
1053
|
+
names = set()
|
|
1054
|
+
_STORE_PROJ[root] = (now, frozenset(names))
|
|
1055
|
+
return set(names)
|
|
1056
|
+
|
|
999
1057
|
def open_store(name):
|
|
1000
1058
|
"""The store row's name, and a pick in the Switch store modal: open
|
|
1001
1059
|
NAME's store session and look at it. A down server is booted by
|
|
@@ -1768,7 +1826,7 @@ def agent_fields(a, window="", dups=(), model_w=0):
|
|
|
1768
1826
|
numbered seat (`alice-e9`): two rows with the same name fall back to
|
|
1769
1827
|
showing the extra number."""
|
|
1770
1828
|
f = _Safe(a)
|
|
1771
|
-
f["glyph"] = GLYPH.get(a
|
|
1829
|
+
f["glyph"] = GLYPH.get(seat_word(a), "?")
|
|
1772
1830
|
# Right-hand columns line up row to row: the model is right-justified to
|
|
1773
1831
|
# the widest one on screen, the context to 4 (`100%` is real). The dash
|
|
1774
1832
|
# is left padded too, so the right-hand side lines up row to row.
|
|
@@ -1810,9 +1868,9 @@ def agent_fields(a, window="", dups=(), model_w=0):
|
|
|
1810
1868
|
# sweeps leftovers at start, so a glyph cannot outlive the sidebar by much.
|
|
1811
1869
|
_TABS = {}
|
|
1812
1870
|
WRITE_TABS = True
|
|
1813
|
-
TAB_CH = {"busy": "\u25d0", "idle": "\u25cf",
|
|
1871
|
+
TAB_CH = {"busy": "\u25d0", "idle": "\u25cf",
|
|
1814
1872
|
"blocked": "\u25a0", "waiting": "\u25a0"}
|
|
1815
|
-
TAB_FG = {"busy": "#f9e2af", "idle": "#a6e3a1",
|
|
1873
|
+
TAB_FG = {"busy": "#f9e2af", "idle": "#a6e3a1",
|
|
1816
1874
|
"blocked": "#f38ba8", "waiting": "#f38ba8"}
|
|
1817
1875
|
|
|
1818
1876
|
def tab_glyphs(by_win, pl):
|
|
@@ -1828,7 +1886,7 @@ def tab_glyphs(by_win, pl):
|
|
|
1828
1886
|
want = {}
|
|
1829
1887
|
for w, states in by_win.items():
|
|
1830
1888
|
st = rollup(states)
|
|
1831
|
-
if st:
|
|
1889
|
+
if st in TAB_CH: # a word with no glyph draws none
|
|
1832
1890
|
want[w] = st
|
|
1833
1891
|
wins = {f"{p[1]}:{p[6]}" for p in pl}
|
|
1834
1892
|
for w in [k for k in set(_TABS) | set(want) if k != "_swept"]:
|
|
@@ -1854,12 +1912,13 @@ def tab_clear():
|
|
|
1854
1912
|
_TABS.clear()
|
|
1855
1913
|
|
|
1856
1914
|
def sort_key(a, keys=None):
|
|
1857
|
-
"""A row's place by the `sort` config (KEYS, default CFG["sort"]):
|
|
1858
|
-
rank, then idle, as configured. Pins are not in
|
|
1915
|
+
"""A row's place by the `sort` config (KEYS, default CFG["sort"]): the
|
|
1916
|
+
seat's rank (seat_word), then idle, as configured. Pins are not in
|
|
1917
|
+
here; see order_agents."""
|
|
1859
1918
|
key = []
|
|
1860
1919
|
for k in (CFG["sort"] if keys is None else keys):
|
|
1861
1920
|
if k == "rank":
|
|
1862
|
-
key.append(RANK.get(a
|
|
1921
|
+
key.append(RANK.get(seat_word(a), 9))
|
|
1863
1922
|
else:
|
|
1864
1923
|
v = a.get(k)
|
|
1865
1924
|
key.append(v if v is not None else (0 if k.endswith("_s") else ""))
|
|
@@ -1941,8 +2000,9 @@ def order_agents(ag, pinned=(), keys=None, grp_of=None, group_order=None, within
|
|
|
1941
2000
|
are stable, so the order within a group (and within the pinned block)
|
|
1942
2001
|
is the first sort's. WITHIN given too: a non-pinned row's key
|
|
1943
2002
|
inside its group is WITHIN(row), ties keeping the first sort's order;
|
|
1944
|
-
the pinned block keeps its pin order. Filtering
|
|
1945
|
-
|
|
2003
|
+
the pinned block keeps its pin order. Filtering is the caller's, BEFORE
|
|
2004
|
+
this: `hide` drops rows, pinned or not; the scope filter (scope_filter)
|
|
2005
|
+
keeps every pinned row. A pin orders a row; this function never adds one."""
|
|
1946
2006
|
pins = pin_rank(pinned)
|
|
1947
2007
|
def pin(a):
|
|
1948
2008
|
return pins.get(a.get("agent"), len(pins))
|
|
@@ -1958,6 +2018,57 @@ def order_agents(ag, pinned=(), keys=None, grp_of=None, group_order=None, within
|
|
|
1958
2018
|
out.sort(key=gkey)
|
|
1959
2019
|
return out
|
|
1960
2020
|
|
|
2021
|
+
def scope_filter(ag, scope, p2s, cur_sess, pinned=(), store_sessions=None, no_store=None):
|
|
2022
|
+
"""The rows of AG that SCOPE shows, in AG's order, pure: AG and its
|
|
2023
|
+
rows are not changed. It runs after `hide` and BEFORE order_agents.
|
|
2024
|
+
|
|
2025
|
+
A pinned row (its `agent` is in PINNED) is kept under every scope. A
|
|
2026
|
+
pinned row whose status is in `hide` stays hidden: `hide` runs first and
|
|
2027
|
+
is the user's own standing rule, not a scope.
|
|
2028
|
+
|
|
2029
|
+
P2S maps a pane to its session on the VIEWED server; CUR_SESS is the
|
|
2030
|
+
selected session. STORE_SESSIONS is the current store's session set
|
|
2031
|
+
(its tm projects plus its own session), or None when no store owns the
|
|
2032
|
+
viewed server. A remote row (`_host`) is never local: its pane id is the
|
|
2033
|
+
remote server's, and pane ids collide across servers.
|
|
2034
|
+
|
|
2035
|
+
- all: every row.
|
|
2036
|
+
- project: the local rows whose session is CUR_SESS.
|
|
2037
|
+
- store, CUR_SESS in STORE_SESSIONS: the local rows whose session is in
|
|
2038
|
+
STORE_SESSIONS.
|
|
2039
|
+
- store, anywhere else (no store, or the selected session is not one of
|
|
2040
|
+
its projects): NO_STORE says, default STORE_SCOPE_NO_STORE -
|
|
2041
|
+
"project" the project rule; "pinned" the pins only; "nostore" the
|
|
2042
|
+
local rows on the viewed server whose session is not in the store
|
|
2043
|
+
(with no store, every local row on the viewed server); "all" every row.
|
|
2044
|
+
It keeps to the viewed server, like the other two scopes: a seat with
|
|
2045
|
+
no pane here has no session here to compare.
|
|
2046
|
+
An unknown SCOPE or NO_STORE shows every row: a typo never hides a seat."""
|
|
2047
|
+
pins = pin_rank(pinned)
|
|
2048
|
+
if no_store is None:
|
|
2049
|
+
no_store = STORE_SCOPE_NO_STORE
|
|
2050
|
+
|
|
2051
|
+
def sess(a): # the row's session on the viewed server, or ""
|
|
2052
|
+
return "" if a.get("_host") else (p2s.get(a.get("tmux_pane") or "") or "")
|
|
2053
|
+
|
|
2054
|
+
def in_project(a):
|
|
2055
|
+
return bool(cur_sess) and sess(a) == cur_sess
|
|
2056
|
+
|
|
2057
|
+
store = (set(store_sessions) - {""}) if store_sessions is not None else None
|
|
2058
|
+
if scope == "project":
|
|
2059
|
+
keep = in_project
|
|
2060
|
+
elif scope == "store" and store is not None and cur_sess in store:
|
|
2061
|
+
keep = lambda a: sess(a) in store
|
|
2062
|
+
elif scope == "store" and no_store == "project":
|
|
2063
|
+
keep = in_project
|
|
2064
|
+
elif scope == "store" and no_store == "pinned":
|
|
2065
|
+
keep = lambda a: False
|
|
2066
|
+
elif scope == "store" and no_store == "nostore":
|
|
2067
|
+
keep = lambda a: bool(sess(a)) and (store is None or sess(a) not in store)
|
|
2068
|
+
else: # all, the "all" no-store option, or an unknown word
|
|
2069
|
+
keep = lambda a: True
|
|
2070
|
+
return [a for a in ag if a.get("agent") in pins or keep(a)]
|
|
2071
|
+
|
|
1961
2072
|
def rollup(states):
|
|
1962
2073
|
return min(states, key=lambda s: RANK.get(s, 9)) if states else None
|
|
1963
2074
|
|
|
@@ -1970,11 +2081,6 @@ IDLE_W = 4 # `999m`: the cell is always this wide, so rows
|
|
|
1970
2081
|
C_IDLE_ORANGE = fg("#fab387") # the palette's peach, beside the busy yellow
|
|
1971
2082
|
C_IDLE_RED = fg("#f38ba8") # the blocked glyph's red
|
|
1972
2083
|
IDLE_DEFAULTS = {"agent_idle_orange": 45, "agent_idle_red": 60, "agent_idle_blink": 0}
|
|
1973
|
-
# The statuses that mean "at its prompt, its turn over": Claude Code writes
|
|
1974
|
-
# `shell`, not `idle`, whenever a background shell lives (a listener left
|
|
1975
|
-
# running is one), so many idle seats say `shell`; the session tools read
|
|
1976
|
-
# both as the state `idle`. busy, waiting and blocked are not idle.
|
|
1977
|
-
IDLE_STATUSES = ("idle", "shell")
|
|
1978
2084
|
|
|
1979
2085
|
def idle_minutes_setting(key):
|
|
1980
2086
|
"""A threshold in minutes from the config, or None for off: 0, a
|
|
@@ -1994,14 +2100,14 @@ def idle_minutes_setting(key):
|
|
|
1994
2100
|
|
|
1995
2101
|
def fmt_idle_cell(a):
|
|
1996
2102
|
"""The {idle_m} cell, IDLE_W columns wide whatever it holds. Blank unless
|
|
1997
|
-
the seat is IDLE (
|
|
2103
|
+
the seat is IDLE (seat_word says idle): idle_s is the time in
|
|
1998
2104
|
the current state, so a seat busy for an hour has a large idle_s too,
|
|
1999
2105
|
and must show nothing.
|
|
2000
2106
|
An idle seat shows its whole minutes (`34m`; hours past 999m): dim grey,
|
|
2001
2107
|
then orange and bold from agent_idle_orange, red and bold from
|
|
2002
2108
|
agent_idle_red, blinking from agent_idle_blink when that is set (it is
|
|
2003
2109
|
off by default)."""
|
|
2004
|
-
if a
|
|
2110
|
+
if seat_word(a) != "idle":
|
|
2005
2111
|
return " " * IDLE_W
|
|
2006
2112
|
try:
|
|
2007
2113
|
secs = max(0, int(a.get("idle_s") or 0))
|
|
@@ -2112,8 +2218,8 @@ def render_base(height=None):
|
|
|
2112
2218
|
pane = a.get("tmux_pane") or ""
|
|
2113
2219
|
s = p2s.get(pane)
|
|
2114
2220
|
if s:
|
|
2115
|
-
by_sess.setdefault(s, []).append(a
|
|
2116
|
-
by_win.setdefault(f"{s}:{p2i[pane]}", []).append(a
|
|
2221
|
+
by_sess.setdefault(s, []).append(seat_word(a) or "")
|
|
2222
|
+
by_win.setdefault(f"{s}:{p2i[pane]}", []).append(seat_word(a) or "")
|
|
2117
2223
|
tab_glyphs(by_win, pl) # the tab-bar glyphs
|
|
2118
2224
|
|
|
2119
2225
|
lines, targets = [], {}
|
|
@@ -2247,7 +2353,7 @@ def render_base(height=None):
|
|
|
2247
2353
|
for a in R["agents"]:
|
|
2248
2354
|
sname = R["p2s"].get(a.get("tmux_pane") or "")
|
|
2249
2355
|
if sname:
|
|
2250
|
-
rby.setdefault(sname, []).append(a
|
|
2356
|
+
rby.setdefault(sname, []).append(seat_word(a) or "")
|
|
2251
2357
|
for name, nwin, att in R["sessions"]:
|
|
2252
2358
|
st = rollup(rby.get(name, []))
|
|
2253
2359
|
g = GLYPH.get(st, " ") if st else " "
|
|
@@ -2267,8 +2373,14 @@ def render_base(height=None):
|
|
|
2267
2373
|
pane = a.get("tmux_pane") or ""
|
|
2268
2374
|
ag.append(dict(a, _host=host, _window=R.get("p2w", {}).get(pane, ""),
|
|
2269
2375
|
_session=R.get("p2s", {}).get(pane, "")))
|
|
2270
|
-
|
|
2271
|
-
|
|
2376
|
+
# the scope, then the sort: a pinned row shows under every scope. The
|
|
2377
|
+
# store's session set (its tm projects, its own session, a live legacy
|
|
2378
|
+
# `store`) is asked for only under `store`: all and project cost nothing.
|
|
2379
|
+
in_sess = None
|
|
2380
|
+
if FILTER == "store" and srec:
|
|
2381
|
+
in_sess = (store_projects(srec) | {ssess}
|
|
2382
|
+
| ({LEGACY_STORE_SESSION} & {n for n, _, _ in live_sessions}))
|
|
2383
|
+
ag = scope_filter(ag, FILTER, p2s, cur_sess, CFG.get("pinned_agents"), in_sess)
|
|
2272
2384
|
gb = CFG.get("group_by", "none") if not GROUPED else (
|
|
2273
2385
|
CFG.get("group_by", "none") if CFG.get("group_by", "none") != "none" else "session")
|
|
2274
2386
|
if not GROUPED:
|
|
@@ -2304,11 +2416,15 @@ def render_base(height=None):
|
|
|
2304
2416
|
targets[len(lines)] = ("new-or-menu", (vislen(left), WIDTH - vislen(right) if right else WIDTH + 1))
|
|
2305
2417
|
lines.append(row(C_DIM + lay(spec, WIDTH, chrome=True) + NOFG))
|
|
2306
2418
|
put("")
|
|
2307
|
-
# heading: "<left from config> all · grouped" -- each word a toggle
|
|
2308
|
-
#
|
|
2419
|
+
# heading: "<left from config> all · grouped" -- each word a toggle.
|
|
2420
|
+
# The first names the scope, lit whenever a scope narrows the list; under
|
|
2421
|
+
# the "pinned" no-store option, outside a store, it says so: `no store`.
|
|
2309
2422
|
hl = (CFG["agents_heading"].split("\t", 1) + [""])[0]
|
|
2310
|
-
filt =
|
|
2311
|
-
|
|
2423
|
+
filt = FILTER
|
|
2424
|
+
if FILTER == "store" and STORE_SCOPE_NO_STORE == "pinned" \
|
|
2425
|
+
and not (cur_sess and in_sess is not None and cur_sess in in_sess):
|
|
2426
|
+
filt = "no store"
|
|
2427
|
+
r_filt = (C_SEL if FILTER != "all" else C_DIM) + filt + C_DIM
|
|
2312
2428
|
r_grp = (C_SEL if GROUPED else C_DIM) + "grouped" + C_DIM
|
|
2313
2429
|
rtxt = f"{filt} \u00b7 grouped "
|
|
2314
2430
|
x_right = WIDTH - len(rtxt)
|
|
@@ -3686,7 +3802,8 @@ def main():
|
|
|
3686
3802
|
if x >= x_grp + 1:
|
|
3687
3803
|
GROUPED = not GROUPED
|
|
3688
3804
|
elif x >= x_filt:
|
|
3689
|
-
FILTER =
|
|
3805
|
+
FILTER = SCOPES[(SCOPES.index(FILTER) + 1) % len(SCOPES)
|
|
3806
|
+
if FILTER in SCOPES else 0]
|
|
3690
3807
|
elif what == "plus-or-collapse": # the bottom control strip
|
|
3691
3808
|
lw, rx = val
|
|
3692
3809
|
if x <= lw + 1:
|
|
@@ -161,9 +161,10 @@ labels.
|
|
|
161
161
|
`session start --list-agents` after. The live
|
|
162
162
|
agents list sorts a running agent whose name is pinned first, in pin
|
|
163
163
|
order, whatever its project, status or idle; the rest keep the `sort`
|
|
164
|
-
order. Grouped, the pinned rows sit above every group. `hide`
|
|
165
|
-
|
|
166
|
-
keeps alice and bob on
|
|
164
|
+
order. Grouped, the pinned rows sit above every group. `hide` applies
|
|
165
|
+
first, and a pinned agent shows under every agents scope (all, store,
|
|
166
|
+
project). Empty by default; `["alice", "bob"]` keeps alice and bob on
|
|
167
|
+
top.
|
|
167
168
|
- `code_nvim` - sessionview-session starts nvim in the code window, listening on
|
|
168
169
|
show-me's socket; off = a shell.
|
|
169
170
|
- `agent_command` - what "Start an agent" runs: an argv list, `{project}`
|
|
@@ -141,7 +141,7 @@
|
|
|
141
141
|
],
|
|
142
142
|
"_window_pins": "Your pins, laid over the `windows` list's `auto`: {\"editor\": true} opens the editor with every session, {\"git\": false} stops git opening. Written by ^p / right-click in the new-tab picker; a delta, so the list itself (template, store or project) keeps flowing to you. EMPTY by default.",
|
|
143
143
|
"window_pins": {},
|
|
144
|
-
"_pinned_agents": "Agent names held at the TOP, in this order - ONE list for two places: pinned means pinned everywhere. (1) The start-an-agent picker lists them first; the rest of `session start --list-agents` follows. (2) The live agents list sorts a running agent whose name is pinned first, in pin order, whatever its project, status or idle; the rest keep the `sort` order below them. Grouped, the pinned rows sit ABOVE every group, not inside their project's. `hide`
|
|
144
|
+
"_pinned_agents": "Agent names held at the TOP, in this order - ONE list for two places: pinned means pinned everywhere. (1) The start-an-agent picker lists them first; the rest of `session start --list-agents` follows. (2) The live agents list sorts a running agent whose name is pinned first, in pin order, whatever its project, status or idle; the rest keep the `sort` order below them. Grouped, the pinned rows sit ABOVE every group, not inside their project's. `hide` still applies first, so a hidden status stays hidden; a pinned agent then shows under every agents scope (all, store, project). [\"alice\", \"bob\"] keeps alice and bob at the top. EMPTY by default: a fresh install pins nobody. A pinned name the launcher cannot start is not offered - a list that lies is worse than no list. ^p or a right-click on a row in the start-an-agent picker toggles it.",
|
|
145
145
|
"pinned_agents": [],
|
|
146
146
|
"_mid_format": "The row between projects and agents. Left word opens a new SESSION. A right word (after a TAB) opens the project menu - there is none by default: the selected project row carries its own menu as three dots at the right. The click target is the row, not the text.",
|
|
147
147
|
"mid_format": " +",
|
|
@@ -175,7 +175,7 @@
|
|
|
175
175
|
"terminal": "",
|
|
176
176
|
"_fullscreen": "FALSE by default: whether the dock app opens sessionview FULL SCREEN (macOS native full screen, the green button - not zoomed). On Terminal.app this needs Accessibility permission for SessionView.app (System Settings > Privacy & Security > Accessibility); the app says so in a dialog rather than failing quietly. iTerm and Ghostty need no permission.",
|
|
177
177
|
"fullscreen": false,
|
|
178
|
-
"_pane_menu": "FALSE by default: the agent pane menu - right-click an agent's pane for Speak / Continue / Read Clipboard / Park / Succeed, each a mail to that agent (tmux/agent-menu.conf, bin/tmux-send-agent). OPT-IN because a tmux key binding is global to the server: turning this on REPLACES your own right-click
|
|
178
|
+
"_pane_menu": "FALSE by default: the agent pane menu - right-click or Option + right-click an agent's pane for Speak / Continue / Read Clipboard / Park / Succeed, each a mail to that agent (tmux/agent-menu.conf, bin/tmux-send-agent). An agent's pane is one whose title is `<glyph> <address>`. The plain right-click opens it even when the program there uses the mouse, as Claude Code does. OPT-IN because a tmux key binding is global to the server: turning this on REPLACES both your own right-click bindings (MouseDown3Pane and M-MouseDown3Pane). When true, sessionview-frame loads it into the server at every launch, so it works whether or not tmux was already running, and its own server passes an Option + right-click on your tmux through to it. Without session view: `source-file -q <sessionview>/tmux/agent-menu.conf` in ~/.tmux.conf (a link or a conf.d glob works too). Turning it OFF takes effect when the tmux server restarts: nothing unbinds a running server's menu, since that would also drop the user's own.",
|
|
179
179
|
"pane_menu": false,
|
|
180
180
|
"_pane_menu_from": "EMPTY by default: the OLDER name for mail_from, still honoured when mail_from is empty. Set mail_from instead.",
|
|
181
181
|
"pane_menu_from": "",
|
|
@@ -81,7 +81,10 @@ The `messages` tab shows a project's mail in neomutt, read-only.
|
|
|
81
81
|
- The tab's sidebar needs notmuch to index two headers:
|
|
82
82
|
`notmuch config set index.header.Type Type` and
|
|
83
83
|
`notmuch config set index.header.Project Project`, then
|
|
84
|
-
`notmuch reindex '*'`.
|
|
84
|
+
`notmuch reindex '*'`. When sessionmail names its own notmuch profile
|
|
85
|
+
(`message path --notmuch-config`, sessionmail 0.3.3), the tab and
|
|
86
|
+
`sessionview-install --check` use that file, and the fix line they
|
|
87
|
+
print carries its `--config=`.
|
|
85
88
|
|
|
86
89
|
Your own mail client, the sidebar's mail row, is set by the
|
|
87
90
|
`user_mail_*` keys on the [settings page][settings].
|
|
@@ -119,9 +122,12 @@ repository.
|
|
|
119
122
|
frame to its own sessions only.
|
|
120
123
|
- `<checkout>/tmux/tabs-top.conf`: the same look for every tmux session.
|
|
121
124
|
Optional: `source-file` it from `~/.tmux.conf`.
|
|
122
|
-
- `<checkout>/tmux/agent-menu.conf`: the agent pane menu
|
|
123
|
-
|
|
124
|
-
`
|
|
125
|
+
- `<checkout>/tmux/agent-menu.conf`: the agent pane menu, on both
|
|
126
|
+
right-clicks (`MouseDown3Pane` and Option's `M-MouseDown3Pane`). The
|
|
127
|
+
frame loads it when the `pane_menu` key is `true`, and an Option +
|
|
128
|
+
right-click on your tmux in the frame reaches it. Without the frame,
|
|
129
|
+
`source-file -q` it from `~/.tmux.conf`. It needs tmux's `mouse`
|
|
130
|
+
option on, the default since tmux 3.8.
|
|
125
131
|
- `tmux set -g @sessionview_tab_close 0`: no close `×` on the tabs.
|
|
126
132
|
|
|
127
133
|
## Paths
|
|
@@ -124,7 +124,7 @@ tabs; [config/README.md][config-readme] describes each field.
|
|
|
124
124
|
| --- | --- | --- | --- |
|
|
125
125
|
| `terminal` | string | `""` | The terminal the dock app opens: `iterm`, `ghostty` or `terminal`. `""` works it out. |
|
|
126
126
|
| `fullscreen` | boolean | `false` | The dock app opens sessionview full screen. |
|
|
127
|
-
| `pane_menu` | boolean | `false` | Right-click on an agent's pane opens the agent pane menu. It replaces your
|
|
127
|
+
| `pane_menu` | boolean | `false` | Right-click or Option + right-click on an agent's pane opens the agent pane menu, even when the program there uses the mouse. It replaces both your tmux right-click bindings. |
|
|
128
128
|
| `mail_from` | string | `""` | The name sessionview's mail is sent as. `""`: the next in line, on the [configuration page][mail-sender]. |
|
|
129
129
|
| `pane_menu_from` | string | `""` | The older name for `mail_from`, read when `mail_from` is empty. |
|
|
130
130
|
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# lib/pane-menu.sh - sourced by sessionview-frame, only when pane_menu is on.
|
|
2
|
+
#
|
|
3
|
+
# sv_frame_option_forward SOCK, on the frame's own tmux server SOCK: an Option
|
|
4
|
+
# + right-click (M-MouseDown3Pane) on a pane running a tmux client goes through
|
|
5
|
+
# to that client's server, where tmux/agent-menu.conf has the agent menu. Every
|
|
6
|
+
# other pane (the sidebar) keeps the binding SOCK already had.
|
|
7
|
+
#
|
|
8
|
+
# Without it the frame's server takes the Option-click itself: it shows its own
|
|
9
|
+
# stock menu for the pane that holds your tmux client, and your tmux never sees
|
|
10
|
+
# the click. The plain right-click needs nothing here: the frame's stock binding
|
|
11
|
+
# already forwards it, because a tmux client's pane has mouse mode on whenever
|
|
12
|
+
# your tmux has `mouse on`.
|
|
13
|
+
#
|
|
14
|
+
# How. The key's current binding is read back with list-keys and becomes the
|
|
15
|
+
# else-branch of one if-shell, so no copy of tmux's menu is kept here (a copy
|
|
16
|
+
# names flags a given tmux may not know, and one unknown flag rejects the whole
|
|
17
|
+
# bind). That binding is tmux's stock menu unless the user's ~/.tmux.conf
|
|
18
|
+
# changed it, and then theirs is kept. A key with no binding sends the click to
|
|
19
|
+
# the pane under the mouse, so its else-branch is `send-keys -M`, which does
|
|
20
|
+
# the same. A second call finds its own wrapper and changes nothing (a second
|
|
21
|
+
# frame on the same server).
|
|
22
|
+
#
|
|
23
|
+
# The separator step. list-keys prints a command SEQUENCE with ` \; ` between
|
|
24
|
+
# its commands. Inside braces that is a literal `;` argument, and the bind
|
|
25
|
+
# fails. So each ` \; ` becomes ` ; ` (and ` \;\; ` becomes ` ;; `). A `\;`
|
|
26
|
+
# inside a quoted string is printed as `\\;` and is left alone.
|
|
27
|
+
#
|
|
28
|
+
# Known limit: an argument that is exactly `;` (`send-keys ';'`) is printed as
|
|
29
|
+
# `\;` too, the same text as a separator. Nothing in the printed line tells the
|
|
30
|
+
# two apart: tmux's own round trip of that line, sourced back as a config line,
|
|
31
|
+
# loses the argument the same way. Then the bind fails (unknown command), the
|
|
32
|
+
# key keeps the user's binding, and the pass-through is not installed. A lone
|
|
33
|
+
# `;` as the binding's LAST word is fine.
|
|
34
|
+
#
|
|
35
|
+
# It runs "${TMUX_BIN:-tmux}" (the frame sets TMUX_BIN) and returns tmux's
|
|
36
|
+
# status; the frame fails open on it.
|
|
37
|
+
|
|
38
|
+
#: the pass-through, as bound: a pane running tmux gets the click
|
|
39
|
+
SV_FRAME_FORWARD="if-shell -F -t = '#{==:#{pane_current_command},tmux}' { select-pane -t = ; send-keys -M }"
|
|
40
|
+
#: how list-keys prints the start of the wrapper (double quotes)
|
|
41
|
+
SV_FRAME_FORWARD_SHOWN='if-shell -F -t = "#{==:#{pane_current_command},tmux}" { select-pane -t = ; send-keys -M }'
|
|
42
|
+
|
|
43
|
+
sv_frame_option_forward() {
|
|
44
|
+
_sv_pm_cur="$("${TMUX_BIN:-tmux}" -L "$1" list-keys -T root M-MouseDown3Pane 2>/dev/null)" || _sv_pm_cur=""
|
|
45
|
+
# bind-key [-r] -T root M-MouseDown3Pane COMMAND: keep -r, and COMMAND
|
|
46
|
+
_sv_pm_r="$(printf '%s\n' "$_sv_pm_cur" | sed -n 's/^bind-key *\(-r\) *-T root *M-MouseDown3Pane .*/\1 /p')"
|
|
47
|
+
_sv_pm_body="$(printf '%s\n' "$_sv_pm_cur" \
|
|
48
|
+
| sed -n 's/^bind-key *\(-r *\)\{0,1\}-T root *M-MouseDown3Pane *//p' \
|
|
49
|
+
| sed 's/ [\\];[\\]; / ;; /g; s/ [\\]; / ; /g')"
|
|
50
|
+
case "$_sv_pm_body" in
|
|
51
|
+
"$SV_FRAME_FORWARD_SHOWN"*) return 0;;
|
|
52
|
+
'') _sv_pm_body="send-keys -M";;
|
|
53
|
+
esac
|
|
54
|
+
# $_sv_pm_r is "-r " or empty, unquoted on purpose
|
|
55
|
+
"${TMUX_BIN:-tmux}" -L "$1" bind-key $_sv_pm_r-T root M-MouseDown3Pane "$SV_FRAME_FORWARD { $_sv_pm_body }"
|
|
56
|
+
}
|
|
@@ -56,11 +56,19 @@ if term is "iterm" then
|
|
|
56
56
|
end tell
|
|
57
57
|
end run"
|
|
58
58
|
try
|
|
59
|
+
-- Compiling that string LOOKS iTERM UP BY NAME, and on a Mac without
|
|
60
|
+
-- iTerm the lookup does not fail: it puts up the "Where is iTerm?"
|
|
61
|
+
-- chooser and waits on it with no time limit, so the alert below was
|
|
62
|
+
-- never reached and the app hung. So ask first, with a check that
|
|
63
|
+
-- cannot prompt (itermFound: shell only, no application reference),
|
|
64
|
+
-- and when iTerm is not there, go straight to the alert.
|
|
65
|
+
if not itermFound() then error "iTerm was not found on this Mac." number -10814
|
|
59
66
|
run script itermScript with parameters {cmd, wantFull}
|
|
60
67
|
on error errMsg number errNum
|
|
61
68
|
if errNum is -128 then error number -128
|
|
62
69
|
-- e.g. `terminal: iterm` in the config on a Mac where iTerm is not
|
|
63
|
-
-- installed
|
|
70
|
+
-- installed (itermFound said no), or the string failing to compile or
|
|
71
|
+
-- run. Say so; never fail mute.
|
|
64
72
|
display alert "SessionView cannot open iTerm" message "iTerm is the terminal chosen for sessionview, but it could not be run:
|
|
65
73
|
" & errMsg & " (" & errNum & ")
|
|
66
74
|
|
|
@@ -242,3 +250,21 @@ on isInstalled(appName)
|
|
|
242
250
|
end try
|
|
243
251
|
end isInstalled
|
|
244
252
|
|
|
253
|
+
-- Is iTerm on this Mac at all? Asked before the iTerm string is compiled,
|
|
254
|
+
-- because compiling it without iTerm prompts instead of failing. Nothing here
|
|
255
|
+
-- is an AppleScript reference to the app, so nothing here can put up a
|
|
256
|
+
-- chooser: is it running, is it in either Applications folder, and does
|
|
257
|
+
-- LaunchServices know its bundle id (a copy kept anywhere else). That last is
|
|
258
|
+
-- asked through JavaScript for Automation in a child process, a plain lookup
|
|
259
|
+
-- that returns nothing when the app is missing. Spotlight (mdfind) is not
|
|
260
|
+
-- used: with indexing off it finds no app at all.
|
|
261
|
+
on itermFound()
|
|
262
|
+
set js to "ObjC.import('AppKit'); $.NSWorkspace.sharedWorkspace.URLForApplicationWithBundleIdentifier('com.googlecode.iterm2').isNil() ? 'no' : 'yes'"
|
|
263
|
+
try
|
|
264
|
+
do shell script "/usr/bin/pgrep -x iTerm2 >/dev/null || test -d /Applications/iTerm.app || test -d \"$HOME/Applications/iTerm.app\" || test \"$(/usr/bin/osascript -l JavaScript -e " & quoted form of js & " 2>/dev/null)\" = yes"
|
|
265
|
+
return true
|
|
266
|
+
on error
|
|
267
|
+
return false
|
|
268
|
+
end try
|
|
269
|
+
end itermFound
|
|
270
|
+
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "sessionview"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.3"
|
|
8
8
|
description = "A sidebar for tmux: your projects (sessions) and your agents, here and on remote hosts."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11,<3.15"
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# agent-menu.conf - the agent pane menu. Right-click an agent's pane, or
|
|
2
|
+
# Option + right-click it, for
|
|
3
|
+
# Speak / Continue / Read Clipboard / Park / Succeed -> <agent>
|
|
4
|
+
# above tmux's own pane menu. Each item mails that agent (bin/tmux-send-agent,
|
|
5
|
+
# sessionmail's `message send`). An agent pane is one whose title carries an
|
|
6
|
+
# agent address: `<glyph> <address>`, as `role.scope` or `name:id`. The agent
|
|
7
|
+
# items appear on agent panes only; other panes get the rest of the menu,
|
|
8
|
+
# sessionview's copy of tmux's own pane menu.
|
|
9
|
+
#
|
|
10
|
+
# Both right-clicks open the same menu:
|
|
11
|
+
# - plain right-click (MouseDown3Pane): on an agent pane it opens the menu
|
|
12
|
+
# even when the program there has mouse mode on (Claude Code does). On any
|
|
13
|
+
# other pane a program with mouse mode on still gets the click, as in
|
|
14
|
+
# tmux's default. A mode other than copy/view (choose-tree, clock-mode)
|
|
15
|
+
# still gets the plain click too.
|
|
16
|
+
# - Option + right-click (M-MouseDown3Pane): always the menu, on every pane,
|
|
17
|
+
# as in tmux's default.
|
|
18
|
+
# With session view, the frame passes an Option + right-click on the pane
|
|
19
|
+
# that holds your tmux through to it, so this menu opens there too.
|
|
20
|
+
#
|
|
21
|
+
# The menu is written ONCE, as SESSIONVIEW_AGENT_MENU below, and both
|
|
22
|
+
# bindings are built from it when this file is read: to change the menu,
|
|
23
|
+
# edit SESSIONVIEW_AGENT_MENU. SESSIONVIEW_AGENT_TITLE is the agent-pane
|
|
24
|
+
# test, shared by the plain right-click and every agent item. Both are
|
|
25
|
+
# removed from the server's environment at the end of the file.
|
|
26
|
+
#
|
|
27
|
+
# OPT-IN, because a key binding is global to the tmux server: loading this
|
|
28
|
+
# REPLACES BOTH your own right-click bindings (MouseDown3Pane and
|
|
29
|
+
# M-MouseDown3Pane). It needs tmux's `mouse` option on, or no right-click
|
|
30
|
+
# reaches tmux at all (on by default since tmux 3.8; older tmux needs
|
|
31
|
+
# `set -g mouse on` in ~/.tmux.conf).
|
|
32
|
+
# - with session view: sessionview-config set pane_menu true
|
|
33
|
+
# (sessionview-frame sources this at every launch)
|
|
34
|
+
# - tmux alone: add to ~/.tmux.conf, then reload (source-file):
|
|
35
|
+
# source-file -q /path/to/sessionview/tmux/agent-menu.conf
|
|
36
|
+
# The sender is you: config mail_from, else $USER (see bin/sessionview-from).
|
|
37
|
+
#
|
|
38
|
+
# Moved here from a personal ~/.tmux.conf, so that it ships with sessionview.
|
|
39
|
+
# The default items are still tmux 3.7b's `list-keys -T root MouseDown3Pane`;
|
|
40
|
+
# re-copy them if tmux's default changes.
|
|
41
|
+
|
|
42
|
+
# The helper sits beside this file's directory, wherever it is installed.
|
|
43
|
+
# First the path as sourced; then, since a conf.d LINK to this file would
|
|
44
|
+
# point that at confd/../bin (found in review), the
|
|
45
|
+
# link is resolved in the shell and the option overwritten with the real one.
|
|
46
|
+
set -gF @sessionview_agent_menu_conf "#{current_file}"
|
|
47
|
+
set -gF @sessionview_agent_menu_bin "#{d:current_file}/../bin/tmux-send-agent"
|
|
48
|
+
run-shell 'f="$(realpath "#{@sessionview_agent_menu_conf}" 2>/dev/null)" && [ -n "$f" ] && tmux set -g @sessionview_agent_menu_bin "$(dirname "$f")/../bin/tmux-send-agent"; true'
|
|
49
|
+
|
|
50
|
+
# An agent pane: its title carries an agent address. One definition, used by
|
|
51
|
+
# the plain right-click's test below and by every agent item in the menu.
|
|
52
|
+
%hidden SESSIONVIEW_AGENT_TITLE='^[^ ]+ ([a-z_][a-z0-9_+-]*[.][^ ]+|[a-z][a-z0-9_-]*:[^ ]+)$'
|
|
53
|
+
|
|
54
|
+
# The menu, once. Both right-click bindings below are built from it.
|
|
55
|
+
%hidden SESSIONVIEW_AGENT_MENU="display-menu -T '#[align=centre]#{pane_index} (#{pane_id})' -t = -x M -y M \
|
|
56
|
+
'#{?#{m/r:$SESSIONVIEW_AGENT_TITLE,#{pane_title}},Speak → #{s/[^A-Za-z0-9_.+-].*//:#{s/^[^ ]+ //:pane_title}},}' S { run-shell -b \"'#{@sessionview_agent_menu_bin}' #{pane_id}\" } \
|
|
57
|
+
'#{?#{m/r:$SESSIONVIEW_AGENT_TITLE,#{pane_title}},Continue → #{s/[^A-Za-z0-9_.+-].*//:#{s/^[^ ]+ //:pane_title}},}' C { run-shell -b \"'#{@sessionview_agent_menu_bin}' #{pane_id} continue\" } \
|
|
58
|
+
'#{?#{m/r:$SESSIONVIEW_AGENT_TITLE,#{pane_title}},Read Clipboard → #{s/[^A-Za-z0-9_.+-].*//:#{s/^[^ ]+ //:pane_title}},}' B { run-shell -b \"'#{@sessionview_agent_menu_bin}' #{pane_id} -- 'please read the clipboard (pbpaste) and act on it'\" } \
|
|
59
|
+
'#{?#{m/r:$SESSIONVIEW_AGENT_TITLE,#{pane_title}},Park → #{s/[^A-Za-z0-9_.+-].*//:#{s/^[^ ]+ //:pane_title}},}' P { run-shell -b \"'#{@sessionview_agent_menu_bin}' #{pane_id} 'please settle your context, then park'\" } \
|
|
60
|
+
'#{?#{m/r:$SESSIONVIEW_AGENT_TITLE,#{pane_title}},Succeed → #{s/[^A-Za-z0-9_.+-].*//:#{s/^[^ ]+ //:pane_title}},}' U { run-shell -b \"'#{@sessionview_agent_menu_bin}' #{pane_id} -- 'please settle your context, then succeed: start your successor'\" } \
|
|
61
|
+
'#{?#{m/r:(copy|view)-mode,#{pane_mode}},Go To Top,}' < { send-keys -X history-top } \
|
|
62
|
+
'#{?#{m/r:(copy|view)-mode,#{pane_mode}},Go To Bottom,}' > { send-keys -X history-bottom } \
|
|
63
|
+
'' \
|
|
64
|
+
'#{?#{&&:#{buffer_size},#{!:#{pane_in_mode}}},Paste #[underscore]#{=/9/...:buffer_sample},}' p { paste-buffer } \
|
|
65
|
+
'' \
|
|
66
|
+
'#{?mouse_word,Search For #[underscore]#{=/9/...:mouse_word},}' C-r { if-shell -F '#{?#{m/r:(copy|view)-mode,#{pane_mode}},0,1}' 'copy-mode -t=' ; send-keys -X -t = search-backward -- '#{q:mouse_word}' } \
|
|
67
|
+
'#{?mouse_word,Type #[underscore]#{=/9/...:mouse_word},}' C-y { copy-mode -q ; send-keys -l '#{q:mouse_word}' } \
|
|
68
|
+
'#{?mouse_word,Copy #[underscore]#{=/9/...:mouse_word},}' c { copy-mode -q ; set-buffer '#{q:mouse_word}' } \
|
|
69
|
+
'#{?mouse_line,Copy Line,}' l { copy-mode -q ; set-buffer '#{q:mouse_line}' } \
|
|
70
|
+
'' \
|
|
71
|
+
'#{?mouse_hyperlink,Type #[underscore]#{=/9/...:mouse_hyperlink},}' C-h { copy-mode -q ; send-keys -l '#{q:mouse_hyperlink}' } \
|
|
72
|
+
'#{?mouse_hyperlink,Copy #[underscore]#{=/9/...:mouse_hyperlink},}' h { copy-mode -q ; set-buffer '#{q:mouse_hyperlink}' } \
|
|
73
|
+
'' \
|
|
74
|
+
'#{?#{!:#{pane_floating_flag}},Horizontal Split,}' h { split-window -h } \
|
|
75
|
+
'#{?#{!:#{pane_floating_flag}},Vertical Split,}' v { split-window -v } \
|
|
76
|
+
'' \
|
|
77
|
+
'#{?#{&&:#{!:#{pane_floating_flag}},#{>:#{window_panes},1}},Swap Up,}' u { swap-pane -U } \
|
|
78
|
+
'#{?#{&&:#{!:#{pane_floating_flag}},#{>:#{window_panes},1}},Swap Down,}' d { swap-pane -D } \
|
|
79
|
+
'#{?pane_marked_set,,-}Swap Marked' s { swap-pane } \
|
|
80
|
+
'' \
|
|
81
|
+
Kill X { kill-pane } \
|
|
82
|
+
Respawn R { respawn-pane -k } \
|
|
83
|
+
'#{?pane_marked,Unmark,Mark}' m { select-pane -m } \
|
|
84
|
+
'#{?#{>:#{window_panes},1},,-}#{?window_zoomed_flag,Unzoom,Zoom}' z { resize-pane -Z }"
|
|
85
|
+
|
|
86
|
+
# Plain right-click: the program gets it when it has mouse mode on and the
|
|
87
|
+
# pane is NOT an agent's, or when a mode other than copy/view owns the pane;
|
|
88
|
+
# otherwise the menu. Each binding is ONE string, so tmux parses it when it
|
|
89
|
+
# binds: a broken menu fails this file loudly and leaves both keys as they were.
|
|
90
|
+
# Option + right-click: always the menu, as in tmux's default.
|
|
91
|
+
bind-key -T root MouseDown3Pane "if-shell -F -t = '#{||:#{&&:#{!:#{m/r:$SESSIONVIEW_AGENT_TITLE,#{pane_title}}},#{mouse_any_flag}},#{&&:#{pane_in_mode},#{?#{m/r:(copy|view)-mode,#{pane_mode}},0,1}}}' { select-pane -t = ; send-keys -M } { $SESSIONVIEW_AGENT_MENU }"
|
|
92
|
+
bind-key -T root M-MouseDown3Pane "$SESSIONVIEW_AGENT_MENU"
|
|
93
|
+
# Leave nothing behind in the server's environment.
|
|
94
|
+
set-environment -gu SESSIONVIEW_AGENT_TITLE
|
|
95
|
+
set-environment -gu SESSIONVIEW_AGENT_MENU
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
# agent-menu.conf - the agent pane menu. Right-click an agent's pane for
|
|
2
|
-
# Speak / Continue / Read Clipboard / Park / Succeed -> <agent>
|
|
3
|
-
# above tmux's own pane menu. Each item mails that agent (bin/tmux-send-agent,
|
|
4
|
-
# sessionmail's `message send`). Items appear only when the pane title
|
|
5
|
-
# carries an agent address; other panes get tmux's plain menu.
|
|
6
|
-
#
|
|
7
|
-
# OPT-IN, because a key binding is global to the tmux server: loading this
|
|
8
|
-
# REPLACES your own right-click (MouseDown3Pane) menu.
|
|
9
|
-
# - with session view: sessionview-config set pane_menu true
|
|
10
|
-
# (sessionview-frame sources this at every launch)
|
|
11
|
-
# - tmux alone: add to ~/.tmux.conf, then reload (source-file):
|
|
12
|
-
# source-file -q /path/to/sessionview/tmux/agent-menu.conf
|
|
13
|
-
# The sender is you: config mail_from, else $USER (see bin/sessionview-from).
|
|
14
|
-
#
|
|
15
|
-
# Moved here from a personal ~/.tmux.conf, so that it ships with sessionview.
|
|
16
|
-
# The default items are tmux 3.7b's `list-keys -T root MouseDown3Pane`; re-copy
|
|
17
|
-
# them if tmux's default changes.
|
|
18
|
-
|
|
19
|
-
# The helper sits beside this file's directory, wherever it is installed.
|
|
20
|
-
# First the path as sourced; then, since a conf.d LINK to this file would
|
|
21
|
-
# point that at confd/../bin (found in review), the
|
|
22
|
-
# link is resolved in the shell and the option overwritten with the real one.
|
|
23
|
-
set -gF @sessionview_agent_menu_conf "#{current_file}"
|
|
24
|
-
set -gF @sessionview_agent_menu_bin "#{d:current_file}/../bin/tmux-send-agent"
|
|
25
|
-
run-shell 'f="$(realpath "#{@sessionview_agent_menu_conf}" 2>/dev/null)" && [ -n "$f" ] && tmux set -g @sessionview_agent_menu_bin "$(dirname "$f")/../bin/tmux-send-agent"; true'
|
|
26
|
-
|
|
27
|
-
bind-key -T root MouseDown3Pane if-shell -F -t = "#{||:#{mouse_any_flag},#{&&:#{pane_in_mode},#{?#{m/r:(copy|view)-mode,#{pane_mode}},0,1}}}" { select-pane -t = ; send-keys -M } { display-menu -T "#[align=centre]#{pane_index} (#{pane_id})" -t = -x M -y M \
|
|
28
|
-
"#{?#{m/r:^[^ ]+ ([a-z_][a-z0-9_+-]*[.][^ ]+|[a-z][a-z0-9_-]*:[^ ]+)$,#{pane_title}},Speak → #{s/[^A-Za-z0-9_.+-].*//:#{s/^[^ ]+ //:pane_title}},}" S { run-shell -b "'#{@sessionview_agent_menu_bin}' #{pane_id}" } \
|
|
29
|
-
"#{?#{m/r:^[^ ]+ ([a-z_][a-z0-9_+-]*[.][^ ]+|[a-z][a-z0-9_-]*:[^ ]+)$,#{pane_title}},Continue → #{s/[^A-Za-z0-9_.+-].*//:#{s/^[^ ]+ //:pane_title}},}" C { run-shell -b "'#{@sessionview_agent_menu_bin}' #{pane_id} continue" } \
|
|
30
|
-
"#{?#{m/r:^[^ ]+ ([a-z_][a-z0-9_+-]*[.][^ ]+|[a-z][a-z0-9_-]*:[^ ]+)$,#{pane_title}},Read Clipboard → #{s/[^A-Za-z0-9_.+-].*//:#{s/^[^ ]+ //:pane_title}},}" B { run-shell -b "'#{@sessionview_agent_menu_bin}' #{pane_id} -- 'please read the clipboard (pbpaste) and act on it'" } \
|
|
31
|
-
"#{?#{m/r:^[^ ]+ ([a-z_][a-z0-9_+-]*[.][^ ]+|[a-z][a-z0-9_-]*:[^ ]+)$,#{pane_title}},Park → #{s/[^A-Za-z0-9_.+-].*//:#{s/^[^ ]+ //:pane_title}},}" P { run-shell -b "'#{@sessionview_agent_menu_bin}' #{pane_id} 'please settle your context, then park'" } \
|
|
32
|
-
"#{?#{m/r:^[^ ]+ ([a-z_][a-z0-9_+-]*[.][^ ]+|[a-z][a-z0-9_-]*:[^ ]+)$,#{pane_title}},Succeed → #{s/[^A-Za-z0-9_.+-].*//:#{s/^[^ ]+ //:pane_title}},}" U { run-shell -b "'#{@sessionview_agent_menu_bin}' #{pane_id} -- 'please settle your context, then succeed: start your successor'" } \
|
|
33
|
-
"#{?#{m/r:(copy|view)-mode,#{pane_mode}},Go To Top,}" < { send-keys -X history-top } "#{?#{m/r:(copy|view)-mode,#{pane_mode}},Go To Bottom,}" > { send-keys -X history-bottom } '' "#{?#{&&:#{buffer_size},#{!:#{pane_in_mode}}},Paste #[underscore]#{=/9/...:buffer_sample},}" p { paste-buffer } '' "#{?mouse_word,Search For #[underscore]#{=/9/...:mouse_word},}" C-r { if-shell -F "#{?#{m/r:(copy|view)-mode,#{pane_mode}},0,1}" "copy-mode -t=" ; send-keys -X -t = search-backward -- "#{q:mouse_word}" } "#{?mouse_word,Type #[underscore]#{=/9/...:mouse_word},}" C-y { copy-mode -q ; send-keys -l "#{q:mouse_word}" } "#{?mouse_word,Copy #[underscore]#{=/9/...:mouse_word},}" c { copy-mode -q ; set-buffer "#{q:mouse_word}" } "#{?mouse_line,Copy Line,}" l { copy-mode -q ; set-buffer "#{q:mouse_line}" } '' "#{?mouse_hyperlink,Type #[underscore]#{=/9/...:mouse_hyperlink},}" C-h { copy-mode -q ; send-keys -l "#{q:mouse_hyperlink}" } "#{?mouse_hyperlink,Copy #[underscore]#{=/9/...:mouse_hyperlink},}" h { copy-mode -q ; set-buffer "#{q:mouse_hyperlink}" } '' "#{?#{!:#{pane_floating_flag}},Horizontal Split,}" h { split-window -h } "#{?#{!:#{pane_floating_flag}},Vertical Split,}" v { split-window -v } '' "#{?#{&&:#{!:#{pane_floating_flag}},#{>:#{window_panes},1}},Swap Up,}" u { swap-pane -U } "#{?#{&&:#{!:#{pane_floating_flag}},#{>:#{window_panes},1}},Swap Down,}" d { swap-pane -D } "#{?pane_marked_set,,-}Swap Marked" s { swap-pane } '' Kill X { kill-pane } Respawn R { respawn-pane -k } "#{?pane_marked,Unmark,Mark}" m { select-pane -m } "#{?#{>:#{window_panes},1},,-}#{?window_zoomed_flag,Unzoom,Zoom}" z { resize-pane -Z } }
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|