@bojackduy/opencode-telescope 0.1.19 → 0.1.21
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 +26 -4
- package/telescope.tsx +299 -86
- package/tui.tsx +6 -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.21",
|
|
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
|
@@ -631,12 +631,31 @@ function indexedTextRows(db: Database, dbPath: string, limit: number, query: str
|
|
|
631
631
|
|
|
632
632
|
function indexedRecentRows(db: Database, dbPath: string, limit: number, directory?: string, offset?: number, role?: SearchRole) {
|
|
633
633
|
try {
|
|
634
|
-
const index = ensureSearchIndex(db, dbPath, { rebuild: false })
|
|
634
|
+
const index = ensureSearchIndex(db, dbPath, { rebuild: false, useStale: true })
|
|
635
635
|
if (!index) {
|
|
636
|
-
|
|
636
|
+
debug.log("query:recent:index:missing", { dbPath })
|
|
637
637
|
return
|
|
638
638
|
}
|
|
639
639
|
|
|
640
|
+
const rowCount = index.query<{ count: number }, []>("SELECT COUNT(*) as count FROM document_index").get()?.count ?? 0
|
|
641
|
+
if (rowCount === 0) {
|
|
642
|
+
debug.log("query:recent:index:empty", { dbPath })
|
|
643
|
+
return
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
const state = sourceState(db, dbPath)
|
|
647
|
+
const currentDataVersion = getMeta(index, "source_data_version")
|
|
648
|
+
const currentMtimeMs = getMeta(index, "source_mtime_ms")
|
|
649
|
+
const currentPath = getMeta(index, "source_path")
|
|
650
|
+
const currentIndexVersion = getMeta(index, "index_version")
|
|
651
|
+
if (currentPath !== dbPath || currentDataVersion !== String(state.dataVersion) || currentMtimeMs !== String(state.mtimeMs) || currentIndexVersion !== SEARCH_INDEX_VERSION) {
|
|
652
|
+
debug.log("query:recent:index:stale", {
|
|
653
|
+
dbPath,
|
|
654
|
+
expectedDataVersion: state.dataVersion,
|
|
655
|
+
actualDataVersion: currentDataVersion,
|
|
656
|
+
})
|
|
657
|
+
}
|
|
658
|
+
|
|
640
659
|
const conditions: string[] = ["part_type = 'text'"]
|
|
641
660
|
const params: (string | number)[] = []
|
|
642
661
|
if (role) {
|
|
@@ -711,7 +730,7 @@ function visibleTextRows(db: Database, limit: number, query?: string, directory?
|
|
|
711
730
|
return rows
|
|
712
731
|
}
|
|
713
732
|
|
|
714
|
-
function ensureSearchIndex(source: Database, sourcePath: string, options?: { rebuild?: boolean }) {
|
|
733
|
+
function ensureSearchIndex(source: Database, sourcePath: string, options?: { rebuild?: boolean; useStale?: boolean }) {
|
|
715
734
|
const indexPath = searchIndexPath(sourcePath)
|
|
716
735
|
if (!_indexDb || _indexDbPath !== indexPath) {
|
|
717
736
|
_indexDb?.close()
|
|
@@ -726,7 +745,10 @@ function ensureSearchIndex(source: Database, sourcePath: string, options?: { reb
|
|
|
726
745
|
const currentPath = getMeta(_indexDb, "source_path")
|
|
727
746
|
const currentIndexVersion = getMeta(_indexDb, "index_version")
|
|
728
747
|
if (currentPath !== sourcePath || currentDataVersion !== String(state.dataVersion) || currentMtimeMs !== String(state.mtimeMs) || currentIndexVersion !== SEARCH_INDEX_VERSION) {
|
|
729
|
-
if (options?.rebuild === false)
|
|
748
|
+
if (options?.rebuild === false) {
|
|
749
|
+
if (options?.useStale) return _indexDb
|
|
750
|
+
return
|
|
751
|
+
}
|
|
730
752
|
rebuildSearchIndex(source, _indexDb, sourcePath, state)
|
|
731
753
|
}
|
|
732
754
|
return _indexDb
|
package/telescope.tsx
CHANGED
|
@@ -23,6 +23,7 @@ import { inputSafeKeys, keyListLabel, matchesKey, prevent } from "./ui/keyboard.
|
|
|
23
23
|
import { findRenderableByID, jumpToRenderedTarget, messageTargetID, previewScrollAmount, scrollPreviewToTarget } from "./ui/render-target.ts"
|
|
24
24
|
|
|
25
25
|
export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; onClose: () => void }) => {
|
|
26
|
+
debug.log("component:render:start")
|
|
26
27
|
type OwnerFilter = "all" | SearchRole
|
|
27
28
|
const dimensions = useTerminalDimensions()
|
|
28
29
|
const [query, setQuery] = createSignal("")
|
|
@@ -52,7 +53,7 @@ export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; o
|
|
|
52
53
|
const MIN_RECENT_BATCH_SIZE = 15
|
|
53
54
|
const RESULT_OVERSCAN_MULTIPLIER = 2
|
|
54
55
|
const RESULT_BATCH_VIEWPORTS = 4
|
|
55
|
-
const RESULT_PREFETCH_VIEWPORTS =
|
|
56
|
+
const RESULT_PREFETCH_VIEWPORTS = 5
|
|
56
57
|
const RESULT_CACHE_BEHIND_VIEWPORTS = 6
|
|
57
58
|
const INITIAL_PREVIEW_BEFORE = 20
|
|
58
59
|
const INITIAL_PREVIEW_AFTER = 30
|
|
@@ -92,10 +93,23 @@ export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; o
|
|
|
92
93
|
let resultNavigationStarted = false
|
|
93
94
|
let resultPrefetchTimer: ReturnType<typeof setTimeout> | undefined
|
|
94
95
|
let resultPreviousTimer: ReturnType<typeof setTimeout> | undefined
|
|
96
|
+
type PreviewBeforeIntent = "passive-prefetch" | "explicit-scroll"
|
|
97
|
+
type PendingPreviewBefore = {
|
|
98
|
+
id: number
|
|
99
|
+
previousContentHeight: number
|
|
100
|
+
previousScrollTop: number
|
|
101
|
+
requestedAmount: number
|
|
102
|
+
intent: PreviewBeforeIntent
|
|
103
|
+
visibleLoad: boolean
|
|
104
|
+
}
|
|
105
|
+
let previewBeforeLoadID = 0
|
|
95
106
|
let previewBeforeTimer: ReturnType<typeof setTimeout> | undefined
|
|
96
107
|
let previewAfterTimer: ReturnType<typeof setTimeout> | undefined
|
|
97
|
-
let pendingPreviewBefore:
|
|
108
|
+
let pendingPreviewBefore: PendingPreviewBefore | undefined
|
|
98
109
|
let pendingPreviewAfterVisible = false
|
|
110
|
+
let previewBeforeAdjusting = false
|
|
111
|
+
let pendingCoalescedExplicit: { requestedAmount: number } | undefined
|
|
112
|
+
let pendingPreviewAnchorCorrection: { anchorPartID: string; visualOffset: number; createdAt: number; expiresAt: number } | undefined
|
|
99
113
|
const previewMeasuredHeights = new Map<string, number>()
|
|
100
114
|
const cancelPreviewPrefetch = () => {
|
|
101
115
|
if (previewBeforeTimer) clearTimeout(previewBeforeTimer)
|
|
@@ -104,6 +118,9 @@ export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; o
|
|
|
104
118
|
previewAfterTimer = undefined
|
|
105
119
|
pendingPreviewBefore = undefined
|
|
106
120
|
pendingPreviewAfterVisible = false
|
|
121
|
+
previewBeforeAdjusting = false
|
|
122
|
+
pendingCoalescedExplicit = undefined
|
|
123
|
+
pendingPreviewAnchorCorrection = undefined
|
|
107
124
|
setPrefetchingPreviewBefore(false)
|
|
108
125
|
setPrefetchingPreviewAfter(false)
|
|
109
126
|
setLoadingPreviewMore(false)
|
|
@@ -150,6 +167,12 @@ export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; o
|
|
|
150
167
|
return { start: base + cachedStart, end: base + cachedEnd, items: cached.slice(cachedStart, cachedEnd) }
|
|
151
168
|
})
|
|
152
169
|
|
|
170
|
+
const resultTopSpacerHeight = () =>
|
|
171
|
+
Math.max(0, resultRenderWindow().start - resultBaseOffset()) * resultRowHeight()
|
|
172
|
+
|
|
173
|
+
const resultBottomSpacerHeight = () =>
|
|
174
|
+
Math.max(0, resultBaseOffset() + results().length - resultRenderWindow().end) * resultRowHeight()
|
|
175
|
+
|
|
153
176
|
let lastResultRenderWindow = ""
|
|
154
177
|
createEffect(() => {
|
|
155
178
|
const window = resultRenderWindow()
|
|
@@ -217,6 +240,7 @@ export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; o
|
|
|
217
240
|
}
|
|
218
241
|
debug.timeEnd("query:recent")
|
|
219
242
|
setLoading(false)
|
|
243
|
+
debug.log("component:interactive")
|
|
220
244
|
}, 1)
|
|
221
245
|
onCleanup(() => clearTimeout(timer))
|
|
222
246
|
return
|
|
@@ -444,6 +468,31 @@ export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; o
|
|
|
444
468
|
onCleanup(() => clearTimeout(timer))
|
|
445
469
|
})
|
|
446
470
|
|
|
471
|
+
createEffect(() => {
|
|
472
|
+
if (!resultNavigationStarted || !hasMore()) return
|
|
473
|
+
const interval = setInterval(() => {
|
|
474
|
+
if (!hasMore() || loadingMore() || loadingPreviousResults() || prefetchingResults() || busy() || loading()) return
|
|
475
|
+
const scroll = resultScroll
|
|
476
|
+
if (!scroll) return
|
|
477
|
+
const children = scroll.getChildren()
|
|
478
|
+
if (!children || children.length === 0) return
|
|
479
|
+
const lastChild = children[children.length - 1] as { y: number; height: number }
|
|
480
|
+
const contentHeight = lastChild.y + lastChild.height
|
|
481
|
+
const scrollThreshold = Math.max(2, Math.floor(scroll.height * RESULT_PREFETCH_VIEWPORTS))
|
|
482
|
+
if (scroll.scrollTop + scroll.height >= contentHeight - scrollThreshold) {
|
|
483
|
+
debug.log("results:scroll-prefetch", {
|
|
484
|
+
scrollTop: scroll.scrollTop,
|
|
485
|
+
height: scroll.height,
|
|
486
|
+
contentHeight,
|
|
487
|
+
scrollThreshold,
|
|
488
|
+
rowsAhead: resultPrefetchState().rowsAhead,
|
|
489
|
+
})
|
|
490
|
+
scheduleResultPrefetch(false)
|
|
491
|
+
}
|
|
492
|
+
}, 200)
|
|
493
|
+
onCleanup(() => clearInterval(interval))
|
|
494
|
+
})
|
|
495
|
+
|
|
447
496
|
const estimatePreviewPartHeight = (part: ConversationPreviewPart) => {
|
|
448
497
|
if (part.type === "tool") {
|
|
449
498
|
if (!part.target) return 2
|
|
@@ -457,15 +506,31 @@ export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; o
|
|
|
457
506
|
|
|
458
507
|
const previewPartHeight = (part: ConversationPreviewPart) => previewMeasuredHeights.get(part.id) ?? estimatePreviewPartHeight(part)
|
|
459
508
|
|
|
460
|
-
const
|
|
461
|
-
previewHeightVersion()
|
|
509
|
+
const buildPreviewLayout = (parts: ConversationPreviewPart[]) => {
|
|
462
510
|
let offset = 0
|
|
463
|
-
return
|
|
511
|
+
return parts.map((part) => {
|
|
464
512
|
const height = previewPartHeight(part)
|
|
465
513
|
const row = { part, top: offset, bottom: offset + height, height }
|
|
466
514
|
offset += height
|
|
467
515
|
return row
|
|
468
516
|
})
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const previewWindowForLayout = (layout: ReturnType<typeof buildPreviewLayout>, scrollTop: number, viewportHeight: number) => {
|
|
520
|
+
if (layout.length === 0) return { start: 0, end: 0 }
|
|
521
|
+
const overscan = Math.max(viewportHeight * 2, 40)
|
|
522
|
+
const from = Math.max(0, scrollTop - overscan)
|
|
523
|
+
const to = scrollTop + viewportHeight + overscan
|
|
524
|
+
const foundStart = layout.findIndex((row) => row.bottom >= from)
|
|
525
|
+
const start = foundStart === -1 ? Math.max(0, layout.length - 1) : Math.max(0, foundStart)
|
|
526
|
+
const foundEnd = layout.findIndex((row) => row.top > to)
|
|
527
|
+
const end = foundEnd === -1 ? layout.length : foundEnd
|
|
528
|
+
return { start, end: Math.min(layout.length, Math.max(start + 1, end)) }
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
const previewVirtualLayout = createMemo(() => {
|
|
532
|
+
previewHeightVersion()
|
|
533
|
+
return buildPreviewLayout(previewParts())
|
|
469
534
|
})
|
|
470
535
|
|
|
471
536
|
const previewContentHeight = () => previewVirtualLayout().at(-1)?.bottom ?? 0
|
|
@@ -484,14 +549,7 @@ export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; o
|
|
|
484
549
|
return
|
|
485
550
|
}
|
|
486
551
|
|
|
487
|
-
const
|
|
488
|
-
const from = Math.max(0, scroll.scrollTop - overscan)
|
|
489
|
-
const to = scroll.scrollTop + scroll.height + overscan
|
|
490
|
-
const foundStart = layout.findIndex((row) => row.bottom >= from)
|
|
491
|
-
const start = foundStart === -1 ? Math.max(0, layout.length - 1) : Math.max(0, foundStart)
|
|
492
|
-
const foundEnd = layout.findIndex((row) => row.top > to)
|
|
493
|
-
const end = foundEnd === -1 ? layout.length : foundEnd
|
|
494
|
-
const next = { start, end: Math.min(layout.length, Math.max(start + 1, end)) }
|
|
552
|
+
const next = previewWindowForLayout(layout, scroll.scrollTop, scroll.height)
|
|
495
553
|
const current = previewWindow()
|
|
496
554
|
if (current.start !== next.start || current.end !== next.end) setPreviewWindow(next)
|
|
497
555
|
}
|
|
@@ -556,6 +614,29 @@ export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; o
|
|
|
556
614
|
totalMeasured: previewMeasuredHeights.size,
|
|
557
615
|
window: previewWindow(),
|
|
558
616
|
})
|
|
617
|
+
const anchorCorrection = pendingPreviewAnchorCorrection
|
|
618
|
+
if (anchorCorrection && scroll) {
|
|
619
|
+
const now = Date.now()
|
|
620
|
+
const shouldCorrect = now <= anchorCorrection.expiresAt && lastPreviewScrollKeyMs <= anchorCorrection.createdAt
|
|
621
|
+
if (shouldCorrect) {
|
|
622
|
+
const nextLayout = buildPreviewLayout(previewParts())
|
|
623
|
+
const anchor = nextLayout.find((row) => row.part.id === anchorCorrection.anchorPartID)
|
|
624
|
+
if (anchor) {
|
|
625
|
+
const correctedScrollTop = Math.max(0, anchor.top + anchorCorrection.visualOffset)
|
|
626
|
+
const correctedWindow = previewWindowForLayout(nextLayout, correctedScrollTop, scroll.height)
|
|
627
|
+
setPreviewWindow(correctedWindow)
|
|
628
|
+
scroll.scrollTo(correctedScrollTop)
|
|
629
|
+
debug.log("preview:measure:anchor-correct", {
|
|
630
|
+
anchorPartID: anchorCorrection.anchorPartID,
|
|
631
|
+
visualOffset: anchorCorrection.visualOffset,
|
|
632
|
+
correctedScrollTop,
|
|
633
|
+
window: correctedWindow,
|
|
634
|
+
})
|
|
635
|
+
}
|
|
636
|
+
} else {
|
|
637
|
+
pendingPreviewAnchorCorrection = undefined
|
|
638
|
+
}
|
|
639
|
+
}
|
|
559
640
|
setPreviewHeightVersion((value) => value + 1)
|
|
560
641
|
setTimeout(updatePreviewWindow, 1)
|
|
561
642
|
}
|
|
@@ -574,75 +655,139 @@ export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; o
|
|
|
574
655
|
onCleanup(() => clearTimeout(timer))
|
|
575
656
|
})
|
|
576
657
|
|
|
577
|
-
const loadPreviewBefore = (
|
|
658
|
+
const loadPreviewBefore = (pending: PendingPreviewBefore) => {
|
|
578
659
|
const item = selectedResult()
|
|
579
660
|
const first = previewParts()[0]
|
|
580
661
|
if (!item || !first || loadingPreviewMore() || prefetchingPreviewBefore()) {
|
|
581
662
|
debug.log("preview:load-before:skip", {
|
|
663
|
+
loadID: pending.id,
|
|
582
664
|
reason: !item ? "no-item" : !first ? "no-first-part" : loadingPreviewMore() ? "loading-preview-more" : "prefetching-before",
|
|
583
|
-
|
|
584
|
-
preserveScroll,
|
|
585
|
-
visibleLoad,
|
|
665
|
+
pending,
|
|
586
666
|
state: previewScrollState(),
|
|
587
667
|
})
|
|
588
668
|
return
|
|
589
669
|
}
|
|
590
670
|
const beforeState = previewScrollState()
|
|
591
|
-
|
|
671
|
+
const beforeLayout = previewVirtualLayout()
|
|
672
|
+
const scrollTop = previewScroll?.scrollTop ?? pending.previousScrollTop
|
|
673
|
+
const anchor = beforeLayout.find((row) => row.bottom >= scrollTop) ?? beforeLayout[0]
|
|
674
|
+
const anchorPartID = anchor?.part.id ?? first.id
|
|
675
|
+
const offsetInAnchor = anchor ? scrollTop - anchor.top : 0
|
|
676
|
+
pending.visibleLoad ? setLoadingPreviewMore(true) : setPrefetchingPreviewBefore(true)
|
|
592
677
|
debug.time("preview:load-before")
|
|
593
678
|
try {
|
|
594
679
|
debug.log("preview:load-before:start", {
|
|
595
|
-
|
|
680
|
+
loadID: pending.id,
|
|
681
|
+
intent: pending.intent,
|
|
596
682
|
cursor: { id: first.id, timeCreated: first.timeCreated },
|
|
597
|
-
previousContentHeight,
|
|
598
|
-
|
|
599
|
-
|
|
683
|
+
previousContentHeight: pending.previousContentHeight,
|
|
684
|
+
previousScrollTop: pending.previousScrollTop,
|
|
685
|
+
anchorPartID,
|
|
686
|
+
offsetInAnchor,
|
|
687
|
+
requestedAmount: pending.requestedAmount,
|
|
688
|
+
visibleLoad: pending.visibleLoad,
|
|
600
689
|
state: beforeState,
|
|
601
690
|
})
|
|
602
691
|
const page = loadConversationBefore(item, { id: first.id, timeCreated: first.timeCreated }, { limit: PREVIEW_PAGE_SIZE, dbPath: dbPath() })
|
|
603
|
-
debug.log("preview:load-before", {
|
|
604
|
-
|
|
692
|
+
debug.log("preview:load-before:query-done", {
|
|
693
|
+
loadID: pending.id,
|
|
605
694
|
added: page.parts.length,
|
|
606
695
|
hasMoreBefore: page.hasMoreBefore,
|
|
607
|
-
preserveScroll,
|
|
608
|
-
visibleLoad,
|
|
609
696
|
first: page.parts[0]?.id,
|
|
610
697
|
last: page.parts.at(-1)?.id,
|
|
611
698
|
})
|
|
612
699
|
if (page.parts.length > 0) {
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
700
|
+
const currentParts = previewParts()
|
|
701
|
+
const nextParts = [...page.parts, ...currentParts]
|
|
702
|
+
const nextLayout = buildPreviewLayout(nextParts)
|
|
703
|
+
const anchorPredicted = nextLayout.find((row) => row.part.id === anchorPartID)
|
|
704
|
+
const anchorPredictedTop = anchorPredicted?.top ?? 0
|
|
705
|
+
const visualOffset = offsetInAnchor - (pending.intent === "explicit-scroll" ? pending.requestedAmount : 0)
|
|
706
|
+
const targetScrollTop = pending.intent === "explicit-scroll"
|
|
707
|
+
? Math.max(0, anchorPredictedTop + offsetInAnchor - pending.requestedAmount)
|
|
708
|
+
: anchorPredictedTop + offsetInAnchor
|
|
709
|
+
const nextWindow = previewWindowForLayout(nextLayout, targetScrollTop, previewScroll?.height ?? Math.max(8, height() - 7))
|
|
710
|
+
|
|
711
|
+
solidBatch(() => {
|
|
712
|
+
setPreviewParts(nextParts)
|
|
713
|
+
setPreviewWindow(nextWindow)
|
|
714
|
+
})
|
|
715
|
+
|
|
716
|
+
previewScroll?.scrollTo(targetScrollTop)
|
|
717
|
+
const anchorCorrectionCreatedAt = Date.now()
|
|
718
|
+
pendingPreviewAnchorCorrection = {
|
|
719
|
+
anchorPartID,
|
|
720
|
+
visualOffset,
|
|
721
|
+
createdAt: anchorCorrectionCreatedAt,
|
|
722
|
+
expiresAt: anchorCorrectionCreatedAt + 300,
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
debug.log("preview:load-before:commit", {
|
|
726
|
+
loadID: pending.id,
|
|
727
|
+
anchorPartID,
|
|
728
|
+
anchorPredictedTop,
|
|
729
|
+
offsetInAnchor,
|
|
730
|
+
visualOffset,
|
|
731
|
+
targetScrollTop,
|
|
732
|
+
window: nextWindow,
|
|
733
|
+
intent: pending.intent,
|
|
734
|
+
newParts: page.parts.length,
|
|
735
|
+
totalParts: nextParts.length,
|
|
736
|
+
})
|
|
737
|
+
|
|
738
|
+
previewBeforeAdjusting = true
|
|
739
|
+
setTimeout(() => {
|
|
740
|
+
const newLayout = previewVirtualLayout()
|
|
741
|
+
const anchorNew = newLayout.find(e => e.part.id === anchorPartID)
|
|
742
|
+
const actualAnchorTop = anchorNew?.top ?? anchorPredictedTop
|
|
743
|
+
const drift = actualAnchorTop - anchorPredictedTop
|
|
744
|
+
|
|
745
|
+
if (Math.abs(drift) >= 1 && previewScroll) {
|
|
746
|
+
const correctedScrollTop = targetScrollTop + drift
|
|
747
|
+
const correctedWindow = previewWindowForLayout(newLayout, correctedScrollTop, previewScroll.height)
|
|
748
|
+
setPreviewWindow(correctedWindow)
|
|
749
|
+
previewScroll.scrollTo(correctedScrollTop)
|
|
750
|
+
debug.log("preview:load-before:drift-correct", {
|
|
751
|
+
loadID: pending.id,
|
|
752
|
+
drift,
|
|
753
|
+
anchorPredictedTop,
|
|
754
|
+
actualAnchorTop,
|
|
755
|
+
correctedScrollTop,
|
|
756
|
+
window: correctedWindow,
|
|
627
757
|
})
|
|
628
|
-
}
|
|
629
|
-
} else {
|
|
630
|
-
setTimeout(() => {
|
|
758
|
+
} else {
|
|
631
759
|
updatePreviewWindow()
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
previewBeforeAdjusting = false
|
|
763
|
+
|
|
764
|
+
const coalesced = pendingCoalescedExplicit
|
|
765
|
+
if (coalesced) {
|
|
766
|
+
pendingCoalescedExplicit = undefined
|
|
767
|
+
const scroll = previewScroll
|
|
768
|
+
if (scroll) {
|
|
769
|
+
debug.log("preview:before:coalesce-replay", {
|
|
770
|
+
requestedAmount: coalesced.requestedAmount,
|
|
771
|
+
state: previewScrollState(),
|
|
772
|
+
})
|
|
773
|
+
schedulePreviewBefore({
|
|
774
|
+
intent: "explicit-scroll",
|
|
775
|
+
visibleLoad: true,
|
|
776
|
+
requestedAmount: coalesced.requestedAmount,
|
|
777
|
+
previousScrollTop: scroll.scrollTop,
|
|
778
|
+
})
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
}, 1)
|
|
639
782
|
}
|
|
640
783
|
setHasMorePreviewBefore(page.hasMoreBefore)
|
|
641
784
|
} catch (err) {
|
|
642
785
|
debug.log("preview:load-before:error", err instanceof Error ? err.message : String(err))
|
|
786
|
+
previewBeforeAdjusting = false
|
|
643
787
|
} finally {
|
|
644
788
|
debug.timeEnd("preview:load-before")
|
|
645
|
-
|
|
789
|
+
lastBeforeLoadMs = Date.now()
|
|
790
|
+
pending.visibleLoad ? setLoadingPreviewMore(false) : setPrefetchingPreviewBefore(false)
|
|
646
791
|
}
|
|
647
792
|
}
|
|
648
793
|
|
|
@@ -688,46 +833,104 @@ export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; o
|
|
|
688
833
|
}
|
|
689
834
|
}
|
|
690
835
|
|
|
691
|
-
const schedulePreviewBefore = (
|
|
836
|
+
const schedulePreviewBefore = (opts: {
|
|
837
|
+
intent: PreviewBeforeIntent
|
|
838
|
+
visibleLoad?: boolean
|
|
839
|
+
previousContentHeight?: number
|
|
840
|
+
requestedAmount?: number
|
|
841
|
+
previousScrollTop?: number
|
|
842
|
+
}) => {
|
|
843
|
+
const id = ++previewBeforeLoadID
|
|
844
|
+
const previousContentHeight = opts.previousContentHeight ?? previewContentHeight()
|
|
845
|
+
const previousScrollTop = opts.previousScrollTop ?? previewScroll?.scrollTop ?? 0
|
|
846
|
+
const requestedAmount = opts.requestedAmount ?? 0
|
|
847
|
+
const visibleLoad = opts.visibleLoad ?? false
|
|
848
|
+
const source = opts.intent === "explicit-scroll" ? "key" : "edge"
|
|
692
849
|
if (!hasMorePreviewBefore() || loadingPreviewMore() || prefetchingPreviewBefore()) {
|
|
693
|
-
debug.log("preview:
|
|
694
|
-
|
|
850
|
+
debug.log("preview:before:schedule-skip", {
|
|
851
|
+
loadID: id,
|
|
852
|
+
reason: !hasMorePreviewBefore() ? "no-more-before" : loadingPreviewMore() ? "loading" : "prefetching",
|
|
853
|
+
source,
|
|
854
|
+
intent: opts.intent,
|
|
695
855
|
previousContentHeight,
|
|
696
|
-
|
|
856
|
+
previousScrollTop,
|
|
857
|
+
requestedAmount,
|
|
697
858
|
visibleLoad,
|
|
698
859
|
state: previewScrollState(),
|
|
699
860
|
})
|
|
700
861
|
return
|
|
701
862
|
}
|
|
863
|
+
if (previewBeforeAdjusting) {
|
|
864
|
+
if (opts.intent === "explicit-scroll") {
|
|
865
|
+
pendingCoalescedExplicit = { requestedAmount }
|
|
866
|
+
debug.log("preview:before:coalesce-explicit", {
|
|
867
|
+
loadID: id,
|
|
868
|
+
requestedAmount,
|
|
869
|
+
state: previewScrollState(),
|
|
870
|
+
})
|
|
871
|
+
} else {
|
|
872
|
+
debug.log("preview:before:schedule-skip", {
|
|
873
|
+
loadID: id,
|
|
874
|
+
reason: "adjusting",
|
|
875
|
+
source,
|
|
876
|
+
intent: opts.intent,
|
|
877
|
+
previousContentHeight,
|
|
878
|
+
previousScrollTop,
|
|
879
|
+
requestedAmount,
|
|
880
|
+
visibleLoad,
|
|
881
|
+
state: previewScrollState(),
|
|
882
|
+
})
|
|
883
|
+
}
|
|
884
|
+
return
|
|
885
|
+
}
|
|
702
886
|
if (previewBeforeTimer) {
|
|
703
887
|
if (pendingPreviewBefore) {
|
|
704
888
|
const previousPending = { ...pendingPreviewBefore }
|
|
705
|
-
pendingPreviewBefore.preserveScroll = pendingPreviewBefore.preserveScroll && preserveScroll
|
|
706
889
|
pendingPreviewBefore.visibleLoad = pendingPreviewBefore.visibleLoad || visibleLoad
|
|
707
|
-
|
|
890
|
+
if (opts.intent === "explicit-scroll" && pendingPreviewBefore.intent === "passive-prefetch") {
|
|
891
|
+
pendingPreviewBefore.intent = "explicit-scroll"
|
|
892
|
+
pendingPreviewBefore.requestedAmount = requestedAmount
|
|
893
|
+
pendingPreviewBefore.previousScrollTop = previousScrollTop
|
|
894
|
+
}
|
|
895
|
+
debug.log("preview:before:schedule-merge", {
|
|
896
|
+
loadID: id,
|
|
708
897
|
previousPending,
|
|
709
898
|
nextPending: pendingPreviewBefore,
|
|
710
|
-
requested: { previousContentHeight,
|
|
711
|
-
state: previewScrollState(),
|
|
899
|
+
requested: { source, intent: opts.intent, previousContentHeight, previousScrollTop, requestedAmount, visibleLoad },
|
|
712
900
|
})
|
|
713
901
|
} else {
|
|
714
|
-
debug.log("preview:
|
|
902
|
+
debug.log("preview:before:schedule-skip", {
|
|
903
|
+
loadID: id,
|
|
715
904
|
reason: "timer-already-set",
|
|
905
|
+
source,
|
|
906
|
+
intent: opts.intent,
|
|
716
907
|
previousContentHeight,
|
|
717
|
-
|
|
908
|
+
previousScrollTop,
|
|
909
|
+
requestedAmount,
|
|
718
910
|
visibleLoad,
|
|
719
911
|
state: previewScrollState(),
|
|
720
912
|
})
|
|
721
913
|
}
|
|
722
914
|
return
|
|
723
915
|
}
|
|
724
|
-
pendingPreviewBefore = { previousContentHeight,
|
|
725
|
-
debug.log("preview:
|
|
916
|
+
pendingPreviewBefore = { id, previousContentHeight, previousScrollTop, requestedAmount, intent: opts.intent, visibleLoad }
|
|
917
|
+
debug.log("preview:before:schedule", {
|
|
918
|
+
loadID: id,
|
|
919
|
+
source,
|
|
920
|
+
intent: opts.intent,
|
|
921
|
+
previousContentHeight,
|
|
922
|
+
previousScrollTop,
|
|
923
|
+
requestedAmount,
|
|
924
|
+
visibleLoad,
|
|
925
|
+
firstPart: previewParts()[0]?.id,
|
|
926
|
+
partCount: previewParts().length,
|
|
927
|
+
state: previewScrollState(),
|
|
928
|
+
})
|
|
726
929
|
previewBeforeTimer = setTimeout(() => {
|
|
727
930
|
const pending = pendingPreviewBefore
|
|
728
931
|
previewBeforeTimer = undefined
|
|
729
932
|
pendingPreviewBefore = undefined
|
|
730
|
-
if (pending) loadPreviewBefore(pending
|
|
933
|
+
if (pending) loadPreviewBefore(pending)
|
|
731
934
|
}, 1)
|
|
732
935
|
}
|
|
733
936
|
|
|
@@ -813,11 +1016,14 @@ export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; o
|
|
|
813
1016
|
debug.timeEnd("preview:load")
|
|
814
1017
|
})
|
|
815
1018
|
|
|
1019
|
+
let lastBeforeLoadMs = 0
|
|
1020
|
+
let lastPreviewScrollKeyMs = 0
|
|
816
1021
|
createEffect(() => {
|
|
817
1022
|
const item = selectedResult()
|
|
818
1023
|
if (!item) return
|
|
819
1024
|
const interval = setInterval(() => {
|
|
820
1025
|
if (loadingPreviewMore() || prefetchingPreviewBefore() || prefetchingPreviewAfter()) return
|
|
1026
|
+
if (previewBeforeAdjusting) return
|
|
821
1027
|
const scroll = previewScroll
|
|
822
1028
|
const children = scroll?.getChildren()
|
|
823
1029
|
if (!scroll || !children || children.length === 0) return
|
|
@@ -827,26 +1033,28 @@ export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; o
|
|
|
827
1033
|
const nearTop = scroll.scrollTop <= prefetchDistance
|
|
828
1034
|
const atBottom = scroll.scrollTop + scroll.height >= totalContentHeight - 1
|
|
829
1035
|
const nearBottom = scroll.scrollTop + scroll.height >= totalContentHeight - prefetchDistance
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
1036
|
+
const msSinceManualScroll = Date.now() - lastPreviewScrollKeyMs
|
|
1037
|
+
const willLoadBefore = atTop && hasMorePreviewBefore() && (Date.now() - lastBeforeLoadMs) > 100 && msSinceManualScroll > 700
|
|
1038
|
+
debug.log("preview:edge:decision", {
|
|
1039
|
+
scrollTop: scroll.scrollTop,
|
|
1040
|
+
height: scroll.height,
|
|
1041
|
+
contentHeight: totalContentHeight,
|
|
1042
|
+
prefetchDistance,
|
|
1043
|
+
atTop,
|
|
1044
|
+
nearTop,
|
|
1045
|
+
atBottom,
|
|
1046
|
+
nearBottom,
|
|
1047
|
+
hasMoreBefore: hasMorePreviewBefore(),
|
|
1048
|
+
hasMoreAfter: hasMorePreviewAfter(),
|
|
1049
|
+
loadingBefore: loadingPreviewMore(),
|
|
1050
|
+
pendingBefore: Boolean(previewBeforeTimer),
|
|
1051
|
+
willLoadBefore,
|
|
1052
|
+
msSinceLastLoad: Date.now() - lastBeforeLoadMs,
|
|
1053
|
+
msSinceManualScroll,
|
|
1054
|
+
})
|
|
1055
|
+
if (willLoadBefore) {
|
|
1056
|
+
schedulePreviewBefore({ intent: "passive-prefetch", previousContentHeight: totalContentHeight, previousScrollTop: scroll.scrollTop })
|
|
848
1057
|
}
|
|
849
|
-
if (atTop && hasMorePreviewBefore()) schedulePreviewBefore(totalContentHeight, true, false)
|
|
850
1058
|
if (nearBottom && hasMorePreviewAfter()) schedulePreviewAfter(atBottom)
|
|
851
1059
|
}, 400)
|
|
852
1060
|
onCleanup(() => clearInterval(interval))
|
|
@@ -881,11 +1089,14 @@ export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; o
|
|
|
881
1089
|
}
|
|
882
1090
|
|
|
883
1091
|
const beforeState = previewScrollState()
|
|
1092
|
+
lastPreviewScrollKeyMs = Date.now()
|
|
884
1093
|
debug.log("preview:scroll-key", { direction, state: beforeState })
|
|
885
1094
|
|
|
1095
|
+
const requestedAmount = previewScrollAmount(scroll)
|
|
1096
|
+
|
|
886
1097
|
if (direction < 0 && scroll.scrollTop <= 0 && hasMorePreviewBefore()) {
|
|
887
|
-
debug.log("preview:scroll-key:load-before", { direction, state: beforeState })
|
|
888
|
-
schedulePreviewBefore(
|
|
1098
|
+
debug.log("preview:scroll-key:load-before", { direction, requestedAmount, state: beforeState })
|
|
1099
|
+
schedulePreviewBefore({ intent: "explicit-scroll", visibleLoad: true, requestedAmount, previousScrollTop: scroll.scrollTop })
|
|
889
1100
|
return
|
|
890
1101
|
}
|
|
891
1102
|
|
|
@@ -896,7 +1107,7 @@ export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; o
|
|
|
896
1107
|
return
|
|
897
1108
|
}
|
|
898
1109
|
|
|
899
|
-
const amount = direction *
|
|
1110
|
+
const amount = direction * requestedAmount
|
|
900
1111
|
scroll.scrollBy(amount)
|
|
901
1112
|
updatePreviewWindow()
|
|
902
1113
|
debug.log("preview:scroll-key:scroll", { direction, amount, before: beforeState, after: previewScrollState() })
|
|
@@ -1025,6 +1236,7 @@ export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; o
|
|
|
1025
1236
|
>
|
|
1026
1237
|
<Show when={!loading()}>
|
|
1027
1238
|
<Show when={results().length > 0} fallback={<EmptyState query={query()} owner={ownerLabel()} theme={theme()} />}>
|
|
1239
|
+
<box height={resultTopSpacerHeight()} flexShrink={0} />
|
|
1028
1240
|
<For each={resultRenderWindow().items}>
|
|
1029
1241
|
{(item, index) => {
|
|
1030
1242
|
const absoluteIndex = () => resultRenderWindow().start + index()
|
|
@@ -1041,6 +1253,7 @@ export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; o
|
|
|
1041
1253
|
)
|
|
1042
1254
|
}}
|
|
1043
1255
|
</For>
|
|
1256
|
+
<box height={resultBottomSpacerHeight()} flexShrink={0} />
|
|
1044
1257
|
<Show when={loadingMore()}>
|
|
1045
1258
|
<SkeletonRow theme={theme()} />
|
|
1046
1259
|
</Show>
|
package/tui.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/** @jsxImportSource @opentui/solid */
|
|
2
2
|
import type { TuiPlugin, TuiPluginApi, TuiPluginModule } from "@opencode-ai/plugin/tui"
|
|
3
3
|
import { Telescope } from "./telescope.tsx"
|
|
4
|
+
import { debug } from "./ui/debug.ts"
|
|
4
5
|
import { loadTelescopeConfig } from "./ui/config.ts"
|
|
5
6
|
|
|
6
7
|
const id = "opencode-telescope"
|
|
@@ -12,15 +13,19 @@ const enabled = (options: unknown) => {
|
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
const tui: TuiPlugin = async (api: TuiPluginApi, options: unknown) => {
|
|
16
|
+
debug.log("plugin:setup:start")
|
|
15
17
|
if (!enabled(options)) return
|
|
16
18
|
|
|
17
19
|
const config = loadTelescopeConfig()
|
|
18
20
|
const command = "opencode.telescope.sessions"
|
|
19
21
|
const open = () => {
|
|
22
|
+
debug.log("plugin:dialog:open:start")
|
|
20
23
|
api.ui.dialog.replace(() => <Telescope api={api} config={config} onClose={() => api.ui.dialog.clear()} />)
|
|
21
24
|
api.ui.dialog.setSize("xlarge")
|
|
25
|
+
debug.log("plugin:dialog:open:done")
|
|
22
26
|
}
|
|
23
27
|
|
|
28
|
+
debug.log("plugin:setup:register-keymap")
|
|
24
29
|
const unregisterKeymap = api.keymap.registerLayer({
|
|
25
30
|
commands: [
|
|
26
31
|
{
|
|
@@ -38,6 +43,7 @@ const tui: TuiPlugin = async (api: TuiPluginApi, options: unknown) => {
|
|
|
38
43
|
api.lifecycle.onDispose(() => {
|
|
39
44
|
unregisterKeymap()
|
|
40
45
|
})
|
|
46
|
+
debug.log("plugin:setup:done")
|
|
41
47
|
}
|
|
42
48
|
|
|
43
49
|
const plugin: TuiPluginModule & { id: string } = {
|