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 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). In the sidebar, pinned directories sort first, then directory groups
167
- by recency; sessions within each group sort by last update regardless of pins.
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`. The query runs in
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
@@ -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 LIVE_TOOLS_CTE = sessionFilter => `
869
- WITH live AS (
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-sesh",
3
- "version": "2.1.7",
3
+ "version": "2.2.0",
4
4
  "description": "OpenCode session browser: search, preview, and resume every session across every project from the terminal or TUI.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -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 LIVE_TOOLS_CTE = (sessionFilter: string) => `
738
- WITH live AS (
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,