@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.
- package/README.md +12 -0
- package/package.json +1 -1
- 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.
|
|
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 [
|
|
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
|
-
//
|
|
640
|
-
|
|
641
|
-
|
|
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(
|
|
644
|
-
fetch('/api/message-ops/'
|
|
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(
|
|
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
|
-
|
|
662
|
-
|
|
663
|
-
|
|
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:
|
|
669
|
-
onClick
|
|
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(
|
|
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))
|