@bojackduy/opencode-telescope 0.1.13 → 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/README.md +39 -1
- package/components/result-list.tsx +2 -2
- package/package.json +1 -1
- package/search.ts +93 -29
- package/telescope.tsx +424 -138
- package/tui.tsx +4 -2
- package/ui/config.test.ts +48 -0
- package/ui/config.ts +93 -0
- package/ui/keyboard.test.ts +40 -0
- package/ui/keyboard.ts +34 -0
- package/ui/render-target.ts +2 -0
package/telescope.tsx
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import type { TuiPluginApi } from "@opencode-ai/plugin/tui"
|
|
3
3
|
import type { InputRenderable, ParsedKey, ScrollBoxRenderable } from "@opentui/core"
|
|
4
4
|
import { useKeyboard, useTerminalDimensions } from "@opentui/solid"
|
|
5
|
-
import { For, Show, createEffect, createMemo, createSignal, onCleanup } from "solid-js"
|
|
5
|
+
import { For, Show, batch as solidBatch, createEffect, createMemo, createSignal, onCleanup } from "solid-js"
|
|
6
6
|
import { ConversationPreview, PreviewHeader } from "./components/preview.tsx"
|
|
7
7
|
import { EmptyState, ResultRow, SkeletonRow } from "./components/result-list.tsx"
|
|
8
8
|
import {
|
|
@@ -14,43 +14,71 @@ import {
|
|
|
14
14
|
searchSessionMessages,
|
|
15
15
|
type ConversationPreviewPart,
|
|
16
16
|
type SearchResult,
|
|
17
|
+
type SearchRole,
|
|
17
18
|
} from "./search.ts"
|
|
18
19
|
import { debug } from "./ui/debug.ts"
|
|
19
20
|
import { syntaxStyle } from "./ui/format.ts"
|
|
20
|
-
import {
|
|
21
|
+
import type { TelescopeConfig } from "./ui/config.ts"
|
|
22
|
+
import { inputSafeKeys, keyListLabel, matchesKey, prevent } from "./ui/keyboard.ts"
|
|
21
23
|
import { jumpToRenderedTarget, messageTargetID, previewScrollAmount, scrollPreviewToTarget } from "./ui/render-target.ts"
|
|
22
24
|
|
|
23
|
-
export const Telescope = (props: { api: TuiPluginApi; onClose: () => void }) => {
|
|
25
|
+
export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; onClose: () => void }) => {
|
|
26
|
+
type OwnerFilter = "all" | SearchRole
|
|
24
27
|
const dimensions = useTerminalDimensions()
|
|
25
28
|
const [query, setQuery] = createSignal("")
|
|
29
|
+
const [ownerFilter, setOwnerFilter] = createSignal<OwnerFilter>("all")
|
|
26
30
|
const [results, setResults] = createSignal<SearchResult[]>([])
|
|
27
31
|
const [previewParts, setPreviewParts] = createSignal<ConversationPreviewPart[]>([])
|
|
28
32
|
const [selected, setSelected] = createSignal(0)
|
|
33
|
+
const [resultBaseOffset, setResultBaseOffset] = createSignal(0)
|
|
34
|
+
const [nextResultOffset, setNextResultOffset] = createSignal(0)
|
|
29
35
|
const [busy, setBusy] = createSignal(false)
|
|
30
36
|
const [error, setError] = createSignal("")
|
|
31
37
|
const [mode, setMode] = createSignal<"normal" | "insert">("normal")
|
|
32
38
|
const [loading, setLoading] = createSignal(true)
|
|
33
39
|
const [hasMore, setHasMore] = createSignal(true)
|
|
34
40
|
const [loadingMore, setLoadingMore] = createSignal(false)
|
|
41
|
+
const [loadingPreviousResults, setLoadingPreviousResults] = createSignal(false)
|
|
35
42
|
const [prefetchingResults, setPrefetchingResults] = createSignal(false)
|
|
36
43
|
const [resultPageInfo, setResultPageInfo] = createSignal({ loadedUntil: 0, hasMore: true, pageSize: 0, lastOffset: 0, lastAdded: 0 })
|
|
37
44
|
const [hasMorePreviewBefore, setHasMorePreviewBefore] = createSignal(false)
|
|
38
45
|
const [hasMorePreviewAfter, setHasMorePreviewAfter] = createSignal(false)
|
|
39
46
|
const [loadingPreviewMore, setLoadingPreviewMore] = createSignal(false)
|
|
40
|
-
const
|
|
41
|
-
const
|
|
47
|
+
const [prefetchingPreviewBefore, setPrefetchingPreviewBefore] = createSignal(false)
|
|
48
|
+
const [prefetchingPreviewAfter, setPrefetchingPreviewAfter] = createSignal(false)
|
|
49
|
+
const [previewEdgeLoadingReady, setPreviewEdgeLoadingReady] = createSignal(false)
|
|
50
|
+
const RESULT_BATCH_SIZE = 50
|
|
51
|
+
const RESULT_PREFETCH_AHEAD_ROWS = 25
|
|
42
52
|
const RESULT_OVERSCAN_MULTIPLIER = 2
|
|
43
|
-
const
|
|
44
|
-
const INITIAL_PREVIEW_BEFORE =
|
|
45
|
-
const INITIAL_PREVIEW_AFTER =
|
|
53
|
+
const RESULT_CACHE_BEHIND_VIEWPORTS = 6
|
|
54
|
+
const INITIAL_PREVIEW_BEFORE = 6
|
|
55
|
+
const INITIAL_PREVIEW_AFTER = 12
|
|
56
|
+
const INITIAL_PREVIEW_DELAY_MS = 75
|
|
46
57
|
const PREVIEW_PAGE_SIZE = 20
|
|
58
|
+
const PREVIEW_PREFETCH_VIEWPORTS = 0.5
|
|
47
59
|
let input: InputRenderable | undefined
|
|
48
60
|
let resultScroll: ScrollBoxRenderable | undefined
|
|
49
61
|
let previewScroll: ScrollBoxRenderable | undefined
|
|
50
62
|
|
|
51
63
|
const theme = createMemo(() => props.api.theme.current)
|
|
52
64
|
const syntax = createMemo(() => syntaxStyle(theme()))
|
|
53
|
-
const
|
|
65
|
+
const ownerRole = createMemo(() => ownerFilter() === "all" ? undefined : ownerFilter() as SearchRole)
|
|
66
|
+
const ownerLabel = createMemo(() => ownerFilter() === "user" ? "you" : ownerFilter())
|
|
67
|
+
const inputKeys = createMemo(() => ({
|
|
68
|
+
moveDown: inputSafeKeys(props.config.keys.moveDown),
|
|
69
|
+
moveUp: inputSafeKeys(props.config.keys.moveUp),
|
|
70
|
+
open: inputSafeKeys(props.config.keys.open),
|
|
71
|
+
normalMode: inputSafeKeys(props.config.keys.normalMode),
|
|
72
|
+
}))
|
|
73
|
+
const normalHelpItems = createMemo(() => [
|
|
74
|
+
`${keyListLabel(props.config.keys.moveUp)}/${keyListLabel(props.config.keys.moveDown)} move`,
|
|
75
|
+
`${keyListLabel(props.config.keys.scrollPreviewDown)}/${keyListLabel(props.config.keys.scrollPreviewUp)} scroll`,
|
|
76
|
+
`${keyListLabel(props.config.keys.toggleOwner)} owner`,
|
|
77
|
+
`${keyListLabel(props.config.keys.insertMode)} search`,
|
|
78
|
+
`${keyListLabel(props.config.keys.open)} open`,
|
|
79
|
+
`${keyListLabel(props.config.keys.close)} close`,
|
|
80
|
+
])
|
|
81
|
+
const selectedResult = createMemo(() => results()[selected() - resultBaseOffset()])
|
|
54
82
|
const popupWidth = createMemo(() => Math.max(72, Math.min(dimensions().width - 2, Math.floor(dimensions().width * 0.92))))
|
|
55
83
|
const leftWidth = createMemo(() => Math.max(36, Math.min(64, Math.floor(popupWidth() * 0.36))))
|
|
56
84
|
const height = createMemo(() => Math.max(18, dimensions().height - 8))
|
|
@@ -58,9 +86,29 @@ export const Telescope = (props: { api: TuiPluginApi; onClose: () => void }) =>
|
|
|
58
86
|
const dbPath = createMemo(() => resolveDatabasePath())
|
|
59
87
|
const directory = props.api.state.path.directory
|
|
60
88
|
let advanceSelectionAfterLoad = false
|
|
89
|
+
let advanceSelectionBeforeLoad = false
|
|
61
90
|
let resultPrefetchTimer: ReturnType<typeof setTimeout> | undefined
|
|
91
|
+
let resultPreviousTimer: ReturnType<typeof setTimeout> | undefined
|
|
92
|
+
let previewBeforeTimer: ReturnType<typeof setTimeout> | undefined
|
|
93
|
+
let previewAfterTimer: ReturnType<typeof setTimeout> | undefined
|
|
94
|
+
let pendingPreviewBefore: { itemId: string; previousContentHeight: number; preserveScroll: boolean; visibleLoad: boolean } | undefined
|
|
95
|
+
let pendingPreviewAfter: { itemId: string; visibleLoad: boolean } | undefined
|
|
96
|
+
const cancelPreviewPrefetch = () => {
|
|
97
|
+
if (previewBeforeTimer) clearTimeout(previewBeforeTimer)
|
|
98
|
+
if (previewAfterTimer) clearTimeout(previewAfterTimer)
|
|
99
|
+
previewBeforeTimer = undefined
|
|
100
|
+
previewAfterTimer = undefined
|
|
101
|
+
pendingPreviewBefore = undefined
|
|
102
|
+
pendingPreviewAfter = undefined
|
|
103
|
+
setPrefetchingPreviewBefore(false)
|
|
104
|
+
setPrefetchingPreviewAfter(false)
|
|
105
|
+
setLoadingPreviewMore(false)
|
|
106
|
+
setPreviewEdgeLoadingReady(false)
|
|
107
|
+
}
|
|
62
108
|
onCleanup(() => {
|
|
63
109
|
if (resultPrefetchTimer) clearTimeout(resultPrefetchTimer)
|
|
110
|
+
if (resultPreviousTimer) clearTimeout(resultPreviousTimer)
|
|
111
|
+
cancelPreviewPrefetch()
|
|
64
112
|
})
|
|
65
113
|
|
|
66
114
|
const resultRowHeight = createMemo(() => leftWidth() >= 48 ? 3 : 4)
|
|
@@ -68,27 +116,47 @@ export const Telescope = (props: { api: TuiPluginApi; onClose: () => void }) =>
|
|
|
68
116
|
const viewportHeight = resultScroll?.height || Math.max(8, height() - 7)
|
|
69
117
|
return Math.max(5, Math.floor(viewportHeight / resultRowHeight()))
|
|
70
118
|
}
|
|
71
|
-
const searchBatchSize = () =>
|
|
72
|
-
const recentBatchSize = () =>
|
|
73
|
-
const resultPrefetchThreshold = () =>
|
|
119
|
+
const searchBatchSize = () => RESULT_BATCH_SIZE
|
|
120
|
+
const recentBatchSize = () => RESULT_BATCH_SIZE
|
|
121
|
+
const resultPrefetchThreshold = () => RESULT_PREFETCH_AHEAD_ROWS
|
|
122
|
+
const resultPrefetchState = () => {
|
|
123
|
+
const cachedEnd = resultBaseOffset() + results().length
|
|
124
|
+
const rowsAhead = Math.max(0, cachedEnd - selected() - 1)
|
|
125
|
+
const threshold = resultPrefetchThreshold()
|
|
126
|
+
return { cachedEnd, rowsAhead, threshold, shouldPrefetch: rowsAhead <= threshold }
|
|
127
|
+
}
|
|
128
|
+
const trimResultCache = (items: SearchResult[], anchorIndex: number) => {
|
|
129
|
+
const base = resultBaseOffset()
|
|
130
|
+
const keepBehind = visibleResultRows() * RESULT_CACHE_BEHIND_VIEWPORTS
|
|
131
|
+
const minOffset = Math.max(0, anchorIndex - keepBehind)
|
|
132
|
+
const drop = Math.min(Math.max(0, minOffset - base), Math.max(0, items.length - 1))
|
|
133
|
+
if (drop === 0) return { base, items }
|
|
134
|
+
|
|
135
|
+
const nextBase = base + drop
|
|
136
|
+
debug.log("results:evict", { fromBase: base, toBase: nextBase, dropped: drop, kept: items.length - drop, anchorIndex })
|
|
137
|
+
return { base: nextBase, items: items.slice(drop) }
|
|
138
|
+
}
|
|
74
139
|
const resultRenderWindow = createMemo(() => {
|
|
75
|
-
const
|
|
140
|
+
const base = resultBaseOffset()
|
|
141
|
+
const cached = results()
|
|
76
142
|
const visible = visibleResultRows()
|
|
77
143
|
const overscan = visible * RESULT_OVERSCAN_MULTIPLIER
|
|
78
144
|
const index = selected()
|
|
79
|
-
const
|
|
80
|
-
const
|
|
81
|
-
return { start, end, items:
|
|
145
|
+
const cachedStart = Math.max(0, index - overscan - base)
|
|
146
|
+
const cachedEnd = Math.min(cached.length, index + visible + overscan - base)
|
|
147
|
+
return { start: base + cachedStart, end: base + cachedEnd, items: cached.slice(cachedStart, cachedEnd) }
|
|
82
148
|
})
|
|
83
149
|
|
|
84
150
|
let lastResultRenderWindow = ""
|
|
85
151
|
createEffect(() => {
|
|
86
152
|
const window = resultRenderWindow()
|
|
87
|
-
const key = `${window.start}:${window.end}:${results().length}`
|
|
153
|
+
const key = `${window.start}:${window.end}:${resultBaseOffset()}:${nextResultOffset()}:${results().length}`
|
|
88
154
|
if (key === lastResultRenderWindow) return
|
|
89
155
|
lastResultRenderWindow = key
|
|
90
156
|
debug.log("results:render-window", {
|
|
91
157
|
selected: selected(),
|
|
158
|
+
baseOffset: resultBaseOffset(),
|
|
159
|
+
nextOffset: nextResultOffset(),
|
|
92
160
|
totalLoaded: results().length,
|
|
93
161
|
start: window.start,
|
|
94
162
|
end: window.end,
|
|
@@ -101,8 +169,18 @@ export const Telescope = (props: { api: TuiPluginApi; onClose: () => void }) =>
|
|
|
101
169
|
|
|
102
170
|
createEffect(() => {
|
|
103
171
|
const q = query().trim()
|
|
172
|
+
const role = ownerRole()
|
|
104
173
|
setError("")
|
|
105
174
|
setHasMore(true)
|
|
175
|
+
if (resultPrefetchTimer) clearTimeout(resultPrefetchTimer)
|
|
176
|
+
if (resultPreviousTimer) clearTimeout(resultPreviousTimer)
|
|
177
|
+
resultPrefetchTimer = undefined
|
|
178
|
+
resultPreviousTimer = undefined
|
|
179
|
+
advanceSelectionAfterLoad = false
|
|
180
|
+
advanceSelectionBeforeLoad = false
|
|
181
|
+
setLoadingMore(false)
|
|
182
|
+
setLoadingPreviousResults(false)
|
|
183
|
+
setPrefetchingResults(false)
|
|
106
184
|
const db = dbPath()
|
|
107
185
|
const dir = directory
|
|
108
186
|
|
|
@@ -112,14 +190,22 @@ export const Telescope = (props: { api: TuiPluginApi; onClose: () => void }) =>
|
|
|
112
190
|
const timer = setTimeout(() => {
|
|
113
191
|
debug.time("query:recent")
|
|
114
192
|
try {
|
|
115
|
-
const batch = recentSessionMessages({ limit, offset: 0, dbPath: db, directory: dir })
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
193
|
+
const batch = recentSessionMessages({ limit, offset: 0, dbPath: db, directory: dir, role })
|
|
194
|
+
solidBatch(() => {
|
|
195
|
+
setResults(batch)
|
|
196
|
+
setResultBaseOffset(0)
|
|
197
|
+
setNextResultOffset(batch.length)
|
|
198
|
+
setHasMore(batch.length >= limit)
|
|
199
|
+
setResultPageInfo({ loadedUntil: batch.length, hasMore: batch.length >= limit, pageSize: limit, lastOffset: 0, lastAdded: batch.length })
|
|
200
|
+
setSelected(0)
|
|
201
|
+
})
|
|
120
202
|
} catch (err) {
|
|
121
|
-
|
|
122
|
-
|
|
203
|
+
solidBatch(() => {
|
|
204
|
+
setResults([])
|
|
205
|
+
setResultBaseOffset(0)
|
|
206
|
+
setNextResultOffset(0)
|
|
207
|
+
setResultPageInfo({ loadedUntil: 0, hasMore: false, pageSize: limit, lastOffset: 0, lastAdded: 0 })
|
|
208
|
+
})
|
|
123
209
|
setError(err instanceof Error ? err.message : String(err))
|
|
124
210
|
}
|
|
125
211
|
debug.timeEnd("query:recent")
|
|
@@ -135,14 +221,22 @@ export const Telescope = (props: { api: TuiPluginApi; onClose: () => void }) =>
|
|
|
135
221
|
const timer = setTimeout(() => {
|
|
136
222
|
debug.time("query:search")
|
|
137
223
|
try {
|
|
138
|
-
const batch = searchSessionMessages(q, { limit, offset: 0, dbPath: db, directory: dir })
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
224
|
+
const batch = searchSessionMessages(q, { limit, offset: 0, dbPath: db, directory: dir, role })
|
|
225
|
+
solidBatch(() => {
|
|
226
|
+
setResults(batch)
|
|
227
|
+
setResultBaseOffset(0)
|
|
228
|
+
setNextResultOffset(batch.length)
|
|
229
|
+
setHasMore(batch.length >= limit)
|
|
230
|
+
setResultPageInfo({ loadedUntil: batch.length, hasMore: batch.length >= limit, pageSize: limit, lastOffset: 0, lastAdded: batch.length })
|
|
231
|
+
setSelected(0)
|
|
232
|
+
})
|
|
143
233
|
} catch (err) {
|
|
144
|
-
|
|
145
|
-
|
|
234
|
+
solidBatch(() => {
|
|
235
|
+
setResults([])
|
|
236
|
+
setResultBaseOffset(0)
|
|
237
|
+
setNextResultOffset(0)
|
|
238
|
+
setResultPageInfo({ loadedUntil: 0, hasMore: false, pageSize: limit, lastOffset: 0, lastAdded: 0 })
|
|
239
|
+
})
|
|
146
240
|
setError(err instanceof Error ? err.message : String(err))
|
|
147
241
|
} finally {
|
|
148
242
|
debug.timeEnd("query:search")
|
|
@@ -159,10 +253,11 @@ export const Telescope = (props: { api: TuiPluginApi; onClose: () => void }) =>
|
|
|
159
253
|
advanceSelectionAfterLoad = false
|
|
160
254
|
return
|
|
161
255
|
}
|
|
162
|
-
if (loadingMore() || prefetchingResults() || busy() || loading()) return
|
|
256
|
+
if (loadingMore() || loadingPreviousResults() || prefetchingResults() || busy() || loading()) return
|
|
163
257
|
|
|
164
|
-
const
|
|
258
|
+
const offset = nextResultOffset()
|
|
165
259
|
const q = query().trim()
|
|
260
|
+
const role = ownerRole()
|
|
166
261
|
const db = dbPath()
|
|
167
262
|
const dir = directory
|
|
168
263
|
const limit = q ? searchBatchSize() : recentBatchSize()
|
|
@@ -171,18 +266,35 @@ export const Telescope = (props: { api: TuiPluginApi; onClose: () => void }) =>
|
|
|
171
266
|
debug.time("query:load-more")
|
|
172
267
|
try {
|
|
173
268
|
const batch = q
|
|
174
|
-
? searchSessionMessages(q, { limit, offset
|
|
175
|
-
: recentSessionMessages({ limit, offset
|
|
269
|
+
? searchSessionMessages(q, { limit, offset, dbPath: db, directory: dir, role })
|
|
270
|
+
: recentSessionMessages({ limit, offset, dbPath: db, directory: dir, role })
|
|
176
271
|
const nextHasMore = batch.length >= limit
|
|
177
|
-
const nextLoadedUntil =
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
272
|
+
const nextLoadedUntil = offset + batch.length
|
|
273
|
+
const previousSelected = selected()
|
|
274
|
+
const nextSelected = advanceSelectionAfterLoad && batch.length > 0 ? offset : selected()
|
|
275
|
+
const nextCache = trimResultCache([...results(), ...batch], nextSelected)
|
|
276
|
+
debug.log("results:prefetch", {
|
|
277
|
+
offset,
|
|
278
|
+
limit,
|
|
279
|
+
added: batch.length,
|
|
280
|
+
baseOffset: nextCache.base,
|
|
281
|
+
cached: nextCache.items.length,
|
|
282
|
+
totalLoaded: nextLoadedUntil,
|
|
283
|
+
hasMore: nextHasMore,
|
|
284
|
+
advance,
|
|
285
|
+
})
|
|
286
|
+
const shouldAdvance = advanceSelectionAfterLoad
|
|
287
|
+
solidBatch(() => {
|
|
288
|
+
setResultBaseOffset(nextCache.base)
|
|
289
|
+
setNextResultOffset(nextLoadedUntil)
|
|
290
|
+
setResults(nextCache.items)
|
|
291
|
+
setResultPageInfo({ loadedUntil: nextLoadedUntil, hasMore: nextHasMore, pageSize: limit, lastOffset: offset, lastAdded: batch.length })
|
|
292
|
+
if (!nextHasMore) setHasMore(false)
|
|
293
|
+
if (shouldAdvance && batch.length > 0) setSelected(offset)
|
|
294
|
+
})
|
|
295
|
+
if (shouldAdvance) {
|
|
296
|
+
debug.log("results:advance-after-load", { from: previousSelected, offset, added: batch.length })
|
|
184
297
|
advanceSelectionAfterLoad = false
|
|
185
|
-
if (batch.length > 0) setSelected(total)
|
|
186
298
|
}
|
|
187
299
|
} catch (err) {
|
|
188
300
|
debug.log("results:load-more:error", err instanceof Error ? err.message : String(err))
|
|
@@ -193,9 +305,51 @@ export const Telescope = (props: { api: TuiPluginApi; onClose: () => void }) =>
|
|
|
193
305
|
}
|
|
194
306
|
}
|
|
195
307
|
|
|
308
|
+
const loadPreviousResults = (advance = false) => {
|
|
309
|
+
if (advance) advanceSelectionBeforeLoad = true
|
|
310
|
+
const base = resultBaseOffset()
|
|
311
|
+
if (base <= 0) {
|
|
312
|
+
advanceSelectionBeforeLoad = false
|
|
313
|
+
return
|
|
314
|
+
}
|
|
315
|
+
if (loadingMore() || loadingPreviousResults() || prefetchingResults() || busy() || loading()) return
|
|
316
|
+
|
|
317
|
+
const q = query().trim()
|
|
318
|
+
const role = ownerRole()
|
|
319
|
+
const db = dbPath()
|
|
320
|
+
const dir = directory
|
|
321
|
+
const pageSize = q ? searchBatchSize() : recentBatchSize()
|
|
322
|
+
const offset = Math.max(0, base - pageSize)
|
|
323
|
+
const limit = base - offset
|
|
324
|
+
|
|
325
|
+
setLoadingPreviousResults(true)
|
|
326
|
+
debug.time("query:load-before")
|
|
327
|
+
try {
|
|
328
|
+
const batch = q
|
|
329
|
+
? searchSessionMessages(q, { limit, offset, dbPath: db, directory: dir, role })
|
|
330
|
+
: recentSessionMessages({ limit, offset, dbPath: db, directory: dir, role })
|
|
331
|
+
const nextSelected = advanceSelectionBeforeLoad && batch.length > 0 ? base - 1 : selected()
|
|
332
|
+
debug.log("results:load-before", { offset, limit, added: batch.length, fromBase: base, toBase: offset, cached: results().length + batch.length, advance })
|
|
333
|
+
const shouldAdvance = advanceSelectionBeforeLoad
|
|
334
|
+
solidBatch(() => {
|
|
335
|
+
setResultBaseOffset(offset)
|
|
336
|
+
setResults([...batch, ...results()])
|
|
337
|
+
setResultPageInfo({ loadedUntil: nextResultOffset(), hasMore: hasMore(), pageSize, lastOffset: offset, lastAdded: batch.length })
|
|
338
|
+
if (shouldAdvance && batch.length > 0) setSelected(nextSelected)
|
|
339
|
+
})
|
|
340
|
+
if (shouldAdvance) advanceSelectionBeforeLoad = false
|
|
341
|
+
} catch (err) {
|
|
342
|
+
debug.log("results:load-before:error", err instanceof Error ? err.message : String(err))
|
|
343
|
+
advanceSelectionBeforeLoad = false
|
|
344
|
+
} finally {
|
|
345
|
+
debug.timeEnd("query:load-before")
|
|
346
|
+
setLoadingPreviousResults(false)
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
196
350
|
const scheduleResultPrefetch = (advance = false) => {
|
|
197
351
|
if (advance) advanceSelectionAfterLoad = true
|
|
198
|
-
if (resultPrefetchTimer || loadingMore() || prefetchingResults()) return
|
|
352
|
+
if (resultPrefetchTimer || loadingMore() || loadingPreviousResults() || prefetchingResults()) return
|
|
199
353
|
debug.log("results:prefetch-scheduled", { advance, pendingAdvance: advanceSelectionAfterLoad, pageInfo: resultPageInfo() })
|
|
200
354
|
resultPrefetchTimer = setTimeout(() => {
|
|
201
355
|
resultPrefetchTimer = undefined
|
|
@@ -203,15 +357,33 @@ export const Telescope = (props: { api: TuiPluginApi; onClose: () => void }) =>
|
|
|
203
357
|
}, 1)
|
|
204
358
|
}
|
|
205
359
|
|
|
360
|
+
const schedulePreviousResultsLoad = (advance = false) => {
|
|
361
|
+
if (advance) advanceSelectionBeforeLoad = true
|
|
362
|
+
if (resultPreviousTimer || loadingMore() || loadingPreviousResults() || prefetchingResults()) return
|
|
363
|
+
debug.log("results:load-before-scheduled", { advance, pendingAdvance: advanceSelectionBeforeLoad, baseOffset: resultBaseOffset(), pageInfo: resultPageInfo() })
|
|
364
|
+
resultPreviousTimer = setTimeout(() => {
|
|
365
|
+
resultPreviousTimer = undefined
|
|
366
|
+
loadPreviousResults(advanceSelectionBeforeLoad)
|
|
367
|
+
}, 1)
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
let lastResultPrefetchDecision = ""
|
|
371
|
+
|
|
206
372
|
const move = (delta: number) => {
|
|
207
373
|
if (results().length === 0) return
|
|
208
374
|
setSelected((index) => {
|
|
375
|
+
const base = resultBaseOffset()
|
|
376
|
+
const cachedEnd = base + results().length
|
|
209
377
|
const next = index + delta
|
|
210
378
|
let finalIndex = next
|
|
211
|
-
if (next < 0) finalIndex =
|
|
212
|
-
else if (next
|
|
379
|
+
if (next < 0) finalIndex = cachedEnd - 1
|
|
380
|
+
else if (next < base) {
|
|
381
|
+
schedulePreviousResultsLoad(true)
|
|
382
|
+
finalIndex = base
|
|
383
|
+
}
|
|
384
|
+
else if (next >= cachedEnd) {
|
|
213
385
|
if (hasMore()) scheduleResultPrefetch(true)
|
|
214
|
-
finalIndex =
|
|
386
|
+
finalIndex = cachedEnd - 1
|
|
215
387
|
}
|
|
216
388
|
|
|
217
389
|
if (finalIndex !== index) debug.time("nav:total")
|
|
@@ -235,11 +407,26 @@ export const Telescope = (props: { api: TuiPluginApi; onClose: () => void }) =>
|
|
|
235
407
|
})
|
|
236
408
|
|
|
237
409
|
createEffect(() => {
|
|
238
|
-
const
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
410
|
+
const state = resultPrefetchState()
|
|
411
|
+
const blockedBy = !hasMore()
|
|
412
|
+
? "no-more"
|
|
413
|
+
: loadingMore()
|
|
414
|
+
? "loading-more"
|
|
415
|
+
: loadingPreviousResults()
|
|
416
|
+
? "loading-before"
|
|
417
|
+
: prefetchingResults()
|
|
418
|
+
? "prefetching"
|
|
419
|
+
: busy()
|
|
420
|
+
? "busy"
|
|
421
|
+
: loading()
|
|
422
|
+
? "loading"
|
|
423
|
+
: ""
|
|
424
|
+
const decisionKey = `${selected()}:${state.cachedEnd}:${state.rowsAhead}:${state.threshold}:${blockedBy}:${state.shouldPrefetch}`
|
|
425
|
+
if (decisionKey !== lastResultPrefetchDecision) {
|
|
426
|
+
lastResultPrefetchDecision = decisionKey
|
|
427
|
+
debug.log("results:prefetch-decision", { selected: selected(), ...state, blockedBy: blockedBy || undefined })
|
|
428
|
+
}
|
|
429
|
+
if (!state.shouldPrefetch || blockedBy) return
|
|
243
430
|
|
|
244
431
|
const timer = setTimeout(() => scheduleResultPrefetch(false), 100)
|
|
245
432
|
onCleanup(() => clearTimeout(timer))
|
|
@@ -251,19 +438,26 @@ export const Telescope = (props: { api: TuiPluginApi; onClose: () => void }) =>
|
|
|
251
438
|
return lastChild ? lastChild.y + lastChild.height : 0
|
|
252
439
|
}
|
|
253
440
|
|
|
254
|
-
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) => {
|
|
255
447
|
const item = selectedResult()
|
|
256
448
|
const first = previewParts()[0]
|
|
257
|
-
if (!item || !first || loadingPreviewMore()) return
|
|
258
|
-
setLoadingPreviewMore(true)
|
|
449
|
+
if (!item || item.id !== itemId || !first || loadingPreviewMore() || prefetchingPreviewBefore()) return
|
|
450
|
+
visibleLoad ? setLoadingPreviewMore(true) : setPrefetchingPreviewBefore(true)
|
|
259
451
|
debug.time("preview:load-before")
|
|
260
452
|
try {
|
|
261
453
|
const page = loadConversationBefore(item, { id: first.id, timeCreated: first.timeCreated }, { limit: PREVIEW_PAGE_SIZE, dbPath: dbPath() })
|
|
454
|
+
if (selectedResult()?.id !== itemId) return
|
|
262
455
|
debug.log("preview:load-before", {
|
|
263
456
|
item: item.id,
|
|
264
457
|
added: page.parts.length,
|
|
265
458
|
hasMoreBefore: page.hasMoreBefore,
|
|
266
459
|
preserveScroll,
|
|
460
|
+
visibleLoad,
|
|
267
461
|
first: page.parts[0]?.id,
|
|
268
462
|
last: page.parts.at(-1)?.id,
|
|
269
463
|
})
|
|
@@ -271,6 +465,7 @@ export const Telescope = (props: { api: TuiPluginApi; onClose: () => void }) =>
|
|
|
271
465
|
setPreviewParts((prev) => [...page.parts, ...prev])
|
|
272
466
|
if (preserveScroll) {
|
|
273
467
|
setTimeout(() => {
|
|
468
|
+
if (selectedResult()?.id !== itemId) return
|
|
274
469
|
const delta = previewContentHeight() - previousContentHeight
|
|
275
470
|
if (delta > 0) previewScroll?.scrollBy(delta)
|
|
276
471
|
}, 1)
|
|
@@ -281,22 +476,24 @@ export const Telescope = (props: { api: TuiPluginApi; onClose: () => void }) =>
|
|
|
281
476
|
debug.log("preview:load-before:error", err instanceof Error ? err.message : String(err))
|
|
282
477
|
} finally {
|
|
283
478
|
debug.timeEnd("preview:load-before")
|
|
284
|
-
setLoadingPreviewMore(false)
|
|
479
|
+
visibleLoad ? setLoadingPreviewMore(false) : setPrefetchingPreviewBefore(false)
|
|
285
480
|
}
|
|
286
481
|
}
|
|
287
482
|
|
|
288
|
-
const loadPreviewAfter = () => {
|
|
483
|
+
const loadPreviewAfter = (itemId: string, visibleLoad = false) => {
|
|
289
484
|
const item = selectedResult()
|
|
290
485
|
const last = previewParts().at(-1)
|
|
291
|
-
if (!item || !last || loadingPreviewMore()) return
|
|
292
|
-
setLoadingPreviewMore(true)
|
|
486
|
+
if (!item || item.id !== itemId || !last || loadingPreviewMore() || prefetchingPreviewAfter()) return
|
|
487
|
+
visibleLoad ? setLoadingPreviewMore(true) : setPrefetchingPreviewAfter(true)
|
|
293
488
|
debug.time("preview:load-after")
|
|
294
489
|
try {
|
|
295
490
|
const page = loadConversationAfter(item, { id: last.id, timeCreated: last.timeCreated }, { limit: PREVIEW_PAGE_SIZE, dbPath: dbPath() })
|
|
491
|
+
if (selectedResult()?.id !== itemId) return
|
|
296
492
|
debug.log("preview:load-after", {
|
|
297
493
|
item: item.id,
|
|
298
494
|
added: page.parts.length,
|
|
299
495
|
hasMoreAfter: page.hasMoreAfter,
|
|
496
|
+
visibleLoad,
|
|
300
497
|
first: page.parts[0]?.id,
|
|
301
498
|
last: page.parts.at(-1)?.id,
|
|
302
499
|
})
|
|
@@ -306,14 +503,49 @@ export const Telescope = (props: { api: TuiPluginApi; onClose: () => void }) =>
|
|
|
306
503
|
debug.log("preview:load-after:error", err instanceof Error ? err.message : String(err))
|
|
307
504
|
} finally {
|
|
308
505
|
debug.timeEnd("preview:load-after")
|
|
309
|
-
setLoadingPreviewMore(false)
|
|
506
|
+
visibleLoad ? setLoadingPreviewMore(false) : setPrefetchingPreviewAfter(false)
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
const schedulePreviewBefore = (previousContentHeight = previewContentHeight(), preserveScroll = true, visibleLoad = false) => {
|
|
511
|
+
const item = selectedResult()
|
|
512
|
+
if (!item || !hasMorePreviewBefore() || loadingPreviewMore() || prefetchingPreviewBefore()) return
|
|
513
|
+
if (previewBeforeTimer) {
|
|
514
|
+
if (pendingPreviewBefore) {
|
|
515
|
+
pendingPreviewBefore.preserveScroll = pendingPreviewBefore.preserveScroll && preserveScroll
|
|
516
|
+
pendingPreviewBefore.visibleLoad = pendingPreviewBefore.visibleLoad || visibleLoad
|
|
517
|
+
}
|
|
518
|
+
return
|
|
310
519
|
}
|
|
520
|
+
pendingPreviewBefore = { itemId: item.id, previousContentHeight, preserveScroll, visibleLoad }
|
|
521
|
+
debug.log("preview:prefetch-before-scheduled", { item: item.id, preserveScroll, visibleLoad })
|
|
522
|
+
previewBeforeTimer = setTimeout(() => {
|
|
523
|
+
const pending = pendingPreviewBefore
|
|
524
|
+
previewBeforeTimer = undefined
|
|
525
|
+
pendingPreviewBefore = undefined
|
|
526
|
+
if (pending && selectedResult()?.id === pending.itemId) loadPreviewBefore(pending.itemId, pending.previousContentHeight, pending.preserveScroll, pending.visibleLoad)
|
|
527
|
+
}, 1)
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
const schedulePreviewAfter = (visibleLoad = false) => {
|
|
531
|
+
const item = selectedResult()
|
|
532
|
+
if (!item || !hasMorePreviewAfter() || loadingPreviewMore() || prefetchingPreviewAfter()) return
|
|
533
|
+
pendingPreviewAfter = { itemId: item.id, visibleLoad: (pendingPreviewAfter?.visibleLoad || visibleLoad) }
|
|
534
|
+
if (previewAfterTimer) return
|
|
535
|
+
debug.log("preview:prefetch-after-scheduled", { item: item.id, visibleLoad })
|
|
536
|
+
previewAfterTimer = setTimeout(() => {
|
|
537
|
+
const pending = pendingPreviewAfter
|
|
538
|
+
previewAfterTimer = undefined
|
|
539
|
+
pendingPreviewAfter = undefined
|
|
540
|
+
if (pending && selectedResult()?.id === pending.itemId) loadPreviewAfter(pending.itemId, pending.visibleLoad)
|
|
541
|
+
}, 1)
|
|
311
542
|
}
|
|
312
543
|
|
|
313
544
|
let lastPreviewItemId = ""
|
|
314
545
|
createEffect(() => {
|
|
315
546
|
const item = selectedResult()
|
|
316
547
|
if (!item) {
|
|
548
|
+
cancelPreviewPrefetch()
|
|
317
549
|
setPreviewParts([])
|
|
318
550
|
setHasMorePreviewBefore(false)
|
|
319
551
|
setHasMorePreviewAfter(false)
|
|
@@ -321,56 +553,92 @@ export const Telescope = (props: { api: TuiPluginApi; onClose: () => void }) =>
|
|
|
321
553
|
}
|
|
322
554
|
if (item.id === lastPreviewItemId) return
|
|
323
555
|
lastPreviewItemId = item.id
|
|
556
|
+
cancelPreviewPrefetch()
|
|
557
|
+
resetPreviewScroll()
|
|
558
|
+
solidBatch(() => {
|
|
559
|
+
setPreviewParts([])
|
|
560
|
+
setHasMorePreviewBefore(false)
|
|
561
|
+
setHasMorePreviewAfter(false)
|
|
562
|
+
setPreviewEdgeLoadingReady(false)
|
|
563
|
+
})
|
|
324
564
|
debug.log("preview:new-item", item.sessionTitle?.slice(0, 40) ?? item.id.slice(-8))
|
|
325
565
|
const db = dbPath()
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
const page = loadConversationAround(item, { before: INITIAL_PREVIEW_BEFORE, after: INITIAL_PREVIEW_AFTER, dbPath: db })
|
|
329
|
-
debug.log("preview:init", {
|
|
330
|
-
item: item.id,
|
|
331
|
-
session: item.sessionID,
|
|
332
|
-
parts: page.parts.length,
|
|
333
|
-
hasMoreBefore: page.hasMoreBefore,
|
|
334
|
-
hasMoreAfter: page.hasMoreAfter,
|
|
335
|
-
first: page.parts[0]?.id,
|
|
336
|
-
last: page.parts.at(-1)?.id,
|
|
337
|
-
})
|
|
338
|
-
setPreviewParts(page.parts)
|
|
339
|
-
setHasMorePreviewBefore(page.hasMoreBefore)
|
|
340
|
-
setHasMorePreviewAfter(page.hasMoreAfter)
|
|
341
|
-
} 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 })
|
|
342
568
|
debug.timeEnd("nav:total")
|
|
343
|
-
|
|
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
|
+
})
|
|
344
606
|
})
|
|
345
607
|
|
|
346
608
|
createEffect(() => {
|
|
347
609
|
const item = selectedResult()
|
|
348
610
|
if (!item) return
|
|
349
611
|
const interval = setInterval(() => {
|
|
350
|
-
if (loadingPreviewMore()) return
|
|
612
|
+
if (!previewEdgeLoadingReady() || loadingPreviewMore() || prefetchingPreviewBefore() || prefetchingPreviewAfter()) return
|
|
351
613
|
const scroll = previewScroll
|
|
352
614
|
const children = scroll?.getChildren()
|
|
353
615
|
if (!scroll || !children || children.length === 0) return
|
|
354
616
|
const lastChild = children[children.length - 1] as { y: number; height: number }
|
|
355
617
|
const totalContentHeight = lastChild.y + lastChild.height
|
|
356
618
|
const atTop = scroll.y <= 0
|
|
357
|
-
const
|
|
619
|
+
const prefetchDistance = Math.max(2, Math.floor(scroll.height * PREVIEW_PREFETCH_VIEWPORTS))
|
|
620
|
+
const nearTop = scroll.y <= prefetchDistance
|
|
358
621
|
const atBottom = scroll.y + scroll.height >= totalContentHeight - 1
|
|
359
|
-
|
|
622
|
+
const nearBottom = scroll.y + scroll.height >= totalContentHeight - prefetchDistance
|
|
623
|
+
if (nearTop || nearBottom) {
|
|
360
624
|
debug.log("preview:scroll-edge", {
|
|
361
625
|
y: scroll.y,
|
|
362
626
|
height: scroll.height,
|
|
363
627
|
contentHeight: totalContentHeight,
|
|
628
|
+
prefetchDistance,
|
|
364
629
|
atTop,
|
|
365
630
|
nearTop,
|
|
366
631
|
atBottom,
|
|
632
|
+
nearBottom,
|
|
367
633
|
hasMoreBefore: hasMorePreviewBefore(),
|
|
368
634
|
hasMoreAfter: hasMorePreviewAfter(),
|
|
635
|
+
prefetchingBefore: prefetchingPreviewBefore(),
|
|
636
|
+
prefetchingAfter: prefetchingPreviewAfter(),
|
|
369
637
|
children: children.length,
|
|
370
638
|
})
|
|
371
639
|
}
|
|
372
|
-
if (
|
|
373
|
-
if (
|
|
640
|
+
if (atTop && hasMorePreviewBefore()) schedulePreviewBefore(totalContentHeight, false, true)
|
|
641
|
+
if (nearBottom && hasMorePreviewAfter()) schedulePreviewAfter(atBottom)
|
|
374
642
|
}, 400)
|
|
375
643
|
onCleanup(() => clearInterval(interval))
|
|
376
644
|
})
|
|
@@ -382,8 +650,19 @@ export const Telescope = (props: { api: TuiPluginApi; onClose: () => void }) =>
|
|
|
382
650
|
if (!item) return
|
|
383
651
|
if (item.id === scrolledItem) return
|
|
384
652
|
scrolledItem = item.id
|
|
385
|
-
const
|
|
386
|
-
|
|
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
|
+
})
|
|
387
666
|
})
|
|
388
667
|
|
|
389
668
|
const open = () => {
|
|
@@ -409,50 +688,54 @@ export const Telescope = (props: { api: TuiPluginApi; onClose: () => void }) =>
|
|
|
409
688
|
el?.blur?.()
|
|
410
689
|
}
|
|
411
690
|
|
|
691
|
+
const toggleOwnerFilter = () => {
|
|
692
|
+
setOwnerFilter((filter) => filter === "all" ? "user" : filter === "user" ? "assistant" : "all")
|
|
693
|
+
}
|
|
694
|
+
|
|
412
695
|
useKeyboard((evt) => {
|
|
413
696
|
if (!props.api.ui.dialog.open) return
|
|
414
697
|
|
|
415
|
-
if (
|
|
698
|
+
if (mode() !== "normal") return
|
|
699
|
+
|
|
700
|
+
if (matchesKey(evt, props.config.keys.moveDown) || matchesKey(evt, props.config.keys.moveUp)) {
|
|
416
701
|
prevent(evt)
|
|
417
|
-
|
|
702
|
+
matchesKey(evt, props.config.keys.moveDown) ? move(1) : move(-1)
|
|
418
703
|
return
|
|
419
704
|
}
|
|
420
705
|
|
|
421
|
-
if (
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
return
|
|
455
|
-
}
|
|
706
|
+
if (matchesKey(evt, props.config.keys.open)) {
|
|
707
|
+
prevent(evt)
|
|
708
|
+
open()
|
|
709
|
+
return
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
if (matchesKey(evt, props.config.keys.close)) {
|
|
713
|
+
prevent(evt)
|
|
714
|
+
props.onClose()
|
|
715
|
+
return
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
if (matchesKey(evt, props.config.keys.scrollPreviewDown)) {
|
|
719
|
+
scrollPreview(1, evt)
|
|
720
|
+
return
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
if (matchesKey(evt, props.config.keys.scrollPreviewUp)) {
|
|
724
|
+
scrollPreview(-1, evt)
|
|
725
|
+
return
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
if (matchesKey(evt, props.config.keys.toggleOwner)) {
|
|
729
|
+
prevent(evt)
|
|
730
|
+
toggleOwnerFilter()
|
|
731
|
+
return
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
if (matchesKey(evt, props.config.keys.insertMode)) {
|
|
735
|
+
prevent(evt)
|
|
736
|
+
setMode("insert")
|
|
737
|
+
focusInput()
|
|
738
|
+
return
|
|
456
739
|
}
|
|
457
740
|
})
|
|
458
741
|
|
|
@@ -483,12 +766,17 @@ export const Telescope = (props: { api: TuiPluginApi; onClose: () => void }) =>
|
|
|
483
766
|
focusedBackgroundColor={theme().backgroundPanel}
|
|
484
767
|
onInput={(value) => setQuery(value)}
|
|
485
768
|
onKeyDown={(evt: ParsedKey) => {
|
|
486
|
-
if (
|
|
769
|
+
if (matchesKey(evt, inputKeys().moveDown) || matchesKey(evt, inputKeys().moveUp)) {
|
|
487
770
|
prevent(evt)
|
|
488
|
-
|
|
771
|
+
matchesKey(evt, inputKeys().moveDown) ? move(1) : move(-1)
|
|
489
772
|
return
|
|
490
773
|
}
|
|
491
|
-
if (
|
|
774
|
+
if (matchesKey(evt, inputKeys().open)) {
|
|
775
|
+
prevent(evt)
|
|
776
|
+
open()
|
|
777
|
+
return
|
|
778
|
+
}
|
|
779
|
+
if (matchesKey(evt, inputKeys().normalMode)) {
|
|
492
780
|
prevent(evt)
|
|
493
781
|
setMode("normal")
|
|
494
782
|
blurInput()
|
|
@@ -496,13 +784,13 @@ export const Telescope = (props: { api: TuiPluginApi; onClose: () => void }) =>
|
|
|
496
784
|
}}
|
|
497
785
|
flexGrow={1}
|
|
498
786
|
/>
|
|
499
|
-
<text fg={theme().textMuted}>{busy() ?
|
|
787
|
+
<text fg={theme().textMuted}>{busy() ? `searching ${ownerLabel()}` : loading() ? `loading ${ownerLabel()}` : query().trim() ? (results().length > 0 ? `${ownerLabel()} ${selected() + 1}/${nextResultOffset()} hits` : `${ownerLabel()} 0 hits`) : (results().length > 0 ? `${ownerLabel()} ${selected() + 1}/${nextResultOffset()} recent` : `${ownerLabel()} 0 recent`)}</text>
|
|
500
788
|
</box>
|
|
501
789
|
</box>
|
|
502
790
|
|
|
503
791
|
<box flexDirection="row" flexGrow={1} minHeight={0}>
|
|
504
792
|
<box width={leftWidth()} flexDirection="column" minHeight={0} backgroundColor={theme().backgroundPanel}>
|
|
505
|
-
<scrollbox ref={(element: ScrollBoxRenderable) => (resultScroll = element)} flexGrow={1} minHeight={0} verticalScrollbarOptions={{ visible:
|
|
793
|
+
<scrollbox ref={(element: ScrollBoxRenderable) => (resultScroll = element)} flexGrow={1} minHeight={0} verticalScrollbarOptions={{ visible: true }}>
|
|
506
794
|
<Show
|
|
507
795
|
when={!error()}
|
|
508
796
|
fallback={
|
|
@@ -513,7 +801,7 @@ export const Telescope = (props: { api: TuiPluginApi; onClose: () => void }) =>
|
|
|
513
801
|
}
|
|
514
802
|
>
|
|
515
803
|
<Show when={!loading()}>
|
|
516
|
-
<Show when={results().length > 0} fallback={<EmptyState query={query()} theme={theme()} />}>
|
|
804
|
+
<Show when={results().length > 0} fallback={<EmptyState query={query()} owner={ownerLabel()} theme={theme()} />}>
|
|
517
805
|
<For each={resultRenderWindow().items}>
|
|
518
806
|
{(item, index) => {
|
|
519
807
|
const absoluteIndex = () => resultRenderWindow().start + index()
|
|
@@ -565,23 +853,21 @@ export const Telescope = (props: { api: TuiPluginApi; onClose: () => void }) =>
|
|
|
565
853
|
<Show when={mode() === "normal"}>
|
|
566
854
|
<box paddingLeft={4} paddingRight={4} flexDirection="row" backgroundColor={theme().backgroundElement} gap={2}>
|
|
567
855
|
<text fg={theme().accent}><span style={{ bold: true }}>NORMAL</span></text>
|
|
568
|
-
<
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
<text fg={theme().textMuted}>·</text>
|
|
577
|
-
<text fg={theme().text}>q close</text>
|
|
856
|
+
<For each={normalHelpItems()}>
|
|
857
|
+
{(item, index) => (
|
|
858
|
+
<>
|
|
859
|
+
<text fg={theme().textMuted}>·</text>
|
|
860
|
+
<text fg={index() % 2 === 0 ? theme().text : theme().textMuted}>{item}</text>
|
|
861
|
+
</>
|
|
862
|
+
)}
|
|
863
|
+
</For>
|
|
578
864
|
</box>
|
|
579
865
|
</Show>
|
|
580
866
|
<Show when={mode() === "insert"}>
|
|
581
867
|
<box paddingLeft={4} paddingRight={4} flexDirection="row" backgroundColor={theme().backgroundElement} gap={2}>
|
|
582
868
|
<text fg={theme().warning}><span style={{ bold: true }}>INSERT</span></text>
|
|
583
869
|
<text fg={theme().textMuted}>·</text>
|
|
584
|
-
<text fg={theme().textMuted}
|
|
870
|
+
<text fg={theme().textMuted}>{keyListLabel(inputKeys().moveUp)}/{keyListLabel(inputKeys().moveDown)} move · {keyListLabel(inputKeys().normalMode)} normal</text>
|
|
585
871
|
</box>
|
|
586
872
|
</Show>
|
|
587
873
|
|