@240xu/dsh-message-ops 0.3.0 → 0.3.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 +18 -2
- package/package.json +1 -1
- package/src/client.js +67 -22
package/README.md
CHANGED
|
@@ -161,8 +161,8 @@ MIT
|
|
|
161
161
|
针对 dsh 0.2.0-rc.2 的用户级操作面(agent 工具保留,但 UI 为主推入口):
|
|
162
162
|
|
|
163
163
|
- **每条 AI 消息旁的原生回撤按钮**:注入官方 `conversation.chat.assistant-actions`
|
|
164
|
-
槽(0.2.0 正式扩展点,条目收持久化 messageId
|
|
165
|
-
|
|
164
|
+
槽(0.2.0 正式扩展点,条目收持久化 messageId)——「回滚到此 / 删除此条 /
|
|
165
|
+
从此分支」直接出现在消息操作行,与官方点赞/点踩并列;0.1.x 无此槽自动 no-op。
|
|
166
166
|
messageId→seq 反查经 `/api/message-ops/messages`(每会话索引缓存)。
|
|
167
167
|
- **分支双路径**:0.2.0+ 优先官方 `sessions.fork({sessionId, atSeq, increaseTitle})`
|
|
168
168
|
(子会话自动进宿主列表,成功后可一键 `uiWorkspace.openSession` 打开);
|
|
@@ -172,3 +172,19 @@ MIT
|
|
|
172
172
|
确认弹窗内联语义说明(评审缺口销账)。
|
|
173
173
|
- 官方已有但易混淆的:0.2.0 原生分支按钮(仅限已完成轮次最后一条消息)与
|
|
174
174
|
本插件的「任意位置分支/回滚」互补不冲突。
|
|
175
|
+
|
|
176
|
+
### 0.3.1 · 槽按钮重构(opencode 式一键 + 去重)
|
|
177
|
+
|
|
178
|
+
- **一键立即执行**:回滚/删除点击即生效(无对话框),devkit toast 反馈;宿主
|
|
179
|
+
0.2.0 chat store 原生响应 surface replace——遮蔽即时呈现,无需刷新页面。
|
|
180
|
+
- **与官方去重**:槽内移除分支按钮——官方 0.2.0 已在消息操作行自带分支
|
|
181
|
+
(限已完成轮次最后一条);任意位置分支保留在对话框(官方没有的能力)。
|
|
182
|
+
- **原生观感**:按钮改用官方 primitives 图标(IconClockOutline/IconTrashOutline,
|
|
183
|
+
require 失败回退内联 SVG);S3 命中区 ≥44px;S11 双提交防护(busy 互斥)。
|
|
184
|
+
|
|
185
|
+
### 0.3.2 · 图标规范化
|
|
186
|
+
|
|
187
|
+
- 槽按钮去掉文字 emoji——0.2.0+ 直接用官方 primitives 图标(与原生操作行 1:1 同款);
|
|
188
|
+
回退路径的 SVG 也改为官方 IconClockOutline/IconTrashOutline 的精确 path 数据
|
|
189
|
+
(1px stroke,与 Regular 变体一致),不再手绘。
|
|
190
|
+
- 字典文案去 emoji(label 仅用于 tooltip/aria,不参与渲染)。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@240xu/dsh-message-ops",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.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
|
@@ -94,9 +94,9 @@ window.__ModuleLoader__.load({
|
|
|
94
94
|
'op.branchDesc': 'Copies everything up to and including the picked message into a new session (original untouched, non-destructive).',
|
|
95
95
|
'op.restore': 'Restore (replay shadowed messages)',
|
|
96
96
|
'op.restoreDesc': 'Only when the selected row is a revert/delete marker. This is a replay, not an un-shadow: shadowed user/assistant messages are re-appended with NEW seqs and a [Restored] prefix; non-replayable events (tool calls) are skipped and counted.',
|
|
97
|
-
'slot.revert': '
|
|
98
|
-
'slot.delete': '
|
|
99
|
-
'slot.branch': '
|
|
97
|
+
'slot.revert': 'Revert to here',
|
|
98
|
+
'slot.delete': 'Delete this message',
|
|
99
|
+
'slot.branch': 'Branch from here',
|
|
100
100
|
'toast.openNew': 'Open new session',
|
|
101
101
|
'fork.official': 'Branched (official fork): new session {id}',
|
|
102
102
|
'fork.disk': 'Branched: new session {id} (visible after list refresh)',
|
|
@@ -523,10 +523,10 @@ window.__ModuleLoader__.load({
|
|
|
523
523
|
}
|
|
524
524
|
|
|
525
525
|
// 成功反馈统一出口:devkit 标准 toast 优先,无 devkit 时对话框内文案兜底。
|
|
526
|
-
function notifyDone(msg) {
|
|
526
|
+
function notifyDone(msg, kind) {
|
|
527
527
|
try {
|
|
528
528
|
const dk = window.__dshDevkit
|
|
529
|
-
if (dk && typeof dk.toast === 'function') dk.toast(msg, { kind: 'ok' })
|
|
529
|
+
if (dk && typeof dk.toast === 'function') dk.toast(msg, { kind: kind || 'ok' })
|
|
530
530
|
} catch { /* toast 缺席不阻断成功反馈 */ }
|
|
531
531
|
}
|
|
532
532
|
|
|
@@ -550,6 +550,30 @@ window.__ModuleLoader__.load({
|
|
|
550
550
|
}, React.createElement(BranchIcon))
|
|
551
551
|
}
|
|
552
552
|
|
|
553
|
+
// 官方图标适配:0.2.0 primitives 的 IconClock/IconTrash 与原生操作行同款;
|
|
554
|
+
// require 失败(0.1.x 或裁剪环境)回退内联 SVG,槽在 0.1.x 本就不存在,仅防御。
|
|
555
|
+
var IconClock = null
|
|
556
|
+
var IconTrash = null
|
|
557
|
+
try {
|
|
558
|
+
const P = require('@deepseek-ai/dsh-client-ui-primitives')
|
|
559
|
+
IconClock = P && P.IconClockOutlineRegular
|
|
560
|
+
IconTrash = P && P.IconTrashOutlineRegular
|
|
561
|
+
} catch { /* fallback below */ }
|
|
562
|
+
if (!IconClock) IconClock = function ClockFallback() {
|
|
563
|
+
// 官方 IconClockOutlineArtwork 1:1 路径( Regular = 1px stroke)
|
|
564
|
+
return React.createElement('svg', { width: 16, height: 16, viewBox: '0 0 16 16', fill: 'none', 'aria-hidden': true, stroke: 'currentColor', strokeWidth: 1 },
|
|
565
|
+
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' }),
|
|
566
|
+
React.createElement('path', { d: 'M8 4.31V8.46L11 10.08' }))
|
|
567
|
+
}
|
|
568
|
+
if (!IconTrash) IconTrash = function TrashFallback() {
|
|
569
|
+
// 官方 IconTrashOutlineArtwork 1:1 路径
|
|
570
|
+
return React.createElement('svg', { width: 16, height: 16, viewBox: '0 0 16 16', fill: 'none', 'aria-hidden': true, stroke: 'currentColor', strokeWidth: 1 },
|
|
571
|
+
React.createElement('path', { d: 'M1.28149 3.88831H14.7187' }),
|
|
572
|
+
React.createElement('path', { d: 'M5.41602 3.88833V2.47962C5.41602 2.29282 5.52492 2.11366 5.71876 1.98157C5.9126 1.84948 6.17551 1.77527 6.44964 1.77527H9.55053C9.82466 1.77527 10.0876 1.84948 10.2814 1.98157C10.4752 2.11366 10.5841 2.29282 10.5841 2.47962V3.88833' }),
|
|
573
|
+
React.createElement('path', { d: 'M3.29749 5.10193L4.06585 13.0192C4.10899 13.4595 4.48223 13.7942 4.92504 13.7942H11.0751C11.5179 13.7942 11.8912 13.4595 11.9343 13.0192L12.7027 5.10193' }),
|
|
574
|
+
React.createElement('path', { d: 'M6.27637 7.51831V11.1829M9.72378 7.51831V11.1829' }))
|
|
575
|
+
}
|
|
576
|
+
|
|
553
577
|
// --- 0.3.0 assistant-actions 官方槽:每条 AI 消息旁的原生回撤按钮 ------------
|
|
554
578
|
// 契约(dsh-cordis-client-runner):scope session,条目组件收 { messageId } +
|
|
555
579
|
// 标准props(useSessions/sessionId/useSession/useChat…)。0.1.x 无此 key,
|
|
@@ -558,22 +582,21 @@ window.__ModuleLoader__.load({
|
|
|
558
582
|
const CHAT_ACTIONS_ID = 'message-ops-row'
|
|
559
583
|
|
|
560
584
|
function MsgSlotActions(props) {
|
|
561
|
-
const { messageId, sessionId, useSessions
|
|
585
|
+
const { messageId, sessionId, useSessions } = props
|
|
562
586
|
const t = (props && props.t) || __t
|
|
563
587
|
useLocaleRevision()
|
|
564
588
|
const sessions = useSessions ? useSessions((s) => s) : undefined
|
|
565
589
|
const summary = sessions && sessions.byId ? sessions.byId[sessionId] : undefined
|
|
566
590
|
const running = summary ? summary.running === true : false
|
|
567
|
-
// messageId → seq 反查:官方槽只给 messageId;seq 由槽按钮需要。
|
|
568
|
-
// 路径:messages 端点一次性拉取建索引(每会话缓存),失败则按钮隐藏。
|
|
569
591
|
const [seq, setSeq] = React.useState(null)
|
|
592
|
+
const [busy, setBusy] = React.useState(null) // 'revert' | 'delete' | null(S11 双提交防护)
|
|
570
593
|
React.useEffect(() => {
|
|
571
594
|
if (!messageId || !sessionId) return
|
|
572
595
|
let alive = true
|
|
573
596
|
const cached = __seqIndexCache.get(sessionId)
|
|
574
597
|
const resolve = (index) => {
|
|
575
598
|
if (!alive) return
|
|
576
|
-
const hit = index.get(String(messageId))
|
|
599
|
+
const hit = index && index.get(String(messageId))
|
|
577
600
|
if (typeof hit === 'number') setSeq(hit)
|
|
578
601
|
}
|
|
579
602
|
if (cached && typeof cached.then === 'function') cached.then(resolve).catch(() => {})
|
|
@@ -594,20 +617,42 @@ window.__ModuleLoader__.load({
|
|
|
594
617
|
}
|
|
595
618
|
return () => { alive = false }
|
|
596
619
|
}, [messageId, sessionId])
|
|
620
|
+
|
|
621
|
+
// opencode 式一键回撤:点击立即执行(无对话框),成功走 devkit toast。
|
|
622
|
+
// 宿主 0.2.0 chat store 原生响应 surface replace 事件——遮蔽无需刷新页面。
|
|
623
|
+
const run = (mode) => {
|
|
624
|
+
if (busy || seq == null || running) return
|
|
625
|
+
setBusy(mode)
|
|
626
|
+
fetch('/api/message-ops/' + mode, {
|
|
627
|
+
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
628
|
+
body: JSON.stringify({ sessionId, seq }),
|
|
629
|
+
})
|
|
630
|
+
.then(async (res) => {
|
|
631
|
+
let data = {}
|
|
632
|
+
try { data = await res.json() } catch { /* keep {} */ }
|
|
633
|
+
if (!res.ok || !data.ok) throw new Error(data.error || ('HTTP ' + res.status))
|
|
634
|
+
notifyDone(t(mode === 'revert' ? 'done.revert' : 'done.delete'))
|
|
635
|
+
})
|
|
636
|
+
.catch((reason) => {
|
|
637
|
+
notifyDone(t('errorPrefix') + (reason && reason.message ? reason.message : String(reason)), 'error')
|
|
638
|
+
})
|
|
639
|
+
.finally(() => setBusy(null))
|
|
640
|
+
}
|
|
641
|
+
|
|
597
642
|
if (seq == null) return null
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
643
|
+
// 官方图标(与原生操作行同款);S3:视觉 16px,命中区 ≥44px。
|
|
644
|
+
const act = (icon, label, mode) => React.createElement('button', {
|
|
645
|
+
type: 'button', title: running ? t('dialog.runningWarn') : t('slot.' + mode),
|
|
646
|
+
'aria-label': t('slot.' + mode), disabled: !!busy || running,
|
|
647
|
+
style: { display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
|
|
648
|
+
minWidth: 44, minHeight: 44, padding: 0, border: 'none', background: 'transparent',
|
|
649
|
+
color: 'inherit', opacity: busy && busy !== mode ? 0.4 : running ? 0.35 : 0.72,
|
|
650
|
+
cursor: busy || running ? 'default' : 'pointer', borderRadius: 6 },
|
|
651
|
+
onClick: () => run(mode),
|
|
652
|
+
}, icon)
|
|
653
|
+
return React.createElement('span', { style: { display: 'inline-flex', gap: 0 } },
|
|
654
|
+
act(React.createElement(IconClock, { size: 16 }), t('slot.revert'), 'revert'),
|
|
655
|
+
act(React.createElement(IconTrash, { size: 16 }), t('slot.delete'), 'delete'),
|
|
611
656
|
)
|
|
612
657
|
}
|
|
613
658
|
|