@morlay/ui-conversation-message-actions 0.0.17 → 0.0.19-alpha.0

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.
Files changed (45) hide show
  1. package/README.md +29 -98
  2. package/dist/client.cjs +1380 -0
  3. package/dist/client.d.cts +6 -0
  4. package/dist/client.d.mts +6 -0
  5. package/dist/index.d.cts +98 -0
  6. package/dist/index.d.mts +3 -10
  7. package/dist/index.mjs +460 -2
  8. package/dist/invariant.d.cts +7 -0
  9. package/dist/{plan-BSa7k9HO.d.mts → plan-CT_6Y1fD.d.cts} +11 -9
  10. package/dist/plan-Cm9hCord.d.mts +101 -0
  11. package/dist/plan.d.cts +2 -0
  12. package/dist/plan.d.mts +2 -2
  13. package/dist/plan.mjs +145 -11
  14. package/dist/shared-LK3nImWW.mjs +55 -0
  15. package/dist/testing.d.cts +3242 -0
  16. package/dist/testing.d.mts +25 -19
  17. package/dist/testing.mjs +2140 -3939
  18. package/package.json +35 -32
  19. package/src/client/chat-node/MessageIconActions.styles.ts +76 -0
  20. package/src/client/chat-node/MessageIconActions.tsx +30 -11
  21. package/src/client/chat-node/MessageItem.styles.ts +247 -0
  22. package/src/client/chat-node/MessageItem.tsx +32 -39
  23. package/src/client/chat-node/message-chrome.ts +0 -2
  24. package/src/client/chat-node/register.ts +3 -4
  25. package/src/client/chat-node/use-calendar-day.ts +0 -4
  26. package/src/client/composer-stats/StatsPills.styles.ts +57 -0
  27. package/src/client/composer-stats/StatsPills.tsx +384 -0
  28. package/src/client/composer-stats/register.ts +17 -0
  29. package/src/client/composer-stats/stat-dialog.styles.ts +70 -0
  30. package/src/client/composer-stats/stat-dialog.ts +52 -0
  31. package/src/client/composer-stats/token-format.ts +85 -0
  32. package/src/client/composer-stats/turn-reading.ts +35 -0
  33. package/src/client/controller.ts +32 -114
  34. package/src/client/index.ts +8 -24
  35. package/src/index.ts +102 -111
  36. package/src/plan.ts +189 -24
  37. package/src/shared.ts +1 -1
  38. package/src/testing.ts +1 -5
  39. package/src/types.ts +1 -1
  40. package/dist/client.js +0 -806
  41. package/dist/src-B4HwXsob.mjs +0 -499
  42. package/src/client/chat-node/MessageIconActions.module.css +0 -86
  43. package/src/client/chat-node/MessageItem.module.css +0 -290
  44. package/src/client/css-modules.d.ts +0 -4
  45. package/src/client/import-action.tsx +0 -74
@@ -0,0 +1,6 @@
1
+ import { Context } from "@deepseek-ai/cordis";
2
+ //#region src/client/index.d.ts
3
+ declare const inject: string[];
4
+ declare function apply(ctx: Context): void;
5
+ //#endregion
6
+ export { apply, inject };
@@ -0,0 +1,6 @@
1
+ import { Context } from "@deepseek-ai/cordis";
2
+ //#region src/client/index.d.ts
3
+ declare const inject: string[];
4
+ declare function apply(ctx: Context): void;
5
+ //#endregion
6
+ export { apply, inject };
@@ -0,0 +1,98 @@
1
+ import { C as SessionEditorResult, S as SessionEditorOperation, _ as RecallOperation, b as RetryableTurn, c as editableMessages, g as EditableMessageBlock, h as EditOperation, i as closedTurns, m as retryableTurns, v as RerollOperation, x as RewindOperation, y as RetryOperation } from "./plan-CT_6Y1fD.cjs";
2
+ import { Context, Service } from "@deepseek-ai/cordis";
3
+ import { UserMessage } from "@deepseek-ai/dsh-llm";
4
+ import { AgentCancelCause, Session, SessionEvent, SessionId } from "@deepseek-ai/dsh-session";
5
+ import { BranchBoundary, BranchTimeline, BranchTimeline as BranchTimeline$1, CascadePolicy, EditableBlockKind, SESSION_BRANCH_VERSION_SCHEMA, SessionBranchVersionEvent } from "@morlay/session-branch";
6
+ //#region src/index.d.ts
7
+ declare module "@deepseek-ai/cordis" {
8
+ interface Context {
9
+ sessionEditor: SessionEditor;
10
+ }
11
+ }
12
+ interface EditorAgent {
13
+ readonly session: Session;
14
+ followup(message: UserMessage): void;
15
+ whenIdle(): Promise<void>;
16
+ cancel?(cause: AgentCancelCause, options?: {
17
+ keepInbox?: boolean;
18
+ }): void;
19
+ }
20
+ interface EditorAgentHandle {
21
+ readonly agent: EditorAgent;
22
+ dispose(): Promise<void>;
23
+ }
24
+ interface EditorAgentRegistry {
25
+ get(sessionId: SessionId): EditorAgent | undefined;
26
+ create(options: {
27
+ sessionId?: SessionId;
28
+ seed?: readonly SessionEvent[];
29
+ meta?: {
30
+ cwd?: string;
31
+ parentSession?: SessionId;
32
+ seedLength?: number;
33
+ agentPreset?: string;
34
+ };
35
+ agentOptions?: {
36
+ provider: string;
37
+ model: string;
38
+ maxTokens?: number;
39
+ };
40
+ }): Promise<EditorAgentHandle>;
41
+ resume(options: {
42
+ resumeSessionId: SessionId;
43
+ agentOptions?: {
44
+ provider: string;
45
+ model: string;
46
+ maxTokens?: number;
47
+ };
48
+ }): Promise<EditorAgentHandle>;
49
+ }
50
+ declare class SessionEditor extends Service {
51
+ static inject: string[];
52
+ constructor(ctx: Context);
53
+ readBranchPrefix(id: SessionId, atSeq?: number, mode?: "after" | "before", signal?: AbortSignal): Promise<BranchBoundary>;
54
+ fork(sourceId: SessionId, atSeq?: number, childSessionId?: SessionId, meta?: {
55
+ cwd?: string;
56
+ agentPreset?: string;
57
+ }, signal?: AbortSignal): Promise<SessionId>;
58
+ rewind(id: SessionId, toBoundary: number, signal?: AbortSignal): Promise<import("@deepseek-ai/dsh-session-persistence").SessionPersistenceSnapshot>;
59
+ timeline(sessionId: SessionId, signal?: AbortSignal): Promise<BranchTimeline$1>;
60
+ edit(operation: EditOperation, signal?: AbortSignal): Promise<SessionEditorResult>;
61
+ reroll(operation: RerollOperation, signal?: AbortSignal): Promise<SessionEditorResult>;
62
+ retry(operation: RetryOperation, signal?: AbortSignal): Promise<SessionEditorResult>;
63
+ recall(operation: RecallOperation, signal?: AbortSignal): Promise<SessionEditorResult>;
64
+ editableMessages(sessionId: SessionId, signal?: AbortSignal): Promise<EditableMessageBlock[]>;
65
+ retryableTurns(sessionId: SessionId, signal?: AbortSignal): Promise<RetryableTurn[]>;
66
+ private branchOperation;
67
+ private recallOperation;
68
+ private seedStart;
69
+ private appendSeedSuffix;
70
+ private stopLoop;
71
+ private readEvents;
72
+ private prepareReplay;
73
+ }
74
+ interface HttpRequestLike {
75
+ method?: string;
76
+ url?: string;
77
+ on(event: "data", listener: (chunk: Uint8Array | string) => void): this;
78
+ on(event: "end", listener: () => void): this;
79
+ on(event: "error", listener: (error: unknown) => void): this;
80
+ }
81
+ interface HttpResponseLike {
82
+ writeHead(status: number, headers?: Record<string, string>): unknown;
83
+ end(body?: string): void;
84
+ }
85
+ interface HttpServerLike {
86
+ register(route: {
87
+ kind: "exact";
88
+ path: string;
89
+ handler: (request: HttpRequestLike, response: HttpResponseLike) => void | Promise<void>;
90
+ }): () => void;
91
+ }
92
+ declare module "@deepseek-ai/cordis" {
93
+ interface Context {
94
+ webServer: HttpServerLike;
95
+ }
96
+ }
97
+ //#endregion
98
+ 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.d.mts CHANGED
@@ -1,4 +1,4 @@
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";
1
+ import { C as SessionEditorResult, S as SessionEditorOperation, _ as RecallOperation, b as RetryableTurn, c as editableMessages, g as EditableMessageBlock, h as EditOperation, i as closedTurns, m as retryableTurns, v as RerollOperation, x as RewindOperation, y as RetryOperation } from "./plan-Cm9hCord.mjs";
2
2
  import { Context, Service } from "@deepseek-ai/cordis";
3
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";
@@ -12,7 +12,6 @@ declare module "@deepseek-ai/cordis" {
12
12
  interface EditorAgent {
13
13
  readonly session: Session;
14
14
  followup(message: UserMessage): void;
15
- /** 等待 agent 到达 quiescence(当前 turn/任务结束后 resolve)。 */
16
15
  whenIdle(): Promise<void>;
17
16
  cancel?(cause: AgentCancelCause, options?: {
18
17
  keepInbox?: boolean;
@@ -65,15 +64,9 @@ declare class SessionEditor extends Service {
65
64
  editableMessages(sessionId: SessionId, signal?: AbortSignal): Promise<EditableMessageBlock[]>;
66
65
  retryableTurns(sessionId: SessionId, signal?: AbortSignal): Promise<RetryableTurn[]>;
67
66
  private branchOperation;
68
- /**
69
- * 撤回(recall):只 rewind 截断,不重写、不重放——被撤回的 user 消息文本
70
- * 由客户端回填到输入框,交给用户修改后重新发送。
71
- *
72
- * 边界见 {@link recallBoundary}:轮首消息整轮截断到前一轮 `turn/end`
73
- * (无前轮则 -1),轮内 followup 与轮外 user 消息只截断到该消息本身
74
- * (exclusive drop),避免留下悬空 `turn/start` 让下一次发送开到错误轮号。
75
- */
76
67
  private recallOperation;
68
+ private seedStart;
69
+ private appendSeedSuffix;
77
70
  private stopLoop;
78
71
  private readEvents;
79
72
  private prepareReplay;
package/dist/index.mjs CHANGED
@@ -1,3 +1,461 @@
1
- import { n as SessionEditor, t as SESSION_BRANCH_VERSION_SCHEMA } from "./src-B4HwXsob.mjs";
2
- import { closedTurns, editableMessages, retryableTurns } from "./plan.mjs";
1
+ import { n as toTimelinePayload, t as SESSION_EDITOR_PATH } from "./shared-LK3nImWW.mjs";
2
+ import { closedTurns, droppedCompactions, editPlan, editableMessages, precedingContentIndex, recallBoundary, rerollPlan, restoreCompactions, retryPlan, retryableTurns } from "./plan.mjs";
3
+ import { Service } from "@deepseek-ai/cordis";
4
+ import { SessionSeq } from "@deepseek-ai/dsh-session";
5
+ import { SESSION_BRANCH_VERSION_SCHEMA, SessionBranchError, balanceRewindPrefix } from "@morlay/session-branch";
6
+ //#region src/index.ts
7
+ function appendLogSeedEvent(events, type, data, ignorable = false) {
8
+ events.push({
9
+ type,
10
+ seq: events.length,
11
+ time: Date.now(),
12
+ data,
13
+ ...ignorable ? { ignorable: true } : {}
14
+ });
15
+ }
16
+ function appendSurfaceSeedEvent(events, type, data, intent) {
17
+ events.push({
18
+ type,
19
+ seq: SessionSeq(events.length),
20
+ time: Date.now(),
21
+ data,
22
+ surfaceOp: intent.surfaceOp,
23
+ ...intent.sourceEventSeqs === void 0 ? {} : { sourceEventSeqs: intent.sourceEventSeqs }
24
+ });
25
+ }
26
+ function appendManualTurn(events, manual) {
27
+ const { turn, user, assistant } = manual;
28
+ appendLogSeedEvent(events, "turn/start", { turn });
29
+ appendSurfaceSeedEvent(events, "user/message", user, { surfaceOp: "append" });
30
+ appendLogSeedEvent(events, "step/start", {
31
+ turn,
32
+ step: 1
33
+ });
34
+ appendSurfaceSeedEvent(events, "assistant/message", {
35
+ turn,
36
+ step: 1,
37
+ message: assistant,
38
+ stream: []
39
+ }, { surfaceOp: "append" });
40
+ appendLogSeedEvent(events, "step/end", {
41
+ turn,
42
+ step: 1
43
+ });
44
+ appendLogSeedEvent(events, "turn/end", {
45
+ turn,
46
+ reason: { kind: "completed" }
47
+ });
48
+ }
49
+ function keepFromOf(events, boundary) {
50
+ if (boundary < 0) return 0;
51
+ return events[boundary]?.type === "turn/end" ? boundary + 1 : boundary;
52
+ }
53
+ async function appendSeedSuffixLive(session, seedSuffix, appendDirect) {
54
+ for (const event of seedSuffix) {
55
+ if (event.ignorable === true) {
56
+ const s = session;
57
+ const seq = s.log.length;
58
+ await appendDirect([{
59
+ ...event,
60
+ seq
61
+ }]);
62
+ s.log.push({
63
+ ...event,
64
+ seq
65
+ });
66
+ s.eventsSnapshot = void 0;
67
+ continue;
68
+ }
69
+ const s = session;
70
+ const raw = event;
71
+ if (raw.surfaceOp !== void 0) s.append(event.type, event.data, {
72
+ surfaceOp: raw.surfaceOp,
73
+ ...raw.sourceEventSeqs === void 0 ? {} : { sourceEventSeqs: raw.sourceEventSeqs }
74
+ });
75
+ else s.append(event.type, event.data);
76
+ }
77
+ }
78
+ var SessionEditor = class extends Service {
79
+ static inject = [
80
+ "sessionBranch",
81
+ "sessionPersistence",
82
+ "sessions"
83
+ ];
84
+ constructor(ctx) {
85
+ super(ctx, "sessionEditor");
86
+ registerHttpRoutes(ctx);
87
+ }
88
+ readBranchPrefix(id, atSeq, mode, signal) {
89
+ return this.ctx.sessionBranch.readBranchPrefix(id, atSeq, mode, signal);
90
+ }
91
+ fork(sourceId, atSeq, childSessionId, meta, signal) {
92
+ return this.ctx.sessionBranch.forkFrom(sourceId, {
93
+ ...atSeq === void 0 ? {} : { atSeq },
94
+ ...childSessionId === void 0 ? {} : { childSessionId },
95
+ ...meta === void 0 ? {} : { meta }
96
+ }, signal);
97
+ }
98
+ async rewind(id, toBoundary, signal) {
99
+ await this.stopLoop(id, signal);
100
+ return this.ctx.sessionBranch.rewind(id, toBoundary, signal);
101
+ }
102
+ timeline(sessionId, signal) {
103
+ return this.ctx.sessionBranch.timeline(sessionId, signal);
104
+ }
105
+ edit(operation, signal) {
106
+ return this.branchOperation(operation, signal);
107
+ }
108
+ reroll(operation, signal) {
109
+ return this.branchOperation(operation, signal);
110
+ }
111
+ retry(operation, signal) {
112
+ return this.branchOperation(operation, signal);
113
+ }
114
+ recall(operation, signal) {
115
+ return this.recallOperation(operation, signal);
116
+ }
117
+ async editableMessages(sessionId, signal) {
118
+ const events = await this.readEvents(sessionId, signal);
119
+ return editableMessages(closedTurns(events));
120
+ }
121
+ async retryableTurns(sessionId, signal) {
122
+ const events = await this.readEvents(sessionId, signal);
123
+ return retryableTurns(closedTurns(events));
124
+ }
125
+ async branchOperation(operation, signal) {
126
+ signal?.throwIfAborted();
127
+ const events = await this.readEvents(operation.sessionId, signal);
128
+ const turns = closedTurns(events);
129
+ const plan = operation.action === "edit" ? editPlan(operation, turns) : operation.action === "retry" ? retryPlan(operation, turns) : rerollPlan(operation, turns);
130
+ const headerConfig = events.findLast((event) => event.type === "request/header")?.data.header.config;
131
+ const turnIndex = turns.findIndex((turn) => turn.startSeq === plan.anchorSeq);
132
+ const preceding = precedingContentIndex(turns, turnIndex);
133
+ const boundary = plan.rewindBoundary !== void 0 ? plan.rewindBoundary : preceding < 0 ? -1 : turns[preceding].endSeq;
134
+ const keepFrom = keepFromOf(events, boundary);
135
+ const compactions = droppedCompactions(events, keepFrom, plan.targetSeq);
136
+ const replayTurn = preceding < 0 ? 1 : turns[preceding].turn + 1;
137
+ const versionSeed = [];
138
+ appendLogSeedEvent(versionSeed, "session-branch/version", plan.version, true);
139
+ const manualSeed = [];
140
+ if (plan.manualTurn !== void 0) appendManualTurn(manualSeed, {
141
+ ...plan.manualTurn,
142
+ turn: replayTurn
143
+ });
144
+ const replay = await this.prepareReplay(operation.sessionId, plan.queuedUsers, signal, headerConfig);
145
+ await this.stopLoop(operation.sessionId, signal);
146
+ const live = this.ctx.sessions.get(operation.sessionId);
147
+ await this.ctx.sessionBranch.rewind(operation.sessionId, boundary, signal);
148
+ const seedStart = this.seedStart(live, events, keepFrom);
149
+ const seedSuffix = [
150
+ ...versionSeed,
151
+ ...restoreCompactions(compactions, keepFrom, seedStart + versionSeed.length),
152
+ ...manualSeed
153
+ ];
154
+ await this.appendSeedSuffix(operation.sessionId, live, seedSuffix, events, keepFrom);
155
+ let queuedTurns = 0;
156
+ if (replay.agent !== void 0 && plan.queuedUsers.length > 0) {
157
+ const lastSeedTurn = seedSuffix.findLast((event) => event.type === "turn/start")?.data.turn;
158
+ if (lastSeedTurn !== void 0) {
159
+ const phase = replay.agent.phase;
160
+ if (phase !== void 0) phase.lastTurn = lastSeedTurn;
161
+ }
162
+ for (const message of plan.queuedUsers) replay.agent.followup(message);
163
+ await this.ctx.sessions.flush(replay.agent.session);
164
+ queuedTurns = plan.queuedUsers.length;
165
+ }
166
+ return {
167
+ sessionId: operation.sessionId,
168
+ queuedTurns,
169
+ live: this.ctx.sessions.get(operation.sessionId) !== void 0
170
+ };
171
+ }
172
+ async recallOperation(operation, signal) {
173
+ signal?.throwIfAborted();
174
+ const events = await this.readEvents(operation.sessionId, signal);
175
+ const boundary = recallBoundary(events, closedTurns(events), operation.eventSeq);
176
+ const keepFrom = keepFromOf(events, boundary);
177
+ const compactions = droppedCompactions(events, keepFrom, operation.eventSeq);
178
+ await this.stopLoop(operation.sessionId, signal);
179
+ const live = this.ctx.sessions.get(operation.sessionId);
180
+ await this.ctx.sessionBranch.rewind(operation.sessionId, boundary, signal);
181
+ await this.appendSeedSuffix(operation.sessionId, live, restoreCompactions(compactions, keepFrom, this.seedStart(live, events, keepFrom)), events, keepFrom);
182
+ return {
183
+ sessionId: operation.sessionId,
184
+ queuedTurns: 0,
185
+ live: this.ctx.sessions.get(operation.sessionId) !== void 0
186
+ };
187
+ }
188
+ seedStart(live, events, keepFrom) {
189
+ return live === void 0 ? balanceRewindPrefix(events.slice(0, keepFrom)).length : Number(live.seq);
190
+ }
191
+ async appendSeedSuffix(sessionId, live, seedSuffix, events, keepFrom) {
192
+ if (seedSuffix.length === 0) return;
193
+ if (live !== void 0) {
194
+ const handle = this.ctx.sessionPersistence.tracker.writerOf(sessionId);
195
+ if (handle === void 0) throw new Error(`session "${sessionId}" has no live write handle for the version effect`);
196
+ await this.ctx.sessions.flush(live);
197
+ await appendSeedSuffixLive(live, seedSuffix, (batch) => handle.append(batch));
198
+ await this.ctx.sessions.flush(live);
199
+ return;
200
+ }
201
+ const keepLength = balanceRewindPrefix(events.slice(0, keepFrom)).length;
202
+ const renumbered = seedSuffix.map((event, index) => ({
203
+ ...event,
204
+ seq: keepLength + index
205
+ }));
206
+ const handle = await this.ctx.sessionPersistence.open(sessionId, "write");
207
+ try {
208
+ await handle.append(renumbered);
209
+ } finally {
210
+ await handle.close();
211
+ }
212
+ }
213
+ async stopLoop(sessionId, signal) {
214
+ signal?.throwIfAborted();
215
+ const existing = this.ctx.get("agents")?.get(sessionId);
216
+ if (existing === void 0) return;
217
+ existing.cancel?.({ kind: "user" }, { keepInbox: true });
218
+ await existing.whenIdle();
219
+ signal?.throwIfAborted();
220
+ }
221
+ async readEvents(sessionId, signal) {
222
+ const live = this.ctx.sessions.get(sessionId);
223
+ if (live !== void 0) return live.snapshotEvents();
224
+ return (await this.ctx.sessionBranch.readRawEvents(sessionId, signal)).events;
225
+ }
226
+ async prepareReplay(sessionId, queuedUsers, signal, headerConfig) {
227
+ if (queuedUsers.length === 0) return { agent: void 0 };
228
+ signal?.throwIfAborted();
229
+ const agents = this.ctx.get("agents");
230
+ if (agents === void 0) return { agent: void 0 };
231
+ const existing = agents.get(sessionId);
232
+ if (existing !== void 0) return { agent: existing };
233
+ const provider = headerConfig?.provider ?? "";
234
+ const model = headerConfig?.model ?? "";
235
+ if (provider.length === 0 || model.length === 0) {
236
+ const config = (await this.readEvents(sessionId, signal)).findLast((event) => event.type === "request/header")?.data.header.config;
237
+ const fallbackProvider = config?.provider ?? "";
238
+ const fallbackModel = config?.model ?? "";
239
+ if (fallbackProvider.length === 0 || fallbackModel.length === 0) throw new SessionBranchError("无法重放:会话没有可解析的模型配置。", "INVALID_BOUNDARY");
240
+ const handle = await agents.resume({
241
+ resumeSessionId: sessionId,
242
+ agentOptions: {
243
+ provider: fallbackProvider,
244
+ model: fallbackModel
245
+ }
246
+ });
247
+ signal?.throwIfAborted();
248
+ return { agent: handle.agent };
249
+ }
250
+ const handle = await agents.resume({
251
+ resumeSessionId: sessionId,
252
+ agentOptions: {
253
+ provider,
254
+ model
255
+ }
256
+ });
257
+ signal?.throwIfAborted();
258
+ return { agent: handle.agent };
259
+ }
260
+ };
261
+ function objectValue(value) {
262
+ if (typeof value !== "object" || value === null || Array.isArray(value)) throw new TypeError("请求体必须是 JSON 对象。");
263
+ return value;
264
+ }
265
+ function sessionIdOf(value) {
266
+ if (typeof value !== "string" || value.length === 0) throw new TypeError("sessionId 必须是非空字符串。");
267
+ return value;
268
+ }
269
+ function integerOf(value, name) {
270
+ if (!Number.isSafeInteger(value) || value < 0) throw new TypeError(`${name} 必须是非负安全整数。`);
271
+ return value;
272
+ }
273
+ function cascadeOf(value) {
274
+ if (value !== "truncate" && value !== "preserve") throw new TypeError("cascade 必须是 truncate 或 preserve。");
275
+ return value;
276
+ }
277
+ function decodeOperation(value) {
278
+ const record = objectValue(value);
279
+ const sessionId = sessionIdOf(record["sessionId"]);
280
+ switch (record["action"]) {
281
+ case "edit":
282
+ if (typeof record["text"] !== "string") throw new TypeError("text 必须是字符串。");
283
+ return {
284
+ action: "edit",
285
+ sessionId,
286
+ eventSeq: integerOf(record["eventSeq"], "eventSeq"),
287
+ blockIndex: integerOf(record["blockIndex"], "blockIndex"),
288
+ text: record["text"],
289
+ cascade: cascadeOf(record["cascade"])
290
+ };
291
+ case "reroll": return {
292
+ action: "reroll",
293
+ sessionId
294
+ };
295
+ case "retry": return {
296
+ action: "retry",
297
+ sessionId,
298
+ turn: integerOf(record["turn"], "turn"),
299
+ cascade: cascadeOf(record["cascade"])
300
+ };
301
+ case "rewind": return {
302
+ action: "rewind",
303
+ sessionId,
304
+ toBoundary: integerOf(record["toBoundary"], "toBoundary")
305
+ };
306
+ case "recall": return {
307
+ action: "recall",
308
+ sessionId,
309
+ eventSeq: integerOf(record["eventSeq"], "eventSeq")
310
+ };
311
+ case "fork": return {
312
+ action: "fork",
313
+ sessionId,
314
+ ...record["atSeq"] === void 0 ? {} : { atSeq: integerOf(record["atSeq"], "atSeq") },
315
+ ...record["childSessionId"] === void 0 ? {} : { childSessionId: sessionIdOf(record["childSessionId"]) }
316
+ };
317
+ default: throw new TypeError("action 必须是 edit、reroll、retry、rewind、recall 或 fork。");
318
+ }
319
+ }
320
+ function requestJson(request) {
321
+ return new Promise((resolve, reject) => {
322
+ const decoder = new TextDecoder();
323
+ let text = "";
324
+ request.on("data", (chunk) => {
325
+ text += typeof chunk === "string" ? chunk : decoder.decode(chunk, { stream: true });
326
+ });
327
+ request.on("end", () => {
328
+ try {
329
+ text += decoder.decode();
330
+ resolve(JSON.parse(text));
331
+ } catch (error) {
332
+ reject(error);
333
+ }
334
+ });
335
+ request.on("error", reject);
336
+ });
337
+ }
338
+ function respondJson(response, status, value) {
339
+ response.writeHead(status, {
340
+ "content-type": "application/json; charset=utf-8",
341
+ "cache-control": "no-store"
342
+ });
343
+ response.end(JSON.stringify(value));
344
+ }
345
+ async function readTimeline(editor, sessionId) {
346
+ const timeline = await editor.timeline(sessionId);
347
+ const messages = await editor.editableMessages(sessionId);
348
+ const retryable = await editor.retryableTurns(sessionId);
349
+ return toTimelinePayload(sessionId, timeline, messages, retryable);
350
+ }
351
+ async function runOperation(editor, operation) {
352
+ switch (operation.action) {
353
+ case "edit": {
354
+ const result = await editor.edit(operation);
355
+ return {
356
+ sessionId: result.sessionId,
357
+ queuedTurns: result.queuedTurns,
358
+ ...result.live === void 0 ? {} : { live: result.live }
359
+ };
360
+ }
361
+ case "reroll": {
362
+ const result = await editor.reroll(operation);
363
+ return {
364
+ sessionId: result.sessionId,
365
+ queuedTurns: result.queuedTurns,
366
+ ...result.live === void 0 ? {} : { live: result.live }
367
+ };
368
+ }
369
+ case "retry": {
370
+ const result = await editor.retry(operation);
371
+ return {
372
+ sessionId: result.sessionId,
373
+ queuedTurns: result.queuedTurns,
374
+ ...result.live === void 0 ? {} : { live: result.live }
375
+ };
376
+ }
377
+ case "rewind":
378
+ await editor.rewind(operation.sessionId, operation.toBoundary);
379
+ return {
380
+ sessionId: operation.sessionId,
381
+ queuedTurns: 0
382
+ };
383
+ case "recall": {
384
+ const result = await editor.recall(operation);
385
+ return {
386
+ sessionId: result.sessionId,
387
+ queuedTurns: result.queuedTurns,
388
+ ...result.live === void 0 ? {} : { live: result.live }
389
+ };
390
+ }
391
+ case "fork": return {
392
+ sessionId: await editor.fork(operation.sessionId, operation.atSeq, operation.childSessionId),
393
+ queuedTurns: 0
394
+ };
395
+ }
396
+ }
397
+ async function handleRoute(editor, request, response) {
398
+ try {
399
+ if (request.method === "GET") {
400
+ respondJson(response, 200, await readTimeline(editor, sessionIdOf(new URL(request.url ?? "/session-editor", "http://session-editor.local").searchParams.get("sessionId"))));
401
+ return;
402
+ }
403
+ if (request.method === "POST") {
404
+ respondJson(response, 200, await runOperation(editor, decodeOperation(await requestJson(request))));
405
+ return;
406
+ }
407
+ response.writeHead(405);
408
+ response.end();
409
+ } catch (error) {
410
+ const message = error instanceof Error ? error.message : String(error);
411
+ respondJson(response, error instanceof TypeError ? 400 : 409, { error: message });
412
+ }
413
+ }
414
+ function registerHttpRoutes(ctx) {
415
+ ctx.effect(() => {
416
+ const webServer = ctx.get("webServer");
417
+ if (webServer === void 0) return () => {};
418
+ const editor = ctx.sessionEditor;
419
+ return webServer.register({
420
+ kind: "exact",
421
+ path: SESSION_EDITOR_PATH,
422
+ handler: (request, response) => handleRoute(editor, request, response)
423
+ });
424
+ }, "session-editor: HTTP route");
425
+ const registerConnectionRoute = () => {
426
+ const connection = ctx.get("connection", false);
427
+ if (connection === void 0) return;
428
+ const editor = ctx.sessionEditor;
429
+ ctx.effect(() => connection.fetch.register({
430
+ path: `/api${SESSION_EDITOR_PATH}`,
431
+ methods: ["GET", "POST"],
432
+ requestBody: "buffered",
433
+ fetch: (request) => handleFetchRoute(editor, request)
434
+ }), "session-editor: connection fetch route");
435
+ };
436
+ ctx.on("internal/service", (name) => {
437
+ if (name === "connection") registerConnectionRoute();
438
+ });
439
+ registerConnectionRoute();
440
+ }
441
+ async function handleFetchRoute(editor, request) {
442
+ try {
443
+ if (request.method === "GET") return jsonResponse(200, await readTimeline(editor, sessionIdOf(new URL(request.url).searchParams.get("sessionId"))));
444
+ if (request.method === "POST") return jsonResponse(200, await runOperation(editor, decodeOperation(await request.json())));
445
+ return new Response(null, { status: 405 });
446
+ } catch (error) {
447
+ const message = error instanceof Error ? error.message : String(error);
448
+ return jsonResponse(error instanceof TypeError ? 400 : 409, { error: message });
449
+ }
450
+ }
451
+ function jsonResponse(status, value) {
452
+ return new Response(JSON.stringify(value), {
453
+ status,
454
+ headers: {
455
+ "content-type": "application/json; charset=utf-8",
456
+ "cache-control": "no-store"
457
+ }
458
+ });
459
+ }
460
+ //#endregion
3
461
  export { SESSION_BRANCH_VERSION_SCHEMA, SessionEditor, SessionEditor as default, closedTurns, editableMessages, retryableTurns };
@@ -0,0 +1,7 @@
1
+ import { Context } from "@deepseek-ai/cordis";
2
+ //#region src/invariant.d.ts
3
+ declare const name = "ui-conversation-message-actions-invariant";
4
+ declare const inject: string[];
5
+ declare const apply: (ctx: Context) => Promise<() => void>;
6
+ //#endregion
7
+ export { apply, inject, name };
@@ -1,6 +1,6 @@
1
+ import { AssistantMessage, UserMessage } from "@deepseek-ai/dsh-llm";
1
2
  import { SessionEvent, SessionId } from "@deepseek-ai/dsh-session";
2
3
  import { BranchTimeline, CascadePolicy, SessionBranchVersionEvent } from "@morlay/session-branch";
3
- import { AssistantMessage, UserMessage } from "@deepseek-ai/dsh-llm";
4
4
  //#region src/types.d.ts
5
5
  interface EditOperation {
6
6
  action: "edit";
@@ -65,7 +65,6 @@ interface ClosedTurn {
65
65
  endSeq?: number;
66
66
  closed: boolean;
67
67
  user?: SessionEvent<"user/message">;
68
- /** 轮内全部 user/message(agent 运行中 followup 追加的输入也计入)。 */
69
68
  users: SessionEvent<"user/message">[];
70
69
  assistants: SessionEvent<"assistant/message">[];
71
70
  }
@@ -79,21 +78,24 @@ interface OperationPlan {
79
78
  assistant: AssistantMessage;
80
79
  };
81
80
  queuedUsers: UserMessage[];
81
+ targetSeq: number;
82
82
  }
83
83
  declare function closedTurns(events: readonly SessionEvent[]): ClosedTurn[];
84
84
  declare function editableMessages(turns: readonly ClosedTurn[]): EditableMessageBlock[];
85
85
  declare function retryableTurns(turns: readonly ClosedTurn[]): RetryableTurn[];
86
- /**
87
- * 目标轮之前最后一个「有内容」轮次的下标:跳过既无用户输入也无助手回复的
88
- * 空轮。空轮是早期重放缺陷的遗留(turn/start 后直接 turn/end),在轨迹 /
89
- * 轮次导航里表现为点不开的空项,并让后续轮号出现空洞。rewind 把它们随
90
- * 目标轮一起截断,重放按保留前缀续号即可自愈。
91
- */
92
86
  declare function precedingContentIndex(turns: readonly ClosedTurn[], turnIndex: number): number;
93
87
  declare function downstreamUsers(turns: readonly ClosedTurn[], start: number): UserMessage[];
94
88
  declare function recallBoundary(events: readonly SessionEvent[], turns: readonly ClosedTurn[], eventSeq: number): number;
95
89
  declare function editPlan(operation: EditOperation, turns: readonly ClosedTurn[]): OperationPlan;
96
90
  declare function retryPlan(operation: RetryOperation, turns: readonly ClosedTurn[]): OperationPlan;
97
91
  declare function rerollPlan(operation: RerollOperation, turns: readonly ClosedTurn[]): OperationPlan;
92
+ interface CompactionBracket {
93
+ start: SessionEvent;
94
+ summary?: SessionEvent;
95
+ checkpoint: SessionEvent;
96
+ end: SessionEvent;
97
+ }
98
+ declare function droppedCompactions(events: readonly SessionEvent[], keepFrom: number, targetSeq: number): CompactionBracket[];
99
+ declare function restoreCompactions(brackets: readonly CompactionBracket[], keepFrom: number, base: number): SessionEvent[];
98
100
  //#endregion
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 };
101
+ export { SessionEditorResult as C, SessionEditorOperation as S, RecallOperation as _, downstreamUsers as a, RetryableTurn as b, editableMessages as c, rerollPlan as d, restoreCompactions as f, EditableMessageBlock as g, EditOperation as h, closedTurns as i, precedingContentIndex as l, retryableTurns as m, CompactionBracket as n, droppedCompactions as o, retryPlan as p, OperationPlan as r, editPlan as s, ClosedTurn as t, recallBoundary as u, RerollOperation as v, RewindOperation as x, RetryOperation as y };