@lyhue1991/dsh-soup 0.4.11 → 0.5.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 CHANGED
@@ -40,6 +40,7 @@ DSH(DeepSeek Harness)功能很强:会话、工具、轨迹、Goal 一应
40
40
  - **类型图标**:按品牌色区分文件类型(notebook 橙、markdown 紫、json 黄、pdf 红…)。
41
41
  - **沙箱围栏**:所有文件操作只能落在你的工作区与会话目录内,越界自动拒绝。
42
42
  - **自动刷新**:每 3 秒轻量探测一次目录签名(mtime),有变化才重拉——agent 或终端里的文件增删改无需手动刷新即可出现;面板关闭且无预览时零开销;宿主不可达(如 DSH 重启中)自动指数退避(封顶 60s)。
43
+ - **新建文件夹**:顶部 🗂+ 在当前目录创建文件夹;工具栏顺序为 🗂+ 新建、⬆ 上传、⟳ 刷新、✕ 关闭。
43
44
  - **一键刷新**:顶部 ⟳ 重拉整个文件树,已展开的子目录保持展开不收起。
44
45
 
45
46
  ### 🫚 会话内文件预览(主打 · 姜 · 去腥增香)
package/lib/client.js CHANGED
@@ -45,10 +45,13 @@ window.__ModuleLoader__.load({
45
45
  // ------------------------------------------------------------------
46
46
  // 样式:一次性注入 <style data-plugin>(与官方插件 css 内联约定一致)
47
47
  // ------------------------------------------------------------------
48
- var EXPL_CSS = '.expl-panel{position:relative;height:100%;min-width:0;width:100%;display:flex;flex-direction:column;background:var(--dsw-specific-sidebar-fill);color:var(--dsw-alias-label-primary);font-size:13px;font-family:var(--dsw-font-family);overflow:hidden;box-sizing:border-box;}' +
48
+ var EXPL_CSS = '.expl-panel{position:relative;height:100%;min-width:300px;max-width:520px;width:100%;display:flex;flex-direction:column;background:var(--dsw-specific-sidebar-fill);color:var(--dsw-alias-label-primary);font-size:13px;font-family:var(--dsw-font-family);overflow:hidden;box-sizing:border-box;}' +
49
49
  '.expl-resize{position:absolute;left:0;top:0;bottom:0;width:7px;cursor:col-resize;z-index:6;touch-action:none;}' +
50
50
  '.expl-resize::after{content:"";position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:3px;height:40px;border-radius:2px;background:var(--dsw-alias-interactive-bg-hover);opacity:0;transition:opacity .15s;}' +
51
51
  '.expl-resize:hover::after{opacity:1;}' +
52
+ /* The official 0.1.5 expand control duplicates dsh-soup's folder
53
+ button; hide it so the explorer has one unambiguous entry point. */
54
+ '[data-sidebar-right-expand],[data-sidebar-right-expand-placeholder]{display:none!important;}' +
52
55
  '[data-side="details"]{display:none!important;}' +
53
56
  '.expl-head{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:12px 14px;border-bottom:1px solid var(--dsw-alias-border-l1);flex:none;}' +
54
57
  '.expl-title{font-weight:600;color:var(--dsw-alias-label-primary);white-space:nowrap;font-size:14px;}' +
@@ -90,9 +93,14 @@ window.__ModuleLoader__.load({
90
93
  '.expl-size{color:var(--dsw-alias-label-tertiary);font-size:11px;flex:none;margin-left:8px;}' +
91
94
  '.expl-muted{color:var(--dsw-alias-label-tertiary);padding:4px 14px;font-size:12px;}' +
92
95
  '.expl-menu-mask{position:fixed;inset:0;z-index:1980;}' +
96
+ '.expl-confirm-mask{position:fixed;inset:0;z-index:2100;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,.28);}' +
97
+ '.expl-confirm{width:min(360px,calc(100vw - 32px));padding:18px;border-radius:12px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);box-shadow:var(--dsw-shadow-lv3);}' +
98
+ '.expl-confirm-title{font-size:14px;font-weight:600;margin-bottom:10px;}.expl-confirm-text{font-size:13px;line-height:20px;overflow-wrap:anywhere;}.expl-confirm-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:18px;}' +
99
+ '.expl-confirm-btn{height:30px;padding:0 14px;border:1px solid var(--dsw-alias-border-l2);border-radius:7px;background:transparent;color:var(--dsw-alias-label-primary);cursor:pointer;font-size:13px;}.expl-confirm-btn:hover{background:var(--dsw-alias-interactive-bg-hover);}.expl-confirm-primary{border-color:var(--dsw-alias-state-business-primary);background:var(--dsw-alias-state-business-primary);color:#fff;}' +
93
100
  '.expl-menu{position:fixed;z-index:1990;min-width:172px;background:var(--dsw-specific-menu);border:1px solid var(--dsw-alias-border-inverted);border-radius:12px;padding:4px;box-shadow:var(--dsw-shadow-lv3);pointer-events:auto;}' +
94
- '.expl-menu-item{display:flex;align-items:center;gap:7px;width:100%;text-align:left;background:transparent;border:none;color:var(--dsw-alias-label-primary);font-size:13px;padding:6px 10px;border-radius:8px;cursor:pointer;line-height:1.4;}' +
95
- '.expl-menu-ico{flex:none;display:inline-flex;align-items:center;}.expl-menu-ico svg{width:14px;height:14px;display:block;}' +
101
+ '.expl-menu-item{display:flex;align-items:center;width:100%;text-align:left;background:transparent;border:none;color:var(--dsw-alias-label-primary);font-size:13px;padding:6px 10px;border-radius:8px;cursor:pointer;line-height:1.4;}' +
102
+ '.expl-menu-ico,.expl-menu-ico-ph{flex:none;display:inline-flex;align-items:center;justify-content:center;width:14px;height:14px;margin-right:7px;}' +
103
+ '.expl-menu-ico svg{width:14px;height:14px;display:block;}' +
96
104
  '.expl-menu-item:hover{background:var(--dsw-alias-interactive-bg-hover);}' +
97
105
  '.expl-danger{color:var(--dsw-alias-state-error-primary);} .expl-menu-item.expl-danger:hover{background:var(--dsw-alias-interactive-bg-hover-danger);}' +
98
106
  '.expl-menu-sep{height:1px;margin:4px 6px;background:var(--dsw-alias-border-l1);}' +
@@ -112,12 +120,13 @@ window.__ModuleLoader__.load({
112
120
  }
113
121
 
114
122
  // ------------------------------------------------------------------
115
- // 工具栏图标:取自 JupyterLab ui-components(BSD-3-Clause,与文件类型
116
- // 图标同源的引用方式),fill 改为 currentColor 以跟随 DSH 主题 token。
117
- // 注意:只引入刷新图标——ICON_CLOSE / ICON_UPLOAD 与 GoalBar 段的
118
- // 同名 React 元素常量冲突(var 提升覆盖),字符样式反而更稳。
123
+ // 工具栏图标:刷新图标取自 JupyterLab ui-components(BSD-3-Clause);
124
+ // fill 改为 currentColor 以跟随 DSH 主题 token。
125
+ // 注意:ICON_CLOSE / ICON_UPLOAD 与 GoalBar 段的同名 React 元素常量
126
+ // 冲突(var 提升覆盖),字符样式反而更稳。
119
127
  // ------------------------------------------------------------------
120
128
  var ICON_REFRESH = '<svg xmlns="http://www.w3.org/2000/svg" width="16" viewBox="0 0 18 18"><path fill="currentColor" d="M9 13.5c-2.49 0-4.5-2.01-4.5-4.5S6.51 4.5 9 4.5c1.24 0 2.36.52 3.17 1.33L10 8h5V3l-1.76 1.76A6 6 0 0 0 9 3C5.69 3 3.01 5.69 3.01 9S5.69 15 9 15a5.98 5.98 0 0 0 5.9-5h-1.52c-.46 2-2.24 3.5-4.38 3.5"/></svg>'
129
+ var ICON_NEW_FOLDER = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><path fill="currentColor" d="M20 6h-8l-2-2H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2zm-2 7h-3v3h-2v-3h-3v-2h3V8h2v3h3v2z"/></svg>'
121
130
  var ICON_MORE = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><circle cx="3" cy="8" r="1.3" fill="currentColor"/><circle cx="8" cy="8" r="1.3" fill="currentColor"/><circle cx="13" cy="8" r="1.3" fill="currentColor"/></svg>'
122
131
  var EXPAND_SVG = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"/></svg>'
123
132
  var SHRINK_SVG = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3v3a2 2 0 0 1-2 2H3m18 0h-3a2 2 0 0 1-2-2V3m0 18v-3a2 2 0 0 1 2-2h3M3 16h3a2 2 0 0 1 2 2v3"/></svg>'
@@ -250,6 +259,8 @@ window.__ModuleLoader__.load({
250
259
  'explorer.label': '资源管理器',
251
260
  'explorer.refresh': '刷新',
252
261
  'explorer.refreshTitle': '刷新(保持已展开目录)',
262
+ 'explorer.newFolder': '新建文件夹',
263
+ 'explorer.newFolderTitle': '在当前目录新建文件夹',
253
264
  'explorer.upload': '上传文件',
254
265
  'explorer.close': '关闭',
255
266
  'explorer.closePanel': '关闭资源管理器',
@@ -260,6 +271,10 @@ window.__ModuleLoader__.load({
260
271
  'explorer.uploading': '上传中 ',
261
272
  'explorer.downloading': '下载中 ',
262
273
  'explorer.uploaded': '已上传 {n} 个文件',
274
+ 'explorer.overwriteConfirm': '文件“{name}”已存在,是否覆盖?',
275
+ 'explorer.overwriteTitle': '确认覆盖',
276
+ 'explorer.overwrite': '覆盖',
277
+ 'explorer.cancel': '取消',
263
278
  'explorer.filesCount': '{n} 个文件',
264
279
  'explorer.filesCountPartial': '{ok}/{total} 个文件',
265
280
  'explorer.downloaded': '已下载 {n} 个文件',
@@ -280,7 +295,7 @@ window.__ModuleLoader__.load({
280
295
  'menu.trash': '🗑 移到废纸篓',
281
296
  'menu.trashMulti': '🗑 移到废纸篓 ({n} 项)',
282
297
  'menu.newFile': '📄 新建文件',
283
- 'menu.newFolder': '📁 新建文件夹',
298
+ 'menu.newFolder': '新建文件夹',
284
299
  'menu.copyPath': '⧉ 复制路径',
285
300
  'menu.download': '⬇ 下载',
286
301
  'menu.deselect': '取消选择',
@@ -314,6 +329,8 @@ window.__ModuleLoader__.load({
314
329
  'explorer.label': 'Explorer',
315
330
  'explorer.refresh': 'Refresh',
316
331
  'explorer.refreshTitle': 'Refresh (keeps expanded folders)',
332
+ 'explorer.newFolder': 'New folder',
333
+ 'explorer.newFolderTitle': 'Create a folder in the current directory',
317
334
  'explorer.upload': 'Upload files',
318
335
  'explorer.close': 'Close',
319
336
  'explorer.closePanel': 'Close explorer',
@@ -324,6 +341,10 @@ window.__ModuleLoader__.load({
324
341
  'explorer.uploading': 'Uploading ',
325
342
  'explorer.downloading': 'Downloading ',
326
343
  'explorer.uploaded': 'Uploaded {n} file(s)',
344
+ 'explorer.overwriteConfirm': 'The file "{name}" already exists. Overwrite it?',
345
+ 'explorer.overwriteTitle': 'Confirm overwrite',
346
+ 'explorer.overwrite': 'Overwrite',
347
+ 'explorer.cancel': 'Cancel',
327
348
  'explorer.filesCount': '{n} file(s)',
328
349
  'explorer.filesCountPartial': '{ok}/{total} file(s)',
329
350
  'explorer.downloaded': 'Downloaded {n} file(s)',
@@ -344,7 +365,7 @@ window.__ModuleLoader__.load({
344
365
  'menu.trash': '🗑 Move to trash',
345
366
  'menu.trashMulti': '🗑 Move to trash ({n})',
346
367
  'menu.newFile': '📄 New file',
347
- 'menu.newFolder': '📁 New folder',
368
+ 'menu.newFolder': 'New folder',
348
369
  'menu.copyPath': '⧉ Copy path',
349
370
  'menu.download': '⬇ Download',
350
371
  'menu.deselect': 'Clear selection',
@@ -387,6 +408,7 @@ window.__ModuleLoader__.load({
387
408
  dropTarget: null, dragPaths: null,
388
409
  files: { list: [], active: null, overlay: false, overlayMax: false, overlayReturn: null },
389
410
  uploads: [],
411
+ uploadConflict: null,
390
412
  }
391
413
  var listeners = new Set()
392
414
  // trackSession() is called by both the hero toggle and the details panel
@@ -1140,9 +1162,12 @@ window.__ModuleLoader__.load({
1140
1162
  }
1141
1163
 
1142
1164
  function finishUploads(count, okCount, failedName, down) {
1143
- var verb = down ? T('explorer.downloaded') : T('explorer.uploaded')
1165
+ // These translations own the `{n}` placeholder; passing no params leaves
1166
+ // the literal token visible in the green completion notice.
1167
+ var verb = down ? T('explorer.downloaded', { n: okCount }) : T('explorer.uploaded', { n: okCount })
1168
+ var suffix = okCount === count ? '' : ' · ' + T('explorer.filesCountPartial', { ok: okCount, total: count })
1144
1169
  setUploadEntry({
1145
- name: '✓ ' + verb + ' ' + (okCount === count ? T('explorer.filesCount', { n: count }) : T('explorer.filesCountPartial', { ok: okCount, total: count }) + (failedName ? '(' + failedName + ')' : '')),
1170
+ name: '✓ ' + verb + suffix + (failedName ? '(' + failedName + ')' : ''),
1146
1171
  loaded: 0, total: 0, idx: 0, totalFiles: 0, done: true, down: !!down,
1147
1172
  })
1148
1173
  window.setTimeout(function () { setState({ uploads: [] }) }, 1600)
@@ -1157,10 +1182,17 @@ window.__ModuleLoader__.load({
1157
1182
  var start = c * UPLOAD_CHUNK_SIZE
1158
1183
  var blob = f.slice(start, Math.min(f.size, start + UPLOAD_CHUNK_SIZE))
1159
1184
  var b64 = await fileToBase64(blob)
1160
- var res = await rpc('upload', {
1185
+ var args = {
1161
1186
  dir: dir, name: f.name, data: b64,
1162
1187
  chunk: chunked ? c + 1 : undefined,
1163
- })
1188
+ }
1189
+ var res = await rpc('upload', args)
1190
+ if (res && res.conflict) {
1191
+ var overwrite = await askOverwrite(f.name)
1192
+ if (!overwrite) throw new Error('已取消上传')
1193
+ args.overwrite = true
1194
+ res = await rpc('upload', args)
1195
+ }
1164
1196
  if (!res || !res.ok) throw new Error((res && res.error) || T('explorer.uploadFail'))
1165
1197
  updateUpload(f.name, Math.min(f.size, start + UPLOAD_CHUNK_SIZE), f.size, idx, totalFiles)
1166
1198
  }
@@ -1381,7 +1413,7 @@ window.__ModuleLoader__.load({
1381
1413
  }
1382
1414
  if (node.type === 'directory') {
1383
1415
  items.push({ key: 'newfile', label: T('menu.newFile'), onClick: function () { startNew(node.path, false) } })
1384
- items.push({ key: 'newfolder', label: T('menu.newFolder'), onClick: function () { startNew(node.path, true) }, separatorAfter: true })
1416
+ items.push({ key: 'newfolder', label: [create('span', { className: 'expl-menu-ico', dangerouslySetInnerHTML: { __html: ICON_NEW_FOLDER } }), T('menu.newFolder')], onClick: function () { startNew(node.path, true) }, separatorAfter: true })
1385
1417
  }
1386
1418
  items.push({ key: 'copy', label: T('menu.copyPath'), onClick: function () { copyPath(node) } })
1387
1419
  if (node.type !== 'directory') {
@@ -1389,7 +1421,7 @@ window.__ModuleLoader__.load({
1389
1421
  }
1390
1422
  } else {
1391
1423
  items.push({ key: 'newfile', label: T('menu.newFile'), onClick: function () { startNew(state.cwd, false) } })
1392
- items.push({ key: 'newfolder', label: T('menu.newFolder'), onClick: function () { startNew(state.cwd, true) } })
1424
+ items.push({ key: 'newfolder', label: [create('span', { className: 'expl-menu-ico', dangerouslySetInnerHTML: { __html: ICON_NEW_FOLDER } }), T('menu.newFolder')], onClick: function () { startNew(state.cwd, true) } })
1393
1425
  items.push({ key: 'refresh', label: [create('span', { className: 'expl-menu-ico', dangerouslySetInnerHTML: { __html: ICON_REFRESH } }), T('explorer.refresh')], onClick: refresh, separatorAfter: true })
1394
1426
  if (state.selected.size > 0) items.push({ key: 'none', label: T('menu.deselect'), onClick: function () { setState({ selected: new Set(), lastIndex: null }) } })
1395
1427
  }
@@ -1428,11 +1460,16 @@ window.__ModuleLoader__.load({
1428
1460
  var items = buildMenuItems(menu.node)
1429
1461
  var menuChildren = []
1430
1462
  items.forEach(function (it) {
1463
+ var children = it.label instanceof Array ? it.label : (function () {
1464
+ var match = it.label.match(/^([\u2190-\u2BFF\u{1F000}-\u{1FAFF}])\s*/u)
1465
+ if (match) return [create('span', { className: 'expl-menu-ico' }, match[1]), it.label.slice(match[0].length)]
1466
+ return [create('span', { className: 'expl-menu-ico-ph' }), it.label]
1467
+ })()
1431
1468
  menuChildren.push(create('button', {
1432
1469
  key: it.key,
1433
1470
  className: 'expl-menu-item' + (it.danger ? ' expl-danger' : ''),
1434
1471
  onClick: function () { setState({ menu: null }); it.onClick() },
1435
- }, it.label))
1472
+ }, children))
1436
1473
  if (it.separatorAfter) menuChildren.push(create('div', { key: it.key + '-sep', className: 'expl-menu-sep' }))
1437
1474
  })
1438
1475
  return create('div', { ref: menuRef, className: 'expl-menu', style: { left: pos.x, top: pos.y } }, menuChildren)
@@ -1492,6 +1529,13 @@ window.__ModuleLoader__.load({
1492
1529
  var last = parseFloat(parts[parts.length - 1])
1493
1530
  if (isNaN(last) || last <= 1) { setState({ open: false }); return }
1494
1531
  setState({ open: true })
1532
+ // Official 0.1.5 rightbar defaults to a viewport ratio. Keep
1533
+ // it within dsh-soup's 300-520px explorer contract.
1534
+ if (last < DETAILS_MIN || last > DETAILS_MAX) {
1535
+ if (Math.round(last) !== DETAILS_DEFAULT) setFrameWidth(DETAILS_DEFAULT)
1536
+ widthRef.current = DETAILS_DEFAULT
1537
+ return
1538
+ }
1495
1539
  if (Math.round(last) !== Math.round(widthRef.current)) {
1496
1540
  // 宿主是宽度偏好的权威(/native drag/窗口求解都可能改它)。
1497
1541
  // 插件只同步基准,不能把宿主拖出来的宽度强行拉回旧默认。
@@ -1571,20 +1615,29 @@ window.__ModuleLoader__.load({
1571
1615
  create('div', { className: 'expl-head-btns' },
1572
1616
  create('button', {
1573
1617
  className: 'expl-btn',
1574
- onClick: function () { refreshAll() },
1575
- title: T('explorer.refreshTitle'),
1576
- 'aria-label': T('explorer.refresh'),
1577
- }, create('span', { className: 'expl-icon', dangerouslySetInnerHTML: { __html: ICON_REFRESH } })),
1618
+ onClick: function () { startNew(state.cwd, true) },
1619
+ title: T('explorer.newFolderTitle'),
1620
+ 'aria-label': T('explorer.newFolder'),
1621
+ }, create('span', { className: 'expl-icon', dangerouslySetInnerHTML: { __html: ICON_NEW_FOLDER } })),
1578
1622
  create('button', {
1579
1623
  className: 'expl-btn',
1580
1624
  onClick: function () { if (uploadInputEl) uploadInputEl.click() },
1581
1625
  title: T('explorer.upload'),
1582
1626
  'aria-label': T('explorer.upload'),
1583
1627
  }, '⬆'),
1628
+ create('button', {
1629
+ className: 'expl-btn',
1630
+ onClick: function () { refreshAll() },
1631
+ title: T('explorer.refreshTitle'),
1632
+ 'aria-label': T('explorer.refresh'),
1633
+ }, create('span', { className: 'expl-icon', dangerouslySetInnerHTML: { __html: ICON_REFRESH } })),
1584
1634
  create('button', {
1585
1635
  className: 'expl-btn',
1586
1636
  onClick: function (event) {
1587
- try { layout.closeDetails() } catch (e) {}
1637
+ try {
1638
+ if (layout && typeof layout.closeRightbar === 'function') layout.closeRightbar()
1639
+ else if (layout && typeof layout.closeDetails === 'function') layout.closeDetails()
1640
+ } catch (e) {}
1588
1641
  setHeroDetailsWidth(event.currentTarget, 0)
1589
1642
  },
1590
1643
  title: T('explorer.close'),
@@ -1615,6 +1668,16 @@ window.__ModuleLoader__.load({
1615
1668
  container,
1616
1669
  s.menu ? create('div', { className: 'expl-menu-mask', onMouseDown: function () { setState({ menu: null }) }, onContextMenu: function (e) { e.preventDefault(); setState({ menu: null }) } }) : null,
1617
1670
  s.menu ? create(Menu, { menu: s.menu }) : null,
1671
+ s.uploadConflict ? create('div', { className: 'expl-confirm-mask', role: 'dialog', 'aria-modal': 'true' },
1672
+ create('div', { className: 'expl-confirm' },
1673
+ create('div', { className: 'expl-confirm-title' }, T('explorer.overwriteTitle')),
1674
+ create('div', { className: 'expl-confirm-text' }, T('explorer.overwriteConfirm', { name: s.uploadConflict.name })),
1675
+ create('div', { className: 'expl-confirm-actions' },
1676
+ create('button', { type: 'button', className: 'expl-confirm-btn', onClick: function () { s.uploadConflict.resolve(false); setState({ uploadConflict: null }) } }, T('explorer.cancel')),
1677
+ create('button', { type: 'button', className: 'expl-confirm-btn expl-confirm-primary', autoFocus: true, onClick: function () { s.uploadConflict.resolve(true); setState({ uploadConflict: null }) } }, T('explorer.overwrite')),
1678
+ ),
1679
+ ),
1680
+ ) : null,
1618
1681
  )
1619
1682
  }
1620
1683
 
@@ -1631,7 +1694,15 @@ window.__ModuleLoader__.load({
1631
1694
  return create('button', {
1632
1695
  type: 'button',
1633
1696
  className: 'expl-toggle expl-tool' + (s.open ? ' expl-active' : ''),
1634
- onClick: function () { try { if (s.open) layout.closeDetails(); else layout.openDetails() } catch (e) {} },
1697
+ onClick: function () {
1698
+ try {
1699
+ if (layout && typeof layout.closeRightbar === 'function') {
1700
+ if (s.open) layout.closeRightbar()
1701
+ else layout.openRightbar(true, false)
1702
+ } else if (s.open) layout.closeDetails()
1703
+ else layout.openDetails()
1704
+ } catch (e) {}
1705
+ },
1635
1706
  title: T('explorer.label'),
1636
1707
  'aria-label': T('explorer.label'),
1637
1708
  }, '📁')
@@ -1661,10 +1732,16 @@ window.__ModuleLoader__.load({
1661
1732
  className: 'expl-toggle expl-tool' + (s.open ? ' expl-active' : ''),
1662
1733
  onClick: function (event) {
1663
1734
  if (s.open) {
1664
- try { layout.closeDetails() } catch (e) {}
1735
+ try {
1736
+ if (layout && typeof layout.closeRightbar === 'function') layout.closeRightbar()
1737
+ else if (layout && typeof layout.closeDetails === 'function') layout.closeDetails()
1738
+ } catch (e) {}
1665
1739
  setHeroDetailsWidth(event.currentTarget, 0)
1666
1740
  } else if (!setHeroDetailsWidth(event.currentTarget, DETAILS_DEFAULT)) {
1667
- try { layout.openDetails() } catch (e) {}
1741
+ try {
1742
+ if (layout && typeof layout.openRightbar === 'function') layout.openRightbar(true, false)
1743
+ else if (layout && typeof layout.openDetails === 'function') layout.openDetails()
1744
+ } catch (e) {}
1668
1745
  }
1669
1746
  },
1670
1747
  title: T('explorer.label'),
@@ -1719,6 +1796,12 @@ window.__ModuleLoader__.load({
1719
1796
  } catch (_) {}
1720
1797
  }
1721
1798
 
1799
+ function askOverwrite(name) {
1800
+ return new Promise(function (resolve) {
1801
+ setState({ uploadConflict: { name: name, resolve: resolve } })
1802
+ })
1803
+ }
1804
+
1722
1805
  function findFileEntry(path) {
1723
1806
  for (var i = 0; i < state.files.list.length; i++) {
1724
1807
  if (state.files.list[i].path === path) return state.files.list[i]
@@ -2972,6 +3055,30 @@ window.__ModuleLoader__.load({
2972
3055
  var sessions = ctx.sessions
2973
3056
  var remoteGoals = ctx.remote.goals
2974
3057
  var disposers = []
3058
+ // 0.1.5's official Sidebar adds a header-corner expand button. Its
3059
+ // default handler opens the official surface, which would diverge from
3060
+ // dsh-soup's resource explorer. Route that button to the same layout
3061
+ // face used by our explorer controls instead.
3062
+ if (typeof document !== 'undefined') {
3063
+ var onOfficialExpand = function (event) {
3064
+ var target = event.target && event.target.closest
3065
+ ? event.target.closest('[data-sidebar-right-expand]') : null
3066
+ if (!target) return
3067
+ event.preventDefault()
3068
+ if (typeof event.stopImmediatePropagation === 'function') event.stopImmediatePropagation()
3069
+ else event.stopPropagation()
3070
+ try {
3071
+ if (state.open) {
3072
+ if (layout && typeof layout.closeRightbar === 'function') layout.closeRightbar()
3073
+ else if (layout && typeof layout.closeDetails === 'function') layout.closeDetails()
3074
+ } else if (layout && typeof layout.openRightbar === 'function') {
3075
+ layout.openRightbar(true, false)
3076
+ } else if (layout && typeof layout.openDetails === 'function') layout.openDetails()
3077
+ } catch (e) {}
3078
+ }
3079
+ document.addEventListener('click', onOfficialExpand, true)
3080
+ disposers.push(function () { document.removeEventListener('click', onOfficialExpand, true) })
3081
+ }
2975
3082
  // i18n:注册双语词典;模块级 T 供组件外回调(菜单/错误横幅)取当前语言文案。
2976
3083
  if (ctx.locale && typeof ctx.locale.register === 'function') {
2977
3084
  ctx.effect(function () { return ctx.locale.register(NS, DICT) }, 'dsh-soup: dictionaries')
@@ -3002,6 +3109,20 @@ window.__ModuleLoader__.load({
3002
3109
  },
3003
3110
  ))
3004
3111
  })
3112
+ // DSH 0.1.5 moved the right column from the legacy `details` seat to
3113
+ // `rightbar`. Register at a lower priority so dsh-soup remains the
3114
+ // occupant when the official Sidebar plugin is present.
3115
+ slots.inject('rightbar', function () {
3116
+ disposers.push(slots.register(
3117
+ { name: 'rightbar', priority: -1, locale: NS },
3118
+ function (props) {
3119
+ return create(React.Fragment, null,
3120
+ create(Panel, props),
3121
+ create(PreviewOverlay),
3122
+ )
3123
+ },
3124
+ ))
3125
+ })
3005
3126
  // 文件标签页:与 对话/轨迹 同级的原生 view tab(ui-trajectory 同款注册方式)。
3006
3127
  // 槽位 entry 是静态的,多文件由 FilesView 内部子 tab 条管理。
3007
3128
  slots.inject('conversation.view', function () {
package/lib/index.js CHANGED
@@ -690,14 +690,22 @@ export function apply(ctx) {
690
690
  }
691
691
  const dirRp = await confine(dir, '目录', sessionIdArg)
692
692
  const target = await confine(joinPath(dirRp, fileName), '上传路径', sessionIdArg)
693
- // 分块上传(JupyterLab 同款语义):chunk=1/缺省覆盖写,chunk>=2 追加。
694
- // 无需服务端状态——按块序到达即顺序落盘。
693
+ // 分块上传:首块先做同名冲突检查;客户端确认覆盖后才写入,
694
+ // 后续块追加到已创建的目标。
695
695
  const chunk = Number((args && args.chunk) || 0)
696
696
  if (chunk < 0 || !Number.isInteger(chunk)) return { ok: false, error: '无效的 chunk 序号' }
697
697
  try {
698
698
  const bytes = Buffer.from(data.replace(/\s+/g, ''), 'base64')
699
- if (chunk >= 2) await appendFile(target, bytes)
700
- else await writeFile(target, bytes)
699
+ if (chunk >= 2) {
700
+ await appendFile(target, bytes)
701
+ return { ok: true, path: target }
702
+ }
703
+ if (!(args && args.overwrite)) {
704
+ try { await stat(target); return { ok: false, conflict: true, error: '目标文件已存在' } } catch (err) {
705
+ if (!err || err.code !== 'ENOENT') throw err
706
+ }
707
+ }
708
+ await writeFile(target, bytes)
701
709
  return { ok: true, path: target }
702
710
  } catch (err) {
703
711
  return { ok: false, error: `写入失败: ${String((err && err.message) || err)}` }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lyhue1991/dsh-soup",
3
3
  "description": "DSH 项目资源管理器:右侧 details 列文件树(并列网格、宽度对齐宿主 details 合同 300-520px),有 Session log 时在其右侧、空白新会话在输入框上方右端提供 📁 切换按钮;支持多选、双击展开/打开、右键菜单(打开/系统打开/重命名/废纸篓/新建/复制路径)、拖拽移动、上传。文件「打开」在会话区新增「文件」标签页(原生 conversation.view,与对话/轨迹同级):文本可编辑 ⌘S 保存、图片预览、二进制提示。附带模型吞吐徽标:输入框正上方显示「正在等待模型...」与彩色 t/s 徽标(数据来自 llm/stream 真实流)。附带 GoalBar 多行展示:把原生单行截断 GoalBar 放开为多行完整显示,编辑用 textarea,工具与数据仍走原生 goal。",
4
- "version": "0.4.11",
4
+ "version": "0.5.1",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "exports": {