opencode-sesh 2.1.7 → 2.2.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 +8 -3
- package/CHANGELOG.md +14 -0
- package/README.md +6 -1
- package/bin/sesh-waiting.sh +20 -1
- package/dist/tui.js +31 -6
- package/package.json +1 -1
- package/tui-plugins/sesh-panel/tui.tsx +37 -6
package/AGENTS.md
CHANGED
|
@@ -163,8 +163,10 @@ when opencode exits, the picker reopens with the query preserved.
|
|
|
163
163
|
- **Sidebar & Home list:** `api.slots.register` on `sidebar_content` (`sidebar.content`
|
|
164
164
|
append mode by default — native sidebar content stays above Sessions;
|
|
165
165
|
`sidebarPlacement: "prepend"` puts Sessions first) and `home_bottom` (`home.footer`
|
|
166
|
-
prepend mode).
|
|
167
|
-
|
|
166
|
+
prepend mode). `showSidebar: false` / `showHome: false` skip registering each
|
|
167
|
+
slot; with both off (picker-only) nothing polls. In the sidebar, pinned
|
|
168
|
+
directories sort first, then directory groups by recency; sessions within
|
|
169
|
+
each group sort by last update regardless of pins.
|
|
168
170
|
In the home list, pinned sessions sort first.
|
|
169
171
|
The home list budgets its height for the native logo, maximum-height composer,
|
|
170
172
|
and status footer: compact spacing first, then fewer rows, hidden when too
|
|
@@ -182,7 +184,10 @@ when opencode exits, the picker reopens with the query preserved.
|
|
|
182
184
|
scrolling inside; the picker remains the place for transcript search.
|
|
183
185
|
Sessions waiting on the user (unanswered questions, stuck runs) pin a
|
|
184
186
|
**Needs input** group above the directories, with the count in the heading —
|
|
185
|
-
same `NEEDS_INPUT_SQL` heuristic as `bin/sesh-waiting.sh`.
|
|
187
|
+
same `NEEDS_INPUT_SQL` heuristic as `bin/sesh-waiting.sh`. Message payloads are
|
|
188
|
+
scanned only for in-flight root sessions (`time_suspended` set, `time_busy` newer
|
|
189
|
+
than `time_idle`); databases lacking those columns (checked via
|
|
190
|
+
`pragma_table_info`) fall back to the full scan. The query runs in
|
|
186
191
|
a blob Worker (`createDbClient`; failure means "local store unavailable"),
|
|
187
192
|
off the TUI thread but still costing worker CPU every poll, so the shared
|
|
188
193
|
poll runs it only while the sidebar is subscribed
|
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
|
+
## [2.2.0] - 2026-11-02
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **tui:** Add showHome option for picker-only mode (#39)
|
|
13
|
+
|
|
14
|
+
## [2.1.8] - 2026-11-02
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- **tui:** Prefilter needs-input candidates by in-flight session state (#38)
|
|
19
|
+
|
|
8
20
|
## [2.1.7] - 2026-11-02
|
|
9
21
|
|
|
10
22
|
### Changed
|
|
@@ -558,6 +570,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
558
570
|
- Fixture-database regression suite, TypeScript typechecking, and ShellCheck in
|
|
559
571
|
CI.
|
|
560
572
|
|
|
573
|
+
[2.2.0]: https://github.com/ozandogrultan/opencode-sesh/compare/v2.1.8...v2.2.0
|
|
574
|
+
[2.1.8]: https://github.com/ozandogrultan/opencode-sesh/compare/v2.1.7...v2.1.8
|
|
561
575
|
[2.1.7]: https://github.com/ozandogrultan/opencode-sesh/compare/v2.1.6...v2.1.7
|
|
562
576
|
[2.1.6]: https://github.com/ozandogrultan/opencode-sesh/compare/v2.1.5...v2.1.6
|
|
563
577
|
[2.1.5]: https://github.com/ozandogrultan/opencode-sesh/compare/v2.1.4...v2.1.5
|
package/README.md
CHANGED
|
@@ -239,7 +239,7 @@ the `opencode.json` entry unchanged:
|
|
|
239
239
|
"plugins": [
|
|
240
240
|
{
|
|
241
241
|
"package": "opencode-sesh",
|
|
242
|
-
"options": { "showSidebar": true, "sidebarPlacement": "append" }
|
|
242
|
+
"options": { "showSidebar": true, "showHome": true, "sidebarPlacement": "append" }
|
|
243
243
|
}
|
|
244
244
|
]
|
|
245
245
|
}
|
|
@@ -271,6 +271,11 @@ terminals to leave room for multiline prompts. On very short terminals it hides;
|
|
|
271
271
|
Option-O and `/sesh` still open the full picker. Long titles and paths stay on a
|
|
272
272
|
single line, and the list adapts when the terminal is resized.
|
|
273
273
|
|
|
274
|
+
`showHome` (boolean, default: `true`) controls the session list on the home
|
|
275
|
+
screen. Set both `showSidebar` and `showHome` to `false` for picker-only mode:
|
|
276
|
+
no lists are mounted and nothing polls in the background, while `option+o`,
|
|
277
|
+
`/sesh`, `/sesh-costs`, and `/sesh-needs` keep working.
|
|
278
|
+
|
|
274
279
|
### Transcript search and needs-input triage
|
|
275
280
|
|
|
276
281
|
The full picker pages through your global session list
|
package/bin/sesh-waiting.sh
CHANGED
|
@@ -15,6 +15,9 @@
|
|
|
15
15
|
# (a long-running subagent/shell tool with recent activity is not stuck;
|
|
16
16
|
# V2 resumes interrupted runs server-side, so `running` with no activity
|
|
17
17
|
# means orphaned or awaiting approval).
|
|
18
|
+
# Only a turn in flight can be waiting (time_suspended is set while a turn
|
|
19
|
+
# runs), so message payloads are scanned only for those candidate sessions;
|
|
20
|
+
# databases without the in-flight columns scan every assistant message.
|
|
18
21
|
#
|
|
19
22
|
# Usage: sesh-waiting.sh [--json]
|
|
20
23
|
# default prints a human table; --json prints [{id,reason,title,directory,updated}]
|
|
@@ -57,8 +60,23 @@ NOW_MS=$(python3 -c 'import time; print(int(time.time() * 1000))' 2>/dev/null ||
|
|
|
57
60
|
STUCK_BEFORE=$((NOW_MS - 600000))
|
|
58
61
|
|
|
59
62
|
# NEEDS_INPUT_SQL (shared heuristic — keep in sync with tui-plugins/sesh-panel/tui.tsx).
|
|
63
|
+
CANDIDATES=''
|
|
64
|
+
PREFILTER=''
|
|
65
|
+
HAS_COLUMNS=0
|
|
66
|
+
if [ -n "$SQLITE_BIN" ]; then
|
|
67
|
+
HAS_COLUMNS=$("$SQLITE_BIN" -readonly "$DB_PATH" "SELECT count(*) = 3 FROM pragma_table_info('session_v2') WHERE name IN ('time_busy', 'time_idle', 'time_suspended');" 2>/dev/null || echo 0)
|
|
68
|
+
fi
|
|
69
|
+
if [ "$HAS_COLUMNS" = 1 ]; then
|
|
70
|
+
CANDIDATES="cand AS (
|
|
71
|
+
SELECT id FROM session_v2
|
|
72
|
+
WHERE time_suspended IS NOT NULL AND COALESCE(parent_id, '') = '' AND COALESCE(time_archived, 0) = 0
|
|
73
|
+
AND time_busy > COALESCE(time_idle, 0)
|
|
74
|
+
), "
|
|
75
|
+
PREFILTER="AND m.session_id IN (SELECT id FROM cand)"
|
|
76
|
+
fi
|
|
77
|
+
|
|
60
78
|
read -r -d '' QUERY <<SQL || true
|
|
61
|
-
WITH live AS (
|
|
79
|
+
WITH ${CANDIDATES}live AS (
|
|
62
80
|
SELECT m.session_id AS session_id, m.seq AS seq,
|
|
63
81
|
json_extract(c.value, '\$.name') AS name,
|
|
64
82
|
json_extract(c.value, '\$.state.status') AS status,
|
|
@@ -68,6 +86,7 @@ WITH live AS (
|
|
|
68
86
|
AND (m.data LIKE '%"status":"running"%' OR m.data LIKE '%"status":"pending"%')
|
|
69
87
|
AND json_extract(c.value, '\$.type') = 'tool'
|
|
70
88
|
AND json_extract(c.value, '\$.state.status') IN ('pending', 'running')
|
|
89
|
+
$PREFILTER
|
|
71
90
|
)
|
|
72
91
|
SELECT s.id AS id, s.directory AS directory, s.title AS title, s.time_updated AS updated,
|
|
73
92
|
CASE WHEN EXISTS (
|
package/dist/tui.js
CHANGED
|
@@ -864,9 +864,19 @@ ORDER BY lifetime_cost DESC`;
|
|
|
864
864
|
// orphaned or awaiting approval). Server-independent on purpose: the TUI
|
|
865
865
|
// sync layer only sees permission/question state for sessions owned by the
|
|
866
866
|
// current server, which is invisible cross-project.
|
|
867
|
+
// Only a turn in flight can be waiting (time_suspended is set while a turn runs
|
|
868
|
+
// and a question/permission prompt keeps it running), so message payloads are
|
|
869
|
+
// scanned only for those candidate sessions. Databases without the in-flight
|
|
870
|
+
// columns scan every assistant message.
|
|
867
871
|
const STUCK_AFTER_MS = 600000;
|
|
868
|
-
const
|
|
869
|
-
|
|
872
|
+
const IN_FLIGHT_COLUMNS = ["time_busy", "time_idle", "time_suspended"];
|
|
873
|
+
const CANDIDATES_CTE = `cand AS (
|
|
874
|
+
SELECT id FROM session_v2
|
|
875
|
+
WHERE time_suspended IS NOT NULL AND parent_id IS NULL AND COALESCE(time_archived, 0) = 0
|
|
876
|
+
AND time_busy > COALESCE(time_idle, 0)
|
|
877
|
+
), `;
|
|
878
|
+
const LIVE_TOOLS_CTE = (sessionFilter, prefilter = false) => `
|
|
879
|
+
WITH ${prefilter ? CANDIDATES_CTE : ""}live AS (
|
|
870
880
|
SELECT m.session_id AS session_id, m.seq AS seq,
|
|
871
881
|
json_extract(c.value, '$.name') AS name,
|
|
872
882
|
json_extract(c.value, '$.state.status') AS status,
|
|
@@ -875,7 +885,7 @@ WITH live AS (
|
|
|
875
885
|
WHERE m.type = 'assistant'
|
|
876
886
|
AND (m.data LIKE '%"status":"running"%' OR m.data LIKE '%"status":"pending"%')
|
|
877
887
|
AND json_extract(c.value, '$.type') = 'tool'
|
|
878
|
-
AND json_extract(c.value, '$.state.status') IN ('pending', 'running')${sessionFilter}
|
|
888
|
+
AND json_extract(c.value, '$.state.status') IN ('pending', 'running')${sessionFilter}${prefilter ? "\n AND m.session_id IN (SELECT id FROM cand)" : ""}
|
|
879
889
|
)`;
|
|
880
890
|
const QUESTION_OPEN = `live.name = 'question'
|
|
881
891
|
AND NOT EXISTS (
|
|
@@ -884,7 +894,7 @@ const QUESTION_OPEN = `live.name = 'question'
|
|
|
884
894
|
const NO_RECENT_ACTIVITY = (sessionIdExpr, stuckBefore) => `NOT EXISTS (
|
|
885
895
|
SELECT 1 FROM session_message r JOIN session_v2 f ON f.id = r.session_id
|
|
886
896
|
WHERE (f.id = ${sessionIdExpr} OR f.parent_id = ${sessionIdExpr}) AND r.time_updated >= ${stuckBefore})`;
|
|
887
|
-
const NEEDS_INPUT_SQL = stuckBefore => `${LIVE_TOOLS_CTE("")}
|
|
897
|
+
const NEEDS_INPUT_SQL = (stuckBefore, prefilter = false) => `${LIVE_TOOLS_CTE("", prefilter)}
|
|
888
898
|
SELECT s.id AS id,
|
|
889
899
|
CASE WHEN EXISTS (SELECT 1 FROM live WHERE live.session_id = s.id AND ${QUESTION_OPEN})
|
|
890
900
|
THEN 'question' ELSE 'stuck' END AS reason
|
|
@@ -897,8 +907,23 @@ WHERE COALESCE(s.time_archived, 0) = 0
|
|
|
897
907
|
WHERE live.session_id = s.id AND live.status = 'running' AND live.created < ${stuckBefore})
|
|
898
908
|
AND ${NO_RECENT_ACTIVITY("s.id", stuckBefore)}))
|
|
899
909
|
ORDER BY s.time_updated DESC`;
|
|
910
|
+
const inFlightSupport = new WeakMap();
|
|
911
|
+
function hasInFlightColumns(run) {
|
|
912
|
+
let known = inFlightSupport.get(run);
|
|
913
|
+
if (!known) {
|
|
914
|
+
known = run("SELECT name FROM pragma_table_info('session_v2')").then(rows => {
|
|
915
|
+
const names = new Set(rows.map(row => row?.name));
|
|
916
|
+
return IN_FLIGHT_COLUMNS.every(name => names.has(name));
|
|
917
|
+
}, () => {
|
|
918
|
+
inFlightSupport.delete(run);
|
|
919
|
+
return false;
|
|
920
|
+
});
|
|
921
|
+
inFlightSupport.set(run, known);
|
|
922
|
+
}
|
|
923
|
+
return known;
|
|
924
|
+
}
|
|
900
925
|
async function queryWaitingIds(run) {
|
|
901
|
-
const rows = await run(NEEDS_INPUT_SQL(Date.now() - STUCK_AFTER_MS));
|
|
926
|
+
const rows = await run(NEEDS_INPUT_SQL(Date.now() - STUCK_AFTER_MS, await hasInFlightColumns(run)));
|
|
902
927
|
const out = new Map();
|
|
903
928
|
for (const row of rows) {
|
|
904
929
|
if (typeof row?.id === "string" && SESSION_ID_PATTERN.test(row.id) && (row.reason === "question" || row.reason === "stuck")) {
|
|
@@ -4542,7 +4567,7 @@ function createApi(ctx) {
|
|
|
4542
4567
|
render: cfg.slots.sidebar_content
|
|
4543
4568
|
}));
|
|
4544
4569
|
}
|
|
4545
|
-
if (cfg.slots.home_bottom) {
|
|
4570
|
+
if (cfg.slots.home_bottom && ctx.options?.showHome !== false) {
|
|
4546
4571
|
slotDisposers.push(ctx.ui.slot({
|
|
4547
4572
|
prepend: "home.footer",
|
|
4548
4573
|
render: cfg.slots.home_bottom
|
package/package.json
CHANGED
|
@@ -733,9 +733,19 @@ ORDER BY lifetime_cost DESC`
|
|
|
733
733
|
// orphaned or awaiting approval). Server-independent on purpose: the TUI
|
|
734
734
|
// sync layer only sees permission/question state for sessions owned by the
|
|
735
735
|
// current server, which is invisible cross-project.
|
|
736
|
+
// Only a turn in flight can be waiting (time_suspended is set while a turn runs
|
|
737
|
+
// and a question/permission prompt keeps it running), so message payloads are
|
|
738
|
+
// scanned only for those candidate sessions. Databases without the in-flight
|
|
739
|
+
// columns scan every assistant message.
|
|
736
740
|
const STUCK_AFTER_MS = 600000
|
|
737
|
-
const
|
|
738
|
-
|
|
741
|
+
const IN_FLIGHT_COLUMNS = ["time_busy", "time_idle", "time_suspended"]
|
|
742
|
+
const CANDIDATES_CTE = `cand AS (
|
|
743
|
+
SELECT id FROM session_v2
|
|
744
|
+
WHERE time_suspended IS NOT NULL AND parent_id IS NULL AND COALESCE(time_archived, 0) = 0
|
|
745
|
+
AND time_busy > COALESCE(time_idle, 0)
|
|
746
|
+
), `
|
|
747
|
+
const LIVE_TOOLS_CTE = (sessionFilter: string, prefilter = false) => `
|
|
748
|
+
WITH ${prefilter ? CANDIDATES_CTE : ""}live AS (
|
|
739
749
|
SELECT m.session_id AS session_id, m.seq AS seq,
|
|
740
750
|
json_extract(c.value, '$.name') AS name,
|
|
741
751
|
json_extract(c.value, '$.state.status') AS status,
|
|
@@ -744,7 +754,9 @@ WITH live AS (
|
|
|
744
754
|
WHERE m.type = 'assistant'
|
|
745
755
|
AND (m.data LIKE '%"status":"running"%' OR m.data LIKE '%"status":"pending"%')
|
|
746
756
|
AND json_extract(c.value, '$.type') = 'tool'
|
|
747
|
-
AND json_extract(c.value, '$.state.status') IN ('pending', 'running')${sessionFilter}
|
|
757
|
+
AND json_extract(c.value, '$.state.status') IN ('pending', 'running')${sessionFilter}${
|
|
758
|
+
prefilter ? "\n AND m.session_id IN (SELECT id FROM cand)" : ""
|
|
759
|
+
}
|
|
748
760
|
)`
|
|
749
761
|
const QUESTION_OPEN = `live.name = 'question'
|
|
750
762
|
AND NOT EXISTS (
|
|
@@ -753,7 +765,7 @@ const QUESTION_OPEN = `live.name = 'question'
|
|
|
753
765
|
const NO_RECENT_ACTIVITY = (sessionIdExpr: string, stuckBefore: number) => `NOT EXISTS (
|
|
754
766
|
SELECT 1 FROM session_message r JOIN session_v2 f ON f.id = r.session_id
|
|
755
767
|
WHERE (f.id = ${sessionIdExpr} OR f.parent_id = ${sessionIdExpr}) AND r.time_updated >= ${stuckBefore})`
|
|
756
|
-
const NEEDS_INPUT_SQL = (stuckBefore: number) => `${LIVE_TOOLS_CTE("")}
|
|
768
|
+
const NEEDS_INPUT_SQL = (stuckBefore: number, prefilter = false) => `${LIVE_TOOLS_CTE("", prefilter)}
|
|
757
769
|
SELECT s.id AS id,
|
|
758
770
|
CASE WHEN EXISTS (SELECT 1 FROM live WHERE live.session_id = s.id AND ${QUESTION_OPEN})
|
|
759
771
|
THEN 'question' ELSE 'stuck' END AS reason
|
|
@@ -767,8 +779,27 @@ WHERE COALESCE(s.time_archived, 0) = 0
|
|
|
767
779
|
AND ${NO_RECENT_ACTIVITY("s.id", stuckBefore)}))
|
|
768
780
|
ORDER BY s.time_updated DESC`
|
|
769
781
|
|
|
782
|
+
const inFlightSupport = new WeakMap<DbRun, Promise<boolean>>()
|
|
783
|
+
function hasInFlightColumns(run: DbRun): Promise<boolean> {
|
|
784
|
+
let known = inFlightSupport.get(run)
|
|
785
|
+
if (!known) {
|
|
786
|
+
known = run("SELECT name FROM pragma_table_info('session_v2')").then(
|
|
787
|
+
(rows) => {
|
|
788
|
+
const names = new Set((rows as { name?: unknown }[]).map((row) => row?.name))
|
|
789
|
+
return IN_FLIGHT_COLUMNS.every((name) => names.has(name))
|
|
790
|
+
},
|
|
791
|
+
() => {
|
|
792
|
+
inFlightSupport.delete(run)
|
|
793
|
+
return false
|
|
794
|
+
},
|
|
795
|
+
)
|
|
796
|
+
inFlightSupport.set(run, known)
|
|
797
|
+
}
|
|
798
|
+
return known
|
|
799
|
+
}
|
|
800
|
+
|
|
770
801
|
async function queryWaitingIds(run: DbRun): Promise<Map<string, string>> {
|
|
771
|
-
const rows = (await run(NEEDS_INPUT_SQL(Date.now() - STUCK_AFTER_MS))) as {
|
|
802
|
+
const rows = (await run(NEEDS_INPUT_SQL(Date.now() - STUCK_AFTER_MS, await hasInFlightColumns(run)))) as {
|
|
772
803
|
id: unknown
|
|
773
804
|
reason: unknown
|
|
774
805
|
}[]
|
|
@@ -3566,7 +3597,7 @@ function createApi(ctx: Plugin.Context) {
|
|
|
3566
3597
|
render: cfg.slots.sidebar_content as any,
|
|
3567
3598
|
}))
|
|
3568
3599
|
}
|
|
3569
|
-
if (cfg.slots.home_bottom) {
|
|
3600
|
+
if (cfg.slots.home_bottom && ctx.options?.showHome !== false) {
|
|
3570
3601
|
slotDisposers.push(ctx.ui.slot({
|
|
3571
3602
|
prepend: "home.footer",
|
|
3572
3603
|
render: cfg.slots.home_bottom as any,
|