dsh-locator 0.8.2 → 0.9.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 +13 -0
- package/README-es.md +21 -5
- package/README-hi.md +22 -5
- package/README-pt.md +22 -5
- package/README.en.md +22 -5
- package/README.md +22 -5
- package/client.js +931 -41
- package/lib/combined.d.ts +20 -3
- package/lib/combined.d.ts.map +1 -1
- package/lib/combined.js +72 -12
- package/lib/combined.js.map +1 -1
- package/lib/content-search-worker.d.ts +3 -3
- package/lib/content-search-worker.js +11 -6
- package/lib/content-search-worker.js.map +1 -1
- package/lib/content-search.d.ts +81 -7
- package/lib/content-search.d.ts.map +1 -1
- package/lib/content-search.js +454 -36
- package/lib/content-search.js.map +1 -1
- package/lib/content.d.ts +20 -0
- package/lib/content.d.ts.map +1 -1
- package/lib/content.js +71 -21
- package/lib/content.js.map +1 -1
- package/lib/find.d.ts.map +1 -1
- package/lib/find.js +2 -1
- package/lib/find.js.map +1 -1
- package/lib/http.d.ts.map +1 -1
- package/lib/http.js +9 -2
- package/lib/http.js.map +1 -1
- package/package.json +1 -1
- package/src/combined.ts +95 -14
- package/src/content-search-worker.ts +15 -8
- package/src/content-search.ts +559 -36
- package/src/content.ts +81 -18
- package/src/find.ts +3 -2
- package/src/http.ts +11 -2
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
|
|
@@ -177,6 +208,25 @@ 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', 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
|
+
panelCaseShort: 'Aa', panelWholeShort: 'Word', panelModeLabel: 'Mode',
|
|
180
230
|
},
|
|
181
231
|
zh: {
|
|
182
232
|
scopeContent: '内容', scopeFiles: '文件', scopeDirs: '目录',
|
|
@@ -202,6 +252,25 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
202
252
|
launcher: '本地文件搜索 (Ctrl+Shift+F)',
|
|
203
253
|
justNow: '刚刚', minutesAgo: '{n} 分钟前', hoursAgo: '{n} 小时前',
|
|
204
254
|
phraseNotFound: '会话里没有找到匹配内容(可能还没加载)',
|
|
255
|
+
findOpen: '在会话中查找', findTitle: '在会话中查找', findAria: '在会话中查找',
|
|
256
|
+
findPlaceholder: '在会话中查找…', findPlaceholderSelection: '在选区内查找…',
|
|
257
|
+
findPrev: '上一个匹配 (Shift+F3)', findNext: '下一个匹配 (F3)', findClose: '关闭 (Esc)',
|
|
258
|
+
findCase: '区分大小写', findWhole: '全词匹配', findWrap: '循环查找', findReverse: '反向查找',
|
|
259
|
+
findMode: '模式', modeNormal: '普通', modeExtended: '扩展', modeRegex: '正则',
|
|
260
|
+
findScope: '仅在选区内', findScopeChip: '选区 {n} 字', findSessionChip: '本会话',
|
|
261
|
+
findNoMatch: '没有匹配', findEmpty: '输入关键词后在此会话中查找',
|
|
262
|
+
findInvalidRegex: '正则无效:{message}', findInvalidEscape: '扩展转义无效:{token}',
|
|
263
|
+
findTimeout: '正则执行超时,请简化表达式', findWrappedTop: '已回到顶部',
|
|
264
|
+
findWrappedBottom: '已到底部,回到开头', findNoWrapEnd: '已到最后一个匹配(循环查找已关闭)',
|
|
265
|
+
findNoWrapStart: '已到第一个匹配(循环查找已关闭)', findOlder: '加载更早消息后重试',
|
|
266
|
+
findLoadingOlder: '正在加载更早消息…', findLoadedOnly: '仅已加载窗口 · 更早历史未加载',
|
|
267
|
+
findCapped: '已截断', findCopied: '已复制匹配', findSelectionLost: '选区已失效,已切回整个会话',
|
|
268
|
+
findNoSession: '请先打开一个会话', findOptions: '选项',
|
|
269
|
+
findExtendedHelp: '扩展:\\n \\r \\t \\0 \\xHH \\\\',
|
|
270
|
+
panelTotal: '已选 {index}/{total}', panelRangeAll: '全部索引', panelRangeSession: '当前会话',
|
|
271
|
+
panelSessionHint: '此范围只查已索引的会话消息;实时查找请按 Ctrl+F',
|
|
272
|
+
panelRegexPaused: '正则模式下内容范围暂停', panelCountBounded: '≥ {n}', panelCountExact: '{n}',
|
|
273
|
+
panelCaseShort: 'Aa', panelWholeShort: '全词', panelModeLabel: '模式',
|
|
205
274
|
},
|
|
206
275
|
es: {
|
|
207
276
|
scopeContent: 'Contenido', scopeFiles: 'Archivos', scopeDirs: 'Carpetas',
|
|
@@ -227,6 +296,25 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
227
296
|
launcher: 'Búsqueda local (Ctrl+Shift+F)',
|
|
228
297
|
justNow: 'ahora', minutesAgo: 'hace {n} min', hoursAgo: 'hace {n} h',
|
|
229
298
|
phraseNotFound: 'No se encontró en la conversación (quizá no está cargada)',
|
|
299
|
+
findOpen: 'Buscar en la conversación', findTitle: 'Buscar en la conversación', findAria: 'Buscar en la conversación',
|
|
300
|
+
findPlaceholder: 'Buscar en la conversación…', findPlaceholderSelection: 'Buscar en la selección…',
|
|
301
|
+
findPrev: 'Coincidencia anterior (Shift+F3)', findNext: 'Siguiente coincidencia (F3)', findClose: 'Cerrar (Esc)',
|
|
302
|
+
findCase: 'Distinguir mayúsculas', findWhole: 'Palabra completa', findWrap: 'Continuar al inicio', findReverse: 'Buscar hacia atrás',
|
|
303
|
+
findMode: 'Modo', modeNormal: 'Normal', modeExtended: 'Extendido', modeRegex: 'Regex',
|
|
304
|
+
findScope: 'En la selección', findScopeChip: 'selección {n} caracteres', findSessionChip: 'esta conversación',
|
|
305
|
+
findNoMatch: 'Sin coincidencias', findEmpty: 'Escribe para buscar en esta conversación',
|
|
306
|
+
findInvalidRegex: 'Expresión regular no válida: {message}', findInvalidEscape: 'Escape extendido no válido: {token}',
|
|
307
|
+
findTimeout: 'La regex agotó el tiempo; simplifícala', findWrappedTop: 'Vuelto al inicio',
|
|
308
|
+
findWrappedBottom: 'Vuelto al final', findNoWrapEnd: 'Última coincidencia (continuación desactivada)',
|
|
309
|
+
findNoWrapStart: 'Primera coincidencia (continuación desactivada)', findOlder: 'Cargar mensajes anteriores y reintentar',
|
|
310
|
+
findLoadingOlder: 'Cargando mensajes anteriores…', findLoadedOnly: 'solo ventana cargada · historial anterior sin cargar',
|
|
311
|
+
findCapped: 'limitado', findCopied: 'Coincidencia copiada', findSelectionLost: 'La selección caducó; buscando en toda la conversación',
|
|
312
|
+
findNoSession: 'Abre primero una conversación', findOptions: 'Opciones',
|
|
313
|
+
findExtendedHelp: 'Extendido: \\n \\r \\t \\0 \\xHH \\\\',
|
|
314
|
+
panelTotal: 'seleccionado {index}/{total}', panelRangeAll: 'Todo el índice', panelRangeSession: 'Esta conversación',
|
|
315
|
+
panelSessionHint: 'Este ámbito solo busca mensajes indexados; pulsa Ctrl+F para buscar en vivo',
|
|
316
|
+
panelRegexPaused: 'Ámbito de contenido en pausa en modo regex', panelCountBounded: '≥ {n}', panelCountExact: '{n}',
|
|
317
|
+
panelCaseShort: 'Aa', panelWholeShort: 'Palabra', panelModeLabel: 'Modo',
|
|
230
318
|
},
|
|
231
319
|
pt: {
|
|
232
320
|
scopeContent: 'Conteúdo', scopeFiles: 'Arquivos', scopeDirs: 'Pastas',
|
|
@@ -252,6 +340,25 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
252
340
|
launcher: 'Busca local (Ctrl+Shift+F)',
|
|
253
341
|
justNow: 'agora', minutesAgo: 'há {n} min', hoursAgo: 'há {n} h',
|
|
254
342
|
phraseNotFound: 'Não encontrado na conversa (talvez ainda não carregada)',
|
|
343
|
+
findOpen: 'Buscar na conversa', findTitle: 'Buscar na conversa', findAria: 'Buscar na conversa',
|
|
344
|
+
findPlaceholder: 'Buscar na conversa…', findPlaceholderSelection: 'Buscar na seleção…',
|
|
345
|
+
findPrev: 'Ocorrência anterior (Shift+F3)', findNext: 'Próxima ocorrência (F3)', findClose: 'Fechar (Esc)',
|
|
346
|
+
findCase: 'Diferenciar maiúsculas', findWhole: 'Palavra inteira', findWrap: 'Continuar do início', findReverse: 'Buscar para trás',
|
|
347
|
+
findMode: 'Modo', modeNormal: 'Normal', modeExtended: 'Estendido', modeRegex: 'Regex',
|
|
348
|
+
findScope: 'Na seleção', findScopeChip: 'seleção {n} caracteres', findSessionChip: 'esta conversa',
|
|
349
|
+
findNoMatch: 'Sem resultados', findEmpty: 'Digite para buscar nesta conversa',
|
|
350
|
+
findInvalidRegex: 'Expressão regular inválida: {message}', findInvalidEscape: 'Escape estendido inválido: {token}',
|
|
351
|
+
findTimeout: 'A regex excedeu o tempo; simplifique', findWrappedTop: 'Voltou ao início',
|
|
352
|
+
findWrappedBottom: 'Voltou ao fim', findNoWrapEnd: 'Última ocorrência (continuação desativada)',
|
|
353
|
+
findNoWrapStart: 'Primeira ocorrência (continuação desativada)', findOlder: 'Carregar mensagens anteriores e tentar novamente',
|
|
354
|
+
findLoadingOlder: 'Carregando mensagens anteriores…', findLoadedOnly: 'apenas janela carregada · histórico anterior não carregado',
|
|
355
|
+
findCapped: 'limitado', findCopied: 'Ocorrência copiada', findSelectionLost: 'A seleção expirou; buscando em toda a conversa',
|
|
356
|
+
findNoSession: 'Abra uma conversa primeiro', findOptions: 'Opções',
|
|
357
|
+
findExtendedHelp: 'Estendido: \\n \\r \\t \\0 \\xHH \\\\',
|
|
358
|
+
panelTotal: 'selecionado {index}/{total}', panelRangeAll: 'Todo o índice', panelRangeSession: 'Esta conversa',
|
|
359
|
+
panelSessionHint: 'Este escopo busca apenas mensagens indexadas; use Ctrl+F para buscar ao vivo',
|
|
360
|
+
panelRegexPaused: 'Escopo de conteúdo pausado no modo regex', panelCountBounded: '≥ {n}', panelCountExact: '{n}',
|
|
361
|
+
panelCaseShort: 'Aa', panelWholeShort: 'Palavra', panelModeLabel: 'Modo',
|
|
255
362
|
},
|
|
256
363
|
hi: {
|
|
257
364
|
scopeContent: 'सामग्री', scopeFiles: 'फ़ाइलें', scopeDirs: 'फ़ोल्डर',
|
|
@@ -277,6 +384,25 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
277
384
|
launcher: 'स्थानीय फ़ाइल खोज (Ctrl+Shift+F)',
|
|
278
385
|
justNow: 'अभी', minutesAgo: '{n} मिनट पहले', hoursAgo: '{n} घंटे पहले',
|
|
279
386
|
phraseNotFound: 'बातचीत में नहीं मिला (शायद अभी लोड नहीं हुआ)',
|
|
387
|
+
findOpen: 'बातचीत में खोजें', findTitle: 'बातचीत में खोजें', findAria: 'बातचीत में खोजें',
|
|
388
|
+
findPlaceholder: 'बातचीत में खोजें…', findPlaceholderSelection: 'चयन में खोजें…',
|
|
389
|
+
findPrev: 'पिछला मेल (Shift+F3)', findNext: 'अगला मेल (F3)', findClose: 'बंद करें (Esc)',
|
|
390
|
+
findCase: 'केस मिलाएँ', findWhole: 'पूरा शब्द', findWrap: 'चक्रीय खोज', findReverse: 'उल्टा खोजें',
|
|
391
|
+
findMode: 'मोड', modeNormal: 'सामान्य', modeExtended: 'विस्तृत', modeRegex: 'रेगेक्स',
|
|
392
|
+
findScope: 'चयन में', findScopeChip: 'चयन {n} अक्षर', findSessionChip: 'यह बातचीत',
|
|
393
|
+
findNoMatch: 'कोई मेल नहीं', findEmpty: 'इस बातचीत में खोजने के लिए लिखें',
|
|
394
|
+
findInvalidRegex: 'अमान्य रेगुलर एक्सप्रेशन: {message}', findInvalidEscape: 'अमान्य विस्तृत एस्केप: {token}',
|
|
395
|
+
findTimeout: 'रेगेक्स का समय समाप्त; सरल बनाएँ', findWrappedTop: 'शीर्ष पर लौटा',
|
|
396
|
+
findWrappedBottom: 'अंत पर लौटा', findNoWrapEnd: 'अंतिम मेल (चक्र बंद)',
|
|
397
|
+
findNoWrapStart: 'पहला मेल (चक्र बंद)', findOlder: 'पुराने संदेश लोड कर पुन: प्रयास',
|
|
398
|
+
findLoadingOlder: 'पुराने संदेश लोड हो रहे हैं…', findLoadedOnly: 'केवल लोडेड विंडो · पुराना इतिहास लोड नहीं',
|
|
399
|
+
findCapped: 'सीमित', findCopied: 'मेल कॉपी हुआ', findSelectionLost: 'चयन समाप्त; पूरी बातचीत में खोज',
|
|
400
|
+
findNoSession: 'पहले एक बातचीत खोलें', findOptions: 'विकल्प',
|
|
401
|
+
findExtendedHelp: 'विस्तृत: \\n \\r \\t \\0 \\xHH \\\\',
|
|
402
|
+
panelTotal: 'चयनित {index}/{total}', panelRangeAll: 'पूरा इंडेक्स', panelRangeSession: 'यह बातचीत',
|
|
403
|
+
panelSessionHint: 'यह क्षेत्र केवल इंडेक्स किए संदेश खोजता है; लाइव खोज के लिए Ctrl+F दबाएँ',
|
|
404
|
+
panelRegexPaused: 'रेगेक्स मोड में सामग्री क्षेत्र रुका है', panelCountBounded: '≥ {n}', panelCountExact: '{n}',
|
|
405
|
+
panelCaseShort: 'Aa', panelWholeShort: 'शब्द', panelModeLabel: 'मोड',
|
|
280
406
|
},
|
|
281
407
|
}
|
|
282
408
|
const LANG = (() => {
|
|
@@ -382,7 +508,8 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
382
508
|
const PREFS_KEY = 'dsh-locator.panel.v1'
|
|
383
509
|
const SORTS = ['mtime', 'name', 'size', 'path']
|
|
384
510
|
function readPrefs() {
|
|
385
|
-
const fallback = { scopes: { content: true, files: true, dirs: true }, sort: 'mtime', desc: true
|
|
511
|
+
const fallback = { scopes: { content: true, files: true, dirs: true }, sort: 'mtime', desc: true,
|
|
512
|
+
matchMode: 'normal', caseSensitive: false, wholeWord: false }
|
|
386
513
|
try {
|
|
387
514
|
const raw = window.localStorage.getItem(PREFS_KEY)
|
|
388
515
|
if (raw === null) return fallback
|
|
@@ -397,6 +524,9 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
397
524
|
},
|
|
398
525
|
sort: SORTS.includes(value.sort) ? value.sort : 'mtime',
|
|
399
526
|
desc: typeof value.desc === 'boolean' ? value.desc : true,
|
|
527
|
+
matchMode: ['normal', 'extended', 'regex'].includes(value.matchMode) ? value.matchMode : 'normal',
|
|
528
|
+
caseSensitive: typeof value.caseSensitive === 'boolean' ? value.caseSensitive : false,
|
|
529
|
+
wholeWord: typeof value.wholeWord === 'boolean' ? value.wholeWord : false,
|
|
400
530
|
}
|
|
401
531
|
} catch {
|
|
402
532
|
return fallback
|
|
@@ -424,26 +554,39 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
424
554
|
legacyTargets.clear()
|
|
425
555
|
}
|
|
426
556
|
|
|
427
|
-
|
|
557
|
+
const HL_SCOPE = 'dsh-locator-find-scope'
|
|
558
|
+
let highlightOwner = null
|
|
559
|
+
|
|
560
|
+
function clearFindHighlight(owner) {
|
|
561
|
+
if (owner && highlightOwner !== null && owner !== highlightOwner) return
|
|
562
|
+
highlightOwner = null
|
|
428
563
|
clearLegacyTargets()
|
|
429
564
|
if (!highlightsAvailable()) return
|
|
430
565
|
CSS.highlights.delete(HL_BASE)
|
|
431
566
|
CSS.highlights.delete(HL_ACTIVE)
|
|
567
|
+
CSS.highlights.delete(HL_SCOPE)
|
|
432
568
|
}
|
|
433
569
|
|
|
434
|
-
function setFindHighlight(ranges) {
|
|
570
|
+
function setFindHighlight(ranges, activeIndex, owner) {
|
|
571
|
+
clearFindHighlight()
|
|
572
|
+
if (ranges.length === 0) return
|
|
573
|
+
highlightOwner = owner || null
|
|
574
|
+
const active = ranges[Math.max(0, Math.min(ranges.length - 1, activeIndex || 0))]
|
|
435
575
|
if (!highlightsAvailable()) {
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
if (row !== null) { row.classList.add('dsh-locator-target'); legacyTargets.add(row) }
|
|
441
|
-
}
|
|
576
|
+
const node = active.startContainer
|
|
577
|
+
const element = node instanceof Element ? node : node.parentElement
|
|
578
|
+
const row = element === null ? null : element.closest('[data-chat-node-key]')
|
|
579
|
+
if (row !== null) { row.classList.add('dsh-locator-target'); legacyTargets.add(row) }
|
|
442
580
|
return
|
|
443
581
|
}
|
|
444
|
-
if (ranges.length === 0) return
|
|
445
582
|
CSS.highlights.set(HL_BASE, new Highlight(...ranges))
|
|
446
|
-
CSS.highlights.set(HL_ACTIVE, new Highlight(
|
|
583
|
+
CSS.highlights.set(HL_ACTIVE, new Highlight(active))
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function setFindScope(range) {
|
|
587
|
+
if (!highlightsAvailable()) return
|
|
588
|
+
CSS.highlights.delete(HL_SCOPE)
|
|
589
|
+
if (range !== null) CSS.highlights.set(HL_SCOPE, new Highlight(range))
|
|
447
590
|
}
|
|
448
591
|
|
|
449
592
|
function isVisible(element) {
|
|
@@ -681,7 +824,7 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
681
824
|
let activeHighlight = null
|
|
682
825
|
|
|
683
826
|
function clearJumpHighlight() {
|
|
684
|
-
clearFindHighlight()
|
|
827
|
+
clearFindHighlight('jump')
|
|
685
828
|
if (activeHighlight !== null) {
|
|
686
829
|
const clear = activeHighlight
|
|
687
830
|
activeHighlight = null
|
|
@@ -735,7 +878,7 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
735
878
|
}
|
|
736
879
|
if (stale()) return false
|
|
737
880
|
if (target === null) { showNotice(t('phraseNotFound')); return false }
|
|
738
|
-
setFindHighlight(target.ranges)
|
|
881
|
+
setFindHighlight(target.ranges, 0, 'jump')
|
|
739
882
|
centerRange(target.ranges[0])
|
|
740
883
|
const start = target.ranges[0].startContainer
|
|
741
884
|
const chain = scrollChain(start instanceof Element ? start : start.parentElement)
|
|
@@ -753,7 +896,7 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
753
896
|
const fn = highlightCleanup.pop()
|
|
754
897
|
try { fn() } catch { /* ignore */ }
|
|
755
898
|
}
|
|
756
|
-
clearFindHighlight()
|
|
899
|
+
clearFindHighlight('jump')
|
|
757
900
|
}
|
|
758
901
|
// Cancelling must drop the scroll lock too, not only the paint: the lock
|
|
759
902
|
// owns timers and scroll/pointer listeners that would otherwise outlive
|
|
@@ -799,7 +942,7 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
799
942
|
const next = finder()
|
|
800
943
|
if (next === null) { cancelJump(); return }
|
|
801
944
|
target = next
|
|
802
|
-
setFindHighlight(target.ranges)
|
|
945
|
+
setFindHighlight(target.ranges, 0, 'jump')
|
|
803
946
|
centerRange(target.ranges[0])
|
|
804
947
|
}, 1_000)
|
|
805
948
|
highlightCleanup.push(() => clearInterval(sessionWatch))
|
|
@@ -812,6 +955,640 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
812
955
|
const PAGE_NAME = 50
|
|
813
956
|
const PAGE_CONTENT = 30
|
|
814
957
|
|
|
958
|
+
// ── in-conversation find bar ────────────────────────────────────────────
|
|
959
|
+
const FIND_MAX_MATCHES = 2000
|
|
960
|
+
const FIND_MAX_CHARS = 4_000_000
|
|
961
|
+
const FIND_BLOCK_SELECTOR = 'p,li,pre,td,th,h1,h2,h3,h4,h5,h6,blockquote,dd,dt'
|
|
962
|
+
const FIND_EXCLUDED = '[hidden]:not([hidden="until-found"]),[data-chat-history],[data-text-interaction-surface],'
|
|
963
|
+
+ '[data-composer-seat],[data-text-interaction-exclude],script,style,button,input,textarea,select,'
|
|
964
|
+
+ '.sr-only,.katex-mathml,[aria-hidden="true"]:not(.katex-html)'
|
|
965
|
+
|
|
966
|
+
function createFindStore() {
|
|
967
|
+
const listeners = new Set()
|
|
968
|
+
let state = {
|
|
969
|
+
open: false, sessionId: '', scope: 'session', selection: null, query: '',
|
|
970
|
+
mode: 'normal', caseSensitive: false, wholeWord: false, wrap: true, direction: 1,
|
|
971
|
+
optionsOpen: false, count: 0, index: -1, capped: false, status: 'idle', error: '', hint: '',
|
|
972
|
+
}
|
|
973
|
+
return {
|
|
974
|
+
get: () => state,
|
|
975
|
+
subscribe(listener) {
|
|
976
|
+
listeners.add(listener)
|
|
977
|
+
return () => listeners.delete(listener)
|
|
978
|
+
},
|
|
979
|
+
set(patch) {
|
|
980
|
+
state = { ...state, ...patch }
|
|
981
|
+
for (const listener of listeners) listener()
|
|
982
|
+
},
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
const findStore = createFindStore()
|
|
986
|
+
const findApi = { move: () => false, cancel: () => {}, focus: () => {} }
|
|
987
|
+
function useFindState() {
|
|
988
|
+
return useSyncExternalStore(findStore.subscribe, findStore.get, findStore.get)
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
function selectionSnapshot(scope) {
|
|
992
|
+
if (scope === null || typeof window.getSelection !== 'function') return null
|
|
993
|
+
const selection = window.getSelection()
|
|
994
|
+
if (selection === null || selection.rangeCount !== 1 || selection.isCollapsed) return null
|
|
995
|
+
const range = selection.getRangeAt(0).cloneRange()
|
|
996
|
+
if (!scope.contains(range.commonAncestorContainer)) return null
|
|
997
|
+
const startEl = range.startContainer instanceof Element ? range.startContainer : range.startContainer.parentElement
|
|
998
|
+
if (startEl !== null && startEl.closest('[data-composer-seat]') !== null) return null
|
|
999
|
+
const text = String(selection.toString() || '').replace(/\s+/g, ' ').trim()
|
|
1000
|
+
if (text === '') return null
|
|
1001
|
+
return { range, text: text.slice(0, 500), chars: text.length }
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
function openFind(options) {
|
|
1005
|
+
const opts = options || {}
|
|
1006
|
+
let scope = opts.sessionId ? conversationScope(opts.sessionId) : null
|
|
1007
|
+
if (scope === null) scope = conversationScope('')
|
|
1008
|
+
if (scope === null) { showNotice(t('findNoSession')); return false }
|
|
1009
|
+
const sessionId = scope.getAttribute('data-conversation-session') || opts.sessionId || ''
|
|
1010
|
+
const selection = opts.selection === false ? null : selectionSnapshot(scope)
|
|
1011
|
+
const useSelection = opts.selection === true && selection !== null
|
|
1012
|
+
findApi.cancel()
|
|
1013
|
+
cancelJump()
|
|
1014
|
+
if (store.isOpen()) store.set(false)
|
|
1015
|
+
findStore.set({
|
|
1016
|
+
open: true, sessionId, scope: useSelection ? 'selection' : 'session',
|
|
1017
|
+
selection: useSelection ? selection : null, query: '', mode: 'normal',
|
|
1018
|
+
caseSensitive: false, wholeWord: false, wrap: true, direction: 1,
|
|
1019
|
+
optionsOpen: false, count: 0, index: -1, capped: false, status: 'idle', error: '', hint: '',
|
|
1020
|
+
})
|
|
1021
|
+
return true
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
function closeFind() {
|
|
1025
|
+
if (!findStore.get().open) return
|
|
1026
|
+
findStore.set({ open: false, selection: null, query: '', error: '', hint: '', status: 'idle' })
|
|
1027
|
+
findApi.cancel()
|
|
1028
|
+
clearFindHighlight('find')
|
|
1029
|
+
setFindScope(null)
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
const FIND_WORKER_SRC = `
|
|
1033
|
+
self.onmessage = event => {
|
|
1034
|
+
const { id, pattern, flags, texts } = event.data
|
|
1035
|
+
try {
|
|
1036
|
+
const re = new RegExp(pattern, flags)
|
|
1037
|
+
const out = []
|
|
1038
|
+
for (let ti = 0; ti < texts.length; ti++) {
|
|
1039
|
+
const text = texts[ti]
|
|
1040
|
+
re.lastIndex = 0
|
|
1041
|
+
let match
|
|
1042
|
+
while ((match = re.exec(text)) !== null) {
|
|
1043
|
+
if (match[0].length === 0) { re.lastIndex++; continue }
|
|
1044
|
+
out.push([ti, match.index, match.index + match[0].length])
|
|
1045
|
+
if (out.length >= 2000) break
|
|
1046
|
+
}
|
|
1047
|
+
if (out.length >= 2000) break
|
|
1048
|
+
}
|
|
1049
|
+
self.postMessage({ id, ok: true, matches: out, capped: out.length >= 2000 })
|
|
1050
|
+
} catch (error) {
|
|
1051
|
+
self.postMessage({ id, ok: false, error: String(error && error.message || error) })
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
`
|
|
1055
|
+
|
|
1056
|
+
function scanRegexInWorker(texts, source, flags) {
|
|
1057
|
+
if (typeof Worker !== 'function' || typeof Blob !== 'function' || typeof URL.createObjectURL !== 'function') {
|
|
1058
|
+
return Promise.resolve(null)
|
|
1059
|
+
}
|
|
1060
|
+
return new Promise(resolve => {
|
|
1061
|
+
let worker = null
|
|
1062
|
+
let url = ''
|
|
1063
|
+
let done = false
|
|
1064
|
+
const finish = value => {
|
|
1065
|
+
if (done) return
|
|
1066
|
+
done = true
|
|
1067
|
+
clearTimeout(timer)
|
|
1068
|
+
try { if (worker !== null) worker.terminate() } catch { /* ignore */ }
|
|
1069
|
+
try { if (url !== '') URL.revokeObjectURL(url) } catch { /* ignore */ }
|
|
1070
|
+
resolve(value)
|
|
1071
|
+
}
|
|
1072
|
+
const timer = setTimeout(() => finish({ timeout: true }), 500)
|
|
1073
|
+
try {
|
|
1074
|
+
const blob = new Blob([FIND_WORKER_SRC], { type: 'text/javascript' })
|
|
1075
|
+
url = URL.createObjectURL(blob)
|
|
1076
|
+
worker = new Worker(url)
|
|
1077
|
+
worker.onmessage = event => finish(event.data)
|
|
1078
|
+
worker.onerror = () => finish(null)
|
|
1079
|
+
worker.postMessage({ id: 1, pattern: source, flags, texts })
|
|
1080
|
+
} catch {
|
|
1081
|
+
finish(null)
|
|
1082
|
+
}
|
|
1083
|
+
})
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
function expandExtended(input) {
|
|
1087
|
+
let out = ''
|
|
1088
|
+
for (let index = 0; index < input.length; index++) {
|
|
1089
|
+
const char = input[index]
|
|
1090
|
+
if (char !== '\\') { out += char; continue }
|
|
1091
|
+
const next = input[index + 1]
|
|
1092
|
+
if (next === undefined) { out += '\\'; continue }
|
|
1093
|
+
index++
|
|
1094
|
+
if (next === 'n') out += '\n'
|
|
1095
|
+
else if (next === 'r') out += '\r'
|
|
1096
|
+
else if (next === 't') out += '\t'
|
|
1097
|
+
else if (next === '0') out += '\u0000'
|
|
1098
|
+
else if (next === '\\') out += '\\'
|
|
1099
|
+
else if (next === 'x') {
|
|
1100
|
+
const hex = input.slice(index + 1, index + 3)
|
|
1101
|
+
if (!/^[0-9a-fA-F]{2}$/u.test(hex)) return { error: '\\x' }
|
|
1102
|
+
out += String.fromCharCode(parseInt(hex, 16))
|
|
1103
|
+
index += 2
|
|
1104
|
+
} else if (next === 'u') {
|
|
1105
|
+
const hex = input.slice(index + 1, index + 5)
|
|
1106
|
+
if (!/^[0-9a-fA-F]{4}$/u.test(hex)) return { error: '\\u' }
|
|
1107
|
+
out += String.fromCharCode(parseInt(hex, 16))
|
|
1108
|
+
index += 4
|
|
1109
|
+
} else out += '\\' + next
|
|
1110
|
+
}
|
|
1111
|
+
return { value: out }
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
function compileFind(query, mode, caseSensitive) {
|
|
1115
|
+
let source = String(query || '')
|
|
1116
|
+
if (mode === 'extended') {
|
|
1117
|
+
const expanded = expandExtended(source)
|
|
1118
|
+
if (expanded.error !== undefined) return { error: t('findInvalidEscape', { token: expanded.error }) }
|
|
1119
|
+
source = expanded.value
|
|
1120
|
+
}
|
|
1121
|
+
if (mode !== 'regex') source = escapeRegExp(source)
|
|
1122
|
+
const flags = 'g' + (caseSensitive ? '' : 'i')
|
|
1123
|
+
try {
|
|
1124
|
+
return { source, flags: /[\u{10000}-\u{10FFFF}]/u.test(source) ? flags + 'u' : flags, re: new RegExp(source, flags) }
|
|
1125
|
+
} catch (error) {
|
|
1126
|
+
if (mode === 'regex') {
|
|
1127
|
+
try { return { source, flags: flags + 'u', re: new RegExp(source, flags + 'u') } } catch { /* fall through */ }
|
|
1128
|
+
}
|
|
1129
|
+
return { error: t('findInvalidRegex', { message: String(error && error.message || error).slice(0, 140) }) }
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
function isWordCharacter(char) {
|
|
1134
|
+
return typeof char === 'string' && char !== '' && /^[A-Za-z0-9_]$/u.test(char)
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
function wholeWordAt(text, start, end) {
|
|
1138
|
+
return !isWordCharacter(text[start - 1]) && !isWordCharacter(text[end])
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
function collectFindGroups(root) {
|
|
1142
|
+
if (root === null) return []
|
|
1143
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT)
|
|
1144
|
+
const groups = new Map()
|
|
1145
|
+
let node = walker.nextNode()
|
|
1146
|
+
while (node !== null) {
|
|
1147
|
+
const parent = node.parentElement
|
|
1148
|
+
if (parent !== null && parent.closest(FIND_EXCLUDED) === null) {
|
|
1149
|
+
const value = node.nodeValue || ''
|
|
1150
|
+
if (value !== '') {
|
|
1151
|
+
const block = parent.closest(FIND_BLOCK_SELECTOR)
|
|
1152
|
+
|| parent.closest('[data-text-interaction-source]')
|
|
1153
|
+
|| parent.closest('[data-chat-node-key]')
|
|
1154
|
+
|| root
|
|
1155
|
+
let group = groups.get(block)
|
|
1156
|
+
if (group === undefined) { group = { block, nodes: [], text: '', starts: [] }; groups.set(block, group) }
|
|
1157
|
+
group.starts.push(group.text.length)
|
|
1158
|
+
group.nodes.push(node)
|
|
1159
|
+
group.text += value
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
node = walker.nextNode()
|
|
1163
|
+
}
|
|
1164
|
+
return [...groups.values()]
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
function clipForGroup(group, range) {
|
|
1168
|
+
if (range === null) return null
|
|
1169
|
+
let start = null
|
|
1170
|
+
let end = null
|
|
1171
|
+
let position = 0
|
|
1172
|
+
for (const node of group.nodes) {
|
|
1173
|
+
const length = (node.nodeValue || '').length
|
|
1174
|
+
let nodeStart = 0
|
|
1175
|
+
let nodeEnd = length
|
|
1176
|
+
try {
|
|
1177
|
+
if (!range.intersectsNode(node)) { position += length; continue }
|
|
1178
|
+
} catch { position += length; continue }
|
|
1179
|
+
if (node === range.startContainer) nodeStart = range.startOffset
|
|
1180
|
+
if (node === range.endContainer) nodeEnd = range.endOffset
|
|
1181
|
+
if (nodeStart < nodeEnd) {
|
|
1182
|
+
if (start === null) start = position + nodeStart
|
|
1183
|
+
end = position + nodeEnd
|
|
1184
|
+
}
|
|
1185
|
+
position += length
|
|
1186
|
+
}
|
|
1187
|
+
if (start === null || end === null || start >= end) return null
|
|
1188
|
+
return [start, end]
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
function rangeForGroupOffsets(group, start, end) {
|
|
1192
|
+
if (group.nodes.length === 0) return null
|
|
1193
|
+
const startOffset = offsetInGroup(group, start)
|
|
1194
|
+
const endOffset = offsetInGroup(group, end)
|
|
1195
|
+
const range = document.createRange()
|
|
1196
|
+
try {
|
|
1197
|
+
range.setStart(startOffset[0], startOffset[1])
|
|
1198
|
+
range.setEnd(endOffset[0], endOffset[1])
|
|
1199
|
+
return range
|
|
1200
|
+
} catch {
|
|
1201
|
+
return null
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
function findHistoryButton(scope) {
|
|
1206
|
+
return scope === null ? null : scope.querySelector('[data-chat-history] button')
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
function FindToggle(props) {
|
|
1210
|
+
const onClick = () => { openFind({ sessionId: props && props.sessionId ? props.sessionId : '', selection: false }) }
|
|
1211
|
+
return h('button', {
|
|
1212
|
+
className: 'dsh-locator-launch',
|
|
1213
|
+
title: t('findOpen') + ' (Ctrl+F)',
|
|
1214
|
+
'aria-label': t('findOpen'),
|
|
1215
|
+
onClick,
|
|
1216
|
+
}, '⌕')
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
function FindBar() {
|
|
1220
|
+
const state = useFindState()
|
|
1221
|
+
const inputRef = useRef(null)
|
|
1222
|
+
const barRef = useRef(null)
|
|
1223
|
+
const scopeRef = useRef(null)
|
|
1224
|
+
const matchesRef = useRef([])
|
|
1225
|
+
const activeRef = useRef(-1)
|
|
1226
|
+
const sequenceRef = useRef(0)
|
|
1227
|
+
const debounceRef = useRef(0)
|
|
1228
|
+
const observerRef = useRef(null)
|
|
1229
|
+
const revealTimersRef = useRef([])
|
|
1230
|
+
const returnFocusRef = useRef(null)
|
|
1231
|
+
const [anchor, setAnchor] = useState(null)
|
|
1232
|
+
const composingRef = useRef(false)
|
|
1233
|
+
|
|
1234
|
+
const applyHighlight = useCallback((index) => {
|
|
1235
|
+
const matches = matchesRef.current
|
|
1236
|
+
if (matches.length === 0) { clearFindHighlight('find'); setFindScope(null); return }
|
|
1237
|
+
const safe = Math.max(0, Math.min(matches.length - 1, index))
|
|
1238
|
+
activeRef.current = safe
|
|
1239
|
+
setFindHighlight(matches.map(match => match.range), safe, 'find')
|
|
1240
|
+
setFindScope(state.scope === 'selection' && state.selection ? state.selection.range : null)
|
|
1241
|
+
}, [state.scope, state.selection])
|
|
1242
|
+
|
|
1243
|
+
const revealActive = useCallback(() => {
|
|
1244
|
+
const match = matchesRef.current[activeRef.current]
|
|
1245
|
+
if (!match || !match.range.startContainer.isConnected) return false
|
|
1246
|
+
expandHidden(match.range.startContainer)
|
|
1247
|
+
const run = () => {
|
|
1248
|
+
if (!match.range.startContainer.isConnected) return
|
|
1249
|
+
centerRange(match.range)
|
|
1250
|
+
}
|
|
1251
|
+
requestAnimationFrame(run)
|
|
1252
|
+
for (const delay of [80, 240]) {
|
|
1253
|
+
const timer = setTimeout(run, delay)
|
|
1254
|
+
revealTimersRef.current.push(timer)
|
|
1255
|
+
}
|
|
1256
|
+
return true
|
|
1257
|
+
}, [])
|
|
1258
|
+
|
|
1259
|
+
const move = useCallback(delta => {
|
|
1260
|
+
const matches = matchesRef.current
|
|
1261
|
+
if (matches.length === 0) return false
|
|
1262
|
+
const direction = delta === 0 ? (findStore.get().direction === -1 ? -1 : 1) : delta
|
|
1263
|
+
if (!findStore.get().wrap && (activeRef.current + direction >= matches.length || activeRef.current + direction < 0)) {
|
|
1264
|
+
findStore.set({ hint: direction > 0 ? t('findNoWrapEnd') : t('findNoWrapStart') })
|
|
1265
|
+
return false
|
|
1266
|
+
}
|
|
1267
|
+
let next = activeRef.current + direction
|
|
1268
|
+
if (next >= matches.length) next = 0
|
|
1269
|
+
if (next < 0) next = matches.length - 1
|
|
1270
|
+
const wrapped = (activeRef.current + direction >= matches.length) || (activeRef.current + direction < 0)
|
|
1271
|
+
activeRef.current = next
|
|
1272
|
+
applyHighlight(next)
|
|
1273
|
+
findStore.set({
|
|
1274
|
+
index: next,
|
|
1275
|
+
hint: wrapped ? (direction > 0 ? t('findWrappedTop') : t('findWrappedBottom')) : '',
|
|
1276
|
+
})
|
|
1277
|
+
revealActive()
|
|
1278
|
+
return true
|
|
1279
|
+
}, [applyHighlight, revealActive])
|
|
1280
|
+
|
|
1281
|
+
const runScan = useCallback(async (keepActive) => {
|
|
1282
|
+
const token = ++sequenceRef.current
|
|
1283
|
+
const scope = scopeRef.current || conversationScope(state.sessionId) || conversationScope('')
|
|
1284
|
+
if (scope === null) { return }
|
|
1285
|
+
scopeRef.current = scope
|
|
1286
|
+
if (state.query === '') {
|
|
1287
|
+
matchesRef.current = []
|
|
1288
|
+
activeRef.current = -1
|
|
1289
|
+
clearFindHighlight('find')
|
|
1290
|
+
setFindScope(state.scope === 'selection' && state.selection ? state.selection.range : null)
|
|
1291
|
+
findStore.set({ count: 0, index: -1, capped: false, status: 'idle', error: '', hint: '' })
|
|
1292
|
+
return
|
|
1293
|
+
}
|
|
1294
|
+
const compiled = compileFind(state.query, state.mode, state.caseSensitive)
|
|
1295
|
+
if (compiled.error !== undefined) {
|
|
1296
|
+
matchesRef.current = []
|
|
1297
|
+
activeRef.current = -1
|
|
1298
|
+
clearFindHighlight('find')
|
|
1299
|
+
findStore.set({ count: 0, index: -1, capped: false, status: 'error', error: compiled.error })
|
|
1300
|
+
return
|
|
1301
|
+
}
|
|
1302
|
+
let selectionRange = null
|
|
1303
|
+
if (state.scope === 'selection' && state.selection) {
|
|
1304
|
+
selectionRange = state.selection.range
|
|
1305
|
+
if (!selectionRange.startContainer.isConnected) {
|
|
1306
|
+
findStore.set({ scope: 'session', selection: null, hint: t('findSelectionLost') })
|
|
1307
|
+
return
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
const groups = collectFindGroups(scope)
|
|
1311
|
+
const texts = []
|
|
1312
|
+
const clips = []
|
|
1313
|
+
let chars = 0
|
|
1314
|
+
for (const group of groups) {
|
|
1315
|
+
const clip = selectionRange === null ? null : clipForGroup(group, selectionRange)
|
|
1316
|
+
if (selectionRange !== null && clip === null) { texts.push(null); clips.push(null); continue }
|
|
1317
|
+
const start = clip === null ? 0 : clip[0]
|
|
1318
|
+
const end = clip === null ? group.text.length : clip[1]
|
|
1319
|
+
const text = group.text.slice(start, end)
|
|
1320
|
+
chars += text.length
|
|
1321
|
+
if (chars > FIND_MAX_CHARS) { texts.push(null); clips.push(null); continue }
|
|
1322
|
+
texts.push(text)
|
|
1323
|
+
clips.push(clip === null ? [0, group.text.length] : clip)
|
|
1324
|
+
}
|
|
1325
|
+
let rawMatches = null
|
|
1326
|
+
let capped = false
|
|
1327
|
+
if (state.mode === 'regex') {
|
|
1328
|
+
try {
|
|
1329
|
+
const result = await scanRegexInWorker(texts.filter(text => text !== null), compiled.source, compiled.flags)
|
|
1330
|
+
if (token !== sequenceRef.current) return
|
|
1331
|
+
if (result !== null) {
|
|
1332
|
+
if (result.timeout === true) {
|
|
1333
|
+
matchesRef.current = []
|
|
1334
|
+
clearFindHighlight('find')
|
|
1335
|
+
findStore.set({ count: 0, index: -1, capped: false, status: 'error', error: t('findTimeout') })
|
|
1336
|
+
return
|
|
1337
|
+
}
|
|
1338
|
+
if (result.ok === false) {
|
|
1339
|
+
matchesRef.current = []
|
|
1340
|
+
clearFindHighlight('find')
|
|
1341
|
+
findStore.set({ count: 0, index: -1, capped: false, status: 'error', error: t('findInvalidRegex', { message: String(result.error || '') }) })
|
|
1342
|
+
return
|
|
1343
|
+
}
|
|
1344
|
+
const compact = []
|
|
1345
|
+
let cursor = 0
|
|
1346
|
+
for (let groupIndex = 0; groupIndex < texts.length; groupIndex++) {
|
|
1347
|
+
if (texts[groupIndex] === null) continue
|
|
1348
|
+
compact.push(groupIndex)
|
|
1349
|
+
cursor++
|
|
1350
|
+
}
|
|
1351
|
+
rawMatches = []
|
|
1352
|
+
for (const item of result.matches) {
|
|
1353
|
+
const groupIndex = compact[item[0]]
|
|
1354
|
+
if (groupIndex === undefined) continue
|
|
1355
|
+
rawMatches.push({ groupIndex, start: item[1] + clips[groupIndex][0], end: item[2] + clips[groupIndex][0] })
|
|
1356
|
+
}
|
|
1357
|
+
capped = result.capped === true
|
|
1358
|
+
}
|
|
1359
|
+
} catch { /* fall back below */ }
|
|
1360
|
+
}
|
|
1361
|
+
if (rawMatches === null) {
|
|
1362
|
+
rawMatches = []
|
|
1363
|
+
const re = new RegExp(compiled.source, compiled.flags)
|
|
1364
|
+
const started = performance.now()
|
|
1365
|
+
for (let groupIndex = 0; groupIndex < texts.length; groupIndex++) {
|
|
1366
|
+
const text = texts[groupIndex]
|
|
1367
|
+
if (text === null) continue
|
|
1368
|
+
re.lastIndex = 0
|
|
1369
|
+
let match
|
|
1370
|
+
while ((match = re.exec(text)) !== null) {
|
|
1371
|
+
if (match[0].length === 0) { re.lastIndex++; continue }
|
|
1372
|
+
const start = clips[groupIndex][0] + match.index
|
|
1373
|
+
const end = start + match[0].length
|
|
1374
|
+
if (state.wholeWord && !wholeWordAt(groups[groupIndex].text, start, end)) continue
|
|
1375
|
+
rawMatches.push({ groupIndex, start, end })
|
|
1376
|
+
if (rawMatches.length >= FIND_MAX_MATCHES) break
|
|
1377
|
+
}
|
|
1378
|
+
if (rawMatches.length >= FIND_MAX_MATCHES) break
|
|
1379
|
+
if (performance.now() - started > 1500) { capped = true; break }
|
|
1380
|
+
}
|
|
1381
|
+
capped = capped || rawMatches.length >= FIND_MAX_MATCHES
|
|
1382
|
+
} else if (state.wholeWord) {
|
|
1383
|
+
rawMatches = rawMatches.filter(raw => {
|
|
1384
|
+
const group = groups[raw.groupIndex]
|
|
1385
|
+
return group !== undefined && wholeWordAt(group.text, raw.start, raw.end)
|
|
1386
|
+
})
|
|
1387
|
+
capped = capped || rawMatches.length >= FIND_MAX_MATCHES
|
|
1388
|
+
}
|
|
1389
|
+
if (token !== sequenceRef.current) return
|
|
1390
|
+
const matches = []
|
|
1391
|
+
for (const raw of rawMatches) {
|
|
1392
|
+
const group = groups[raw.groupIndex]
|
|
1393
|
+
if (group === undefined) continue
|
|
1394
|
+
const range = rangeForGroupOffsets(group, raw.start, raw.end)
|
|
1395
|
+
if (range === null) continue
|
|
1396
|
+
const row = group.block.closest('[data-chat-node-key]')
|
|
1397
|
+
matches.push({ range, rowKey: row === null ? '' : (row.getAttribute('data-chat-node-key') || '') })
|
|
1398
|
+
}
|
|
1399
|
+
if (token !== sequenceRef.current) return
|
|
1400
|
+
matchesRef.current = matches
|
|
1401
|
+
const next = matches.length === 0 ? -1 : (keepActive && activeRef.current >= 0 ? Math.min(activeRef.current, matches.length - 1) : 0)
|
|
1402
|
+
activeRef.current = next
|
|
1403
|
+
const history = findHistoryButton(scopeRef.current)
|
|
1404
|
+
findStore.set({
|
|
1405
|
+
count: matches.length,
|
|
1406
|
+
index: next,
|
|
1407
|
+
capped,
|
|
1408
|
+
status: matches.length === 0 ? 'none' : 'idle',
|
|
1409
|
+
error: '',
|
|
1410
|
+
hint: matches.length === 0 && history !== null && !history.disabled ? t('findLoadedOnly') : '',
|
|
1411
|
+
})
|
|
1412
|
+
if (next >= 0) { applyHighlight(next); revealActive() }
|
|
1413
|
+
else { clearFindHighlight('find'); setFindScope(state.scope === 'selection' && state.selection ? state.selection.range : null) }
|
|
1414
|
+
}, [applyHighlight, revealActive, state.caseSensitive, state.mode, state.query, state.scope, state.selection, state.sessionId, state.wholeWord])
|
|
1415
|
+
const runScanRef = useRef(runScan)
|
|
1416
|
+
runScanRef.current = runScan
|
|
1417
|
+
|
|
1418
|
+
useEffect(() => {
|
|
1419
|
+
if (!state.open) return undefined
|
|
1420
|
+
returnFocusRef.current = document.activeElement instanceof HTMLElement ? document.activeElement : null
|
|
1421
|
+
const measure = () => {
|
|
1422
|
+
const scope = conversationScope(findStore.get().sessionId) || conversationScope('')
|
|
1423
|
+
if (scope === null) { closeFind(); return }
|
|
1424
|
+
scopeRef.current = scope
|
|
1425
|
+
const rect = scope.getBoundingClientRect()
|
|
1426
|
+
const width = Math.max(280, Math.min(560, rect.width - 24))
|
|
1427
|
+
setAnchor({ top: Math.max(8, rect.top + 8), right: Math.max(8, window.innerWidth - rect.right + 12), width })
|
|
1428
|
+
}
|
|
1429
|
+
measure()
|
|
1430
|
+
const timer = setTimeout(() => {
|
|
1431
|
+
if (inputRef.current !== null) { inputRef.current.focus(); inputRef.current.select() }
|
|
1432
|
+
}, 0)
|
|
1433
|
+
const onResize = () => measure()
|
|
1434
|
+
window.addEventListener('resize', onResize)
|
|
1435
|
+
let observer = null
|
|
1436
|
+
const schedule = () => {
|
|
1437
|
+
clearTimeout(debounceRef.current)
|
|
1438
|
+
debounceRef.current = setTimeout(() => { if (findStore.get().open) void runScanRef.current(true) }, 400)
|
|
1439
|
+
}
|
|
1440
|
+
const scope = scopeRef.current
|
|
1441
|
+
if (scope !== null) {
|
|
1442
|
+
try {
|
|
1443
|
+
observer = new MutationObserver(schedule)
|
|
1444
|
+
observer.observe(scope, { childList: true, subtree: true, characterData: true })
|
|
1445
|
+
} catch { /* observer unavailable */ }
|
|
1446
|
+
}
|
|
1447
|
+
return () => {
|
|
1448
|
+
clearTimeout(timer)
|
|
1449
|
+
window.removeEventListener('resize', onResize)
|
|
1450
|
+
clearTimeout(debounceRef.current)
|
|
1451
|
+
if (observer !== null) observer.disconnect()
|
|
1452
|
+
for (const id of revealTimersRef.current) clearTimeout(id)
|
|
1453
|
+
revealTimersRef.current = []
|
|
1454
|
+
sequenceRef.current++
|
|
1455
|
+
clearFindHighlight('find')
|
|
1456
|
+
setFindScope(null)
|
|
1457
|
+
const target = returnFocusRef.current
|
|
1458
|
+
if (target !== null && target.isConnected) target.focus({ preventScroll: true })
|
|
1459
|
+
}
|
|
1460
|
+
}, [state.open])
|
|
1461
|
+
|
|
1462
|
+
useEffect(() => {
|
|
1463
|
+
if (!state.open) return undefined
|
|
1464
|
+
const timer = setTimeout(() => { void runScan(false) }, 150)
|
|
1465
|
+
return () => clearTimeout(timer)
|
|
1466
|
+
}, [state.open, state.query, state.mode, state.caseSensitive, state.wholeWord, state.scope, state.sessionId, state.selection, runScan])
|
|
1467
|
+
|
|
1468
|
+
// Close when the conversation is switched, hidden, or the panel opens.
|
|
1469
|
+
useEffect(() => {
|
|
1470
|
+
if (!state.open) return undefined
|
|
1471
|
+
const timer = setInterval(() => {
|
|
1472
|
+
if (!findStore.get().open) return
|
|
1473
|
+
if (store.isOpen()) { closeFind(); return }
|
|
1474
|
+
if (conversationScope(findStore.get().sessionId) === null) closeFind()
|
|
1475
|
+
}, 800)
|
|
1476
|
+
return () => clearInterval(timer)
|
|
1477
|
+
}, [state.open])
|
|
1478
|
+
|
|
1479
|
+
useEffect(() => {
|
|
1480
|
+
findApi.move = delta => move(delta)
|
|
1481
|
+
findApi.cancel = () => { sequenceRef.current++ }
|
|
1482
|
+
findApi.focus = () => { if (inputRef.current !== null) { inputRef.current.focus(); inputRef.current.select() } }
|
|
1483
|
+
return () => { findApi.move = () => false; findApi.cancel = () => {}; findApi.focus = () => {} }
|
|
1484
|
+
}, [move])
|
|
1485
|
+
|
|
1486
|
+
if (!state.open) return null
|
|
1487
|
+
const onInput = event => { findStore.set({ query: event.target.value, error: '', hint: '' }) }
|
|
1488
|
+
const onKeyDown = event => {
|
|
1489
|
+
if (composingRef.current || event.isComposing) return
|
|
1490
|
+
if (event.key === 'Enter') { event.preventDefault(); move(event.shiftKey ? -1 : 0); return }
|
|
1491
|
+
if (event.key === 'Escape') { event.preventDefault(); closeFind(); return }
|
|
1492
|
+
if ((event.ctrlKey || event.metaKey) && event.shiftKey && String(event.key).toLowerCase() === 'c') {
|
|
1493
|
+
const match = matchesRef.current[activeRef.current]
|
|
1494
|
+
const text = match && match.range.startContainer.isConnected ? match.range.toString() : findStore.get().query
|
|
1495
|
+
if (text !== '') { event.preventDefault(); copyText(text).then(() => findStore.set({ hint: t('findCopied') })).catch(() => {}) }
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
const pickMode = event => { findStore.set({ mode: event.target.value }) }
|
|
1499
|
+
const toggle = key => { findStore.set({ [key]: !findStore.get()[key] }) }
|
|
1500
|
+
const toggleScope = () => {
|
|
1501
|
+
if (state.scope === 'selection') findStore.set({ scope: 'session', selection: null, hint: '' })
|
|
1502
|
+
else {
|
|
1503
|
+
const scope = scopeRef.current
|
|
1504
|
+
const selection = scope === null ? null : selectionSnapshot(scope)
|
|
1505
|
+
if (selection === null) { findStore.set({ hint: t('findPlaceholderSelection') }); return }
|
|
1506
|
+
findStore.set({ scope: 'selection', selection })
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
const countText = state.query === '' ? '—'
|
|
1510
|
+
: (state.count === 0 ? '0/0' : (state.index < 0 ? '…' : (state.index + 1) + '/' + state.count + (state.capped ? '+' : '')))
|
|
1511
|
+
const history = scopeRef.current === null ? null : findHistoryButton(scopeRef.current)
|
|
1512
|
+
const canLoadOlder = history !== null && !history.disabled
|
|
1513
|
+
const loadOlder = () => {
|
|
1514
|
+
if (scopeRef.current === null || !revealOlder(scopeRef.current)) return
|
|
1515
|
+
findStore.set({ hint: t('findLoadingOlder') })
|
|
1516
|
+
setTimeout(() => { if (findStore.get().open) void runScanRef.current(true) }, 700)
|
|
1517
|
+
}
|
|
1518
|
+
return h('div', {
|
|
1519
|
+
ref: barRef,
|
|
1520
|
+
className: 'dsh-locator-findbar',
|
|
1521
|
+
role: 'search',
|
|
1522
|
+
'aria-label': t('findAria'),
|
|
1523
|
+
style: anchor === null ? { top: 12, right: 12, width: 'min(560px,calc(100vw - 24px))' } : { top: anchor.top, right: anchor.right, width: anchor.width },
|
|
1524
|
+
},
|
|
1525
|
+
h('div', { className: 'dsh-locator-findbar-main' },
|
|
1526
|
+
h('span', { className: 'dsh-locator-findbar-glyph', 'aria-hidden': 'true' }, '⌕'),
|
|
1527
|
+
h('input', {
|
|
1528
|
+
ref: inputRef,
|
|
1529
|
+
className: 'dsh-locator-findbar-input',
|
|
1530
|
+
value: state.query,
|
|
1531
|
+
placeholder: state.scope === 'selection' ? t('findPlaceholderSelection') : t('findPlaceholder'),
|
|
1532
|
+
'aria-label': t('findAria'),
|
|
1533
|
+
'aria-invalid': state.error !== '' ? 'true' : undefined,
|
|
1534
|
+
spellCheck: false,
|
|
1535
|
+
autoComplete: 'off',
|
|
1536
|
+
onInput,
|
|
1537
|
+
onKeyDown,
|
|
1538
|
+
onCompositionStart: () => { composingRef.current = true },
|
|
1539
|
+
onCompositionEnd: () => { composingRef.current = false; void runScan(false) },
|
|
1540
|
+
}),
|
|
1541
|
+
h('span', { className: 'dsh-locator-findbar-count', 'aria-hidden': 'true' }, countText),
|
|
1542
|
+
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' },
|
|
1543
|
+
state.error !== '' ? state.error
|
|
1544
|
+
: state.query === '' ? ''
|
|
1545
|
+
: state.count === 0 ? t('findNoMatch')
|
|
1546
|
+
: (state.index + 1) + '/' + state.count),
|
|
1547
|
+
h('button', { className: 'dsh-locator-findbar-btn', title: t('findPrev'), 'aria-label': t('findPrev'),
|
|
1548
|
+
disabled: state.count === 0, onMouseDown: event => event.preventDefault(), onClick: () => move(-1) }, '↑'),
|
|
1549
|
+
h('button', { className: 'dsh-locator-findbar-btn', title: t('findNext'), 'aria-label': t('findNext'),
|
|
1550
|
+
disabled: state.count === 0, onMouseDown: event => event.preventDefault(), onClick: () => move(1) }, '↓'),
|
|
1551
|
+
h('button', { className: 'dsh-locator-findbar-btn', title: t('findClose'), 'aria-label': t('findClose'),
|
|
1552
|
+
onClick: () => closeFind() }, '×'),
|
|
1553
|
+
),
|
|
1554
|
+
h('div', { className: 'dsh-locator-findbar-opts' },
|
|
1555
|
+
h('label', { className: 'dsh-locator-findbar-label' }, t('findMode'),
|
|
1556
|
+
h('select', { value: state.mode, onChange: pickMode, 'aria-label': t('findMode') },
|
|
1557
|
+
h('option', { value: 'normal' }, t('modeNormal')),
|
|
1558
|
+
h('option', { value: 'extended' }, t('modeExtended')),
|
|
1559
|
+
h('option', { value: 'regex' }, t('modeRegex')),
|
|
1560
|
+
)),
|
|
1561
|
+
h('button', { className: 'dsh-locator-findbar-btn', title: t('findCase'), 'aria-label': t('findCase'),
|
|
1562
|
+
'aria-pressed': state.caseSensitive ? 'true' : 'false', onClick: () => toggle('caseSensitive') }, t('panelCaseShort')),
|
|
1563
|
+
h('button', { className: 'dsh-locator-findbar-btn', title: t('findWhole'), 'aria-label': t('findWhole'),
|
|
1564
|
+
'aria-pressed': state.wholeWord ? 'true' : 'false', onClick: () => toggle('wholeWord') }, t('panelWholeShort')),
|
|
1565
|
+
h('button', { className: 'dsh-locator-findbar-btn', title: t('findWrap'), 'aria-label': t('findWrap'),
|
|
1566
|
+
'aria-pressed': state.wrap ? 'true' : 'false', onClick: () => toggle('wrap') }, '↻'),
|
|
1567
|
+
h('button', { className: 'dsh-locator-findbar-btn', title: t('findReverse'), 'aria-label': t('findReverse'),
|
|
1568
|
+
'aria-pressed': state.direction === -1 ? 'true' : 'false', onClick: () => findStore.set({ direction: state.direction === -1 ? 1 : -1 }) }, '⇅'),
|
|
1569
|
+
h('button', { className: 'dsh-locator-findbar-btn', title: t('findScope'), 'aria-label': t('findScope'),
|
|
1570
|
+
'aria-pressed': state.scope === 'selection' ? 'true' : 'false', onClick: toggleScope }, '⛶'),
|
|
1571
|
+
state.scope === 'selection' && state.selection
|
|
1572
|
+
? h('span', { className: 'dsh-locator-findbar-chip' }, t('findScopeChip', { n: state.selection.chars }),
|
|
1573
|
+
h('button', { onClick: () => findStore.set({ scope: 'session', selection: null, hint: '' }), 'aria-label': t('findClose') }, '×'))
|
|
1574
|
+
: h('span', { className: 'dsh-locator-findbar-chip' }, t('findSessionChip')),
|
|
1575
|
+
state.mode === 'extended' ? h('span', { className: 'dsh-locator-findbar-label', style: { opacity: .55 } }, t('findExtendedHelp')) : null,
|
|
1576
|
+
),
|
|
1577
|
+
state.error !== '' ? h('div', { className: 'dsh-locator-findbar-error', role: 'alert' }, state.error) : null,
|
|
1578
|
+
h('div', { className: 'dsh-locator-findbar-status' },
|
|
1579
|
+
h('span', null, state.query === '' ? t('findEmpty') : state.count === 0 ? t('findNoMatch') : (state.capped ? t('findCapped') : '')),
|
|
1580
|
+
state.hint !== '' ? h('span', null, state.hint) : null,
|
|
1581
|
+
h('span', { className: 'grow' }),
|
|
1582
|
+
canLoadOlder && state.query !== '' && state.count === 0
|
|
1583
|
+
? h('button', { className: 'dsh-locator-dirbtn', onClick: loadOlder }, t('findOlder'))
|
|
1584
|
+
: null,
|
|
1585
|
+
h('kbd', null, 'Enter'), h('span', null, '↓'),
|
|
1586
|
+
h('kbd', null, 'Shift+Enter'), h('span', null, '↑'),
|
|
1587
|
+
h('kbd', null, 'Esc'),
|
|
1588
|
+
),
|
|
1589
|
+
)
|
|
1590
|
+
}
|
|
1591
|
+
|
|
815
1592
|
function Panel() {
|
|
816
1593
|
const open = useOpen()
|
|
817
1594
|
const prefs = useMemo(readPrefs, [])
|
|
@@ -825,6 +1602,10 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
825
1602
|
const [took, setTook] = useState(0)
|
|
826
1603
|
const [status, setStatus] = useState(null)
|
|
827
1604
|
const [availableContent, setAvailableContent] = useState(true)
|
|
1605
|
+
const [matchMode, setMatchMode] = useState(prefs.matchMode || 'normal')
|
|
1606
|
+
const [caseSensitive, setCaseSensitive] = useState(prefs.caseSensitive === true)
|
|
1607
|
+
const [wholeWord, setWholeWord] = useState(prefs.wholeWord === true)
|
|
1608
|
+
const [sessionOnly, setSessionOnly] = useState(false)
|
|
828
1609
|
const [selected, setSelected] = useState(0)
|
|
829
1610
|
const [toast, setToast] = useState('')
|
|
830
1611
|
const [refreshTick, setRefreshTick] = useState(0)
|
|
@@ -834,14 +1615,20 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
834
1615
|
const listRef = useRef(null)
|
|
835
1616
|
|
|
836
1617
|
const close = useCallback(() => store.set(false), [])
|
|
837
|
-
const
|
|
1618
|
+
const activeScope = conversationScope('')
|
|
1619
|
+
const activeSessionId = activeScope === null ? '' : (activeScope.getAttribute('data-conversation-session') || '')
|
|
1620
|
+
const effectiveScopes = sessionOnly
|
|
1621
|
+
? { content: scopes.content, files: false, dirs: false }
|
|
1622
|
+
: { content: scopes.content, files: scopes.files, dirs: scopes.dirs }
|
|
1623
|
+
const anyScope = effectiveScopes.content || effectiveScopes.files || effectiveScopes.dirs
|
|
838
1624
|
|
|
839
1625
|
useEffect(() => {
|
|
840
|
-
writePrefs({ scopes, sort, desc })
|
|
841
|
-
}, [scopes, sort, desc])
|
|
1626
|
+
writePrefs({ scopes, sort, desc, matchMode, caseSensitive, wholeWord })
|
|
1627
|
+
}, [scopes, sort, desc, matchMode, caseSensitive, wholeWord])
|
|
842
1628
|
|
|
843
1629
|
useEffect(() => {
|
|
844
1630
|
if (!open) return undefined
|
|
1631
|
+
closeFind()
|
|
845
1632
|
const previous = document.activeElement
|
|
846
1633
|
setError('')
|
|
847
1634
|
const timer = setTimeout(() => {
|
|
@@ -894,13 +1681,18 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
894
1681
|
setError('')
|
|
895
1682
|
post('/dsh-locator/combined', {
|
|
896
1683
|
query,
|
|
897
|
-
content:
|
|
898
|
-
files:
|
|
899
|
-
dirs:
|
|
1684
|
+
content: effectiveScopes.content,
|
|
1685
|
+
files: effectiveScopes.files,
|
|
1686
|
+
dirs: effectiveScopes.dirs,
|
|
900
1687
|
limit: PAGE_NAME,
|
|
901
1688
|
contentLimit: PAGE_CONTENT,
|
|
902
1689
|
sort,
|
|
903
1690
|
desc,
|
|
1691
|
+
matchMode,
|
|
1692
|
+
caseSensitive,
|
|
1693
|
+
wholeWord,
|
|
1694
|
+
sessionId: sessionOnly ? activeSessionId : '',
|
|
1695
|
+
count: matchMode === 'regex' ? 'none' : 'exact',
|
|
904
1696
|
}, controller.signal).then(data => {
|
|
905
1697
|
if (controller.signal.aborted) return
|
|
906
1698
|
setGroups({
|
|
@@ -918,7 +1710,7 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
918
1710
|
})
|
|
919
1711
|
}, 140)
|
|
920
1712
|
return () => clearTimeout(timer)
|
|
921
|
-
}, [open, query, sort, desc, scopes.content, scopes.files, scopes.dirs, refreshTick, anyScope])
|
|
1713
|
+
}, [open, query, sort, desc, scopes.content, scopes.files, scopes.dirs, matchMode, caseSensitive, wholeWord, sessionOnly, activeSessionId, refreshTick, anyScope, effectiveScopes.content, effectiveScopes.files, effectiveScopes.dirs])
|
|
922
1714
|
|
|
923
1715
|
const contentBuilding = groups !== null && groups.content !== undefined && groups.content.building === true
|
|
924
1716
|
useEffect(() => {
|
|
@@ -931,17 +1723,23 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
931
1723
|
if (groups === null) return []
|
|
932
1724
|
const out = []
|
|
933
1725
|
for (const group of ['content', 'files', 'dirs']) {
|
|
934
|
-
if (!
|
|
1726
|
+
if (!effectiveScopes[group]) continue
|
|
935
1727
|
const hits = groups[group] && Array.isArray(groups[group].hits) ? groups[group].hits : []
|
|
936
1728
|
for (const hit of hits) out.push({ group, hit })
|
|
937
1729
|
}
|
|
938
1730
|
return out
|
|
939
|
-
}, [groups,
|
|
1731
|
+
}, [groups, effectiveScopes.content, effectiveScopes.files, effectiveScopes.dirs])
|
|
940
1732
|
|
|
941
1733
|
useEffect(() => {
|
|
942
1734
|
setSelected(value => Math.min(Math.max(0, value), Math.max(0, rows.length - 1)))
|
|
943
1735
|
}, [rows.length])
|
|
944
1736
|
|
|
1737
|
+
useEffect(() => {
|
|
1738
|
+
if (!open) return
|
|
1739
|
+
const row = document.getElementById('dsh-locator-opt-' + selected)
|
|
1740
|
+
if (row !== null) row.scrollIntoView({ block: 'nearest' })
|
|
1741
|
+
}, [open, selected, rows.length])
|
|
1742
|
+
|
|
945
1743
|
const notify = useCallback(message => {
|
|
946
1744
|
setToast(message)
|
|
947
1745
|
setTimeout(() => setToast(''), 1600)
|
|
@@ -1016,6 +1814,11 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
1016
1814
|
toggleScope(scopeDigit === '1' ? 'content' : scopeDigit === '2' ? 'files' : 'dirs')
|
|
1017
1815
|
return
|
|
1018
1816
|
}
|
|
1817
|
+
if (event.key === 'F3') {
|
|
1818
|
+
event.preventDefault()
|
|
1819
|
+
if (rows.length > 0) setSelected(value => (value + (event.shiftKey ? -1 : 1) + rows.length) % rows.length)
|
|
1820
|
+
return
|
|
1821
|
+
}
|
|
1019
1822
|
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
|
1020
1823
|
event.preventDefault()
|
|
1021
1824
|
setSelected(value => Math.max(0, Math.min(rows.length - 1, value + (event.key === 'ArrowDown' ? 1 : -1))))
|
|
@@ -1071,23 +1874,35 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
1071
1874
|
const content = groups !== null ? groups.content : undefined
|
|
1072
1875
|
const files = groups !== null ? groups.files : undefined
|
|
1073
1876
|
const dirs = groups !== null ? groups.dirs : undefined
|
|
1877
|
+
const countLabel = (group, fallback) => {
|
|
1878
|
+
const count = group && group.count
|
|
1879
|
+
if (count && typeof count.value === 'number' && Number.isFinite(count.value)) {
|
|
1880
|
+
return (count.kind === 'bounded' ? '≥ ' : '') + String(count.value)
|
|
1881
|
+
}
|
|
1882
|
+
return String(fallback)
|
|
1883
|
+
}
|
|
1074
1884
|
const contentCount = content === undefined ? 0 : (content.returned ?? 0)
|
|
1075
1885
|
const filesCount = files === undefined ? 0 : (files.returned ?? files.hits?.length ?? 0)
|
|
1076
1886
|
const dirsCount = dirs === undefined ? 0 : (dirs.returned ?? dirs.hits?.length ?? 0)
|
|
1887
|
+
const contentCountText = content === undefined ? '' : countLabel(content, contentCount)
|
|
1888
|
+
const filesCountText = files === undefined ? '' : countLabel(files, filesCount)
|
|
1889
|
+
const dirsCountText = dirs === undefined ? '' : countLabel(dirs, dirsCount)
|
|
1077
1890
|
const totalRows = rows.length
|
|
1078
1891
|
|
|
1079
1892
|
const scopeBox = (key, label, count) => h('label', {
|
|
1080
|
-
className: 'dsh-locator-check' + (key === 'content' && !availableContent ? ' disabled' : '')
|
|
1081
|
-
|
|
1893
|
+
className: 'dsh-locator-check' + (key === 'content' && !availableContent ? ' disabled' : '')
|
|
1894
|
+
+ (sessionOnly && key !== 'content' ? ' disabled' : ''),
|
|
1895
|
+
title: key === 'content' && !availableContent ? t('contentDisabled')
|
|
1896
|
+
: sessionOnly && key !== 'content' ? t('panelSessionHint') : label,
|
|
1082
1897
|
},
|
|
1083
1898
|
h('input', {
|
|
1084
1899
|
type: 'checkbox',
|
|
1085
1900
|
checked: scopes[key],
|
|
1086
|
-
disabled: key === 'content' && !availableContent,
|
|
1901
|
+
disabled: (key === 'content' && !availableContent) || (sessionOnly && key !== 'content'),
|
|
1087
1902
|
onChange: () => toggleScope(key),
|
|
1088
1903
|
}),
|
|
1089
1904
|
label,
|
|
1090
|
-
count
|
|
1905
|
+
count ? h('span', { className: 'dsh-locator-count' }, count) : null,
|
|
1091
1906
|
)
|
|
1092
1907
|
|
|
1093
1908
|
const groupHead = (key, label, count, note) => h('div', {
|
|
@@ -1139,23 +1954,26 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
1139
1954
|
const hits = Array.isArray(group.hits) ? group.hits : []
|
|
1140
1955
|
const count = typeof group.returned === 'number' ? group.returned : hits.length
|
|
1141
1956
|
const note = key === 'content'
|
|
1142
|
-
? (
|
|
1143
|
-
:
|
|
1957
|
+
? (matchMode === 'regex' ? t('panelRegexPaused')
|
|
1958
|
+
: contentBuilding ? t('contentBuilding', { files: content?.files ?? 0, messages: content?.messages ?? 0 })
|
|
1959
|
+
: (content?.capped ? t('contentCapped', { n: count }) : (query.trim() !== '' ? t('relevance') : null)))
|
|
1144
1960
|
: (group.deduped > 0 ? t('deduped', { n: group.deduped }) : (group.truncated ? t('showing', { n: hits.length }) : null))
|
|
1145
1961
|
if (typeof group.error === 'string' && group.error !== '') {
|
|
1962
|
+
const message = group.error === 'content-regex-not-supported' ? t('panelRegexPaused') : group.error
|
|
1146
1963
|
return h('div', { className: 'dsh-locator-group', role: 'group', 'aria-labelledby': 'dsh-locator-group-' + key },
|
|
1147
1964
|
groupHead(key, label, count, null),
|
|
1148
|
-
h('div', { className: 'dsh-locator-groupempty', role: 'alert' }, t('errorPrefix') +
|
|
1965
|
+
h('div', { className: 'dsh-locator-groupempty', role: 'alert' }, t('errorPrefix') + message,
|
|
1149
1966
|
h('button', { className: 'dsh-locator-dirbtn dsh-locator-retry', onClick: () => setRefreshTick(v => v + 1) }, t('retry'))),
|
|
1150
1967
|
)
|
|
1151
1968
|
}
|
|
1152
1969
|
return h('div', { className: 'dsh-locator-group', role: 'group', 'aria-labelledby': 'dsh-locator-group-' + key },
|
|
1153
|
-
groupHead(key, label, count, note),
|
|
1970
|
+
groupHead(key, label, countLabel(group, count), note),
|
|
1154
1971
|
extra === undefined ? null : extra,
|
|
1155
1972
|
hits.length === 0
|
|
1156
1973
|
? h('div', { className: 'dsh-locator-groupempty' },
|
|
1157
1974
|
key === 'content' && scopes.content
|
|
1158
|
-
? (query.trim().length
|
|
1975
|
+
? (sessionOnly && query.trim().length >= 2 ? t('panelSessionHint')
|
|
1976
|
+
: query.trim().length < 2 ? t('contentShort') : (contentBuilding ? t('loading') : t('emptyResults')))
|
|
1159
1977
|
: (query.trim() === '' ? t('emptyQuery') : t('emptyResults')))
|
|
1160
1978
|
: hits.map(hit => { index++; return rowFor(key, hit, index) }),
|
|
1161
1979
|
)
|
|
@@ -1189,14 +2007,32 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
1189
2007
|
'aria-autocomplete': 'list',
|
|
1190
2008
|
'aria-activedescendant': totalRows > 0 ? 'dsh-locator-opt-' + selected : undefined,
|
|
1191
2009
|
}),
|
|
2010
|
+
h('span', { className: 'dsh-locator-count', 'aria-hidden': 'true' },
|
|
2011
|
+
loading ? t('loading') : totalRows > 0 ? t('panelTotal', { index: selected + 1, total: totalRows }) : ''),
|
|
1192
2012
|
h('span', { className: 'dsh-locator-kbd' }, 'Ctrl+Shift+F'),
|
|
1193
2013
|
h('button', { className: 'dsh-locator-close', onClick: close, title: t('closeTitle'), 'aria-label': t('closeLabel') }, '×'),
|
|
1194
2014
|
),
|
|
1195
2015
|
h('div', { className: 'dsh-locator-bar' },
|
|
1196
|
-
scopeBox('content', t('scopeContent'),
|
|
1197
|
-
scopeBox('files', t('scopeFiles'),
|
|
1198
|
-
scopeBox('dirs', t('scopeDirs'),
|
|
2016
|
+
scopeBox('content', t('scopeContent'), contentCountText),
|
|
2017
|
+
scopeBox('files', t('scopeFiles'), filesCountText),
|
|
2018
|
+
scopeBox('dirs', t('scopeDirs'), dirsCountText),
|
|
1199
2019
|
h('span', { className: 'grow' }),
|
|
2020
|
+
h('label', { className: 'dsh-locator-check' + (activeSessionId === '' ? ' disabled' : ''),
|
|
2021
|
+
title: activeSessionId === '' ? t('findNoSession') : t('panelSessionHint') },
|
|
2022
|
+
h('input', { type: 'checkbox', checked: sessionOnly, disabled: activeSessionId === '',
|
|
2023
|
+
onChange: () => setSessionOnly(value => !value) }),
|
|
2024
|
+
t('panelRangeSession')),
|
|
2025
|
+
h('label', { className: 'dsh-locator-check' }, t('panelModeLabel'),
|
|
2026
|
+
h('select', { className: 'dsh-locator-sort', value: matchMode,
|
|
2027
|
+
onChange: event => setMatchMode(['normal', 'extended', 'regex'].includes(event.target.value) ? event.target.value : 'normal') },
|
|
2028
|
+
h('option', { value: 'normal' }, t('modeNormal')),
|
|
2029
|
+
h('option', { value: 'extended' }, t('modeExtended')),
|
|
2030
|
+
h('option', { value: 'regex' }, t('modeRegex')),
|
|
2031
|
+
)),
|
|
2032
|
+
h('button', { className: 'dsh-locator-dirbtn', title: t('findCase'),
|
|
2033
|
+
'aria-pressed': caseSensitive ? 'true' : 'false', onClick: () => setCaseSensitive(value => !value) }, t('panelCaseShort')),
|
|
2034
|
+
h('button', { className: 'dsh-locator-dirbtn', title: t('findWhole'),
|
|
2035
|
+
'aria-pressed': wholeWord ? 'true' : 'false', onClick: () => setWholeWord(value => !value) }, t('panelWholeShort')),
|
|
1200
2036
|
h('label', { className: 'dsh-locator-check' }, t('sortLabel'),
|
|
1201
2037
|
h('select', {
|
|
1202
2038
|
className: 'dsh-locator-sort',
|
|
@@ -1224,6 +2060,7 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
1224
2060
|
roots: (status.roots || []).length,
|
|
1225
2061
|
})),
|
|
1226
2062
|
),
|
|
2063
|
+
sessionOnly ? h('div', { className: 'dsh-locator-groupempty' }, t('panelSessionHint')) : null,
|
|
1227
2064
|
h('div', { id: 'dsh-locator-results', className: 'dsh-locator-scroll', ref: listRef, role: 'listbox', 'aria-busy': loading ? 'true' : 'false' },
|
|
1228
2065
|
error !== ''
|
|
1229
2066
|
? h('div', { className: 'dsh-locator-empty', role: 'alert' }, t('errorPrefix') + error,
|
|
@@ -1245,7 +2082,7 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
1245
2082
|
+ (contentBuilding ? ' · ' + t('indexing') : '')),
|
|
1246
2083
|
h('span', { className: 'grow' }),
|
|
1247
2084
|
toast === '' ? null : h('span', { className: 'dsh-locator-toast' }, toast),
|
|
1248
|
-
h('span', null, t('footer')),
|
|
2085
|
+
h('span', null, t('footer') + ' · Ctrl+F ' + t('findOpen')),
|
|
1249
2086
|
),
|
|
1250
2087
|
h('div', { style: { position: 'absolute', width: 1, height: 1, overflow: 'hidden', clip: 'rect(0 0 0 0)' }, role: 'status', 'aria-live': 'polite' },
|
|
1251
2088
|
t('results', { content: contentCount, files: filesCount, dirs: dirsCount, ms: Math.round(took) })),
|
|
@@ -1267,22 +2104,69 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
1267
2104
|
inject: ['slots', 'uiWorkspace'],
|
|
1268
2105
|
apply(ctx) {
|
|
1269
2106
|
workspaceService = ctx.uiWorkspace ?? (typeof ctx.get === 'function' ? ctx.get('uiWorkspace') : null)
|
|
1270
|
-
|
|
2107
|
+
ctx.effect(() => {
|
|
2108
|
+
ensureStyles()
|
|
2109
|
+
return () => {
|
|
2110
|
+
const tag = document.querySelector('style[data-plugin-css="' + NS + '"]')
|
|
2111
|
+
if (tag !== null) tag.remove()
|
|
2112
|
+
}
|
|
2113
|
+
}, 'dsh-locator: styles')
|
|
1271
2114
|
ctx.effect(() => {
|
|
1272
2115
|
const onKey = event => {
|
|
1273
2116
|
if ((event.metaKey || event.ctrlKey) && event.shiftKey && String(event.key).toLowerCase() === 'f') {
|
|
1274
2117
|
event.preventDefault()
|
|
1275
2118
|
event.stopPropagation()
|
|
2119
|
+
closeFind()
|
|
1276
2120
|
store.toggle()
|
|
1277
2121
|
}
|
|
1278
2122
|
}
|
|
1279
2123
|
document.addEventListener('keydown', onKey, true)
|
|
1280
2124
|
return () => document.removeEventListener('keydown', onKey, true)
|
|
1281
2125
|
}, 'dsh-locator: search hotkey')
|
|
1282
|
-
ctx.effect(() =>
|
|
2126
|
+
ctx.effect(() => {
|
|
2127
|
+
const onFindKey = event => {
|
|
2128
|
+
if (event.defaultPrevented || event.isComposing) return
|
|
2129
|
+
const target = event.target
|
|
2130
|
+
if (target instanceof Element && target.closest('.xterm,.monaco-editor,[data-code-editor]') !== null) return
|
|
2131
|
+
const inFind = target instanceof Element && target.closest('.dsh-locator-findbar') !== null
|
|
2132
|
+
const ctrl = event.ctrlKey || event.metaKey
|
|
2133
|
+
if (ctrl && !event.shiftKey && !event.altKey && event.code === 'KeyF') {
|
|
2134
|
+
if (store.isOpen()) {
|
|
2135
|
+
event.preventDefault()
|
|
2136
|
+
event.stopPropagation()
|
|
2137
|
+
const panelInput = document.querySelector('.dsh-locator-input')
|
|
2138
|
+
if (panelInput !== null) { panelInput.focus(); panelInput.select() }
|
|
2139
|
+
return
|
|
2140
|
+
}
|
|
2141
|
+
if (document.querySelector('[role="dialog"][aria-modal="true"]') !== null && !inFind) return
|
|
2142
|
+
if (findStore.get().open) { event.preventDefault(); event.stopPropagation(); findApi.focus(); return }
|
|
2143
|
+
if (conversationScope('') === null) return
|
|
2144
|
+
event.preventDefault()
|
|
2145
|
+
event.stopPropagation()
|
|
2146
|
+
openFind({ selection: true })
|
|
2147
|
+
return
|
|
2148
|
+
}
|
|
2149
|
+
if (!ctrl && !event.altKey && event.code === 'F3') {
|
|
2150
|
+
if (!findStore.get().open) return
|
|
2151
|
+
event.preventDefault()
|
|
2152
|
+
event.stopPropagation()
|
|
2153
|
+
findApi.move(event.shiftKey ? -1 : 1)
|
|
2154
|
+
return
|
|
2155
|
+
}
|
|
2156
|
+
if (event.key === 'Escape' && findStore.get().open) {
|
|
2157
|
+
event.preventDefault()
|
|
2158
|
+
event.stopPropagation()
|
|
2159
|
+
closeFind()
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
document.addEventListener('keydown', onFindKey, true)
|
|
2163
|
+
return () => document.removeEventListener('keydown', onFindKey, true)
|
|
2164
|
+
}, 'dsh-locator: find hotkeys')
|
|
2165
|
+
ctx.effect(() => () => { closeFind(); cancelJump() }, 'dsh-locator: find cleanup')
|
|
1283
2166
|
try {
|
|
1284
2167
|
ctx.slots.inject('shell.overlay', () => {
|
|
1285
2168
|
const disposers = [
|
|
2169
|
+
ctx.slots.register({ name: 'shell.overlay', id: 'dsh-locator-findbar', order: 59 }, FindBar),
|
|
1286
2170
|
ctx.slots.register({ name: 'shell.overlay', id: 'dsh-locator-panel', order: 60 }, Panel),
|
|
1287
2171
|
ctx.slots.register({ name: 'shell.overlay', id: 'dsh-locator-notice', order: 61 }, Notice),
|
|
1288
2172
|
]
|
|
@@ -1295,6 +2179,12 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
1295
2179
|
} catch (error) {
|
|
1296
2180
|
console.error('dsh-locator: overlay slot unavailable', error)
|
|
1297
2181
|
}
|
|
2182
|
+
try {
|
|
2183
|
+
ctx.slots.inject('conversation.session.header.utilities', () =>
|
|
2184
|
+
ctx.slots.register({ name: 'conversation.session.header.utilities', id: 'dsh-locator-find-toggle', order: 30 }, FindToggle))
|
|
2185
|
+
} catch (error) {
|
|
2186
|
+
console.error('dsh-locator: session header slot unavailable', error)
|
|
2187
|
+
}
|
|
1298
2188
|
try {
|
|
1299
2189
|
ctx.slots.inject('sidebar.footer.action', () => ctx.slots.register({ name: 'sidebar.footer.action', id: 'dsh-locator-launch', order: 36 }, Launcher))
|
|
1300
2190
|
} catch (error) {
|