@240xu/dsh-message-ops 0.4.1 → 0.4.3
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 +17 -0
- package/package.json +1 -1
- package/src/client.js +69 -55
- package/src/ops-core.js +3 -1
- package/src/session-file.js +3 -0
package/README.md
CHANGED
|
@@ -208,3 +208,20 @@ MIT
|
|
|
208
208
|
(`--dsw-specific-menu` + `--dsw-elevation-panel` + `--dsw-radius-md`)、
|
|
209
209
|
composer 卡片对齐宽度(`--dsh-composer-dock-inset` 系列)、13px/500 label、
|
|
210
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 空间。
|
|
222
|
+
|
|
223
|
+
### 0.4.3 · dock 崩溃修复(Hooks 规则违规)
|
|
224
|
+
|
|
225
|
+
- 0.4.1 的自动折叠 useEffect 位于「无标记早退」之后——标记从 0 变非 0 时多出
|
|
226
|
+
第 4 个 hook,React 抛出 hooks 顺序错误并卸载整个槽(dock 永远不可见,
|
|
227
|
+
Playwright 抓到 React #310)。已把该 effect 移到早退之前(无条件 hook)。
|
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.3",
|
|
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,8 +70,9 @@ window.__ModuleLoader__.load({
|
|
|
70
70
|
'busy.restore': '恢复中…',
|
|
71
71
|
'confirm.restore': '恢复',
|
|
72
72
|
'ack.restore': '我已了解:恢复将以新 seq 重放被遮蔽的消息(原日志不变)',
|
|
73
|
-
'dock.title': '
|
|
74
|
-
'dock.
|
|
73
|
+
'dock.title': '已回撤 {n} 条消息',
|
|
74
|
+
'dock.expand': '展开回撤列表',
|
|
75
|
+
'dock.collapse': '折叠回撤列表',
|
|
75
76
|
'dock.restore': '恢复',
|
|
76
77
|
'dock.restoring': '恢复中…',
|
|
77
78
|
'menu.ops': '消息操作',
|
|
@@ -121,7 +122,9 @@ window.__ModuleLoader__.load({
|
|
|
121
122
|
'done.delete': 'Deleted; reload to apply',
|
|
122
123
|
'action.reload': 'Reload page',
|
|
123
124
|
'errorPrefix': 'Operation failed: ',
|
|
124
|
-
'dock.title': '
|
|
125
|
+
'dock.title': '{n} messages rolled back',
|
|
126
|
+
'dock.expand': 'Expand revert list',
|
|
127
|
+
'dock.collapse': 'Collapse revert list',
|
|
125
128
|
'dock.shadowedN': '{n} shadowed',
|
|
126
129
|
'dock.restore': 'Restore',
|
|
127
130
|
'dock.restoring': 'Restoring…',
|
|
@@ -677,6 +680,16 @@ window.__ModuleLoader__.load({
|
|
|
677
680
|
const INPUT_DOCK_SLOT = 'conversation.input.dock'
|
|
678
681
|
const INPUT_DOCK_ID = 'message-ops-revert-dock'
|
|
679
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
|
+
|
|
680
693
|
function RevertDock(props) {
|
|
681
694
|
const { sessionId, useSessions, inputActions } = props
|
|
682
695
|
const t = (props && props.t) || __t
|
|
@@ -686,27 +699,34 @@ window.__ModuleLoader__.load({
|
|
|
686
699
|
const summary = sessions && sessions.byId ? sessions.byId[sessionId] : undefined
|
|
687
700
|
const running = summary ? summary.running === true : false
|
|
688
701
|
const [open, setOpen] = useState(false)
|
|
689
|
-
const [markers, setMarkers] = useState(null) //
|
|
690
|
-
const [hiddenByMarker, setHiddenByMarker] = useState({}) // markerSeq -> 当前不可见条数
|
|
702
|
+
const [markers, setMarkers] = useState(null) // 活跃回撤标记(已恢复的自动消失)
|
|
691
703
|
const [restoring, setRestoring] = useState(null)
|
|
692
704
|
const [styleInjected, setStyleInjected] = useState(false)
|
|
693
705
|
|
|
694
|
-
//
|
|
706
|
+
// opencode SessionRevertDock 视觉(v2 布局,DSH 令牌等价映射)
|
|
695
707
|
useEffect(() => {
|
|
696
708
|
if (styleInjected || typeof document === 'undefined') return
|
|
697
709
|
if (document.getElementById('dsh-message-ops-dock-style')) { setStyleInjected(true); return }
|
|
698
710
|
const tag = document.createElement('style')
|
|
699
711
|
tag.id = 'dsh-message-ops-dock-style'
|
|
700
712
|
tag.textContent = [
|
|
701
|
-
|
|
702
|
-
'.
|
|
703
|
-
|
|
704
|
-
'.
|
|
705
|
-
'.
|
|
706
|
-
'.
|
|
707
|
-
'.
|
|
708
|
-
'.
|
|
709
|
-
'.
|
|
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}',
|
|
710
730
|
].join('')
|
|
711
731
|
document.head.appendChild(tag)
|
|
712
732
|
setStyleInjected(true)
|
|
@@ -718,25 +738,16 @@ window.__ModuleLoader__.load({
|
|
|
718
738
|
.then((r) => (r.ok ? r.json() : null))
|
|
719
739
|
.then((data) => {
|
|
720
740
|
if (!alive || !data || !data.ok) return
|
|
721
|
-
|
|
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)
|
|
741
|
+
setMarkers(activeMarkers(data.messages || []))
|
|
733
742
|
})
|
|
734
743
|
.catch(() => {})
|
|
735
744
|
load()
|
|
736
745
|
return () => { alive = false }
|
|
737
746
|
}, [sessionId])
|
|
738
747
|
|
|
739
|
-
|
|
748
|
+
// items 变化自动折叠(opencode createEffect 同款;必须在早退之前——Hooks 规则)
|
|
749
|
+
useEffect(() => { setOpen(false) }, [markers && markers.length, markers && markers[0] && markers[0].seq])
|
|
750
|
+
if (!markers || !markers.length) return null
|
|
740
751
|
|
|
741
752
|
const restoreRow = (row) => {
|
|
742
753
|
if (restoring != null || running) return
|
|
@@ -751,20 +762,8 @@ window.__ModuleLoader__.load({
|
|
|
751
762
|
if (!res.ok || !data.ok) throw new Error(data.error || ('HTTP ' + res.status))
|
|
752
763
|
notifyDone(t('done.restore', { n: String(data.restoredCount != null ? data.restoredCount : '?'), s: String(data.skipped != null ? data.skipped : 0) }))
|
|
753
764
|
setRestoring(null)
|
|
754
|
-
// 重放后刷新标记与计数
|
|
755
765
|
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
|
-
}
|
|
766
|
+
if (fresh && fresh.ok) setMarkers(activeMarkers(fresh.messages || []))
|
|
768
767
|
})
|
|
769
768
|
.catch((reason) => {
|
|
770
769
|
notifyDone(t('errorPrefix') + (reason && reason.message ? reason.message : String(reason)), 'error')
|
|
@@ -772,25 +771,40 @@ window.__ModuleLoader__.load({
|
|
|
772
771
|
})
|
|
773
772
|
}
|
|
774
773
|
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
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 || '')),
|
|
779
781
|
React.createElement('button', {
|
|
780
|
-
type: 'button',
|
|
781
|
-
className: 'mopsDockRestore',
|
|
782
|
+
type: 'button', className: 'mopsRdRestore', disabled: restoring != null || running,
|
|
782
783
|
onClick: () => restoreRow(row),
|
|
783
784
|
}, restoring === row.seq ? t('dock.restoring') : t('dock.restore')),
|
|
784
785
|
)
|
|
785
786
|
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
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,
|
|
799
|
+
},
|
|
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,
|
|
794
808
|
)
|
|
795
809
|
}
|
|
796
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
|
@@ -188,6 +188,9 @@ export function listMessages(events) {
|
|
|
188
188
|
range: isReplaceMarker && e.surfaceOp.startSeq != null && e.surfaceOp.endSeq != null
|
|
189
189
|
? { start: e.surfaceOp.startSeq, end: e.surfaceOp.endSeq } : null,
|
|
190
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,
|
|
191
194
|
time: e.time ?? null,
|
|
192
195
|
turn: (e.data && e.data.turn) ?? null,
|
|
193
196
|
});
|