opencode-sesh 0.1.5 → 0.1.6
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/AGENTS.md +5 -0
- package/CHANGELOG.md +19 -1
- package/README.md +5 -0
- package/bin/sesh-list.sh +56 -38
- package/package.json +1 -1
- package/tui/sesh-panel.tsx +156 -58
package/AGENTS.md
CHANGED
|
@@ -108,6 +108,11 @@ Resume happens in place (`exec` after `cd` to the session's cwd).
|
|
|
108
108
|
`api.route.navigate("session", …)`. `ctrl+o` and the command palette open it;
|
|
109
109
|
Ctrl-X deletes via the opencode CLI and removes the row only after the server
|
|
110
110
|
confirms the deletion.
|
|
111
|
+
- **Picker search:** pages the global session list (`SESSION_PAGE_LIMIT`, capped
|
|
112
|
+
at `SESSION_MAX` and reported when truncated) instead of a fixed 500-row
|
|
113
|
+
window, then indexes transcript text for **every** session in batches
|
|
114
|
+
(`TRANSCRIPT_BATCH`) with bounded remote concurrency (`REMOTE_CONCURRENCY`).
|
|
115
|
+
The header shows indexing progress/coverage. `tests/tui.mjs` locks this.
|
|
111
116
|
|
|
112
117
|
### Hard-won rules — do not regress
|
|
113
118
|
|
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.1.6] - 2026-09-13
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- The TUI picker pages through the entire global session list instead of a
|
|
15
|
+
fixed 500-session window, indexes transcript text for every session (not just
|
|
16
|
+
the newest 150) in batches, bounds remote transcript fetching, and shows
|
|
17
|
+
indexing progress/coverage in the search header.
|
|
18
|
+
- Transcript extraction reads parts in batches and assembles all records in a
|
|
19
|
+
single `jq` pass. A 250-session cold index dropped from about 28 s to about
|
|
20
|
+
7 s locally (warm ~0.3 s).
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- TUI data-layer contract tests (pagination, full index coverage, bounded
|
|
25
|
+
concurrency, progress, and the truncated cap), run on Node 24 in CI.
|
|
26
|
+
|
|
10
27
|
## [0.1.5] - 2026-09-13
|
|
11
28
|
|
|
12
29
|
### Fixed
|
|
@@ -102,7 +119,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
102
119
|
- Fixture-database regression suite, TypeScript typechecking, and ShellCheck in
|
|
103
120
|
CI.
|
|
104
121
|
|
|
105
|
-
[Unreleased]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.1.
|
|
122
|
+
[Unreleased]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.1.6...HEAD
|
|
123
|
+
[0.1.6]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.1.5...v0.1.6
|
|
106
124
|
[0.1.5]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.1.4...v0.1.5
|
|
107
125
|
[0.1.4]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.1.3...v0.1.4
|
|
108
126
|
[0.1.3]: https://github.com/ozandogrultan/opencode-sesh/compare/v0.1.2...v0.1.3
|
package/README.md
CHANGED
|
@@ -137,6 +137,11 @@ read -r id cwd < <(sesh --print --query "auth")
|
|
|
137
137
|
Click the sidebar's `search…` box to filter recent sessions in place; click
|
|
138
138
|
elsewhere or press Esc to leave search.
|
|
139
139
|
|
|
140
|
+
The full picker pages through your whole global session list (no fixed window)
|
|
141
|
+
and indexes transcript text for every session in the background, showing
|
|
142
|
+
indexing progress under the search box. It only stops at a safety cap of 5,000
|
|
143
|
+
sessions, and says so when it does.
|
|
144
|
+
|
|
140
145
|
## Configuration
|
|
141
146
|
|
|
142
147
|
All variables are optional.
|
package/bin/sesh-list.sh
CHANGED
|
@@ -260,53 +260,71 @@ refresh() {
|
|
|
260
260
|
"$JQ_BIN" -c '.stale[]?' "$cache_state" > "$needs_extract"
|
|
261
261
|
while IFS= read -r cache; do [ -n "$cache" ] && rm -f "$cache"; done < <("$JQ_BIN" -r '.prune[]?' "$cache_state")
|
|
262
262
|
|
|
263
|
-
local sid
|
|
264
|
-
local extract_failed=0
|
|
263
|
+
local stale_ids=() sid chunk j i
|
|
265
264
|
while IFS= read -r row; do
|
|
266
265
|
[ -n "$row" ] || continue
|
|
267
266
|
sid=$(printf '%s' "$row" | "$JQ_BIN" -r '.id')
|
|
268
267
|
valid_session_id "$sid" || continue
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
268
|
+
stale_ids+=("$sid")
|
|
269
|
+
done < "$needs_extract"
|
|
270
|
+
|
|
271
|
+
# Batch the transcript read: one SQL query per chunk of sessions and a single
|
|
272
|
+
# jq pass to assemble every record, instead of a query plus three jq processes
|
|
273
|
+
# per session. Text still travels through files, never argv. Any failure leaves
|
|
274
|
+
# every stale session uncached, so the next refresh retries them.
|
|
275
|
+
local extract_failed=0 parts_raw="$scratch/parts.jsonl" records="$scratch/records.jsonl"
|
|
276
|
+
: > "$parts_raw"
|
|
277
|
+
j=0
|
|
278
|
+
while [ "$j" -lt "${#stale_ids[@]}" ]; do
|
|
279
|
+
chunk=''
|
|
280
|
+
for ((i = j; i < j + 200 && i < ${#stale_ids[@]}; i++)); do
|
|
281
|
+
chunk="${chunk:+$chunk,}'${stale_ids[$i]}'"
|
|
282
|
+
done
|
|
283
|
+
if ! db_json "SELECT session_id, data FROM part WHERE session_id IN ($chunk) AND json_extract(data, '\$.type') = 'text' ORDER BY session_id, time_created;" 2>/dev/null \
|
|
284
|
+
| "$JQ_BIN" -c '.[]?' >> "$parts_raw"; then
|
|
286
285
|
extract_failed=1
|
|
287
|
-
|
|
288
|
-
fi
|
|
289
|
-
if [ -z "$title" ]; then
|
|
290
|
-
title=$("$JQ_BIN" -Rrs 'gsub("[\r\n]+"; " ") | if length > 70 then .[0:67] + "..." else . end | select(length > 0) // "(untitled)"' "$text_file" 2>/dev/null) || title='(untitled)'
|
|
291
|
-
[ -n "$title" ] || title='(untitled)'
|
|
286
|
+
break
|
|
292
287
|
fi
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
288
|
+
j=$((j + 200))
|
|
289
|
+
done
|
|
290
|
+
|
|
291
|
+
if [ "$extract_failed" = 0 ]; then
|
|
292
|
+
"$JQ_BIN" -cnr --arg extractions "$EXTRACTIONS" --slurpfile stale "$needs_extract" --slurpfile parts "$parts_raw" '
|
|
293
|
+
($parts
|
|
294
|
+
| map({sid: .session_id,
|
|
295
|
+
text: ((.data | try fromjson catch null)
|
|
296
|
+
| if type == "object" and .type == "text" and (.text | type) == "string" then .text else empty end)})
|
|
297
|
+
| group_by(.sid)
|
|
298
|
+
| map({key: .[0].sid, value: ([.[].text] | join(" "))})
|
|
299
|
+
| from_entries) as $bySid
|
|
300
|
+
| $stale[]
|
|
301
|
+
| select((.id | type) == "string" and (.id | test("^ses_[A-Za-z0-9]+$")))
|
|
302
|
+
| . as $s
|
|
303
|
+
| ($bySid[$s.id] // "") as $text
|
|
304
|
+
| (if ($s.title // "") == ""
|
|
305
|
+
then (($text | gsub("[\r\n]+"; " ") | if length > 70 then .[0:67] + "..." else . end) | select(length > 0) // "(untitled)")
|
|
306
|
+
else $s.title end) as $title
|
|
307
|
+
| {sessionId: $s.id,
|
|
308
|
+
cachePath: ($extractions + "/" + $s.id + ".json"),
|
|
309
|
+
record: {cacheSchema: 2, cacheSession: $s.id,
|
|
310
|
+
cacheUpdated: ($s.time_updated // 0), cacheParts: ($s.partCount // 0), cacheMax: ($s.partMax // 0),
|
|
311
|
+
sessionId: $s.id, transcriptPath: "", source: "opencode",
|
|
312
|
+
cwd: ($s.directory // ""),
|
|
313
|
+
title: ($title | gsub("[\r\n]+"; " ") | if length > 70 then .[0:67] + "..." else . end),
|
|
314
|
+
updatedEpoch: ((($s.time_updated // 0) / 1000 | floor) | if . > 0 then . else ((($s.time_created // 0) / 1000) | floor) end),
|
|
315
|
+
fulltextLower: (($title + " " + $text) | ascii_downcase)}}
|
|
316
|
+
| "\(.sessionId)\n\(.cachePath)\n\(.record | tojson)"' > "$records" || extract_failed=1
|
|
317
|
+
fi
|
|
318
|
+
|
|
319
|
+
if [ "$extract_failed" = 0 ]; then
|
|
320
|
+
while IFS= read -r sid && IFS= read -r cache_path && IFS= read -r record; do
|
|
321
|
+
[ -n "$sid" ] || continue
|
|
302
322
|
cache_tmp=$(mktemp "$EXTRACTIONS/$sid.json.XXXXXX")
|
|
303
323
|
printf '%s\n' "$record" > "$cache_tmp"
|
|
304
|
-
mv "$cache_tmp" "$
|
|
324
|
+
mv "$cache_tmp" "$cache_path"
|
|
305
325
|
printf '%s\n' "$record" >> "$historical"
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
fi
|
|
309
|
-
done < "$needs_extract"
|
|
326
|
+
done < "$records"
|
|
327
|
+
fi
|
|
310
328
|
|
|
311
329
|
if ! "$JQ_BIN" -c --slurpfile hist "$historical" --arg scope "$SCOPE_CWD" --argjson limit "$LIMIT" --argjson archived "$INCLUDE_ARCHIVED" '
|
|
312
330
|
.meta as $meta
|
package/package.json
CHANGED
package/tui/sesh-panel.tsx
CHANGED
|
@@ -159,20 +159,47 @@ function ensureRootMouse(renderer: TuiPluginApi["renderer"]) {
|
|
|
159
159
|
}
|
|
160
160
|
|
|
161
161
|
type Entry = { id: string; title: string; dir: string; group: string; updated: number }
|
|
162
|
+
type EntryResult = { entries: Entry[]; truncated: boolean }
|
|
162
163
|
|
|
163
|
-
|
|
164
|
+
const SESSION_PAGE_LIMIT = 200
|
|
165
|
+
const SESSION_MAX = 5000
|
|
166
|
+
|
|
167
|
+
async function fetchEntries(api: TuiPluginApi): Promise<EntryResult> {
|
|
164
168
|
const home = process.env.HOME ?? ""
|
|
165
|
-
const
|
|
166
|
-
api.client.experimental.session.list({ limit: 500, roots: true, directory: "" }),
|
|
167
|
-
api.client.project.list({}),
|
|
168
|
-
])
|
|
169
|
-
const sessions = ((sessionResult?.data ?? sessionResult) as Session[]) ?? []
|
|
169
|
+
const projectResult = await api.client.project.list({})
|
|
170
170
|
const projects = ((projectResult?.data ?? projectResult) as Project[]) ?? []
|
|
171
|
-
if (!Array.isArray(sessions)) return []
|
|
172
171
|
const projectName = new Map<string, string>(
|
|
173
172
|
projects.map((p) => [p.id, p.name?.trim() || shortDir(p.worktree ?? "", home)]),
|
|
174
173
|
)
|
|
175
|
-
|
|
174
|
+
const sessions: Session[] = []
|
|
175
|
+
let cursor: number | undefined
|
|
176
|
+
let truncated = false
|
|
177
|
+
// Page through the global list instead of one fixed window, so sessions in
|
|
178
|
+
// older directories stay searchable. `cursor` is the previous page's oldest
|
|
179
|
+
// `time.updated`; a repeated or missing cursor stops the walk.
|
|
180
|
+
for (;;) {
|
|
181
|
+
const result = await api.client.experimental.session.list({
|
|
182
|
+
limit: SESSION_PAGE_LIMIT,
|
|
183
|
+
roots: true,
|
|
184
|
+
directory: "",
|
|
185
|
+
...(cursor === undefined ? {} : { cursor }),
|
|
186
|
+
})
|
|
187
|
+
const page = ((result?.data ?? result) as Session[]) ?? []
|
|
188
|
+
if (!Array.isArray(page) || page.length === 0) break
|
|
189
|
+
sessions.push(...page)
|
|
190
|
+
if (page.length < SESSION_PAGE_LIMIT) break
|
|
191
|
+
if (sessions.length >= SESSION_MAX) {
|
|
192
|
+
truncated = true
|
|
193
|
+
break
|
|
194
|
+
}
|
|
195
|
+
const next = page[page.length - 1]?.time?.updated
|
|
196
|
+
if (typeof next !== "number" || next === cursor) {
|
|
197
|
+
truncated = true
|
|
198
|
+
break
|
|
199
|
+
}
|
|
200
|
+
cursor = next
|
|
201
|
+
}
|
|
202
|
+
const entries = sessions
|
|
176
203
|
.filter((s) => !s.time?.archived && !s.parentID)
|
|
177
204
|
.sort((a, b) => b.time.updated - a.time.updated)
|
|
178
205
|
.map((s) => {
|
|
@@ -187,76 +214,118 @@ async function fetchEntries(api: TuiPluginApi): Promise<Entry[]> {
|
|
|
187
214
|
updated: s.time.updated,
|
|
188
215
|
}
|
|
189
216
|
})
|
|
217
|
+
return { entries, truncated }
|
|
190
218
|
}
|
|
191
219
|
|
|
192
|
-
const TRANSCRIPT_INDEX_LIMIT = 150
|
|
193
220
|
const SESSION_ID_PATTERN = /^ses_[A-Za-z0-9]+$/
|
|
221
|
+
const TRANSCRIPT_BATCH = 64
|
|
222
|
+
const REMOTE_CONCURRENCY = 8
|
|
194
223
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
}
|
|
201
|
-
const ids = targets.map((entry) => entry.id).filter((id) => SESSION_ID_PATTERN.test(id))
|
|
202
|
-
if (ids.length === 0) return index
|
|
224
|
+
type IndexProgress = { indexed: number; total: number; complete: boolean }
|
|
225
|
+
|
|
226
|
+
const nextTick = () => new Promise<void>((resolve) => setTimeout(resolve, 0))
|
|
227
|
+
|
|
228
|
+
async function openTranscriptDb(): Promise<any | undefined> {
|
|
203
229
|
try {
|
|
204
230
|
const home = process.env.HOME ?? ""
|
|
205
231
|
const dataHome = process.env.XDG_DATA_HOME ?? `${home}/.local/share`
|
|
206
232
|
// @ts-ignore - bun:sqlite ships with the Bun runtime (verified); no type package installed
|
|
207
233
|
const sqlite: any = await import("bun:sqlite")
|
|
208
|
-
if (typeof sqlite?.Database !== "function")
|
|
209
|
-
|
|
234
|
+
if (typeof sqlite?.Database !== "function") return undefined
|
|
235
|
+
return new sqlite.Database(`${dataHome}/opencode/opencode.db`, { readonly: true })
|
|
236
|
+
} catch {
|
|
237
|
+
return undefined
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function addTranscript(index: Map<string, string>, sid: string, data: string): void {
|
|
242
|
+
try {
|
|
243
|
+
const part = JSON.parse(data) as { type?: unknown; text?: unknown }
|
|
244
|
+
if (part?.type !== "text" || typeof part.text !== "string") return
|
|
245
|
+
const base = index.get(sid) ?? ""
|
|
246
|
+
index.set(sid, `${base} ${part.text.toLowerCase()}`.trim())
|
|
247
|
+
} catch {
|
|
248
|
+
// ignore malformed parts
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Index titles/directories immediately, then add transcript text progressively
|
|
253
|
+
// in batches so the render thread can breathe. Covers every entry, not a fixed
|
|
254
|
+
// newest window; `onProgress` lets the picker show coverage.
|
|
255
|
+
async function buildSearchIndex(
|
|
256
|
+
api: TuiPluginApi,
|
|
257
|
+
entries: Entry[],
|
|
258
|
+
onProgress?: (progress: IndexProgress) => void,
|
|
259
|
+
): Promise<Map<string, string>> {
|
|
260
|
+
const index = new Map<string, string>()
|
|
261
|
+
for (const entry of entries) {
|
|
262
|
+
index.set(entry.id, `${entry.title.toLowerCase()} ${entry.dir.toLowerCase()}`)
|
|
263
|
+
}
|
|
264
|
+
const ids = entries.map((entry) => entry.id).filter((id) => SESSION_ID_PATTERN.test(id))
|
|
265
|
+
const remaining = new Set(ids)
|
|
266
|
+
const total = ids.length
|
|
267
|
+
let indexed = 0
|
|
268
|
+
const report = (complete: boolean) => onProgress?.({ indexed, total, complete })
|
|
269
|
+
report(false)
|
|
270
|
+
|
|
271
|
+
const db = await openTranscriptDb()
|
|
272
|
+
if (db) {
|
|
210
273
|
try {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
.
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
byId.set(row.sid, list)
|
|
225
|
-
}
|
|
226
|
-
} catch {}
|
|
227
|
-
}
|
|
228
|
-
for (const entry of targets) {
|
|
229
|
-
const text = (byId.get(entry.id) ?? []).join(" ").toLowerCase()
|
|
230
|
-
if (text) index.set(entry.id, `${index.get(entry.id)} ${text}`)
|
|
274
|
+
for (let i = 0; i < ids.length; i += TRANSCRIPT_BATCH) {
|
|
275
|
+
const batch = ids.slice(i, i + TRANSCRIPT_BATCH)
|
|
276
|
+
const placeholders = batch.map(() => "?").join(",")
|
|
277
|
+
const rows = (db
|
|
278
|
+
.query(
|
|
279
|
+
`SELECT session_id AS sid, data FROM part WHERE session_id IN (${placeholders}) AND json_extract(data, '$.type') = 'text' ORDER BY time_created`,
|
|
280
|
+
)
|
|
281
|
+
.all(...batch) ?? []) as { sid: string; data: string }[]
|
|
282
|
+
for (const row of rows) addTranscript(index, row.sid, row.data)
|
|
283
|
+
for (const id of batch) remaining.delete(id)
|
|
284
|
+
indexed += batch.length
|
|
285
|
+
report(false)
|
|
286
|
+
await nextTick()
|
|
231
287
|
}
|
|
288
|
+
} catch {
|
|
289
|
+
// local store unavailable mid-walk; fall back to the remote path below
|
|
232
290
|
} finally {
|
|
233
291
|
db.close()
|
|
234
292
|
}
|
|
235
|
-
} catch {
|
|
236
|
-
await buildSearchIndexRemote(api, targets, index)
|
|
237
293
|
}
|
|
294
|
+
|
|
295
|
+
if (remaining.size > 0) {
|
|
296
|
+
await buildSearchIndexRemote(api, [...remaining], index, () => {
|
|
297
|
+
indexed += 1
|
|
298
|
+
report(false)
|
|
299
|
+
})
|
|
300
|
+
}
|
|
301
|
+
report(true)
|
|
238
302
|
return index
|
|
239
303
|
}
|
|
240
304
|
|
|
241
305
|
async function buildSearchIndexRemote(
|
|
242
306
|
api: TuiPluginApi,
|
|
243
|
-
|
|
307
|
+
ids: string[],
|
|
244
308
|
index: Map<string, string>,
|
|
309
|
+
onEach: () => void,
|
|
245
310
|
): Promise<void> {
|
|
246
311
|
const home = process.env.HOME ?? ""
|
|
247
312
|
const cacheDir = process.env.SESH_CACHE_DIR ?? `${home}/.cache/sesh`
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
313
|
+
let cursor = 0
|
|
314
|
+
// Bounded concurrency: a fixed worker pool instead of one request per session.
|
|
315
|
+
const worker = async () => {
|
|
316
|
+
for (;;) {
|
|
317
|
+
const i = cursor++
|
|
318
|
+
if (i >= ids.length) return
|
|
319
|
+
const id = ids[i]
|
|
320
|
+
const base = index.get(id) ?? ""
|
|
252
321
|
try {
|
|
253
|
-
const raw = await readFile(`${cacheDir}/extractions/${
|
|
322
|
+
const raw = await readFile(`${cacheDir}/extractions/${id}.json`, "utf8")
|
|
254
323
|
const record = JSON.parse(raw) as { fulltextLower?: unknown }
|
|
255
324
|
const fulltext = typeof record?.fulltextLower === "string" ? record.fulltextLower : ""
|
|
256
|
-
index.set(
|
|
325
|
+
index.set(id, `${base} ${fulltext}`.trim())
|
|
257
326
|
} catch {
|
|
258
327
|
try {
|
|
259
|
-
const result = await api.client.session.messages({ sessionID:
|
|
328
|
+
const result = await api.client.session.messages({ sessionID: id })
|
|
260
329
|
const messages = result.data ?? []
|
|
261
330
|
const text = messages
|
|
262
331
|
.flatMap((message) =>
|
|
@@ -266,11 +335,13 @@ async function buildSearchIndexRemote(
|
|
|
266
335
|
)
|
|
267
336
|
.join(" ")
|
|
268
337
|
.toLowerCase()
|
|
269
|
-
index.set(
|
|
338
|
+
index.set(id, `${base} ${text}`.trim())
|
|
270
339
|
} catch {}
|
|
271
340
|
}
|
|
272
|
-
|
|
273
|
-
|
|
341
|
+
onEach()
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
await Promise.all(Array.from({ length: Math.min(REMOTE_CONCURRENCY, ids.length) }, worker))
|
|
274
345
|
}
|
|
275
346
|
|
|
276
347
|
const TRANSCRIPT_PREVIEW_TURNS = 8
|
|
@@ -397,7 +468,7 @@ function SidebarSessions(props: { api: TuiPluginApi }) {
|
|
|
397
468
|
let alive = true
|
|
398
469
|
const load = async () => {
|
|
399
470
|
try {
|
|
400
|
-
const all = await fetchEntries(props.api)
|
|
471
|
+
const { entries: all } = await fetchEntries(props.api)
|
|
401
472
|
if (alive) setEntries(all)
|
|
402
473
|
} catch {
|
|
403
474
|
// leave the previous list in place rather than flashing to blank
|
|
@@ -469,7 +540,7 @@ function SidebarSessions(props: { api: TuiPluginApi }) {
|
|
|
469
540
|
directory: entry.dir || undefined,
|
|
470
541
|
})
|
|
471
542
|
if (result && "error" in result && result.error) throw result.error
|
|
472
|
-
const remaining = await fetchEntries(props.api)
|
|
543
|
+
const { entries: remaining } = await fetchEntries(props.api)
|
|
473
544
|
if (remaining.some((item) => item.id === entry.id)) {
|
|
474
545
|
setEntries(remaining)
|
|
475
546
|
props.api.ui.toast({ message: "Session was not deleted", variant: "error" })
|
|
@@ -723,7 +794,7 @@ function HomeSessions(props: { api: TuiPluginApi }) {
|
|
|
723
794
|
let alive = true
|
|
724
795
|
const load = async () => {
|
|
725
796
|
try {
|
|
726
|
-
const all = await fetchEntries(props.api)
|
|
797
|
+
const { entries: all } = await fetchEntries(props.api)
|
|
727
798
|
if (alive) setEntries(all)
|
|
728
799
|
} catch {}
|
|
729
800
|
}
|
|
@@ -827,13 +898,14 @@ const tui: TuiPlugin = async (api) => {
|
|
|
827
898
|
if (api.mode.current() !== BASE_MODE) return
|
|
828
899
|
if (api.renderer.currentFocusedEditor === null) return
|
|
829
900
|
|
|
830
|
-
let
|
|
901
|
+
let entriesResult: EntryResult
|
|
831
902
|
try {
|
|
832
|
-
|
|
903
|
+
entriesResult = await fetchEntries(api)
|
|
833
904
|
} catch {
|
|
834
905
|
api.ui.toast({ message: "Could not load sessions", variant: "error" })
|
|
835
906
|
return
|
|
836
907
|
}
|
|
908
|
+
const entries = entriesResult.entries
|
|
837
909
|
if (entries.length === 0) {
|
|
838
910
|
api.ui.toast({ message: "No sessions found", variant: "warning" })
|
|
839
911
|
return
|
|
@@ -844,7 +916,12 @@ const tui: TuiPlugin = async (api) => {
|
|
|
844
916
|
const [query, setQuery] = createSignal("")
|
|
845
917
|
const [cursor, setCursor] = createSignal(0)
|
|
846
918
|
const [searchIndex, setSearchIndex] = createSignal<Map<string, string>>(new Map())
|
|
847
|
-
|
|
919
|
+
const [indexProgress, setIndexProgress] = createSignal<IndexProgress>({
|
|
920
|
+
indexed: 0,
|
|
921
|
+
total: 0,
|
|
922
|
+
complete: false,
|
|
923
|
+
})
|
|
924
|
+
void buildSearchIndex(api, entries, setIndexProgress).then(setSearchIndex)
|
|
848
925
|
let pickerInput: { value: string; isDestroyed?: boolean } | undefined
|
|
849
926
|
|
|
850
927
|
type DialogRow = { kind: "group"; label: string; count: number } | { kind: "item"; entry: Entry }
|
|
@@ -868,6 +945,22 @@ const tui: TuiPlugin = async (api) => {
|
|
|
868
945
|
|
|
869
946
|
const selectableEntries = createMemo(() => matchedGroups().flatMap((g) => g.list))
|
|
870
947
|
|
|
948
|
+
// Visible bounding: say how much transcript text search actually covers and
|
|
949
|
+
// when the metadata walk hit its cap, instead of silently under-reporting.
|
|
950
|
+
const coverageLabel = createMemo(() => {
|
|
951
|
+
const progress = indexProgress()
|
|
952
|
+
const bits: string[] = []
|
|
953
|
+
if (progress.total > 0) {
|
|
954
|
+
bits.push(
|
|
955
|
+
progress.complete
|
|
956
|
+
? `transcripts ${progress.total}`
|
|
957
|
+
: `indexing transcripts ${progress.indexed}/${progress.total}`,
|
|
958
|
+
)
|
|
959
|
+
}
|
|
960
|
+
if (entriesResult.truncated) bits.push(`showing newest ${entries.length}`)
|
|
961
|
+
return bits.join(" · ")
|
|
962
|
+
})
|
|
963
|
+
|
|
871
964
|
createEffect(() => {
|
|
872
965
|
const count = selectableEntries().length
|
|
873
966
|
if (cursor() >= count) setCursor(Math.max(0, count - 1))
|
|
@@ -1041,6 +1134,11 @@ const tui: TuiPlugin = async (api) => {
|
|
|
1041
1134
|
}}
|
|
1042
1135
|
/>
|
|
1043
1136
|
</box>
|
|
1137
|
+
<Show when={coverageLabel()}>
|
|
1138
|
+
<box paddingTop={1}>
|
|
1139
|
+
<text style={{ fg: api.theme.current.textMuted }}>{coverageLabel()}</text>
|
|
1140
|
+
</box>
|
|
1141
|
+
</Show>
|
|
1044
1142
|
</box>
|
|
1045
1143
|
<box
|
|
1046
1144
|
flexDirection="column"
|