opencode-sesh 1.0.3 → 1.0.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 -9
- package/CHANGELOG.md +7 -0
- package/README.md +9 -8
- package/bin/sesh-costs.sh +3 -3
- package/bin/sesh-list.sh +6 -5
- package/bin/sesh-preview.sh +2 -1
- package/bin/sesh-prune.sh +2 -2
- package/bin/sesh-retitle.sh +6 -8
- package/bin/sesh-waiting.sh +22 -24
- package/install.sh +3 -5
- package/package.json +4 -3
- package/plugins/sesh-list.ts +1 -1
- package/tui-plugins/sesh-panel/tui.tsx +89 -67
package/AGENTS.md
CHANGED
|
@@ -59,16 +59,18 @@ The tool name in opencode is `sesh_list`. Do not rename it without updating the
|
|
|
59
59
|
## Data flow (fzf picker)
|
|
60
60
|
|
|
61
61
|
`bin/sesh-list.sh` owns the fzf picker's session store access (opencode SQLite
|
|
62
|
-
DB: `
|
|
63
|
-
the same store through `
|
|
62
|
+
DB: `session_v2` / `session_message` tables); the `sesh-list` agent tool queries
|
|
63
|
+
the same store read-only through `bun:sqlite`. V2 discovers the tool from the
|
|
64
|
+
global config directory's `plugins/`. Keep its SQL column names in sync with
|
|
64
65
|
the schema below. Keystrokes never touch the database:
|
|
65
66
|
|
|
66
67
|
- `--refresh` (expensive): bulk-reads session metadata and per-session text
|
|
67
68
|
parts, validates the persistent extraction cache
|
|
68
|
-
(`$SESH_CACHE_DIR/extractions/<ses_*.json>`, schema
|
|
69
|
-
|
|
69
|
+
(`$SESH_CACHE_DIR/extractions/<ses_*.json>`, schema 3) against `time_updated`,
|
|
70
|
+
message count and the latest message timestamp, re-extracts only changed sessions,
|
|
70
71
|
prunes vanished ones, and atomically publishes `STATE_DIR/snapshot.jsonl`.
|
|
71
|
-
Extraction
|
|
72
|
+
Extraction reads user `data.text` and assistant `data.content` text entries,
|
|
73
|
+
ordered by message `seq`, through files, never argv (no ARG_MAX limit); if
|
|
72
74
|
any session fails, the published status is `stale` and that session stays
|
|
73
75
|
uncached so the next refresh retries, otherwise `status = fresh`. Scope
|
|
74
76
|
(`--cwd`/toggle) and `--limit` apply at assembly, so metadata always covers
|
|
@@ -141,7 +143,7 @@ Resume happens in place (`exec` after `cd` to the session's cwd).
|
|
|
141
143
|
|
|
142
144
|
## TUI panel (`tui-plugins/sesh-panel/tui.tsx`)
|
|
143
145
|
|
|
144
|
-
- **Sidebar:** `api.slots.register` on `sidebar_content` (
|
|
146
|
+
- **Sidebar:** `api.slots.register` on `sidebar_content` (prepend mode — native
|
|
145
147
|
sidebar content stays). Pinned directories sort first, then directory groups
|
|
146
148
|
by recency; sessions within each group sort by last update regardless of pins.
|
|
147
149
|
Row markers alone signal session state: a running agent animates a loading
|
|
@@ -171,8 +173,9 @@ Resume happens in place (`exec` after `cd` to the session's cwd).
|
|
|
171
173
|
`/sesh-needs` uses the shared needs-input set plus the oldest unresolved
|
|
172
174
|
question or running-tool timestamp, lists longest-waiting first, and `n`
|
|
173
175
|
opens the next waiting session. `tests/tui.mjs` locks the data layer.
|
|
174
|
-
- **Durable install:**
|
|
175
|
-
|
|
176
|
+
- **Durable install:** V2 reloads plugins on watched configuration changes;
|
|
177
|
+
unwatched dependencies may need a restart. If a panel update does not appear,
|
|
178
|
+
fully quit/reopen opencode. `install.sh --sync-panel`
|
|
176
179
|
(run by the `postinstall`) refreshes an already-installed panel and is a
|
|
177
180
|
no-op when absent, and `sesh --check` reports when the installed copy differs
|
|
178
181
|
from the bundled one.
|
|
@@ -234,7 +237,7 @@ Resume happens in place (`exec` after `cd` to the session's cwd).
|
|
|
234
237
|
history in a temp dir (included in `bun run test`).
|
|
235
238
|
- `bun run test:picker` — PTY suite driving the real fzf picker (needs fzf
|
|
236
239
|
`>= 0.73` and Python 3).
|
|
237
|
-
- `bun run typecheck` — `tsc` over `tui/` and `
|
|
240
|
+
- `bun run typecheck` — `tsc` over `tui-plugins/` and `plugins/`.
|
|
238
241
|
- `bun run lint:sh` — `bash -n` on every script.
|
|
239
242
|
- `HOME=/tmp/fakehome bash install.sh` — installer smoke test.
|
|
240
243
|
- Keep `tui-plugins/sesh-panel/tui.tsx` and any installed copy byte-identical when testing
|
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,12 @@ 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.0.4] - 2026-10-08
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- Restore V2 session store, dialog and fork compatibility
|
|
13
|
+
|
|
8
14
|
## [1.0.3] - 2026-10-08
|
|
9
15
|
|
|
10
16
|
### Fixed
|
|
@@ -436,6 +442,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
436
442
|
- Fixture-database regression suite, TypeScript typechecking, and ShellCheck in
|
|
437
443
|
CI.
|
|
438
444
|
|
|
445
|
+
[1.0.4]: https://github.com/ozandogrultan/opencode-sesh/compare/v1.0.3...v1.0.4
|
|
439
446
|
[1.0.3]: https://github.com/ozandogrultan/opencode-sesh/compare/v1.0.2...v1.0.3
|
|
440
447
|
[1.0.2]: https://github.com/ozandogrultan/opencode-sesh/compare/v1.0.1...v1.0.2
|
|
441
448
|
[1.0.1]: https://github.com/ozandogrultan/opencode-sesh/compare/v1.0.0...v1.0.1
|
package/README.md
CHANGED
|
@@ -72,7 +72,7 @@ bash install.sh
|
|
|
72
72
|
`sesh install` (or `bash install.sh`) links `sesh` into `$XDG_BIN_HOME`
|
|
73
73
|
(default `~/.local/bin`), copies
|
|
74
74
|
the `sesh-list` tool and the TUI panel into
|
|
75
|
-
`${XDG_CONFIG_HOME:-~/.config}/opencode`, registers the panel in `
|
|
75
|
+
`${XDG_CONFIG_HOME:-~/.config}/opencode`, registers the panel in `opencode.json`, and
|
|
76
76
|
declares the plugin dependencies (opencode installs them on next start). The
|
|
77
77
|
panel registers the `/sesh` slash command. It never edits your shell rc, and any
|
|
78
78
|
file it would overwrite is backed up first.
|
|
@@ -80,8 +80,8 @@ file it would overwrite is backed up first.
|
|
|
80
80
|
The npm package ships the same scripts and installer; the runtime tools below
|
|
81
81
|
are still required.
|
|
82
82
|
|
|
83
|
-
|
|
84
|
-
|
|
83
|
+
V2 reloads plugins when watched configuration files change. If a panel update
|
|
84
|
+
does not appear, fully quit opencode and reopen it. Upgrades
|
|
85
85
|
refresh an already-installed panel automatically (the npm `postinstall` syncs
|
|
86
86
|
it), and `sesh --check` reports when the installed panel is out of date.
|
|
87
87
|
|
|
@@ -247,10 +247,10 @@ workspace, and a global "resume from anywhere" hotkey — all caller-side.
|
|
|
247
247
|
|
|
248
248
|
## How it works
|
|
249
249
|
|
|
250
|
-
|
|
251
|
-
store (SQLite: `
|
|
250
|
+
The `bin/` readers use the V2 opencode session
|
|
251
|
+
store (SQLite: `session_v2` / `session_message`). Each refresh extracts session
|
|
252
252
|
metadata and text-only parts into a persistent per-session cache keyed on
|
|
253
|
-
`time_updated`,
|
|
253
|
+
`time_updated`, message count and the latest message timestamp, then atomically
|
|
254
254
|
publishes a `snapshot.jsonl`. Every keystroke re-renders that snapshot with `jq`
|
|
255
255
|
alone, so typing never starts competing database scans. Reasoning and tool
|
|
256
256
|
payloads are never indexed or previewed, and session ids are validated before
|
|
@@ -261,7 +261,8 @@ Flat scripts back the terminal UI — the picker (`sesh.sh`), the refresh engine
|
|
|
261
261
|
scans), preview renderer (`sesh-preview.sh`), deleter (`sesh-delete.sh`) and
|
|
262
262
|
shortcut help (`sesh-shortcuts.sh`). The TUI panel (`tui-plugins/sesh-panel/tui.tsx`) is a
|
|
263
263
|
SolidJS OpenTUI plugin that talks to the opencode SDK over the same store; the
|
|
264
|
-
`sesh-list` agent tool reads it through `
|
|
264
|
+
`sesh-list` agent tool reads it read-only through `bun:sqlite`. V2 discovers
|
|
265
|
+
the installed tool automatically in the global config directory's `plugins/`.
|
|
265
266
|
|
|
266
267
|
## FAQ
|
|
267
268
|
|
|
@@ -292,7 +293,7 @@ checks before opening a PR:
|
|
|
292
293
|
bun install
|
|
293
294
|
bun run test # fixture-database regression suite
|
|
294
295
|
bun run test:picker # PTY picker suite (needs fzf >= 0.73)
|
|
295
|
-
bun run typecheck # tsc over tui/ and
|
|
296
|
+
bun run typecheck # tsc over tui-plugins/ and plugins/
|
|
296
297
|
bun run lint:sh # bash -n on every script
|
|
297
298
|
```
|
|
298
299
|
|
package/bin/sesh-costs.sh
CHANGED
|
@@ -59,9 +59,9 @@ SELECT s.directory AS directory,
|
|
|
59
59
|
ROUND(SUM(COALESCE(json_extract(m.data, '\$.cost'), 0)), 4) AS lifetime_cost,
|
|
60
60
|
COUNT(DISTINCT s.id) AS sessions,
|
|
61
61
|
SUM(CASE WHEN m.time_created >= $CUTOFF THEN COALESCE(json_extract(m.data, '\$.tokens.input'), 0) + COALESCE(json_extract(m.data, '\$.tokens.output'), 0) ELSE 0 END) AS window_tokens
|
|
62
|
-
FROM
|
|
63
|
-
JOIN
|
|
64
|
-
WHERE
|
|
62
|
+
FROM session_message m
|
|
63
|
+
JOIN session_v2 s ON s.id = m.session_id
|
|
64
|
+
WHERE m.type = 'assistant'
|
|
65
65
|
AND json_extract(m.data, '\$.cost') IS NOT NULL
|
|
66
66
|
GROUP BY s.directory
|
|
67
67
|
ORDER BY lifetime_cost DESC;"
|
package/bin/sesh-list.sh
CHANGED
|
@@ -276,7 +276,7 @@ refresh() {
|
|
|
276
276
|
historical="$scratch/historical.jsonl"; needs_extract="$scratch/needs-extract.json"
|
|
277
277
|
out="$scratch/snapshot.jsonl"
|
|
278
278
|
|
|
279
|
-
db_json "SELECT id, directory, title, time_created, time_updated, COALESCE(time_archived, 0) AS archived, COALESCE(parent_id, '') AS parent FROM
|
|
279
|
+
db_json "SELECT id, directory, title, time_created, time_updated, COALESCE(time_archived, 0) AS archived, COALESCE(parent_id, '') AS parent FROM session_v2 ORDER BY time_updated DESC;" > "$sessions" 2>/dev/null || {
|
|
280
280
|
if [ -f "$SNAPSHOT" ]; then set_status 'stale: opencode database query failed; showing last good snapshot'; else set_status 'unavailable: opencode database query failed'; fi
|
|
281
281
|
return 0
|
|
282
282
|
}
|
|
@@ -287,7 +287,7 @@ refresh() {
|
|
|
287
287
|
if [ -f "$SNAPSHOT" ]; then set_status 'stale: opencode database query failed; showing last good snapshot'; else set_status 'unavailable: opencode database query failed'; fi
|
|
288
288
|
return 0
|
|
289
289
|
}
|
|
290
|
-
db_json "SELECT session_id, COUNT(*) AS n, MAX(time_updated) AS m FROM
|
|
290
|
+
db_json "SELECT session_id, COUNT(*) AS n, MAX(time_updated) AS m FROM session_message GROUP BY session_id;" > "$parts" 2>/dev/null || printf '[]\n' > "$parts"
|
|
291
291
|
"$JQ_BIN" -e 'type == "array"' "$parts" >/dev/null 2>&1 || printf '[]\n' > "$parts"
|
|
292
292
|
|
|
293
293
|
# Same bulk cache validation as the Claude list: every cache arrives as its
|
|
@@ -302,7 +302,7 @@ refresh() {
|
|
|
302
302
|
| ($parts[0] | map({key:.session_id, value:.}) | from_entries) as $stats
|
|
303
303
|
| [$sessions[0][] | . + {partCount: ($stats[.id].n // 0), partMax: ($stats[.id].m // 0)}] as $meta
|
|
304
304
|
| ($meta | map({key:.id, value:.}) | from_entries) as $byId
|
|
305
|
-
| [$caches[0][]? | select(.cacheSchema ==
|
|
305
|
+
| [$caches[0][]? | select(.cacheSchema == 3 and (.sessionId | type) == "string")
|
|
306
306
|
| select($byId[.sessionId] as $m | $m != null and .cacheUpdated == $m.time_updated and .cacheParts == $m.partCount and .cacheMax == $m.partMax)] as $cached
|
|
307
307
|
| ($cached | map({key: .sessionId, value:true}) | from_entries) as $cachedIds
|
|
308
308
|
| {cached: $cached,
|
|
@@ -337,7 +337,8 @@ refresh() {
|
|
|
337
337
|
for ((i = j; i < j + 200 && i < ${#stale_ids[@]}; i++)); do
|
|
338
338
|
chunk="${chunk:+$chunk,}'${stale_ids[$i]}'"
|
|
339
339
|
done
|
|
340
|
-
if ! db_json "SELECT session_id, data FROM
|
|
340
|
+
if ! db_json "SELECT session_id, json_object('type', 'text', 'text', json_extract(data, '\$.text')) AS data, seq, -1 AS ordinal FROM session_message WHERE session_id IN ($chunk) AND type = 'user' AND json_type(data, '\$.text') = 'text'
|
|
341
|
+
UNION ALL SELECT m.session_id, c.value AS data, m.seq, CAST(c.key AS INTEGER) AS ordinal FROM session_message m, json_each(m.data, '\$.content') c WHERE m.session_id IN ($chunk) AND m.type = 'assistant' AND json_extract(c.value, '\$.type') = 'text' ORDER BY session_id, seq, ordinal;" 2>/dev/null \
|
|
341
342
|
| "$JQ_BIN" -c '.[]?' >> "$parts_raw"; then
|
|
342
343
|
extract_failed=1
|
|
343
344
|
break
|
|
@@ -363,7 +364,7 @@ refresh() {
|
|
|
363
364
|
else $s.title end) as $title
|
|
364
365
|
| {sessionId: $s.id,
|
|
365
366
|
cachePath: ($extractions + "/" + $s.id + ".json"),
|
|
366
|
-
|
|
367
|
+
record: {cacheSchema: 3, cacheSession: $s.id,
|
|
367
368
|
cacheUpdated: ($s.time_updated // 0), cacheParts: ($s.partCount // 0), cacheMax: ($s.partMax // 0),
|
|
368
369
|
sessionId: $s.id, transcriptPath: "", source: "opencode",
|
|
369
370
|
cwd: ($s.directory // ""),
|
package/bin/sesh-preview.sh
CHANGED
|
@@ -26,7 +26,8 @@ fi
|
|
|
26
26
|
|
|
27
27
|
SQLITE_BIN=${SESH_SQLITE:-}
|
|
28
28
|
if [ -z "$SQLITE_BIN" ] && command -v sqlite3 >/dev/null 2>&1; then SQLITE_BIN=sqlite3; fi
|
|
29
|
-
SQL="SELECT
|
|
29
|
+
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
|
+
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;"
|
|
30
31
|
if [ -n "$SQLITE_BIN" ]; then
|
|
31
32
|
rows=$("$SQLITE_BIN" -json -readonly "$DB_PATH" "$SQL" 2>/dev/null) || rows=''
|
|
32
33
|
else
|
package/bin/sesh-prune.sh
CHANGED
|
@@ -84,7 +84,7 @@ fi
|
|
|
84
84
|
JQ_BIN=${SESH_JQ:-$(command -v jq || true)}
|
|
85
85
|
[ -n "$JQ_BIN" ] || { echo 'sesh: jq is required' >&2; exit 1; }
|
|
86
86
|
|
|
87
|
-
CANDIDATES=$(query "SELECT id, directory, title, time_updated AS updated FROM
|
|
87
|
+
CANDIDATES=$(query "SELECT id, directory, title, time_updated AS updated FROM session_v2 WHERE time_updated < $CUTOFF AND COALESCE(time_archived, 0) = 0 AND parent_id IS NULL ORDER BY time_updated ASC;")
|
|
88
88
|
# sqlite3 -json prints nothing (not []) when no rows match.
|
|
89
89
|
[ -n "$CANDIDATES" ] || CANDIDATES='[]'
|
|
90
90
|
PINS=$("$SCRIPT_DIR/sesh-pins.sh" read 2>/dev/null || printf '{"sessions":[],"directories":[]}')
|
|
@@ -152,7 +152,7 @@ if [ "$hard_delete" = 0 ]; then
|
|
|
152
152
|
esac
|
|
153
153
|
done
|
|
154
154
|
IN_LIST=${IN_LIST%,}
|
|
155
|
-
write_db "UPDATE
|
|
155
|
+
write_db "UPDATE session_v2 SET time_archived = $NOW_MS WHERE id IN ($IN_LIST);"
|
|
156
156
|
echo "Archived $COUNT session(s) older than $older_than."
|
|
157
157
|
else
|
|
158
158
|
command -v "$OPENCODE_BIN" >/dev/null 2>&1 || { echo 'sesh: opencode executable is unavailable for deletes.' >&2; exit 1; }
|
package/bin/sesh-retitle.sh
CHANGED
|
@@ -72,13 +72,11 @@ JQ_BIN=${SESH_JQ:-$(command -v jq || true)}
|
|
|
72
72
|
# first `max_words` words kept.
|
|
73
73
|
CANDIDATES=$(query "
|
|
74
74
|
SELECT s.id AS id, s.directory AS directory, s.title AS old_title,
|
|
75
|
-
(SELECT
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
ORDER BY p.time_created ASC LIMIT 1) AS first_part
|
|
81
|
-
FROM session s
|
|
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
|
|
82
80
|
WHERE s.title LIKE 'New session - %'
|
|
83
81
|
OR TRIM(COALESCE(s.title, '')) = ''
|
|
84
82
|
ORDER BY s.time_updated DESC;")
|
|
@@ -136,6 +134,6 @@ printf '%s' "$PLAN" | "$JQ_BIN" -r '.[] | [.id, .new_title] | @tsv' | while IFS=
|
|
|
136
134
|
ses_[A-Za-z0-9]*) ;;
|
|
137
135
|
*) echo "Refusing: unexpected session id shape." >&2; exit 1 ;;
|
|
138
136
|
esac
|
|
139
|
-
write_db "UPDATE
|
|
137
|
+
write_db "UPDATE session_v2 SET title = '$(sql_escape "$new")' WHERE id = '$id' AND (title LIKE 'New session - %' OR TRIM(COALESCE(title, '')) = '');"
|
|
140
138
|
done
|
|
141
139
|
echo "Retitled $COUNT session(s)."
|
package/bin/sesh-waiting.sh
CHANGED
|
@@ -54,39 +54,37 @@ STUCK_BEFORE=$((NOW_MS - 600000))
|
|
|
54
54
|
|
|
55
55
|
# NEEDS_INPUT_SQL (shared heuristic — keep in sync with tui-plugins/sesh-panel/tui.tsx).
|
|
56
56
|
read -r -d '' QUERY <<SQL || true
|
|
57
|
+
WITH live AS (
|
|
58
|
+
SELECT m.session_id AS session_id, m.seq AS seq,
|
|
59
|
+
json_extract(c.value, '\$.name') AS name,
|
|
60
|
+
json_extract(c.value, '\$.state.status') AS status,
|
|
61
|
+
COALESCE(json_extract(c.value, '\$.time.created'), m.time_created) AS created
|
|
62
|
+
FROM session_message m, json_each(m.data, '\$.content') c
|
|
63
|
+
WHERE m.type = 'assistant'
|
|
64
|
+
AND (m.data LIKE '%"status":"running"%' OR m.data LIKE '%"status":"pending"%')
|
|
65
|
+
AND json_extract(c.value, '\$.type') = 'tool'
|
|
66
|
+
AND json_extract(c.value, '\$.state.status') IN ('pending', 'running')
|
|
67
|
+
)
|
|
57
68
|
SELECT s.id AS id, s.directory AS directory, s.title AS title, s.time_updated AS updated,
|
|
58
69
|
CASE WHEN EXISTS (
|
|
59
|
-
SELECT 1 FROM
|
|
60
|
-
WHERE
|
|
61
|
-
AND json_extract(p.data, '\$.type') = 'tool'
|
|
62
|
-
AND json_extract(p.data, '\$.tool') = 'question'
|
|
63
|
-
AND COALESCE(json_extract(p.data, '\$.state.status'), 'pending') != 'completed'
|
|
70
|
+
SELECT 1 FROM live
|
|
71
|
+
WHERE live.session_id = s.id AND live.name = 'question'
|
|
64
72
|
AND NOT EXISTS (
|
|
65
|
-
SELECT 1 FROM
|
|
66
|
-
WHERE
|
|
67
|
-
AND json_extract(m.data, '\$.role') = 'user'
|
|
68
|
-
AND m.time_created > p.time_created)
|
|
73
|
+
SELECT 1 FROM session_message u
|
|
74
|
+
WHERE u.session_id = live.session_id AND u.type = 'user' AND u.seq > live.seq)
|
|
69
75
|
) THEN 'question' ELSE 'stuck' END AS reason
|
|
70
|
-
FROM
|
|
76
|
+
FROM session_v2 s
|
|
71
77
|
WHERE COALESCE(s.time_archived, 0) = 0
|
|
72
78
|
AND s.parent_id IS NULL
|
|
73
79
|
AND (EXISTS (
|
|
74
|
-
SELECT 1 FROM
|
|
75
|
-
WHERE
|
|
76
|
-
AND json_extract(p.data, '\$.type') = 'tool'
|
|
77
|
-
AND json_extract(p.data, '\$.tool') = 'question'
|
|
78
|
-
AND COALESCE(json_extract(p.data, '\$.state.status'), 'pending') != 'completed'
|
|
80
|
+
SELECT 1 FROM live
|
|
81
|
+
WHERE live.session_id = s.id AND live.name = 'question'
|
|
79
82
|
AND NOT EXISTS (
|
|
80
|
-
SELECT 1 FROM
|
|
81
|
-
WHERE
|
|
82
|
-
AND json_extract(m.data, '\$.role') = 'user'
|
|
83
|
-
AND m.time_created > p.time_created)
|
|
83
|
+
SELECT 1 FROM session_message u
|
|
84
|
+
WHERE u.session_id = live.session_id AND u.type = 'user' AND u.seq > live.seq)
|
|
84
85
|
) OR EXISTS (
|
|
85
|
-
SELECT 1 FROM
|
|
86
|
-
WHERE
|
|
87
|
-
AND json_extract(p2.data, '\$.type') = 'tool'
|
|
88
|
-
AND json_extract(p2.data, '\$.state.status') = 'running'
|
|
89
|
-
AND p2.time_created < $STUCK_BEFORE))
|
|
86
|
+
SELECT 1 FROM live
|
|
87
|
+
WHERE live.session_id = s.id AND live.status = 'running' AND live.created < $STUCK_BEFORE))
|
|
90
88
|
ORDER BY s.time_updated DESC;
|
|
91
89
|
SQL
|
|
92
90
|
|
package/install.sh
CHANGED
|
@@ -210,11 +210,8 @@ install_tui_panel() {
|
|
|
210
210
|
fi
|
|
211
211
|
}
|
|
212
212
|
|
|
213
|
-
# opencode imports plugins once at startup and has no hot reload, so a running
|
|
214
|
-
# instance keeps the old panel until it is fully quit (reloading a window reuses
|
|
215
|
-
# the same process). Say so explicitly and name the processes still holding it.
|
|
216
213
|
note_restart() {
|
|
217
|
-
note "
|
|
214
|
+
note "V2 reloads watched plugins automatically; if the panel stays unchanged, restart opencode: quit it completely"
|
|
218
215
|
note " (all windows and background processes), then reopen."
|
|
219
216
|
if command -v pgrep >/dev/null 2>&1; then
|
|
220
217
|
local pids
|
|
@@ -238,7 +235,7 @@ if [ "$UNINSTALL" = 1 ]; then
|
|
|
238
235
|
if [ -d "$PANEL_DIR" ]; then
|
|
239
236
|
rm -rf "$PANEL_DIR" && note "removed $PANEL_DIR"
|
|
240
237
|
fi
|
|
241
|
-
remove_if_marker "$CONFIG_DIR/plugins/sesh-panel.tsx"
|
|
238
|
+
remove_if_marker "$CONFIG_DIR/plugins/sesh-panel.tsx" 'id: "sesh-panel"' && note "removed $CONFIG_DIR/plugins/sesh-panel.tsx"
|
|
242
239
|
remove_opencode_entry && note "unregistered the panel from $OPENCODE_JSON"
|
|
243
240
|
remove_tui_entry && note "unregistered the panel from $TUI_JSON"
|
|
244
241
|
note "uninstalled (extraction cache and plugin dependencies left intact)"
|
|
@@ -309,6 +306,7 @@ remove_if_marker "$COMMAND_DST" "rich interactive picker" && note "removed obsol
|
|
|
309
306
|
[ "$INSTALL_TOOL" = 1 ] && install_file "$SOURCE_DIR/plugins/sesh-list.ts" "$TOOL_DST"
|
|
310
307
|
# Clean up obsolete V1 tool if present
|
|
311
308
|
remove_if_marker "$CONFIG_DIR/tools/sesh-list.ts" "list opencode sessions for the agent" && note "removed obsolete V1 tool"
|
|
309
|
+
remove_if_marker "$CONFIG_DIR/plugins/sesh-panel.tsx" 'id: "sesh-panel"' && note "removed obsolete V1 panel"
|
|
312
310
|
|
|
313
311
|
if [ "$INSTALL_TUI" = 1 ]; then
|
|
314
312
|
install_tui_panel
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-sesh",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "Browse, search, preview and resume opencode sessions from the terminal and the TUI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -66,8 +66,9 @@
|
|
|
66
66
|
"devDependencies": {
|
|
67
67
|
"@commitlint/cli": "^21.2.2",
|
|
68
68
|
"@commitlint/config-conventional": "^21.2.2",
|
|
69
|
-
"@opencode/client": "2.0.
|
|
70
|
-
"@opencode/plugin": "2.0.
|
|
69
|
+
"@opencode/client": "2.0.25",
|
|
70
|
+
"@opencode/plugin": "2.0.25",
|
|
71
|
+
"@opencode/theme": "2.0.25",
|
|
71
72
|
"@opentui/core": "^0.5.11",
|
|
72
73
|
"@opentui/keymap": "^0.5.11",
|
|
73
74
|
"@opentui/solid": "^0.5.11",
|
package/plugins/sesh-list.ts
CHANGED
|
@@ -26,7 +26,7 @@ const input = {
|
|
|
26
26
|
export default Plugin.define({
|
|
27
27
|
id: "sesh-list",
|
|
28
28
|
async setup(ctx) {
|
|
29
|
-
ctx.tool.transform((editor) => {
|
|
29
|
+
await ctx.tool.transform((editor) => {
|
|
30
30
|
editor.add({
|
|
31
31
|
name: "sesh_list",
|
|
32
32
|
description: "List opencode sessions across all project directories, newest first",
|
|
@@ -3,7 +3,7 @@ import { Plugin } from "@opencode/plugin/tui"
|
|
|
3
3
|
import type { Project, SessionInfo as Session } from "@opencode/client"
|
|
4
4
|
import type { Accessor } from "solid-js"
|
|
5
5
|
|
|
6
|
-
type TuiPluginApi =
|
|
6
|
+
type TuiPluginApi = ReturnType<typeof createApi>
|
|
7
7
|
type TuiPlugin = (api: TuiPluginApi) => Promise<void>
|
|
8
8
|
import { createEffect, createMemo, createRoot, createSignal, For, getOwner, on, onCleanup, onMount, runWithOwner, Show } from "solid-js"
|
|
9
9
|
import { mkdir, readFile, rename, rmdir, unlink, writeFile } from "node:fs/promises"
|
|
@@ -237,6 +237,8 @@ function searchBindings(append: (input: string) => void, exit: () => void) {
|
|
|
237
237
|
|
|
238
238
|
const rootMouseHandlers = new Set<() => void>()
|
|
239
239
|
let rootMouseInstalled = false
|
|
240
|
+
let removeRootMouse: (() => void) | undefined
|
|
241
|
+
const ROOT_MOUSE = Symbol.for("opencode-sesh.root-mouse")
|
|
240
242
|
|
|
241
243
|
function ensureRootMouse(renderer: TuiPluginApi["renderer"]) {
|
|
242
244
|
if (rootMouseInstalled || !renderer?.root) return
|
|
@@ -244,12 +246,26 @@ function ensureRootMouse(renderer: TuiPluginApi["renderer"]) {
|
|
|
244
246
|
const root = renderer.root as unknown as {
|
|
245
247
|
onMouseDown?: (event: unknown) => void
|
|
246
248
|
_mouseListeners?: { down?: (event: unknown) => void }
|
|
249
|
+
[ROOT_MOUSE]?: Set<() => void>
|
|
247
250
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
+
if (!root[ROOT_MOUSE]) {
|
|
252
|
+
const handlers = new Set<() => void>()
|
|
253
|
+
root[ROOT_MOUSE] = handlers
|
|
254
|
+
const previous = root._mouseListeners?.down
|
|
255
|
+
root.onMouseDown = (event) => {
|
|
256
|
+
previous?.(event)
|
|
257
|
+
for (const handler of [...handlers]) handler()
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
const dispatch = () => {
|
|
251
261
|
for (const handler of [...rootMouseHandlers]) handler()
|
|
252
262
|
}
|
|
263
|
+
root[ROOT_MOUSE].add(dispatch)
|
|
264
|
+
removeRootMouse = () => {
|
|
265
|
+
root[ROOT_MOUSE]?.delete(dispatch)
|
|
266
|
+
rootMouseHandlers.clear()
|
|
267
|
+
rootMouseInstalled = false
|
|
268
|
+
}
|
|
253
269
|
}
|
|
254
270
|
|
|
255
271
|
type Entry = { id: string; title: string; dir: string; group: string; updated: number }
|
|
@@ -422,17 +438,14 @@ const SESSION_MAX = 5000
|
|
|
422
438
|
|
|
423
439
|
async function fetchEntries(api: TuiPluginApi): Promise<EntryResult> {
|
|
424
440
|
const home = process.env.HOME ?? ""
|
|
425
|
-
const projectResult = await api.client.project.list(
|
|
441
|
+
const projectResult = await api.client.project.list()
|
|
426
442
|
const projects = ((projectResult?.data ?? projectResult) as Project[]) ?? []
|
|
427
443
|
const projectName = new Map<string, string>(
|
|
428
444
|
projects.map((p) => [p.id, p.name?.trim() || shortDir(p.canonical, home)]),
|
|
429
445
|
)
|
|
430
446
|
const sessions: Session[] = []
|
|
431
|
-
let cursor:
|
|
447
|
+
let cursor: string | undefined
|
|
432
448
|
let truncated = false
|
|
433
|
-
// Page through the global list instead of one fixed window, so sessions in
|
|
434
|
-
// older directories stay searchable. `cursor` is the previous page's oldest
|
|
435
|
-
// `time.updated`; a repeated or missing cursor stops the walk.
|
|
436
449
|
for (;;) {
|
|
437
450
|
const result = await api.client.experimental.session.list({
|
|
438
451
|
limit: SESSION_PAGE_LIMIT,
|
|
@@ -443,13 +456,13 @@ async function fetchEntries(api: TuiPluginApi): Promise<EntryResult> {
|
|
|
443
456
|
const page = ((result?.data ?? result) as Session[]) ?? []
|
|
444
457
|
if (!Array.isArray(page) || page.length === 0) break
|
|
445
458
|
sessions.push(...page)
|
|
446
|
-
if (
|
|
459
|
+
if (!result.cursor?.next) break
|
|
447
460
|
if (sessions.length >= SESSION_MAX) {
|
|
448
461
|
truncated = true
|
|
449
462
|
break
|
|
450
463
|
}
|
|
451
|
-
const next =
|
|
452
|
-
if (
|
|
464
|
+
const next = result.cursor.next
|
|
465
|
+
if (next === cursor) {
|
|
453
466
|
truncated = true
|
|
454
467
|
break
|
|
455
468
|
}
|
|
@@ -773,7 +786,7 @@ function Highlighted(props: {
|
|
|
773
786
|
|
|
774
787
|
async function fetchTranscriptText(api: TuiPluginApi, sessionID: string): Promise<string> {
|
|
775
788
|
try {
|
|
776
|
-
const result = await api.client.session.messages({ sessionID })
|
|
789
|
+
const result = await api.client.session.messages({ sessionID, preview: true })
|
|
777
790
|
const messages = result.data ?? []
|
|
778
791
|
const blocks = messages
|
|
779
792
|
.map((message: any) => {
|
|
@@ -815,6 +828,7 @@ function createTranscriptPreview(api: TuiPluginApi) {
|
|
|
815
828
|
close()
|
|
816
829
|
return
|
|
817
830
|
}
|
|
831
|
+
if (previewID()) close()
|
|
818
832
|
setPreviewID(entry.id)
|
|
819
833
|
setPreviewText("Loading…")
|
|
820
834
|
void fetchTranscriptText(api, entry.id).then((text) => {
|
|
@@ -857,6 +871,7 @@ function createTranscriptPreview(api: TuiPluginApi) {
|
|
|
857
871
|
api.ui.dialog.setSize("large")
|
|
858
872
|
disposePreviewKeys?.()
|
|
859
873
|
disposePreviewKeys = api.keymap.registerLayer({
|
|
874
|
+
mode: "modal",
|
|
860
875
|
bindings: [
|
|
861
876
|
{
|
|
862
877
|
key: "escape",
|
|
@@ -2044,10 +2059,10 @@ const tui: TuiPlugin = async (api) => {
|
|
|
2044
2059
|
},
|
|
2045
2060
|
})
|
|
2046
2061
|
|
|
2047
|
-
const openPicker = async () => {
|
|
2062
|
+
const openPicker = async (command = false) => {
|
|
2048
2063
|
refreshPins()
|
|
2049
|
-
if (api.mode.current() !== BASE_MODE) return
|
|
2050
|
-
if (api.renderer.currentFocusedEditor === null) return
|
|
2064
|
+
if (!command && api.mode.current() !== BASE_MODE) return
|
|
2065
|
+
if (!command && api.renderer.currentFocusedEditor === null) return
|
|
2051
2066
|
|
|
2052
2067
|
let entriesResult: EntryResult
|
|
2053
2068
|
try {
|
|
@@ -2357,6 +2372,7 @@ const tui: TuiPlugin = async (api) => {
|
|
|
2357
2372
|
confirmTimer = setTimeout(cancelDelete, 5000)
|
|
2358
2373
|
disposeConfirm?.()
|
|
2359
2374
|
disposeConfirm = api.keymap.registerLayer({
|
|
2375
|
+
mode: "modal",
|
|
2360
2376
|
priority: 20,
|
|
2361
2377
|
bindings: [
|
|
2362
2378
|
{
|
|
@@ -2415,9 +2431,7 @@ const tui: TuiPlugin = async (api) => {
|
|
|
2415
2431
|
}
|
|
2416
2432
|
|
|
2417
2433
|
const disposeNav = api.keymap.registerLayer({
|
|
2418
|
-
|
|
2419
|
-
// the dialog host closes the whole thing.
|
|
2420
|
-
mode: "global",
|
|
2434
|
+
mode: "modal",
|
|
2421
2435
|
priority: 1,
|
|
2422
2436
|
bindings: [
|
|
2423
2437
|
{ key: "up", desc: "Previous session", preventDefault: true, cmd: () => moveCursor(-1) },
|
|
@@ -2490,6 +2504,7 @@ const tui: TuiPlugin = async (api) => {
|
|
|
2490
2504
|
})
|
|
2491
2505
|
|
|
2492
2506
|
const cleanup = () => {
|
|
2507
|
+
if (!alive) return
|
|
2493
2508
|
alive = false
|
|
2494
2509
|
if (confirmTimer) clearTimeout(confirmTimer)
|
|
2495
2510
|
pickerState = { query: query(), scope: scope(), waitingOnly: waitingOnly(), pinnedOnly: pinnedOnly() }
|
|
@@ -2793,9 +2808,9 @@ const tui: TuiPlugin = async (api) => {
|
|
|
2793
2808
|
|
|
2794
2809
|
// Read-only digest dialog: spend per project, last day beside lifetime.
|
|
2795
2810
|
// Same message-level sums as `sesh costs`; the dialog just renders them.
|
|
2796
|
-
const openCosts = async () => {
|
|
2797
|
-
if (api.mode.current() !== BASE_MODE) return
|
|
2798
|
-
if (api.renderer.currentFocusedEditor === null) return
|
|
2811
|
+
const openCosts = async (command = false) => {
|
|
2812
|
+
if (!command && api.mode.current() !== BASE_MODE) return
|
|
2813
|
+
if (!command && api.renderer.currentFocusedEditor === null) return
|
|
2799
2814
|
type CostRow = { directory: string; window: number; lifetime: number; sessions: number }
|
|
2800
2815
|
let rows: CostRow[] = []
|
|
2801
2816
|
let failed = false
|
|
@@ -2853,6 +2868,7 @@ ORDER BY lifetime_cost DESC`).all() ?? []) as {
|
|
|
2853
2868
|
const money = (n: number) => (n === 0 ? "—" : `$${n.toFixed(2)}`)
|
|
2854
2869
|
const home = process.env.HOME ?? ""
|
|
2855
2870
|
const disposeKeys = api.keymap.registerLayer({
|
|
2871
|
+
mode: "modal",
|
|
2856
2872
|
bindings: [
|
|
2857
2873
|
{ key: "escape", desc: "Close", preventDefault: true, cmd: () => api.ui.dialog.clear() },
|
|
2858
2874
|
],
|
|
@@ -2906,9 +2922,9 @@ ORDER BY lifetime_cost DESC`).all() ?? []) as {
|
|
|
2906
2922
|
// Triage dialog: the waiting set as an openable list. Enter opens the
|
|
2907
2923
|
// highlighted session; the sidebar section stays the always-visible view.
|
|
2908
2924
|
let lastNeedsID: string | undefined
|
|
2909
|
-
const openNeeds = async () => {
|
|
2910
|
-
if (api.mode.current() !== BASE_MODE) return
|
|
2911
|
-
if (api.renderer.currentFocusedEditor === null) return
|
|
2925
|
+
const openNeeds = async (command = false) => {
|
|
2926
|
+
if (!command && api.mode.current() !== BASE_MODE) return
|
|
2927
|
+
if (!command && api.renderer.currentFocusedEditor === null) return
|
|
2912
2928
|
let items: { entry: Entry; reason: string; since?: number }[] = []
|
|
2913
2929
|
try {
|
|
2914
2930
|
const { entries } = await fetchEntries(api)
|
|
@@ -2953,6 +2969,7 @@ ORDER BY lifetime_cost DESC`).all() ?? []) as {
|
|
|
2953
2969
|
api.route.navigate("session", { sessionID: target.entry.id })
|
|
2954
2970
|
}
|
|
2955
2971
|
const disposeNav = api.keymap.registerLayer({
|
|
2972
|
+
mode: "modal",
|
|
2956
2973
|
bindings: [
|
|
2957
2974
|
{ key: "up", desc: "Previous session", preventDefault: true, cmd: () => moveCursor(-1) },
|
|
2958
2975
|
{ key: "down", desc: "Next session", preventDefault: true, cmd: () => moveCursor(1) },
|
|
@@ -3054,7 +3071,7 @@ ORDER BY lifetime_cost DESC`).all() ?? []) as {
|
|
|
3054
3071
|
category: "Sessions",
|
|
3055
3072
|
slash: { name: "sesh" },
|
|
3056
3073
|
onSelect: () => {
|
|
3057
|
-
void openPicker()
|
|
3074
|
+
void openPicker(true)
|
|
3058
3075
|
},
|
|
3059
3076
|
},
|
|
3060
3077
|
{
|
|
@@ -3064,7 +3081,7 @@ ORDER BY lifetime_cost DESC`).all() ?? []) as {
|
|
|
3064
3081
|
category: "Sessions",
|
|
3065
3082
|
slash: { name: "sesh-costs" },
|
|
3066
3083
|
onSelect: () => {
|
|
3067
|
-
void openCosts()
|
|
3084
|
+
void openCosts(true)
|
|
3068
3085
|
},
|
|
3069
3086
|
},
|
|
3070
3087
|
{
|
|
@@ -3074,7 +3091,7 @@ ORDER BY lifetime_cost DESC`).all() ?? []) as {
|
|
|
3074
3091
|
category: "Sessions",
|
|
3075
3092
|
slash: { name: "sesh-needs" },
|
|
3076
3093
|
onSelect: () => {
|
|
3077
|
-
void openNeeds()
|
|
3094
|
+
void openNeeds(true)
|
|
3078
3095
|
},
|
|
3079
3096
|
},
|
|
3080
3097
|
])
|
|
@@ -3120,7 +3137,7 @@ function adaptTheme(theme: Plugin.Context["theme"]) {
|
|
|
3120
3137
|
}
|
|
3121
3138
|
}
|
|
3122
3139
|
|
|
3123
|
-
function createApi(ctx: Plugin.Context)
|
|
3140
|
+
function createApi(ctx: Plugin.Context) {
|
|
3124
3141
|
const [dialogOpen, setDialogOpen] = createSignal(false)
|
|
3125
3142
|
let sourceTheme: Plugin.Context["theme"] | undefined
|
|
3126
3143
|
let currentTheme: ReturnType<typeof adaptTheme>
|
|
@@ -3135,9 +3152,9 @@ function createApi(ctx: Plugin.Context): TuiPluginApi {
|
|
|
3135
3152
|
},
|
|
3136
3153
|
},
|
|
3137
3154
|
ui: {
|
|
3138
|
-
toast(opts: { message: string; variant?: "info" | "warning" | "error" | "success" }) {
|
|
3155
|
+
toast(opts: { title?: string; message: string; variant?: "info" | "warning" | "error" | "success" }) {
|
|
3139
3156
|
ctx.ui.toast.show({
|
|
3140
|
-
title: "
|
|
3157
|
+
title: opts.title ?? "Sessions",
|
|
3141
3158
|
message: opts.message,
|
|
3142
3159
|
variant: opts.variant ?? "info",
|
|
3143
3160
|
})
|
|
@@ -3147,12 +3164,18 @@ function createApi(ctx: Plugin.Context): TuiPluginApi {
|
|
|
3147
3164
|
ctx.ui.dialog.clear()
|
|
3148
3165
|
setDialogOpen(false)
|
|
3149
3166
|
},
|
|
3150
|
-
replace(render: () => any) {
|
|
3167
|
+
replace(render: () => any, onClose?: () => void) {
|
|
3168
|
+
let closed = false
|
|
3169
|
+
ctx.ui.dialog.show(render, () => {
|
|
3170
|
+
if (closed) return
|
|
3171
|
+
closed = true
|
|
3172
|
+
setDialogOpen(false)
|
|
3173
|
+
onClose?.()
|
|
3174
|
+
})
|
|
3151
3175
|
setDialogOpen(true)
|
|
3152
|
-
ctx.ui.dialog.show(render, () => setDialogOpen(false))
|
|
3153
3176
|
},
|
|
3154
|
-
setSize(size:
|
|
3155
|
-
ctx.ui.dialog.set({ size
|
|
3177
|
+
setSize(size: Parameters<Plugin.Context["ui"]["dialog"]["set"]>[0]["size"]) {
|
|
3178
|
+
ctx.ui.dialog.set({ size })
|
|
3156
3179
|
},
|
|
3157
3180
|
// V2's Dialog has no reactive open/isOpen accessor; track it locally
|
|
3158
3181
|
// from the replace/clear calls above and the host's onClose callback.
|
|
@@ -3224,8 +3247,7 @@ function createApi(ctx: Plugin.Context): TuiPluginApi {
|
|
|
3224
3247
|
ctx.keymap.layer(() => ({
|
|
3225
3248
|
mode: layer.mode ?? "base",
|
|
3226
3249
|
priority: layer.priority ?? 20,
|
|
3227
|
-
commands: (layer.bindings ?? []).map((b
|
|
3228
|
-
id: `sesh-cmd-${Math.random().toString(36).slice(2)}-${i}`,
|
|
3250
|
+
commands: (layer.bindings ?? []).map((b) => ({
|
|
3229
3251
|
title: b.desc,
|
|
3230
3252
|
bind: b.key,
|
|
3231
3253
|
run: () => {
|
|
@@ -3234,7 +3256,7 @@ function createApi(ctx: Plugin.Context): TuiPluginApi {
|
|
|
3234
3256
|
})),
|
|
3235
3257
|
}))
|
|
3236
3258
|
return dispose
|
|
3237
|
-
}))
|
|
3259
|
+
}))!
|
|
3238
3260
|
},
|
|
3239
3261
|
},
|
|
3240
3262
|
slots: {
|
|
@@ -3291,20 +3313,28 @@ function createApi(ctx: Plugin.Context): TuiPluginApi {
|
|
|
3291
3313
|
await ctx.client.session.remove({ sessionID: req.sessionID })
|
|
3292
3314
|
return { data: undefined }
|
|
3293
3315
|
},
|
|
3294
|
-
async fork(req: {
|
|
3295
|
-
const res
|
|
3296
|
-
return { data: res
|
|
3316
|
+
async fork(req: { sessionID: string; directory?: string }) {
|
|
3317
|
+
const res = await ctx.client.session.fork({ sessionID: req.sessionID })
|
|
3318
|
+
return { data: res }
|
|
3297
3319
|
},
|
|
3298
|
-
async messages(req: {
|
|
3299
|
-
|
|
3300
|
-
const
|
|
3301
|
-
|
|
3320
|
+
async messages(req: { sessionID: string; preview?: boolean }) {
|
|
3321
|
+
let cursor: string | undefined
|
|
3322
|
+
const all = []
|
|
3323
|
+
do {
|
|
3324
|
+
const res = await ctx.client.message.list({ sessionID: req.sessionID, order: req.preview ? "desc" : "asc", limit: 100, cursor })
|
|
3325
|
+
all.push(...res.data)
|
|
3326
|
+
const next = res.cursor.next
|
|
3327
|
+
if (req.preview || !next || next === cursor) break
|
|
3328
|
+
cursor = next
|
|
3329
|
+
} while (true)
|
|
3330
|
+
if (req.preview) all.reverse()
|
|
3331
|
+
const messages = all.map((m) => {
|
|
3302
3332
|
let text = ""
|
|
3303
3333
|
if (m.type === "user") text = m.text ?? ""
|
|
3304
3334
|
else if (m.type === "assistant") {
|
|
3305
3335
|
text = (m.content ?? [])
|
|
3306
|
-
.filter((c
|
|
3307
|
-
.map((c
|
|
3336
|
+
.filter((c) => c.type === "text")
|
|
3337
|
+
.map((c) => c.text)
|
|
3308
3338
|
.join("\n")
|
|
3309
3339
|
}
|
|
3310
3340
|
return {
|
|
@@ -3318,30 +3348,17 @@ function createApi(ctx: Plugin.Context): TuiPluginApi {
|
|
|
3318
3348
|
},
|
|
3319
3349
|
experimental: {
|
|
3320
3350
|
session: {
|
|
3321
|
-
async list(input:
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
// fetch once and page in memory to keep the caller's loop intact.
|
|
3325
|
-
const limit = Number(input?.limit) > 0 ? Number(input.limit) : 200
|
|
3326
|
-
const cursor = typeof input?.cursor === "number" ? input.cursor : undefined
|
|
3327
|
-
const res: any = await ctx.client.session.list({ limit: 5000, order: "desc" })
|
|
3328
|
-
const all: any[] = Array.isArray(res) ? res : (res?.data ?? [])
|
|
3329
|
-
const normalized = all
|
|
3330
|
-
.map((s: any) => ({
|
|
3351
|
+
async list(input: { limit: number; cursor?: string; roots?: boolean; directory?: string }) {
|
|
3352
|
+
const res = await ctx.client.session.list({ parentID: null, order: "desc", limit: input.limit, cursor: input.cursor })
|
|
3353
|
+
return { cursor: res.cursor, data: res.data.map((s) => ({
|
|
3331
3354
|
...s,
|
|
3332
|
-
directory: s.
|
|
3333
|
-
}))
|
|
3334
|
-
.sort((a: any, b: any) => (b.time?.updated ?? 0) - (a.time?.updated ?? 0))
|
|
3335
|
-
const page =
|
|
3336
|
-
cursor === undefined
|
|
3337
|
-
? normalized.slice(0, limit)
|
|
3338
|
-
: normalized.filter((s: any) => (s.time?.updated ?? 0) < cursor).slice(0, limit)
|
|
3339
|
-
return { data: page }
|
|
3355
|
+
directory: s.location?.directory ?? "",
|
|
3356
|
+
})) }
|
|
3340
3357
|
},
|
|
3341
3358
|
},
|
|
3342
3359
|
},
|
|
3343
3360
|
},
|
|
3344
|
-
}
|
|
3361
|
+
}
|
|
3345
3362
|
}
|
|
3346
3363
|
|
|
3347
3364
|
export default Plugin.define({
|
|
@@ -3349,7 +3366,7 @@ export default Plugin.define({
|
|
|
3349
3366
|
async setup(ctx) {
|
|
3350
3367
|
const api = createApi(ctx)
|
|
3351
3368
|
let initialized = false
|
|
3352
|
-
|
|
3369
|
+
const disposeSlot = ctx.ui.slot({
|
|
3353
3370
|
append: "app",
|
|
3354
3371
|
render: () => {
|
|
3355
3372
|
if (!initialized) {
|
|
@@ -3360,5 +3377,10 @@ export default Plugin.define({
|
|
|
3360
3377
|
return <box />
|
|
3361
3378
|
},
|
|
3362
3379
|
})
|
|
3380
|
+
return () => {
|
|
3381
|
+
if (api.ui.dialog.open) api.ui.dialog.clear()
|
|
3382
|
+
disposeSlot()
|
|
3383
|
+
removeRootMouse?.()
|
|
3384
|
+
}
|
|
3363
3385
|
},
|
|
3364
3386
|
})
|