opencode-sesh 1.0.7 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +9 -8
- package/CHANGELOG.md +14 -0
- package/README.md +4 -9
- package/bin/sesh +0 -4
- package/bin/sesh-costs.sh +5 -9
- package/bin/sesh-list.sh +6 -17
- package/bin/sesh-preview.sh +6 -13
- package/bin/sesh-prune.sh +13 -23
- package/bin/sesh-waiting.sh +8 -13
- package/bin/sesh.sh +9 -4
- package/install.sh +4 -5
- package/package.json +2 -3
- package/plugins/sesh-list.ts +4 -4
- package/tui-plugins/sesh-panel/package.json +1 -2
- package/tui-plugins/sesh-panel/tui.tsx +35 -6
- package/ROADMAP.md +0 -60
- package/bin/sesh-retitle.sh +0 -139
package/AGENTS.md
CHANGED
|
@@ -40,11 +40,10 @@ HOME=/tmp/fakehome bash install.sh # installer smoke test (never touches ~/.co
|
|
|
40
40
|
Never hardcode install paths. `sesh-waiting.sh` owns the needs-input query
|
|
41
41
|
(shared heuristic with the TUI sidebar copy, marked `NEEDS_INPUT_SQL` in
|
|
42
42
|
both); `sesh-prune.sh` archives stale sessions; `sesh-costs.sh` sums
|
|
43
|
-
assistant-message cost per directory
|
|
44
|
-
|
|
45
|
-
`
|
|
46
|
-
|
|
47
|
-
stay on `opencode session delete`.
|
|
43
|
+
assistant-message cost per directory. Reads go through `sqlite3 -readonly`
|
|
44
|
+
(required; `opencode db` no longer exists); the direct `UPDATE` in
|
|
45
|
+
`sesh-prune.sh` (archive) is the only sanctioned database write — the API has
|
|
46
|
+
no archive endpoint — and hard deletes stay on `opencode session delete`.
|
|
48
47
|
- `contrib/` — caller-side integrations (currently cmux). Nothing here may add
|
|
49
48
|
window management to sesh itself.
|
|
50
49
|
- `tui-plugins/sesh-panel/tui.tsx` — SolidJS panel, `/** @jsxImportSource @opentui/solid */`.
|
|
@@ -105,19 +104,21 @@ which is a pure snapshot lookup — and prints a progress line to stderr while i
|
|
|
105
104
|
waits, because fzf has already exited and the terminal is idle. opencode has no
|
|
106
105
|
live-agent API, so `liveState` is always `unknown` and rows render gray unless
|
|
107
106
|
they are archived (opt-in, tagged `· archived`);
|
|
108
|
-
dispatch is always `opencode --session <id
|
|
107
|
+
dispatch is always `opencode --session <id>`; Ctrl-F or `--fork` first creates the
|
|
108
|
+
fork with `opencode api session.fork` (the CLI has no fork flag) and resumes the new id.
|
|
109
109
|
Resume happens in place (`exec` after `cd` to the session's cwd).
|
|
110
110
|
|
|
111
111
|
## Conventions
|
|
112
112
|
|
|
113
113
|
- Default scope is **all** sessions, every directory. `--cwd`/Ctrl-G narrows.
|
|
114
|
-
Archived sessions and child sessions (
|
|
114
|
+
Archived sessions and child sessions (spawned subagents) are
|
|
115
115
|
hidden unless `--archived`; metadata and cache pruning still see them.
|
|
116
116
|
- **Text parts only** in search/preview; reasoning and tool payloads are
|
|
117
117
|
excluded (asserted in tests).
|
|
118
118
|
- Validate session ids (`^ses_[A-Za-z0-9]+$`) before SQL interpolation — never
|
|
119
119
|
interpolate unvalidated input. Open SQLite read-only (`sqlite3 -readonly`).
|
|
120
|
-
-
|
|
120
|
+
- Resolve the database as `SESH_DB`, then `$XDG_DATA_HOME/opencode/opencode.db`, then the
|
|
121
|
+
`db` line of `opencode debug paths`. Never parse
|
|
121
122
|
`sqlite3 -json` in `-R` mode; it pretty-prints — parse as JSON.
|
|
122
123
|
- `jq -R` output stays JSON-quoted: use `-Rr` when a shell variable needs the
|
|
123
124
|
raw string.
|
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,18 @@ All notable changes to this project are documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.1.0] - 2026-10-09
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- Align database access, fork and writes with the OpenCode V2 CLI
|
|
13
|
+
|
|
14
|
+
## [1.0.8] - 2026-10-08
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- **tui:** Keep sidebar context menu clickable and dismiss preview in one press
|
|
19
|
+
|
|
8
20
|
## [1.0.7] - 2026-10-08
|
|
9
21
|
|
|
10
22
|
### Fixed
|
|
@@ -460,6 +472,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
460
472
|
- Fixture-database regression suite, TypeScript typechecking, and ShellCheck in
|
|
461
473
|
CI.
|
|
462
474
|
|
|
475
|
+
[1.1.0]: https://github.com/ozandogrultan/opencode-sesh/compare/v1.0.8...v1.1.0
|
|
476
|
+
[1.0.8]: https://github.com/ozandogrultan/opencode-sesh/compare/v1.0.7...v1.0.8
|
|
463
477
|
[1.0.7]: https://github.com/ozandogrultan/opencode-sesh/compare/v1.0.6...v1.0.7
|
|
464
478
|
[1.0.6]: https://github.com/ozandogrultan/opencode-sesh/compare/v1.0.5...v1.0.6
|
|
465
479
|
[1.0.5]: https://github.com/ozandogrultan/opencode-sesh/compare/v1.0.4...v1.0.5
|
package/README.md
CHANGED
|
@@ -100,8 +100,7 @@ option+o # inside opencode — picker
|
|
|
100
100
|
|
|
101
101
|
- [opencode](https://opencode.ai), used at least once
|
|
102
102
|
- `bash`, `jq`, and [`fzf`](https://github.com/junegunn/fzf) `>= 0.73`
|
|
103
|
-
- `sqlite3`
|
|
104
|
-
milliseconds and falls back to the slower `opencode db` CLI without it
|
|
103
|
+
- `sqlite3` — the picker queries the session DB directly
|
|
105
104
|
- [`glow`](https://github.com/charmbracelet/glow) optional — styles the
|
|
106
105
|
transcript preview as Markdown; without it the preview shows the plain
|
|
107
106
|
Markdown, unchanged otherwise
|
|
@@ -140,7 +139,7 @@ Flags: `--cwd` (current directory only), `--limit N` (default: all),
|
|
|
140
139
|
|
|
141
140
|
`sesh prune [--older-than 30d] [--dry-run] [--yes] [--delete]` archives stale
|
|
142
141
|
sessions (not updated within the threshold) so the list stays triageable.
|
|
143
|
-
Pinned sessions, sessions waiting on you,
|
|
142
|
+
Pinned sessions, sessions waiting on you, subagent child sessions and already-archived
|
|
144
143
|
sessions are never touched; archiving is reversible, while `--delete`
|
|
145
144
|
hard-deletes through `opencode session delete`. Without `--dry-run`, a TTY run
|
|
146
145
|
confirms first and a non-TTY run needs `--yes`.
|
|
@@ -149,10 +148,6 @@ confirms first and a non-TTY run needs `--yes`.
|
|
|
149
148
|
recent window beside the lifetime total, so a day's work reads as a
|
|
150
149
|
per-project record.
|
|
151
150
|
|
|
152
|
-
`sesh retitle [--dry-run] [--yes]` replaces auto-generated placeholder titles
|
|
153
|
-
(`New session - …`) with the start of the session's first user message. Only
|
|
154
|
-
placeholder titles are touched, and the same dry-run/`--yes` gating applies.
|
|
155
|
-
|
|
156
151
|
When searching, title matches outrank transcript-only matches and the current
|
|
157
152
|
project rises, so the list answers "where was that thing I worked on" before it
|
|
158
153
|
answers "what is newest". Pins still win outright.
|
|
@@ -230,8 +225,8 @@ All variables are optional.
|
|
|
230
225
|
|
|
231
226
|
| Variable | Default | Effect |
|
|
232
227
|
| --- | --- | --- |
|
|
233
|
-
| `SESH_DB` | `opencode
|
|
234
|
-
| `SESH_SQLITE` | `sqlite3` | query tool
|
|
228
|
+
| `SESH_DB` | `opencode debug paths` | opencode SQLite database |
|
|
229
|
+
| `SESH_SQLITE` | `sqlite3` | query tool |
|
|
235
230
|
| `SESH_JQ` / `SESH_FZF` | `PATH` | explicit executable overrides |
|
|
236
231
|
| `SESH_GLOW` | first `glow` on `PATH` | optional Markdown preview renderer |
|
|
237
232
|
| `SESH_OPENCODE` | `opencode` | opencode executable (resume / delete) |
|
package/bin/sesh
CHANGED
package/bin/sesh-costs.sh
CHANGED
|
@@ -29,12 +29,12 @@ case "$days" in ''|*[!0-9]*) echo "--days must be a non-negative integer" >&2; e
|
|
|
29
29
|
|
|
30
30
|
resolve_db() {
|
|
31
31
|
if [ -n "${SESH_DB:-}" ]; then printf '%s\n' "$SESH_DB"; return 0; fi
|
|
32
|
-
local from_cli=''
|
|
32
|
+
local default="${XDG_DATA_HOME:-$HOME/.local/share}/opencode/opencode.db" from_cli=''
|
|
33
|
+
if [ -f "$default" ]; then printf '%s\n' "$default"; return 0; fi
|
|
33
34
|
if command -v "$OPENCODE_BIN" >/dev/null 2>&1; then
|
|
34
|
-
from_cli=$("$OPENCODE_BIN"
|
|
35
|
+
from_cli=$("$OPENCODE_BIN" debug paths 2>/dev/null | awk '$1 == "db" { print $2; exit }' || true)
|
|
35
36
|
fi
|
|
36
37
|
if [ -n "$from_cli" ] && [ -f "$from_cli" ]; then printf '%s\n' "$from_cli"; return 0; fi
|
|
37
|
-
if [ -f "$HOME/.local/share/opencode/opencode.db" ]; then printf '%s\n' "$HOME/.local/share/opencode/opencode.db"; return 0; fi
|
|
38
38
|
return 1
|
|
39
39
|
}
|
|
40
40
|
|
|
@@ -66,12 +66,8 @@ WHERE m.type = 'assistant'
|
|
|
66
66
|
GROUP BY s.directory
|
|
67
67
|
ORDER BY lifetime_cost DESC;"
|
|
68
68
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
else
|
|
72
|
-
command -v "$OPENCODE_BIN" >/dev/null 2>&1 || { echo 'sesh: neither sqlite3 nor opencode is available' >&2; exit 1; }
|
|
73
|
-
ROWS=$("$OPENCODE_BIN" db "$ROWS_QUERY" --format json)
|
|
74
|
-
fi
|
|
69
|
+
[ -n "$SQLITE_BIN" ] || { echo 'sesh: sqlite3 is required' >&2; exit 1; }
|
|
70
|
+
ROWS=$("$SQLITE_BIN" -json -readonly "$DB_PATH" "$ROWS_QUERY")
|
|
75
71
|
[ -n "$ROWS" ] || ROWS='[]'
|
|
76
72
|
|
|
77
73
|
if [ "$emit_json" = 1 ]; then
|
package/bin/sesh-list.sh
CHANGED
|
@@ -76,12 +76,12 @@ GREEN=$'\033[32m'; CYAN=$'\033[36m'; GRAY=$'\033[90m'; MAGENTA=$'\033[35m'; RED=
|
|
|
76
76
|
|
|
77
77
|
resolve_db() {
|
|
78
78
|
if [ -n "${SESH_DB:-}" ]; then printf '%s\n' "$SESH_DB"; return 0; fi
|
|
79
|
-
local from_cli=''
|
|
79
|
+
local default="${XDG_DATA_HOME:-$HOME/.local/share}/opencode/opencode.db" from_cli=''
|
|
80
|
+
if [ -f "$default" ]; then printf '%s\n' "$default"; return 0; fi
|
|
80
81
|
if command -v "$OPENCODE_BIN" >/dev/null 2>&1; then
|
|
81
|
-
from_cli=$("$OPENCODE_BIN"
|
|
82
|
+
from_cli=$("$OPENCODE_BIN" debug paths 2>/dev/null | awk '$1 == "db" { print $2; exit }' || true)
|
|
82
83
|
fi
|
|
83
84
|
if [ -n "$from_cli" ] && [ -f "$from_cli" ]; then printf '%s\n' "$from_cli"; return 0; fi
|
|
84
|
-
if [ -f "$HOME/.local/share/opencode/opencode.db" ]; then printf '%s\n' "$HOME/.local/share/opencode/opencode.db"; return 0; fi
|
|
85
85
|
return 1
|
|
86
86
|
}
|
|
87
87
|
|
|
@@ -92,18 +92,7 @@ elif command -v sqlite3 >/dev/null 2>&1; then
|
|
|
92
92
|
SQLITE_BIN=sqlite3
|
|
93
93
|
fi
|
|
94
94
|
|
|
95
|
-
|
|
96
|
-
# in milliseconds, while `opencode db` pays a full CLI startup per call and
|
|
97
|
-
# would make every preview keystroke laggy.
|
|
98
|
-
db_json() {
|
|
99
|
-
if [ -n "$SQLITE_BIN" ]; then
|
|
100
|
-
# Read-only: sesh never writes the store directly (deletes go through
|
|
101
|
-
# `opencode session delete`).
|
|
102
|
-
"$SQLITE_BIN" -json -readonly "$DB_PATH" "$1"
|
|
103
|
-
else
|
|
104
|
-
"$OPENCODE_BIN" db "$1" --format json
|
|
105
|
-
fi
|
|
106
|
-
}
|
|
95
|
+
db_json() { "$SQLITE_BIN" -json -readonly "$DB_PATH" "$1"; }
|
|
107
96
|
|
|
108
97
|
valid_session_id() { [[ "$1" =~ ^ses_[A-Za-z0-9]+$ ]]; }
|
|
109
98
|
|
|
@@ -258,8 +247,8 @@ refresh() {
|
|
|
258
247
|
if [ -f "$SNAPSHOT" ]; then set_status 'stale: opencode database is unavailable; showing last good snapshot'; else set_status 'unavailable: opencode database is unavailable'; fi
|
|
259
248
|
return 0
|
|
260
249
|
}
|
|
261
|
-
if [ -z "$SQLITE_BIN" ]
|
|
262
|
-
if [ -f "$SNAPSHOT" ]; then set_status 'stale:
|
|
250
|
+
if [ -z "$SQLITE_BIN" ]; then
|
|
251
|
+
if [ -f "$SNAPSHOT" ]; then set_status 'stale: sqlite3 is not available; showing last good snapshot'; else set_status 'unavailable: sqlite3 is not available'; fi
|
|
263
252
|
return 0
|
|
264
253
|
fi
|
|
265
254
|
local scratch sessions parts cache_records cache_state historical needs_extract out
|
package/bin/sesh-preview.sh
CHANGED
|
@@ -14,13 +14,10 @@ JQ_BIN=${SESH_JQ:-jq}
|
|
|
14
14
|
"$JQ_BIN" -se --arg sid "$session_id" 'any(.sessionId == $sid)' "$snapshot" >/dev/null 2>&1 \
|
|
15
15
|
|| { echo "(selected session is no longer in this snapshot)"; exit 0; }
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
DB_PATH=$("$OPENCODE_BIN" db path 2>/dev/null || true)
|
|
22
|
-
fi
|
|
23
|
-
[ -n "$DB_PATH" ] || DB_PATH="$HOME/.local/share/opencode/opencode.db"
|
|
17
|
+
OPENCODE_BIN=${SESH_OPENCODE:-opencode}
|
|
18
|
+
DB_PATH=${SESH_DB:-"${XDG_DATA_HOME:-$HOME/.local/share}/opencode/opencode.db"}
|
|
19
|
+
if [ -z "${SESH_DB:-}" ] && [ ! -f "$DB_PATH" ] && command -v "$OPENCODE_BIN" >/dev/null 2>&1; then
|
|
20
|
+
DB_PATH=$("$OPENCODE_BIN" debug paths 2>/dev/null | awk '$1 == "db" { print $2; exit }' || true)
|
|
24
21
|
fi
|
|
25
22
|
[ -f "$DB_PATH" ] || { echo "(opencode database is unavailable)"; exit 0; }
|
|
26
23
|
|
|
@@ -28,12 +25,8 @@ SQLITE_BIN=${SESH_SQLITE:-}
|
|
|
28
25
|
if [ -z "$SQLITE_BIN" ] && command -v sqlite3 >/dev/null 2>&1; then SQLITE_BIN=sqlite3; fi
|
|
29
26
|
SQL="SELECT json_object('role', type) AS msg, json_object('type', 'text', 'text', json_extract(data, '\$.text')) AS part, seq, -1 AS ordinal FROM session_message WHERE session_id = '$session_id' AND type = 'user' AND json_type(data, '\$.text') = 'text'
|
|
30
27
|
UNION ALL SELECT json_object('role', m.type) AS msg, c.value AS part, m.seq, CAST(c.key AS INTEGER) AS ordinal FROM session_message m, json_each(m.data, '\$.content') c WHERE m.session_id = '$session_id' AND m.type = 'assistant' AND json_extract(c.value, '\$.type') = 'text' ORDER BY seq DESC, ordinal DESC LIMIT 200;"
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
else
|
|
34
|
-
OPENCODE_BIN=${SESH_OPENCODE:-opencode}
|
|
35
|
-
rows=$("$OPENCODE_BIN" db "$SQL" --format json 2>/dev/null) || rows=''
|
|
36
|
-
fi
|
|
28
|
+
[ -n "$SQLITE_BIN" ] || { echo "(sqlite3 is required for previews)"; exit 0; }
|
|
29
|
+
rows=$("$SQLITE_BIN" -json -readonly "$DB_PATH" "$SQL" 2>/dev/null) || rows=''
|
|
37
30
|
[ -n "$rows" ] || { echo "(no displayable transcript messages)"; exit 0; }
|
|
38
31
|
|
|
39
32
|
# Text parts only, newest first, with role headers. The 120-block cap is applied
|
package/bin/sesh-prune.sh
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# Archive (or, with --delete, hard-delete) stale sessions: not updated within
|
|
3
3
|
# the threshold, never pinned, never waiting on the user, never already
|
|
4
|
-
# archived, never a
|
|
4
|
+
# archived, never a child (subagent) session.
|
|
5
5
|
#
|
|
6
6
|
# Archive is the default because it is reversible; hard deletes go through
|
|
7
7
|
# `opencode session delete` so its own safeguards apply. The archive itself
|
|
8
|
-
# is a single atomic UPDATE through the
|
|
9
|
-
#
|
|
10
|
-
#
|
|
8
|
+
# is a single atomic UPDATE through sqlite3 — the opencode server API has no
|
|
9
|
+
# archive endpoint, so a direct write is the only route (deletes stay on the
|
|
10
|
+
# CLI).
|
|
11
11
|
#
|
|
12
12
|
# Usage: sesh-prune.sh [--older-than 30d] [--dry-run] [--yes] [--delete]
|
|
13
13
|
# --older-than Nd|Nh|Nw (bare N means days; default 30d)
|
|
@@ -53,12 +53,12 @@ CUTOFF=$((NOW_MS - threshold_ms))
|
|
|
53
53
|
|
|
54
54
|
resolve_db() {
|
|
55
55
|
if [ -n "${SESH_DB:-}" ]; then printf '%s\n' "$SESH_DB"; return 0; fi
|
|
56
|
-
local from_cli=''
|
|
56
|
+
local default="${XDG_DATA_HOME:-$HOME/.local/share}/opencode/opencode.db" from_cli=''
|
|
57
|
+
if [ -f "$default" ]; then printf '%s\n' "$default"; return 0; fi
|
|
57
58
|
if command -v "$OPENCODE_BIN" >/dev/null 2>&1; then
|
|
58
|
-
from_cli=$("$OPENCODE_BIN"
|
|
59
|
+
from_cli=$("$OPENCODE_BIN" debug paths 2>/dev/null | awk '$1 == "db" { print $2; exit }' || true)
|
|
59
60
|
fi
|
|
60
61
|
if [ -n "$from_cli" ] && [ -f "$from_cli" ]; then printf '%s\n' "$from_cli"; return 0; fi
|
|
61
|
-
if [ -f "$HOME/.local/share/opencode/opencode.db" ]; then printf '%s\n' "$HOME/.local/share/opencode/opencode.db"; return 0; fi
|
|
62
62
|
return 1
|
|
63
63
|
}
|
|
64
64
|
|
|
@@ -72,14 +72,9 @@ fi
|
|
|
72
72
|
DB_PATH=$(resolve_db) || { echo 'sesh: session database unavailable' >&2; exit 1; }
|
|
73
73
|
[ -f "$DB_PATH" ] || { echo 'sesh: session database unavailable' >&2; exit 1; }
|
|
74
74
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
else
|
|
79
|
-
command -v "$OPENCODE_BIN" >/dev/null 2>&1 || { echo 'sesh: neither sqlite3 nor opencode is available' >&2; exit 1; }
|
|
80
|
-
query() { "$OPENCODE_BIN" db "$1" --format json; }
|
|
81
|
-
write_db() { "$OPENCODE_BIN" db "$1" --format json > /dev/null; }
|
|
82
|
-
fi
|
|
75
|
+
[ -n "$SQLITE_BIN" ] || { echo 'sesh: sqlite3 is required' >&2; exit 1; }
|
|
76
|
+
query() { "$SQLITE_BIN" -json -readonly "$DB_PATH" "$1"; }
|
|
77
|
+
write_db() { "$SQLITE_BIN" "$DB_PATH" "$1"; }
|
|
83
78
|
|
|
84
79
|
JQ_BIN=${SESH_JQ:-$(command -v jq || true)}
|
|
85
80
|
[ -n "$JQ_BIN" ] || { echo 'sesh: jq is required' >&2; exit 1; }
|
|
@@ -146,10 +141,8 @@ if [ "$hard_delete" = 0 ]; then
|
|
|
146
141
|
IDS=$(printf '%s' "$ELIGIBLE" | "$JQ_BIN" -r '.[].id')
|
|
147
142
|
IN_LIST=''
|
|
148
143
|
for id in $IDS; do
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
*) echo "Refusing: unexpected session id shape." >&2; exit 1 ;;
|
|
152
|
-
esac
|
|
144
|
+
[[ "$id" =~ ^ses_[A-Za-z0-9]+$ ]] || { echo "Refusing: unexpected session id shape." >&2; exit 1; }
|
|
145
|
+
IN_LIST="$IN_LIST'$id',"
|
|
153
146
|
done
|
|
154
147
|
IN_LIST=${IN_LIST%,}
|
|
155
148
|
write_db "UPDATE session_v2 SET time_archived = $NOW_MS WHERE id IN ($IN_LIST);"
|
|
@@ -159,10 +152,7 @@ else
|
|
|
159
152
|
failed=0
|
|
160
153
|
done_count=0
|
|
161
154
|
for id in $(printf '%s' "$ELIGIBLE" | "$JQ_BIN" -r '.[].id'); do
|
|
162
|
-
|
|
163
|
-
ses_[A-Za-z0-9]*) ;;
|
|
164
|
-
*) echo "Refusing: unexpected session id shape." >&2; exit 1 ;;
|
|
165
|
-
esac
|
|
155
|
+
[[ "$id" =~ ^ses_[A-Za-z0-9]+$ ]] || { echo "Refusing: unexpected session id shape." >&2; exit 1; }
|
|
166
156
|
if "$OPENCODE_BIN" session delete "$id" >/dev/null 2>&1; then
|
|
167
157
|
done_count=$((done_count + 1))
|
|
168
158
|
else
|
package/bin/sesh-waiting.sh
CHANGED
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
# is invisible cross-project.
|
|
7
7
|
#
|
|
8
8
|
# A session waits when it is not archived, is not a fork child, and either
|
|
9
|
-
# - has a `question` tool part
|
|
10
|
-
# message in the same session (awaiting an answer
|
|
9
|
+
# - has a `question` tool part still pending or running with no later user
|
|
10
|
+
# message in the same session (awaiting an answer; a dismissed question
|
|
11
|
+
# that errored is not waiting), or
|
|
11
12
|
# - has a `tool` part still marked `running` older than STUCK_AFTER_MS
|
|
12
13
|
# (awaiting approval, or orphaned by a dead server).
|
|
13
14
|
#
|
|
@@ -30,12 +31,12 @@ done
|
|
|
30
31
|
|
|
31
32
|
resolve_db() {
|
|
32
33
|
if [ -n "${SESH_DB:-}" ]; then printf '%s\n' "$SESH_DB"; return 0; fi
|
|
33
|
-
local from_cli=''
|
|
34
|
+
local default="${XDG_DATA_HOME:-$HOME/.local/share}/opencode/opencode.db" from_cli=''
|
|
35
|
+
if [ -f "$default" ]; then printf '%s\n' "$default"; return 0; fi
|
|
34
36
|
if command -v "$OPENCODE_BIN" >/dev/null 2>&1; then
|
|
35
|
-
from_cli=$("$OPENCODE_BIN"
|
|
37
|
+
from_cli=$("$OPENCODE_BIN" debug paths 2>/dev/null | awk '$1 == "db" { print $2; exit }' || true)
|
|
36
38
|
fi
|
|
37
39
|
if [ -n "$from_cli" ] && [ -f "$from_cli" ]; then printf '%s\n' "$from_cli"; return 0; fi
|
|
38
|
-
if [ -f "$HOME/.local/share/opencode/opencode.db" ]; then printf '%s\n' "$HOME/.local/share/opencode/opencode.db"; return 0; fi
|
|
39
40
|
return 1
|
|
40
41
|
}
|
|
41
42
|
|
|
@@ -88,14 +89,8 @@ WHERE COALESCE(s.time_archived, 0) = 0
|
|
|
88
89
|
ORDER BY s.time_updated DESC;
|
|
89
90
|
SQL
|
|
90
91
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
# through `opencode db`, hard deletes through `opencode session delete`).
|
|
94
|
-
ROWS=$("$SQLITE_BIN" -json -readonly "$DB_PATH" "$QUERY")
|
|
95
|
-
else
|
|
96
|
-
command -v "$OPENCODE_BIN" >/dev/null 2>&1 || { echo 'sesh: neither sqlite3 nor opencode is available' >&2; exit 1; }
|
|
97
|
-
ROWS=$("$OPENCODE_BIN" db "$QUERY" --format json)
|
|
98
|
-
fi
|
|
92
|
+
[ -n "$SQLITE_BIN" ] || { echo 'sesh: sqlite3 is required' >&2; exit 1; }
|
|
93
|
+
ROWS=$("$SQLITE_BIN" -json -readonly "$DB_PATH" "$QUERY")
|
|
99
94
|
|
|
100
95
|
# sqlite3 -json prints nothing (not []) when no rows match.
|
|
101
96
|
[ -n "$ROWS" ] || ROWS='[]'
|
package/bin/sesh.sh
CHANGED
|
@@ -37,7 +37,7 @@ fi
|
|
|
37
37
|
|
|
38
38
|
if [ "${1:-}" = --check ]; then
|
|
39
39
|
"$SESH_JQ" -en '"2026-01-01T00:00:00Z" | fromdateiso8601 | type == "number"' >/dev/null || { echo 'jq capability check failed' >&2; exit 1; }
|
|
40
|
-
command -v
|
|
40
|
+
command -v "${SESH_SQLITE:-sqlite3}" >/dev/null 2>&1 || { echo 'sqlite3 is not available' >&2; exit 1; }
|
|
41
41
|
# The TUI panel is a copied file that opencode imports once at startup. Report
|
|
42
42
|
# when the installed copy has fallen behind this package so upgrades are not
|
|
43
43
|
# silently stale.
|
|
@@ -203,9 +203,14 @@ fi
|
|
|
203
203
|
command -v "$OPENCODE_BIN" >/dev/null 2>&1 || { echo 'sesh: opencode executable unavailable' >&2; exit 1; }
|
|
204
204
|
# Resume in place: this terminal becomes the session. No tabs, no panes.
|
|
205
205
|
if [ "$fork" = 1 ]; then
|
|
206
|
-
(
|
|
207
|
-
|
|
208
|
-
|
|
206
|
+
forked=$(cd "$cwd" && "$OPENCODE_BIN" api session.fork --param "sessionID=$session_id" -d '{}' 2>/dev/null | "$SESH_JQ" -r '.data.id // empty' 2>/dev/null || true)
|
|
207
|
+
if [[ "$forked" =~ ^ses_[A-Za-z0-9]+$ ]]; then
|
|
208
|
+
session_id=$forked
|
|
209
|
+
else
|
|
210
|
+
echo 'sesh: could not fork the session; returning to the picker.' >&2
|
|
211
|
+
continue
|
|
212
|
+
fi
|
|
209
213
|
fi
|
|
214
|
+
( cd "$cwd" && exec "$OPENCODE_BIN" --session "$session_id" )
|
|
210
215
|
echo 'sesh: opencode exited; returning to the picker.' >&2
|
|
211
216
|
done
|
package/install.sh
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
# Install sesh for the current user.
|
|
3
3
|
# Terminal- and OS-agnostic: macOS and Linux, any terminal. No administrator
|
|
4
4
|
# privileges, downloads, or shell-rc edits. Runtime requires jq, fzf >= 0.73
|
|
5
|
-
# and sqlite3
|
|
6
|
-
# packages opencode installs for local plugins (handled
|
|
5
|
+
# and sqlite3; resuming and deleting need the opencode CLI. The in-TUI panel
|
|
6
|
+
# additionally needs the packages opencode installs for local plugins (handled
|
|
7
|
+
# below).
|
|
7
8
|
set -euo pipefail
|
|
8
9
|
|
|
9
10
|
fail() { printf 'sesh install: %s\n' "$*" >&2; exit 1; }
|
|
@@ -251,7 +252,6 @@ for path in \
|
|
|
251
252
|
bin/sesh-delete.sh \
|
|
252
253
|
bin/sesh-prune.sh \
|
|
253
254
|
bin/sesh-refresh-worker.sh \
|
|
254
|
-
bin/sesh-retitle.sh \
|
|
255
255
|
bin/sesh-waiting.sh \
|
|
256
256
|
bin/sesh-shortcuts.sh \
|
|
257
257
|
plugins/sesh-list.ts \
|
|
@@ -284,8 +284,7 @@ if [[ "$fzf_version" =~ ^([0-9]+)\.([0-9]+)(\.([0-9]+))? ]]; then
|
|
|
284
284
|
else
|
|
285
285
|
fail "cannot parse fzf version from: $fzf_version."
|
|
286
286
|
fi
|
|
287
|
-
command -v sqlite3 >/dev/null 2>&1 ||
|
|
288
|
-
|| fail "either sqlite3 or the opencode CLI is required."
|
|
287
|
+
command -v sqlite3 >/dev/null 2>&1 || fail "sqlite3 is required."
|
|
289
288
|
command -v opencode >/dev/null 2>&1 || note "opencode CLI not found: listing and previews work via sqlite3, but resume and delete need opencode."
|
|
290
289
|
|
|
291
290
|
umask 077
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-sesh",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Browse, search, preview and resume opencode sessions from the terminal and the TUI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -38,8 +38,7 @@
|
|
|
38
38
|
"AGENTS.md",
|
|
39
39
|
"CHANGELOG.md",
|
|
40
40
|
"LICENSE",
|
|
41
|
-
"README.md"
|
|
42
|
-
"ROADMAP.md"
|
|
41
|
+
"README.md"
|
|
43
42
|
],
|
|
44
43
|
"publishConfig": {
|
|
45
44
|
"access": "public"
|
package/plugins/sesh-list.ts
CHANGED
|
@@ -2,8 +2,8 @@ import { Plugin } from "@opencode/plugin"
|
|
|
2
2
|
import { homedir } from "node:os"
|
|
3
3
|
import { join } from "node:path"
|
|
4
4
|
|
|
5
|
-
function
|
|
6
|
-
return process.env.XDG_DATA_HOME ?? join(homedir(), ".local", "share")
|
|
5
|
+
function dbPath(): string {
|
|
6
|
+
return process.env.SESH_DB ?? join(process.env.XDG_DATA_HOME ?? join(homedir(), ".local", "share"), "opencode", "opencode.db")
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
const input = {
|
|
@@ -44,12 +44,12 @@ export default Plugin.define({
|
|
|
44
44
|
throw new Error("sesh_list requires the Bun runtime (bun:sqlite)")
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
const db = new sqlite.Database(
|
|
47
|
+
const db = new sqlite.Database(dbPath(), { readonly: true })
|
|
48
48
|
try {
|
|
49
49
|
const scoped = typeof args.directory === "string" && args.directory.length > 0
|
|
50
50
|
const query = `SELECT id, title, directory, time_updated AS updated, time_created AS created
|
|
51
51
|
FROM session_v2
|
|
52
|
-
WHERE parent_id IS NULL AND time_archived
|
|
52
|
+
WHERE parent_id IS NULL AND COALESCE(time_archived, 0) = 0${scoped ? " AND directory = ?" : ""}
|
|
53
53
|
ORDER BY time_updated DESC, id DESC
|
|
54
54
|
LIMIT ${limit}`
|
|
55
55
|
const rows = scoped ? db.query(query).all(args.directory) : db.query(query).all()
|
|
@@ -352,13 +352,21 @@ function createContextMenu(host: ContextMenuHost) {
|
|
|
352
352
|
}
|
|
353
353
|
|
|
354
354
|
const select = (index: number) => {
|
|
355
|
-
if (state)
|
|
355
|
+
if (!state) return
|
|
356
|
+
const next = Math.max(0, Math.min(state.items.length - 1, index))
|
|
357
|
+
// A still pointer re-fires over events; republishing an identical state
|
|
358
|
+
// would rebuild the keyed menu on every frame, churning renderables until
|
|
359
|
+
// clicks fall through to whatever is beneath.
|
|
360
|
+
if (next === state.index) return
|
|
361
|
+
publish({ ...state, index: next })
|
|
356
362
|
}
|
|
357
363
|
|
|
358
364
|
const move = (delta: number) => {
|
|
359
365
|
if (!state) return
|
|
360
366
|
const count = state.items.length
|
|
361
|
-
|
|
367
|
+
const next = (state.index + delta + count) % count
|
|
368
|
+
if (next === state.index) return
|
|
369
|
+
publish({ ...state, index: next })
|
|
362
370
|
}
|
|
363
371
|
|
|
364
372
|
const activate = (index?: number) => {
|
|
@@ -501,7 +509,7 @@ async function openTranscriptDb(): Promise<any | undefined> {
|
|
|
501
509
|
// @ts-ignore - bun:sqlite ships with the Bun runtime (verified); no type package installed
|
|
502
510
|
const sqlite: any = await import("bun:sqlite")
|
|
503
511
|
if (typeof sqlite?.Database !== "function") return undefined
|
|
504
|
-
return new sqlite.Database(`${dataHome}/opencode/opencode.db`, { readonly: true })
|
|
512
|
+
return new sqlite.Database(process.env.SESH_DB ?? `${dataHome}/opencode/opencode.db`, { readonly: true })
|
|
505
513
|
} catch {
|
|
506
514
|
return undefined
|
|
507
515
|
}
|
|
@@ -835,7 +843,15 @@ function createTranscriptPreview(api: TuiPluginApi) {
|
|
|
835
843
|
})
|
|
836
844
|
api.ui.dialog.replace(
|
|
837
845
|
() => (
|
|
838
|
-
<box
|
|
846
|
+
<box
|
|
847
|
+
flexDirection="column"
|
|
848
|
+
paddingLeft={4}
|
|
849
|
+
paddingRight={4}
|
|
850
|
+
paddingBottom={1}
|
|
851
|
+
gap={1}
|
|
852
|
+
backgroundColor={api.theme.current.backgroundPanel}
|
|
853
|
+
onMouseDown={(event: { stopPropagation: () => void }) => event.stopPropagation()}
|
|
854
|
+
>
|
|
839
855
|
<box flexDirection="row" justifyContent="space-between">
|
|
840
856
|
<text attributes={TextAttributes.BOLD}>{entry.title}</text>
|
|
841
857
|
<text style={{ fg: api.theme.current.textMuted }} onMouseUp={close}>esc</text>
|
|
@@ -884,7 +900,14 @@ function createTranscriptPreview(api: TuiPluginApi) {
|
|
|
884
900
|
|
|
885
901
|
onCleanup(() => disposePreviewKeys?.())
|
|
886
902
|
|
|
887
|
-
|
|
903
|
+
// Outside presses reach the root handler; close on the first one instead of
|
|
904
|
+
// relying on the host backdrop. Gated on our own preview so other dialogs
|
|
905
|
+
// are never touched.
|
|
906
|
+
const closeIfOpen = () => {
|
|
907
|
+
if (previewID()) close()
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
return { open, closeIfOpen }
|
|
888
911
|
}
|
|
889
912
|
|
|
890
913
|
type TreeRow =
|
|
@@ -1313,6 +1336,7 @@ function SidebarSessions(props: { api: TuiPluginApi } & PinProps) {
|
|
|
1313
1336
|
|
|
1314
1337
|
const onRootMouseDown = () => {
|
|
1315
1338
|
menu.close()
|
|
1339
|
+
preview.closeIfOpen()
|
|
1316
1340
|
if (insideSearchBox) {
|
|
1317
1341
|
insideSearchBox = false
|
|
1318
1342
|
return
|
|
@@ -1575,8 +1599,9 @@ function SidebarSessions(props: { api: TuiPluginApi } & PinProps) {
|
|
|
1575
1599
|
<For each={tree()}>
|
|
1576
1600
|
{(row) => {
|
|
1577
1601
|
if (row.kind === "group") return (
|
|
1578
|
-
<box flexDirection="row" paddingTop={1} onMouseDown={(event: { stopPropagation: () => void }) => {
|
|
1602
|
+
<box flexDirection="row" paddingTop={1} onMouseDown={(event: { button?: number; stopPropagation: () => void }) => {
|
|
1579
1603
|
event.stopPropagation()
|
|
1604
|
+
if (event.button === RIGHT_BUTTON) return
|
|
1580
1605
|
toggleGroup(row.dir)
|
|
1581
1606
|
}}>
|
|
1582
1607
|
<text flexGrow={1} flexShrink={1} wrapMode="none">
|
|
@@ -1701,6 +1726,9 @@ function SidebarSessions(props: { api: TuiPluginApi } & PinProps) {
|
|
|
1701
1726
|
height={1}
|
|
1702
1727
|
backgroundColor={view.state.index === index() ? theme().backgroundElement : RGBA.fromInts(0, 0, 0, 0)}
|
|
1703
1728
|
onMouseOver={() => menu.select(index())}
|
|
1729
|
+
// Release activates so the press that opens a dialog cannot
|
|
1730
|
+
// straddle it: the release is consumed here before any dialog
|
|
1731
|
+
// opened by the action exists.
|
|
1704
1732
|
onMouseDown={(event: { stopPropagation: () => void }) => event.stopPropagation()}
|
|
1705
1733
|
onMouseUp={(event: { button?: number; stopPropagation: () => void }) => {
|
|
1706
1734
|
event.stopPropagation()
|
|
@@ -1825,6 +1853,7 @@ function HomeSessions(props: { api: TuiPluginApi } & PinProps) {
|
|
|
1825
1853
|
})
|
|
1826
1854
|
|
|
1827
1855
|
const onRootMouseDown = () => {
|
|
1856
|
+
preview.closeIfOpen()
|
|
1828
1857
|
if (insideSearchBox) {
|
|
1829
1858
|
insideSearchBox = false
|
|
1830
1859
|
return
|
package/ROADMAP.md
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
# Roadmap
|
|
2
|
-
|
|
3
|
-
Productivity bets for sesh, roughly ordered by payoff. Status is tracked here
|
|
4
|
-
as items land.
|
|
5
|
-
|
|
6
|
-
## 1. Needs-input triage (done)
|
|
7
|
-
|
|
8
|
-
**Problem:** with 100+ sessions, the bottleneck is not finding a session but
|
|
9
|
-
knowing which ones are *waiting on you* (unanswered agent questions, stale
|
|
10
|
-
runs awaiting approval) versus done versus dead. Today that means polling rows
|
|
11
|
-
by hand.
|
|
12
|
-
|
|
13
|
-
**Approach:** server-independent detection straight from the opencode SQLite
|
|
14
|
-
store, so it works for every session, not just ones owned by the current TUI
|
|
15
|
-
server (whose in-memory permission/question state is invisible cross-project):
|
|
16
|
-
|
|
17
|
-
- a `question` tool part that is not `completed` with no later user message in
|
|
18
|
-
the same session → awaiting an answer;
|
|
19
|
-
- a `tool` part stuck in `running` older than ~10 minutes → awaiting approval
|
|
20
|
-
or orphaned by a dead server.
|
|
21
|
-
|
|
22
|
-
**Surfaces:** a "Needs input" section pinned above the directory groups in the
|
|
23
|
-
TUI sidebar, plus a terminal listing (`sesh --needs-input`).
|
|
24
|
-
|
|
25
|
-
## 2. Stale-session pruning (done)
|
|
26
|
-
|
|
27
|
-
**Problem:** the list grows without bound (127+ rows), which slows scanning
|
|
28
|
-
and bloats the transcript search index with archaeology.
|
|
29
|
-
|
|
30
|
-
**Approach:** `sesh prune --older-than 30d` archives (never deletes) sessions
|
|
31
|
-
older than the threshold: never pinned ones, never ones flagged as needing
|
|
32
|
-
input. Archive is reversible; `--delete` stays the explicit hard-delete path
|
|
33
|
-
through the existing two-step confirm. Interactive runs confirm with a
|
|
34
|
-
candidate summary; non-TTY runs require `--yes`; `--dry-run` lists only.
|
|
35
|
-
|
|
36
|
-
## 3. sesh ↔ cmux bridge (done)
|
|
37
|
-
|
|
38
|
-
**Approach:** [`contrib/cmux/`](contrib/cmux/README.md) ships the cmux
|
|
39
|
-
actions, shortcuts and a `--print --json` recipe that resumes a session in its
|
|
40
|
-
own workspace; the TUI's right-click action opens one directly.
|
|
41
|
-
|
|
42
|
-
## 4. Per-project cost digest (done)
|
|
43
|
-
|
|
44
|
-
`sesh costs [--days N] [--json]` sums assistant-message cost per directory with
|
|
45
|
-
a recent window beside the lifetime total, so a day's work reads as a
|
|
46
|
-
per-project record. Message-level summing (not `session.cost`) keeps a
|
|
47
|
-
half-finished day from dragging in a session's earlier history.
|
|
48
|
-
|
|
49
|
-
## 5. Resume from anywhere (done)
|
|
50
|
-
|
|
51
|
-
Covered by the same contrib recipes: `sesh --print --json` resolves a session
|
|
52
|
-
non-interactively, so a cmux action, skhd binding or launcher can resume a
|
|
53
|
-
specific session — or open the picker — from any context.
|
|
54
|
-
|
|
55
|
-
## Smaller items (done)
|
|
56
|
-
|
|
57
|
-
- Search weighting: title matches outrank transcript-only matches and the
|
|
58
|
-
current project rises while a query is active; pins still win outright.
|
|
59
|
-
- `sesh retitle [--dry-run] [--yes]` replaces placeholder titles
|
|
60
|
-
(`New session - …`) with the start of the first user message.
|
package/bin/sesh-retitle.sh
DELETED
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# Give auto-generated session titles a readable first line.
|
|
3
|
-
#
|
|
4
|
-
# opencode titles most sessions itself; a few keep their placeholder
|
|
5
|
-
# ("New session - <ISO>") because they ended before a title was derived. This
|
|
6
|
-
# replaces just those placeholders with the start of the session's first user
|
|
7
|
-
# message, so the row scans like the rest of the list.
|
|
8
|
-
#
|
|
9
|
-
# It only ever touches titles matching the placeholder pattern, and the value
|
|
10
|
-
# it overwrites carries no information (a timestamp), so nothing recoverable is
|
|
11
|
-
# lost. The write is a single direct UPDATE through the same sqlite3-or-
|
|
12
|
-
# `opencode db` path as `sesh prune` — opencode exposes no title endpoint in
|
|
13
|
-
# the CLI, so a direct write is the only route.
|
|
14
|
-
#
|
|
15
|
-
# Usage: sesh-retitle.sh [--dry-run] [--yes] [--max-words N]
|
|
16
|
-
set -euo pipefail
|
|
17
|
-
|
|
18
|
-
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
19
|
-
OPENCODE_BIN=${SESH_OPENCODE:-opencode}
|
|
20
|
-
dry_run=0
|
|
21
|
-
assume_yes=0
|
|
22
|
-
max_words=8
|
|
23
|
-
while [ "$#" -gt 0 ]; do
|
|
24
|
-
case "$1" in
|
|
25
|
-
--dry-run) dry_run=1 ;;
|
|
26
|
-
--yes|-y) assume_yes=1 ;;
|
|
27
|
-
--max-words) max_words=${2:-}; shift ;;
|
|
28
|
-
--max-words=*) max_words=${1#--max-words=} ;;
|
|
29
|
-
--help) echo "usage: ${0##*/} [--dry-run] [--yes] [--max-words N]"; exit 0 ;;
|
|
30
|
-
*) echo "unknown option: $1" >&2; exit 2 ;;
|
|
31
|
-
esac
|
|
32
|
-
shift
|
|
33
|
-
done
|
|
34
|
-
case "$max_words" in ''|*[!0-9]*) echo "--max-words must be a positive integer" >&2; exit 2;; esac
|
|
35
|
-
[ "$max_words" -gt 0 ] || { echo "--max-words must be positive" >&2; exit 2; }
|
|
36
|
-
|
|
37
|
-
resolve_db() {
|
|
38
|
-
if [ -n "${SESH_DB:-}" ]; then printf '%s\n' "$SESH_DB"; return 0; fi
|
|
39
|
-
local from_cli=''
|
|
40
|
-
if command -v "$OPENCODE_BIN" >/dev/null 2>&1; then
|
|
41
|
-
from_cli=$("$OPENCODE_BIN" db path 2>/dev/null || true)
|
|
42
|
-
fi
|
|
43
|
-
if [ -n "$from_cli" ] && [ -f "$from_cli" ]; then printf '%s\n' "$from_cli"; return 0; fi
|
|
44
|
-
if [ -f "$HOME/.local/share/opencode/opencode.db" ]; then printf '%s\n' "$HOME/.local/share/opencode/opencode.db"; return 0; fi
|
|
45
|
-
return 1
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
SQLITE_BIN=''
|
|
49
|
-
if [ -n "${SESH_SQLITE:-}" ]; then
|
|
50
|
-
SQLITE_BIN=$SESH_SQLITE
|
|
51
|
-
elif command -v sqlite3 >/dev/null 2>&1; then
|
|
52
|
-
SQLITE_BIN=sqlite3
|
|
53
|
-
fi
|
|
54
|
-
|
|
55
|
-
DB_PATH=$(resolve_db) || { echo 'sesh: session database unavailable' >&2; exit 1; }
|
|
56
|
-
[ -f "$DB_PATH" ] || { echo 'sesh: session database unavailable' >&2; exit 1; }
|
|
57
|
-
|
|
58
|
-
if [ -n "$SQLITE_BIN" ]; then
|
|
59
|
-
query() { "$SQLITE_BIN" -json -readonly "$DB_PATH" "$1"; }
|
|
60
|
-
write_db() { "$SQLITE_BIN" "$DB_PATH" "$1"; }
|
|
61
|
-
else
|
|
62
|
-
command -v "$OPENCODE_BIN" >/dev/null 2>&1 || { echo 'sesh: neither sqlite3 nor opencode is available' >&2; exit 1; }
|
|
63
|
-
query() { "$OPENCODE_BIN" db "$1" --format json; }
|
|
64
|
-
write_db() { "$OPENCODE_BIN" db "$1" --format json > /dev/null; }
|
|
65
|
-
fi
|
|
66
|
-
|
|
67
|
-
JQ_BIN=${SESH_JQ:-$(command -v jq || true)}
|
|
68
|
-
[ -n "$JQ_BIN" ] || { echo 'sesh: jq is required' >&2; exit 1; }
|
|
69
|
-
|
|
70
|
-
# The first user text part per placeholder-titled session, cleaned into a
|
|
71
|
-
# candidate: image chips and markdown noise dropped, whitespace collapsed,
|
|
72
|
-
# first `max_words` words kept.
|
|
73
|
-
CANDIDATES=$(query "
|
|
74
|
-
SELECT s.id AS id, s.directory AS directory, s.title AS old_title,
|
|
75
|
-
(SELECT m.data FROM session_message m
|
|
76
|
-
WHERE m.session_id = s.id AND m.type = 'user'
|
|
77
|
-
AND json_type(m.data, '\$.text') = 'text'
|
|
78
|
-
ORDER BY m.seq ASC LIMIT 1) AS first_part
|
|
79
|
-
FROM session_v2 s
|
|
80
|
-
WHERE s.title LIKE 'New session - %'
|
|
81
|
-
OR TRIM(COALESCE(s.title, '')) = ''
|
|
82
|
-
ORDER BY s.time_updated DESC;")
|
|
83
|
-
[ -n "$CANDIDATES" ] || CANDIDATES='[]'
|
|
84
|
-
|
|
85
|
-
PLAN=$(printf '%s' "$CANDIDATES" | "$JQ_BIN" -c --argjson words "$max_words" '
|
|
86
|
-
def derive:
|
|
87
|
-
(.first_part // "")
|
|
88
|
-
| (try (fromjson | .text) catch "")
|
|
89
|
-
| gsub("\\[Image #[0-9]+\\]"; " ")
|
|
90
|
-
| gsub("[#*_`>]"; " ")
|
|
91
|
-
| gsub("\\s+"; " ")
|
|
92
|
-
| sub("^ +"; "") | sub(" +$"; "")
|
|
93
|
-
| (split(" ") | map(select(. != "")))
|
|
94
|
-
| (if length == 0 then "" else .[0:$words] | join(" ") end)
|
|
95
|
-
| if length > 60 then .[0:57] + "..." else . end;
|
|
96
|
-
[ .[] | .new_title = derive | select(.new_title != "") ]')
|
|
97
|
-
COUNT=$(printf '%s' "$PLAN" | "$JQ_BIN" -r 'length')
|
|
98
|
-
|
|
99
|
-
if [ "$COUNT" = 0 ]; then
|
|
100
|
-
echo "No placeholder-titled sessions with a usable first message."
|
|
101
|
-
exit 0
|
|
102
|
-
fi
|
|
103
|
-
|
|
104
|
-
printf '%s' "$PLAN" | "$JQ_BIN" -r '
|
|
105
|
-
.[] | "\(.id)\t\(.new_title)\t\(.old_title)"' | while IFS=$'\t' read -r id new old; do
|
|
106
|
-
printf '%s\n \033[90m%s\033[0m\n → %s\n' "$id" "$old" "$new"
|
|
107
|
-
done
|
|
108
|
-
|
|
109
|
-
if [ "$dry_run" = 1 ]; then
|
|
110
|
-
printf 'Retitle %s session(s) (--dry-run: nothing changed).\n' "$COUNT" >&2
|
|
111
|
-
exit 0
|
|
112
|
-
fi
|
|
113
|
-
|
|
114
|
-
if [ "$assume_yes" = 0 ]; then
|
|
115
|
-
if [ -t 0 ]; then
|
|
116
|
-
printf 'Retitle %s session(s)? [y/N] ' "$COUNT"
|
|
117
|
-
reply=''
|
|
118
|
-
IFS= read -r -n 1 reply || reply=''
|
|
119
|
-
printf '\n'
|
|
120
|
-
case "$reply" in
|
|
121
|
-
y|Y) ;;
|
|
122
|
-
*) echo 'Cancelled; nothing was changed.'; exit 0 ;;
|
|
123
|
-
esac
|
|
124
|
-
else
|
|
125
|
-
echo "Refusing to retitle $COUNT session(s): pass --yes when stdin is not a terminal." >&2
|
|
126
|
-
exit 2
|
|
127
|
-
fi
|
|
128
|
-
fi
|
|
129
|
-
|
|
130
|
-
# Escape single quotes for the SQL string literal; ids are validated by shape.
|
|
131
|
-
sql_escape() { printf '%s' "$1" | sed "s/'/''/g"; }
|
|
132
|
-
printf '%s' "$PLAN" | "$JQ_BIN" -r '.[] | [.id, .new_title] | @tsv' | while IFS=$'\t' read -r id new; do
|
|
133
|
-
case "$id" in
|
|
134
|
-
ses_[A-Za-z0-9]*) ;;
|
|
135
|
-
*) echo "Refusing: unexpected session id shape." >&2; exit 1 ;;
|
|
136
|
-
esac
|
|
137
|
-
write_db "UPDATE session_v2 SET title = '$(sql_escape "$new")' WHERE id = '$id' AND (title LIKE 'New session - %' OR TRIM(COALESCE(title, '')) = '');"
|
|
138
|
-
done
|
|
139
|
-
echo "Retitled $COUNT session(s)."
|