@morlay/ui-conversation-message-actions 0.0.14-alpha.2 → 0.0.14-alpha.4

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
@@ -21,6 +21,12 @@
21
21
  - **edit / retry / reroll 是就地操作**:`rewind` 截断到目标轮之前的闭合
22
22
  `turn/end` 边界,再 `append` 版本效果 + 手工回合 / 重放输入回**同一会话**
23
23
  ——session id 不变,版本树保持单根;
24
+ - **重放复用目标轮号**:轮首 user 编辑(含未闭合轮)整轮截断,重放接回原
25
+ 轮号;编辑 assistant 写入 manualTurn 后同步 agent 的轮次游标;
26
+ - **空轮吸收**:目标轮之前的空轮(`turn/start` 后直接 `turn/end`,早期重放
27
+ 缺陷的遗留形状)随截断一并删除,重放按保留前缀续号——轮次导航不再出现
28
+ 点不开的空项与轮号空洞;manualTurn 的轮号同样按保留前缀收敛(正常会话
29
+ 等于目标轮号)。版本效果仍记录**原**目标轮号(操作历史);
24
30
  - **只有 `fork` 创建新 id**(`ForkOperation` / `forkFrom`,纯 append 派生);
25
31
  - 版本效果事件(`session-branch/version`)携带 `ignorable: true`:live log 可见、
26
32
  **不落 canonical log**(rdb 持久化时过滤并稠密化剩余事件);
@@ -32,6 +38,8 @@
32
38
  缺失时退化为「已 durable 的就地版本」(可随时 resume 续跑):
33
39
 
34
40
  - **live agent**(会话驻留 / 已恢复):直接 `followup` 排队,不重建、不换 id;
41
+ 编辑前只等 agent 停下(`whenIdle`),残留排队输入由 rewind 在截断后强制
42
+ durable 取消(session-rdb 的 `LiveSessionHooks.inbox.clear`);
35
43
  - **cold 会话**:`resume` 已持久化会话(`create` 对已持久化日志必失败),
36
44
  resume 后 agent 驻留(不 dispose,避免 session 被移出 store 破坏客户端窗口);
37
45
  - 模型 provider/model 在 **rewind 之前**从 `request/header` 解析(就地编辑
@@ -50,7 +58,10 @@
50
58
  增长;重试先弹确认;未闭合轮次不显示重试;
51
59
  - **操作后刷新**:就地编辑后优先调用客户端会话级 `resync()`(重置窗口并重新
52
60
  拉取历史,不整页重载——rewind 的删除无法经 append-only 事件流表达,seq
53
- 回退只做增量会残留旧节点),不可用时回退 `location.reload()`;
61
+ 回退只做增量会残留旧节点),随后丢弃该会话的全部投影行
62
+ (`projections.truncate(-1)`)——投影 store 按 higher-seq-wins 保留 rewind
63
+ 前的高 seq 旧值,截断后的正确值 seq 更小,永远覆盖不上(轮次导航残留已
64
+ 删除的轮次);不可用时回退 `location.reload()`;
54
65
  - **构建约束**:client bundle 必须是**单文件**(client-modules 只服务/加载
55
66
  `client.js`)——`noExternal` 全内联第三方 + `inlineDynamicImports` 合并
56
67
  动态 import,`@deepseek-ai/*` 一律 external(平台 seed 词或独立插件,
package/dist/client.js CHANGED
@@ -14,6 +14,10 @@ window.__ModuleLoader__.load({
14
14
  const SESSION_EDITOR_PATH = "/session-editor";
15
15
  //#endregion
16
16
  //#region src/client/controller.ts
17
+ /** desktop 壳(官方 dsh-desktop-host)注入的 transport 标记:ownsHost 时 API 走 /api 前缀。 */
18
+ function editorApiPath() {
19
+ return globalThis.__DSH_TRANSPORT__?.ownsHost === true ? `/api${SESSION_EDITOR_PATH}` : SESSION_EDITOR_PATH;
20
+ }
17
21
  function messageOf(error) {
18
22
  return error instanceof Error ? error.message : String(error);
19
23
  }
@@ -127,7 +131,7 @@ window.__ModuleLoader__.load({
127
131
  state.error = null;
128
132
  });
129
133
  try {
130
- const response = await fetch(`${SESSION_EDITOR_PATH}?sessionId=${encodeURIComponent(this.sessionId)}`, {
134
+ const response = await fetch(`${editorApiPath()}?sessionId=${encodeURIComponent(this.sessionId)}`, {
131
135
  method: "GET",
132
136
  headers: { accept: "application/json" },
133
137
  cache: "no-store"
@@ -161,7 +165,7 @@ window.__ModuleLoader__.load({
161
165
  state.error = null;
162
166
  });
163
167
  try {
164
- const response = await fetch(SESSION_EDITOR_PATH, {
168
+ const response = await fetch(editorApiPath(), {
165
169
  method: "POST",
166
170
  headers: {
167
171
  accept: "application/json",
@@ -187,6 +191,12 @@ window.__ModuleLoader__.load({
187
191
  const resync = face.resync;
188
192
  if (resync !== void 0) try {
189
193
  await resync.call(face);
194
+ const projections = face.projections;
195
+ if (typeof projections?.truncate !== "function") {
196
+ location.reload();
197
+ return true;
198
+ }
199
+ projections.truncate(-1);
190
200
  this.load();
191
201
  return true;
192
202
  } catch {}
@@ -352,11 +362,11 @@ window.__ModuleLoader__.load({
352
362
  }
353
363
  var MessageIconActions_module_css_default = {
354
364
  "runTimeDot": "lKOSUG_runTimeDot",
355
- "actions": "lKOSUG_actions",
356
- "visuallyHidden": "lKOSUG_visuallyHidden",
357
365
  "timeEnd": "lKOSUG_timeEnd",
366
+ "action": "lKOSUG_action",
358
367
  "timeStart": "lKOSUG_timeStart",
359
- "action": "lKOSUG_action"
368
+ "actions": "lKOSUG_actions",
369
+ "visuallyHidden": "lKOSUG_visuallyHidden"
360
370
  };
361
371
  //#endregion
362
372
  //#region src/client/chat-node/MessageIconActions.tsx
@@ -498,8 +508,8 @@ window.__ModuleLoader__.load({
498
508
  document.head.appendChild(tag);
499
509
  }
500
510
  var MessageEditDialog_module_css_default = {
501
- "actions": "_7VqDXa_actions",
502
- "input": "_7VqDXa_input"
511
+ "input": "_7VqDXa_input",
512
+ "actions": "_7VqDXa_actions"
503
513
  };
504
514
  //#endregion
505
515
  //#region src/client/chat-node/MessageEditDialog.tsx
@@ -570,34 +580,34 @@ window.__ModuleLoader__.load({
570
580
  document.head.appendChild(tag);
571
581
  }
572
582
  var MessageItem_module_css_default = {
573
- "compactionTitle": "okW0Ua_compactionTitle",
583
+ "retryRow": "okW0Ua_retryRow",
574
584
  "turnErrorRow": "okW0Ua_turnErrorRow",
575
- "maxTokensTitle": "okW0Ua_maxTokensTitle",
576
- "retryText": "okW0Ua_retryText",
577
- "retryDetails": "okW0Ua_retryDetails",
578
- "retryDetailLabel": "okW0Ua_retryDetailLabel",
579
- "turnErrorCopy": "okW0Ua_turnErrorCopy",
580
- "userStack": "okW0Ua_userStack",
581
- "compactionRow": "okW0Ua_compactionRow",
585
+ "bubble": "okW0Ua_bubble",
586
+ "confirmActions": "okW0Ua_confirmActions",
587
+ "userRow": "okW0Ua_userRow",
588
+ "compactionLeading": "okW0Ua_compactionLeading",
589
+ "compactionButton": "okW0Ua_compactionButton",
590
+ "retry-shimmer": "okW0Ua_retry-shimmer",
582
591
  "compactionContextIcon": "okW0Ua_compactionContextIcon",
583
- "compactionSep": "okW0Ua_compactionSep",
592
+ "turnErrorDot": "okW0Ua_turnErrorDot",
593
+ "compactionSummary": "okW0Ua_compactionSummary",
584
594
  "compactionBody": "okW0Ua_compactionBody",
585
- "turnErrorCode": "okW0Ua_turnErrorCode",
595
+ "turnErrorCopy": "okW0Ua_turnErrorCopy",
596
+ "userStack": "okW0Ua_userStack",
597
+ "turnErrorTitle": "okW0Ua_turnErrorTitle",
586
598
  "turnErrorMessage": "okW0Ua_turnErrorMessage",
587
- "retryRow": "okW0Ua_retryRow",
588
- "compactionDisclosureIcon": "okW0Ua_compactionDisclosureIcon",
589
- "retry-shimmer": "okW0Ua_retry-shimmer",
590
- "compactionLeading": "okW0Ua_compactionLeading",
591
- "contextRow": "okW0Ua_contextRow",
592
- "compactionSummary": "okW0Ua_compactionSummary",
593
- "bubble": "okW0Ua_bubble",
594
599
  "retrySummary": "okW0Ua_retrySummary",
595
- "turnErrorTitle": "okW0Ua_turnErrorTitle",
600
+ "retryDetailLabel": "okW0Ua_retryDetailLabel",
601
+ "maxTokensTitle": "okW0Ua_maxTokensTitle",
602
+ "compactionDisclosureIcon": "okW0Ua_compactionDisclosureIcon",
603
+ "retryText": "okW0Ua_retryText",
596
604
  "refChip": "okW0Ua_refChip",
597
- "confirmActions": "okW0Ua_confirmActions",
598
- "compactionButton": "okW0Ua_compactionButton",
599
- "turnErrorDot": "okW0Ua_turnErrorDot",
600
- "userRow": "okW0Ua_userRow"
605
+ "turnErrorCode": "okW0Ua_turnErrorCode",
606
+ "compactionRow": "okW0Ua_compactionRow",
607
+ "retryDetails": "okW0Ua_retryDetails",
608
+ "contextRow": "okW0Ua_contextRow",
609
+ "compactionTitle": "okW0Ua_compactionTitle",
610
+ "compactionSep": "okW0Ua_compactionSep"
601
611
  };
602
612
  //#endregion
603
613
  //#region src/client/chat-node/MessageItem.tsx
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { _ as SessionEditorResult, d as EditableMessageBlock, f as RerollOperation, g as SessionEditorOperation, h as RewindOperation, l as retryableTurns, m as RetryableTurn, o as editableMessages, p as RetryOperation, r as closedTurns, u as EditOperation } from "./plan-C-_enFsA.mjs";
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";
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";
@@ -14,14 +14,6 @@ interface EditorAgent {
14
14
  followup(message: UserMessage): void;
15
15
  /** 等待 agent 到达 quiescence(当前 turn/任务结束后 resolve)。 */
16
16
  whenIdle(): Promise<void>;
17
- /**
18
- * 队列中是否有待处理输入(next-turn / next-step)。rewind 截断会删除这些
19
- * 输入所属轮次的事件,残留的 inbox 状态会与截断后的 log 失配,使后续
20
- * splice 落库后无法从日志重放——编辑前必须清空。
21
- */
22
- readonly inboxPending: boolean;
23
- /** 清空待处理输入(落库 canceled splice,须在 rewind 前调用)。 */
24
- clearInbox(): void;
25
17
  }
26
18
  interface EditorAgentHandle {
27
19
  readonly agent: EditorAgent;
package/dist/index.mjs CHANGED
@@ -1,3 +1,3 @@
1
- import { n as SessionEditor, t as SESSION_BRANCH_VERSION_SCHEMA } from "./src-DqUEYw-N.mjs";
1
+ import { n as SessionEditor, t as SESSION_BRANCH_VERSION_SCHEMA } from "./src-ks167MhO.mjs";
2
2
  import { closedTurns, editableMessages, retryableTurns } from "./plan.mjs";
3
3
  export { SESSION_BRANCH_VERSION_SCHEMA, SessionEditor, SessionEditor as default, closedTurns, editableMessages, retryableTurns };
@@ -78,9 +78,16 @@ interface OperationPlan {
78
78
  declare function closedTurns(events: readonly SessionEvent[]): ClosedTurn[];
79
79
  declare function editableMessages(turns: readonly ClosedTurn[]): EditableMessageBlock[];
80
80
  declare function retryableTurns(turns: readonly ClosedTurn[]): RetryableTurn[];
81
+ /**
82
+ * 目标轮之前最后一个「有内容」轮次的下标:跳过既无用户输入也无助手回复的
83
+ * 空轮。空轮是早期重放缺陷的遗留(turn/start 后直接 turn/end),在轨迹 /
84
+ * 轮次导航里表现为点不开的空项,并让后续轮号出现空洞。rewind 把它们随
85
+ * 目标轮一起截断,重放按保留前缀续号即可自愈。
86
+ */
87
+ declare function precedingContentIndex(turns: readonly ClosedTurn[], turnIndex: number): number;
81
88
  declare function downstreamUsers(turns: readonly ClosedTurn[], start: number): UserMessage[];
82
89
  declare function editPlan(operation: EditOperation, turns: readonly ClosedTurn[]): OperationPlan;
83
90
  declare function retryPlan(operation: RetryOperation, turns: readonly ClosedTurn[]): OperationPlan;
84
91
  declare function rerollPlan(operation: RerollOperation, turns: readonly ClosedTurn[]): OperationPlan;
85
92
  //#endregion
86
- export { SessionEditorResult as _, editPlan as a, retryPlan as c, EditableMessageBlock as d, RerollOperation as f, SessionEditorOperation as g, RewindOperation as h, downstreamUsers as i, retryableTurns as l, RetryableTurn as m, OperationPlan as n, editableMessages as o, RetryOperation as p, closedTurns as r, rerollPlan as s, ClosedTurn as t, EditOperation as u };
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 };
package/dist/plan.d.mts CHANGED
@@ -1,2 +1,2 @@
1
- import { a as editPlan, c as retryPlan, i as downstreamUsers, l as retryableTurns, n as OperationPlan, o as editableMessages, r as closedTurns, s as rerollPlan, t as ClosedTurn } from "./plan-C-_enFsA.mjs";
2
- export { ClosedTurn, OperationPlan, closedTurns, downstreamUsers, editPlan, editableMessages, rerollPlan, retryPlan, retryableTurns };
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 };
package/dist/plan.mjs CHANGED
@@ -100,6 +100,21 @@ function retryableTurns(turns) {
100
100
  time: turn.user.time
101
101
  }]);
102
102
  }
103
+ /**
104
+ * 目标轮之前最后一个「有内容」轮次的下标:跳过既无用户输入也无助手回复的
105
+ * 空轮。空轮是早期重放缺陷的遗留(turn/start 后直接 turn/end),在轨迹 /
106
+ * 轮次导航里表现为点不开的空项,并让后续轮号出现空洞。rewind 把它们随
107
+ * 目标轮一起截断,重放按保留前缀续号即可自愈。
108
+ */
109
+ function precedingContentIndex(turns, turnIndex) {
110
+ let index = turnIndex - 1;
111
+ while (index >= 0) {
112
+ const turn = turns[index];
113
+ if (turn.users.length > 0 || turn.assistants.length > 0) break;
114
+ index -= 1;
115
+ }
116
+ return index;
117
+ }
103
118
  function downstreamUsers(turns, start) {
104
119
  return turns.slice(start).flatMap((turn) => turn.users.map((user) => cloneUser(user.data)));
105
120
  }
@@ -144,7 +159,7 @@ function editPlan(operation, turns) {
144
159
  const later = operation.cascade === "preserve" ? downstreamUsers(turns, turnIndex + 1) : [];
145
160
  return {
146
161
  anchorSeq: turn.startSeq,
147
- ...turn.closed && userIndex === 0 ? {} : { rewindBoundary: event.seq },
162
+ ...userIndex === 0 ? {} : { rewindBoundary: event.seq },
148
163
  version: pairVersionEffect(operation.sessionId, {
149
164
  operation: "edit",
150
165
  cascade: operation.cascade,
@@ -222,4 +237,4 @@ function rerollPlan(operation, turns) {
222
237
  throw new SessionBranchError("当前会话没有可重生成的已落定助手回复。", "INVALID_BOUNDARY");
223
238
  }
224
239
  //#endregion
225
- export { closedTurns, downstreamUsers, editPlan, editableMessages, rerollPlan, retryPlan, retryableTurns };
240
+ export { closedTurns, downstreamUsers, editPlan, editableMessages, precedingContentIndex, rerollPlan, retryPlan, retryableTurns };
@@ -1,4 +1,4 @@
1
- import { closedTurns, editPlan, editableMessages, rerollPlan, retryPlan, retryableTurns } from "./plan.mjs";
1
+ import { closedTurns, editPlan, editableMessages, precedingContentIndex, 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";
@@ -173,18 +173,24 @@ var SessionEditor = class extends Service {
173
173
  const turns = closedTurns(events);
174
174
  const plan = operation.action === "edit" ? editPlan(operation, turns) : operation.action === "retry" ? retryPlan(operation, turns) : rerollPlan(operation, turns);
175
175
  const headerConfig = events.findLast((event) => event.type === "request/header")?.data.header.config;
176
+ const turnIndex = turns.findIndex((turn) => turn.startSeq === plan.anchorSeq);
177
+ const preceding = precedingContentIndex(turns, turnIndex);
178
+ const boundary = plan.rewindBoundary !== void 0 ? plan.rewindBoundary : preceding < 0 ? -1 : turns[preceding].endSeq;
179
+ const replayTurn = preceding < 0 ? 1 : turns[preceding].turn + 1;
176
180
  const seedSuffix = [];
177
181
  appendLogSeedEvent(seedSuffix, "session-branch/version", plan.version, true);
178
- if (plan.manualTurn !== void 0) appendManualTurn(seedSuffix, plan.manualTurn);
182
+ if (plan.manualTurn !== void 0) appendManualTurn(seedSuffix, {
183
+ ...plan.manualTurn,
184
+ turn: replayTurn
185
+ });
179
186
  const replay = await this.prepareReplay(operation.sessionId, plan.queuedUsers, signal, headerConfig);
180
- const turnIndex = turns.findIndex((turn) => turn.startSeq === plan.anchorSeq);
181
- const boundary = plan.rewindBoundary !== void 0 ? plan.rewindBoundary : turnIndex <= 0 ? -1 : turns[turnIndex - 1].endSeq;
182
187
  const live = this.ctx.sessions.get(operation.sessionId);
183
188
  await this.ctx.sessionBranch.rewind(operation.sessionId, boundary, signal);
184
189
  if (seedSuffix.length > 0) {
185
190
  if (live !== void 0) {
186
191
  const handle = this.ctx.sessionPersistence.tracker.writerOf(operation.sessionId);
187
192
  if (handle === void 0) throw new Error(`session "${operation.sessionId}" has no live write handle for the version effect`);
193
+ await this.ctx.sessions.flush(live);
188
194
  await appendSeedSuffixLive(live, seedSuffix, (events) => handle.append(events));
189
195
  await this.ctx.sessions.flush(live);
190
196
  } else {
@@ -204,6 +210,11 @@ var SessionEditor = class extends Service {
204
210
  }
205
211
  let queuedTurns = 0;
206
212
  if (replay.agent !== void 0 && plan.queuedUsers.length > 0) {
213
+ const lastSeedTurn = seedSuffix.findLast((event) => event.type === "turn/start")?.data.turn;
214
+ if (lastSeedTurn !== void 0) {
215
+ const phase = replay.agent.phase;
216
+ if (phase !== void 0) phase.lastTurn = lastSeedTurn;
217
+ }
207
218
  for (const message of plan.queuedUsers) replay.agent.followup(message);
208
219
  await this.ctx.sessions.flush(replay.agent.session);
209
220
  queuedTurns = plan.queuedUsers.length;
@@ -234,7 +245,6 @@ var SessionEditor = class extends Service {
234
245
  if (existing !== void 0) {
235
246
  await existing.whenIdle();
236
247
  signal?.throwIfAborted();
237
- if (existing.inboxPending) existing.clearInbox();
238
248
  return { agent: existing };
239
249
  }
240
250
  const provider = headerConfig?.provider ?? "";
@@ -403,9 +413,9 @@ async function handleRoute(editor, request, response) {
403
413
  }
404
414
  }
405
415
  function registerHttpRoutes(ctx) {
406
- const webServer = ctx.get("webServer");
407
- if (webServer === void 0) return;
408
416
  ctx.effect(() => {
417
+ const webServer = ctx.get("webServer");
418
+ if (webServer === void 0) return () => {};
409
419
  const editor = ctx.sessionEditor;
410
420
  return webServer.register({
411
421
  kind: "exact",
@@ -413,6 +423,41 @@ function registerHttpRoutes(ctx) {
413
423
  handler: (request, response) => handleRoute(editor, request, response)
414
424
  });
415
425
  }, "session-editor: HTTP route");
426
+ const registerConnectionRoute = () => {
427
+ const connection = ctx.get("connection", false);
428
+ if (connection === void 0) return;
429
+ const editor = ctx.sessionEditor;
430
+ ctx.effect(() => connection.fetch.register({
431
+ path: `/api${SESSION_EDITOR_PATH}`,
432
+ methods: ["GET", "POST"],
433
+ requestBody: "buffered",
434
+ fetch: (request) => handleFetchRoute(editor, request)
435
+ }), "session-editor: connection fetch route");
436
+ };
437
+ ctx.on("internal/service", (name) => {
438
+ if (name === "connection") registerConnectionRoute();
439
+ });
440
+ registerConnectionRoute();
441
+ }
442
+ /** connection.fetch 路由的 Fetch 形态处理(与 webServer 的 node:http 形态同语义)。 */
443
+ async function handleFetchRoute(editor, request) {
444
+ try {
445
+ if (request.method === "GET") return jsonResponse(200, await readTimeline(editor, sessionIdOf(new URL(request.url).searchParams.get("sessionId"))));
446
+ if (request.method === "POST") return jsonResponse(200, await runOperation(editor, decodeOperation(await request.json())));
447
+ return new Response(null, { status: 405 });
448
+ } catch (error) {
449
+ const message = error instanceof Error ? error.message : String(error);
450
+ return jsonResponse(error instanceof TypeError ? 400 : 409, { error: message });
451
+ }
452
+ }
453
+ function jsonResponse(status, value) {
454
+ return new Response(JSON.stringify(value), {
455
+ status,
456
+ headers: {
457
+ "content-type": "application/json; charset=utf-8",
458
+ "cache-control": "no-store"
459
+ }
460
+ });
416
461
  }
417
462
  //#endregion
418
463
  export { SessionEditor as n, SESSION_BRANCH_VERSION_SCHEMA$1 as t };