@morlay/ui-conversation-message-actions 0.0.15 → 0.0.17

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 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,11 @@
28
29
  点不开的空项与轮号空洞;manualTurn 的轮号同样按保留前缀收敛(正常会话
29
30
  等于目标轮号)。版本效果仍记录**原**目标轮号(操作历史);
30
31
  - **只有 `fork` 创建新 id**(`ForkOperation` / `forkFrom`,纯 append 派生);
32
+ - **`recall` 只截断不重写**:轮内消息沿用 `edit` 的轮首规则(轮首整轮截断到前一轮
33
+ `turn/end`,无前轮则清空全部;轮内 followup exclusive drop),轮外 user 消息
34
+ (`location` 无轮次归属的排队落定消息)按消息自身位置 exclusive drop(截断该消息
35
+ 及其后全部),但只 `rewind` 即返回——不写版本效果、不重放;被撤回的 user 文本由
36
+ 浏览器半回填到 composer,用户修改后自行发送;
31
37
  - 版本效果事件(`session-branch/version`)携带 `ignorable: true`:**原样落库**
32
38
  (rdb 不按信封过滤,与 JSONL 一致),非 branch 读者凭信封跳过;
33
39
  - 重放输入经 agent 驱动(见下)排队到原会话,agent 基于截断后历史回复。
@@ -38,8 +44,13 @@
38
44
  缺失时退化为「已 durable 的就地版本」(可随时 resume 续跑):
39
45
 
40
46
  - **live agent**(会话驻留 / 已恢复):直接 `followup` 排队,不重建、不换 id;
41
- 编辑前只等 agent 停下(`whenIdle`),残留排队输入由 rewind 在截断后强制
42
- durable 取消(session-rdb live 会话钩子 `inbox.clear`);
47
+ - **rewind 前必须停止运行中的 loop**:每次 rewind(edit / retry / reroll /
48
+ recall / HTTP `rewind`)都先对驻留 agent `cancel({ kind: "user" }, { keepInbox: true })`
49
+ 再 `whenIdle()`,然后才截断——截断会重写 agent 的 session 内存 log,不能与
50
+ 运行中的 loop 并发;`keepInbox` 表示停止本身不丢弃待处理输入,残留排队输入由
51
+ rewind 在截断后强制 durable 取消(session-rdb 的 live 会话钩子 `inbox.clear`);
52
+ 实现无 `cancel` 能力时退化为等待其自然停下
53
+ ([ADR 0005](./docs/adr/0005-rewind前主动停止运行中的loop.md));
43
54
  - **cold 会话**:`resume` 已持久化会话(`create` 对已持久化日志必失败),
44
55
  resume 后 agent 驻留(不 dispose,避免 session 被移出 store 破坏客户端窗口);
45
56
  - 模型 provider/model 在 **rewind 之前**从 `request/header` 解析(就地编辑
@@ -52,9 +63,12 @@
52
63
 
53
64
  - **shadow 注册**:`user` / `steering` 两个 key 以 `priority: -1` 重新注册
54
65
  (最低优先级渲染,shadow 上游默认注册);其余 key 沿用上游渲染器;
55
- - **编辑 / 重试按钮只挂在 user 消息**(`UserMessageNodeView`):编辑弹窗复用
56
- dsh settings 同款 `Modal` + `Button`,输入框复用 composer-card 视觉与自动
57
- 增长;重试先弹确认;未闭合轮次不显示重试;
66
+ - **编辑 / 重试按钮只挂在 user 消息**(`UserMessageNodeView`):编辑只要消息存在
67
+ 可编辑文本块就显示(不要求轮次归属——`location` turn/step 的已落定消息同样
68
+ 可撤回),点击后不再打开编辑弹窗,而是确认(`Modal` + `Button`)后调用
69
+ `recall`——服务端只 `rewind` 截断,客户端把消息文本 `setDraft` 回填到主输入框
70
+ (`conversation.input`),交给用户修改后自行发送;重试仍先弹确认,且只对已闭合
71
+ 轮次开放;
58
72
  - **操作后刷新**:就地编辑后优先调用客户端会话级 `resync()`(重置窗口并重新
59
73
  拉取历史,不整页重载——rewind 的删除无法经 append-only 事件流表达,seq
60
74
  回退只做增量会残留旧节点),随后丢弃该会话的全部投影行
@@ -73,7 +87,7 @@
73
87
  `ctx.sessionBranch`(provider 实现,如 `@morlay/session-rdb`);
74
88
  - 本服务只做**编排**:闭合轮次扫描、版本效果事件构造、派生 seed 组装、
75
89
  rewind 命令透传、agent 驱动、HTTP 面(`POST /session-editor` 执行
76
- edit / reroll / retry / rewind / fork)。
90
+ edit / reroll / retry / rewind / recall / fork)。
77
91
 
78
92
  ## 装配
79
93
 
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) => {
@@ -350,23 +367,23 @@ window.__ModuleLoader__.load({
350
367
  return day;
351
368
  }
352
369
  //#endregion
353
- //#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$2 = ".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}";
355
- const tagId$2 = "@morlay/ui-conversation-message-actions/MessageIconActions.module.css";
356
- if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$2) + "]") === null) {
370
+ //#region \0dsh-css:/Users/morlay/src/github.com/morlay/better-session/packages/session/ui-conversation-message-actions/src/client/chat-node/MessageIconActions.module.css.mjs
371
+ const css$1 = ".zUzlda_actions{align-items:center;gap:10px;height:28px;display:flex}.zUzlda_timeStart{color:var(--dsw-alias-label-tertiary);white-space:nowrap;padding-right:12px;font-size:14px;line-height:24px}.zUzlda_timeEnd{color:var(--dsw-alias-label-tertiary);white-space:nowrap;padding-left:12px;font-size:14px;line-height:24px}.zUzlda_runTimeDot{margin:0 10px}@media (hover:hover){[data-time-hover-root] :is(.zUzlda_timeStart,.zUzlda_timeEnd){opacity:0;transition:opacity 80ms}[data-time-hover-root]:hover :is(.zUzlda_timeStart,.zUzlda_timeEnd),[data-time-hover-root]:focus-within :is(.zUzlda_timeStart,.zUzlda_timeEnd){opacity:1}}.zUzlda_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}.zUzlda_action:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary)}.zUzlda_action[data-unavailable]{cursor:default;opacity:.4}.zUzlda_action[data-unavailable]:hover{color:var(--dsw-alias-label-tertiary);background:0 0}.zUzlda_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$2;
360
- tag.textContent = css$2;
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
- "runTimeDot": "lKOSUG_runTimeDot",
365
- "actions": "lKOSUG_actions",
366
- "timeStart": "lKOSUG_timeStart",
367
- "timeEnd": "lKOSUG_timeEnd",
368
- "action": "lKOSUG_action",
369
- "visuallyHidden": "lKOSUG_visuallyHidden"
381
+ "runTimeDot": "zUzlda_runTimeDot",
382
+ "timeStart": "zUzlda_timeStart",
383
+ "action": "zUzlda_action",
384
+ "visuallyHidden": "zUzlda_visuallyHidden",
385
+ "timeEnd": "zUzlda_timeEnd",
386
+ "actions": "zUzlda_actions"
370
387
  };
371
388
  //#endregion
372
389
  //#region src/client/chat-node/MessageIconActions.tsx
@@ -497,80 +514,8 @@ 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
- //#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
- 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}";
517
+ //#region \0dsh-css:/Users/morlay/src/github.com/morlay/better-session/packages/session/ui-conversation-message-actions/src/client/chat-node/MessageItem.module.css.mjs
518
+ const css = ".rSGQoW_userRow{flex-direction:column;align-items:flex-end;gap:6px;display:flex}.rSGQoW_userStack{flex-direction:column;align-items:flex-end;gap:8px;min-width:0;max-width:min(525px,82%);display:flex}.rSGQoW_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}.rSGQoW_contextRow,.rSGQoW_compactionRow{padding:2px 0}.rSGQoW_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}.rSGQoW_compactionButton:not(:disabled){cursor:pointer}.rSGQoW_compactionButton:not(:disabled):hover{background:var(--dsw-alias-interactive-bg-hover)}.rSGQoW_compactionLeading{width:16px;height:16px;color:var(--dsw-alias-label-secondary);flex:none;place-items:center;margin-right:6px;display:inline-grid}.rSGQoW_compactionContextIcon,.rSGQoW_compactionDisclosureIcon{grid-area:1/1;justify-content:center;align-items:center;display:inline-flex}.rSGQoW_compactionDisclosureIcon,.rSGQoW_compactionButton:not(:disabled):hover .rSGQoW_compactionContextIcon,.rSGQoW_compactionButton:not(:disabled):focus-visible .rSGQoW_compactionContextIcon{opacity:0}.rSGQoW_compactionButton:not(:disabled):hover .rSGQoW_compactionDisclosureIcon,.rSGQoW_compactionButton:not(:disabled):focus-visible .rSGQoW_compactionDisclosureIcon{opacity:1}.rSGQoW_compactionTitle{color:var(--dsw-alias-label-primary-dimmed);flex:none;font-size:14px;line-height:24px}.rSGQoW_compactionSep{background:var(--dsw-alias-label-caption);border-radius:1px;flex:none;width:2px;height:2px;margin:0 8px}.rSGQoW_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}.rSGQoW_compactionBody{color:var(--dsw-alias-label-tertiary);padding:4px 0 4px 22px;font-size:14px;line-height:24px}.rSGQoW_retryRow{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:20px}.rSGQoW_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}.rSGQoW_retrySummary::-webkit-details-marker{display:none}.rSGQoW_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)}.rSGQoW_retrySummary:hover{color:var(--dsw-alias-label-secondary)}.rSGQoW_retrySummary:focus-visible{outline:1.5px solid var(--dsw-alias-button-info-fill);outline-offset:2px}.rSGQoW_retryText{color:inherit}.rSGQoW_retryRow[data-active] .rSGQoW_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 rSGQoW_retry-shimmer}.rSGQoW_retryRow[open] .rSGQoW_retrySummary:after{transform:rotate(45deg)}.rSGQoW_retryDetails{overflow-wrap:anywhere;gap:2px;margin-top:3px;padding-left:14px;font-size:12px;line-height:18px;display:grid}.rSGQoW_retryDetailLabel{color:var(--dsw-alias-label-secondary)}.rSGQoW_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}.rSGQoW_turnErrorDot{margin-top:5px}.rSGQoW_turnErrorCopy{overflow-wrap:anywhere;min-width:0}.rSGQoW_turnErrorTitle{color:var(--dsw-alias-state-error-primary);margin-right:6px;font-weight:600}.rSGQoW_turnErrorMessage{color:var(--dsw-alias-label-secondary)}.rSGQoW_turnErrorCode{color:var(--dsw-alias-label-tertiary);font:var(--dsw-font-markdown-code-block-small)}.rSGQoW_maxTokensTitle{color:var(--dsw-alias-state-warn-primary);margin-right:6px;font-weight:600}@keyframes rSGQoW_retry-shimmer{0%{background-position:100%}to{background-position:0}}@media (prefers-reduced-motion:reduce){.rSGQoW_retryRow[data-active] .rSGQoW_retryText{color:inherit;background:0 0;animation:none}}.rSGQoW_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}.rSGQoW_confirmActions{justify-content:flex-end;gap:8px;display:flex}";
574
519
  const tagId = "@morlay/ui-conversation-message-actions/MessageItem.module.css";
575
520
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
576
521
  const tag = document.createElement("style");
@@ -580,34 +525,34 @@ window.__ModuleLoader__.load({
580
525
  document.head.appendChild(tag);
581
526
  }
582
527
  var MessageItem_module_css_default = {
583
- "userRow": "okW0Ua_userRow",
584
- "turnErrorMessage": "okW0Ua_turnErrorMessage",
585
- "refChip": "okW0Ua_refChip",
586
- "compactionSep": "okW0Ua_compactionSep",
587
- "retry-shimmer": "okW0Ua_retry-shimmer",
588
- "compactionButton": "okW0Ua_compactionButton",
589
- "compactionBody": "okW0Ua_compactionBody",
590
- "confirmActions": "okW0Ua_confirmActions",
591
- "compactionSummary": "okW0Ua_compactionSummary",
592
- "bubble": "okW0Ua_bubble",
593
- "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
- "retryDetailLabel": "okW0Ua_retryDetailLabel",
602
- "turnErrorTitle": "okW0Ua_turnErrorTitle",
603
- "turnErrorCopy": "okW0Ua_turnErrorCopy",
604
- "retryRow": "okW0Ua_retryRow",
605
- "maxTokensTitle": "okW0Ua_maxTokensTitle",
606
- "turnErrorCode": "okW0Ua_turnErrorCode",
607
- "compactionTitle": "okW0Ua_compactionTitle",
608
- "compactionRow": "okW0Ua_compactionRow",
609
- "turnErrorRow": "okW0Ua_turnErrorRow",
610
- "compactionLeading": "okW0Ua_compactionLeading"
528
+ "turnErrorCode": "rSGQoW_turnErrorCode",
529
+ "maxTokensTitle": "rSGQoW_maxTokensTitle",
530
+ "retryDetailLabel": "rSGQoW_retryDetailLabel",
531
+ "refChip": "rSGQoW_refChip",
532
+ "compactionContextIcon": "rSGQoW_compactionContextIcon",
533
+ "turnErrorDot": "rSGQoW_turnErrorDot",
534
+ "retryRow": "rSGQoW_retryRow",
535
+ "bubble": "rSGQoW_bubble",
536
+ "compactionSummary": "rSGQoW_compactionSummary",
537
+ "compactionBody": "rSGQoW_compactionBody",
538
+ "userStack": "rSGQoW_userStack",
539
+ "compactionButton": "rSGQoW_compactionButton",
540
+ "compactionDisclosureIcon": "rSGQoW_compactionDisclosureIcon",
541
+ "retryText": "rSGQoW_retryText",
542
+ "compactionTitle": "rSGQoW_compactionTitle",
543
+ "compactionLeading": "rSGQoW_compactionLeading",
544
+ "contextRow": "rSGQoW_contextRow",
545
+ "compactionSep": "rSGQoW_compactionSep",
546
+ "turnErrorCopy": "rSGQoW_turnErrorCopy",
547
+ "retry-shimmer": "rSGQoW_retry-shimmer",
548
+ "turnErrorRow": "rSGQoW_turnErrorRow",
549
+ "turnErrorTitle": "rSGQoW_turnErrorTitle",
550
+ "retryDetails": "rSGQoW_retryDetails",
551
+ "compactionRow": "rSGQoW_compactionRow",
552
+ "turnErrorMessage": "rSGQoW_turnErrorMessage",
553
+ "confirmActions": "rSGQoW_confirmActions",
554
+ "retrySummary": "rSGQoW_retrySummary",
555
+ "userRow": "rSGQoW_userRow"
611
556
  };
612
557
  //#endregion
613
558
  //#region src/client/chat-node/MessageItem.tsx
@@ -670,19 +615,19 @@ window.__ModuleLoader__.load({
670
615
  }), actions?.(text)]
671
616
  });
672
617
  }
673
- const UserMessageNodeView = (0, react.memo)(function UserMessageNodeView({ node, renderMessageImages, t, edit, retry }) {
618
+ const UserMessageNodeView = (0, react.memo)(function UserMessageNodeView({ node, renderMessageImages, t, recall, retry }) {
674
619
  const data = node.data;
675
- const [editing, setEditing] = (0, react.useState)(null);
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;
679
624
  const retryable = turnLocation?.status === "closed";
680
625
  const textBlockIndex = data.content.findIndex((block) => block.type === "text");
681
626
  const textBlock = textBlockIndex === -1 ? void 0 : data.content[textBlockIndex];
682
- const onEdit = textBlock === void 0 || turn === void 0 ? void 0 : () => {
683
- setEditing({
627
+ const onEdit = textBlock === void 0 ? void 0 : () => {
628
+ setConfirmingRecall({
684
629
  key: `${node.anchorSeq}:${String(textBlockIndex)}`,
685
- turn,
630
+ ...turn === void 0 ? {} : { turn },
686
631
  eventSeq: node.anchorSeq,
687
632
  blockIndex: textBlockIndex,
688
633
  kind: "user",
@@ -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
- editing !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MessageEditDialog, {
698
- block: editing,
699
- onSave: (text) => edit(editing, text, "truncate"),
700
- onClose: () => setEditing(null)
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,6 +1,6 @@
1
- import { _ as SessionEditorOperation, d as EditOperation, f as EditableMessageBlock, g as RewindOperation, h as RetryableTurn, m as RetryOperation, o as editableMessages, p as RerollOperation, r as closedTurns, u as retryableTurns, v as SessionEditorResult } from "./plan-BdU_mnTv.mjs";
1
+ import { _ as RetryableTurn, b as SessionEditorResult, d as retryableTurns, f as EditOperation, g as RetryOperation, h as RerollOperation, m as RecallOperation, o as editableMessages, p as EditableMessageBlock, r as closedTurns, v as RewindOperation, y as SessionEditorOperation } from "./plan-BSa7k9HO.mjs";
2
2
  import { Context, Service } from "@deepseek-ai/cordis";
3
- import { Session, SessionEvent, SessionId } from "@deepseek-ai/dsh-session";
3
+ import { AgentCancelCause, 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";
5
5
  import { UserMessage } from "@deepseek-ai/dsh-llm";
6
6
  //#region src/index.d.ts
@@ -14,6 +14,9 @@ interface EditorAgent {
14
14
  followup(message: UserMessage): void;
15
15
  /** 等待 agent 到达 quiescence(当前 turn/任务结束后 resolve)。 */
16
16
  whenIdle(): Promise<void>;
17
+ cancel?(cause: AgentCancelCause, options?: {
18
+ keepInbox?: boolean;
19
+ }): void;
17
20
  }
18
21
  interface EditorAgentHandle {
19
22
  readonly agent: EditorAgent;
@@ -58,16 +61,21 @@ declare class SessionEditor extends Service {
58
61
  edit(operation: EditOperation, signal?: AbortSignal): Promise<SessionEditorResult>;
59
62
  reroll(operation: RerollOperation, signal?: AbortSignal): Promise<SessionEditorResult>;
60
63
  retry(operation: RetryOperation, signal?: AbortSignal): Promise<SessionEditorResult>;
64
+ recall(operation: RecallOperation, signal?: AbortSignal): Promise<SessionEditorResult>;
61
65
  editableMessages(sessionId: SessionId, signal?: AbortSignal): Promise<EditableMessageBlock[]>;
62
66
  retryableTurns(sessionId: SessionId, signal?: AbortSignal): Promise<RetryableTurn[]>;
63
67
  private branchOperation;
64
- private readEvents;
65
68
  /**
66
- * rewind 前确保 agent 可驱动重放:live agent 先等待其停下(rewind 会截断其
67
- * session 内存 log,须在 quiescence 后执行);cold 会话先 resume 出驻留
68
- * agent(此时会话完整,resume 的 prepare 不与截断冲突)。agents 服务缺失或
69
- * 无需重放时返回空——调用方退化为就地截断版本。
69
+ * 撤回(recall):只 rewind 截断,不重写、不重放——被撤回的 user 消息文本
70
+ * 由客户端回填到输入框,交给用户修改后重新发送。
71
+ *
72
+ * 边界见 {@link recallBoundary}:轮首消息整轮截断到前一轮 `turn/end`
73
+ * (无前轮则 -1),轮内 followup 与轮外 user 消息只截断到该消息本身
74
+ * (exclusive drop),避免留下悬空 `turn/start` 让下一次发送开到错误轮号。
70
75
  */
76
+ private recallOperation;
77
+ private stopLoop;
78
+ private readEvents;
71
79
  private prepareReplay;
72
80
  }
73
81
  interface HttpRequestLike {
@@ -94,4 +102,4 @@ declare module "@deepseek-ai/cordis" {
94
102
  }
95
103
  }
96
104
  //#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 };
105
+ 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-ks167MhO.mjs";
1
+ import { n as SessionEditor, t as SESSION_BRANCH_VERSION_SCHEMA } from "./src-B4HwXsob.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;
@@ -39,7 +44,7 @@ interface SessionEditorResult {
39
44
  }
40
45
  interface EditableMessageBlock {
41
46
  key: string;
42
- turn: number;
47
+ turn?: number;
43
48
  eventSeq: number;
44
49
  blockIndex: number;
45
50
  kind: "user" | "assistant.reasoning" | "assistant.response";
@@ -86,8 +91,9 @@ declare function retryableTurns(turns: readonly ClosedTurn[]): RetryableTurn[];
86
91
  */
87
92
  declare function precedingContentIndex(turns: readonly ClosedTurn[], turnIndex: number): number;
88
93
  declare function downstreamUsers(turns: readonly ClosedTurn[], start: number): UserMessage[];
94
+ declare function recallBoundary(events: readonly SessionEvent[], turns: readonly ClosedTurn[], eventSeq: number): number;
89
95
  declare function editPlan(operation: EditOperation, turns: readonly ClosedTurn[]): OperationPlan;
90
96
  declare function retryPlan(operation: RetryOperation, turns: readonly ClosedTurn[]): OperationPlan;
91
97
  declare function rerollPlan(operation: RerollOperation, turns: readonly ClosedTurn[]): OperationPlan;
92
98
  //#endregion
93
- export { SessionEditorOperation as _, editPlan as a, rerollPlan as c, EditOperation as d, EditableMessageBlock as f, RewindOperation as g, RetryableTurn as h, downstreamUsers as i, retryPlan as l, RetryOperation as m, OperationPlan as n, editableMessages as o, RerollOperation as p, closedTurns as r, precedingContentIndex as s, ClosedTurn as t, retryableTurns as u, SessionEditorResult as v };
99
+ export { RetryableTurn as _, editPlan as a, SessionEditorResult as b, recallBoundary as c, retryableTurns as d, EditOperation as f, RetryOperation as g, RerollOperation as h, downstreamUsers as i, rerollPlan as l, RecallOperation as m, OperationPlan as n, editableMessages as o, EditableMessageBlock as p, closedTurns as r, precedingContentIndex as s, ClosedTurn as t, retryPlan as u, RewindOperation as v, SessionEditorOperation 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-BdU_mnTv.mjs";
2
- export { ClosedTurn, OperationPlan, closedTurns, downstreamUsers, editPlan, editableMessages, precedingContentIndex, rerollPlan, retryPlan, retryableTurns };
1
+ import { a as editPlan, c as recallBoundary, d as retryableTurns, i as downstreamUsers, l as rerollPlan, n as OperationPlan, o as editableMessages, r as closedTurns, s as precedingContentIndex, t as ClosedTurn, u as retryPlan } from "./plan-BSa7k9HO.mjs";
2
+ export { ClosedTurn, OperationPlan, closedTurns, downstreamUsers, editPlan, editableMessages, precedingContentIndex, recallBoundary, rerollPlan, retryPlan, retryableTurns };
package/dist/plan.mjs CHANGED
@@ -118,6 +118,19 @@ function precedingContentIndex(turns, turnIndex) {
118
118
  function downstreamUsers(turns, start) {
119
119
  return turns.slice(start).flatMap((turn) => turn.users.map((user) => cloneUser(user.data)));
120
120
  }
121
+ function recallBoundary(events, turns, eventSeq) {
122
+ const turnIndex = turns.findIndex((turn) => eventSeq > turn.startSeq && (turn.endSeq === void 0 || eventSeq < turn.endSeq));
123
+ const turn = turns[turnIndex];
124
+ if (turn === void 0) {
125
+ const target = events.find((event) => event.seq === eventSeq);
126
+ if (target?.type !== "user/message" || target.data.source.kind !== "user") throw new SessionBranchError("所选消息不属于已落定回合。", "INVALID_BOUNDARY");
127
+ return eventSeq;
128
+ }
129
+ const userIndex = turn.users.findIndex((user) => user.seq === eventSeq);
130
+ if (userIndex === -1) throw new SessionBranchError("所选消息不存在或不可撤回。", "INVALID_BOUNDARY");
131
+ const preceding = precedingContentIndex(turns, turnIndex);
132
+ return userIndex === 0 ? preceding < 0 ? -1 : turns[preceding].endSeq : eventSeq;
133
+ }
121
134
  function assistantReplacement(event, blockIndex, text) {
122
135
  const replaced = replaceTextBlock(event.data.message.content, blockIndex, text).filter((block) => block.type === "text" || block.type === "reasoning");
123
136
  return Object.freeze({
@@ -237,4 +250,4 @@ function rerollPlan(operation, turns) {
237
250
  throw new SessionBranchError("当前会话没有可重生成的已落定助手回复。", "INVALID_BOUNDARY");
238
251
  }
239
252
  //#endregion
240
- export { closedTurns, downstreamUsers, editPlan, editableMessages, precedingContentIndex, rerollPlan, retryPlan, retryableTurns };
253
+ export { closedTurns, downstreamUsers, editPlan, editableMessages, precedingContentIndex, recallBoundary, rerollPlan, retryPlan, retryableTurns };
@@ -1,4 +1,4 @@
1
- import { closedTurns, editPlan, editableMessages, precedingContentIndex, rerollPlan, retryPlan, retryableTurns } from "./plan.mjs";
1
+ import { closedTurns, editPlan, editableMessages, precedingContentIndex, recallBoundary, rerollPlan, retryPlan, retryableTurns } from "./plan.mjs";
2
2
  import { Service } from "@deepseek-ai/cordis";
3
3
  import { SessionSeq } from "@deepseek-ai/dsh-session";
4
4
  import { SESSION_BRANCH_VERSION_SCHEMA as SESSION_BRANCH_VERSION_SCHEMA$1, SessionBranchError, balanceRewindPrefix } from "@morlay/session-branch";
@@ -144,7 +144,8 @@ var SessionEditor = class extends Service {
144
144
  ...meta === void 0 ? {} : { meta }
145
145
  }, signal);
146
146
  }
147
- rewind(id, toBoundary, signal) {
147
+ async rewind(id, toBoundary, signal) {
148
+ await this.stopLoop(id, signal);
148
149
  return this.ctx.sessionBranch.rewind(id, toBoundary, signal);
149
150
  }
150
151
  timeline(sessionId, signal) {
@@ -159,6 +160,9 @@ var SessionEditor = class extends Service {
159
160
  retry(operation, signal) {
160
161
  return this.branchOperation(operation, signal);
161
162
  }
163
+ recall(operation, signal) {
164
+ return this.recallOperation(operation, signal);
165
+ }
162
166
  async editableMessages(sessionId, signal) {
163
167
  const events = await this.readEvents(sessionId, signal);
164
168
  return editableMessages(closedTurns(events));
@@ -184,6 +188,7 @@ var SessionEditor = class extends Service {
184
188
  turn: replayTurn
185
189
  });
186
190
  const replay = await this.prepareReplay(operation.sessionId, plan.queuedUsers, signal, headerConfig);
191
+ await this.stopLoop(operation.sessionId, signal);
187
192
  const live = this.ctx.sessions.get(operation.sessionId);
188
193
  await this.ctx.sessionBranch.rewind(operation.sessionId, boundary, signal);
189
194
  if (seedSuffix.length > 0) {
@@ -225,28 +230,46 @@ var SessionEditor = class extends Service {
225
230
  live: this.ctx.sessions.get(operation.sessionId) !== void 0
226
231
  };
227
232
  }
233
+ /**
234
+ * 撤回(recall):只 rewind 截断,不重写、不重放——被撤回的 user 消息文本
235
+ * 由客户端回填到输入框,交给用户修改后重新发送。
236
+ *
237
+ * 边界见 {@link recallBoundary}:轮首消息整轮截断到前一轮 `turn/end`
238
+ * (无前轮则 -1),轮内 followup 与轮外 user 消息只截断到该消息本身
239
+ * (exclusive drop),避免留下悬空 `turn/start` 让下一次发送开到错误轮号。
240
+ */
241
+ async recallOperation(operation, signal) {
242
+ signal?.throwIfAborted();
243
+ const events = await this.readEvents(operation.sessionId, signal);
244
+ const boundary = recallBoundary(events, closedTurns(events), operation.eventSeq);
245
+ await this.stopLoop(operation.sessionId, signal);
246
+ await this.ctx.sessionBranch.rewind(operation.sessionId, boundary, signal);
247
+ return {
248
+ sessionId: operation.sessionId,
249
+ queuedTurns: 0,
250
+ live: this.ctx.sessions.get(operation.sessionId) !== void 0
251
+ };
252
+ }
253
+ async stopLoop(sessionId, signal) {
254
+ signal?.throwIfAborted();
255
+ const existing = this.ctx.get("agents")?.get(sessionId);
256
+ if (existing === void 0) return;
257
+ existing.cancel?.({ kind: "user" }, { keepInbox: true });
258
+ await existing.whenIdle();
259
+ signal?.throwIfAborted();
260
+ }
228
261
  async readEvents(sessionId, signal) {
229
262
  const live = this.ctx.sessions.get(sessionId);
230
263
  if (live !== void 0) return live.snapshotEvents();
231
264
  return (await this.ctx.sessionBranch.readRawEvents(sessionId, signal)).events;
232
265
  }
233
- /**
234
- * rewind 前确保 agent 可驱动重放:live agent 先等待其停下(rewind 会截断其
235
- * session 内存 log,须在 quiescence 后执行);cold 会话先 resume 出驻留
236
- * agent(此时会话完整,resume 的 prepare 不与截断冲突)。agents 服务缺失或
237
- * 无需重放时返回空——调用方退化为就地截断版本。
238
- */
239
266
  async prepareReplay(sessionId, queuedUsers, signal, headerConfig) {
240
267
  if (queuedUsers.length === 0) return { agent: void 0 };
241
268
  signal?.throwIfAborted();
242
269
  const agents = this.ctx.get("agents");
243
270
  if (agents === void 0) return { agent: void 0 };
244
271
  const existing = agents.get(sessionId);
245
- if (existing !== void 0) {
246
- await existing.whenIdle();
247
- signal?.throwIfAborted();
248
- return { agent: existing };
249
- }
272
+ if (existing !== void 0) return { agent: existing };
250
273
  const provider = headerConfig?.provider ?? "";
251
274
  const model = headerConfig?.model ?? "";
252
275
  if (provider.length === 0 || model.length === 0) {
@@ -320,13 +343,18 @@ function decodeOperation(value) {
320
343
  sessionId,
321
344
  toBoundary: integerOf(record["toBoundary"], "toBoundary")
322
345
  };
346
+ case "recall": return {
347
+ action: "recall",
348
+ sessionId,
349
+ eventSeq: integerOf(record["eventSeq"], "eventSeq")
350
+ };
323
351
  case "fork": return {
324
352
  action: "fork",
325
353
  sessionId,
326
354
  ...record["atSeq"] === void 0 ? {} : { atSeq: integerOf(record["atSeq"], "atSeq") },
327
355
  ...record["childSessionId"] === void 0 ? {} : { childSessionId: sessionIdOf(record["childSessionId"]) }
328
356
  };
329
- default: throw new TypeError("action 必须是 edit、reroll、retry、rewind 或 fork。");
357
+ default: throw new TypeError("action 必须是 edit、reroll、retry、rewind、recall 或 fork。");
330
358
  }
331
359
  }
332
360
  function requestJson(request) {
@@ -389,6 +417,14 @@ async function runOperation(editor, operation) {
389
417
  sessionId: operation.sessionId,
390
418
  queuedTurns: 0
391
419
  };
420
+ case "recall": {
421
+ const result = await editor.recall(operation);
422
+ return {
423
+ sessionId: result.sessionId,
424
+ queuedTurns: result.queuedTurns,
425
+ ...result.live === void 0 ? {} : { live: result.live }
426
+ };
427
+ }
392
428
  case "fork": return {
393
429
  sessionId: await editor.fork(operation.sessionId, operation.atSeq, operation.childSessionId),
394
430
  queuedTurns: 0
@@ -460,4 +496,4 @@ function jsonResponse(status, value) {
460
496
  });
461
497
  }
462
498
  //#endregion
463
- export { SessionEditor as n, SESSION_BRANCH_VERSION_SCHEMA$1 as t };
499
+ export { SessionEditor as n, SESSION_EDITOR_PATH as r, SESSION_BRANCH_VERSION_SCHEMA$1 as t };