@lyhue1991/dsh-soup 0.3.0 → 0.4.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/README.md +5 -1
- package/lib/client.js +647 -426
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -180,7 +180,13 @@ window.__ModuleLoader__.load({
|
|
|
180
180
|
'.dfv-table-wrap{flex:1;min-height:0;overflow:auto;}' +
|
|
181
181
|
'.dfv-table{border-collapse:collapse;font-size:12px;font-family:var(--ds-font-family-code);}' +
|
|
182
182
|
'.dfv-th{position:sticky;top:0;z-index:1;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font-weight:600;text-align:left;padding:5px 10px;border-bottom:1px solid var(--dsw-alias-border-l2);border-right:1px solid var(--dsw-alias-border-l1);white-space:nowrap;}' +
|
|
183
|
-
'.dfv-td{padding:4px 10px;border-bottom:1px solid var(--dsw-alias-border-l1);border-right:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-primary);white-space:pre-wrap;overflow-wrap:anywhere;max-width:360px;}'
|
|
183
|
+
'.dfv-td{padding:4px 10px;border-bottom:1px solid var(--dsw-alias-border-l1);border-right:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-primary);white-space:pre-wrap;overflow-wrap:anywhere;max-width:360px;}' +
|
|
184
|
+
// 纯阅读模式:预览 tab 激活时隐藏整个输入区(composerSeat:输入框 +
|
|
185
|
+
// GoalBar + 速度徽标所在的 input.dock)与轮次统计行(StatsLine)。
|
|
186
|
+
// class 来自 DSH 客户端编译产物(CSS module hash),升级后若失效仅
|
|
187
|
+
// 表现为「预览页仍显示输入区」,不影响其它功能——属可接受的降级。
|
|
188
|
+
'[data-dsh-soup-preview="1"] .wSkVaW_composerSeat{display:none !important;}' +
|
|
189
|
+
'[data-dsh-soup-preview="1"] .FJxK0a_root{display:none !important;}'
|
|
184
190
|
if (typeof document !== 'undefined' && document.querySelector('style[data-plugin-css="@lyhue1991/dsh-soup/fileview.css"]') === null) {
|
|
185
191
|
var fileviewTag = document.createElement('style')
|
|
186
192
|
fileviewTag.dataset.plugin = '@lyhue1991/dsh-soup'
|
|
@@ -189,8 +195,150 @@ window.__ModuleLoader__.load({
|
|
|
189
195
|
document.head.appendChild(fileviewTag)
|
|
190
196
|
}
|
|
191
197
|
|
|
192
|
-
/** 需要的客户端服务:slots、layout、timer、sessions
|
|
193
|
-
var inject = ['slots', 'layout', 'timer', 'sessions', 'remote', 'remote.goals']
|
|
198
|
+
/** 需要的客户端服务:slots、layout、timer、sessions、locale(i18n 词典)+ remote.goals(goal 动作动词)。 */
|
|
199
|
+
var inject = ['slots', 'layout', 'timer', 'sessions', 'locale', 'remote', 'remote.goals']
|
|
200
|
+
|
|
201
|
+
// ------------------------------------------------------------------
|
|
202
|
+
// ------------------------------------------------------------------
|
|
203
|
+
// ▓▓ 区域零 · i18n / 词典
|
|
204
|
+
// 注册双语词典 NS/DICT;组件内 t()、组件外 T(),语言切换即时生效。
|
|
205
|
+
// ------------------------------------------------------------------
|
|
206
|
+
// i18n:走 DSH 官方插件词典机制(ui-cordis / ui-conversation 同款)——
|
|
207
|
+
// ctx.locale.register(NS, DICT) 注册;槽位配置加 locale: NS 后组件 props
|
|
208
|
+
// 获得 t(语言切换铸造新 t 引用,靠浅比较自动重渲染整棵子树);组件外
|
|
209
|
+
// 回调(菜单 label、错误横幅)用模块级 T(bind 的 t 每次调用解析当前语言)。
|
|
210
|
+
// ------------------------------------------------------------------
|
|
211
|
+
var NS = 'dsh-soup'
|
|
212
|
+
var DICT = {
|
|
213
|
+
zh: {
|
|
214
|
+
'explorer.title': '📁 资源管理器',
|
|
215
|
+
'explorer.label': '资源管理器',
|
|
216
|
+
'explorer.refresh': '刷新',
|
|
217
|
+
'explorer.refreshTitle': '刷新(保持已展开目录)',
|
|
218
|
+
'explorer.upload': '上传文件',
|
|
219
|
+
'explorer.close': '关闭',
|
|
220
|
+
'explorer.closePanel': '关闭资源管理器',
|
|
221
|
+
'explorer.emptyDir': '(空目录)',
|
|
222
|
+
'explorer.bulkSelected': '已选 {n} 项 · 拖到文件夹可移动',
|
|
223
|
+
'explorer.readFail': '读取失败',
|
|
224
|
+
'explorer.actionFail': '失败',
|
|
225
|
+
'explorer.uploading': '上传中 ',
|
|
226
|
+
'explorer.downloading': '下载中 ',
|
|
227
|
+
'explorer.uploaded': '已上传 {n} 个文件',
|
|
228
|
+
'explorer.filesCount': '{n} 个文件',
|
|
229
|
+
'explorer.filesCountPartial': '{ok}/{total} 个文件',
|
|
230
|
+
'explorer.downloaded': '已下载 {n} 个文件',
|
|
231
|
+
'explorer.uploadFail': '上传失败',
|
|
232
|
+
'explorer.downloadFail': '下载失败',
|
|
233
|
+
'explorer.downloadFailWith': '下载失败: {reason}',
|
|
234
|
+
'explorer.downloadStart': '已开始下载 {name}(大文件,由浏览器接管)',
|
|
235
|
+
'explorer.renamed': '已复制路径',
|
|
236
|
+
'explorer.renameFail': '重命名失败',
|
|
237
|
+
'explorer.createFail': '创建失败',
|
|
238
|
+
'explorer.moveIntoSelf': '不能把文件夹移入自身: {path}',
|
|
239
|
+
'explorer.hostUnreachable': '宿主不可达: {reason}',
|
|
240
|
+
'explorer.hostBadResponse': '宿主响应解析失败',
|
|
241
|
+
'menu.preview': '👁 预览',
|
|
242
|
+
'menu.open': '🖥 用系统打开',
|
|
243
|
+
'menu.rename': '✎ 重命名',
|
|
244
|
+
'menu.trash': '🗑 移到废纸篓',
|
|
245
|
+
'menu.trashMulti': '🗑 移到废纸篓 ({n} 项)',
|
|
246
|
+
'menu.newFile': '📄 新建文件',
|
|
247
|
+
'menu.newFolder': '📁 新建文件夹',
|
|
248
|
+
'menu.copyPath': '⧉ 复制路径',
|
|
249
|
+
'menu.download': '⬇ 下载',
|
|
250
|
+
'menu.deselect': '取消选择',
|
|
251
|
+
'files.tab': '预览',
|
|
252
|
+
'files.emptyHint': '在右侧资源管理器中双击或右键「预览」文件',
|
|
253
|
+
'files.loading': '加载中…',
|
|
254
|
+
'files.readFail': '读取文件失败',
|
|
255
|
+
'files.reload': '重载',
|
|
256
|
+
'files.reloadTitle': '从磁盘重新读取(刷新预览)',
|
|
257
|
+
'files.binary': '二进制文件({size}),请在资源管理器中用系统打开',
|
|
258
|
+
'files.imageTooLarge': '图片过大({size},上限 {limit})',
|
|
259
|
+
'files.pdfTooLarge': 'PDF 过大({size},上限 {limit}),请用系统打开',
|
|
260
|
+
'files.truncated': '文件超过 2 MB,仅预览前 2 MB;完整内容请用系统编辑器打开。',
|
|
261
|
+
'files.jsonFail': 'JSON 解析失败,按纯文本显示',
|
|
262
|
+
'files.nbFail': 'ipynb 解析失败,按纯文本显示',
|
|
263
|
+
'files.nbEmpty': '(空笔记本)',
|
|
264
|
+
'files.nbCells': '共 {n} 个单元格',
|
|
265
|
+
'files.csvTruncated': '共 {total} 行,仅预览前 {n} 行',
|
|
266
|
+
'files.htmlFrame': 'HTML 预览',
|
|
267
|
+
'files.pdfFrame': 'PDF 预览',
|
|
268
|
+
'files.pdfLoading': 'PDF 加载中…',
|
|
269
|
+
'files.pdfFail': 'PDF 预览失败: {reason}',
|
|
270
|
+
'files.pdfUnsupported': '当前浏览器无法内嵌预览 PDF,请右键用系统打开。',
|
|
271
|
+
'files.nbHtmlOut': 'HTML 输出',
|
|
272
|
+
'speed.label': '速度徽标',
|
|
273
|
+
'speed.waiting': '正在等待模型',
|
|
274
|
+
'speed.operateFail': '操作失败',
|
|
275
|
+
},
|
|
276
|
+
en: {
|
|
277
|
+
'explorer.title': '📁 Explorer',
|
|
278
|
+
'explorer.label': 'Explorer',
|
|
279
|
+
'explorer.refresh': 'Refresh',
|
|
280
|
+
'explorer.refreshTitle': 'Refresh (keeps expanded folders)',
|
|
281
|
+
'explorer.upload': 'Upload files',
|
|
282
|
+
'explorer.close': 'Close',
|
|
283
|
+
'explorer.closePanel': 'Close explorer',
|
|
284
|
+
'explorer.emptyDir': '(empty)',
|
|
285
|
+
'explorer.bulkSelected': '{n} selected · drag onto a folder to move',
|
|
286
|
+
'explorer.readFail': 'Failed to read',
|
|
287
|
+
'explorer.actionFail': 'Failed',
|
|
288
|
+
'explorer.uploading': 'Uploading ',
|
|
289
|
+
'explorer.downloading': 'Downloading ',
|
|
290
|
+
'explorer.uploaded': 'Uploaded {n} file(s)',
|
|
291
|
+
'explorer.filesCount': '{n} file(s)',
|
|
292
|
+
'explorer.filesCountPartial': '{ok}/{total} file(s)',
|
|
293
|
+
'explorer.downloaded': 'Downloaded {n} file(s)',
|
|
294
|
+
'explorer.uploadFail': 'Upload failed',
|
|
295
|
+
'explorer.downloadFail': 'Download failed',
|
|
296
|
+
'explorer.downloadFailWith': 'Download failed: {reason}',
|
|
297
|
+
'explorer.downloadStart': 'Downloading {name} (large file, handled by the browser)',
|
|
298
|
+
'explorer.renamed': 'Path copied',
|
|
299
|
+
'explorer.renameFail': 'Rename failed',
|
|
300
|
+
'explorer.createFail': 'Create failed',
|
|
301
|
+
'explorer.moveIntoSelf': 'Cannot move a folder into itself: {path}',
|
|
302
|
+
'explorer.hostUnreachable': 'Host unreachable: {reason}',
|
|
303
|
+
'explorer.hostBadResponse': 'Failed to parse host response',
|
|
304
|
+
'menu.preview': '👁 Preview',
|
|
305
|
+
'menu.open': '🖥 Open with system',
|
|
306
|
+
'menu.rename': '✎ Rename',
|
|
307
|
+
'menu.trash': '🗑 Move to trash',
|
|
308
|
+
'menu.trashMulti': '🗑 Move to trash ({n})',
|
|
309
|
+
'menu.newFile': '📄 New file',
|
|
310
|
+
'menu.newFolder': '📁 New folder',
|
|
311
|
+
'menu.copyPath': '⧉ Copy path',
|
|
312
|
+
'menu.download': '⬇ Download',
|
|
313
|
+
'menu.deselect': 'Clear selection',
|
|
314
|
+
'files.tab': 'Preview',
|
|
315
|
+
'files.emptyHint': 'Double-click a file (or right-click → Preview) in the explorer',
|
|
316
|
+
'files.loading': 'Loading…',
|
|
317
|
+
'files.readFail': 'Failed to read file',
|
|
318
|
+
'files.reload': 'Reload',
|
|
319
|
+
'files.reloadTitle': 'Re-read from disk (refresh preview)',
|
|
320
|
+
'files.binary': 'Binary file ({size}) — open with the system explorer',
|
|
321
|
+
'files.imageTooLarge': 'Image too large ({size}, limit {limit})',
|
|
322
|
+
'files.pdfTooLarge': 'PDF too large ({size}, limit {limit}) — open with the system viewer',
|
|
323
|
+
'files.truncated': 'File exceeds 2 MB — showing the first 2 MB; open in a system editor for the full content.',
|
|
324
|
+
'files.jsonFail': 'JSON parse failed — shown as plain text',
|
|
325
|
+
'files.nbFail': 'ipynb parse failed — shown as plain text',
|
|
326
|
+
'files.nbEmpty': '(empty notebook)',
|
|
327
|
+
'files.nbCells': '{n} cells',
|
|
328
|
+
'files.csvTruncated': '{total} rows — showing the first {n}',
|
|
329
|
+
'files.htmlFrame': 'HTML preview',
|
|
330
|
+
'files.pdfFrame': 'PDF preview',
|
|
331
|
+
'files.pdfLoading': 'Loading PDF…',
|
|
332
|
+
'files.pdfFail': 'PDF preview failed: {reason}',
|
|
333
|
+
'files.pdfUnsupported': 'This browser cannot embed PDF previews — right-click and open with the system viewer.',
|
|
334
|
+
'files.nbHtmlOut': 'HTML output',
|
|
335
|
+
'speed.label': 'Speed badge',
|
|
336
|
+
'speed.waiting': 'Waiting for the model',
|
|
337
|
+
'speed.operateFail': 'Action failed',
|
|
338
|
+
},
|
|
339
|
+
}
|
|
340
|
+
/** 模块级翻译函数(apply 里由 ctx.locale.bind 填充)。 */
|
|
341
|
+
var T = function (key, params) { return key }
|
|
194
342
|
|
|
195
343
|
// ------------------------------------------------------------------
|
|
196
344
|
// 状态存储(模块级,供 Panel 与 HeaderAction 共享)
|
|
@@ -218,6 +366,10 @@ window.__ModuleLoader__.load({
|
|
|
218
366
|
return state
|
|
219
367
|
}
|
|
220
368
|
|
|
369
|
+
// ------------------------------------------------------------------
|
|
370
|
+
// ▓▓ 区域一 · 资源管理器 · 🧅 葱(共享数据层)
|
|
371
|
+
// 共享 store、/api/dsh-soup RPC、树数据加载/自动刷新、selection、拖拽/上传/下载、类型图标 —— 预览亦复用其中 store/rpc。
|
|
372
|
+
// ------------------------------------------------------------------
|
|
221
373
|
var visibleRows = []
|
|
222
374
|
var uploadInputEl = null
|
|
223
375
|
var layout = null
|
|
@@ -254,9 +406,9 @@ window.__ModuleLoader__.load({
|
|
|
254
406
|
headers: { 'content-type': 'application/json', 'x-dsh-soup': '1' },
|
|
255
407
|
body: JSON.stringify({ action: action, args: args || {} }),
|
|
256
408
|
}).then(function (res) {
|
|
257
|
-
return res.json().catch(function () { return { ok: false, error: '
|
|
409
|
+
return res.json().catch(function () { return { ok: false, error: T('explorer.hostBadResponse') } })
|
|
258
410
|
}).catch(function (err) {
|
|
259
|
-
return { ok: false, error: '
|
|
411
|
+
return { ok: false, error: T('explorer.hostUnreachable', { reason: String((err && err.message) || err) }) }
|
|
260
412
|
})
|
|
261
413
|
}
|
|
262
414
|
|
|
@@ -268,7 +420,7 @@ window.__ModuleLoader__.load({
|
|
|
268
420
|
return loadDir(path, (attempt || 0) + 1)
|
|
269
421
|
}
|
|
270
422
|
if (!res || !res.ok) {
|
|
271
|
-
setState({ error: (res && res.error) || '
|
|
423
|
+
setState({ error: (res && res.error) || T('explorer.readFail') })
|
|
272
424
|
return null
|
|
273
425
|
}
|
|
274
426
|
return res.entries || []
|
|
@@ -599,7 +751,7 @@ window.__ModuleLoader__.load({
|
|
|
599
751
|
var errors = []
|
|
600
752
|
for (var i = 0; i < paths.length; i++) {
|
|
601
753
|
var res = await rpc('trash', { path: paths[i] })
|
|
602
|
-
if (!res || !res.ok) errors.push(baseName(paths[i]) + ': ' + ((res && res.error) || '
|
|
754
|
+
if (!res || !res.ok) errors.push(baseName(paths[i]) + ': ' + ((res && res.error) || T('explorer.actionFail')) + ((res && res.hint) ? '(' + res.hint + ')' : ''))
|
|
603
755
|
}
|
|
604
756
|
if (errors.length) setState({ error: errors.join(';') })
|
|
605
757
|
setState({ selected: new Set(), lastIndex: null })
|
|
@@ -620,7 +772,7 @@ window.__ModuleLoader__.load({
|
|
|
620
772
|
setState({ renaming: null, selected: sel })
|
|
621
773
|
;(async function () {
|
|
622
774
|
var res = await rpc('move', { from: path, to: dest })
|
|
623
|
-
if (!res || !res.ok) setState({ error: (res && res.error) || '
|
|
775
|
+
if (!res || !res.ok) setState({ error: (res && res.error) || T('explorer.renameFail') })
|
|
624
776
|
await refresh()
|
|
625
777
|
})()
|
|
626
778
|
}
|
|
@@ -641,7 +793,7 @@ window.__ModuleLoader__.load({
|
|
|
641
793
|
setState({ newItem: null })
|
|
642
794
|
;(async function () {
|
|
643
795
|
var res = await rpc('create', { dir: parent, name: name, isDir: isDir })
|
|
644
|
-
if (!res || !res.ok) setState({ error: (res && res.error) || '
|
|
796
|
+
if (!res || !res.ok) setState({ error: (res && res.error) || T('explorer.createFail') })
|
|
645
797
|
await refresh()
|
|
646
798
|
})()
|
|
647
799
|
}
|
|
@@ -661,7 +813,7 @@ window.__ModuleLoader__.load({
|
|
|
661
813
|
setState({ menu: null })
|
|
662
814
|
try {
|
|
663
815
|
if (navigator && navigator.clipboard && navigator.clipboard.writeText) {
|
|
664
|
-
navigator.clipboard.writeText(p).then(function () { showNotice('
|
|
816
|
+
navigator.clipboard.writeText(p).then(function () { showNotice(T('explorer.renamed')) }).catch(function () {})
|
|
665
817
|
}
|
|
666
818
|
} catch (err) {}
|
|
667
819
|
}
|
|
@@ -703,9 +855,9 @@ window.__ModuleLoader__.load({
|
|
|
703
855
|
var name = baseName(p)
|
|
704
856
|
var dest = pathJoin(targetDir, name)
|
|
705
857
|
if (dest === p) continue
|
|
706
|
-
if (targetDir === p || targetDir.startsWith(p + '/')) { errors.push('
|
|
858
|
+
if (targetDir === p || targetDir.startsWith(p + '/')) { errors.push(T('explorer.moveIntoSelf', { path: name })); continue }
|
|
707
859
|
var res = await rpc('move', { from: p, to: dest })
|
|
708
|
-
if (!res || !res.ok) errors.push(name + ': ' + ((res && res.error) || '
|
|
860
|
+
if (!res || !res.ok) errors.push(name + ': ' + ((res && res.error) || T('explorer.actionFail')))
|
|
709
861
|
}
|
|
710
862
|
setState({ dragPaths: null })
|
|
711
863
|
if (errors.length) setState({ error: errors.join(';') })
|
|
@@ -716,7 +868,7 @@ window.__ModuleLoader__.load({
|
|
|
716
868
|
return new Promise(function (resolve, reject) {
|
|
717
869
|
var r = new FileReader()
|
|
718
870
|
r.onload = function () { var s = String(r.result || ''); var i = s.indexOf(','); resolve(i >= 0 ? s.slice(i + 1) : s) }
|
|
719
|
-
r.onerror = function () { reject(new Error('
|
|
871
|
+
r.onerror = function () { reject(new Error(T('files.readFail'))) }
|
|
720
872
|
r.readAsDataURL(file)
|
|
721
873
|
})
|
|
722
874
|
}
|
|
@@ -739,9 +891,9 @@ window.__ModuleLoader__.load({
|
|
|
739
891
|
}
|
|
740
892
|
|
|
741
893
|
function finishUploads(count, okCount, failedName, down) {
|
|
742
|
-
var verb = down ? '
|
|
894
|
+
var verb = down ? T('explorer.downloaded') : T('explorer.uploaded')
|
|
743
895
|
setUploadEntry({
|
|
744
|
-
name: '✓ ' + verb + ' ' + (okCount === count ?
|
|
896
|
+
name: '✓ ' + verb + ' ' + (okCount === count ? T('explorer.filesCount', { n: count }) : T('explorer.filesCountPartial', { ok: okCount, total: count }) + (failedName ? '(' + failedName + ')' : '')),
|
|
745
897
|
loaded: 0, total: 0, idx: 0, totalFiles: 0, done: true, down: !!down,
|
|
746
898
|
})
|
|
747
899
|
window.setTimeout(function () { setState({ uploads: [] }) }, 1600)
|
|
@@ -760,7 +912,7 @@ window.__ModuleLoader__.load({
|
|
|
760
912
|
dir: dir, name: f.name, data: b64,
|
|
761
913
|
chunk: chunked ? c + 1 : undefined,
|
|
762
914
|
})
|
|
763
|
-
if (!res || !res.ok) throw new Error((res && res.error) || '
|
|
915
|
+
if (!res || !res.ok) throw new Error((res && res.error) || T('explorer.uploadFail'))
|
|
764
916
|
updateUpload(f.name, Math.min(f.size, start + UPLOAD_CHUNK_SIZE), f.size, idx, totalFiles)
|
|
765
917
|
}
|
|
766
918
|
}
|
|
@@ -794,7 +946,7 @@ window.__ModuleLoader__.load({
|
|
|
794
946
|
res = await rpc('download', { path: path, sessionId: activeSessionId })
|
|
795
947
|
}
|
|
796
948
|
if (!res || !res.ok) {
|
|
797
|
-
setState({ error: (res && res.error) || '
|
|
949
|
+
setState({ error: (res && res.error) || T('explorer.downloadFail') })
|
|
798
950
|
return
|
|
799
951
|
}
|
|
800
952
|
var name = res.name || baseName(path)
|
|
@@ -807,7 +959,7 @@ window.__ModuleLoader__.load({
|
|
|
807
959
|
document.body.appendChild(a)
|
|
808
960
|
a.click()
|
|
809
961
|
a.remove()
|
|
810
|
-
showNotice('
|
|
962
|
+
showNotice(T('explorer.downloadStart', { name: name }))
|
|
811
963
|
return
|
|
812
964
|
}
|
|
813
965
|
// 中小文件:fetch 流式读字节计进度,完成后 Blob 另存(峰值≈文件体积)
|
|
@@ -836,7 +988,7 @@ window.__ModuleLoader__.load({
|
|
|
836
988
|
window.setTimeout(function () { URL.revokeObjectURL(blobUrl) }, 5000)
|
|
837
989
|
finishUploads(1, 1, '', true)
|
|
838
990
|
} catch (err) {
|
|
839
|
-
setState({ error: '
|
|
991
|
+
setState({ error: T('explorer.downloadFailWith', { reason: String((err && err.message) || err) }), uploads: [] })
|
|
840
992
|
}
|
|
841
993
|
}
|
|
842
994
|
|
|
@@ -847,137 +999,10 @@ window.__ModuleLoader__.load({
|
|
|
847
999
|
}
|
|
848
1000
|
|
|
849
1001
|
// ------------------------------------------------------------------
|
|
850
|
-
// 文件标签页:右键/双击「预览」把文件以只读预览展示进会话区的「预览」
|
|
851
|
-
// tab(原生 conversation.view 槽位,与 对话/轨迹 同级)。
|
|
852
|
-
// markdown/html/json/csv 有专属渲染器;图片预览;其余按纯文本。
|
|
853
|
-
// 状态存模块级 store,切走 tab 再切回来内容不丢。
|
|
854
1002
|
// ------------------------------------------------------------------
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
var activeSessionId = null
|
|
859
|
-
|
|
860
|
-
/** 同时打开的预览 tab 上限:超过后挤掉最早打开的(FIFO)。 */
|
|
861
|
-
var FILES_MAX_OPEN = 5
|
|
862
|
-
|
|
863
|
-
/** path 是否落在 base 目录子树内(base 为根或空时视为全量放行)。 */
|
|
864
|
-
function withinScope(path, base) {
|
|
865
|
-
if (!base || base === '/') return true
|
|
866
|
-
return path === base || path.indexOf(base + '/') === 0
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
/**
|
|
870
|
-
* 会话切换时按新 cwd 裁剪预览列表:范围外的 tab 自动关闭。
|
|
871
|
-
* 若当前激活文件被裁掉,激活项顺移到剩余最后一个。
|
|
872
|
-
* 列表无变化时返回原引用,避免触发多余渲染。
|
|
873
|
-
*/
|
|
874
|
-
function pruneFilesToScope(files, cwd) {
|
|
875
|
-
var kept = files.list.filter(function (f) { return withinScope(f.path, cwd) })
|
|
876
|
-
try { window.__DFV_PRUNE = (window.__DFV_PRUNE || []) ; window.__DFV_PRUNE.push({ cwd: cwd, before: files.list.length, after: kept.length }) } catch (_) {}
|
|
877
|
-
if (kept.length === files.list.length) return files
|
|
878
|
-
var activeKept = kept.some(function (f) { return f.path === files.active })
|
|
879
|
-
var active = files.active
|
|
880
|
-
if (!activeKept) active = kept.length ? kept[kept.length - 1].path : null
|
|
881
|
-
return { list: kept, active: active }
|
|
882
|
-
}
|
|
883
|
-
|
|
884
|
-
function setFiles(patch) {
|
|
885
|
-
setState({ files: Object.assign({}, state.files, patch) })
|
|
886
|
-
try {
|
|
887
|
-
var f = state.files
|
|
888
|
-
window.__DFV_DBG = { n: f.list.length, active: f.active, ops: ((window.__DFV_DBG && window.__DFV_DBG.ops) || 0) + 1 }
|
|
889
|
-
} catch (_) {}
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
function findFileEntry(path) {
|
|
893
|
-
for (var i = 0; i < state.files.list.length; i++) {
|
|
894
|
-
if (state.files.list[i].path === path) return state.files.list[i]
|
|
895
|
-
}
|
|
896
|
-
return null
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
/** 点击原生 header 的「预览」tab,把 view ring 切到预览视图。 */
|
|
900
|
-
function activateFilesView() {
|
|
901
|
-
try {
|
|
902
|
-
var tabs = document.querySelectorAll('[role="tab"]')
|
|
903
|
-
for (var i = 0; i < tabs.length; i++) {
|
|
904
|
-
if (tabs[i].textContent === FILES_TAB_LABEL) { tabs[i].click(); return true }
|
|
905
|
-
}
|
|
906
|
-
} catch (err) {}
|
|
907
|
-
return false
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
async function openFileInTab(path) {
|
|
911
|
-
setState({ menu: null })
|
|
912
|
-
var entry = findFileEntry(path)
|
|
913
|
-
if (!entry) {
|
|
914
|
-
entry = {
|
|
915
|
-
path: path, name: baseName(path),
|
|
916
|
-
loading: true, loaded: false, error: '',
|
|
917
|
-
kind: null, content: '', dataUrl: '', data: '',
|
|
918
|
-
truncated: false, size: 0,
|
|
919
|
-
}
|
|
920
|
-
// FIFO:最多同时 5 个预览,新开的挤掉最早打开的
|
|
921
|
-
var list = state.files.list.slice()
|
|
922
|
-
while (list.length >= FILES_MAX_OPEN) list.shift()
|
|
923
|
-
list.push(entry)
|
|
924
|
-
setFiles({ list: list, active: path })
|
|
925
|
-
} else if (entry.loaded) {
|
|
926
|
-
setFiles({ active: path })
|
|
927
|
-
activateFilesView()
|
|
928
|
-
return
|
|
929
|
-
} else {
|
|
930
|
-
setFiles({ active: path })
|
|
931
|
-
}
|
|
932
|
-
activateFilesView()
|
|
933
|
-
var res = await rpc('read', { path: path, sessionId: activeSessionId })
|
|
934
|
-
if (!res.ok && /超出允许范围/.test(res.error || '')) {
|
|
935
|
-
await new Promise(function (r) { setTimeout(r, 600) })
|
|
936
|
-
res = await rpc('read', { path: path, sessionId: activeSessionId })
|
|
937
|
-
}
|
|
938
|
-
var cur = findFileEntry(path)
|
|
939
|
-
if (!cur) return
|
|
940
|
-
cur.loading = false
|
|
941
|
-
if (!res || !res.ok) {
|
|
942
|
-
cur.error = (res && res.error) || '读取失败'
|
|
943
|
-
} else {
|
|
944
|
-
cur.error = ''
|
|
945
|
-
cur.loaded = true
|
|
946
|
-
cur.kind = res.kind
|
|
947
|
-
cur.size = res.size || 0
|
|
948
|
-
if (res.kind === 'text') { cur.content = res.content; cur.truncated = !!res.truncated }
|
|
949
|
-
else if (res.kind === 'image') { cur.dataUrl = 'data:' + res.mime + ';base64,' + res.data }
|
|
950
|
-
else if (res.kind === 'pdf') { cur.data = res.data }
|
|
951
|
-
}
|
|
952
|
-
setFiles({ list: state.files.list.slice(), active: path })
|
|
953
|
-
}
|
|
954
|
-
|
|
955
|
-
async function reloadFile(path) {
|
|
956
|
-
var entry = findFileEntry(path)
|
|
957
|
-
if (!entry) return
|
|
958
|
-
// 复位 loaded,让 openFileInTab 走重读路径而不是「已打开直接激活」。
|
|
959
|
-
entry.loaded = false
|
|
960
|
-
await openFileInTab(path)
|
|
961
|
-
}
|
|
962
|
-
|
|
963
|
-
function closeFileTab(path) {
|
|
964
|
-
var list = state.files.list
|
|
965
|
-
var idx = -1
|
|
966
|
-
for (var i = 0; i < list.length; i++) if (list[i].path === path) { idx = i; break }
|
|
967
|
-
if (idx < 0) return
|
|
968
|
-
var next = list.slice(0, idx).concat(list.slice(idx + 1))
|
|
969
|
-
var active = state.files.active
|
|
970
|
-
if (active === path) {
|
|
971
|
-
var neighbor = next[Math.min(idx, next.length - 1)]
|
|
972
|
-
active = neighbor ? neighbor.path : null
|
|
973
|
-
}
|
|
974
|
-
setFiles({ list: next, active: active })
|
|
975
|
-
}
|
|
976
|
-
|
|
977
|
-
function setActiveFile(path) {
|
|
978
|
-
setFiles({ active: path })
|
|
979
|
-
}
|
|
980
|
-
|
|
1003
|
+
// ▓▓ 区域一 · 资源管理器 · 🧅 葱(列表 UI)
|
|
1004
|
+
// NameInput / Row / buildMenuItems / Menu:行渲染、多选、右键菜单、重命名/新建。
|
|
1005
|
+
// ------------------------------------------------------------------
|
|
981
1006
|
function NameInput(props) {
|
|
982
1007
|
var initial = props.initial
|
|
983
1008
|
var selectBase = props.selectBase !== false
|
|
@@ -1071,7 +1096,7 @@ window.__ModuleLoader__.load({
|
|
|
1071
1096
|
}
|
|
1072
1097
|
if (isDir && node.open) {
|
|
1073
1098
|
if (node.loading) {
|
|
1074
|
-
children.push(create('div', { key: '__loading__', className: 'expl-muted', style: { paddingLeft: 8 + (depth + 1) * 16 } }, '
|
|
1099
|
+
children.push(create('div', { key: '__loading__', className: 'expl-muted', style: { paddingLeft: 8 + (depth + 1) * 16 } }, T('files.loading')))
|
|
1075
1100
|
} else {
|
|
1076
1101
|
;(node.children || []).forEach(function (c) { children.push(create(Row, { key: c.path, node: c, depth: depth + 1 })) })
|
|
1077
1102
|
}
|
|
@@ -1086,29 +1111,29 @@ window.__ModuleLoader__.load({
|
|
|
1086
1111
|
if (node) {
|
|
1087
1112
|
if (!multi) {
|
|
1088
1113
|
if (node.type !== 'directory') {
|
|
1089
|
-
items.push({ key: 'open-tab', label: '
|
|
1090
|
-
items.push({ key: 'open', label: '
|
|
1114
|
+
items.push({ key: 'open-tab', label: T('menu.preview'), onClick: function () { openFileInTab(node.path) } })
|
|
1115
|
+
items.push({ key: 'open', label: T('menu.open'), onClick: openSelection, separatorAfter: true })
|
|
1091
1116
|
} else {
|
|
1092
|
-
items.push({ key: 'open', label: '
|
|
1117
|
+
items.push({ key: 'open', label: T('menu.open'), onClick: openSelection, separatorAfter: true })
|
|
1093
1118
|
}
|
|
1094
|
-
items.push({ key: 'rename', label: '
|
|
1095
|
-
items.push({ key: 'trash', label: '
|
|
1119
|
+
items.push({ key: 'rename', label: T('menu.rename'), onClick: function () { startRename(node) } })
|
|
1120
|
+
items.push({ key: 'trash', label: T('menu.trash'), danger: true, onClick: trashSelection, separatorAfter: true })
|
|
1096
1121
|
} else {
|
|
1097
|
-
items.push({ key: 'trash', label: '
|
|
1122
|
+
items.push({ key: 'trash', label: T('menu.trashMulti', { n: state.selected.size }), danger: true, onClick: trashSelection, separatorAfter: true })
|
|
1098
1123
|
}
|
|
1099
1124
|
if (node.type === 'directory') {
|
|
1100
|
-
items.push({ key: 'newfile', label: '
|
|
1101
|
-
items.push({ key: 'newfolder', label: '
|
|
1125
|
+
items.push({ key: 'newfile', label: T('menu.newFile'), onClick: function () { startNew(node.path, false) } })
|
|
1126
|
+
items.push({ key: 'newfolder', label: T('menu.newFolder'), onClick: function () { startNew(node.path, true) }, separatorAfter: true })
|
|
1102
1127
|
}
|
|
1103
|
-
items.push({ key: 'copy', label: '
|
|
1128
|
+
items.push({ key: 'copy', label: T('menu.copyPath'), onClick: function () { copyPath(node) } })
|
|
1104
1129
|
if (node.type !== 'directory') {
|
|
1105
|
-
items.push({ key: 'download', label: '
|
|
1130
|
+
items.push({ key: 'download', label: T('menu.download'), onClick: function () { downloadFile(node.path) } })
|
|
1106
1131
|
}
|
|
1107
1132
|
} else {
|
|
1108
|
-
items.push({ key: 'newfile', label: '
|
|
1109
|
-
items.push({ key: 'newfolder', label: '
|
|
1110
|
-
items.push({ key: 'refresh', label: [create('span', { className: 'expl-menu-ico', dangerouslySetInnerHTML: { __html: ICON_REFRESH } }), '
|
|
1111
|
-
if (state.selected.size > 0) items.push({ key: 'none', label: '
|
|
1133
|
+
items.push({ key: 'newfile', label: T('menu.newFile'), onClick: function () { startNew(state.cwd, false) } })
|
|
1134
|
+
items.push({ key: 'newfolder', label: T('menu.newFolder'), onClick: function () { startNew(state.cwd, true) } })
|
|
1135
|
+
items.push({ key: 'refresh', label: [create('span', { className: 'expl-menu-ico', dangerouslySetInnerHTML: { __html: ICON_REFRESH } }), T('explorer.refresh')], onClick: refresh, separatorAfter: true })
|
|
1136
|
+
if (state.selected.size > 0) items.push({ key: 'none', label: T('menu.deselect'), onClick: function () { setState({ selected: new Set(), lastIndex: null }) } })
|
|
1112
1137
|
}
|
|
1113
1138
|
return items
|
|
1114
1139
|
}
|
|
@@ -1142,19 +1167,386 @@ window.__ModuleLoader__.load({
|
|
|
1142
1167
|
}
|
|
1143
1168
|
}, [])
|
|
1144
1169
|
|
|
1145
|
-
var items = buildMenuItems(menu.node)
|
|
1146
|
-
var menuChildren = []
|
|
1147
|
-
items.forEach(function (it) {
|
|
1148
|
-
menuChildren.push(create('button', {
|
|
1149
|
-
key: it.key,
|
|
1150
|
-
className: 'expl-menu-item' + (it.danger ? ' expl-danger' : ''),
|
|
1151
|
-
onClick: function () { setState({ menu: null }); it.onClick() },
|
|
1152
|
-
}, it.label))
|
|
1153
|
-
if (it.separatorAfter) menuChildren.push(create('div', { key: it.key + '-sep', className: 'expl-menu-sep' }))
|
|
1154
|
-
})
|
|
1155
|
-
return create('div', { ref: menuRef, className: 'expl-menu', style: { left: pos.x, top: pos.y } }, menuChildren)
|
|
1170
|
+
var items = buildMenuItems(menu.node)
|
|
1171
|
+
var menuChildren = []
|
|
1172
|
+
items.forEach(function (it) {
|
|
1173
|
+
menuChildren.push(create('button', {
|
|
1174
|
+
key: it.key,
|
|
1175
|
+
className: 'expl-menu-item' + (it.danger ? ' expl-danger' : ''),
|
|
1176
|
+
onClick: function () { setState({ menu: null }); it.onClick() },
|
|
1177
|
+
}, it.label))
|
|
1178
|
+
if (it.separatorAfter) menuChildren.push(create('div', { key: it.key + '-sep', className: 'expl-menu-sep' }))
|
|
1179
|
+
})
|
|
1180
|
+
return create('div', { ref: menuRef, className: 'expl-menu', style: { left: pos.x, top: pos.y } }, menuChildren)
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
// ------------------------------------------------------------------
|
|
1184
|
+
// ------------------------------------------------------------------
|
|
1185
|
+
// ▓▓ 区域一 · 资源管理器 · 🧅 葱(面板与入口)
|
|
1186
|
+
// Panel(右列 details 面板)/ HeaderAction(Session log 侧 📁)/ HeroAction(空白会话 📁)。
|
|
1187
|
+
// ------------------------------------------------------------------
|
|
1188
|
+
function Panel(props) {
|
|
1189
|
+
var s = useStore()
|
|
1190
|
+
var sessionId = props && props.sessionId
|
|
1191
|
+
// 客户端会话快照是本会话 cwd 的可信来源(与官方 DetailsPanel 同源,
|
|
1192
|
+
// 始终是当前会话所在项目目录,而非宿主启动时的根目录)。
|
|
1193
|
+
var useSessions = props && props.useSessions
|
|
1194
|
+
var liveCwd = useSessions ? useSessions(function (list) {
|
|
1195
|
+
return (sessionId && list.byId[sessionId]) ? list.byId[sessionId].cwd : undefined
|
|
1196
|
+
}) : undefined
|
|
1197
|
+
var cwdRef = React.useRef(liveCwd)
|
|
1198
|
+
cwdRef.current = liveCwd || cwdRef.current
|
|
1199
|
+
var panelRef = React.useRef(null)
|
|
1200
|
+
var frameRef = React.useRef(null)
|
|
1201
|
+
var widthRef = React.useRef(DETAILS_DEFAULT)
|
|
1202
|
+
var dragRef = React.useRef({ startX: 0, startW: 0, active: false })
|
|
1203
|
+
|
|
1204
|
+
React.useEffect(function () { trackSession(sessionId, cwdRef.current) }, [sessionId])
|
|
1205
|
+
|
|
1206
|
+
var setFrameWidth = React.useCallback(function (w) {
|
|
1207
|
+
var frame = frameRef.current
|
|
1208
|
+
if (!frame) return
|
|
1209
|
+
try {
|
|
1210
|
+
var gtc = frame.style.gridTemplateColumns
|
|
1211
|
+
if (!gtc) return
|
|
1212
|
+
var parts = gtc.trim().split(/\s+/)
|
|
1213
|
+
if (parts.length < 3) return
|
|
1214
|
+
parts[parts.length - 1] = Math.round(w) + 'px'
|
|
1215
|
+
frame.style.gridTemplateColumns = parts.join(' ')
|
|
1216
|
+
} catch (e) {}
|
|
1217
|
+
}, [])
|
|
1218
|
+
|
|
1219
|
+
React.useEffect(function () {
|
|
1220
|
+
var el = panelRef.current
|
|
1221
|
+
if (!el) return
|
|
1222
|
+
var mo = null
|
|
1223
|
+
var cancelled = false
|
|
1224
|
+
function attach() {
|
|
1225
|
+
var frame = findFrameEl(el)
|
|
1226
|
+
if (!frame || mo) return
|
|
1227
|
+
frameRef.current = frame
|
|
1228
|
+
function sync() {
|
|
1229
|
+
try {
|
|
1230
|
+
var gtc = frame.style.gridTemplateColumns
|
|
1231
|
+
if (!gtc) return
|
|
1232
|
+
var parts = gtc.trim().split(/\s+/)
|
|
1233
|
+
if (parts.length < 3) return
|
|
1234
|
+
var last = parseFloat(parts[parts.length - 1])
|
|
1235
|
+
if (isNaN(last) || last <= 1) { setState({ open: false }); return }
|
|
1236
|
+
setState({ open: true })
|
|
1237
|
+
if (Math.round(last) !== Math.round(widthRef.current)) {
|
|
1238
|
+
parts[parts.length - 1] = Math.round(widthRef.current) + 'px'
|
|
1239
|
+
frame.style.gridTemplateColumns = parts.join(' ')
|
|
1240
|
+
}
|
|
1241
|
+
} catch (e) {}
|
|
1242
|
+
}
|
|
1243
|
+
mo = new MutationObserver(sync)
|
|
1244
|
+
mo.observe(frame, { attributes: true, attributeFilter: ['style'] })
|
|
1245
|
+
sync()
|
|
1246
|
+
}
|
|
1247
|
+
attach()
|
|
1248
|
+
if (!mo) {
|
|
1249
|
+
var tries = 0
|
|
1250
|
+
var t = setInterval(function () {
|
|
1251
|
+
if (cancelled || mo) { clearInterval(t); return }
|
|
1252
|
+
attach()
|
|
1253
|
+
if (++tries > 40) clearInterval(t)
|
|
1254
|
+
}, 100)
|
|
1255
|
+
}
|
|
1256
|
+
return function () { cancelled = true; if (mo) mo.disconnect() }
|
|
1257
|
+
}, [])
|
|
1258
|
+
|
|
1259
|
+
function onResizeDown(e) {
|
|
1260
|
+
e.preventDefault()
|
|
1261
|
+
try { e.currentTarget.setPointerCapture(e.pointerId) } catch (err) {}
|
|
1262
|
+
dragRef.current = { startX: e.clientX, startW: widthRef.current, active: true }
|
|
1263
|
+
if (frameRef.current) { try { frameRef.current.style.transition = 'none' } catch (e) {} }
|
|
1264
|
+
}
|
|
1265
|
+
function onResizeMove(e) {
|
|
1266
|
+
if (!dragRef.current.active) return
|
|
1267
|
+
var w = clampW(dragRef.current.startW - (e.clientX - dragRef.current.startX))
|
|
1268
|
+
widthRef.current = w
|
|
1269
|
+
setFrameWidth(w)
|
|
1270
|
+
}
|
|
1271
|
+
function onResizeUp(e) {
|
|
1272
|
+
dragRef.current.active = false
|
|
1273
|
+
try { e.currentTarget.releasePointerCapture(e.pointerId) } catch (err) {}
|
|
1274
|
+
if (frameRef.current) { try { frameRef.current.style.transition = '' } catch (e) {} }
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
visibleRows.length = 0
|
|
1278
|
+
|
|
1279
|
+
var bodyChildren = []
|
|
1280
|
+
if (state.newItem && state.newItem.parent === state.cwd) {
|
|
1281
|
+
bodyChildren.push(create('div', { key: '__new__', className: 'expl-row' },
|
|
1282
|
+
create('div', { className: 'expl-row-main', style: { paddingLeft: 8 } },
|
|
1283
|
+
create('span', { className: 'expl-icon' }, state.newItem.isDir ? '📁' : '📄'),
|
|
1284
|
+
create(NameInput, {
|
|
1285
|
+
initial: '',
|
|
1286
|
+
selectBase: false,
|
|
1287
|
+
onCommit: function (name) { commitNew(state.newItem.parent, name, state.newItem.isDir) },
|
|
1288
|
+
onCancel: function () { setState({ newItem: null }) },
|
|
1289
|
+
}),
|
|
1290
|
+
),
|
|
1291
|
+
))
|
|
1292
|
+
}
|
|
1293
|
+
if (s.tree.length === 0 && !(state.newItem && state.newItem.parent === state.cwd)) {
|
|
1294
|
+
bodyChildren.push(create('div', { key: '__empty__', className: 'expl-muted' }, T('explorer.emptyDir')))
|
|
1295
|
+
} else {
|
|
1296
|
+
s.tree.forEach(function (node) { bodyChildren.push(create(Row, { key: node.path, node: node, depth: 0 })) })
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
var container = create('div', {
|
|
1300
|
+
className: 'expl-body' + (state.dropTarget === state.cwd ? ' drop-target' : ''),
|
|
1301
|
+
onClick: onBlankClick,
|
|
1302
|
+
onContextMenu: onBlankContext,
|
|
1303
|
+
onDragOver: function (e) { e.preventDefault(); e.stopPropagation(); if (state.dropTarget !== state.cwd) setState({ dropTarget: state.cwd }) },
|
|
1304
|
+
onDragLeave: function (e) { if (state.dropTarget === state.cwd) setState({ dropTarget: null }) },
|
|
1305
|
+
onDrop: onBodyDrop,
|
|
1306
|
+
}, bodyChildren)
|
|
1307
|
+
|
|
1308
|
+
return create('div', { ref: panelRef, className: 'expl-panel' },
|
|
1309
|
+
create('div', { className: 'expl-resize', onPointerDown: onResizeDown, onPointerMove: onResizeMove, onPointerUp: onResizeUp, onPointerCancel: onResizeUp }),
|
|
1310
|
+
create('div', { className: 'expl-head' },
|
|
1311
|
+
create('span', { className: 'expl-title' }, T('explorer.title')),
|
|
1312
|
+
create('div', { className: 'expl-head-btns' },
|
|
1313
|
+
create('button', {
|
|
1314
|
+
className: 'expl-btn',
|
|
1315
|
+
onClick: function () { refreshAll() },
|
|
1316
|
+
title: T('explorer.refreshTitle'),
|
|
1317
|
+
'aria-label': T('explorer.refresh'),
|
|
1318
|
+
}, create('span', { className: 'expl-icon', dangerouslySetInnerHTML: { __html: ICON_REFRESH } })),
|
|
1319
|
+
create('button', {
|
|
1320
|
+
className: 'expl-btn',
|
|
1321
|
+
onClick: function () { if (uploadInputEl) uploadInputEl.click() },
|
|
1322
|
+
title: T('explorer.upload'),
|
|
1323
|
+
'aria-label': T('explorer.upload'),
|
|
1324
|
+
}, '⬆'),
|
|
1325
|
+
create('button', {
|
|
1326
|
+
className: 'expl-btn',
|
|
1327
|
+
style: { fontSize: '18px' },
|
|
1328
|
+
onClick: function (event) {
|
|
1329
|
+
try { layout.closeDetails() } catch (e) {}
|
|
1330
|
+
setHeroDetailsWidth(event.currentTarget, 0)
|
|
1331
|
+
},
|
|
1332
|
+
title: T('explorer.close'),
|
|
1333
|
+
'aria-label': T('explorer.closePanel'),
|
|
1334
|
+
}, '✕'),
|
|
1335
|
+
),
|
|
1336
|
+
),
|
|
1337
|
+
create('div', { className: 'expl-path', title: s.cwd || '' }, s.cwd || '…'),
|
|
1338
|
+
(s.uploads && s.uploads.length) ? create('div', { className: 'expl-uploads' },
|
|
1339
|
+
s.uploads.map(function (u, i) {
|
|
1340
|
+
var pct = u.done ? null : (u.total ? Math.round((u.loaded / u.total) * 100) : 0)
|
|
1341
|
+
return create('div', { key: i, className: 'expl-upload-row' + (u.done ? ' expl-upload-done' : '') },
|
|
1342
|
+
create('span', { className: 'expl-upload-glyph' }, u.done ? '✓' : (u.down ? '⬇' : '⬆')),
|
|
1343
|
+
create('span', { className: 'expl-upload-text' },
|
|
1344
|
+
u.done ? u.name
|
|
1345
|
+
: (u.down ? T('explorer.downloading') : T('explorer.uploading')) + (u.totalFiles > 1 ? '(' + u.idx + '/' + u.totalFiles + ') ' : '') + u.name,
|
|
1346
|
+
),
|
|
1347
|
+
!u.done && u.total ? create('span', { className: 'expl-upload-pct' }, pct + '%') : null,
|
|
1348
|
+
)
|
|
1349
|
+
}),
|
|
1350
|
+
) : null,
|
|
1351
|
+
create('input', { type: 'file', multiple: true, style: { display: 'none' }, ref: function (el) { uploadInputEl = el }, onChange: onUploadPicker }),
|
|
1352
|
+
s.error ? create('div', { className: 'expl-error' }, s.error) : null,
|
|
1353
|
+
s.notice ? create('div', { className: 'expl-notice' }, s.notice) : null,
|
|
1354
|
+
s.selected.size > 1
|
|
1355
|
+
? create('div', { className: 'expl-bulk' }, T('explorer.bulkSelected', { n: s.selected.size }))
|
|
1356
|
+
: null,
|
|
1357
|
+
container,
|
|
1358
|
+
s.menu ? create('div', { className: 'expl-menu-mask', onMouseDown: function () { setState({ menu: null }) }, onContextMenu: function (e) { e.preventDefault(); setState({ menu: null }) } }) : null,
|
|
1359
|
+
s.menu ? create(Menu, { menu: s.menu }) : null,
|
|
1360
|
+
)
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
function HeaderAction(props) {
|
|
1364
|
+
var s = useStore()
|
|
1365
|
+
var sessionId = props && props.sessionId
|
|
1366
|
+
var useSessions = props && props.useSessions
|
|
1367
|
+
var liveCwd = useSessions ? useSessions(function (list) {
|
|
1368
|
+
return (sessionId && list.byId[sessionId]) ? list.byId[sessionId].cwd : undefined
|
|
1369
|
+
}) : undefined
|
|
1370
|
+
var cwdRef = React.useRef(liveCwd)
|
|
1371
|
+
cwdRef.current = liveCwd || cwdRef.current
|
|
1372
|
+
React.useEffect(function () { trackSession(sessionId, cwdRef.current) }, [sessionId])
|
|
1373
|
+
return create('button', {
|
|
1374
|
+
type: 'button',
|
|
1375
|
+
className: 'expl-toggle expl-tool' + (s.open ? ' expl-active' : ''),
|
|
1376
|
+
onClick: function () { try { if (s.open) layout.closeDetails(); else layout.openDetails() } catch (e) {} },
|
|
1377
|
+
title: T('explorer.label'),
|
|
1378
|
+
'aria-label': T('explorer.label'),
|
|
1379
|
+
}, '📁')
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
function HeroAction(props) {
|
|
1383
|
+
var s = useStore()
|
|
1384
|
+
var session = props && props.session
|
|
1385
|
+
var useSessions = props && props.useSessions
|
|
1386
|
+
var liveCwd = useSessions ? useSessions(function (list) {
|
|
1387
|
+
return (props.sessionId && list.byId[props.sessionId]) ? list.byId[props.sessionId].cwd : undefined
|
|
1388
|
+
}) : undefined
|
|
1389
|
+
var blank = !!(session && session.blank && session.composerPhase === 'blank')
|
|
1390
|
+
React.useEffect(function () {
|
|
1391
|
+
if (blank) trackSession(props.sessionId, liveCwd)
|
|
1392
|
+
}, [blank, props.sessionId])
|
|
1393
|
+
if (!blank) return null
|
|
1394
|
+
return create('div', { className: 'expl-hero-dock' },
|
|
1395
|
+
create('button', {
|
|
1396
|
+
type: 'button',
|
|
1397
|
+
className: 'expl-toggle expl-tool' + (s.open ? ' expl-active' : ''),
|
|
1398
|
+
onClick: function (event) {
|
|
1399
|
+
if (s.open) {
|
|
1400
|
+
try { layout.closeDetails() } catch (e) {}
|
|
1401
|
+
setHeroDetailsWidth(event.currentTarget, 0)
|
|
1402
|
+
} else if (!setHeroDetailsWidth(event.currentTarget, DETAILS_DEFAULT)) {
|
|
1403
|
+
try { layout.openDetails() } catch (e) {}
|
|
1404
|
+
}
|
|
1405
|
+
},
|
|
1406
|
+
title: T('explorer.label'),
|
|
1407
|
+
'aria-label': T('explorer.label'),
|
|
1408
|
+
}, '📁'))
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
// ------------------------------------------------------------------
|
|
1412
|
+
// ------------------------------------------------------------------
|
|
1413
|
+
// ▓▓ 区域二 · 会话内预览 · 🫚 姜(files tab 状态)
|
|
1414
|
+
// files tab 状态机:打开/重载/关闭、FIFO(≤5)、会话切换按 cwd 裁剪。
|
|
1415
|
+
// ------------------------------------------------------------------
|
|
1416
|
+
// 文件标签页:右键/双击「预览」把文件以只读预览展示进会话区的「预览」
|
|
1417
|
+
// tab(原生 conversation.view 槽位,与 对话/轨迹 同级)。
|
|
1418
|
+
// markdown/html/json/csv 有专属渲染器;图片预览;其余按纯文本。
|
|
1419
|
+
// 状态存模块级 store,切走 tab 再切回来内容不丢。
|
|
1420
|
+
// ------------------------------------------------------------------
|
|
1421
|
+
var FILES_TAB_LABEL = function () { return T('files.tab') }
|
|
1422
|
+
|
|
1423
|
+
/** 当前活跃会话:list/read/download 携带给宿主,供围栏直查该会话 cwd。 */
|
|
1424
|
+
var activeSessionId = null
|
|
1425
|
+
|
|
1426
|
+
/** 同时打开的预览 tab 上限:超过后挤掉最早打开的(FIFO)。 */
|
|
1427
|
+
var FILES_MAX_OPEN = 5
|
|
1428
|
+
|
|
1429
|
+
/** path 是否落在 base 目录子树内(base 为根或空时视为全量放行)。 */
|
|
1430
|
+
function withinScope(path, base) {
|
|
1431
|
+
if (!base || base === '/') return true
|
|
1432
|
+
return path === base || path.indexOf(base + '/') === 0
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
/**
|
|
1436
|
+
* 会话切换时按新 cwd 裁剪预览列表:范围外的 tab 自动关闭。
|
|
1437
|
+
* 若当前激活文件被裁掉,激活项顺移到剩余最后一个。
|
|
1438
|
+
* 列表无变化时返回原引用,避免触发多余渲染。
|
|
1439
|
+
*/
|
|
1440
|
+
function pruneFilesToScope(files, cwd) {
|
|
1441
|
+
var kept = files.list.filter(function (f) { return withinScope(f.path, cwd) })
|
|
1442
|
+
try { window.__DFV_PRUNE = (window.__DFV_PRUNE || []) ; window.__DFV_PRUNE.push({ cwd: cwd, before: files.list.length, after: kept.length }) } catch (_) {}
|
|
1443
|
+
if (kept.length === files.list.length) return files
|
|
1444
|
+
var activeKept = kept.some(function (f) { return f.path === files.active })
|
|
1445
|
+
var active = files.active
|
|
1446
|
+
if (!activeKept) active = kept.length ? kept[kept.length - 1].path : null
|
|
1447
|
+
return { list: kept, active: active }
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
function setFiles(patch) {
|
|
1451
|
+
setState({ files: Object.assign({}, state.files, patch) })
|
|
1452
|
+
try {
|
|
1453
|
+
var f = state.files
|
|
1454
|
+
window.__DFV_DBG = { n: f.list.length, active: f.active, ops: ((window.__DFV_DBG && window.__DFV_DBG.ops) || 0) + 1 }
|
|
1455
|
+
} catch (_) {}
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
function findFileEntry(path) {
|
|
1459
|
+
for (var i = 0; i < state.files.list.length; i++) {
|
|
1460
|
+
if (state.files.list[i].path === path) return state.files.list[i]
|
|
1461
|
+
}
|
|
1462
|
+
return null
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
/** 点击原生 header 的「预览」tab,把 view ring 切到预览视图。 */
|
|
1466
|
+
function activateFilesView() {
|
|
1467
|
+
try {
|
|
1468
|
+
var tabs = document.querySelectorAll('[role="tab"]')
|
|
1469
|
+
for (var i = 0; i < tabs.length; i++) {
|
|
1470
|
+
if (tabs[i].textContent === FILES_TAB_LABEL()) { tabs[i].click(); return true }
|
|
1471
|
+
}
|
|
1472
|
+
} catch (err) {}
|
|
1473
|
+
return false
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
async function openFileInTab(path) {
|
|
1477
|
+
setState({ menu: null })
|
|
1478
|
+
var entry = findFileEntry(path)
|
|
1479
|
+
if (!entry) {
|
|
1480
|
+
entry = {
|
|
1481
|
+
path: path, name: baseName(path),
|
|
1482
|
+
loading: true, loaded: false, error: '',
|
|
1483
|
+
kind: null, content: '', dataUrl: '', data: '',
|
|
1484
|
+
truncated: false, size: 0,
|
|
1485
|
+
}
|
|
1486
|
+
// FIFO:最多同时 5 个预览,新开的挤掉最早打开的
|
|
1487
|
+
var list = state.files.list.slice()
|
|
1488
|
+
while (list.length >= FILES_MAX_OPEN) list.shift()
|
|
1489
|
+
list.push(entry)
|
|
1490
|
+
setFiles({ list: list, active: path })
|
|
1491
|
+
} else if (entry.loaded) {
|
|
1492
|
+
setFiles({ active: path })
|
|
1493
|
+
activateFilesView()
|
|
1494
|
+
return
|
|
1495
|
+
} else {
|
|
1496
|
+
setFiles({ active: path })
|
|
1497
|
+
}
|
|
1498
|
+
activateFilesView()
|
|
1499
|
+
var res = await rpc('read', { path: path, sessionId: activeSessionId })
|
|
1500
|
+
if (!res.ok && /超出允许范围/.test(res.error || '')) {
|
|
1501
|
+
await new Promise(function (r) { setTimeout(r, 600) })
|
|
1502
|
+
res = await rpc('read', { path: path, sessionId: activeSessionId })
|
|
1503
|
+
}
|
|
1504
|
+
var cur = findFileEntry(path)
|
|
1505
|
+
if (!cur) return
|
|
1506
|
+
cur.loading = false
|
|
1507
|
+
if (!res || !res.ok) {
|
|
1508
|
+
cur.error = (res && res.error) || T('explorer.readFail')
|
|
1509
|
+
} else {
|
|
1510
|
+
cur.error = ''
|
|
1511
|
+
cur.loaded = true
|
|
1512
|
+
cur.kind = res.kind
|
|
1513
|
+
cur.size = res.size || 0
|
|
1514
|
+
if (res.kind === 'text') { cur.content = res.content; cur.truncated = !!res.truncated }
|
|
1515
|
+
else if (res.kind === 'image') { cur.dataUrl = 'data:' + res.mime + ';base64,' + res.data }
|
|
1516
|
+
else if (res.kind === 'pdf') { cur.data = res.data }
|
|
1517
|
+
}
|
|
1518
|
+
setFiles({ list: state.files.list.slice(), active: path })
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
async function reloadFile(path) {
|
|
1522
|
+
var entry = findFileEntry(path)
|
|
1523
|
+
if (!entry) return
|
|
1524
|
+
// 复位 loaded,让 openFileInTab 走重读路径而不是「已打开直接激活」。
|
|
1525
|
+
entry.loaded = false
|
|
1526
|
+
await openFileInTab(path)
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
function closeFileTab(path) {
|
|
1530
|
+
var list = state.files.list
|
|
1531
|
+
var idx = -1
|
|
1532
|
+
for (var i = 0; i < list.length; i++) if (list[i].path === path) { idx = i; break }
|
|
1533
|
+
if (idx < 0) return
|
|
1534
|
+
var next = list.slice(0, idx).concat(list.slice(idx + 1))
|
|
1535
|
+
var active = state.files.active
|
|
1536
|
+
if (active === path) {
|
|
1537
|
+
var neighbor = next[Math.min(idx, next.length - 1)]
|
|
1538
|
+
active = neighbor ? neighbor.path : null
|
|
1539
|
+
}
|
|
1540
|
+
setFiles({ list: next, active: active })
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
function setActiveFile(path) {
|
|
1544
|
+
setFiles({ active: path })
|
|
1156
1545
|
}
|
|
1157
1546
|
|
|
1547
|
+
// ------------------------------------------------------------------
|
|
1548
|
+
// ▓▓ 区域二 · 会话内预览 · 🫚 姜(渲染器)
|
|
1549
|
+
// 按扩展名分派 md/html/pdf/notebook/json/csv + 图片/纯文本兜底 + FileContent + FilesView 子标签条。
|
|
1158
1550
|
// ------------------------------------------------------------------
|
|
1159
1551
|
// 文件视图(conversation.view 'dsh-soup-files'):
|
|
1160
1552
|
// 子 tab 条 + 只读预览区(markdown/html/json/csv 渲染 / 图片预览 / 二进制提示)。
|
|
@@ -1306,7 +1698,7 @@ window.__ModuleLoader__.load({
|
|
|
1306
1698
|
className: 'dfv-frame',
|
|
1307
1699
|
sandbox: 'allow-scripts allow-popups allow-forms allow-modals',
|
|
1308
1700
|
referrerPolicy: 'no-referrer',
|
|
1309
|
-
title: props.title || '
|
|
1701
|
+
title: props.title || T('files.htmlFrame'),
|
|
1310
1702
|
srcDoc: props.text,
|
|
1311
1703
|
})
|
|
1312
1704
|
}
|
|
@@ -1335,14 +1727,14 @@ window.__ModuleLoader__.load({
|
|
|
1335
1727
|
if (made) URL.revokeObjectURL(made)
|
|
1336
1728
|
}
|
|
1337
1729
|
}, [props.data])
|
|
1338
|
-
if (state.error) return create('div', { className: 'dfv-error' }, '
|
|
1339
|
-
if (!state.url) return create('div', { className: 'dfv-empty' }, '
|
|
1730
|
+
if (state.error) return create('div', { className: 'dfv-error' }, T('files.pdfFail', { reason: state.error }))
|
|
1731
|
+
if (!state.url) return create('div', { className: 'dfv-empty' }, T('files.pdfLoading'))
|
|
1340
1732
|
return create('object', {
|
|
1341
1733
|
className: 'dfv-frame dfv-pdf',
|
|
1342
1734
|
type: 'application/pdf',
|
|
1343
1735
|
data: state.url,
|
|
1344
|
-
'aria-label': '
|
|
1345
|
-
}, '
|
|
1736
|
+
'aria-label': T('files.pdfFrame'),
|
|
1737
|
+
}, T('files.pdfUnsupported'))
|
|
1346
1738
|
}
|
|
1347
1739
|
|
|
1348
1740
|
// ------------------------------------------------------------------
|
|
@@ -1395,7 +1787,7 @@ window.__ModuleLoader__.load({
|
|
|
1395
1787
|
return create('img', { className: 'dfv-nb-img', src: 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(nbSource(out.data[mime])))), alt: 'notebook output svg' })
|
|
1396
1788
|
}
|
|
1397
1789
|
if (mime === 'text/html') {
|
|
1398
|
-
return create('iframe', { className: 'dfv-nb-html', sandbox: '', title: '
|
|
1790
|
+
return create('iframe', { className: 'dfv-nb-html', sandbox: '', title: T('files.nbHtmlOut'), srcDoc: nbSource(out.data[mime]) })
|
|
1399
1791
|
}
|
|
1400
1792
|
if (mime === 'text/markdown' && MarkdownText) {
|
|
1401
1793
|
return create('div', { className: 'dfv-md-wrap dfv-nb-md' }, create(MarkdownText, { text: nbSource(out.data[mime]) }))
|
|
@@ -1411,7 +1803,7 @@ window.__ModuleLoader__.load({
|
|
|
1411
1803
|
try { nb = JSON.parse(props.text) } catch (err) { nb = undefined }
|
|
1412
1804
|
if (nb === undefined || typeof nb !== 'object' || nb === null) {
|
|
1413
1805
|
return create(React.Fragment, null,
|
|
1414
|
-
create('div', { className: 'dfv-cap' }, '
|
|
1806
|
+
create('div', { className: 'dfv-cap' }, T('files.nbFail')),
|
|
1415
1807
|
create('pre', { className: 'dfv-code' }, props.text))
|
|
1416
1808
|
}
|
|
1417
1809
|
var cells = Array.isArray(nb.cells) ? nb.cells : []
|
|
@@ -1458,8 +1850,8 @@ window.__ModuleLoader__.load({
|
|
|
1458
1850
|
|
|
1459
1851
|
return create('div', { className: 'dfv-nb' },
|
|
1460
1852
|
create('div', { className: 'dfv-cap' },
|
|
1461
|
-
'Jupyter Notebook' + (kernelName ? ' · ' + kernelName : '') + ' · ' + cells.length
|
|
1462
|
-
cellEls.length ? cellEls : create('div', { className: 'dfv-muted' }, '
|
|
1853
|
+
'Jupyter Notebook' + (kernelName ? ' · ' + kernelName : '') + ' · ' + T('files.nbCells', { n: cells.length })),
|
|
1854
|
+
cellEls.length ? cellEls : create('div', { className: 'dfv-muted' }, T('files.nbEmpty')))
|
|
1463
1855
|
}
|
|
1464
1856
|
|
|
1465
1857
|
function JsonPreview(props) {
|
|
@@ -1467,7 +1859,7 @@ window.__ModuleLoader__.load({
|
|
|
1467
1859
|
try { JSON.parse(props.text) } catch (err) { failed = true }
|
|
1468
1860
|
if (failed) {
|
|
1469
1861
|
return create(React.Fragment, null,
|
|
1470
|
-
create('div', { className: 'dfv-cap' }, '
|
|
1862
|
+
create('div', { className: 'dfv-cap' }, T('files.jsonFail')),
|
|
1471
1863
|
create('pre', { className: 'dfv-code' }, props.text))
|
|
1472
1864
|
}
|
|
1473
1865
|
return create('pre', { className: 'dfv-json dfv-code' }, jsonColorNodes(JSON.stringify(JSON.parse(props.text), null, 2)))
|
|
@@ -1484,7 +1876,7 @@ window.__ModuleLoader__.load({
|
|
|
1484
1876
|
cols = Math.max(cols, Math.min(rows[r].length, CSV_MAX_COLS))
|
|
1485
1877
|
}
|
|
1486
1878
|
var caps = []
|
|
1487
|
-
if (totalRows > CSV_MAX_ROWS + 1) caps.push('
|
|
1879
|
+
if (totalRows > CSV_MAX_ROWS + 1) caps.push(T('files.csvTruncated', { total: totalRows, n: CSV_MAX_ROWS }))
|
|
1488
1880
|
var trs = []
|
|
1489
1881
|
for (var ri = 1; ri <= Math.min(totalRows - 1, CSV_MAX_ROWS); ri++) {
|
|
1490
1882
|
var cells = rows[ri]
|
|
@@ -1520,18 +1912,18 @@ window.__ModuleLoader__.load({
|
|
|
1520
1912
|
function FileContent(props) {
|
|
1521
1913
|
var entry = props.entry
|
|
1522
1914
|
if (entry.loading) {
|
|
1523
|
-
return create('div', { className: 'dfv-empty' }, '
|
|
1915
|
+
return create('div', { className: 'dfv-empty' }, T('files.loading'))
|
|
1524
1916
|
}
|
|
1525
1917
|
if (entry.error) {
|
|
1526
1918
|
return create('div', { className: 'dfv-error' }, entry.error)
|
|
1527
1919
|
}
|
|
1528
1920
|
if (entry.kind === 'binary') {
|
|
1529
1921
|
return create('div', { className: 'dfv-muted' },
|
|
1530
|
-
'
|
|
1922
|
+
T('files.binary', { size: formatBytes(entry.size) }))
|
|
1531
1923
|
}
|
|
1532
1924
|
if (entry.kind === 'image-too-large') {
|
|
1533
1925
|
return create('div', { className: 'dfv-muted' },
|
|
1534
|
-
'
|
|
1926
|
+
T('files.imageTooLarge', { size: formatBytes(entry.size), limit: formatBytes(entry.limit) }))
|
|
1535
1927
|
}
|
|
1536
1928
|
if (entry.kind === 'image') {
|
|
1537
1929
|
return create('div', { className: 'dfv-image-wrap' },
|
|
@@ -1539,7 +1931,7 @@ window.__ModuleLoader__.load({
|
|
|
1539
1931
|
}
|
|
1540
1932
|
if (entry.kind === 'pdf-too-large') {
|
|
1541
1933
|
return create('div', { className: 'dfv-muted' },
|
|
1542
|
-
'
|
|
1934
|
+
T('files.pdfTooLarge', { size: formatBytes(entry.size), limit: formatBytes(entry.limit) }))
|
|
1543
1935
|
}
|
|
1544
1936
|
if (entry.kind === 'pdf') {
|
|
1545
1937
|
return create(PdfPreview, { data: entry.data })
|
|
@@ -1565,15 +1957,51 @@ window.__ModuleLoader__.load({
|
|
|
1565
1957
|
}
|
|
1566
1958
|
return create(React.Fragment, null,
|
|
1567
1959
|
entry.truncated ? create('div', { className: 'dfv-cap' },
|
|
1568
|
-
'
|
|
1960
|
+
T('files.truncated')) : null,
|
|
1569
1961
|
view,
|
|
1570
1962
|
)
|
|
1571
1963
|
}
|
|
1572
1964
|
|
|
1965
|
+
/**
|
|
1966
|
+
* 纯阅读模式标记:在会话根(含 composerSeat 的最近祖先,即 DSH
|
|
1967
|
+
* ConversationRoot 的 root 节点)上打 data-dsh-soup-preview,由上面
|
|
1968
|
+
* 注入的 CSS 隐藏输入区与轮次统计行。DSH 重渲染可能重建该节点——
|
|
1969
|
+
* MutationObserver 在 FilesView 存活期间持续确保标记存在。
|
|
1970
|
+
*/
|
|
1971
|
+
function usePreviewReadingMode(rootRef) {
|
|
1972
|
+
React.useEffect(function () {
|
|
1973
|
+
var observer = null
|
|
1974
|
+
var marked = null
|
|
1975
|
+
function apply() {
|
|
1976
|
+
var el = rootRef.current
|
|
1977
|
+
if (!el) return
|
|
1978
|
+
if (marked && marked.contains(el) && marked.getAttribute('data-dsh-soup-preview') === '1') return
|
|
1979
|
+
var host = el.parentElement
|
|
1980
|
+
while (host) {
|
|
1981
|
+
if (host.querySelector && host.querySelector('.wSkVaW_composerSeat')) break
|
|
1982
|
+
host = host.parentElement
|
|
1983
|
+
}
|
|
1984
|
+
if (!host) return
|
|
1985
|
+
host.setAttribute('data-dsh-soup-preview', '1')
|
|
1986
|
+
marked = host
|
|
1987
|
+
}
|
|
1988
|
+
apply()
|
|
1989
|
+
observer = new MutationObserver(function () { apply() })
|
|
1990
|
+
observer.observe(document.body, { childList: true, subtree: true })
|
|
1991
|
+
return function () {
|
|
1992
|
+
if (observer) observer.disconnect()
|
|
1993
|
+
if (marked) marked.removeAttribute('data-dsh-soup-preview')
|
|
1994
|
+
marked = null
|
|
1995
|
+
}
|
|
1996
|
+
}, [])
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1573
1999
|
function FilesView(props) {
|
|
1574
2000
|
var s = useStore()
|
|
1575
2001
|
var files = s.files
|
|
1576
2002
|
var active = files.active ? findFileEntry(files.active) : null
|
|
2003
|
+
var rootRef = React.useRef(null)
|
|
2004
|
+
usePreviewReadingMode(rootRef)
|
|
1577
2005
|
|
|
1578
2006
|
var tabs = files.list.map(function (entry) {
|
|
1579
2007
|
return create('button', {
|
|
@@ -1587,7 +2015,7 @@ window.__ModuleLoader__.load({
|
|
|
1587
2015
|
create('span', {
|
|
1588
2016
|
className: 'dfv-close',
|
|
1589
2017
|
role: 'button',
|
|
1590
|
-
title: '
|
|
2018
|
+
title: T('explorer.close'),
|
|
1591
2019
|
onClick: function (e) { e.stopPropagation(); closeFileTab(entry.path) },
|
|
1592
2020
|
}, '✕'),
|
|
1593
2021
|
)
|
|
@@ -1597,7 +2025,7 @@ window.__ModuleLoader__.load({
|
|
|
1597
2025
|
if (!active) {
|
|
1598
2026
|
body = create('div', { className: 'dfv-empty' },
|
|
1599
2027
|
create('span', { style: { fontSize: 22 } }, '📄'),
|
|
1600
|
-
'
|
|
2028
|
+
T('files.emptyHint'),
|
|
1601
2029
|
)
|
|
1602
2030
|
} else {
|
|
1603
2031
|
body = create('div', { className: 'dfv-body' },
|
|
@@ -1607,18 +2035,18 @@ window.__ModuleLoader__.load({
|
|
|
1607
2035
|
active.loaded ? create('button', {
|
|
1608
2036
|
type: 'button',
|
|
1609
2037
|
className: 'dfv-btn',
|
|
1610
|
-
title: '
|
|
2038
|
+
title: T('files.reloadTitle'),
|
|
1611
2039
|
onClick: function () { reloadFile(active.path) },
|
|
1612
2040
|
},
|
|
1613
2041
|
create('span', { className: 'expl-icon', dangerouslySetInnerHTML: { __html: ICON_REFRESH } }),
|
|
1614
|
-
'
|
|
2042
|
+
T('files.reload'),
|
|
1615
2043
|
) : null,
|
|
1616
2044
|
),
|
|
1617
2045
|
create(FileContent, { key: active.path, entry: active }),
|
|
1618
2046
|
)
|
|
1619
2047
|
}
|
|
1620
2048
|
|
|
1621
|
-
return create('div', { className: 'dfv-root' },
|
|
2049
|
+
return create('div', { className: 'dfv-root', ref: rootRef },
|
|
1622
2050
|
files.list.length > 0 ? create('div', { className: 'dfv-tabbar' }, tabs) : null,
|
|
1623
2051
|
body,
|
|
1624
2052
|
)
|
|
@@ -1652,229 +2080,9 @@ window.__ModuleLoader__.load({
|
|
|
1652
2080
|
}
|
|
1653
2081
|
}
|
|
1654
2082
|
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
// 客户端会话快照是本会话 cwd 的可信来源(与官方 DetailsPanel 同源,
|
|
1659
|
-
// 始终是当前会话所在项目目录,而非宿主启动时的根目录)。
|
|
1660
|
-
var useSessions = props && props.useSessions
|
|
1661
|
-
var liveCwd = useSessions ? useSessions(function (list) {
|
|
1662
|
-
return (sessionId && list.byId[sessionId]) ? list.byId[sessionId].cwd : undefined
|
|
1663
|
-
}) : undefined
|
|
1664
|
-
var cwdRef = React.useRef(liveCwd)
|
|
1665
|
-
cwdRef.current = liveCwd || cwdRef.current
|
|
1666
|
-
var panelRef = React.useRef(null)
|
|
1667
|
-
var frameRef = React.useRef(null)
|
|
1668
|
-
var widthRef = React.useRef(DETAILS_DEFAULT)
|
|
1669
|
-
var dragRef = React.useRef({ startX: 0, startW: 0, active: false })
|
|
1670
|
-
|
|
1671
|
-
React.useEffect(function () { trackSession(sessionId, cwdRef.current) }, [sessionId])
|
|
1672
|
-
|
|
1673
|
-
var setFrameWidth = React.useCallback(function (w) {
|
|
1674
|
-
var frame = frameRef.current
|
|
1675
|
-
if (!frame) return
|
|
1676
|
-
try {
|
|
1677
|
-
var gtc = frame.style.gridTemplateColumns
|
|
1678
|
-
if (!gtc) return
|
|
1679
|
-
var parts = gtc.trim().split(/\s+/)
|
|
1680
|
-
if (parts.length < 3) return
|
|
1681
|
-
parts[parts.length - 1] = Math.round(w) + 'px'
|
|
1682
|
-
frame.style.gridTemplateColumns = parts.join(' ')
|
|
1683
|
-
} catch (e) {}
|
|
1684
|
-
}, [])
|
|
1685
|
-
|
|
1686
|
-
React.useEffect(function () {
|
|
1687
|
-
var el = panelRef.current
|
|
1688
|
-
if (!el) return
|
|
1689
|
-
var mo = null
|
|
1690
|
-
var cancelled = false
|
|
1691
|
-
function attach() {
|
|
1692
|
-
var frame = findFrameEl(el)
|
|
1693
|
-
if (!frame || mo) return
|
|
1694
|
-
frameRef.current = frame
|
|
1695
|
-
function sync() {
|
|
1696
|
-
try {
|
|
1697
|
-
var gtc = frame.style.gridTemplateColumns
|
|
1698
|
-
if (!gtc) return
|
|
1699
|
-
var parts = gtc.trim().split(/\s+/)
|
|
1700
|
-
if (parts.length < 3) return
|
|
1701
|
-
var last = parseFloat(parts[parts.length - 1])
|
|
1702
|
-
if (isNaN(last) || last <= 1) { setState({ open: false }); return }
|
|
1703
|
-
setState({ open: true })
|
|
1704
|
-
if (Math.round(last) !== Math.round(widthRef.current)) {
|
|
1705
|
-
parts[parts.length - 1] = Math.round(widthRef.current) + 'px'
|
|
1706
|
-
frame.style.gridTemplateColumns = parts.join(' ')
|
|
1707
|
-
}
|
|
1708
|
-
} catch (e) {}
|
|
1709
|
-
}
|
|
1710
|
-
mo = new MutationObserver(sync)
|
|
1711
|
-
mo.observe(frame, { attributes: true, attributeFilter: ['style'] })
|
|
1712
|
-
sync()
|
|
1713
|
-
}
|
|
1714
|
-
attach()
|
|
1715
|
-
if (!mo) {
|
|
1716
|
-
var tries = 0
|
|
1717
|
-
var t = setInterval(function () {
|
|
1718
|
-
if (cancelled || mo) { clearInterval(t); return }
|
|
1719
|
-
attach()
|
|
1720
|
-
if (++tries > 40) clearInterval(t)
|
|
1721
|
-
}, 100)
|
|
1722
|
-
}
|
|
1723
|
-
return function () { cancelled = true; if (mo) mo.disconnect() }
|
|
1724
|
-
}, [])
|
|
1725
|
-
|
|
1726
|
-
function onResizeDown(e) {
|
|
1727
|
-
e.preventDefault()
|
|
1728
|
-
try { e.currentTarget.setPointerCapture(e.pointerId) } catch (err) {}
|
|
1729
|
-
dragRef.current = { startX: e.clientX, startW: widthRef.current, active: true }
|
|
1730
|
-
if (frameRef.current) { try { frameRef.current.style.transition = 'none' } catch (e) {} }
|
|
1731
|
-
}
|
|
1732
|
-
function onResizeMove(e) {
|
|
1733
|
-
if (!dragRef.current.active) return
|
|
1734
|
-
var w = clampW(dragRef.current.startW - (e.clientX - dragRef.current.startX))
|
|
1735
|
-
widthRef.current = w
|
|
1736
|
-
setFrameWidth(w)
|
|
1737
|
-
}
|
|
1738
|
-
function onResizeUp(e) {
|
|
1739
|
-
dragRef.current.active = false
|
|
1740
|
-
try { e.currentTarget.releasePointerCapture(e.pointerId) } catch (err) {}
|
|
1741
|
-
if (frameRef.current) { try { frameRef.current.style.transition = '' } catch (e) {} }
|
|
1742
|
-
}
|
|
1743
|
-
|
|
1744
|
-
visibleRows.length = 0
|
|
1745
|
-
|
|
1746
|
-
var bodyChildren = []
|
|
1747
|
-
if (state.newItem && state.newItem.parent === state.cwd) {
|
|
1748
|
-
bodyChildren.push(create('div', { key: '__new__', className: 'expl-row' },
|
|
1749
|
-
create('div', { className: 'expl-row-main', style: { paddingLeft: 8 } },
|
|
1750
|
-
create('span', { className: 'expl-icon' }, state.newItem.isDir ? '📁' : '📄'),
|
|
1751
|
-
create(NameInput, {
|
|
1752
|
-
initial: '',
|
|
1753
|
-
selectBase: false,
|
|
1754
|
-
onCommit: function (name) { commitNew(state.newItem.parent, name, state.newItem.isDir) },
|
|
1755
|
-
onCancel: function () { setState({ newItem: null }) },
|
|
1756
|
-
}),
|
|
1757
|
-
),
|
|
1758
|
-
))
|
|
1759
|
-
}
|
|
1760
|
-
if (s.tree.length === 0 && !(state.newItem && state.newItem.parent === state.cwd)) {
|
|
1761
|
-
bodyChildren.push(create('div', { key: '__empty__', className: 'expl-muted' }, '(空目录)'))
|
|
1762
|
-
} else {
|
|
1763
|
-
s.tree.forEach(function (node) { bodyChildren.push(create(Row, { key: node.path, node: node, depth: 0 })) })
|
|
1764
|
-
}
|
|
1765
|
-
|
|
1766
|
-
var container = create('div', {
|
|
1767
|
-
className: 'expl-body' + (state.dropTarget === state.cwd ? ' drop-target' : ''),
|
|
1768
|
-
onClick: onBlankClick,
|
|
1769
|
-
onContextMenu: onBlankContext,
|
|
1770
|
-
onDragOver: function (e) { e.preventDefault(); e.stopPropagation(); if (state.dropTarget !== state.cwd) setState({ dropTarget: state.cwd }) },
|
|
1771
|
-
onDragLeave: function (e) { if (state.dropTarget === state.cwd) setState({ dropTarget: null }) },
|
|
1772
|
-
onDrop: onBodyDrop,
|
|
1773
|
-
}, bodyChildren)
|
|
1774
|
-
|
|
1775
|
-
return create('div', { ref: panelRef, className: 'expl-panel' },
|
|
1776
|
-
create('div', { className: 'expl-resize', onPointerDown: onResizeDown, onPointerMove: onResizeMove, onPointerUp: onResizeUp, onPointerCancel: onResizeUp }),
|
|
1777
|
-
create('div', { className: 'expl-head' },
|
|
1778
|
-
create('span', { className: 'expl-title' }, '📁 资源管理器'),
|
|
1779
|
-
create('div', { className: 'expl-head-btns' },
|
|
1780
|
-
create('button', {
|
|
1781
|
-
className: 'expl-btn',
|
|
1782
|
-
onClick: function () { refreshAll() },
|
|
1783
|
-
title: '刷新(保持已展开目录)',
|
|
1784
|
-
'aria-label': '刷新',
|
|
1785
|
-
}, create('span', { className: 'expl-icon', dangerouslySetInnerHTML: { __html: ICON_REFRESH } })),
|
|
1786
|
-
create('button', {
|
|
1787
|
-
className: 'expl-btn',
|
|
1788
|
-
onClick: function () { if (uploadInputEl) uploadInputEl.click() },
|
|
1789
|
-
title: '上传文件',
|
|
1790
|
-
'aria-label': '上传文件',
|
|
1791
|
-
}, '⬆'),
|
|
1792
|
-
create('button', {
|
|
1793
|
-
className: 'expl-btn',
|
|
1794
|
-
style: { fontSize: '18px' },
|
|
1795
|
-
onClick: function (event) {
|
|
1796
|
-
try { layout.closeDetails() } catch (e) {}
|
|
1797
|
-
setHeroDetailsWidth(event.currentTarget, 0)
|
|
1798
|
-
},
|
|
1799
|
-
title: '关闭',
|
|
1800
|
-
'aria-label': '关闭资源管理器',
|
|
1801
|
-
}, '✕'),
|
|
1802
|
-
),
|
|
1803
|
-
),
|
|
1804
|
-
create('div', { className: 'expl-path', title: s.cwd || '' }, s.cwd || '…'),
|
|
1805
|
-
(s.uploads && s.uploads.length) ? create('div', { className: 'expl-uploads' },
|
|
1806
|
-
s.uploads.map(function (u, i) {
|
|
1807
|
-
var pct = u.done ? null : (u.total ? Math.round((u.loaded / u.total) * 100) : 0)
|
|
1808
|
-
return create('div', { key: i, className: 'expl-upload-row' + (u.done ? ' expl-upload-done' : '') },
|
|
1809
|
-
create('span', { className: 'expl-upload-glyph' }, u.done ? '✓' : (u.down ? '⬇' : '⬆')),
|
|
1810
|
-
create('span', { className: 'expl-upload-text' },
|
|
1811
|
-
u.done ? u.name
|
|
1812
|
-
: (u.down ? '下载中 ' : '上传中 ') + (u.totalFiles > 1 ? '(' + u.idx + '/' + u.totalFiles + ') ' : '') + u.name,
|
|
1813
|
-
),
|
|
1814
|
-
!u.done && u.total ? create('span', { className: 'expl-upload-pct' }, pct + '%') : null,
|
|
1815
|
-
)
|
|
1816
|
-
}),
|
|
1817
|
-
) : null,
|
|
1818
|
-
create('input', { type: 'file', multiple: true, style: { display: 'none' }, ref: function (el) { uploadInputEl = el }, onChange: onUploadPicker }),
|
|
1819
|
-
s.error ? create('div', { className: 'expl-error' }, s.error) : null,
|
|
1820
|
-
s.notice ? create('div', { className: 'expl-notice' }, s.notice) : null,
|
|
1821
|
-
s.selected.size > 1
|
|
1822
|
-
? create('div', { className: 'expl-bulk' }, '已选 ' + s.selected.size + ' 项 · 拖到文件夹可移动')
|
|
1823
|
-
: null,
|
|
1824
|
-
container,
|
|
1825
|
-
s.menu ? create('div', { className: 'expl-menu-mask', onMouseDown: function () { setState({ menu: null }) }, onContextMenu: function (e) { e.preventDefault(); setState({ menu: null }) } }) : null,
|
|
1826
|
-
s.menu ? create(Menu, { menu: s.menu }) : null,
|
|
1827
|
-
)
|
|
1828
|
-
}
|
|
1829
|
-
|
|
1830
|
-
function HeaderAction(props) {
|
|
1831
|
-
var s = useStore()
|
|
1832
|
-
var sessionId = props && props.sessionId
|
|
1833
|
-
var useSessions = props && props.useSessions
|
|
1834
|
-
var liveCwd = useSessions ? useSessions(function (list) {
|
|
1835
|
-
return (sessionId && list.byId[sessionId]) ? list.byId[sessionId].cwd : undefined
|
|
1836
|
-
}) : undefined
|
|
1837
|
-
var cwdRef = React.useRef(liveCwd)
|
|
1838
|
-
cwdRef.current = liveCwd || cwdRef.current
|
|
1839
|
-
React.useEffect(function () { trackSession(sessionId, cwdRef.current) }, [sessionId])
|
|
1840
|
-
return create('button', {
|
|
1841
|
-
type: 'button',
|
|
1842
|
-
className: 'expl-toggle expl-tool' + (s.open ? ' expl-active' : ''),
|
|
1843
|
-
onClick: function () { try { if (s.open) layout.closeDetails(); else layout.openDetails() } catch (e) {} },
|
|
1844
|
-
title: '项目资源管理器',
|
|
1845
|
-
'aria-label': '项目资源管理器',
|
|
1846
|
-
}, '📁')
|
|
1847
|
-
}
|
|
1848
|
-
|
|
1849
|
-
function HeroAction(props) {
|
|
1850
|
-
var s = useStore()
|
|
1851
|
-
var session = props && props.session
|
|
1852
|
-
var useSessions = props && props.useSessions
|
|
1853
|
-
var liveCwd = useSessions ? useSessions(function (list) {
|
|
1854
|
-
return (props.sessionId && list.byId[props.sessionId]) ? list.byId[props.sessionId].cwd : undefined
|
|
1855
|
-
}) : undefined
|
|
1856
|
-
var blank = !!(session && session.blank && session.composerPhase === 'blank')
|
|
1857
|
-
React.useEffect(function () {
|
|
1858
|
-
if (blank) trackSession(props.sessionId, liveCwd)
|
|
1859
|
-
}, [blank, props.sessionId])
|
|
1860
|
-
if (!blank) return null
|
|
1861
|
-
return create('div', { className: 'expl-hero-dock' },
|
|
1862
|
-
create('button', {
|
|
1863
|
-
type: 'button',
|
|
1864
|
-
className: 'expl-toggle expl-tool' + (s.open ? ' expl-active' : ''),
|
|
1865
|
-
onClick: function (event) {
|
|
1866
|
-
if (s.open) {
|
|
1867
|
-
try { layout.closeDetails() } catch (e) {}
|
|
1868
|
-
setHeroDetailsWidth(event.currentTarget, 0)
|
|
1869
|
-
} else if (!setHeroDetailsWidth(event.currentTarget, DETAILS_DEFAULT)) {
|
|
1870
|
-
try { layout.openDetails() } catch (e) {}
|
|
1871
|
-
}
|
|
1872
|
-
},
|
|
1873
|
-
title: '项目资源管理器',
|
|
1874
|
-
'aria-label': '项目资源管理器',
|
|
1875
|
-
}, '📁'))
|
|
1876
|
-
}
|
|
1877
|
-
|
|
2083
|
+
// ------------------------------------------------------------------
|
|
2084
|
+
// ▓▓ 区域三 · 速率徽标 · 🧂 盐
|
|
2085
|
+
// 输入框上方实时 t/s 吞吐徽标,数据来自 llm/stream 真实流;timerRef 为共享模块级计时引用。
|
|
1878
2086
|
// ------------------------------------------------------------------
|
|
1879
2087
|
// 速度徽标:DOM 注入到 Deep diving(role=status)旁。
|
|
1880
2088
|
// 不改 DSH 源码。用 MutationObserver 定位消息流里的 Deep diving,
|
|
@@ -1938,7 +2146,7 @@ window.__ModuleLoader__.load({
|
|
|
1938
2146
|
el.style.cssText = 'display:inline-flex;align-items:center;margin-left:10px;font-weight:400;font-size:13px;color:var(--dsw-alias-label-caption);'
|
|
1939
2147
|
el.style.setProperty('color', 'var(--dsw-alias-label-caption)', 'important')
|
|
1940
2148
|
el.style.setProperty('-webkit-text-fill-color', 'var(--dsw-alias-label-caption)', 'important')
|
|
1941
|
-
el.textContent = '
|
|
2149
|
+
el.textContent = T('speed.waiting') + new Array(dotsRef.current + 1).join('.')
|
|
1942
2150
|
return
|
|
1943
2151
|
}
|
|
1944
2152
|
// 流式阶段:token 计数立即显示;瞬时速率窗口未满(tps=0)时先不显示徽标,
|
|
@@ -2022,6 +2230,10 @@ window.__ModuleLoader__.load({
|
|
|
2022
2230
|
|
|
2023
2231
|
// ------------------------------------------------------------------
|
|
2024
2232
|
// ------------------------------------------------------------------
|
|
2233
|
+
// ------------------------------------------------------------------
|
|
2234
|
+
// ▓▓ 区域四 · 多行 GoalBar · 🧄 蒜
|
|
2235
|
+
// 复用原生 goal projection 与动作动词,多行完整展示 + textarea 编辑。
|
|
2236
|
+
// ------------------------------------------------------------------
|
|
2025
2237
|
// GoalBar 多行版:复用 DSH 原生 GoalBar 的 goal projection 与动作动词
|
|
2026
2238
|
// (onEdit/onPause/onResume/onClear),仅在展示上把单行截断放开为多行、
|
|
2027
2239
|
// 编辑用 textarea。通过 conversation.input.dock 同 id 'goal' + 更低 priority
|
|
@@ -2112,7 +2324,7 @@ window.__ModuleLoader__.load({
|
|
|
2112
2324
|
pendingRef.current = false
|
|
2113
2325
|
pending[1](false)
|
|
2114
2326
|
if (!result || !result.ok) {
|
|
2115
|
-
var err = result && result.error ? (result.error.message + ' (' + result.error.code + ')') : '
|
|
2327
|
+
var err = result && result.error ? (result.error.message + ' (' + result.error.code + ')') : T('speed.operateFail')
|
|
2116
2328
|
actionError[1](err)
|
|
2117
2329
|
}
|
|
2118
2330
|
return result
|
|
@@ -2223,6 +2435,10 @@ window.__ModuleLoader__.load({
|
|
|
2223
2435
|
}
|
|
2224
2436
|
|
|
2225
2437
|
/** 应用浏览器半区:注册会话头部/空白会话切换按钮、details 右列面板、GoalBar、速度徽标。 */
|
|
2438
|
+
// ------------------------------------------------------------------
|
|
2439
|
+
// ▓▓ 区域五 · 应用注册(apply)
|
|
2440
|
+
// 把上述各区域组件注册进 slots / inject;导出插件浏览器半区入口 apply/inject。
|
|
2441
|
+
// ------------------------------------------------------------------
|
|
2226
2442
|
function apply(ctx) {
|
|
2227
2443
|
var slots = ctx.slots
|
|
2228
2444
|
layout = ctx.layout
|
|
@@ -2230,6 +2446,11 @@ window.__ModuleLoader__.load({
|
|
|
2230
2446
|
var sessions = ctx.sessions
|
|
2231
2447
|
var remoteGoals = ctx.remote.goals
|
|
2232
2448
|
var disposers = []
|
|
2449
|
+
// i18n:注册双语词典;模块级 T 供组件外回调(菜单/错误横幅)取当前语言文案。
|
|
2450
|
+
if (ctx.locale && typeof ctx.locale.register === 'function') {
|
|
2451
|
+
ctx.effect(function () { return ctx.locale.register(NS, DICT) }, 'dsh-soup: dictionaries')
|
|
2452
|
+
if (typeof ctx.locale.bind === 'function') T = ctx.locale.bind(NS)
|
|
2453
|
+
}
|
|
2233
2454
|
// 自动刷新心跳:1s 固定心跳 + 到期才探测(基础 3s;宿主不可达时指数退避,封顶 60s)。
|
|
2234
2455
|
// 面板关且无预览时 tick 内直接返回(零请求);首个 tick 只建基线。
|
|
2235
2456
|
if (timerRef && typeof timerRef.interval === 'function') {
|
|
@@ -2238,7 +2459,7 @@ window.__ModuleLoader__.load({
|
|
|
2238
2459
|
}
|
|
2239
2460
|
slots.inject('conversation.session.header.utilities', function () {
|
|
2240
2461
|
disposers.push(slots.register(
|
|
2241
|
-
{ name: 'conversation.session.header.utilities', id: 'dsh-soup-toggle', order: 10, label: '
|
|
2462
|
+
{ name: 'conversation.session.header.utilities', id: 'dsh-soup-toggle', order: 10, locale: NS, label: function () { return T('explorer.label') } },
|
|
2242
2463
|
function (props) { return create(HeaderAction, props) },
|
|
2243
2464
|
))
|
|
2244
2465
|
})
|
|
@@ -2246,7 +2467,7 @@ window.__ModuleLoader__.load({
|
|
|
2246
2467
|
disposers.push(slots.register(
|
|
2247
2468
|
// `details` is a single slot occupied by the shell at priority 0.
|
|
2248
2469
|
// Lower priorities render first, so this intentionally shadows it.
|
|
2249
|
-
{ name: 'details', priority: -1 },
|
|
2470
|
+
{ name: 'details', priority: -1, locale: NS },
|
|
2250
2471
|
function (props) { return create(Panel, props) },
|
|
2251
2472
|
))
|
|
2252
2473
|
})
|
|
@@ -2254,7 +2475,7 @@ window.__ModuleLoader__.load({
|
|
|
2254
2475
|
// 槽位 entry 是静态的,多文件由 FilesView 内部子 tab 条管理。
|
|
2255
2476
|
slots.inject('conversation.view', function () {
|
|
2256
2477
|
disposers.push(slots.register(
|
|
2257
|
-
{ name: 'conversation.view', id: 'dsh-soup-files', order: 20, label:
|
|
2478
|
+
{ name: 'conversation.view', id: 'dsh-soup-files', order: 20, locale: NS, label: function () { return T('files.tab') } },
|
|
2258
2479
|
function (props) { return create(FilesView, props) },
|
|
2259
2480
|
))
|
|
2260
2481
|
})
|
|
@@ -2274,7 +2495,7 @@ window.__ModuleLoader__.load({
|
|
|
2274
2495
|
}
|
|
2275
2496
|
slots.inject('conversation.input.dock', function () {
|
|
2276
2497
|
disposers.push(slots.register(
|
|
2277
|
-
{ name: 'conversation.input.dock', id: 'dsh-soup-hero-toggle', order: -10 },
|
|
2498
|
+
{ name: 'conversation.input.dock', id: 'dsh-soup-hero-toggle', order: -10, locale: NS },
|
|
2278
2499
|
function (props) { return create(HeroAction, props) },
|
|
2279
2500
|
))
|
|
2280
2501
|
// GoalBar 多行版:同 id 'goal'、更低 priority (-1) -> 遮蔽默认单行实现。
|
|
@@ -2310,7 +2531,7 @@ window.__ModuleLoader__.load({
|
|
|
2310
2531
|
function (props) { return create(GoalDock, props) },
|
|
2311
2532
|
))
|
|
2312
2533
|
disposers.push(slots.register(
|
|
2313
|
-
{ name: 'conversation.input.dock', id: 'dsh-speed-badge', order: 30, label: '
|
|
2534
|
+
{ name: 'conversation.input.dock', id: 'dsh-speed-badge', order: 30, locale: NS, label: function () { return T('speed.label') } },
|
|
2314
2535
|
function (props) { return create(SpeedBadge, props) },
|
|
2315
2536
|
))
|
|
2316
2537
|
})
|