@bojackduy/opencode-telescope 0.1.12 → 0.1.14
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 +144 -22
- package/telescope.tsx +545 -127
- package/tui.tsx +4 -2
- package/ui/config.test.ts +48 -0
- package/ui/config.ts +93 -0
- package/ui/debug.ts +31 -2
- package/ui/keyboard.test.ts +40 -0
- package/ui/keyboard.ts +34 -0
package/telescope.tsx
CHANGED
|
@@ -2,72 +2,209 @@
|
|
|
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 {
|
|
9
|
-
|
|
9
|
+
loadConversationAfter,
|
|
10
|
+
loadConversationAround,
|
|
11
|
+
loadConversationBefore,
|
|
10
12
|
recentSessionMessages,
|
|
11
13
|
resolveDatabasePath,
|
|
12
14
|
searchSessionMessages,
|
|
13
15
|
type ConversationPreviewPart,
|
|
14
16
|
type SearchResult,
|
|
17
|
+
type SearchRole,
|
|
15
18
|
} from "./search.ts"
|
|
16
19
|
import { debug } from "./ui/debug.ts"
|
|
17
20
|
import { syntaxStyle } from "./ui/format.ts"
|
|
18
|
-
import {
|
|
21
|
+
import type { TelescopeConfig } from "./ui/config.ts"
|
|
22
|
+
import { inputSafeKeys, keyListLabel, matchesKey, prevent } from "./ui/keyboard.ts"
|
|
19
23
|
import { jumpToRenderedTarget, messageTargetID, previewScrollAmount, scrollPreviewToTarget } from "./ui/render-target.ts"
|
|
20
24
|
|
|
21
|
-
export const Telescope = (props: { api: TuiPluginApi; onClose: () => void }) => {
|
|
25
|
+
export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; onClose: () => void }) => {
|
|
26
|
+
type OwnerFilter = "all" | SearchRole
|
|
22
27
|
const dimensions = useTerminalDimensions()
|
|
23
28
|
const [query, setQuery] = createSignal("")
|
|
29
|
+
const [ownerFilter, setOwnerFilter] = createSignal<OwnerFilter>("all")
|
|
24
30
|
const [results, setResults] = createSignal<SearchResult[]>([])
|
|
25
31
|
const [previewParts, setPreviewParts] = createSignal<ConversationPreviewPart[]>([])
|
|
26
32
|
const [selected, setSelected] = createSignal(0)
|
|
33
|
+
const [resultBaseOffset, setResultBaseOffset] = createSignal(0)
|
|
34
|
+
const [nextResultOffset, setNextResultOffset] = createSignal(0)
|
|
27
35
|
const [busy, setBusy] = createSignal(false)
|
|
28
36
|
const [error, setError] = createSignal("")
|
|
29
37
|
const [mode, setMode] = createSignal<"normal" | "insert">("normal")
|
|
30
38
|
const [loading, setLoading] = createSignal(true)
|
|
31
39
|
const [hasMore, setHasMore] = createSignal(true)
|
|
32
40
|
const [loadingMore, setLoadingMore] = createSignal(false)
|
|
33
|
-
const
|
|
34
|
-
const
|
|
35
|
-
const
|
|
36
|
-
const
|
|
37
|
-
const
|
|
38
|
-
const [
|
|
41
|
+
const [loadingPreviousResults, setLoadingPreviousResults] = createSignal(false)
|
|
42
|
+
const [prefetchingResults, setPrefetchingResults] = createSignal(false)
|
|
43
|
+
const [resultPageInfo, setResultPageInfo] = createSignal({ loadedUntil: 0, hasMore: true, pageSize: 0, lastOffset: 0, lastAdded: 0 })
|
|
44
|
+
const [hasMorePreviewBefore, setHasMorePreviewBefore] = createSignal(false)
|
|
45
|
+
const [hasMorePreviewAfter, setHasMorePreviewAfter] = createSignal(false)
|
|
46
|
+
const [loadingPreviewMore, setLoadingPreviewMore] = createSignal(false)
|
|
47
|
+
const [prefetchingPreviewBefore, setPrefetchingPreviewBefore] = createSignal(false)
|
|
48
|
+
const [prefetchingPreviewAfter, setPrefetchingPreviewAfter] = createSignal(false)
|
|
49
|
+
const MIN_SEARCH_BATCH_SIZE = 25
|
|
50
|
+
const MIN_RECENT_BATCH_SIZE = 15
|
|
51
|
+
const RESULT_OVERSCAN_MULTIPLIER = 2
|
|
52
|
+
const RESULT_BATCH_VIEWPORTS = 4
|
|
53
|
+
const RESULT_PREFETCH_VIEWPORTS = 10
|
|
54
|
+
const RESULT_CACHE_BEHIND_VIEWPORTS = 6
|
|
55
|
+
const INITIAL_PREVIEW_BEFORE = 20
|
|
56
|
+
const INITIAL_PREVIEW_AFTER = 30
|
|
57
|
+
const PREVIEW_PAGE_SIZE = 20
|
|
58
|
+
const PREVIEW_PREFETCH_VIEWPORTS = 0.5
|
|
39
59
|
let input: InputRenderable | undefined
|
|
40
60
|
let resultScroll: ScrollBoxRenderable | undefined
|
|
41
61
|
let previewScroll: ScrollBoxRenderable | undefined
|
|
42
62
|
|
|
43
63
|
const theme = createMemo(() => props.api.theme.current)
|
|
44
64
|
const syntax = createMemo(() => syntaxStyle(theme()))
|
|
45
|
-
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()])
|
|
46
82
|
const popupWidth = createMemo(() => Math.max(72, Math.min(dimensions().width - 2, Math.floor(dimensions().width * 0.92))))
|
|
47
83
|
const leftWidth = createMemo(() => Math.max(36, Math.min(64, Math.floor(popupWidth() * 0.36))))
|
|
48
84
|
const height = createMemo(() => Math.max(18, dimensions().height - 8))
|
|
49
85
|
const verticalOffset = createMemo(() => Math.floor(dimensions().height / 4 - height() / 2) - 2)
|
|
50
86
|
const dbPath = createMemo(() => resolveDatabasePath())
|
|
51
87
|
const directory = props.api.state.path.directory
|
|
88
|
+
let advanceSelectionAfterLoad = false
|
|
89
|
+
let advanceSelectionBeforeLoad = false
|
|
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: { previousContentHeight: number; preserveScroll: boolean; visibleLoad: boolean } | undefined
|
|
95
|
+
let pendingPreviewAfterVisible = false
|
|
96
|
+
const cancelPreviewPrefetch = () => {
|
|
97
|
+
if (previewBeforeTimer) clearTimeout(previewBeforeTimer)
|
|
98
|
+
if (previewAfterTimer) clearTimeout(previewAfterTimer)
|
|
99
|
+
previewBeforeTimer = undefined
|
|
100
|
+
previewAfterTimer = undefined
|
|
101
|
+
pendingPreviewBefore = undefined
|
|
102
|
+
pendingPreviewAfterVisible = false
|
|
103
|
+
setPrefetchingPreviewBefore(false)
|
|
104
|
+
setPrefetchingPreviewAfter(false)
|
|
105
|
+
setLoadingPreviewMore(false)
|
|
106
|
+
}
|
|
107
|
+
onCleanup(() => {
|
|
108
|
+
if (resultPrefetchTimer) clearTimeout(resultPrefetchTimer)
|
|
109
|
+
if (resultPreviousTimer) clearTimeout(resultPreviousTimer)
|
|
110
|
+
cancelPreviewPrefetch()
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
const resultRowHeight = createMemo(() => leftWidth() >= 48 ? 3 : 4)
|
|
114
|
+
const visibleResultRows = () => {
|
|
115
|
+
const viewportHeight = resultScroll?.height || Math.max(8, height() - 7)
|
|
116
|
+
return Math.max(5, Math.floor(viewportHeight / resultRowHeight()))
|
|
117
|
+
}
|
|
118
|
+
const searchBatchSize = () => Math.max(MIN_SEARCH_BATCH_SIZE, visibleResultRows() * RESULT_BATCH_VIEWPORTS)
|
|
119
|
+
const recentBatchSize = () => Math.max(MIN_RECENT_BATCH_SIZE, visibleResultRows() * RESULT_BATCH_VIEWPORTS)
|
|
120
|
+
const resultPrefetchThreshold = () => visibleResultRows() * RESULT_PREFETCH_VIEWPORTS
|
|
121
|
+
const resultPrefetchState = () => {
|
|
122
|
+
const cachedEnd = resultBaseOffset() + results().length
|
|
123
|
+
const rowsAhead = Math.max(0, cachedEnd - selected() - 1)
|
|
124
|
+
const threshold = resultPrefetchThreshold()
|
|
125
|
+
return { cachedEnd, rowsAhead, threshold, shouldPrefetch: rowsAhead <= threshold }
|
|
126
|
+
}
|
|
127
|
+
const trimResultCache = (items: SearchResult[], anchorIndex: number) => {
|
|
128
|
+
const base = resultBaseOffset()
|
|
129
|
+
const keepBehind = visibleResultRows() * RESULT_CACHE_BEHIND_VIEWPORTS
|
|
130
|
+
const minOffset = Math.max(0, anchorIndex - keepBehind)
|
|
131
|
+
const drop = Math.min(Math.max(0, minOffset - base), Math.max(0, items.length - 1))
|
|
132
|
+
if (drop === 0) return { base, items }
|
|
133
|
+
|
|
134
|
+
const nextBase = base + drop
|
|
135
|
+
debug.log("results:evict", { fromBase: base, toBase: nextBase, dropped: drop, kept: items.length - drop, anchorIndex })
|
|
136
|
+
return { base: nextBase, items: items.slice(drop) }
|
|
137
|
+
}
|
|
138
|
+
const resultRenderWindow = createMemo(() => {
|
|
139
|
+
const base = resultBaseOffset()
|
|
140
|
+
const cached = results()
|
|
141
|
+
const visible = visibleResultRows()
|
|
142
|
+
const overscan = visible * RESULT_OVERSCAN_MULTIPLIER
|
|
143
|
+
const index = selected()
|
|
144
|
+
const cachedStart = Math.max(0, index - overscan - base)
|
|
145
|
+
const cachedEnd = Math.min(cached.length, index + visible + overscan - base)
|
|
146
|
+
return { start: base + cachedStart, end: base + cachedEnd, items: cached.slice(cachedStart, cachedEnd) }
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
let lastResultRenderWindow = ""
|
|
150
|
+
createEffect(() => {
|
|
151
|
+
const window = resultRenderWindow()
|
|
152
|
+
const key = `${window.start}:${window.end}:${resultBaseOffset()}:${nextResultOffset()}:${results().length}`
|
|
153
|
+
if (key === lastResultRenderWindow) return
|
|
154
|
+
lastResultRenderWindow = key
|
|
155
|
+
debug.log("results:render-window", {
|
|
156
|
+
selected: selected(),
|
|
157
|
+
baseOffset: resultBaseOffset(),
|
|
158
|
+
nextOffset: nextResultOffset(),
|
|
159
|
+
totalLoaded: results().length,
|
|
160
|
+
start: window.start,
|
|
161
|
+
end: window.end,
|
|
162
|
+
rendered: window.items.length,
|
|
163
|
+
visibleRows: visibleResultRows(),
|
|
164
|
+
overscanRows: visibleResultRows() * RESULT_OVERSCAN_MULTIPLIER,
|
|
165
|
+
pageInfo: resultPageInfo(),
|
|
166
|
+
})
|
|
167
|
+
})
|
|
52
168
|
|
|
53
169
|
createEffect(() => {
|
|
54
170
|
const q = query().trim()
|
|
171
|
+
const role = ownerRole()
|
|
55
172
|
setError("")
|
|
56
173
|
setHasMore(true)
|
|
174
|
+
if (resultPrefetchTimer) clearTimeout(resultPrefetchTimer)
|
|
175
|
+
if (resultPreviousTimer) clearTimeout(resultPreviousTimer)
|
|
176
|
+
resultPrefetchTimer = undefined
|
|
177
|
+
resultPreviousTimer = undefined
|
|
178
|
+
advanceSelectionAfterLoad = false
|
|
179
|
+
advanceSelectionBeforeLoad = false
|
|
180
|
+
setLoadingMore(false)
|
|
181
|
+
setLoadingPreviousResults(false)
|
|
182
|
+
setPrefetchingResults(false)
|
|
57
183
|
const db = dbPath()
|
|
58
184
|
const dir = directory
|
|
59
185
|
|
|
60
186
|
if (!q) {
|
|
61
187
|
setLoading(true)
|
|
188
|
+
const limit = recentBatchSize()
|
|
62
189
|
const timer = setTimeout(() => {
|
|
63
190
|
debug.time("query:recent")
|
|
64
191
|
try {
|
|
65
|
-
const batch = recentSessionMessages({ limit
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
192
|
+
const batch = recentSessionMessages({ limit, offset: 0, dbPath: db, directory: dir, role })
|
|
193
|
+
solidBatch(() => {
|
|
194
|
+
setResults(batch)
|
|
195
|
+
setResultBaseOffset(0)
|
|
196
|
+
setNextResultOffset(batch.length)
|
|
197
|
+
setHasMore(batch.length >= limit)
|
|
198
|
+
setResultPageInfo({ loadedUntil: batch.length, hasMore: batch.length >= limit, pageSize: limit, lastOffset: 0, lastAdded: batch.length })
|
|
199
|
+
setSelected(0)
|
|
200
|
+
})
|
|
69
201
|
} catch (err) {
|
|
70
|
-
|
|
202
|
+
solidBatch(() => {
|
|
203
|
+
setResults([])
|
|
204
|
+
setResultBaseOffset(0)
|
|
205
|
+
setNextResultOffset(0)
|
|
206
|
+
setResultPageInfo({ loadedUntil: 0, hasMore: false, pageSize: limit, lastOffset: 0, lastAdded: 0 })
|
|
207
|
+
})
|
|
71
208
|
setError(err instanceof Error ? err.message : String(err))
|
|
72
209
|
}
|
|
73
210
|
debug.timeEnd("query:recent")
|
|
@@ -79,15 +216,26 @@ export const Telescope = (props: { api: TuiPluginApi; onClose: () => void }) =>
|
|
|
79
216
|
|
|
80
217
|
setLoading(true)
|
|
81
218
|
setBusy(true)
|
|
219
|
+
const limit = searchBatchSize()
|
|
82
220
|
const timer = setTimeout(() => {
|
|
83
221
|
debug.time("query:search")
|
|
84
222
|
try {
|
|
85
|
-
const batch = searchSessionMessages(q, { limit
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
223
|
+
const batch = searchSessionMessages(q, { limit, offset: 0, dbPath: db, directory: dir, role })
|
|
224
|
+
solidBatch(() => {
|
|
225
|
+
setResults(batch)
|
|
226
|
+
setResultBaseOffset(0)
|
|
227
|
+
setNextResultOffset(batch.length)
|
|
228
|
+
setHasMore(batch.length >= limit)
|
|
229
|
+
setResultPageInfo({ loadedUntil: batch.length, hasMore: batch.length >= limit, pageSize: limit, lastOffset: 0, lastAdded: batch.length })
|
|
230
|
+
setSelected(0)
|
|
231
|
+
})
|
|
89
232
|
} catch (err) {
|
|
90
|
-
|
|
233
|
+
solidBatch(() => {
|
|
234
|
+
setResults([])
|
|
235
|
+
setResultBaseOffset(0)
|
|
236
|
+
setNextResultOffset(0)
|
|
237
|
+
setResultPageInfo({ loadedUntil: 0, hasMore: false, pageSize: limit, lastOffset: 0, lastAdded: 0 })
|
|
238
|
+
})
|
|
91
239
|
setError(err instanceof Error ? err.message : String(err))
|
|
92
240
|
} finally {
|
|
93
241
|
debug.timeEnd("query:search")
|
|
@@ -98,14 +246,145 @@ export const Telescope = (props: { api: TuiPluginApi; onClose: () => void }) =>
|
|
|
98
246
|
onCleanup(() => clearTimeout(timer))
|
|
99
247
|
})
|
|
100
248
|
|
|
249
|
+
const loadMoreResults = (advance = false) => {
|
|
250
|
+
if (advance) advanceSelectionAfterLoad = true
|
|
251
|
+
if (!hasMore()) {
|
|
252
|
+
advanceSelectionAfterLoad = false
|
|
253
|
+
return
|
|
254
|
+
}
|
|
255
|
+
if (loadingMore() || loadingPreviousResults() || prefetchingResults() || busy() || loading()) return
|
|
256
|
+
|
|
257
|
+
const offset = nextResultOffset()
|
|
258
|
+
const q = query().trim()
|
|
259
|
+
const role = ownerRole()
|
|
260
|
+
const db = dbPath()
|
|
261
|
+
const dir = directory
|
|
262
|
+
const limit = q ? searchBatchSize() : recentBatchSize()
|
|
263
|
+
|
|
264
|
+
advance ? setLoadingMore(true) : setPrefetchingResults(true)
|
|
265
|
+
debug.time("query:load-more")
|
|
266
|
+
try {
|
|
267
|
+
const batch = q
|
|
268
|
+
? searchSessionMessages(q, { limit, offset, dbPath: db, directory: dir, role })
|
|
269
|
+
: recentSessionMessages({ limit, offset, dbPath: db, directory: dir, role })
|
|
270
|
+
const nextHasMore = batch.length >= limit
|
|
271
|
+
const nextLoadedUntil = offset + batch.length
|
|
272
|
+
const previousSelected = selected()
|
|
273
|
+
const nextSelected = advanceSelectionAfterLoad && batch.length > 0 ? offset : selected()
|
|
274
|
+
const nextCache = trimResultCache([...results(), ...batch], nextSelected)
|
|
275
|
+
debug.log("results:prefetch", {
|
|
276
|
+
offset,
|
|
277
|
+
limit,
|
|
278
|
+
added: batch.length,
|
|
279
|
+
baseOffset: nextCache.base,
|
|
280
|
+
cached: nextCache.items.length,
|
|
281
|
+
totalLoaded: nextLoadedUntil,
|
|
282
|
+
hasMore: nextHasMore,
|
|
283
|
+
advance,
|
|
284
|
+
})
|
|
285
|
+
const shouldAdvance = advanceSelectionAfterLoad
|
|
286
|
+
solidBatch(() => {
|
|
287
|
+
setResultBaseOffset(nextCache.base)
|
|
288
|
+
setNextResultOffset(nextLoadedUntil)
|
|
289
|
+
setResults(nextCache.items)
|
|
290
|
+
setResultPageInfo({ loadedUntil: nextLoadedUntil, hasMore: nextHasMore, pageSize: limit, lastOffset: offset, lastAdded: batch.length })
|
|
291
|
+
if (!nextHasMore) setHasMore(false)
|
|
292
|
+
if (shouldAdvance && batch.length > 0) setSelected(offset)
|
|
293
|
+
})
|
|
294
|
+
if (shouldAdvance) {
|
|
295
|
+
debug.log("results:advance-after-load", { from: previousSelected, offset, added: batch.length })
|
|
296
|
+
advanceSelectionAfterLoad = false
|
|
297
|
+
}
|
|
298
|
+
} catch (err) {
|
|
299
|
+
debug.log("results:load-more:error", err instanceof Error ? err.message : String(err))
|
|
300
|
+
advanceSelectionAfterLoad = false
|
|
301
|
+
} finally {
|
|
302
|
+
debug.timeEnd("query:load-more")
|
|
303
|
+
advance ? setLoadingMore(false) : setPrefetchingResults(false)
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const loadPreviousResults = (advance = false) => {
|
|
308
|
+
if (advance) advanceSelectionBeforeLoad = true
|
|
309
|
+
const base = resultBaseOffset()
|
|
310
|
+
if (base <= 0) {
|
|
311
|
+
advanceSelectionBeforeLoad = false
|
|
312
|
+
return
|
|
313
|
+
}
|
|
314
|
+
if (loadingMore() || loadingPreviousResults() || prefetchingResults() || busy() || loading()) return
|
|
315
|
+
|
|
316
|
+
const q = query().trim()
|
|
317
|
+
const role = ownerRole()
|
|
318
|
+
const db = dbPath()
|
|
319
|
+
const dir = directory
|
|
320
|
+
const pageSize = q ? searchBatchSize() : recentBatchSize()
|
|
321
|
+
const offset = Math.max(0, base - pageSize)
|
|
322
|
+
const limit = base - offset
|
|
323
|
+
|
|
324
|
+
setLoadingPreviousResults(true)
|
|
325
|
+
debug.time("query:load-before")
|
|
326
|
+
try {
|
|
327
|
+
const batch = q
|
|
328
|
+
? searchSessionMessages(q, { limit, offset, dbPath: db, directory: dir, role })
|
|
329
|
+
: recentSessionMessages({ limit, offset, dbPath: db, directory: dir, role })
|
|
330
|
+
const nextSelected = advanceSelectionBeforeLoad && batch.length > 0 ? base - 1 : selected()
|
|
331
|
+
debug.log("results:load-before", { offset, limit, added: batch.length, fromBase: base, toBase: offset, cached: results().length + batch.length, advance })
|
|
332
|
+
const shouldAdvance = advanceSelectionBeforeLoad
|
|
333
|
+
solidBatch(() => {
|
|
334
|
+
setResultBaseOffset(offset)
|
|
335
|
+
setResults([...batch, ...results()])
|
|
336
|
+
setResultPageInfo({ loadedUntil: nextResultOffset(), hasMore: hasMore(), pageSize, lastOffset: offset, lastAdded: batch.length })
|
|
337
|
+
if (shouldAdvance && batch.length > 0) setSelected(nextSelected)
|
|
338
|
+
})
|
|
339
|
+
if (shouldAdvance) advanceSelectionBeforeLoad = false
|
|
340
|
+
} catch (err) {
|
|
341
|
+
debug.log("results:load-before:error", err instanceof Error ? err.message : String(err))
|
|
342
|
+
advanceSelectionBeforeLoad = false
|
|
343
|
+
} finally {
|
|
344
|
+
debug.timeEnd("query:load-before")
|
|
345
|
+
setLoadingPreviousResults(false)
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const scheduleResultPrefetch = (advance = false) => {
|
|
350
|
+
if (advance) advanceSelectionAfterLoad = true
|
|
351
|
+
if (resultPrefetchTimer || loadingMore() || loadingPreviousResults() || prefetchingResults()) return
|
|
352
|
+
debug.log("results:prefetch-scheduled", { advance, pendingAdvance: advanceSelectionAfterLoad, pageInfo: resultPageInfo() })
|
|
353
|
+
resultPrefetchTimer = setTimeout(() => {
|
|
354
|
+
resultPrefetchTimer = undefined
|
|
355
|
+
loadMoreResults(advanceSelectionAfterLoad)
|
|
356
|
+
}, 1)
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const schedulePreviousResultsLoad = (advance = false) => {
|
|
360
|
+
if (advance) advanceSelectionBeforeLoad = true
|
|
361
|
+
if (resultPreviousTimer || loadingMore() || loadingPreviousResults() || prefetchingResults()) return
|
|
362
|
+
debug.log("results:load-before-scheduled", { advance, pendingAdvance: advanceSelectionBeforeLoad, baseOffset: resultBaseOffset(), pageInfo: resultPageInfo() })
|
|
363
|
+
resultPreviousTimer = setTimeout(() => {
|
|
364
|
+
resultPreviousTimer = undefined
|
|
365
|
+
loadPreviousResults(advanceSelectionBeforeLoad)
|
|
366
|
+
}, 1)
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
let lastResultPrefetchDecision = ""
|
|
370
|
+
|
|
101
371
|
const move = (delta: number) => {
|
|
102
372
|
if (results().length === 0) return
|
|
103
373
|
setSelected((index) => {
|
|
374
|
+
const base = resultBaseOffset()
|
|
375
|
+
const cachedEnd = base + results().length
|
|
104
376
|
const next = index + delta
|
|
105
377
|
let finalIndex = next
|
|
106
|
-
if (next < 0) finalIndex =
|
|
107
|
-
else if (next
|
|
108
|
-
|
|
378
|
+
if (next < 0) finalIndex = cachedEnd - 1
|
|
379
|
+
else if (next < base) {
|
|
380
|
+
schedulePreviousResultsLoad(true)
|
|
381
|
+
finalIndex = base
|
|
382
|
+
}
|
|
383
|
+
else if (next >= cachedEnd) {
|
|
384
|
+
if (hasMore()) scheduleResultPrefetch(true)
|
|
385
|
+
finalIndex = cachedEnd - 1
|
|
386
|
+
}
|
|
387
|
+
|
|
109
388
|
if (finalIndex !== index) debug.time("nav:total")
|
|
110
389
|
return finalIndex
|
|
111
390
|
})
|
|
@@ -113,7 +392,8 @@ export const Telescope = (props: { api: TuiPluginApi; onClose: () => void }) =>
|
|
|
113
392
|
|
|
114
393
|
createEffect(() => {
|
|
115
394
|
const index = selected()
|
|
116
|
-
const
|
|
395
|
+
const window = resultRenderWindow()
|
|
396
|
+
const row = resultScroll?.getChildren()[index - window.start]
|
|
117
397
|
if (!resultScroll || !row) return
|
|
118
398
|
const y = row.y - resultScroll.y
|
|
119
399
|
if (y < 0) {
|
|
@@ -126,52 +406,160 @@ export const Telescope = (props: { api: TuiPluginApi; onClose: () => void }) =>
|
|
|
126
406
|
})
|
|
127
407
|
|
|
128
408
|
createEffect(() => {
|
|
129
|
-
const
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
409
|
+
const state = resultPrefetchState()
|
|
410
|
+
const blockedBy = !hasMore()
|
|
411
|
+
? "no-more"
|
|
412
|
+
: loadingMore()
|
|
413
|
+
? "loading-more"
|
|
414
|
+
: loadingPreviousResults()
|
|
415
|
+
? "loading-before"
|
|
416
|
+
: prefetchingResults()
|
|
417
|
+
? "prefetching"
|
|
418
|
+
: busy()
|
|
419
|
+
? "busy"
|
|
420
|
+
: loading()
|
|
421
|
+
? "loading"
|
|
422
|
+
: ""
|
|
423
|
+
const decisionKey = `${selected()}:${state.cachedEnd}:${state.rowsAhead}:${state.threshold}:${blockedBy}:${state.shouldPrefetch}`
|
|
424
|
+
if (decisionKey !== lastResultPrefetchDecision) {
|
|
425
|
+
lastResultPrefetchDecision = decisionKey
|
|
426
|
+
debug.log("results:prefetch-decision", { selected: selected(), ...state, blockedBy: blockedBy || undefined })
|
|
427
|
+
}
|
|
428
|
+
if (!state.shouldPrefetch || blockedBy) return
|
|
133
429
|
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
430
|
+
const timer = setTimeout(() => scheduleResultPrefetch(false), 100)
|
|
431
|
+
onCleanup(() => clearTimeout(timer))
|
|
432
|
+
})
|
|
137
433
|
|
|
138
|
-
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
434
|
+
const previewContentHeight = () => {
|
|
435
|
+
const children = previewScroll?.getChildren()
|
|
436
|
+
const lastChild = children?.[children.length - 1] as { y: number; height: number } | undefined
|
|
437
|
+
return lastChild ? lastChild.y + lastChild.height : 0
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const loadPreviewBefore = (previousContentHeight = previewContentHeight(), preserveScroll = true, visibleLoad = false) => {
|
|
441
|
+
const item = selectedResult()
|
|
442
|
+
const first = previewParts()[0]
|
|
443
|
+
if (!item || !first || loadingPreviewMore() || prefetchingPreviewBefore()) return
|
|
444
|
+
visibleLoad ? setLoadingPreviewMore(true) : setPrefetchingPreviewBefore(true)
|
|
445
|
+
debug.time("preview:load-before")
|
|
446
|
+
try {
|
|
447
|
+
const page = loadConversationBefore(item, { id: first.id, timeCreated: first.timeCreated }, { limit: PREVIEW_PAGE_SIZE, dbPath: dbPath() })
|
|
448
|
+
debug.log("preview:load-before", {
|
|
449
|
+
item: item.id,
|
|
450
|
+
added: page.parts.length,
|
|
451
|
+
hasMoreBefore: page.hasMoreBefore,
|
|
452
|
+
preserveScroll,
|
|
453
|
+
visibleLoad,
|
|
454
|
+
first: page.parts[0]?.id,
|
|
455
|
+
last: page.parts.at(-1)?.id,
|
|
456
|
+
})
|
|
457
|
+
if (page.parts.length > 0) {
|
|
458
|
+
setPreviewParts((prev) => [...page.parts, ...prev])
|
|
459
|
+
if (preserveScroll) {
|
|
460
|
+
setTimeout(() => {
|
|
461
|
+
const delta = previewContentHeight() - previousContentHeight
|
|
462
|
+
if (delta > 0) previewScroll?.scrollBy(delta)
|
|
463
|
+
}, 1)
|
|
464
|
+
}
|
|
152
465
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
466
|
+
setHasMorePreviewBefore(page.hasMoreBefore)
|
|
467
|
+
} catch (err) {
|
|
468
|
+
debug.log("preview:load-before:error", err instanceof Error ? err.message : String(err))
|
|
469
|
+
} finally {
|
|
470
|
+
debug.timeEnd("preview:load-before")
|
|
471
|
+
visibleLoad ? setLoadingPreviewMore(false) : setPrefetchingPreviewBefore(false)
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const loadPreviewAfter = (visibleLoad = false) => {
|
|
476
|
+
const item = selectedResult()
|
|
477
|
+
const last = previewParts().at(-1)
|
|
478
|
+
if (!item || !last || loadingPreviewMore() || prefetchingPreviewAfter()) return
|
|
479
|
+
visibleLoad ? setLoadingPreviewMore(true) : setPrefetchingPreviewAfter(true)
|
|
480
|
+
debug.time("preview:load-after")
|
|
481
|
+
try {
|
|
482
|
+
const page = loadConversationAfter(item, { id: last.id, timeCreated: last.timeCreated }, { limit: PREVIEW_PAGE_SIZE, dbPath: dbPath() })
|
|
483
|
+
debug.log("preview:load-after", {
|
|
484
|
+
item: item.id,
|
|
485
|
+
added: page.parts.length,
|
|
486
|
+
hasMoreAfter: page.hasMoreAfter,
|
|
487
|
+
visibleLoad,
|
|
488
|
+
first: page.parts[0]?.id,
|
|
489
|
+
last: page.parts.at(-1)?.id,
|
|
490
|
+
})
|
|
491
|
+
if (page.parts.length > 0) setPreviewParts((prev) => [...prev, ...page.parts])
|
|
492
|
+
setHasMorePreviewAfter(page.hasMoreAfter)
|
|
493
|
+
} catch (err) {
|
|
494
|
+
debug.log("preview:load-after:error", err instanceof Error ? err.message : String(err))
|
|
495
|
+
} finally {
|
|
496
|
+
debug.timeEnd("preview:load-after")
|
|
497
|
+
visibleLoad ? setLoadingPreviewMore(false) : setPrefetchingPreviewAfter(false)
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const schedulePreviewBefore = (previousContentHeight = previewContentHeight(), preserveScroll = true, visibleLoad = false) => {
|
|
502
|
+
if (!hasMorePreviewBefore() || loadingPreviewMore() || prefetchingPreviewBefore()) return
|
|
503
|
+
if (previewBeforeTimer) {
|
|
504
|
+
if (pendingPreviewBefore) {
|
|
505
|
+
pendingPreviewBefore.preserveScroll = pendingPreviewBefore.preserveScroll && preserveScroll
|
|
506
|
+
pendingPreviewBefore.visibleLoad = pendingPreviewBefore.visibleLoad || visibleLoad
|
|
507
|
+
}
|
|
508
|
+
return
|
|
509
|
+
}
|
|
510
|
+
pendingPreviewBefore = { previousContentHeight, preserveScroll, visibleLoad }
|
|
511
|
+
debug.log("preview:prefetch-before-scheduled", { preserveScroll, visibleLoad })
|
|
512
|
+
previewBeforeTimer = setTimeout(() => {
|
|
513
|
+
const pending = pendingPreviewBefore
|
|
514
|
+
previewBeforeTimer = undefined
|
|
515
|
+
pendingPreviewBefore = undefined
|
|
516
|
+
if (pending) loadPreviewBefore(pending.previousContentHeight, pending.preserveScroll, pending.visibleLoad)
|
|
517
|
+
}, 1)
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const schedulePreviewAfter = (visibleLoad = false) => {
|
|
521
|
+
if (!hasMorePreviewAfter() || loadingPreviewMore() || prefetchingPreviewAfter()) return
|
|
522
|
+
pendingPreviewAfterVisible = pendingPreviewAfterVisible || visibleLoad
|
|
523
|
+
if (previewAfterTimer) return
|
|
524
|
+
debug.log("preview:prefetch-after-scheduled", { visibleLoad })
|
|
525
|
+
previewAfterTimer = setTimeout(() => {
|
|
526
|
+
const pendingVisibleLoad = pendingPreviewAfterVisible
|
|
527
|
+
previewAfterTimer = undefined
|
|
528
|
+
pendingPreviewAfterVisible = false
|
|
529
|
+
loadPreviewAfter(pendingVisibleLoad)
|
|
530
|
+
}, 1)
|
|
531
|
+
}
|
|
156
532
|
|
|
157
533
|
let lastPreviewItemId = ""
|
|
158
534
|
createEffect(() => {
|
|
159
535
|
const item = selectedResult()
|
|
160
|
-
const range = previewRange()
|
|
161
536
|
if (!item) {
|
|
537
|
+
cancelPreviewPrefetch()
|
|
162
538
|
setPreviewParts([])
|
|
539
|
+
setHasMorePreviewBefore(false)
|
|
540
|
+
setHasMorePreviewAfter(false)
|
|
163
541
|
return
|
|
164
542
|
}
|
|
165
|
-
if (item.id
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
return
|
|
170
|
-
}
|
|
543
|
+
if (item.id === lastPreviewItemId) return
|
|
544
|
+
lastPreviewItemId = item.id
|
|
545
|
+
cancelPreviewPrefetch()
|
|
546
|
+
debug.log("preview:new-item", item.sessionTitle?.slice(0, 40) ?? item.id.slice(-8))
|
|
171
547
|
const db = dbPath()
|
|
172
548
|
debug.time("preview:load")
|
|
173
549
|
try {
|
|
174
|
-
|
|
550
|
+
const page = loadConversationAround(item, { before: INITIAL_PREVIEW_BEFORE, after: INITIAL_PREVIEW_AFTER, dbPath: db })
|
|
551
|
+
debug.log("preview:init", {
|
|
552
|
+
item: item.id,
|
|
553
|
+
session: item.sessionID,
|
|
554
|
+
parts: page.parts.length,
|
|
555
|
+
hasMoreBefore: page.hasMoreBefore,
|
|
556
|
+
hasMoreAfter: page.hasMoreAfter,
|
|
557
|
+
first: page.parts[0]?.id,
|
|
558
|
+
last: page.parts.at(-1)?.id,
|
|
559
|
+
})
|
|
560
|
+
setPreviewParts(page.parts)
|
|
561
|
+
setHasMorePreviewBefore(page.hasMoreBefore)
|
|
562
|
+
setHasMorePreviewAfter(page.hasMoreAfter)
|
|
175
563
|
} catch {}
|
|
176
564
|
debug.timeEnd("nav:total")
|
|
177
565
|
debug.timeEnd("preview:load")
|
|
@@ -181,16 +569,36 @@ export const Telescope = (props: { api: TuiPluginApi; onClose: () => void }) =>
|
|
|
181
569
|
const item = selectedResult()
|
|
182
570
|
if (!item) return
|
|
183
571
|
const interval = setInterval(() => {
|
|
572
|
+
if (loadingPreviewMore() || prefetchingPreviewBefore() || prefetchingPreviewAfter()) return
|
|
184
573
|
const scroll = previewScroll
|
|
185
574
|
const children = scroll?.getChildren()
|
|
186
575
|
if (!scroll || !children || children.length === 0) return
|
|
187
|
-
const range = previewRange()
|
|
188
576
|
const lastChild = children[children.length - 1] as { y: number; height: number }
|
|
189
577
|
const totalContentHeight = lastChild.y + lastChild.height
|
|
190
|
-
const atTop = scroll.y <= 0
|
|
578
|
+
const atTop = scroll.y <= 0
|
|
579
|
+
const prefetchDistance = Math.max(2, Math.floor(scroll.height * PREVIEW_PREFETCH_VIEWPORTS))
|
|
580
|
+
const nearTop = scroll.y <= prefetchDistance
|
|
191
581
|
const atBottom = scroll.y + scroll.height >= totalContentHeight - 1
|
|
192
|
-
|
|
193
|
-
if (
|
|
582
|
+
const nearBottom = scroll.y + scroll.height >= totalContentHeight - prefetchDistance
|
|
583
|
+
if (nearTop || nearBottom) {
|
|
584
|
+
debug.log("preview:scroll-edge", {
|
|
585
|
+
y: scroll.y,
|
|
586
|
+
height: scroll.height,
|
|
587
|
+
contentHeight: totalContentHeight,
|
|
588
|
+
prefetchDistance,
|
|
589
|
+
atTop,
|
|
590
|
+
nearTop,
|
|
591
|
+
atBottom,
|
|
592
|
+
nearBottom,
|
|
593
|
+
hasMoreBefore: hasMorePreviewBefore(),
|
|
594
|
+
hasMoreAfter: hasMorePreviewAfter(),
|
|
595
|
+
prefetchingBefore: prefetchingPreviewBefore(),
|
|
596
|
+
prefetchingAfter: prefetchingPreviewAfter(),
|
|
597
|
+
children: children.length,
|
|
598
|
+
})
|
|
599
|
+
}
|
|
600
|
+
if (atTop && hasMorePreviewBefore()) schedulePreviewBefore(totalContentHeight, false, true)
|
|
601
|
+
if (nearBottom && hasMorePreviewAfter()) schedulePreviewAfter(atBottom)
|
|
194
602
|
}, 400)
|
|
195
603
|
onCleanup(() => clearInterval(interval))
|
|
196
604
|
})
|
|
@@ -229,50 +637,54 @@ export const Telescope = (props: { api: TuiPluginApi; onClose: () => void }) =>
|
|
|
229
637
|
el?.blur?.()
|
|
230
638
|
}
|
|
231
639
|
|
|
640
|
+
const toggleOwnerFilter = () => {
|
|
641
|
+
setOwnerFilter((filter) => filter === "all" ? "user" : filter === "user" ? "assistant" : "all")
|
|
642
|
+
}
|
|
643
|
+
|
|
232
644
|
useKeyboard((evt) => {
|
|
233
645
|
if (!props.api.ui.dialog.open) return
|
|
234
646
|
|
|
235
|
-
if (
|
|
647
|
+
if (mode() !== "normal") return
|
|
648
|
+
|
|
649
|
+
if (matchesKey(evt, props.config.keys.moveDown) || matchesKey(evt, props.config.keys.moveUp)) {
|
|
236
650
|
prevent(evt)
|
|
237
|
-
|
|
651
|
+
matchesKey(evt, props.config.keys.moveDown) ? move(1) : move(-1)
|
|
238
652
|
return
|
|
239
653
|
}
|
|
240
654
|
|
|
241
|
-
if (
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
return
|
|
275
|
-
}
|
|
655
|
+
if (matchesKey(evt, props.config.keys.open)) {
|
|
656
|
+
prevent(evt)
|
|
657
|
+
open()
|
|
658
|
+
return
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
if (matchesKey(evt, props.config.keys.close)) {
|
|
662
|
+
prevent(evt)
|
|
663
|
+
props.onClose()
|
|
664
|
+
return
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
if (matchesKey(evt, props.config.keys.scrollPreviewDown)) {
|
|
668
|
+
scrollPreview(1, evt)
|
|
669
|
+
return
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
if (matchesKey(evt, props.config.keys.scrollPreviewUp)) {
|
|
673
|
+
scrollPreview(-1, evt)
|
|
674
|
+
return
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
if (matchesKey(evt, props.config.keys.toggleOwner)) {
|
|
678
|
+
prevent(evt)
|
|
679
|
+
toggleOwnerFilter()
|
|
680
|
+
return
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
if (matchesKey(evt, props.config.keys.insertMode)) {
|
|
684
|
+
prevent(evt)
|
|
685
|
+
setMode("insert")
|
|
686
|
+
focusInput()
|
|
687
|
+
return
|
|
276
688
|
}
|
|
277
689
|
})
|
|
278
690
|
|
|
@@ -303,12 +715,17 @@ export const Telescope = (props: { api: TuiPluginApi; onClose: () => void }) =>
|
|
|
303
715
|
focusedBackgroundColor={theme().backgroundPanel}
|
|
304
716
|
onInput={(value) => setQuery(value)}
|
|
305
717
|
onKeyDown={(evt: ParsedKey) => {
|
|
306
|
-
if (
|
|
718
|
+
if (matchesKey(evt, inputKeys().moveDown) || matchesKey(evt, inputKeys().moveUp)) {
|
|
307
719
|
prevent(evt)
|
|
308
|
-
|
|
720
|
+
matchesKey(evt, inputKeys().moveDown) ? move(1) : move(-1)
|
|
309
721
|
return
|
|
310
722
|
}
|
|
311
|
-
if (
|
|
723
|
+
if (matchesKey(evt, inputKeys().open)) {
|
|
724
|
+
prevent(evt)
|
|
725
|
+
open()
|
|
726
|
+
return
|
|
727
|
+
}
|
|
728
|
+
if (matchesKey(evt, inputKeys().normalMode)) {
|
|
312
729
|
prevent(evt)
|
|
313
730
|
setMode("normal")
|
|
314
731
|
blurInput()
|
|
@@ -316,13 +733,13 @@ export const Telescope = (props: { api: TuiPluginApi; onClose: () => void }) =>
|
|
|
316
733
|
}}
|
|
317
734
|
flexGrow={1}
|
|
318
735
|
/>
|
|
319
|
-
<text fg={theme().textMuted}>{busy() ?
|
|
736
|
+
<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>
|
|
320
737
|
</box>
|
|
321
738
|
</box>
|
|
322
739
|
|
|
323
740
|
<box flexDirection="row" flexGrow={1} minHeight={0}>
|
|
324
741
|
<box width={leftWidth()} flexDirection="column" minHeight={0} backgroundColor={theme().backgroundPanel}>
|
|
325
|
-
<scrollbox ref={(element: ScrollBoxRenderable) => (resultScroll = element)} flexGrow={1} minHeight={0} verticalScrollbarOptions={{ visible:
|
|
742
|
+
<scrollbox ref={(element: ScrollBoxRenderable) => (resultScroll = element)} flexGrow={1} minHeight={0} verticalScrollbarOptions={{ visible: true }}>
|
|
326
743
|
<Show
|
|
327
744
|
when={!error()}
|
|
328
745
|
fallback={
|
|
@@ -333,19 +750,22 @@ export const Telescope = (props: { api: TuiPluginApi; onClose: () => void }) =>
|
|
|
333
750
|
}
|
|
334
751
|
>
|
|
335
752
|
<Show when={!loading()}>
|
|
336
|
-
<Show when={results().length > 0} fallback={<EmptyState query={query()} theme={theme()} />}>
|
|
337
|
-
<For each={
|
|
338
|
-
{(item, index) =>
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
753
|
+
<Show when={results().length > 0} fallback={<EmptyState query={query()} owner={ownerLabel()} theme={theme()} />}>
|
|
754
|
+
<For each={resultRenderWindow().items}>
|
|
755
|
+
{(item, index) => {
|
|
756
|
+
const absoluteIndex = () => resultRenderWindow().start + index()
|
|
757
|
+
return (
|
|
758
|
+
<ResultRow
|
|
759
|
+
item={item}
|
|
760
|
+
active={absoluteIndex() === selected()}
|
|
761
|
+
width={leftWidth()}
|
|
762
|
+
query={query()}
|
|
763
|
+
theme={theme()}
|
|
764
|
+
onMouseOver={() => setSelected(absoluteIndex())}
|
|
765
|
+
onOpen={open}
|
|
766
|
+
/>
|
|
767
|
+
)
|
|
768
|
+
}}
|
|
349
769
|
</For>
|
|
350
770
|
<Show when={loadingMore()}>
|
|
351
771
|
<SkeletonRow theme={theme()} />
|
|
@@ -382,23 +802,21 @@ export const Telescope = (props: { api: TuiPluginApi; onClose: () => void }) =>
|
|
|
382
802
|
<Show when={mode() === "normal"}>
|
|
383
803
|
<box paddingLeft={4} paddingRight={4} flexDirection="row" backgroundColor={theme().backgroundElement} gap={2}>
|
|
384
804
|
<text fg={theme().accent}><span style={{ bold: true }}>NORMAL</span></text>
|
|
385
|
-
<
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
<text fg={theme().textMuted}>·</text>
|
|
394
|
-
<text fg={theme().text}>q close</text>
|
|
805
|
+
<For each={normalHelpItems()}>
|
|
806
|
+
{(item, index) => (
|
|
807
|
+
<>
|
|
808
|
+
<text fg={theme().textMuted}>·</text>
|
|
809
|
+
<text fg={index() % 2 === 0 ? theme().text : theme().textMuted}>{item}</text>
|
|
810
|
+
</>
|
|
811
|
+
)}
|
|
812
|
+
</For>
|
|
395
813
|
</box>
|
|
396
814
|
</Show>
|
|
397
815
|
<Show when={mode() === "insert"}>
|
|
398
816
|
<box paddingLeft={4} paddingRight={4} flexDirection="row" backgroundColor={theme().backgroundElement} gap={2}>
|
|
399
817
|
<text fg={theme().warning}><span style={{ bold: true }}>INSERT</span></text>
|
|
400
818
|
<text fg={theme().textMuted}>·</text>
|
|
401
|
-
<text fg={theme().textMuted}
|
|
819
|
+
<text fg={theme().textMuted}>{keyListLabel(inputKeys().moveUp)}/{keyListLabel(inputKeys().moveDown)} move · {keyListLabel(inputKeys().normalMode)} normal</text>
|
|
402
820
|
</box>
|
|
403
821
|
</Show>
|
|
404
822
|
|