dsh-locator 0.8.2 → 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 +47 -0
- package/README-es.md +30 -5
- package/README-hi.md +31 -5
- package/README-pt.md +31 -5
- package/README.en.md +31 -5
- package/README.md +31 -5
- 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 +1198 -117
- package/lib/combined.d.ts +20 -3
- package/lib/combined.d.ts.map +1 -1
- package/lib/combined.js +85 -15
- package/lib/combined.js.map +1 -1
- package/lib/content-search-worker.d.ts +8 -3
- package/lib/content-search-worker.d.ts.map +1 -1
- package/lib/content-search-worker.js +54 -27
- package/lib/content-search-worker.js.map +1 -1
- package/lib/content-search.d.ts +127 -7
- package/lib/content-search.d.ts.map +1 -1
- package/lib/content-search.js +571 -36
- package/lib/content-search.js.map +1 -1
- package/lib/content.d.ts +83 -5
- package/lib/content.d.ts.map +1 -1
- package/lib/content.js +351 -87
- 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 +73 -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 +279 -82
- 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 +107 -17
- package/src/content-search-worker.ts +52 -24
- package/src/content-search.ts +710 -36
- package/src/content.ts +349 -73
- package/src/export.ts +240 -50
- package/src/find.ts +83 -16
- package/src/http.ts +271 -83
- package/src/reveal.ts +148 -10
- package/src/tools.ts +83 -6
package/client.js
CHANGED
|
@@ -123,8 +123,39 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
123
123
|
background:var(--dsw-alias-bg-layer-2,rgba(30,32,38,.92));color:var(--dsw-alias-label-primary,#fff);
|
|
124
124
|
border:1px solid var(--dsw-alias-border-base,rgba(120,125,135,.3));box-shadow:0 8px 24px rgba(0,0,0,.25);
|
|
125
125
|
animation:dsh-locator-in 120ms ease-out}
|
|
126
|
-
::highlight(dsh-locator-find){background-color:
|
|
127
|
-
::highlight(dsh-locator-find-active){background-color:
|
|
126
|
+
::highlight(dsh-locator-find){background-color:color-mix(in srgb,var(--dsw-alias-state-warn-primary,#f59e0b) 28%,transparent);color:inherit}
|
|
127
|
+
::highlight(dsh-locator-find-active){background-color:color-mix(in srgb,var(--dsw-alias-state-warn-primary,#f59e0b) 50%,transparent);color:inherit;
|
|
128
|
+
text-decoration:underline;text-decoration-thickness:2px;text-underline-offset:2px}
|
|
129
|
+
::highlight(dsh-locator-find-scope){background-color:color-mix(in srgb,var(--dsw-alias-bg-document-selection,rgba(90,120,255,.25)) 35%,transparent)}
|
|
130
|
+
.dsh-locator-findbar{position:fixed;z-index:70;display:flex;flex-direction:column;width:min(560px,calc(100vw - 24px));
|
|
131
|
+
border:1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border-base,rgba(120,125,135,.28)));
|
|
132
|
+
border-radius:var(--dsw-radius-md,12px);background:var(--dsw-menu-surface-fill,var(--dsw-alias-bg-base,#fff));
|
|
133
|
+
color:var(--dsw-alias-label-primary,inherit);box-shadow:var(--dsw-elevation-prominent,0 12px 32px rgba(0,0,0,.24));
|
|
134
|
+
font:13px/1.45 system-ui,-apple-system,"Segoe UI",sans-serif;animation:dsh-locator-in 120ms ease-out}
|
|
135
|
+
@media (prefers-reduced-motion:reduce){.dsh-locator-findbar{animation:none}}
|
|
136
|
+
.dsh-locator-findbar-main{display:flex;align-items:center;gap:6px;padding:6px 8px}
|
|
137
|
+
.dsh-locator-findbar-glyph{font-size:14px;opacity:.6;padding:0 2px}
|
|
138
|
+
.dsh-locator-findbar-input{flex:1;min-width:0;border:0;outline:0;background:transparent;color:inherit;font:inherit;font-size:13px;padding:3px 0}
|
|
139
|
+
.dsh-locator-findbar-count{font-variant-numeric:tabular-nums;font-size:12px;opacity:.75;white-space:nowrap;min-width:52px;text-align:right}
|
|
140
|
+
.dsh-locator-findbar-btn{display:inline-flex;align-items:center;justify-content:center;min-width:26px;height:26px;padding:0 5px;
|
|
141
|
+
border:0;border-radius:6px;background:transparent;color:inherit;cursor:pointer;font:inherit;font-size:13px}
|
|
142
|
+
.dsh-locator-findbar-btn:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(120,125,135,.14))}
|
|
143
|
+
.dsh-locator-findbar-btn:disabled{opacity:.4;cursor:default}
|
|
144
|
+
.dsh-locator-findbar-btn[aria-pressed=true]{background:var(--dsw-alias-interactive-bg-active,rgba(64,116,255,.18));
|
|
145
|
+
color:var(--dsw-alias-state-business-primary,#4f7cff)}
|
|
146
|
+
.dsh-locator-findbar-opts{display:flex;align-items:center;gap:8px;padding:5px 9px 7px;flex-wrap:wrap;
|
|
147
|
+
border-top:1px solid var(--dsw-alias-border-l1,var(--dsw-alias-border-base,rgba(120,125,135,.16)));font-size:11.5px}
|
|
148
|
+
.dsh-locator-findbar-opts select{border:1px solid var(--dsw-alias-border-base,rgba(120,125,135,.3));border-radius:5px;
|
|
149
|
+
background:transparent;color:inherit;font:inherit;font-size:11.5px;padding:1px 3px}
|
|
150
|
+
.dsh-locator-findbar-label{display:inline-flex;align-items:center;gap:4px;opacity:.8;white-space:nowrap}
|
|
151
|
+
.dsh-locator-findbar-status{display:flex;align-items:center;gap:8px;padding:4px 10px 6px;font-size:11.5px;opacity:.75;flex-wrap:wrap}
|
|
152
|
+
.dsh-locator-findbar-status .grow{flex:1}
|
|
153
|
+
.dsh-locator-findbar-error{padding:4px 10px 6px;font-size:11.5px;color:var(--dsw-alias-state-error-primary,#d33);
|
|
154
|
+
background:color-mix(in srgb,var(--dsw-alias-state-error-primary,#d33) 9%,transparent)}
|
|
155
|
+
.dsh-locator-findbar-chip{display:inline-flex;align-items:center;gap:3px;padding:1px 6px;border-radius:999px;
|
|
156
|
+
background:var(--dsw-alias-bg-layer-2,rgba(120,125,135,.14));font-size:11px;white-space:nowrap}
|
|
157
|
+
.dsh-locator-findbar-chip button{border:0;background:transparent;color:inherit;cursor:pointer;padding:0 1px;font:inherit;font-size:12px}
|
|
158
|
+
.dsh-locator-findbar kbd{border:1px solid var(--dsw-alias-border-base,rgba(120,125,135,.3));border-radius:4px;padding:0 3px;font-size:10px;opacity:.6}
|
|
128
159
|
`
|
|
129
160
|
function ensureStyles() {
|
|
130
161
|
if (typeof document === 'undefined') return
|
|
@@ -161,7 +192,7 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
161
192
|
asc: 'Ascending', descOrder: 'Descending',
|
|
162
193
|
groupContent: 'Content', groupFiles: 'Files', groupDirs: 'Folders',
|
|
163
194
|
relevance: 'by relevance', contentBuilding: 'indexing… {files} files / {messages} messages',
|
|
164
|
-
contentShort: 'Type at least
|
|
195
|
+
contentShort: 'Type at least 3 characters to search bodies and sessions (Ctrl+F handles shorter queries)',
|
|
165
196
|
contentDisabled: 'Content index is disabled (contentIndex: false)',
|
|
166
197
|
contentCapped: 'first {n}', showing: 'first {n}', deduped: '{n} name hits merged into content', loading: 'Searching…',
|
|
167
198
|
emptyQuery: 'Type to search file names, paths, bodies and sessions',
|
|
@@ -177,6 +208,26 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
177
208
|
launcher: 'Local file search (Ctrl+Shift+F)',
|
|
178
209
|
justNow: 'just now', minutesAgo: '{n} min ago', hoursAgo: '{n} h ago',
|
|
179
210
|
phraseNotFound: 'No match in the conversation (it may not be loaded yet)',
|
|
211
|
+
findOpen: 'Find in conversation', findTitle: 'Find in conversation', findAria: 'Find in conversation',
|
|
212
|
+
findPlaceholder: 'Find in conversation…', findPlaceholderSelection: 'Find in selection…',
|
|
213
|
+
findPrev: 'Previous match (Shift+F3)', findNext: 'Next match (F3)', findClose: 'Close (Esc)',
|
|
214
|
+
findCase: 'Match case', findWhole: 'Whole word', findWrap: 'Wrap around', findReverse: 'Search backwards',
|
|
215
|
+
findMode: 'Mode', modeNormal: 'Normal', modeExtended: 'Extended', modeRegex: 'Regex',
|
|
216
|
+
findScope: 'In selection', findScopeChip: 'selection {n} chars', findSessionChip: 'this conversation',
|
|
217
|
+
findNoMatch: 'No matches', findEmpty: 'Type to find in this conversation',
|
|
218
|
+
findInvalidRegex: 'Invalid regular expression: {message}', findInvalidEscape: 'Invalid extended escape: {token}',
|
|
219
|
+
findTimeout: 'Regex timed out; simplify the expression', findRegexUnavailable: 'Regex search is unavailable here (worker blocked)', findWrappedTop: 'Wrapped to top',
|
|
220
|
+
findWrappedBottom: 'Wrapped to bottom', findNoWrapEnd: 'Reached the last match (wrap is off)',
|
|
221
|
+
findNoWrapStart: 'Reached the first match (wrap is off)', findOlder: 'Load earlier messages and retry',
|
|
222
|
+
findLoadingOlder: 'Loading earlier messages…', findLoadedOnly: 'loaded window only · earlier history not loaded',
|
|
223
|
+
findCapped: 'capped', findCopied: 'Match copied', findSelectionLost: 'Selection expired; searching the whole conversation',
|
|
224
|
+
findNoSession: 'Open a conversation first', findOptions: 'Options',
|
|
225
|
+
findExtendedHelp: 'Extended: \\n \\r \\t \\0 \\xHH \\\\',
|
|
226
|
+
panelTotal: 'selected {index}/{total}', panelRangeAll: 'All indexed', panelRangeSession: 'This conversation',
|
|
227
|
+
panelSessionHint: 'This scope searches indexed session messages only; press Ctrl+F for live in-conversation find',
|
|
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',
|
|
230
|
+
panelCaseShort: 'Aa', panelWholeShort: 'Word', panelModeLabel: 'Mode',
|
|
180
231
|
},
|
|
181
232
|
zh: {
|
|
182
233
|
scopeContent: '内容', scopeFiles: '文件', scopeDirs: '目录',
|
|
@@ -186,7 +237,7 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
186
237
|
asc: '升序', descOrder: '降序',
|
|
187
238
|
groupContent: '内容', groupFiles: '文件', groupDirs: '目录',
|
|
188
239
|
relevance: '按相关度', contentBuilding: '索引构建中… {files} 文件 / {messages} 消息',
|
|
189
|
-
contentShort: '输入至少
|
|
240
|
+
contentShort: '输入至少 3 个字符后搜索正文与会话(更短的词请用 Ctrl+F)',
|
|
190
241
|
contentDisabled: '内容索引已关闭(contentIndex: false)',
|
|
191
242
|
contentCapped: '前 {n} 条', showing: '前 {n} 条', deduped: '{n} 条文件命中已并入内容', loading: '搜索中…',
|
|
192
243
|
emptyQuery: '输入关键词:文件名、路径、正文或会话内容',
|
|
@@ -202,6 +253,26 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
202
253
|
launcher: '本地文件搜索 (Ctrl+Shift+F)',
|
|
203
254
|
justNow: '刚刚', minutesAgo: '{n} 分钟前', hoursAgo: '{n} 小时前',
|
|
204
255
|
phraseNotFound: '会话里没有找到匹配内容(可能还没加载)',
|
|
256
|
+
findOpen: '在会话中查找', findTitle: '在会话中查找', findAria: '在会话中查找',
|
|
257
|
+
findPlaceholder: '在会话中查找…', findPlaceholderSelection: '在选区内查找…',
|
|
258
|
+
findPrev: '上一个匹配 (Shift+F3)', findNext: '下一个匹配 (F3)', findClose: '关闭 (Esc)',
|
|
259
|
+
findCase: '区分大小写', findWhole: '全词匹配', findWrap: '循环查找', findReverse: '反向查找',
|
|
260
|
+
findMode: '模式', modeNormal: '普通', modeExtended: '扩展', modeRegex: '正则',
|
|
261
|
+
findScope: '仅在选区内', findScopeChip: '选区 {n} 字', findSessionChip: '本会话',
|
|
262
|
+
findNoMatch: '没有匹配', findEmpty: '输入关键词后在此会话中查找',
|
|
263
|
+
findInvalidRegex: '正则无效:{message}', findInvalidEscape: '扩展转义无效:{token}',
|
|
264
|
+
findTimeout: '正则执行超时,请简化表达式', findRegexUnavailable: '此处无法运行正则(worker 被阻止)', findWrappedTop: '已回到顶部',
|
|
265
|
+
findWrappedBottom: '已到底部,回到开头', findNoWrapEnd: '已到最后一个匹配(循环查找已关闭)',
|
|
266
|
+
findNoWrapStart: '已到第一个匹配(循环查找已关闭)', findOlder: '加载更早消息后重试',
|
|
267
|
+
findLoadingOlder: '正在加载更早消息…', findLoadedOnly: '仅已加载窗口 · 更早历史未加载',
|
|
268
|
+
findCapped: '已截断', findCopied: '已复制匹配', findSelectionLost: '选区已失效,已切回整个会话',
|
|
269
|
+
findNoSession: '请先打开一个会话', findOptions: '选项',
|
|
270
|
+
findExtendedHelp: '扩展:\\n \\r \\t \\0 \\xHH \\\\',
|
|
271
|
+
panelTotal: '已选 {index}/{total}', panelRangeAll: '全部索引', panelRangeSession: '当前会话',
|
|
272
|
+
panelSessionHint: '此范围只查已索引的会话消息;实时查找请按 Ctrl+F',
|
|
273
|
+
panelRegexPaused: '正则模式下内容范围暂停', panelCountBounded: '≥ {n}', panelCountExact: '{n}',
|
|
274
|
+
contentWarning: '索引警告:{list}', contentBudget: '内容索引已达预算上限,部分文件未被收录', contentTooShort: '内容搜索至少需要 3 个字符(更短的词请用 Ctrl+F 在会话内查找)', contentTimeout: '内容搜索超时,请缩小关键词或范围', contentBadChars: '内容搜索不支持 NUL/控制字符',
|
|
275
|
+
panelCaseShort: 'Aa', panelWholeShort: '全词', panelModeLabel: '模式',
|
|
205
276
|
},
|
|
206
277
|
es: {
|
|
207
278
|
scopeContent: 'Contenido', scopeFiles: 'Archivos', scopeDirs: 'Carpetas',
|
|
@@ -211,7 +282,7 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
211
282
|
asc: 'Ascendente', descOrder: 'Descendente',
|
|
212
283
|
groupContent: 'Contenido', groupFiles: 'Archivos', groupDirs: 'Carpetas',
|
|
213
284
|
relevance: 'por relevancia', contentBuilding: 'indexando… {files} archivos / {messages} mensajes',
|
|
214
|
-
contentShort: 'Escribe al menos
|
|
285
|
+
contentShort: 'Escribe al menos 3 caracteres para buscar contenido y sesiones (Ctrl+F para consultas cortas)',
|
|
215
286
|
contentDisabled: 'Índice de contenido desactivado (contentIndex: false)',
|
|
216
287
|
contentCapped: 'primeros {n}', showing: 'primeros {n}', deduped: '{n} aciertos de nombre fusionados', loading: 'Buscando…',
|
|
217
288
|
emptyQuery: 'Escribe para buscar nombres, rutas, contenido y sesiones',
|
|
@@ -227,6 +298,26 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
227
298
|
launcher: 'Búsqueda local (Ctrl+Shift+F)',
|
|
228
299
|
justNow: 'ahora', minutesAgo: 'hace {n} min', hoursAgo: 'hace {n} h',
|
|
229
300
|
phraseNotFound: 'No se encontró en la conversación (quizá no está cargada)',
|
|
301
|
+
findOpen: 'Buscar en la conversación', findTitle: 'Buscar en la conversación', findAria: 'Buscar en la conversación',
|
|
302
|
+
findPlaceholder: 'Buscar en la conversación…', findPlaceholderSelection: 'Buscar en la selección…',
|
|
303
|
+
findPrev: 'Coincidencia anterior (Shift+F3)', findNext: 'Siguiente coincidencia (F3)', findClose: 'Cerrar (Esc)',
|
|
304
|
+
findCase: 'Distinguir mayúsculas', findWhole: 'Palabra completa', findWrap: 'Continuar al inicio', findReverse: 'Buscar hacia atrás',
|
|
305
|
+
findMode: 'Modo', modeNormal: 'Normal', modeExtended: 'Extendido', modeRegex: 'Regex',
|
|
306
|
+
findScope: 'En la selección', findScopeChip: 'selección {n} caracteres', findSessionChip: 'esta conversación',
|
|
307
|
+
findNoMatch: 'Sin coincidencias', findEmpty: 'Escribe para buscar en esta conversación',
|
|
308
|
+
findInvalidRegex: 'Expresión regular no válida: {message}', findInvalidEscape: 'Escape extendido no válido: {token}',
|
|
309
|
+
findTimeout: 'La regex agotó el tiempo; simplifícala', findRegexUnavailable: 'La búsqueda regex no está disponible aquí (worker bloqueado)', findWrappedTop: 'Vuelto al inicio',
|
|
310
|
+
findWrappedBottom: 'Vuelto al final', findNoWrapEnd: 'Última coincidencia (continuación desactivada)',
|
|
311
|
+
findNoWrapStart: 'Primera coincidencia (continuación desactivada)', findOlder: 'Cargar mensajes anteriores y reintentar',
|
|
312
|
+
findLoadingOlder: 'Cargando mensajes anteriores…', findLoadedOnly: 'solo ventana cargada · historial anterior sin cargar',
|
|
313
|
+
findCapped: 'limitado', findCopied: 'Coincidencia copiada', findSelectionLost: 'La selección caducó; buscando en toda la conversación',
|
|
314
|
+
findNoSession: 'Abre primero una conversación', findOptions: 'Opciones',
|
|
315
|
+
findExtendedHelp: 'Extendido: \\n \\r \\t \\0 \\xHH \\\\',
|
|
316
|
+
panelTotal: 'seleccionado {index}/{total}', panelRangeAll: 'Todo el índice', panelRangeSession: 'Esta conversación',
|
|
317
|
+
panelSessionHint: 'Este ámbito solo busca mensajes indexados; pulsa Ctrl+F para buscar en vivo',
|
|
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',
|
|
320
|
+
panelCaseShort: 'Aa', panelWholeShort: 'Palabra', panelModeLabel: 'Modo',
|
|
230
321
|
},
|
|
231
322
|
pt: {
|
|
232
323
|
scopeContent: 'Conteúdo', scopeFiles: 'Arquivos', scopeDirs: 'Pastas',
|
|
@@ -236,7 +327,7 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
236
327
|
asc: 'Ascendente', descOrder: 'Descendente',
|
|
237
328
|
groupContent: 'Conteúdo', groupFiles: 'Arquivos', groupDirs: 'Pastas',
|
|
238
329
|
relevance: 'por relevância', contentBuilding: 'indexando… {files} arquivos / {messages} mensagens',
|
|
239
|
-
contentShort: 'Digite pelo menos
|
|
330
|
+
contentShort: 'Digite pelo menos 3 caracteres para buscar conteúdo e sessões (Ctrl+F para consultas curtas)',
|
|
240
331
|
contentDisabled: 'Índice de conteúdo desativado (contentIndex: false)',
|
|
241
332
|
contentCapped: 'primeiros {n}', showing: 'primeiros {n}', deduped: '{n} acertos de nome mesclados', loading: 'Buscando…',
|
|
242
333
|
emptyQuery: 'Digite para buscar nomes, caminhos, conteúdo e sessões',
|
|
@@ -252,6 +343,26 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
252
343
|
launcher: 'Busca local (Ctrl+Shift+F)',
|
|
253
344
|
justNow: 'agora', minutesAgo: 'há {n} min', hoursAgo: 'há {n} h',
|
|
254
345
|
phraseNotFound: 'Não encontrado na conversa (talvez ainda não carregada)',
|
|
346
|
+
findOpen: 'Buscar na conversa', findTitle: 'Buscar na conversa', findAria: 'Buscar na conversa',
|
|
347
|
+
findPlaceholder: 'Buscar na conversa…', findPlaceholderSelection: 'Buscar na seleção…',
|
|
348
|
+
findPrev: 'Ocorrência anterior (Shift+F3)', findNext: 'Próxima ocorrência (F3)', findClose: 'Fechar (Esc)',
|
|
349
|
+
findCase: 'Diferenciar maiúsculas', findWhole: 'Palavra inteira', findWrap: 'Continuar do início', findReverse: 'Buscar para trás',
|
|
350
|
+
findMode: 'Modo', modeNormal: 'Normal', modeExtended: 'Estendido', modeRegex: 'Regex',
|
|
351
|
+
findScope: 'Na seleção', findScopeChip: 'seleção {n} caracteres', findSessionChip: 'esta conversa',
|
|
352
|
+
findNoMatch: 'Sem resultados', findEmpty: 'Digite para buscar nesta conversa',
|
|
353
|
+
findInvalidRegex: 'Expressão regular inválida: {message}', findInvalidEscape: 'Escape estendido inválido: {token}',
|
|
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',
|
|
355
|
+
findWrappedBottom: 'Voltou ao fim', findNoWrapEnd: 'Última ocorrência (continuação desativada)',
|
|
356
|
+
findNoWrapStart: 'Primeira ocorrência (continuação desativada)', findOlder: 'Carregar mensagens anteriores e tentar novamente',
|
|
357
|
+
findLoadingOlder: 'Carregando mensagens anteriores…', findLoadedOnly: 'apenas janela carregada · histórico anterior não carregado',
|
|
358
|
+
findCapped: 'limitado', findCopied: 'Ocorrência copiada', findSelectionLost: 'A seleção expirou; buscando em toda a conversa',
|
|
359
|
+
findNoSession: 'Abra uma conversa primeiro', findOptions: 'Opções',
|
|
360
|
+
findExtendedHelp: 'Estendido: \\n \\r \\t \\0 \\xHH \\\\',
|
|
361
|
+
panelTotal: 'selecionado {index}/{total}', panelRangeAll: 'Todo o índice', panelRangeSession: 'Esta conversa',
|
|
362
|
+
panelSessionHint: 'Este escopo busca apenas mensagens indexadas; use Ctrl+F para buscar ao vivo',
|
|
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',
|
|
365
|
+
panelCaseShort: 'Aa', panelWholeShort: 'Palavra', panelModeLabel: 'Modo',
|
|
255
366
|
},
|
|
256
367
|
hi: {
|
|
257
368
|
scopeContent: 'सामग्री', scopeFiles: 'फ़ाइलें', scopeDirs: 'फ़ोल्डर',
|
|
@@ -261,7 +372,7 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
261
372
|
asc: 'आरोही', descOrder: 'अवरोही',
|
|
262
373
|
groupContent: 'सामग्री', groupFiles: 'फ़ाइलें', groupDirs: 'फ़ोल्डर',
|
|
263
374
|
relevance: 'प्रासंगिकता से', contentBuilding: 'इंडेक्स बन रहा है… {files} फ़ाइलें / {messages} संदेश',
|
|
264
|
-
contentShort: 'सामग्री और सत्र खोजने के लिए कम से कम
|
|
375
|
+
contentShort: 'सामग्री और सत्र खोजने के लिए कम से कम 3 अक्षर लिखें (छोटी खोज के लिए Ctrl+F)',
|
|
265
376
|
contentDisabled: 'सामग्री इंडेक्स बंद है (contentIndex: false)',
|
|
266
377
|
contentCapped: 'पहले {n}', showing: 'पहले {n}', deduped: '{n} नाम-परिणाम सामग्री में मिलाए', loading: 'खोज रहे हैं…',
|
|
267
378
|
emptyQuery: 'नाम, पथ, सामग्री और सत्र खोजने के लिए लिखें',
|
|
@@ -277,6 +388,26 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
277
388
|
launcher: 'स्थानीय फ़ाइल खोज (Ctrl+Shift+F)',
|
|
278
389
|
justNow: 'अभी', minutesAgo: '{n} मिनट पहले', hoursAgo: '{n} घंटे पहले',
|
|
279
390
|
phraseNotFound: 'बातचीत में नहीं मिला (शायद अभी लोड नहीं हुआ)',
|
|
391
|
+
findOpen: 'बातचीत में खोजें', findTitle: 'बातचीत में खोजें', findAria: 'बातचीत में खोजें',
|
|
392
|
+
findPlaceholder: 'बातचीत में खोजें…', findPlaceholderSelection: 'चयन में खोजें…',
|
|
393
|
+
findPrev: 'पिछला मेल (Shift+F3)', findNext: 'अगला मेल (F3)', findClose: 'बंद करें (Esc)',
|
|
394
|
+
findCase: 'केस मिलाएँ', findWhole: 'पूरा शब्द', findWrap: 'चक्रीय खोज', findReverse: 'उल्टा खोजें',
|
|
395
|
+
findMode: 'मोड', modeNormal: 'सामान्य', modeExtended: 'विस्तृत', modeRegex: 'रेगेक्स',
|
|
396
|
+
findScope: 'चयन में', findScopeChip: 'चयन {n} अक्षर', findSessionChip: 'यह बातचीत',
|
|
397
|
+
findNoMatch: 'कोई मेल नहीं', findEmpty: 'इस बातचीत में खोजने के लिए लिखें',
|
|
398
|
+
findInvalidRegex: 'अमान्य रेगुलर एक्सप्रेशन: {message}', findInvalidEscape: 'अमान्य विस्तृत एस्केप: {token}',
|
|
399
|
+
findTimeout: 'रेगेक्स का समय समाप्त; सरल बनाएँ', findRegexUnavailable: 'यहाँ रेगेक्स उपलब्ध नहीं है (वर्कर अवरुद्ध)', findWrappedTop: 'शीर्ष पर लौटा',
|
|
400
|
+
findWrappedBottom: 'अंत पर लौटा', findNoWrapEnd: 'अंतिम मेल (चक्र बंद)',
|
|
401
|
+
findNoWrapStart: 'पहला मेल (चक्र बंद)', findOlder: 'पुराने संदेश लोड कर पुन: प्रयास',
|
|
402
|
+
findLoadingOlder: 'पुराने संदेश लोड हो रहे हैं…', findLoadedOnly: 'केवल लोडेड विंडो · पुराना इतिहास लोड नहीं',
|
|
403
|
+
findCapped: 'सीमित', findCopied: 'मेल कॉपी हुआ', findSelectionLost: 'चयन समाप्त; पूरी बातचीत में खोज',
|
|
404
|
+
findNoSession: 'पहले एक बातचीत खोलें', findOptions: 'विकल्प',
|
|
405
|
+
findExtendedHelp: 'विस्तृत: \\n \\r \\t \\0 \\xHH \\\\',
|
|
406
|
+
panelTotal: 'चयनित {index}/{total}', panelRangeAll: 'पूरा इंडेक्स', panelRangeSession: 'यह बातचीत',
|
|
407
|
+
panelSessionHint: 'यह क्षेत्र केवल इंडेक्स किए संदेश खोजता है; लाइव खोज के लिए Ctrl+F दबाएँ',
|
|
408
|
+
panelRegexPaused: 'रेगेक्स मोड में सामग्री क्षेत्र रुका है', panelCountBounded: '≥ {n}', panelCountExact: '{n}',
|
|
409
|
+
contentWarning: 'इंडेक्स चेतावनी: {list}', contentBudget: 'सामग्री इंडेक्स बजट पूरा; कुछ फ़ाइलें इंडेक्स नहीं हैं', contentTooShort: 'सामग्री के लिए कम से कम 3 अक्षर चाहिए (छोटी खोज के लिए Ctrl+F)', contentTimeout: 'सामग्री खोज का समय समाप्त; क्वेरी या क्षेत्र घटाएँ', contentBadChars: 'सामग्री खोज NUL/नियंत्रण अक्षर नहीं लेती',
|
|
410
|
+
panelCaseShort: 'Aa', panelWholeShort: 'शब्द', panelModeLabel: 'मोड',
|
|
280
411
|
},
|
|
281
412
|
}
|
|
282
413
|
const LANG = (() => {
|
|
@@ -382,7 +513,8 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
382
513
|
const PREFS_KEY = 'dsh-locator.panel.v1'
|
|
383
514
|
const SORTS = ['mtime', 'name', 'size', 'path']
|
|
384
515
|
function readPrefs() {
|
|
385
|
-
const fallback = { scopes: { content: true, files: true, dirs: true }, sort: 'mtime', desc: true
|
|
516
|
+
const fallback = { scopes: { content: true, files: true, dirs: true }, sort: 'mtime', desc: true,
|
|
517
|
+
matchMode: 'normal', caseSensitive: false, wholeWord: false }
|
|
386
518
|
try {
|
|
387
519
|
const raw = window.localStorage.getItem(PREFS_KEY)
|
|
388
520
|
if (raw === null) return fallback
|
|
@@ -397,6 +529,9 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
397
529
|
},
|
|
398
530
|
sort: SORTS.includes(value.sort) ? value.sort : 'mtime',
|
|
399
531
|
desc: typeof value.desc === 'boolean' ? value.desc : true,
|
|
532
|
+
matchMode: ['normal', 'extended', 'regex'].includes(value.matchMode) ? value.matchMode : 'normal',
|
|
533
|
+
caseSensitive: typeof value.caseSensitive === 'boolean' ? value.caseSensitive : false,
|
|
534
|
+
wholeWord: typeof value.wholeWord === 'boolean' ? value.wholeWord : false,
|
|
400
535
|
}
|
|
401
536
|
} catch {
|
|
402
537
|
return fallback
|
|
@@ -410,7 +545,8 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
410
545
|
const HL_BASE = 'dsh-locator-find'
|
|
411
546
|
const HL_ACTIVE = 'dsh-locator-find-active'
|
|
412
547
|
const GESTURES = ['wheel', 'touchstart', 'pointerdown', 'keydown']
|
|
413
|
-
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]'
|
|
414
550
|
const BLOCK_SELECTOR = 'p,li,pre,td,th,h1,h2,h3,h4,h5,h6,blockquote,dd,dt'
|
|
415
551
|
const legacyTargets = new Set()
|
|
416
552
|
|
|
@@ -424,26 +560,39 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
424
560
|
legacyTargets.clear()
|
|
425
561
|
}
|
|
426
562
|
|
|
427
|
-
|
|
563
|
+
const HL_SCOPE = 'dsh-locator-find-scope'
|
|
564
|
+
let highlightOwner = null
|
|
565
|
+
|
|
566
|
+
function clearFindHighlight(owner) {
|
|
567
|
+
if (owner && highlightOwner !== null && owner !== highlightOwner) return
|
|
568
|
+
highlightOwner = null
|
|
428
569
|
clearLegacyTargets()
|
|
429
570
|
if (!highlightsAvailable()) return
|
|
430
571
|
CSS.highlights.delete(HL_BASE)
|
|
431
572
|
CSS.highlights.delete(HL_ACTIVE)
|
|
573
|
+
CSS.highlights.delete(HL_SCOPE)
|
|
432
574
|
}
|
|
433
575
|
|
|
434
|
-
function setFindHighlight(ranges) {
|
|
576
|
+
function setFindHighlight(ranges, activeIndex, owner) {
|
|
577
|
+
clearFindHighlight()
|
|
578
|
+
if (ranges.length === 0) return
|
|
579
|
+
highlightOwner = owner || null
|
|
580
|
+
const active = ranges[Math.max(0, Math.min(ranges.length - 1, activeIndex || 0))]
|
|
435
581
|
if (!highlightsAvailable()) {
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
if (row !== null) { row.classList.add('dsh-locator-target'); legacyTargets.add(row) }
|
|
441
|
-
}
|
|
582
|
+
const node = active.startContainer
|
|
583
|
+
const element = node instanceof Element ? node : node.parentElement
|
|
584
|
+
const row = element === null ? null : element.closest('[data-chat-node-key]')
|
|
585
|
+
if (row !== null) { row.classList.add('dsh-locator-target'); legacyTargets.add(row) }
|
|
442
586
|
return
|
|
443
587
|
}
|
|
444
|
-
if (ranges.length === 0) return
|
|
445
588
|
CSS.highlights.set(HL_BASE, new Highlight(...ranges))
|
|
446
|
-
CSS.highlights.set(HL_ACTIVE, new Highlight(
|
|
589
|
+
CSS.highlights.set(HL_ACTIVE, new Highlight(active))
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function setFindScope(range) {
|
|
593
|
+
if (!highlightsAvailable()) return
|
|
594
|
+
CSS.highlights.delete(HL_SCOPE)
|
|
595
|
+
if (range !== null) CSS.highlights.set(HL_SCOPE, new Highlight(range))
|
|
447
596
|
}
|
|
448
597
|
|
|
449
598
|
function isVisible(element) {
|
|
@@ -468,21 +617,28 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
468
617
|
return scope === null ? null : scope.querySelector('[data-chat-flow]')
|
|
469
618
|
}
|
|
470
619
|
|
|
471
|
-
function
|
|
472
|
-
if (flow === null) return
|
|
620
|
+
function findMessageRows(flow, messageId, nodeKey) {
|
|
621
|
+
if (flow === null) return []
|
|
473
622
|
const rows = flow.querySelectorAll('[data-chat-node-key]')
|
|
623
|
+
const exact = []
|
|
474
624
|
if (nodeKey) {
|
|
475
625
|
for (const row of rows) {
|
|
476
|
-
if (row.getAttribute('data-chat-node-key') === String(nodeKey))
|
|
626
|
+
if (row.getAttribute('data-chat-node-key') === String(nodeKey)) exact.push(row)
|
|
477
627
|
}
|
|
478
628
|
}
|
|
479
|
-
if (
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
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
|
+
}
|
|
484
635
|
}
|
|
485
|
-
|
|
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
|
+
})
|
|
486
642
|
}
|
|
487
643
|
|
|
488
644
|
function needlesFromQuery(query) {
|
|
@@ -492,57 +648,173 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
492
648
|
return [...new Set(words)].sort((a, b) => b.length - a.length).slice(0, 8)
|
|
493
649
|
}
|
|
494
650
|
|
|
495
|
-
function
|
|
496
|
-
|
|
651
|
+
function projectGroups(root, excluded) {
|
|
652
|
+
if (root === null) return []
|
|
497
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
|
+
}
|
|
498
685
|
let node = walker.nextNode()
|
|
499
686
|
while (node !== null) {
|
|
500
687
|
const parent = node.parentElement
|
|
501
|
-
if (
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
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))
|
|
509
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)
|
|
510
702
|
}
|
|
511
703
|
node = walker.nextNode()
|
|
512
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
|
+
}
|
|
513
736
|
return [...groups.values()]
|
|
514
737
|
}
|
|
515
738
|
|
|
516
|
-
function
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
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
|
+
}
|
|
522
782
|
}
|
|
523
783
|
const last = group.nodes[group.nodes.length - 1]
|
|
524
|
-
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
|
+
}
|
|
525
804
|
}
|
|
526
805
|
|
|
527
806
|
function rangesForNeedles(root, needles) {
|
|
528
807
|
const ranges = []
|
|
529
808
|
if (root === null || needles.length === 0) return ranges
|
|
809
|
+
const patterns = needles.map(escapeRegExp)
|
|
810
|
+
const expression = new RegExp(patterns.join('|'), 'giu')
|
|
530
811
|
for (const group of inlineGroups(root)) {
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
if (index < 0) break
|
|
538
|
-
const start = offsetInGroup(group, index)
|
|
539
|
-
const end = offsetInGroup(group, index + target.length)
|
|
540
|
-
const range = document.createRange()
|
|
541
|
-
range.setStart(start[0], start[1])
|
|
542
|
-
range.setEnd(end[0], end[1])
|
|
543
|
-
ranges.push(range)
|
|
544
|
-
from = index + target.length
|
|
545
|
-
}
|
|
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)
|
|
546
818
|
}
|
|
547
819
|
}
|
|
548
820
|
return ranges
|
|
@@ -575,16 +847,12 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
575
847
|
function phraseRange(root, snippet) {
|
|
576
848
|
if (root === null) return null
|
|
577
849
|
for (const phrase of phraseCandidates(snippet)) {
|
|
578
|
-
const
|
|
850
|
+
const expression = new RegExp(escapeRegExp(phrase), 'iu')
|
|
579
851
|
for (const group of inlineGroups(root)) {
|
|
580
|
-
const
|
|
581
|
-
if (
|
|
582
|
-
const
|
|
583
|
-
|
|
584
|
-
const range = document.createRange()
|
|
585
|
-
range.setStart(start[0], start[1])
|
|
586
|
-
range.setEnd(end[0], end[1])
|
|
587
|
-
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
|
|
588
856
|
}
|
|
589
857
|
}
|
|
590
858
|
return null
|
|
@@ -681,7 +949,7 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
681
949
|
let activeHighlight = null
|
|
682
950
|
|
|
683
951
|
function clearJumpHighlight() {
|
|
684
|
-
clearFindHighlight()
|
|
952
|
+
clearFindHighlight('jump')
|
|
685
953
|
if (activeHighlight !== null) {
|
|
686
954
|
const clear = activeHighlight
|
|
687
955
|
activeHighlight = null
|
|
@@ -708,15 +976,26 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
708
976
|
if (flow === null) { showNotice(t('jumpFailed')); return false }
|
|
709
977
|
const needles = needlesFromQuery(query)
|
|
710
978
|
const finder = () => {
|
|
711
|
-
const
|
|
712
|
-
if (
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
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
|
+
}
|
|
718
993
|
return null
|
|
719
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
|
|
720
999
|
const phrase = phraseRange(flow, hit.snippet)
|
|
721
1000
|
if (phrase === null) return null
|
|
722
1001
|
expandHidden(phrase.startContainer)
|
|
@@ -729,13 +1008,12 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
729
1008
|
let target = await waitFor(flow, finder, 6_000, 120)
|
|
730
1009
|
for (let attempt = 0; target === null && attempt < 10; attempt++) {
|
|
731
1010
|
if (stale()) return false
|
|
732
|
-
for (const hidden of flow.querySelectorAll('[hidden="until-found"]')) hidden.dispatchEvent(new Event('beforematch'))
|
|
733
1011
|
if (!revealOlder(scope)) break
|
|
734
1012
|
target = await waitFor(flow, finder, 1_500, 150)
|
|
735
1013
|
}
|
|
736
1014
|
if (stale()) return false
|
|
737
1015
|
if (target === null) { showNotice(t('phraseNotFound')); return false }
|
|
738
|
-
setFindHighlight(target.ranges)
|
|
1016
|
+
setFindHighlight(target.ranges, 0, 'jump')
|
|
739
1017
|
centerRange(target.ranges[0])
|
|
740
1018
|
const start = target.ranges[0].startContainer
|
|
741
1019
|
const chain = scrollChain(start instanceof Element ? start : start.parentElement)
|
|
@@ -753,7 +1031,7 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
753
1031
|
const fn = highlightCleanup.pop()
|
|
754
1032
|
try { fn() } catch { /* ignore */ }
|
|
755
1033
|
}
|
|
756
|
-
clearFindHighlight()
|
|
1034
|
+
clearFindHighlight('jump')
|
|
757
1035
|
}
|
|
758
1036
|
// Cancelling must drop the scroll lock too, not only the paint: the lock
|
|
759
1037
|
// owns timers and scroll/pointer listeners that would otherwise outlive
|
|
@@ -799,7 +1077,7 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
799
1077
|
const next = finder()
|
|
800
1078
|
if (next === null) { cancelJump(); return }
|
|
801
1079
|
target = next
|
|
802
|
-
setFindHighlight(target.ranges)
|
|
1080
|
+
setFindHighlight(target.ranges, 0, 'jump')
|
|
803
1081
|
centerRange(target.ranges[0])
|
|
804
1082
|
}, 1_000)
|
|
805
1083
|
highlightCleanup.push(() => clearInterval(sessionWatch))
|
|
@@ -812,6 +1090,641 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
812
1090
|
const PAGE_NAME = 50
|
|
813
1091
|
const PAGE_CONTENT = 30
|
|
814
1092
|
|
|
1093
|
+
// ── in-conversation find bar ────────────────────────────────────────────
|
|
1094
|
+
const FIND_MAX_MATCHES = 2000
|
|
1095
|
+
const FIND_MAX_CHARS = 4_000_000
|
|
1096
|
+
const FIND_BLOCK_SELECTOR = 'p,li,pre,td,th,h1,h2,h3,h4,h5,h6,blockquote,dd,dt'
|
|
1097
|
+
const FIND_EXCLUDED = '[hidden]:not([hidden="until-found"]),[data-chat-history],[data-text-interaction-surface],'
|
|
1098
|
+
+ '[data-composer-seat],[data-text-interaction-exclude],script,style,button,input,textarea,select,'
|
|
1099
|
+
+ '.sr-only,[class*="visuallyHidden"],[role="status"],[aria-live],.katex-mathml,[aria-hidden="true"]:not(.katex-html)'
|
|
1100
|
+
|
|
1101
|
+
function createFindStore() {
|
|
1102
|
+
const listeners = new Set()
|
|
1103
|
+
let state = {
|
|
1104
|
+
open: false, sessionId: '', scope: 'session', selection: null, query: '',
|
|
1105
|
+
mode: 'normal', caseSensitive: false, wholeWord: false, wrap: true, direction: 1,
|
|
1106
|
+
optionsOpen: false, count: 0, index: -1, capped: false, status: 'idle', error: '', hint: '',
|
|
1107
|
+
}
|
|
1108
|
+
return {
|
|
1109
|
+
get: () => state,
|
|
1110
|
+
subscribe(listener) {
|
|
1111
|
+
listeners.add(listener)
|
|
1112
|
+
return () => listeners.delete(listener)
|
|
1113
|
+
},
|
|
1114
|
+
set(patch) {
|
|
1115
|
+
state = { ...state, ...patch }
|
|
1116
|
+
for (const listener of listeners) listener()
|
|
1117
|
+
},
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
const findStore = createFindStore()
|
|
1121
|
+
const findApi = { move: () => false, cancel: () => {}, focus: () => {} }
|
|
1122
|
+
function useFindState() {
|
|
1123
|
+
return useSyncExternalStore(findStore.subscribe, findStore.get, findStore.get)
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
function selectionSnapshot(scope) {
|
|
1127
|
+
if (scope === null || typeof window.getSelection !== 'function') return null
|
|
1128
|
+
const selection = window.getSelection()
|
|
1129
|
+
if (selection === null || selection.rangeCount !== 1 || selection.isCollapsed) return null
|
|
1130
|
+
const range = selection.getRangeAt(0).cloneRange()
|
|
1131
|
+
if (!scope.contains(range.commonAncestorContainer)) return null
|
|
1132
|
+
const startEl = range.startContainer instanceof Element ? range.startContainer : range.startContainer.parentElement
|
|
1133
|
+
if (startEl !== null && startEl.closest('[data-composer-seat]') !== null) return null
|
|
1134
|
+
const text = String(selection.toString() || '').replace(/\s+/g, ' ').trim()
|
|
1135
|
+
if (text === '') return null
|
|
1136
|
+
return { range, text: text.slice(0, 500), chars: text.length }
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
function openFind(options) {
|
|
1140
|
+
const opts = options || {}
|
|
1141
|
+
let scope = opts.sessionId ? conversationScope(opts.sessionId) : null
|
|
1142
|
+
if (scope === null) scope = conversationScope('')
|
|
1143
|
+
if (scope === null) { showNotice(t('findNoSession')); return false }
|
|
1144
|
+
const sessionId = scope.getAttribute('data-conversation-session') || opts.sessionId || ''
|
|
1145
|
+
const selection = opts.selection === false ? null : selectionSnapshot(scope)
|
|
1146
|
+
const useSelection = opts.selection === true && selection !== null
|
|
1147
|
+
findApi.cancel()
|
|
1148
|
+
cancelJump()
|
|
1149
|
+
if (store.isOpen()) store.set(false)
|
|
1150
|
+
findStore.set({
|
|
1151
|
+
open: true, sessionId, scope: useSelection ? 'selection' : 'session',
|
|
1152
|
+
selection: useSelection ? selection : null, query: '', mode: 'normal',
|
|
1153
|
+
caseSensitive: false, wholeWord: false, wrap: true, direction: 1,
|
|
1154
|
+
optionsOpen: false, count: 0, index: -1, capped: false, status: 'idle', error: '', hint: '',
|
|
1155
|
+
})
|
|
1156
|
+
return true
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
function closeFind() {
|
|
1160
|
+
if (!findStore.get().open) return
|
|
1161
|
+
findStore.set({ open: false, selection: null, query: '', error: '', hint: '', status: 'idle' })
|
|
1162
|
+
findApi.cancel()
|
|
1163
|
+
clearFindHighlight('find')
|
|
1164
|
+
setFindScope(null)
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
const FIND_WORKER_SRC = `
|
|
1168
|
+
self.onmessage = event => {
|
|
1169
|
+
const { id, pattern, flags, texts } = event.data
|
|
1170
|
+
try {
|
|
1171
|
+
const re = new RegExp(pattern, flags)
|
|
1172
|
+
const out = []
|
|
1173
|
+
for (let ti = 0; ti < texts.length; ti++) {
|
|
1174
|
+
const text = texts[ti]
|
|
1175
|
+
re.lastIndex = 0
|
|
1176
|
+
let match
|
|
1177
|
+
while ((match = re.exec(text)) !== null) {
|
|
1178
|
+
if (match[0].length === 0) { re.lastIndex++; continue }
|
|
1179
|
+
out.push([ti, match.index, match.index + match[0].length])
|
|
1180
|
+
if (out.length >= 2000) break
|
|
1181
|
+
}
|
|
1182
|
+
if (out.length >= 2000) break
|
|
1183
|
+
}
|
|
1184
|
+
self.postMessage({ id, ok: true, matches: out, capped: out.length >= 2000 })
|
|
1185
|
+
} catch (error) {
|
|
1186
|
+
self.postMessage({ id, ok: false, error: String(error && error.message || error) })
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
`
|
|
1190
|
+
|
|
1191
|
+
function scanRegexInWorker(texts, source, flags) {
|
|
1192
|
+
if (typeof Worker !== 'function' || typeof Blob !== 'function' || typeof URL.createObjectURL !== 'function') {
|
|
1193
|
+
return Promise.resolve(null)
|
|
1194
|
+
}
|
|
1195
|
+
return new Promise(resolve => {
|
|
1196
|
+
let worker = null
|
|
1197
|
+
let url = ''
|
|
1198
|
+
let done = false
|
|
1199
|
+
const finish = value => {
|
|
1200
|
+
if (done) return
|
|
1201
|
+
done = true
|
|
1202
|
+
clearTimeout(timer)
|
|
1203
|
+
try { if (worker !== null) worker.terminate() } catch { /* ignore */ }
|
|
1204
|
+
try { if (url !== '') URL.revokeObjectURL(url) } catch { /* ignore */ }
|
|
1205
|
+
resolve(value)
|
|
1206
|
+
}
|
|
1207
|
+
const timer = setTimeout(() => finish({ timeout: true }), 500)
|
|
1208
|
+
try {
|
|
1209
|
+
const blob = new Blob([FIND_WORKER_SRC], { type: 'text/javascript' })
|
|
1210
|
+
url = URL.createObjectURL(blob)
|
|
1211
|
+
worker = new Worker(url)
|
|
1212
|
+
worker.onmessage = event => finish(event.data)
|
|
1213
|
+
worker.onerror = () => finish(null)
|
|
1214
|
+
worker.postMessage({ id: 1, pattern: source, flags, texts })
|
|
1215
|
+
} catch {
|
|
1216
|
+
finish(null)
|
|
1217
|
+
}
|
|
1218
|
+
})
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
function expandExtended(input) {
|
|
1222
|
+
let out = ''
|
|
1223
|
+
for (let index = 0; index < input.length; index++) {
|
|
1224
|
+
const char = input[index]
|
|
1225
|
+
if (char !== '\\') { out += char; continue }
|
|
1226
|
+
const next = input[index + 1]
|
|
1227
|
+
if (next === undefined) { out += '\\'; continue }
|
|
1228
|
+
index++
|
|
1229
|
+
if (next === 'n') out += '\n'
|
|
1230
|
+
else if (next === 'r') out += '\r'
|
|
1231
|
+
else if (next === 't') out += '\t'
|
|
1232
|
+
else if (next === '0') out += '\u0000'
|
|
1233
|
+
else if (next === '\\') out += '\\'
|
|
1234
|
+
else if (next === 'x') {
|
|
1235
|
+
const hex = input.slice(index + 1, index + 3)
|
|
1236
|
+
if (!/^[0-9a-fA-F]{2}$/u.test(hex)) return { error: '\\x' }
|
|
1237
|
+
out += String.fromCharCode(parseInt(hex, 16))
|
|
1238
|
+
index += 2
|
|
1239
|
+
} else if (next === 'u') {
|
|
1240
|
+
const hex = input.slice(index + 1, index + 5)
|
|
1241
|
+
if (!/^[0-9a-fA-F]{4}$/u.test(hex)) return { error: '\\u' }
|
|
1242
|
+
out += String.fromCharCode(parseInt(hex, 16))
|
|
1243
|
+
index += 4
|
|
1244
|
+
} else out += '\\' + next
|
|
1245
|
+
}
|
|
1246
|
+
return { value: out }
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
function compileFind(query, mode, caseSensitive) {
|
|
1250
|
+
let source = String(query || '')
|
|
1251
|
+
if (mode === 'extended') {
|
|
1252
|
+
const expanded = expandExtended(source)
|
|
1253
|
+
if (expanded.error !== undefined) return { error: t('findInvalidEscape', { token: expanded.error }) }
|
|
1254
|
+
source = expanded.value
|
|
1255
|
+
}
|
|
1256
|
+
if (mode !== 'regex') source = escapeRegExp(source)
|
|
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).
|
|
1261
|
+
try {
|
|
1262
|
+
return { source, flags: flags + 'u', re: new RegExp(source, flags + 'u') }
|
|
1263
|
+
} catch (error) {
|
|
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) }) }
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
function isWordCharacter(char) {
|
|
1273
|
+
return typeof char === 'string' && char !== '' && /^[A-Za-z0-9_]$/u.test(char)
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
function wholeWordAt(text, start, end) {
|
|
1277
|
+
return !isWordCharacter(text[start - 1]) && !isWordCharacter(text[end])
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
function collectFindGroups(root) {
|
|
1281
|
+
return projectGroups(root, FIND_EXCLUDED)
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
function clipForGroup(group, range) {
|
|
1285
|
+
if (range === null) return null
|
|
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 }
|
|
1294
|
+
let nodeStart = 0
|
|
1295
|
+
let nodeEnd = length
|
|
1296
|
+
if (node === range.startContainer) nodeStart = range.startOffset
|
|
1297
|
+
if (node === range.endContainer) nodeEnd = range.endOffset
|
|
1298
|
+
if (nodeStart < nodeEnd) {
|
|
1299
|
+
if (rawStart === null) rawStart = entry.start + nodeStart
|
|
1300
|
+
rawEnd = entry.start + nodeEnd
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
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
|
|
1307
|
+
return [start, end]
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
function rangeForGroupOffsets(group, start, end) {
|
|
1311
|
+
if (group.nodes.length === 0) return null
|
|
1312
|
+
return rangeAt(group, start, end)
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
function findHistoryButton(scope) {
|
|
1316
|
+
return scope === null ? null : scope.querySelector('[data-chat-history] button')
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
function FindToggle(props) {
|
|
1320
|
+
const onClick = () => { openFind({ sessionId: props && props.sessionId ? props.sessionId : '', selection: false }) }
|
|
1321
|
+
return h('button', {
|
|
1322
|
+
className: 'dsh-locator-launch',
|
|
1323
|
+
title: t('findOpen') + ' (Ctrl+F)',
|
|
1324
|
+
'aria-label': t('findOpen'),
|
|
1325
|
+
onClick,
|
|
1326
|
+
}, '⌕')
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
function FindBar() {
|
|
1330
|
+
const state = useFindState()
|
|
1331
|
+
const inputRef = useRef(null)
|
|
1332
|
+
const barRef = useRef(null)
|
|
1333
|
+
const scopeRef = useRef(null)
|
|
1334
|
+
const matchesRef = useRef([])
|
|
1335
|
+
const activeRef = useRef(-1)
|
|
1336
|
+
const sequenceRef = useRef(0)
|
|
1337
|
+
const debounceRef = useRef(0)
|
|
1338
|
+
const observerRef = useRef(null)
|
|
1339
|
+
const revealTimersRef = useRef([])
|
|
1340
|
+
const revealFrameRef = useRef(0)
|
|
1341
|
+
const loadOlderTimerRef = useRef(0)
|
|
1342
|
+
const returnFocusRef = useRef(null)
|
|
1343
|
+
const [anchor, setAnchor] = useState(null)
|
|
1344
|
+
const composingRef = useRef(false)
|
|
1345
|
+
|
|
1346
|
+
const applyHighlight = useCallback((index) => {
|
|
1347
|
+
const matches = matchesRef.current
|
|
1348
|
+
if (matches.length === 0) { clearFindHighlight('find'); setFindScope(null); return }
|
|
1349
|
+
const safe = Math.max(0, Math.min(matches.length - 1, index))
|
|
1350
|
+
activeRef.current = safe
|
|
1351
|
+
setFindHighlight(matches.map(match => match.range), safe, 'find')
|
|
1352
|
+
setFindScope(state.scope === 'selection' && state.selection ? state.selection.range : null)
|
|
1353
|
+
}, [state.scope, state.selection])
|
|
1354
|
+
|
|
1355
|
+
const revealActive = useCallback(() => {
|
|
1356
|
+
const match = matchesRef.current[activeRef.current]
|
|
1357
|
+
if (!match || !match.range.startContainer.isConnected) return false
|
|
1358
|
+
expandHidden(match.range.startContainer)
|
|
1359
|
+
const run = () => {
|
|
1360
|
+
if (!match.range.startContainer.isConnected) return
|
|
1361
|
+
centerRange(match.range)
|
|
1362
|
+
}
|
|
1363
|
+
if (typeof cancelAnimationFrame === 'function') cancelAnimationFrame(revealFrameRef.current)
|
|
1364
|
+
revealFrameRef.current = typeof requestAnimationFrame === 'function' ? requestAnimationFrame(run) : 0
|
|
1365
|
+
if (revealFrameRef.current === 0) run()
|
|
1366
|
+
for (const delay of [80, 240]) {
|
|
1367
|
+
const timer = setTimeout(run, delay)
|
|
1368
|
+
revealTimersRef.current.push(timer)
|
|
1369
|
+
}
|
|
1370
|
+
return true
|
|
1371
|
+
}, [])
|
|
1372
|
+
|
|
1373
|
+
const move = useCallback(delta => {
|
|
1374
|
+
const matches = matchesRef.current
|
|
1375
|
+
if (matches.length === 0) return false
|
|
1376
|
+
const direction = delta === 0 ? (findStore.get().direction === -1 ? -1 : 1) : delta
|
|
1377
|
+
if (!findStore.get().wrap && (activeRef.current + direction >= matches.length || activeRef.current + direction < 0)) {
|
|
1378
|
+
findStore.set({ hint: direction > 0 ? t('findNoWrapEnd') : t('findNoWrapStart') })
|
|
1379
|
+
return false
|
|
1380
|
+
}
|
|
1381
|
+
let next = activeRef.current + direction
|
|
1382
|
+
if (next >= matches.length) next = 0
|
|
1383
|
+
if (next < 0) next = matches.length - 1
|
|
1384
|
+
const wrapped = (activeRef.current + direction >= matches.length) || (activeRef.current + direction < 0)
|
|
1385
|
+
activeRef.current = next
|
|
1386
|
+
applyHighlight(next)
|
|
1387
|
+
findStore.set({
|
|
1388
|
+
index: next,
|
|
1389
|
+
hint: wrapped ? (direction > 0 ? t('findWrappedTop') : t('findWrappedBottom')) : '',
|
|
1390
|
+
})
|
|
1391
|
+
revealActive()
|
|
1392
|
+
return true
|
|
1393
|
+
}, [applyHighlight, revealActive])
|
|
1394
|
+
|
|
1395
|
+
const runScan = useCallback(async (keepActive) => {
|
|
1396
|
+
const token = ++sequenceRef.current
|
|
1397
|
+
const scope = scopeRef.current || conversationScope(state.sessionId) || conversationScope('')
|
|
1398
|
+
if (scope === null) { return }
|
|
1399
|
+
scopeRef.current = scope
|
|
1400
|
+
if (state.query === '') {
|
|
1401
|
+
matchesRef.current = []
|
|
1402
|
+
activeRef.current = -1
|
|
1403
|
+
clearFindHighlight('find')
|
|
1404
|
+
setFindScope(state.scope === 'selection' && state.selection ? state.selection.range : null)
|
|
1405
|
+
findStore.set({ count: 0, index: -1, capped: false, status: 'idle', error: '', hint: '' })
|
|
1406
|
+
return
|
|
1407
|
+
}
|
|
1408
|
+
const compiled = compileFind(state.query, state.mode, state.caseSensitive)
|
|
1409
|
+
if (compiled.error !== undefined) {
|
|
1410
|
+
matchesRef.current = []
|
|
1411
|
+
activeRef.current = -1
|
|
1412
|
+
clearFindHighlight('find')
|
|
1413
|
+
findStore.set({ count: 0, index: -1, capped: false, status: 'error', error: compiled.error })
|
|
1414
|
+
return
|
|
1415
|
+
}
|
|
1416
|
+
let selectionRange = null
|
|
1417
|
+
if (state.scope === 'selection' && state.selection) {
|
|
1418
|
+
selectionRange = state.selection.range
|
|
1419
|
+
let selectionDead = !selectionRange.startContainer.isConnected || selectionRange.collapsed
|
|
1420
|
+
if (!selectionDead) {
|
|
1421
|
+
try { selectionDead = selectionRange.toString().trim() === '' } catch { selectionDead = true }
|
|
1422
|
+
}
|
|
1423
|
+
if (selectionDead) {
|
|
1424
|
+
findStore.set({ scope: 'session', selection: null, hint: t('findSelectionLost') })
|
|
1425
|
+
return
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
const groups = collectFindGroups(scope)
|
|
1429
|
+
const texts = []
|
|
1430
|
+
const clips = []
|
|
1431
|
+
let chars = 0
|
|
1432
|
+
for (const group of groups) {
|
|
1433
|
+
const clip = selectionRange === null ? null : clipForGroup(group, selectionRange)
|
|
1434
|
+
if (selectionRange !== null && clip === null) { texts.push(null); clips.push(null); continue }
|
|
1435
|
+
const start = clip === null ? 0 : clip[0]
|
|
1436
|
+
const end = clip === null ? group.text.length : clip[1]
|
|
1437
|
+
const text = group.text.slice(start, end)
|
|
1438
|
+
chars += text.length
|
|
1439
|
+
if (chars > FIND_MAX_CHARS) { texts.push(null); clips.push(null); continue }
|
|
1440
|
+
texts.push(text)
|
|
1441
|
+
clips.push(clip === null ? [0, group.text.length] : clip)
|
|
1442
|
+
}
|
|
1443
|
+
if (selectionRange !== null && clips.every(clip => clip === null)) {
|
|
1444
|
+
findStore.set({ scope: 'session', selection: null, hint: t('findSelectionLost') })
|
|
1445
|
+
return
|
|
1446
|
+
}
|
|
1447
|
+
let rawMatches = null
|
|
1448
|
+
let capped = false
|
|
1449
|
+
if (state.mode === 'regex') {
|
|
1450
|
+
try {
|
|
1451
|
+
const result = await scanRegexInWorker(texts.filter(text => text !== null), compiled.source, compiled.flags)
|
|
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
|
+
}
|
|
1457
|
+
if (result !== null) {
|
|
1458
|
+
if (result.timeout === true) {
|
|
1459
|
+
matchesRef.current = []
|
|
1460
|
+
clearFindHighlight('find')
|
|
1461
|
+
findStore.set({ count: 0, index: -1, capped: false, status: 'error', error: t('findTimeout') })
|
|
1462
|
+
return
|
|
1463
|
+
}
|
|
1464
|
+
if (result.ok === false) {
|
|
1465
|
+
matchesRef.current = []
|
|
1466
|
+
clearFindHighlight('find')
|
|
1467
|
+
findStore.set({ count: 0, index: -1, capped: false, status: 'error', error: t('findInvalidRegex', { message: String(result.error || '') }) })
|
|
1468
|
+
return
|
|
1469
|
+
}
|
|
1470
|
+
const compact = []
|
|
1471
|
+
let cursor = 0
|
|
1472
|
+
for (let groupIndex = 0; groupIndex < texts.length; groupIndex++) {
|
|
1473
|
+
if (texts[groupIndex] === null) continue
|
|
1474
|
+
compact.push(groupIndex)
|
|
1475
|
+
cursor++
|
|
1476
|
+
}
|
|
1477
|
+
rawMatches = []
|
|
1478
|
+
for (const item of result.matches) {
|
|
1479
|
+
const groupIndex = compact[item[0]]
|
|
1480
|
+
if (groupIndex === undefined) continue
|
|
1481
|
+
rawMatches.push({ groupIndex, start: item[1] + clips[groupIndex][0], end: item[2] + clips[groupIndex][0] })
|
|
1482
|
+
}
|
|
1483
|
+
capped = result.capped === true
|
|
1484
|
+
}
|
|
1485
|
+
} catch { /* fall back below */ }
|
|
1486
|
+
}
|
|
1487
|
+
if (rawMatches === null) {
|
|
1488
|
+
rawMatches = []
|
|
1489
|
+
const re = new RegExp(compiled.source, compiled.flags)
|
|
1490
|
+
const started = performance.now()
|
|
1491
|
+
for (let groupIndex = 0; groupIndex < texts.length; groupIndex++) {
|
|
1492
|
+
const text = texts[groupIndex]
|
|
1493
|
+
if (text === null) continue
|
|
1494
|
+
re.lastIndex = 0
|
|
1495
|
+
let match
|
|
1496
|
+
while ((match = re.exec(text)) !== null) {
|
|
1497
|
+
if (match[0].length === 0) { re.lastIndex++; continue }
|
|
1498
|
+
const start = clips[groupIndex][0] + match.index
|
|
1499
|
+
const end = start + match[0].length
|
|
1500
|
+
if (state.wholeWord && !wholeWordAt(groups[groupIndex].text, start, end)) continue
|
|
1501
|
+
rawMatches.push({ groupIndex, start, end })
|
|
1502
|
+
if (rawMatches.length >= FIND_MAX_MATCHES) break
|
|
1503
|
+
}
|
|
1504
|
+
if (rawMatches.length >= FIND_MAX_MATCHES) break
|
|
1505
|
+
if (performance.now() - started > 1500) { capped = true; break }
|
|
1506
|
+
}
|
|
1507
|
+
capped = capped || rawMatches.length >= FIND_MAX_MATCHES
|
|
1508
|
+
} else if (state.wholeWord) {
|
|
1509
|
+
rawMatches = rawMatches.filter(raw => {
|
|
1510
|
+
const group = groups[raw.groupIndex]
|
|
1511
|
+
return group !== undefined && wholeWordAt(group.text, raw.start, raw.end)
|
|
1512
|
+
})
|
|
1513
|
+
capped = capped || rawMatches.length >= FIND_MAX_MATCHES
|
|
1514
|
+
}
|
|
1515
|
+
if (token !== sequenceRef.current) return
|
|
1516
|
+
const matches = []
|
|
1517
|
+
for (const raw of rawMatches) {
|
|
1518
|
+
const group = groups[raw.groupIndex]
|
|
1519
|
+
if (group === undefined) continue
|
|
1520
|
+
const range = rangeForGroupOffsets(group, raw.start, raw.end)
|
|
1521
|
+
if (range === null) continue
|
|
1522
|
+
const row = group.block.closest('[data-chat-node-key]')
|
|
1523
|
+
matches.push({ range, rowKey: row === null ? '' : (row.getAttribute('data-chat-node-key') || '') })
|
|
1524
|
+
}
|
|
1525
|
+
if (token !== sequenceRef.current) return
|
|
1526
|
+
matchesRef.current = matches
|
|
1527
|
+
const next = matches.length === 0 ? -1 : (keepActive && activeRef.current >= 0 ? Math.min(activeRef.current, matches.length - 1) : 0)
|
|
1528
|
+
activeRef.current = next
|
|
1529
|
+
const history = findHistoryButton(scopeRef.current)
|
|
1530
|
+
findStore.set({
|
|
1531
|
+
count: matches.length,
|
|
1532
|
+
index: next,
|
|
1533
|
+
capped,
|
|
1534
|
+
status: matches.length === 0 ? 'none' : 'idle',
|
|
1535
|
+
error: '',
|
|
1536
|
+
hint: matches.length === 0 && history !== null && !history.disabled ? t('findLoadedOnly') : '',
|
|
1537
|
+
})
|
|
1538
|
+
if (next >= 0) { applyHighlight(next); revealActive() }
|
|
1539
|
+
else { clearFindHighlight('find'); setFindScope(state.scope === 'selection' && state.selection ? state.selection.range : null) }
|
|
1540
|
+
}, [applyHighlight, revealActive, state.caseSensitive, state.mode, state.query, state.scope, state.selection, state.sessionId, state.wholeWord])
|
|
1541
|
+
const runScanRef = useRef(runScan)
|
|
1542
|
+
runScanRef.current = runScan
|
|
1543
|
+
|
|
1544
|
+
useEffect(() => {
|
|
1545
|
+
if (!state.open) return undefined
|
|
1546
|
+
returnFocusRef.current = document.activeElement instanceof HTMLElement ? document.activeElement : null
|
|
1547
|
+
const measure = () => {
|
|
1548
|
+
const scope = conversationScope(findStore.get().sessionId) || conversationScope('')
|
|
1549
|
+
if (scope === null) { closeFind(); return }
|
|
1550
|
+
scopeRef.current = scope
|
|
1551
|
+
const rect = scope.getBoundingClientRect()
|
|
1552
|
+
const width = Math.max(280, Math.min(560, rect.width - 24))
|
|
1553
|
+
setAnchor({ top: Math.max(8, rect.top + 8), right: Math.max(8, window.innerWidth - rect.right + 12), width })
|
|
1554
|
+
}
|
|
1555
|
+
measure()
|
|
1556
|
+
const timer = setTimeout(() => {
|
|
1557
|
+
if (inputRef.current !== null) { inputRef.current.focus(); inputRef.current.select() }
|
|
1558
|
+
}, 0)
|
|
1559
|
+
const onResize = () => measure()
|
|
1560
|
+
window.addEventListener('resize', onResize)
|
|
1561
|
+
let observer = null
|
|
1562
|
+
const schedule = () => {
|
|
1563
|
+
clearTimeout(debounceRef.current)
|
|
1564
|
+
debounceRef.current = setTimeout(() => { if (findStore.get().open) void runScanRef.current(true) }, 400)
|
|
1565
|
+
}
|
|
1566
|
+
const scope = scopeRef.current
|
|
1567
|
+
const observed = scope === null ? null : (chatFlow(scope) || scope)
|
|
1568
|
+
if (observed !== null) {
|
|
1569
|
+
try {
|
|
1570
|
+
observer = new MutationObserver(schedule)
|
|
1571
|
+
observer.observe(observed, { childList: true, subtree: true, characterData: true })
|
|
1572
|
+
} catch { /* observer unavailable */ }
|
|
1573
|
+
}
|
|
1574
|
+
return () => {
|
|
1575
|
+
clearTimeout(timer)
|
|
1576
|
+
window.removeEventListener('resize', onResize)
|
|
1577
|
+
clearTimeout(debounceRef.current)
|
|
1578
|
+
if (observer !== null) observer.disconnect()
|
|
1579
|
+
for (const id of revealTimersRef.current) clearTimeout(id)
|
|
1580
|
+
revealTimersRef.current = []
|
|
1581
|
+
if (typeof cancelAnimationFrame === 'function') cancelAnimationFrame(revealFrameRef.current)
|
|
1582
|
+
clearTimeout(loadOlderTimerRef.current)
|
|
1583
|
+
sequenceRef.current++
|
|
1584
|
+
clearFindHighlight('find')
|
|
1585
|
+
setFindScope(null)
|
|
1586
|
+
const target = returnFocusRef.current
|
|
1587
|
+
if (target !== null && target.isConnected) target.focus({ preventScroll: true })
|
|
1588
|
+
}
|
|
1589
|
+
}, [state.open])
|
|
1590
|
+
|
|
1591
|
+
useEffect(() => {
|
|
1592
|
+
if (!state.open) return undefined
|
|
1593
|
+
const timer = setTimeout(() => { void runScan(false) }, 150)
|
|
1594
|
+
return () => clearTimeout(timer)
|
|
1595
|
+
}, [state.open, state.query, state.mode, state.caseSensitive, state.wholeWord, state.scope, state.sessionId, state.selection, runScan])
|
|
1596
|
+
|
|
1597
|
+
// Close when the conversation is switched, hidden, or the panel opens.
|
|
1598
|
+
useEffect(() => {
|
|
1599
|
+
if (!state.open) return undefined
|
|
1600
|
+
const timer = setInterval(() => {
|
|
1601
|
+
if (!findStore.get().open) return
|
|
1602
|
+
if (store.isOpen()) { closeFind(); return }
|
|
1603
|
+
if (conversationScope(findStore.get().sessionId) === null) closeFind()
|
|
1604
|
+
}, 800)
|
|
1605
|
+
return () => clearInterval(timer)
|
|
1606
|
+
}, [state.open])
|
|
1607
|
+
|
|
1608
|
+
useEffect(() => {
|
|
1609
|
+
findApi.move = delta => move(delta)
|
|
1610
|
+
findApi.cancel = () => { sequenceRef.current++ }
|
|
1611
|
+
findApi.focus = () => { if (inputRef.current !== null) { inputRef.current.focus(); inputRef.current.select() } }
|
|
1612
|
+
return () => { findApi.move = () => false; findApi.cancel = () => {}; findApi.focus = () => {} }
|
|
1613
|
+
}, [move])
|
|
1614
|
+
|
|
1615
|
+
if (!state.open) return null
|
|
1616
|
+
const onInput = event => { findStore.set({ query: event.target.value, error: '', hint: '' }) }
|
|
1617
|
+
const onKeyDown = event => {
|
|
1618
|
+
if (composingRef.current || event.isComposing) return
|
|
1619
|
+
if (event.key === 'Enter') { event.preventDefault(); move(event.shiftKey ? -1 : 0); return }
|
|
1620
|
+
if (event.key === 'Escape') { event.preventDefault(); closeFind(); return }
|
|
1621
|
+
if ((event.ctrlKey || event.metaKey) && event.shiftKey && String(event.key).toLowerCase() === 'c') {
|
|
1622
|
+
const match = matchesRef.current[activeRef.current]
|
|
1623
|
+
const text = match && match.range.startContainer.isConnected ? match.range.toString() : findStore.get().query
|
|
1624
|
+
if (text !== '') { event.preventDefault(); copyText(text).then(() => findStore.set({ hint: t('findCopied') })).catch(() => {}) }
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
const pickMode = event => {
|
|
1628
|
+
findStore.set({ mode: event.target.value })
|
|
1629
|
+
if (inputRef.current !== null) inputRef.current.focus()
|
|
1630
|
+
}
|
|
1631
|
+
const toggle = key => { findStore.set({ [key]: !findStore.get()[key] }) }
|
|
1632
|
+
const toggleScope = () => {
|
|
1633
|
+
if (state.scope === 'selection') findStore.set({ scope: 'session', selection: null, hint: '' })
|
|
1634
|
+
else {
|
|
1635
|
+
const scope = scopeRef.current
|
|
1636
|
+
const selection = scope === null ? null : selectionSnapshot(scope)
|
|
1637
|
+
if (selection === null) { findStore.set({ hint: t('findPlaceholderSelection') }); return }
|
|
1638
|
+
findStore.set({ scope: 'selection', selection })
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
const countText = state.query === '' ? '—'
|
|
1642
|
+
: (state.count === 0 ? '0/0' : (state.index < 0 ? '…' : (state.index + 1) + '/' + state.count + (state.capped ? '+' : '')))
|
|
1643
|
+
const history = scopeRef.current === null ? null : findHistoryButton(scopeRef.current)
|
|
1644
|
+
const canLoadOlder = history !== null && !history.disabled
|
|
1645
|
+
const loadOlder = () => {
|
|
1646
|
+
if (scopeRef.current === null || !revealOlder(scopeRef.current)) return
|
|
1647
|
+
findStore.set({ hint: t('findLoadingOlder') })
|
|
1648
|
+
clearTimeout(loadOlderTimerRef.current)
|
|
1649
|
+
loadOlderTimerRef.current = setTimeout(() => { if (findStore.get().open) void runScanRef.current(true) }, 700)
|
|
1650
|
+
}
|
|
1651
|
+
return h('div', {
|
|
1652
|
+
ref: barRef,
|
|
1653
|
+
className: 'dsh-locator-findbar',
|
|
1654
|
+
role: 'search',
|
|
1655
|
+
'aria-label': t('findAria'),
|
|
1656
|
+
style: anchor === null ? { top: 12, right: 12, width: 'min(560px,calc(100vw - 24px))' } : { top: anchor.top, right: anchor.right, width: anchor.width },
|
|
1657
|
+
},
|
|
1658
|
+
h('div', { className: 'dsh-locator-findbar-main' },
|
|
1659
|
+
h('span', { className: 'dsh-locator-findbar-glyph', 'aria-hidden': 'true' }, '⌕'),
|
|
1660
|
+
h('input', {
|
|
1661
|
+
ref: inputRef,
|
|
1662
|
+
className: 'dsh-locator-findbar-input',
|
|
1663
|
+
value: state.query,
|
|
1664
|
+
placeholder: state.scope === 'selection' ? t('findPlaceholderSelection') : t('findPlaceholder'),
|
|
1665
|
+
'aria-label': t('findAria'),
|
|
1666
|
+
'aria-invalid': state.error !== '' ? 'true' : undefined,
|
|
1667
|
+
spellCheck: false,
|
|
1668
|
+
autoComplete: 'off',
|
|
1669
|
+
onInput,
|
|
1670
|
+
onKeyDown,
|
|
1671
|
+
onCompositionStart: () => { composingRef.current = true },
|
|
1672
|
+
onCompositionEnd: () => { composingRef.current = false; void runScan(false) },
|
|
1673
|
+
}),
|
|
1674
|
+
h('span', { className: 'dsh-locator-findbar-count', 'aria-hidden': 'true' }, countText),
|
|
1675
|
+
h('span', { style: { position: 'absolute', width: 1, height: 1, overflow: 'hidden', clip: 'rect(0 0 0 0)' }, role: 'status', 'aria-live': 'polite', 'aria-atomic': 'true' },
|
|
1676
|
+
state.error !== '' ? state.error
|
|
1677
|
+
: state.query === '' ? ''
|
|
1678
|
+
: state.count === 0 ? t('findNoMatch')
|
|
1679
|
+
: (state.index + 1) + '/' + state.count),
|
|
1680
|
+
h('button', { className: 'dsh-locator-findbar-btn', title: t('findPrev'), 'aria-label': t('findPrev'),
|
|
1681
|
+
disabled: state.count === 0, onMouseDown: event => event.preventDefault(), onClick: () => move(-1) }, '↑'),
|
|
1682
|
+
h('button', { className: 'dsh-locator-findbar-btn', title: t('findNext'), 'aria-label': t('findNext'),
|
|
1683
|
+
disabled: state.count === 0, onMouseDown: event => event.preventDefault(), onClick: () => move(1) }, '↓'),
|
|
1684
|
+
h('button', { className: 'dsh-locator-findbar-btn', title: t('findClose'), 'aria-label': t('findClose'),
|
|
1685
|
+
onClick: () => closeFind() }, '×'),
|
|
1686
|
+
),
|
|
1687
|
+
h('div', { className: 'dsh-locator-findbar-opts', onMouseDown: event => {
|
|
1688
|
+
if (event.target instanceof HTMLSelectElement) return
|
|
1689
|
+
event.preventDefault()
|
|
1690
|
+
} },
|
|
1691
|
+
h('label', { className: 'dsh-locator-findbar-label' }, t('findMode'),
|
|
1692
|
+
h('select', { value: state.mode, onChange: pickMode, 'aria-label': t('findMode') },
|
|
1693
|
+
h('option', { value: 'normal' }, t('modeNormal')),
|
|
1694
|
+
h('option', { value: 'extended' }, t('modeExtended')),
|
|
1695
|
+
h('option', { value: 'regex' }, t('modeRegex')),
|
|
1696
|
+
)),
|
|
1697
|
+
h('button', { className: 'dsh-locator-findbar-btn', title: t('findCase'), 'aria-label': t('findCase'),
|
|
1698
|
+
'aria-pressed': state.caseSensitive ? 'true' : 'false', onClick: () => toggle('caseSensitive') }, t('panelCaseShort')),
|
|
1699
|
+
h('button', { className: 'dsh-locator-findbar-btn', title: t('findWhole'), 'aria-label': t('findWhole'),
|
|
1700
|
+
'aria-pressed': state.wholeWord ? 'true' : 'false', onClick: () => toggle('wholeWord') }, t('panelWholeShort')),
|
|
1701
|
+
h('button', { className: 'dsh-locator-findbar-btn', title: t('findWrap'), 'aria-label': t('findWrap'),
|
|
1702
|
+
'aria-pressed': state.wrap ? 'true' : 'false', onClick: () => toggle('wrap') }, '↻'),
|
|
1703
|
+
h('button', { className: 'dsh-locator-findbar-btn', title: t('findReverse'), 'aria-label': t('findReverse'),
|
|
1704
|
+
'aria-pressed': state.direction === -1 ? 'true' : 'false', onClick: () => findStore.set({ direction: state.direction === -1 ? 1 : -1 }) }, '⇅'),
|
|
1705
|
+
h('button', { className: 'dsh-locator-findbar-btn', title: t('findScope'), 'aria-label': t('findScope'),
|
|
1706
|
+
'aria-pressed': state.scope === 'selection' ? 'true' : 'false', onClick: toggleScope }, '⛶'),
|
|
1707
|
+
state.scope === 'selection' && state.selection
|
|
1708
|
+
? h('span', { className: 'dsh-locator-findbar-chip' }, t('findScopeChip', { n: state.selection.chars }),
|
|
1709
|
+
h('button', { onClick: () => findStore.set({ scope: 'session', selection: null, hint: '' }), 'aria-label': t('findClose') }, '×'))
|
|
1710
|
+
: h('span', { className: 'dsh-locator-findbar-chip' }, t('findSessionChip')),
|
|
1711
|
+
state.mode === 'extended' ? h('span', { className: 'dsh-locator-findbar-label', style: { opacity: .55 } }, t('findExtendedHelp')) : null,
|
|
1712
|
+
),
|
|
1713
|
+
state.error !== '' ? h('div', { className: 'dsh-locator-findbar-error', role: 'alert' }, state.error) : null,
|
|
1714
|
+
h('div', { className: 'dsh-locator-findbar-status' },
|
|
1715
|
+
h('span', null, state.query === '' ? t('findEmpty') : state.count === 0 ? t('findNoMatch') : (state.capped ? t('findCapped') : '')),
|
|
1716
|
+
state.hint !== '' ? h('span', null, state.hint) : null,
|
|
1717
|
+
h('span', { className: 'grow' }),
|
|
1718
|
+
canLoadOlder && state.query !== '' && state.count === 0
|
|
1719
|
+
? h('button', { className: 'dsh-locator-dirbtn', onClick: loadOlder }, t('findOlder'))
|
|
1720
|
+
: null,
|
|
1721
|
+
h('kbd', null, 'Enter'), h('span', null, '↓'),
|
|
1722
|
+
h('kbd', null, 'Shift+Enter'), h('span', null, '↑'),
|
|
1723
|
+
h('kbd', null, 'Esc'),
|
|
1724
|
+
),
|
|
1725
|
+
)
|
|
1726
|
+
}
|
|
1727
|
+
|
|
815
1728
|
function Panel() {
|
|
816
1729
|
const open = useOpen()
|
|
817
1730
|
const prefs = useMemo(readPrefs, [])
|
|
@@ -825,6 +1738,10 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
825
1738
|
const [took, setTook] = useState(0)
|
|
826
1739
|
const [status, setStatus] = useState(null)
|
|
827
1740
|
const [availableContent, setAvailableContent] = useState(true)
|
|
1741
|
+
const [matchMode, setMatchMode] = useState(prefs.matchMode || 'normal')
|
|
1742
|
+
const [caseSensitive, setCaseSensitive] = useState(prefs.caseSensitive === true)
|
|
1743
|
+
const [wholeWord, setWholeWord] = useState(prefs.wholeWord === true)
|
|
1744
|
+
const [sessionOnly, setSessionOnly] = useState(false)
|
|
828
1745
|
const [selected, setSelected] = useState(0)
|
|
829
1746
|
const [toast, setToast] = useState('')
|
|
830
1747
|
const [refreshTick, setRefreshTick] = useState(0)
|
|
@@ -832,16 +1749,26 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
832
1749
|
const panelRef = useRef(null)
|
|
833
1750
|
const abortRef = useRef(null)
|
|
834
1751
|
const listRef = useRef(null)
|
|
1752
|
+
const toastTimerRef = useRef(0)
|
|
1753
|
+
const insertTimerRef = useRef(0)
|
|
835
1754
|
|
|
836
1755
|
const close = useCallback(() => store.set(false), [])
|
|
837
|
-
const
|
|
1756
|
+
const activeScope = conversationScope('')
|
|
1757
|
+
const activeSessionId = activeScope === null ? '' : (activeScope.getAttribute('data-conversation-session') || '')
|
|
1758
|
+
const effectiveScopes = sessionOnly
|
|
1759
|
+
? { content: scopes.content, files: false, dirs: false }
|
|
1760
|
+
: { content: scopes.content, files: scopes.files, dirs: scopes.dirs }
|
|
1761
|
+
const anyScope = effectiveScopes.content || effectiveScopes.files || effectiveScopes.dirs
|
|
838
1762
|
|
|
839
1763
|
useEffect(() => {
|
|
840
|
-
writePrefs({ scopes, sort, desc })
|
|
841
|
-
}, [scopes, sort, desc])
|
|
1764
|
+
writePrefs({ scopes, sort, desc, matchMode, caseSensitive, wholeWord })
|
|
1765
|
+
}, [scopes, sort, desc, matchMode, caseSensitive, wholeWord])
|
|
842
1766
|
|
|
843
1767
|
useEffect(() => {
|
|
844
1768
|
if (!open) return undefined
|
|
1769
|
+
if (sessionOnly && activeSessionId === '') setSessionOnly(false)
|
|
1770
|
+
closeFind()
|
|
1771
|
+
setGroups(null)
|
|
845
1772
|
const previous = document.activeElement
|
|
846
1773
|
setError('')
|
|
847
1774
|
const timer = setTimeout(() => {
|
|
@@ -852,9 +1779,24 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
852
1779
|
}, 30)
|
|
853
1780
|
return () => {
|
|
854
1781
|
clearTimeout(timer)
|
|
1782
|
+
if (abortRef.current !== null) { abortRef.current.abort(); abortRef.current = null }
|
|
1783
|
+
setGroups(null)
|
|
855
1784
|
if (previous instanceof HTMLElement && previous.isConnected) previous.focus()
|
|
856
1785
|
}
|
|
857
|
-
}, [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])
|
|
858
1800
|
|
|
859
1801
|
useEffect(() => {
|
|
860
1802
|
if (!open) return undefined
|
|
@@ -894,13 +1836,18 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
894
1836
|
setError('')
|
|
895
1837
|
post('/dsh-locator/combined', {
|
|
896
1838
|
query,
|
|
897
|
-
content:
|
|
898
|
-
files:
|
|
899
|
-
dirs:
|
|
1839
|
+
content: effectiveScopes.content,
|
|
1840
|
+
files: effectiveScopes.files,
|
|
1841
|
+
dirs: effectiveScopes.dirs,
|
|
900
1842
|
limit: PAGE_NAME,
|
|
901
1843
|
contentLimit: PAGE_CONTENT,
|
|
902
1844
|
sort,
|
|
903
1845
|
desc,
|
|
1846
|
+
matchMode,
|
|
1847
|
+
caseSensitive,
|
|
1848
|
+
wholeWord,
|
|
1849
|
+
sessionId: sessionOnly ? activeSessionId : '',
|
|
1850
|
+
count: matchMode === 'regex' ? 'none' : 'exact',
|
|
904
1851
|
}, controller.signal).then(data => {
|
|
905
1852
|
if (controller.signal.aborted) return
|
|
906
1853
|
setGroups({
|
|
@@ -918,7 +1865,7 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
918
1865
|
})
|
|
919
1866
|
}, 140)
|
|
920
1867
|
return () => clearTimeout(timer)
|
|
921
|
-
}, [open, query, sort, desc, scopes.content, scopes.files, scopes.dirs, refreshTick, anyScope])
|
|
1868
|
+
}, [open, query, sort, desc, scopes.content, scopes.files, scopes.dirs, matchMode, caseSensitive, wholeWord, sessionOnly, activeSessionId, refreshTick, anyScope, effectiveScopes.content, effectiveScopes.files, effectiveScopes.dirs])
|
|
922
1869
|
|
|
923
1870
|
const contentBuilding = groups !== null && groups.content !== undefined && groups.content.building === true
|
|
924
1871
|
useEffect(() => {
|
|
@@ -931,20 +1878,31 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
931
1878
|
if (groups === null) return []
|
|
932
1879
|
const out = []
|
|
933
1880
|
for (const group of ['content', 'files', 'dirs']) {
|
|
934
|
-
if (!
|
|
1881
|
+
if (!effectiveScopes[group]) continue
|
|
935
1882
|
const hits = groups[group] && Array.isArray(groups[group].hits) ? groups[group].hits : []
|
|
936
1883
|
for (const hit of hits) out.push({ group, hit })
|
|
937
1884
|
}
|
|
938
1885
|
return out
|
|
939
|
-
}, [groups,
|
|
1886
|
+
}, [groups, effectiveScopes.content, effectiveScopes.files, effectiveScopes.dirs])
|
|
940
1887
|
|
|
941
1888
|
useEffect(() => {
|
|
942
1889
|
setSelected(value => Math.min(Math.max(0, value), Math.max(0, rows.length - 1)))
|
|
943
1890
|
}, [rows.length])
|
|
944
1891
|
|
|
1892
|
+
useEffect(() => {
|
|
1893
|
+
if (!open) return
|
|
1894
|
+
const row = document.getElementById('dsh-locator-opt-' + selected)
|
|
1895
|
+
if (row !== null) row.scrollIntoView({ block: 'nearest' })
|
|
1896
|
+
}, [open, selected, rows.length])
|
|
1897
|
+
|
|
945
1898
|
const notify = useCallback(message => {
|
|
946
1899
|
setToast(message)
|
|
947
|
-
|
|
1900
|
+
clearTimeout(toastTimerRef.current)
|
|
1901
|
+
toastTimerRef.current = setTimeout(() => setToast(''), 1600)
|
|
1902
|
+
}, [])
|
|
1903
|
+
useEffect(() => () => {
|
|
1904
|
+
clearTimeout(toastTimerRef.current)
|
|
1905
|
+
clearTimeout(insertTimerRef.current)
|
|
948
1906
|
}, [])
|
|
949
1907
|
|
|
950
1908
|
const copy = useCallback(hit => {
|
|
@@ -976,7 +1934,8 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
976
1934
|
if (!hit || hit.kind === 'session' || loading) return
|
|
977
1935
|
const mention = '@"' + hit.path.replace(/"/g, '') + '" '
|
|
978
1936
|
close()
|
|
979
|
-
|
|
1937
|
+
clearTimeout(insertTimerRef.current)
|
|
1938
|
+
insertTimerRef.current = setTimeout(() => {
|
|
980
1939
|
if (insertIntoComposer(mention)) notify(t('inserted'))
|
|
981
1940
|
else copyText(mention).then(() => notify(t('insertFailed'))).catch(() => {})
|
|
982
1941
|
}, 80)
|
|
@@ -1008,7 +1967,8 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
1008
1967
|
&& (event.target.tagName === 'BUTTON' || event.target.tagName === 'SELECT'
|
|
1009
1968
|
|| (event.target.tagName === 'INPUT' && event.target.type !== 'text'))
|
|
1010
1969
|
if (interactive) return
|
|
1011
|
-
const
|
|
1970
|
+
const altGraph = typeof event.getModifierState === 'function' && event.getModifierState('AltGraph')
|
|
1971
|
+
const scopeDigit = event.altKey && !event.ctrlKey && !event.metaKey && !altGraph
|
|
1012
1972
|
? (event.code === 'Digit1' ? '1' : event.code === 'Digit2' ? '2' : event.code === 'Digit3' ? '3' : ['1', '2', '3'].includes(event.key) ? event.key : '')
|
|
1013
1973
|
: ''
|
|
1014
1974
|
if (scopeDigit !== '') {
|
|
@@ -1016,6 +1976,11 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
1016
1976
|
toggleScope(scopeDigit === '1' ? 'content' : scopeDigit === '2' ? 'files' : 'dirs')
|
|
1017
1977
|
return
|
|
1018
1978
|
}
|
|
1979
|
+
if (event.key === 'F3') {
|
|
1980
|
+
event.preventDefault()
|
|
1981
|
+
if (rows.length > 0) setSelected(value => (value + (event.shiftKey ? -1 : 1) + rows.length) % rows.length)
|
|
1982
|
+
return
|
|
1983
|
+
}
|
|
1019
1984
|
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
|
1020
1985
|
event.preventDefault()
|
|
1021
1986
|
setSelected(value => Math.max(0, Math.min(rows.length - 1, value + (event.key === 'ArrowDown' ? 1 : -1))))
|
|
@@ -1071,28 +2036,41 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
1071
2036
|
const content = groups !== null ? groups.content : undefined
|
|
1072
2037
|
const files = groups !== null ? groups.files : undefined
|
|
1073
2038
|
const dirs = groups !== null ? groups.dirs : undefined
|
|
2039
|
+
const countLabel = (group, fallback) => {
|
|
2040
|
+
const count = group && group.count
|
|
2041
|
+
if (count && typeof count.value === 'number' && Number.isFinite(count.value)) {
|
|
2042
|
+
return (count.kind === 'bounded' ? '≥ ' : '') + String(count.value)
|
|
2043
|
+
}
|
|
2044
|
+
return String(fallback)
|
|
2045
|
+
}
|
|
1074
2046
|
const contentCount = content === undefined ? 0 : (content.returned ?? 0)
|
|
1075
2047
|
const filesCount = files === undefined ? 0 : (files.returned ?? files.hits?.length ?? 0)
|
|
1076
2048
|
const dirsCount = dirs === undefined ? 0 : (dirs.returned ?? dirs.hits?.length ?? 0)
|
|
2049
|
+
const contentCountText = content === undefined ? '' : countLabel(content, contentCount)
|
|
2050
|
+
const filesCountText = files === undefined ? '' : countLabel(files, filesCount)
|
|
2051
|
+
const dirsCountText = dirs === undefined ? '' : countLabel(dirs, dirsCount)
|
|
1077
2052
|
const totalRows = rows.length
|
|
1078
2053
|
|
|
1079
2054
|
const scopeBox = (key, label, count) => h('label', {
|
|
1080
|
-
className: 'dsh-locator-check' + (key === 'content' && !availableContent ? ' disabled' : '')
|
|
1081
|
-
|
|
2055
|
+
className: 'dsh-locator-check' + (key === 'content' && !availableContent ? ' disabled' : '')
|
|
2056
|
+
+ (sessionOnly && key !== 'content' ? ' disabled' : ''),
|
|
2057
|
+
title: key === 'content' && !availableContent ? t('contentDisabled')
|
|
2058
|
+
: sessionOnly && key !== 'content' ? t('panelSessionHint') : label,
|
|
1082
2059
|
},
|
|
1083
2060
|
h('input', {
|
|
1084
2061
|
type: 'checkbox',
|
|
1085
2062
|
checked: scopes[key],
|
|
1086
|
-
disabled: key === 'content' && !availableContent,
|
|
2063
|
+
disabled: (key === 'content' && !availableContent) || (sessionOnly && key !== 'content'),
|
|
1087
2064
|
onChange: () => toggleScope(key),
|
|
1088
2065
|
}),
|
|
1089
2066
|
label,
|
|
1090
|
-
count
|
|
2067
|
+
count ? h('span', { className: 'dsh-locator-count' }, count) : null,
|
|
1091
2068
|
)
|
|
1092
2069
|
|
|
1093
2070
|
const groupHead = (key, label, count, note) => h('div', {
|
|
1094
2071
|
className: 'dsh-locator-grouphead',
|
|
1095
2072
|
id: 'dsh-locator-group-' + key,
|
|
2073
|
+
onMouseDown: event => event.preventDefault(),
|
|
1096
2074
|
},
|
|
1097
2075
|
h('span', null, label),
|
|
1098
2076
|
h('span', { className: 'dsh-locator-count' }, String(count)),
|
|
@@ -1119,6 +2097,7 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
1119
2097
|
className: 'dsh-locator-row' + (isContent ? ' dsh-locator-rowcontent' : '') + (index === selected ? ' sel' : ''),
|
|
1120
2098
|
title: hit.path,
|
|
1121
2099
|
onClick: () => setSelected(index),
|
|
2100
|
+
onMouseDown: event => event.preventDefault(),
|
|
1122
2101
|
onDoubleClick: () => activate(hit),
|
|
1123
2102
|
},
|
|
1124
2103
|
h('div', { className: 'dsh-locator-title' },
|
|
@@ -1133,29 +2112,46 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
1133
2112
|
)
|
|
1134
2113
|
}
|
|
1135
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
|
+
}
|
|
1136
2122
|
let index = -1
|
|
1137
2123
|
const renderGroup = (key, label, group, extra) => {
|
|
1138
2124
|
if (!scopes[key] || group === undefined) return null
|
|
1139
2125
|
const hits = Array.isArray(group.hits) ? group.hits : []
|
|
1140
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
|
|
1141
2131
|
const note = key === 'content'
|
|
1142
|
-
? (
|
|
1143
|
-
:
|
|
2132
|
+
? (matchMode === 'regex' ? t('panelRegexPaused')
|
|
2133
|
+
: contentBuilding ? t('contentBuilding', { files: content?.files ?? 0, messages: content?.messages ?? 0 })
|
|
2134
|
+
: (content?.capped ? t('contentCapped', { n: count }) : (query.trim() !== '' ? t('relevance') : null)))
|
|
1144
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(' · ')
|
|
1145
2137
|
if (typeof group.error === 'string' && group.error !== '') {
|
|
2138
|
+
const informational = ['content-regex-not-supported', 'content-query-too-short', 'invalid-control-char'].includes(group.error)
|
|
2139
|
+
const message = contentErrorText(group.error)
|
|
1146
2140
|
return h('div', { className: 'dsh-locator-group', role: 'group', 'aria-labelledby': 'dsh-locator-group-' + key },
|
|
1147
|
-
groupHead(key, label, count, null),
|
|
1148
|
-
h('div', { className: 'dsh-locator-groupempty', role: '
|
|
1149
|
-
|
|
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'))),
|
|
1150
2145
|
)
|
|
1151
2146
|
}
|
|
1152
2147
|
return h('div', { className: 'dsh-locator-group', role: 'group', 'aria-labelledby': 'dsh-locator-group-' + key },
|
|
1153
|
-
groupHead(key, label, count,
|
|
2148
|
+
groupHead(key, label, countLabel(group, count), combinedNote === '' ? null : combinedNote),
|
|
1154
2149
|
extra === undefined ? null : extra,
|
|
1155
2150
|
hits.length === 0
|
|
1156
2151
|
? h('div', { className: 'dsh-locator-groupempty' },
|
|
1157
2152
|
key === 'content' && scopes.content
|
|
1158
|
-
? (query.trim().length
|
|
2153
|
+
? (sessionOnly && query.trim().length >= 3 ? t('panelSessionHint')
|
|
2154
|
+
: query.trim().length < 3 ? t('contentShort') : (contentBuilding ? t('loading') : t('emptyResults')))
|
|
1159
2155
|
: (query.trim() === '' ? t('emptyQuery') : t('emptyResults')))
|
|
1160
2156
|
: hits.map(hit => { index++; return rowFor(key, hit, index) }),
|
|
1161
2157
|
)
|
|
@@ -1189,14 +2185,32 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
1189
2185
|
'aria-autocomplete': 'list',
|
|
1190
2186
|
'aria-activedescendant': totalRows > 0 ? 'dsh-locator-opt-' + selected : undefined,
|
|
1191
2187
|
}),
|
|
2188
|
+
h('span', { className: 'dsh-locator-count', 'aria-hidden': 'true' },
|
|
2189
|
+
loading ? t('loading') : totalRows > 0 ? t('panelTotal', { index: selected + 1, total: totalRows }) : ''),
|
|
1192
2190
|
h('span', { className: 'dsh-locator-kbd' }, 'Ctrl+Shift+F'),
|
|
1193
2191
|
h('button', { className: 'dsh-locator-close', onClick: close, title: t('closeTitle'), 'aria-label': t('closeLabel') }, '×'),
|
|
1194
2192
|
),
|
|
1195
2193
|
h('div', { className: 'dsh-locator-bar' },
|
|
1196
|
-
scopeBox('content', t('scopeContent'),
|
|
1197
|
-
scopeBox('files', t('scopeFiles'),
|
|
1198
|
-
scopeBox('dirs', t('scopeDirs'),
|
|
2194
|
+
scopeBox('content', t('scopeContent'), contentCountText),
|
|
2195
|
+
scopeBox('files', t('scopeFiles'), filesCountText),
|
|
2196
|
+
scopeBox('dirs', t('scopeDirs'), dirsCountText),
|
|
1199
2197
|
h('span', { className: 'grow' }),
|
|
2198
|
+
h('label', { className: 'dsh-locator-check' + (activeSessionId === '' ? ' disabled' : ''),
|
|
2199
|
+
title: activeSessionId === '' ? t('findNoSession') : t('panelSessionHint') },
|
|
2200
|
+
h('input', { type: 'checkbox', checked: sessionOnly, disabled: activeSessionId === '',
|
|
2201
|
+
onChange: () => setSessionOnly(value => !value) }),
|
|
2202
|
+
t('panelRangeSession')),
|
|
2203
|
+
h('label', { className: 'dsh-locator-check' }, t('panelModeLabel'),
|
|
2204
|
+
h('select', { className: 'dsh-locator-sort', value: matchMode,
|
|
2205
|
+
onChange: event => setMatchMode(['normal', 'extended', 'regex'].includes(event.target.value) ? event.target.value : 'normal') },
|
|
2206
|
+
h('option', { value: 'normal' }, t('modeNormal')),
|
|
2207
|
+
h('option', { value: 'extended' }, t('modeExtended')),
|
|
2208
|
+
h('option', { value: 'regex' }, t('modeRegex')),
|
|
2209
|
+
)),
|
|
2210
|
+
h('button', { className: 'dsh-locator-dirbtn', title: t('findCase'),
|
|
2211
|
+
'aria-pressed': caseSensitive ? 'true' : 'false', onClick: () => setCaseSensitive(value => !value) }, t('panelCaseShort')),
|
|
2212
|
+
h('button', { className: 'dsh-locator-dirbtn', title: t('findWhole'),
|
|
2213
|
+
'aria-pressed': wholeWord ? 'true' : 'false', onClick: () => setWholeWord(value => !value) }, t('panelWholeShort')),
|
|
1200
2214
|
h('label', { className: 'dsh-locator-check' }, t('sortLabel'),
|
|
1201
2215
|
h('select', {
|
|
1202
2216
|
className: 'dsh-locator-sort',
|
|
@@ -1224,6 +2238,7 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
1224
2238
|
roots: (status.roots || []).length,
|
|
1225
2239
|
})),
|
|
1226
2240
|
),
|
|
2241
|
+
sessionOnly ? h('div', { className: 'dsh-locator-groupempty' }, t('panelSessionHint')) : null,
|
|
1227
2242
|
h('div', { id: 'dsh-locator-results', className: 'dsh-locator-scroll', ref: listRef, role: 'listbox', 'aria-busy': loading ? 'true' : 'false' },
|
|
1228
2243
|
error !== ''
|
|
1229
2244
|
? h('div', { className: 'dsh-locator-empty', role: 'alert' }, t('errorPrefix') + error,
|
|
@@ -1240,12 +2255,17 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
1240
2255
|
],
|
|
1241
2256
|
),
|
|
1242
2257
|
h('div', { className: 'dsh-locator-foot' },
|
|
1243
|
-
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
|
+
})
|
|
1244
2264
|
+ (status && status.scanning ? ' · ' + t('indexing') : '')
|
|
1245
2265
|
+ (contentBuilding ? ' · ' + t('indexing') : '')),
|
|
1246
2266
|
h('span', { className: 'grow' }),
|
|
1247
2267
|
toast === '' ? null : h('span', { className: 'dsh-locator-toast' }, toast),
|
|
1248
|
-
h('span', null, t('footer')),
|
|
2268
|
+
h('span', null, t('footer') + ' · Ctrl+F ' + t('findOpen')),
|
|
1249
2269
|
),
|
|
1250
2270
|
h('div', { style: { position: 'absolute', width: 1, height: 1, overflow: 'hidden', clip: 'rect(0 0 0 0)' }, role: 'status', 'aria-live': 'polite' },
|
|
1251
2271
|
t('results', { content: contentCount, files: filesCount, dirs: dirsCount, ms: Math.round(took) })),
|
|
@@ -1267,22 +2287,77 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
1267
2287
|
inject: ['slots', 'uiWorkspace'],
|
|
1268
2288
|
apply(ctx) {
|
|
1269
2289
|
workspaceService = ctx.uiWorkspace ?? (typeof ctx.get === 'function' ? ctx.get('uiWorkspace') : null)
|
|
1270
|
-
|
|
2290
|
+
ctx.effect(() => {
|
|
2291
|
+
ensureStyles()
|
|
2292
|
+
return () => {
|
|
2293
|
+
const tag = document.querySelector('style[data-plugin-css="' + NS + '"]')
|
|
2294
|
+
if (tag !== null) tag.remove()
|
|
2295
|
+
}
|
|
2296
|
+
}, 'dsh-locator: styles')
|
|
1271
2297
|
ctx.effect(() => {
|
|
1272
2298
|
const onKey = event => {
|
|
1273
|
-
if (
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
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()
|
|
1278
2309
|
}
|
|
1279
2310
|
document.addEventListener('keydown', onKey, true)
|
|
1280
2311
|
return () => document.removeEventListener('keydown', onKey, true)
|
|
1281
2312
|
}, 'dsh-locator: search hotkey')
|
|
1282
|
-
ctx.effect(() =>
|
|
2313
|
+
ctx.effect(() => {
|
|
2314
|
+
const onFindKey = event => {
|
|
2315
|
+
if (event.defaultPrevented || event.isComposing) return
|
|
2316
|
+
const target = event.target
|
|
2317
|
+
if (target instanceof Element && target.closest('.xterm,.monaco-editor,[data-code-editor]') !== null) return
|
|
2318
|
+
const inFind = target instanceof Element && target.closest('.dsh-locator-findbar') !== null
|
|
2319
|
+
const ctrl = event.ctrlKey || event.metaKey
|
|
2320
|
+
if (ctrl && !event.shiftKey && !event.altKey && event.code === 'KeyF') {
|
|
2321
|
+
if (store.isOpen()) {
|
|
2322
|
+
event.preventDefault()
|
|
2323
|
+
event.stopPropagation()
|
|
2324
|
+
const panelInput = document.querySelector('.dsh-locator-input')
|
|
2325
|
+
if (panelInput !== null) { panelInput.focus(); panelInput.select() }
|
|
2326
|
+
return
|
|
2327
|
+
}
|
|
2328
|
+
if (document.querySelector('[role="dialog"][aria-modal="true"]') !== null && !inFind) return
|
|
2329
|
+
if (findStore.get().open) { event.preventDefault(); event.stopPropagation(); findApi.focus(); return }
|
|
2330
|
+
if (conversationScope('') === null) return
|
|
2331
|
+
event.preventDefault()
|
|
2332
|
+
event.stopPropagation()
|
|
2333
|
+
openFind({ selection: true })
|
|
2334
|
+
return
|
|
2335
|
+
}
|
|
2336
|
+
if (!ctrl && !event.altKey && event.code === 'F3') {
|
|
2337
|
+
if (!findStore.get().open) return
|
|
2338
|
+
event.preventDefault()
|
|
2339
|
+
event.stopPropagation()
|
|
2340
|
+
findApi.move(event.shiftKey ? -1 : 1)
|
|
2341
|
+
return
|
|
2342
|
+
}
|
|
2343
|
+
if (event.key === 'Escape' && findStore.get().open) {
|
|
2344
|
+
event.preventDefault()
|
|
2345
|
+
event.stopPropagation()
|
|
2346
|
+
closeFind()
|
|
2347
|
+
}
|
|
2348
|
+
}
|
|
2349
|
+
document.addEventListener('keydown', onFindKey, true)
|
|
2350
|
+
return () => document.removeEventListener('keydown', onFindKey, true)
|
|
2351
|
+
}, 'dsh-locator: find hotkeys')
|
|
2352
|
+
ctx.effect(() => () => {
|
|
2353
|
+
closeFind()
|
|
2354
|
+
cancelJump()
|
|
2355
|
+
store.set(false)
|
|
2356
|
+
}, 'dsh-locator: find cleanup')
|
|
1283
2357
|
try {
|
|
1284
2358
|
ctx.slots.inject('shell.overlay', () => {
|
|
1285
2359
|
const disposers = [
|
|
2360
|
+
ctx.slots.register({ name: 'shell.overlay', id: 'dsh-locator-findbar', order: 59 }, FindBar),
|
|
1286
2361
|
ctx.slots.register({ name: 'shell.overlay', id: 'dsh-locator-panel', order: 60 }, Panel),
|
|
1287
2362
|
ctx.slots.register({ name: 'shell.overlay', id: 'dsh-locator-notice', order: 61 }, Notice),
|
|
1288
2363
|
]
|
|
@@ -1295,6 +2370,12 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
1295
2370
|
} catch (error) {
|
|
1296
2371
|
console.error('dsh-locator: overlay slot unavailable', error)
|
|
1297
2372
|
}
|
|
2373
|
+
try {
|
|
2374
|
+
ctx.slots.inject('conversation.session.header.utilities', () =>
|
|
2375
|
+
ctx.slots.register({ name: 'conversation.session.header.utilities', id: 'dsh-locator-find-toggle', order: 30 }, FindToggle))
|
|
2376
|
+
} catch (error) {
|
|
2377
|
+
console.error('dsh-locator: session header slot unavailable', error)
|
|
2378
|
+
}
|
|
1298
2379
|
try {
|
|
1299
2380
|
ctx.slots.inject('sidebar.footer.action', () => ctx.slots.register({ name: 'sidebar.footer.action', id: 'dsh-locator-launch', order: 36 }, Launcher))
|
|
1300
2381
|
} catch (error) {
|