@240xu/dsh-message-ops 0.4.0 → 0.4.2
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 +21 -0
- package/package.json +1 -1
- package/src/client.js +85 -41
- package/src/ops-core.js +3 -1
- package/src/session-file.js +7 -0
package/README.md
CHANGED
|
@@ -198,3 +198,24 @@ MIT
|
|
|
198
198
|
逐条「恢复」(restoring 中间态禁用,S11);无标记时 dock 不渲染。
|
|
199
199
|
- 数据链:messages 端点为 user 消息新增 `fullText` 字段(回填原文)。
|
|
200
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 视觉完全一致。
|
|
211
|
+
|
|
212
|
+
### 0.4.2 · dock 像素级对齐 opencode + 回撤生命周期修正
|
|
213
|
+
|
|
214
|
+
- **回撤生命周期**(对齐 opencode clear 语义):restore 的 notice 事件现携带
|
|
215
|
+
`restoresSeq` 引用被恢复的标记——dock 只显示**活跃**标记,已恢复项自动消失
|
|
216
|
+
(此前恢复过的标记永远挂在列表里,计数随之虚高)。
|
|
217
|
+
- **像素级对齐 opencode SessionRevertDock**(读其 dev 分支源码逐项落地):
|
|
218
|
+
42px 头部(reset 图标 + 13px/500 label + 折叠时第一条预览文本 + 180° 旋转
|
|
219
|
+
chevron)、rounded-xl + 0.5px 边框 + bg-layer-01 容器、24px 行高 + neutral
|
|
220
|
+
小按钮(非文字链)、items 变化自动折叠、Enter/Space 键盘切换、18px
|
|
221
|
+
sacrificial 空间。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@240xu/dsh-message-ops",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
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,7 +70,9 @@ window.__ModuleLoader__.load({
|
|
|
70
70
|
'busy.restore': '恢复中…',
|
|
71
71
|
'confirm.restore': '恢复',
|
|
72
72
|
'ack.restore': '我已了解:恢复将以新 seq 重放被遮蔽的消息(原日志不变)',
|
|
73
|
-
'dock.title': '已回撤 {n}
|
|
73
|
+
'dock.title': '已回撤 {n} 条消息',
|
|
74
|
+
'dock.expand': '展开回撤列表',
|
|
75
|
+
'dock.collapse': '折叠回撤列表',
|
|
74
76
|
'dock.restore': '恢复',
|
|
75
77
|
'dock.restoring': '恢复中…',
|
|
76
78
|
'menu.ops': '消息操作',
|
|
@@ -120,7 +122,10 @@ window.__ModuleLoader__.load({
|
|
|
120
122
|
'done.delete': 'Deleted; reload to apply',
|
|
121
123
|
'action.reload': 'Reload page',
|
|
122
124
|
'errorPrefix': 'Operation failed: ',
|
|
123
|
-
'dock.title': '{n} rolled back
|
|
125
|
+
'dock.title': '{n} messages rolled back',
|
|
126
|
+
'dock.expand': 'Expand revert list',
|
|
127
|
+
'dock.collapse': 'Collapse revert list',
|
|
128
|
+
'dock.shadowedN': '{n} shadowed',
|
|
124
129
|
'dock.restore': 'Restore',
|
|
125
130
|
'dock.restoring': 'Restoring…',
|
|
126
131
|
'menu.ops': 'Message ops',
|
|
@@ -675,6 +680,16 @@ window.__ModuleLoader__.load({
|
|
|
675
680
|
const INPUT_DOCK_SLOT = 'conversation.input.dock'
|
|
676
681
|
const INPUT_DOCK_ID = 'message-ops-revert-dock'
|
|
677
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
|
+
|
|
678
693
|
function RevertDock(props) {
|
|
679
694
|
const { sessionId, useSessions, inputActions } = props
|
|
680
695
|
const t = (props && props.t) || __t
|
|
@@ -684,25 +699,53 @@ window.__ModuleLoader__.load({
|
|
|
684
699
|
const summary = sessions && sessions.byId ? sessions.byId[sessionId] : undefined
|
|
685
700
|
const running = summary ? summary.running === true : false
|
|
686
701
|
const [open, setOpen] = useState(false)
|
|
687
|
-
const [
|
|
688
|
-
const [shadowedCount, setShadowedCount] = useState(0)
|
|
702
|
+
const [markers, setMarkers] = useState(null) // 活跃回撤标记(已恢复的自动消失)
|
|
689
703
|
const [restoring, setRestoring] = useState(null)
|
|
704
|
+
const [styleInjected, setStyleInjected] = useState(false)
|
|
705
|
+
|
|
706
|
+
// opencode SessionRevertDock 视觉(v2 布局,DSH 令牌等价映射)
|
|
707
|
+
useEffect(() => {
|
|
708
|
+
if (styleInjected || typeof document === 'undefined') return
|
|
709
|
+
if (document.getElementById('dsh-message-ops-dock-style')) { setStyleInjected(true); return }
|
|
710
|
+
const tag = document.createElement('style')
|
|
711
|
+
tag.id = 'dsh-message-ops-dock-style'
|
|
712
|
+
tag.textContent = [
|
|
713
|
+
// 容器:rounded-xl + 0.5px 边框 + bg-layer-01(M1 修正令牌)
|
|
714
|
+
'.mopsRd{width:100%;overflow:hidden;border-radius:var(--dsw-radius-md,12px);border:.5px solid var(--dsw-alias-border-l1,rgba(128,128,128,.25));background:var(--dsw-alias-bg-layer-1,#1e1e20)}',
|
|
715
|
+
// 头部 42px:图标 + label + 折叠预览 + 旋转 chevron
|
|
716
|
+
'.mopsRdHead{display:flex;height:42px;align-items:center;gap:8px;padding-left:16px;padding-right:8px;cursor:pointer;user-select:none}',
|
|
717
|
+
'.mopsRdIcon{display:inline-flex;color:var(--dsw-alias-label-tertiary,#9a9aa0);flex:none}',
|
|
718
|
+
'.mopsRdLabel{font-size:13px;font-weight:500;line-height:20px;letter-spacing:-.04px;flex:none;cursor:default;color:var(--dsw-alias-label-primary,inherit)}',
|
|
719
|
+
'.mopsRdLabelCollapsed{color:var(--dsw-alias-label-secondary,#a8a8ae)}',
|
|
720
|
+
'.mopsRdPreview{min-width:0;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:13px;font-weight:400;line-height:20px;letter-spacing:-.04px;cursor:default;color:var(--dsw-alias-label-tertiary,#9a9aa0)}',
|
|
721
|
+
'.mopsRdChevron{margin-left:auto;flex:none;display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;border:none;border-radius:999px;background:transparent;color:var(--dsw-alias-label-tertiary,#9a9aa0);cursor:pointer;transition:transform .15s ease}',
|
|
722
|
+
// 列表:24px 行 + neutral 小按钮(非文字链)
|
|
723
|
+
'.mopsRdList{display:flex;flex-direction:column;gap:8px;max-height:168px;overflow-y:auto;padding:1px 16px 12px}',
|
|
724
|
+
'.mopsRdRow{display:flex;height:24px;min-width:0;align-items:center;gap:8px}',
|
|
725
|
+
'.mopsRdRowText{min-width:0;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:13px;font-weight:400;line-height:20px;letter-spacing:-.04px;color:var(--dsw-alias-label-secondary,#a8a8ae)}',
|
|
726
|
+
'.mopsRdRestore{flex:none;font:inherit;font-size:12px;line-height:18px;padding:2px 10px;border-radius:6px;cursor:pointer;color:var(--dsw-alias-label-primary,inherit);background:var(--dsw-alias-interactive-bg-hover,rgba(128,128,128,.12));border:1px solid var(--dsw-alias-border-l1,rgba(128,128,128,.2));opacity:1}',
|
|
727
|
+
'.mopsRdRestore:disabled{opacity:.45;cursor:default}',
|
|
728
|
+
// 折叠时 18px sacrificial 空间(composer 负 lift 重叠)
|
|
729
|
+
'.mopsRdSpacer{height:18px}',
|
|
730
|
+
].join('')
|
|
731
|
+
document.head.appendChild(tag)
|
|
732
|
+
setStyleInjected(true)
|
|
733
|
+
}, [styleInjected])
|
|
690
734
|
|
|
691
735
|
useEffect(() => {
|
|
692
736
|
let alive = true
|
|
693
|
-
fetch('/api/message-ops/messages?sessionId=' + encodeURIComponent(sessionId))
|
|
737
|
+
const load = () => fetch('/api/message-ops/messages?sessionId=' + encodeURIComponent(sessionId))
|
|
694
738
|
.then((r) => (r.ok ? r.json() : null))
|
|
695
739
|
.then((data) => {
|
|
696
740
|
if (!alive || !data || !data.ok) return
|
|
697
|
-
|
|
698
|
-
setShadowedCount(msgs.filter((m) => m.visible === false && m.role !== 'system').length)
|
|
699
|
-
setRows(msgs.filter((m) => m.marker))
|
|
741
|
+
setMarkers(activeMarkers(data.messages || []))
|
|
700
742
|
})
|
|
701
743
|
.catch(() => {})
|
|
744
|
+
load()
|
|
702
745
|
return () => { alive = false }
|
|
703
746
|
}, [sessionId])
|
|
704
747
|
|
|
705
|
-
if (!
|
|
748
|
+
if (!markers || !markers.length) return null
|
|
706
749
|
|
|
707
750
|
const restoreRow = (row) => {
|
|
708
751
|
if (restoring != null || running) return
|
|
@@ -716,51 +759,52 @@ window.__ModuleLoader__.load({
|
|
|
716
759
|
try { data = await res.json() } catch { /* keep {} */ }
|
|
717
760
|
if (!res.ok || !data.ok) throw new Error(data.error || ('HTTP ' + res.status))
|
|
718
761
|
notifyDone(t('done.restore', { n: String(data.restoredCount != null ? data.restoredCount : '?'), s: String(data.skipped != null ? data.skipped : 0) }))
|
|
719
|
-
|
|
762
|
+
setRestoring(null)
|
|
720
763
|
const fresh = await fetch('/api/message-ops/messages?sessionId=' + encodeURIComponent(sessionId)).then((r) => (r.ok ? r.json() : null)).catch(() => null)
|
|
721
|
-
if (fresh && fresh.ok)
|
|
722
|
-
const msgs = fresh.messages || []
|
|
723
|
-
setShadowedCount(msgs.filter((m) => m.visible === false && m.role !== 'system').length)
|
|
724
|
-
setRows(msgs.filter((m) => m.marker))
|
|
725
|
-
}
|
|
764
|
+
if (fresh && fresh.ok) setMarkers(activeMarkers(fresh.messages || []))
|
|
726
765
|
})
|
|
727
766
|
.catch((reason) => {
|
|
728
767
|
notifyDone(t('errorPrefix') + (reason && reason.message ? reason.message : String(reason)), 'error')
|
|
768
|
+
setRestoring(null)
|
|
729
769
|
})
|
|
730
|
-
.finally(() => setRestoring(null))
|
|
731
770
|
}
|
|
732
771
|
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
|
|
772
|
+
// items 变化自动折叠(opencode createEffect 同款)
|
|
773
|
+
useEffect(() => { setOpen(false) }, [markers && markers.length, markers && markers[0] && markers[0].seq])
|
|
774
|
+
|
|
775
|
+
const headerKbd = (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); setOpen((v) => !v) } }
|
|
776
|
+
const label = t('dock.title', { n: String(markers.length) })
|
|
777
|
+
const preview = markers[0] && markers[0].snippet ? markers[0].snippet : ''
|
|
778
|
+
|
|
779
|
+
const rowEl = (row) => React.createElement('div', { key: row.seq, className: 'mopsRdRow' },
|
|
780
|
+
React.createElement('span', { className: 'mopsRdRowText' }, '#' + row.seq + ' · ' + (row.snippet || '')),
|
|
740
781
|
React.createElement('button', {
|
|
741
|
-
type: 'button',
|
|
742
|
-
style: { border: 'none', background: 'transparent', color: 'var(--dsw-alias-link,#8ab4f8)',
|
|
743
|
-
font: 'inherit', fontSize: 12, cursor: restoring != null || running ? 'default' : 'pointer',
|
|
744
|
-
opacity: restoring != null || running ? 0.5 : 1, padding: '4px 8px', minHeight: 28 },
|
|
782
|
+
type: 'button', className: 'mopsRdRestore', disabled: restoring != null || running,
|
|
745
783
|
onClick: () => restoreRow(row),
|
|
746
784
|
}, restoring === row.seq ? t('dock.restoring') : t('dock.restore')),
|
|
747
785
|
)
|
|
748
786
|
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
},
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
787
|
+
const chevron = React.createElement('button', {
|
|
788
|
+
type: 'button', 'aria-label': open ? t('dock.collapse') : t('dock.expand'), 'aria-expanded': open,
|
|
789
|
+
className: 'mopsRdChevron',
|
|
790
|
+
onClick: (e) => { e.stopPropagation(); setOpen((v) => !v) },
|
|
791
|
+
}, React.createElement('svg', { width: 14, height: 14, viewBox: '0 0 16 16', fill: 'none',
|
|
792
|
+
style: { transform: open ? 'rotate(0deg)' : 'rotate(180deg)', transition: 'transform .15s ease' } },
|
|
793
|
+
React.createElement('path', { d: 'M4 6l4 4 4-4', stroke: 'currentColor', strokeWidth: 1.3, strokeLinecap: 'round', strokeLinejoin: 'round' })))
|
|
794
|
+
|
|
795
|
+
return React.createElement('div', { className: 'mopsRd', role: 'region', 'aria-label': label },
|
|
796
|
+
React.createElement('div', {
|
|
797
|
+
className: 'mopsRdHead', role: 'button', tabIndex: 0,
|
|
798
|
+
onClick: () => setOpen((v) => !v), onKeyDown: headerKbd,
|
|
760
799
|
},
|
|
761
|
-
React.createElement('span',
|
|
762
|
-
|
|
763
|
-
|
|
800
|
+
React.createElement('span', { className: 'mopsRdIcon' },
|
|
801
|
+
React.createElement('svg', { width: 15, height: 15, viewBox: '0 0 16 16', fill: 'none' },
|
|
802
|
+
React.createElement('path', { d: 'M2.5 8a5.5 5.5 0 1 0 1.6-3.9', stroke: 'currentColor', strokeWidth: 1.2, strokeLinecap: 'round' }),
|
|
803
|
+
React.createElement('path', { d: 'M2.2 2.8v3h3', stroke: 'currentColor', strokeWidth: 1.2, strokeLinecap: 'round', strokeLinejoin: 'round' }))),
|
|
804
|
+
React.createElement('span', { className: 'mopsRdLabel' + (open ? '' : ' mopsRdLabelCollapsed') }, label),
|
|
805
|
+
open ? null : React.createElement('span', { className: 'mopsRdPreview' }, preview),
|
|
806
|
+
React.createElement('span', { style: { marginLeft: 'auto', flex: 'none' } }, chevron)),
|
|
807
|
+
open ? React.createElement('div', { className: 'mopsRdList' }, markers.map(rowEl)) : null,
|
|
764
808
|
)
|
|
765
809
|
}
|
|
766
810
|
|
package/src/ops-core.js
CHANGED
|
@@ -143,9 +143,11 @@ export function applyRestore(session, plan, { flush } = {}) {
|
|
|
143
143
|
const notice = `[消息恢复] 重放 seq ${plan.startSeq}..${plan.endSeq} 的 ${plan.replayable.length} 条消息` +
|
|
144
144
|
(plan.skipped > 0 ? `(另有 ${plan.skipped} 条不可重放事件已跳过)` : "") +
|
|
145
145
|
`;原区间仍处于遮蔽状态,恢复为重放而非解除遮蔽`;
|
|
146
|
+
// 0.4.2:notice 事件携带 restoresSeq —— dock 据此把被恢复的标记从「活跃回撤」
|
|
147
|
+
// 中移除(对齐 opencode clear 语义:恢复后不再显示为待恢复项)。
|
|
146
148
|
const noticeEvent = session.append(
|
|
147
149
|
"system/message",
|
|
148
|
-
{ message: { role: "system", content: [{ type: "text", text: notice }] } },
|
|
150
|
+
{ message: { role: "system", content: [{ type: "text", text: notice }] }, restoresSeq: plan.restoreSeq },
|
|
149
151
|
{ surfaceOp: "append" },
|
|
150
152
|
);
|
|
151
153
|
if (noticeEvent && noticeEvent.seq != null) eventSeqs.push(noticeEvent.seq);
|
package/src/session-file.js
CHANGED
|
@@ -184,6 +184,13 @@ export function listMessages(events) {
|
|
|
184
184
|
snippet: text.replace(/\s+/g, " ").trim().slice(0, 160),
|
|
185
185
|
// 0.4.0 composer 回填:user 消息携带全文(revert 后 setDraft 原文)
|
|
186
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,
|
|
191
|
+
// 0.4.2:恢复标记引用(restore 的 notice 事件带回它所恢复的标记 seq)
|
|
192
|
+
restoresSeq: (e.type === "system/message" && e.data && typeof e.data.restoresSeq === "number")
|
|
193
|
+
? e.data.restoresSeq : null,
|
|
187
194
|
time: e.time ?? null,
|
|
188
195
|
turn: (e.data && e.data.turn) ?? null,
|
|
189
196
|
});
|