@bojackduy/opencode-telescope 0.1.16 → 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 +128 -12
- package/ui/render-target.ts +2 -0
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,6 +11,8 @@ export type SearchResult = {
|
|
|
11
11
|
sessionTitle: string
|
|
12
12
|
directory: string
|
|
13
13
|
role: "user" | "assistant"
|
|
14
|
+
partType: "text" | "reasoning" | "tool"
|
|
15
|
+
tool?: string
|
|
14
16
|
timeCreated: number
|
|
15
17
|
snippet: string
|
|
16
18
|
matchStart: number
|
|
@@ -56,6 +58,7 @@ export type ConversationPreviewCursor = {
|
|
|
56
58
|
export type ToolState = {
|
|
57
59
|
status: "pending" | "running" | "completed" | "error"
|
|
58
60
|
input?: unknown
|
|
61
|
+
metadata?: unknown
|
|
59
62
|
output?: string
|
|
60
63
|
error?: string
|
|
61
64
|
}
|
|
@@ -67,10 +70,16 @@ type Row = {
|
|
|
67
70
|
session_title: string | null
|
|
68
71
|
directory: string
|
|
69
72
|
role: SearchRole
|
|
73
|
+
part_type?: SearchResult["partType"]
|
|
74
|
+
tool?: string | null
|
|
70
75
|
time_created: number
|
|
71
76
|
text: string
|
|
72
77
|
}
|
|
73
78
|
|
|
79
|
+
type IndexSourceRow = Omit<Row, "text"> & {
|
|
80
|
+
data: string
|
|
81
|
+
}
|
|
82
|
+
|
|
74
83
|
type ConversationRow = {
|
|
75
84
|
id: string
|
|
76
85
|
message_id: string
|
|
@@ -318,6 +327,8 @@ export function rowToSearchResult(row: Row, query: string): SearchResult | undef
|
|
|
318
327
|
sessionTitle: row.session_title || "Untitled session",
|
|
319
328
|
directory: row.directory,
|
|
320
329
|
role: row.role,
|
|
330
|
+
partType: row.part_type ?? "text",
|
|
331
|
+
tool: row.tool ?? undefined,
|
|
321
332
|
timeCreated: row.time_created,
|
|
322
333
|
snippet: makeSnippet(text, query),
|
|
323
334
|
matchStart: match.start,
|
|
@@ -490,7 +501,7 @@ function parseConversationPart(row: ConversationRow, target: boolean): Conversat
|
|
|
490
501
|
role: row.role,
|
|
491
502
|
type: row.type,
|
|
492
503
|
timeCreated: row.time_created,
|
|
493
|
-
text:
|
|
504
|
+
text: extractToolIndexText(data).trim(),
|
|
494
505
|
tool: typeof data.tool === "string" ? data.tool : "tool",
|
|
495
506
|
state: parseToolState(data.state),
|
|
496
507
|
target,
|
|
@@ -532,6 +543,7 @@ function parseToolState(value: unknown): ToolState | undefined {
|
|
|
532
543
|
return {
|
|
533
544
|
status: state.status as ToolState["status"],
|
|
534
545
|
input: state.input,
|
|
546
|
+
metadata: state.metadata,
|
|
535
547
|
output: typeof state.output === "string" ? state.output : undefined,
|
|
536
548
|
error: typeof state.error === "string" ? state.error : undefined,
|
|
537
549
|
}
|
|
@@ -568,7 +580,7 @@ function indexedTextRows(db: Database, dbPath: string, limit: number, query: str
|
|
|
568
580
|
const offsetClause = offset ? "OFFSET ?" : ""
|
|
569
581
|
debug.time("query:fts:exec")
|
|
570
582
|
const rows = index.query<Row, (string | number)[]>(`
|
|
571
|
-
SELECT id, message_id, session_id, session_title, directory, role,
|
|
583
|
+
SELECT id, message_id, session_id, session_title, directory, role, part_type, tool,
|
|
572
584
|
CAST(time_created AS INTEGER) AS time_created, text
|
|
573
585
|
FROM document_fts
|
|
574
586
|
WHERE ${conditions.join(" AND ")}
|
|
@@ -638,15 +650,18 @@ function ensureSearchIndex(source: Database, sourcePath: string) {
|
|
|
638
650
|
const currentDataVersion = getMeta(_indexDb, "source_data_version")
|
|
639
651
|
const currentMtimeMs = getMeta(_indexDb, "source_mtime_ms")
|
|
640
652
|
const currentPath = getMeta(_indexDb, "source_path")
|
|
641
|
-
|
|
653
|
+
const currentIndexVersion = getMeta(_indexDb, "index_version")
|
|
654
|
+
if (currentPath !== sourcePath || currentDataVersion !== String(state.dataVersion) || currentMtimeMs !== String(state.mtimeMs) || currentIndexVersion !== SEARCH_INDEX_VERSION) {
|
|
642
655
|
rebuildSearchIndex(source, _indexDb, sourcePath, state)
|
|
643
656
|
}
|
|
644
657
|
return _indexDb
|
|
645
658
|
}
|
|
646
659
|
|
|
660
|
+
const SEARCH_INDEX_VERSION = "4"
|
|
661
|
+
|
|
647
662
|
function searchIndexPath(sourcePath: string) {
|
|
648
663
|
const parsed = path.parse(sourcePath)
|
|
649
|
-
return path.join(parsed.dir, `${parsed.name}-search.db`)
|
|
664
|
+
return path.join(parsed.dir, `${parsed.name}-telescope-search.db`)
|
|
650
665
|
}
|
|
651
666
|
|
|
652
667
|
function migrateSearchIndex(db: Database) {
|
|
@@ -662,11 +677,33 @@ function migrateSearchIndex(db: Database) {
|
|
|
662
677
|
session_title,
|
|
663
678
|
directory UNINDEXED,
|
|
664
679
|
role UNINDEXED,
|
|
680
|
+
part_type UNINDEXED,
|
|
681
|
+
tool UNINDEXED,
|
|
665
682
|
time_created UNINDEXED,
|
|
666
683
|
text,
|
|
667
684
|
tokenize='unicode61'
|
|
668
685
|
);
|
|
669
686
|
`)
|
|
687
|
+
|
|
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")) {
|
|
690
|
+
db.exec("DROP TABLE document_fts")
|
|
691
|
+
db.exec(`
|
|
692
|
+
CREATE VIRTUAL TABLE document_fts USING fts5(
|
|
693
|
+
id UNINDEXED,
|
|
694
|
+
message_id UNINDEXED,
|
|
695
|
+
session_id UNINDEXED,
|
|
696
|
+
session_title,
|
|
697
|
+
directory UNINDEXED,
|
|
698
|
+
role UNINDEXED,
|
|
699
|
+
part_type UNINDEXED,
|
|
700
|
+
tool UNINDEXED,
|
|
701
|
+
time_created UNINDEXED,
|
|
702
|
+
text,
|
|
703
|
+
tokenize='unicode61'
|
|
704
|
+
);
|
|
705
|
+
`)
|
|
706
|
+
}
|
|
670
707
|
}
|
|
671
708
|
|
|
672
709
|
function sourceState(db: Database, sourcePath: string) {
|
|
@@ -677,27 +714,34 @@ function sourceState(db: Database, sourcePath: string) {
|
|
|
677
714
|
|
|
678
715
|
function rebuildSearchIndex(source: Database, index: Database, sourcePath: string, state: { dataVersion: number; mtimeMs: number }) {
|
|
679
716
|
debug.time("fts:rebuild")
|
|
680
|
-
const rows = source.query<
|
|
717
|
+
const rows = source.query<IndexSourceRow, []>(`
|
|
681
718
|
SELECT p.id, p.message_id, p.session_id, s.title AS session_title, s.directory,
|
|
682
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,
|
|
683
722
|
p.time_created,
|
|
684
|
-
|
|
723
|
+
p.data
|
|
685
724
|
FROM part p
|
|
686
725
|
JOIN message m ON m.id = p.message_id
|
|
687
726
|
JOIN session s ON s.id = p.session_id
|
|
688
|
-
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
|
+
)
|
|
689
734
|
AND json_extract(m.data, '$.role') IN ('user', 'assistant')
|
|
690
|
-
AND json_extract(p.data, '$.text') IS NOT NULL
|
|
691
735
|
ORDER BY p.time_created DESC
|
|
692
736
|
`).all()
|
|
693
|
-
const insert = index.query<Row, [string, string, string, string, string, string, number, string]>(`
|
|
694
|
-
INSERT INTO document_fts(id, message_id, session_id, session_title, directory, role, time_created, text)
|
|
695
|
-
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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
696
740
|
`)
|
|
697
741
|
index.exec("BEGIN IMMEDIATE")
|
|
698
742
|
try {
|
|
699
743
|
index.exec("DELETE FROM document_fts")
|
|
700
|
-
for (const row of rows) {
|
|
744
|
+
for (const row of rows.flatMap(indexSourceRowToRows)) {
|
|
701
745
|
insert.run(
|
|
702
746
|
row.id,
|
|
703
747
|
row.message_id,
|
|
@@ -705,6 +749,8 @@ function rebuildSearchIndex(source: Database, index: Database, sourcePath: strin
|
|
|
705
749
|
row.session_title ?? "Untitled session",
|
|
706
750
|
row.directory,
|
|
707
751
|
row.role,
|
|
752
|
+
row.part_type ?? "text",
|
|
753
|
+
row.tool ?? null,
|
|
708
754
|
row.time_created,
|
|
709
755
|
row.text,
|
|
710
756
|
)
|
|
@@ -712,6 +758,7 @@ function rebuildSearchIndex(source: Database, index: Database, sourcePath: strin
|
|
|
712
758
|
setMeta(index, "source_path", sourcePath)
|
|
713
759
|
setMeta(index, "source_data_version", String(state.dataVersion))
|
|
714
760
|
setMeta(index, "source_mtime_ms", String(state.mtimeMs))
|
|
761
|
+
setMeta(index, "index_version", SEARCH_INDEX_VERSION)
|
|
715
762
|
index.exec("COMMIT")
|
|
716
763
|
} catch (err) {
|
|
717
764
|
index.exec("ROLLBACK")
|
|
@@ -740,6 +787,75 @@ function setMeta(db: Database, key: string, value: string) {
|
|
|
740
787
|
`).run(key, value)
|
|
741
788
|
}
|
|
742
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
|
+
|
|
743
859
|
const tableCache = new Map<string, boolean>()
|
|
744
860
|
function tableExists(db: Database, name: string) {
|
|
745
861
|
if (tableCache.has(name)) return tableCache.get(name)!
|
package/ui/render-target.ts
CHANGED
|
@@ -6,6 +6,8 @@ export function previewScrollAmount(scroll: ScrollBoxRenderable | undefined) {
|
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
export function messageTargetID(item: SearchResult) {
|
|
9
|
+
if (item.partType === "tool") return `tool-${item.messageID}-${item.id}`
|
|
10
|
+
if (item.partType === "reasoning") return `text-${item.messageID}-${item.id}`
|
|
9
11
|
if (item.role === "assistant") return `text-${item.messageID}-${item.id}`
|
|
10
12
|
return item.messageID
|
|
11
13
|
}
|