opencode-sesh 2.1.4 → 2.1.5

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
@@ -31,6 +31,7 @@ bun run test # build + fixture-DB regression suite — must stay green
31
31
  bun run test:changelog # release-tooling regressions (scripts/changelog.sh)
32
32
  bun run test:picker # PTY picker suite
33
33
  bun run test:tui-home # real OpenTUI home layout regressions (also in bun run test)
34
+ bun run test:tui-worker # real blob Worker + bun:sqlite regressions (also in bun run test)
34
35
  bun run typecheck # tsc over tui-plugins/ and plugins/
35
36
  bun run lint:sh # bash -n on every script
36
37
  HOME=/tmp/fakehome bash install.sh # installer smoke test (never touches ~/.config)
@@ -181,10 +182,11 @@ when opencode exits, the picker reopens with the query preserved.
181
182
  scrolling inside; the picker remains the place for transcript search.
182
183
  Sessions waiting on the user (unanswered questions, stuck runs) pin a
183
184
  **Needs input** group above the directories, with the count in the heading —
184
- same `NEEDS_INPUT_SQL` heuristic as `bin/sesh-waiting.sh`. The query is
185
- synchronous on the TUI thread, so the shared poll runs it only while the
186
- sidebar is subscribed (`poll(listener, { waiting: true })`); the home list
187
- never triggers it.
185
+ same `NEEDS_INPUT_SQL` heuristic as `bin/sesh-waiting.sh`. The query runs in
186
+ a blob Worker (`createDbClient`; failure means "local store unavailable"),
187
+ off the TUI thread but still costing worker CPU every poll, so the shared
188
+ poll runs it only while the sidebar is subscribed
189
+ (`poll(listener, { waiting: true })`); the home list never triggers it.
188
190
  - **Picker:** a custom dialog (not `DialogSelect`) grouped by
189
191
  project/directory, recency-ordered, resume via
190
192
  `api.route.navigate("session", …)`. `option+o` and the command palette open it;
@@ -269,6 +271,9 @@ when opencode exits, the picker reopens with the query preserved.
269
271
  and Python 3).
270
272
  - `bun run test:tui-home` — real OpenTUI render regressions for the home list
271
273
  (also run by `bun run test`).
274
+ - `bun run test:tui-worker` — starts the real blob Worker against a bun:sqlite
275
+ fixture; asserts results round-trip and the main thread keeps ticking during
276
+ a slow query (also run by `bun run test`).
272
277
  - `bun run typecheck` — `tsc` over `tui-plugins/` and `plugins/`.
273
278
  - `bun run lint:sh` — `bash -n` on every script.
274
279
  - `HOME=/tmp/fakehome bash install.sh` — installer smoke test.
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
+ ## [2.1.5] - 2026-11-02
9
+
10
+ ### Fixed
11
+
12
+ - **tui:** Run needs-input SQLite reads in a worker (#34)
13
+
8
14
  ## [2.1.4] - 2026-11-02
9
15
 
10
16
  ### Fixed
@@ -536,6 +542,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
536
542
  - Fixture-database regression suite, TypeScript typechecking, and ShellCheck in
537
543
  CI.
538
544
 
545
+ [2.1.5]: https://github.com/ozandogrultan/opencode-sesh/compare/v2.1.4...v2.1.5
539
546
  [2.1.4]: https://github.com/ozandogrultan/opencode-sesh/compare/v2.1.3...v2.1.4
540
547
  [2.1.3]: https://github.com/ozandogrultan/opencode-sesh/compare/v2.1.2...v2.1.3
541
548
  [2.1.2]: https://github.com/ozandogrultan/opencode-sesh/compare/v2.1.1...v2.1.2
package/dist/tui.js CHANGED
@@ -636,9 +636,106 @@ async function openTranscriptDb() {
636
636
  return undefined;
637
637
  }
638
638
  }
639
+
640
+ // Runs in a blob Worker so slow queries never block the TUI thread. The
641
+ // connection opens on the first request and stays open; `loadSqlite` is
642
+ // injected only by tests.
643
+ const DB_WORKER_SOURCE = `
644
+ let opening
645
+ const load = typeof loadSqlite === "function" ? loadSqlite : () => import("bun:sqlite")
646
+ const open = (path) => opening ??= load().then((sqlite) => new sqlite.Database(path, { readonly: true }), (error) => { opening = undefined; throw error })
647
+ self.onmessage = async (event) => {
648
+ const { id, path, sql, params } = event.data
649
+ try {
650
+ const db = await open(path)
651
+ self.postMessage({ id, rows: db.query(sql).all(...(params ?? [])) })
652
+ } catch (error) {
653
+ self.postMessage({ id, error: String(error?.message ?? error) })
654
+ }
655
+ }`;
656
+ const DB_QUERY_TIMEOUT_MS = 30000;
657
+ function createDbWorker() {
658
+ return new globalThis.Worker(URL.createObjectURL(new Blob([DB_WORKER_SOURCE], {
659
+ type: "text/javascript"
660
+ })));
661
+ }
662
+
663
+ // Read-only queries against the local store, served by one lazily started
664
+ // worker. Any failure (no worker, no bun:sqlite, query error, timeout) rejects;
665
+ // a timeout or worker error discards the worker so the next call starts afresh.
666
+ function createDbClient(createWorker = createDbWorker, timeoutMs = DB_QUERY_TIMEOUT_MS) {
667
+ let worker;
668
+ let nextId = 0;
669
+ const pending = new Map();
670
+ const reset = error => {
671
+ const dead = worker;
672
+ worker = undefined;
673
+ try {
674
+ dead?.terminate();
675
+ } catch {}
676
+ const requests = [...pending.values()];
677
+ pending.clear();
678
+ for (const request of requests) {
679
+ clearTimeout(request.timer);
680
+ request.reject(error);
681
+ }
682
+ };
683
+ const start = () => {
684
+ const created = createWorker();
685
+ created.onmessage = event => {
686
+ const {
687
+ id,
688
+ rows,
689
+ error
690
+ } = event?.data ?? {};
691
+ const request = pending.get(id);
692
+ if (!request) return;
693
+ pending.delete(id);
694
+ clearTimeout(request.timer);
695
+ if (typeof error === "string") request.reject(new Error(error));else request.resolve(Array.isArray(rows) ? rows : []);
696
+ };
697
+ created.onerror = event => {
698
+ event?.preventDefault?.();
699
+ if (worker === created) reset(new Error(String(event?.message ?? "local store worker failed")));
700
+ };
701
+ created.unref?.();
702
+ return created;
703
+ };
704
+ const run = (sql, params = []) => new Promise((resolve, reject) => {
705
+ try {
706
+ worker ??= start();
707
+ } catch (error) {
708
+ reject(error);
709
+ return;
710
+ }
711
+ const id = ++nextId;
712
+ const timer = setTimeout(() => reset(new Error("local store query timed out")), timeoutMs);
713
+ timer.unref?.();
714
+ pending.set(id, {
715
+ resolve,
716
+ reject,
717
+ timer
718
+ });
719
+ try {
720
+ worker.postMessage({
721
+ id,
722
+ path: resolveDbPath(),
723
+ sql,
724
+ params
725
+ });
726
+ } catch (error) {
727
+ reset(error);
728
+ }
729
+ });
730
+ return {
731
+ run,
732
+ close: () => reset(new Error("local store closed"))
733
+ };
734
+ }
735
+ const dbClient = createDbClient();
639
736
  function createSessionPoll(api, refreshPins) {
640
- // Listener -> whether it reads `snapshot.waiting`. The needs-input query is
641
- // synchronous on the TUI thread, so it only runs while such a listener exists.
737
+ // Listener -> whether it reads `snapshot.waiting`. The needs-input query
738
+ // costs worker CPU on every poll, so it only runs while such a listener exists.
642
739
  const listeners = new Map();
643
740
  let snapshot = {
644
741
  entries: [],
@@ -677,20 +774,18 @@ function createSessionPoll(api, refreshPins) {
677
774
  };
678
775
  }
679
776
  if (!active()) return;
680
- const db = wantsWaiting() ? await openTranscriptDb() : undefined;
681
- if (db) {
777
+ if (wantsWaiting()) {
682
778
  try {
779
+ const waiting = await queryWaitingIds(dbClient.run);
683
780
  if (wantsWaiting()) {
684
781
  snapshot = {
685
782
  ...snapshot,
686
- waiting: await queryWaitingIds(db)
783
+ waiting
687
784
  };
688
785
  waitingLoaded = true;
689
786
  }
690
787
  } catch {
691
788
  // Keep the previous waiting set when the local store is unavailable.
692
- } finally {
693
- db.close();
694
789
  }
695
790
  } else if (!wantsWaiting()) {
696
791
  snapshot = {
@@ -788,8 +883,8 @@ WHERE COALESCE(s.time_archived, 0) = 0
788
883
  WHERE live.session_id = s.id AND live.status = 'running' AND live.created < ${stuckBefore})
789
884
  AND ${NO_RECENT_ACTIVITY("s.id", stuckBefore)}))
790
885
  ORDER BY s.time_updated DESC`;
791
- async function queryWaitingIds(db) {
792
- const rows = db.query(NEEDS_INPUT_SQL(Date.now() - STUCK_AFTER_MS)).all() ?? [];
886
+ async function queryWaitingIds(run) {
887
+ const rows = await run(NEEDS_INPUT_SQL(Date.now() - STUCK_AFTER_MS));
793
888
  const out = new Map();
794
889
  for (const row of rows) {
795
890
  if (typeof row?.id === "string" && SESSION_ID_PATTERN.test(row.id) && (row.reason === "question" || row.reason === "stuck")) {
@@ -801,8 +896,8 @@ async function queryWaitingIds(db) {
801
896
  // The shared needs-input query decides *which* sessions are waiting. Read the
802
897
  // oldest still-unanswered question or stuck tool for each result so triage can
803
898
  // show how long it has waited, rather than its last (unrelated) update time.
804
- async function queryWaitingDetails(db) {
805
- const reasons = await queryWaitingIds(db);
899
+ async function queryWaitingDetails(run) {
900
+ const reasons = await queryWaitingIds(run);
806
901
  const details = new Map([...reasons].map(([id, reason]) => [id, {
807
902
  reason: reason
808
903
  }]));
@@ -810,12 +905,12 @@ async function queryWaitingDetails(db) {
810
905
  const cutoff = Date.now() - STUCK_AFTER_MS;
811
906
  for (let i = 0; i < ids.length; i += 200) {
812
907
  const batch = ids.slice(i, i + 200);
813
- const rows = db.query(`${LIVE_TOOLS_CTE(` AND m.session_id IN (${batch.map(() => "?").join(",")})`)}
908
+ const rows = await run(`${LIVE_TOOLS_CTE(` AND m.session_id IN (${batch.map(() => "?").join(",")})`)}
814
909
  SELECT live.session_id AS id,
815
910
  MIN(CASE WHEN ${QUESTION_OPEN} THEN live.created END) AS question_since,
816
911
  MIN(CASE WHEN live.status = 'running' AND live.created < ${cutoff} THEN live.created END) AS stuck_since
817
912
  FROM live
818
- GROUP BY live.session_id`).all(...batch) ?? [];
913
+ GROUP BY live.session_id`, batch);
819
914
  for (const row of rows) {
820
915
  const detail = details.get(row.id);
821
916
  if (!detail) continue;
@@ -2749,21 +2844,14 @@ const tui = async api => {
2749
2844
  const [waitingCoverage, setWaitingCoverage] = createSignal("loading");
2750
2845
  let alive = true;
2751
2846
  void (async () => {
2752
- const db = await openTranscriptDb();
2753
- if (!db) {
2754
- if (alive) setWaitingCoverage("unavailable");
2755
- return;
2756
- }
2757
2847
  try {
2758
- const ids = await queryWaitingIds(db);
2848
+ const ids = await queryWaitingIds(dbClient.run);
2759
2849
  if (alive) {
2760
2850
  setWaitingIds(new Set(ids.keys()));
2761
2851
  setWaitingCoverage("ready");
2762
2852
  }
2763
2853
  } catch {
2764
2854
  if (alive) setWaitingCoverage("unavailable");
2765
- } finally {
2766
- db.close();
2767
2855
  }
2768
2856
  })();
2769
2857
  const [pendingDelete, setPendingDelete] = createSignal();
@@ -3924,20 +4012,16 @@ const tui = async api => {
3924
4012
  const {
3925
4013
  entries
3926
4014
  } = await fetchEntries(api);
3927
- const db = await openTranscriptDb();
3928
- if (!db) {
4015
+ let waiting;
4016
+ try {
4017
+ waiting = await queryWaitingDetails(dbClient.run);
4018
+ } catch {
3929
4019
  api.ui.toast({
3930
4020
  message: "Needs-input data unavailable",
3931
4021
  variant: "error"
3932
4022
  });
3933
4023
  return;
3934
4024
  }
3935
- let waiting;
3936
- try {
3937
- waiting = await queryWaitingDetails(db);
3938
- } finally {
3939
- db.close();
3940
- }
3941
4025
  items = entries.filter(entry => waiting.has(entry.id)).map(entry => ({
3942
4026
  entry,
3943
4027
  reason: waiting.get(entry.id)?.reason === "question" ? "awaiting answer" : "run stuck",
@@ -4491,6 +4575,7 @@ export default Plugin.define({
4491
4575
  if (api.ui.dialog.open) api.ui.dialog.clear();
4492
4576
  disposeSlot();
4493
4577
  removeRootMouse?.();
4578
+ dbClient.close();
4494
4579
  };
4495
4580
  }
4496
4581
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-sesh",
3
- "version": "2.1.4",
3
+ "version": "2.1.5",
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",
@@ -57,8 +57,9 @@
57
57
  "build": "node scripts/build-tui.mjs",
58
58
  "prepack": "node scripts/build-tui.mjs",
59
59
  "test": "bun run test:unit && bash tests/changelog.sh",
60
- "test:unit": "node scripts/build-tui.mjs && bun run test:tui-home && bash tests/sesh.sh && bash tests/install.sh",
60
+ "test:unit": "node scripts/build-tui.mjs && bun run test:tui-home && bun run test:tui-worker && bash tests/sesh.sh && bash tests/install.sh",
61
61
  "test:tui-home": "bun --conditions=browser tests/tui-home.mjs",
62
+ "test:tui-worker": "bun tests/tui-worker.mjs",
62
63
  "test:changelog": "bash tests/changelog.sh",
63
64
  "test:picker": "python3 tests/picker.py",
64
65
  "typecheck": "tsc --noEmit",
@@ -537,11 +537,96 @@ async function openTranscriptDb(): Promise<any | undefined> {
537
537
  }
538
538
  }
539
539
 
540
+ // Runs in a blob Worker so slow queries never block the TUI thread. The
541
+ // connection opens on the first request and stays open; `loadSqlite` is
542
+ // injected only by tests.
543
+ const DB_WORKER_SOURCE = `
544
+ let opening
545
+ const load = typeof loadSqlite === "function" ? loadSqlite : () => import("bun:sqlite")
546
+ const open = (path) => opening ??= load().then((sqlite) => new sqlite.Database(path, { readonly: true }), (error) => { opening = undefined; throw error })
547
+ self.onmessage = async (event) => {
548
+ const { id, path, sql, params } = event.data
549
+ try {
550
+ const db = await open(path)
551
+ self.postMessage({ id, rows: db.query(sql).all(...(params ?? [])) })
552
+ } catch (error) {
553
+ self.postMessage({ id, error: String(error?.message ?? error) })
554
+ }
555
+ }`
556
+
557
+ const DB_QUERY_TIMEOUT_MS = 30000
558
+
559
+ type DbRun = (sql: string, params?: unknown[]) => Promise<unknown[]>
560
+
561
+ function createDbWorker(): any {
562
+ return new (globalThis as any).Worker(URL.createObjectURL(new Blob([DB_WORKER_SOURCE], { type: "text/javascript" })))
563
+ }
564
+
565
+ // Read-only queries against the local store, served by one lazily started
566
+ // worker. Any failure (no worker, no bun:sqlite, query error, timeout) rejects;
567
+ // a timeout or worker error discards the worker so the next call starts afresh.
568
+ function createDbClient(createWorker: () => any = createDbWorker, timeoutMs = DB_QUERY_TIMEOUT_MS) {
569
+ type Pending = { resolve: (rows: unknown[]) => void; reject: (error: Error) => void; timer: ReturnType<typeof setTimeout> }
570
+ let worker: any
571
+ let nextId = 0
572
+ const pending = new Map<number, Pending>()
573
+ const reset = (error: Error) => {
574
+ const dead = worker
575
+ worker = undefined
576
+ try { dead?.terminate() } catch {}
577
+ const requests = [...pending.values()]
578
+ pending.clear()
579
+ for (const request of requests) {
580
+ clearTimeout(request.timer)
581
+ request.reject(error)
582
+ }
583
+ }
584
+ const start = () => {
585
+ const created = createWorker()
586
+ created.onmessage = (event: any) => {
587
+ const { id, rows, error } = event?.data ?? {}
588
+ const request = pending.get(id)
589
+ if (!request) return
590
+ pending.delete(id)
591
+ clearTimeout(request.timer)
592
+ if (typeof error === "string") request.reject(new Error(error))
593
+ else request.resolve(Array.isArray(rows) ? rows : [])
594
+ }
595
+ created.onerror = (event: any) => {
596
+ event?.preventDefault?.()
597
+ if (worker === created) reset(new Error(String(event?.message ?? "local store worker failed")))
598
+ }
599
+ created.unref?.()
600
+ return created
601
+ }
602
+ const run: DbRun = (sql, params = []) =>
603
+ new Promise((resolve, reject) => {
604
+ try {
605
+ worker ??= start()
606
+ } catch (error) {
607
+ reject(error)
608
+ return
609
+ }
610
+ const id = ++nextId
611
+ const timer = setTimeout(() => reset(new Error("local store query timed out")), timeoutMs)
612
+ ;(timer as any).unref?.()
613
+ pending.set(id, { resolve, reject, timer })
614
+ try {
615
+ worker.postMessage({ id, path: resolveDbPath(), sql, params })
616
+ } catch (error) {
617
+ reset(error as Error)
618
+ }
619
+ })
620
+ return { run, close: () => reset(new Error("local store closed")) }
621
+ }
622
+
623
+ const dbClient = createDbClient()
624
+
540
625
  type SessionSnapshot = { entries: Entry[]; waiting: Map<string, string>; failed: boolean }
541
626
 
542
627
  function createSessionPoll(api: TuiPluginApi, refreshPins: () => void) {
543
- // Listener -> whether it reads `snapshot.waiting`. The needs-input query is
544
- // synchronous on the TUI thread, so it only runs while such a listener exists.
628
+ // Listener -> whether it reads `snapshot.waiting`. The needs-input query
629
+ // costs worker CPU on every poll, so it only runs while such a listener exists.
545
630
  const listeners = new Map<(snapshot: SessionSnapshot) => void, boolean>()
546
631
  let snapshot: SessionSnapshot = { entries: [], waiting: new Map(), failed: false }
547
632
  let clock: ReturnType<typeof setInterval> | undefined
@@ -569,17 +654,15 @@ function createSessionPoll(api: TuiPluginApi, refreshPins: () => void) {
569
654
  snapshot = { ...snapshot, failed: true }
570
655
  }
571
656
  if (!active()) return
572
- const db = wantsWaiting() ? await openTranscriptDb() : undefined
573
- if (db) {
657
+ if (wantsWaiting()) {
574
658
  try {
659
+ const waiting = await queryWaitingIds(dbClient.run)
575
660
  if (wantsWaiting()) {
576
- snapshot = { ...snapshot, waiting: await queryWaitingIds(db) }
661
+ snapshot = { ...snapshot, waiting }
577
662
  waitingLoaded = true
578
663
  }
579
664
  } catch {
580
665
  // Keep the previous waiting set when the local store is unavailable.
581
- } finally {
582
- db.close()
583
666
  }
584
667
  } else if (!wantsWaiting()) {
585
668
  snapshot = { ...snapshot, waiting: new Map() }
@@ -674,8 +757,8 @@ WHERE COALESCE(s.time_archived, 0) = 0
674
757
  AND ${NO_RECENT_ACTIVITY("s.id", stuckBefore)}))
675
758
  ORDER BY s.time_updated DESC`
676
759
 
677
- async function queryWaitingIds(db: any): Promise<Map<string, string>> {
678
- const rows = (db.query(NEEDS_INPUT_SQL(Date.now() - STUCK_AFTER_MS)).all() ?? []) as {
760
+ async function queryWaitingIds(run: DbRun): Promise<Map<string, string>> {
761
+ const rows = (await run(NEEDS_INPUT_SQL(Date.now() - STUCK_AFTER_MS))) as {
679
762
  id: unknown
680
763
  reason: unknown
681
764
  }[]
@@ -697,8 +780,8 @@ type WaitingDetail = { reason: "question" | "stuck"; since?: number }
697
780
  // The shared needs-input query decides *which* sessions are waiting. Read the
698
781
  // oldest still-unanswered question or stuck tool for each result so triage can
699
782
  // show how long it has waited, rather than its last (unrelated) update time.
700
- async function queryWaitingDetails(db: any): Promise<Map<string, WaitingDetail>> {
701
- const reasons = await queryWaitingIds(db)
783
+ async function queryWaitingDetails(run: DbRun): Promise<Map<string, WaitingDetail>> {
784
+ const reasons = await queryWaitingIds(run)
702
785
  const details = new Map<string, WaitingDetail>(
703
786
  [...reasons].map(([id, reason]) => [id, { reason: reason as WaitingDetail["reason"] }]),
704
787
  )
@@ -706,12 +789,12 @@ async function queryWaitingDetails(db: any): Promise<Map<string, WaitingDetail>>
706
789
  const cutoff = Date.now() - STUCK_AFTER_MS
707
790
  for (let i = 0; i < ids.length; i += 200) {
708
791
  const batch = ids.slice(i, i + 200)
709
- const rows = (db.query(`${LIVE_TOOLS_CTE(` AND m.session_id IN (${batch.map(() => "?").join(",")})`)}
792
+ const rows = (await run(`${LIVE_TOOLS_CTE(` AND m.session_id IN (${batch.map(() => "?").join(",")})`)}
710
793
  SELECT live.session_id AS id,
711
794
  MIN(CASE WHEN ${QUESTION_OPEN} THEN live.created END) AS question_since,
712
795
  MIN(CASE WHEN live.status = 'running' AND live.created < ${cutoff} THEN live.created END) AS stuck_since
713
796
  FROM live
714
- GROUP BY live.session_id`).all(...batch) ?? []) as { id: string; question_since: number | null; stuck_since: number | null }[]
797
+ GROUP BY live.session_id`, batch)) as { id: string; question_since: number | null; stuck_since: number | null }[]
715
798
  for (const row of rows) {
716
799
  const detail = details.get(row.id)
717
800
  if (!detail) continue
@@ -2227,21 +2310,14 @@ const tui: TuiPlugin = async (api) => {
2227
2310
  const [waitingCoverage, setWaitingCoverage] = createSignal<"loading" | "ready" | "unavailable">("loading")
2228
2311
  let alive = true
2229
2312
  void (async () => {
2230
- const db = await openTranscriptDb()
2231
- if (!db) {
2232
- if (alive) setWaitingCoverage("unavailable")
2233
- return
2234
- }
2235
2313
  try {
2236
- const ids = await queryWaitingIds(db)
2314
+ const ids = await queryWaitingIds(dbClient.run)
2237
2315
  if (alive) {
2238
2316
  setWaitingIds(new Set(ids.keys()))
2239
2317
  setWaitingCoverage("ready")
2240
2318
  }
2241
2319
  } catch {
2242
2320
  if (alive) setWaitingCoverage("unavailable")
2243
- } finally {
2244
- db.close()
2245
2321
  }
2246
2322
  })()
2247
2323
  const [pendingDelete, setPendingDelete] = createSignal<Entry>()
@@ -3059,16 +3135,12 @@ const tui: TuiPlugin = async (api) => {
3059
3135
  let items: { entry: Entry; reason: string; since?: number }[] = []
3060
3136
  try {
3061
3137
  const { entries } = await fetchEntries(api)
3062
- const db = await openTranscriptDb()
3063
- if (!db) {
3064
- api.ui.toast({ message: "Needs-input data unavailable", variant: "error" })
3065
- return
3066
- }
3067
3138
  let waiting: Map<string, WaitingDetail>
3068
3139
  try {
3069
- waiting = await queryWaitingDetails(db)
3070
- } finally {
3071
- db.close()
3140
+ waiting = await queryWaitingDetails(dbClient.run)
3141
+ } catch {
3142
+ api.ui.toast({ message: "Needs-input data unavailable", variant: "error" })
3143
+ return
3072
3144
  }
3073
3145
  items = entries
3074
3146
  .filter((entry) => waiting.has(entry.id))
@@ -3521,6 +3593,7 @@ export default Plugin.define({
3521
3593
  if (api.ui.dialog.open) api.ui.dialog.clear()
3522
3594
  disposeSlot()
3523
3595
  removeRootMouse?.()
3596
+ dbClient.close()
3524
3597
  }
3525
3598
  },
3526
3599
  })