@morlay/ui-conversation-message-actions 0.0.15 → 0.0.16
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 -5
- package/dist/client.js +72 -109
- package/dist/index.d.mts +14 -2
- package/dist/index.mjs +1 -1
- package/dist/{plan-BdU_mnTv.d.mts → plan-Cn3TPTa0.d.mts} +7 -2
- package/dist/plan.d.mts +1 -1
- package/dist/{src-ks167MhO.mjs → src-Dq1X3rOO.mjs} +52 -1
- package/dist/testing.mjs +1 -1
- package/package.json +19 -19
- package/src/client/chat-node/MessageItem.tsx +27 -9
- package/src/client/controller.ts +30 -2
- package/src/index.ts +72 -1
- package/src/shared.ts +7 -0
- package/src/types.ts +8 -0
- package/src/client/chat-node/MessageEditDialog.module.css +0 -24
- package/src/client/chat-node/MessageEditDialog.tsx +0 -71
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @morlay/ui-conversation-message-actions
|
|
2
2
|
|
|
3
|
-
`rewind / retry / fork` 的**编排层**:在 `@morlay/session-branch` 的 provider
|
|
3
|
+
`rewind / retry / recall / fork` 的**编排层**:在 `@morlay/session-branch` 的 provider
|
|
4
4
|
抽象之上组装完整功能(产品语义对齐 [dsh-message-edit](https://github.com/Moeblack/dsh-message-edit)),
|
|
5
5
|
并提供**浏览器半**(client bundle):替换 `conversation.chat.node` 的
|
|
6
6
|
`user` / `steering` 渲染,在 user 消息行内直接挂编辑 / 重试入口。
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
| `reroll` | 重生成最后一条已落定助手回复(使用原用户输入) |
|
|
14
14
|
| `retry` | 重试任意历史回合(`truncate` 只重放目标输入 / `preserve` 重放后续全部) |
|
|
15
15
|
| `rewind` | 截断式回退:原会话回退到闭合 `turn/end` 边界 |
|
|
16
|
+
| `recall` | 撤回 user 消息到输入框:只 `rewind` 截断,不重写、不重放(交给用户改后重发) |
|
|
16
17
|
| `fork` | 从任意闭合边界派生**新会话**(唯一产生新 id 的操作) |
|
|
17
18
|
| `timeline` | 版本树投影(HTTP:`GET /session-editor?sessionId=…`) |
|
|
18
19
|
|
|
@@ -28,6 +29,9 @@
|
|
|
28
29
|
点不开的空项与轮号空洞;manualTurn 的轮号同样按保留前缀收敛(正常会话
|
|
29
30
|
等于目标轮号)。版本效果仍记录**原**目标轮号(操作历史);
|
|
30
31
|
- **只有 `fork` 创建新 id**(`ForkOperation` / `forkFrom`,纯 append 派生);
|
|
32
|
+
- **`recall` 只截断不重写**:边界与 `edit` 的轮首 user 一致(轮首整轮截断到前一轮
|
|
33
|
+
`turn/end`,轮内 followup exclusive drop),但只 `rewind` 即返回——不写版本效果、
|
|
34
|
+
不重放;被撤回的 user 文本由浏览器半回填到 composer,用户修改后自行发送;
|
|
31
35
|
- 版本效果事件(`session-branch/version`)携带 `ignorable: true`:**原样落库**
|
|
32
36
|
(rdb 不按信封过滤,与 JSONL 一致),非 branch 读者凭信封跳过;
|
|
33
37
|
- 重放输入经 agent 驱动(见下)排队到原会话,agent 基于截断后历史回复。
|
|
@@ -52,9 +56,10 @@
|
|
|
52
56
|
|
|
53
57
|
- **shadow 注册**:`user` / `steering` 两个 key 以 `priority: -1` 重新注册
|
|
54
58
|
(最低优先级渲染,shadow 上游默认注册);其余 key 沿用上游渲染器;
|
|
55
|
-
- **编辑 / 重试按钮只挂在 user 消息**(`UserMessageNodeView
|
|
56
|
-
|
|
57
|
-
|
|
59
|
+
- **编辑 / 重试按钮只挂在 user 消息**(`UserMessageNodeView`):编辑不再打开编辑
|
|
60
|
+
弹窗,而是确认(`Modal` + `Button`)后调用 `recall`——服务端只 `rewind` 截断,
|
|
61
|
+
客户端把消息文本 `setDraft` 回填到主输入框(`conversation.input`),交给用户
|
|
62
|
+
修改后自行发送;重试仍先弹确认;未闭合轮次不显示重试;
|
|
58
63
|
- **操作后刷新**:就地编辑后优先调用客户端会话级 `resync()`(重置窗口并重新
|
|
59
64
|
拉取历史,不整页重载——rewind 的删除无法经 append-only 事件流表达,seq
|
|
60
65
|
回退只做增量会残留旧节点),随后丢弃该会话的全部投影行
|
|
@@ -73,7 +78,7 @@
|
|
|
73
78
|
`ctx.sessionBranch`(provider 实现,如 `@morlay/session-rdb`);
|
|
74
79
|
- 本服务只做**编排**:闭合轮次扫描、版本效果事件构造、派生 seed 组装、
|
|
75
80
|
rewind 命令透传、agent 驱动、HTTP 面(`POST /session-editor` 执行
|
|
76
|
-
edit / reroll / retry / rewind / fork)。
|
|
81
|
+
edit / reroll / retry / rewind / recall / fork)。
|
|
77
82
|
|
|
78
83
|
## 装配
|
|
79
84
|
|
package/dist/client.js
CHANGED
|
@@ -82,6 +82,11 @@ window.__ModuleLoader__.load({
|
|
|
82
82
|
sessionId: this.sessionId,
|
|
83
83
|
toBoundary
|
|
84
84
|
}),
|
|
85
|
+
recall: (message) => this.mutate({
|
|
86
|
+
action: "recall",
|
|
87
|
+
sessionId: this.sessionId,
|
|
88
|
+
eventSeq: message.eventSeq
|
|
89
|
+
}, () => this.setComposerDraft(message.text)),
|
|
85
90
|
importSession: (file) => this.importSession(file),
|
|
86
91
|
openVersion: (sessionId) => this.openWhenListed(sessionId)
|
|
87
92
|
};
|
|
@@ -158,7 +163,7 @@ window.__ModuleLoader__.load({
|
|
|
158
163
|
});
|
|
159
164
|
}
|
|
160
165
|
}
|
|
161
|
-
async mutate(operation) {
|
|
166
|
+
async mutate(operation, onApplied) {
|
|
162
167
|
if (this.store.getSnapshot().pending !== null) return false;
|
|
163
168
|
this.store.update((state) => {
|
|
164
169
|
state.pending = operation.action;
|
|
@@ -182,6 +187,7 @@ window.__ModuleLoader__.load({
|
|
|
182
187
|
this.store.update((state) => {
|
|
183
188
|
state.pending = null;
|
|
184
189
|
});
|
|
190
|
+
onApplied?.();
|
|
185
191
|
const result = value;
|
|
186
192
|
if (String(result.sessionId) !== String(this.sessionId)) {
|
|
187
193
|
await this.openWhenListed(result.sessionId);
|
|
@@ -211,6 +217,17 @@ window.__ModuleLoader__.load({
|
|
|
211
217
|
return false;
|
|
212
218
|
}
|
|
213
219
|
}
|
|
220
|
+
/**
|
|
221
|
+
* 把撤回的消息文本回填到该会话的 composer 草稿(`conversation.input` 的
|
|
222
|
+
* `setDraft`,即主输入框的整段替换语义)。会话 scope / binding / conversation
|
|
223
|
+
* 服务缺失时静默跳过——撤回本身已成功,草稿回填失败不应回滚会话状态。
|
|
224
|
+
*/
|
|
225
|
+
setComposerDraft(text) {
|
|
226
|
+
if (this.sessions.binding(this.sessionId) === void 0) return;
|
|
227
|
+
const scoped = this.sessions.scope(this.sessionId);
|
|
228
|
+
if (scoped === void 0) return;
|
|
229
|
+
scoped.get("conversation")?.input?.for(scoped).setDraft(text);
|
|
230
|
+
}
|
|
214
231
|
async importSession(file) {
|
|
215
232
|
if (this.store.getSnapshot().pending !== null) return false;
|
|
216
233
|
this.store.update((state) => {
|
|
@@ -351,22 +368,22 @@ window.__ModuleLoader__.load({
|
|
|
351
368
|
}
|
|
352
369
|
//#endregion
|
|
353
370
|
//#region \0dsh-css:/Users/morlay/src/github.com/morlay/better-session/packages/ui-conversation-message-actions/src/client/chat-node/MessageIconActions.module.css.mjs
|
|
354
|
-
const css$
|
|
355
|
-
const tagId$
|
|
356
|
-
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$
|
|
371
|
+
const css$1 = ".lKOSUG_actions{align-items:center;gap:10px;height:28px;display:flex}.lKOSUG_timeStart{color:var(--dsw-alias-label-tertiary);white-space:nowrap;padding-right:12px;font-size:14px;line-height:24px}.lKOSUG_timeEnd{color:var(--dsw-alias-label-tertiary);white-space:nowrap;padding-left:12px;font-size:14px;line-height:24px}.lKOSUG_runTimeDot{margin:0 10px}@media (hover:hover){[data-time-hover-root] :is(.lKOSUG_timeStart,.lKOSUG_timeEnd){opacity:0;transition:opacity 80ms}[data-time-hover-root]:hover :is(.lKOSUG_timeStart,.lKOSUG_timeEnd),[data-time-hover-root]:focus-within :is(.lKOSUG_timeStart,.lKOSUG_timeEnd){opacity:1}}.lKOSUG_action{width:28px;height:28px;color:var(--dsw-alias-label-tertiary);cursor:pointer;background:0 0;border:none;border-radius:28px;justify-content:center;align-items:center;padding:6px;display:inline-flex}.lKOSUG_action:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary)}.lKOSUG_action[data-unavailable]{cursor:default;opacity:.4}.lKOSUG_action[data-unavailable]:hover{color:var(--dsw-alias-label-tertiary);background:0 0}.lKOSUG_visuallyHidden{clip:rect(0 0 0 0);white-space:nowrap;width:1px;height:1px;position:absolute;overflow:hidden}";
|
|
372
|
+
const tagId$1 = "@morlay/ui-conversation-message-actions/MessageIconActions.module.css";
|
|
373
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]") === null) {
|
|
357
374
|
const tag = document.createElement("style");
|
|
358
375
|
tag.dataset.plugin = "@morlay/ui-conversation-message-actions";
|
|
359
|
-
tag.dataset.pluginCss = tagId$
|
|
360
|
-
tag.textContent = css$
|
|
376
|
+
tag.dataset.pluginCss = tagId$1;
|
|
377
|
+
tag.textContent = css$1;
|
|
361
378
|
document.head.appendChild(tag);
|
|
362
379
|
}
|
|
363
380
|
var MessageIconActions_module_css_default = {
|
|
364
|
-
"
|
|
381
|
+
"visuallyHidden": "lKOSUG_visuallyHidden",
|
|
365
382
|
"actions": "lKOSUG_actions",
|
|
366
|
-
"timeStart": "lKOSUG_timeStart",
|
|
367
383
|
"timeEnd": "lKOSUG_timeEnd",
|
|
384
|
+
"timeStart": "lKOSUG_timeStart",
|
|
368
385
|
"action": "lKOSUG_action",
|
|
369
|
-
"
|
|
386
|
+
"runTimeDot": "lKOSUG_runTimeDot"
|
|
370
387
|
};
|
|
371
388
|
//#endregion
|
|
372
389
|
//#region src/client/chat-node/MessageIconActions.tsx
|
|
@@ -497,78 +514,6 @@ window.__ModuleLoader__.load({
|
|
|
497
514
|
});
|
|
498
515
|
}
|
|
499
516
|
//#endregion
|
|
500
|
-
//#region \0dsh-css:/Users/morlay/src/github.com/morlay/better-session/packages/ui-conversation-message-actions/src/client/chat-node/MessageEditDialog.module.css.mjs
|
|
501
|
-
const css$1 = "._7VqDXa_input{box-sizing:border-box;width:100%;min-height:96px;font:inherit;border:1px solid var(--dsw-alias-border-l2-darkmode-thin);background:var(--dsw-specific-input-major);box-shadow:var(--dsw-shadow-lv2);color:var(--dsw-text,inherit);resize:none;border-radius:22px;padding:10px 16px;font-size:16px;line-height:24px;overflow-y:auto}._7VqDXa_actions{justify-content:flex-end;gap:8px;display:flex}";
|
|
502
|
-
const tagId$1 = "@morlay/ui-conversation-message-actions/MessageEditDialog.module.css";
|
|
503
|
-
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]") === null) {
|
|
504
|
-
const tag = document.createElement("style");
|
|
505
|
-
tag.dataset.plugin = "@morlay/ui-conversation-message-actions";
|
|
506
|
-
tag.dataset.pluginCss = tagId$1;
|
|
507
|
-
tag.textContent = css$1;
|
|
508
|
-
document.head.appendChild(tag);
|
|
509
|
-
}
|
|
510
|
-
var MessageEditDialog_module_css_default = {
|
|
511
|
-
"input": "_7VqDXa_input",
|
|
512
|
-
"actions": "_7VqDXa_actions"
|
|
513
|
-
};
|
|
514
|
-
//#endregion
|
|
515
|
-
//#region src/client/chat-node/MessageEditDialog.tsx
|
|
516
|
-
const BLOCK_TITLE = {
|
|
517
|
-
user: "编辑用户消息",
|
|
518
|
-
"assistant.reasoning": "编辑助手思考",
|
|
519
|
-
"assistant.response": "编辑助手回复"
|
|
520
|
-
};
|
|
521
|
-
function MessageEditDialog({ block, onSave, onClose }) {
|
|
522
|
-
const [text, setText] = (0, react.useState)(block.text);
|
|
523
|
-
const [saving, setSaving] = (0, react.useState)(false);
|
|
524
|
-
const inputRef = (0, react.useRef)(null);
|
|
525
|
-
const autosize = () => {
|
|
526
|
-
const el = inputRef.current;
|
|
527
|
-
if (el === null) return;
|
|
528
|
-
el.style.height = "auto";
|
|
529
|
-
el.style.height = `${el.scrollHeight}px`;
|
|
530
|
-
};
|
|
531
|
-
const save = () => {
|
|
532
|
-
if (saving) return;
|
|
533
|
-
setSaving(true);
|
|
534
|
-
onSave(text).then((applied) => {
|
|
535
|
-
if (applied) onClose();
|
|
536
|
-
else setSaving(false);
|
|
537
|
-
});
|
|
538
|
-
};
|
|
539
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
|
|
540
|
-
open: true,
|
|
541
|
-
onClose,
|
|
542
|
-
title: BLOCK_TITLE[block.kind],
|
|
543
|
-
closeLabel: "关闭",
|
|
544
|
-
footer: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
545
|
-
className: MessageEditDialog_module_css_default.actions,
|
|
546
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
547
|
-
variant: "outline",
|
|
548
|
-
onClick: onClose,
|
|
549
|
-
disabled: saving,
|
|
550
|
-
children: "取消"
|
|
551
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
552
|
-
variant: "primary",
|
|
553
|
-
onClick: save,
|
|
554
|
-
disabled: saving,
|
|
555
|
-
children: saving ? "保存中…" : "保存"
|
|
556
|
-
})]
|
|
557
|
-
}),
|
|
558
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
|
|
559
|
-
ref: inputRef,
|
|
560
|
-
className: MessageEditDialog_module_css_default.input,
|
|
561
|
-
value: text,
|
|
562
|
-
onChange: (event) => {
|
|
563
|
-
setText(event.target.value);
|
|
564
|
-
autosize();
|
|
565
|
-
},
|
|
566
|
-
autoFocus: true,
|
|
567
|
-
rows: 4
|
|
568
|
-
})
|
|
569
|
-
});
|
|
570
|
-
}
|
|
571
|
-
//#endregion
|
|
572
517
|
//#region \0dsh-css:/Users/morlay/src/github.com/morlay/better-session/packages/ui-conversation-message-actions/src/client/chat-node/MessageItem.module.css.mjs
|
|
573
518
|
const css = ".okW0Ua_userRow{flex-direction:column;align-items:flex-end;gap:6px;display:flex}.okW0Ua_userStack{flex-direction:column;align-items:flex-end;gap:8px;min-width:0;max-width:min(525px,82%);display:flex}.okW0Ua_bubble{background:var(--dsw-specific-bubble);max-width:100%;color:var(--dsw-alias-label-primary);border-radius:22px;padding:10px 16px;font-size:16px;line-height:24px}.okW0Ua_contextRow,.okW0Ua_compactionRow{padding:2px 0}.okW0Ua_compactionButton{width:100%;min-width:0;height:24px;color:inherit;font:inherit;text-align:left;background:0 0;border:none;border-radius:6px;align-items:center;padding:0;display:flex}.okW0Ua_compactionButton:not(:disabled){cursor:pointer}.okW0Ua_compactionButton:not(:disabled):hover{background:var(--dsw-alias-interactive-bg-hover)}.okW0Ua_compactionLeading{width:16px;height:16px;color:var(--dsw-alias-label-secondary);flex:none;place-items:center;margin-right:6px;display:inline-grid}.okW0Ua_compactionContextIcon,.okW0Ua_compactionDisclosureIcon{grid-area:1/1;justify-content:center;align-items:center;display:inline-flex}.okW0Ua_compactionDisclosureIcon,.okW0Ua_compactionButton:not(:disabled):hover .okW0Ua_compactionContextIcon,.okW0Ua_compactionButton:not(:disabled):focus-visible .okW0Ua_compactionContextIcon{opacity:0}.okW0Ua_compactionButton:not(:disabled):hover .okW0Ua_compactionDisclosureIcon,.okW0Ua_compactionButton:not(:disabled):focus-visible .okW0Ua_compactionDisclosureIcon{opacity:1}.okW0Ua_compactionTitle{color:var(--dsw-alias-label-primary-dimmed);flex:none;font-size:14px;line-height:24px}.okW0Ua_compactionSep{background:var(--dsw-alias-label-caption);border-radius:1px;flex:none;width:2px;height:2px;margin:0 8px}.okW0Ua_compactionSummary{min-width:0;color:var(--dsw-alias-label-tertiary);text-overflow:ellipsis;white-space:nowrap;flex:auto;font-size:14px;line-height:24px;overflow:hidden}.okW0Ua_compactionBody{color:var(--dsw-alias-label-tertiary);padding:4px 0 4px 22px;font-size:14px;line-height:24px}.okW0Ua_retryRow{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:20px}.okW0Ua_retrySummary{width:fit-content;color:inherit;cursor:pointer;user-select:none;border-radius:3px;align-items:center;gap:7px;padding:2px 0;list-style:none;display:inline-flex}.okW0Ua_retrySummary::-webkit-details-marker{display:none}.okW0Ua_retrySummary:after{content:\"\";opacity:.8;border-bottom:1.5px solid;border-right:1.5px solid;width:6px;height:6px;transition:transform .12s;transform:rotate(-45deg)}.okW0Ua_retrySummary:hover{color:var(--dsw-alias-label-secondary)}.okW0Ua_retrySummary:focus-visible{outline:1.5px solid var(--dsw-alias-button-info-fill);outline-offset:2px}.okW0Ua_retryText{color:inherit}.okW0Ua_retryRow[data-active] .okW0Ua_retryText{background:linear-gradient(90deg, var(--dsw-alias-label-tertiary) 0%, var(--dsw-alias-label-tertiary) 40%, var(--dsw-alias-label-secondary) 50%, var(--dsw-alias-label-tertiary) 60%, var(--dsw-alias-label-tertiary) 100%);color:#0000;background-position:100%;background-size:200% 100%;background-clip:text;animation:1.6s ease-in-out infinite okW0Ua_retry-shimmer}.okW0Ua_retryRow[open] .okW0Ua_retrySummary:after{transform:rotate(45deg)}.okW0Ua_retryDetails{overflow-wrap:anywhere;gap:2px;margin-top:3px;padding-left:14px;font-size:12px;line-height:18px;display:grid}.okW0Ua_retryDetailLabel{color:var(--dsw-alias-label-secondary)}.okW0Ua_turnErrorRow{grid-template-columns:10px minmax(0,1fr) auto;align-items:start;gap:8px;padding:2px 0;font-size:13px;line-height:20px;display:grid}.okW0Ua_turnErrorDot{margin-top:5px}.okW0Ua_turnErrorCopy{overflow-wrap:anywhere;min-width:0}.okW0Ua_turnErrorTitle{color:var(--dsw-alias-state-error-primary);margin-right:6px;font-weight:600}.okW0Ua_turnErrorMessage{color:var(--dsw-alias-label-secondary)}.okW0Ua_turnErrorCode{color:var(--dsw-alias-label-tertiary);font:var(--dsw-font-markdown-code-block-small)}.okW0Ua_maxTokensTitle{color:var(--dsw-alias-state-warn-primary);margin-right:6px;font-weight:600}@keyframes okW0Ua_retry-shimmer{0%{background-position:100%}to{background-position:0}}@media (prefers-reduced-motion:reduce){.okW0Ua_retryRow[data-active] .okW0Ua_retryText{color:inherit;background:0 0;animation:none}}.okW0Ua_refChip{color:var(--dsw-alias-label-primary);white-space:nowrap;vertical-align:baseline;background:#6187d838;border-radius:6px;margin:0 2px;padding:0 8px;font-size:.85em;line-height:1.6;display:inline-block}.okW0Ua_confirmActions{justify-content:flex-end;gap:8px;display:flex}";
|
|
574
519
|
const tagId = "@morlay/ui-conversation-message-actions/MessageItem.module.css";
|
|
@@ -580,34 +525,34 @@ window.__ModuleLoader__.load({
|
|
|
580
525
|
document.head.appendChild(tag);
|
|
581
526
|
}
|
|
582
527
|
var MessageItem_module_css_default = {
|
|
583
|
-
"
|
|
584
|
-
"turnErrorMessage": "okW0Ua_turnErrorMessage",
|
|
585
|
-
"refChip": "okW0Ua_refChip",
|
|
586
|
-
"compactionSep": "okW0Ua_compactionSep",
|
|
587
|
-
"retry-shimmer": "okW0Ua_retry-shimmer",
|
|
528
|
+
"retrySummary": "okW0Ua_retrySummary",
|
|
588
529
|
"compactionButton": "okW0Ua_compactionButton",
|
|
589
|
-
"compactionBody": "okW0Ua_compactionBody",
|
|
590
|
-
"confirmActions": "okW0Ua_confirmActions",
|
|
591
|
-
"compactionSummary": "okW0Ua_compactionSummary",
|
|
592
|
-
"bubble": "okW0Ua_bubble",
|
|
593
530
|
"contextRow": "okW0Ua_contextRow",
|
|
594
|
-
"retryDetails": "okW0Ua_retryDetails",
|
|
595
|
-
"userStack": "okW0Ua_userStack",
|
|
596
|
-
"compactionContextIcon": "okW0Ua_compactionContextIcon",
|
|
597
|
-
"compactionDisclosureIcon": "okW0Ua_compactionDisclosureIcon",
|
|
598
|
-
"retrySummary": "okW0Ua_retrySummary",
|
|
599
|
-
"turnErrorDot": "okW0Ua_turnErrorDot",
|
|
600
|
-
"retryText": "okW0Ua_retryText",
|
|
601
531
|
"retryDetailLabel": "okW0Ua_retryDetailLabel",
|
|
602
|
-
"
|
|
532
|
+
"compactionTitle": "okW0Ua_compactionTitle",
|
|
533
|
+
"compactionRow": "okW0Ua_compactionRow",
|
|
603
534
|
"turnErrorCopy": "okW0Ua_turnErrorCopy",
|
|
535
|
+
"retryDetails": "okW0Ua_retryDetails",
|
|
536
|
+
"compactionDisclosureIcon": "okW0Ua_compactionDisclosureIcon",
|
|
537
|
+
"compactionBody": "okW0Ua_compactionBody",
|
|
538
|
+
"retry-shimmer": "okW0Ua_retry-shimmer",
|
|
539
|
+
"bubble": "okW0Ua_bubble",
|
|
540
|
+
"userRow": "okW0Ua_userRow",
|
|
541
|
+
"compactionSep": "okW0Ua_compactionSep",
|
|
604
542
|
"retryRow": "okW0Ua_retryRow",
|
|
605
|
-
"
|
|
543
|
+
"compactionSummary": "okW0Ua_compactionSummary",
|
|
544
|
+
"turnErrorTitle": "okW0Ua_turnErrorTitle",
|
|
606
545
|
"turnErrorCode": "okW0Ua_turnErrorCode",
|
|
607
|
-
"
|
|
608
|
-
"compactionRow": "okW0Ua_compactionRow",
|
|
546
|
+
"compactionLeading": "okW0Ua_compactionLeading",
|
|
609
547
|
"turnErrorRow": "okW0Ua_turnErrorRow",
|
|
610
|
-
"
|
|
548
|
+
"maxTokensTitle": "okW0Ua_maxTokensTitle",
|
|
549
|
+
"turnErrorMessage": "okW0Ua_turnErrorMessage",
|
|
550
|
+
"compactionContextIcon": "okW0Ua_compactionContextIcon",
|
|
551
|
+
"confirmActions": "okW0Ua_confirmActions",
|
|
552
|
+
"turnErrorDot": "okW0Ua_turnErrorDot",
|
|
553
|
+
"refChip": "okW0Ua_refChip",
|
|
554
|
+
"retryText": "okW0Ua_retryText",
|
|
555
|
+
"userStack": "okW0Ua_userStack"
|
|
611
556
|
};
|
|
612
557
|
//#endregion
|
|
613
558
|
//#region src/client/chat-node/MessageItem.tsx
|
|
@@ -670,9 +615,9 @@ window.__ModuleLoader__.load({
|
|
|
670
615
|
}), actions?.(text)]
|
|
671
616
|
});
|
|
672
617
|
}
|
|
673
|
-
const UserMessageNodeView = (0, react.memo)(function UserMessageNodeView({ node, renderMessageImages, t,
|
|
618
|
+
const UserMessageNodeView = (0, react.memo)(function UserMessageNodeView({ node, renderMessageImages, t, recall, retry }) {
|
|
674
619
|
const data = node.data;
|
|
675
|
-
const [
|
|
620
|
+
const [confirmingRecall, setConfirmingRecall] = (0, react.useState)(null);
|
|
676
621
|
const [confirmingRetry, setConfirmingRetry] = (0, react.useState)(false);
|
|
677
622
|
const turnLocation = node.location.kind === "turn" || node.location.kind === "step" ? node.location.turn : void 0;
|
|
678
623
|
const turn = turnLocation?.turn;
|
|
@@ -680,7 +625,7 @@ window.__ModuleLoader__.load({
|
|
|
680
625
|
const textBlockIndex = data.content.findIndex((block) => block.type === "text");
|
|
681
626
|
const textBlock = textBlockIndex === -1 ? void 0 : data.content[textBlockIndex];
|
|
682
627
|
const onEdit = textBlock === void 0 || turn === void 0 ? void 0 : () => {
|
|
683
|
-
|
|
628
|
+
setConfirmingRecall({
|
|
684
629
|
key: `${node.anchorSeq}:${String(textBlockIndex)}`,
|
|
685
630
|
turn,
|
|
686
631
|
eventSeq: node.anchorSeq,
|
|
@@ -694,10 +639,28 @@ window.__ModuleLoader__.load({
|
|
|
694
639
|
setConfirmingRetry(true);
|
|
695
640
|
} : void 0;
|
|
696
641
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
642
|
+
confirmingRecall !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
|
|
643
|
+
open: true,
|
|
644
|
+
onClose: () => setConfirmingRecall(null),
|
|
645
|
+
title: "编辑消息",
|
|
646
|
+
closeLabel: "关闭",
|
|
647
|
+
description: "将撤回该消息及其之后的对话内容到输入框,请修改后重新发送。",
|
|
648
|
+
footer: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
649
|
+
className: MessageItem_module_css_default.confirmActions,
|
|
650
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
651
|
+
variant: "outline",
|
|
652
|
+
onClick: () => setConfirmingRecall(null),
|
|
653
|
+
children: "取消"
|
|
654
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
655
|
+
variant: "primary",
|
|
656
|
+
onClick: () => {
|
|
657
|
+
const target = confirmingRecall;
|
|
658
|
+
setConfirmingRecall(null);
|
|
659
|
+
recall(target);
|
|
660
|
+
},
|
|
661
|
+
children: "撤回并编辑"
|
|
662
|
+
})]
|
|
663
|
+
})
|
|
701
664
|
}),
|
|
702
665
|
confirmingRetry && turn !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
|
|
703
666
|
open: true,
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { _ as
|
|
1
|
+
import { _ as RewindOperation, d as EditOperation, f as EditableMessageBlock, g as RetryableTurn, h as RetryOperation, m as RerollOperation, o as editableMessages, p as RecallOperation, r as closedTurns, u as retryableTurns, v as SessionEditorOperation, y as SessionEditorResult } from "./plan-Cn3TPTa0.mjs";
|
|
2
2
|
import { Context, Service } from "@deepseek-ai/cordis";
|
|
3
3
|
import { Session, SessionEvent, SessionId } from "@deepseek-ai/dsh-session";
|
|
4
4
|
import { BranchBoundary, BranchTimeline, BranchTimeline as BranchTimeline$1, CascadePolicy, EditableBlockKind, SESSION_BRANCH_VERSION_SCHEMA, SessionBranchVersionEvent } from "@morlay/session-branch";
|
|
@@ -58,9 +58,21 @@ declare class SessionEditor extends Service {
|
|
|
58
58
|
edit(operation: EditOperation, signal?: AbortSignal): Promise<SessionEditorResult>;
|
|
59
59
|
reroll(operation: RerollOperation, signal?: AbortSignal): Promise<SessionEditorResult>;
|
|
60
60
|
retry(operation: RetryOperation, signal?: AbortSignal): Promise<SessionEditorResult>;
|
|
61
|
+
recall(operation: RecallOperation, signal?: AbortSignal): Promise<SessionEditorResult>;
|
|
61
62
|
editableMessages(sessionId: SessionId, signal?: AbortSignal): Promise<EditableMessageBlock[]>;
|
|
62
63
|
retryableTurns(sessionId: SessionId, signal?: AbortSignal): Promise<RetryableTurn[]>;
|
|
63
64
|
private branchOperation;
|
|
65
|
+
/**
|
|
66
|
+
* 撤回(recall):只 rewind 截断,不重写、不重放——被撤回的 user 消息文本
|
|
67
|
+
* 由客户端回填到输入框,交给用户修改后重新发送。
|
|
68
|
+
*
|
|
69
|
+
* 边界与 edit 的轮首 user 一致:轮首消息整轮截断到前一轮 `turn/end`
|
|
70
|
+
* (无前轮则 -1),轮内 followup 只截断到该消息本身(exclusive drop),
|
|
71
|
+
* 避免留下悬空 `turn/start` 让下一次发送开到错误轮号。
|
|
72
|
+
*/
|
|
73
|
+
private recallOperation;
|
|
74
|
+
/** rewind 前等待驻留 agent 到达 quiescence(无 agent / 无 live owner 时空操作)。 */
|
|
75
|
+
private prepareIdle;
|
|
64
76
|
private readEvents;
|
|
65
77
|
/**
|
|
66
78
|
* rewind 前确保 agent 可驱动重放:live agent 先等待其停下(rewind 会截断其
|
|
@@ -94,4 +106,4 @@ declare module "@deepseek-ai/cordis" {
|
|
|
94
106
|
}
|
|
95
107
|
}
|
|
96
108
|
//#endregion
|
|
97
|
-
export { type BranchTimeline, type CascadePolicy, type EditOperation, type EditableBlockKind, type EditableMessageBlock, EditorAgent, EditorAgentHandle, EditorAgentRegistry, type RerollOperation, type RetryOperation, type RetryableTurn, type RewindOperation, SESSION_BRANCH_VERSION_SCHEMA, type SessionBranchVersionEvent, SessionEditor, SessionEditor as default, type SessionEditorOperation, type SessionEditorResult, closedTurns, editableMessages, retryableTurns };
|
|
109
|
+
export { type BranchTimeline, type CascadePolicy, type EditOperation, type EditableBlockKind, type EditableMessageBlock, EditorAgent, EditorAgentHandle, EditorAgentRegistry, type RecallOperation, type RerollOperation, type RetryOperation, type RetryableTurn, type RewindOperation, SESSION_BRANCH_VERSION_SCHEMA, type SessionBranchVersionEvent, SessionEditor, SessionEditor as default, type SessionEditorOperation, type SessionEditorResult, closedTurns, editableMessages, retryableTurns };
|
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { n as SessionEditor, t as SESSION_BRANCH_VERSION_SCHEMA } from "./src-
|
|
1
|
+
import { n as SessionEditor, t as SESSION_BRANCH_VERSION_SCHEMA } from "./src-Dq1X3rOO.mjs";
|
|
2
2
|
import { closedTurns, editableMessages, retryableTurns } from "./plan.mjs";
|
|
3
3
|
export { SESSION_BRANCH_VERSION_SCHEMA, SessionEditor, SessionEditor as default, closedTurns, editableMessages, retryableTurns };
|
|
@@ -25,13 +25,18 @@ interface RewindOperation {
|
|
|
25
25
|
sessionId: SessionId;
|
|
26
26
|
toBoundary: number;
|
|
27
27
|
}
|
|
28
|
+
interface RecallOperation {
|
|
29
|
+
action: "recall";
|
|
30
|
+
sessionId: SessionId;
|
|
31
|
+
eventSeq: number;
|
|
32
|
+
}
|
|
28
33
|
interface ForkOperation {
|
|
29
34
|
action: "fork";
|
|
30
35
|
sessionId: SessionId;
|
|
31
36
|
atSeq?: number;
|
|
32
37
|
childSessionId?: SessionId;
|
|
33
38
|
}
|
|
34
|
-
type SessionEditorOperation = EditOperation | RerollOperation | RetryOperation | RewindOperation | ForkOperation;
|
|
39
|
+
type SessionEditorOperation = EditOperation | RerollOperation | RetryOperation | RewindOperation | RecallOperation | ForkOperation;
|
|
35
40
|
interface SessionEditorResult {
|
|
36
41
|
sessionId: SessionId;
|
|
37
42
|
queuedTurns: number;
|
|
@@ -90,4 +95,4 @@ declare function editPlan(operation: EditOperation, turns: readonly ClosedTurn[]
|
|
|
90
95
|
declare function retryPlan(operation: RetryOperation, turns: readonly ClosedTurn[]): OperationPlan;
|
|
91
96
|
declare function rerollPlan(operation: RerollOperation, turns: readonly ClosedTurn[]): OperationPlan;
|
|
92
97
|
//#endregion
|
|
93
|
-
export {
|
|
98
|
+
export { RewindOperation as _, editPlan as a, rerollPlan as c, EditOperation as d, EditableMessageBlock as f, RetryableTurn as g, RetryOperation as h, downstreamUsers as i, retryPlan as l, RerollOperation as m, OperationPlan as n, editableMessages as o, RecallOperation as p, closedTurns as r, precedingContentIndex as s, ClosedTurn as t, retryableTurns as u, SessionEditorOperation as v, SessionEditorResult as y };
|
package/dist/plan.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as editPlan, c as rerollPlan, i as downstreamUsers, l as retryPlan, n as OperationPlan, o as editableMessages, r as closedTurns, s as precedingContentIndex, t as ClosedTurn, u as retryableTurns } from "./plan-
|
|
1
|
+
import { a as editPlan, c as rerollPlan, i as downstreamUsers, l as retryPlan, n as OperationPlan, o as editableMessages, r as closedTurns, s as precedingContentIndex, t as ClosedTurn, u as retryableTurns } from "./plan-Cn3TPTa0.mjs";
|
|
2
2
|
export { ClosedTurn, OperationPlan, closedTurns, downstreamUsers, editPlan, editableMessages, precedingContentIndex, rerollPlan, retryPlan, retryableTurns };
|
|
@@ -159,6 +159,9 @@ var SessionEditor = class extends Service {
|
|
|
159
159
|
retry(operation, signal) {
|
|
160
160
|
return this.branchOperation(operation, signal);
|
|
161
161
|
}
|
|
162
|
+
recall(operation, signal) {
|
|
163
|
+
return this.recallOperation(operation, signal);
|
|
164
|
+
}
|
|
162
165
|
async editableMessages(sessionId, signal) {
|
|
163
166
|
const events = await this.readEvents(sessionId, signal);
|
|
164
167
|
return editableMessages(closedTurns(events));
|
|
@@ -225,6 +228,41 @@ var SessionEditor = class extends Service {
|
|
|
225
228
|
live: this.ctx.sessions.get(operation.sessionId) !== void 0
|
|
226
229
|
};
|
|
227
230
|
}
|
|
231
|
+
/**
|
|
232
|
+
* 撤回(recall):只 rewind 截断,不重写、不重放——被撤回的 user 消息文本
|
|
233
|
+
* 由客户端回填到输入框,交给用户修改后重新发送。
|
|
234
|
+
*
|
|
235
|
+
* 边界与 edit 的轮首 user 一致:轮首消息整轮截断到前一轮 `turn/end`
|
|
236
|
+
* (无前轮则 -1),轮内 followup 只截断到该消息本身(exclusive drop),
|
|
237
|
+
* 避免留下悬空 `turn/start` 让下一次发送开到错误轮号。
|
|
238
|
+
*/
|
|
239
|
+
async recallOperation(operation, signal) {
|
|
240
|
+
signal?.throwIfAborted();
|
|
241
|
+
const events = await this.readEvents(operation.sessionId, signal);
|
|
242
|
+
const turns = closedTurns(events);
|
|
243
|
+
const turnIndex = turns.findIndex((turn) => operation.eventSeq > turn.startSeq && (turn.endSeq === void 0 || operation.eventSeq < turn.endSeq));
|
|
244
|
+
const turn = turns[turnIndex];
|
|
245
|
+
if (turn === void 0) throw new SessionBranchError("所选消息不属于已落定回合。", "INVALID_BOUNDARY");
|
|
246
|
+
const userIndex = turn.users.findIndex((user) => user.seq === operation.eventSeq);
|
|
247
|
+
if (userIndex === -1) throw new SessionBranchError("所选消息不存在或不可撤回。", "INVALID_BOUNDARY");
|
|
248
|
+
const preceding = precedingContentIndex(turns, turnIndex);
|
|
249
|
+
const boundary = userIndex === 0 ? preceding < 0 ? -1 : turns[preceding].endSeq : operation.eventSeq;
|
|
250
|
+
await this.prepareIdle(operation.sessionId, signal);
|
|
251
|
+
await this.ctx.sessionBranch.rewind(operation.sessionId, boundary, signal);
|
|
252
|
+
return {
|
|
253
|
+
sessionId: operation.sessionId,
|
|
254
|
+
queuedTurns: 0,
|
|
255
|
+
live: this.ctx.sessions.get(operation.sessionId) !== void 0
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
/** rewind 前等待驻留 agent 到达 quiescence(无 agent / 无 live owner 时空操作)。 */
|
|
259
|
+
async prepareIdle(sessionId, signal) {
|
|
260
|
+
signal?.throwIfAborted();
|
|
261
|
+
const existing = this.ctx.get("agents")?.get(sessionId);
|
|
262
|
+
if (existing === void 0) return;
|
|
263
|
+
await existing.whenIdle();
|
|
264
|
+
signal?.throwIfAborted();
|
|
265
|
+
}
|
|
228
266
|
async readEvents(sessionId, signal) {
|
|
229
267
|
const live = this.ctx.sessions.get(sessionId);
|
|
230
268
|
if (live !== void 0) return live.snapshotEvents();
|
|
@@ -320,13 +358,18 @@ function decodeOperation(value) {
|
|
|
320
358
|
sessionId,
|
|
321
359
|
toBoundary: integerOf(record["toBoundary"], "toBoundary")
|
|
322
360
|
};
|
|
361
|
+
case "recall": return {
|
|
362
|
+
action: "recall",
|
|
363
|
+
sessionId,
|
|
364
|
+
eventSeq: integerOf(record["eventSeq"], "eventSeq")
|
|
365
|
+
};
|
|
323
366
|
case "fork": return {
|
|
324
367
|
action: "fork",
|
|
325
368
|
sessionId,
|
|
326
369
|
...record["atSeq"] === void 0 ? {} : { atSeq: integerOf(record["atSeq"], "atSeq") },
|
|
327
370
|
...record["childSessionId"] === void 0 ? {} : { childSessionId: sessionIdOf(record["childSessionId"]) }
|
|
328
371
|
};
|
|
329
|
-
default: throw new TypeError("action 必须是 edit、reroll、retry、rewind 或 fork。");
|
|
372
|
+
default: throw new TypeError("action 必须是 edit、reroll、retry、rewind、recall 或 fork。");
|
|
330
373
|
}
|
|
331
374
|
}
|
|
332
375
|
function requestJson(request) {
|
|
@@ -389,6 +432,14 @@ async function runOperation(editor, operation) {
|
|
|
389
432
|
sessionId: operation.sessionId,
|
|
390
433
|
queuedTurns: 0
|
|
391
434
|
};
|
|
435
|
+
case "recall": {
|
|
436
|
+
const result = await editor.recall(operation);
|
|
437
|
+
return {
|
|
438
|
+
sessionId: result.sessionId,
|
|
439
|
+
queuedTurns: result.queuedTurns,
|
|
440
|
+
...result.live === void 0 ? {} : { live: result.live }
|
|
441
|
+
};
|
|
442
|
+
}
|
|
392
443
|
case "fork": return {
|
|
393
444
|
sessionId: await editor.fork(operation.sessionId, operation.atSeq, operation.childSessionId),
|
|
394
445
|
queuedTurns: 0
|
package/dist/testing.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as SessionEditor } from "./src-
|
|
1
|
+
import { n as SessionEditor } from "./src-Dq1X3rOO.mjs";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { Context, Service } from "@deepseek-ai/cordis";
|
|
4
4
|
import { KNOWN_SESSION_EVENT_TYPES, SESSION_FORMAT_VERSION, Session, Session as Session$1, SessionId, SessionId as SessionIdBrand, SessionLogOffset, SessionSeq, SessionSeq as SessionSeq$1, SessionStore } from "@deepseek-ai/dsh-session";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@morlay/ui-conversation-message-actions",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.16",
|
|
4
4
|
"description": "Rewind / retry / fork orchestration (host) plus conversation.chat.node renderer replacement (browser) — message edit / retry actions on user bubbles.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"conversation-chat-node",
|
|
@@ -35,10 +35,10 @@
|
|
|
35
35
|
"./client": "./dist/client.js"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@morlay/session-branch": "^0.0.
|
|
38
|
+
"@morlay/session-branch": "^0.0.6"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
|
-
"@morlay/session-rdb": "^0.0.
|
|
41
|
+
"@morlay/session-rdb": "^0.0.18",
|
|
42
42
|
"@types/react": "^19.2.18",
|
|
43
43
|
"@types/react-dom": "^19.2.5",
|
|
44
44
|
"lightningcss": "^1.32.0",
|
|
@@ -47,22 +47,22 @@
|
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
49
|
"@deepseek-ai/cordis": "^4.0.2",
|
|
50
|
-
"@deepseek-ai/dsh-api-session-controller": "^0.1.5-rc.
|
|
51
|
-
"@deepseek-ai/dsh-attachment": "^0.1.5-rc.
|
|
52
|
-
"@deepseek-ai/dsh-client-connection": "^0.1.5-rc.
|
|
53
|
-
"@deepseek-ai/dsh-client-store": "^0.1.5-rc.
|
|
54
|
-
"@deepseek-ai/dsh-client-ui-attachment": "^0.1.5-rc.
|
|
55
|
-
"@deepseek-ai/dsh-client-ui-chat": "^0.1.5-rc.
|
|
56
|
-
"@deepseek-ai/dsh-client-ui-conversation": "^0.1.5-rc.
|
|
57
|
-
"@deepseek-ai/dsh-client-ui-primitives": "^0.1.5-rc.
|
|
58
|
-
"@deepseek-ai/dsh-client-ui-renderer": "^0.1.5-rc.
|
|
59
|
-
"@deepseek-ai/dsh-client-ui-slots": "^0.1.5-rc.
|
|
60
|
-
"@deepseek-ai/dsh-invariants": "^0.1.5-rc.
|
|
61
|
-
"@deepseek-ai/dsh-llm": "^0.1.5-rc.
|
|
62
|
-
"@deepseek-ai/dsh-session": "^0.1.5-rc.
|
|
63
|
-
"@deepseek-ai/dsh-session-persistence": "^0.1.5-rc.
|
|
64
|
-
"@deepseek-ai/dsh-session-projection": "^0.1.5-rc.
|
|
65
|
-
"@deepseek-ai/dsh-token-meter": "^0.1.5-rc.
|
|
50
|
+
"@deepseek-ai/dsh-api-session-controller": "^0.1.5-rc.2",
|
|
51
|
+
"@deepseek-ai/dsh-attachment": "^0.1.5-rc.2",
|
|
52
|
+
"@deepseek-ai/dsh-client-connection": "^0.1.5-rc.2",
|
|
53
|
+
"@deepseek-ai/dsh-client-store": "^0.1.5-rc.2",
|
|
54
|
+
"@deepseek-ai/dsh-client-ui-attachment": "^0.1.5-rc.2",
|
|
55
|
+
"@deepseek-ai/dsh-client-ui-chat": "^0.1.5-rc.2",
|
|
56
|
+
"@deepseek-ai/dsh-client-ui-conversation": "^0.1.5-rc.2",
|
|
57
|
+
"@deepseek-ai/dsh-client-ui-primitives": "^0.1.5-rc.2",
|
|
58
|
+
"@deepseek-ai/dsh-client-ui-renderer": "^0.1.5-rc.2",
|
|
59
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.1.5-rc.2",
|
|
60
|
+
"@deepseek-ai/dsh-invariants": "^0.1.5-rc.2",
|
|
61
|
+
"@deepseek-ai/dsh-llm": "^0.1.5-rc.2",
|
|
62
|
+
"@deepseek-ai/dsh-session": "^0.1.5-rc.2",
|
|
63
|
+
"@deepseek-ai/dsh-session-persistence": "^0.1.5-rc.2",
|
|
64
|
+
"@deepseek-ai/dsh-session-projection": "^0.1.5-rc.2",
|
|
65
|
+
"@deepseek-ai/dsh-token-meter": "^0.1.5-rc.2"
|
|
66
66
|
},
|
|
67
67
|
"dsh": {
|
|
68
68
|
"bundle": {
|
|
@@ -12,7 +12,6 @@ import { JsonBlock } from "@deepseek-ai/dsh-client-ui-primitives";
|
|
|
12
12
|
import type { ChatNodeViewProps, ChatViewSlotProps } from "@deepseek-ai/dsh-client-ui-chat/client";
|
|
13
13
|
import type { RenderMessageImages } from "@deepseek-ai/dsh-client-ui-conversation/client";
|
|
14
14
|
import { MessageIconActions } from "./MessageIconActions.tsx";
|
|
15
|
-
import { MessageEditDialog } from "./MessageEditDialog.tsx";
|
|
16
15
|
import css from "./MessageItem.module.css";
|
|
17
16
|
import type { EditableMessageBlock } from "../../shared.ts";
|
|
18
17
|
import type { SessionEditorFace } from "../controller.ts";
|
|
@@ -104,11 +103,11 @@ export const UserMessageNodeView = memo(function UserMessageNodeView({
|
|
|
104
103
|
node,
|
|
105
104
|
renderMessageImages,
|
|
106
105
|
t,
|
|
107
|
-
|
|
106
|
+
recall,
|
|
108
107
|
retry,
|
|
109
108
|
}: ChatNodeViewProps<"user" | "steering"> & InjectFace<SessionEditorFace>) {
|
|
110
109
|
const data = node.data;
|
|
111
|
-
const [
|
|
110
|
+
const [confirmingRecall, setConfirmingRecall] = useState<EditableMessageBlock | null>(null);
|
|
112
111
|
const [confirmingRetry, setConfirmingRetry] = useState(false);
|
|
113
112
|
const turnLocation =
|
|
114
113
|
node.location.kind === "turn" || node.location.kind === "step" ? node.location.turn : undefined;
|
|
@@ -125,7 +124,7 @@ export const UserMessageNodeView = memo(function UserMessageNodeView({
|
|
|
125
124
|
textBlock === undefined || turn === undefined
|
|
126
125
|
? undefined
|
|
127
126
|
: () => {
|
|
128
|
-
|
|
127
|
+
setConfirmingRecall({
|
|
129
128
|
key: `${node.anchorSeq}:${String(textBlockIndex)}`,
|
|
130
129
|
turn,
|
|
131
130
|
eventSeq: node.anchorSeq,
|
|
@@ -144,11 +143,30 @@ export const UserMessageNodeView = memo(function UserMessageNodeView({
|
|
|
144
143
|
: undefined;
|
|
145
144
|
return (
|
|
146
145
|
<>
|
|
147
|
-
{
|
|
148
|
-
<
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
146
|
+
{confirmingRecall !== null && (
|
|
147
|
+
<Modal
|
|
148
|
+
open
|
|
149
|
+
onClose={() => setConfirmingRecall(null)}
|
|
150
|
+
title="编辑消息"
|
|
151
|
+
closeLabel="关闭"
|
|
152
|
+
description="将撤回该消息及其之后的对话内容到输入框,请修改后重新发送。"
|
|
153
|
+
footer={
|
|
154
|
+
<div className={css.confirmActions}>
|
|
155
|
+
<Button variant="outline" onClick={() => setConfirmingRecall(null)}>
|
|
156
|
+
取消
|
|
157
|
+
</Button>
|
|
158
|
+
<Button
|
|
159
|
+
variant="primary"
|
|
160
|
+
onClick={() => {
|
|
161
|
+
const target = confirmingRecall;
|
|
162
|
+
setConfirmingRecall(null);
|
|
163
|
+
void recall(target);
|
|
164
|
+
}}
|
|
165
|
+
>
|
|
166
|
+
撤回并编辑
|
|
167
|
+
</Button>
|
|
168
|
+
</div>
|
|
169
|
+
}
|
|
152
170
|
/>
|
|
153
171
|
)}
|
|
154
172
|
{confirmingRetry && turn !== undefined && (
|
package/src/client/controller.ts
CHANGED
|
@@ -26,7 +26,7 @@ function editorApiPath(): string {
|
|
|
26
26
|
export interface SessionEditorState {
|
|
27
27
|
status: "idle" | "loading" | "ready" | "error";
|
|
28
28
|
error: string | null;
|
|
29
|
-
pending: VersionOperation | "import" | null;
|
|
29
|
+
pending: VersionOperation | "recall" | "import" | null;
|
|
30
30
|
timeline: SessionEditorTimeline | null;
|
|
31
31
|
}
|
|
32
32
|
|
|
@@ -42,6 +42,7 @@ export interface SessionEditorFace {
|
|
|
42
42
|
retry(turn: number, cascade: "truncate" | "preserve"): Promise<boolean>;
|
|
43
43
|
reroll(): Promise<boolean>;
|
|
44
44
|
rewind(toBoundary: number): Promise<boolean>;
|
|
45
|
+
recall(message: EditableMessageBlock): Promise<boolean>;
|
|
45
46
|
|
|
46
47
|
importSession(file: File): Promise<boolean>;
|
|
47
48
|
openVersion(sessionId: string): Promise<void>;
|
|
@@ -105,6 +106,11 @@ export class SessionEditorController {
|
|
|
105
106
|
reroll: () => this.mutate({ action: "reroll", sessionId: this.sessionId }),
|
|
106
107
|
rewind: (toBoundary) =>
|
|
107
108
|
this.mutate({ action: "rewind", sessionId: this.sessionId, toBoundary }),
|
|
109
|
+
recall: (message) =>
|
|
110
|
+
this.mutate(
|
|
111
|
+
{ action: "recall", sessionId: this.sessionId, eventSeq: message.eventSeq },
|
|
112
|
+
() => this.setComposerDraft(message.text),
|
|
113
|
+
),
|
|
108
114
|
importSession: (file) => this.importSession(file),
|
|
109
115
|
openVersion: (sessionId) => this.openWhenListed(sessionId as SessionId),
|
|
110
116
|
};
|
|
@@ -196,7 +202,10 @@ export class SessionEditorController {
|
|
|
196
202
|
}
|
|
197
203
|
}
|
|
198
204
|
|
|
199
|
-
private async mutate(
|
|
205
|
+
private async mutate(
|
|
206
|
+
operation: SessionEditorOperation,
|
|
207
|
+
onApplied?: () => void,
|
|
208
|
+
): Promise<boolean> {
|
|
200
209
|
// 只拦截并发操作;不要求 status === "ready"——编辑/重试的数据来自客户端
|
|
201
210
|
// conversation 节点,与 timeline 加载无关,status 为 idle 时也可发起请求。
|
|
202
211
|
const current = this.store.getSnapshot();
|
|
@@ -222,6 +231,9 @@ export class SessionEditorController {
|
|
|
222
231
|
this.store.update((state) => {
|
|
223
232
|
state.pending = null;
|
|
224
233
|
});
|
|
234
|
+
// recall 的草稿回填必须在刷新(resync / 整页重载)之前:草稿经会话级
|
|
235
|
+
// 持久化 mirror 落 store,整页重载后仍能恢复。
|
|
236
|
+
onApplied?.();
|
|
225
237
|
const result = value as SessionEditorOperationResult;
|
|
226
238
|
// 仅 fork 产生新 id 时需要等列表发布并打开新版本;就地编辑不改 id。
|
|
227
239
|
if (String(result.sessionId) !== String(this.sessionId)) {
|
|
@@ -266,6 +278,22 @@ export class SessionEditorController {
|
|
|
266
278
|
}
|
|
267
279
|
}
|
|
268
280
|
|
|
281
|
+
/**
|
|
282
|
+
* 把撤回的消息文本回填到该会话的 composer 草稿(`conversation.input` 的
|
|
283
|
+
* `setDraft`,即主输入框的整段替换语义)。会话 scope / binding / conversation
|
|
284
|
+
* 服务缺失时静默跳过——撤回本身已成功,草稿回填失败不应回滚会话状态。
|
|
285
|
+
*/
|
|
286
|
+
private setComposerDraft(text: string): void {
|
|
287
|
+
// binding 缺失时 input.for 会抛(会话 scope 已拆除),先短路。
|
|
288
|
+
if (this.sessions.binding(this.sessionId) === undefined) return;
|
|
289
|
+
const scoped = this.sessions.scope(this.sessionId);
|
|
290
|
+
if (scoped === undefined) return;
|
|
291
|
+
const conversation = scoped.get("conversation") as
|
|
292
|
+
| { input?: { for(ctx: ClientContext): { setDraft(text: string): void } } }
|
|
293
|
+
| undefined;
|
|
294
|
+
conversation?.input?.for(scoped).setDraft(text);
|
|
295
|
+
}
|
|
296
|
+
|
|
269
297
|
private async importSession(file: File): Promise<boolean> {
|
|
270
298
|
const current = this.store.getSnapshot();
|
|
271
299
|
if (current.pending !== null) return false;
|
package/src/index.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
import type {
|
|
18
18
|
EditOperation,
|
|
19
19
|
EditableMessageBlock,
|
|
20
|
+
RecallOperation,
|
|
20
21
|
RerollOperation,
|
|
21
22
|
RetryOperation,
|
|
22
23
|
RetryableTurn,
|
|
@@ -43,6 +44,7 @@ export type { CascadePolicy, EditableBlockKind } from "@morlay/session-branch";
|
|
|
43
44
|
export type {
|
|
44
45
|
EditableMessageBlock,
|
|
45
46
|
EditOperation,
|
|
47
|
+
RecallOperation,
|
|
46
48
|
RerollOperation,
|
|
47
49
|
RetryOperation,
|
|
48
50
|
RetryableTurn,
|
|
@@ -250,6 +252,10 @@ export class SessionEditor extends Service {
|
|
|
250
252
|
return this.branchOperation(operation, signal);
|
|
251
253
|
}
|
|
252
254
|
|
|
255
|
+
recall(operation: RecallOperation, signal?: AbortSignal): Promise<SessionEditorResult> {
|
|
256
|
+
return this.recallOperation(operation, signal);
|
|
257
|
+
}
|
|
258
|
+
|
|
253
259
|
async editableMessages(
|
|
254
260
|
sessionId: SessionId,
|
|
255
261
|
signal?: AbortSignal,
|
|
@@ -386,6 +392,57 @@ export class SessionEditor extends Service {
|
|
|
386
392
|
};
|
|
387
393
|
}
|
|
388
394
|
|
|
395
|
+
/**
|
|
396
|
+
* 撤回(recall):只 rewind 截断,不重写、不重放——被撤回的 user 消息文本
|
|
397
|
+
* 由客户端回填到输入框,交给用户修改后重新发送。
|
|
398
|
+
*
|
|
399
|
+
* 边界与 edit 的轮首 user 一致:轮首消息整轮截断到前一轮 `turn/end`
|
|
400
|
+
* (无前轮则 -1),轮内 followup 只截断到该消息本身(exclusive drop),
|
|
401
|
+
* 避免留下悬空 `turn/start` 让下一次发送开到错误轮号。
|
|
402
|
+
*/
|
|
403
|
+
private async recallOperation(
|
|
404
|
+
operation: RecallOperation,
|
|
405
|
+
signal?: AbortSignal,
|
|
406
|
+
): Promise<SessionEditorResult> {
|
|
407
|
+
signal?.throwIfAborted();
|
|
408
|
+
const events = await this.readEvents(operation.sessionId, signal);
|
|
409
|
+
const turns = closedTurns(events);
|
|
410
|
+
const turnIndex = turns.findIndex(
|
|
411
|
+
(turn) =>
|
|
412
|
+
operation.eventSeq > turn.startSeq &&
|
|
413
|
+
(turn.endSeq === undefined || operation.eventSeq < turn.endSeq),
|
|
414
|
+
);
|
|
415
|
+
const turn = turns[turnIndex];
|
|
416
|
+
if (turn === undefined) {
|
|
417
|
+
throw new SessionBranchError("所选消息不属于已落定回合。", "INVALID_BOUNDARY");
|
|
418
|
+
}
|
|
419
|
+
const userIndex = turn.users.findIndex((user) => user.seq === operation.eventSeq);
|
|
420
|
+
if (userIndex === -1) {
|
|
421
|
+
throw new SessionBranchError("所选消息不存在或不可撤回。", "INVALID_BOUNDARY");
|
|
422
|
+
}
|
|
423
|
+
const preceding = precedingContentIndex(turns, turnIndex);
|
|
424
|
+
const boundary =
|
|
425
|
+
userIndex === 0 ? (preceding < 0 ? -1 : turns[preceding]!.endSeq!) : operation.eventSeq;
|
|
426
|
+
// live agent 运行中时先等其停下:rewind 会截断其 session 内存 log。
|
|
427
|
+
await this.prepareIdle(operation.sessionId, signal);
|
|
428
|
+
await this.ctx.sessionBranch.rewind(operation.sessionId, boundary, signal);
|
|
429
|
+
return {
|
|
430
|
+
sessionId: operation.sessionId,
|
|
431
|
+
queuedTurns: 0,
|
|
432
|
+
live: this.ctx.sessions.get(operation.sessionId) !== undefined,
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/** rewind 前等待驻留 agent 到达 quiescence(无 agent / 无 live owner 时空操作)。 */
|
|
437
|
+
private async prepareIdle(sessionId: SessionId, signal?: AbortSignal): Promise<void> {
|
|
438
|
+
signal?.throwIfAborted();
|
|
439
|
+
const agents = this.ctx.get("agents") as EditorAgentRegistry | undefined;
|
|
440
|
+
const existing = agents?.get(sessionId);
|
|
441
|
+
if (existing === undefined) return;
|
|
442
|
+
await existing.whenIdle();
|
|
443
|
+
signal?.throwIfAborted();
|
|
444
|
+
}
|
|
445
|
+
|
|
389
446
|
private async readEvents(
|
|
390
447
|
sessionId: SessionId,
|
|
391
448
|
signal?: AbortSignal,
|
|
@@ -546,6 +603,12 @@ function decodeOperation(value: unknown): SessionEditorOperation {
|
|
|
546
603
|
sessionId,
|
|
547
604
|
toBoundary: integerOf(record["toBoundary"], "toBoundary"),
|
|
548
605
|
};
|
|
606
|
+
case "recall":
|
|
607
|
+
return {
|
|
608
|
+
action: "recall",
|
|
609
|
+
sessionId,
|
|
610
|
+
eventSeq: integerOf(record["eventSeq"], "eventSeq"),
|
|
611
|
+
};
|
|
549
612
|
case "fork":
|
|
550
613
|
return {
|
|
551
614
|
action: "fork",
|
|
@@ -556,7 +619,7 @@ function decodeOperation(value: unknown): SessionEditorOperation {
|
|
|
556
619
|
: { childSessionId: sessionIdOf(record["childSessionId"]) }),
|
|
557
620
|
};
|
|
558
621
|
default:
|
|
559
|
-
throw new TypeError("action 必须是 edit、reroll、retry、rewind 或 fork。");
|
|
622
|
+
throw new TypeError("action 必须是 edit、reroll、retry、rewind、recall 或 fork。");
|
|
560
623
|
}
|
|
561
624
|
}
|
|
562
625
|
|
|
@@ -629,6 +692,14 @@ async function runOperation(
|
|
|
629
692
|
case "rewind":
|
|
630
693
|
await editor.rewind(operation.sessionId, operation.toBoundary);
|
|
631
694
|
return { sessionId: operation.sessionId, queuedTurns: 0 };
|
|
695
|
+
case "recall": {
|
|
696
|
+
const result = await editor.recall(operation);
|
|
697
|
+
return {
|
|
698
|
+
sessionId: result.sessionId,
|
|
699
|
+
queuedTurns: result.queuedTurns,
|
|
700
|
+
...(result.live === undefined ? {} : { live: result.live }),
|
|
701
|
+
};
|
|
702
|
+
}
|
|
632
703
|
case "fork":
|
|
633
704
|
return {
|
|
634
705
|
sessionId: await editor.fork(
|
package/src/shared.ts
CHANGED
|
@@ -33,6 +33,12 @@ export interface RewindOperation {
|
|
|
33
33
|
toBoundary: number;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
export interface RecallOperation {
|
|
37
|
+
action: "recall";
|
|
38
|
+
sessionId: SessionId;
|
|
39
|
+
eventSeq: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
36
42
|
export interface ForkOperation {
|
|
37
43
|
action: "fork";
|
|
38
44
|
sessionId: SessionId;
|
|
@@ -45,6 +51,7 @@ export type SessionEditorOperation =
|
|
|
45
51
|
| RerollOperation
|
|
46
52
|
| RetryOperation
|
|
47
53
|
| RewindOperation
|
|
54
|
+
| RecallOperation
|
|
48
55
|
| ForkOperation;
|
|
49
56
|
|
|
50
57
|
export interface SessionEditorOperationResult {
|
package/src/types.ts
CHANGED
|
@@ -31,6 +31,13 @@ export interface RewindOperation {
|
|
|
31
31
|
toBoundary: number;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
export interface RecallOperation {
|
|
35
|
+
action: "recall";
|
|
36
|
+
sessionId: SessionId;
|
|
37
|
+
|
|
38
|
+
eventSeq: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
34
41
|
export interface ForkOperation {
|
|
35
42
|
action: "fork";
|
|
36
43
|
sessionId: SessionId;
|
|
@@ -43,6 +50,7 @@ export type SessionEditorOperation =
|
|
|
43
50
|
| RerollOperation
|
|
44
51
|
| RetryOperation
|
|
45
52
|
| RewindOperation
|
|
53
|
+
| RecallOperation
|
|
46
54
|
| ForkOperation;
|
|
47
55
|
|
|
48
56
|
export interface SessionEditorResult {
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
.input {
|
|
2
|
-
/* composer-card 同款视觉(ui-conversation InputBar.module.css 的 .card):
|
|
3
|
-
圆角胶囊 + 输入 surface token + 阴影,编辑弹窗与主输入框观感一致。 */
|
|
4
|
-
width: 100%;
|
|
5
|
-
min-height: 96px;
|
|
6
|
-
box-sizing: border-box;
|
|
7
|
-
font: inherit;
|
|
8
|
-
font-size: 16px;
|
|
9
|
-
line-height: 24px;
|
|
10
|
-
padding: 10px 16px;
|
|
11
|
-
border: 1px solid var(--dsw-alias-border-l2-darkmode-thin);
|
|
12
|
-
border-radius: 22px;
|
|
13
|
-
background: var(--dsw-specific-input-major);
|
|
14
|
-
box-shadow: var(--dsw-shadow-lv2);
|
|
15
|
-
color: var(--dsw-text, inherit);
|
|
16
|
-
resize: none;
|
|
17
|
-
overflow-y: auto;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
.actions {
|
|
21
|
-
display: flex;
|
|
22
|
-
justify-content: flex-end;
|
|
23
|
-
gap: 8px;
|
|
24
|
-
}
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import { useRef, useState } from "react";
|
|
2
|
-
import { Button, Modal } from "@deepseek-ai/dsh-client-ui-primitives";
|
|
3
|
-
import type { EditableMessageBlock } from "../../shared.ts";
|
|
4
|
-
import css from "./MessageEditDialog.module.css";
|
|
5
|
-
|
|
6
|
-
const BLOCK_TITLE: Record<EditableMessageBlock["kind"], string> = {
|
|
7
|
-
user: "编辑用户消息",
|
|
8
|
-
"assistant.reasoning": "编辑助手思考",
|
|
9
|
-
"assistant.response": "编辑助手回复",
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
export function MessageEditDialog({
|
|
13
|
-
block,
|
|
14
|
-
onSave,
|
|
15
|
-
onClose,
|
|
16
|
-
}: {
|
|
17
|
-
block: EditableMessageBlock;
|
|
18
|
-
onSave: (text: string) => Promise<boolean>;
|
|
19
|
-
onClose: () => void;
|
|
20
|
-
}) {
|
|
21
|
-
const [text, setText] = useState(block.text);
|
|
22
|
-
const [saving, setSaving] = useState(false);
|
|
23
|
-
const inputRef = useRef<HTMLTextAreaElement | null>(null);
|
|
24
|
-
|
|
25
|
-
const autosize = (): void => {
|
|
26
|
-
const el = inputRef.current;
|
|
27
|
-
if (el === null) return;
|
|
28
|
-
el.style.height = "auto";
|
|
29
|
-
el.style.height = `${el.scrollHeight}px`;
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
const save = (): void => {
|
|
33
|
-
if (saving) return;
|
|
34
|
-
setSaving(true);
|
|
35
|
-
void onSave(text).then((applied) => {
|
|
36
|
-
if (applied) onClose();
|
|
37
|
-
else setSaving(false);
|
|
38
|
-
});
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
return (
|
|
42
|
-
<Modal
|
|
43
|
-
open
|
|
44
|
-
onClose={onClose}
|
|
45
|
-
title={BLOCK_TITLE[block.kind]}
|
|
46
|
-
closeLabel="关闭"
|
|
47
|
-
footer={
|
|
48
|
-
<div className={css.actions}>
|
|
49
|
-
<Button variant="outline" onClick={onClose} disabled={saving}>
|
|
50
|
-
取消
|
|
51
|
-
</Button>
|
|
52
|
-
<Button variant="primary" onClick={save} disabled={saving}>
|
|
53
|
-
{saving ? "保存中…" : "保存"}
|
|
54
|
-
</Button>
|
|
55
|
-
</div>
|
|
56
|
-
}
|
|
57
|
-
>
|
|
58
|
-
<textarea
|
|
59
|
-
ref={inputRef}
|
|
60
|
-
className={css.input}
|
|
61
|
-
value={text}
|
|
62
|
-
onChange={(event) => {
|
|
63
|
-
setText(event.target.value);
|
|
64
|
-
autosize();
|
|
65
|
-
}}
|
|
66
|
-
autoFocus
|
|
67
|
-
rows={4}
|
|
68
|
-
/>
|
|
69
|
-
</Modal>
|
|
70
|
-
);
|
|
71
|
-
}
|