@240xu/dsh-message-ops 0.4.3 → 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.
Files changed (3) hide show
  1. package/README.md +18 -0
  2. package/package.json +1 -1
  3. package/src/client.js +89 -35
package/README.md CHANGED
@@ -225,3 +225,21 @@ MIT
225
225
  - 0.4.1 的自动折叠 useEffect 位于「无标记早退」之后——标记从 0 变非 0 时多出
226
226
  第 4 个 hook,React 抛出 hooks 顺序错误并卸载整个槽(dock 永远不可见,
227
227
  Playwright 抓到 React #310)。已把该 effect 移到早退之前(无条件 hook)。
228
+
229
+ ## 0.5.0 · 「按钮没有用」根因修复 + 消息引用
230
+
231
+ - **根因**:宿主 live 投影只处理 compaction 类 surface replace——插件标记落盘后
232
+ 打开的会话视图不会收起,所以回撤/删除按钮点了「没反应」(0.3.x 引入的回归)。
233
+ - **修复**:回撤成功后经官方 `uiWorkspace.openSession(sessionId)` 重建会话视图,
234
+ 遮蔽立即呈现(无需刷新页面);dock 的恢复同样触发视图重建。
235
+ - **消息引用(新)**:AI 消息操作行新增「引用到输入框」——经官方
236
+ `InputActions.captureInsertion + insertText` 把消息全文以 markdown 引用块
237
+ (`> ` 前缀)插入输入框,零 DOM hack。
238
+ - 槽按钮收敛为两个:**⏱ 回撤到此** 与 **❝ 引用**;删除/分支保留在对话框
239
+ (官方分支按钮已覆盖最后一条消息场景)。
240
+
241
+ ### 0.5.1 · 修复 0.5.0 客户端激活失败
242
+
243
+ - 0.4.2 的 dock 重写误删了 `INPUT_DOCK_SLOT/ID` 常量声明——apply() 引用未定义
244
+ 标识符导致**整个客户端激活失败**(页面横幅 Failed to load plugins)。
245
+ 已恢复常量;新增 vm 冒烟自测路径(见 .e2e/)。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@240xu/dsh-message-ops",
3
- "version": "0.4.3",
3
+ "version": "0.5.1",
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
@@ -22,6 +22,8 @@ window.__ModuleLoader__.load({
22
22
  const SLOT = 'conversation.session.header.actions'
23
23
  const ROW_ID = 'message-ops'
24
24
  const OVERLAY_SLOT = 'shell.overlay'
25
+ const INPUT_DOCK_SLOT = 'conversation.input.dock'
26
+ const INPUT_DOCK_ID = 'message-ops-revert-dock'
25
27
  const DIALOG_ID = 'message-ops-dialog'
26
28
  const EVENT = 'dsh-message-ops:open'
27
29
  const NS = 'dsh-message-ops'
@@ -75,6 +77,9 @@ window.__ModuleLoader__.load({
75
77
  'dock.collapse': '折叠回撤列表',
76
78
  'dock.restore': '恢复',
77
79
  'dock.restoring': '恢复中…',
80
+ 'slot.quote': '引用到输入框',
81
+ 'quote.done': '已引用到输入框',
82
+ 'quote.unavailable': '引用不可用(输入框不可写或无全文)',
78
83
  'menu.ops': '消息操作',
79
84
  }
80
85
 
@@ -128,6 +133,9 @@ window.__ModuleLoader__.load({
128
133
  'dock.shadowedN': '{n} shadowed',
129
134
  'dock.restore': 'Restore',
130
135
  'dock.restoring': 'Restoring…',
136
+ 'slot.quote': 'Quote to composer',
137
+ 'quote.done': 'Quoted into composer',
138
+ 'quote.unavailable': 'Quote unavailable (composer locked or no full text)',
131
139
  'menu.ops': 'Message ops',
132
140
  }
133
141
 
@@ -137,6 +145,8 @@ window.__ModuleLoader__.load({
137
145
  var __uiWorkspace = null
138
146
  // messageId→seq 索引缓存(每会话一次拉取;0.3.0 assistant-actions 槽用)
139
147
  var __seqIndexCache = new Map()
148
+ // messageId→全文缓存(0.5.0 消息引用用)
149
+ var __fullTextCache = new Map()
140
150
 
141
151
  function localeFallbackLang() {
142
152
  if (typeof navigator === 'undefined') return 'zh'
@@ -582,6 +592,15 @@ window.__ModuleLoader__.load({
582
592
  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' }),
583
593
  React.createElement('path', { d: 'M8 4.31V8.46L11 10.08' }))
584
594
  }
595
+ var IconQuote = null
596
+ try {
597
+ const P2 = require('@deepseek-ai/dsh-client-ui-primitives')
598
+ IconQuote = P2 && (P2.IconChatOutlineRegular || P2.IconCopyOutlineRegular)
599
+ } catch { /* fallback below */ }
600
+ if (!IconQuote) IconQuote = function QuoteFallback() {
601
+ return React.createElement('svg', { width: 16, height: 16, viewBox: '0 0 16 16', fill: 'none', 'aria-hidden': true, stroke: 'currentColor', strokeWidth: 1 },
602
+ React.createElement('path', { d: 'M3 6.5A3.5 3.5 0 0 1 6.5 3H7v1.5h-.5A2 2 0 0 0 4.5 6.5V7H7v3.5H3V6.5ZM9 6.5A3.5 3.5 0 0 1 12.5 3H13v1.5h-.5A2 2 0 0 0 10.5 6.5V7H13v3.5H9V6.5Z' }))
603
+ }
585
604
  if (!IconTrash) IconTrash = function TrashFallback() {
586
605
  // 官方 IconTrashOutlineArtwork 1:1 路径
587
606
  return React.createElement('svg', { width: 16, height: 16, viewBox: '0 0 16 16', fill: 'none', 'aria-hidden': true, stroke: 'currentColor', strokeWidth: 1 },
@@ -599,15 +618,16 @@ window.__ModuleLoader__.load({
599
618
  const CHAT_ACTIONS_ID = 'message-ops-row'
600
619
 
601
620
  function MsgSlotActions(props) {
602
- const { messageId, sessionId, useSessions, inputActions } = props
603
- if (inputActions && typeof inputActions.setDraft === 'function') __inputActions = inputActions
621
+ const { messageId, sessionId, useSessions, inputActions, useChat } = props
604
622
  const t = (props && props.t) || __t
605
623
  useLocaleRevision()
624
+ if (inputActions && typeof inputActions.setDraft === 'function') __inputActions = inputActions
606
625
  const sessions = useSessions ? useSessions((s) => s) : undefined
607
626
  const summary = sessions && sessions.byId ? sessions.byId[sessionId] : undefined
608
627
  const running = summary ? summary.running === true : false
609
628
  const [seq, setSeq] = React.useState(null)
610
- const [busy, setBusy] = React.useState(null) // 'revert' | 'delete' | null(S11 双提交防护)
629
+ const [fullText, setFullText] = React.useState(null)
630
+ const [busy, setBusy] = React.useState(null) // 'revert' | null(S11 双提交防护)
611
631
  React.useEffect(() => {
612
632
  if (!messageId || !sessionId) return
613
633
  let alive = true
@@ -624,10 +644,13 @@ window.__ModuleLoader__.load({
624
644
  .then((r) => (r.ok ? r.json() : { messages: [] }))
625
645
  .then((data) => {
626
646
  const index = new Map()
647
+ const full = new Map()
627
648
  for (const m of (data && data.messages) || []) {
628
649
  if (m && m.id != null) index.set(String(m.id), m.seq)
650
+ if (m && m.id != null && m.fullText) full.set(String(m.id), m.fullText)
629
651
  }
630
652
  __seqIndexCache.set(sessionId, index)
653
+ __fullTextCache.set(sessionId, full)
631
654
  return index
632
655
  })
633
656
  __seqIndexCache.set(sessionId, p)
@@ -636,12 +659,37 @@ window.__ModuleLoader__.load({
636
659
  return () => { alive = false }
637
660
  }, [messageId, sessionId])
638
661
 
639
- // opencode 式一键回撤:点击立即执行(无对话框),成功走 devkit toast。
640
- // 宿主 0.2.0 chat store 原生响应 surface replace 事件——遮蔽无需刷新页面。
641
- const run = (mode) => {
662
+ // 原文(引用用):seq 解析后按需取全文
663
+ React.useEffect(() => {
664
+ if (seq == null || !sessionId) return
665
+ let alive = true
666
+ const cached = __fullTextCache.get(sessionId)
667
+ const resolve = (full) => {
668
+ if (!alive) return
669
+ const hit = full && full.get(String(messageId))
670
+ if (typeof hit === 'string') setFullText(hit)
671
+ }
672
+ if (cached && typeof cached.then === 'function') cached.then(resolve).catch(() => {})
673
+ else if (cached) resolve(cached)
674
+ return () => { alive = false }
675
+ }, [seq, messageId, sessionId])
676
+
677
+ // 0.5.0 回撤后的视图刷新:openSession(reveal) 重建会话视图 → 遮蔽立即呈现。
678
+ // (宿主 live 投影只处理 compaction 类 replace,插件标记需要视图重建才可见。)
679
+ const revealSession = () => {
680
+ try {
681
+ if (__uiWorkspace && typeof __uiWorkspace.openSession === 'function') {
682
+ __uiWorkspace.openSession(sessionId)
683
+ return true
684
+ }
685
+ } catch { /* fallthrough */ }
686
+ return false
687
+ }
688
+
689
+ const runRevert = () => {
642
690
  if (busy || seq == null || running) return
643
- setBusy(mode)
644
- fetch('/api/message-ops/' + mode, {
691
+ setBusy('revert')
692
+ fetch('/api/message-ops/revert', {
645
693
  method: 'POST', headers: { 'content-type': 'application/json' },
646
694
  body: JSON.stringify({ sessionId, seq }),
647
695
  })
@@ -649,7 +697,8 @@ window.__ModuleLoader__.load({
649
697
  let data = {}
650
698
  try { data = await res.json() } catch { /* keep {} */ }
651
699
  if (!res.ok || !data.ok) throw new Error(data.error || ('HTTP ' + res.status))
652
- notifyDone(t(mode === 'revert' ? 'done.revert' : 'done.delete'))
700
+ notifyDone(t('done.revert'))
701
+ revealSession()
653
702
  })
654
703
  .catch((reason) => {
655
704
  notifyDone(t('errorPrefix') + (reason && reason.message ? reason.message : String(reason)), 'error')
@@ -657,39 +706,40 @@ window.__ModuleLoader__.load({
657
706
  .finally(() => setBusy(null))
658
707
  }
659
708
 
709
+ // 0.5.0 消息引用:官方 InputActions.captureInsertion + insertText(零 DOM hack)
710
+ const runQuote = () => {
711
+ try {
712
+ if (!inputActions || typeof inputActions.captureInsertion !== 'function' || typeof inputActions.insertText !== 'function') {
713
+ notifyDone(t('quote.unavailable'), 'warn')
714
+ return
715
+ }
716
+ const span = inputActions.captureInsertion()
717
+ const text = fullText != null ? fullText : (summary && summary.title ? '' : null)
718
+ if (text == null) { notifyDone(t('quote.unavailable'), 'warn'); return }
719
+ const quoted = text.split('\n').map((l) => '> ' + l).join('\n') + '\n\n'
720
+ const ok = inputActions.insertText(quoted, span)
721
+ notifyDone(ok ? t('quote.done') : t('quote.unavailable'), ok ? 'ok' : 'warn')
722
+ } catch (e) {
723
+ notifyDone(t('errorPrefix') + (e && e.message ? e.message : String(e)), 'error')
724
+ }
725
+ }
726
+
660
727
  if (seq == null) return null
661
- // 官方图标(与原生操作行同款);S3:视觉 16px,命中区 ≥44px。
662
- const act = (icon, label, mode) => React.createElement('button', {
663
- type: 'button', title: running ? t('dialog.runningWarn') : t('slot.' + mode),
664
- 'aria-label': t('slot.' + mode), disabled: !!busy || running,
728
+ const act = (icon, label, mode, onClick, disabled) => React.createElement('button', {
729
+ type: 'button', title: running && mode === 'revert' ? t('dialog.runningWarn') : label,
730
+ 'aria-label': label, disabled: !!disabled,
665
731
  style: { display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
666
732
  minWidth: 44, minHeight: 44, padding: 0, border: 'none', background: 'transparent',
667
- color: 'inherit', opacity: busy && busy !== mode ? 0.4 : running ? 0.35 : 0.72,
668
- cursor: busy || running ? 'default' : 'pointer', borderRadius: 6 },
669
- onClick: () => run(mode),
733
+ color: 'inherit', opacity: busy && busy !== mode ? 0.4 : running && mode === 'revert' ? 0.35 : 0.72,
734
+ cursor: disabled ? 'default' : 'pointer', borderRadius: 6 },
735
+ onClick,
670
736
  }, icon)
671
737
  return React.createElement('span', { style: { display: 'inline-flex', gap: 0 } },
672
- act(React.createElement(IconClock, { size: 16 }), t('slot.revert'), 'revert'),
673
- act(React.createElement(IconTrash, { size: 16 }), t('slot.delete'), 'delete'),
738
+ act(React.createElement(IconClock, { size: 16 }), t('slot.revert'), 'revert', runRevert, running),
739
+ act(React.createElement(IconQuote, { size: 16 }), t('slot.quote'), 'quote', runQuote, false),
674
740
  )
675
741
  }
676
742
 
677
- // --- 0.4.0 回撤 dock:composer 上方「N 条已回撤」结构化面板(opencode 式) ----
678
- // 官方槽 conversation.input.dock(scope session,composer 卡片上方全宽条目)。
679
- // ownerProps/standardProps 含 sessionId + useSessions + inputActions。
680
- const INPUT_DOCK_SLOT = 'conversation.input.dock'
681
- const INPUT_DOCK_ID = 'message-ops-revert-dock'
682
-
683
- // 0.4.2 活跃标记计算:排除 compaction 标记 + 已被恢复(后续 restore notice
684
- // 引用其 seq)的标记 —— 对齐 opencode clear 语义:恢复后不再显示为待恢复项。
685
- function activeMarkers(msgs) {
686
- const restored = new Set()
687
- for (const m of msgs || []) {
688
- if (m && typeof m.restoresSeq === 'number') restored.add(m.restoresSeq)
689
- }
690
- return (msgs || []).filter((m) => m.marker && m.sourceKind !== 'compact-checkpoint' && !restored.has(m.seq))
691
- }
692
-
693
743
  function RevertDock(props) {
694
744
  const { sessionId, useSessions, inputActions } = props
695
745
  const t = (props && props.t) || __t
@@ -762,6 +812,10 @@ window.__ModuleLoader__.load({
762
812
  if (!res.ok || !data.ok) throw new Error(data.error || ('HTTP ' + res.status))
763
813
  notifyDone(t('done.restore', { n: String(data.restoredCount != null ? data.restoredCount : '?'), s: String(data.skipped != null ? data.skipped : 0) }))
764
814
  setRestoring(null)
815
+ // 0.5.0:恢复后重建会话视图(重放消息立即可见)
816
+ try {
817
+ if (__uiWorkspace && typeof __uiWorkspace.openSession === 'function') __uiWorkspace.openSession(sessionId)
818
+ } catch { /* fallthrough */ }
765
819
  const fresh = await fetch('/api/message-ops/messages?sessionId=' + encodeURIComponent(sessionId)).then((r) => (r.ok ? r.json() : null)).catch(() => null)
766
820
  if (fresh && fresh.ok) setMarkers(activeMarkers(fresh.messages || []))
767
821
  })
@@ -926,7 +980,7 @@ window.__ModuleLoader__.load({
926
980
  })
927
981
  }
928
982
  ctx.on('locale/change', refreshSidebarOpsLabel)
929
- ctx.slots.inject(SLOT, () => ctx.slots.register({
983
+ if (window.__MOPS_DISABLE_HEADER !== true) ctx.slots.inject(SLOT, () => ctx.slots.register({
930
984
  name: SLOT, id: ROW_ID, order: 31,
931
985
  ...(__locale ? { locale: NS } : {}),
932
986
  }, OpsButton))