@240xu/dsh-message-ops 0.3.2 → 0.4.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 +20 -0
- package/package.json +1 -1
- package/src/client.js +144 -1
- package/src/session-file.js +6 -0
package/README.md
CHANGED
|
@@ -188,3 +188,23 @@ MIT
|
|
|
188
188
|
回退路径的 SVG 也改为官方 IconClockOutline/IconTrashOutline 的精确 path 数据
|
|
189
189
|
(1px stroke,与 Regular 变体一致),不再手绘。
|
|
190
190
|
- 字典文案去 emoji(label 仅用于 tooltip/aria,不参与渲染)。
|
|
191
|
+
|
|
192
|
+
### 0.4.0 · opencode 式回撤深化(composer 回填 + 回撤 dock)
|
|
193
|
+
|
|
194
|
+
- **Composer 回填**:回滚**用户消息**后,原文经官方 `InputActions.setDraft`
|
|
195
|
+
自动回填输入框——「编辑重发」零按钮(opencode 招牌交互)。
|
|
196
|
+
- **结构化回撤 dock**:注入官方 `conversation.input.dock` 槽(composer 卡片上方
|
|
197
|
+
全宽条目)——「已回撤 N 条(可恢复)」折叠面板,展开列出每个回撤/删除标记,
|
|
198
|
+
逐条「恢复」(restoring 中间态禁用,S11);无标记时 dock 不渲染。
|
|
199
|
+
- 数据链:messages 端点为 user 消息新增 `fullText` 字段(回填原文)。
|
|
200
|
+
- 0.1.x 宿主两个槽均不存在,inject 自动 no-op。
|
|
201
|
+
|
|
202
|
+
### 0.4.1 · dock 修复:统计口径 + 原生观感
|
|
203
|
+
|
|
204
|
+
- **统计修复**:旧版把历史累积的被遮蔽消息全算进「已回撤 N 条」(多次恢复重放后
|
|
205
|
+
虚高到数百)。现在 dock 只列**手工回撤/删除标记**(排除 compaction checkpoint),
|
|
206
|
+
每条标记显示其 range 内**当前不可见**的消息数;头部为标记计数。
|
|
207
|
+
- **原生观感**:样式改为官方 GoalDock/TodoDock 同款——36px 高毛玻璃条
|
|
208
|
+
(`--dsw-specific-menu` + `--dsw-elevation-panel` + `--dsw-radius-md`)、
|
|
209
|
+
composer 卡片对齐宽度(`--dsh-composer-dock-inset` 系列)、13px/500 label、
|
|
210
|
+
28px 圆形切换钮——与目标/计划 dock 视觉完全一致。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@240xu/dsh-message-ops",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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
|
@@ -70,6 +70,10 @@ window.__ModuleLoader__.load({
|
|
|
70
70
|
'busy.restore': '恢复中…',
|
|
71
71
|
'confirm.restore': '恢复',
|
|
72
72
|
'ack.restore': '我已了解:恢复将以新 seq 重放被遮蔽的消息(原日志不变)',
|
|
73
|
+
'dock.title': '回撤记录 · {n}',
|
|
74
|
+
'dock.shadowedN': '遮蔽 {n} 条',
|
|
75
|
+
'dock.restore': '恢复',
|
|
76
|
+
'dock.restoring': '恢复中…',
|
|
73
77
|
'menu.ops': '消息操作',
|
|
74
78
|
}
|
|
75
79
|
|
|
@@ -117,11 +121,16 @@ window.__ModuleLoader__.load({
|
|
|
117
121
|
'done.delete': 'Deleted; reload to apply',
|
|
118
122
|
'action.reload': 'Reload page',
|
|
119
123
|
'errorPrefix': 'Operation failed: ',
|
|
124
|
+
'dock.title': 'Revert history · {n}',
|
|
125
|
+
'dock.shadowedN': '{n} shadowed',
|
|
126
|
+
'dock.restore': 'Restore',
|
|
127
|
+
'dock.restoring': 'Restoring…',
|
|
120
128
|
'menu.ops': 'Message ops',
|
|
121
129
|
}
|
|
122
130
|
|
|
123
131
|
var __locale = null
|
|
124
132
|
var __sessionsSvc = null
|
|
133
|
+
var __inputActions = null // 0.4.0: 从 session 槽捕获(InputActions.setDraft → composer 回填)
|
|
125
134
|
var __uiWorkspace = null
|
|
126
135
|
// messageId→seq 索引缓存(每会话一次拉取;0.3.0 assistant-actions 槽用)
|
|
127
136
|
var __seqIndexCache = new Map()
|
|
@@ -379,6 +388,11 @@ window.__ModuleLoader__.load({
|
|
|
379
388
|
const okMsg = mode === 'restore'
|
|
380
389
|
? t('done.restore', { n: String(data.restoredCount != null ? data.restoredCount : '?'), s: String(data.skipped != null ? data.skipped : 0) })
|
|
381
390
|
: t(mode === 'revert' ? 'done.revert' : 'done.delete')
|
|
391
|
+
// 0.4.0 composer 回填(opencode 式):回滚用户消息 → 原文回填输入框,「编辑重发」零按钮
|
|
392
|
+
if (mode === 'revert' && pickedMsg && pickedMsg.role === 'user' && pickedMsg.fullText
|
|
393
|
+
&& __inputActions && typeof __inputActions.setDraft === 'function') {
|
|
394
|
+
try { __inputActions.setDraft(pickedMsg.fullText) } catch { /* 回填失败不阻断成功反馈 */ }
|
|
395
|
+
}
|
|
382
396
|
setDoneMsg(okMsg)
|
|
383
397
|
setBusyMsg('')
|
|
384
398
|
setState('done')
|
|
@@ -582,7 +596,8 @@ window.__ModuleLoader__.load({
|
|
|
582
596
|
const CHAT_ACTIONS_ID = 'message-ops-row'
|
|
583
597
|
|
|
584
598
|
function MsgSlotActions(props) {
|
|
585
|
-
const { messageId, sessionId, useSessions } = props
|
|
599
|
+
const { messageId, sessionId, useSessions, inputActions } = props
|
|
600
|
+
if (inputActions && typeof inputActions.setDraft === 'function') __inputActions = inputActions
|
|
586
601
|
const t = (props && props.t) || __t
|
|
587
602
|
useLocaleRevision()
|
|
588
603
|
const sessions = useSessions ? useSessions((s) => s) : undefined
|
|
@@ -656,6 +671,129 @@ window.__ModuleLoader__.load({
|
|
|
656
671
|
)
|
|
657
672
|
}
|
|
658
673
|
|
|
674
|
+
// --- 0.4.0 回撤 dock:composer 上方「N 条已回撤」结构化面板(opencode 式) ----
|
|
675
|
+
// 官方槽 conversation.input.dock(scope session,composer 卡片上方全宽条目)。
|
|
676
|
+
// ownerProps/standardProps 含 sessionId + useSessions + inputActions。
|
|
677
|
+
const INPUT_DOCK_SLOT = 'conversation.input.dock'
|
|
678
|
+
const INPUT_DOCK_ID = 'message-ops-revert-dock'
|
|
679
|
+
|
|
680
|
+
function RevertDock(props) {
|
|
681
|
+
const { sessionId, useSessions, inputActions } = props
|
|
682
|
+
const t = (props && props.t) || __t
|
|
683
|
+
useLocaleRevision()
|
|
684
|
+
if (inputActions && typeof inputActions.setDraft === 'function') __inputActions = inputActions
|
|
685
|
+
const sessions = useSessions ? useSessions((s) => s) : undefined
|
|
686
|
+
const summary = sessions && sessions.byId ? sessions.byId[sessionId] : undefined
|
|
687
|
+
const running = summary ? summary.running === true : false
|
|
688
|
+
const [open, setOpen] = useState(false)
|
|
689
|
+
const [markers, setMarkers] = useState(null) // [{seq, snippet, range}] 非 compaction 的回撤标记
|
|
690
|
+
const [hiddenByMarker, setHiddenByMarker] = useState({}) // markerSeq -> 当前不可见条数
|
|
691
|
+
const [restoring, setRestoring] = useState(null)
|
|
692
|
+
const [styleInjected, setStyleInjected] = useState(false)
|
|
693
|
+
|
|
694
|
+
// 官方 dock 视觉(GoalDock/TodoDock 同款值,一次注入)
|
|
695
|
+
useEffect(() => {
|
|
696
|
+
if (styleInjected || typeof document === 'undefined') return
|
|
697
|
+
if (document.getElementById('dsh-message-ops-dock-style')) { setStyleInjected(true); return }
|
|
698
|
+
const tag = document.createElement('style')
|
|
699
|
+
tag.id = 'dsh-message-ops-dock-style'
|
|
700
|
+
tag.textContent = [
|
|
701
|
+
'.mopsDock{box-sizing:border-box;width:calc(100% - var(--dsh-composer-side-clearance)*2 - var(--dsh-composer-dock-inset)*4);margin:0 auto 8px}',
|
|
702
|
+
'.mopsDockBar{isolation:isolate;box-sizing:border-box;width:100%;max-width:calc(var(--dsh-composer-card-max-width) - 4 * var(--dsh-composer-dock-inset));border-radius:var(--dsw-radius-md);min-height:36px;box-shadow:var(--dsw-elevation-panel);border:0;align-items:center;gap:10px;margin:0 auto;padding:4px 5px 4px 12px;display:flex;position:relative}',
|
|
703
|
+
'.mopsDockBar:before{z-index:-1;border-radius:inherit;background:var(--dsw-specific-menu);backdrop-filter:var(--dsw-menu-backdrop-filter);content:"";pointer-events:none;position:absolute;inset:0}',
|
|
704
|
+
'.mopsDockLabel{color:var(--dsw-alias-label-primary);flex:none;font-size:13px;font-weight:500;line-height:24px}',
|
|
705
|
+
'.mopsDockToggle{width:28px;height:28px;color:var(--dsw-alias-label-tertiary);cursor:pointer;background:0 0;border:none;border-radius:999px;justify-content:center;align-items:center;padding:0;display:inline-flex;font:inherit}',
|
|
706
|
+
'.mopsDockRow{display:flex;align-items:center;gap:8px;padding:4px 12px;border-top:1px solid var(--dsw-alias-border-l1,rgba(128,128,128,.15));position:relative}',
|
|
707
|
+
'.mopsDockRowText{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:12px;opacity:.85;color:var(--dsw-alias-label-primary)}',
|
|
708
|
+
'.mopsDockRestore{border:none;background:transparent;color:var(--dsw-alias-link,#8ab4f8);font:inherit;font-size:12px;cursor:pointer;padding:4px 8px;min-height:28px;opacity:1}',
|
|
709
|
+
'.mopsDockList{max-height:180px;overflow-y:auto;padding-bottom:4px}',
|
|
710
|
+
].join('')
|
|
711
|
+
document.head.appendChild(tag)
|
|
712
|
+
setStyleInjected(true)
|
|
713
|
+
}, [styleInjected])
|
|
714
|
+
|
|
715
|
+
useEffect(() => {
|
|
716
|
+
let alive = true
|
|
717
|
+
const load = () => fetch('/api/message-ops/messages?sessionId=' + encodeURIComponent(sessionId))
|
|
718
|
+
.then((r) => (r.ok ? r.json() : null))
|
|
719
|
+
.then((data) => {
|
|
720
|
+
if (!alive || !data || !data.ok) return
|
|
721
|
+
const msgs = data.messages || []
|
|
722
|
+
// 仅手工回撤/删除产生的标记(排除 compaction checkpoint)
|
|
723
|
+
const marks = msgs.filter((m) => m.marker && m.sourceKind !== 'compact-checkpoint')
|
|
724
|
+
setMarkers(marks)
|
|
725
|
+
// 每标记的当前不可见条数:落在该标记 range 内、当前不可见的 user/assistant 消息
|
|
726
|
+
const hidden = {}
|
|
727
|
+
for (const mk of marks) {
|
|
728
|
+
if (!mk.range) { hidden[mk.seq] = null; continue }
|
|
729
|
+
hidden[mk.seq] = msgs.filter((m) => m.visible === false && m.role !== 'system'
|
|
730
|
+
&& m.seq >= mk.range.start && m.seq <= mk.range.end).length
|
|
731
|
+
}
|
|
732
|
+
setHiddenByMarker(hidden)
|
|
733
|
+
})
|
|
734
|
+
.catch(() => {})
|
|
735
|
+
load()
|
|
736
|
+
return () => { alive = false }
|
|
737
|
+
}, [sessionId])
|
|
738
|
+
|
|
739
|
+
if (!markers || !markers.length) return null // 无手工回撤标记 → dock 不渲染
|
|
740
|
+
|
|
741
|
+
const restoreRow = (row) => {
|
|
742
|
+
if (restoring != null || running) return
|
|
743
|
+
setRestoring(row.seq)
|
|
744
|
+
fetch('/api/message-ops/restore', {
|
|
745
|
+
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
746
|
+
body: JSON.stringify({ sessionId, seq: row.seq }),
|
|
747
|
+
})
|
|
748
|
+
.then(async (res) => {
|
|
749
|
+
let data = {}
|
|
750
|
+
try { data = await res.json() } catch { /* keep {} */ }
|
|
751
|
+
if (!res.ok || !data.ok) throw new Error(data.error || ('HTTP ' + res.status))
|
|
752
|
+
notifyDone(t('done.restore', { n: String(data.restoredCount != null ? data.restoredCount : '?'), s: String(data.skipped != null ? data.skipped : 0) }))
|
|
753
|
+
setRestoring(null)
|
|
754
|
+
// 重放后刷新标记与计数
|
|
755
|
+
const fresh = await fetch('/api/message-ops/messages?sessionId=' + encodeURIComponent(sessionId)).then((r) => (r.ok ? r.json() : null)).catch(() => null)
|
|
756
|
+
if (fresh && fresh.ok) {
|
|
757
|
+
const msgs = fresh.messages || []
|
|
758
|
+
const marks = msgs.filter((m) => m.marker && m.sourceKind !== 'compact-checkpoint')
|
|
759
|
+
setMarkers(marks)
|
|
760
|
+
const hidden = {}
|
|
761
|
+
for (const mk of marks) {
|
|
762
|
+
if (!mk.range) { hidden[mk.seq] = null; continue }
|
|
763
|
+
hidden[mk.seq] = msgs.filter((m) => m.visible === false && m.role !== 'system'
|
|
764
|
+
&& m.seq >= mk.range.start && m.seq <= mk.range.end).length
|
|
765
|
+
}
|
|
766
|
+
setHiddenByMarker(hidden)
|
|
767
|
+
}
|
|
768
|
+
})
|
|
769
|
+
.catch((reason) => {
|
|
770
|
+
notifyDone(t('errorPrefix') + (reason && reason.message ? reason.message : String(reason)), 'error')
|
|
771
|
+
setRestoring(null)
|
|
772
|
+
})
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
const rowEl = (row) => React.createElement('div', { key: row.seq, className: 'mopsDockRow' },
|
|
776
|
+
React.createElement('span', { className: 'mopsDockRowText' },
|
|
777
|
+
'#' + row.seq + (hiddenByMarker[row.seq] != null ? ' · ' + t('dock.shadowedN', { n: String(hiddenByMarker[row.seq]) }) : '')
|
|
778
|
+
+ ' · ' + (row.snippet || '')),
|
|
779
|
+
React.createElement('button', {
|
|
780
|
+
type: 'button', 'aria-label': t('dock.restore'), disabled: restoring != null || running,
|
|
781
|
+
className: 'mopsDockRestore',
|
|
782
|
+
onClick: () => restoreRow(row),
|
|
783
|
+
}, restoring === row.seq ? t('dock.restoring') : t('dock.restore')),
|
|
784
|
+
)
|
|
785
|
+
|
|
786
|
+
return React.createElement('div', { className: 'mopsDock', role: 'region', 'aria-label': t('dock.title', { n: String(markers.length) }) },
|
|
787
|
+
React.createElement('div', { className: 'mopsDockBar' },
|
|
788
|
+
React.createElement('span', { className: 'mopsDockLabel' }, t('dock.title', { n: String(markers.length) })),
|
|
789
|
+
React.createElement('button', {
|
|
790
|
+
type: 'button', 'aria-expanded': open, className: 'mopsDockToggle',
|
|
791
|
+
onClick: () => setOpen((v) => !v),
|
|
792
|
+
}, open ? '▾' : '▸')),
|
|
793
|
+
open ? React.createElement('div', { className: 'mopsDockList' }, markers.map(rowEl)) : null,
|
|
794
|
+
)
|
|
795
|
+
}
|
|
796
|
+
|
|
659
797
|
// --- 侧栏行菜单注入(DOM 级,同 session-delete 模式) ------------------------
|
|
660
798
|
|
|
661
799
|
function findOpenSessionRow() {
|
|
@@ -783,6 +921,11 @@ window.__ModuleLoader__.load({
|
|
|
783
921
|
name: CHAT_ACTIONS_SLOT, id: CHAT_ACTIONS_ID, order: 20,
|
|
784
922
|
...(__locale ? { locale: NS } : {}),
|
|
785
923
|
}, MsgSlotActions))
|
|
924
|
+
// 0.4.0:composer 上方回撤 dock(0.1.x 无此槽自动 no-op)
|
|
925
|
+
ctx.slots.inject(INPUT_DOCK_SLOT, () => ctx.slots.register({
|
|
926
|
+
name: INPUT_DOCK_SLOT, id: INPUT_DOCK_ID, order: 10,
|
|
927
|
+
...(__locale ? { locale: NS } : {}),
|
|
928
|
+
}, RevertDock))
|
|
786
929
|
ctx.slots.inject(OVERLAY_SLOT, () => ctx.slots.register({
|
|
787
930
|
name: OVERLAY_SLOT, id: DIALOG_ID, order: 101,
|
|
788
931
|
...(__locale ? { locale: NS } : {}),
|
package/src/session-file.js
CHANGED
|
@@ -182,6 +182,12 @@ export function listMessages(events) {
|
|
|
182
182
|
id: (msg && msg.id) || e.data.id || null,
|
|
183
183
|
marker: isReplaceMarker,
|
|
184
184
|
snippet: text.replace(/\s+/g, " ").trim().slice(0, 160),
|
|
185
|
+
// 0.4.0 composer 回填:user 消息携带全文(revert 后 setDraft 原文)
|
|
186
|
+
fullText: e.type === "user/message" ? text : null,
|
|
187
|
+
// 0.4.1 dock:replace 标记携带范围与来源(区分手工回撤 vs compaction)
|
|
188
|
+
range: isReplaceMarker && e.surfaceOp.startSeq != null && e.surfaceOp.endSeq != null
|
|
189
|
+
? { start: e.surfaceOp.startSeq, end: e.surfaceOp.endSeq } : null,
|
|
190
|
+
sourceKind: (e.data && e.data.source && e.data.source.kind) || null,
|
|
185
191
|
time: e.time ?? null,
|
|
186
192
|
turn: (e.data && e.data.turn) ?? null,
|
|
187
193
|
});
|