@240xu/dsh-message-ops 0.4.3 → 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.
Files changed (3) hide show
  1. package/README.md +12 -0
  2. package/package.json +1 -1
  3. package/src/client.js +87 -35
package/README.md CHANGED
@@ -225,3 +225,15 @@ 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
+ (官方分支按钮已覆盖最后一条消息场景)。
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.0",
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
@@ -75,6 +75,9 @@ window.__ModuleLoader__.load({
75
75
  'dock.collapse': '折叠回撤列表',
76
76
  'dock.restore': '恢复',
77
77
  'dock.restoring': '恢复中…',
78
+ 'slot.quote': '引用到输入框',
79
+ 'quote.done': '已引用到输入框',
80
+ 'quote.unavailable': '引用不可用(输入框不可写或无全文)',
78
81
  'menu.ops': '消息操作',
79
82
  }
80
83
 
@@ -128,6 +131,9 @@ window.__ModuleLoader__.load({
128
131
  'dock.shadowedN': '{n} shadowed',
129
132
  'dock.restore': 'Restore',
130
133
  'dock.restoring': 'Restoring…',
134
+ 'slot.quote': 'Quote to composer',
135
+ 'quote.done': 'Quoted into composer',
136
+ 'quote.unavailable': 'Quote unavailable (composer locked or no full text)',
131
137
  'menu.ops': 'Message ops',
132
138
  }
133
139
 
@@ -137,6 +143,8 @@ window.__ModuleLoader__.load({
137
143
  var __uiWorkspace = null
138
144
  // messageId→seq 索引缓存(每会话一次拉取;0.3.0 assistant-actions 槽用)
139
145
  var __seqIndexCache = new Map()
146
+ // messageId→全文缓存(0.5.0 消息引用用)
147
+ var __fullTextCache = new Map()
140
148
 
141
149
  function localeFallbackLang() {
142
150
  if (typeof navigator === 'undefined') return 'zh'
@@ -582,6 +590,15 @@ window.__ModuleLoader__.load({
582
590
  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
591
  React.createElement('path', { d: 'M8 4.31V8.46L11 10.08' }))
584
592
  }
593
+ var IconQuote = null
594
+ try {
595
+ const P2 = require('@deepseek-ai/dsh-client-ui-primitives')
596
+ IconQuote = P2 && (P2.IconChatOutlineRegular || P2.IconCopyOutlineRegular)
597
+ } catch { /* fallback below */ }
598
+ if (!IconQuote) IconQuote = function QuoteFallback() {
599
+ return React.createElement('svg', { width: 16, height: 16, viewBox: '0 0 16 16', fill: 'none', 'aria-hidden': true, stroke: 'currentColor', strokeWidth: 1 },
600
+ 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' }))
601
+ }
585
602
  if (!IconTrash) IconTrash = function TrashFallback() {
586
603
  // 官方 IconTrashOutlineArtwork 1:1 路径
587
604
  return React.createElement('svg', { width: 16, height: 16, viewBox: '0 0 16 16', fill: 'none', 'aria-hidden': true, stroke: 'currentColor', strokeWidth: 1 },
@@ -599,15 +616,16 @@ window.__ModuleLoader__.load({
599
616
  const CHAT_ACTIONS_ID = 'message-ops-row'
600
617
 
601
618
  function MsgSlotActions(props) {
602
- const { messageId, sessionId, useSessions, inputActions } = props
603
- if (inputActions && typeof inputActions.setDraft === 'function') __inputActions = inputActions
619
+ const { messageId, sessionId, useSessions, inputActions, useChat } = props
604
620
  const t = (props && props.t) || __t
605
621
  useLocaleRevision()
622
+ if (inputActions && typeof inputActions.setDraft === 'function') __inputActions = inputActions
606
623
  const sessions = useSessions ? useSessions((s) => s) : undefined
607
624
  const summary = sessions && sessions.byId ? sessions.byId[sessionId] : undefined
608
625
  const running = summary ? summary.running === true : false
609
626
  const [seq, setSeq] = React.useState(null)
610
- const [busy, setBusy] = React.useState(null) // 'revert' | 'delete' | null(S11 双提交防护)
627
+ const [fullText, setFullText] = React.useState(null)
628
+ const [busy, setBusy] = React.useState(null) // 'revert' | null(S11 双提交防护)
611
629
  React.useEffect(() => {
612
630
  if (!messageId || !sessionId) return
613
631
  let alive = true
@@ -624,10 +642,13 @@ window.__ModuleLoader__.load({
624
642
  .then((r) => (r.ok ? r.json() : { messages: [] }))
625
643
  .then((data) => {
626
644
  const index = new Map()
645
+ const full = new Map()
627
646
  for (const m of (data && data.messages) || []) {
628
647
  if (m && m.id != null) index.set(String(m.id), m.seq)
648
+ if (m && m.id != null && m.fullText) full.set(String(m.id), m.fullText)
629
649
  }
630
650
  __seqIndexCache.set(sessionId, index)
651
+ __fullTextCache.set(sessionId, full)
631
652
  return index
632
653
  })
633
654
  __seqIndexCache.set(sessionId, p)
@@ -636,12 +657,37 @@ window.__ModuleLoader__.load({
636
657
  return () => { alive = false }
637
658
  }, [messageId, sessionId])
638
659
 
639
- // opencode 式一键回撤:点击立即执行(无对话框),成功走 devkit toast。
640
- // 宿主 0.2.0 chat store 原生响应 surface replace 事件——遮蔽无需刷新页面。
641
- const run = (mode) => {
660
+ // 原文(引用用):seq 解析后按需取全文
661
+ React.useEffect(() => {
662
+ if (seq == null || !sessionId) return
663
+ let alive = true
664
+ const cached = __fullTextCache.get(sessionId)
665
+ const resolve = (full) => {
666
+ if (!alive) return
667
+ const hit = full && full.get(String(messageId))
668
+ if (typeof hit === 'string') setFullText(hit)
669
+ }
670
+ if (cached && typeof cached.then === 'function') cached.then(resolve).catch(() => {})
671
+ else if (cached) resolve(cached)
672
+ return () => { alive = false }
673
+ }, [seq, messageId, sessionId])
674
+
675
+ // 0.5.0 回撤后的视图刷新:openSession(reveal) 重建会话视图 → 遮蔽立即呈现。
676
+ // (宿主 live 投影只处理 compaction 类 replace,插件标记需要视图重建才可见。)
677
+ const revealSession = () => {
678
+ try {
679
+ if (__uiWorkspace && typeof __uiWorkspace.openSession === 'function') {
680
+ __uiWorkspace.openSession(sessionId)
681
+ return true
682
+ }
683
+ } catch { /* fallthrough */ }
684
+ return false
685
+ }
686
+
687
+ const runRevert = () => {
642
688
  if (busy || seq == null || running) return
643
- setBusy(mode)
644
- fetch('/api/message-ops/' + mode, {
689
+ setBusy('revert')
690
+ fetch('/api/message-ops/revert', {
645
691
  method: 'POST', headers: { 'content-type': 'application/json' },
646
692
  body: JSON.stringify({ sessionId, seq }),
647
693
  })
@@ -649,7 +695,8 @@ window.__ModuleLoader__.load({
649
695
  let data = {}
650
696
  try { data = await res.json() } catch { /* keep {} */ }
651
697
  if (!res.ok || !data.ok) throw new Error(data.error || ('HTTP ' + res.status))
652
- notifyDone(t(mode === 'revert' ? 'done.revert' : 'done.delete'))
698
+ notifyDone(t('done.revert'))
699
+ revealSession()
653
700
  })
654
701
  .catch((reason) => {
655
702
  notifyDone(t('errorPrefix') + (reason && reason.message ? reason.message : String(reason)), 'error')
@@ -657,39 +704,40 @@ window.__ModuleLoader__.load({
657
704
  .finally(() => setBusy(null))
658
705
  }
659
706
 
707
+ // 0.5.0 消息引用:官方 InputActions.captureInsertion + insertText(零 DOM hack)
708
+ const runQuote = () => {
709
+ try {
710
+ if (!inputActions || typeof inputActions.captureInsertion !== 'function' || typeof inputActions.insertText !== 'function') {
711
+ notifyDone(t('quote.unavailable'), 'warn')
712
+ return
713
+ }
714
+ const span = inputActions.captureInsertion()
715
+ const text = fullText != null ? fullText : (summary && summary.title ? '' : null)
716
+ if (text == null) { notifyDone(t('quote.unavailable'), 'warn'); return }
717
+ const quoted = text.split('\n').map((l) => '> ' + l).join('\n') + '\n\n'
718
+ const ok = inputActions.insertText(quoted, span)
719
+ notifyDone(ok ? t('quote.done') : t('quote.unavailable'), ok ? 'ok' : 'warn')
720
+ } catch (e) {
721
+ notifyDone(t('errorPrefix') + (e && e.message ? e.message : String(e)), 'error')
722
+ }
723
+ }
724
+
660
725
  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,
726
+ const act = (icon, label, mode, onClick, disabled) => React.createElement('button', {
727
+ type: 'button', title: running && mode === 'revert' ? t('dialog.runningWarn') : label,
728
+ 'aria-label': label, disabled: !!disabled,
665
729
  style: { display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
666
730
  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),
731
+ color: 'inherit', opacity: busy && busy !== mode ? 0.4 : running && mode === 'revert' ? 0.35 : 0.72,
732
+ cursor: disabled ? 'default' : 'pointer', borderRadius: 6 },
733
+ onClick,
670
734
  }, icon)
671
735
  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'),
736
+ act(React.createElement(IconClock, { size: 16 }), t('slot.revert'), 'revert', runRevert, running),
737
+ act(React.createElement(IconQuote, { size: 16 }), t('slot.quote'), 'quote', runQuote, false),
674
738
  )
675
739
  }
676
740
 
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
741
  function RevertDock(props) {
694
742
  const { sessionId, useSessions, inputActions } = props
695
743
  const t = (props && props.t) || __t
@@ -762,6 +810,10 @@ window.__ModuleLoader__.load({
762
810
  if (!res.ok || !data.ok) throw new Error(data.error || ('HTTP ' + res.status))
763
811
  notifyDone(t('done.restore', { n: String(data.restoredCount != null ? data.restoredCount : '?'), s: String(data.skipped != null ? data.skipped : 0) }))
764
812
  setRestoring(null)
813
+ // 0.5.0:恢复后重建会话视图(重放消息立即可见)
814
+ try {
815
+ if (__uiWorkspace && typeof __uiWorkspace.openSession === 'function') __uiWorkspace.openSession(sessionId)
816
+ } catch { /* fallthrough */ }
765
817
  const fresh = await fetch('/api/message-ops/messages?sessionId=' + encodeURIComponent(sessionId)).then((r) => (r.ok ? r.json() : null)).catch(() => null)
766
818
  if (fresh && fresh.ok) setMarkers(activeMarkers(fresh.messages || []))
767
819
  })
@@ -926,7 +978,7 @@ window.__ModuleLoader__.load({
926
978
  })
927
979
  }
928
980
  ctx.on('locale/change', refreshSidebarOpsLabel)
929
- ctx.slots.inject(SLOT, () => ctx.slots.register({
981
+ if (window.__MOPS_DISABLE_HEADER !== true) ctx.slots.inject(SLOT, () => ctx.slots.register({
930
982
  name: SLOT, id: ROW_ID, order: 31,
931
983
  ...(__locale ? { locale: NS } : {}),
932
984
  }, OpsButton))