@bojackduy/opencode-telescope 0.1.20 → 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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/telescope.tsx +297 -86
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.20",
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/telescope.tsx CHANGED
@@ -53,7 +53,7 @@ export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; o
53
53
  const MIN_RECENT_BATCH_SIZE = 15
54
54
  const RESULT_OVERSCAN_MULTIPLIER = 2
55
55
  const RESULT_BATCH_VIEWPORTS = 4
56
- const RESULT_PREFETCH_VIEWPORTS = 3
56
+ const RESULT_PREFETCH_VIEWPORTS = 5
57
57
  const RESULT_CACHE_BEHIND_VIEWPORTS = 6
58
58
  const INITIAL_PREVIEW_BEFORE = 20
59
59
  const INITIAL_PREVIEW_AFTER = 30
@@ -93,10 +93,23 @@ export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; o
93
93
  let resultNavigationStarted = false
94
94
  let resultPrefetchTimer: ReturnType<typeof setTimeout> | undefined
95
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
96
106
  let previewBeforeTimer: ReturnType<typeof setTimeout> | undefined
97
107
  let previewAfterTimer: ReturnType<typeof setTimeout> | undefined
98
- let pendingPreviewBefore: { previousContentHeight: number; preserveScroll: boolean; visibleLoad: boolean } | undefined
108
+ let pendingPreviewBefore: PendingPreviewBefore | undefined
99
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
100
113
  const previewMeasuredHeights = new Map<string, number>()
101
114
  const cancelPreviewPrefetch = () => {
102
115
  if (previewBeforeTimer) clearTimeout(previewBeforeTimer)
@@ -105,6 +118,9 @@ export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; o
105
118
  previewAfterTimer = undefined
106
119
  pendingPreviewBefore = undefined
107
120
  pendingPreviewAfterVisible = false
121
+ previewBeforeAdjusting = false
122
+ pendingCoalescedExplicit = undefined
123
+ pendingPreviewAnchorCorrection = undefined
108
124
  setPrefetchingPreviewBefore(false)
109
125
  setPrefetchingPreviewAfter(false)
110
126
  setLoadingPreviewMore(false)
@@ -151,6 +167,12 @@ export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; o
151
167
  return { start: base + cachedStart, end: base + cachedEnd, items: cached.slice(cachedStart, cachedEnd) }
152
168
  })
153
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
+
154
176
  let lastResultRenderWindow = ""
155
177
  createEffect(() => {
156
178
  const window = resultRenderWindow()
@@ -446,6 +468,31 @@ export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; o
446
468
  onCleanup(() => clearTimeout(timer))
447
469
  })
448
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
+
449
496
  const estimatePreviewPartHeight = (part: ConversationPreviewPart) => {
450
497
  if (part.type === "tool") {
451
498
  if (!part.target) return 2
@@ -459,15 +506,31 @@ export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; o
459
506
 
460
507
  const previewPartHeight = (part: ConversationPreviewPart) => previewMeasuredHeights.get(part.id) ?? estimatePreviewPartHeight(part)
461
508
 
462
- const previewVirtualLayout = createMemo(() => {
463
- previewHeightVersion()
509
+ const buildPreviewLayout = (parts: ConversationPreviewPart[]) => {
464
510
  let offset = 0
465
- return previewParts().map((part) => {
511
+ return parts.map((part) => {
466
512
  const height = previewPartHeight(part)
467
513
  const row = { part, top: offset, bottom: offset + height, height }
468
514
  offset += height
469
515
  return row
470
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())
471
534
  })
472
535
 
473
536
  const previewContentHeight = () => previewVirtualLayout().at(-1)?.bottom ?? 0
@@ -486,14 +549,7 @@ export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; o
486
549
  return
487
550
  }
488
551
 
489
- const overscan = Math.max(scroll.height * 2, 40)
490
- const from = Math.max(0, scroll.scrollTop - overscan)
491
- const to = scroll.scrollTop + scroll.height + overscan
492
- const foundStart = layout.findIndex((row) => row.bottom >= from)
493
- const start = foundStart === -1 ? Math.max(0, layout.length - 1) : Math.max(0, foundStart)
494
- const foundEnd = layout.findIndex((row) => row.top > to)
495
- const end = foundEnd === -1 ? layout.length : foundEnd
496
- const next = { start, end: Math.min(layout.length, Math.max(start + 1, end)) }
552
+ const next = previewWindowForLayout(layout, scroll.scrollTop, scroll.height)
497
553
  const current = previewWindow()
498
554
  if (current.start !== next.start || current.end !== next.end) setPreviewWindow(next)
499
555
  }
@@ -558,6 +614,29 @@ export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; o
558
614
  totalMeasured: previewMeasuredHeights.size,
559
615
  window: previewWindow(),
560
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
+ }
561
640
  setPreviewHeightVersion((value) => value + 1)
562
641
  setTimeout(updatePreviewWindow, 1)
563
642
  }
@@ -576,75 +655,139 @@ export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; o
576
655
  onCleanup(() => clearTimeout(timer))
577
656
  })
578
657
 
579
- const loadPreviewBefore = (previousContentHeight = previewContentHeight(), preserveScroll = true, visibleLoad = false) => {
658
+ const loadPreviewBefore = (pending: PendingPreviewBefore) => {
580
659
  const item = selectedResult()
581
660
  const first = previewParts()[0]
582
661
  if (!item || !first || loadingPreviewMore() || prefetchingPreviewBefore()) {
583
662
  debug.log("preview:load-before:skip", {
663
+ loadID: pending.id,
584
664
  reason: !item ? "no-item" : !first ? "no-first-part" : loadingPreviewMore() ? "loading-preview-more" : "prefetching-before",
585
- previousContentHeight,
586
- preserveScroll,
587
- visibleLoad,
665
+ pending,
588
666
  state: previewScrollState(),
589
667
  })
590
668
  return
591
669
  }
592
670
  const beforeState = previewScrollState()
593
- visibleLoad ? setLoadingPreviewMore(true) : setPrefetchingPreviewBefore(true)
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)
594
677
  debug.time("preview:load-before")
595
678
  try {
596
679
  debug.log("preview:load-before:start", {
597
- item: item.id,
680
+ loadID: pending.id,
681
+ intent: pending.intent,
598
682
  cursor: { id: first.id, timeCreated: first.timeCreated },
599
- previousContentHeight,
600
- preserveScroll,
601
- visibleLoad,
683
+ previousContentHeight: pending.previousContentHeight,
684
+ previousScrollTop: pending.previousScrollTop,
685
+ anchorPartID,
686
+ offsetInAnchor,
687
+ requestedAmount: pending.requestedAmount,
688
+ visibleLoad: pending.visibleLoad,
602
689
  state: beforeState,
603
690
  })
604
691
  const page = loadConversationBefore(item, { id: first.id, timeCreated: first.timeCreated }, { limit: PREVIEW_PAGE_SIZE, dbPath: dbPath() })
605
- debug.log("preview:load-before", {
606
- item: item.id,
692
+ debug.log("preview:load-before:query-done", {
693
+ loadID: pending.id,
607
694
  added: page.parts.length,
608
695
  hasMoreBefore: page.hasMoreBefore,
609
- preserveScroll,
610
- visibleLoad,
611
696
  first: page.parts[0]?.id,
612
697
  last: page.parts.at(-1)?.id,
613
698
  })
614
699
  if (page.parts.length > 0) {
615
- setPreviewParts((prev) => [...page.parts, ...prev])
616
- if (preserveScroll) {
617
- setTimeout(() => {
618
- const beforeAdjust = previewScrollState()
619
- const delta = previewContentHeight() - previousContentHeight
620
- if (delta > 0) previewScroll?.scrollBy(delta)
621
- updatePreviewWindow()
622
- debug.log("preview:load-before:adjust", {
623
- delta,
624
- previousContentHeight,
625
- newContentHeight: previewContentHeight(),
626
- scrolled: delta > 0,
627
- beforeAdjust,
628
- afterAdjust: previewScrollState(),
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,
629
757
  })
630
- }, 1)
631
- } else {
632
- setTimeout(() => {
758
+ } else {
633
759
  updatePreviewWindow()
634
- debug.log("preview:load-before:no-adjust", {
635
- previousContentHeight,
636
- newContentHeight: previewContentHeight(),
637
- state: previewScrollState(),
638
- })
639
- }, 1)
640
- }
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)
641
782
  }
642
783
  setHasMorePreviewBefore(page.hasMoreBefore)
643
784
  } catch (err) {
644
785
  debug.log("preview:load-before:error", err instanceof Error ? err.message : String(err))
786
+ previewBeforeAdjusting = false
645
787
  } finally {
646
788
  debug.timeEnd("preview:load-before")
647
- visibleLoad ? setLoadingPreviewMore(false) : setPrefetchingPreviewBefore(false)
789
+ lastBeforeLoadMs = Date.now()
790
+ pending.visibleLoad ? setLoadingPreviewMore(false) : setPrefetchingPreviewBefore(false)
648
791
  }
649
792
  }
650
793
 
@@ -690,46 +833,104 @@ export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; o
690
833
  }
691
834
  }
692
835
 
693
- const schedulePreviewBefore = (previousContentHeight = previewContentHeight(), preserveScroll = true, visibleLoad = false) => {
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"
694
849
  if (!hasMorePreviewBefore() || loadingPreviewMore() || prefetchingPreviewBefore()) {
695
- debug.log("preview:prefetch-before:skip", {
696
- reason: !hasMorePreviewBefore() ? "no-more-before" : loadingPreviewMore() ? "loading-preview-more" : "prefetching-before",
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,
697
855
  previousContentHeight,
698
- preserveScroll,
856
+ previousScrollTop,
857
+ requestedAmount,
699
858
  visibleLoad,
700
859
  state: previewScrollState(),
701
860
  })
702
861
  return
703
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
+ }
704
886
  if (previewBeforeTimer) {
705
887
  if (pendingPreviewBefore) {
706
888
  const previousPending = { ...pendingPreviewBefore }
707
- pendingPreviewBefore.preserveScroll = pendingPreviewBefore.preserveScroll && preserveScroll
708
889
  pendingPreviewBefore.visibleLoad = pendingPreviewBefore.visibleLoad || visibleLoad
709
- debug.log("preview:prefetch-before:merge", {
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,
710
897
  previousPending,
711
898
  nextPending: pendingPreviewBefore,
712
- requested: { previousContentHeight, preserveScroll, visibleLoad },
713
- state: previewScrollState(),
899
+ requested: { source, intent: opts.intent, previousContentHeight, previousScrollTop, requestedAmount, visibleLoad },
714
900
  })
715
901
  } else {
716
- debug.log("preview:prefetch-before:skip", {
902
+ debug.log("preview:before:schedule-skip", {
903
+ loadID: id,
717
904
  reason: "timer-already-set",
905
+ source,
906
+ intent: opts.intent,
718
907
  previousContentHeight,
719
- preserveScroll,
908
+ previousScrollTop,
909
+ requestedAmount,
720
910
  visibleLoad,
721
911
  state: previewScrollState(),
722
912
  })
723
913
  }
724
914
  return
725
915
  }
726
- pendingPreviewBefore = { previousContentHeight, preserveScroll, visibleLoad }
727
- debug.log("preview:prefetch-before-scheduled", { previousContentHeight, preserveScroll, visibleLoad, state: previewScrollState() })
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
+ })
728
929
  previewBeforeTimer = setTimeout(() => {
729
930
  const pending = pendingPreviewBefore
730
931
  previewBeforeTimer = undefined
731
932
  pendingPreviewBefore = undefined
732
- if (pending) loadPreviewBefore(pending.previousContentHeight, pending.preserveScroll, pending.visibleLoad)
933
+ if (pending) loadPreviewBefore(pending)
733
934
  }, 1)
734
935
  }
735
936
 
@@ -815,11 +1016,14 @@ export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; o
815
1016
  debug.timeEnd("preview:load")
816
1017
  })
817
1018
 
1019
+ let lastBeforeLoadMs = 0
1020
+ let lastPreviewScrollKeyMs = 0
818
1021
  createEffect(() => {
819
1022
  const item = selectedResult()
820
1023
  if (!item) return
821
1024
  const interval = setInterval(() => {
822
1025
  if (loadingPreviewMore() || prefetchingPreviewBefore() || prefetchingPreviewAfter()) return
1026
+ if (previewBeforeAdjusting) return
823
1027
  const scroll = previewScroll
824
1028
  const children = scroll?.getChildren()
825
1029
  if (!scroll || !children || children.length === 0) return
@@ -829,26 +1033,28 @@ export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; o
829
1033
  const nearTop = scroll.scrollTop <= prefetchDistance
830
1034
  const atBottom = scroll.scrollTop + scroll.height >= totalContentHeight - 1
831
1035
  const nearBottom = scroll.scrollTop + scroll.height >= totalContentHeight - prefetchDistance
832
- if (nearTop || nearBottom) {
833
- debug.log("preview:scroll-edge", {
834
- y: scroll.y,
835
- scrollTop: scroll.scrollTop,
836
- height: scroll.height,
837
- contentHeight: totalContentHeight,
838
- childContentHeight: previewScrollState().childContentHeight,
839
- prefetchDistance,
840
- atTop,
841
- nearTop,
842
- atBottom,
843
- nearBottom,
844
- hasMoreBefore: hasMorePreviewBefore(),
845
- hasMoreAfter: hasMorePreviewAfter(),
846
- prefetchingBefore: prefetchingPreviewBefore(),
847
- prefetchingAfter: prefetchingPreviewAfter(),
848
- children: children.length,
849
- })
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 })
850
1057
  }
851
- if (atTop && hasMorePreviewBefore()) schedulePreviewBefore(totalContentHeight, true, false)
852
1058
  if (nearBottom && hasMorePreviewAfter()) schedulePreviewAfter(atBottom)
853
1059
  }, 400)
854
1060
  onCleanup(() => clearInterval(interval))
@@ -883,11 +1089,14 @@ export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; o
883
1089
  }
884
1090
 
885
1091
  const beforeState = previewScrollState()
1092
+ lastPreviewScrollKeyMs = Date.now()
886
1093
  debug.log("preview:scroll-key", { direction, state: beforeState })
887
1094
 
1095
+ const requestedAmount = previewScrollAmount(scroll)
1096
+
888
1097
  if (direction < 0 && scroll.scrollTop <= 0 && hasMorePreviewBefore()) {
889
- debug.log("preview:scroll-key:load-before", { direction, state: beforeState })
890
- schedulePreviewBefore(previewContentHeight(), true, true)
1098
+ debug.log("preview:scroll-key:load-before", { direction, requestedAmount, state: beforeState })
1099
+ schedulePreviewBefore({ intent: "explicit-scroll", visibleLoad: true, requestedAmount, previousScrollTop: scroll.scrollTop })
891
1100
  return
892
1101
  }
893
1102
 
@@ -898,7 +1107,7 @@ export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; o
898
1107
  return
899
1108
  }
900
1109
 
901
- const amount = direction * previewScrollAmount(scroll)
1110
+ const amount = direction * requestedAmount
902
1111
  scroll.scrollBy(amount)
903
1112
  updatePreviewWindow()
904
1113
  debug.log("preview:scroll-key:scroll", { direction, amount, before: beforeState, after: previewScrollState() })
@@ -1027,6 +1236,7 @@ export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; o
1027
1236
  >
1028
1237
  <Show when={!loading()}>
1029
1238
  <Show when={results().length > 0} fallback={<EmptyState query={query()} owner={ownerLabel()} theme={theme()} />}>
1239
+ <box height={resultTopSpacerHeight()} flexShrink={0} />
1030
1240
  <For each={resultRenderWindow().items}>
1031
1241
  {(item, index) => {
1032
1242
  const absoluteIndex = () => resultRenderWindow().start + index()
@@ -1043,6 +1253,7 @@ export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; o
1043
1253
  )
1044
1254
  }}
1045
1255
  </For>
1256
+ <box height={resultBottomSpacerHeight()} flexShrink={0} />
1046
1257
  <Show when={loadingMore()}>
1047
1258
  <SkeletonRow theme={theme()} />
1048
1259
  </Show>