opencode-sesh 2.1.6 → 2.1.7
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 +10 -1
- package/CHANGELOG.md +11 -0
- package/dist/tui.js +108 -13
- package/package.json +1 -1
- package/tui-plugins/sesh-panel/tui.tsx +98 -12
package/AGENTS.md
CHANGED
|
@@ -197,7 +197,16 @@ when opencode exits, the picker reopens with the query preserved.
|
|
|
197
197
|
- **Picker search:** pages the global session list (`SESSION_PAGE_LIMIT`, capped
|
|
198
198
|
at `SESSION_MAX` and reported when truncated) rather than a fixed-size
|
|
199
199
|
window, then indexes transcript text for **every** session in batches
|
|
200
|
-
(`TRANSCRIPT_BATCH
|
|
200
|
+
(`TRANSCRIPT_BATCH`, also capped by `TRANSCRIPT_BATCH_BYTES` of message
|
|
201
|
+
payload; a single larger session forms its own batch) with bounded remote
|
|
202
|
+
concurrency (`REMOTE_CONCURRENCY`). Transcript text is cached in memory for
|
|
203
|
+
the plugin lifetime (never on disk, cleared on dispose); a worker signature
|
|
204
|
+
query (session timestamp, message count, latest message timestamp, payload
|
|
205
|
+
size; chunked by `SIGNATURE_CHUNK`) lets reopened pickers reuse unchanged
|
|
206
|
+
sessions, extract only new or changed ones, and prune vanished ones. Sessions
|
|
207
|
+
indexed through the remote path are not cached. If the signature query fails
|
|
208
|
+
(`octet_length` needs SQLite ≥ 3.43; macOS Bun uses the system SQLite),
|
|
209
|
+
extraction still runs locally, batched by count only, and caches nothing.
|
|
201
210
|
Local extraction is one SQL statement per batch in the DB worker
|
|
202
211
|
(`transcriptTextQuery`, JSON-guarded) that returns only `{sid, text}` rows,
|
|
203
212
|
so tool payloads never reach the UI thread; if the worker rejects, sessions
|
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,16 @@ 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.7] - 2026-11-02
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- **tui:** Reuse unchanged transcript text and bound index batches by bytes (#37)
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- **tui:** Stop the DB worker and sidebar polls on plugin dispose (#36)
|
|
17
|
+
|
|
8
18
|
## [2.1.6] - 2026-11-02
|
|
9
19
|
|
|
10
20
|
### Changed
|
|
@@ -548,6 +558,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
548
558
|
- Fixture-database regression suite, TypeScript typechecking, and ShellCheck in
|
|
549
559
|
CI.
|
|
550
560
|
|
|
561
|
+
[2.1.7]: https://github.com/ozandogrultan/opencode-sesh/compare/v2.1.6...v2.1.7
|
|
551
562
|
[2.1.6]: https://github.com/ozandogrultan/opencode-sesh/compare/v2.1.5...v2.1.6
|
|
552
563
|
[2.1.5]: https://github.com/ozandogrultan/opencode-sesh/compare/v2.1.4...v2.1.5
|
|
553
564
|
[2.1.4]: https://github.com/ozandogrultan/opencode-sesh/compare/v2.1.3...v2.1.4
|
package/dist/tui.js
CHANGED
|
@@ -605,6 +605,8 @@ async function fetchEntries(api, active = () => true) {
|
|
|
605
605
|
}
|
|
606
606
|
const SESSION_ID_PATTERN = /^ses_[A-Za-z0-9]+$/;
|
|
607
607
|
const TRANSCRIPT_BATCH = 64;
|
|
608
|
+
const TRANSCRIPT_BATCH_BYTES = 8 * 1024 * 1024;
|
|
609
|
+
const SIGNATURE_CHUNK = 500;
|
|
608
610
|
const REMOTE_CONCURRENCY = 8;
|
|
609
611
|
const nextTick = () => new Promise(resolve => setTimeout(resolve, 0));
|
|
610
612
|
let resolvedDbPath;
|
|
@@ -663,9 +665,11 @@ function createDbWorker() {
|
|
|
663
665
|
// Read-only queries against the local store, served by one lazily started
|
|
664
666
|
// worker. Any failure (no worker, no bun:sqlite, query error, timeout) rejects;
|
|
665
667
|
// a timeout or worker error discards the worker so the next call starts afresh.
|
|
668
|
+
// `close` is final: later calls reject and never start a worker.
|
|
666
669
|
function createDbClient(createWorker = createDbWorker, timeoutMs = DB_QUERY_TIMEOUT_MS) {
|
|
667
670
|
let worker;
|
|
668
671
|
let nextId = 0;
|
|
672
|
+
let closed = false;
|
|
669
673
|
const pending = new Map();
|
|
670
674
|
const reset = error => {
|
|
671
675
|
const dead = worker;
|
|
@@ -702,6 +706,10 @@ function createDbClient(createWorker = createDbWorker, timeoutMs = DB_QUERY_TIME
|
|
|
702
706
|
return created;
|
|
703
707
|
};
|
|
704
708
|
const run = (sql, params = []) => new Promise((resolve, reject) => {
|
|
709
|
+
if (closed) {
|
|
710
|
+
reject(new Error("local store closed"));
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
705
713
|
try {
|
|
706
714
|
worker ??= start();
|
|
707
715
|
} catch (error) {
|
|
@@ -729,10 +737,16 @@ function createDbClient(createWorker = createDbWorker, timeoutMs = DB_QUERY_TIME
|
|
|
729
737
|
});
|
|
730
738
|
return {
|
|
731
739
|
run,
|
|
732
|
-
close: () =>
|
|
740
|
+
close: () => {
|
|
741
|
+
closed = true;
|
|
742
|
+
reset(new Error("local store closed"));
|
|
743
|
+
}
|
|
733
744
|
};
|
|
734
745
|
}
|
|
735
|
-
|
|
746
|
+
|
|
747
|
+
// Each plugin setup installs its own client and closes only that one on
|
|
748
|
+
// dispose: a reload reuses this module instance.
|
|
749
|
+
let dbClient = createDbClient();
|
|
736
750
|
function createSessionPoll(api, refreshPins) {
|
|
737
751
|
// Listener -> whether it reads `snapshot.waiting`. The needs-input query
|
|
738
752
|
// costs worker CPU on every poll, so it only runs while such a listener exists.
|
|
@@ -948,11 +962,33 @@ function transcriptTextQuery(count) {
|
|
|
948
962
|
SELECT sid, text FROM parts WHERE text IS NOT NULL AND text <> '' ORDER BY sid, seq, rid, part`;
|
|
949
963
|
}
|
|
950
964
|
|
|
965
|
+
// Transcript text per session for the plugin lifetime; memory only, so
|
|
966
|
+
// conversation text is never written anywhere. `sig` identifies the extracted
|
|
967
|
+
// state of the session (see transcriptSignatureQuery).
|
|
968
|
+
const transcriptCache = new Map();
|
|
969
|
+
const clearTranscriptCache = () => transcriptCache.clear();
|
|
970
|
+
|
|
971
|
+
// Cheap change detector mirroring the fzf extraction cache: message count and
|
|
972
|
+
// latest message timestamp, plus the payload size that bounds extraction
|
|
973
|
+
// batches. octet_length() reads the stored size, never the payload; length()
|
|
974
|
+
// would read every payload to count characters.
|
|
975
|
+
function transcriptSignatureQuery(count) {
|
|
976
|
+
const placeholders = Array.from({
|
|
977
|
+
length: count
|
|
978
|
+
}, () => "?").join(",");
|
|
979
|
+
return `SELECT session_id AS sid, COUNT(*) AS n, MAX(time_updated) AS m, SUM(octet_length(data)) AS bytes
|
|
980
|
+
FROM session_message
|
|
981
|
+
WHERE session_id IN (${placeholders}) AND type IN ('user', 'assistant')
|
|
982
|
+
GROUP BY session_id`;
|
|
983
|
+
}
|
|
984
|
+
|
|
951
985
|
// Index titles/directories immediately, then add transcript text progressively
|
|
952
986
|
// in batches so the render thread can breathe. Covers every entry, not a fixed
|
|
953
987
|
// newest window; `onProgress` lets the picker show coverage. Local extraction
|
|
954
|
-
// runs through `run` (the worker client) and returns text only
|
|
955
|
-
|
|
988
|
+
// runs through `run` (the worker client) and returns text only; sessions whose
|
|
989
|
+
// signature is unchanged reuse their cached text, and extraction batches are
|
|
990
|
+
// bounded by session count and payload bytes.
|
|
991
|
+
async function buildSearchIndex(api, entries, onProgress, active = () => true, run = dbClient.run, cache = transcriptCache) {
|
|
956
992
|
const index = new Map();
|
|
957
993
|
for (const entry of entries) {
|
|
958
994
|
index.set(entry.id, `${entry.title} ${entry.dir}`);
|
|
@@ -971,9 +1007,54 @@ async function buildSearchIndex(api, entries, onProgress, active = () => true, r
|
|
|
971
1007
|
report(false);
|
|
972
1008
|
if (!active()) return index;
|
|
973
1009
|
try {
|
|
974
|
-
|
|
1010
|
+
// `entries` must be the full session list: sessions missing from it are pruned.
|
|
1011
|
+
const idSet = new Set(ids);
|
|
1012
|
+
for (const sid of cache.keys()) if (!idSet.has(sid)) cache.delete(sid);
|
|
1013
|
+
// Undefined when the signature query fails (octet_length needs SQLite
|
|
1014
|
+
// >= 3.43, and macOS Bun uses the system SQLite): extract everything
|
|
1015
|
+
// locally, batched by count only, and cache nothing.
|
|
1016
|
+
let signatures = new Map();
|
|
1017
|
+
try {
|
|
1018
|
+
for (let i = 0; i < ids.length; i += SIGNATURE_CHUNK) {
|
|
1019
|
+
const chunk = ids.slice(i, i + SIGNATURE_CHUNK);
|
|
1020
|
+
const rows = await run(transcriptSignatureQuery(chunk.length), chunk);
|
|
1021
|
+
if (!active()) return index;
|
|
1022
|
+
for (const row of rows) signatures.set(row.sid, {
|
|
1023
|
+
sig: `${row.n}:${row.m}:${row.bytes}`,
|
|
1024
|
+
bytes: row.bytes ?? 0
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
} catch {
|
|
1028
|
+
if (!active()) return index;
|
|
1029
|
+
signatures = undefined;
|
|
1030
|
+
}
|
|
1031
|
+
const sigs = signatures && new Map(entries.map(entry => [entry.id, `${entry.updated}:${signatures.get(entry.id)?.sig ?? "0:0:0"}`]));
|
|
1032
|
+
const stale = [];
|
|
1033
|
+
for (const id of ids) {
|
|
1034
|
+
const hit = cache.get(id);
|
|
1035
|
+
if (hit && sigs && hit.sig === sigs.get(id)) {
|
|
1036
|
+
if (hit.text) index.set(id, `${index.get(id) ?? ""} ${hit.text}`.trim());
|
|
1037
|
+
remaining.delete(id);
|
|
1038
|
+
indexed += 1;
|
|
1039
|
+
} else stale.push(id);
|
|
1040
|
+
}
|
|
1041
|
+
if (indexed > 0) report(false);
|
|
1042
|
+
const batches = [];
|
|
1043
|
+
let current = [];
|
|
1044
|
+
let currentBytes = 0;
|
|
1045
|
+
for (const id of stale) {
|
|
1046
|
+
const bytes = signatures?.get(id)?.bytes ?? 0;
|
|
1047
|
+
if (current.length > 0 && (current.length >= TRANSCRIPT_BATCH || currentBytes + bytes > TRANSCRIPT_BATCH_BYTES)) {
|
|
1048
|
+
batches.push(current);
|
|
1049
|
+
current = [];
|
|
1050
|
+
currentBytes = 0;
|
|
1051
|
+
}
|
|
1052
|
+
current.push(id);
|
|
1053
|
+
currentBytes += bytes;
|
|
1054
|
+
}
|
|
1055
|
+
if (current.length > 0) batches.push(current);
|
|
1056
|
+
for (const batch of batches) {
|
|
975
1057
|
if (!active()) break;
|
|
976
|
-
const batch = ids.slice(i, i + TRANSCRIPT_BATCH);
|
|
977
1058
|
const rows = await run(transcriptTextQuery(batch.length), batch);
|
|
978
1059
|
if (!active()) break;
|
|
979
1060
|
const texts = new Map();
|
|
@@ -981,8 +1062,15 @@ async function buildSearchIndex(api, entries, onProgress, active = () => true, r
|
|
|
981
1062
|
const list = texts.get(row.sid);
|
|
982
1063
|
if (list) list.push(row.text);else texts.set(row.sid, [row.text]);
|
|
983
1064
|
}
|
|
984
|
-
for (const
|
|
985
|
-
|
|
1065
|
+
for (const id of batch) {
|
|
1066
|
+
const list = texts.get(id) ?? [];
|
|
1067
|
+
if (sigs) cache.set(id, {
|
|
1068
|
+
sig: sigs.get(id),
|
|
1069
|
+
text: list.join(" ").trim()
|
|
1070
|
+
});
|
|
1071
|
+
if (list.length > 0) index.set(id, [index.get(id) ?? "", ...list].join(" ").trim());
|
|
1072
|
+
remaining.delete(id);
|
|
1073
|
+
}
|
|
986
1074
|
indexed += batch.length;
|
|
987
1075
|
report(false);
|
|
988
1076
|
await nextTick();
|
|
@@ -4306,7 +4394,11 @@ function createApi(ctx) {
|
|
|
4306
4394
|
const [dialogOpen, setDialogOpen] = createSignal(false);
|
|
4307
4395
|
let sourceTheme;
|
|
4308
4396
|
let currentTheme;
|
|
4397
|
+
const slotDisposers = [];
|
|
4309
4398
|
return {
|
|
4399
|
+
dispose() {
|
|
4400
|
+
for (const dispose of slotDisposers.splice(0)) dispose();
|
|
4401
|
+
},
|
|
4310
4402
|
theme: {
|
|
4311
4403
|
get current() {
|
|
4312
4404
|
if (sourceTheme !== ctx.theme) {
|
|
@@ -4441,20 +4533,20 @@ function createApi(ctx) {
|
|
|
4441
4533
|
slots: {
|
|
4442
4534
|
register(cfg) {
|
|
4443
4535
|
if (cfg.slots.sidebar_content && ctx.options?.showSidebar !== false) {
|
|
4444
|
-
ctx.ui.slot({
|
|
4536
|
+
slotDisposers.push(ctx.ui.slot({
|
|
4445
4537
|
...(ctx.options?.sidebarPlacement === "prepend" ? {
|
|
4446
4538
|
prepend: "sidebar.content"
|
|
4447
4539
|
} : {
|
|
4448
4540
|
append: "sidebar.content"
|
|
4449
4541
|
}),
|
|
4450
4542
|
render: cfg.slots.sidebar_content
|
|
4451
|
-
});
|
|
4543
|
+
}));
|
|
4452
4544
|
}
|
|
4453
4545
|
if (cfg.slots.home_bottom) {
|
|
4454
|
-
ctx.ui.slot({
|
|
4546
|
+
slotDisposers.push(ctx.ui.slot({
|
|
4455
4547
|
prepend: "home.footer",
|
|
4456
4548
|
render: cfg.slots.home_bottom
|
|
4457
|
-
});
|
|
4549
|
+
}));
|
|
4458
4550
|
}
|
|
4459
4551
|
}
|
|
4460
4552
|
},
|
|
@@ -4568,6 +4660,7 @@ function createApi(ctx) {
|
|
|
4568
4660
|
export default Plugin.define({
|
|
4569
4661
|
id: "sesh-panel",
|
|
4570
4662
|
async setup(ctx) {
|
|
4663
|
+
const client = dbClient = createDbClient();
|
|
4571
4664
|
const api = createApi(ctx);
|
|
4572
4665
|
let initialized = false;
|
|
4573
4666
|
const disposeSlot = ctx.ui.slot({
|
|
@@ -4584,8 +4677,10 @@ export default Plugin.define({
|
|
|
4584
4677
|
return () => {
|
|
4585
4678
|
if (api.ui.dialog.open) api.ui.dialog.clear();
|
|
4586
4679
|
disposeSlot();
|
|
4680
|
+
api.dispose();
|
|
4587
4681
|
removeRootMouse?.();
|
|
4588
|
-
|
|
4682
|
+
clearTranscriptCache();
|
|
4683
|
+
client.close();
|
|
4589
4684
|
};
|
|
4590
4685
|
}
|
|
4591
4686
|
});
|
package/package.json
CHANGED
|
@@ -503,6 +503,8 @@ async function fetchEntries(api: TuiPluginApi, active = () => true): Promise<Ent
|
|
|
503
503
|
|
|
504
504
|
const SESSION_ID_PATTERN = /^ses_[A-Za-z0-9]+$/
|
|
505
505
|
const TRANSCRIPT_BATCH = 64
|
|
506
|
+
const TRANSCRIPT_BATCH_BYTES = 8 * 1024 * 1024
|
|
507
|
+
const SIGNATURE_CHUNK = 500
|
|
506
508
|
const REMOTE_CONCURRENCY = 8
|
|
507
509
|
|
|
508
510
|
type IndexProgress = { indexed: number; total: number; complete: boolean }
|
|
@@ -565,10 +567,12 @@ function createDbWorker(): any {
|
|
|
565
567
|
// Read-only queries against the local store, served by one lazily started
|
|
566
568
|
// worker. Any failure (no worker, no bun:sqlite, query error, timeout) rejects;
|
|
567
569
|
// a timeout or worker error discards the worker so the next call starts afresh.
|
|
570
|
+
// `close` is final: later calls reject and never start a worker.
|
|
568
571
|
function createDbClient(createWorker: () => any = createDbWorker, timeoutMs = DB_QUERY_TIMEOUT_MS) {
|
|
569
572
|
type Pending = { resolve: (rows: unknown[]) => void; reject: (error: Error) => void; timer: ReturnType<typeof setTimeout> }
|
|
570
573
|
let worker: any
|
|
571
574
|
let nextId = 0
|
|
575
|
+
let closed = false
|
|
572
576
|
const pending = new Map<number, Pending>()
|
|
573
577
|
const reset = (error: Error) => {
|
|
574
578
|
const dead = worker
|
|
@@ -601,6 +605,10 @@ function createDbClient(createWorker: () => any = createDbWorker, timeoutMs = DB
|
|
|
601
605
|
}
|
|
602
606
|
const run: DbRun = (sql, params = []) =>
|
|
603
607
|
new Promise((resolve, reject) => {
|
|
608
|
+
if (closed) {
|
|
609
|
+
reject(new Error("local store closed"))
|
|
610
|
+
return
|
|
611
|
+
}
|
|
604
612
|
try {
|
|
605
613
|
worker ??= start()
|
|
606
614
|
} catch (error) {
|
|
@@ -617,10 +625,12 @@ function createDbClient(createWorker: () => any = createDbWorker, timeoutMs = DB
|
|
|
617
625
|
reset(error as Error)
|
|
618
626
|
}
|
|
619
627
|
})
|
|
620
|
-
return { run, close: () => reset(new Error("local store closed")) }
|
|
628
|
+
return { run, close: () => { closed = true; reset(new Error("local store closed")) } }
|
|
621
629
|
}
|
|
622
630
|
|
|
623
|
-
|
|
631
|
+
// Each plugin setup installs its own client and closes only that one on
|
|
632
|
+
// dispose: a reload reuses this module instance.
|
|
633
|
+
let dbClient = createDbClient()
|
|
624
634
|
|
|
625
635
|
type SessionSnapshot = { entries: Entry[]; waiting: Map<string, string>; failed: boolean }
|
|
626
636
|
|
|
@@ -830,16 +840,37 @@ function transcriptTextQuery(count: number): string {
|
|
|
830
840
|
SELECT sid, text FROM parts WHERE text IS NOT NULL AND text <> '' ORDER BY sid, seq, rid, part`
|
|
831
841
|
}
|
|
832
842
|
|
|
843
|
+
// Transcript text per session for the plugin lifetime; memory only, so
|
|
844
|
+
// conversation text is never written anywhere. `sig` identifies the extracted
|
|
845
|
+
// state of the session (see transcriptSignatureQuery).
|
|
846
|
+
const transcriptCache = new Map<string, { sig: string; text: string }>()
|
|
847
|
+
const clearTranscriptCache = () => transcriptCache.clear()
|
|
848
|
+
|
|
849
|
+
// Cheap change detector mirroring the fzf extraction cache: message count and
|
|
850
|
+
// latest message timestamp, plus the payload size that bounds extraction
|
|
851
|
+
// batches. octet_length() reads the stored size, never the payload; length()
|
|
852
|
+
// would read every payload to count characters.
|
|
853
|
+
function transcriptSignatureQuery(count: number): string {
|
|
854
|
+
const placeholders = Array.from({ length: count }, () => "?").join(",")
|
|
855
|
+
return `SELECT session_id AS sid, COUNT(*) AS n, MAX(time_updated) AS m, SUM(octet_length(data)) AS bytes
|
|
856
|
+
FROM session_message
|
|
857
|
+
WHERE session_id IN (${placeholders}) AND type IN ('user', 'assistant')
|
|
858
|
+
GROUP BY session_id`
|
|
859
|
+
}
|
|
860
|
+
|
|
833
861
|
// Index titles/directories immediately, then add transcript text progressively
|
|
834
862
|
// in batches so the render thread can breathe. Covers every entry, not a fixed
|
|
835
863
|
// newest window; `onProgress` lets the picker show coverage. Local extraction
|
|
836
|
-
// runs through `run` (the worker client) and returns text only
|
|
864
|
+
// runs through `run` (the worker client) and returns text only; sessions whose
|
|
865
|
+
// signature is unchanged reuse their cached text, and extraction batches are
|
|
866
|
+
// bounded by session count and payload bytes.
|
|
837
867
|
async function buildSearchIndex(
|
|
838
868
|
api: TuiPluginApi,
|
|
839
869
|
entries: Entry[],
|
|
840
870
|
onProgress?: (progress: IndexProgress, index: Map<string, string>) => void,
|
|
841
871
|
active = () => true,
|
|
842
872
|
run: DbRun = dbClient.run,
|
|
873
|
+
cache = transcriptCache,
|
|
843
874
|
): Promise<Map<string, string>> {
|
|
844
875
|
const index = new Map<string, string>()
|
|
845
876
|
for (const entry of entries) {
|
|
@@ -854,9 +885,53 @@ async function buildSearchIndex(
|
|
|
854
885
|
|
|
855
886
|
if (!active()) return index
|
|
856
887
|
try {
|
|
857
|
-
|
|
888
|
+
// `entries` must be the full session list: sessions missing from it are pruned.
|
|
889
|
+
const idSet = new Set(ids)
|
|
890
|
+
for (const sid of cache.keys()) if (!idSet.has(sid)) cache.delete(sid)
|
|
891
|
+
// Undefined when the signature query fails (octet_length needs SQLite
|
|
892
|
+
// >= 3.43, and macOS Bun uses the system SQLite): extract everything
|
|
893
|
+
// locally, batched by count only, and cache nothing.
|
|
894
|
+
let signatures: Map<string, { sig: string; bytes: number }> | undefined = new Map()
|
|
895
|
+
try {
|
|
896
|
+
for (let i = 0; i < ids.length; i += SIGNATURE_CHUNK) {
|
|
897
|
+
const chunk = ids.slice(i, i + SIGNATURE_CHUNK)
|
|
898
|
+
const rows = (await run(transcriptSignatureQuery(chunk.length), chunk)) as { sid: string; n: number; m: number; bytes: number }[]
|
|
899
|
+
if (!active()) return index
|
|
900
|
+
for (const row of rows) signatures.set(row.sid, { sig: `${row.n}:${row.m}:${row.bytes}`, bytes: row.bytes ?? 0 })
|
|
901
|
+
}
|
|
902
|
+
} catch {
|
|
903
|
+
if (!active()) return index
|
|
904
|
+
signatures = undefined
|
|
905
|
+
}
|
|
906
|
+
const sigs = signatures && new Map(entries.map((entry) => [entry.id, `${entry.updated}:${signatures!.get(entry.id)?.sig ?? "0:0:0"}`]))
|
|
907
|
+
const stale: string[] = []
|
|
908
|
+
for (const id of ids) {
|
|
909
|
+
const hit = cache.get(id)
|
|
910
|
+
if (hit && sigs && hit.sig === sigs.get(id)) {
|
|
911
|
+
if (hit.text) index.set(id, `${index.get(id) ?? ""} ${hit.text}`.trim())
|
|
912
|
+
remaining.delete(id)
|
|
913
|
+
indexed += 1
|
|
914
|
+
} else stale.push(id)
|
|
915
|
+
}
|
|
916
|
+
if (indexed > 0) report(false)
|
|
917
|
+
|
|
918
|
+
const batches: string[][] = []
|
|
919
|
+
let current: string[] = []
|
|
920
|
+
let currentBytes = 0
|
|
921
|
+
for (const id of stale) {
|
|
922
|
+
const bytes = signatures?.get(id)?.bytes ?? 0
|
|
923
|
+
if (current.length > 0 && (current.length >= TRANSCRIPT_BATCH || currentBytes + bytes > TRANSCRIPT_BATCH_BYTES)) {
|
|
924
|
+
batches.push(current)
|
|
925
|
+
current = []
|
|
926
|
+
currentBytes = 0
|
|
927
|
+
}
|
|
928
|
+
current.push(id)
|
|
929
|
+
currentBytes += bytes
|
|
930
|
+
}
|
|
931
|
+
if (current.length > 0) batches.push(current)
|
|
932
|
+
|
|
933
|
+
for (const batch of batches) {
|
|
858
934
|
if (!active()) break
|
|
859
|
-
const batch = ids.slice(i, i + TRANSCRIPT_BATCH)
|
|
860
935
|
const rows = (await run(transcriptTextQuery(batch.length), batch)) as { sid: string; text: string }[]
|
|
861
936
|
if (!active()) break
|
|
862
937
|
const texts = new Map<string, string[]>()
|
|
@@ -865,8 +940,12 @@ async function buildSearchIndex(
|
|
|
865
940
|
if (list) list.push(row.text)
|
|
866
941
|
else texts.set(row.sid, [row.text])
|
|
867
942
|
}
|
|
868
|
-
for (const
|
|
869
|
-
|
|
943
|
+
for (const id of batch) {
|
|
944
|
+
const list = texts.get(id) ?? []
|
|
945
|
+
if (sigs) cache.set(id, { sig: sigs.get(id)!, text: list.join(" ").trim() })
|
|
946
|
+
if (list.length > 0) index.set(id, [index.get(id) ?? "", ...list].join(" ").trim())
|
|
947
|
+
remaining.delete(id)
|
|
948
|
+
}
|
|
870
949
|
indexed += batch.length
|
|
871
950
|
report(false)
|
|
872
951
|
await nextTick()
|
|
@@ -3352,7 +3431,11 @@ function createApi(ctx: Plugin.Context) {
|
|
|
3352
3431
|
const [dialogOpen, setDialogOpen] = createSignal(false)
|
|
3353
3432
|
let sourceTheme: Plugin.Context["theme"] | undefined
|
|
3354
3433
|
let currentTheme: ReturnType<typeof adaptTheme>
|
|
3434
|
+
const slotDisposers: Array<() => void> = []
|
|
3355
3435
|
return {
|
|
3436
|
+
dispose() {
|
|
3437
|
+
for (const dispose of slotDisposers.splice(0)) dispose()
|
|
3438
|
+
},
|
|
3356
3439
|
theme: {
|
|
3357
3440
|
get current() {
|
|
3358
3441
|
if (sourceTheme !== ctx.theme) {
|
|
@@ -3476,18 +3559,18 @@ function createApi(ctx: Plugin.Context) {
|
|
|
3476
3559
|
slots: {
|
|
3477
3560
|
register(cfg: { order?: number; slots: Record<string, () => any> }) {
|
|
3478
3561
|
if (cfg.slots.sidebar_content && ctx.options?.showSidebar !== false) {
|
|
3479
|
-
ctx.ui.slot({
|
|
3562
|
+
slotDisposers.push(ctx.ui.slot({
|
|
3480
3563
|
...(ctx.options?.sidebarPlacement === "prepend"
|
|
3481
3564
|
? { prepend: "sidebar.content" as const }
|
|
3482
3565
|
: { append: "sidebar.content" as const }),
|
|
3483
3566
|
render: cfg.slots.sidebar_content as any,
|
|
3484
|
-
})
|
|
3567
|
+
}))
|
|
3485
3568
|
}
|
|
3486
3569
|
if (cfg.slots.home_bottom) {
|
|
3487
|
-
ctx.ui.slot({
|
|
3570
|
+
slotDisposers.push(ctx.ui.slot({
|
|
3488
3571
|
prepend: "home.footer",
|
|
3489
3572
|
render: cfg.slots.home_bottom as any,
|
|
3490
|
-
})
|
|
3573
|
+
}))
|
|
3491
3574
|
}
|
|
3492
3575
|
},
|
|
3493
3576
|
},
|
|
@@ -3581,6 +3664,7 @@ function createApi(ctx: Plugin.Context) {
|
|
|
3581
3664
|
export default Plugin.define({
|
|
3582
3665
|
id: "sesh-panel",
|
|
3583
3666
|
async setup(ctx) {
|
|
3667
|
+
const client = dbClient = createDbClient()
|
|
3584
3668
|
const api = createApi(ctx)
|
|
3585
3669
|
let initialized = false
|
|
3586
3670
|
const disposeSlot = ctx.ui.slot({
|
|
@@ -3597,8 +3681,10 @@ export default Plugin.define({
|
|
|
3597
3681
|
return () => {
|
|
3598
3682
|
if (api.ui.dialog.open) api.ui.dialog.clear()
|
|
3599
3683
|
disposeSlot()
|
|
3684
|
+
api.dispose()
|
|
3600
3685
|
removeRootMouse?.()
|
|
3601
|
-
|
|
3686
|
+
clearTranscriptCache()
|
|
3687
|
+
client.close()
|
|
3602
3688
|
}
|
|
3603
3689
|
},
|
|
3604
3690
|
})
|