dsh-locator 0.9.0 → 0.10.1
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 +42 -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 +344 -146
- 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 +274 -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/runtime.d.ts +7 -0
- package/lib/runtime.d.ts.map +1 -1
- package/lib/runtime.js +4 -0
- package/lib/runtime.js.map +1 -1
- package/lib/tools.d.ts +30 -0
- package/lib/tools.d.ts.map +1 -1
- package/lib/tools.js +72 -4
- 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 +264 -81
- package/src/reveal.ts +148 -10
- package/src/runtime.ts +11 -0
- package/src/tools.ts +93 -7
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,7 +226,9 @@ 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',
|
|
231
|
+
watchDegraded: 'watcher degraded; periodic refresh active',
|
|
230
232
|
},
|
|
231
233
|
zh: {
|
|
232
234
|
scopeContent: '内容', scopeFiles: '文件', scopeDirs: '目录',
|
|
@@ -236,7 +238,7 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
236
238
|
asc: '升序', descOrder: '降序',
|
|
237
239
|
groupContent: '内容', groupFiles: '文件', groupDirs: '目录',
|
|
238
240
|
relevance: '按相关度', contentBuilding: '索引构建中… {files} 文件 / {messages} 消息',
|
|
239
|
-
contentShort: '输入至少
|
|
241
|
+
contentShort: '输入至少 3 个字符后搜索正文与会话(更短的词请用 Ctrl+F)',
|
|
240
242
|
contentDisabled: '内容索引已关闭(contentIndex: false)',
|
|
241
243
|
contentCapped: '前 {n} 条', showing: '前 {n} 条', deduped: '{n} 条文件命中已并入内容', loading: '搜索中…',
|
|
242
244
|
emptyQuery: '输入关键词:文件名、路径、正文或会话内容',
|
|
@@ -260,7 +262,7 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
260
262
|
findScope: '仅在选区内', findScopeChip: '选区 {n} 字', findSessionChip: '本会话',
|
|
261
263
|
findNoMatch: '没有匹配', findEmpty: '输入关键词后在此会话中查找',
|
|
262
264
|
findInvalidRegex: '正则无效:{message}', findInvalidEscape: '扩展转义无效:{token}',
|
|
263
|
-
findTimeout: '正则执行超时,请简化表达式', findWrappedTop: '已回到顶部',
|
|
265
|
+
findTimeout: '正则执行超时,请简化表达式', findRegexUnavailable: '此处无法运行正则(worker 被阻止)', findWrappedTop: '已回到顶部',
|
|
264
266
|
findWrappedBottom: '已到底部,回到开头', findNoWrapEnd: '已到最后一个匹配(循环查找已关闭)',
|
|
265
267
|
findNoWrapStart: '已到第一个匹配(循环查找已关闭)', findOlder: '加载更早消息后重试',
|
|
266
268
|
findLoadingOlder: '正在加载更早消息…', findLoadedOnly: '仅已加载窗口 · 更早历史未加载',
|
|
@@ -270,7 +272,9 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
270
272
|
panelTotal: '已选 {index}/{total}', panelRangeAll: '全部索引', panelRangeSession: '当前会话',
|
|
271
273
|
panelSessionHint: '此范围只查已索引的会话消息;实时查找请按 Ctrl+F',
|
|
272
274
|
panelRegexPaused: '正则模式下内容范围暂停', panelCountBounded: '≥ {n}', panelCountExact: '{n}',
|
|
275
|
+
contentWarning: '索引警告:{list}', contentBudget: '内容索引已达预算上限,部分文件未被收录', contentTooShort: '内容搜索至少需要 3 个字符(更短的词请用 Ctrl+F 在会话内查找)', contentTimeout: '内容搜索超时,请缩小关键词或范围', contentBadChars: '内容搜索不支持 NUL/控制字符',
|
|
273
276
|
panelCaseShort: 'Aa', panelWholeShort: '全词', panelModeLabel: '模式',
|
|
277
|
+
watchDegraded: '文件监听降级,正在周期刷新',
|
|
274
278
|
},
|
|
275
279
|
es: {
|
|
276
280
|
scopeContent: 'Contenido', scopeFiles: 'Archivos', scopeDirs: 'Carpetas',
|
|
@@ -280,7 +284,7 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
280
284
|
asc: 'Ascendente', descOrder: 'Descendente',
|
|
281
285
|
groupContent: 'Contenido', groupFiles: 'Archivos', groupDirs: 'Carpetas',
|
|
282
286
|
relevance: 'por relevancia', contentBuilding: 'indexando… {files} archivos / {messages} mensajes',
|
|
283
|
-
contentShort: 'Escribe al menos
|
|
287
|
+
contentShort: 'Escribe al menos 3 caracteres para buscar contenido y sesiones (Ctrl+F para consultas cortas)',
|
|
284
288
|
contentDisabled: 'Índice de contenido desactivado (contentIndex: false)',
|
|
285
289
|
contentCapped: 'primeros {n}', showing: 'primeros {n}', deduped: '{n} aciertos de nombre fusionados', loading: 'Buscando…',
|
|
286
290
|
emptyQuery: 'Escribe para buscar nombres, rutas, contenido y sesiones',
|
|
@@ -304,7 +308,7 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
304
308
|
findScope: 'En la selección', findScopeChip: 'selección {n} caracteres', findSessionChip: 'esta conversación',
|
|
305
309
|
findNoMatch: 'Sin coincidencias', findEmpty: 'Escribe para buscar en esta conversación',
|
|
306
310
|
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',
|
|
311
|
+
findTimeout: 'La regex agotó el tiempo; simplifícala', findRegexUnavailable: 'La búsqueda regex no está disponible aquí (worker bloqueado)', findWrappedTop: 'Vuelto al inicio',
|
|
308
312
|
findWrappedBottom: 'Vuelto al final', findNoWrapEnd: 'Última coincidencia (continuación desactivada)',
|
|
309
313
|
findNoWrapStart: 'Primera coincidencia (continuación desactivada)', findOlder: 'Cargar mensajes anteriores y reintentar',
|
|
310
314
|
findLoadingOlder: 'Cargando mensajes anteriores…', findLoadedOnly: 'solo ventana cargada · historial anterior sin cargar',
|
|
@@ -314,7 +318,9 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
314
318
|
panelTotal: 'seleccionado {index}/{total}', panelRangeAll: 'Todo el índice', panelRangeSession: 'Esta conversación',
|
|
315
319
|
panelSessionHint: 'Este ámbito solo busca mensajes indexados; pulsa Ctrl+F para buscar en vivo',
|
|
316
320
|
panelRegexPaused: 'Ámbito de contenido en pausa en modo regex', panelCountBounded: '≥ {n}', panelCountExact: '{n}',
|
|
321
|
+
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
322
|
panelCaseShort: 'Aa', panelWholeShort: 'Palabra', panelModeLabel: 'Modo',
|
|
323
|
+
watchDegraded: 'vigilante degradado; refresco periódico activo',
|
|
318
324
|
},
|
|
319
325
|
pt: {
|
|
320
326
|
scopeContent: 'Conteúdo', scopeFiles: 'Arquivos', scopeDirs: 'Pastas',
|
|
@@ -324,7 +330,7 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
324
330
|
asc: 'Ascendente', descOrder: 'Descendente',
|
|
325
331
|
groupContent: 'Conteúdo', groupFiles: 'Arquivos', groupDirs: 'Pastas',
|
|
326
332
|
relevance: 'por relevância', contentBuilding: 'indexando… {files} arquivos / {messages} mensagens',
|
|
327
|
-
contentShort: 'Digite pelo menos
|
|
333
|
+
contentShort: 'Digite pelo menos 3 caracteres para buscar conteúdo e sessões (Ctrl+F para consultas curtas)',
|
|
328
334
|
contentDisabled: 'Índice de conteúdo desativado (contentIndex: false)',
|
|
329
335
|
contentCapped: 'primeiros {n}', showing: 'primeiros {n}', deduped: '{n} acertos de nome mesclados', loading: 'Buscando…',
|
|
330
336
|
emptyQuery: 'Digite para buscar nomes, caminhos, conteúdo e sessões',
|
|
@@ -348,7 +354,7 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
348
354
|
findScope: 'Na seleção', findScopeChip: 'seleção {n} caracteres', findSessionChip: 'esta conversa',
|
|
349
355
|
findNoMatch: 'Sem resultados', findEmpty: 'Digite para buscar nesta conversa',
|
|
350
356
|
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',
|
|
357
|
+
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
358
|
findWrappedBottom: 'Voltou ao fim', findNoWrapEnd: 'Última ocorrência (continuação desativada)',
|
|
353
359
|
findNoWrapStart: 'Primeira ocorrência (continuação desativada)', findOlder: 'Carregar mensagens anteriores e tentar novamente',
|
|
354
360
|
findLoadingOlder: 'Carregando mensagens anteriores…', findLoadedOnly: 'apenas janela carregada · histórico anterior não carregado',
|
|
@@ -358,7 +364,9 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
358
364
|
panelTotal: 'selecionado {index}/{total}', panelRangeAll: 'Todo o índice', panelRangeSession: 'Esta conversa',
|
|
359
365
|
panelSessionHint: 'Este escopo busca apenas mensagens indexadas; use Ctrl+F para buscar ao vivo',
|
|
360
366
|
panelRegexPaused: 'Escopo de conteúdo pausado no modo regex', panelCountBounded: '≥ {n}', panelCountExact: '{n}',
|
|
367
|
+
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
368
|
panelCaseShort: 'Aa', panelWholeShort: 'Palavra', panelModeLabel: 'Modo',
|
|
369
|
+
watchDegraded: 'watcher degradado; atualização periódica ativa',
|
|
362
370
|
},
|
|
363
371
|
hi: {
|
|
364
372
|
scopeContent: 'सामग्री', scopeFiles: 'फ़ाइलें', scopeDirs: 'फ़ोल्डर',
|
|
@@ -368,7 +376,7 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
368
376
|
asc: 'आरोही', descOrder: 'अवरोही',
|
|
369
377
|
groupContent: 'सामग्री', groupFiles: 'फ़ाइलें', groupDirs: 'फ़ोल्डर',
|
|
370
378
|
relevance: 'प्रासंगिकता से', contentBuilding: 'इंडेक्स बन रहा है… {files} फ़ाइलें / {messages} संदेश',
|
|
371
|
-
contentShort: 'सामग्री और सत्र खोजने के लिए कम से कम
|
|
379
|
+
contentShort: 'सामग्री और सत्र खोजने के लिए कम से कम 3 अक्षर लिखें (छोटी खोज के लिए Ctrl+F)',
|
|
372
380
|
contentDisabled: 'सामग्री इंडेक्स बंद है (contentIndex: false)',
|
|
373
381
|
contentCapped: 'पहले {n}', showing: 'पहले {n}', deduped: '{n} नाम-परिणाम सामग्री में मिलाए', loading: 'खोज रहे हैं…',
|
|
374
382
|
emptyQuery: 'नाम, पथ, सामग्री और सत्र खोजने के लिए लिखें',
|
|
@@ -392,7 +400,7 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
392
400
|
findScope: 'चयन में', findScopeChip: 'चयन {n} अक्षर', findSessionChip: 'यह बातचीत',
|
|
393
401
|
findNoMatch: 'कोई मेल नहीं', findEmpty: 'इस बातचीत में खोजने के लिए लिखें',
|
|
394
402
|
findInvalidRegex: 'अमान्य रेगुलर एक्सप्रेशन: {message}', findInvalidEscape: 'अमान्य विस्तृत एस्केप: {token}',
|
|
395
|
-
findTimeout: 'रेगेक्स का समय समाप्त; सरल बनाएँ', findWrappedTop: 'शीर्ष पर लौटा',
|
|
403
|
+
findTimeout: 'रेगेक्स का समय समाप्त; सरल बनाएँ', findRegexUnavailable: 'यहाँ रेगेक्स उपलब्ध नहीं है (वर्कर अवरुद्ध)', findWrappedTop: 'शीर्ष पर लौटा',
|
|
396
404
|
findWrappedBottom: 'अंत पर लौटा', findNoWrapEnd: 'अंतिम मेल (चक्र बंद)',
|
|
397
405
|
findNoWrapStart: 'पहला मेल (चक्र बंद)', findOlder: 'पुराने संदेश लोड कर पुन: प्रयास',
|
|
398
406
|
findLoadingOlder: 'पुराने संदेश लोड हो रहे हैं…', findLoadedOnly: 'केवल लोडेड विंडो · पुराना इतिहास लोड नहीं',
|
|
@@ -402,7 +410,9 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
402
410
|
panelTotal: 'चयनित {index}/{total}', panelRangeAll: 'पूरा इंडेक्स', panelRangeSession: 'यह बातचीत',
|
|
403
411
|
panelSessionHint: 'यह क्षेत्र केवल इंडेक्स किए संदेश खोजता है; लाइव खोज के लिए Ctrl+F दबाएँ',
|
|
404
412
|
panelRegexPaused: 'रेगेक्स मोड में सामग्री क्षेत्र रुका है', panelCountBounded: '≥ {n}', panelCountExact: '{n}',
|
|
413
|
+
contentWarning: 'इंडेक्स चेतावनी: {list}', contentBudget: 'सामग्री इंडेक्स बजट पूरा; कुछ फ़ाइलें इंडेक्स नहीं हैं', contentTooShort: 'सामग्री के लिए कम से कम 3 अक्षर चाहिए (छोटी खोज के लिए Ctrl+F)', contentTimeout: 'सामग्री खोज का समय समाप्त; क्वेरी या क्षेत्र घटाएँ', contentBadChars: 'सामग्री खोज NUL/नियंत्रण अक्षर नहीं लेती',
|
|
405
414
|
panelCaseShort: 'Aa', panelWholeShort: 'शब्द', panelModeLabel: 'मोड',
|
|
415
|
+
watchDegraded: 'वॉचर क्षीण; आवधिक रिफ्रेश सक्रिय',
|
|
406
416
|
},
|
|
407
417
|
}
|
|
408
418
|
const LANG = (() => {
|
|
@@ -540,7 +550,8 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
540
550
|
const HL_BASE = 'dsh-locator-find'
|
|
541
551
|
const HL_ACTIVE = 'dsh-locator-find-active'
|
|
542
552
|
const GESTURES = ['wheel', 'touchstart', 'pointerdown', 'keydown']
|
|
543
|
-
const EXCLUDED = '[hidden],[data-chat-history],[data-text-interaction-surface],[data-composer-seat],script,style'
|
|
553
|
+
const EXCLUDED = '[hidden],[data-chat-history],[data-text-interaction-surface],[data-composer-seat],script,style,'
|
|
554
|
+
+ '.sr-only,[class*="visuallyHidden"],[role="status"],[aria-live]'
|
|
544
555
|
const BLOCK_SELECTOR = 'p,li,pre,td,th,h1,h2,h3,h4,h5,h6,blockquote,dd,dt'
|
|
545
556
|
const legacyTargets = new Set()
|
|
546
557
|
|
|
@@ -611,21 +622,28 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
611
622
|
return scope === null ? null : scope.querySelector('[data-chat-flow]')
|
|
612
623
|
}
|
|
613
624
|
|
|
614
|
-
function
|
|
615
|
-
if (flow === null) return
|
|
625
|
+
function findMessageRows(flow, messageId, nodeKey) {
|
|
626
|
+
if (flow === null) return []
|
|
616
627
|
const rows = flow.querySelectorAll('[data-chat-node-key]')
|
|
628
|
+
const exact = []
|
|
617
629
|
if (nodeKey) {
|
|
618
630
|
for (const row of rows) {
|
|
619
|
-
if (row.getAttribute('data-chat-node-key') === String(nodeKey))
|
|
631
|
+
if (row.getAttribute('data-chat-node-key') === String(nodeKey)) exact.push(row)
|
|
620
632
|
}
|
|
621
633
|
}
|
|
622
|
-
if (
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
634
|
+
if (exact.length === 0 && messageId) {
|
|
635
|
+
const id = String(messageId)
|
|
636
|
+
for (const row of rows) {
|
|
637
|
+
const key = row.getAttribute('data-chat-node-key') || ''
|
|
638
|
+
if (key === id || key.endsWith(id)) exact.push(row)
|
|
639
|
+
}
|
|
627
640
|
}
|
|
628
|
-
|
|
641
|
+
// A single assistant step can be rendered as a reasoning row and a
|
|
642
|
+
// response row under the same node key; prefer the response row.
|
|
643
|
+
return exact.sort((left, right) => {
|
|
644
|
+
const score = row => row.getAttribute('data-chat-group-part') === 'response' ? 0 : 1
|
|
645
|
+
return score(left) - score(right)
|
|
646
|
+
})
|
|
629
647
|
}
|
|
630
648
|
|
|
631
649
|
function needlesFromQuery(query) {
|
|
@@ -635,57 +653,173 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
635
653
|
return [...new Set(words)].sort((a, b) => b.length - a.length).slice(0, 8)
|
|
636
654
|
}
|
|
637
655
|
|
|
638
|
-
function
|
|
639
|
-
|
|
656
|
+
function projectGroups(root, excluded) {
|
|
657
|
+
if (root === null) return []
|
|
640
658
|
const groups = new Map()
|
|
659
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT, {
|
|
660
|
+
acceptNode(node) {
|
|
661
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
662
|
+
if (node.matches(excluded)) return NodeFilter.FILTER_REJECT
|
|
663
|
+
if (node.tagName === 'BR') return NodeFilter.FILTER_ACCEPT
|
|
664
|
+
return NodeFilter.FILTER_SKIP
|
|
665
|
+
}
|
|
666
|
+
return NodeFilter.FILTER_ACCEPT
|
|
667
|
+
},
|
|
668
|
+
})
|
|
669
|
+
const groupFor = block => {
|
|
670
|
+
let group = groups.get(block)
|
|
671
|
+
if (group === undefined) {
|
|
672
|
+
group = {
|
|
673
|
+
block, raw: '', rawLength: 0, text: '', runs: [], nodes: [], breaks: [],
|
|
674
|
+
collapse: !(block instanceof Element && block.tagName === 'PRE'),
|
|
675
|
+
}
|
|
676
|
+
groups.set(block, group)
|
|
677
|
+
}
|
|
678
|
+
return group
|
|
679
|
+
}
|
|
680
|
+
const appendText = (group, node) => {
|
|
681
|
+
const value = node.nodeValue || ''
|
|
682
|
+
if (value === '') return
|
|
683
|
+
group.nodes.push({ node, start: group.raw.length, end: group.raw.length + value.length })
|
|
684
|
+
group.raw += value
|
|
685
|
+
}
|
|
686
|
+
const appendBreak = group => {
|
|
687
|
+
group.breaks.push(group.raw.length)
|
|
688
|
+
group.raw += '\u0000'
|
|
689
|
+
}
|
|
641
690
|
let node = walker.nextNode()
|
|
642
691
|
while (node !== null) {
|
|
643
692
|
const parent = node.parentElement
|
|
644
|
-
if (
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
group.text += value
|
|
693
|
+
if (node.nodeType === Node.ELEMENT_NODE && node.tagName === 'BR') {
|
|
694
|
+
if (parent !== null && parent.closest(excluded) === null) {
|
|
695
|
+
const block = parent.closest(BLOCK_SELECTOR)
|
|
696
|
+
|| parent.closest('[data-text-interaction-source]')
|
|
697
|
+
|| parent.closest('[data-chat-node-key]')
|
|
698
|
+
|| root
|
|
699
|
+
appendBreak(groupFor(block))
|
|
652
700
|
}
|
|
701
|
+
} else if (parent !== null && parent.closest(excluded) === null) {
|
|
702
|
+
const block = parent.closest(BLOCK_SELECTOR)
|
|
703
|
+
|| parent.closest('[data-text-interaction-source]')
|
|
704
|
+
|| parent.closest('[data-chat-node-key]')
|
|
705
|
+
|| root
|
|
706
|
+
appendText(groupFor(block), node)
|
|
653
707
|
}
|
|
654
708
|
node = walker.nextNode()
|
|
655
709
|
}
|
|
710
|
+
// Project the concatenated raw text once per block. Soft newlines and
|
|
711
|
+
// repeated whitespace become one space outside <pre>; <br> becomes a
|
|
712
|
+
// separator. rums maps projected offsets back to raw offsets without
|
|
713
|
+
// allocating one object per character (a multi-MB transcript would
|
|
714
|
+
// otherwise create millions of position objects).
|
|
715
|
+
for (const group of groups.values()) {
|
|
716
|
+
const raw = group.raw
|
|
717
|
+
group.rawLength = raw.length
|
|
718
|
+
if (!group.collapse) {
|
|
719
|
+
group.text = raw.replace(/\u0000/g, '\n')
|
|
720
|
+
group.runs = []
|
|
721
|
+
continue
|
|
722
|
+
}
|
|
723
|
+
let text = ''
|
|
724
|
+
const runs = []
|
|
725
|
+
let index = 0
|
|
726
|
+
while (index < raw.length) {
|
|
727
|
+
const char = raw[index]
|
|
728
|
+
if (char === '\u0000' || /\s/u.test(char)) {
|
|
729
|
+
const start = index
|
|
730
|
+
while (index < raw.length && (raw[index] === '\u0000' || /\s/u.test(raw[index]))) index++
|
|
731
|
+
runs.push({ rawStart: start, rawLength: index - start, projectedStart: text.length })
|
|
732
|
+
text += ' '
|
|
733
|
+
} else {
|
|
734
|
+
text += char
|
|
735
|
+
index++
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
group.text = text
|
|
739
|
+
group.runs = runs
|
|
740
|
+
}
|
|
656
741
|
return [...groups.values()]
|
|
657
742
|
}
|
|
658
743
|
|
|
659
|
-
function
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
744
|
+
function inlineGroups(root) {
|
|
745
|
+
return projectGroups(root, EXCLUDED)
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
function projectedToRaw(group, index, isEnd) {
|
|
749
|
+
if (!group.collapse) return Math.max(0, Math.min(group.rawLength, index))
|
|
750
|
+
let extra = 0
|
|
751
|
+
for (const run of group.runs) {
|
|
752
|
+
if (run.projectedStart < index || (isEnd && run.projectedStart === index)) extra += run.rawLength - 1
|
|
753
|
+
else break
|
|
754
|
+
}
|
|
755
|
+
return Math.max(0, Math.min(group.rawLength, index + extra))
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
function rawToProjected(group, raw) {
|
|
759
|
+
if (!group.collapse) return Math.max(0, Math.min(group.rawLength, raw))
|
|
760
|
+
let extra = 0
|
|
761
|
+
for (const run of group.runs) {
|
|
762
|
+
const end = run.rawStart + run.rawLength
|
|
763
|
+
if (raw >= end) extra += run.rawLength - 1
|
|
764
|
+
else if (raw > run.rawStart) return run.projectedStart
|
|
765
|
+
else break
|
|
766
|
+
}
|
|
767
|
+
return Math.max(0, Math.min(group.rawLength, raw - extra))
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
function pointForRaw(group, raw, isEnd) {
|
|
771
|
+
for (const entry of group.nodes) {
|
|
772
|
+
if (raw < entry.start) continue
|
|
773
|
+
if (raw <= entry.end) return { node: entry.node, offset: Math.min(raw - entry.start, entry.end - entry.start) }
|
|
774
|
+
}
|
|
775
|
+
for (const br of group.breaks) {
|
|
776
|
+
if (raw !== br) continue
|
|
777
|
+
if (isEnd) {
|
|
778
|
+
for (let i = group.nodes.length - 1; i >= 0; i--) {
|
|
779
|
+
const entry = group.nodes[i]
|
|
780
|
+
if (entry.end <= br) return { node: entry.node, offset: entry.end - entry.start }
|
|
781
|
+
}
|
|
782
|
+
} else {
|
|
783
|
+
for (const entry of group.nodes) {
|
|
784
|
+
if (entry.start > br) return { node: entry.node, offset: 0 }
|
|
785
|
+
}
|
|
786
|
+
}
|
|
665
787
|
}
|
|
666
788
|
const last = group.nodes[group.nodes.length - 1]
|
|
667
|
-
return
|
|
789
|
+
return last === undefined ? null : { node: last.node, offset: last.end - last.start }
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
function positionAt(group, index, isEnd) {
|
|
793
|
+
const raw = projectedToRaw(group, index, isEnd)
|
|
794
|
+
return pointForRaw(group, raw, isEnd)
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
function rangeAt(group, start, end) {
|
|
798
|
+
const from = positionAt(group, start, false)
|
|
799
|
+
const to = positionAt(group, end, true)
|
|
800
|
+
if (from === null || to === null) return null
|
|
801
|
+
const range = document.createRange()
|
|
802
|
+
try {
|
|
803
|
+
range.setStart(from.node, from.offset)
|
|
804
|
+
range.setEnd(to.node, to.offset)
|
|
805
|
+
return range
|
|
806
|
+
} catch {
|
|
807
|
+
return null
|
|
808
|
+
}
|
|
668
809
|
}
|
|
669
810
|
|
|
670
811
|
function rangesForNeedles(root, needles) {
|
|
671
812
|
const ranges = []
|
|
672
813
|
if (root === null || needles.length === 0) return ranges
|
|
814
|
+
const patterns = needles.map(escapeRegExp)
|
|
815
|
+
const expression = new RegExp(patterns.join('|'), 'giu')
|
|
673
816
|
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
|
-
}
|
|
817
|
+
expression.lastIndex = 0
|
|
818
|
+
let match
|
|
819
|
+
while (ranges.length < 200 && (match = expression.exec(group.text)) !== null) {
|
|
820
|
+
if (match[0].length === 0) { expression.lastIndex++; continue }
|
|
821
|
+
const range = rangeAt(group, match.index, match.index + match[0].length)
|
|
822
|
+
if (range !== null) ranges.push(range)
|
|
689
823
|
}
|
|
690
824
|
}
|
|
691
825
|
return ranges
|
|
@@ -718,16 +852,12 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
718
852
|
function phraseRange(root, snippet) {
|
|
719
853
|
if (root === null) return null
|
|
720
854
|
for (const phrase of phraseCandidates(snippet)) {
|
|
721
|
-
const
|
|
855
|
+
const expression = new RegExp(escapeRegExp(phrase), 'iu')
|
|
722
856
|
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
|
|
857
|
+
const match = expression.exec(group.text)
|
|
858
|
+
if (match === null) continue
|
|
859
|
+
const range = rangeAt(group, match.index, match.index + match[0].length)
|
|
860
|
+
if (range !== null) return range
|
|
731
861
|
}
|
|
732
862
|
}
|
|
733
863
|
return null
|
|
@@ -851,15 +981,26 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
851
981
|
if (flow === null) { showNotice(t('jumpFailed')); return false }
|
|
852
982
|
const needles = needlesFromQuery(query)
|
|
853
983
|
const finder = () => {
|
|
854
|
-
const
|
|
855
|
-
if (
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
984
|
+
const rows = findMessageRows(flow, hit.messageId, hit.nodeKey)
|
|
985
|
+
if (rows.length > 0) {
|
|
986
|
+
for (const row of rows) {
|
|
987
|
+
expandHidden(row)
|
|
988
|
+
const ranges = rangesForNeedles(row, needles)
|
|
989
|
+
if (ranges.length > 0) return { row, ranges }
|
|
990
|
+
const phrase = phraseRange(row, hit.snippet)
|
|
991
|
+
if (phrase !== null) {
|
|
992
|
+
// Keep the phrase range inside the anchored row instead of
|
|
993
|
+
// falling through to a wrong copy elsewhere in the transcript.
|
|
994
|
+
const inside = row.contains(phrase.commonAncestorContainer)
|
|
995
|
+
if (inside) return { row, ranges: [phrase] }
|
|
996
|
+
}
|
|
997
|
+
}
|
|
861
998
|
return null
|
|
862
999
|
}
|
|
1000
|
+
// With an exact node key there is nothing safe to fall back to in the
|
|
1001
|
+
// already loaded window; wait for older pages instead of landing on a
|
|
1002
|
+
// similar phrase in a different message.
|
|
1003
|
+
if (hit.nodeKey || hit.messageId) return null
|
|
863
1004
|
const phrase = phraseRange(flow, hit.snippet)
|
|
864
1005
|
if (phrase === null) return null
|
|
865
1006
|
expandHidden(phrase.startContainer)
|
|
@@ -872,7 +1013,6 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
872
1013
|
let target = await waitFor(flow, finder, 6_000, 120)
|
|
873
1014
|
for (let attempt = 0; target === null && attempt < 10; attempt++) {
|
|
874
1015
|
if (stale()) return false
|
|
875
|
-
for (const hidden of flow.querySelectorAll('[hidden="until-found"]')) hidden.dispatchEvent(new Event('beforematch'))
|
|
876
1016
|
if (!revealOlder(scope)) break
|
|
877
1017
|
target = await waitFor(flow, finder, 1_500, 150)
|
|
878
1018
|
}
|
|
@@ -961,7 +1101,7 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
961
1101
|
const FIND_BLOCK_SELECTOR = 'p,li,pre,td,th,h1,h2,h3,h4,h5,h6,blockquote,dd,dt'
|
|
962
1102
|
const FIND_EXCLUDED = '[hidden]:not([hidden="until-found"]),[data-chat-history],[data-text-interaction-surface],'
|
|
963
1103
|
+ '[data-composer-seat],[data-text-interaction-exclude],script,style,button,input,textarea,select,'
|
|
964
|
-
+ '.sr-only,.katex-mathml,[aria-hidden="true"]:not(.katex-html)'
|
|
1104
|
+
+ '.sr-only,[class*="visuallyHidden"],[role="status"],[aria-live],.katex-mathml,[aria-hidden="true"]:not(.katex-html)'
|
|
965
1105
|
|
|
966
1106
|
function createFindStore() {
|
|
967
1107
|
const listeners = new Set()
|
|
@@ -1120,13 +1260,17 @@ self.onmessage = event => {
|
|
|
1120
1260
|
}
|
|
1121
1261
|
if (mode !== 'regex') source = escapeRegExp(source)
|
|
1122
1262
|
const flags = 'g' + (caseSensitive ? '' : 'i')
|
|
1263
|
+
// Prefer the Unicode flag so \p{...}, astral literals and modern syntax
|
|
1264
|
+
// behave as users expect. Fall back once for legacy patterns that the
|
|
1265
|
+
// unicode grammar rejects (for example some lone escapes).
|
|
1123
1266
|
try {
|
|
1124
|
-
return { source, flags:
|
|
1267
|
+
return { source, flags: flags + 'u', re: new RegExp(source, flags + 'u') }
|
|
1125
1268
|
} catch (error) {
|
|
1126
|
-
|
|
1127
|
-
|
|
1269
|
+
try {
|
|
1270
|
+
return { source, flags, re: new RegExp(source, flags) }
|
|
1271
|
+
} catch (fallbackError) {
|
|
1272
|
+
return { error: t('findInvalidRegex', { message: String(fallbackError && fallbackError.message || fallbackError).slice(0, 140) }) }
|
|
1128
1273
|
}
|
|
1129
|
-
return { error: t('findInvalidRegex', { message: String(error && error.message || error).slice(0, 140) }) }
|
|
1130
1274
|
}
|
|
1131
1275
|
}
|
|
1132
1276
|
|
|
@@ -1139,67 +1283,38 @@ self.onmessage = event => {
|
|
|
1139
1283
|
}
|
|
1140
1284
|
|
|
1141
1285
|
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()]
|
|
1286
|
+
return projectGroups(root, FIND_EXCLUDED)
|
|
1165
1287
|
}
|
|
1166
1288
|
|
|
1167
1289
|
function clipForGroup(group, range) {
|
|
1168
1290
|
if (range === null) return null
|
|
1169
|
-
let
|
|
1170
|
-
let
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
const length =
|
|
1291
|
+
let rawStart = null
|
|
1292
|
+
let rawEnd = null
|
|
1293
|
+
for (const entry of group.nodes) {
|
|
1294
|
+
const node = entry.node
|
|
1295
|
+
const length = entry.end - entry.start
|
|
1296
|
+
try {
|
|
1297
|
+
if (!range.intersectsNode(node)) continue
|
|
1298
|
+
} catch { continue }
|
|
1174
1299
|
let nodeStart = 0
|
|
1175
1300
|
let nodeEnd = length
|
|
1176
|
-
try {
|
|
1177
|
-
if (!range.intersectsNode(node)) { position += length; continue }
|
|
1178
|
-
} catch { position += length; continue }
|
|
1179
1301
|
if (node === range.startContainer) nodeStart = range.startOffset
|
|
1180
1302
|
if (node === range.endContainer) nodeEnd = range.endOffset
|
|
1181
1303
|
if (nodeStart < nodeEnd) {
|
|
1182
|
-
if (
|
|
1183
|
-
|
|
1304
|
+
if (rawStart === null) rawStart = entry.start + nodeStart
|
|
1305
|
+
rawEnd = entry.start + nodeEnd
|
|
1184
1306
|
}
|
|
1185
|
-
position += length
|
|
1186
1307
|
}
|
|
1187
|
-
if (
|
|
1308
|
+
if (rawStart === null || rawEnd === null) return null
|
|
1309
|
+
const start = rawToProjected(group, rawStart)
|
|
1310
|
+
const end = rawToProjected(group, rawEnd)
|
|
1311
|
+
if (start >= end) return null
|
|
1188
1312
|
return [start, end]
|
|
1189
1313
|
}
|
|
1190
1314
|
|
|
1191
1315
|
function rangeForGroupOffsets(group, start, end) {
|
|
1192
1316
|
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
|
-
}
|
|
1317
|
+
return rangeAt(group, start, end)
|
|
1203
1318
|
}
|
|
1204
1319
|
|
|
1205
1320
|
function findHistoryButton(scope) {
|
|
@@ -1227,6 +1342,8 @@ self.onmessage = event => {
|
|
|
1227
1342
|
const debounceRef = useRef(0)
|
|
1228
1343
|
const observerRef = useRef(null)
|
|
1229
1344
|
const revealTimersRef = useRef([])
|
|
1345
|
+
const revealFrameRef = useRef(0)
|
|
1346
|
+
const loadOlderTimerRef = useRef(0)
|
|
1230
1347
|
const returnFocusRef = useRef(null)
|
|
1231
1348
|
const [anchor, setAnchor] = useState(null)
|
|
1232
1349
|
const composingRef = useRef(false)
|
|
@@ -1248,7 +1365,9 @@ self.onmessage = event => {
|
|
|
1248
1365
|
if (!match.range.startContainer.isConnected) return
|
|
1249
1366
|
centerRange(match.range)
|
|
1250
1367
|
}
|
|
1251
|
-
|
|
1368
|
+
if (typeof cancelAnimationFrame === 'function') cancelAnimationFrame(revealFrameRef.current)
|
|
1369
|
+
revealFrameRef.current = typeof requestAnimationFrame === 'function' ? requestAnimationFrame(run) : 0
|
|
1370
|
+
if (revealFrameRef.current === 0) run()
|
|
1252
1371
|
for (const delay of [80, 240]) {
|
|
1253
1372
|
const timer = setTimeout(run, delay)
|
|
1254
1373
|
revealTimersRef.current.push(timer)
|
|
@@ -1302,7 +1421,11 @@ self.onmessage = event => {
|
|
|
1302
1421
|
let selectionRange = null
|
|
1303
1422
|
if (state.scope === 'selection' && state.selection) {
|
|
1304
1423
|
selectionRange = state.selection.range
|
|
1305
|
-
|
|
1424
|
+
let selectionDead = !selectionRange.startContainer.isConnected || selectionRange.collapsed
|
|
1425
|
+
if (!selectionDead) {
|
|
1426
|
+
try { selectionDead = selectionRange.toString().trim() === '' } catch { selectionDead = true }
|
|
1427
|
+
}
|
|
1428
|
+
if (selectionDead) {
|
|
1306
1429
|
findStore.set({ scope: 'session', selection: null, hint: t('findSelectionLost') })
|
|
1307
1430
|
return
|
|
1308
1431
|
}
|
|
@@ -1322,12 +1445,20 @@ self.onmessage = event => {
|
|
|
1322
1445
|
texts.push(text)
|
|
1323
1446
|
clips.push(clip === null ? [0, group.text.length] : clip)
|
|
1324
1447
|
}
|
|
1448
|
+
if (selectionRange !== null && clips.every(clip => clip === null)) {
|
|
1449
|
+
findStore.set({ scope: 'session', selection: null, hint: t('findSelectionLost') })
|
|
1450
|
+
return
|
|
1451
|
+
}
|
|
1325
1452
|
let rawMatches = null
|
|
1326
1453
|
let capped = false
|
|
1327
1454
|
if (state.mode === 'regex') {
|
|
1328
1455
|
try {
|
|
1329
1456
|
const result = await scanRegexInWorker(texts.filter(text => text !== null), compiled.source, compiled.flags)
|
|
1330
1457
|
if (token !== sequenceRef.current) return
|
|
1458
|
+
if (result === null) {
|
|
1459
|
+
findStore.set({ count: 0, index: -1, capped: false, status: 'error', error: t('findRegexUnavailable') })
|
|
1460
|
+
return
|
|
1461
|
+
}
|
|
1331
1462
|
if (result !== null) {
|
|
1332
1463
|
if (result.timeout === true) {
|
|
1333
1464
|
matchesRef.current = []
|
|
@@ -1438,10 +1569,11 @@ self.onmessage = event => {
|
|
|
1438
1569
|
debounceRef.current = setTimeout(() => { if (findStore.get().open) void runScanRef.current(true) }, 400)
|
|
1439
1570
|
}
|
|
1440
1571
|
const scope = scopeRef.current
|
|
1441
|
-
|
|
1572
|
+
const observed = scope === null ? null : (chatFlow(scope) || scope)
|
|
1573
|
+
if (observed !== null) {
|
|
1442
1574
|
try {
|
|
1443
1575
|
observer = new MutationObserver(schedule)
|
|
1444
|
-
observer.observe(
|
|
1576
|
+
observer.observe(observed, { childList: true, subtree: true, characterData: true })
|
|
1445
1577
|
} catch { /* observer unavailable */ }
|
|
1446
1578
|
}
|
|
1447
1579
|
return () => {
|
|
@@ -1451,6 +1583,8 @@ self.onmessage = event => {
|
|
|
1451
1583
|
if (observer !== null) observer.disconnect()
|
|
1452
1584
|
for (const id of revealTimersRef.current) clearTimeout(id)
|
|
1453
1585
|
revealTimersRef.current = []
|
|
1586
|
+
if (typeof cancelAnimationFrame === 'function') cancelAnimationFrame(revealFrameRef.current)
|
|
1587
|
+
clearTimeout(loadOlderTimerRef.current)
|
|
1454
1588
|
sequenceRef.current++
|
|
1455
1589
|
clearFindHighlight('find')
|
|
1456
1590
|
setFindScope(null)
|
|
@@ -1495,7 +1629,10 @@ self.onmessage = event => {
|
|
|
1495
1629
|
if (text !== '') { event.preventDefault(); copyText(text).then(() => findStore.set({ hint: t('findCopied') })).catch(() => {}) }
|
|
1496
1630
|
}
|
|
1497
1631
|
}
|
|
1498
|
-
const pickMode = event => {
|
|
1632
|
+
const pickMode = event => {
|
|
1633
|
+
findStore.set({ mode: event.target.value })
|
|
1634
|
+
if (inputRef.current !== null) inputRef.current.focus()
|
|
1635
|
+
}
|
|
1499
1636
|
const toggle = key => { findStore.set({ [key]: !findStore.get()[key] }) }
|
|
1500
1637
|
const toggleScope = () => {
|
|
1501
1638
|
if (state.scope === 'selection') findStore.set({ scope: 'session', selection: null, hint: '' })
|
|
@@ -1513,7 +1650,8 @@ self.onmessage = event => {
|
|
|
1513
1650
|
const loadOlder = () => {
|
|
1514
1651
|
if (scopeRef.current === null || !revealOlder(scopeRef.current)) return
|
|
1515
1652
|
findStore.set({ hint: t('findLoadingOlder') })
|
|
1516
|
-
|
|
1653
|
+
clearTimeout(loadOlderTimerRef.current)
|
|
1654
|
+
loadOlderTimerRef.current = setTimeout(() => { if (findStore.get().open) void runScanRef.current(true) }, 700)
|
|
1517
1655
|
}
|
|
1518
1656
|
return h('div', {
|
|
1519
1657
|
ref: barRef,
|
|
@@ -1551,7 +1689,10 @@ self.onmessage = event => {
|
|
|
1551
1689
|
h('button', { className: 'dsh-locator-findbar-btn', title: t('findClose'), 'aria-label': t('findClose'),
|
|
1552
1690
|
onClick: () => closeFind() }, '×'),
|
|
1553
1691
|
),
|
|
1554
|
-
h('div', { className: 'dsh-locator-findbar-opts'
|
|
1692
|
+
h('div', { className: 'dsh-locator-findbar-opts', onMouseDown: event => {
|
|
1693
|
+
if (event.target instanceof HTMLSelectElement) return
|
|
1694
|
+
event.preventDefault()
|
|
1695
|
+
} },
|
|
1555
1696
|
h('label', { className: 'dsh-locator-findbar-label' }, t('findMode'),
|
|
1556
1697
|
h('select', { value: state.mode, onChange: pickMode, 'aria-label': t('findMode') },
|
|
1557
1698
|
h('option', { value: 'normal' }, t('modeNormal')),
|
|
@@ -1613,6 +1754,8 @@ self.onmessage = event => {
|
|
|
1613
1754
|
const panelRef = useRef(null)
|
|
1614
1755
|
const abortRef = useRef(null)
|
|
1615
1756
|
const listRef = useRef(null)
|
|
1757
|
+
const toastTimerRef = useRef(0)
|
|
1758
|
+
const insertTimerRef = useRef(0)
|
|
1616
1759
|
|
|
1617
1760
|
const close = useCallback(() => store.set(false), [])
|
|
1618
1761
|
const activeScope = conversationScope('')
|
|
@@ -1628,7 +1771,9 @@ self.onmessage = event => {
|
|
|
1628
1771
|
|
|
1629
1772
|
useEffect(() => {
|
|
1630
1773
|
if (!open) return undefined
|
|
1774
|
+
if (sessionOnly && activeSessionId === '') setSessionOnly(false)
|
|
1631
1775
|
closeFind()
|
|
1776
|
+
setGroups(null)
|
|
1632
1777
|
const previous = document.activeElement
|
|
1633
1778
|
setError('')
|
|
1634
1779
|
const timer = setTimeout(() => {
|
|
@@ -1639,9 +1784,24 @@ self.onmessage = event => {
|
|
|
1639
1784
|
}, 30)
|
|
1640
1785
|
return () => {
|
|
1641
1786
|
clearTimeout(timer)
|
|
1787
|
+
if (abortRef.current !== null) { abortRef.current.abort(); abortRef.current = null }
|
|
1788
|
+
setGroups(null)
|
|
1642
1789
|
if (previous instanceof HTMLElement && previous.isConnected) previous.focus()
|
|
1643
1790
|
}
|
|
1644
|
-
}, [open])
|
|
1791
|
+
}, [open, sessionOnly, activeSessionId])
|
|
1792
|
+
|
|
1793
|
+
useEffect(() => {
|
|
1794
|
+
if (!open) return undefined
|
|
1795
|
+
const onDocumentKey = event => {
|
|
1796
|
+
if (event.key !== 'Escape' || event.defaultPrevented) return
|
|
1797
|
+
const target = event.target
|
|
1798
|
+
if (target instanceof Element && target.closest('.dsh-locator-panel') !== null) return
|
|
1799
|
+
event.preventDefault()
|
|
1800
|
+
close()
|
|
1801
|
+
}
|
|
1802
|
+
document.addEventListener('keydown', onDocumentKey, true)
|
|
1803
|
+
return () => document.removeEventListener('keydown', onDocumentKey, true)
|
|
1804
|
+
}, [open, close])
|
|
1645
1805
|
|
|
1646
1806
|
useEffect(() => {
|
|
1647
1807
|
if (!open) return undefined
|
|
@@ -1742,7 +1902,12 @@ self.onmessage = event => {
|
|
|
1742
1902
|
|
|
1743
1903
|
const notify = useCallback(message => {
|
|
1744
1904
|
setToast(message)
|
|
1745
|
-
|
|
1905
|
+
clearTimeout(toastTimerRef.current)
|
|
1906
|
+
toastTimerRef.current = setTimeout(() => setToast(''), 1600)
|
|
1907
|
+
}, [])
|
|
1908
|
+
useEffect(() => () => {
|
|
1909
|
+
clearTimeout(toastTimerRef.current)
|
|
1910
|
+
clearTimeout(insertTimerRef.current)
|
|
1746
1911
|
}, [])
|
|
1747
1912
|
|
|
1748
1913
|
const copy = useCallback(hit => {
|
|
@@ -1774,7 +1939,8 @@ self.onmessage = event => {
|
|
|
1774
1939
|
if (!hit || hit.kind === 'session' || loading) return
|
|
1775
1940
|
const mention = '@"' + hit.path.replace(/"/g, '') + '" '
|
|
1776
1941
|
close()
|
|
1777
|
-
|
|
1942
|
+
clearTimeout(insertTimerRef.current)
|
|
1943
|
+
insertTimerRef.current = setTimeout(() => {
|
|
1778
1944
|
if (insertIntoComposer(mention)) notify(t('inserted'))
|
|
1779
1945
|
else copyText(mention).then(() => notify(t('insertFailed'))).catch(() => {})
|
|
1780
1946
|
}, 80)
|
|
@@ -1806,7 +1972,8 @@ self.onmessage = event => {
|
|
|
1806
1972
|
&& (event.target.tagName === 'BUTTON' || event.target.tagName === 'SELECT'
|
|
1807
1973
|
|| (event.target.tagName === 'INPUT' && event.target.type !== 'text'))
|
|
1808
1974
|
if (interactive) return
|
|
1809
|
-
const
|
|
1975
|
+
const altGraph = typeof event.getModifierState === 'function' && event.getModifierState('AltGraph')
|
|
1976
|
+
const scopeDigit = event.altKey && !event.ctrlKey && !event.metaKey && !altGraph
|
|
1810
1977
|
? (event.code === 'Digit1' ? '1' : event.code === 'Digit2' ? '2' : event.code === 'Digit3' ? '3' : ['1', '2', '3'].includes(event.key) ? event.key : '')
|
|
1811
1978
|
: ''
|
|
1812
1979
|
if (scopeDigit !== '') {
|
|
@@ -1908,6 +2075,7 @@ self.onmessage = event => {
|
|
|
1908
2075
|
const groupHead = (key, label, count, note) => h('div', {
|
|
1909
2076
|
className: 'dsh-locator-grouphead',
|
|
1910
2077
|
id: 'dsh-locator-group-' + key,
|
|
2078
|
+
onMouseDown: event => event.preventDefault(),
|
|
1911
2079
|
},
|
|
1912
2080
|
h('span', null, label),
|
|
1913
2081
|
h('span', { className: 'dsh-locator-count' }, String(count)),
|
|
@@ -1934,6 +2102,7 @@ self.onmessage = event => {
|
|
|
1934
2102
|
className: 'dsh-locator-row' + (isContent ? ' dsh-locator-rowcontent' : '') + (index === selected ? ' sel' : ''),
|
|
1935
2103
|
title: hit.path,
|
|
1936
2104
|
onClick: () => setSelected(index),
|
|
2105
|
+
onMouseDown: event => event.preventDefault(),
|
|
1937
2106
|
onDoubleClick: () => activate(hit),
|
|
1938
2107
|
},
|
|
1939
2108
|
h('div', { className: 'dsh-locator-title' },
|
|
@@ -1948,32 +2117,46 @@ self.onmessage = event => {
|
|
|
1948
2117
|
)
|
|
1949
2118
|
}
|
|
1950
2119
|
|
|
2120
|
+
const contentErrorText = code => {
|
|
2121
|
+
if (code === 'content-regex-not-supported') return t('panelRegexPaused')
|
|
2122
|
+
if (code === 'content-query-too-short') return t('contentTooShort')
|
|
2123
|
+
if (code === 'content-search-timeout') return t('contentTimeout')
|
|
2124
|
+
if (code === 'invalid-control-char') return t('contentBadChars')
|
|
2125
|
+
return code
|
|
2126
|
+
}
|
|
1951
2127
|
let index = -1
|
|
1952
2128
|
const renderGroup = (key, label, group, extra) => {
|
|
1953
2129
|
if (!scopes[key] || group === undefined) return null
|
|
1954
2130
|
const hits = Array.isArray(group.hits) ? group.hits : []
|
|
1955
2131
|
const count = typeof group.returned === 'number' ? group.returned : hits.length
|
|
2132
|
+
const warnings = Array.isArray(group.warnings) && group.warnings.length > 0
|
|
2133
|
+
? t('contentWarning', { list: group.warnings.join(', ') })
|
|
2134
|
+
: null
|
|
2135
|
+
const budget = key === 'content' && group.truncated === true ? t('contentBudget') : null
|
|
1956
2136
|
const note = key === 'content'
|
|
1957
2137
|
? (matchMode === 'regex' ? t('panelRegexPaused')
|
|
1958
2138
|
: contentBuilding ? t('contentBuilding', { files: content?.files ?? 0, messages: content?.messages ?? 0 })
|
|
1959
2139
|
: (content?.capped ? t('contentCapped', { n: count }) : (query.trim() !== '' ? t('relevance') : null)))
|
|
1960
2140
|
: (group.deduped > 0 ? t('deduped', { n: group.deduped }) : (group.truncated ? t('showing', { n: hits.length }) : null))
|
|
2141
|
+
const combinedNote = [note, warnings, budget].filter(value => value !== null && value !== '').join(' · ')
|
|
1961
2142
|
if (typeof group.error === 'string' && group.error !== '') {
|
|
1962
|
-
const
|
|
2143
|
+
const informational = ['content-regex-not-supported', 'content-query-too-short', 'invalid-control-char'].includes(group.error)
|
|
2144
|
+
const message = contentErrorText(group.error)
|
|
1963
2145
|
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
|
-
|
|
2146
|
+
groupHead(key, label, countLabel(group, count), combinedNote === '' ? null : combinedNote),
|
|
2147
|
+
h('div', { className: 'dsh-locator-groupempty', role: informational ? 'status' : 'alert' },
|
|
2148
|
+
informational ? message : t('errorPrefix') + message,
|
|
2149
|
+
informational ? null : h('button', { className: 'dsh-locator-dirbtn dsh-locator-retry', onClick: () => setRefreshTick(v => v + 1) }, t('retry'))),
|
|
1967
2150
|
)
|
|
1968
2151
|
}
|
|
1969
2152
|
return h('div', { className: 'dsh-locator-group', role: 'group', 'aria-labelledby': 'dsh-locator-group-' + key },
|
|
1970
|
-
groupHead(key, label, countLabel(group, count),
|
|
2153
|
+
groupHead(key, label, countLabel(group, count), combinedNote === '' ? null : combinedNote),
|
|
1971
2154
|
extra === undefined ? null : extra,
|
|
1972
2155
|
hits.length === 0
|
|
1973
2156
|
? h('div', { className: 'dsh-locator-groupempty' },
|
|
1974
2157
|
key === 'content' && scopes.content
|
|
1975
|
-
? (sessionOnly && query.trim().length >=
|
|
1976
|
-
: query.trim().length <
|
|
2158
|
+
? (sessionOnly && query.trim().length >= 3 ? t('panelSessionHint')
|
|
2159
|
+
: query.trim().length < 3 ? t('contentShort') : (contentBuilding ? t('loading') : t('emptyResults')))
|
|
1977
2160
|
: (query.trim() === '' ? t('emptyQuery') : t('emptyResults')))
|
|
1978
2161
|
: hits.map(hit => { index++; return rowFor(key, hit, index) }),
|
|
1979
2162
|
)
|
|
@@ -2058,7 +2241,9 @@ self.onmessage = event => {
|
|
|
2058
2241
|
entries: (status.entries || 0).toLocaleString(),
|
|
2059
2242
|
mode: status.watching ? t('live') : t('snapshot'),
|
|
2060
2243
|
roots: (status.roots || []).length,
|
|
2061
|
-
})
|
|
2244
|
+
}) + (status.watchDegraded === true || status.refreshFallback === true
|
|
2245
|
+
? ' · ' + t('watchDegraded')
|
|
2246
|
+
: '')),
|
|
2062
2247
|
),
|
|
2063
2248
|
sessionOnly ? h('div', { className: 'dsh-locator-groupempty' }, t('panelSessionHint')) : null,
|
|
2064
2249
|
h('div', { id: 'dsh-locator-results', className: 'dsh-locator-scroll', ref: listRef, role: 'listbox', 'aria-busy': loading ? 'true' : 'false' },
|
|
@@ -2077,7 +2262,12 @@ self.onmessage = event => {
|
|
|
2077
2262
|
],
|
|
2078
2263
|
),
|
|
2079
2264
|
h('div', { className: 'dsh-locator-foot' },
|
|
2080
|
-
h('span', null, (loading ? t('loading') + ' · ' : '') + t('results', {
|
|
2265
|
+
h('span', null, (loading ? t('loading') + ' · ' : '') + t('results', {
|
|
2266
|
+
content: contentCountText || '0',
|
|
2267
|
+
files: filesCountText || '0',
|
|
2268
|
+
dirs: dirsCountText || '0',
|
|
2269
|
+
ms: took.toFixed(1),
|
|
2270
|
+
})
|
|
2081
2271
|
+ (status && status.scanning ? ' · ' + t('indexing') : '')
|
|
2082
2272
|
+ (contentBuilding ? ' · ' + t('indexing') : '')),
|
|
2083
2273
|
h('span', { className: 'grow' }),
|
|
@@ -2113,12 +2303,16 @@ self.onmessage = event => {
|
|
|
2113
2303
|
}, 'dsh-locator: styles')
|
|
2114
2304
|
ctx.effect(() => {
|
|
2115
2305
|
const onKey = event => {
|
|
2116
|
-
if (
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2306
|
+
if (event.defaultPrevented || event.isComposing || event.repeat) return
|
|
2307
|
+
if (!((event.metaKey || event.ctrlKey) && event.shiftKey && String(event.key).toLowerCase() === 'f')) return
|
|
2308
|
+
const target = event.target
|
|
2309
|
+
if (target instanceof Element && target.closest('.xterm,.monaco-editor,[data-code-editor]') !== null) return
|
|
2310
|
+
const foreignModal = document.querySelector('[role="dialog"][aria-modal="true"]:not(.dsh-locator-panel)')
|
|
2311
|
+
if (foreignModal !== null) return
|
|
2312
|
+
event.preventDefault()
|
|
2313
|
+
event.stopPropagation()
|
|
2314
|
+
closeFind()
|
|
2315
|
+
store.toggle()
|
|
2122
2316
|
}
|
|
2123
2317
|
document.addEventListener('keydown', onKey, true)
|
|
2124
2318
|
return () => document.removeEventListener('keydown', onKey, true)
|
|
@@ -2162,7 +2356,11 @@ self.onmessage = event => {
|
|
|
2162
2356
|
document.addEventListener('keydown', onFindKey, true)
|
|
2163
2357
|
return () => document.removeEventListener('keydown', onFindKey, true)
|
|
2164
2358
|
}, 'dsh-locator: find hotkeys')
|
|
2165
|
-
ctx.effect(() => () => {
|
|
2359
|
+
ctx.effect(() => () => {
|
|
2360
|
+
closeFind()
|
|
2361
|
+
cancelJump()
|
|
2362
|
+
store.set(false)
|
|
2363
|
+
}, 'dsh-locator: find cleanup')
|
|
2166
2364
|
try {
|
|
2167
2365
|
ctx.slots.inject('shell.overlay', () => {
|
|
2168
2366
|
const disposers = [
|