@bojackduy/opencode-telescope 0.1.15 → 0.1.17
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/components/preview.tsx +184 -13
- package/package.json +1 -1
- package/search.ts +109 -49
- package/telescope.tsx +46 -97
- package/ui/render-target.ts +1 -1
package/components/preview.tsx
CHANGED
|
@@ -59,7 +59,7 @@ export const ConversationPreview = (props: { item: SearchResult; parts: Conversa
|
|
|
59
59
|
)
|
|
60
60
|
|
|
61
61
|
const PreviewConversationPart = (props: { part: ConversationPreviewPart; item: SearchResult; syntax: SyntaxStyle; theme: TuiThemeCurrent }) => {
|
|
62
|
-
if (props.part.type === "tool") return <PreviewToolPart part={props.part} theme={props.theme} />
|
|
62
|
+
if (props.part.type === "tool") return <PreviewToolPart part={props.part} item={props.item} syntax={props.syntax} theme={props.theme} />
|
|
63
63
|
if (props.part.type === "reasoning") return <PreviewReasoningPart part={props.part} syntax={props.syntax} theme={props.theme} />
|
|
64
64
|
if (props.part.role === "assistant") return <PreviewAssistantPart part={props.part} item={props.item} syntax={props.syntax} theme={props.theme} />
|
|
65
65
|
return <PreviewUserPart part={props.part} item={props.item} theme={props.theme} />
|
|
@@ -129,7 +129,7 @@ const PreviewReasoningPart = (props: { part: ConversationPreviewPart; syntax: Sy
|
|
|
129
129
|
)
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
-
const PreviewToolPart = (props: { part: ConversationPreviewPart; theme: TuiThemeCurrent }) => {
|
|
132
|
+
const PreviewToolPart = (props: { part: ConversationPreviewPart; item: SearchResult; syntax: SyntaxStyle; theme: TuiThemeCurrent }) => {
|
|
133
133
|
const status = createMemo(() => props.part.state?.status ?? "pending")
|
|
134
134
|
const failed = createMemo(() => status() === "error")
|
|
135
135
|
const color = createMemo(() => {
|
|
@@ -137,27 +137,139 @@ const PreviewToolPart = (props: { part: ConversationPreviewPart; theme: TuiTheme
|
|
|
137
137
|
if (status() === "completed") return props.theme.textMuted
|
|
138
138
|
return props.theme.text
|
|
139
139
|
})
|
|
140
|
+
const codeTool = createMemo(() => props.part.tool === "apply_patch" || props.part.tool === "edit" || props.part.tool === "write")
|
|
140
141
|
return (
|
|
141
|
-
<box id={`tool
|
|
142
|
+
<box id={`tool-${props.part.messageID}-${props.part.id}`} paddingLeft={3} marginTop={1} flexDirection="column" flexShrink={0}>
|
|
142
143
|
<Show when={props.part.target}>
|
|
143
|
-
<TargetMarker part={props.part} role="tool" time={props.part.timeCreated} theme={props.theme} />
|
|
144
|
+
<TargetMarker part={props.part} item={props.item} role={props.part.tool ?? "tool"} time={props.part.timeCreated} theme={props.theme} />
|
|
145
|
+
</Show>
|
|
146
|
+
<Show when={codeTool()} fallback={<CompactToolRow part={props.part} color={color()} failed={failed()} theme={props.theme} />}>
|
|
147
|
+
<CodeToolPreview part={props.part} syntax={props.syntax} theme={props.theme} />
|
|
144
148
|
</Show>
|
|
145
|
-
<text fg={color()} wrapMode="none" overflow="hidden">
|
|
146
|
-
<span style={{ fg: failed() ? props.theme.error : props.theme.textMuted }}>{toolIcon(props.part.tool)} </span>
|
|
147
|
-
<span>{toolLabel(props.part.tool)}</span>
|
|
148
|
-
<span style={{ fg: props.theme.textMuted }}> {toolInputSummary(props.part.state?.input)}</span>
|
|
149
|
-
<span style={{ fg: props.theme.textMuted }}> · {status()}</span>
|
|
150
|
-
</text>
|
|
151
149
|
<Show when={props.part.state?.error}>
|
|
152
150
|
{(error) => <text fg={props.theme.error}>{error()}</text>}
|
|
153
151
|
</Show>
|
|
154
|
-
<Show when={failed() ? props.part.state?.output : undefined}>
|
|
155
|
-
{(output) => <text fg={props.theme.textMuted}>{truncate(output().trim(), 300)}</text>}
|
|
156
|
-
</Show>
|
|
157
152
|
</box>
|
|
158
153
|
)
|
|
159
154
|
}
|
|
160
155
|
|
|
156
|
+
const CompactToolRow = (props: { part: ConversationPreviewPart; color: any; failed: boolean; theme: TuiThemeCurrent }) => (
|
|
157
|
+
<>
|
|
158
|
+
<text fg={props.color} wrapMode="none" overflow="hidden">
|
|
159
|
+
<span style={{ fg: props.failed ? props.theme.error : props.theme.textMuted }}>{toolIcon(props.part.tool)} </span>
|
|
160
|
+
<span>{toolLabel(props.part.tool)}</span>
|
|
161
|
+
<span style={{ fg: props.theme.textMuted }}> {toolInputSummary(props.part.state?.input)}</span>
|
|
162
|
+
<span style={{ fg: props.theme.textMuted }}> · {props.part.state?.status ?? "pending"}</span>
|
|
163
|
+
</text>
|
|
164
|
+
<Show when={props.failed ? props.part.state?.output : undefined}>
|
|
165
|
+
{(output) => <text fg={props.theme.textMuted}>{truncate(output().trim(), 300)}</text>}
|
|
166
|
+
</Show>
|
|
167
|
+
</>
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
const CodeToolPreview = (props: { part: ConversationPreviewPart; syntax: SyntaxStyle; theme: TuiThemeCurrent }) => {
|
|
171
|
+
if (props.part.tool === "apply_patch") return <ApplyPatchPreview part={props.part} syntax={props.syntax} theme={props.theme} />
|
|
172
|
+
if (props.part.tool === "edit") return <EditPreview part={props.part} syntax={props.syntax} theme={props.theme} />
|
|
173
|
+
if (props.part.tool === "write") return <WritePreview part={props.part} syntax={props.syntax} theme={props.theme} />
|
|
174
|
+
return <CompactToolRow part={props.part} color={props.theme.textMuted} failed={false} theme={props.theme} />
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const ApplyPatchPreview = (props: { part: ConversationPreviewPart; syntax: SyntaxStyle; theme: TuiThemeCurrent }) => {
|
|
178
|
+
const files = createMemo(() => parseApplyPatchFiles(props.part.state?.metadata))
|
|
179
|
+
return (
|
|
180
|
+
<Show when={files().length > 0} fallback={<CompactToolRow part={props.part} color={props.theme.textMuted} failed={false} theme={props.theme} />}>
|
|
181
|
+
<For each={files()}>
|
|
182
|
+
{(file) => (
|
|
183
|
+
<ToolBlock title={patchTitle(file)} theme={props.theme}>
|
|
184
|
+
<DiffBlock diff={file.patch} filePath={file.filePath} syntax={props.syntax} theme={props.theme} />
|
|
185
|
+
</ToolBlock>
|
|
186
|
+
)}
|
|
187
|
+
</For>
|
|
188
|
+
</Show>
|
|
189
|
+
)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const EditPreview = (props: { part: ConversationPreviewPart; syntax: SyntaxStyle; theme: TuiThemeCurrent }) => {
|
|
193
|
+
const input = createMemo(() => recordValue(props.part.state?.input))
|
|
194
|
+
const metadata = createMemo(() => recordValue(props.part.state?.metadata))
|
|
195
|
+
const diff = createMemo(() => stringValue(metadata()?.diff) ?? stringValue(recordValue(metadata()?.filediff)?.patch) ?? "")
|
|
196
|
+
const filePath = createMemo(() => stringValue(input()?.filePath) ?? stringValue(recordValue(metadata()?.filediff)?.file) ?? "")
|
|
197
|
+
return (
|
|
198
|
+
<Show when={diff()} fallback={<CompactToolRow part={props.part} color={props.theme.textMuted} failed={false} theme={props.theme} />}>
|
|
199
|
+
{(value) => (
|
|
200
|
+
<ToolBlock title={`← Edit ${shortPath(filePath())}`} theme={props.theme}>
|
|
201
|
+
<DiffBlock diff={value()} filePath={filePath()} syntax={props.syntax} theme={props.theme} />
|
|
202
|
+
</ToolBlock>
|
|
203
|
+
)}
|
|
204
|
+
</Show>
|
|
205
|
+
)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const WritePreview = (props: { part: ConversationPreviewPart; syntax: SyntaxStyle; theme: TuiThemeCurrent }) => {
|
|
209
|
+
const input = createMemo(() => recordValue(props.part.state?.input))
|
|
210
|
+
const filePath = createMemo(() => stringValue(input()?.filePath) ?? "")
|
|
211
|
+
const content = createMemo(() => stringValue(input()?.content) ?? "")
|
|
212
|
+
return (
|
|
213
|
+
<Show when={content()} fallback={<CompactToolRow part={props.part} color={props.theme.textMuted} failed={false} theme={props.theme} />}>
|
|
214
|
+
{(value) => (
|
|
215
|
+
<ToolBlock title={`# Wrote ${shortPath(filePath())}`} theme={props.theme}>
|
|
216
|
+
<line_number fg={props.theme.textMuted} minWidth={3} paddingRight={1}>
|
|
217
|
+
<code
|
|
218
|
+
conceal={false}
|
|
219
|
+
fg={props.theme.text}
|
|
220
|
+
filetype={filetype(filePath())}
|
|
221
|
+
syntaxStyle={props.syntax}
|
|
222
|
+
content={value()}
|
|
223
|
+
/>
|
|
224
|
+
</line_number>
|
|
225
|
+
</ToolBlock>
|
|
226
|
+
)}
|
|
227
|
+
</Show>
|
|
228
|
+
)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const ToolBlock = (props: { title: string; children: any; theme: TuiThemeCurrent }) => (
|
|
232
|
+
<box
|
|
233
|
+
border={["left"]}
|
|
234
|
+
paddingTop={1}
|
|
235
|
+
paddingBottom={1}
|
|
236
|
+
paddingLeft={2}
|
|
237
|
+
marginTop={1}
|
|
238
|
+
gap={1}
|
|
239
|
+
backgroundColor={props.theme.backgroundPanel}
|
|
240
|
+
customBorderChars={splitBorderChars}
|
|
241
|
+
borderColor={props.theme.background}
|
|
242
|
+
flexDirection="column"
|
|
243
|
+
>
|
|
244
|
+
<text paddingLeft={3} fg={props.theme.textMuted}>{props.title}</text>
|
|
245
|
+
{props.children}
|
|
246
|
+
</box>
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
const DiffBlock = (props: { diff: string; filePath: string; syntax: SyntaxStyle; theme: TuiThemeCurrent }) => (
|
|
250
|
+
<box paddingLeft={1}>
|
|
251
|
+
<diff
|
|
252
|
+
diff={props.diff}
|
|
253
|
+
view="unified"
|
|
254
|
+
filetype={filetype(props.filePath)}
|
|
255
|
+
syntaxStyle={props.syntax}
|
|
256
|
+
showLineNumbers={true}
|
|
257
|
+
width="100%"
|
|
258
|
+
wrapMode="word"
|
|
259
|
+
fg={props.theme.text}
|
|
260
|
+
addedBg={props.theme.diffAddedBg}
|
|
261
|
+
removedBg={props.theme.diffRemovedBg}
|
|
262
|
+
contextBg={props.theme.diffContextBg}
|
|
263
|
+
addedSignColor={props.theme.diffHighlightAdded}
|
|
264
|
+
removedSignColor={props.theme.diffHighlightRemoved}
|
|
265
|
+
lineNumberFg={props.theme.diffLineNumber}
|
|
266
|
+
lineNumberBg={props.theme.diffContextBg}
|
|
267
|
+
addedLineNumberBg={props.theme.diffAddedLineNumberBg}
|
|
268
|
+
removedLineNumberBg={props.theme.diffRemovedLineNumberBg}
|
|
269
|
+
/>
|
|
270
|
+
</box>
|
|
271
|
+
)
|
|
272
|
+
|
|
161
273
|
const TargetMarker = (props: { part: ConversationPreviewPart; item?: SearchResult; role: string; time: number; theme: TuiThemeCurrent }) => (
|
|
162
274
|
<box flexDirection="column" flexShrink={0}>
|
|
163
275
|
<text fg={props.theme.warning} wrapMode="none" overflow="hidden">
|
|
@@ -273,3 +385,62 @@ function matchExcerpt(text: string, query: string, radius = 80) {
|
|
|
273
385
|
after: `${text.slice(lastEnd, afterEnd).replace(/\s+/g, " ")}${afterEnd < text.length ? "..." : ""}`,
|
|
274
386
|
}
|
|
275
387
|
}
|
|
388
|
+
|
|
389
|
+
function parseApplyPatchFiles(metadata: unknown) {
|
|
390
|
+
const files = recordValue(metadata)?.files
|
|
391
|
+
if (!Array.isArray(files)) return []
|
|
392
|
+
return files.flatMap((item) => {
|
|
393
|
+
const file = recordValue(item)
|
|
394
|
+
const filePath = stringValue(file?.filePath)
|
|
395
|
+
const relativePath = stringValue(file?.relativePath) ?? filePath
|
|
396
|
+
const patch = stringValue(file?.patch)
|
|
397
|
+
const type = stringValue(file?.type) ?? "update"
|
|
398
|
+
const deletions = numberValue(file?.deletions) ?? 0
|
|
399
|
+
if (!filePath || !relativePath || patch === undefined) return []
|
|
400
|
+
return [{ filePath, relativePath, patch, type, deletions }]
|
|
401
|
+
})
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function patchTitle(file: { type: string; relativePath: string; filePath: string; deletions: number }) {
|
|
405
|
+
if (file.type === "delete") return `# Deleted ${file.relativePath}`
|
|
406
|
+
if (file.type === "add") return `# Created ${file.relativePath}`
|
|
407
|
+
if (file.type === "move") return `# Moved ${shortPath(file.filePath)} -> ${file.relativePath}`
|
|
408
|
+
return `← Patched ${file.relativePath}`
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function recordValue(value: unknown): Record<string, unknown> | undefined {
|
|
412
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return
|
|
413
|
+
return value as Record<string, unknown>
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function stringValue(value: unknown) {
|
|
417
|
+
return typeof value === "string" ? value : undefined
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function numberValue(value: unknown) {
|
|
421
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function shortPath(value: string) {
|
|
425
|
+
if (!value) return "file"
|
|
426
|
+
const parts = value.split(/[\\/]/)
|
|
427
|
+
return parts.slice(-3).join("/")
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function filetype(input: string) {
|
|
431
|
+
const ext = input.split(".").at(-1)?.toLowerCase()
|
|
432
|
+
if (!ext || ext === input.toLowerCase()) return "none"
|
|
433
|
+
if (["ts", "tsx", "js", "jsx", "mts", "cts"].includes(ext)) return "typescript"
|
|
434
|
+
if (ext === "py") return "python"
|
|
435
|
+
if (ext === "go") return "go"
|
|
436
|
+
if (ext === "rs") return "rust"
|
|
437
|
+
if (ext === "rb") return "ruby"
|
|
438
|
+
if (ext === "java") return "java"
|
|
439
|
+
if (ext === "json") return "json"
|
|
440
|
+
if (ext === "md") return "markdown"
|
|
441
|
+
if (ext === "yml" || ext === "yaml") return "yaml"
|
|
442
|
+
if (ext === "sql") return "sql"
|
|
443
|
+
if (ext === "sh" || ext === "bash" || ext === "zsh") return "shellscript"
|
|
444
|
+
if (ext === "diff" || ext === "patch") return "diff"
|
|
445
|
+
return ext
|
|
446
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "@bojackduy/opencode-telescope",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.17",
|
|
5
5
|
"description": "Fuzzy search across all OpenCode conversations — grep session and chat history, find code snippets, and jump to any chat instantly",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"license": "MIT",
|
package/search.ts
CHANGED
|
@@ -11,7 +11,8 @@ export type SearchResult = {
|
|
|
11
11
|
sessionTitle: string
|
|
12
12
|
directory: string
|
|
13
13
|
role: "user" | "assistant"
|
|
14
|
-
partType: "text" | "reasoning" | "tool"
|
|
14
|
+
partType: "text" | "reasoning" | "tool"
|
|
15
|
+
tool?: string
|
|
15
16
|
timeCreated: number
|
|
16
17
|
snippet: string
|
|
17
18
|
matchStart: number
|
|
@@ -57,6 +58,7 @@ export type ConversationPreviewCursor = {
|
|
|
57
58
|
export type ToolState = {
|
|
58
59
|
status: "pending" | "running" | "completed" | "error"
|
|
59
60
|
input?: unknown
|
|
61
|
+
metadata?: unknown
|
|
60
62
|
output?: string
|
|
61
63
|
error?: string
|
|
62
64
|
}
|
|
@@ -69,11 +71,12 @@ type Row = {
|
|
|
69
71
|
directory: string
|
|
70
72
|
role: SearchRole
|
|
71
73
|
part_type?: SearchResult["partType"]
|
|
74
|
+
tool?: string | null
|
|
72
75
|
time_created: number
|
|
73
76
|
text: string
|
|
74
77
|
}
|
|
75
78
|
|
|
76
|
-
type
|
|
79
|
+
type IndexSourceRow = Omit<Row, "text"> & {
|
|
77
80
|
data: string
|
|
78
81
|
}
|
|
79
82
|
|
|
@@ -325,6 +328,7 @@ export function rowToSearchResult(row: Row, query: string): SearchResult | undef
|
|
|
325
328
|
directory: row.directory,
|
|
326
329
|
role: row.role,
|
|
327
330
|
partType: row.part_type ?? "text",
|
|
331
|
+
tool: row.tool ?? undefined,
|
|
328
332
|
timeCreated: row.time_created,
|
|
329
333
|
snippet: makeSnippet(text, query),
|
|
330
334
|
matchStart: match.start,
|
|
@@ -344,7 +348,7 @@ export function rowToSearchResult(row: Row, query: string): SearchResult | undef
|
|
|
344
348
|
|
|
345
349
|
export function extractSearchText(data: string) {
|
|
346
350
|
try {
|
|
347
|
-
return
|
|
351
|
+
return extractFromValue(JSON.parse(data)).replace(/\s+/g, " ").trim()
|
|
348
352
|
} catch {
|
|
349
353
|
return data.replace(/\s+/g, " ").trim()
|
|
350
354
|
}
|
|
@@ -497,7 +501,7 @@ function parseConversationPart(row: ConversationRow, target: boolean): Conversat
|
|
|
497
501
|
role: row.role,
|
|
498
502
|
type: row.type,
|
|
499
503
|
timeCreated: row.time_created,
|
|
500
|
-
text:
|
|
504
|
+
text: extractToolIndexText(data).trim(),
|
|
501
505
|
tool: typeof data.tool === "string" ? data.tool : "tool",
|
|
502
506
|
state: parseToolState(data.state),
|
|
503
507
|
target,
|
|
@@ -539,6 +543,7 @@ function parseToolState(value: unknown): ToolState | undefined {
|
|
|
539
543
|
return {
|
|
540
544
|
status: state.status as ToolState["status"],
|
|
541
545
|
input: state.input,
|
|
546
|
+
metadata: state.metadata,
|
|
542
547
|
output: typeof state.output === "string" ? state.output : undefined,
|
|
543
548
|
error: typeof state.error === "string" ? state.error : undefined,
|
|
544
549
|
}
|
|
@@ -575,8 +580,8 @@ function indexedTextRows(db: Database, dbPath: string, limit: number, query: str
|
|
|
575
580
|
const offsetClause = offset ? "OFFSET ?" : ""
|
|
576
581
|
debug.time("query:fts:exec")
|
|
577
582
|
const rows = index.query<Row, (string | number)[]>(`
|
|
578
|
-
SELECT id, message_id, session_id, session_title, directory, role,
|
|
579
|
-
|
|
583
|
+
SELECT id, message_id, session_id, session_title, directory, role, part_type, tool,
|
|
584
|
+
CAST(time_created AS INTEGER) AS time_created, text
|
|
580
585
|
FROM document_fts
|
|
581
586
|
WHERE ${conditions.join(" AND ")}
|
|
582
587
|
ORDER BY bm25(document_fts), CAST(time_created AS INTEGER) DESC
|
|
@@ -593,7 +598,7 @@ function indexedTextRows(db: Database, dbPath: string, limit: number, query: str
|
|
|
593
598
|
function visibleTextRows(db: Database, limit: number, query?: string, directory?: string, offset?: number, role?: SearchRole) {
|
|
594
599
|
const offsetClause = offset ? "OFFSET ?" : ""
|
|
595
600
|
const conditions: string[] = [
|
|
596
|
-
"json_extract(p.data, '$.type')
|
|
601
|
+
"json_extract(p.data, '$.type') = 'text'",
|
|
597
602
|
role ? "json_extract(m.data, '$.role') = ?" : "json_extract(m.data, '$.role') IN ('user', 'assistant')",
|
|
598
603
|
]
|
|
599
604
|
const params: (string | number)[] = []
|
|
@@ -607,7 +612,7 @@ function visibleTextRows(db: Database, limit: number, query?: string, directory?
|
|
|
607
612
|
|
|
608
613
|
const tokens = query ? query.trim().split(/\s+/).filter(Boolean) : []
|
|
609
614
|
for (const token of tokens) {
|
|
610
|
-
conditions.push("p.data LIKE ?")
|
|
615
|
+
conditions.push("json_extract(p.data, '$.text') LIKE ?")
|
|
611
616
|
params.push(`%${token}%`)
|
|
612
617
|
}
|
|
613
618
|
|
|
@@ -617,9 +622,8 @@ function visibleTextRows(db: Database, limit: number, query?: string, directory?
|
|
|
617
622
|
const sql = `
|
|
618
623
|
SELECT p.id, p.message_id, p.session_id, s.title AS session_title, s.directory,
|
|
619
624
|
json_extract(m.data, '$.role') AS role,
|
|
620
|
-
json_extract(p.data, '$.type') AS part_type,
|
|
621
625
|
p.time_created,
|
|
622
|
-
p.data
|
|
626
|
+
json_extract(p.data, '$.text') AS text
|
|
623
627
|
FROM part p
|
|
624
628
|
JOIN message m ON m.id = p.message_id
|
|
625
629
|
JOIN session s ON s.id = p.session_id
|
|
@@ -628,9 +632,9 @@ function visibleTextRows(db: Database, limit: number, query?: string, directory?
|
|
|
628
632
|
LIMIT ? ${offsetClause}
|
|
629
633
|
`
|
|
630
634
|
debug.time("query:sql:exec")
|
|
631
|
-
const rows = db.query<
|
|
635
|
+
const rows = db.query<Row, (string | number)[]>(sql).all(...params as any[])
|
|
632
636
|
debug.timeEnd("query:sql:exec")
|
|
633
|
-
return rows
|
|
637
|
+
return rows
|
|
634
638
|
}
|
|
635
639
|
|
|
636
640
|
function ensureSearchIndex(source: Database, sourcePath: string) {
|
|
@@ -653,11 +657,11 @@ function ensureSearchIndex(source: Database, sourcePath: string) {
|
|
|
653
657
|
return _indexDb
|
|
654
658
|
}
|
|
655
659
|
|
|
656
|
-
const SEARCH_INDEX_VERSION = "
|
|
660
|
+
const SEARCH_INDEX_VERSION = "4"
|
|
657
661
|
|
|
658
662
|
function searchIndexPath(sourcePath: string) {
|
|
659
663
|
const parsed = path.parse(sourcePath)
|
|
660
|
-
return path.join(parsed.dir, `${parsed.name}-search.db`)
|
|
664
|
+
return path.join(parsed.dir, `${parsed.name}-telescope-search.db`)
|
|
661
665
|
}
|
|
662
666
|
|
|
663
667
|
function migrateSearchIndex(db: Database) {
|
|
@@ -674,14 +678,15 @@ function migrateSearchIndex(db: Database) {
|
|
|
674
678
|
directory UNINDEXED,
|
|
675
679
|
role UNINDEXED,
|
|
676
680
|
part_type UNINDEXED,
|
|
681
|
+
tool UNINDEXED,
|
|
677
682
|
time_created UNINDEXED,
|
|
678
683
|
text,
|
|
679
684
|
tokenize='unicode61'
|
|
680
685
|
);
|
|
681
686
|
`)
|
|
682
687
|
|
|
683
|
-
const
|
|
684
|
-
if (!
|
|
688
|
+
const columns = db.query<{ name: string }, []>("PRAGMA table_info(document_fts)").all().map((column) => column.name)
|
|
689
|
+
if (!columns.includes("part_type") || !columns.includes("tool")) {
|
|
685
690
|
db.exec("DROP TABLE document_fts")
|
|
686
691
|
db.exec(`
|
|
687
692
|
CREATE VIRTUAL TABLE document_fts USING fts5(
|
|
@@ -692,6 +697,7 @@ function migrateSearchIndex(db: Database) {
|
|
|
692
697
|
directory UNINDEXED,
|
|
693
698
|
role UNINDEXED,
|
|
694
699
|
part_type UNINDEXED,
|
|
700
|
+
tool UNINDEXED,
|
|
695
701
|
time_created UNINDEXED,
|
|
696
702
|
text,
|
|
697
703
|
tokenize='unicode61'
|
|
@@ -708,27 +714,34 @@ function sourceState(db: Database, sourcePath: string) {
|
|
|
708
714
|
|
|
709
715
|
function rebuildSearchIndex(source: Database, index: Database, sourcePath: string, state: { dataVersion: number; mtimeMs: number }) {
|
|
710
716
|
debug.time("fts:rebuild")
|
|
711
|
-
const rows = source.query<
|
|
717
|
+
const rows = source.query<IndexSourceRow, []>(`
|
|
712
718
|
SELECT p.id, p.message_id, p.session_id, s.title AS session_title, s.directory,
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
719
|
+
json_extract(m.data, '$.role') AS role,
|
|
720
|
+
json_extract(p.data, '$.type') AS part_type,
|
|
721
|
+
json_extract(p.data, '$.tool') AS tool,
|
|
722
|
+
p.time_created,
|
|
723
|
+
p.data
|
|
717
724
|
FROM part p
|
|
718
725
|
JOIN message m ON m.id = p.message_id
|
|
719
726
|
JOIN session s ON s.id = p.session_id
|
|
720
|
-
WHERE
|
|
727
|
+
WHERE (
|
|
728
|
+
json_extract(p.data, '$.type') = 'text'
|
|
729
|
+
OR (
|
|
730
|
+
json_extract(p.data, '$.type') = 'tool'
|
|
731
|
+
AND json_extract(p.data, '$.tool') IN ('apply_patch', 'edit', 'write')
|
|
732
|
+
)
|
|
733
|
+
)
|
|
721
734
|
AND json_extract(m.data, '$.role') IN ('user', 'assistant')
|
|
722
735
|
ORDER BY p.time_created DESC
|
|
723
736
|
`).all()
|
|
724
|
-
const insert = index.query<Row, [string, string, string, string, string, string, SearchResult["partType"], number, string]>(`
|
|
725
|
-
INSERT INTO document_fts(id, message_id, session_id, session_title, directory, role, part_type, time_created, text)
|
|
726
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
737
|
+
const insert = index.query<Row, [string, string, string, string, string, string, SearchResult["partType"], string | null, number, string]>(`
|
|
738
|
+
INSERT INTO document_fts(id, message_id, session_id, session_title, directory, role, part_type, tool, time_created, text)
|
|
739
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
727
740
|
`)
|
|
728
741
|
index.exec("BEGIN IMMEDIATE")
|
|
729
742
|
try {
|
|
730
743
|
index.exec("DELETE FROM document_fts")
|
|
731
|
-
for (const row of rows.flatMap(
|
|
744
|
+
for (const row of rows.flatMap(indexSourceRowToRows)) {
|
|
732
745
|
insert.run(
|
|
733
746
|
row.id,
|
|
734
747
|
row.message_id,
|
|
@@ -737,6 +750,7 @@ function rebuildSearchIndex(source: Database, index: Database, sourcePath: strin
|
|
|
737
750
|
row.directory,
|
|
738
751
|
row.role,
|
|
739
752
|
row.part_type ?? "text",
|
|
753
|
+
row.tool ?? null,
|
|
740
754
|
row.time_created,
|
|
741
755
|
row.text,
|
|
742
756
|
)
|
|
@@ -773,6 +787,75 @@ function setMeta(db: Database, key: string, value: string) {
|
|
|
773
787
|
`).run(key, value)
|
|
774
788
|
}
|
|
775
789
|
|
|
790
|
+
function indexSourceRowToRows(row: IndexSourceRow): Row[] {
|
|
791
|
+
const text = extractIndexText(row.data)
|
|
792
|
+
if (!text) return []
|
|
793
|
+
return [{ ...row, text }]
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
function extractIndexText(data: string) {
|
|
797
|
+
try {
|
|
798
|
+
const value = JSON.parse(data) as unknown
|
|
799
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return ""
|
|
800
|
+
const record = value as Record<string, unknown>
|
|
801
|
+
if (record.type === "text") return typeof record.text === "string" ? record.text.trim() : ""
|
|
802
|
+
if (record.type !== "tool") return ""
|
|
803
|
+
return extractToolIndexText(record).replace(/\s+/g, " ").trim()
|
|
804
|
+
} catch {
|
|
805
|
+
return ""
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
function extractToolIndexText(part: Record<string, unknown>) {
|
|
810
|
+
const tool = typeof part.tool === "string" ? part.tool : ""
|
|
811
|
+
const state = recordValue(part.state)
|
|
812
|
+
const input = recordValue(state?.input)
|
|
813
|
+
const metadata = recordValue(state?.metadata)
|
|
814
|
+
|
|
815
|
+
if (tool === "apply_patch") {
|
|
816
|
+
const files = Array.isArray(metadata?.files) ? metadata.files : []
|
|
817
|
+
const renderedPatches = files.map(applyPatchFileIndexText).filter(Boolean).join("\n")
|
|
818
|
+
const patchText = stringValue(input?.patchText)
|
|
819
|
+
return [renderedPatches, patchText].filter(Boolean).join("\n")
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
if (tool === "edit") {
|
|
823
|
+
const filediff = recordValue(metadata?.filediff)
|
|
824
|
+
return [
|
|
825
|
+
stringValue(input?.filePath),
|
|
826
|
+
stringValue(metadata?.diff),
|
|
827
|
+
stringValue(filediff?.patch),
|
|
828
|
+
stringValue(input?.oldString),
|
|
829
|
+
stringValue(input?.newString),
|
|
830
|
+
].filter(Boolean).join("\n")
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
if (tool === "write") {
|
|
834
|
+
return [stringValue(input?.filePath), stringValue(input?.content)].filter(Boolean).join("\n")
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
return ""
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
function applyPatchFileIndexText(value: unknown) {
|
|
841
|
+
const file = recordValue(value)
|
|
842
|
+
if (!file) return ""
|
|
843
|
+
return [
|
|
844
|
+
stringValue(file.filePath),
|
|
845
|
+
stringValue(file.relativePath),
|
|
846
|
+
stringValue(file.patch),
|
|
847
|
+
].filter(Boolean).join("\n")
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
function recordValue(value: unknown): Record<string, unknown> | undefined {
|
|
851
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return
|
|
852
|
+
return value as Record<string, unknown>
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
function stringValue(value: unknown) {
|
|
856
|
+
return typeof value === "string" ? value : undefined
|
|
857
|
+
}
|
|
858
|
+
|
|
776
859
|
const tableCache = new Map<string, boolean>()
|
|
777
860
|
function tableExists(db: Database, name: string) {
|
|
778
861
|
if (tableCache.has(name)) return tableCache.get(name)!
|
|
@@ -816,29 +899,6 @@ function extractFromValue(value: unknown): string {
|
|
|
816
899
|
.join("\n")
|
|
817
900
|
}
|
|
818
901
|
|
|
819
|
-
function sourceRowToSearchRow(row: SourceRow): Row[] {
|
|
820
|
-
const text = extractSearchText(row.data)
|
|
821
|
-
if (!text) return []
|
|
822
|
-
return [{ ...row, text }]
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
function searchablePartText(value: unknown): string {
|
|
826
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return extractFromValue(value)
|
|
827
|
-
const record = value as Record<string, unknown>
|
|
828
|
-
const type = record.type
|
|
829
|
-
|
|
830
|
-
if (type === "text" || type === "reasoning") return typeof record.text === "string" ? record.text : extractFromValue(record)
|
|
831
|
-
if (type === "patch") return extractFromValue(record.files)
|
|
832
|
-
|
|
833
|
-
if (type === "tool") {
|
|
834
|
-
const state = record.state && typeof record.state === "object" && !Array.isArray(record.state) ? record.state as Record<string, unknown> : undefined
|
|
835
|
-
if (record.tool !== "apply_patch") return ""
|
|
836
|
-
return extractFromValue(state?.input)
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
return extractFromValue(record)
|
|
840
|
-
}
|
|
841
|
-
|
|
842
902
|
function truncate(value: string, length: number) {
|
|
843
903
|
if (value.length <= length) return value
|
|
844
904
|
return `${value.slice(0, length - 3)}...`
|
package/telescope.tsx
CHANGED
|
@@ -46,14 +46,14 @@ export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; o
|
|
|
46
46
|
const [loadingPreviewMore, setLoadingPreviewMore] = createSignal(false)
|
|
47
47
|
const [prefetchingPreviewBefore, setPrefetchingPreviewBefore] = createSignal(false)
|
|
48
48
|
const [prefetchingPreviewAfter, setPrefetchingPreviewAfter] = createSignal(false)
|
|
49
|
-
const
|
|
50
|
-
const
|
|
51
|
-
const RESULT_PREFETCH_AHEAD_ROWS = 25
|
|
49
|
+
const MIN_SEARCH_BATCH_SIZE = 25
|
|
50
|
+
const MIN_RECENT_BATCH_SIZE = 15
|
|
52
51
|
const RESULT_OVERSCAN_MULTIPLIER = 2
|
|
52
|
+
const RESULT_BATCH_VIEWPORTS = 4
|
|
53
|
+
const RESULT_PREFETCH_VIEWPORTS = 10
|
|
53
54
|
const RESULT_CACHE_BEHIND_VIEWPORTS = 6
|
|
54
|
-
const INITIAL_PREVIEW_BEFORE =
|
|
55
|
-
const INITIAL_PREVIEW_AFTER =
|
|
56
|
-
const INITIAL_PREVIEW_DELAY_MS = 75
|
|
55
|
+
const INITIAL_PREVIEW_BEFORE = 20
|
|
56
|
+
const INITIAL_PREVIEW_AFTER = 30
|
|
57
57
|
const PREVIEW_PAGE_SIZE = 20
|
|
58
58
|
const PREVIEW_PREFETCH_VIEWPORTS = 0.5
|
|
59
59
|
let input: InputRenderable | undefined
|
|
@@ -91,19 +91,18 @@ export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; o
|
|
|
91
91
|
let resultPreviousTimer: ReturnType<typeof setTimeout> | undefined
|
|
92
92
|
let previewBeforeTimer: ReturnType<typeof setTimeout> | undefined
|
|
93
93
|
let previewAfterTimer: ReturnType<typeof setTimeout> | undefined
|
|
94
|
-
let pendingPreviewBefore: {
|
|
95
|
-
let
|
|
94
|
+
let pendingPreviewBefore: { previousContentHeight: number; preserveScroll: boolean; visibleLoad: boolean } | undefined
|
|
95
|
+
let pendingPreviewAfterVisible = false
|
|
96
96
|
const cancelPreviewPrefetch = () => {
|
|
97
97
|
if (previewBeforeTimer) clearTimeout(previewBeforeTimer)
|
|
98
98
|
if (previewAfterTimer) clearTimeout(previewAfterTimer)
|
|
99
99
|
previewBeforeTimer = undefined
|
|
100
100
|
previewAfterTimer = undefined
|
|
101
101
|
pendingPreviewBefore = undefined
|
|
102
|
-
|
|
102
|
+
pendingPreviewAfterVisible = false
|
|
103
103
|
setPrefetchingPreviewBefore(false)
|
|
104
104
|
setPrefetchingPreviewAfter(false)
|
|
105
105
|
setLoadingPreviewMore(false)
|
|
106
|
-
setPreviewEdgeLoadingReady(false)
|
|
107
106
|
}
|
|
108
107
|
onCleanup(() => {
|
|
109
108
|
if (resultPrefetchTimer) clearTimeout(resultPrefetchTimer)
|
|
@@ -116,9 +115,9 @@ export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; o
|
|
|
116
115
|
const viewportHeight = resultScroll?.height || Math.max(8, height() - 7)
|
|
117
116
|
return Math.max(5, Math.floor(viewportHeight / resultRowHeight()))
|
|
118
117
|
}
|
|
119
|
-
const searchBatchSize = () =>
|
|
120
|
-
const recentBatchSize = () =>
|
|
121
|
-
const resultPrefetchThreshold = () =>
|
|
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
|
|
122
121
|
const resultPrefetchState = () => {
|
|
123
122
|
const cachedEnd = resultBaseOffset() + results().length
|
|
124
123
|
const rowsAhead = Math.max(0, cachedEnd - selected() - 1)
|
|
@@ -438,20 +437,14 @@ export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; o
|
|
|
438
437
|
return lastChild ? lastChild.y + lastChild.height : 0
|
|
439
438
|
}
|
|
440
439
|
|
|
441
|
-
const
|
|
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) => {
|
|
440
|
+
const loadPreviewBefore = (previousContentHeight = previewContentHeight(), preserveScroll = true, visibleLoad = false) => {
|
|
447
441
|
const item = selectedResult()
|
|
448
442
|
const first = previewParts()[0]
|
|
449
|
-
if (!item ||
|
|
443
|
+
if (!item || !first || loadingPreviewMore() || prefetchingPreviewBefore()) return
|
|
450
444
|
visibleLoad ? setLoadingPreviewMore(true) : setPrefetchingPreviewBefore(true)
|
|
451
445
|
debug.time("preview:load-before")
|
|
452
446
|
try {
|
|
453
447
|
const page = loadConversationBefore(item, { id: first.id, timeCreated: first.timeCreated }, { limit: PREVIEW_PAGE_SIZE, dbPath: dbPath() })
|
|
454
|
-
if (selectedResult()?.id !== itemId) return
|
|
455
448
|
debug.log("preview:load-before", {
|
|
456
449
|
item: item.id,
|
|
457
450
|
added: page.parts.length,
|
|
@@ -465,7 +458,6 @@ export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; o
|
|
|
465
458
|
setPreviewParts((prev) => [...page.parts, ...prev])
|
|
466
459
|
if (preserveScroll) {
|
|
467
460
|
setTimeout(() => {
|
|
468
|
-
if (selectedResult()?.id !== itemId) return
|
|
469
461
|
const delta = previewContentHeight() - previousContentHeight
|
|
470
462
|
if (delta > 0) previewScroll?.scrollBy(delta)
|
|
471
463
|
}, 1)
|
|
@@ -480,15 +472,14 @@ export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; o
|
|
|
480
472
|
}
|
|
481
473
|
}
|
|
482
474
|
|
|
483
|
-
const loadPreviewAfter = (
|
|
475
|
+
const loadPreviewAfter = (visibleLoad = false) => {
|
|
484
476
|
const item = selectedResult()
|
|
485
477
|
const last = previewParts().at(-1)
|
|
486
|
-
if (!item ||
|
|
478
|
+
if (!item || !last || loadingPreviewMore() || prefetchingPreviewAfter()) return
|
|
487
479
|
visibleLoad ? setLoadingPreviewMore(true) : setPrefetchingPreviewAfter(true)
|
|
488
480
|
debug.time("preview:load-after")
|
|
489
481
|
try {
|
|
490
482
|
const page = loadConversationAfter(item, { id: last.id, timeCreated: last.timeCreated }, { limit: PREVIEW_PAGE_SIZE, dbPath: dbPath() })
|
|
491
|
-
if (selectedResult()?.id !== itemId) return
|
|
492
483
|
debug.log("preview:load-after", {
|
|
493
484
|
item: item.id,
|
|
494
485
|
added: page.parts.length,
|
|
@@ -508,8 +499,7 @@ export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; o
|
|
|
508
499
|
}
|
|
509
500
|
|
|
510
501
|
const schedulePreviewBefore = (previousContentHeight = previewContentHeight(), preserveScroll = true, visibleLoad = false) => {
|
|
511
|
-
|
|
512
|
-
if (!item || !hasMorePreviewBefore() || loadingPreviewMore() || prefetchingPreviewBefore()) return
|
|
502
|
+
if (!hasMorePreviewBefore() || loadingPreviewMore() || prefetchingPreviewBefore()) return
|
|
513
503
|
if (previewBeforeTimer) {
|
|
514
504
|
if (pendingPreviewBefore) {
|
|
515
505
|
pendingPreviewBefore.preserveScroll = pendingPreviewBefore.preserveScroll && preserveScroll
|
|
@@ -517,27 +507,26 @@ export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; o
|
|
|
517
507
|
}
|
|
518
508
|
return
|
|
519
509
|
}
|
|
520
|
-
pendingPreviewBefore = {
|
|
521
|
-
debug.log("preview:prefetch-before-scheduled", {
|
|
510
|
+
pendingPreviewBefore = { previousContentHeight, preserveScroll, visibleLoad }
|
|
511
|
+
debug.log("preview:prefetch-before-scheduled", { preserveScroll, visibleLoad })
|
|
522
512
|
previewBeforeTimer = setTimeout(() => {
|
|
523
513
|
const pending = pendingPreviewBefore
|
|
524
514
|
previewBeforeTimer = undefined
|
|
525
515
|
pendingPreviewBefore = undefined
|
|
526
|
-
if (pending
|
|
516
|
+
if (pending) loadPreviewBefore(pending.previousContentHeight, pending.preserveScroll, pending.visibleLoad)
|
|
527
517
|
}, 1)
|
|
528
518
|
}
|
|
529
519
|
|
|
530
520
|
const schedulePreviewAfter = (visibleLoad = false) => {
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
pendingPreviewAfter = { itemId: item.id, visibleLoad: (pendingPreviewAfter?.visibleLoad || visibleLoad) }
|
|
521
|
+
if (!hasMorePreviewAfter() || loadingPreviewMore() || prefetchingPreviewAfter()) return
|
|
522
|
+
pendingPreviewAfterVisible = pendingPreviewAfterVisible || visibleLoad
|
|
534
523
|
if (previewAfterTimer) return
|
|
535
|
-
debug.log("preview:prefetch-after-scheduled", {
|
|
524
|
+
debug.log("preview:prefetch-after-scheduled", { visibleLoad })
|
|
536
525
|
previewAfterTimer = setTimeout(() => {
|
|
537
|
-
const
|
|
526
|
+
const pendingVisibleLoad = pendingPreviewAfterVisible
|
|
538
527
|
previewAfterTimer = undefined
|
|
539
|
-
|
|
540
|
-
|
|
528
|
+
pendingPreviewAfterVisible = false
|
|
529
|
+
loadPreviewAfter(pendingVisibleLoad)
|
|
541
530
|
}, 1)
|
|
542
531
|
}
|
|
543
532
|
|
|
@@ -554,62 +543,33 @@ export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; o
|
|
|
554
543
|
if (item.id === lastPreviewItemId) return
|
|
555
544
|
lastPreviewItemId = item.id
|
|
556
545
|
cancelPreviewPrefetch()
|
|
557
|
-
resetPreviewScroll()
|
|
558
|
-
solidBatch(() => {
|
|
559
|
-
setPreviewParts([])
|
|
560
|
-
setHasMorePreviewBefore(false)
|
|
561
|
-
setHasMorePreviewAfter(false)
|
|
562
|
-
setPreviewEdgeLoadingReady(false)
|
|
563
|
-
})
|
|
564
546
|
debug.log("preview:new-item", item.sessionTitle?.slice(0, 40) ?? item.id.slice(-8))
|
|
565
547
|
const db = dbPath()
|
|
566
|
-
|
|
567
|
-
|
|
548
|
+
debug.time("preview:load")
|
|
549
|
+
try {
|
|
550
|
+
const page = loadConversationAround(item, { before: INITIAL_PREVIEW_BEFORE, after: INITIAL_PREVIEW_AFTER, dbPath: db })
|
|
551
|
+
debug.log("preview:init", {
|
|
552
|
+
item: item.id,
|
|
553
|
+
session: item.sessionID,
|
|
554
|
+
parts: page.parts.length,
|
|
555
|
+
hasMoreBefore: page.hasMoreBefore,
|
|
556
|
+
hasMoreAfter: page.hasMoreAfter,
|
|
557
|
+
first: page.parts[0]?.id,
|
|
558
|
+
last: page.parts.at(-1)?.id,
|
|
559
|
+
})
|
|
560
|
+
setPreviewParts(page.parts)
|
|
561
|
+
setHasMorePreviewBefore(page.hasMoreBefore)
|
|
562
|
+
setHasMorePreviewAfter(page.hasMoreAfter)
|
|
563
|
+
} catch {}
|
|
568
564
|
debug.timeEnd("nav:total")
|
|
569
|
-
|
|
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
|
-
})
|
|
565
|
+
debug.timeEnd("preview:load")
|
|
606
566
|
})
|
|
607
567
|
|
|
608
568
|
createEffect(() => {
|
|
609
569
|
const item = selectedResult()
|
|
610
570
|
if (!item) return
|
|
611
571
|
const interval = setInterval(() => {
|
|
612
|
-
if (
|
|
572
|
+
if (loadingPreviewMore() || prefetchingPreviewBefore() || prefetchingPreviewAfter()) return
|
|
613
573
|
const scroll = previewScroll
|
|
614
574
|
const children = scroll?.getChildren()
|
|
615
575
|
if (!scroll || !children || children.length === 0) return
|
|
@@ -650,19 +610,8 @@ export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; o
|
|
|
650
610
|
if (!item) return
|
|
651
611
|
if (item.id === scrolledItem) return
|
|
652
612
|
scrolledItem = item.id
|
|
653
|
-
const
|
|
654
|
-
|
|
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
|
-
})
|
|
613
|
+
const timer = setTimeout(() => scrollPreviewToTarget(previewScroll, messageTargetID(item)), 1)
|
|
614
|
+
onCleanup(() => clearTimeout(timer))
|
|
666
615
|
})
|
|
667
616
|
|
|
668
617
|
const open = () => {
|
package/ui/render-target.ts
CHANGED
|
@@ -6,7 +6,7 @@ export function previewScrollAmount(scroll: ScrollBoxRenderable | undefined) {
|
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
export function messageTargetID(item: SearchResult) {
|
|
9
|
-
if (item.partType === "tool") return `tool
|
|
9
|
+
if (item.partType === "tool") return `tool-${item.messageID}-${item.id}`
|
|
10
10
|
if (item.partType === "reasoning") return `text-${item.messageID}-${item.id}`
|
|
11
11
|
if (item.role === "assistant") return `text-${item.messageID}-${item.id}`
|
|
12
12
|
return item.messageID
|