@240xu/dsh-message-ops 0.4.0 → 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 +10 -0
- package/package.json +1 -1
- package/src/client.js +67 -37
- package/src/session-file.js +4 -0
package/README.md
CHANGED
|
@@ -198,3 +198,13 @@ 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 视觉完全一致。
|
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.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,7 +70,8 @@ window.__ModuleLoader__.load({
|
|
|
70
70
|
'busy.restore': '恢复中…',
|
|
71
71
|
'confirm.restore': '恢复',
|
|
72
72
|
'ack.restore': '我已了解:恢复将以新 seq 重放被遮蔽的消息(原日志不变)',
|
|
73
|
-
'dock.title': '
|
|
73
|
+
'dock.title': '回撤记录 · {n}',
|
|
74
|
+
'dock.shadowedN': '遮蔽 {n} 条',
|
|
74
75
|
'dock.restore': '恢复',
|
|
75
76
|
'dock.restoring': '恢复中…',
|
|
76
77
|
'menu.ops': '消息操作',
|
|
@@ -120,7 +121,8 @@ window.__ModuleLoader__.load({
|
|
|
120
121
|
'done.delete': 'Deleted; reload to apply',
|
|
121
122
|
'action.reload': 'Reload page',
|
|
122
123
|
'errorPrefix': 'Operation failed: ',
|
|
123
|
-
'dock.title': '{n}
|
|
124
|
+
'dock.title': 'Revert history · {n}',
|
|
125
|
+
'dock.shadowedN': '{n} shadowed',
|
|
124
126
|
'dock.restore': 'Restore',
|
|
125
127
|
'dock.restoring': 'Restoring…',
|
|
126
128
|
'menu.ops': 'Message ops',
|
|
@@ -684,25 +686,57 @@ window.__ModuleLoader__.load({
|
|
|
684
686
|
const summary = sessions && sessions.byId ? sessions.byId[sessionId] : undefined
|
|
685
687
|
const running = summary ? summary.running === true : false
|
|
686
688
|
const [open, setOpen] = useState(false)
|
|
687
|
-
const [
|
|
688
|
-
const [
|
|
689
|
+
const [markers, setMarkers] = useState(null) // [{seq, snippet, range}] 非 compaction 的回撤标记
|
|
690
|
+
const [hiddenByMarker, setHiddenByMarker] = useState({}) // markerSeq -> 当前不可见条数
|
|
689
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])
|
|
690
714
|
|
|
691
715
|
useEffect(() => {
|
|
692
716
|
let alive = true
|
|
693
|
-
fetch('/api/message-ops/messages?sessionId=' + encodeURIComponent(sessionId))
|
|
717
|
+
const load = () => fetch('/api/message-ops/messages?sessionId=' + encodeURIComponent(sessionId))
|
|
694
718
|
.then((r) => (r.ok ? r.json() : null))
|
|
695
719
|
.then((data) => {
|
|
696
720
|
if (!alive || !data || !data.ok) return
|
|
697
721
|
const msgs = data.messages || []
|
|
698
|
-
|
|
699
|
-
|
|
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)
|
|
700
733
|
})
|
|
701
734
|
.catch(() => {})
|
|
735
|
+
load()
|
|
702
736
|
return () => { alive = false }
|
|
703
737
|
}, [sessionId])
|
|
704
738
|
|
|
705
|
-
if (!
|
|
739
|
+
if (!markers || !markers.length) return null // 无手工回撤标记 → dock 不渲染
|
|
706
740
|
|
|
707
741
|
const restoreRow = (row) => {
|
|
708
742
|
if (restoring != null || running) return
|
|
@@ -716,51 +750,47 @@ window.__ModuleLoader__.load({
|
|
|
716
750
|
try { data = await res.json() } catch { /* keep {} */ }
|
|
717
751
|
if (!res.ok || !data.ok) throw new Error(data.error || ('HTTP ' + res.status))
|
|
718
752
|
notifyDone(t('done.restore', { n: String(data.restoredCount != null ? data.restoredCount : '?'), s: String(data.skipped != null ? data.skipped : 0) }))
|
|
719
|
-
|
|
753
|
+
setRestoring(null)
|
|
754
|
+
// 重放后刷新标记与计数
|
|
720
755
|
const fresh = await fetch('/api/message-ops/messages?sessionId=' + encodeURIComponent(sessionId)).then((r) => (r.ok ? r.json() : null)).catch(() => null)
|
|
721
756
|
if (fresh && fresh.ok) {
|
|
722
757
|
const msgs = fresh.messages || []
|
|
723
|
-
|
|
724
|
-
|
|
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)
|
|
725
767
|
}
|
|
726
768
|
})
|
|
727
769
|
.catch((reason) => {
|
|
728
770
|
notifyDone(t('errorPrefix') + (reason && reason.message ? reason.message : String(reason)), 'error')
|
|
771
|
+
setRestoring(null)
|
|
729
772
|
})
|
|
730
|
-
.finally(() => setRestoring(null))
|
|
731
773
|
}
|
|
732
774
|
|
|
733
|
-
const rowEl = (row) => React.createElement('div', {
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
},
|
|
738
|
-
React.createElement('span', { style: { flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', fontSize: 12, opacity: 0.85 } },
|
|
739
|
-
'#' + row.seq + ' ' + (row.role || '') + ' · ' + (row.snippet || '')),
|
|
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 || '')),
|
|
740
779
|
React.createElement('button', {
|
|
741
780
|
type: 'button', 'aria-label': t('dock.restore'), disabled: restoring != null || running,
|
|
742
|
-
|
|
743
|
-
font: 'inherit', fontSize: 12, cursor: restoring != null || running ? 'default' : 'pointer',
|
|
744
|
-
opacity: restoring != null || running ? 0.5 : 1, padding: '4px 8px', minHeight: 28 },
|
|
781
|
+
className: 'mopsDockRestore',
|
|
745
782
|
onClick: () => restoreRow(row),
|
|
746
783
|
}, restoring === row.seq ? t('dock.restoring') : t('dock.restore')),
|
|
747
784
|
)
|
|
748
785
|
|
|
749
|
-
return React.createElement('div', {
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
border: 'none', background: 'transparent', color: 'inherit',
|
|
758
|
-
font: 'inherit', fontSize: 12, padding: '6px 10px', cursor: 'pointer', minHeight: 32 },
|
|
759
|
-
onClick: () => setOpen((v) => !v),
|
|
760
|
-
},
|
|
761
|
-
React.createElement('span', null, t('dock.title', { n: String(shadowedCount) })),
|
|
762
|
-
React.createElement('span', { style: { opacity: 0.6 } }, open ? '▾' : '▸')),
|
|
763
|
-
open ? React.createElement('div', { style: { maxHeight: 180, overflowY: 'auto' } }, rows.map(rowEl)) : null,
|
|
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,
|
|
764
794
|
)
|
|
765
795
|
}
|
|
766
796
|
|
package/src/session-file.js
CHANGED
|
@@ -184,6 +184,10 @@ 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,
|
|
187
191
|
time: e.time ?? null,
|
|
188
192
|
turn: (e.data && e.data.turn) ?? null,
|
|
189
193
|
});
|