@lyhue1991/dsh-soup 0.4.11 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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,6 +93,10 @@ 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
101
  '.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
102
  '.expl-menu-ico{flex:none;display:inline-flex;align-items:center;}.expl-menu-ico svg{width:14px;height:14px;display:block;}' +
@@ -260,6 +267,10 @@ window.__ModuleLoader__.load({
260
267
  'explorer.uploading': '上传中 ',
261
268
  'explorer.downloading': '下载中 ',
262
269
  'explorer.uploaded': '已上传 {n} 个文件',
270
+ 'explorer.overwriteConfirm': '文件“{name}”已存在,是否覆盖?',
271
+ 'explorer.overwriteTitle': '确认覆盖',
272
+ 'explorer.overwrite': '覆盖',
273
+ 'explorer.cancel': '取消',
263
274
  'explorer.filesCount': '{n} 个文件',
264
275
  'explorer.filesCountPartial': '{ok}/{total} 个文件',
265
276
  'explorer.downloaded': '已下载 {n} 个文件',
@@ -324,6 +335,10 @@ window.__ModuleLoader__.load({
324
335
  'explorer.uploading': 'Uploading ',
325
336
  'explorer.downloading': 'Downloading ',
326
337
  'explorer.uploaded': 'Uploaded {n} file(s)',
338
+ 'explorer.overwriteConfirm': 'The file "{name}" already exists. Overwrite it?',
339
+ 'explorer.overwriteTitle': 'Confirm overwrite',
340
+ 'explorer.overwrite': 'Overwrite',
341
+ 'explorer.cancel': 'Cancel',
327
342
  'explorer.filesCount': '{n} file(s)',
328
343
  'explorer.filesCountPartial': '{ok}/{total} file(s)',
329
344
  'explorer.downloaded': 'Downloaded {n} file(s)',
@@ -387,6 +402,7 @@ window.__ModuleLoader__.load({
387
402
  dropTarget: null, dragPaths: null,
388
403
  files: { list: [], active: null, overlay: false, overlayMax: false, overlayReturn: null },
389
404
  uploads: [],
405
+ uploadConflict: null,
390
406
  }
391
407
  var listeners = new Set()
392
408
  // trackSession() is called by both the hero toggle and the details panel
@@ -1140,9 +1156,12 @@ window.__ModuleLoader__.load({
1140
1156
  }
1141
1157
 
1142
1158
  function finishUploads(count, okCount, failedName, down) {
1143
- var verb = down ? T('explorer.downloaded') : T('explorer.uploaded')
1159
+ // These translations own the `{n}` placeholder; passing no params leaves
1160
+ // the literal token visible in the green completion notice.
1161
+ var verb = down ? T('explorer.downloaded', { n: okCount }) : T('explorer.uploaded', { n: okCount })
1162
+ var suffix = okCount === count ? '' : ' · ' + T('explorer.filesCountPartial', { ok: okCount, total: count })
1144
1163
  setUploadEntry({
1145
- name: '✓ ' + verb + ' ' + (okCount === count ? T('explorer.filesCount', { n: count }) : T('explorer.filesCountPartial', { ok: okCount, total: count }) + (failedName ? '(' + failedName + ')' : '')),
1164
+ name: '✓ ' + verb + suffix + (failedName ? '(' + failedName + ')' : ''),
1146
1165
  loaded: 0, total: 0, idx: 0, totalFiles: 0, done: true, down: !!down,
1147
1166
  })
1148
1167
  window.setTimeout(function () { setState({ uploads: [] }) }, 1600)
@@ -1157,10 +1176,17 @@ window.__ModuleLoader__.load({
1157
1176
  var start = c * UPLOAD_CHUNK_SIZE
1158
1177
  var blob = f.slice(start, Math.min(f.size, start + UPLOAD_CHUNK_SIZE))
1159
1178
  var b64 = await fileToBase64(blob)
1160
- var res = await rpc('upload', {
1179
+ var args = {
1161
1180
  dir: dir, name: f.name, data: b64,
1162
1181
  chunk: chunked ? c + 1 : undefined,
1163
- })
1182
+ }
1183
+ var res = await rpc('upload', args)
1184
+ if (res && res.conflict) {
1185
+ var overwrite = await askOverwrite(f.name)
1186
+ if (!overwrite) throw new Error('已取消上传')
1187
+ args.overwrite = true
1188
+ res = await rpc('upload', args)
1189
+ }
1164
1190
  if (!res || !res.ok) throw new Error((res && res.error) || T('explorer.uploadFail'))
1165
1191
  updateUpload(f.name, Math.min(f.size, start + UPLOAD_CHUNK_SIZE), f.size, idx, totalFiles)
1166
1192
  }
@@ -1492,6 +1518,13 @@ window.__ModuleLoader__.load({
1492
1518
  var last = parseFloat(parts[parts.length - 1])
1493
1519
  if (isNaN(last) || last <= 1) { setState({ open: false }); return }
1494
1520
  setState({ open: true })
1521
+ // Official 0.1.5 rightbar defaults to a viewport ratio. Keep
1522
+ // it within dsh-soup's 300-520px explorer contract.
1523
+ if (last < DETAILS_MIN || last > DETAILS_MAX) {
1524
+ if (Math.round(last) !== DETAILS_DEFAULT) setFrameWidth(DETAILS_DEFAULT)
1525
+ widthRef.current = DETAILS_DEFAULT
1526
+ return
1527
+ }
1495
1528
  if (Math.round(last) !== Math.round(widthRef.current)) {
1496
1529
  // 宿主是宽度偏好的权威(/native drag/窗口求解都可能改它)。
1497
1530
  // 插件只同步基准,不能把宿主拖出来的宽度强行拉回旧默认。
@@ -1584,7 +1617,10 @@ window.__ModuleLoader__.load({
1584
1617
  create('button', {
1585
1618
  className: 'expl-btn',
1586
1619
  onClick: function (event) {
1587
- try { layout.closeDetails() } catch (e) {}
1620
+ try {
1621
+ if (layout && typeof layout.closeRightbar === 'function') layout.closeRightbar()
1622
+ else if (layout && typeof layout.closeDetails === 'function') layout.closeDetails()
1623
+ } catch (e) {}
1588
1624
  setHeroDetailsWidth(event.currentTarget, 0)
1589
1625
  },
1590
1626
  title: T('explorer.close'),
@@ -1615,6 +1651,16 @@ window.__ModuleLoader__.load({
1615
1651
  container,
1616
1652
  s.menu ? create('div', { className: 'expl-menu-mask', onMouseDown: function () { setState({ menu: null }) }, onContextMenu: function (e) { e.preventDefault(); setState({ menu: null }) } }) : null,
1617
1653
  s.menu ? create(Menu, { menu: s.menu }) : null,
1654
+ s.uploadConflict ? create('div', { className: 'expl-confirm-mask', role: 'dialog', 'aria-modal': 'true' },
1655
+ create('div', { className: 'expl-confirm' },
1656
+ create('div', { className: 'expl-confirm-title' }, T('explorer.overwriteTitle')),
1657
+ create('div', { className: 'expl-confirm-text' }, T('explorer.overwriteConfirm', { name: s.uploadConflict.name })),
1658
+ create('div', { className: 'expl-confirm-actions' },
1659
+ create('button', { type: 'button', className: 'expl-confirm-btn', onClick: function () { s.uploadConflict.resolve(false); setState({ uploadConflict: null }) } }, T('explorer.cancel')),
1660
+ 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')),
1661
+ ),
1662
+ ),
1663
+ ) : null,
1618
1664
  )
1619
1665
  }
1620
1666
 
@@ -1631,7 +1677,15 @@ window.__ModuleLoader__.load({
1631
1677
  return create('button', {
1632
1678
  type: 'button',
1633
1679
  className: 'expl-toggle expl-tool' + (s.open ? ' expl-active' : ''),
1634
- onClick: function () { try { if (s.open) layout.closeDetails(); else layout.openDetails() } catch (e) {} },
1680
+ onClick: function () {
1681
+ try {
1682
+ if (layout && typeof layout.closeRightbar === 'function') {
1683
+ if (s.open) layout.closeRightbar()
1684
+ else layout.openRightbar(true, false)
1685
+ } else if (s.open) layout.closeDetails()
1686
+ else layout.openDetails()
1687
+ } catch (e) {}
1688
+ },
1635
1689
  title: T('explorer.label'),
1636
1690
  'aria-label': T('explorer.label'),
1637
1691
  }, '📁')
@@ -1661,10 +1715,16 @@ window.__ModuleLoader__.load({
1661
1715
  className: 'expl-toggle expl-tool' + (s.open ? ' expl-active' : ''),
1662
1716
  onClick: function (event) {
1663
1717
  if (s.open) {
1664
- try { layout.closeDetails() } catch (e) {}
1718
+ try {
1719
+ if (layout && typeof layout.closeRightbar === 'function') layout.closeRightbar()
1720
+ else if (layout && typeof layout.closeDetails === 'function') layout.closeDetails()
1721
+ } catch (e) {}
1665
1722
  setHeroDetailsWidth(event.currentTarget, 0)
1666
1723
  } else if (!setHeroDetailsWidth(event.currentTarget, DETAILS_DEFAULT)) {
1667
- try { layout.openDetails() } catch (e) {}
1724
+ try {
1725
+ if (layout && typeof layout.openRightbar === 'function') layout.openRightbar(true, false)
1726
+ else if (layout && typeof layout.openDetails === 'function') layout.openDetails()
1727
+ } catch (e) {}
1668
1728
  }
1669
1729
  },
1670
1730
  title: T('explorer.label'),
@@ -1719,6 +1779,12 @@ window.__ModuleLoader__.load({
1719
1779
  } catch (_) {}
1720
1780
  }
1721
1781
 
1782
+ function askOverwrite(name) {
1783
+ return new Promise(function (resolve) {
1784
+ setState({ uploadConflict: { name: name, resolve: resolve } })
1785
+ })
1786
+ }
1787
+
1722
1788
  function findFileEntry(path) {
1723
1789
  for (var i = 0; i < state.files.list.length; i++) {
1724
1790
  if (state.files.list[i].path === path) return state.files.list[i]
@@ -2972,6 +3038,30 @@ window.__ModuleLoader__.load({
2972
3038
  var sessions = ctx.sessions
2973
3039
  var remoteGoals = ctx.remote.goals
2974
3040
  var disposers = []
3041
+ // 0.1.5's official Sidebar adds a header-corner expand button. Its
3042
+ // default handler opens the official surface, which would diverge from
3043
+ // dsh-soup's resource explorer. Route that button to the same layout
3044
+ // face used by our explorer controls instead.
3045
+ if (typeof document !== 'undefined') {
3046
+ var onOfficialExpand = function (event) {
3047
+ var target = event.target && event.target.closest
3048
+ ? event.target.closest('[data-sidebar-right-expand]') : null
3049
+ if (!target) return
3050
+ event.preventDefault()
3051
+ if (typeof event.stopImmediatePropagation === 'function') event.stopImmediatePropagation()
3052
+ else event.stopPropagation()
3053
+ try {
3054
+ if (state.open) {
3055
+ if (layout && typeof layout.closeRightbar === 'function') layout.closeRightbar()
3056
+ else if (layout && typeof layout.closeDetails === 'function') layout.closeDetails()
3057
+ } else if (layout && typeof layout.openRightbar === 'function') {
3058
+ layout.openRightbar(true, false)
3059
+ } else if (layout && typeof layout.openDetails === 'function') layout.openDetails()
3060
+ } catch (e) {}
3061
+ }
3062
+ document.addEventListener('click', onOfficialExpand, true)
3063
+ disposers.push(function () { document.removeEventListener('click', onOfficialExpand, true) })
3064
+ }
2975
3065
  // i18n:注册双语词典;模块级 T 供组件外回调(菜单/错误横幅)取当前语言文案。
2976
3066
  if (ctx.locale && typeof ctx.locale.register === 'function') {
2977
3067
  ctx.effect(function () { return ctx.locale.register(NS, DICT) }, 'dsh-soup: dictionaries')
@@ -3002,6 +3092,20 @@ window.__ModuleLoader__.load({
3002
3092
  },
3003
3093
  ))
3004
3094
  })
3095
+ // DSH 0.1.5 moved the right column from the legacy `details` seat to
3096
+ // `rightbar`. Register at a lower priority so dsh-soup remains the
3097
+ // occupant when the official Sidebar plugin is present.
3098
+ slots.inject('rightbar', function () {
3099
+ disposers.push(slots.register(
3100
+ { name: 'rightbar', priority: -1, locale: NS },
3101
+ function (props) {
3102
+ return create(React.Fragment, null,
3103
+ create(Panel, props),
3104
+ create(PreviewOverlay),
3105
+ )
3106
+ },
3107
+ ))
3108
+ })
3005
3109
  // 文件标签页:与 对话/轨迹 同级的原生 view tab(ui-trajectory 同款注册方式)。
3006
3110
  // 槽位 entry 是静态的,多文件由 FilesView 内部子 tab 条管理。
3007
3111
  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.0",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "exports": {