opencode-sesh 0.1.7 → 0.2.1
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/AGENTS.md +78 -16
- package/CHANGELOG.md +87 -1
- package/README.md +51 -24
- package/bin/sesh-delete.sh +38 -4
- package/bin/sesh-list.sh +68 -17
- package/bin/sesh-pins.sh +55 -0
- package/bin/sesh-shortcuts.sh +6 -3
- package/bin/sesh.sh +52 -16
- package/package.json +6 -4
- package/tui/sesh-panel.tsx +661 -121
package/bin/sesh-pins.sh
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Persistent pins shared by the fzf picker and the opencode TUI plugin.
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
umask 077
|
|
5
|
+
|
|
6
|
+
PINS_FILE=${SESH_PINS_FILE:-"${XDG_DATA_HOME:-$HOME/.local/share}/sesh/pins.json"}
|
|
7
|
+
JQ_BIN=${SESH_JQ:-jq}
|
|
8
|
+
empty='{"sessions":[],"directories":[]}'
|
|
9
|
+
action=${1:-}
|
|
10
|
+
value=${2:-}
|
|
11
|
+
|
|
12
|
+
read_pins() {
|
|
13
|
+
if [ -f "$PINS_FILE" ]; then
|
|
14
|
+
local pins=''
|
|
15
|
+
pins=$("$JQ_BIN" -c 'select(type == "object") | {sessions: ((.sessions // []) | map(select(type == "string"))), directories: ((.directories // []) | map(select(type == "string")))}' "$PINS_FILE" 2>/dev/null) || pins=''
|
|
16
|
+
printf '%s\n' "${pins:-$empty}"
|
|
17
|
+
else
|
|
18
|
+
printf '%s\n' "$empty"
|
|
19
|
+
fi
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
case "$action" in
|
|
23
|
+
read) read_pins; exit 0 ;;
|
|
24
|
+
toggle-session)
|
|
25
|
+
[[ "$value" =~ ^ses_[A-Za-z0-9]+$ ]] || exit 2
|
|
26
|
+
field=sessions ;;
|
|
27
|
+
toggle-directory)
|
|
28
|
+
[[ "$value" == /* && "$value" != *$'\n'* ]] || exit 2
|
|
29
|
+
field=directories ;;
|
|
30
|
+
*) echo 'usage: sesh-pins.sh read|toggle-session ID|toggle-directory PATH' >&2; exit 2 ;;
|
|
31
|
+
esac
|
|
32
|
+
|
|
33
|
+
mkdir -p "$(dirname "$PINS_FILE")"
|
|
34
|
+
lock="$PINS_FILE.lock"
|
|
35
|
+
for ((attempt = 0; attempt < 50; attempt++)); do
|
|
36
|
+
if mkdir "$lock" 2>/dev/null; then printf '%s\n' "$$" > "$lock/pid"; break; fi
|
|
37
|
+
owner=''
|
|
38
|
+
[ ! -f "$lock/pid" ] || owner=$(cat "$lock/pid" 2>/dev/null || true)
|
|
39
|
+
if [[ "$owner" =~ ^[0-9]+$ ]] && ! kill -0 "$owner" 2>/dev/null; then
|
|
40
|
+
rm -f "$lock/pid" 2>/dev/null || true
|
|
41
|
+
rmdir "$lock" 2>/dev/null || true
|
|
42
|
+
fi
|
|
43
|
+
[ "$attempt" -lt 49 ] || { echo 'sesh: pins are busy; try again' >&2; exit 1; }
|
|
44
|
+
sleep 0.1
|
|
45
|
+
done
|
|
46
|
+
tmp=''
|
|
47
|
+
cleanup() { [ -z "$tmp" ] || rm -f "$tmp"; rm -f "$lock/pid"; rmdir "$lock" 2>/dev/null || true; }
|
|
48
|
+
trap cleanup EXIT
|
|
49
|
+
tmp=$(mktemp "${PINS_FILE}.XXXXXX")
|
|
50
|
+
read_pins | "$JQ_BIN" -c --arg field "$field" --arg value "$value" '
|
|
51
|
+
.[$field] |= (if index($value) then map(select(. != $value)) else . + [$value] end)
|
|
52
|
+
' > "$tmp"
|
|
53
|
+
mv -f "$tmp" "$PINS_FILE"
|
|
54
|
+
tmp=''
|
|
55
|
+
read_pins
|
package/bin/sesh-shortcuts.sh
CHANGED
|
@@ -7,11 +7,14 @@ sesh shortcuts
|
|
|
7
7
|
Enter Resume the selected session here
|
|
8
8
|
Ctrl-F Resume the selected session as a fork
|
|
9
9
|
Ctrl-G Toggle current-directory scope / all sessions
|
|
10
|
-
|
|
10
|
+
Ctrl-P Toggle transcript preview
|
|
11
|
+
Option-S Pin / unpin selected session
|
|
12
|
+
Option-D Pin / unpin selected directory
|
|
11
13
|
? Toggle this help
|
|
12
|
-
Ctrl-X Delete the selected session
|
|
14
|
+
Ctrl-X Delete the selected session (asks to confirm)
|
|
13
15
|
Escape Exit the picker
|
|
14
16
|
|
|
15
17
|
Flags: --cwd (current directory only), --limit N (default: all),
|
|
16
|
-
--archived (include archived), --print (print id instead of resuming)
|
|
18
|
+
--archived (include archived), --print (print id instead of resuming),
|
|
19
|
+
--json (with --print, emit JSON)
|
|
17
20
|
HELP
|
package/bin/sesh.sh
CHANGED
|
@@ -59,6 +59,7 @@ picker_limit=0
|
|
|
59
59
|
picker_archived=0
|
|
60
60
|
picker_print=0
|
|
61
61
|
picker_fork=0
|
|
62
|
+
picker_json=0
|
|
62
63
|
picker_query=''
|
|
63
64
|
while [ "$#" -gt 0 ]; do
|
|
64
65
|
case "$1" in
|
|
@@ -67,12 +68,14 @@ while [ "$#" -gt 0 ]; do
|
|
|
67
68
|
--archived) picker_archived=1 ;;
|
|
68
69
|
--print) picker_print=1 ;;
|
|
69
70
|
--fork) picker_fork=1 ;;
|
|
71
|
+
--json) picker_json=1 ;;
|
|
70
72
|
--query) picker_query=${2:-}; shift ;;
|
|
71
73
|
*) echo "unknown option: $1" >&2; exit 2 ;;
|
|
72
74
|
esac
|
|
73
75
|
shift
|
|
74
76
|
done
|
|
75
77
|
case "$picker_limit" in ''|*[!0-9]*) echo "--limit must be a non-negative integer" >&2; exit 2;; esac
|
|
78
|
+
[ "$picker_json" = 0 ] || [ "$picker_print" = 1 ] || { echo '--json requires --print' >&2; exit 2; }
|
|
76
79
|
|
|
77
80
|
shell_quote() {
|
|
78
81
|
# Quotes fixed command arguments embedded in fzf's shell action strings.
|
|
@@ -87,6 +90,7 @@ q_list=$(shell_quote "$SCRIPT_DIR/sesh-list.sh")
|
|
|
87
90
|
q_worker=$(shell_quote "$SCRIPT_DIR/sesh-refresh-worker.sh")
|
|
88
91
|
q_preview=$(shell_quote "$SCRIPT_DIR/sesh-preview.sh")
|
|
89
92
|
q_delete=$(shell_quote "$SCRIPT_DIR/sesh-delete.sh")
|
|
93
|
+
q_pins=$(shell_quote "$SCRIPT_DIR/sesh-pins.sh")
|
|
90
94
|
q_shortcuts=$(shell_quote "$SCRIPT_DIR/sesh-shortcuts.sh")
|
|
91
95
|
q_state=$(shell_quote "$state_dir")
|
|
92
96
|
export SESH_LIST="$SCRIPT_DIR/sesh-list.sh"
|
|
@@ -106,23 +110,35 @@ trap cleanup EXIT
|
|
|
106
110
|
trap 'exit 130' HUP INT TERM
|
|
107
111
|
|
|
108
112
|
cache_cmd="$q_worker render --state-dir $q_state --query {q} --selected-id {6}"
|
|
113
|
+
# fzf's --header is the only place a persistent scope/status line can live
|
|
114
|
+
# without becoming a selectable row, so it is refreshed by transform-header on
|
|
115
|
+
# start, on the 3s poll and after either action that can change it. Order
|
|
116
|
+
# matters: a transform-* action placed AFTER reload/reload-sync in the chain
|
|
117
|
+
# leaves the list empty, so the header is always transformed first.
|
|
118
|
+
header_cmd="$q_list --header --state-dir $q_state"
|
|
109
119
|
preview_cmd="if [ -f $q_state/help ]; then $q_shortcuts; else $q_preview $q_state {2}; fi"
|
|
110
120
|
help_bind="?:transform:[ -z {q} ] && ( [ -f $q_state/help ] && rm -f $q_state/help && echo hide-preview || ( : > $q_state/help && echo show-preview+refresh-preview ) ) || echo 'put(?)'"
|
|
111
121
|
toggle_cmd="$q_list --toggle-scope --state-dir $q_state >/dev/null"
|
|
112
122
|
delete_cmd="$q_delete $q_state {2}"
|
|
123
|
+
pin_session_cmd="$q_pins toggle-session {2}"
|
|
124
|
+
pin_directory_cmd="$q_pins toggle-directory {5}"
|
|
113
125
|
|
|
114
126
|
query="$picker_query"
|
|
115
127
|
while :; do
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
--
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
128
|
+
header_line=$("$SCRIPT_DIR/sesh-list.sh" --header --state-dir "$state_dir" 2>/dev/null || true)
|
|
129
|
+
selection=''
|
|
130
|
+
set +e
|
|
131
|
+
selection=$(FZF_DEFAULT_COMMAND='printf ""' FZF_DEFAULT_OPTS='' FZF_DEFAULT_OPTS_FILE='' "$SESH_FZF" --print-query --expect=ctrl-f --query="$query" --ansi --delimiter=$'\t' --with-nth=4 --track --id-nth=6 --disabled --layout=reverse --info=hidden --prompt='❯ ' \
|
|
132
|
+
--preview="$preview_cmd" --preview-window='right,50%,wrap,hidden' \
|
|
133
|
+
--header-first --header="$header_line" \
|
|
134
|
+
--bind='ctrl-p:toggle-preview' \
|
|
135
|
+
--bind="$help_bind" \
|
|
136
|
+
--bind="ctrl-x:execute($delete_cmd)+transform-header($header_cmd)+reload($cache_cmd)" \
|
|
137
|
+
--bind="ctrl-g:execute($toggle_cmd)+transform-header($header_cmd)+reload($cache_cmd)" \
|
|
138
|
+
--bind="alt-s:execute-silent($pin_session_cmd)+reload($cache_cmd)" \
|
|
139
|
+
--bind="alt-d:execute-silent($pin_directory_cmd)+reload($cache_cmd)" \
|
|
140
|
+
--bind="change:reload:$cache_cmd" \
|
|
141
|
+
--bind="start,every(3):transform-header($header_cmd)+reload-sync:$cache_cmd")
|
|
126
142
|
status=$?
|
|
127
143
|
set -e
|
|
128
144
|
case "$status" in 0) ;; 1|130) exit 0;; *) echo "sesh: picker failed ($status)" >&2; exit "$status";; esac
|
|
@@ -132,13 +148,30 @@ selection=${selection#*$'\n'}
|
|
|
132
148
|
key=${selection%%$'\n'*}
|
|
133
149
|
selection=${selection#*$'\n'}
|
|
134
150
|
session_id=$(printf '%s\n' "$selection" | cut -f2)
|
|
135
|
-
|
|
151
|
+
tracking_id=$(printf '%s\n' "$selection" | cut -f6)
|
|
152
|
+
if [ -z "$session_id" ]; then
|
|
153
|
+
# Enter/Ctrl-F on a directory header or a notice row. Leave a one-shot
|
|
154
|
+
# message for the reopened picker so the keypress visibly did something,
|
|
155
|
+
# and keep the query so the user has not lost their place.
|
|
156
|
+
case "$tracking_id" in
|
|
157
|
+
hdr:*) printf '%s' 'That row is a directory header, not a session.' > "$state_dir/action-notice" ;;
|
|
158
|
+
notice:*) printf '%s' 'That row is a notice, not a session; pick a session row.' > "$state_dir/action-notice" ;;
|
|
159
|
+
esac
|
|
160
|
+
continue
|
|
161
|
+
fi
|
|
136
162
|
# Dispatch queues a new worker scan and waits for that exact serialized poll.
|
|
137
163
|
# It cannot accept a previously fresh status while the worker owns its lock.
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
164
|
+
# A --print lookup reads the published snapshot, so it skips the wait entirely
|
|
165
|
+
# and stays usable from scripts without paying a rescan.
|
|
166
|
+
if [ "$picker_print" = 0 ]; then
|
|
167
|
+
printf 'sesh: refreshing session state…' >&2
|
|
168
|
+
poll_target=$("$SCRIPT_DIR/sesh-refresh-worker.sh" request --state-dir "$state_dir")
|
|
169
|
+
if ! "$SCRIPT_DIR/sesh-refresh-worker.sh" wait --state-dir "$state_dir" --target "$poll_target" --timeout 12; then
|
|
170
|
+
printf '\r\033[K' >&2
|
|
171
|
+
echo 'sesh: session state unavailable; selection cancelled, retry after refresh' >&2
|
|
172
|
+
continue
|
|
173
|
+
fi
|
|
174
|
+
printf '\r\033[K' >&2
|
|
142
175
|
fi
|
|
143
176
|
record=$("$SESH_JQ" -c --arg sid "$session_id" 'select(.sessionId==$sid)' "$state_dir/snapshot.jsonl")
|
|
144
177
|
[ -n "$record" ] || continue
|
|
@@ -146,7 +179,10 @@ cwd=$(printf '%s' "$record" | "$SESH_JQ" -r '.cwd')
|
|
|
146
179
|
fork=0
|
|
147
180
|
if [ "$picker_fork" = 1 ] || [ "$key" = ctrl-f ]; then fork=1; fi
|
|
148
181
|
if [ "$picker_print" = 1 ]; then
|
|
149
|
-
if [ "$
|
|
182
|
+
if [ "$picker_json" = 1 ]; then
|
|
183
|
+
"$SESH_JQ" -cn --arg sessionId "$session_id" --arg cwd "$cwd" --argjson fork "$fork" \
|
|
184
|
+
'{sessionId: $sessionId, cwd: $cwd, fork: ($fork == 1)}'
|
|
185
|
+
elif [ "$fork" = 1 ]; then printf '%s\t%s\tfork\n' "$session_id" "$cwd"; else printf '%s\t%s\n' "$session_id" "$cwd"; fi
|
|
150
186
|
exit 0
|
|
151
187
|
fi
|
|
152
188
|
if [ ! -d "$cwd" ]; then
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-sesh",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Browse, search, preview and resume opencode sessions from the terminal and the TUI.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -36,17 +36,19 @@
|
|
|
36
36
|
"publishConfig": {
|
|
37
37
|
"access": "public"
|
|
38
38
|
},
|
|
39
|
+
"packageManager": "bun@1.4.2",
|
|
39
40
|
"engines": {
|
|
40
41
|
"node": ">=20"
|
|
41
42
|
},
|
|
42
43
|
"scripts": {
|
|
43
44
|
"prepare": "husky",
|
|
44
45
|
"postinstall": "bash install.sh --sync-panel || true",
|
|
45
|
-
"test": "bash tests/sesh.sh",
|
|
46
|
+
"test": "bash tests/sesh.sh && bash tests/changelog.sh",
|
|
47
|
+
"test:changelog": "bash tests/changelog.sh",
|
|
46
48
|
"test:picker": "python3 tests/picker.py",
|
|
47
49
|
"typecheck": "tsc -p tsconfig.json",
|
|
48
|
-
"lint:sh": "for script in bin/*.sh bin/sesh install.sh tests/*.sh; do bash -n \"$script\" || exit; done",
|
|
49
|
-
"prepublishOnly": "
|
|
50
|
+
"lint:sh": "for script in bin/*.sh bin/sesh install.sh tests/*.sh scripts/*.sh; do bash -n \"$script\" || exit; done",
|
|
51
|
+
"prepublishOnly": "bun run test && bun run typecheck"
|
|
50
52
|
},
|
|
51
53
|
"devDependencies": {
|
|
52
54
|
"@commitlint/cli": "^21.2.2",
|