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.
@@ -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
@@ -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
- Space Toggle transcript preview
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
- selection=''
117
- set +e
118
- 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='❯ ' \
119
- --preview="$preview_cmd" --preview-window='right,50%,wrap,hidden' \
120
- --bind='space:transform:[ -z {q} ] && echo toggle-preview || echo "put( )"' \
121
- --bind="$help_bind" \
122
- --bind="ctrl-x:execute($delete_cmd)+reload($cache_cmd)" \
123
- --bind="ctrl-g:execute($toggle_cmd)+reload($cache_cmd)" \
124
- --bind="change:reload:$cache_cmd" \
125
- --bind="start,every(3):reload-sync:$cache_cmd")
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
- [ -n "$session_id" ] || continue
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
- poll_target=$("$SCRIPT_DIR/sesh-refresh-worker.sh" request --state-dir "$state_dir")
139
- if ! "$SCRIPT_DIR/sesh-refresh-worker.sh" wait --state-dir "$state_dir" --target "$poll_target" --timeout 12; then
140
- echo 'Live state unavailable; retry selection after refresh' >&2
141
- continue
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 [ "$fork" = 1 ]; then printf '%s\t%s\tfork\n' "$session_id" "$cwd"; else printf '%s\t%s\n' "$session_id" "$cwd"; fi
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.7",
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": "npm test && npm run typecheck"
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",