@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.
@@ -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-inline-${props.part.messageID}-${props.part.id}`} paddingLeft={3} marginTop={1} flexDirection="column" flexShrink={0}>
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.16",
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
- if (currentPath !== sourcePath || currentDataVersion !== String(state.dataVersion) || currentMtimeMs !== String(state.mtimeMs)) {
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<Row, []>(`
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
- json_extract(p.data, '$.text') AS text
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 json_extract(p.data, '$.type') = 'text'
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)!
@@ -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
  }