dsh-locator 0.7.6 → 0.8.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 +20 -0
- package/README-es.md +335 -106
- package/README-hi.md +335 -104
- package/README-pt.md +336 -103
- package/README.en.md +254 -125
- package/README.md +229 -85
- package/client.js +782 -320
- package/lib/combined.d.ts +110 -0
- package/lib/combined.d.ts.map +1 -0
- package/lib/combined.js +190 -0
- package/lib/combined.js.map +1 -0
- package/lib/http.d.ts.map +1 -1
- package/lib/http.js +30 -2
- package/lib/http.js.map +1 -1
- package/package.json +1 -1
- package/src/combined.ts +267 -0
- package/src/http.ts +36 -2
package/client.js
CHANGED
|
@@ -34,51 +34,65 @@ window.__ModuleLoader__.load({
|
|
|
34
34
|
const NS = 'dsh-locator'
|
|
35
35
|
const CSS = `
|
|
36
36
|
.dsh-locator-overlay{position:fixed;inset:0;z-index:80;display:flex;align-items:flex-start;justify-content:center;
|
|
37
|
-
padding:9vh 16px 16px;background:rgba(15,17,21,.38)
|
|
38
|
-
.dsh-locator-panel{display:flex;flex-direction:column;width:min(
|
|
39
|
-
border-radius:14px;border:1px solid var(--dsw-alias-border-base,rgba(120,125,135,.28));
|
|
40
|
-
background:var(--dsw-alias-bg-base,#fff);box-shadow:0 24px 64px rgba(0,0,0,.28);font:13px/1.45 system-ui,-apple-system,"Segoe UI",sans-serif
|
|
37
|
+
padding:max(9vh,64px) 16px 16px;background:var(--dsw-alias-bg-mask-1,rgba(15,17,21,.38));color-scheme:light dark}
|
|
38
|
+
.dsh-locator-panel{display:flex;flex-direction:column;width:min(920px,94vw);max-height:min(76vh,720px);overflow:hidden;
|
|
39
|
+
border-radius:var(--dsw-radius-lg,14px);border:1px solid var(--dsw-alias-border-base,rgba(120,125,135,.28));
|
|
40
|
+
background:var(--dsw-alias-bg-base,#fff);box-shadow:0 24px 64px rgba(0,0,0,.28);font:13px/1.45 system-ui,-apple-system,"Segoe UI",sans-serif;
|
|
41
|
+
animation:dsh-locator-in 120ms ease-out}
|
|
42
|
+
@keyframes dsh-locator-in{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}
|
|
43
|
+
@media (prefers-reduced-motion:reduce){.dsh-locator-panel{animation:none}}
|
|
41
44
|
.dsh-locator-head{display:flex;align-items:center;gap:10px;padding:12px 14px;border-bottom:1px solid var(--dsw-alias-border-base,rgba(120,125,135,.2))}
|
|
42
45
|
.dsh-locator-glyph{font-size:15px;opacity:.7}
|
|
43
46
|
.dsh-locator-input{flex:1;min-width:0;border:0;outline:0;background:transparent;color:inherit;font:inherit;font-size:14px;padding:2px 0}
|
|
44
47
|
.dsh-locator-kbd{border:1px solid var(--dsw-alias-border-base,rgba(120,125,135,.35));border-radius:6px;padding:1px 6px;font-size:11px;opacity:.6;white-space:nowrap}
|
|
45
48
|
.dsh-locator-close{border:0;background:transparent;color:inherit;opacity:.55;cursor:pointer;font-size:16px;padding:2px 4px;border-radius:6px}
|
|
46
|
-
.dsh-locator-close:hover{opacity:.9;background:rgba(120,125,135,.12)}
|
|
49
|
+
.dsh-locator-close:hover{opacity:.9;background:var(--dsw-alias-interactive-bg-hover,rgba(120,125,135,.12))}
|
|
47
50
|
.dsh-locator-bar{display:flex;align-items:center;gap:12px;padding:7px 14px;border-bottom:1px solid var(--dsw-alias-border-base,rgba(120,125,135,.16));
|
|
48
|
-
font-size:12px;
|
|
49
|
-
.dsh-locator-check{display:inline-flex;align-items:center;gap:5px;cursor:pointer;user-select:none}
|
|
50
|
-
.dsh-locator-
|
|
51
|
-
.dsh-locator-
|
|
52
|
-
.dsh-locator-
|
|
53
|
-
.dsh-locator-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
.dsh-locator-
|
|
57
|
-
.dsh-locator-
|
|
58
|
-
.dsh-locator-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
.dsh-locator-
|
|
63
|
-
.dsh-locator-
|
|
64
|
-
.dsh-locator-
|
|
65
|
-
|
|
66
|
-
.dsh-locator-
|
|
67
|
-
.dsh-locator-
|
|
68
|
-
.dsh-locator-
|
|
69
|
-
.dsh-locator-
|
|
70
|
-
|
|
51
|
+
font-size:12px;flex-wrap:wrap}
|
|
52
|
+
.dsh-locator-check{display:inline-flex;align-items:center;gap:5px;cursor:pointer;user-select:none;white-space:nowrap}
|
|
53
|
+
.dsh-locator-check.disabled{opacity:.45;cursor:not-allowed}
|
|
54
|
+
.dsh-locator-count{opacity:.55;font-variant-numeric:tabular-nums}
|
|
55
|
+
.dsh-locator-bar .grow{flex:1}
|
|
56
|
+
.dsh-locator-sort{border:1px solid var(--dsw-alias-border-base,rgba(120,125,135,.3));border-radius:6px;background:transparent;color:inherit;font:inherit;font-size:11.5px;padding:2px 4px}
|
|
57
|
+
.dsh-locator-dirbtn{border:1px solid var(--dsw-alias-border-base,rgba(120,125,135,.3));border-radius:6px;background:transparent;color:inherit;font:inherit;font-size:11.5px;padding:2px 6px;cursor:pointer}
|
|
58
|
+
.dsh-locator-status{margin-left:auto;opacity:.65;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:50%}
|
|
59
|
+
.dsh-locator-scroll{overflow:auto;flex:1;min-height:160px}
|
|
60
|
+
.dsh-locator-group{display:block;border-bottom:1px solid var(--dsw-alias-border-base,rgba(120,125,135,.12))}
|
|
61
|
+
.dsh-locator-grouphead{position:sticky;top:0;z-index:2;display:flex;align-items:center;gap:8px;padding:6px 14px;
|
|
62
|
+
font-size:11px;font-weight:500;letter-spacing:.03em;text-transform:uppercase;color:var(--dsw-alias-label-tertiary,rgba(120,125,135,.9));
|
|
63
|
+
background:var(--dsw-alias-menu-group-header-fill,var(--dsw-alias-bg-base,#fff));border-bottom:1px solid var(--dsw-alias-border-base,rgba(120,125,135,.1));
|
|
64
|
+
cursor:default}
|
|
65
|
+
.dsh-locator-grouphead .grow{flex:1}
|
|
66
|
+
.dsh-locator-groupempty{padding:10px 14px;font-size:12px;opacity:.55}
|
|
67
|
+
.dsh-locator-row{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:2px 12px;align-items:baseline;
|
|
68
|
+
padding:6px 14px;border-bottom:1px solid var(--dsw-alias-border-base,rgba(120,125,135,.07));cursor:pointer;position:relative}
|
|
69
|
+
.dsh-locator-row:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(120,125,135,.08))}
|
|
70
|
+
.dsh-locator-row.sel{background:var(--dsw-alias-interactive-bg-active,rgba(64,116,255,.12))}
|
|
71
|
+
.dsh-locator-row.sel::before{content:'';position:absolute;left:0;top:0;bottom:0;width:2px;background:#4f7cff}
|
|
72
|
+
.dsh-locator-rowcontent{padding-top:8px;padding-bottom:8px}
|
|
73
|
+
.dsh-locator-title{display:flex;align-items:baseline;gap:7px;min-width:0;font-size:13px}
|
|
74
|
+
.dsh-locator-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
75
|
+
.dsh-locator-badge{flex:none;font-size:10px;padding:0 5px;border-radius:4px;background:var(--dsw-alias-bg-layer-2,rgba(120,125,135,.14));opacity:.8;text-transform:uppercase;letter-spacing:.04em}
|
|
76
|
+
.dsh-locator-badge.chat{background:rgba(79,124,255,.16);color:var(--dsw-alias-brand-primary,#4f7cff)}
|
|
77
|
+
.dsh-locator-snippet{grid-column:1/-1;margin-top:2px;font-size:12.5px;opacity:.85;overflow:hidden;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;line-height:1.4}
|
|
78
|
+
.dsh-locator-path{grid-column:1/-1;font-size:11.5px;opacity:.5;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;direction:rtl;text-align:left}
|
|
79
|
+
.dsh-locator-meta{font-size:11.5px;opacity:.65;white-space:nowrap;font-variant-numeric:tabular-nums;text-align:right}
|
|
80
|
+
.dsh-locator-hit{background:rgba(245,158,11,.32);border-radius:2px;color:inherit}
|
|
81
|
+
.dsh-locator-empty{padding:36px 16px;text-align:center;opacity:.65;font-size:12.5px;white-space:pre-wrap}
|
|
82
|
+
.dsh-locator-empty .dsh-locator-retry{margin-left:8px}
|
|
83
|
+
.dsh-locator-foot{display:flex;align-items:center;gap:10px;padding:6px 14px;border-top:1px solid var(--dsw-alias-border-base,rgba(120,125,135,.16));
|
|
84
|
+
font-size:11px;opacity:.7;flex-wrap:wrap}
|
|
71
85
|
.dsh-locator-foot .grow{flex:1}
|
|
72
|
-
.dsh-locator-toast{color
|
|
73
|
-
@media (prefers-color-scheme:dark){
|
|
74
|
-
.dsh-locator-overlay{background:rgba(0,0,0,.55)}
|
|
75
|
-
.dsh-locator-panel{box-shadow:0 24px 64px rgba(0,0,0,.6)}
|
|
76
|
-
}
|
|
86
|
+
.dsh-locator-toast{color:var(--dsw-alias-state-success-primary,#2e7d32)}
|
|
77
87
|
.dsh-locator-launch{display:inline-flex;align-items:center;justify-content:center;width:26px;height:26px;border:0;border-radius:7px;
|
|
78
88
|
background:transparent;color:inherit;opacity:.65;cursor:pointer;font-size:14px}
|
|
79
89
|
.dsh-locator-launch:hover{opacity:1;background:rgba(120,125,135,.14)}
|
|
90
|
+
body[data-ds-dark-theme] .dsh-locator-panel{box-shadow:0 24px 64px rgba(0,0,0,.6)}
|
|
91
|
+
body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask-1,rgba(0,0,0,.55))}
|
|
92
|
+
.dsh-locator-target{outline:2px solid rgba(245,158,11,.75);outline-offset:2px;border-radius:6px}
|
|
93
|
+
::highlight(dsh-locator-find){background-color:rgba(245,158,11,.28);color:inherit}
|
|
94
|
+
::highlight(dsh-locator-find-active){background-color:rgba(245,158,11,.45);color:inherit}
|
|
80
95
|
`
|
|
81
|
-
|
|
82
96
|
function ensureStyles() {
|
|
83
97
|
if (typeof document === 'undefined') return
|
|
84
98
|
if (document.querySelector('style[data-plugin-css="' + NS + '"]') !== null) return
|
|
@@ -104,78 +118,132 @@ window.__ModuleLoader__.load({
|
|
|
104
118
|
return data
|
|
105
119
|
}
|
|
106
120
|
|
|
107
|
-
|
|
108
121
|
// ── i18n ────────────────────────────────────────────────────────────────
|
|
109
122
|
const STRINGS = {
|
|
110
123
|
en: {
|
|
111
|
-
|
|
112
|
-
placeholder: 'Search
|
|
124
|
+
scopeContent: 'Content', scopeFiles: 'Files', scopeDirs: 'Folders',
|
|
125
|
+
placeholder: 'Search names, paths, bodies or sessions… e.g. ext:pdf size:>10mb dm:thisweek path:src',
|
|
113
126
|
searchLabel: 'Search files', dialog: 'Local file search', closeTitle: 'Close (Esc)', closeLabel: 'Close',
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
127
|
+
sortLabel: 'Sort', sortMtime: 'Modified', sortName: 'Name', sortSize: 'Size', sortPath: 'Path',
|
|
128
|
+
asc: 'Ascending', descOrder: 'Descending',
|
|
129
|
+
groupContent: 'Content', groupFiles: 'Files', groupDirs: 'Folders',
|
|
130
|
+
relevance: 'by relevance', contentBuilding: 'indexing… {files} files / {messages} messages',
|
|
131
|
+
contentShort: 'Type at least 2 characters to search bodies and sessions',
|
|
132
|
+
contentDisabled: 'Content index is disabled (contentIndex: false)',
|
|
133
|
+
contentCapped: 'first {n}', showing: 'first {n}', deduped: '{n} name hits merged into content', loading: 'Searching…',
|
|
134
|
+
emptyQuery: 'Type to search file names, paths, bodies and sessions',
|
|
135
|
+
emptyResults: 'No matches · try fewer scopes or another keyword',
|
|
136
|
+
emptyScopes: 'Select at least one scope (Content, Files, Folders)',
|
|
137
|
+
errorPrefix: 'Search failed: ', retry: 'Retry', indexing: 'indexing, results may be incomplete',
|
|
138
|
+
statusLoading: 'Reading index status…', status: 'Index {entries} entries · {mode} · {roots} roots',
|
|
139
|
+
live: 'live', snapshot: 'snapshot', usn: 'USN',
|
|
140
|
+
copiedPath: 'Path copied', revealed: 'Revealed in file manager', opened: 'Opened', inserted: 'Inserted @file',
|
|
141
|
+
insertFailed: 'Insert failed; @file copied', jumped: 'Opened session', jumpFailed: 'Opened the session but could not locate the match',
|
|
142
|
+
footer: '↑↓ select · Enter open/jump · Ctrl+Shift+C copy path · Ctrl+O open · Alt+1–3 scopes · Esc close',
|
|
143
|
+
results: 'content {content} · files {files} · folders {dirs} · {ms} ms',
|
|
121
144
|
launcher: 'Local file search (Ctrl+Shift+F)',
|
|
122
145
|
justNow: 'just now', minutesAgo: '{n} min ago', hoursAgo: '{n} h ago',
|
|
146
|
+
phraseNotFound: 'No match in the conversation (it may not be loaded yet)',
|
|
123
147
|
},
|
|
124
148
|
zh: {
|
|
125
|
-
|
|
126
|
-
placeholder: '
|
|
149
|
+
scopeContent: '内容', scopeFiles: '文件', scopeDirs: '目录',
|
|
150
|
+
placeholder: '搜文件名、路径、正文或会话… 支持 ext:pdf size:>10mb dm:thisweek path:src',
|
|
127
151
|
searchLabel: '文件搜索', dialog: '本地文件搜索', closeTitle: '关闭 (Esc)', closeLabel: '关闭',
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
152
|
+
sortLabel: '排序', sortMtime: '修改时间', sortName: '名称', sortSize: '大小', sortPath: '路径',
|
|
153
|
+
asc: '升序', descOrder: '降序',
|
|
154
|
+
groupContent: '内容', groupFiles: '文件', groupDirs: '目录',
|
|
155
|
+
relevance: '按相关度', contentBuilding: '索引构建中… {files} 文件 / {messages} 消息',
|
|
156
|
+
contentShort: '输入至少 2 个字符后搜索正文与会话',
|
|
157
|
+
contentDisabled: '内容索引已关闭(contentIndex: false)',
|
|
158
|
+
contentCapped: '前 {n} 条', showing: '前 {n} 条', deduped: '{n} 条文件命中已并入内容', loading: '搜索中…',
|
|
159
|
+
emptyQuery: '输入关键词:文件名、路径、正文或会话内容',
|
|
160
|
+
emptyResults: '没有匹配结果 · 可试试减少范围或换个关键词',
|
|
161
|
+
emptyScopes: '至少选择一个搜索范围(内容 / 文件 / 目录)',
|
|
162
|
+
errorPrefix: '搜索失败:', retry: '重试', indexing: '索引构建中,结果可能不完整',
|
|
163
|
+
statusLoading: '索引状态读取中…', status: '索引 {entries} 条 · {mode} · {roots} 个根目录',
|
|
164
|
+
live: '实时监听', snapshot: '快照', usn: 'USN',
|
|
165
|
+
copiedPath: '已复制路径', revealed: '已在文件管理器中定位', opened: '已打开', inserted: '已插入 @file',
|
|
166
|
+
insertFailed: '插入失败,已复制 @file', jumped: '已打开会话', jumpFailed: '已打开会话,但未定位到该消息',
|
|
167
|
+
footer: '↑↓ 选择 · Enter 打开/跳转 · Ctrl+Shift+C 复制路径 · Ctrl+O 打开 · Alt+1–3 切换范围 · Esc 关闭',
|
|
168
|
+
results: '内容 {content} · 文件 {files} · 目录 {dirs} · {ms} ms',
|
|
135
169
|
launcher: '本地文件搜索 (Ctrl+Shift+F)',
|
|
136
170
|
justNow: '刚刚', minutesAgo: '{n} 分钟前', hoursAgo: '{n} 小时前',
|
|
171
|
+
phraseNotFound: '会话里没有找到匹配内容(可能还没加载)',
|
|
137
172
|
},
|
|
138
173
|
es: {
|
|
139
|
-
|
|
140
|
-
placeholder: 'Busca nombres o
|
|
174
|
+
scopeContent: 'Contenido', scopeFiles: 'Archivos', scopeDirs: 'Carpetas',
|
|
175
|
+
placeholder: 'Busca nombres, rutas, contenido o sesiones… p. ej. ext:pdf size:>10mb dm:thisweek path:src',
|
|
141
176
|
searchLabel: 'Buscar archivos', dialog: 'Búsqueda local de archivos', closeTitle: 'Cerrar (Esc)', closeLabel: 'Cerrar',
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
177
|
+
sortLabel: 'Orden', sortMtime: 'Modificado', sortName: 'Nombre', sortSize: 'Tamaño', sortPath: 'Ruta',
|
|
178
|
+
asc: 'Ascendente', descOrder: 'Descendente',
|
|
179
|
+
groupContent: 'Contenido', groupFiles: 'Archivos', groupDirs: 'Carpetas',
|
|
180
|
+
relevance: 'por relevancia', contentBuilding: 'indexando… {files} archivos / {messages} mensajes',
|
|
181
|
+
contentShort: 'Escribe al menos 2 caracteres para buscar contenido y sesiones',
|
|
182
|
+
contentDisabled: 'Índice de contenido desactivado (contentIndex: false)',
|
|
183
|
+
contentCapped: 'primeros {n}', showing: 'primeros {n}', deduped: '{n} aciertos de nombre fusionados', loading: 'Buscando…',
|
|
184
|
+
emptyQuery: 'Escribe para buscar nombres, rutas, contenido y sesiones',
|
|
185
|
+
emptyResults: 'Sin coincidencias · prueba menos ámbitos u otra palabra',
|
|
186
|
+
emptyScopes: 'Elige al menos un ámbito (Contenido, Archivos, Carpetas)',
|
|
187
|
+
errorPrefix: 'Error de búsqueda: ', retry: 'Reintentar', indexing: 'indexando, resultados incompletos',
|
|
188
|
+
statusLoading: 'Leyendo el estado del índice…', status: 'Índice: {entries} entradas · {mode} · {roots} raíces',
|
|
189
|
+
live: 'en vivo', snapshot: 'instantánea', usn: 'USN',
|
|
190
|
+
copiedPath: 'Ruta copiada', revealed: 'Mostrado en el gestor de archivos', opened: 'Abierto', inserted: '@file insertado',
|
|
191
|
+
insertFailed: 'No se pudo insertar; @file copiado', jumped: 'Sesión abierta', jumpFailed: 'Sesión abierta, pero no se localizó el mensaje',
|
|
192
|
+
footer: '↑↓ elegir · Enter abrir/ir · Ctrl+Shift+C copiar ruta · Ctrl+O abrir · Alt+1–3 ámbitos · Esc cerrar',
|
|
193
|
+
results: 'contenido {content} · archivos {files} · carpetas {dirs} · {ms} ms',
|
|
149
194
|
launcher: 'Búsqueda local (Ctrl+Shift+F)',
|
|
150
195
|
justNow: 'ahora', minutesAgo: 'hace {n} min', hoursAgo: 'hace {n} h',
|
|
196
|
+
phraseNotFound: 'No se encontró en la conversación (quizá no está cargada)',
|
|
151
197
|
},
|
|
152
198
|
pt: {
|
|
153
|
-
|
|
154
|
-
placeholder: 'Busque nomes ou
|
|
199
|
+
scopeContent: 'Conteúdo', scopeFiles: 'Arquivos', scopeDirs: 'Pastas',
|
|
200
|
+
placeholder: 'Busque nomes, caminhos, conteúdo ou sessões… ex. ext:pdf size:>10mb dm:thisweek path:src',
|
|
155
201
|
searchLabel: 'Buscar arquivos', dialog: 'Busca local de arquivos', closeTitle: 'Fechar (Esc)', closeLabel: 'Fechar',
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
202
|
+
sortLabel: 'Ordem', sortMtime: 'Modificado', sortName: 'Nome', sortSize: 'Tamanho', sortPath: 'Caminho',
|
|
203
|
+
asc: 'Ascendente', descOrder: 'Descendente',
|
|
204
|
+
groupContent: 'Conteúdo', groupFiles: 'Arquivos', groupDirs: 'Pastas',
|
|
205
|
+
relevance: 'por relevância', contentBuilding: 'indexando… {files} arquivos / {messages} mensagens',
|
|
206
|
+
contentShort: 'Digite pelo menos 2 caracteres para buscar conteúdo e sessões',
|
|
207
|
+
contentDisabled: 'Índice de conteúdo desativado (contentIndex: false)',
|
|
208
|
+
contentCapped: 'primeiros {n}', showing: 'primeiros {n}', deduped: '{n} acertos de nome mesclados', loading: 'Buscando…',
|
|
209
|
+
emptyQuery: 'Digite para buscar nomes, caminhos, conteúdo e sessões',
|
|
210
|
+
emptyResults: 'Sem resultados · tente menos escopos ou outra palavra',
|
|
211
|
+
emptyScopes: 'Escolha pelo menos um escopo (Conteúdo, Arquivos, Pastas)',
|
|
212
|
+
errorPrefix: 'Falha na busca: ', retry: 'Tentar novamente', indexing: 'indexando, resultados incompletos',
|
|
213
|
+
statusLoading: 'Lendo o estado do índice…', status: 'Índice: {entries} entradas · {mode} · {roots} raízes',
|
|
214
|
+
live: 'ao vivo', snapshot: 'instantâneo', usn: 'USN',
|
|
215
|
+
copiedPath: 'Caminho copiado', revealed: 'Mostrado no gerenciador de arquivos', opened: 'Aberto', inserted: '@file inserido',
|
|
216
|
+
insertFailed: 'Falha ao inserir; @file copiado', jumped: 'Sessão aberta', jumpFailed: 'Sessão aberta, mas a mensagem não foi localizada',
|
|
217
|
+
footer: '↑↓ escolher · Enter abrir/ir · Ctrl+Shift+C copiar caminho · Ctrl+O abrir · Alt+1–3 escopos · Esc fechar',
|
|
218
|
+
results: 'conteúdo {content} · arquivos {files} · pastas {dirs} · {ms} ms',
|
|
163
219
|
launcher: 'Busca local (Ctrl+Shift+F)',
|
|
164
220
|
justNow: 'agora', minutesAgo: 'há {n} min', hoursAgo: 'há {n} h',
|
|
221
|
+
phraseNotFound: 'Não encontrado na conversa (talvez ainda não carregada)',
|
|
165
222
|
},
|
|
166
223
|
hi: {
|
|
167
|
-
|
|
168
|
-
placeholder: '
|
|
224
|
+
scopeContent: 'सामग्री', scopeFiles: 'फ़ाइलें', scopeDirs: 'फ़ोल्डर',
|
|
225
|
+
placeholder: 'नाम, पथ, सामग्री या सत्र खोजें… जैसे ext:pdf size:>10mb dm:thisweek path:src',
|
|
169
226
|
searchLabel: 'फ़ाइल खोज', dialog: 'स्थानीय फ़ाइल खोज', closeTitle: 'बंद करें (Esc)', closeLabel: 'बंद करें',
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
227
|
+
sortLabel: 'क्रम', sortMtime: 'संशोधित', sortName: 'नाम', sortSize: 'आकार', sortPath: 'पथ',
|
|
228
|
+
asc: 'आरोही', descOrder: 'अवरोही',
|
|
229
|
+
groupContent: 'सामग्री', groupFiles: 'फ़ाइलें', groupDirs: 'फ़ोल्डर',
|
|
230
|
+
relevance: 'प्रासंगिकता से', contentBuilding: 'इंडेक्स बन रहा है… {files} फ़ाइलें / {messages} संदेश',
|
|
231
|
+
contentShort: 'सामग्री और सत्र खोजने के लिए कम से कम 2 अक्षर लिखें',
|
|
232
|
+
contentDisabled: 'सामग्री इंडेक्स बंद है (contentIndex: false)',
|
|
233
|
+
contentCapped: 'पहले {n}', showing: 'पहले {n}', deduped: '{n} नाम-परिणाम सामग्री में मिलाए', loading: 'खोज रहे हैं…',
|
|
234
|
+
emptyQuery: 'नाम, पथ, सामग्री और सत्र खोजने के लिए लिखें',
|
|
235
|
+
emptyResults: 'कोई मेल नहीं · कम क्षेत्र या दूसरा शब्द आज़माएँ',
|
|
236
|
+
emptyScopes: 'कम से कम एक क्षेत्र चुनें (सामग्री, फ़ाइलें, फ़ोल्डर)',
|
|
237
|
+
errorPrefix: 'खोज विफल: ', retry: 'पुन: प्रयास', indexing: 'इंडेक्स बन रहा है, परिणाम अधूरे हो सकते हैं',
|
|
238
|
+
statusLoading: 'इंडेक्स स्थिति पढ़ी जा रही है…', status: 'इंडेक्स {entries} प्रविष्टियाँ · {mode} · {roots} रूट',
|
|
239
|
+
live: 'लाइव', snapshot: 'स्नैपशॉट', usn: 'USN',
|
|
240
|
+
copiedPath: 'पथ कॉपी हुआ', revealed: 'फ़ाइल मैनेजर में दिखाया', opened: 'खोला गया', inserted: '@file डाला गया',
|
|
241
|
+
insertFailed: 'डालना विफल; @file कॉपी हुआ', jumped: 'सत्र खुला', jumpFailed: 'सत्र खुला, पर संदेश नहीं मिला',
|
|
242
|
+
footer: '↑↓ चुनें · Enter खोलें/जाएँ · Ctrl+Shift+C पथ कॉपी · Ctrl+O खोलें · Alt+1–3 क्षेत्र · Esc बंद',
|
|
243
|
+
results: 'सामग्री {content} · फ़ाइलें {files} · फ़ोल्डर {dirs} · {ms} ms',
|
|
177
244
|
launcher: 'स्थानीय फ़ाइल खोज (Ctrl+Shift+F)',
|
|
178
245
|
justNow: 'अभी', minutesAgo: '{n} मिनट पहले', hoursAgo: '{n} घंटे पहले',
|
|
246
|
+
phraseNotFound: 'बातचीत में नहीं मिला (शायद अभी लोड नहीं हुआ)',
|
|
179
247
|
},
|
|
180
248
|
}
|
|
181
249
|
const LANG = (() => {
|
|
@@ -202,9 +270,7 @@ window.__ModuleLoader__.load({
|
|
|
202
270
|
}
|
|
203
271
|
|
|
204
272
|
function copyText(text) {
|
|
205
|
-
if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function')
|
|
206
|
-
return navigator.clipboard.writeText(text)
|
|
207
|
-
}
|
|
273
|
+
if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') return navigator.clipboard.writeText(text)
|
|
208
274
|
return new Promise((resolve, reject) => {
|
|
209
275
|
const area = document.createElement('textarea')
|
|
210
276
|
area.value = text
|
|
@@ -234,7 +300,6 @@ window.__ModuleLoader__.load({
|
|
|
234
300
|
return document.execCommand('insertText', false, text)
|
|
235
301
|
}
|
|
236
302
|
|
|
237
|
-
// ── helpers ─────────────────────────────────────────────────────────────
|
|
238
303
|
function formatSize(bytes) {
|
|
239
304
|
if (!bytes) return ''
|
|
240
305
|
if (bytes < 1024) return bytes + ' B'
|
|
@@ -262,72 +327,186 @@ window.__ModuleLoader__.load({
|
|
|
262
327
|
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
263
328
|
}
|
|
264
329
|
|
|
265
|
-
function
|
|
266
|
-
const words = query.split(/\s+/).filter(word => word !== ''
|
|
267
|
-
|
|
330
|
+
function queryWords(query) {
|
|
331
|
+
const words = String(query || '').split(/\s+/).filter(word => word !== ''
|
|
332
|
+
&& !word.includes(':') && !word.startsWith('!') && !word.includes('*') && !word.includes('?'))
|
|
333
|
+
return words.map(word => word.replace(/^"|"$/g, '')).filter(word => word.length > 0)
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function highlight(text, query) {
|
|
337
|
+
const words = queryWords(query)
|
|
338
|
+
if (words.length === 0) return text
|
|
268
339
|
try {
|
|
269
340
|
const pattern = new RegExp('(' + words.map(escapeRegExp).join('|') + ')', 'gi')
|
|
270
|
-
const parts =
|
|
341
|
+
const parts = String(text).split(pattern)
|
|
271
342
|
return parts.map((part, index) => (index % 2 === 1 ? h('mark', { className: 'dsh-locator-hit', key: index }, part) : part))
|
|
272
343
|
} catch {
|
|
273
|
-
return
|
|
344
|
+
return text
|
|
274
345
|
}
|
|
275
346
|
}
|
|
276
347
|
|
|
277
|
-
// ── panel
|
|
278
|
-
|
|
348
|
+
// ── panel preferences ───────────────────────────────────────────────────
|
|
349
|
+
const PREFS_KEY = 'dsh-locator.panel.v1'
|
|
350
|
+
const SORTS = ['mtime', 'name', 'size', 'path']
|
|
351
|
+
function readPrefs() {
|
|
352
|
+
const fallback = { scopes: { content: true, files: true, dirs: true }, sort: 'mtime', desc: true }
|
|
353
|
+
try {
|
|
354
|
+
const raw = window.localStorage.getItem(PREFS_KEY)
|
|
355
|
+
if (raw === null) return fallback
|
|
356
|
+
const value = JSON.parse(raw)
|
|
357
|
+
if (typeof value !== 'object' || value === null) return fallback
|
|
358
|
+
const scopes = value.scopes
|
|
359
|
+
return {
|
|
360
|
+
scopes: {
|
|
361
|
+
content: typeof scopes?.content === 'boolean' ? scopes.content : true,
|
|
362
|
+
files: typeof scopes?.files === 'boolean' ? scopes.files : true,
|
|
363
|
+
dirs: typeof scopes?.dirs === 'boolean' ? scopes.dirs : true,
|
|
364
|
+
},
|
|
365
|
+
sort: SORTS.includes(value.sort) ? value.sort : 'mtime',
|
|
366
|
+
desc: typeof value.desc === 'boolean' ? value.desc : true,
|
|
367
|
+
}
|
|
368
|
+
} catch {
|
|
369
|
+
return fallback
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
function writePrefs(prefs) {
|
|
373
|
+
try { window.localStorage.setItem(PREFS_KEY, JSON.stringify(prefs)) } catch { /* storage unavailable */ }
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// ── find highlight: CSS Custom Highlight API ────────────────────────────
|
|
377
|
+
const HL_BASE = 'dsh-locator-find'
|
|
378
|
+
const HL_ACTIVE = 'dsh-locator-find-active'
|
|
379
|
+
const GESTURES = ['wheel', 'touchstart', 'pointerdown', 'keydown']
|
|
380
|
+
const EXCLUDED = '[hidden],[data-chat-history],[data-text-interaction-surface],[data-composer-seat],script,style'
|
|
381
|
+
const BLOCK_SELECTOR = 'p,li,pre,td,th,h1,h2,h3,h4,h5,h6,blockquote,dd,dt'
|
|
382
|
+
const legacyTargets = new Set()
|
|
383
|
+
|
|
384
|
+
function highlightsAvailable() {
|
|
385
|
+
return typeof Highlight === 'function' && typeof CSS !== 'undefined' && CSS !== null
|
|
386
|
+
&& typeof CSS.highlights === 'object' && CSS.highlights !== null
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function clearLegacyTargets() {
|
|
390
|
+
for (const element of legacyTargets) element.classList.remove('dsh-locator-target')
|
|
391
|
+
legacyTargets.clear()
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function clearFindHighlight() {
|
|
395
|
+
clearLegacyTargets()
|
|
396
|
+
if (!highlightsAvailable()) return
|
|
397
|
+
CSS.highlights.delete(HL_BASE)
|
|
398
|
+
CSS.highlights.delete(HL_ACTIVE)
|
|
399
|
+
}
|
|
279
400
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
401
|
+
function setFindHighlight(ranges) {
|
|
402
|
+
if (!highlightsAvailable()) {
|
|
403
|
+
for (const range of ranges.slice(0, 1)) {
|
|
404
|
+
const node = range.startContainer
|
|
405
|
+
const element = node instanceof Element ? node : node.parentElement
|
|
406
|
+
const row = element === null ? null : element.closest('[data-chat-node-key]')
|
|
407
|
+
if (row !== null) { row.classList.add('dsh-locator-target'); legacyTargets.add(row) }
|
|
408
|
+
}
|
|
409
|
+
return
|
|
410
|
+
}
|
|
411
|
+
if (ranges.length === 0) return
|
|
412
|
+
CSS.highlights.set(HL_BASE, new Highlight(...ranges))
|
|
413
|
+
CSS.highlights.set(HL_ACTIVE, new Highlight(ranges[0]))
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function conversationScope(sessionId) {
|
|
417
|
+
const id = sessionId === undefined || sessionId === null ? '' : String(sessionId)
|
|
418
|
+
let embedded = null
|
|
419
|
+
for (const element of document.querySelectorAll('[data-conversation-content][data-conversation-session]')) {
|
|
420
|
+
if (id !== '' && element.getAttribute('data-conversation-session') !== id) continue
|
|
421
|
+
if (element.closest('[data-sidebar-chat]') === null) return element
|
|
422
|
+
embedded = element
|
|
423
|
+
}
|
|
424
|
+
return embedded
|
|
425
|
+
}
|
|
286
426
|
|
|
287
|
-
function
|
|
288
|
-
return
|
|
289
|
-
|| document.querySelector('[data-slot="conversation.chat.flow"]')
|
|
290
|
-
|| null
|
|
427
|
+
function chatFlow(scope) {
|
|
428
|
+
return scope === null ? null : scope.querySelector('[data-chat-flow]')
|
|
291
429
|
}
|
|
292
430
|
|
|
293
|
-
function
|
|
294
|
-
if (!messageId) return null
|
|
431
|
+
function findMessageRow(flow, messageId) {
|
|
432
|
+
if (flow === null || !messageId) return null
|
|
295
433
|
const id = String(messageId)
|
|
296
|
-
|
|
297
|
-
const rows = document.querySelectorAll('[data-chat-node-key]')
|
|
298
|
-
for (const row of rows) {
|
|
434
|
+
for (const row of flow.querySelectorAll('[data-chat-node-key]')) {
|
|
299
435
|
const key = row.getAttribute('data-chat-node-key') || ''
|
|
300
436
|
if (key === id || key.endsWith(id)) return row
|
|
301
437
|
}
|
|
302
438
|
return null
|
|
303
439
|
}
|
|
304
440
|
|
|
305
|
-
function
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
441
|
+
function needlesFromQuery(query) {
|
|
442
|
+
const raw = String(query || '').trim()
|
|
443
|
+
const words = queryWords(raw).filter(word => word.length >= 1)
|
|
444
|
+
if (words.length === 0 && raw.length >= 2) return [raw]
|
|
445
|
+
return [...new Set(words)].sort((a, b) => b.length - a.length).slice(0, 8)
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function inlineGroups(root) {
|
|
449
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT)
|
|
450
|
+
const groups = new Map()
|
|
451
|
+
let node = walker.nextNode()
|
|
452
|
+
while (node !== null) {
|
|
453
|
+
const parent = node.parentElement
|
|
454
|
+
if (parent !== null && parent.closest(EXCLUDED) === null) {
|
|
455
|
+
const value = node.nodeValue || ''
|
|
456
|
+
if (value !== '') {
|
|
457
|
+
const block = parent.closest(BLOCK_SELECTOR) || root
|
|
458
|
+
let group = groups.get(block)
|
|
459
|
+
if (group === undefined) { group = { nodes: [], text: '' }; groups.set(block, group) }
|
|
460
|
+
group.nodes.push(node)
|
|
461
|
+
group.text += value
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
node = walker.nextNode()
|
|
465
|
+
}
|
|
466
|
+
return [...groups.values()]
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function offsetInGroup(group, index) {
|
|
470
|
+
let consumed = 0
|
|
471
|
+
for (const node of group.nodes) {
|
|
472
|
+
const length = (node.nodeValue || '').length
|
|
473
|
+
if (index <= consumed + length) return [node, index - consumed]
|
|
474
|
+
consumed += length
|
|
475
|
+
}
|
|
476
|
+
const last = group.nodes[group.nodes.length - 1]
|
|
477
|
+
return [last, (last.nodeValue || '').length]
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function rangesForNeedles(root, needles) {
|
|
481
|
+
const ranges = []
|
|
482
|
+
if (root === null || needles.length === 0) return ranges
|
|
483
|
+
for (const group of inlineGroups(root)) {
|
|
484
|
+
const lower = group.text.toLowerCase()
|
|
485
|
+
for (const needle of needles) {
|
|
486
|
+
const target = needle.toLowerCase()
|
|
487
|
+
let from = 0
|
|
488
|
+
while (ranges.length < 200) {
|
|
489
|
+
const index = lower.indexOf(target, from)
|
|
490
|
+
if (index < 0) break
|
|
491
|
+
const start = offsetInGroup(group, index)
|
|
492
|
+
const end = offsetInGroup(group, index + target.length)
|
|
493
|
+
const range = document.createRange()
|
|
494
|
+
range.setStart(start[0], start[1])
|
|
495
|
+
range.setEnd(end[0], end[1])
|
|
496
|
+
ranges.push(range)
|
|
497
|
+
from = index + target.length
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
return ranges
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function normalizePhrase(text) {
|
|
505
|
+
return String(text || '').replace(/[\u0001\u0002\u2026]/g, ' ').replace(/\s+/g, ' ').trim()
|
|
321
506
|
}
|
|
322
507
|
|
|
323
|
-
// Progressively shorter fallbacks: a FTS snippet may be cut mid-word or
|
|
324
|
-
// separated by ellipses, so try the whole phrase before the leading words.
|
|
325
508
|
function phraseCandidates(snippet) {
|
|
326
|
-
const clean =
|
|
327
|
-
.replace(/[\u0001\u0002]/g, ' ')
|
|
328
|
-
.replace(/…/g, ' ')
|
|
329
|
-
.replace(/\s+/g, ' ')
|
|
330
|
-
.trim()
|
|
509
|
+
const clean = normalizePhrase(snippet)
|
|
331
510
|
const out = []
|
|
332
511
|
const push = value => {
|
|
333
512
|
const text = String(value || '').trim()
|
|
@@ -346,65 +525,261 @@ window.__ModuleLoader__.load({
|
|
|
346
525
|
return out
|
|
347
526
|
}
|
|
348
527
|
|
|
349
|
-
function
|
|
350
|
-
if (
|
|
351
|
-
const
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
528
|
+
function phraseRange(root, snippet) {
|
|
529
|
+
if (root === null) return null
|
|
530
|
+
for (const phrase of phraseCandidates(snippet)) {
|
|
531
|
+
const needle = phrase.toLowerCase()
|
|
532
|
+
for (const group of inlineGroups(root)) {
|
|
533
|
+
const index = group.text.toLowerCase().indexOf(needle)
|
|
534
|
+
if (index < 0) continue
|
|
535
|
+
const start = offsetInGroup(group, index)
|
|
536
|
+
const end = offsetInGroup(group, index + needle.length)
|
|
537
|
+
const range = document.createRange()
|
|
538
|
+
range.setStart(start[0], start[1])
|
|
539
|
+
range.setEnd(end[0], end[1])
|
|
540
|
+
return range
|
|
541
|
+
}
|
|
361
542
|
}
|
|
362
543
|
return null
|
|
363
544
|
}
|
|
364
545
|
|
|
365
|
-
function
|
|
366
|
-
const
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
546
|
+
function rangeRect(range) {
|
|
547
|
+
for (const rect of range.getClientRects()) if (rect.width > 0 && rect.height > 0) return rect
|
|
548
|
+
const rect = range.getBoundingClientRect()
|
|
549
|
+
return rect.width > 0 && rect.height > 0 ? rect : null
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function isScrollable(element) {
|
|
553
|
+
if (!(element instanceof HTMLElement)) return false
|
|
554
|
+
return /(auto|scroll|overlay)/u.test(getComputedStyle(element).overflowY)
|
|
555
|
+
&& element.scrollHeight - element.clientHeight > 2
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function scrollChain(element) {
|
|
559
|
+
const chain = []
|
|
560
|
+
for (let node = element; node !== null; node = node.parentElement) if (isScrollable(node)) chain.push(node)
|
|
561
|
+
return chain
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function visibleBand(scroller) {
|
|
565
|
+
const rect = scroller.getBoundingClientRect()
|
|
566
|
+
const seat = scroller.querySelector('[data-composer-seat]')
|
|
567
|
+
const seatRect = seat === null ? null : seat.getBoundingClientRect()
|
|
568
|
+
const bottom = seatRect !== null && seatRect.height > 0 ? Math.min(rect.bottom, seatRect.top) : rect.bottom
|
|
569
|
+
return { top: rect.top, bottom, height: Math.max(0, bottom - rect.top) }
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function centerRange(range) {
|
|
573
|
+
const start = range.startContainer
|
|
574
|
+
const element = start instanceof Element ? start : start.parentElement
|
|
575
|
+
if (element === null) return false
|
|
576
|
+
let moved = false
|
|
577
|
+
for (const scroller of scrollChain(element)) {
|
|
578
|
+
const rect = rangeRect(range)
|
|
579
|
+
if (rect === null) return moved
|
|
580
|
+
const band = visibleBand(scroller)
|
|
581
|
+
if (band.height <= 0) continue
|
|
582
|
+
const next = Math.max(0, Math.min(
|
|
583
|
+
Math.max(0, scroller.scrollHeight - scroller.clientHeight),
|
|
584
|
+
scroller.scrollTop + (rect.top + rect.height / 2) - (band.top + band.height / 2),
|
|
585
|
+
))
|
|
586
|
+
if (Math.abs(next - scroller.scrollTop) > 1) { scroller.scrollTop = next; moved = true }
|
|
587
|
+
}
|
|
588
|
+
return moved
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function expandHidden(node) {
|
|
592
|
+
let element = node instanceof Element ? node : node.parentElement
|
|
593
|
+
for (; element !== null; element = element.parentElement) {
|
|
594
|
+
if (element.getAttribute('hidden') === 'until-found') {
|
|
595
|
+
element.dispatchEvent(new Event('beforematch'))
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function waitFor(scope, test, timeoutMs, intervalMs) {
|
|
601
|
+
return new Promise(resolve => {
|
|
602
|
+
let settled = false
|
|
603
|
+
const finish = value => {
|
|
604
|
+
if (settled) return
|
|
605
|
+
settled = true
|
|
606
|
+
clearInterval(timer)
|
|
607
|
+
clearTimeout(timeout)
|
|
608
|
+
resolve(value)
|
|
609
|
+
}
|
|
610
|
+
const check = () => {
|
|
611
|
+
try {
|
|
612
|
+
const value = test()
|
|
613
|
+
if (value !== null && value !== undefined) finish(value)
|
|
614
|
+
} catch { /* DOM not ready yet */ }
|
|
615
|
+
}
|
|
616
|
+
const timer = setInterval(check, intervalMs || 120)
|
|
617
|
+
const timeout = setTimeout(() => finish(null), timeoutMs)
|
|
618
|
+
check()
|
|
619
|
+
})
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function waitForScope(sessionId, timeoutMs) {
|
|
623
|
+
return waitFor(document, () => conversationScope(sessionId), timeoutMs, 120)
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function revealOlder(scope) {
|
|
627
|
+
const button = scope === null ? null : scope.querySelector('[data-chat-history] button')
|
|
628
|
+
if (button === null || button.disabled === true) return false
|
|
629
|
+
button.click()
|
|
630
|
+
return true
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
let activeJump = null
|
|
634
|
+
|
|
635
|
+
function cancelJump() {
|
|
636
|
+
clearFindHighlight()
|
|
637
|
+
if (activeJump !== null) {
|
|
638
|
+
const cancel = activeJump
|
|
639
|
+
activeJump = null
|
|
640
|
+
cancel()
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
async function jumpToContentHit(hit, query, notify) {
|
|
645
|
+
cancelJump()
|
|
646
|
+
if (!hit || hit.kind !== 'session' || !hit.sessionId) return false
|
|
647
|
+
if (!workspaceService || typeof workspaceService.openSession !== 'function') return false
|
|
648
|
+
try { workspaceService.openSession(hit.sessionId) } catch { return false }
|
|
649
|
+
const scope = await waitForScope(hit.sessionId, 10_000)
|
|
650
|
+
if (scope === null) { notify?.(t('jumpFailed')); return false }
|
|
651
|
+
const flow = chatFlow(scope)
|
|
652
|
+
if (flow === null) { notify?.(t('jumpFailed')); return false }
|
|
653
|
+
const needles = needlesFromQuery(query)
|
|
654
|
+
const finder = () => {
|
|
655
|
+
const row = findMessageRow(flow, hit.messageId)
|
|
656
|
+
if (row !== null) {
|
|
657
|
+
expandHidden(row)
|
|
658
|
+
const ranges = rangesForNeedles(row, needles)
|
|
659
|
+
if (ranges.length > 0) return { row, ranges }
|
|
660
|
+
const phrase = phraseRange(row, hit.snippet)
|
|
661
|
+
if (phrase !== null) return { row, ranges: [phrase] }
|
|
662
|
+
return null
|
|
663
|
+
}
|
|
664
|
+
const phrase = phraseRange(flow, hit.snippet)
|
|
665
|
+
if (phrase === null) return null
|
|
666
|
+
const phraseRow = phrase.startContainer instanceof Element
|
|
667
|
+
? phrase.startContainer.closest('[data-chat-node-key]')
|
|
668
|
+
: phrase.startContainer.parentElement?.closest('[data-chat-node-key]') ?? null
|
|
669
|
+
if (phraseRow !== null) expandHidden(phraseRow)
|
|
670
|
+
const ranges = phraseRow === null ? [phrase] : rangesForNeedles(phraseRow, needles)
|
|
671
|
+
return { row: phraseRow, ranges: ranges.length > 0 ? ranges : [phrase] }
|
|
672
|
+
}
|
|
673
|
+
let target = await waitFor(flow, finder, 6_000, 120)
|
|
674
|
+
for (let attempt = 0; target === null && attempt < 10; attempt++) {
|
|
675
|
+
if (!revealOlder(scope)) break
|
|
676
|
+
target = await waitFor(flow, finder, 1_500, 150)
|
|
677
|
+
}
|
|
678
|
+
if (target === null) { notify?.(t('phraseNotFound')); return false }
|
|
679
|
+
setFindHighlight(target.ranges)
|
|
680
|
+
centerRange(target.ranges[0])
|
|
681
|
+
const start = target.ranges[0].startContainer
|
|
682
|
+
const chain = scrollChain(start instanceof Element ? start : start.parentElement)
|
|
683
|
+
const scroller = chain.length > 0 ? chain[chain.length - 1] : null
|
|
684
|
+
const cleanup = []
|
|
685
|
+
const release = () => {
|
|
686
|
+
while (cleanup.length > 0) {
|
|
687
|
+
const fn = cleanup.pop()
|
|
688
|
+
try { fn() } catch { /* ignore */ }
|
|
689
|
+
}
|
|
372
690
|
}
|
|
373
|
-
|
|
691
|
+
activeJump = release
|
|
692
|
+
if (scroller !== null) {
|
|
693
|
+
let writing = false
|
|
694
|
+
const expected = scroller.scrollTop
|
|
695
|
+
const recenter = () => {
|
|
696
|
+
if (activeJump !== release) return
|
|
697
|
+
const rect = rangeRect(target.ranges[0])
|
|
698
|
+
if (rect === null) { release(); clearFindHighlight(); activeJump = null; return }
|
|
699
|
+
writing = true
|
|
700
|
+
centerRange(target.ranges[0])
|
|
701
|
+
requestAnimationFrame(() => { writing = false })
|
|
702
|
+
}
|
|
703
|
+
for (const delay of [80, 240, 800, 1600]) {
|
|
704
|
+
const id = setTimeout(recenter, delay)
|
|
705
|
+
cleanup.push(() => clearTimeout(id))
|
|
706
|
+
}
|
|
707
|
+
const onUser = () => { if (!writing) release() }
|
|
708
|
+
for (const type of GESTURES) {
|
|
709
|
+
scroller.addEventListener(type, onUser, true)
|
|
710
|
+
cleanup.push(() => scroller.removeEventListener(type, onUser, true))
|
|
711
|
+
}
|
|
712
|
+
const onScroll = event => {
|
|
713
|
+
if (writing) return
|
|
714
|
+
if (event.target === scroller && Math.abs(scroller.scrollTop - expected) <= 2) return
|
|
715
|
+
release()
|
|
716
|
+
}
|
|
717
|
+
scroller.addEventListener('scroll', onScroll, true)
|
|
718
|
+
cleanup.push(() => scroller.removeEventListener('scroll', onScroll, true))
|
|
719
|
+
}
|
|
720
|
+
const onKey = event => {
|
|
721
|
+
if (event.key !== 'Escape') return
|
|
722
|
+
cancelJump()
|
|
723
|
+
}
|
|
724
|
+
document.addEventListener('keydown', onKey, true)
|
|
725
|
+
cleanup.push(() => document.removeEventListener('keydown', onKey, true))
|
|
726
|
+
const sessionWatch = setInterval(() => {
|
|
727
|
+
const still = target.ranges[0].startContainer.isConnected && conversationScope(hit.sessionId) !== null
|
|
728
|
+
if (!still) cancelJump()
|
|
729
|
+
}, 1_000)
|
|
730
|
+
cleanup.push(() => clearInterval(sessionWatch))
|
|
731
|
+
cleanup.push(() => clearFindHighlight())
|
|
732
|
+
notify?.(t('jumped'))
|
|
733
|
+
return true
|
|
374
734
|
}
|
|
375
735
|
|
|
376
|
-
|
|
736
|
+
// ── panel ───────────────────────────────────────────────────────────────
|
|
737
|
+
let workspaceService = null
|
|
738
|
+
const PAGE_NAME = 50
|
|
739
|
+
const PAGE_CONTENT = 30
|
|
377
740
|
|
|
378
741
|
function Panel() {
|
|
379
742
|
const open = useOpen()
|
|
743
|
+
const prefs = useMemo(readPrefs, [])
|
|
380
744
|
const [query, setQuery] = useState('')
|
|
381
|
-
const [sort, setSort] = useState(
|
|
382
|
-
const [desc, setDesc] = useState(
|
|
383
|
-
const [
|
|
384
|
-
const [
|
|
385
|
-
const [
|
|
386
|
-
const [
|
|
387
|
-
const [total, setTotal] = useState(0)
|
|
745
|
+
const [sort, setSort] = useState(prefs.sort)
|
|
746
|
+
const [desc, setDesc] = useState(prefs.desc)
|
|
747
|
+
const [scopes, setScopes] = useState(prefs.scopes)
|
|
748
|
+
const [groups, setGroups] = useState(null)
|
|
749
|
+
const [loading, setLoading] = useState(false)
|
|
750
|
+
const [error, setError] = useState('')
|
|
388
751
|
const [took, setTook] = useState(0)
|
|
389
|
-
const [truncated, setTruncated] = useState(false)
|
|
390
|
-
const [contentBuilding, setContentBuilding] = useState(false)
|
|
391
|
-
const [refreshTick, setRefreshTick] = useState(0)
|
|
392
752
|
const [status, setStatus] = useState(null)
|
|
393
|
-
const [
|
|
394
|
-
const [loading, setLoading] = useState(false)
|
|
753
|
+
const [availableContent, setAvailableContent] = useState(true)
|
|
395
754
|
const [selected, setSelected] = useState(0)
|
|
396
755
|
const [toast, setToast] = useState('')
|
|
756
|
+
const [refreshTick, setRefreshTick] = useState(0)
|
|
397
757
|
const inputRef = useRef(null)
|
|
758
|
+
const panelRef = useRef(null)
|
|
398
759
|
const abortRef = useRef(null)
|
|
399
760
|
const listRef = useRef(null)
|
|
400
761
|
|
|
401
762
|
const close = useCallback(() => store.set(false), [])
|
|
763
|
+
const anyScope = scopes.content || scopes.files || scopes.dirs
|
|
764
|
+
|
|
765
|
+
useEffect(() => {
|
|
766
|
+
writePrefs({ scopes, sort, desc })
|
|
767
|
+
}, [scopes, sort, desc])
|
|
402
768
|
|
|
403
769
|
useEffect(() => {
|
|
404
770
|
if (!open) return undefined
|
|
771
|
+
const previous = document.activeElement
|
|
405
772
|
setError('')
|
|
406
|
-
const timer = setTimeout(() =>
|
|
407
|
-
|
|
773
|
+
const timer = setTimeout(() => {
|
|
774
|
+
if (inputRef.current) {
|
|
775
|
+
inputRef.current.focus()
|
|
776
|
+
if (inputRef.current.value !== '') inputRef.current.select()
|
|
777
|
+
}
|
|
778
|
+
}, 30)
|
|
779
|
+
return () => {
|
|
780
|
+
clearTimeout(timer)
|
|
781
|
+
if (previous instanceof HTMLElement && previous.isConnected) previous.focus()
|
|
782
|
+
}
|
|
408
783
|
}, [open])
|
|
409
784
|
|
|
410
785
|
useEffect(() => {
|
|
@@ -412,10 +787,13 @@ window.__ModuleLoader__.load({
|
|
|
412
787
|
let cancelled = false
|
|
413
788
|
let timer = 0
|
|
414
789
|
const load = () => {
|
|
415
|
-
|
|
416
|
-
post('/dsh-locator/status', {}, controller.signal).then(next => {
|
|
790
|
+
post('/dsh-locator/status', {}).then(next => {
|
|
417
791
|
if (cancelled) return
|
|
418
792
|
setStatus(next)
|
|
793
|
+
if (next && next.content && next.content.enabled === false) {
|
|
794
|
+
setAvailableContent(false)
|
|
795
|
+
setScopes(value => (value.content ? { ...value, content: false } : value))
|
|
796
|
+
}
|
|
419
797
|
if (next && next.scanning) timer = setTimeout(load, 1500)
|
|
420
798
|
}).catch(() => {})
|
|
421
799
|
}
|
|
@@ -423,63 +801,71 @@ window.__ModuleLoader__.load({
|
|
|
423
801
|
return () => { cancelled = true; clearTimeout(timer) }
|
|
424
802
|
}, [open])
|
|
425
803
|
|
|
804
|
+
// One request for every enabled scope; the server owns the grouping and
|
|
805
|
+
// de-duplication. Empty and one-character queries never start a content
|
|
806
|
+
// build (see src/combined.ts), so opening the panel stays cheap.
|
|
426
807
|
useEffect(() => {
|
|
427
808
|
if (!open) return undefined
|
|
809
|
+
if (!anyScope) {
|
|
810
|
+
setGroups(null)
|
|
811
|
+
setLoading(false)
|
|
812
|
+
setError('')
|
|
813
|
+
return undefined
|
|
814
|
+
}
|
|
428
815
|
const timer = setTimeout(() => {
|
|
429
816
|
if (abortRef.current) abortRef.current.abort()
|
|
430
817
|
const controller = new AbortController()
|
|
431
818
|
abortRef.current = controller
|
|
432
819
|
setLoading(true)
|
|
433
820
|
setError('')
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
:
|
|
437
|
-
|
|
821
|
+
post('/dsh-locator/combined', {
|
|
822
|
+
query,
|
|
823
|
+
content: scopes.content,
|
|
824
|
+
files: scopes.files,
|
|
825
|
+
dirs: scopes.dirs,
|
|
826
|
+
limit: PAGE_NAME,
|
|
827
|
+
contentLimit: PAGE_CONTENT,
|
|
828
|
+
sort,
|
|
829
|
+
desc,
|
|
830
|
+
}, controller.signal).then(data => {
|
|
438
831
|
if (controller.signal.aborted) return
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
mtime: 0,
|
|
446
|
-
content: true,
|
|
447
|
-
}))
|
|
448
|
-
: (Array.isArray(data.hits) ? data.hits : []).map(hit => ({ ...hit, content: false }))
|
|
449
|
-
setHits(nextHits)
|
|
450
|
-
setTotal(typeof data.total === 'number' ? data.total : nextHits.length)
|
|
832
|
+
setGroups({
|
|
833
|
+
content: data.content,
|
|
834
|
+
files: data.files,
|
|
835
|
+
dirs: data.dirs,
|
|
836
|
+
})
|
|
837
|
+
if (typeof data.availableContent === 'boolean') setAvailableContent(data.availableContent)
|
|
451
838
|
setTook(typeof data.tookMs === 'number' ? data.tookMs : 0)
|
|
452
|
-
setTruncated(data.truncated === true)
|
|
453
|
-
setContentBuilding(contentMode && data.state != null && data.state.building === true)
|
|
454
|
-
setSelected(0)
|
|
455
839
|
setLoading(false)
|
|
456
840
|
}).catch(cause => {
|
|
457
841
|
if (controller.signal.aborted) return
|
|
458
|
-
setHits([])
|
|
459
|
-
setTotal(0)
|
|
460
842
|
setError(cause instanceof Error ? cause.message : String(cause))
|
|
461
843
|
setLoading(false)
|
|
462
844
|
})
|
|
463
|
-
},
|
|
845
|
+
}, 140)
|
|
464
846
|
return () => clearTimeout(timer)
|
|
465
|
-
}, [open, query, sort, desc,
|
|
847
|
+
}, [open, query, sort, desc, scopes.content, scopes.files, scopes.dirs, refreshTick, anyScope])
|
|
466
848
|
|
|
467
|
-
|
|
468
|
-
// the first query fills in without the user retyping it.
|
|
849
|
+
const contentBuilding = groups !== null && groups.content !== undefined && groups.content.building === true
|
|
469
850
|
useEffect(() => {
|
|
470
|
-
if (!open || !
|
|
851
|
+
if (!open || !contentBuilding) return undefined
|
|
471
852
|
const timer = setTimeout(() => setRefreshTick(value => value + 1), 2500)
|
|
472
853
|
return () => clearTimeout(timer)
|
|
473
|
-
}, [open,
|
|
854
|
+
}, [open, contentBuilding, refreshTick])
|
|
474
855
|
|
|
475
|
-
const
|
|
476
|
-
if (
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
856
|
+
const rows = useMemo(() => {
|
|
857
|
+
if (groups === null) return []
|
|
858
|
+
const out = []
|
|
859
|
+
for (const group of ['content', 'files', 'dirs']) {
|
|
860
|
+
const hits = groups[group] && Array.isArray(groups[group].hits) ? groups[group].hits : []
|
|
861
|
+
for (const hit of hits) out.push({ group, hit })
|
|
481
862
|
}
|
|
482
|
-
|
|
863
|
+
return out
|
|
864
|
+
}, [groups])
|
|
865
|
+
|
|
866
|
+
useEffect(() => {
|
|
867
|
+
setSelected(value => Math.min(Math.max(0, value), Math.max(0, rows.length - 1)))
|
|
868
|
+
}, [rows.length])
|
|
483
869
|
|
|
484
870
|
const notify = useCallback(message => {
|
|
485
871
|
setToast(message)
|
|
@@ -488,7 +874,7 @@ window.__ModuleLoader__.load({
|
|
|
488
874
|
|
|
489
875
|
const copy = useCallback(hit => {
|
|
490
876
|
if (!hit) return
|
|
491
|
-
copyText(hit.path).then(() => notify(t('
|
|
877
|
+
copyText(hit.path).then(() => notify(t('copiedPath'))).catch(() => {})
|
|
492
878
|
}, [notify])
|
|
493
879
|
|
|
494
880
|
const reveal = useCallback(hit => {
|
|
@@ -496,97 +882,100 @@ window.__ModuleLoader__.load({
|
|
|
496
882
|
post('/dsh-locator/reveal', { path: hit.path }).then(() => notify(t('revealed'))).catch(() => copy(hit))
|
|
497
883
|
}, [copy, notify])
|
|
498
884
|
|
|
499
|
-
const jumpSession = useCallback(hit => {
|
|
500
|
-
if (!hit || hit.kind !== 'session' || !hit.sessionId) return
|
|
501
|
-
close()
|
|
502
|
-
if (!workspaceService || typeof workspaceService.openSession !== 'function') {
|
|
503
|
-
copy(hit)
|
|
504
|
-
return
|
|
505
|
-
}
|
|
506
|
-
try {
|
|
507
|
-
workspaceService.openSession(hit.sessionId)
|
|
508
|
-
notify(t('jumped'))
|
|
509
|
-
} catch {
|
|
510
|
-
copy(hit)
|
|
511
|
-
return
|
|
512
|
-
}
|
|
513
|
-
const snippet = hit.snippet
|
|
514
|
-
const messageId = hit.messageId
|
|
515
|
-
let tries = 0
|
|
516
|
-
let pulledEarlier = false
|
|
517
|
-
const timer = setInterval(() => {
|
|
518
|
-
tries++
|
|
519
|
-
if (jumpHighlight(snippet, messageId)) {
|
|
520
|
-
clearInterval(timer)
|
|
521
|
-
return
|
|
522
|
-
}
|
|
523
|
-
// A long session renders its tail first: nudge the transcript to the
|
|
524
|
-
// top once so paging can load the message the hit points at.
|
|
525
|
-
if (!pulledEarlier && tries === 15) {
|
|
526
|
-
pulledEarlier = true
|
|
527
|
-
const scroller = document.querySelector('[data-conversation-scroll]')
|
|
528
|
-
if (scroller !== null && scroller.scrollTop > 0) scroller.scrollTop = 0
|
|
529
|
-
}
|
|
530
|
-
if (tries > 100) clearInterval(timer)
|
|
531
|
-
}, 150)
|
|
532
|
-
}, [close, copy, notify])
|
|
533
|
-
|
|
534
|
-
const activate = useCallback(hit => {
|
|
535
|
-
if (!hit) return
|
|
536
|
-
if (hit.kind === 'session') jumpSession(hit)
|
|
537
|
-
else reveal(hit)
|
|
538
|
-
}, [jumpSession, reveal])
|
|
539
|
-
|
|
540
885
|
const openFile = useCallback(hit => {
|
|
541
886
|
if (!hit || hit.dir) return
|
|
542
887
|
post('/dsh-locator/open', { path: hit.path }).then(() => notify(t('opened'))).catch(() => copy(hit))
|
|
543
888
|
}, [copy, notify])
|
|
544
889
|
|
|
545
|
-
const
|
|
890
|
+
const activate = useCallback(hit => {
|
|
546
891
|
if (!hit) return
|
|
892
|
+
if (hit.kind === 'session' && hit.sessionId) {
|
|
893
|
+
close()
|
|
894
|
+
void jumpToContentHit(hit, query, notify)
|
|
895
|
+
return
|
|
896
|
+
}
|
|
897
|
+
reveal(hit)
|
|
898
|
+
}, [close, notify, query, reveal])
|
|
899
|
+
|
|
900
|
+
const insert = useCallback(hit => {
|
|
901
|
+
if (!hit || hit.kind === 'session') return
|
|
547
902
|
const mention = '@"' + hit.path.replace(/"/g, '') + '" '
|
|
548
903
|
close()
|
|
549
904
|
setTimeout(() => {
|
|
550
|
-
if (insertIntoComposer(mention))
|
|
551
|
-
|
|
552
|
-
} else {
|
|
553
|
-
copyText(mention).then(() => notify(t('insertFailed'))).catch(() => {})
|
|
554
|
-
}
|
|
905
|
+
if (insertIntoComposer(mention)) notify(t('inserted'))
|
|
906
|
+
else copyText(mention).then(() => notify(t('insertFailed'))).catch(() => {})
|
|
555
907
|
}, 80)
|
|
556
908
|
}, [close, notify])
|
|
557
909
|
|
|
910
|
+
const toggleScope = useCallback(key => {
|
|
911
|
+
if (key === 'content' && !availableContent) return
|
|
912
|
+
setScopes(value => ({ ...value, [key]: !value[key] }))
|
|
913
|
+
}, [availableContent])
|
|
914
|
+
|
|
558
915
|
const onKeyDown = useCallback(event => {
|
|
916
|
+
if (event.target instanceof HTMLSelectElement) return
|
|
559
917
|
if (event.key === 'Escape') {
|
|
560
918
|
event.preventDefault()
|
|
561
919
|
close()
|
|
562
920
|
return
|
|
563
921
|
}
|
|
564
|
-
if (event.key === '
|
|
922
|
+
if (event.key === 'Tab') {
|
|
923
|
+
const panel = panelRef.current
|
|
924
|
+
if (!panel) return
|
|
925
|
+
const focusables = [...panel.querySelectorAll('input,select,button')].filter(element => !element.disabled)
|
|
926
|
+
if (focusables.length === 0) return
|
|
927
|
+
const first = focusables[0]
|
|
928
|
+
const last = focusables[focusables.length - 1]
|
|
929
|
+
if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus() }
|
|
930
|
+
if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last.focus() }
|
|
931
|
+
return
|
|
932
|
+
}
|
|
933
|
+
if (event.altKey && ['1', '2', '3'].includes(event.key)) {
|
|
565
934
|
event.preventDefault()
|
|
566
|
-
|
|
935
|
+
toggleScope(event.key === '1' ? 'content' : event.key === '2' ? 'files' : 'dirs')
|
|
567
936
|
return
|
|
568
937
|
}
|
|
569
|
-
if (event.key === 'ArrowUp') {
|
|
938
|
+
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
|
570
939
|
event.preventDefault()
|
|
571
|
-
setSelected(value => Math.max(0,
|
|
940
|
+
setSelected(value => Math.max(0, Math.min(rows.length - 1, value + (event.key === 'ArrowDown' ? 1 : -1))))
|
|
941
|
+
return
|
|
942
|
+
}
|
|
943
|
+
if (event.key === 'Home' || event.key === 'End') {
|
|
944
|
+
event.preventDefault()
|
|
945
|
+
setSelected(event.key === 'Home' ? 0 : Math.max(0, rows.length - 1))
|
|
946
|
+
return
|
|
947
|
+
}
|
|
948
|
+
if (event.key === 'PageDown' || event.key === 'PageUp') {
|
|
949
|
+
event.preventDefault()
|
|
950
|
+
setSelected(value => Math.max(0, Math.min(rows.length - 1, value + (event.key === 'PageDown' ? 8 : -8))))
|
|
951
|
+
return
|
|
952
|
+
}
|
|
953
|
+
if (event.key === 'Enter' && !event.isComposing && !event.ctrlKey && !event.metaKey && !event.altKey) {
|
|
954
|
+
event.preventDefault()
|
|
955
|
+
activate(rows[selected]?.hit)
|
|
572
956
|
return
|
|
573
957
|
}
|
|
574
958
|
const modifier = event.ctrlKey || event.metaKey
|
|
575
|
-
if (modifier && String(event.key).toLowerCase() === '
|
|
959
|
+
if (modifier && event.shiftKey && String(event.key).toLowerCase() === 'c') {
|
|
576
960
|
event.preventDefault()
|
|
577
|
-
|
|
961
|
+
copy(rows[selected]?.hit)
|
|
578
962
|
return
|
|
579
963
|
}
|
|
580
|
-
if (modifier && event.key === '
|
|
964
|
+
if (modifier && event.shiftKey && String(event.key).toLowerCase() === 'o') {
|
|
965
|
+
event.preventDefault()
|
|
966
|
+
reveal(rows[selected]?.hit)
|
|
967
|
+
return
|
|
968
|
+
}
|
|
969
|
+
if (modifier && String(event.key).toLowerCase() === 'o') {
|
|
581
970
|
event.preventDefault()
|
|
582
|
-
|
|
971
|
+
openFile(rows[selected]?.hit)
|
|
583
972
|
return
|
|
584
973
|
}
|
|
585
|
-
if (event.key === 'Enter' && !event.isComposing) {
|
|
974
|
+
if (modifier && event.key === 'Enter' && !event.isComposing) {
|
|
586
975
|
event.preventDefault()
|
|
587
|
-
|
|
976
|
+
insert(rows[selected]?.hit)
|
|
588
977
|
}
|
|
589
|
-
}, [close, copy,
|
|
978
|
+
}, [activate, close, copy, insert, openFile, reveal, rows, selected, toggleScope])
|
|
590
979
|
|
|
591
980
|
useEffect(() => {
|
|
592
981
|
const list = listRef.current
|
|
@@ -597,18 +986,97 @@ window.__ModuleLoader__.load({
|
|
|
597
986
|
|
|
598
987
|
if (!open) return null
|
|
599
988
|
|
|
600
|
-
const
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
989
|
+
const content = groups !== null ? groups.content : undefined
|
|
990
|
+
const files = groups !== null ? groups.files : undefined
|
|
991
|
+
const dirs = groups !== null ? groups.dirs : undefined
|
|
992
|
+
const contentCount = content === undefined ? 0 : (content.returned ?? 0)
|
|
993
|
+
const filesCount = files === undefined ? 0 : (files.returned ?? files.hits?.length ?? 0)
|
|
994
|
+
const dirsCount = dirs === undefined ? 0 : (dirs.returned ?? dirs.hits?.length ?? 0)
|
|
995
|
+
const totalRows = rows.length
|
|
996
|
+
|
|
997
|
+
const scopeBox = (key, label, count) => h('label', {
|
|
998
|
+
className: 'dsh-locator-check' + (key === 'content' && !availableContent ? ' disabled' : ''),
|
|
999
|
+
title: key === 'content' && !availableContent ? t('contentDisabled') : label,
|
|
1000
|
+
},
|
|
1001
|
+
h('input', {
|
|
1002
|
+
type: 'checkbox',
|
|
1003
|
+
checked: scopes[key],
|
|
1004
|
+
disabled: key === 'content' && !availableContent,
|
|
1005
|
+
onChange: () => toggleScope(key),
|
|
1006
|
+
}),
|
|
1007
|
+
label,
|
|
1008
|
+
count > 0 ? h('span', { className: 'dsh-locator-count' }, String(count)) : null,
|
|
1009
|
+
)
|
|
1010
|
+
|
|
1011
|
+
const groupHead = (key, label, count, note) => h('div', {
|
|
1012
|
+
className: 'dsh-locator-grouphead',
|
|
1013
|
+
id: 'dsh-locator-group-' + key,
|
|
1014
|
+
},
|
|
1015
|
+
h('span', null, label),
|
|
1016
|
+
h('span', { className: 'dsh-locator-count' }, String(count)),
|
|
1017
|
+
h('span', { className: 'grow' }),
|
|
1018
|
+
note === null ? null : h('span', { className: 'dsh-locator-count' }, note),
|
|
1019
|
+
)
|
|
1020
|
+
|
|
1021
|
+
const rowFor = (group, hit, index) => {
|
|
1022
|
+
const isContent = group === 'content'
|
|
1023
|
+
const isSession = isContent && hit.kind === 'session'
|
|
1024
|
+
const badge = isContent
|
|
1025
|
+
? (isSession ? t('groupContent') : (hit.title || '').split('.').pop() || t('groupFiles'))
|
|
1026
|
+
: (hit.dir ? t('groupDirs') : (hit.ext || t('groupFiles')))
|
|
1027
|
+
const name = isSession ? (hit.title || hit.path) : (hit.name || hit.path)
|
|
1028
|
+
const meta = isContent
|
|
1029
|
+
? formatTime(hit.mtime)
|
|
1030
|
+
: [formatSize(hit.size), formatTime(hit.mtime)].filter(Boolean).join(' · ')
|
|
1031
|
+
return h('div', {
|
|
1032
|
+
key: group + ':' + hit.path + ':' + String(hit.messageId || index),
|
|
1033
|
+
id: 'dsh-locator-opt-' + index,
|
|
1034
|
+
role: 'option',
|
|
1035
|
+
'aria-selected': index === selected ? 'true' : 'false',
|
|
1036
|
+
'data-index': index,
|
|
1037
|
+
className: 'dsh-locator-row' + (isContent ? ' dsh-locator-rowcontent' : '') + (index === selected ? ' sel' : ''),
|
|
1038
|
+
title: hit.path,
|
|
1039
|
+
onClick: () => setSelected(index),
|
|
1040
|
+
onDoubleClick: () => activate(hit),
|
|
1041
|
+
},
|
|
1042
|
+
h('div', { className: 'dsh-locator-title' },
|
|
1043
|
+
h('span', { className: 'dsh-locator-badge' + (isSession ? ' chat' : '') }, String(badge).slice(0, 6)),
|
|
1044
|
+
h('span', { className: 'dsh-locator-name' }, isSession ? name : highlight(name, query)),
|
|
1045
|
+
h('span', { className: 'dsh-locator-meta', style: { marginLeft: 'auto' } }, meta),
|
|
1046
|
+
),
|
|
1047
|
+
isContent && hit.snippet
|
|
1048
|
+
? h('div', { className: 'dsh-locator-snippet' }, highlight(hit.snippet, query))
|
|
1049
|
+
: null,
|
|
1050
|
+
h('div', { className: 'dsh-locator-path' }, hit.path),
|
|
1051
|
+
)
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
let index = -1
|
|
1055
|
+
const renderGroup = (key, label, group, extra) => {
|
|
1056
|
+
if (!scopes[key] || group === undefined) return null
|
|
1057
|
+
const hits = Array.isArray(group.hits) ? group.hits : []
|
|
1058
|
+
const count = typeof group.returned === 'number' ? group.returned : hits.length
|
|
1059
|
+
const note = key === 'content'
|
|
1060
|
+
? (contentBuilding ? t('contentBuilding', { files: content?.files ?? 0, messages: content?.messages ?? 0 })
|
|
1061
|
+
: (content?.capped ? t('contentCapped', { n: count }) : (query.trim() !== '' ? t('relevance') : null)))
|
|
1062
|
+
: (group.deduped > 0 ? t('deduped', { n: group.deduped }) : (group.truncated ? t('showing', { n: hits.length }) : null))
|
|
1063
|
+
return h('div', { className: 'dsh-locator-group', role: 'group', 'aria-labelledby': 'dsh-locator-group-' + key },
|
|
1064
|
+
groupHead(key, label, count, note),
|
|
1065
|
+
extra === undefined ? null : extra,
|
|
1066
|
+
hits.length === 0
|
|
1067
|
+
? h('div', { className: 'dsh-locator-groupempty' },
|
|
1068
|
+
key === 'content' && scopes.content
|
|
1069
|
+
? (query.trim().length < 2 ? t('contentShort') : (contentBuilding ? t('loading') : t('emptyResults')))
|
|
1070
|
+
: (query.trim() === '' ? t('emptyQuery') : t('emptyResults')))
|
|
1071
|
+
: hits.map(hit => { index++; return rowFor(key, hit, index) }),
|
|
1072
|
+
)
|
|
1073
|
+
}
|
|
607
1074
|
|
|
608
1075
|
return h('div', {
|
|
609
1076
|
className: 'dsh-locator-overlay',
|
|
610
1077
|
onMouseDown: event => { if (event.target === event.currentTarget) close() },
|
|
611
1078
|
}, h('div', {
|
|
1079
|
+
ref: panelRef,
|
|
612
1080
|
className: 'dsh-locator-panel',
|
|
613
1081
|
role: 'dialog',
|
|
614
1082
|
'aria-modal': 'true',
|
|
@@ -624,22 +1092,40 @@ window.__ModuleLoader__.load({
|
|
|
624
1092
|
spellCheck: false,
|
|
625
1093
|
autoComplete: 'off',
|
|
626
1094
|
placeholder: t('placeholder'),
|
|
627
|
-
onChange: event => setQuery(event.target.value),
|
|
1095
|
+
onChange: event => { setQuery(event.target.value); setSelected(0) },
|
|
628
1096
|
'aria-label': t('searchLabel'),
|
|
1097
|
+
role: 'combobox',
|
|
1098
|
+
'aria-expanded': 'true',
|
|
1099
|
+
'aria-controls': 'dsh-locator-results',
|
|
1100
|
+
'aria-autocomplete': 'list',
|
|
1101
|
+
'aria-activedescendant': totalRows > 0 ? 'dsh-locator-opt-' + selected : undefined,
|
|
629
1102
|
}),
|
|
630
1103
|
h('span', { className: 'dsh-locator-kbd' }, 'Ctrl+Shift+F'),
|
|
631
1104
|
h('button', { className: 'dsh-locator-close', onClick: close, title: t('closeTitle'), 'aria-label': t('closeLabel') }, '×'),
|
|
632
1105
|
),
|
|
633
1106
|
h('div', { className: 'dsh-locator-bar' },
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
h('
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
1107
|
+
scopeBox('content', t('scopeContent'), contentCount),
|
|
1108
|
+
scopeBox('files', t('scopeFiles'), filesCount),
|
|
1109
|
+
scopeBox('dirs', t('scopeDirs'), dirsCount),
|
|
1110
|
+
h('span', { className: 'grow' }),
|
|
1111
|
+
h('label', { className: 'dsh-locator-check' }, t('sortLabel'),
|
|
1112
|
+
h('select', {
|
|
1113
|
+
className: 'dsh-locator-sort',
|
|
1114
|
+
value: sort,
|
|
1115
|
+
onChange: event => setSort(SORTS.includes(event.target.value) ? event.target.value : 'mtime'),
|
|
1116
|
+
'aria-label': t('sortLabel'),
|
|
1117
|
+
},
|
|
1118
|
+
h('option', { value: 'mtime' }, t('sortMtime')),
|
|
1119
|
+
h('option', { value: 'name' }, t('sortName')),
|
|
1120
|
+
h('option', { value: 'size' }, t('sortSize')),
|
|
1121
|
+
h('option', { value: 'path' }, t('sortPath')),
|
|
1122
|
+
)),
|
|
1123
|
+
h('button', {
|
|
1124
|
+
className: 'dsh-locator-dirbtn',
|
|
1125
|
+
onClick: () => setDesc(value => !value),
|
|
1126
|
+
title: desc ? t('descOrder') : t('asc'),
|
|
1127
|
+
'aria-label': desc ? t('descOrder') : t('asc'),
|
|
1128
|
+
}, desc ? '↓' : '↑'),
|
|
643
1129
|
h('span', { className: 'dsh-locator-status' },
|
|
644
1130
|
status === null
|
|
645
1131
|
? t('statusLoading')
|
|
@@ -647,57 +1133,32 @@ window.__ModuleLoader__.load({
|
|
|
647
1133
|
entries: (status.entries || 0).toLocaleString(),
|
|
648
1134
|
mode: status.watching ? t('live') : t('snapshot'),
|
|
649
1135
|
roots: (status.roots || []).length,
|
|
650
|
-
})
|
|
1136
|
+
})),
|
|
651
1137
|
),
|
|
652
|
-
h('div', { className: 'dsh-locator-scroll', ref: listRef },
|
|
653
|
-
|
|
654
|
-
h('
|
|
655
|
-
|
|
656
|
-
h('
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
)),
|
|
666
|
-
h('tbody', null,
|
|
667
|
-
hits.length === 0
|
|
668
|
-
? h('tr', null, h('td', { colSpan: 4, className: 'dsh-locator-empty' },
|
|
669
|
-
loading ? t('loading') : error !== '' ? t('errorPrefix') + error : t('empty')))
|
|
670
|
-
: hits.map((hit, index) => h('tr', {
|
|
671
|
-
key: hit.path,
|
|
672
|
-
'data-index': index,
|
|
673
|
-
className: 'dsh-locator-row' + (index === selected ? ' sel' : ''),
|
|
674
|
-
title: hit.path,
|
|
675
|
-
onClick: () => setSelected(index),
|
|
676
|
-
onDoubleClick: () => activate(hit),
|
|
677
|
-
},
|
|
678
|
-
h('td', null, h('div', { className: 'dsh-locator-name' },
|
|
679
|
-
h('span', { className: 'dsh-locator-badge' }, hit.kind === 'session' ? t('sessionBadge') : (hit.ext || t('fileBadge')).toUpperCase()),
|
|
680
|
-
h('span', null, hit.content ? hit.name : highlight(hit.name, query)),
|
|
681
|
-
hit.content && hit.snippet ? h('span', { className: 'dsh-locator-snippet' }, hit.snippet.slice(0, 180)) : null,
|
|
682
|
-
)),
|
|
683
|
-
h('td', { className: 'dsh-locator-size' }, hit.dir ? '' : formatSize(hit.size)),
|
|
684
|
-
h('td', { className: 'dsh-locator-time' }, formatTime(hit.mtime)),
|
|
685
|
-
h('td', { className: 'dsh-locator-path' }, hit.path),
|
|
686
|
-
)),
|
|
687
|
-
),
|
|
688
|
-
),
|
|
1138
|
+
h('div', { id: 'dsh-locator-results', className: 'dsh-locator-scroll', ref: listRef, role: 'listbox', 'aria-busy': loading ? 'true' : 'false' },
|
|
1139
|
+
!anyScope
|
|
1140
|
+
? h('div', { className: 'dsh-locator-empty' }, t('emptyScopes'))
|
|
1141
|
+
: error !== ''
|
|
1142
|
+
? h('div', { className: 'dsh-locator-empty', role: 'alert' }, t('errorPrefix') + error,
|
|
1143
|
+
h('button', { className: 'dsh-locator-dirbtn dsh-locator-retry', onClick: () => setRefreshTick(v => v + 1) }, t('retry')))
|
|
1144
|
+
: groups === null
|
|
1145
|
+
? h('div', { className: 'dsh-locator-empty' }, t('loading'))
|
|
1146
|
+
: [
|
|
1147
|
+
renderGroup('content', t('groupContent'), content),
|
|
1148
|
+
renderGroup('files', t('groupFiles'), files),
|
|
1149
|
+
renderGroup('dirs', t('groupDirs'), dirs),
|
|
1150
|
+
],
|
|
689
1151
|
),
|
|
690
1152
|
h('div', { className: 'dsh-locator-foot' },
|
|
691
|
-
h('span', null,
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
: t('results', { total: total.toLocaleString() }) + (truncated ? ' (' + t('showing', { n: hits.length }) + ')' : '') + ' · ' + took.toFixed(1) + ' ms'
|
|
695
|
-
+ (status && status.scanning ? ' · ' + t('indexing') : '')
|
|
696
|
-
+ (contentBuilding ? ' · ' + t('indexing') : '')),
|
|
1153
|
+
h('span', null, t('results', { content: contentCount, files: filesCount, dirs: dirsCount, ms: took.toFixed(1) })
|
|
1154
|
+
+ (status && status.scanning ? ' · ' + t('indexing') : '')
|
|
1155
|
+
+ (contentBuilding ? ' · ' + t('indexing') : '')),
|
|
697
1156
|
h('span', { className: 'grow' }),
|
|
698
1157
|
toast === '' ? null : h('span', { className: 'dsh-locator-toast' }, toast),
|
|
699
1158
|
h('span', null, t('footer')),
|
|
700
1159
|
),
|
|
1160
|
+
h('div', { style: { position: 'absolute', width: 1, height: 1, overflow: 'hidden', clip: 'rect(0 0 0 0)' }, role: 'status', 'aria-live': 'polite' },
|
|
1161
|
+
t('results', { content: contentCount, files: filesCount, dirs: dirsCount, ms: Math.round(took) })),
|
|
701
1162
|
))
|
|
702
1163
|
}
|
|
703
1164
|
|
|
@@ -728,6 +1189,7 @@ window.__ModuleLoader__.load({
|
|
|
728
1189
|
document.addEventListener('keydown', onKey, true)
|
|
729
1190
|
return () => document.removeEventListener('keydown', onKey, true)
|
|
730
1191
|
}, 'dsh-locator: search hotkey')
|
|
1192
|
+
ctx.effect(() => () => { cancelJump() }, 'dsh-locator: find highlight')
|
|
731
1193
|
try {
|
|
732
1194
|
ctx.slots.inject('shell.overlay', () => ctx.slots.register({ name: 'shell.overlay', id: 'dsh-locator-panel', order: 60 }, Panel))
|
|
733
1195
|
} catch (error) {
|