zidane 6.2.13 → 6.2.14
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/dist/chat/pure.d.ts +3 -3
- package/dist/chat/pure.js +3 -2
- package/dist/chat.d.ts +3 -3
- package/dist/chat.js +4 -3
- package/dist/chat.js.map +1 -1
- package/dist/history-search-C4bk1Wz_.js +486 -0
- package/dist/history-search-C4bk1Wz_.js.map +1 -0
- package/dist/history-search-worker.d.ts +1 -0
- package/dist/history-search-worker.js +86 -0
- package/dist/history-search-worker.js.map +1 -0
- package/dist/{tool-formatters-BSwVkSX2.d.ts → tool-formatters-DdgS-xE2.d.ts} +90 -4
- package/dist/tool-formatters-DdgS-xE2.d.ts.map +1 -0
- package/dist/{transcript-anchors-yogJHuBW.js → transcript-anchors-DE6D0dNo.js} +2 -2
- package/dist/transcript-anchors-DE6D0dNo.js.map +1 -0
- package/dist/transcript-anchors-DKjXpQ0h.d.ts.map +1 -1
- package/dist/tui.d.ts +52 -10
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +1339 -245
- package/dist/tui.js.map +1 -1
- package/dist/{turn-operations-CXjRaltC.js → turn-operations-CokV32wf.js} +82 -11
- package/dist/turn-operations-CokV32wf.js.map +1 -0
- package/dist/{turn-operations-CjkgQKmR.d.ts → turn-operations-sE-Sli9H.d.ts} +2 -2
- package/dist/{turn-operations-CjkgQKmR.d.ts.map → turn-operations-sE-Sli9H.d.ts.map} +1 -1
- package/package.json +1 -1
- package/dist/tool-formatters-BSwVkSX2.d.ts.map +0 -1
- package/dist/transcript-anchors-yogJHuBW.js.map +0 -1
- package/dist/turn-operations-CXjRaltC.js.map +0 -1
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
//#region src/chat/history-search.ts
|
|
2
|
+
const EXCERPT_WIDTH = 120;
|
|
3
|
+
const EXCERPT_MARGIN = 28;
|
|
4
|
+
function buildSessionSearchIndex(turns) {
|
|
5
|
+
return { turns: turns.flatMap((turn) => {
|
|
6
|
+
const text = searchableTurnText(turn);
|
|
7
|
+
return text ? [{
|
|
8
|
+
turn,
|
|
9
|
+
text,
|
|
10
|
+
lowerText: text.toLocaleLowerCase()
|
|
11
|
+
}] : [];
|
|
12
|
+
}) };
|
|
13
|
+
}
|
|
14
|
+
/** Yielding index builder for TUI callers — never monopolizes a frame. */
|
|
15
|
+
async function buildSessionSearchIndexAsync(turns, options = {}) {
|
|
16
|
+
const indexed = [];
|
|
17
|
+
const yieldEvery = Math.max(1, options.yieldEvery ?? 80);
|
|
18
|
+
for (let i = 0; i < turns.length; i++) {
|
|
19
|
+
options.signal?.throwIfAborted();
|
|
20
|
+
const turn = turns[i];
|
|
21
|
+
const text = searchableTurnText(turn);
|
|
22
|
+
if (text) indexed.push({
|
|
23
|
+
turn,
|
|
24
|
+
text,
|
|
25
|
+
lowerText: text.toLocaleLowerCase()
|
|
26
|
+
});
|
|
27
|
+
if ((i + 1) % yieldEvery === 0) {
|
|
28
|
+
options.onProgress?.(i + 1, turns.length);
|
|
29
|
+
await yieldToEventLoop();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (turns.length === 0 || turns.length % yieldEvery !== 0) options.onProgress?.(turns.length, turns.length);
|
|
33
|
+
return { turns: indexed };
|
|
34
|
+
}
|
|
35
|
+
function searchSessionIndex(index, query, limit = 10) {
|
|
36
|
+
if (limit <= 0) return [];
|
|
37
|
+
const matches = searchSessionIndexChunk(index, query, 0, index.turns.length).map((result) => ({
|
|
38
|
+
...result.match,
|
|
39
|
+
turn: index.turns[result.index].turn
|
|
40
|
+
}));
|
|
41
|
+
matches.sort(compareMatches);
|
|
42
|
+
return matches.slice(0, limit);
|
|
43
|
+
}
|
|
44
|
+
/** Bounded synchronous matching unit backing `searchSessionIndex`. */
|
|
45
|
+
function searchSessionIndexChunk(index, query, start, end) {
|
|
46
|
+
const parsed = parseQuery(query);
|
|
47
|
+
if (!parsed.tokens.length) return [];
|
|
48
|
+
const matches = [];
|
|
49
|
+
const stop = Math.min(end, index.turns.length);
|
|
50
|
+
for (let i = Math.max(0, start); i < stop; i++) {
|
|
51
|
+
const indexed = index.turns[i];
|
|
52
|
+
const match = matchTurn(indexed.turn, indexed.text, indexed.lowerText, parsed);
|
|
53
|
+
if (match) matches.push({
|
|
54
|
+
index: i,
|
|
55
|
+
match
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
return matches;
|
|
59
|
+
}
|
|
60
|
+
/** Yielding matcher for very large indexes; progress is checked per chunk. */
|
|
61
|
+
async function searchSessionIndexAsync(index, query, options = {}) {
|
|
62
|
+
const parsed = parseQuery(query);
|
|
63
|
+
const limit = options.limit ?? 10;
|
|
64
|
+
if (!parsed.tokens.length || limit <= 0) return [];
|
|
65
|
+
const matches = [];
|
|
66
|
+
const yieldEvery = Math.max(1, options.yieldEvery ?? 250);
|
|
67
|
+
for (let i = 0; i < index.turns.length; i++) {
|
|
68
|
+
options.signal?.throwIfAborted();
|
|
69
|
+
const indexed = index.turns[i];
|
|
70
|
+
const match = matchTurn(indexed.turn, indexed.text, indexed.lowerText, parsed);
|
|
71
|
+
if (match) matches.push({
|
|
72
|
+
...match,
|
|
73
|
+
turn: indexed.turn
|
|
74
|
+
});
|
|
75
|
+
if ((i + 1) % yieldEvery === 0) {
|
|
76
|
+
options.onProgress?.(i + 1, index.turns.length);
|
|
77
|
+
await yieldToEventLoop();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (index.turns.length === 0 || index.turns.length % yieldEvery !== 0) options.onProgress?.(index.turns.length, index.turns.length);
|
|
81
|
+
matches.sort(compareMatches);
|
|
82
|
+
return matches.slice(0, limit);
|
|
83
|
+
}
|
|
84
|
+
function searchTurns(turns, query, limit = 10) {
|
|
85
|
+
return searchSessionIndex(buildSessionSearchIndex(turns), query, limit);
|
|
86
|
+
}
|
|
87
|
+
function searchSessionTurns(turns, query, maxExcerpts = 3) {
|
|
88
|
+
return searchTurns(turns, query, turns.length).map((match) => ({
|
|
89
|
+
id: match.turn.id,
|
|
90
|
+
score: match.score,
|
|
91
|
+
matchKind: match.matchKind,
|
|
92
|
+
turn: match.turn,
|
|
93
|
+
text: match.text,
|
|
94
|
+
highlighted: match.highlighted,
|
|
95
|
+
excerpts: match.excerpts.slice(0, maxExcerpts)
|
|
96
|
+
}));
|
|
97
|
+
}
|
|
98
|
+
function searchSessionHistory(turns, query, maxExcerpts = 3) {
|
|
99
|
+
return sessionHistoryFromMatches(searchTurns(turns, query, turns.length), maxExcerpts);
|
|
100
|
+
}
|
|
101
|
+
function searchSessionHistoryIndex(index, query, maxExcerpts = 3) {
|
|
102
|
+
return sessionHistoryFromMatches(searchSessionIndex(index, query, index.turns.length), maxExcerpts);
|
|
103
|
+
}
|
|
104
|
+
async function searchSessionHistoryIndexAsync(index, query, maxExcerpts = 3, onProgress, signal) {
|
|
105
|
+
return sessionHistoryFromMatches(await searchSessionIndexAsync(index, query, {
|
|
106
|
+
limit: index.turns.length,
|
|
107
|
+
onProgress,
|
|
108
|
+
signal
|
|
109
|
+
}), maxExcerpts);
|
|
110
|
+
}
|
|
111
|
+
function sessionHistoryFromMatches(matches, maxExcerpts) {
|
|
112
|
+
if (matches.length === 0) return null;
|
|
113
|
+
const excerpts = [];
|
|
114
|
+
for (const match of matches) {
|
|
115
|
+
for (const excerpt of match.excerpts) {
|
|
116
|
+
excerpts.push({
|
|
117
|
+
...excerpt,
|
|
118
|
+
turnId: match.turn.id,
|
|
119
|
+
role: match.turn.role
|
|
120
|
+
});
|
|
121
|
+
if (excerpts.length >= maxExcerpts) break;
|
|
122
|
+
}
|
|
123
|
+
if (excerpts.length >= maxExcerpts) break;
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
score: matches[0]?.score ?? 0,
|
|
127
|
+
matchedTurnIds: matches.map((match) => match.turn.id),
|
|
128
|
+
excerpts
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function filterSessionSearchCatalog(sessions, query, contentMatches) {
|
|
132
|
+
const terms = query.trim().toLocaleLowerCase().split(/\s+/u).filter(Boolean);
|
|
133
|
+
if (terms.length === 0) return [...sessions];
|
|
134
|
+
return sessions.flatMap((meta) => {
|
|
135
|
+
const projectBasename = meta.projectRoot?.split(/[\\/]/).pop() ?? "";
|
|
136
|
+
const corpus = `${meta.title} ${meta.id} ${projectBasename} ${meta.projectRoot ?? ""}`.toLocaleLowerCase();
|
|
137
|
+
const direct = terms.every((term) => corpus.includes(term));
|
|
138
|
+
const fuzzy = !direct && terms.every((term) => findFuzzyWordRange(corpus, term) !== null);
|
|
139
|
+
const metadataRank = direct ? 2 : fuzzy ? 1 : 0;
|
|
140
|
+
const contentMatch = contentMatches.get(meta.id);
|
|
141
|
+
return metadataRank > 0 || contentMatch ? [{
|
|
142
|
+
meta,
|
|
143
|
+
metadataRank,
|
|
144
|
+
contentMatch
|
|
145
|
+
}] : [];
|
|
146
|
+
}).sort((a, b) => b.metadataRank - a.metadataRank || (b.contentMatch?.score ?? 0) - (a.contentMatch?.score ?? 0) || b.meta.updatedAt - a.meta.updatedAt).map((row) => row.meta);
|
|
147
|
+
}
|
|
148
|
+
function searchHighlightRanges(text, query) {
|
|
149
|
+
const parsed = parseQuery(query.trim());
|
|
150
|
+
if (parsed.tokens.length === 0) return [];
|
|
151
|
+
const lowerText = lower(text);
|
|
152
|
+
const phrase = findTermRanges(lowerText, parsed.phraseLower);
|
|
153
|
+
if (phrase.length > 0) return mergeRanges(phrase, text.length);
|
|
154
|
+
const exact = parsed.tokens.flatMap((token) => findTermRanges(lowerText, token.lower));
|
|
155
|
+
if (exact.length > 0) return mergeRanges(exact, text.length);
|
|
156
|
+
return mergeRanges(parsed.tokens.flatMap((token) => findFuzzyWordRange(lowerText, token.lower) ?? []), text.length);
|
|
157
|
+
}
|
|
158
|
+
function highlightSegments(text, ranges) {
|
|
159
|
+
const normalized = mergeRanges(ranges, text.length);
|
|
160
|
+
if (!normalized.length) return [{
|
|
161
|
+
text,
|
|
162
|
+
highlighted: false
|
|
163
|
+
}];
|
|
164
|
+
const segments = [];
|
|
165
|
+
let cursor = 0;
|
|
166
|
+
for (const range of normalized) {
|
|
167
|
+
if (range.start > cursor) segments.push({
|
|
168
|
+
text: text.slice(cursor, range.start),
|
|
169
|
+
highlighted: false
|
|
170
|
+
});
|
|
171
|
+
if (range.end > range.start) segments.push({
|
|
172
|
+
text: text.slice(range.start, range.end),
|
|
173
|
+
highlighted: true
|
|
174
|
+
});
|
|
175
|
+
cursor = range.end;
|
|
176
|
+
}
|
|
177
|
+
if (cursor < text.length) segments.push({
|
|
178
|
+
text: text.slice(cursor),
|
|
179
|
+
highlighted: false
|
|
180
|
+
});
|
|
181
|
+
return segments.filter((segment) => segment.text.length > 0);
|
|
182
|
+
}
|
|
183
|
+
function matchTurn(turn, text, lowerText, query) {
|
|
184
|
+
const phraseRanges = findTermRanges(lowerText, query.phraseLower);
|
|
185
|
+
if (phraseRanges.length) return buildResult(turn, text, phraseRanges, "exact-phrase", 3e3 + phraseRanges.length);
|
|
186
|
+
const termRanges = [];
|
|
187
|
+
let exactTerms = 0;
|
|
188
|
+
for (const token of query.tokens) {
|
|
189
|
+
const exact = findTermRanges(lowerText, token.lower);
|
|
190
|
+
if (exact.length) {
|
|
191
|
+
exactTerms += 1;
|
|
192
|
+
termRanges.push(...exact);
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
const fuzzy = findFuzzyWordRange(lowerText, token.lower);
|
|
196
|
+
if (!fuzzy) return null;
|
|
197
|
+
termRanges.push(...fuzzy);
|
|
198
|
+
}
|
|
199
|
+
const merged = mergeRanges(termRanges, text.length);
|
|
200
|
+
if (!merged.length) return null;
|
|
201
|
+
const allExact = exactTerms === query.tokens.length;
|
|
202
|
+
return buildResult(turn, text, merged, allExact ? "exact-terms" : "fuzzy", allExact ? 2e3 : 1e3 + exactTerms);
|
|
203
|
+
}
|
|
204
|
+
function buildResult(turn, text, highlighted, matchKind, score) {
|
|
205
|
+
return {
|
|
206
|
+
text,
|
|
207
|
+
highlighted,
|
|
208
|
+
excerpts: buildExcerpts(text, highlighted),
|
|
209
|
+
matchKind,
|
|
210
|
+
score
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
function buildExcerpts(text, ranges) {
|
|
214
|
+
const lines = splitLines(text);
|
|
215
|
+
return rangesToLineMatches(text, lines, ranges).slice(0, 3).map((match) => {
|
|
216
|
+
return clipLine(lines[match.lineIndex] ?? "", match.ranges, EXCERPT_WIDTH, EXCERPT_MARGIN);
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
function clipLine(text, ranges, width, margin) {
|
|
220
|
+
if (text.length <= width) return {
|
|
221
|
+
text,
|
|
222
|
+
highlighted: mergeRanges(ranges, text.length)
|
|
223
|
+
};
|
|
224
|
+
const merged = mergeRanges(ranges, text.length);
|
|
225
|
+
if (!merged.length) return {
|
|
226
|
+
text: text.slice(0, width),
|
|
227
|
+
highlighted: []
|
|
228
|
+
};
|
|
229
|
+
const focusStart = merged[0].start;
|
|
230
|
+
const focusEnd = merged[merged.length - 1].end;
|
|
231
|
+
let start = Math.max(0, Math.min(focusStart - margin, text.length - width));
|
|
232
|
+
let end = Math.min(text.length, Math.max(focusEnd + margin, start + width));
|
|
233
|
+
if (end - start > width) {
|
|
234
|
+
const over = end - start - width;
|
|
235
|
+
start += Math.floor(over / 2);
|
|
236
|
+
end = start + width;
|
|
237
|
+
}
|
|
238
|
+
const clippedText = text.slice(start, end);
|
|
239
|
+
return {
|
|
240
|
+
text: clippedText,
|
|
241
|
+
highlighted: merged.map((range) => ({
|
|
242
|
+
start: Math.max(0, range.start - start),
|
|
243
|
+
end: Math.min(clippedText.length, range.end - start)
|
|
244
|
+
})).filter((range) => range.end > range.start)
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
function rangesToLineMatches(text, lines, ranges) {
|
|
248
|
+
const matches = [];
|
|
249
|
+
let offset = 0;
|
|
250
|
+
for (let index = 0; index < lines.length; index++) {
|
|
251
|
+
const line = lines[index];
|
|
252
|
+
const lineStart = offset;
|
|
253
|
+
const lineEnd = offset + line.length;
|
|
254
|
+
const lineRanges = ranges.map((range) => intersectRange(range, lineStart, lineEnd)).filter((range) => Boolean(range)).map((range) => ({
|
|
255
|
+
start: range.start - lineStart,
|
|
256
|
+
end: range.end - lineStart
|
|
257
|
+
}));
|
|
258
|
+
if (lineRanges.length) matches.push({
|
|
259
|
+
lineIndex: index,
|
|
260
|
+
lineStart,
|
|
261
|
+
lineEnd,
|
|
262
|
+
ranges: lineRanges
|
|
263
|
+
});
|
|
264
|
+
offset = lineEnd + 1;
|
|
265
|
+
}
|
|
266
|
+
return matches;
|
|
267
|
+
}
|
|
268
|
+
function splitLines(text) {
|
|
269
|
+
return text.split("\n");
|
|
270
|
+
}
|
|
271
|
+
const MAX_SEARCHABLE_TEXT_CHARS = 512e3;
|
|
272
|
+
const MAX_SEARCHABLE_TOOL_CHARS = 128e3;
|
|
273
|
+
const BINARY_KEY_RE = /(?:^|_)(?:data|base64|bytes|blob|attachment|image|audio|video|binary|file_content)(?:$|_)/i;
|
|
274
|
+
const BASE64_RUN_RE = /[a-z0-9+/]{256,}={0,2}/gi;
|
|
275
|
+
/**
|
|
276
|
+
* Search-specific serializer. Unlike `turnAsText`, it never serializes media,
|
|
277
|
+
* document attachments, encrypted reasoning, or base64-looking payloads.
|
|
278
|
+
*/
|
|
279
|
+
function searchableTurnText(turn) {
|
|
280
|
+
const parts = [];
|
|
281
|
+
for (const block of turn.content) if (block.type === "text") pushSearchText(parts, block.text, MAX_SEARCHABLE_TEXT_CHARS);
|
|
282
|
+
else if (block.type === "thinking") pushSearchText(parts, `[thinking]\n${block.text}`, MAX_SEARCHABLE_TEXT_CHARS);
|
|
283
|
+
else if (block.type === "compact-summary") pushSearchText(parts, `[compaction summary]\n${block.summary}`, MAX_SEARCHABLE_TEXT_CHARS);
|
|
284
|
+
else if (block.type === "tool_call") {
|
|
285
|
+
const args = searchableJson(block.input);
|
|
286
|
+
parts.push(`[tool call · ${block.name}]${args ? `\n${args}` : ""}`);
|
|
287
|
+
} else if (block.type === "tool_result") {
|
|
288
|
+
const output = searchableToolResult(block.output);
|
|
289
|
+
if (output) parts.push(`[tool result]\n${output}`);
|
|
290
|
+
} else if (block.type === "server_tool_use") {
|
|
291
|
+
const input = searchableJson(block.input);
|
|
292
|
+
parts.push(`[server tool · ${block.name}]${input ? `\n${input}` : ""}`);
|
|
293
|
+
} else if (block.type === "server_tool_result") {
|
|
294
|
+
const content = searchableJson(block.content);
|
|
295
|
+
if (content) parts.push(`[server tool result · ${block.toolName}]\n${content}`);
|
|
296
|
+
}
|
|
297
|
+
return parts.join("\n\n");
|
|
298
|
+
}
|
|
299
|
+
function searchableToolResult(output) {
|
|
300
|
+
if (typeof output === "string") return safeSearchString(output, MAX_SEARCHABLE_TOOL_CHARS);
|
|
301
|
+
return output.flatMap((part) => {
|
|
302
|
+
if (part.type === "text") {
|
|
303
|
+
const text = safeSearchString(part.text, MAX_SEARCHABLE_TOOL_CHARS);
|
|
304
|
+
return text ? [text] : [];
|
|
305
|
+
}
|
|
306
|
+
return [];
|
|
307
|
+
}).join("\n");
|
|
308
|
+
}
|
|
309
|
+
function pushSearchText(parts, text, max) {
|
|
310
|
+
const safe = safeSearchString(text, max);
|
|
311
|
+
if (safe) parts.push(safe);
|
|
312
|
+
}
|
|
313
|
+
function safeSearchString(value, max) {
|
|
314
|
+
const trimmed = value.trim();
|
|
315
|
+
if (!trimmed || isLikelyBase64(trimmed)) return "";
|
|
316
|
+
const withoutPayloads = trimmed.replace(BASE64_RUN_RE, "").trim();
|
|
317
|
+
if (!withoutPayloads) return "";
|
|
318
|
+
return withoutPayloads.length <= max ? withoutPayloads : `${withoutPayloads.slice(0, max)}\n… [search text truncated]`;
|
|
319
|
+
}
|
|
320
|
+
function isLikelyBase64(value) {
|
|
321
|
+
if (/^data:[^;,]+;base64,/i.test(value)) return true;
|
|
322
|
+
if (value.length < 256 || value.length % 4 !== 0 || /\s/.test(value)) return false;
|
|
323
|
+
return /^[a-z0-9+/]+={0,2}$/i.test(value);
|
|
324
|
+
}
|
|
325
|
+
function searchableJson(value) {
|
|
326
|
+
const sanitized = sanitizeSearchValue(value, 0);
|
|
327
|
+
if (sanitized === void 0) return "";
|
|
328
|
+
try {
|
|
329
|
+
return safeSearchString(JSON.stringify(sanitized, null, 2), MAX_SEARCHABLE_TOOL_CHARS);
|
|
330
|
+
} catch {
|
|
331
|
+
return "";
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
function sanitizeSearchValue(value, depth) {
|
|
335
|
+
if (depth > 6 || value === null || typeof value === "number" || typeof value === "boolean") return value;
|
|
336
|
+
if (typeof value === "string") return safeSearchString(value, MAX_SEARCHABLE_TOOL_CHARS) || void 0;
|
|
337
|
+
if (Array.isArray(value)) return value.slice(0, 200).flatMap((item) => sanitizeSearchValue(item, depth + 1) ?? []);
|
|
338
|
+
if (typeof value !== "object") return void 0;
|
|
339
|
+
const out = {};
|
|
340
|
+
for (const [key, child] of Object.entries(value)) {
|
|
341
|
+
if (BINARY_KEY_RE.test(key)) continue;
|
|
342
|
+
const safe = sanitizeSearchValue(child, depth + 1);
|
|
343
|
+
if (safe !== void 0) out[key] = safe;
|
|
344
|
+
}
|
|
345
|
+
return Object.keys(out).length ? out : void 0;
|
|
346
|
+
}
|
|
347
|
+
function compareMatches(a, b) {
|
|
348
|
+
return b.score - a.score || b.turn.createdAt - a.turn.createdAt || a.turn.id.localeCompare(b.turn.id);
|
|
349
|
+
}
|
|
350
|
+
function yieldToEventLoop() {
|
|
351
|
+
return new Promise((resolve) => setTimeout(resolve, 0));
|
|
352
|
+
}
|
|
353
|
+
function parseQuery(query) {
|
|
354
|
+
const tokens = tokenize(query);
|
|
355
|
+
const phrase = tokens.map((token) => token.text).join(" ");
|
|
356
|
+
return {
|
|
357
|
+
tokens: tokens.map((token) => ({
|
|
358
|
+
...token,
|
|
359
|
+
lower: lower(token.text)
|
|
360
|
+
})),
|
|
361
|
+
phraseLower: lower(phrase)
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
function tokenize(query) {
|
|
365
|
+
const tokens = [];
|
|
366
|
+
let i = 0;
|
|
367
|
+
while (i < query.length) {
|
|
368
|
+
while (i < query.length && /\s/u.test(query[i])) i += 1;
|
|
369
|
+
if (i >= query.length) break;
|
|
370
|
+
if (query[i] === "\"") {
|
|
371
|
+
const start = i + 1;
|
|
372
|
+
i += 1;
|
|
373
|
+
let value = "";
|
|
374
|
+
while (i < query.length && query[i] !== "\"") {
|
|
375
|
+
value += query[i];
|
|
376
|
+
i += 1;
|
|
377
|
+
}
|
|
378
|
+
tokens.push({
|
|
379
|
+
text: value,
|
|
380
|
+
start,
|
|
381
|
+
end: i
|
|
382
|
+
});
|
|
383
|
+
if (query[i] === "\"") i += 1;
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
const start = i;
|
|
387
|
+
while (i < query.length && !/\s/u.test(query[i])) i += 1;
|
|
388
|
+
tokens.push({
|
|
389
|
+
text: query.slice(start, i),
|
|
390
|
+
start,
|
|
391
|
+
end: i
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
return tokens.filter((token) => token.text.length > 0);
|
|
395
|
+
}
|
|
396
|
+
function findTermRanges(text, termLower) {
|
|
397
|
+
if (!termLower) return [];
|
|
398
|
+
const ranges = [];
|
|
399
|
+
let from = 0;
|
|
400
|
+
while (from <= text.length) {
|
|
401
|
+
const index = text.indexOf(termLower, from);
|
|
402
|
+
if (index === -1) break;
|
|
403
|
+
ranges.push({
|
|
404
|
+
start: index,
|
|
405
|
+
end: index + termLower.length
|
|
406
|
+
});
|
|
407
|
+
from = index + Math.max(1, termLower.length);
|
|
408
|
+
}
|
|
409
|
+
return ranges;
|
|
410
|
+
}
|
|
411
|
+
/** Word-local typo matching: one word within a bounded edit distance of the term. */
|
|
412
|
+
function findFuzzyWordRange(text, termLower) {
|
|
413
|
+
if (termLower.length < 4) return null;
|
|
414
|
+
const maxEdits = termLower.length >= 9 ? 2 : 1;
|
|
415
|
+
const words = text.matchAll(/[\p{L}\p{N}_-]+/gu);
|
|
416
|
+
let best = null;
|
|
417
|
+
for (const match of words) {
|
|
418
|
+
const candidate = match[0].toLocaleLowerCase();
|
|
419
|
+
if (Math.abs(candidate.length - termLower.length) > maxEdits) continue;
|
|
420
|
+
const distance = boundedEditDistance(candidate, termLower, maxEdits);
|
|
421
|
+
if (distance === null) continue;
|
|
422
|
+
if (1 - distance / Math.max(candidate.length, termLower.length) < .8) continue;
|
|
423
|
+
if (!best || distance < best.distance || distance === best.distance && candidate.length < best.end - best.start) {
|
|
424
|
+
const start = match.index ?? 0;
|
|
425
|
+
best = {
|
|
426
|
+
start,
|
|
427
|
+
end: start + match[0].length,
|
|
428
|
+
distance
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
return best ? [{
|
|
433
|
+
start: best.start,
|
|
434
|
+
end: best.end
|
|
435
|
+
}] : null;
|
|
436
|
+
}
|
|
437
|
+
function boundedEditDistance(left, right, limit) {
|
|
438
|
+
let previous = Array.from({ length: right.length + 1 }, (_, i) => i);
|
|
439
|
+
for (let i = 1; i <= left.length; i++) {
|
|
440
|
+
const current = [i];
|
|
441
|
+
let rowMin = i;
|
|
442
|
+
for (let j = 1; j <= right.length; j++) {
|
|
443
|
+
const value = Math.min(current[j - 1] + 1, previous[j] + 1, previous[j - 1] + (left[i - 1] === right[j - 1] ? 0 : 1));
|
|
444
|
+
current.push(value);
|
|
445
|
+
rowMin = Math.min(rowMin, value);
|
|
446
|
+
}
|
|
447
|
+
if (rowMin > limit) return null;
|
|
448
|
+
previous = current;
|
|
449
|
+
}
|
|
450
|
+
const distance = previous[right.length];
|
|
451
|
+
return distance <= limit ? distance : null;
|
|
452
|
+
}
|
|
453
|
+
function mergeRanges(ranges, max) {
|
|
454
|
+
const sorted = ranges.map((range) => ({
|
|
455
|
+
start: clamp(range.start, 0, max),
|
|
456
|
+
end: clamp(range.end, 0, max)
|
|
457
|
+
})).filter((range) => range.end > range.start).sort((a, b) => a.start - b.start || a.end - b.end);
|
|
458
|
+
const merged = [];
|
|
459
|
+
for (const range of sorted) {
|
|
460
|
+
const last = merged[merged.length - 1];
|
|
461
|
+
if (!last || range.start > last.end) {
|
|
462
|
+
merged.push({ ...range });
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
last.end = Math.max(last.end, range.end);
|
|
466
|
+
}
|
|
467
|
+
return merged;
|
|
468
|
+
}
|
|
469
|
+
function intersectRange(range, start, end) {
|
|
470
|
+
const s = Math.max(range.start, start);
|
|
471
|
+
const e = Math.min(range.end, end);
|
|
472
|
+
return e > s ? {
|
|
473
|
+
start: s,
|
|
474
|
+
end: e
|
|
475
|
+
} : null;
|
|
476
|
+
}
|
|
477
|
+
function clamp(value, min, max) {
|
|
478
|
+
return Math.min(max, Math.max(min, value));
|
|
479
|
+
}
|
|
480
|
+
function lower(value) {
|
|
481
|
+
return value.toLocaleLowerCase();
|
|
482
|
+
}
|
|
483
|
+
//#endregion
|
|
484
|
+
export { searchHighlightRanges as a, searchSessionHistoryIndexAsync as c, searchSessionTurns as d, searchTurns as f, highlightSegments as i, searchSessionIndex as l, buildSessionSearchIndexAsync as n, searchSessionHistory as o, searchableTurnText as p, filterSessionSearchCatalog as r, searchSessionHistoryIndex as s, buildSessionSearchIndex as t, searchSessionIndexAsync as u };
|
|
485
|
+
|
|
486
|
+
//# sourceMappingURL=history-search-C4bk1Wz_.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"history-search-C4bk1Wz_.js","names":[],"sources":["../src/chat/history-search.ts"],"sourcesContent":["import type { SessionTurn, ToolResultContent } from '../types'\nimport type { SessionMeta } from './types'\n\nexport interface HighlightRange {\n start: number\n end: number\n}\n\nexport interface HighlightSegment {\n text: string\n highlighted: boolean\n}\n\nexport interface SearchExcerpt {\n text: string\n highlighted: HighlightRange[]\n}\n\nexport interface SearchTurnMatch {\n turn: SessionTurn\n text: string\n highlighted: HighlightRange[]\n excerpts: SearchExcerpt[]\n score: number\n matchKind: 'exact-phrase' | 'exact-terms' | 'fuzzy'\n}\n\nexport type SearchTurnResult = SearchTurnMatch\n\nexport interface SearchSessionResult {\n id: string\n score: number\n matchKind: 'exact-phrase' | 'exact-terms' | 'fuzzy'\n turn: SessionTurn\n text: string\n highlighted: HighlightRange[]\n excerpts: SearchExcerpt[]\n}\n\nexport interface SessionSearchExcerpt extends SearchExcerpt {\n turnId: string\n role: SessionTurn['role']\n}\n\n/** Aggregated row-level match used by the conversations list. */\nexport interface SessionHistoryMatch {\n score: number\n matchedTurnIds: string[]\n excerpts: SessionSearchExcerpt[]\n}\n\nexport interface IndexedSearchTurn {\n turn: SessionTurn\n /** Attachment/base64-free text shown in search results. */\n text: string\n /** Pre-normalized once, reused for every query. */\n lowerText: string\n}\n\nexport interface SessionSearchIndex {\n turns: IndexedSearchTurn[]\n}\n\ninterface QueryToken { text: string, start: number, end: number }\n\ntype MatchKind = 'exact-phrase' | 'exact-terms' | 'fuzzy'\n\ninterface LineMatch {\n lineIndex: number\n lineStart: number\n lineEnd: number\n ranges: HighlightRange[]\n}\n\nconst EXCERPT_WIDTH = 120\nconst EXCERPT_MARGIN = 28\n\nexport function buildSessionSearchIndex(turns: readonly SessionTurn[]): SessionSearchIndex {\n return { turns: turns.flatMap((turn) => {\n const text = searchableTurnText(turn)\n return text ? [{ turn, text, lowerText: text.toLocaleLowerCase() }] : []\n }) }\n}\n\n/** Yielding index builder for TUI callers — never monopolizes a frame. */\nexport async function buildSessionSearchIndexAsync(\n turns: readonly SessionTurn[],\n options: { yieldEvery?: number, onProgress?: (indexed: number, total: number) => void, signal?: AbortSignal } = {},\n): Promise<SessionSearchIndex> {\n const indexed: IndexedSearchTurn[] = []\n const yieldEvery = Math.max(1, options.yieldEvery ?? 80)\n for (let i = 0; i < turns.length; i++) {\n options.signal?.throwIfAborted()\n const turn = turns[i]!\n const text = searchableTurnText(turn)\n if (text)\n indexed.push({ turn, text, lowerText: text.toLocaleLowerCase() })\n if ((i + 1) % yieldEvery === 0) {\n options.onProgress?.(i + 1, turns.length)\n await yieldToEventLoop()\n }\n }\n if (turns.length === 0 || turns.length % yieldEvery !== 0)\n options.onProgress?.(turns.length, turns.length)\n return { turns: indexed }\n}\n\nexport function searchSessionIndex(index: SessionSearchIndex, query: string, limit = 10): SearchTurnMatch[] {\n if (limit <= 0)\n return []\n const matches = searchSessionIndexChunk(index, query, 0, index.turns.length)\n .map(result => ({ ...result.match, turn: index.turns[result.index]!.turn }))\n matches.sort(compareMatches)\n return matches.slice(0, limit)\n}\n\ninterface SearchIndexChunkMatch {\n index: number\n match: Omit<SearchTurnMatch, 'turn'>\n}\n\n/** Bounded synchronous matching unit backing `searchSessionIndex`. */\nfunction searchSessionIndexChunk(index: SessionSearchIndex, query: string, start: number, end: number): SearchIndexChunkMatch[] {\n const parsed = parseQuery(query)\n if (!parsed.tokens.length)\n return []\n const matches: SearchIndexChunkMatch[] = []\n const stop = Math.min(end, index.turns.length)\n for (let i = Math.max(0, start); i < stop; i++) {\n const indexed = index.turns[i]!\n const match = matchTurn(indexed.turn, indexed.text, indexed.lowerText, parsed)\n if (match)\n matches.push({ index: i, match })\n }\n return matches\n}\n\n/** Yielding matcher for very large indexes; progress is checked per chunk. */\nexport async function searchSessionIndexAsync(\n index: SessionSearchIndex,\n query: string,\n options: { limit?: number, yieldEvery?: number, onProgress?: (searched: number, total: number) => void, signal?: AbortSignal } = {},\n): Promise<SearchTurnMatch[]> {\n const parsed = parseQuery(query)\n const limit = options.limit ?? 10\n if (!parsed.tokens.length || limit <= 0)\n return []\n const matches: SearchTurnMatch[] = []\n const yieldEvery = Math.max(1, options.yieldEvery ?? 250)\n for (let i = 0; i < index.turns.length; i++) {\n options.signal?.throwIfAborted()\n const indexed = index.turns[i]!\n const match = matchTurn(indexed.turn, indexed.text, indexed.lowerText, parsed)\n if (match)\n matches.push({ ...match, turn: indexed.turn })\n if ((i + 1) % yieldEvery === 0) {\n options.onProgress?.(i + 1, index.turns.length)\n await yieldToEventLoop()\n }\n }\n if (index.turns.length === 0 || index.turns.length % yieldEvery !== 0)\n options.onProgress?.(index.turns.length, index.turns.length)\n matches.sort(compareMatches)\n return matches.slice(0, limit)\n}\n\nexport function searchTurns(turns: readonly SessionTurn[], query: string, limit = 10): SearchTurnMatch[] {\n return searchSessionIndex(buildSessionSearchIndex(turns), query, limit)\n}\n\nexport function searchSessionTurns(turns: readonly SessionTurn[], query: string, maxExcerpts = 3): SearchSessionResult[] {\n return searchTurns(turns, query, turns.length)\n .map(match => ({\n id: match.turn.id,\n score: match.score,\n matchKind: match.matchKind,\n turn: match.turn,\n text: match.text,\n highlighted: match.highlighted,\n excerpts: match.excerpts.slice(0, maxExcerpts),\n }))\n}\n\nexport function searchSessionHistory(turns: readonly SessionTurn[], query: string, maxExcerpts = 3): SessionHistoryMatch | null {\n return sessionHistoryFromMatches(searchTurns(turns, query, turns.length), maxExcerpts)\n}\n\nexport function searchSessionHistoryIndex(index: SessionSearchIndex, query: string, maxExcerpts = 3): SessionHistoryMatch | null {\n return sessionHistoryFromMatches(searchSessionIndex(index, query, index.turns.length), maxExcerpts)\n}\n\nexport async function searchSessionHistoryIndexAsync(\n index: SessionSearchIndex,\n query: string,\n maxExcerpts = 3,\n onProgress?: (searched: number, total: number) => void,\n signal?: AbortSignal,\n): Promise<SessionHistoryMatch | null> {\n const matches = await searchSessionIndexAsync(index, query, {\n limit: index.turns.length,\n onProgress,\n signal,\n })\n return sessionHistoryFromMatches(matches, maxExcerpts)\n}\n\nfunction sessionHistoryFromMatches(matches: readonly SearchTurnMatch[], maxExcerpts: number): SessionHistoryMatch | null {\n if (matches.length === 0)\n return null\n const excerpts: SessionSearchExcerpt[] = []\n for (const match of matches) {\n for (const excerpt of match.excerpts) {\n excerpts.push({ ...excerpt, turnId: match.turn.id, role: match.turn.role })\n if (excerpts.length >= maxExcerpts)\n break\n }\n if (excerpts.length >= maxExcerpts)\n break\n }\n return {\n score: matches[0]?.score ?? 0,\n matchedTurnIds: matches.map(match => match.turn.id),\n excerpts,\n }\n}\n\nexport function filterSessionSearchCatalog(\n sessions: readonly SessionMeta[],\n query: string,\n contentMatches: ReadonlyMap<string, SessionHistoryMatch>,\n): SessionMeta[] {\n const terms = query.trim().toLocaleLowerCase().split(/\\s+/u).filter(Boolean)\n if (terms.length === 0)\n return [...sessions]\n return sessions\n .flatMap((meta) => {\n const projectBasename = meta.projectRoot?.split(/[\\\\/]/).pop() ?? ''\n const corpus = `${meta.title} ${meta.id} ${projectBasename} ${meta.projectRoot ?? ''}`.toLocaleLowerCase()\n const direct = terms.every(term => corpus.includes(term))\n const fuzzy = !direct && terms.every(term => findFuzzyWordRange(corpus, term) !== null)\n const metadataRank = direct ? 2 : fuzzy ? 1 : 0\n const contentMatch = contentMatches.get(meta.id)\n return metadataRank > 0 || contentMatch ? [{ meta, metadataRank, contentMatch }] : []\n })\n .sort((a, b) => b.metadataRank - a.metadataRank\n || (b.contentMatch?.score ?? 0) - (a.contentMatch?.score ?? 0)\n || b.meta.updatedAt - a.meta.updatedAt)\n .map(row => row.meta)\n}\n\nexport function searchHighlightRanges(text: string, query: string): HighlightRange[] {\n const parsed = parseQuery(query.trim())\n if (parsed.tokens.length === 0)\n return []\n const lowerText = lower(text)\n const phrase = findTermRanges(lowerText, parsed.phraseLower)\n if (phrase.length > 0)\n return mergeRanges(phrase, text.length)\n const exact = parsed.tokens.flatMap(token => findTermRanges(lowerText, token.lower))\n if (exact.length > 0)\n return mergeRanges(exact, text.length)\n return mergeRanges(\n parsed.tokens.flatMap(token => findFuzzyWordRange(lowerText, token.lower) ?? []),\n text.length,\n )\n}\n\nexport function highlightSegments(text: string, ranges: readonly HighlightRange[]): HighlightSegment[] {\n const normalized = mergeRanges(ranges, text.length)\n if (!normalized.length)\n return [{ text, highlighted: false }]\n const segments: HighlightSegment[] = []\n let cursor = 0\n for (const range of normalized) {\n if (range.start > cursor)\n segments.push({ text: text.slice(cursor, range.start), highlighted: false })\n if (range.end > range.start)\n segments.push({ text: text.slice(range.start, range.end), highlighted: true })\n cursor = range.end\n }\n if (cursor < text.length)\n segments.push({ text: text.slice(cursor), highlighted: false })\n return segments.filter(segment => segment.text.length > 0)\n}\n\nfunction matchTurn(turn: SessionTurn, text: string, lowerText: string, query: ParsedQuery): Omit<SearchTurnMatch, 'turn'> | null {\n const phraseRanges = findTermRanges(lowerText, query.phraseLower)\n if (phraseRanges.length)\n return buildResult(turn, text, phraseRanges, 'exact-phrase', 3000 + phraseRanges.length)\n\n const termRanges: HighlightRange[] = []\n let exactTerms = 0\n for (const token of query.tokens) {\n const exact = findTermRanges(lowerText, token.lower)\n if (exact.length) {\n exactTerms += 1\n termRanges.push(...exact)\n continue\n }\n const fuzzy = findFuzzyWordRange(lowerText, token.lower)\n if (!fuzzy)\n return null\n termRanges.push(...fuzzy)\n }\n\n const merged = mergeRanges(termRanges, text.length)\n if (!merged.length)\n return null\n const allExact = exactTerms === query.tokens.length\n // Within the fuzzy band, more exact tokens = cleaner match = higher rank.\n return buildResult(turn, text, merged, allExact ? 'exact-terms' : 'fuzzy', allExact ? 2000 : 1000 + exactTerms)\n}\n\nfunction buildResult(turn: SessionTurn, text: string, highlighted: HighlightRange[], matchKind: MatchKind, score: number): Omit<SearchTurnMatch, 'turn'> {\n return {\n text,\n highlighted,\n excerpts: buildExcerpts(text, highlighted),\n matchKind,\n score,\n }\n}\n\nfunction buildExcerpts(text: string, ranges: readonly HighlightRange[]): SearchExcerpt[] {\n const lines = splitLines(text)\n const matches = rangesToLineMatches(text, lines, ranges)\n return matches.slice(0, 3).map((match) => {\n const lineText = lines[match.lineIndex] ?? ''\n const clipped = clipLine(lineText, match.ranges, EXCERPT_WIDTH, EXCERPT_MARGIN)\n return clipped\n })\n}\n\nfunction clipLine(text: string, ranges: readonly HighlightRange[], width: number, margin: number): SearchExcerpt {\n if (text.length <= width)\n return { text, highlighted: mergeRanges(ranges, text.length) }\n\n const merged = mergeRanges(ranges, text.length)\n if (!merged.length)\n return { text: text.slice(0, width), highlighted: [] }\n\n const focusStart = merged[0].start\n const focusEnd = merged[merged.length - 1].end\n let start = Math.max(0, Math.min(focusStart - margin, text.length - width))\n let end = Math.min(text.length, Math.max(focusEnd + margin, start + width))\n if (end - start > width) {\n const over = end - start - width\n start += Math.floor(over / 2)\n end = start + width\n }\n const clippedText = text.slice(start, end)\n return {\n text: clippedText,\n highlighted: merged.map(range => ({ start: Math.max(0, range.start - start), end: Math.min(clippedText.length, range.end - start) })).filter(range => range.end > range.start),\n }\n}\n\nfunction rangesToLineMatches(text: string, lines: string[], ranges: readonly HighlightRange[]): LineMatch[] {\n const matches: LineMatch[] = []\n let offset = 0\n for (let index = 0; index < lines.length; index++) {\n const line = lines[index]\n const lineStart = offset\n const lineEnd = offset + line.length\n const lineRanges = ranges\n .map(range => intersectRange(range, lineStart, lineEnd))\n .filter((range): range is HighlightRange => Boolean(range))\n .map(range => ({ start: range.start - lineStart, end: range.end - lineStart }))\n if (lineRanges.length)\n matches.push({ lineIndex: index, lineStart, lineEnd, ranges: lineRanges })\n offset = lineEnd + 1\n }\n return matches\n}\n\nfunction splitLines(text: string): string[] {\n return text.split('\\n')\n}\n\nconst MAX_SEARCHABLE_TEXT_CHARS = 512_000\nconst MAX_SEARCHABLE_TOOL_CHARS = 128_000\nconst BINARY_KEY_RE = /(?:^|_)(?:data|base64|bytes|blob|attachment|image|audio|video|binary|file_content)(?:$|_)/i\nconst BASE64_RUN_RE = /[a-z0-9+/]{256,}={0,2}/gi\n\n/**\n * Search-specific serializer. Unlike `turnAsText`, it never serializes media,\n * document attachments, encrypted reasoning, or base64-looking payloads.\n */\nexport function searchableTurnText(turn: SessionTurn): string {\n const parts: string[] = []\n for (const block of turn.content) {\n if (block.type === 'text') {\n pushSearchText(parts, block.text, MAX_SEARCHABLE_TEXT_CHARS)\n }\n else if (block.type === 'thinking') {\n pushSearchText(parts, `[thinking]\\n${block.text}`, MAX_SEARCHABLE_TEXT_CHARS)\n }\n else if (block.type === 'compact-summary') {\n pushSearchText(parts, `[compaction summary]\\n${block.summary}`, MAX_SEARCHABLE_TEXT_CHARS)\n }\n else if (block.type === 'tool_call') {\n const args = searchableJson(block.input)\n parts.push(`[tool call · ${block.name}]${args ? `\\n${args}` : ''}`)\n }\n else if (block.type === 'tool_result') {\n const output = searchableToolResult(block.output)\n if (output)\n parts.push(`[tool result]\\n${output}`)\n }\n else if (block.type === 'server_tool_use') {\n const input = searchableJson(block.input)\n parts.push(`[server tool · ${block.name}]${input ? `\\n${input}` : ''}`)\n }\n else if (block.type === 'server_tool_result') {\n const content = searchableJson(block.content)\n if (content)\n parts.push(`[server tool result · ${block.toolName}]\\n${content}`)\n }\n // image/audio/video/document/redacted/provider-reasoning blocks are\n // intentionally absent: names and payloads belong to attachments/wire.\n }\n return parts.join('\\n\\n')\n}\n\nfunction searchableToolResult(output: string | ToolResultContent[]): string {\n if (typeof output === 'string')\n return safeSearchString(output, MAX_SEARCHABLE_TOOL_CHARS)\n return output.flatMap((part) => {\n if (part.type === 'text') {\n const text = safeSearchString(part.text, MAX_SEARCHABLE_TOOL_CHARS)\n return text ? [text] : []\n }\n // Structured image/audio/video/document content is an attachment.\n return []\n }).join('\\n')\n}\n\nfunction pushSearchText(parts: string[], text: string, max: number): void {\n const safe = safeSearchString(text, max)\n if (safe)\n parts.push(safe)\n}\n\nfunction safeSearchString(value: string, max: number): string {\n const trimmed = value.trim()\n if (!trimmed || isLikelyBase64(trimmed))\n return ''\n const withoutPayloads = trimmed.replace(BASE64_RUN_RE, '').trim()\n if (!withoutPayloads)\n return ''\n return withoutPayloads.length <= max ? withoutPayloads : `${withoutPayloads.slice(0, max)}\\n… [search text truncated]`\n}\n\nfunction isLikelyBase64(value: string): boolean {\n if (/^data:[^;,]+;base64,/i.test(value))\n return true\n if (value.length < 256 || value.length % 4 !== 0 || /\\s/.test(value))\n return false\n return /^[a-z0-9+/]+={0,2}$/i.test(value)\n}\n\nfunction searchableJson(value: unknown): string {\n const sanitized = sanitizeSearchValue(value, 0)\n if (sanitized === undefined)\n return ''\n try {\n return safeSearchString(JSON.stringify(sanitized, null, 2), MAX_SEARCHABLE_TOOL_CHARS)\n }\n catch {\n return ''\n }\n}\n\nfunction sanitizeSearchValue(value: unknown, depth: number): unknown {\n if (depth > 6 || value === null || typeof value === 'number' || typeof value === 'boolean')\n return value\n if (typeof value === 'string') {\n const safe = safeSearchString(value, MAX_SEARCHABLE_TOOL_CHARS)\n return safe || undefined\n }\n if (Array.isArray(value))\n return value.slice(0, 200).flatMap(item => sanitizeSearchValue(item, depth + 1) ?? [])\n if (typeof value !== 'object')\n return undefined\n const out: Record<string, unknown> = {}\n for (const [key, child] of Object.entries(value as Record<string, unknown>)) {\n if (BINARY_KEY_RE.test(key))\n continue\n const safe = sanitizeSearchValue(child, depth + 1)\n if (safe !== undefined)\n out[key] = safe\n }\n return Object.keys(out).length ? out : undefined\n}\n\nfunction compareMatches(a: SearchTurnMatch, b: SearchTurnMatch): number {\n return b.score - a.score || b.turn.createdAt - a.turn.createdAt || a.turn.id.localeCompare(b.turn.id)\n}\n\nfunction yieldToEventLoop(): Promise<void> {\n return new Promise(resolve => setTimeout(resolve, 0))\n}\n\nfunction parseQuery(query: string): ParsedQuery {\n const tokens = tokenize(query)\n const phrase = tokens.map(token => token.text).join(' ')\n return {\n tokens: tokens.map(token => ({ ...token, lower: lower(token.text) })),\n phraseLower: lower(phrase),\n }\n}\n\nfunction tokenize(query: string): QueryToken[] {\n const tokens: QueryToken[] = []\n let i = 0\n while (i < query.length) {\n while (i < query.length && /\\s/u.test(query[i]!))\n i += 1\n if (i >= query.length)\n break\n if (query[i] === '\"') {\n const start = i + 1\n i += 1\n let value = ''\n while (i < query.length && query[i] !== '\"') {\n value += query[i]\n i += 1\n }\n tokens.push({ text: value, start, end: i })\n if (query[i] === '\"')\n i += 1\n continue\n }\n const start = i\n while (i < query.length && !/\\s/u.test(query[i]!))\n i += 1\n tokens.push({ text: query.slice(start, i), start, end: i })\n }\n return tokens.filter(token => token.text.length > 0)\n}\n\nfunction findTermRanges(text: string, termLower: string): HighlightRange[] {\n if (!termLower)\n return []\n const ranges: HighlightRange[] = []\n let from = 0\n while (from <= text.length) {\n const index = text.indexOf(termLower, from)\n if (index === -1)\n break\n ranges.push({ start: index, end: index + termLower.length })\n from = index + Math.max(1, termLower.length)\n }\n return ranges\n}\n\n/** Word-local typo matching: one word within a bounded edit distance of the term. */\nfunction findFuzzyWordRange(text: string, termLower: string): HighlightRange[] | null {\n // Short fuzzy tokens are overwhelmingly noisy (`api` matching `app`, for\n // example). Exact substring matching already handles useful prefixes.\n if (termLower.length < 4)\n return null\n const maxEdits = termLower.length >= 9 ? 2 : 1\n const words = text.matchAll(/[\\p{L}\\p{N}_-]+/gu)\n let best: { start: number, end: number, distance: number } | null = null\n for (const match of words) {\n const candidate = match[0]!.toLocaleLowerCase()\n if (Math.abs(candidate.length - termLower.length) > maxEdits)\n continue\n const distance = boundedEditDistance(candidate, termLower, maxEdits)\n if (distance === null)\n continue\n const similarity = 1 - distance / Math.max(candidate.length, termLower.length)\n if (similarity < 0.8)\n continue\n if (!best || distance < best.distance || (distance === best.distance && candidate.length < best.end - best.start)) {\n const start = match.index ?? 0\n best = { start, end: start + match[0]!.length, distance }\n }\n }\n return best ? [{ start: best.start, end: best.end }] : null\n}\n\nfunction boundedEditDistance(left: string, right: string, limit: number): number | null {\n let previous = Array.from({ length: right.length + 1 }, (_, i) => i)\n for (let i = 1; i <= left.length; i++) {\n const current = [i]\n let rowMin = i\n for (let j = 1; j <= right.length; j++) {\n const value = Math.min(\n current[j - 1]! + 1,\n previous[j]! + 1,\n previous[j - 1]! + (left[i - 1] === right[j - 1] ? 0 : 1),\n )\n current.push(value)\n rowMin = Math.min(rowMin, value)\n }\n if (rowMin > limit)\n return null\n previous = current\n }\n const distance = previous[right.length]!\n return distance <= limit ? distance : null\n}\n\nfunction mergeRanges(ranges: readonly HighlightRange[], max: number): HighlightRange[] {\n const sorted = ranges\n .map(range => ({ start: clamp(range.start, 0, max), end: clamp(range.end, 0, max) }))\n .filter(range => range.end > range.start)\n .sort((a, b) => a.start - b.start || a.end - b.end)\n const merged: HighlightRange[] = []\n for (const range of sorted) {\n const last = merged[merged.length - 1]\n if (!last || range.start > last.end) {\n merged.push({ ...range })\n continue\n }\n last.end = Math.max(last.end, range.end)\n }\n return merged\n}\n\nfunction intersectRange(range: HighlightRange, start: number, end: number): HighlightRange | null {\n const s = Math.max(range.start, start)\n const e = Math.min(range.end, end)\n return e > s ? { start: s, end: e } : null\n}\n\nfunction clamp(value: number, min: number, max: number): number {\n return Math.min(max, Math.max(min, value))\n}\n\nfunction lower(value: string): string {\n return value.toLocaleLowerCase()\n}\n\ninterface ParsedQuery {\n tokens: (QueryToken & { lower: string })[]\n phraseLower: string\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { c as searchSessionHistoryIndexAsync, n as buildSessionSearchIndexAsync, u as searchSessionIndexAsync } from "./history-search-C4bk1Wz_.js";
|
|
2
|
+
//#region src/tui/history-search-worker.ts
|
|
3
|
+
const indexes = /* @__PURE__ */ new Map();
|
|
4
|
+
const controllers = /* @__PURE__ */ new Map();
|
|
5
|
+
function trimIndexes() {
|
|
6
|
+
while (indexes.size > 64) {
|
|
7
|
+
const oldest = indexes.keys().next().value;
|
|
8
|
+
if (oldest === void 0) return;
|
|
9
|
+
indexes.delete(oldest);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
globalThis.onmessage = (event) => {
|
|
13
|
+
const request = event.data;
|
|
14
|
+
if (request.type === "register") {
|
|
15
|
+
indexes.set(request.indexId, request.index);
|
|
16
|
+
trimIndexes();
|
|
17
|
+
postMessage({
|
|
18
|
+
type: "registered",
|
|
19
|
+
indexId: request.indexId
|
|
20
|
+
});
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if (request.type === "cancel") {
|
|
24
|
+
controllers.get(request.requestId)?.abort();
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (request.type === "build") {
|
|
28
|
+
const controller = new AbortController();
|
|
29
|
+
controllers.set(request.requestId, controller);
|
|
30
|
+
buildSessionSearchIndexAsync(request.turns, { signal: controller.signal }).then((index) => {
|
|
31
|
+
if (controller.signal.aborted) return;
|
|
32
|
+
indexes.set(request.indexId, index);
|
|
33
|
+
trimIndexes();
|
|
34
|
+
postMessage({
|
|
35
|
+
type: "result",
|
|
36
|
+
requestId: request.requestId,
|
|
37
|
+
result: {
|
|
38
|
+
indexId: request.indexId,
|
|
39
|
+
length: index.turns.length
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
}).catch((error) => {
|
|
43
|
+
if (!controller.signal.aborted) postMessage({
|
|
44
|
+
type: "error",
|
|
45
|
+
requestId: request.requestId,
|
|
46
|
+
message: error instanceof Error ? error.message : String(error)
|
|
47
|
+
});
|
|
48
|
+
}).finally(() => controllers.delete(request.requestId));
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const index = indexes.get(request.indexId);
|
|
52
|
+
if (!index) {
|
|
53
|
+
postMessage({
|
|
54
|
+
type: "error",
|
|
55
|
+
requestId: request.requestId,
|
|
56
|
+
message: `Unknown search index: ${request.indexId}`
|
|
57
|
+
});
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
indexes.delete(request.indexId);
|
|
61
|
+
indexes.set(request.indexId, index);
|
|
62
|
+
const controller = new AbortController();
|
|
63
|
+
controllers.set(request.requestId, controller);
|
|
64
|
+
(request.type === "search" ? searchSessionIndexAsync(index, request.query, {
|
|
65
|
+
limit: request.limit,
|
|
66
|
+
signal: controller.signal
|
|
67
|
+
}) : searchSessionHistoryIndexAsync(index, request.query, request.maxExcerpts, void 0, controller.signal)).then((result) => {
|
|
68
|
+
if (!controller.signal.aborted) postMessage({
|
|
69
|
+
type: "result",
|
|
70
|
+
requestId: request.requestId,
|
|
71
|
+
result
|
|
72
|
+
});
|
|
73
|
+
}).catch((error) => {
|
|
74
|
+
if (!controller.signal.aborted) postMessage({
|
|
75
|
+
type: "error",
|
|
76
|
+
requestId: request.requestId,
|
|
77
|
+
message: error instanceof Error ? error.message : String(error)
|
|
78
|
+
});
|
|
79
|
+
}).finally(() => {
|
|
80
|
+
controllers.delete(request.requestId);
|
|
81
|
+
});
|
|
82
|
+
};
|
|
83
|
+
//#endregion
|
|
84
|
+
export {};
|
|
85
|
+
|
|
86
|
+
//# sourceMappingURL=history-search-worker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"history-search-worker.js","names":[],"sources":["../src/tui/history-search-worker.ts"],"sourcesContent":["import type { SessionSearchIndex } from '../chat/history-search'\nimport {\n buildSessionSearchIndexAsync,\n searchSessionHistoryIndexAsync,\n searchSessionIndexAsync,\n} from '../chat/history-search'\n\ninterface RegisterRequest {\n type: 'register'\n indexId: string\n index: SessionSearchIndex\n}\n\ninterface BuildRequest {\n type: 'build'\n requestId: number\n indexId: string\n turns: SessionSearchIndex['turns'][number]['turn'][]\n}\n\ninterface SearchRequest {\n type: 'search'\n requestId: number\n indexId: string\n query: string\n limit: number\n}\n\ninterface HistoryRequest {\n type: 'history'\n requestId: number\n indexId: string\n query: string\n maxExcerpts: number\n}\n\ninterface CancelRequest {\n type: 'cancel'\n requestId: number\n}\n\ntype WorkerRequest = RegisterRequest | BuildRequest | SearchRequest | HistoryRequest | CancelRequest\n\nconst indexes = new Map<string, SessionSearchIndex>()\nconst controllers = new Map<number, AbortController>()\n\nfunction trimIndexes(): void {\n while (indexes.size > 64) {\n const oldest = indexes.keys().next().value\n if (oldest === undefined)\n return\n indexes.delete(oldest)\n }\n}\n\nglobalThis.onmessage = (event: MessageEvent<WorkerRequest>) => {\n const request = event.data\n if (request.type === 'register') {\n indexes.set(request.indexId, request.index)\n trimIndexes()\n postMessage({ type: 'registered', indexId: request.indexId })\n return\n }\n if (request.type === 'cancel') {\n controllers.get(request.requestId)?.abort()\n return\n }\n if (request.type === 'build') {\n const controller = new AbortController()\n controllers.set(request.requestId, controller)\n void buildSessionSearchIndexAsync(request.turns, { signal: controller.signal }).then((index) => {\n if (controller.signal.aborted)\n return\n indexes.set(request.indexId, index)\n trimIndexes()\n postMessage({ type: 'result', requestId: request.requestId, result: { indexId: request.indexId, length: index.turns.length } })\n }).catch((error) => {\n if (!controller.signal.aborted)\n postMessage({ type: 'error', requestId: request.requestId, message: error instanceof Error ? error.message : String(error) })\n }).finally(() => controllers.delete(request.requestId))\n return\n }\n const index = indexes.get(request.indexId)\n if (!index) {\n postMessage({ type: 'error', requestId: request.requestId, message: `Unknown search index: ${request.indexId}` })\n return\n }\n // Refresh LRU recency on hit (Map preserves insertion order), mirroring the\n // app-side cache — otherwise the hottest index is the first one evicted.\n indexes.delete(request.indexId)\n indexes.set(request.indexId, index)\n const controller = new AbortController()\n controllers.set(request.requestId, controller)\n const work = request.type === 'search'\n ? searchSessionIndexAsync(index, request.query, { limit: request.limit, signal: controller.signal })\n : searchSessionHistoryIndexAsync(index, request.query, request.maxExcerpts, undefined, controller.signal)\n void work.then((result) => {\n if (!controller.signal.aborted)\n postMessage({ type: 'result', requestId: request.requestId, result })\n }).catch((error) => {\n if (!controller.signal.aborted)\n postMessage({ type: 'error', requestId: request.requestId, message: error instanceof Error ? error.message : String(error) })\n }).finally(() => {\n controllers.delete(request.requestId)\n })\n}\n"],"mappings":";;AA2CA,MAAM,0BAAU,IAAI,IAAgC;AACpD,MAAM,8BAAc,IAAI,IAA6B;AAErD,SAAS,cAAoB;CAC3B,OAAO,QAAQ,OAAO,IAAI;EACxB,MAAM,SAAS,QAAQ,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC;EACrC,IAAI,WAAW,KAAA,GACb;EACF,QAAQ,OAAO,MAAM;CACvB;AACF;AAEA,WAAW,aAAa,UAAuC;CAC7D,MAAM,UAAU,MAAM;CACtB,IAAI,QAAQ,SAAS,YAAY;EAC/B,QAAQ,IAAI,QAAQ,SAAS,QAAQ,KAAK;EAC1C,YAAY;EACZ,YAAY;GAAE,MAAM;GAAc,SAAS,QAAQ;EAAQ,CAAC;EAC5D;CACF;CACA,IAAI,QAAQ,SAAS,UAAU;EAC7B,YAAY,IAAI,QAAQ,SAAS,CAAC,EAAE,MAAM;EAC1C;CACF;CACA,IAAI,QAAQ,SAAS,SAAS;EAC5B,MAAM,aAAa,IAAI,gBAAgB;EACvC,YAAY,IAAI,QAAQ,WAAW,UAAU;EAC7C,6BAAkC,QAAQ,OAAO,EAAE,QAAQ,WAAW,OAAO,CAAC,CAAC,CAAC,MAAM,UAAU;GAC9F,IAAI,WAAW,OAAO,SACpB;GACF,QAAQ,IAAI,QAAQ,SAAS,KAAK;GAClC,YAAY;GACZ,YAAY;IAAE,MAAM;IAAU,WAAW,QAAQ;IAAW,QAAQ;KAAE,SAAS,QAAQ;KAAS,QAAQ,MAAM,MAAM;IAAO;GAAE,CAAC;EAChI,CAAC,CAAC,CAAC,OAAO,UAAU;GAClB,IAAI,CAAC,WAAW,OAAO,SACrB,YAAY;IAAE,MAAM;IAAS,WAAW,QAAQ;IAAW,SAAS,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;GAAE,CAAC;EAChI,CAAC,CAAC,CAAC,cAAc,YAAY,OAAO,QAAQ,SAAS,CAAC;EACtD;CACF;CACA,MAAM,QAAQ,QAAQ,IAAI,QAAQ,OAAO;CACzC,IAAI,CAAC,OAAO;EACV,YAAY;GAAE,MAAM;GAAS,WAAW,QAAQ;GAAW,SAAS,yBAAyB,QAAQ;EAAU,CAAC;EAChH;CACF;CAGA,QAAQ,OAAO,QAAQ,OAAO;CAC9B,QAAQ,IAAI,QAAQ,SAAS,KAAK;CAClC,MAAM,aAAa,IAAI,gBAAgB;CACvC,YAAY,IAAI,QAAQ,WAAW,UAAU;CAI7C,CAHa,QAAQ,SAAS,WAC1B,wBAAwB,OAAO,QAAQ,OAAO;EAAE,OAAO,QAAQ;EAAO,QAAQ,WAAW;CAAO,CAAC,IACjG,+BAA+B,OAAO,QAAQ,OAAO,QAAQ,aAAa,KAAA,GAAW,WAAW,MAAM,EAAA,CAChG,MAAM,WAAW;EACzB,IAAI,CAAC,WAAW,OAAO,SACrB,YAAY;GAAE,MAAM;GAAU,WAAW,QAAQ;GAAW;EAAO,CAAC;CACxE,CAAC,CAAC,CAAC,OAAO,UAAU;EAClB,IAAI,CAAC,WAAW,OAAO,SACrB,YAAY;GAAE,MAAM;GAAS,WAAW,QAAQ;GAAW,SAAS,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;EAAE,CAAC;CAChI,CAAC,CAAC,CAAC,cAAc;EACf,YAAY,OAAO,QAAQ,SAAS;CACtC,CAAC;AACH"}
|