dsh-locator 0.9.0 → 0.10.0
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/CHANGELOG.md +34 -0
- package/README-es.md +9 -0
- package/README-hi.md +9 -0
- package/README-pt.md +9 -0
- package/README.en.md +10 -1
- package/README.md +10 -1
- package/bin/darwin-arm64/indexd +0 -0
- package/bin/darwin-x64/indexd +0 -0
- package/bin/linux-arm64/indexd +0 -0
- package/bin/linux-x64/indexd +0 -0
- package/bin/win32-arm64/indexd.exe +0 -0
- package/bin/win32-x64/indexd.exe +0 -0
- package/client.js +336 -145
- package/lib/combined.d.ts.map +1 -1
- package/lib/combined.js +14 -4
- package/lib/combined.js.map +1 -1
- package/lib/content-search-worker.d.ts +5 -0
- package/lib/content-search-worker.d.ts.map +1 -1
- package/lib/content-search-worker.js +51 -29
- package/lib/content-search-worker.js.map +1 -1
- package/lib/content-search.d.ts +46 -0
- package/lib/content-search.d.ts.map +1 -1
- package/lib/content-search.js +186 -69
- package/lib/content-search.js.map +1 -1
- package/lib/content.d.ts +66 -8
- package/lib/content.d.ts.map +1 -1
- package/lib/content.js +292 -78
- package/lib/content.js.map +1 -1
- package/lib/export.d.ts +61 -0
- package/lib/export.d.ts.map +1 -1
- package/lib/export.js +217 -52
- package/lib/export.js.map +1 -1
- package/lib/find.d.ts.map +1 -1
- package/lib/find.js +72 -16
- package/lib/find.js.map +1 -1
- package/lib/http.d.ts +38 -2
- package/lib/http.d.ts.map +1 -1
- package/lib/http.js +270 -80
- package/lib/http.js.map +1 -1
- package/lib/reveal.d.ts +69 -3
- package/lib/reveal.d.ts.map +1 -1
- package/lib/reveal.js +140 -11
- package/lib/reveal.js.map +1 -1
- package/lib/tools.d.ts +30 -0
- package/lib/tools.d.ts.map +1 -1
- package/lib/tools.js +65 -3
- package/lib/tools.js.map +1 -1
- package/package.json +1 -1
- package/src/combined.ts +13 -4
- package/src/content-search-worker.ts +47 -26
- package/src/content-search.ts +217 -66
- package/src/content.ts +287 -74
- package/src/export.ts +240 -50
- package/src/find.ts +82 -16
- package/src/http.ts +260 -81
- package/src/reveal.ts +148 -10
- package/src/tools.ts +83 -6
package/client.js
CHANGED
|
@@ -192,7 +192,7 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
192
192
|
asc: 'Ascending', descOrder: 'Descending',
|
|
193
193
|
groupContent: 'Content', groupFiles: 'Files', groupDirs: 'Folders',
|
|
194
194
|
relevance: 'by relevance', contentBuilding: 'indexing… {files} files / {messages} messages',
|
|
195
|
-
contentShort: 'Type at least
|
|
195
|
+
contentShort: 'Type at least 3 characters to search bodies and sessions (Ctrl+F handles shorter queries)',
|
|
196
196
|
contentDisabled: 'Content index is disabled (contentIndex: false)',
|
|
197
197
|
contentCapped: 'first {n}', showing: 'first {n}', deduped: '{n} name hits merged into content', loading: 'Searching…',
|
|
198
198
|
emptyQuery: 'Type to search file names, paths, bodies and sessions',
|
|
@@ -216,7 +216,7 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
216
216
|
findScope: 'In selection', findScopeChip: 'selection {n} chars', findSessionChip: 'this conversation',
|
|
217
217
|
findNoMatch: 'No matches', findEmpty: 'Type to find in this conversation',
|
|
218
218
|
findInvalidRegex: 'Invalid regular expression: {message}', findInvalidEscape: 'Invalid extended escape: {token}',
|
|
219
|
-
findTimeout: 'Regex timed out; simplify the expression', findWrappedTop: 'Wrapped to top',
|
|
219
|
+
findTimeout: 'Regex timed out; simplify the expression', findRegexUnavailable: 'Regex search is unavailable here (worker blocked)', findWrappedTop: 'Wrapped to top',
|
|
220
220
|
findWrappedBottom: 'Wrapped to bottom', findNoWrapEnd: 'Reached the last match (wrap is off)',
|
|
221
221
|
findNoWrapStart: 'Reached the first match (wrap is off)', findOlder: 'Load earlier messages and retry',
|
|
222
222
|
findLoadingOlder: 'Loading earlier messages…', findLoadedOnly: 'loaded window only · earlier history not loaded',
|
|
@@ -226,6 +226,7 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
226
226
|
panelTotal: 'selected {index}/{total}', panelRangeAll: 'All indexed', panelRangeSession: 'This conversation',
|
|
227
227
|
panelSessionHint: 'This scope searches indexed session messages only; press Ctrl+F for live in-conversation find',
|
|
228
228
|
panelRegexPaused: 'Content scope paused in regex mode', panelCountBounded: '≥ {n}', panelCountExact: '{n}',
|
|
229
|
+
contentWarning: 'Index warning: {list}', contentBudget: 'Content index budget reached; some files are not indexed', contentTooShort: 'Content needs at least 3 characters (use Ctrl+F for shorter searches)', contentTimeout: 'Content search timed out; narrow the query or scope', contentBadChars: 'Content search does not support NUL/control characters',
|
|
229
230
|
panelCaseShort: 'Aa', panelWholeShort: 'Word', panelModeLabel: 'Mode',
|
|
230
231
|
},
|
|
231
232
|
zh: {
|
|
@@ -236,7 +237,7 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
236
237
|
asc: '升序', descOrder: '降序',
|
|
237
238
|
groupContent: '内容', groupFiles: '文件', groupDirs: '目录',
|
|
238
239
|
relevance: '按相关度', contentBuilding: '索引构建中… {files} 文件 / {messages} 消息',
|
|
239
|
-
contentShort: '输入至少
|
|
240
|
+
contentShort: '输入至少 3 个字符后搜索正文与会话(更短的词请用 Ctrl+F)',
|
|
240
241
|
contentDisabled: '内容索引已关闭(contentIndex: false)',
|
|
241
242
|
contentCapped: '前 {n} 条', showing: '前 {n} 条', deduped: '{n} 条文件命中已并入内容', loading: '搜索中…',
|
|
242
243
|
emptyQuery: '输入关键词:文件名、路径、正文或会话内容',
|
|
@@ -260,7 +261,7 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
260
261
|
findScope: '仅在选区内', findScopeChip: '选区 {n} 字', findSessionChip: '本会话',
|
|
261
262
|
findNoMatch: '没有匹配', findEmpty: '输入关键词后在此会话中查找',
|
|
262
263
|
findInvalidRegex: '正则无效:{message}', findInvalidEscape: '扩展转义无效:{token}',
|
|
263
|
-
findTimeout: '正则执行超时,请简化表达式', findWrappedTop: '已回到顶部',
|
|
264
|
+
findTimeout: '正则执行超时,请简化表达式', findRegexUnavailable: '此处无法运行正则(worker 被阻止)', findWrappedTop: '已回到顶部',
|
|
264
265
|
findWrappedBottom: '已到底部,回到开头', findNoWrapEnd: '已到最后一个匹配(循环查找已关闭)',
|
|
265
266
|
findNoWrapStart: '已到第一个匹配(循环查找已关闭)', findOlder: '加载更早消息后重试',
|
|
266
267
|
findLoadingOlder: '正在加载更早消息…', findLoadedOnly: '仅已加载窗口 · 更早历史未加载',
|
|
@@ -270,6 +271,7 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
270
271
|
panelTotal: '已选 {index}/{total}', panelRangeAll: '全部索引', panelRangeSession: '当前会话',
|
|
271
272
|
panelSessionHint: '此范围只查已索引的会话消息;实时查找请按 Ctrl+F',
|
|
272
273
|
panelRegexPaused: '正则模式下内容范围暂停', panelCountBounded: '≥ {n}', panelCountExact: '{n}',
|
|
274
|
+
contentWarning: '索引警告:{list}', contentBudget: '内容索引已达预算上限,部分文件未被收录', contentTooShort: '内容搜索至少需要 3 个字符(更短的词请用 Ctrl+F 在会话内查找)', contentTimeout: '内容搜索超时,请缩小关键词或范围', contentBadChars: '内容搜索不支持 NUL/控制字符',
|
|
273
275
|
panelCaseShort: 'Aa', panelWholeShort: '全词', panelModeLabel: '模式',
|
|
274
276
|
},
|
|
275
277
|
es: {
|
|
@@ -280,7 +282,7 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
280
282
|
asc: 'Ascendente', descOrder: 'Descendente',
|
|
281
283
|
groupContent: 'Contenido', groupFiles: 'Archivos', groupDirs: 'Carpetas',
|
|
282
284
|
relevance: 'por relevancia', contentBuilding: 'indexando… {files} archivos / {messages} mensajes',
|
|
283
|
-
contentShort: 'Escribe al menos
|
|
285
|
+
contentShort: 'Escribe al menos 3 caracteres para buscar contenido y sesiones (Ctrl+F para consultas cortas)',
|
|
284
286
|
contentDisabled: 'Índice de contenido desactivado (contentIndex: false)',
|
|
285
287
|
contentCapped: 'primeros {n}', showing: 'primeros {n}', deduped: '{n} aciertos de nombre fusionados', loading: 'Buscando…',
|
|
286
288
|
emptyQuery: 'Escribe para buscar nombres, rutas, contenido y sesiones',
|
|
@@ -304,7 +306,7 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
304
306
|
findScope: 'En la selección', findScopeChip: 'selección {n} caracteres', findSessionChip: 'esta conversación',
|
|
305
307
|
findNoMatch: 'Sin coincidencias', findEmpty: 'Escribe para buscar en esta conversación',
|
|
306
308
|
findInvalidRegex: 'Expresión regular no válida: {message}', findInvalidEscape: 'Escape extendido no válido: {token}',
|
|
307
|
-
findTimeout: 'La regex agotó el tiempo; simplifícala', findWrappedTop: 'Vuelto al inicio',
|
|
309
|
+
findTimeout: 'La regex agotó el tiempo; simplifícala', findRegexUnavailable: 'La búsqueda regex no está disponible aquí (worker bloqueado)', findWrappedTop: 'Vuelto al inicio',
|
|
308
310
|
findWrappedBottom: 'Vuelto al final', findNoWrapEnd: 'Última coincidencia (continuación desactivada)',
|
|
309
311
|
findNoWrapStart: 'Primera coincidencia (continuación desactivada)', findOlder: 'Cargar mensajes anteriores y reintentar',
|
|
310
312
|
findLoadingOlder: 'Cargando mensajes anteriores…', findLoadedOnly: 'solo ventana cargada · historial anterior sin cargar',
|
|
@@ -314,6 +316,7 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
314
316
|
panelTotal: 'seleccionado {index}/{total}', panelRangeAll: 'Todo el índice', panelRangeSession: 'Esta conversación',
|
|
315
317
|
panelSessionHint: 'Este ámbito solo busca mensajes indexados; pulsa Ctrl+F para buscar en vivo',
|
|
316
318
|
panelRegexPaused: 'Ámbito de contenido en pausa en modo regex', panelCountBounded: '≥ {n}', panelCountExact: '{n}',
|
|
319
|
+
contentWarning: 'Aviso del índice: {list}', contentBudget: 'Se alcanzó el presupuesto del índice; algunos archivos no están indexados', contentTooShort: 'El contenido requiere al menos 3 caracteres (usa Ctrl+F para búsquedas más cortas)', contentTimeout: 'La búsqueda de contenido agotó el tiempo; reduce la consulta o el ámbito', contentBadChars: 'La búsqueda de contenido no admite NUL/caracteres de control',
|
|
317
320
|
panelCaseShort: 'Aa', panelWholeShort: 'Palabra', panelModeLabel: 'Modo',
|
|
318
321
|
},
|
|
319
322
|
pt: {
|
|
@@ -324,7 +327,7 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
324
327
|
asc: 'Ascendente', descOrder: 'Descendente',
|
|
325
328
|
groupContent: 'Conteúdo', groupFiles: 'Arquivos', groupDirs: 'Pastas',
|
|
326
329
|
relevance: 'por relevância', contentBuilding: 'indexando… {files} arquivos / {messages} mensagens',
|
|
327
|
-
contentShort: 'Digite pelo menos
|
|
330
|
+
contentShort: 'Digite pelo menos 3 caracteres para buscar conteúdo e sessões (Ctrl+F para consultas curtas)',
|
|
328
331
|
contentDisabled: 'Índice de conteúdo desativado (contentIndex: false)',
|
|
329
332
|
contentCapped: 'primeiros {n}', showing: 'primeiros {n}', deduped: '{n} acertos de nome mesclados', loading: 'Buscando…',
|
|
330
333
|
emptyQuery: 'Digite para buscar nomes, caminhos, conteúdo e sessões',
|
|
@@ -348,7 +351,7 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
348
351
|
findScope: 'Na seleção', findScopeChip: 'seleção {n} caracteres', findSessionChip: 'esta conversa',
|
|
349
352
|
findNoMatch: 'Sem resultados', findEmpty: 'Digite para buscar nesta conversa',
|
|
350
353
|
findInvalidRegex: 'Expressão regular inválida: {message}', findInvalidEscape: 'Escape estendido inválido: {token}',
|
|
351
|
-
findTimeout: 'A regex excedeu o tempo; simplifique', findWrappedTop: 'Voltou ao início',
|
|
354
|
+
findTimeout: 'A regex excedeu o tempo; simplifique', findRegexUnavailable: 'A busca regex não está disponível aqui (worker bloqueado)', findWrappedTop: 'Voltou ao início',
|
|
352
355
|
findWrappedBottom: 'Voltou ao fim', findNoWrapEnd: 'Última ocorrência (continuação desativada)',
|
|
353
356
|
findNoWrapStart: 'Primeira ocorrência (continuação desativada)', findOlder: 'Carregar mensagens anteriores e tentar novamente',
|
|
354
357
|
findLoadingOlder: 'Carregando mensagens anteriores…', findLoadedOnly: 'apenas janela carregada · histórico anterior não carregado',
|
|
@@ -358,6 +361,7 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
358
361
|
panelTotal: 'selecionado {index}/{total}', panelRangeAll: 'Todo o índice', panelRangeSession: 'Esta conversa',
|
|
359
362
|
panelSessionHint: 'Este escopo busca apenas mensagens indexadas; use Ctrl+F para buscar ao vivo',
|
|
360
363
|
panelRegexPaused: 'Escopo de conteúdo pausado no modo regex', panelCountBounded: '≥ {n}', panelCountExact: '{n}',
|
|
364
|
+
contentWarning: 'Aviso do índice: {list}', contentBudget: 'O orçamento do índice foi atingido; alguns arquivos não estão indexados', contentTooShort: 'O conteúdo exige pelo menos 3 caracteres (use Ctrl+F para buscas menores)', contentTimeout: 'A busca de conteúdo excedeu o tempo; reduza a consulta ou o escopo', contentBadChars: 'A busca de conteúdo não suporta NUL/caracteres de controle',
|
|
361
365
|
panelCaseShort: 'Aa', panelWholeShort: 'Palavra', panelModeLabel: 'Modo',
|
|
362
366
|
},
|
|
363
367
|
hi: {
|
|
@@ -368,7 +372,7 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
368
372
|
asc: 'आरोही', descOrder: 'अवरोही',
|
|
369
373
|
groupContent: 'सामग्री', groupFiles: 'फ़ाइलें', groupDirs: 'फ़ोल्डर',
|
|
370
374
|
relevance: 'प्रासंगिकता से', contentBuilding: 'इंडेक्स बन रहा है… {files} फ़ाइलें / {messages} संदेश',
|
|
371
|
-
contentShort: 'सामग्री और सत्र खोजने के लिए कम से कम
|
|
375
|
+
contentShort: 'सामग्री और सत्र खोजने के लिए कम से कम 3 अक्षर लिखें (छोटी खोज के लिए Ctrl+F)',
|
|
372
376
|
contentDisabled: 'सामग्री इंडेक्स बंद है (contentIndex: false)',
|
|
373
377
|
contentCapped: 'पहले {n}', showing: 'पहले {n}', deduped: '{n} नाम-परिणाम सामग्री में मिलाए', loading: 'खोज रहे हैं…',
|
|
374
378
|
emptyQuery: 'नाम, पथ, सामग्री और सत्र खोजने के लिए लिखें',
|
|
@@ -392,7 +396,7 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
392
396
|
findScope: 'चयन में', findScopeChip: 'चयन {n} अक्षर', findSessionChip: 'यह बातचीत',
|
|
393
397
|
findNoMatch: 'कोई मेल नहीं', findEmpty: 'इस बातचीत में खोजने के लिए लिखें',
|
|
394
398
|
findInvalidRegex: 'अमान्य रेगुलर एक्सप्रेशन: {message}', findInvalidEscape: 'अमान्य विस्तृत एस्केप: {token}',
|
|
395
|
-
findTimeout: 'रेगेक्स का समय समाप्त; सरल बनाएँ', findWrappedTop: 'शीर्ष पर लौटा',
|
|
399
|
+
findTimeout: 'रेगेक्स का समय समाप्त; सरल बनाएँ', findRegexUnavailable: 'यहाँ रेगेक्स उपलब्ध नहीं है (वर्कर अवरुद्ध)', findWrappedTop: 'शीर्ष पर लौटा',
|
|
396
400
|
findWrappedBottom: 'अंत पर लौटा', findNoWrapEnd: 'अंतिम मेल (चक्र बंद)',
|
|
397
401
|
findNoWrapStart: 'पहला मेल (चक्र बंद)', findOlder: 'पुराने संदेश लोड कर पुन: प्रयास',
|
|
398
402
|
findLoadingOlder: 'पुराने संदेश लोड हो रहे हैं…', findLoadedOnly: 'केवल लोडेड विंडो · पुराना इतिहास लोड नहीं',
|
|
@@ -402,6 +406,7 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
402
406
|
panelTotal: 'चयनित {index}/{total}', panelRangeAll: 'पूरा इंडेक्स', panelRangeSession: 'यह बातचीत',
|
|
403
407
|
panelSessionHint: 'यह क्षेत्र केवल इंडेक्स किए संदेश खोजता है; लाइव खोज के लिए Ctrl+F दबाएँ',
|
|
404
408
|
panelRegexPaused: 'रेगेक्स मोड में सामग्री क्षेत्र रुका है', panelCountBounded: '≥ {n}', panelCountExact: '{n}',
|
|
409
|
+
contentWarning: 'इंडेक्स चेतावनी: {list}', contentBudget: 'सामग्री इंडेक्स बजट पूरा; कुछ फ़ाइलें इंडेक्स नहीं हैं', contentTooShort: 'सामग्री के लिए कम से कम 3 अक्षर चाहिए (छोटी खोज के लिए Ctrl+F)', contentTimeout: 'सामग्री खोज का समय समाप्त; क्वेरी या क्षेत्र घटाएँ', contentBadChars: 'सामग्री खोज NUL/नियंत्रण अक्षर नहीं लेती',
|
|
405
410
|
panelCaseShort: 'Aa', panelWholeShort: 'शब्द', panelModeLabel: 'मोड',
|
|
406
411
|
},
|
|
407
412
|
}
|
|
@@ -540,7 +545,8 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
540
545
|
const HL_BASE = 'dsh-locator-find'
|
|
541
546
|
const HL_ACTIVE = 'dsh-locator-find-active'
|
|
542
547
|
const GESTURES = ['wheel', 'touchstart', 'pointerdown', 'keydown']
|
|
543
|
-
const EXCLUDED = '[hidden],[data-chat-history],[data-text-interaction-surface],[data-composer-seat],script,style'
|
|
548
|
+
const EXCLUDED = '[hidden],[data-chat-history],[data-text-interaction-surface],[data-composer-seat],script,style,'
|
|
549
|
+
+ '.sr-only,[class*="visuallyHidden"],[role="status"],[aria-live]'
|
|
544
550
|
const BLOCK_SELECTOR = 'p,li,pre,td,th,h1,h2,h3,h4,h5,h6,blockquote,dd,dt'
|
|
545
551
|
const legacyTargets = new Set()
|
|
546
552
|
|
|
@@ -611,21 +617,28 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
611
617
|
return scope === null ? null : scope.querySelector('[data-chat-flow]')
|
|
612
618
|
}
|
|
613
619
|
|
|
614
|
-
function
|
|
615
|
-
if (flow === null) return
|
|
620
|
+
function findMessageRows(flow, messageId, nodeKey) {
|
|
621
|
+
if (flow === null) return []
|
|
616
622
|
const rows = flow.querySelectorAll('[data-chat-node-key]')
|
|
623
|
+
const exact = []
|
|
617
624
|
if (nodeKey) {
|
|
618
625
|
for (const row of rows) {
|
|
619
|
-
if (row.getAttribute('data-chat-node-key') === String(nodeKey))
|
|
626
|
+
if (row.getAttribute('data-chat-node-key') === String(nodeKey)) exact.push(row)
|
|
620
627
|
}
|
|
621
628
|
}
|
|
622
|
-
if (
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
629
|
+
if (exact.length === 0 && messageId) {
|
|
630
|
+
const id = String(messageId)
|
|
631
|
+
for (const row of rows) {
|
|
632
|
+
const key = row.getAttribute('data-chat-node-key') || ''
|
|
633
|
+
if (key === id || key.endsWith(id)) exact.push(row)
|
|
634
|
+
}
|
|
627
635
|
}
|
|
628
|
-
|
|
636
|
+
// A single assistant step can be rendered as a reasoning row and a
|
|
637
|
+
// response row under the same node key; prefer the response row.
|
|
638
|
+
return exact.sort((left, right) => {
|
|
639
|
+
const score = row => row.getAttribute('data-chat-group-part') === 'response' ? 0 : 1
|
|
640
|
+
return score(left) - score(right)
|
|
641
|
+
})
|
|
629
642
|
}
|
|
630
643
|
|
|
631
644
|
function needlesFromQuery(query) {
|
|
@@ -635,57 +648,173 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
635
648
|
return [...new Set(words)].sort((a, b) => b.length - a.length).slice(0, 8)
|
|
636
649
|
}
|
|
637
650
|
|
|
638
|
-
function
|
|
639
|
-
|
|
651
|
+
function projectGroups(root, excluded) {
|
|
652
|
+
if (root === null) return []
|
|
640
653
|
const groups = new Map()
|
|
654
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT, {
|
|
655
|
+
acceptNode(node) {
|
|
656
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
657
|
+
if (node.matches(excluded)) return NodeFilter.FILTER_REJECT
|
|
658
|
+
if (node.tagName === 'BR') return NodeFilter.FILTER_ACCEPT
|
|
659
|
+
return NodeFilter.FILTER_SKIP
|
|
660
|
+
}
|
|
661
|
+
return NodeFilter.FILTER_ACCEPT
|
|
662
|
+
},
|
|
663
|
+
})
|
|
664
|
+
const groupFor = block => {
|
|
665
|
+
let group = groups.get(block)
|
|
666
|
+
if (group === undefined) {
|
|
667
|
+
group = {
|
|
668
|
+
block, raw: '', rawLength: 0, text: '', runs: [], nodes: [], breaks: [],
|
|
669
|
+
collapse: !(block instanceof Element && block.tagName === 'PRE'),
|
|
670
|
+
}
|
|
671
|
+
groups.set(block, group)
|
|
672
|
+
}
|
|
673
|
+
return group
|
|
674
|
+
}
|
|
675
|
+
const appendText = (group, node) => {
|
|
676
|
+
const value = node.nodeValue || ''
|
|
677
|
+
if (value === '') return
|
|
678
|
+
group.nodes.push({ node, start: group.raw.length, end: group.raw.length + value.length })
|
|
679
|
+
group.raw += value
|
|
680
|
+
}
|
|
681
|
+
const appendBreak = group => {
|
|
682
|
+
group.breaks.push(group.raw.length)
|
|
683
|
+
group.raw += '\u0000'
|
|
684
|
+
}
|
|
641
685
|
let node = walker.nextNode()
|
|
642
686
|
while (node !== null) {
|
|
643
687
|
const parent = node.parentElement
|
|
644
|
-
if (
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
group.text += value
|
|
688
|
+
if (node.nodeType === Node.ELEMENT_NODE && node.tagName === 'BR') {
|
|
689
|
+
if (parent !== null && parent.closest(excluded) === null) {
|
|
690
|
+
const block = parent.closest(BLOCK_SELECTOR)
|
|
691
|
+
|| parent.closest('[data-text-interaction-source]')
|
|
692
|
+
|| parent.closest('[data-chat-node-key]')
|
|
693
|
+
|| root
|
|
694
|
+
appendBreak(groupFor(block))
|
|
652
695
|
}
|
|
696
|
+
} else if (parent !== null && parent.closest(excluded) === null) {
|
|
697
|
+
const block = parent.closest(BLOCK_SELECTOR)
|
|
698
|
+
|| parent.closest('[data-text-interaction-source]')
|
|
699
|
+
|| parent.closest('[data-chat-node-key]')
|
|
700
|
+
|| root
|
|
701
|
+
appendText(groupFor(block), node)
|
|
653
702
|
}
|
|
654
703
|
node = walker.nextNode()
|
|
655
704
|
}
|
|
705
|
+
// Project the concatenated raw text once per block. Soft newlines and
|
|
706
|
+
// repeated whitespace become one space outside <pre>; <br> becomes a
|
|
707
|
+
// separator. rums maps projected offsets back to raw offsets without
|
|
708
|
+
// allocating one object per character (a multi-MB transcript would
|
|
709
|
+
// otherwise create millions of position objects).
|
|
710
|
+
for (const group of groups.values()) {
|
|
711
|
+
const raw = group.raw
|
|
712
|
+
group.rawLength = raw.length
|
|
713
|
+
if (!group.collapse) {
|
|
714
|
+
group.text = raw.replace(/\u0000/g, '\n')
|
|
715
|
+
group.runs = []
|
|
716
|
+
continue
|
|
717
|
+
}
|
|
718
|
+
let text = ''
|
|
719
|
+
const runs = []
|
|
720
|
+
let index = 0
|
|
721
|
+
while (index < raw.length) {
|
|
722
|
+
const char = raw[index]
|
|
723
|
+
if (char === '\u0000' || /\s/u.test(char)) {
|
|
724
|
+
const start = index
|
|
725
|
+
while (index < raw.length && (raw[index] === '\u0000' || /\s/u.test(raw[index]))) index++
|
|
726
|
+
runs.push({ rawStart: start, rawLength: index - start, projectedStart: text.length })
|
|
727
|
+
text += ' '
|
|
728
|
+
} else {
|
|
729
|
+
text += char
|
|
730
|
+
index++
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
group.text = text
|
|
734
|
+
group.runs = runs
|
|
735
|
+
}
|
|
656
736
|
return [...groups.values()]
|
|
657
737
|
}
|
|
658
738
|
|
|
659
|
-
function
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
739
|
+
function inlineGroups(root) {
|
|
740
|
+
return projectGroups(root, EXCLUDED)
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
function projectedToRaw(group, index, isEnd) {
|
|
744
|
+
if (!group.collapse) return Math.max(0, Math.min(group.rawLength, index))
|
|
745
|
+
let extra = 0
|
|
746
|
+
for (const run of group.runs) {
|
|
747
|
+
if (run.projectedStart < index || (isEnd && run.projectedStart === index)) extra += run.rawLength - 1
|
|
748
|
+
else break
|
|
749
|
+
}
|
|
750
|
+
return Math.max(0, Math.min(group.rawLength, index + extra))
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
function rawToProjected(group, raw) {
|
|
754
|
+
if (!group.collapse) return Math.max(0, Math.min(group.rawLength, raw))
|
|
755
|
+
let extra = 0
|
|
756
|
+
for (const run of group.runs) {
|
|
757
|
+
const end = run.rawStart + run.rawLength
|
|
758
|
+
if (raw >= end) extra += run.rawLength - 1
|
|
759
|
+
else if (raw > run.rawStart) return run.projectedStart
|
|
760
|
+
else break
|
|
761
|
+
}
|
|
762
|
+
return Math.max(0, Math.min(group.rawLength, raw - extra))
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
function pointForRaw(group, raw, isEnd) {
|
|
766
|
+
for (const entry of group.nodes) {
|
|
767
|
+
if (raw < entry.start) continue
|
|
768
|
+
if (raw <= entry.end) return { node: entry.node, offset: Math.min(raw - entry.start, entry.end - entry.start) }
|
|
769
|
+
}
|
|
770
|
+
for (const br of group.breaks) {
|
|
771
|
+
if (raw !== br) continue
|
|
772
|
+
if (isEnd) {
|
|
773
|
+
for (let i = group.nodes.length - 1; i >= 0; i--) {
|
|
774
|
+
const entry = group.nodes[i]
|
|
775
|
+
if (entry.end <= br) return { node: entry.node, offset: entry.end - entry.start }
|
|
776
|
+
}
|
|
777
|
+
} else {
|
|
778
|
+
for (const entry of group.nodes) {
|
|
779
|
+
if (entry.start > br) return { node: entry.node, offset: 0 }
|
|
780
|
+
}
|
|
781
|
+
}
|
|
665
782
|
}
|
|
666
783
|
const last = group.nodes[group.nodes.length - 1]
|
|
667
|
-
return
|
|
784
|
+
return last === undefined ? null : { node: last.node, offset: last.end - last.start }
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
function positionAt(group, index, isEnd) {
|
|
788
|
+
const raw = projectedToRaw(group, index, isEnd)
|
|
789
|
+
return pointForRaw(group, raw, isEnd)
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
function rangeAt(group, start, end) {
|
|
793
|
+
const from = positionAt(group, start, false)
|
|
794
|
+
const to = positionAt(group, end, true)
|
|
795
|
+
if (from === null || to === null) return null
|
|
796
|
+
const range = document.createRange()
|
|
797
|
+
try {
|
|
798
|
+
range.setStart(from.node, from.offset)
|
|
799
|
+
range.setEnd(to.node, to.offset)
|
|
800
|
+
return range
|
|
801
|
+
} catch {
|
|
802
|
+
return null
|
|
803
|
+
}
|
|
668
804
|
}
|
|
669
805
|
|
|
670
806
|
function rangesForNeedles(root, needles) {
|
|
671
807
|
const ranges = []
|
|
672
808
|
if (root === null || needles.length === 0) return ranges
|
|
809
|
+
const patterns = needles.map(escapeRegExp)
|
|
810
|
+
const expression = new RegExp(patterns.join('|'), 'giu')
|
|
673
811
|
for (const group of inlineGroups(root)) {
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
if (index < 0) break
|
|
681
|
-
const start = offsetInGroup(group, index)
|
|
682
|
-
const end = offsetInGroup(group, index + target.length)
|
|
683
|
-
const range = document.createRange()
|
|
684
|
-
range.setStart(start[0], start[1])
|
|
685
|
-
range.setEnd(end[0], end[1])
|
|
686
|
-
ranges.push(range)
|
|
687
|
-
from = index + target.length
|
|
688
|
-
}
|
|
812
|
+
expression.lastIndex = 0
|
|
813
|
+
let match
|
|
814
|
+
while (ranges.length < 200 && (match = expression.exec(group.text)) !== null) {
|
|
815
|
+
if (match[0].length === 0) { expression.lastIndex++; continue }
|
|
816
|
+
const range = rangeAt(group, match.index, match.index + match[0].length)
|
|
817
|
+
if (range !== null) ranges.push(range)
|
|
689
818
|
}
|
|
690
819
|
}
|
|
691
820
|
return ranges
|
|
@@ -718,16 +847,12 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
718
847
|
function phraseRange(root, snippet) {
|
|
719
848
|
if (root === null) return null
|
|
720
849
|
for (const phrase of phraseCandidates(snippet)) {
|
|
721
|
-
const
|
|
850
|
+
const expression = new RegExp(escapeRegExp(phrase), 'iu')
|
|
722
851
|
for (const group of inlineGroups(root)) {
|
|
723
|
-
const
|
|
724
|
-
if (
|
|
725
|
-
const
|
|
726
|
-
|
|
727
|
-
const range = document.createRange()
|
|
728
|
-
range.setStart(start[0], start[1])
|
|
729
|
-
range.setEnd(end[0], end[1])
|
|
730
|
-
return range
|
|
852
|
+
const match = expression.exec(group.text)
|
|
853
|
+
if (match === null) continue
|
|
854
|
+
const range = rangeAt(group, match.index, match.index + match[0].length)
|
|
855
|
+
if (range !== null) return range
|
|
731
856
|
}
|
|
732
857
|
}
|
|
733
858
|
return null
|
|
@@ -851,15 +976,26 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
851
976
|
if (flow === null) { showNotice(t('jumpFailed')); return false }
|
|
852
977
|
const needles = needlesFromQuery(query)
|
|
853
978
|
const finder = () => {
|
|
854
|
-
const
|
|
855
|
-
if (
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
979
|
+
const rows = findMessageRows(flow, hit.messageId, hit.nodeKey)
|
|
980
|
+
if (rows.length > 0) {
|
|
981
|
+
for (const row of rows) {
|
|
982
|
+
expandHidden(row)
|
|
983
|
+
const ranges = rangesForNeedles(row, needles)
|
|
984
|
+
if (ranges.length > 0) return { row, ranges }
|
|
985
|
+
const phrase = phraseRange(row, hit.snippet)
|
|
986
|
+
if (phrase !== null) {
|
|
987
|
+
// Keep the phrase range inside the anchored row instead of
|
|
988
|
+
// falling through to a wrong copy elsewhere in the transcript.
|
|
989
|
+
const inside = row.contains(phrase.commonAncestorContainer)
|
|
990
|
+
if (inside) return { row, ranges: [phrase] }
|
|
991
|
+
}
|
|
992
|
+
}
|
|
861
993
|
return null
|
|
862
994
|
}
|
|
995
|
+
// With an exact node key there is nothing safe to fall back to in the
|
|
996
|
+
// already loaded window; wait for older pages instead of landing on a
|
|
997
|
+
// similar phrase in a different message.
|
|
998
|
+
if (hit.nodeKey || hit.messageId) return null
|
|
863
999
|
const phrase = phraseRange(flow, hit.snippet)
|
|
864
1000
|
if (phrase === null) return null
|
|
865
1001
|
expandHidden(phrase.startContainer)
|
|
@@ -872,7 +1008,6 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
872
1008
|
let target = await waitFor(flow, finder, 6_000, 120)
|
|
873
1009
|
for (let attempt = 0; target === null && attempt < 10; attempt++) {
|
|
874
1010
|
if (stale()) return false
|
|
875
|
-
for (const hidden of flow.querySelectorAll('[hidden="until-found"]')) hidden.dispatchEvent(new Event('beforematch'))
|
|
876
1011
|
if (!revealOlder(scope)) break
|
|
877
1012
|
target = await waitFor(flow, finder, 1_500, 150)
|
|
878
1013
|
}
|
|
@@ -961,7 +1096,7 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
961
1096
|
const FIND_BLOCK_SELECTOR = 'p,li,pre,td,th,h1,h2,h3,h4,h5,h6,blockquote,dd,dt'
|
|
962
1097
|
const FIND_EXCLUDED = '[hidden]:not([hidden="until-found"]),[data-chat-history],[data-text-interaction-surface],'
|
|
963
1098
|
+ '[data-composer-seat],[data-text-interaction-exclude],script,style,button,input,textarea,select,'
|
|
964
|
-
+ '.sr-only,.katex-mathml,[aria-hidden="true"]:not(.katex-html)'
|
|
1099
|
+
+ '.sr-only,[class*="visuallyHidden"],[role="status"],[aria-live],.katex-mathml,[aria-hidden="true"]:not(.katex-html)'
|
|
965
1100
|
|
|
966
1101
|
function createFindStore() {
|
|
967
1102
|
const listeners = new Set()
|
|
@@ -1120,13 +1255,17 @@ self.onmessage = event => {
|
|
|
1120
1255
|
}
|
|
1121
1256
|
if (mode !== 'regex') source = escapeRegExp(source)
|
|
1122
1257
|
const flags = 'g' + (caseSensitive ? '' : 'i')
|
|
1258
|
+
// Prefer the Unicode flag so \p{...}, astral literals and modern syntax
|
|
1259
|
+
// behave as users expect. Fall back once for legacy patterns that the
|
|
1260
|
+
// unicode grammar rejects (for example some lone escapes).
|
|
1123
1261
|
try {
|
|
1124
|
-
return { source, flags:
|
|
1262
|
+
return { source, flags: flags + 'u', re: new RegExp(source, flags + 'u') }
|
|
1125
1263
|
} catch (error) {
|
|
1126
|
-
|
|
1127
|
-
|
|
1264
|
+
try {
|
|
1265
|
+
return { source, flags, re: new RegExp(source, flags) }
|
|
1266
|
+
} catch (fallbackError) {
|
|
1267
|
+
return { error: t('findInvalidRegex', { message: String(fallbackError && fallbackError.message || fallbackError).slice(0, 140) }) }
|
|
1128
1268
|
}
|
|
1129
|
-
return { error: t('findInvalidRegex', { message: String(error && error.message || error).slice(0, 140) }) }
|
|
1130
1269
|
}
|
|
1131
1270
|
}
|
|
1132
1271
|
|
|
@@ -1139,67 +1278,38 @@ self.onmessage = event => {
|
|
|
1139
1278
|
}
|
|
1140
1279
|
|
|
1141
1280
|
function collectFindGroups(root) {
|
|
1142
|
-
|
|
1143
|
-
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT)
|
|
1144
|
-
const groups = new Map()
|
|
1145
|
-
let node = walker.nextNode()
|
|
1146
|
-
while (node !== null) {
|
|
1147
|
-
const parent = node.parentElement
|
|
1148
|
-
if (parent !== null && parent.closest(FIND_EXCLUDED) === null) {
|
|
1149
|
-
const value = node.nodeValue || ''
|
|
1150
|
-
if (value !== '') {
|
|
1151
|
-
const block = parent.closest(FIND_BLOCK_SELECTOR)
|
|
1152
|
-
|| parent.closest('[data-text-interaction-source]')
|
|
1153
|
-
|| parent.closest('[data-chat-node-key]')
|
|
1154
|
-
|| root
|
|
1155
|
-
let group = groups.get(block)
|
|
1156
|
-
if (group === undefined) { group = { block, nodes: [], text: '', starts: [] }; groups.set(block, group) }
|
|
1157
|
-
group.starts.push(group.text.length)
|
|
1158
|
-
group.nodes.push(node)
|
|
1159
|
-
group.text += value
|
|
1160
|
-
}
|
|
1161
|
-
}
|
|
1162
|
-
node = walker.nextNode()
|
|
1163
|
-
}
|
|
1164
|
-
return [...groups.values()]
|
|
1281
|
+
return projectGroups(root, FIND_EXCLUDED)
|
|
1165
1282
|
}
|
|
1166
1283
|
|
|
1167
1284
|
function clipForGroup(group, range) {
|
|
1168
1285
|
if (range === null) return null
|
|
1169
|
-
let
|
|
1170
|
-
let
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
const length =
|
|
1286
|
+
let rawStart = null
|
|
1287
|
+
let rawEnd = null
|
|
1288
|
+
for (const entry of group.nodes) {
|
|
1289
|
+
const node = entry.node
|
|
1290
|
+
const length = entry.end - entry.start
|
|
1291
|
+
try {
|
|
1292
|
+
if (!range.intersectsNode(node)) continue
|
|
1293
|
+
} catch { continue }
|
|
1174
1294
|
let nodeStart = 0
|
|
1175
1295
|
let nodeEnd = length
|
|
1176
|
-
try {
|
|
1177
|
-
if (!range.intersectsNode(node)) { position += length; continue }
|
|
1178
|
-
} catch { position += length; continue }
|
|
1179
1296
|
if (node === range.startContainer) nodeStart = range.startOffset
|
|
1180
1297
|
if (node === range.endContainer) nodeEnd = range.endOffset
|
|
1181
1298
|
if (nodeStart < nodeEnd) {
|
|
1182
|
-
if (
|
|
1183
|
-
|
|
1299
|
+
if (rawStart === null) rawStart = entry.start + nodeStart
|
|
1300
|
+
rawEnd = entry.start + nodeEnd
|
|
1184
1301
|
}
|
|
1185
|
-
position += length
|
|
1186
1302
|
}
|
|
1187
|
-
if (
|
|
1303
|
+
if (rawStart === null || rawEnd === null) return null
|
|
1304
|
+
const start = rawToProjected(group, rawStart)
|
|
1305
|
+
const end = rawToProjected(group, rawEnd)
|
|
1306
|
+
if (start >= end) return null
|
|
1188
1307
|
return [start, end]
|
|
1189
1308
|
}
|
|
1190
1309
|
|
|
1191
1310
|
function rangeForGroupOffsets(group, start, end) {
|
|
1192
1311
|
if (group.nodes.length === 0) return null
|
|
1193
|
-
|
|
1194
|
-
const endOffset = offsetInGroup(group, end)
|
|
1195
|
-
const range = document.createRange()
|
|
1196
|
-
try {
|
|
1197
|
-
range.setStart(startOffset[0], startOffset[1])
|
|
1198
|
-
range.setEnd(endOffset[0], endOffset[1])
|
|
1199
|
-
return range
|
|
1200
|
-
} catch {
|
|
1201
|
-
return null
|
|
1202
|
-
}
|
|
1312
|
+
return rangeAt(group, start, end)
|
|
1203
1313
|
}
|
|
1204
1314
|
|
|
1205
1315
|
function findHistoryButton(scope) {
|
|
@@ -1227,6 +1337,8 @@ self.onmessage = event => {
|
|
|
1227
1337
|
const debounceRef = useRef(0)
|
|
1228
1338
|
const observerRef = useRef(null)
|
|
1229
1339
|
const revealTimersRef = useRef([])
|
|
1340
|
+
const revealFrameRef = useRef(0)
|
|
1341
|
+
const loadOlderTimerRef = useRef(0)
|
|
1230
1342
|
const returnFocusRef = useRef(null)
|
|
1231
1343
|
const [anchor, setAnchor] = useState(null)
|
|
1232
1344
|
const composingRef = useRef(false)
|
|
@@ -1248,7 +1360,9 @@ self.onmessage = event => {
|
|
|
1248
1360
|
if (!match.range.startContainer.isConnected) return
|
|
1249
1361
|
centerRange(match.range)
|
|
1250
1362
|
}
|
|
1251
|
-
|
|
1363
|
+
if (typeof cancelAnimationFrame === 'function') cancelAnimationFrame(revealFrameRef.current)
|
|
1364
|
+
revealFrameRef.current = typeof requestAnimationFrame === 'function' ? requestAnimationFrame(run) : 0
|
|
1365
|
+
if (revealFrameRef.current === 0) run()
|
|
1252
1366
|
for (const delay of [80, 240]) {
|
|
1253
1367
|
const timer = setTimeout(run, delay)
|
|
1254
1368
|
revealTimersRef.current.push(timer)
|
|
@@ -1302,7 +1416,11 @@ self.onmessage = event => {
|
|
|
1302
1416
|
let selectionRange = null
|
|
1303
1417
|
if (state.scope === 'selection' && state.selection) {
|
|
1304
1418
|
selectionRange = state.selection.range
|
|
1305
|
-
|
|
1419
|
+
let selectionDead = !selectionRange.startContainer.isConnected || selectionRange.collapsed
|
|
1420
|
+
if (!selectionDead) {
|
|
1421
|
+
try { selectionDead = selectionRange.toString().trim() === '' } catch { selectionDead = true }
|
|
1422
|
+
}
|
|
1423
|
+
if (selectionDead) {
|
|
1306
1424
|
findStore.set({ scope: 'session', selection: null, hint: t('findSelectionLost') })
|
|
1307
1425
|
return
|
|
1308
1426
|
}
|
|
@@ -1322,12 +1440,20 @@ self.onmessage = event => {
|
|
|
1322
1440
|
texts.push(text)
|
|
1323
1441
|
clips.push(clip === null ? [0, group.text.length] : clip)
|
|
1324
1442
|
}
|
|
1443
|
+
if (selectionRange !== null && clips.every(clip => clip === null)) {
|
|
1444
|
+
findStore.set({ scope: 'session', selection: null, hint: t('findSelectionLost') })
|
|
1445
|
+
return
|
|
1446
|
+
}
|
|
1325
1447
|
let rawMatches = null
|
|
1326
1448
|
let capped = false
|
|
1327
1449
|
if (state.mode === 'regex') {
|
|
1328
1450
|
try {
|
|
1329
1451
|
const result = await scanRegexInWorker(texts.filter(text => text !== null), compiled.source, compiled.flags)
|
|
1330
1452
|
if (token !== sequenceRef.current) return
|
|
1453
|
+
if (result === null) {
|
|
1454
|
+
findStore.set({ count: 0, index: -1, capped: false, status: 'error', error: t('findRegexUnavailable') })
|
|
1455
|
+
return
|
|
1456
|
+
}
|
|
1331
1457
|
if (result !== null) {
|
|
1332
1458
|
if (result.timeout === true) {
|
|
1333
1459
|
matchesRef.current = []
|
|
@@ -1438,10 +1564,11 @@ self.onmessage = event => {
|
|
|
1438
1564
|
debounceRef.current = setTimeout(() => { if (findStore.get().open) void runScanRef.current(true) }, 400)
|
|
1439
1565
|
}
|
|
1440
1566
|
const scope = scopeRef.current
|
|
1441
|
-
|
|
1567
|
+
const observed = scope === null ? null : (chatFlow(scope) || scope)
|
|
1568
|
+
if (observed !== null) {
|
|
1442
1569
|
try {
|
|
1443
1570
|
observer = new MutationObserver(schedule)
|
|
1444
|
-
observer.observe(
|
|
1571
|
+
observer.observe(observed, { childList: true, subtree: true, characterData: true })
|
|
1445
1572
|
} catch { /* observer unavailable */ }
|
|
1446
1573
|
}
|
|
1447
1574
|
return () => {
|
|
@@ -1451,6 +1578,8 @@ self.onmessage = event => {
|
|
|
1451
1578
|
if (observer !== null) observer.disconnect()
|
|
1452
1579
|
for (const id of revealTimersRef.current) clearTimeout(id)
|
|
1453
1580
|
revealTimersRef.current = []
|
|
1581
|
+
if (typeof cancelAnimationFrame === 'function') cancelAnimationFrame(revealFrameRef.current)
|
|
1582
|
+
clearTimeout(loadOlderTimerRef.current)
|
|
1454
1583
|
sequenceRef.current++
|
|
1455
1584
|
clearFindHighlight('find')
|
|
1456
1585
|
setFindScope(null)
|
|
@@ -1495,7 +1624,10 @@ self.onmessage = event => {
|
|
|
1495
1624
|
if (text !== '') { event.preventDefault(); copyText(text).then(() => findStore.set({ hint: t('findCopied') })).catch(() => {}) }
|
|
1496
1625
|
}
|
|
1497
1626
|
}
|
|
1498
|
-
const pickMode = event => {
|
|
1627
|
+
const pickMode = event => {
|
|
1628
|
+
findStore.set({ mode: event.target.value })
|
|
1629
|
+
if (inputRef.current !== null) inputRef.current.focus()
|
|
1630
|
+
}
|
|
1499
1631
|
const toggle = key => { findStore.set({ [key]: !findStore.get()[key] }) }
|
|
1500
1632
|
const toggleScope = () => {
|
|
1501
1633
|
if (state.scope === 'selection') findStore.set({ scope: 'session', selection: null, hint: '' })
|
|
@@ -1513,7 +1645,8 @@ self.onmessage = event => {
|
|
|
1513
1645
|
const loadOlder = () => {
|
|
1514
1646
|
if (scopeRef.current === null || !revealOlder(scopeRef.current)) return
|
|
1515
1647
|
findStore.set({ hint: t('findLoadingOlder') })
|
|
1516
|
-
|
|
1648
|
+
clearTimeout(loadOlderTimerRef.current)
|
|
1649
|
+
loadOlderTimerRef.current = setTimeout(() => { if (findStore.get().open) void runScanRef.current(true) }, 700)
|
|
1517
1650
|
}
|
|
1518
1651
|
return h('div', {
|
|
1519
1652
|
ref: barRef,
|
|
@@ -1551,7 +1684,10 @@ self.onmessage = event => {
|
|
|
1551
1684
|
h('button', { className: 'dsh-locator-findbar-btn', title: t('findClose'), 'aria-label': t('findClose'),
|
|
1552
1685
|
onClick: () => closeFind() }, '×'),
|
|
1553
1686
|
),
|
|
1554
|
-
h('div', { className: 'dsh-locator-findbar-opts'
|
|
1687
|
+
h('div', { className: 'dsh-locator-findbar-opts', onMouseDown: event => {
|
|
1688
|
+
if (event.target instanceof HTMLSelectElement) return
|
|
1689
|
+
event.preventDefault()
|
|
1690
|
+
} },
|
|
1555
1691
|
h('label', { className: 'dsh-locator-findbar-label' }, t('findMode'),
|
|
1556
1692
|
h('select', { value: state.mode, onChange: pickMode, 'aria-label': t('findMode') },
|
|
1557
1693
|
h('option', { value: 'normal' }, t('modeNormal')),
|
|
@@ -1613,6 +1749,8 @@ self.onmessage = event => {
|
|
|
1613
1749
|
const panelRef = useRef(null)
|
|
1614
1750
|
const abortRef = useRef(null)
|
|
1615
1751
|
const listRef = useRef(null)
|
|
1752
|
+
const toastTimerRef = useRef(0)
|
|
1753
|
+
const insertTimerRef = useRef(0)
|
|
1616
1754
|
|
|
1617
1755
|
const close = useCallback(() => store.set(false), [])
|
|
1618
1756
|
const activeScope = conversationScope('')
|
|
@@ -1628,7 +1766,9 @@ self.onmessage = event => {
|
|
|
1628
1766
|
|
|
1629
1767
|
useEffect(() => {
|
|
1630
1768
|
if (!open) return undefined
|
|
1769
|
+
if (sessionOnly && activeSessionId === '') setSessionOnly(false)
|
|
1631
1770
|
closeFind()
|
|
1771
|
+
setGroups(null)
|
|
1632
1772
|
const previous = document.activeElement
|
|
1633
1773
|
setError('')
|
|
1634
1774
|
const timer = setTimeout(() => {
|
|
@@ -1639,9 +1779,24 @@ self.onmessage = event => {
|
|
|
1639
1779
|
}, 30)
|
|
1640
1780
|
return () => {
|
|
1641
1781
|
clearTimeout(timer)
|
|
1782
|
+
if (abortRef.current !== null) { abortRef.current.abort(); abortRef.current = null }
|
|
1783
|
+
setGroups(null)
|
|
1642
1784
|
if (previous instanceof HTMLElement && previous.isConnected) previous.focus()
|
|
1643
1785
|
}
|
|
1644
|
-
}, [open])
|
|
1786
|
+
}, [open, sessionOnly, activeSessionId])
|
|
1787
|
+
|
|
1788
|
+
useEffect(() => {
|
|
1789
|
+
if (!open) return undefined
|
|
1790
|
+
const onDocumentKey = event => {
|
|
1791
|
+
if (event.key !== 'Escape' || event.defaultPrevented) return
|
|
1792
|
+
const target = event.target
|
|
1793
|
+
if (target instanceof Element && target.closest('.dsh-locator-panel') !== null) return
|
|
1794
|
+
event.preventDefault()
|
|
1795
|
+
close()
|
|
1796
|
+
}
|
|
1797
|
+
document.addEventListener('keydown', onDocumentKey, true)
|
|
1798
|
+
return () => document.removeEventListener('keydown', onDocumentKey, true)
|
|
1799
|
+
}, [open, close])
|
|
1645
1800
|
|
|
1646
1801
|
useEffect(() => {
|
|
1647
1802
|
if (!open) return undefined
|
|
@@ -1742,7 +1897,12 @@ self.onmessage = event => {
|
|
|
1742
1897
|
|
|
1743
1898
|
const notify = useCallback(message => {
|
|
1744
1899
|
setToast(message)
|
|
1745
|
-
|
|
1900
|
+
clearTimeout(toastTimerRef.current)
|
|
1901
|
+
toastTimerRef.current = setTimeout(() => setToast(''), 1600)
|
|
1902
|
+
}, [])
|
|
1903
|
+
useEffect(() => () => {
|
|
1904
|
+
clearTimeout(toastTimerRef.current)
|
|
1905
|
+
clearTimeout(insertTimerRef.current)
|
|
1746
1906
|
}, [])
|
|
1747
1907
|
|
|
1748
1908
|
const copy = useCallback(hit => {
|
|
@@ -1774,7 +1934,8 @@ self.onmessage = event => {
|
|
|
1774
1934
|
if (!hit || hit.kind === 'session' || loading) return
|
|
1775
1935
|
const mention = '@"' + hit.path.replace(/"/g, '') + '" '
|
|
1776
1936
|
close()
|
|
1777
|
-
|
|
1937
|
+
clearTimeout(insertTimerRef.current)
|
|
1938
|
+
insertTimerRef.current = setTimeout(() => {
|
|
1778
1939
|
if (insertIntoComposer(mention)) notify(t('inserted'))
|
|
1779
1940
|
else copyText(mention).then(() => notify(t('insertFailed'))).catch(() => {})
|
|
1780
1941
|
}, 80)
|
|
@@ -1806,7 +1967,8 @@ self.onmessage = event => {
|
|
|
1806
1967
|
&& (event.target.tagName === 'BUTTON' || event.target.tagName === 'SELECT'
|
|
1807
1968
|
|| (event.target.tagName === 'INPUT' && event.target.type !== 'text'))
|
|
1808
1969
|
if (interactive) return
|
|
1809
|
-
const
|
|
1970
|
+
const altGraph = typeof event.getModifierState === 'function' && event.getModifierState('AltGraph')
|
|
1971
|
+
const scopeDigit = event.altKey && !event.ctrlKey && !event.metaKey && !altGraph
|
|
1810
1972
|
? (event.code === 'Digit1' ? '1' : event.code === 'Digit2' ? '2' : event.code === 'Digit3' ? '3' : ['1', '2', '3'].includes(event.key) ? event.key : '')
|
|
1811
1973
|
: ''
|
|
1812
1974
|
if (scopeDigit !== '') {
|
|
@@ -1908,6 +2070,7 @@ self.onmessage = event => {
|
|
|
1908
2070
|
const groupHead = (key, label, count, note) => h('div', {
|
|
1909
2071
|
className: 'dsh-locator-grouphead',
|
|
1910
2072
|
id: 'dsh-locator-group-' + key,
|
|
2073
|
+
onMouseDown: event => event.preventDefault(),
|
|
1911
2074
|
},
|
|
1912
2075
|
h('span', null, label),
|
|
1913
2076
|
h('span', { className: 'dsh-locator-count' }, String(count)),
|
|
@@ -1934,6 +2097,7 @@ self.onmessage = event => {
|
|
|
1934
2097
|
className: 'dsh-locator-row' + (isContent ? ' dsh-locator-rowcontent' : '') + (index === selected ? ' sel' : ''),
|
|
1935
2098
|
title: hit.path,
|
|
1936
2099
|
onClick: () => setSelected(index),
|
|
2100
|
+
onMouseDown: event => event.preventDefault(),
|
|
1937
2101
|
onDoubleClick: () => activate(hit),
|
|
1938
2102
|
},
|
|
1939
2103
|
h('div', { className: 'dsh-locator-title' },
|
|
@@ -1948,32 +2112,46 @@ self.onmessage = event => {
|
|
|
1948
2112
|
)
|
|
1949
2113
|
}
|
|
1950
2114
|
|
|
2115
|
+
const contentErrorText = code => {
|
|
2116
|
+
if (code === 'content-regex-not-supported') return t('panelRegexPaused')
|
|
2117
|
+
if (code === 'content-query-too-short') return t('contentTooShort')
|
|
2118
|
+
if (code === 'content-search-timeout') return t('contentTimeout')
|
|
2119
|
+
if (code === 'invalid-control-char') return t('contentBadChars')
|
|
2120
|
+
return code
|
|
2121
|
+
}
|
|
1951
2122
|
let index = -1
|
|
1952
2123
|
const renderGroup = (key, label, group, extra) => {
|
|
1953
2124
|
if (!scopes[key] || group === undefined) return null
|
|
1954
2125
|
const hits = Array.isArray(group.hits) ? group.hits : []
|
|
1955
2126
|
const count = typeof group.returned === 'number' ? group.returned : hits.length
|
|
2127
|
+
const warnings = Array.isArray(group.warnings) && group.warnings.length > 0
|
|
2128
|
+
? t('contentWarning', { list: group.warnings.join(', ') })
|
|
2129
|
+
: null
|
|
2130
|
+
const budget = key === 'content' && group.truncated === true ? t('contentBudget') : null
|
|
1956
2131
|
const note = key === 'content'
|
|
1957
2132
|
? (matchMode === 'regex' ? t('panelRegexPaused')
|
|
1958
2133
|
: contentBuilding ? t('contentBuilding', { files: content?.files ?? 0, messages: content?.messages ?? 0 })
|
|
1959
2134
|
: (content?.capped ? t('contentCapped', { n: count }) : (query.trim() !== '' ? t('relevance') : null)))
|
|
1960
2135
|
: (group.deduped > 0 ? t('deduped', { n: group.deduped }) : (group.truncated ? t('showing', { n: hits.length }) : null))
|
|
2136
|
+
const combinedNote = [note, warnings, budget].filter(value => value !== null && value !== '').join(' · ')
|
|
1961
2137
|
if (typeof group.error === 'string' && group.error !== '') {
|
|
1962
|
-
const
|
|
2138
|
+
const informational = ['content-regex-not-supported', 'content-query-too-short', 'invalid-control-char'].includes(group.error)
|
|
2139
|
+
const message = contentErrorText(group.error)
|
|
1963
2140
|
return h('div', { className: 'dsh-locator-group', role: 'group', 'aria-labelledby': 'dsh-locator-group-' + key },
|
|
1964
|
-
groupHead(key, label, count, null),
|
|
1965
|
-
h('div', { className: 'dsh-locator-groupempty', role: '
|
|
1966
|
-
|
|
2141
|
+
groupHead(key, label, countLabel(group, count), combinedNote === '' ? null : combinedNote),
|
|
2142
|
+
h('div', { className: 'dsh-locator-groupempty', role: informational ? 'status' : 'alert' },
|
|
2143
|
+
informational ? message : t('errorPrefix') + message,
|
|
2144
|
+
informational ? null : h('button', { className: 'dsh-locator-dirbtn dsh-locator-retry', onClick: () => setRefreshTick(v => v + 1) }, t('retry'))),
|
|
1967
2145
|
)
|
|
1968
2146
|
}
|
|
1969
2147
|
return h('div', { className: 'dsh-locator-group', role: 'group', 'aria-labelledby': 'dsh-locator-group-' + key },
|
|
1970
|
-
groupHead(key, label, countLabel(group, count),
|
|
2148
|
+
groupHead(key, label, countLabel(group, count), combinedNote === '' ? null : combinedNote),
|
|
1971
2149
|
extra === undefined ? null : extra,
|
|
1972
2150
|
hits.length === 0
|
|
1973
2151
|
? h('div', { className: 'dsh-locator-groupempty' },
|
|
1974
2152
|
key === 'content' && scopes.content
|
|
1975
|
-
? (sessionOnly && query.trim().length >=
|
|
1976
|
-
: query.trim().length <
|
|
2153
|
+
? (sessionOnly && query.trim().length >= 3 ? t('panelSessionHint')
|
|
2154
|
+
: query.trim().length < 3 ? t('contentShort') : (contentBuilding ? t('loading') : t('emptyResults')))
|
|
1977
2155
|
: (query.trim() === '' ? t('emptyQuery') : t('emptyResults')))
|
|
1978
2156
|
: hits.map(hit => { index++; return rowFor(key, hit, index) }),
|
|
1979
2157
|
)
|
|
@@ -2077,7 +2255,12 @@ self.onmessage = event => {
|
|
|
2077
2255
|
],
|
|
2078
2256
|
),
|
|
2079
2257
|
h('div', { className: 'dsh-locator-foot' },
|
|
2080
|
-
h('span', null, (loading ? t('loading') + ' · ' : '') + t('results', {
|
|
2258
|
+
h('span', null, (loading ? t('loading') + ' · ' : '') + t('results', {
|
|
2259
|
+
content: contentCountText || '0',
|
|
2260
|
+
files: filesCountText || '0',
|
|
2261
|
+
dirs: dirsCountText || '0',
|
|
2262
|
+
ms: took.toFixed(1),
|
|
2263
|
+
})
|
|
2081
2264
|
+ (status && status.scanning ? ' · ' + t('indexing') : '')
|
|
2082
2265
|
+ (contentBuilding ? ' · ' + t('indexing') : '')),
|
|
2083
2266
|
h('span', { className: 'grow' }),
|
|
@@ -2113,12 +2296,16 @@ self.onmessage = event => {
|
|
|
2113
2296
|
}, 'dsh-locator: styles')
|
|
2114
2297
|
ctx.effect(() => {
|
|
2115
2298
|
const onKey = event => {
|
|
2116
|
-
if (
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2299
|
+
if (event.defaultPrevented || event.isComposing || event.repeat) return
|
|
2300
|
+
if (!((event.metaKey || event.ctrlKey) && event.shiftKey && String(event.key).toLowerCase() === 'f')) return
|
|
2301
|
+
const target = event.target
|
|
2302
|
+
if (target instanceof Element && target.closest('.xterm,.monaco-editor,[data-code-editor]') !== null) return
|
|
2303
|
+
const foreignModal = document.querySelector('[role="dialog"][aria-modal="true"]:not(.dsh-locator-panel)')
|
|
2304
|
+
if (foreignModal !== null) return
|
|
2305
|
+
event.preventDefault()
|
|
2306
|
+
event.stopPropagation()
|
|
2307
|
+
closeFind()
|
|
2308
|
+
store.toggle()
|
|
2122
2309
|
}
|
|
2123
2310
|
document.addEventListener('keydown', onKey, true)
|
|
2124
2311
|
return () => document.removeEventListener('keydown', onKey, true)
|
|
@@ -2162,7 +2349,11 @@ self.onmessage = event => {
|
|
|
2162
2349
|
document.addEventListener('keydown', onFindKey, true)
|
|
2163
2350
|
return () => document.removeEventListener('keydown', onFindKey, true)
|
|
2164
2351
|
}, 'dsh-locator: find hotkeys')
|
|
2165
|
-
ctx.effect(() => () => {
|
|
2352
|
+
ctx.effect(() => () => {
|
|
2353
|
+
closeFind()
|
|
2354
|
+
cancelJump()
|
|
2355
|
+
store.set(false)
|
|
2356
|
+
}, 'dsh-locator: find cleanup')
|
|
2166
2357
|
try {
|
|
2167
2358
|
ctx.slots.inject('shell.overlay', () => {
|
|
2168
2359
|
const disposers = [
|