@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.
Files changed (3) hide show
  1. package/README.md +5 -1
  2. package/lib/client.js +647 -426
  3. 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 + remote.goals(goal 动作动词)。 */
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: '宿主不可达: ' + String((err && err.message) || err) }
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) || '失败') + ((res && res.hint) ? '(' + res.hint + ')' : ''))
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('已复制路径') }).catch(function () {})
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('不能把文件夹移入自身: ' + name); continue }
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 ? count + ' 个文件' : okCount + '/' + count + ' 个文件' + (failedName ? '(' + failedName + ' 失败)' : '')),
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('已开始下载 ' + name + '(大文件,由浏览器接管)')
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: '下载失败: ' + String((err && err.message) || err), uploads: [] })
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
- var FILES_TAB_LABEL = '预览'
856
-
857
- /** 当前活跃会话:list/read/download 携带给宿主,供围栏直查该会话 cwd。 */
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: '👁 预览', onClick: function () { openFileInTab(node.path) } })
1090
- items.push({ key: 'open', label: '🖥 用系统打开', onClick: openSelection, separatorAfter: true })
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: '🖥 用系统打开', onClick: openSelection, separatorAfter: true })
1117
+ items.push({ key: 'open', label: T('menu.open'), onClick: openSelection, separatorAfter: true })
1093
1118
  }
1094
- items.push({ key: 'rename', label: '✎ 重命名', onClick: function () { startRename(node) } })
1095
- items.push({ key: 'trash', label: '🗑 移到废纸篓', danger: true, onClick: trashSelection, separatorAfter: true })
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: '🗑 移到废纸篓 (' + state.selected.size + ' 项)', danger: true, onClick: trashSelection, separatorAfter: true })
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: '📄 新建文件', onClick: function () { startNew(node.path, false) } })
1101
- items.push({ key: 'newfolder', label: '📁 新建文件夹', onClick: function () { startNew(node.path, true) }, separatorAfter: true })
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: '⧉ 复制路径', onClick: function () { copyPath(node) } })
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: '⬇ 下载', onClick: function () { downloadFile(node.path) } })
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: '📄 新建文件', onClick: function () { startNew(state.cwd, false) } })
1109
- items.push({ key: 'newfolder', label: '📁 新建文件夹', onClick: function () { startNew(state.cwd, true) } })
1110
- items.push({ key: 'refresh', label: [create('span', { className: 'expl-menu-ico', dangerouslySetInnerHTML: { __html: ICON_REFRESH } }), '刷新'], onClick: refresh, separatorAfter: true })
1111
- if (state.selected.size > 0) items.push({ key: 'none', label: '取消选择', onClick: function () { setState({ selected: new Set(), lastIndex: null }) } })
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 || 'HTML 预览',
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' }, 'PDF 预览失败: ' + state.error)
1339
- if (!state.url) return create('div', { className: 'dfv-empty' }, 'PDF 加载中…')
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': 'PDF 预览',
1345
- }, '当前浏览器无法内嵌预览 PDF,请右键用系统打开。')
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: 'HTML 输出', srcDoc: nbSource(out.data[mime]) })
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' }, 'ipynb 解析失败,按纯文本显示'),
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' }, 'JSON 解析失败,按纯文本显示'),
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('共 ' + totalRows + ' 行,仅预览前 ' + CSV_MAX_ROWS + ' 行')
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
- '二进制文件(' + formatBytes(entry.size) + '),请在资源管理器中用系统打开')
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
- '图片过大(' + formatBytes(entry.size) + ',上限 ' + formatBytes(entry.limit) + ')')
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
- 'PDF 过大(' + formatBytes(entry.size) + ',上限 ' + formatBytes(entry.limit) + '),请用系统打开')
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
- '文件超过 2 MB,仅预览前 2 MB;完整内容请用系统编辑器打开。') : null,
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
- function Panel(props) {
1656
- var s = useStore()
1657
- var sessionId = props && props.sessionId
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 = '正在等待模型' + new Array(dotsRef.current + 1).join('.')
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: FILES_TAB_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
  })