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