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