@bojackduy/opencode-telescope 0.1.21 → 0.1.22

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 CHANGED
@@ -1,9 +1,18 @@
1
1
  # opencode-telescope
2
2
 
3
- Fuzzy search across all your OpenCode conversations grep through session and chat history, find code snippets, and jump to any chat instantly.
3
+ OpenCode TUI plugin for fuzzy-searching local conversation history, session transcripts, and past AI coding chats.
4
+
5
+ Install the npm package `@bojackduy/opencode-telescope` to grep through OpenCode chat history, find old code snippets, and jump back to any session instantly.
4
6
 
5
7
  > Inspired by [telescope.nvim](https://github.com/nvim-telescope/telescope.nvim) — a fuzzy finder for your conversation history.
6
8
 
9
+ ## Links
10
+
11
+ - Documentation: https://bojackduy.github.io/opencode-telescope/
12
+ - npm: https://www.npmjs.com/package/@bojackduy/opencode-telescope
13
+ - GitHub: https://github.com/bojackduy/opencode-telescope
14
+ - Issues: https://github.com/bojackduy/opencode-telescope/issues
15
+
7
16
  ![Demo](./assets/demo.png)
8
17
 
9
18
  ## Use cases
@@ -19,9 +28,12 @@ Fuzzy search across all your OpenCode conversations — grep through session and
19
28
  - **Live preview** — preview the matched conversation result before opening
20
29
  - **Find & jump** — select any result and jump straight to that session
21
30
  - **Neovim Telescope-style UX** — familiar `<leader>f` keybind and `/telescope` command
31
+ - **Local-first search** — reads your OpenCode session database without sending chat history to a remote service
22
32
 
23
33
  ## Installation
24
34
 
35
+ Install from npm as `@bojackduy/opencode-telescope`.
36
+
25
37
  Add the plugin to your `tui.json`:
26
38
 
27
39
  ```jsonc
@@ -88,4 +100,8 @@ Key strings support simple names like `j`, `k`, `down`, `up`, `enter`, `return`,
88
100
 
89
101
  Reads the OpenCode local SQLite session database in read-only mode, parses conversations into searchable text, and opens the selected session through the existing TUI route.
90
102
 
103
+ ## Keywords
104
+
105
+ OpenCode plugin, OpenCode TUI, fuzzy finder, conversation search, chat history search, AI coding session search, LLM history, local session search, Telescope-style picker.
106
+
91
107
  ![Demo animation](./assets/demo.gif)
package/package.json CHANGED
@@ -1,17 +1,17 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "@bojackduy/opencode-telescope",
4
- "version": "0.1.21",
5
- "description": "Fuzzy search across all OpenCode conversations grep session and chat history, find code snippets, and jump to any chat instantly",
4
+ "version": "0.1.22",
5
+ "description": "OpenCode TUI plugin for fuzzy-searching local conversation history, session transcripts, and past AI coding chats",
6
6
  "type": "module",
7
7
  "license": "MIT",
8
8
  "repository": {
9
9
  "type": "git",
10
- "url": "git+https://github.com/Duyyy123/opencode-telescope.git"
10
+ "url": "git+https://github.com/bojackduy/opencode-telescope.git"
11
11
  },
12
- "homepage": "https://github.com/Duyyy123/opencode-telescope#readme",
12
+ "homepage": "https://bojackduy.github.io/opencode-telescope/",
13
13
  "bugs": {
14
- "url": "https://github.com/Duyyy123/opencode-telescope/issues"
14
+ "url": "https://github.com/bojackduy/opencode-telescope/issues"
15
15
  },
16
16
  "keywords": [
17
17
  "opencode",
@@ -21,11 +21,15 @@
21
21
  "search",
22
22
  "session",
23
23
  "conversation",
24
+ "conversation-search",
24
25
  "history",
26
+ "ai-chat-history",
27
+ "llm-history",
25
28
  "fuzzy",
26
29
  "fuzzy-finder",
27
30
  "grep",
28
31
  "finder",
32
+ "opencode-tui",
29
33
  "neovim",
30
34
  "chat-history"
31
35
  ],
package/telescope.tsx CHANGED
@@ -93,6 +93,9 @@ export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; o
93
93
  let resultNavigationStarted = false
94
94
  let resultPrefetchTimer: ReturnType<typeof setTimeout> | undefined
95
95
  let resultPreviousTimer: ReturnType<typeof setTimeout> | undefined
96
+ let searchTimer: ReturnType<typeof setTimeout> | undefined
97
+ let lastFiredQuery = ""
98
+ const SEARCH_DEBOUNCE_MS = 200
96
99
  type PreviewBeforeIntent = "passive-prefetch" | "explicit-scroll"
97
100
  type PendingPreviewBefore = {
98
101
  id: number
@@ -193,6 +196,51 @@ export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; o
193
196
  })
194
197
  })
195
198
 
199
+ const executeSearch = (q: string, role: SearchRole | undefined, db: string, dir: string) => {
200
+ lastFiredQuery = q
201
+ const limit = q ? Math.min(searchBatchSize(), 80) : recentBatchSize()
202
+ setLoading(true)
203
+ if (q) setBusy(true)
204
+ debug.time("query:search")
205
+ try {
206
+ debug.log("bootstrap:search:start", { limit, directory: dir, role, query: q || "(all recent)" })
207
+ const batch = q
208
+ ? searchSessionMessages(q, { limit, offset: 0, dbPath: db, directory: dir, role })
209
+ : recentSessionMessages({ limit, offset: 0, dbPath: db, directory: dir, role })
210
+ debug.log("bootstrap:search:done", { rows: batch.length, limit })
211
+ solidBatch(() => {
212
+ setResults(batch)
213
+ setResultBaseOffset(0)
214
+ setNextResultOffset(batch.length)
215
+ setHasMore(batch.length >= limit)
216
+ setResultPageInfo({ loadedUntil: batch.length, hasMore: batch.length >= limit, pageSize: limit, lastOffset: 0, lastAdded: batch.length })
217
+ setSelected(0)
218
+ })
219
+ } catch (err) {
220
+ debug.log("bootstrap:search:error", err instanceof Error ? err.message : String(err))
221
+ solidBatch(() => {
222
+ setResults([])
223
+ setResultBaseOffset(0)
224
+ setNextResultOffset(0)
225
+ setResultPageInfo({ loadedUntil: 0, hasMore: false, pageSize: limit, lastOffset: 0, lastAdded: 0 })
226
+ })
227
+ setError(err instanceof Error ? err.message : String(err))
228
+ } finally {
229
+ debug.timeEnd("query:search")
230
+ setBusy(false)
231
+ setLoading(false)
232
+ }
233
+ }
234
+
235
+ const scheduleSearch = (q: string, role: SearchRole | undefined, db: string, dir: string) => {
236
+ if (searchTimer) clearTimeout(searchTimer)
237
+ if (q === lastFiredQuery && results().length > 0) return
238
+ searchTimer = setTimeout(() => {
239
+ searchTimer = undefined
240
+ executeSearch(q, role, db, dir)
241
+ }, q ? SEARCH_DEBOUNCE_MS : 1)
242
+ }
243
+
196
244
  createEffect(() => {
197
245
  const q = query().trim()
198
246
  const role = ownerRole()
@@ -211,73 +259,11 @@ export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; o
211
259
  const db = dbPath()
212
260
  const dir = directory
213
261
 
214
- if (!q) {
215
- setLoading(true)
216
- const limit = recentBatchSize()
217
- const timer = setTimeout(() => {
218
- debug.log("bootstrap:recent:start", { limit, directory: dir, role })
219
- debug.time("query:recent")
220
- try {
221
- const batch = recentSessionMessages({ limit, offset: 0, dbPath: db, directory: dir, role })
222
- debug.log("bootstrap:recent:done", { rows: batch.length, limit })
223
- solidBatch(() => {
224
- setResults(batch)
225
- setResultBaseOffset(0)
226
- setNextResultOffset(batch.length)
227
- setHasMore(batch.length >= limit)
228
- setResultPageInfo({ loadedUntil: batch.length, hasMore: batch.length >= limit, pageSize: limit, lastOffset: 0, lastAdded: batch.length })
229
- setSelected(0)
230
- })
231
- } catch (err) {
232
- debug.log("bootstrap:recent:error", err instanceof Error ? err.message : String(err))
233
- solidBatch(() => {
234
- setResults([])
235
- setResultBaseOffset(0)
236
- setNextResultOffset(0)
237
- setResultPageInfo({ loadedUntil: 0, hasMore: false, pageSize: limit, lastOffset: 0, lastAdded: 0 })
238
- })
239
- setError(err instanceof Error ? err.message : String(err))
240
- }
241
- debug.timeEnd("query:recent")
242
- setLoading(false)
243
- debug.log("component:interactive")
244
- }, 1)
245
- onCleanup(() => clearTimeout(timer))
246
- return
247
- }
248
-
249
- setLoading(true)
250
- setBusy(true)
251
- const limit = searchBatchSize()
252
- const timer = setTimeout(() => {
253
- debug.time("query:search")
254
- try {
255
- debug.log("bootstrap:search:start", { limit, directory: dir, role, query: q })
256
- const batch = searchSessionMessages(q, { limit, offset: 0, dbPath: db, directory: dir, role })
257
- debug.log("bootstrap:search:done", { rows: batch.length, limit })
258
- solidBatch(() => {
259
- setResults(batch)
260
- setResultBaseOffset(0)
261
- setNextResultOffset(batch.length)
262
- setHasMore(batch.length >= limit)
263
- setResultPageInfo({ loadedUntil: batch.length, hasMore: batch.length >= limit, pageSize: limit, lastOffset: 0, lastAdded: batch.length })
264
- setSelected(0)
265
- })
266
- } catch (err) {
267
- solidBatch(() => {
268
- setResults([])
269
- setResultBaseOffset(0)
270
- setNextResultOffset(0)
271
- setResultPageInfo({ loadedUntil: 0, hasMore: false, pageSize: limit, lastOffset: 0, lastAdded: 0 })
272
- })
273
- setError(err instanceof Error ? err.message : String(err))
274
- } finally {
275
- debug.timeEnd("query:search")
276
- setBusy(false)
277
- setLoading(false)
278
- }
279
- }, 180)
280
- onCleanup(() => clearTimeout(timer))
262
+ scheduleSearch(q, role, db, dir)
263
+ onCleanup(() => {
264
+ if (searchTimer) clearTimeout(searchTimer)
265
+ searchTimer = undefined
266
+ })
281
267
  })
282
268
 
283
269
  const loadMoreResults = (advance = false) => {
@@ -778,6 +764,33 @@ export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; o
778
764
  })
779
765
  }
780
766
  }
767
+
768
+ if (pending.intent === "passive-prefetch") {
769
+ const matchItem = selectedResult()
770
+ const scroll = previewScroll
771
+ if (matchItem && scroll) {
772
+ const layout = previewVirtualLayout()
773
+ const matchRow = layout.find((r) => r.part.id === matchItem.id)
774
+ if (matchRow) {
775
+ const viewportHeight = scroll.height
776
+ const desiredScrollTop = Math.max(0, matchRow.top - Math.floor(viewportHeight / 3))
777
+ const msSinceManual = Date.now() - lastPreviewScrollKeyMs
778
+ if (Math.abs(scroll.scrollTop - desiredScrollTop) >= 2 && msSinceManual > 700) {
779
+ const correctedWindow = previewWindowForLayout(layout, desiredScrollTop, viewportHeight)
780
+ setPreviewWindow(correctedWindow)
781
+ scroll.scrollTo(desiredScrollTop)
782
+ debug.log("preview:load-before:recenter", {
783
+ matchPartID: matchItem.id,
784
+ matchTop: matchRow.top,
785
+ scrollTop: scroll.scrollTop,
786
+ desiredScrollTop,
787
+ viewportHeight,
788
+ window: correctedWindow,
789
+ })
790
+ }
791
+ }
792
+ }
793
+ }
781
794
  }, 1)
782
795
  }
783
796
  setHasMorePreviewBefore(page.hasMoreBefore)
@@ -971,7 +984,24 @@ export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; o
971
984
  debug.log("preview:target-scroll:estimated", { targetID, targetTop: row.top, scrollTop: scroll.scrollTop, nextScrollTop: top, window: previewWindow() })
972
985
  scroll.scrollTo(top)
973
986
  updatePreviewWindow()
974
- setTimeout(() => scrollPreviewToTarget(scroll, targetID), 1)
987
+ const scrollTopBeforeReal = scroll.scrollTop
988
+ setTimeout(() => {
989
+ scrollPreviewToTarget(scroll, targetID)
990
+ const now = Date.now()
991
+ const adjustmentMs = lastPreviewScrollKeyMs
992
+ if (adjustmentMs > 0 && now - adjustmentMs > 300) {
993
+ const targetTopReal = row.top
994
+ const expectedScrollTop = Math.max(0, targetTopReal - Math.max(1, Math.floor(scroll.height / 3)))
995
+ const drift = scroll.scrollTop - expectedScrollTop
996
+ if (Math.abs(drift) >= 2) {
997
+ const correctedScrollTop = scroll.scrollTop - drift
998
+ const correctedWindow = previewWindowForLayout(previewVirtualLayout(), correctedScrollTop, scroll.height)
999
+ setPreviewWindow(correctedWindow)
1000
+ scroll.scrollTo(correctedScrollTop)
1001
+ debug.log("preview:target-scroll:real-correct", { targetID, drift, scrollTopAfterReal: scroll.scrollTop, correctedScrollTop, window: correctedWindow })
1002
+ }
1003
+ }
1004
+ }, 1)
975
1005
  }
976
1006
 
977
1007
  let lastPreviewItemId = ""
@@ -31,18 +31,20 @@ export function scrollPreviewToTarget(scroll: ScrollBoxRenderable | undefined, t
31
31
  })
32
32
  return
33
33
  }
34
- const delta = target.y - scroll.y - Math.max(1, Math.floor(scroll.height / 3))
34
+ const contentY = target.y + scroll.scrollTop - scroll.y
35
+ const desiredScrollTop = Math.max(0, contentY - Math.max(1, Math.floor(scroll.height / 3)))
35
36
  debug.log("preview:target-scroll", {
36
37
  targetID,
37
38
  targetY: target.y,
38
39
  scrollY: scroll.y,
39
40
  scrollTop: scroll.scrollTop,
41
+ contentY,
42
+ desiredScrollTop,
40
43
  scrollHeight: scroll.height,
41
44
  contentHeight: scroll.scrollHeight,
42
- delta,
43
45
  })
44
- scroll.scrollBy(delta)
45
- debug.log("preview:target-scroll:after", { targetID, scrollY: scroll.y, scrollTop: scroll.scrollTop })
46
+ scroll.scrollTo(desiredScrollTop)
47
+ debug.log("preview:target-scroll:after", { targetID, scrollY: scroll.y, scrollTop: scroll.scrollTop, desiredScrollTop })
46
48
  }
47
49
 
48
50
  export function jumpToRenderedTarget(root: unknown, targetID: string) {