opencode-sesh 0.1.3 → 0.1.4
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 +12 -7
- package/CHANGELOG.md +28 -1
- package/README.md +2 -2
- package/bin/sesh-list.sh +32 -6
- package/bin/sesh-refresh-worker.sh +4 -1
- package/bin/sesh-shortcuts.sh +1 -1
- package/bin/sesh.sh +8 -6
- package/install.sh +3 -3
- package/opencode/tools/sesh-list.ts +23 -14
- package/package.json +8 -2
package/AGENTS.md
CHANGED
|
@@ -13,8 +13,8 @@ territory: read this first, then the specific file you need.
|
|
|
13
13
|
2. **`tui/sesh-panel.tsx`** — a SolidJS/OpenTUI plugin for the opencode TUI: a
|
|
14
14
|
recent-sessions section in the sidebar plus a `ctrl+o` / `/sesh` picker.
|
|
15
15
|
3. **`opencode/tools/sesh-list.ts`** — the `sesh-list` custom tool, so the agent
|
|
16
|
-
can list sessions across every directory
|
|
17
|
-
is project-scoped).
|
|
16
|
+
can list sessions across every directory by querying the global `opencode
|
|
17
|
+
db` store (the native `opencode session list` is project-scoped).
|
|
18
18
|
|
|
19
19
|
No iTerm2, no AppleScript, no panes, no window management — **never add any.**
|
|
20
20
|
opencode has no persistent-sidebar API, so the "sidebar" is a `sidebar_content`
|
|
@@ -43,9 +43,10 @@ The **filename becomes the tool name** in opencode, so `sesh-list.ts` is the
|
|
|
43
43
|
|
|
44
44
|
## Data flow (fzf picker)
|
|
45
45
|
|
|
46
|
-
`bin/sesh-list.sh`
|
|
47
|
-
|
|
48
|
-
|
|
46
|
+
`bin/sesh-list.sh` owns the fzf picker's session store access (opencode SQLite
|
|
47
|
+
DB: `session` / `message` / `part` tables); the `sesh-list` agent tool queries
|
|
48
|
+
the same store through `opencode db`. Keep its SQL column names in sync with
|
|
49
|
+
the schema below. Keystrokes never touch the database:
|
|
49
50
|
|
|
50
51
|
- `--refresh` (expensive): bulk-reads session metadata and per-session text
|
|
51
52
|
parts, validates the persistent extraction cache
|
|
@@ -67,7 +68,7 @@ One worker (`sesh-refresh-worker.sh`, per picker, TERMed on exit) serializes
|
|
|
67
68
|
scans; fzf's `start`/`every(3)`/`change` bindings render only. Selection requests
|
|
68
69
|
a fresh poll (`request`/`wait`, ≤12 s) before dispatch. opencode has no
|
|
69
70
|
live-agent API, so `liveState` is always `unknown` and rows render gray;
|
|
70
|
-
dispatch is always `opencode --session <id>` (`--fork` with Ctrl-F
|
|
71
|
+
dispatch is always `opencode --session <id>` (`--fork` with Ctrl-F or --fork).
|
|
71
72
|
Resume happens in place (`exec` after `cd` to the session's cwd).
|
|
72
73
|
|
|
73
74
|
## Conventions
|
|
@@ -82,12 +83,16 @@ Resume happens in place (`exec` after `cd` to the session's cwd).
|
|
|
82
83
|
`sqlite3 -json` in `-R` mode; it pretty-prints — parse as JSON.
|
|
83
84
|
- `jq -R` output stays JSON-quoted: use `-Rr` when a shell variable needs the
|
|
84
85
|
raw string.
|
|
85
|
-
- fzf: stock only, `>= 0.
|
|
86
|
+
- fzf: stock only, `>= 0.73` (`transform:`, `every():`). `{q}` in binds is
|
|
86
87
|
shell-quoted by fzf — don't add quoting. Ctrl-G is bind-only (stays open);
|
|
87
88
|
Ctrl-F is `--expect` (accepts with fork).
|
|
88
89
|
- The installer prefers the local opencode version for `@opencode-ai/*` and
|
|
89
90
|
merges (never clobbers) the config `package.json`; opencode runs `bun install`
|
|
90
91
|
at startup.
|
|
92
|
+
- Commit messages follow [Conventional Commits](https://www.conventionalcommits.org/)
|
|
93
|
+
(`feat`, `fix`, `docs`, `ci`, …). See
|
|
94
|
+
[CONTRIBUTING.md](CONTRIBUTING.md#commit-messages) for the types and the
|
|
95
|
+
version mapping.
|
|
91
96
|
|
|
92
97
|
## TUI panel (`tui/sesh-panel.tsx`)
|
|
93
98
|
|
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.1.4] - 2026-09-13
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- The picker keeps the selected session across automatic refreshes and no
|
|
15
|
+
longer retargets Enter, Ctrl-F, or Ctrl-X when the selected session
|
|
16
|
+
disappears (`--track`/`--id-nth` plus a non-actionable placeholder row).
|
|
17
|
+
- Ctrl-F now forks: fzf's `--print-query`/`--expect` output (query, key, row)
|
|
18
|
+
was parsed as key, query, row, so forking resumed the original session.
|
|
19
|
+
- Require fzf >= 0.73 in the launcher, installer, README, and AGENTS — the
|
|
20
|
+
`every()` refresh event the picker relies on was added in 0.73.
|
|
21
|
+
- Transcript extraction caches are written 0700/0600 regardless of the caller's
|
|
22
|
+
umask, and existing caches are tightened on refresh.
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
- The `sesh-list` agent tool queries the global session store, so it lists
|
|
27
|
+
every project directory (roots, non-archived) instead of only the current
|
|
28
|
+
project; directory filtering now happens before the limit.
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
|
|
32
|
+
- Real-fzf PTY regressions for selection tracking and fork parsing
|
|
33
|
+
(`npm run test:picker`), an agent-tool contract/SQL suite, and fzf
|
|
34
|
+
version-gate and cache-permission regressions.
|
|
35
|
+
|
|
10
36
|
## [0.1.3] - 2026-09-12
|
|
11
37
|
|
|
12
38
|
### Added
|
|
@@ -48,7 +74,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
48
74
|
- Fixture-database regression suite, TypeScript typechecking, and ShellCheck in
|
|
49
75
|
CI.
|
|
50
76
|
|
|
51
|
-
[Unreleased]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.1.
|
|
77
|
+
[Unreleased]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.1.4...HEAD
|
|
78
|
+
[0.1.4]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.1.3...v0.1.4
|
|
52
79
|
[0.1.3]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.1.2...v0.1.3
|
|
53
80
|
[0.1.2]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.1.1...v0.1.2
|
|
54
81
|
[0.1.1]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.1.0...v0.1.1
|
package/README.md
CHANGED
|
@@ -82,7 +82,7 @@ ctrl+o # inside opencode — picker
|
|
|
82
82
|
### Requirements
|
|
83
83
|
|
|
84
84
|
- [opencode](https://opencode.ai), used at least once
|
|
85
|
-
- `bash`, `jq`, and [`fzf`](https://github.com/junegunn/fzf) `>= 0.
|
|
85
|
+
- `bash`, `jq`, and [`fzf`](https://github.com/junegunn/fzf) `>= 0.73`
|
|
86
86
|
- `sqlite3` recommended — the picker queries the session DB directly in
|
|
87
87
|
milliseconds and falls back to the slower `opencode db` CLI without it
|
|
88
88
|
- [`glow`](https://github.com/charmbracelet/glow) optional — styles the
|
|
@@ -104,7 +104,7 @@ bash install.sh --uninstall # from source
|
|
|
104
104
|
| --- | --- |
|
|
105
105
|
| Type | Search titles and transcript text across all directories |
|
|
106
106
|
| Enter | Resume the selected session in this terminal |
|
|
107
|
-
| Ctrl-F
|
|
107
|
+
| Ctrl-F | Resume as a fork (the original is untouched) |
|
|
108
108
|
| Ctrl-G | Toggle current-directory scope / all sessions |
|
|
109
109
|
| Space (empty query) | Toggle the transcript preview |
|
|
110
110
|
| `?` (empty query) | Toggle shortcut help |
|
package/bin/sesh-list.sh
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
# TSV: agentId, sessionId, liveState, display, cwd, trackingId.
|
|
7
7
|
# Every row, including headers/notices, requires a unique nonempty trackingId.
|
|
8
8
|
set -euo pipefail
|
|
9
|
+
umask 077
|
|
9
10
|
|
|
10
11
|
LIMIT=0
|
|
11
12
|
LIMIT_SET=0
|
|
@@ -17,6 +18,7 @@ INCLUDE_ARCHIVED=0
|
|
|
17
18
|
ARCHIVED_SET=0
|
|
18
19
|
STATE_DIR=''
|
|
19
20
|
QUERY=''
|
|
21
|
+
SELECTED_ID=''
|
|
20
22
|
SCOPE_CWD=''
|
|
21
23
|
usage() { echo "usage: ${0##*/} [--refresh] [--wait-lock SECONDS] --state-dir DIR [--cwd] [--limit N] [--archived] [--toggle-scope] [--query TEXT]" >&2; }
|
|
22
24
|
while [ "$#" -gt 0 ]; do
|
|
@@ -29,6 +31,7 @@ while [ "$#" -gt 0 ]; do
|
|
|
29
31
|
--archived) INCLUDE_ARCHIVED=1; ARCHIVED_SET=1 ;;
|
|
30
32
|
--toggle-scope) TOGGLE_SCOPE=1 ;;
|
|
31
33
|
--query) QUERY=${2:-}; shift ;;
|
|
34
|
+
--selected-id) SELECTED_ID=${2:-}; shift ;;
|
|
32
35
|
--help) usage; exit 0 ;;
|
|
33
36
|
*) echo "unknown option: $1" >&2; usage; exit 2 ;;
|
|
34
37
|
esac
|
|
@@ -111,18 +114,24 @@ set_status() { atomic_text "$STATUS" "$1"; }
|
|
|
111
114
|
render() {
|
|
112
115
|
local state=''
|
|
113
116
|
[ -f "$STATUS" ] && state=$(cat "$STATUS" 2>/dev/null || true)
|
|
114
|
-
if [ ! -
|
|
117
|
+
if [ ! -f "$SNAPSHOT" ]; then
|
|
115
118
|
[ -n "$state" ] && [ "$state" != fresh ] && printf '\t\tunknown\t%s%s%s\t\tnotice:live-state\n' "$YELLOW" "$state" "$RESET"
|
|
116
119
|
return 0
|
|
117
120
|
fi
|
|
118
121
|
{
|
|
119
122
|
[ -n "$state" ] && [ "$state" != fresh ] && printf '\t\tunknown\t%s%s%s\t\tnotice:live-state\n' "$YELLOW" "$state" "$RESET"
|
|
120
|
-
"$JQ_BIN" -sr --arg green "$GREEN" --arg cyan "$CYAN" --arg gray "$GRAY" --arg magenta "$MAGENTA" --arg red "$RED" --arg bold "$BOLD" --arg reset "$RESET" --arg query "$QUERY" --arg home "$HOME" '
|
|
123
|
+
"$JQ_BIN" -sr --arg green "$GREEN" --arg cyan "$CYAN" --arg gray "$GRAY" --arg magenta "$MAGENTA" --arg red "$RED" --arg bold "$BOLD" --arg yellow "$YELLOW" --arg reset "$RESET" --arg query "$QUERY" --arg home "$HOME" --arg selected "$SELECTED_ID" '
|
|
121
124
|
def ago($epoch):
|
|
122
125
|
((now - ($epoch | tonumber)) | floor) as $s
|
|
123
126
|
| if $s < 60 then "now" elif $s < 3600 then "\($s / 60 | floor)m" elif $s < 86400 then "\($s / 3600 | floor)h" else "\($s / 86400 | floor)d" end;
|
|
124
127
|
def homepath:
|
|
125
128
|
. as $p | if $p == "" then "?" elif $p == $home then "~" elif ($home != "" and startswith($home + "/")) then "~" + .[($home|length):] else . end;
|
|
129
|
+
# Preserve a vanished selection as a non-actionable row with its original
|
|
130
|
+
# tracking key. Otherwise fzf falls back to a different session. Keep it
|
|
131
|
+
# until the user moves away; empty field 2 makes Enter/Ctrl-F/Ctrl-X no-ops.
|
|
132
|
+
(if ($selected | test("^ses_[A-Za-z0-9]+$")) and (any(.sessionId == $selected) | not)
|
|
133
|
+
then (["", "", "unknown", ($yellow + "Selected session is no longer available; choose another session" + $reset), "", $selected] | @tsv)
|
|
134
|
+
else empty end),
|
|
126
135
|
(($query | ascii_downcase) as $q
|
|
127
136
|
| map(select($q == "" or (.searchText | contains($q))))
|
|
128
137
|
| group_by(.cwd)
|
|
@@ -159,8 +168,22 @@ acquire_lock_wait() {
|
|
|
159
168
|
done
|
|
160
169
|
}
|
|
161
170
|
|
|
171
|
+
private_dir() {
|
|
172
|
+
# These are dedicated sesh directories, never the shared XDG parent. Refuse
|
|
173
|
+
# symlinks/foreign ownership rather than changing permissions on their target.
|
|
174
|
+
[ ! -L "$1" ] || { echo "sesh: private directory is a symlink: $1" >&2; return 1; }
|
|
175
|
+
mkdir -p "$1"
|
|
176
|
+
[ -O "$1" ] || { echo "sesh: private directory is not owned by this user: $1" >&2; return 1; }
|
|
177
|
+
chmod 700 "$1"
|
|
178
|
+
}
|
|
179
|
+
|
|
162
180
|
refresh() {
|
|
163
|
-
|
|
181
|
+
private_dir "$STATE_DIR"
|
|
182
|
+
private_dir "$CACHE_DIR"
|
|
183
|
+
private_dir "$EXTRACTIONS"
|
|
184
|
+
# Migrate warm caches too, including temporary files left by older versions.
|
|
185
|
+
# Do not follow symlinks or chmod unrelated files outside our extraction store.
|
|
186
|
+
find "$EXTRACTIONS" -maxdepth 1 -type f -user "$(id -u)" \( -name 'ses_*.json' -o -name 'ses_*.json.tmp' \) -exec chmod 600 {} +
|
|
164
187
|
if [ "${SESH_LOCK_HELD:-}" != "$STATE_DIR" ]; then
|
|
165
188
|
if [ "$WAIT_LOCK" -gt 0 ]; then acquire_lock_wait || return $?; else acquire_lock || return 0; fi
|
|
166
189
|
fi
|
|
@@ -180,7 +203,6 @@ refresh() {
|
|
|
180
203
|
if [ -f "$SNAPSHOT" ]; then set_status 'stale: neither sqlite3 nor opencode is available; showing last good snapshot'; else set_status 'unavailable: neither sqlite3 nor opencode is available'; fi
|
|
181
204
|
return 0
|
|
182
205
|
fi
|
|
183
|
-
mkdir -p "$EXTRACTIONS"
|
|
184
206
|
local scratch sessions parts cache_records cache_state historical needs_extract out
|
|
185
207
|
scratch=$(mktemp -d "$STATE_DIR/.refresh.XXXXXX")
|
|
186
208
|
refresh_scratch=$scratch
|
|
@@ -199,6 +221,9 @@ refresh() {
|
|
|
199
221
|
if [ -f "$SNAPSHOT" ]; then set_status 'stale: opencode database query failed; showing last good snapshot'; else set_status 'unavailable: opencode database query failed'; fi
|
|
200
222
|
return 0
|
|
201
223
|
}
|
|
224
|
+
# sqlite3 emits no bytes for zero rows. Publish an empty snapshot so removal
|
|
225
|
+
# of the final session also produces the non-actionable selection notice.
|
|
226
|
+
[ -s "$sessions" ] || printf '[]\n' > "$sessions"
|
|
202
227
|
"$JQ_BIN" -e 'type == "array"' "$sessions" >/dev/null 2>&1 || {
|
|
203
228
|
if [ -f "$SNAPSHOT" ]; then set_status 'stale: opencode database query failed; showing last good snapshot'; else set_status 'unavailable: opencode database query failed'; fi
|
|
204
229
|
return 0
|
|
@@ -261,8 +286,9 @@ refresh() {
|
|
|
261
286
|
updatedEpoch: (($updated / 1000 | floor) | if . > 0 then . else ($created / 1000 | floor) end),
|
|
262
287
|
fulltextLower: $fulltext}') || record=''
|
|
263
288
|
if [ -n "$record" ]; then
|
|
264
|
-
|
|
265
|
-
|
|
289
|
+
cache_tmp=$(mktemp "$EXTRACTIONS/$sid.json.XXXXXX")
|
|
290
|
+
printf '%s\n' "$record" > "$cache_tmp"
|
|
291
|
+
mv "$cache_tmp" "$cache"
|
|
266
292
|
printf '%s\n' "$record" >> "$historical"
|
|
267
293
|
fi
|
|
268
294
|
done < "$needs_extract"
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
# A short-lived worker owned by one picker. It serializes expensive refreshes;
|
|
3
3
|
# fzf only ever asks the list script to render the last published snapshot.
|
|
4
4
|
set -euo pipefail
|
|
5
|
+
umask 077
|
|
5
6
|
|
|
6
7
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
7
8
|
# The list script can be overridden for reuse; default is the opencode list.
|
|
@@ -14,6 +15,7 @@ SCOPE=''
|
|
|
14
15
|
LIMIT=60
|
|
15
16
|
TARGET=''
|
|
16
17
|
TIMEOUT=12
|
|
18
|
+
SELECTED_ID=''
|
|
17
19
|
|
|
18
20
|
usage() { echo "usage: ${0##*/} init|worker|render|request|wait --state-dir DIR [options]" >&2; exit 2; }
|
|
19
21
|
while [ "$#" -gt 0 ]; do
|
|
@@ -22,6 +24,7 @@ while [ "$#" -gt 0 ]; do
|
|
|
22
24
|
--scope) [ "$#" -ge 2 ] || usage; SCOPE=$2; shift ;;
|
|
23
25
|
--limit) [ "$#" -ge 2 ] || usage; LIMIT=$2; shift ;;
|
|
24
26
|
--query) [ "$#" -ge 2 ] || usage; QUERY=$2; shift ;;
|
|
27
|
+
--selected-id) [ "$#" -ge 2 ] || usage; SELECTED_ID=$2; shift ;;
|
|
25
28
|
--target) [ "$#" -ge 2 ] || usage; TARGET=$2; shift ;;
|
|
26
29
|
--timeout) [ "$#" -ge 2 ] || usage; TIMEOUT=$2; shift ;;
|
|
27
30
|
*) usage ;;
|
|
@@ -49,7 +52,7 @@ case "$MODE" in
|
|
|
49
52
|
atomic_text "$STATE_DIR/worker-complete" 0
|
|
50
53
|
;;
|
|
51
54
|
render)
|
|
52
|
-
"$LIST" --state-dir "$STATE_DIR" --query "${QUERY:-}"
|
|
55
|
+
"$LIST" --state-dir "$STATE_DIR" --query "${QUERY:-}" --selected-id "$SELECTED_ID"
|
|
53
56
|
# An empty, first snapshot should not look like a broken picker.
|
|
54
57
|
if [ ! -f "$STATE_DIR/status" ] && [ -f "$STATE_DIR/worker.pid" ]; then
|
|
55
58
|
printf '\t\tunknown\tLoading sessions…\t\tnotice:loading\n'
|
package/bin/sesh-shortcuts.sh
CHANGED
|
@@ -5,7 +5,7 @@ sesh shortcuts
|
|
|
5
5
|
|
|
6
6
|
Type Search session titles and transcript text
|
|
7
7
|
Enter Resume the selected session here
|
|
8
|
-
Ctrl-F
|
|
8
|
+
Ctrl-F Resume the selected session as a fork
|
|
9
9
|
Ctrl-G Toggle current-directory scope / all sessions
|
|
10
10
|
Space Toggle transcript preview
|
|
11
11
|
? Toggle this help
|
package/bin/sesh.sh
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
# Refreshes publish one state-dir snapshot; fzf query changes render that
|
|
5
5
|
# snapshot only, so typing never starts competing scans.
|
|
6
6
|
set -euo pipefail
|
|
7
|
+
umask 077
|
|
7
8
|
|
|
8
9
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
9
10
|
|
|
@@ -25,8 +26,8 @@ version=$("$SESH_FZF" --version 2>/dev/null || true)
|
|
|
25
26
|
if [[ "$version" =~ ^([0-9]+)\.([0-9]+)(\.([0-9]+))? ]]; then
|
|
26
27
|
fzf_major=$((10#${BASH_REMATCH[1]}))
|
|
27
28
|
fzf_minor=$((10#${BASH_REMATCH[2]}))
|
|
28
|
-
if [ "$fzf_major" -eq 0 ] && [ "$fzf_minor" -lt
|
|
29
|
-
echo "sesh: fzf >= 0.
|
|
29
|
+
if [ "$fzf_major" -eq 0 ] && [ "$fzf_minor" -lt 73 ]; then
|
|
30
|
+
echo "sesh: fzf >= 0.73.0 is required (found $version)" >&2
|
|
30
31
|
exit 1
|
|
31
32
|
fi
|
|
32
33
|
else
|
|
@@ -92,7 +93,7 @@ cleanup() {
|
|
|
92
93
|
trap cleanup EXIT
|
|
93
94
|
trap 'exit 130' HUP INT TERM
|
|
94
95
|
|
|
95
|
-
cache_cmd="$q_worker render --state-dir $q_state --query {q}"
|
|
96
|
+
cache_cmd="$q_worker render --state-dir $q_state --query {q} --selected-id {6}"
|
|
96
97
|
preview_cmd="if [ -f $q_state/help ]; then $q_shortcuts; else $q_preview $q_state {2}; fi"
|
|
97
98
|
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(?)'"
|
|
98
99
|
toggle_cmd="$q_list --toggle-scope --state-dir $q_state >/dev/null"
|
|
@@ -102,7 +103,7 @@ query="$picker_query"
|
|
|
102
103
|
while :; do
|
|
103
104
|
selection=''
|
|
104
105
|
set +e
|
|
105
|
-
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 --disabled --layout=reverse --info=hidden --prompt='❯ ' \
|
|
106
|
+
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='❯ ' \
|
|
106
107
|
--preview="$preview_cmd" --preview-window='right,50%,wrap,hidden' \
|
|
107
108
|
--bind='space:transform:[ -z {q} ] && echo toggle-preview || echo "put( )"' \
|
|
108
109
|
--bind="$help_bind" \
|
|
@@ -113,10 +114,11 @@ selection=$(FZF_DEFAULT_COMMAND='printf ""' FZF_DEFAULT_OPTS='' FZF_DEFAULT_OPTS
|
|
|
113
114
|
status=$?
|
|
114
115
|
set -e
|
|
115
116
|
case "$status" in 0) ;; 1|130) exit 0;; *) echo "sesh: picker failed ($status)" >&2; exit "$status";; esac
|
|
116
|
-
key
|
|
117
|
-
selection=${selection#*$'\n'}
|
|
117
|
+
# --print-query precedes the --expect key (empty for Enter), then the row.
|
|
118
118
|
query=${selection%%$'\n'*}
|
|
119
119
|
selection=${selection#*$'\n'}
|
|
120
|
+
key=${selection%%$'\n'*}
|
|
121
|
+
selection=${selection#*$'\n'}
|
|
120
122
|
session_id=$(printf '%s\n' "$selection" | cut -f2)
|
|
121
123
|
[ -n "$session_id" ] || continue
|
|
122
124
|
# Dispatch queues a new worker scan and waits for that exact serialized poll.
|
package/install.sh
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# Install sesh for the current user.
|
|
3
3
|
# Terminal- and OS-agnostic: macOS and Linux, any terminal. No administrator
|
|
4
|
-
# privileges, downloads, or shell-rc edits. Runtime requires jq, fzf >= 0.
|
|
4
|
+
# privileges, downloads, or shell-rc edits. Runtime requires jq, fzf >= 0.73
|
|
5
5
|
# and sqlite3 or the opencode CLI; the in-TUI panel additionally needs the
|
|
6
6
|
# packages opencode installs for local plugins (handled below).
|
|
7
7
|
set -euo pipefail
|
|
@@ -120,8 +120,8 @@ for tool in bash jq fzf; do
|
|
|
120
120
|
done
|
|
121
121
|
fzf_version=$(fzf --version 2>/dev/null || true)
|
|
122
122
|
if [[ "$fzf_version" =~ ^([0-9]+)\.([0-9]+)(\.([0-9]+))? ]]; then
|
|
123
|
-
if [ "$((10#${BASH_REMATCH[1]}))" -eq 0 ] && [ "$((10#${BASH_REMATCH[2]}))" -lt
|
|
124
|
-
fail "fzf >= 0.
|
|
123
|
+
if [ "$((10#${BASH_REMATCH[1]}))" -eq 0 ] && [ "$((10#${BASH_REMATCH[2]}))" -lt 73 ]; then
|
|
124
|
+
fail "fzf >= 0.73.0 is required (found $fzf_version)."
|
|
125
125
|
fi
|
|
126
126
|
else
|
|
127
127
|
fail "cannot parse fzf version from: $fzf_version."
|
|
@@ -12,7 +12,10 @@ export default tool({
|
|
|
12
12
|
args: {
|
|
13
13
|
limit: tool.schema
|
|
14
14
|
.number()
|
|
15
|
-
.
|
|
15
|
+
.int()
|
|
16
|
+
.min(0)
|
|
17
|
+
.max(1000)
|
|
18
|
+
.describe("Maximum sessions to return (0–1000; 0 returns none)")
|
|
16
19
|
.default(50),
|
|
17
20
|
directory: tool.schema
|
|
18
21
|
.string()
|
|
@@ -20,23 +23,29 @@ export default tool({
|
|
|
20
23
|
.optional(),
|
|
21
24
|
},
|
|
22
25
|
async execute(args) {
|
|
26
|
+
// Validate here as well as in the schema for direct/programmatic callers.
|
|
27
|
+
if (!Number.isInteger(args.limit) || args.limit < 0 || args.limit > 1000) {
|
|
28
|
+
throw new Error("limit must be an integer between 0 and 1000")
|
|
29
|
+
}
|
|
23
30
|
const bun = (globalThis as any).Bun
|
|
24
31
|
if (!bun || typeof bun.$ !== "function") {
|
|
25
32
|
throw new Error("This tool requires the Bun runtime (globalThis.Bun.$)")
|
|
26
33
|
}
|
|
27
34
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
)
|
|
35
|
+
// `session list` is project-scoped and hides only children of forks. The
|
|
36
|
+
// global store has no project instance, so query the SQLite session table
|
|
37
|
+
// directly (OpenCode 1.18.30). Match `Session.listGlobal`/the picker: roots
|
|
38
|
+
// only, non-archived. Bun quotes the SQL as one shell argument; SQL string
|
|
39
|
+
// literals need their own escaping as well.
|
|
40
|
+
const where = args.directory === undefined
|
|
41
|
+
? ""
|
|
42
|
+
: ` AND directory = '${args.directory.replaceAll("'", "''")}'`
|
|
43
|
+
const query = `SELECT id, title, directory, time_updated AS updated, time_created AS created
|
|
44
|
+
FROM session WHERE parent_id IS NULL AND time_archived IS NULL${where}
|
|
45
|
+
ORDER BY time_updated DESC, id DESC LIMIT ${args.limit}`
|
|
46
|
+
const text = await bun.$`opencode db ${query} --format json`.text()
|
|
47
|
+
const sessions = JSON.parse(text.trim() || "[]")
|
|
48
|
+
if (!Array.isArray(sessions)) throw new Error("Unexpected opencode db result: expected an array")
|
|
49
|
+
return JSON.stringify(sessions)
|
|
41
50
|
},
|
|
42
51
|
})
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-sesh",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Browse, search, preview and resume opencode sessions from the terminal and the TUI.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -40,17 +40,23 @@
|
|
|
40
40
|
"node": ">=20"
|
|
41
41
|
},
|
|
42
42
|
"scripts": {
|
|
43
|
+
"prepare": "husky",
|
|
43
44
|
"test": "bash tests/sesh.sh",
|
|
45
|
+
"test:picker": "python3 tests/picker.py",
|
|
44
46
|
"typecheck": "tsc -p tsconfig.json",
|
|
45
|
-
"lint:sh": "
|
|
47
|
+
"lint:sh": "for script in bin/*.sh bin/sesh install.sh tests/*.sh; do bash -n \"$script\" || exit; done",
|
|
48
|
+
"prepublishOnly": "npm test && npm run typecheck"
|
|
46
49
|
},
|
|
47
50
|
"devDependencies": {
|
|
51
|
+
"@commitlint/cli": "^21.2.2",
|
|
52
|
+
"@commitlint/config-conventional": "^21.2.2",
|
|
48
53
|
"@opencode-ai/plugin": "1.18.30",
|
|
49
54
|
"@opencode-ai/sdk": "1.18.30",
|
|
50
55
|
"@opentui/core": "^0.5.11",
|
|
51
56
|
"@opentui/keymap": "^0.5.11",
|
|
52
57
|
"@opentui/solid": "^0.5.11",
|
|
53
58
|
"@types/node": "26.5.1",
|
|
59
|
+
"husky": "^9.1.7",
|
|
54
60
|
"solid-js": "1.9.12",
|
|
55
61
|
"typescript": "7.0.2"
|
|
56
62
|
}
|