@bojackduy/opencode-telescope 0.1.14 → 0.1.15
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/package.json +1 -1
- package/search.ts +74 -18
- package/telescope.tsx +97 -46
- package/ui/render-target.ts +2 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "@bojackduy/opencode-telescope",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.15",
|
|
5
5
|
"description": "Fuzzy search across all OpenCode conversations — grep session and chat history, find code snippets, and jump to any chat instantly",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"license": "MIT",
|
package/search.ts
CHANGED
|
@@ -11,6 +11,7 @@ export type SearchResult = {
|
|
|
11
11
|
sessionTitle: string
|
|
12
12
|
directory: string
|
|
13
13
|
role: "user" | "assistant"
|
|
14
|
+
partType: "text" | "reasoning" | "tool" | "patch"
|
|
14
15
|
timeCreated: number
|
|
15
16
|
snippet: string
|
|
16
17
|
matchStart: number
|
|
@@ -67,10 +68,15 @@ type Row = {
|
|
|
67
68
|
session_title: string | null
|
|
68
69
|
directory: string
|
|
69
70
|
role: SearchRole
|
|
71
|
+
part_type?: SearchResult["partType"]
|
|
70
72
|
time_created: number
|
|
71
73
|
text: string
|
|
72
74
|
}
|
|
73
75
|
|
|
76
|
+
type SourceRow = Omit<Row, "text"> & {
|
|
77
|
+
data: string
|
|
78
|
+
}
|
|
79
|
+
|
|
74
80
|
type ConversationRow = {
|
|
75
81
|
id: string
|
|
76
82
|
message_id: string
|
|
@@ -318,6 +324,7 @@ export function rowToSearchResult(row: Row, query: string): SearchResult | undef
|
|
|
318
324
|
sessionTitle: row.session_title || "Untitled session",
|
|
319
325
|
directory: row.directory,
|
|
320
326
|
role: row.role,
|
|
327
|
+
partType: row.part_type ?? "text",
|
|
321
328
|
timeCreated: row.time_created,
|
|
322
329
|
snippet: makeSnippet(text, query),
|
|
323
330
|
matchStart: match.start,
|
|
@@ -337,7 +344,7 @@ export function rowToSearchResult(row: Row, query: string): SearchResult | undef
|
|
|
337
344
|
|
|
338
345
|
export function extractSearchText(data: string) {
|
|
339
346
|
try {
|
|
340
|
-
return
|
|
347
|
+
return searchablePartText(JSON.parse(data)).replace(/\s+/g, " ").trim()
|
|
341
348
|
} catch {
|
|
342
349
|
return data.replace(/\s+/g, " ").trim()
|
|
343
350
|
}
|
|
@@ -569,7 +576,7 @@ function indexedTextRows(db: Database, dbPath: string, limit: number, query: str
|
|
|
569
576
|
debug.time("query:fts:exec")
|
|
570
577
|
const rows = index.query<Row, (string | number)[]>(`
|
|
571
578
|
SELECT id, message_id, session_id, session_title, directory, role,
|
|
572
|
-
CAST(time_created AS INTEGER) AS time_created, text
|
|
579
|
+
part_type, CAST(time_created AS INTEGER) AS time_created, text
|
|
573
580
|
FROM document_fts
|
|
574
581
|
WHERE ${conditions.join(" AND ")}
|
|
575
582
|
ORDER BY bm25(document_fts), CAST(time_created AS INTEGER) DESC
|
|
@@ -586,7 +593,7 @@ function indexedTextRows(db: Database, dbPath: string, limit: number, query: str
|
|
|
586
593
|
function visibleTextRows(db: Database, limit: number, query?: string, directory?: string, offset?: number, role?: SearchRole) {
|
|
587
594
|
const offsetClause = offset ? "OFFSET ?" : ""
|
|
588
595
|
const conditions: string[] = [
|
|
589
|
-
"json_extract(p.data, '$.type')
|
|
596
|
+
"json_extract(p.data, '$.type') IN ('text', 'reasoning', 'tool', 'patch')",
|
|
590
597
|
role ? "json_extract(m.data, '$.role') = ?" : "json_extract(m.data, '$.role') IN ('user', 'assistant')",
|
|
591
598
|
]
|
|
592
599
|
const params: (string | number)[] = []
|
|
@@ -600,7 +607,7 @@ function visibleTextRows(db: Database, limit: number, query?: string, directory?
|
|
|
600
607
|
|
|
601
608
|
const tokens = query ? query.trim().split(/\s+/).filter(Boolean) : []
|
|
602
609
|
for (const token of tokens) {
|
|
603
|
-
conditions.push("
|
|
610
|
+
conditions.push("p.data LIKE ?")
|
|
604
611
|
params.push(`%${token}%`)
|
|
605
612
|
}
|
|
606
613
|
|
|
@@ -610,8 +617,9 @@ function visibleTextRows(db: Database, limit: number, query?: string, directory?
|
|
|
610
617
|
const sql = `
|
|
611
618
|
SELECT p.id, p.message_id, p.session_id, s.title AS session_title, s.directory,
|
|
612
619
|
json_extract(m.data, '$.role') AS role,
|
|
620
|
+
json_extract(p.data, '$.type') AS part_type,
|
|
613
621
|
p.time_created,
|
|
614
|
-
|
|
622
|
+
p.data
|
|
615
623
|
FROM part p
|
|
616
624
|
JOIN message m ON m.id = p.message_id
|
|
617
625
|
JOIN session s ON s.id = p.session_id
|
|
@@ -620,9 +628,9 @@ function visibleTextRows(db: Database, limit: number, query?: string, directory?
|
|
|
620
628
|
LIMIT ? ${offsetClause}
|
|
621
629
|
`
|
|
622
630
|
debug.time("query:sql:exec")
|
|
623
|
-
const rows = db.query<
|
|
631
|
+
const rows = db.query<SourceRow, (string | number)[]>(sql).all(...params as any[])
|
|
624
632
|
debug.timeEnd("query:sql:exec")
|
|
625
|
-
return rows
|
|
633
|
+
return rows.flatMap(sourceRowToSearchRow)
|
|
626
634
|
}
|
|
627
635
|
|
|
628
636
|
function ensureSearchIndex(source: Database, sourcePath: string) {
|
|
@@ -638,12 +646,15 @@ function ensureSearchIndex(source: Database, sourcePath: string) {
|
|
|
638
646
|
const currentDataVersion = getMeta(_indexDb, "source_data_version")
|
|
639
647
|
const currentMtimeMs = getMeta(_indexDb, "source_mtime_ms")
|
|
640
648
|
const currentPath = getMeta(_indexDb, "source_path")
|
|
641
|
-
|
|
649
|
+
const currentIndexVersion = getMeta(_indexDb, "index_version")
|
|
650
|
+
if (currentPath !== sourcePath || currentDataVersion !== String(state.dataVersion) || currentMtimeMs !== String(state.mtimeMs) || currentIndexVersion !== SEARCH_INDEX_VERSION) {
|
|
642
651
|
rebuildSearchIndex(source, _indexDb, sourcePath, state)
|
|
643
652
|
}
|
|
644
653
|
return _indexDb
|
|
645
654
|
}
|
|
646
655
|
|
|
656
|
+
const SEARCH_INDEX_VERSION = "3"
|
|
657
|
+
|
|
647
658
|
function searchIndexPath(sourcePath: string) {
|
|
648
659
|
const parsed = path.parse(sourcePath)
|
|
649
660
|
return path.join(parsed.dir, `${parsed.name}-search.db`)
|
|
@@ -662,11 +673,31 @@ function migrateSearchIndex(db: Database) {
|
|
|
662
673
|
session_title,
|
|
663
674
|
directory UNINDEXED,
|
|
664
675
|
role UNINDEXED,
|
|
676
|
+
part_type UNINDEXED,
|
|
665
677
|
time_created UNINDEXED,
|
|
666
678
|
text,
|
|
667
679
|
tokenize='unicode61'
|
|
668
680
|
);
|
|
669
681
|
`)
|
|
682
|
+
|
|
683
|
+
const hasPartType = db.query<{ name: string }, []>("PRAGMA table_info(document_fts)").all().some((column) => column.name === "part_type")
|
|
684
|
+
if (!hasPartType) {
|
|
685
|
+
db.exec("DROP TABLE document_fts")
|
|
686
|
+
db.exec(`
|
|
687
|
+
CREATE VIRTUAL TABLE document_fts USING fts5(
|
|
688
|
+
id UNINDEXED,
|
|
689
|
+
message_id UNINDEXED,
|
|
690
|
+
session_id UNINDEXED,
|
|
691
|
+
session_title,
|
|
692
|
+
directory UNINDEXED,
|
|
693
|
+
role UNINDEXED,
|
|
694
|
+
part_type UNINDEXED,
|
|
695
|
+
time_created UNINDEXED,
|
|
696
|
+
text,
|
|
697
|
+
tokenize='unicode61'
|
|
698
|
+
);
|
|
699
|
+
`)
|
|
700
|
+
}
|
|
670
701
|
}
|
|
671
702
|
|
|
672
703
|
function sourceState(db: Database, sourcePath: string) {
|
|
@@ -677,27 +708,27 @@ function sourceState(db: Database, sourcePath: string) {
|
|
|
677
708
|
|
|
678
709
|
function rebuildSearchIndex(source: Database, index: Database, sourcePath: string, state: { dataVersion: number; mtimeMs: number }) {
|
|
679
710
|
debug.time("fts:rebuild")
|
|
680
|
-
const rows = source.query<
|
|
711
|
+
const rows = source.query<SourceRow, []>(`
|
|
681
712
|
SELECT p.id, p.message_id, p.session_id, s.title AS session_title, s.directory,
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
713
|
+
json_extract(m.data, '$.role') AS role,
|
|
714
|
+
json_extract(p.data, '$.type') AS part_type,
|
|
715
|
+
p.time_created,
|
|
716
|
+
p.data
|
|
685
717
|
FROM part p
|
|
686
718
|
JOIN message m ON m.id = p.message_id
|
|
687
719
|
JOIN session s ON s.id = p.session_id
|
|
688
|
-
WHERE json_extract(p.data, '$.type')
|
|
720
|
+
WHERE json_extract(p.data, '$.type') IN ('text', 'reasoning', 'tool', 'patch')
|
|
689
721
|
AND json_extract(m.data, '$.role') IN ('user', 'assistant')
|
|
690
|
-
AND json_extract(p.data, '$.text') IS NOT NULL
|
|
691
722
|
ORDER BY p.time_created DESC
|
|
692
723
|
`).all()
|
|
693
|
-
const insert = index.query<Row, [string, string, string, string, string, string, number, string]>(`
|
|
694
|
-
INSERT INTO document_fts(id, message_id, session_id, session_title, directory, role, time_created, text)
|
|
695
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
724
|
+
const insert = index.query<Row, [string, string, string, string, string, string, SearchResult["partType"], number, string]>(`
|
|
725
|
+
INSERT INTO document_fts(id, message_id, session_id, session_title, directory, role, part_type, time_created, text)
|
|
726
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
696
727
|
`)
|
|
697
728
|
index.exec("BEGIN IMMEDIATE")
|
|
698
729
|
try {
|
|
699
730
|
index.exec("DELETE FROM document_fts")
|
|
700
|
-
for (const row of rows) {
|
|
731
|
+
for (const row of rows.flatMap(sourceRowToSearchRow)) {
|
|
701
732
|
insert.run(
|
|
702
733
|
row.id,
|
|
703
734
|
row.message_id,
|
|
@@ -705,6 +736,7 @@ function rebuildSearchIndex(source: Database, index: Database, sourcePath: strin
|
|
|
705
736
|
row.session_title ?? "Untitled session",
|
|
706
737
|
row.directory,
|
|
707
738
|
row.role,
|
|
739
|
+
row.part_type ?? "text",
|
|
708
740
|
row.time_created,
|
|
709
741
|
row.text,
|
|
710
742
|
)
|
|
@@ -712,6 +744,7 @@ function rebuildSearchIndex(source: Database, index: Database, sourcePath: strin
|
|
|
712
744
|
setMeta(index, "source_path", sourcePath)
|
|
713
745
|
setMeta(index, "source_data_version", String(state.dataVersion))
|
|
714
746
|
setMeta(index, "source_mtime_ms", String(state.mtimeMs))
|
|
747
|
+
setMeta(index, "index_version", SEARCH_INDEX_VERSION)
|
|
715
748
|
index.exec("COMMIT")
|
|
716
749
|
} catch (err) {
|
|
717
750
|
index.exec("ROLLBACK")
|
|
@@ -783,6 +816,29 @@ function extractFromValue(value: unknown): string {
|
|
|
783
816
|
.join("\n")
|
|
784
817
|
}
|
|
785
818
|
|
|
819
|
+
function sourceRowToSearchRow(row: SourceRow): Row[] {
|
|
820
|
+
const text = extractSearchText(row.data)
|
|
821
|
+
if (!text) return []
|
|
822
|
+
return [{ ...row, text }]
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
function searchablePartText(value: unknown): string {
|
|
826
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return extractFromValue(value)
|
|
827
|
+
const record = value as Record<string, unknown>
|
|
828
|
+
const type = record.type
|
|
829
|
+
|
|
830
|
+
if (type === "text" || type === "reasoning") return typeof record.text === "string" ? record.text : extractFromValue(record)
|
|
831
|
+
if (type === "patch") return extractFromValue(record.files)
|
|
832
|
+
|
|
833
|
+
if (type === "tool") {
|
|
834
|
+
const state = record.state && typeof record.state === "object" && !Array.isArray(record.state) ? record.state as Record<string, unknown> : undefined
|
|
835
|
+
if (record.tool !== "apply_patch") return ""
|
|
836
|
+
return extractFromValue(state?.input)
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
return extractFromValue(record)
|
|
840
|
+
}
|
|
841
|
+
|
|
786
842
|
function truncate(value: string, length: number) {
|
|
787
843
|
if (value.length <= length) return value
|
|
788
844
|
return `${value.slice(0, length - 3)}...`
|
package/telescope.tsx
CHANGED
|
@@ -46,14 +46,14 @@ export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; o
|
|
|
46
46
|
const [loadingPreviewMore, setLoadingPreviewMore] = createSignal(false)
|
|
47
47
|
const [prefetchingPreviewBefore, setPrefetchingPreviewBefore] = createSignal(false)
|
|
48
48
|
const [prefetchingPreviewAfter, setPrefetchingPreviewAfter] = createSignal(false)
|
|
49
|
-
const
|
|
50
|
-
const
|
|
49
|
+
const [previewEdgeLoadingReady, setPreviewEdgeLoadingReady] = createSignal(false)
|
|
50
|
+
const RESULT_BATCH_SIZE = 50
|
|
51
|
+
const RESULT_PREFETCH_AHEAD_ROWS = 25
|
|
51
52
|
const RESULT_OVERSCAN_MULTIPLIER = 2
|
|
52
|
-
const RESULT_BATCH_VIEWPORTS = 4
|
|
53
|
-
const RESULT_PREFETCH_VIEWPORTS = 10
|
|
54
53
|
const RESULT_CACHE_BEHIND_VIEWPORTS = 6
|
|
55
|
-
const INITIAL_PREVIEW_BEFORE =
|
|
56
|
-
const INITIAL_PREVIEW_AFTER =
|
|
54
|
+
const INITIAL_PREVIEW_BEFORE = 6
|
|
55
|
+
const INITIAL_PREVIEW_AFTER = 12
|
|
56
|
+
const INITIAL_PREVIEW_DELAY_MS = 75
|
|
57
57
|
const PREVIEW_PAGE_SIZE = 20
|
|
58
58
|
const PREVIEW_PREFETCH_VIEWPORTS = 0.5
|
|
59
59
|
let input: InputRenderable | undefined
|
|
@@ -91,18 +91,19 @@ export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; o
|
|
|
91
91
|
let resultPreviousTimer: ReturnType<typeof setTimeout> | undefined
|
|
92
92
|
let previewBeforeTimer: ReturnType<typeof setTimeout> | undefined
|
|
93
93
|
let previewAfterTimer: ReturnType<typeof setTimeout> | undefined
|
|
94
|
-
let pendingPreviewBefore: { previousContentHeight: number; preserveScroll: boolean; visibleLoad: boolean } | undefined
|
|
95
|
-
let
|
|
94
|
+
let pendingPreviewBefore: { itemId: string; previousContentHeight: number; preserveScroll: boolean; visibleLoad: boolean } | undefined
|
|
95
|
+
let pendingPreviewAfter: { itemId: string; visibleLoad: boolean } | undefined
|
|
96
96
|
const cancelPreviewPrefetch = () => {
|
|
97
97
|
if (previewBeforeTimer) clearTimeout(previewBeforeTimer)
|
|
98
98
|
if (previewAfterTimer) clearTimeout(previewAfterTimer)
|
|
99
99
|
previewBeforeTimer = undefined
|
|
100
100
|
previewAfterTimer = undefined
|
|
101
101
|
pendingPreviewBefore = undefined
|
|
102
|
-
|
|
102
|
+
pendingPreviewAfter = undefined
|
|
103
103
|
setPrefetchingPreviewBefore(false)
|
|
104
104
|
setPrefetchingPreviewAfter(false)
|
|
105
105
|
setLoadingPreviewMore(false)
|
|
106
|
+
setPreviewEdgeLoadingReady(false)
|
|
106
107
|
}
|
|
107
108
|
onCleanup(() => {
|
|
108
109
|
if (resultPrefetchTimer) clearTimeout(resultPrefetchTimer)
|
|
@@ -115,9 +116,9 @@ export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; o
|
|
|
115
116
|
const viewportHeight = resultScroll?.height || Math.max(8, height() - 7)
|
|
116
117
|
return Math.max(5, Math.floor(viewportHeight / resultRowHeight()))
|
|
117
118
|
}
|
|
118
|
-
const searchBatchSize = () =>
|
|
119
|
-
const recentBatchSize = () =>
|
|
120
|
-
const resultPrefetchThreshold = () =>
|
|
119
|
+
const searchBatchSize = () => RESULT_BATCH_SIZE
|
|
120
|
+
const recentBatchSize = () => RESULT_BATCH_SIZE
|
|
121
|
+
const resultPrefetchThreshold = () => RESULT_PREFETCH_AHEAD_ROWS
|
|
121
122
|
const resultPrefetchState = () => {
|
|
122
123
|
const cachedEnd = resultBaseOffset() + results().length
|
|
123
124
|
const rowsAhead = Math.max(0, cachedEnd - selected() - 1)
|
|
@@ -437,14 +438,20 @@ export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; o
|
|
|
437
438
|
return lastChild ? lastChild.y + lastChild.height : 0
|
|
438
439
|
}
|
|
439
440
|
|
|
440
|
-
const
|
|
441
|
+
const resetPreviewScroll = () => {
|
|
442
|
+
if (!previewScroll || previewScroll.y === 0) return
|
|
443
|
+
previewScroll.scrollBy(-previewScroll.y)
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const loadPreviewBefore = (itemId: string, previousContentHeight = previewContentHeight(), preserveScroll = true, visibleLoad = false) => {
|
|
441
447
|
const item = selectedResult()
|
|
442
448
|
const first = previewParts()[0]
|
|
443
|
-
if (!item || !first || loadingPreviewMore() || prefetchingPreviewBefore()) return
|
|
449
|
+
if (!item || item.id !== itemId || !first || loadingPreviewMore() || prefetchingPreviewBefore()) return
|
|
444
450
|
visibleLoad ? setLoadingPreviewMore(true) : setPrefetchingPreviewBefore(true)
|
|
445
451
|
debug.time("preview:load-before")
|
|
446
452
|
try {
|
|
447
453
|
const page = loadConversationBefore(item, { id: first.id, timeCreated: first.timeCreated }, { limit: PREVIEW_PAGE_SIZE, dbPath: dbPath() })
|
|
454
|
+
if (selectedResult()?.id !== itemId) return
|
|
448
455
|
debug.log("preview:load-before", {
|
|
449
456
|
item: item.id,
|
|
450
457
|
added: page.parts.length,
|
|
@@ -458,6 +465,7 @@ export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; o
|
|
|
458
465
|
setPreviewParts((prev) => [...page.parts, ...prev])
|
|
459
466
|
if (preserveScroll) {
|
|
460
467
|
setTimeout(() => {
|
|
468
|
+
if (selectedResult()?.id !== itemId) return
|
|
461
469
|
const delta = previewContentHeight() - previousContentHeight
|
|
462
470
|
if (delta > 0) previewScroll?.scrollBy(delta)
|
|
463
471
|
}, 1)
|
|
@@ -472,14 +480,15 @@ export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; o
|
|
|
472
480
|
}
|
|
473
481
|
}
|
|
474
482
|
|
|
475
|
-
const loadPreviewAfter = (visibleLoad = false) => {
|
|
483
|
+
const loadPreviewAfter = (itemId: string, visibleLoad = false) => {
|
|
476
484
|
const item = selectedResult()
|
|
477
485
|
const last = previewParts().at(-1)
|
|
478
|
-
if (!item || !last || loadingPreviewMore() || prefetchingPreviewAfter()) return
|
|
486
|
+
if (!item || item.id !== itemId || !last || loadingPreviewMore() || prefetchingPreviewAfter()) return
|
|
479
487
|
visibleLoad ? setLoadingPreviewMore(true) : setPrefetchingPreviewAfter(true)
|
|
480
488
|
debug.time("preview:load-after")
|
|
481
489
|
try {
|
|
482
490
|
const page = loadConversationAfter(item, { id: last.id, timeCreated: last.timeCreated }, { limit: PREVIEW_PAGE_SIZE, dbPath: dbPath() })
|
|
491
|
+
if (selectedResult()?.id !== itemId) return
|
|
483
492
|
debug.log("preview:load-after", {
|
|
484
493
|
item: item.id,
|
|
485
494
|
added: page.parts.length,
|
|
@@ -499,7 +508,8 @@ export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; o
|
|
|
499
508
|
}
|
|
500
509
|
|
|
501
510
|
const schedulePreviewBefore = (previousContentHeight = previewContentHeight(), preserveScroll = true, visibleLoad = false) => {
|
|
502
|
-
|
|
511
|
+
const item = selectedResult()
|
|
512
|
+
if (!item || !hasMorePreviewBefore() || loadingPreviewMore() || prefetchingPreviewBefore()) return
|
|
503
513
|
if (previewBeforeTimer) {
|
|
504
514
|
if (pendingPreviewBefore) {
|
|
505
515
|
pendingPreviewBefore.preserveScroll = pendingPreviewBefore.preserveScroll && preserveScroll
|
|
@@ -507,26 +517,27 @@ export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; o
|
|
|
507
517
|
}
|
|
508
518
|
return
|
|
509
519
|
}
|
|
510
|
-
pendingPreviewBefore = { previousContentHeight, preserveScroll, visibleLoad }
|
|
511
|
-
debug.log("preview:prefetch-before-scheduled", { preserveScroll, visibleLoad })
|
|
520
|
+
pendingPreviewBefore = { itemId: item.id, previousContentHeight, preserveScroll, visibleLoad }
|
|
521
|
+
debug.log("preview:prefetch-before-scheduled", { item: item.id, preserveScroll, visibleLoad })
|
|
512
522
|
previewBeforeTimer = setTimeout(() => {
|
|
513
523
|
const pending = pendingPreviewBefore
|
|
514
524
|
previewBeforeTimer = undefined
|
|
515
525
|
pendingPreviewBefore = undefined
|
|
516
|
-
if (pending) loadPreviewBefore(pending.previousContentHeight, pending.preserveScroll, pending.visibleLoad)
|
|
526
|
+
if (pending && selectedResult()?.id === pending.itemId) loadPreviewBefore(pending.itemId, pending.previousContentHeight, pending.preserveScroll, pending.visibleLoad)
|
|
517
527
|
}, 1)
|
|
518
528
|
}
|
|
519
529
|
|
|
520
530
|
const schedulePreviewAfter = (visibleLoad = false) => {
|
|
521
|
-
|
|
522
|
-
|
|
531
|
+
const item = selectedResult()
|
|
532
|
+
if (!item || !hasMorePreviewAfter() || loadingPreviewMore() || prefetchingPreviewAfter()) return
|
|
533
|
+
pendingPreviewAfter = { itemId: item.id, visibleLoad: (pendingPreviewAfter?.visibleLoad || visibleLoad) }
|
|
523
534
|
if (previewAfterTimer) return
|
|
524
|
-
debug.log("preview:prefetch-after-scheduled", { visibleLoad })
|
|
535
|
+
debug.log("preview:prefetch-after-scheduled", { item: item.id, visibleLoad })
|
|
525
536
|
previewAfterTimer = setTimeout(() => {
|
|
526
|
-
const
|
|
537
|
+
const pending = pendingPreviewAfter
|
|
527
538
|
previewAfterTimer = undefined
|
|
528
|
-
|
|
529
|
-
loadPreviewAfter(
|
|
539
|
+
pendingPreviewAfter = undefined
|
|
540
|
+
if (pending && selectedResult()?.id === pending.itemId) loadPreviewAfter(pending.itemId, pending.visibleLoad)
|
|
530
541
|
}, 1)
|
|
531
542
|
}
|
|
532
543
|
|
|
@@ -543,33 +554,62 @@ export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; o
|
|
|
543
554
|
if (item.id === lastPreviewItemId) return
|
|
544
555
|
lastPreviewItemId = item.id
|
|
545
556
|
cancelPreviewPrefetch()
|
|
557
|
+
resetPreviewScroll()
|
|
558
|
+
solidBatch(() => {
|
|
559
|
+
setPreviewParts([])
|
|
560
|
+
setHasMorePreviewBefore(false)
|
|
561
|
+
setHasMorePreviewAfter(false)
|
|
562
|
+
setPreviewEdgeLoadingReady(false)
|
|
563
|
+
})
|
|
546
564
|
debug.log("preview:new-item", item.sessionTitle?.slice(0, 40) ?? item.id.slice(-8))
|
|
547
565
|
const db = dbPath()
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
const page = loadConversationAround(item, { before: INITIAL_PREVIEW_BEFORE, after: INITIAL_PREVIEW_AFTER, dbPath: db })
|
|
551
|
-
debug.log("preview:init", {
|
|
552
|
-
item: item.id,
|
|
553
|
-
session: item.sessionID,
|
|
554
|
-
parts: page.parts.length,
|
|
555
|
-
hasMoreBefore: page.hasMoreBefore,
|
|
556
|
-
hasMoreAfter: page.hasMoreAfter,
|
|
557
|
-
first: page.parts[0]?.id,
|
|
558
|
-
last: page.parts.at(-1)?.id,
|
|
559
|
-
})
|
|
560
|
-
setPreviewParts(page.parts)
|
|
561
|
-
setHasMorePreviewBefore(page.hasMoreBefore)
|
|
562
|
-
setHasMorePreviewAfter(page.hasMoreAfter)
|
|
563
|
-
} catch {}
|
|
566
|
+
const itemId = item.id
|
|
567
|
+
debug.log("preview:schedule", { item: item.id, delayMs: INITIAL_PREVIEW_DELAY_MS, before: INITIAL_PREVIEW_BEFORE, after: INITIAL_PREVIEW_AFTER })
|
|
564
568
|
debug.timeEnd("nav:total")
|
|
565
|
-
|
|
569
|
+
const timer = setTimeout(() => {
|
|
570
|
+
if (selectedResult()?.id !== itemId) {
|
|
571
|
+
debug.log("preview:load:cancelled", { item: itemId })
|
|
572
|
+
return
|
|
573
|
+
}
|
|
574
|
+
debug.time("preview:load")
|
|
575
|
+
try {
|
|
576
|
+
const page = loadConversationAround(item, { before: INITIAL_PREVIEW_BEFORE, after: INITIAL_PREVIEW_AFTER, dbPath: db })
|
|
577
|
+
if (selectedResult()?.id !== itemId) {
|
|
578
|
+
debug.log("preview:load:cancelled", { item: itemId, afterLoad: true })
|
|
579
|
+
return
|
|
580
|
+
}
|
|
581
|
+
debug.log("preview:init", {
|
|
582
|
+
item: item.id,
|
|
583
|
+
session: item.sessionID,
|
|
584
|
+
parts: page.parts.length,
|
|
585
|
+
hasMoreBefore: page.hasMoreBefore,
|
|
586
|
+
hasMoreAfter: page.hasMoreAfter,
|
|
587
|
+
first: page.parts[0]?.id,
|
|
588
|
+
last: page.parts.at(-1)?.id,
|
|
589
|
+
})
|
|
590
|
+
solidBatch(() => {
|
|
591
|
+
setPreviewParts(page.parts)
|
|
592
|
+
setHasMorePreviewBefore(page.hasMoreBefore)
|
|
593
|
+
setHasMorePreviewAfter(page.hasMoreAfter)
|
|
594
|
+
})
|
|
595
|
+
} catch {
|
|
596
|
+
} finally {
|
|
597
|
+
debug.timeEnd("preview:load")
|
|
598
|
+
}
|
|
599
|
+
}, INITIAL_PREVIEW_DELAY_MS)
|
|
600
|
+
onCleanup(() => {
|
|
601
|
+
clearTimeout(timer)
|
|
602
|
+
if (selectedResult()?.id !== itemId) {
|
|
603
|
+
debug.log("preview:load:cancelled", { item: itemId, cleanup: true })
|
|
604
|
+
}
|
|
605
|
+
})
|
|
566
606
|
})
|
|
567
607
|
|
|
568
608
|
createEffect(() => {
|
|
569
609
|
const item = selectedResult()
|
|
570
610
|
if (!item) return
|
|
571
611
|
const interval = setInterval(() => {
|
|
572
|
-
if (loadingPreviewMore() || prefetchingPreviewBefore() || prefetchingPreviewAfter()) return
|
|
612
|
+
if (!previewEdgeLoadingReady() || loadingPreviewMore() || prefetchingPreviewBefore() || prefetchingPreviewAfter()) return
|
|
573
613
|
const scroll = previewScroll
|
|
574
614
|
const children = scroll?.getChildren()
|
|
575
615
|
if (!scroll || !children || children.length === 0) return
|
|
@@ -610,8 +650,19 @@ export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; o
|
|
|
610
650
|
if (!item) return
|
|
611
651
|
if (item.id === scrolledItem) return
|
|
612
652
|
scrolledItem = item.id
|
|
613
|
-
const
|
|
614
|
-
|
|
653
|
+
const itemId = item.id
|
|
654
|
+
let readyTimer: ReturnType<typeof setTimeout> | undefined
|
|
655
|
+
const timer = setTimeout(() => {
|
|
656
|
+
if (selectedResult()?.id !== itemId) return
|
|
657
|
+
scrollPreviewToTarget(previewScroll, messageTargetID(item))
|
|
658
|
+
readyTimer = setTimeout(() => {
|
|
659
|
+
if (selectedResult()?.id === itemId) setPreviewEdgeLoadingReady(true)
|
|
660
|
+
}, 100)
|
|
661
|
+
}, 1)
|
|
662
|
+
onCleanup(() => {
|
|
663
|
+
clearTimeout(timer)
|
|
664
|
+
if (readyTimer) clearTimeout(readyTimer)
|
|
665
|
+
})
|
|
615
666
|
})
|
|
616
667
|
|
|
617
668
|
const open = () => {
|
package/ui/render-target.ts
CHANGED
|
@@ -6,6 +6,8 @@ export function previewScrollAmount(scroll: ScrollBoxRenderable | undefined) {
|
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
export function messageTargetID(item: SearchResult) {
|
|
9
|
+
if (item.partType === "tool") return `tool-inline-${item.messageID}-${item.id}`
|
|
10
|
+
if (item.partType === "reasoning") return `text-${item.messageID}-${item.id}`
|
|
9
11
|
if (item.role === "assistant") return `text-${item.messageID}-${item.id}`
|
|
10
12
|
return item.messageID
|
|
11
13
|
}
|