@lyhue1991/dsh-soup 0.5.6 → 0.5.8

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.
@@ -72,8 +72,9 @@ export function createExplorerData(ctx) {
72
72
  async function loadDir(path, attempt) {
73
73
  var res = await rpc('list', { path: path, sessionId: getActiveSessionId() })
74
74
  // 新会话刚切换时,宿主会话注册表可能尚未纳入其 cwd——稍候重试。
75
- if ((!res || !res.ok) && /超出允许范围/.test((res && res.error) || '') && (attempt || 0) < 3) {
76
- await new Promise(function (r) { setTimeout(r, 600) })
75
+ // 会话 attach(含冷会话持久化读取)可能超过 2s,放宽退避至 6 次 × 1s。
76
+ if ((!res || !res.ok) && /超出允许范围/.test((res && res.error) || '') && (attempt || 0) < 6) {
77
+ await new Promise(function (r) { setTimeout(r, 1000) })
77
78
  return loadDir(path, (attempt || 0) + 1)
78
79
  }
79
80
  if (!res || !res.ok) {
@@ -230,7 +231,7 @@ export function createExplorerData(ctx) {
230
231
  if (!paths.length) return 'idle'
231
232
  autoTicking = true
232
233
  try {
233
- var res = await rpc('mtime', { paths: paths })
234
+ var res = await rpc('mtime', { paths: paths, sessionId: getActiveSessionId() })
234
235
  if (!res || !res.ok) return false
235
236
  var mtimes = res.mtimes || {}
236
237
  var dirtyDirs = []
@@ -597,6 +598,7 @@ export function createExplorerData(ctx) {
597
598
  var b64 = await fileToBase64(blob)
598
599
  var args = {
599
600
  dir: dir, name: f.name, data: b64,
601
+ sessionId: getActiveSessionId(),
600
602
  chunk: chunked ? c + 1 : undefined,
601
603
  }
602
604
  var res = await rpc('upload', args)
@@ -635,8 +637,9 @@ export function createExplorerData(ctx) {
635
637
  async function downloadFile(path) {
636
638
  setState({ menu: null })
637
639
  var res = await rpc('download', { path: path, sessionId: getActiveSessionId() })
640
+ // 下载同样可能落在会话切换空窗:403 时等待宿主 attach 后重试一次。
638
641
  if (!res.ok && /超出允许范围/.test(res.error || '')) {
639
- await new Promise(function (r) { setTimeout(r, 600) })
642
+ await new Promise(function (r) { setTimeout(r, 1000) })
640
643
  res = await rpc('download', { path: path, sessionId: getActiveSessionId() })
641
644
  }
642
645
  if (!res || !res.ok) {
@@ -686,9 +689,9 @@ export function createExplorerData(ctx) {
686
689
  }
687
690
  }
688
691
 
689
- function onUploadPicker(e) {
692
+ function onUploadPicker(e, targetDir) {
690
693
  var files = Array.from((e.target && e.target.files) || [])
691
- uploadFiles(getState().cwd, files)
694
+ uploadFiles(targetDir || getState().cwd, files)
692
695
  e.target.value = ''
693
696
  }
694
697
 
@@ -17,6 +17,7 @@ export function createExplorerView(ctx) {
17
17
  var getLayout = ctx.getLayout
18
18
  var visibleRows = ctx.visibleRows
19
19
  var uploadInputEl = null
20
+ var uploadTargetDir = null
20
21
  var DETAILS_MIN = 300
21
22
  var DETAILS_MAX = 520
22
23
  var DETAILS_DEFAULT = 360
@@ -51,6 +52,18 @@ export function createExplorerView(ctx) {
51
52
  var trackSession = ctx.trackSession
52
53
  var onUploadPicker = ctx.onUploadPicker
53
54
  var openFileInTab = ctx.openFileInTab
55
+
56
+ function chooseUploadDir(dir) {
57
+ uploadTargetDir = dir || getState().cwd
58
+ setState({ menu: null })
59
+ if (uploadInputEl) uploadInputEl.click()
60
+ }
61
+
62
+ function onUploadInputChange(e) {
63
+ var targetDir = uploadTargetDir || getState().cwd
64
+ uploadTargetDir = null
65
+ onUploadPicker(e, targetDir)
66
+ }
54
67
  // ▓▓ 区域一 · 资源管理器 · 🧅 葱(列表 UI)
55
68
  // NameInput / Row / buildMenuItems / Menu:行渲染、多选、右键菜单、重命名/新建。
56
69
  // ------------------------------------------------------------------
@@ -183,7 +196,8 @@ export function createExplorerView(ctx) {
183
196
  }
184
197
  if (node.type === 'directory') {
185
198
  items.push({ key: 'newfile', label: T('menu.newFile'), onClick: function () { startNew(node.path, false) } })
186
- 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 })
199
+ 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) } })
200
+ items.push({ key: 'upload', label: T('menu.upload'), onClick: function () { chooseUploadDir(node.path) }, separatorAfter: true })
187
201
  }
188
202
  items.push({ key: 'copy', label: T('menu.copyPath'), onClick: function () { copyPath(node) } })
189
203
  if (node.type !== 'directory') {
@@ -392,7 +406,7 @@ export function createExplorerView(ctx) {
392
406
  }, create('span', { className: 'expl-icon', dangerouslySetInnerHTML: { __html: ICON_NEW_FOLDER } })),
393
407
  create('button', {
394
408
  className: 'expl-btn',
395
- onClick: function () { if (uploadInputEl) uploadInputEl.click() },
409
+ onClick: function () { chooseUploadDir(getState().cwd) },
396
410
  title: T('explorer.upload'),
397
411
  'aria-label': T('explorer.upload'),
398
412
  }, '⬆'),
@@ -431,7 +445,7 @@ export function createExplorerView(ctx) {
431
445
  )
432
446
  }),
433
447
  ) : null,
434
- create('input', { type: 'file', multiple: true, style: { display: 'none' }, ref: function (el) { uploadInputEl = el }, onChange: onUploadPicker }),
448
+ create('input', { type: 'file', multiple: true, style: { display: 'none' }, ref: function (el) { uploadInputEl = el }, onChange: onUploadInputChange }),
435
449
  s.error ? create('div', { className: 'expl-error' }, s.error) : null,
436
450
  s.notice ? create('div', { className: 'expl-notice' }, s.notice) : null,
437
451
  s.selected.size > 1
@@ -56,6 +56,7 @@ export var DICT = {
56
56
  'menu.trashMulti': '🗑 移到废纸篓 ({n} 项)',
57
57
  'menu.newFile': '📄 新建文件',
58
58
  'menu.newFolder': '新建文件夹',
59
+ 'menu.upload': '⬆ 上传文件',
59
60
  'menu.copyPath': '⧉ 复制路径',
60
61
  'menu.download': '⬇ 下载',
61
62
  'menu.deselect': '取消选择',
@@ -145,6 +146,7 @@ export var DICT = {
145
146
  'menu.trashMulti': '🗑 Move to trash ({n})',
146
147
  'menu.newFile': '📄 New file',
147
148
  'menu.newFolder': 'New folder',
149
+ 'menu.upload': '⬆ Upload files',
148
150
  'menu.copyPath': '⧉ Copy path',
149
151
  'menu.download': '⬇ Download',
150
152
  'menu.deselect': 'Clear selection',
@@ -1,8 +1,9 @@
1
1
  /**
2
- * 速度徽标组件(区域三 · 🧂 盐):Deep diving 行内的实时 t/s 吞吐徽标。
2
+ * 速度徽标组件(区域三 · 🧂 盐):会话标题旁的实时 t/s 吞吐徽标。
3
3
  *
4
- * 不改 DSH 源码:MutationObserver 定位消息流里的 Deep diving(role=status),
5
- * 把徽标节点 append 进其行内(inline-flex 同行),实现永远紧贴。
4
+ * 挂载点:slots 槽位 conversation.session.header.actions(与官方 Agent
5
+ * 预设标签同槽,order 更大排在其后),React 状态驱动渲染,无 DOM 注入、
6
+ * 无 MutationObserver——不受消息流内部结构与文案变动影响。
6
7
  * timerRef 由 apply 生命周期赋值,经 getTimerRef 包装函数取当前值。
7
8
  */
8
9
 
@@ -10,23 +11,32 @@
10
11
  * @param {object} ctx - { React, T, rpc, getTimerRef }。
11
12
  */
12
13
  export function createSpeedBadge({ React, T, rpc, getTimerRef }) {
14
+ var create = React.createElement
15
+
13
16
  function SpeedBadge(props) {
14
- var sessionId = props && (props.sessionId || (props.session && (props.session.sessionId || props.session.id)))
17
+ // Hook 一律无条件调用(条件调用会在重渲染时打破 hook 链,
18
+ // 触发宿主 React 内部 RangeError 并让整个槽位条目崩溃卸载)。
19
+ var sessionId = props && props.sessionId
20
+ var useSessions = props && props.useSessions
21
+ var fallbackId = useSessions ? useSessions(function (list) {
22
+ var ids = list && list.ids
23
+ return ids && ids.length === 1 ? ids[0] : undefined
24
+ }) : undefined
25
+ if (!sessionId) sessionId = fallbackId
15
26
 
16
- // 持有最新状态的最新值的 ref(供 MutationObserver 回调读取)
17
- var statusRef = React.useRef(null)
18
- var dotsRef = React.useRef(1)
19
- var badgeElRef = React.useRef(null)
20
- // hostRef:当前挂载的 Deep diving 宿主元素(observer 找到后持有)
21
- var hostRef = React.useRef(null)
27
+ var st = React.useState(function () { return { phase: 'idle' } })
28
+ var status = st[0]
29
+ var setStatus = st[1]
30
+ var dt = React.useState(function () { return { dots: 1 } })
31
+ var dots = dt[0].dots
32
+ var setDots = dt[1]
22
33
 
23
- // 省略号循环
34
+ // 省略号循环(仅 waiting 阶段有视觉意义)
24
35
  React.useEffect(function () {
25
36
  var timerRef = getTimerRef()
26
37
  if (!timerRef) return
27
38
  var stop = timerRef.interval(function () {
28
- dotsRef.current = dotsRef.current >= 3 ? 1 : dotsRef.current + 1
29
- if (hostRef.current) renderBadge()
39
+ setDots(function (prev) { return prev >= 3 ? 1 : prev + 1 })
30
40
  }, 400)
31
41
  return function () { stop() }
32
42
  }, [])
@@ -39,10 +49,7 @@ export function createSpeedBadge({ React, T, rpc, getTimerRef }) {
39
49
  var cancelled = false
40
50
  var poll = function () {
41
51
  rpc('speed-status', { sessionId: sessionId }).then(function (res) {
42
- if (!cancelled && res && res.ok) {
43
- statusRef.current = res
44
- if (hostRef.current) renderBadge()
45
- }
52
+ if (!cancelled && res && res.ok) setStatus(res)
46
53
  }).catch(function () {})
47
54
  }
48
55
  poll()
@@ -50,110 +57,63 @@ export function createSpeedBadge({ React, T, rpc, getTimerRef }) {
50
57
  return function () { cancelled = true; stop() }
51
58
  }, [sessionId])
52
59
 
53
- // 渲染徽标内容到 badgeEl(由需要时调用;这里用函数声明提升,需放在 effect 外)
54
- function renderBadge() {
55
- var el = badgeElRef.current
56
- if (!el) return
57
- var st = statusRef.current
58
- // 清空
59
- while (el.firstChild) el.removeChild(el.firstChild)
60
- if (!st || st.phase === 'idle' || st.phase === 'done') {
61
- el.style.display = 'none'
62
- return
63
- }
64
- el.style.display = 'inline-flex'
65
- if (st.phase === 'waiting') {
66
- // 宿主 .turnStatus 用 background-clip:text + 渐变透明色,内部子元素
67
- // 会继承 text-fill-color 而把颜色冲成渐变;必须 important 覆盖。
68
- el.style.cssText = 'display:inline-flex;align-items:center;margin-left:10px;font-weight:400;font-size:13px;color:var(--dsw-alias-label-caption);'
69
- el.style.setProperty('color', 'var(--dsw-alias-label-caption)', 'important')
70
- el.style.setProperty('-webkit-text-fill-color', 'var(--dsw-alias-label-caption)', 'important')
71
- el.textContent = T('speed.waiting') + new Array(dotsRef.current + 1).join('.')
72
- return
73
- }
74
- // 流式阶段:token 计数立即显示;瞬时速率窗口未满(tps=0)时先不显示徽标,
75
- // 而不是回退到「正在等待模型」(否则短输出全程都显示等待)。
76
- var tps = st.tps || 0
77
- var bg = tps >= 50 ? '#53b3cb' : tps >= 30 ? '#9bc53d' : tps >= 15 ? '#f9c22e' : '#e01a4f'
78
- el.style.cssText = 'display:inline-flex;align-items:center;gap:6px;margin-left:10px;font-weight:400;font-size:11px;color:var(--dsw-alias-label-primary);-webkit-text-fill-color:var(--dsw-alias-label-primary);'
79
- var tok = document.createElement('span')
80
- tok.style.cssText = 'display:inline-flex;align-items:center;gap:4px;color:var(--dsw-alias-label-caption);-webkit-text-fill-color:var(--dsw-alias-label-caption);'
81
- var SVG = 'http://www.w3.org/2000/svg'
82
- var svg = document.createElementNS(SVG, 'svg')
83
- svg.setAttribute('width', '10'); svg.setAttribute('height', '10'); svg.setAttribute('viewBox', '0 0 10 10')
84
- svg.setAttribute('fill', 'none'); svg.setAttribute('stroke', 'currentColor'); svg.setAttribute('stroke-width', '1.2')
85
- svg.setAttribute('stroke-linecap', 'round'); svg.setAttribute('stroke-linejoin', 'round')
86
- var ln = document.createElementNS(SVG, 'line'); ln.setAttribute('x1', '5'); ln.setAttribute('y1', '1.5'); ln.setAttribute('x2', '5'); ln.setAttribute('y2', '8.5'); svg.appendChild(ln)
87
- var poly = document.createElementNS(SVG, 'polyline'); poly.setAttribute('points', '2 6 5 8.5 8 6'); svg.appendChild(poly)
88
- tok.appendChild(svg)
89
- tok.appendChild(document.createTextNode(String(Math.round(st.tokens))))
90
- el.appendChild(tok)
91
- if (tps > 0) {
92
- var pill = document.createElement('span')
93
- pill.style.cssText = 'margin-left:6px;padding:1px 6px;border-radius:4px;background:' + bg + ';color:#fff;-webkit-text-fill-color:#fff;font-size:11px;font-weight:500;'
94
- pill.textContent = tps.toFixed(1) + ' t/s'
95
- el.appendChild(pill)
96
- }
60
+ // idle/done 不渲染任何可见内容;保留隐藏占位便于 e2e 验证挂载。
61
+ if (!status || status.phase === 'idle' || status.phase === 'done') {
62
+ return create('span', { className: 'dsh-soup-speed', style: { display: 'none' }, 'data-phase': 'idle' })
97
63
  }
98
64
 
99
- // MutationObserver:定位 Deep diving 并把 badgeEl 挂进去
100
- React.useEffect(function () {
101
- if (typeof document === 'undefined' || typeof MutationObserver === 'undefined') return
102
- // 初始化徽标 DOM 节点
103
- if (badgeElRef.current === null) {
104
- badgeElRef.current = document.createElement('span')
105
- badgeElRef.current.setAttribute('data-dsh-speed-badge', '')
106
- badgeElRef.current.style.display = 'none'
107
- }
108
- var badge = badgeElRef.current
109
-
110
- function findTurnStatus() {
111
- var candidates = document.querySelectorAll('[data-chat-flow] [role="status"], [data-chat-flow] [aria-live="polite"]')
112
- for (var i = 0; i < candidates.length; i++) {
113
- var text = candidates[i].textContent || ''
114
- // DSH i18n(locale chat.deepDiving):英文 'Deep diving...' / 中文 '深度求索中...'
115
- if (text.indexOf('Deep diving') !== -1 || text.indexOf('深度求索') !== -1) return candidates[i]
116
- }
117
- return null
118
- }
119
-
120
- function attach() {
121
- // 已挂载且宿主仍在 DOM 中时无需重复查找:徽标内容由 timer/poll 驱动
122
- // renderBadge 更新,observer 只负责把徽标挂进/移出宿主元素。
123
- if (hostRef.current && hostRef.current.isConnected) {
124
- // 若 DSH 在徽标之后又追加了子元素(如 15s 后的 elapsed clock),
125
- // 把徽标移到末尾,让它始终紧跟 Deep diving 的计时。
126
- if (badge.parentNode !== hostRef.current || hostRef.current.lastElementChild !== badge) {
127
- try { hostRef.current.appendChild(badge) } catch (e) {}
128
- }
129
- return
130
- }
131
- var target = findTurnStatus()
132
- if (target !== null) {
133
- hostRef.current = target
134
- if (badge.parentNode !== target) { try { target.appendChild(badge) } catch (e) {} }
135
- renderBadge()
136
- } else if (hostRef.current !== null) {
137
- if (badge.parentNode) { try { badge.parentNode.removeChild(badge) } catch (e) {} }
138
- hostRef.current = null
139
- }
140
- }
141
-
142
- var mo = new MutationObserver(function () { attach() })
143
- mo.observe(document.body, { childList: true, subtree: true })
144
- attach()
145
- return function () {
146
- mo.disconnect()
147
- if (badge.parentNode) { try { badge.parentNode.removeChild(badge) } catch (e) {} }
148
- hostRef.current = null
149
- }
150
- }, [])
65
+ if (status.phase === 'waiting') {
66
+ return create('span', {
67
+ className: 'dsh-soup-speed',
68
+ style: {
69
+ display: 'inline-flex', alignItems: 'center',
70
+ fontSize: '12px', fontWeight: 400,
71
+ color: 'var(--dsw-alias-label-caption)',
72
+ whiteSpace: 'nowrap',
73
+ },
74
+ }, T('speed.waiting') + '...'.slice(0, dots))
75
+ }
151
76
 
152
- return null
77
+ // 流式阶段:token 计数立即显示;瞬时速率窗口未满(tps=0)时先不显示
78
+ // 速率药丸,而不是回退到等待文案(否则短输出全程都显示等待)。
79
+ var tps = status.tps || 0
80
+ var bg = tps >= 50 ? '#53b3cb' : tps >= 30 ? '#9bc53d' : tps >= 15 ? '#f9c22e' : '#e01a4f'
81
+ var children = [
82
+ create('span', {
83
+ key: 'tok',
84
+ style: {
85
+ display: 'inline-flex', alignItems: 'center', gap: '3px',
86
+ color: 'var(--dsw-alias-label-caption)', fontSize: '11px',
87
+ },
88
+ },
89
+ create('svg', {
90
+ width: '10', height: '10', viewBox: '0 0 10 10', fill: 'none',
91
+ stroke: 'currentColor', strokeWidth: '1.2',
92
+ strokeLinecap: 'round', strokeLinejoin: 'round',
93
+ },
94
+ create('line', { x1: '5', y1: '1.5', x2: '5', y2: '8.5' }),
95
+ create('polyline', { points: '2 6 5 8.5 8 6' }),
96
+ ),
97
+ String(Math.round(status.tokens || 0)),
98
+ ),
99
+ ]
100
+ if (tps > 0) {
101
+ children.push(create('span', {
102
+ key: 'pill',
103
+ style: {
104
+ padding: '1px 6px', borderRadius: '4px',
105
+ background: bg, color: '#fff',
106
+ fontSize: '11px', fontWeight: 500,
107
+ },
108
+ }, tps.toFixed(1) + ' t/s'))
109
+ }
110
+ return create('span', {
111
+ className: 'dsh-soup-speed',
112
+ style: { display: 'inline-flex', alignItems: 'center', gap: '6px', whiteSpace: 'nowrap' },
113
+ }, children)
153
114
  }
154
115
 
155
116
  return SpeedBadge
156
117
  }
157
118
 
158
119
  if (typeof window !== 'undefined') window.__DSH_SOUP_SPEED_BADGE__ = { createSpeedBadge }
159
-