@240xu/dsh-message-ops 0.3.0 → 0.3.2

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 +18 -2
  2. package/package.json +1 -1
  3. package/src/client.js +67 -22
package/README.md CHANGED
@@ -161,8 +161,8 @@ MIT
161
161
  针对 dsh 0.2.0-rc.2 的用户级操作面(agent 工具保留,但 UI 为主推入口):
162
162
 
163
163
  - **每条 AI 消息旁的原生回撤按钮**:注入官方 `conversation.chat.assistant-actions`
164
- 槽(0.2.0 正式扩展点,条目收持久化 messageId)——「⏪ 回滚到此 / ✂ 删除此条 /
165
- ⑂ 从此分支」直接出现在消息操作行,与官方点赞/点踩并列;0.1.x 无此槽自动 no-op。
164
+ 槽(0.2.0 正式扩展点,条目收持久化 messageId)——「回滚到此 / 删除此条 /
165
+ 从此分支」直接出现在消息操作行,与官方点赞/点踩并列;0.1.x 无此槽自动 no-op。
166
166
  messageId→seq 反查经 `/api/message-ops/messages`(每会话索引缓存)。
167
167
  - **分支双路径**:0.2.0+ 优先官方 `sessions.fork({sessionId, atSeq, increaseTitle})`
168
168
  (子会话自动进宿主列表,成功后可一键 `uiWorkspace.openSession` 打开);
@@ -172,3 +172,19 @@ MIT
172
172
  确认弹窗内联语义说明(评审缺口销账)。
173
173
  - 官方已有但易混淆的:0.2.0 原生分支按钮(仅限已完成轮次最后一条消息)与
174
174
  本插件的「任意位置分支/回滚」互补不冲突。
175
+
176
+ ### 0.3.1 · 槽按钮重构(opencode 式一键 + 去重)
177
+
178
+ - **一键立即执行**:回滚/删除点击即生效(无对话框),devkit toast 反馈;宿主
179
+ 0.2.0 chat store 原生响应 surface replace——遮蔽即时呈现,无需刷新页面。
180
+ - **与官方去重**:槽内移除分支按钮——官方 0.2.0 已在消息操作行自带分支
181
+ (限已完成轮次最后一条);任意位置分支保留在对话框(官方没有的能力)。
182
+ - **原生观感**:按钮改用官方 primitives 图标(IconClockOutline/IconTrashOutline,
183
+ require 失败回退内联 SVG);S3 命中区 ≥44px;S11 双提交防护(busy 互斥)。
184
+
185
+ ### 0.3.2 · 图标规范化
186
+
187
+ - 槽按钮去掉文字 emoji——0.2.0+ 直接用官方 primitives 图标(与原生操作行 1:1 同款);
188
+ 回退路径的 SVG 也改为官方 IconClockOutline/IconTrashOutline 的精确 path 数据
189
+ (1px stroke,与 Regular 变体一致),不再手绘。
190
+ - 字典文案去 emoji(label 仅用于 tooltip/aria,不参与渲染)。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@240xu/dsh-message-ops",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Roll back, branch, and export any DSH conversation — from the UI or straight from the agent via a message_ops tool. Non-destructive: logs stay append-only and recoverable. 消息回滚/删除/分支/导出四合一,内置 message_ops agent 工具;非破坏语义,日志只追加可恢复。",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/client.js CHANGED
@@ -94,9 +94,9 @@ window.__ModuleLoader__.load({
94
94
  'op.branchDesc': 'Copies everything up to and including the picked message into a new session (original untouched, non-destructive).',
95
95
  'op.restore': 'Restore (replay shadowed messages)',
96
96
  'op.restoreDesc': 'Only when the selected row is a revert/delete marker. This is a replay, not an un-shadow: shadowed user/assistant messages are re-appended with NEW seqs and a [Restored] prefix; non-replayable events (tool calls) are skipped and counted.',
97
- 'slot.revert': '⏪ Revert here',
98
- 'slot.delete': '✂ Delete this',
99
- 'slot.branch': '⑂ Branch here',
97
+ 'slot.revert': 'Revert to here',
98
+ 'slot.delete': 'Delete this message',
99
+ 'slot.branch': 'Branch from here',
100
100
  'toast.openNew': 'Open new session',
101
101
  'fork.official': 'Branched (official fork): new session {id}',
102
102
  'fork.disk': 'Branched: new session {id} (visible after list refresh)',
@@ -523,10 +523,10 @@ window.__ModuleLoader__.load({
523
523
  }
524
524
 
525
525
  // 成功反馈统一出口:devkit 标准 toast 优先,无 devkit 时对话框内文案兜底。
526
- function notifyDone(msg) {
526
+ function notifyDone(msg, kind) {
527
527
  try {
528
528
  const dk = window.__dshDevkit
529
- if (dk && typeof dk.toast === 'function') dk.toast(msg, { kind: 'ok' })
529
+ if (dk && typeof dk.toast === 'function') dk.toast(msg, { kind: kind || 'ok' })
530
530
  } catch { /* toast 缺席不阻断成功反馈 */ }
531
531
  }
532
532
 
@@ -550,6 +550,30 @@ window.__ModuleLoader__.load({
550
550
  }, React.createElement(BranchIcon))
551
551
  }
552
552
 
553
+ // 官方图标适配:0.2.0 primitives 的 IconClock/IconTrash 与原生操作行同款;
554
+ // require 失败(0.1.x 或裁剪环境)回退内联 SVG,槽在 0.1.x 本就不存在,仅防御。
555
+ var IconClock = null
556
+ var IconTrash = null
557
+ try {
558
+ const P = require('@deepseek-ai/dsh-client-ui-primitives')
559
+ IconClock = P && P.IconClockOutlineRegular
560
+ IconTrash = P && P.IconTrashOutlineRegular
561
+ } catch { /* fallback below */ }
562
+ if (!IconClock) IconClock = function ClockFallback() {
563
+ // 官方 IconClockOutlineArtwork 1:1 路径( Regular = 1px stroke)
564
+ return React.createElement('svg', { width: 16, height: 16, viewBox: '0 0 16 16', fill: 'none', 'aria-hidden': true, stroke: 'currentColor', strokeWidth: 1 },
565
+ React.createElement('path', { d: 'M8 14C11.3137 14 14 11.3137 14 8C14 4.68629 11.3137 2 8 2C4.68629 2 2 4.68629 2 8C2 11.3137 4.68629 14 8 14Z' }),
566
+ React.createElement('path', { d: 'M8 4.31V8.46L11 10.08' }))
567
+ }
568
+ if (!IconTrash) IconTrash = function TrashFallback() {
569
+ // 官方 IconTrashOutlineArtwork 1:1 路径
570
+ return React.createElement('svg', { width: 16, height: 16, viewBox: '0 0 16 16', fill: 'none', 'aria-hidden': true, stroke: 'currentColor', strokeWidth: 1 },
571
+ React.createElement('path', { d: 'M1.28149 3.88831H14.7187' }),
572
+ React.createElement('path', { d: 'M5.41602 3.88833V2.47962C5.41602 2.29282 5.52492 2.11366 5.71876 1.98157C5.9126 1.84948 6.17551 1.77527 6.44964 1.77527H9.55053C9.82466 1.77527 10.0876 1.84948 10.2814 1.98157C10.4752 2.11366 10.5841 2.29282 10.5841 2.47962V3.88833' }),
573
+ React.createElement('path', { d: 'M3.29749 5.10193L4.06585 13.0192C4.10899 13.4595 4.48223 13.7942 4.92504 13.7942H11.0751C11.5179 13.7942 11.8912 13.4595 11.9343 13.0192L12.7027 5.10193' }),
574
+ React.createElement('path', { d: 'M6.27637 7.51831V11.1829M9.72378 7.51831V11.1829' }))
575
+ }
576
+
553
577
  // --- 0.3.0 assistant-actions 官方槽:每条 AI 消息旁的原生回撤按钮 ------------
554
578
  // 契约(dsh-cordis-client-runner):scope session,条目组件收 { messageId } +
555
579
  // 标准props(useSessions/sessionId/useSession/useChat…)。0.1.x 无此 key,
@@ -558,22 +582,21 @@ window.__ModuleLoader__.load({
558
582
  const CHAT_ACTIONS_ID = 'message-ops-row'
559
583
 
560
584
  function MsgSlotActions(props) {
561
- const { messageId, sessionId, useSessions, useSession } = props
585
+ const { messageId, sessionId, useSessions } = props
562
586
  const t = (props && props.t) || __t
563
587
  useLocaleRevision()
564
588
  const sessions = useSessions ? useSessions((s) => s) : undefined
565
589
  const summary = sessions && sessions.byId ? sessions.byId[sessionId] : undefined
566
590
  const running = summary ? summary.running === true : false
567
- // messageId → seq 反查:官方槽只给 messageId;seq 由槽按钮需要。
568
- // 路径:messages 端点一次性拉取建索引(每会话缓存),失败则按钮隐藏。
569
591
  const [seq, setSeq] = React.useState(null)
592
+ const [busy, setBusy] = React.useState(null) // 'revert' | 'delete' | null(S11 双提交防护)
570
593
  React.useEffect(() => {
571
594
  if (!messageId || !sessionId) return
572
595
  let alive = true
573
596
  const cached = __seqIndexCache.get(sessionId)
574
597
  const resolve = (index) => {
575
598
  if (!alive) return
576
- const hit = index.get(String(messageId))
599
+ const hit = index && index.get(String(messageId))
577
600
  if (typeof hit === 'number') setSeq(hit)
578
601
  }
579
602
  if (cached && typeof cached.then === 'function') cached.then(resolve).catch(() => {})
@@ -594,20 +617,42 @@ window.__ModuleLoader__.load({
594
617
  }
595
618
  return () => { alive = false }
596
619
  }, [messageId, sessionId])
620
+
621
+ // opencode 式一键回撤:点击立即执行(无对话框),成功走 devkit toast。
622
+ // 宿主 0.2.0 chat store 原生响应 surface replace 事件——遮蔽无需刷新页面。
623
+ const run = (mode) => {
624
+ if (busy || seq == null || running) return
625
+ setBusy(mode)
626
+ fetch('/api/message-ops/' + mode, {
627
+ method: 'POST', headers: { 'content-type': 'application/json' },
628
+ body: JSON.stringify({ sessionId, seq }),
629
+ })
630
+ .then(async (res) => {
631
+ let data = {}
632
+ try { data = await res.json() } catch { /* keep {} */ }
633
+ if (!res.ok || !data.ok) throw new Error(data.error || ('HTTP ' + res.status))
634
+ notifyDone(t(mode === 'revert' ? 'done.revert' : 'done.delete'))
635
+ })
636
+ .catch((reason) => {
637
+ notifyDone(t('errorPrefix') + (reason && reason.message ? reason.message : String(reason)), 'error')
638
+ })
639
+ .finally(() => setBusy(null))
640
+ }
641
+
597
642
  if (seq == null) return null
598
- const open = (mode) => window.dispatchEvent(new CustomEvent(EVENT, {
599
- detail: { sessionId, seq, mode, title: summary && summary.title ? summary.title : null, running },
600
- }))
601
- const btn = (label, mode, disabled) => React.createElement('button', {
602
- type: 'button', title: t('slot.' + mode), 'aria-label': t('slot.' + mode), disabled: !!disabled,
603
- style: { padding: '2px 6px', border: 'none', background: 'transparent', color: 'inherit',
604
- opacity: disabled ? 0.4 : 0.72, cursor: disabled ? 'default' : 'pointer', font: 'inherit', fontSize: 12 },
605
- onClick: () => { if (!disabled) open(mode) },
606
- }, label)
607
- return React.createElement('span', { style: { display: 'inline-flex', gap: 2 } },
608
- btn(t('slot.revert'), 'revert', running),
609
- btn(t('slot.delete'), 'delete', running),
610
- btn(t('slot.branch'), 'branch', false),
643
+ // 官方图标(与原生操作行同款);S3:视觉 16px,命中区 ≥44px。
644
+ const act = (icon, label, mode) => React.createElement('button', {
645
+ type: 'button', title: running ? t('dialog.runningWarn') : t('slot.' + mode),
646
+ 'aria-label': t('slot.' + mode), disabled: !!busy || running,
647
+ style: { display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
648
+ minWidth: 44, minHeight: 44, padding: 0, border: 'none', background: 'transparent',
649
+ color: 'inherit', opacity: busy && busy !== mode ? 0.4 : running ? 0.35 : 0.72,
650
+ cursor: busy || running ? 'default' : 'pointer', borderRadius: 6 },
651
+ onClick: () => run(mode),
652
+ }, icon)
653
+ return React.createElement('span', { style: { display: 'inline-flex', gap: 0 } },
654
+ act(React.createElement(IconClock, { size: 16 }), t('slot.revert'), 'revert'),
655
+ act(React.createElement(IconTrash, { size: 16 }), t('slot.delete'), 'delete'),
611
656
  )
612
657
  }
613
658