@morlay/ui-conversation-message-actions 0.0.11 → 0.0.13

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 (42) hide show
  1. package/README.md +0 -6
  2. package/dist/client.js +831 -0
  3. package/dist/index.d.mts +105 -0
  4. package/dist/index.mjs +3 -0
  5. package/dist/invariant.d.mts +7 -0
  6. package/dist/invariant.mjs +8 -0
  7. package/dist/plan-Bw4zoI4N.d.mts +86 -0
  8. package/dist/plan.d.mts +2 -0
  9. package/dist/plan.mjs +225 -0
  10. package/dist/src-CXYW0Bc0.mjs +408 -0
  11. package/dist/testing.d.mts +707 -0
  12. package/dist/testing.mjs +23690 -0
  13. package/package.json +60 -31
  14. package/src/client/chat-node/MessageEditDialog.module.css +24 -0
  15. package/src/client/chat-node/MessageEditDialog.tsx +71 -0
  16. package/src/client/chat-node/MessageIconActions.module.css +86 -0
  17. package/src/client/chat-node/MessageIconActions.tsx +189 -0
  18. package/src/client/chat-node/MessageItem.module.css +290 -0
  19. package/src/client/chat-node/MessageItem.tsx +198 -0
  20. package/src/client/chat-node/message-chrome.ts +61 -0
  21. package/src/client/chat-node/register.ts +33 -0
  22. package/src/client/chat-node/use-calendar-day.ts +23 -0
  23. package/src/client/controller.ts +327 -0
  24. package/src/client/css-modules.d.ts +4 -0
  25. package/src/client/import-action.tsx +74 -0
  26. package/src/client/index.ts +50 -0
  27. package/src/index.ts +650 -0
  28. package/src/invariant.ts +13 -0
  29. package/src/plan.ts +322 -0
  30. package/src/shared.ts +167 -0
  31. package/src/testing.ts +151 -0
  32. package/src/types.ts +73 -0
  33. package/lib/client.js +0 -2243
  34. package/lib/client.js.map +0 -1
  35. package/lib/index.d.mts +0 -211
  36. package/lib/index.d.mts.map +0 -1
  37. package/lib/index.mjs +0 -685
  38. package/lib/index.mjs.map +0 -1
  39. package/lib/invariant.d.mts +0 -15
  40. package/lib/invariant.d.mts.map +0 -1
  41. package/lib/invariant.mjs +0 -22
  42. package/lib/invariant.mjs.map +0 -1
@@ -0,0 +1,105 @@
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-Bw4zoI4N.mjs";
2
+ import { Context, Service } from "@deepseek-ai/cordis";
3
+ import { BranchBoundary, BranchTimeline, BranchTimeline as BranchTimeline$1, CascadePolicy, EditableBlockKind, SESSION_BRANCH_VERSION_SCHEMA, SessionBranchVersionEvent } from "@morlay/session-branch";
4
+ import { Session, SessionEvent, SessionId } from "@deepseek-ai/dsh-session";
5
+ import { UserMessage } from "@deepseek-ai/dsh-llm";
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
+ /** 等待 agent 到达 quiescence(当前 turn/任务结束后 resolve)。 */
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
+ }
26
+ interface EditorAgentHandle {
27
+ readonly agent: EditorAgent;
28
+ dispose(): Promise<void>;
29
+ }
30
+ interface EditorAgentRegistry {
31
+ get(sessionId: SessionId): EditorAgent | undefined;
32
+ create(options: {
33
+ sessionId?: SessionId;
34
+ seed?: readonly SessionEvent[];
35
+ meta?: {
36
+ cwd?: string;
37
+ parentSession?: SessionId;
38
+ seedLength?: number;
39
+ agentPreset?: string;
40
+ };
41
+ agentOptions?: {
42
+ provider: string;
43
+ model: string;
44
+ maxTokens?: number;
45
+ };
46
+ }): Promise<EditorAgentHandle>;
47
+ resume(options: {
48
+ resumeSessionId: SessionId;
49
+ agentOptions?: {
50
+ provider: string;
51
+ model: string;
52
+ maxTokens?: number;
53
+ };
54
+ }): Promise<EditorAgentHandle>;
55
+ }
56
+ declare class SessionEditor extends Service {
57
+ static inject: string[];
58
+ constructor(ctx: Context);
59
+ readBranchPrefix(id: SessionId, atSeq?: number, mode?: "after" | "before", signal?: AbortSignal): Promise<BranchBoundary>;
60
+ fork(sourceId: SessionId, atSeq?: number, childSessionId?: SessionId, meta?: {
61
+ cwd?: string;
62
+ agentPreset?: string;
63
+ }, signal?: AbortSignal): Promise<SessionId>;
64
+ rewind(id: SessionId, toBoundary: number, signal?: AbortSignal): Promise<import("@deepseek-ai/dsh-session-persistence").SessionPersistenceSnapshot>;
65
+ timeline(sessionId: SessionId, signal?: AbortSignal): Promise<BranchTimeline$1>;
66
+ edit(operation: EditOperation, signal?: AbortSignal): Promise<SessionEditorResult>;
67
+ reroll(operation: RerollOperation, signal?: AbortSignal): Promise<SessionEditorResult>;
68
+ retry(operation: RetryOperation, signal?: AbortSignal): Promise<SessionEditorResult>;
69
+ editableMessages(sessionId: SessionId, signal?: AbortSignal): Promise<EditableMessageBlock[]>;
70
+ retryableTurns(sessionId: SessionId, signal?: AbortSignal): Promise<RetryableTurn[]>;
71
+ private branchOperation;
72
+ private readEvents;
73
+ /**
74
+ * rewind 前确保 agent 可驱动重放:live agent 先等待其停下(rewind 会截断其
75
+ * session 内存 log,须在 quiescence 后执行);cold 会话先 resume 出驻留
76
+ * agent(此时会话完整,resume 的 prepare 不与截断冲突)。agents 服务缺失或
77
+ * 无需重放时返回空——调用方退化为就地截断版本。
78
+ */
79
+ private prepareReplay;
80
+ }
81
+ interface HttpRequestLike {
82
+ method?: string;
83
+ url?: string;
84
+ on(event: "data", listener: (chunk: Uint8Array | string) => void): this;
85
+ on(event: "end", listener: () => void): this;
86
+ on(event: "error", listener: (error: unknown) => void): this;
87
+ }
88
+ interface HttpResponseLike {
89
+ writeHead(status: number, headers?: Record<string, string>): unknown;
90
+ end(body?: string): void;
91
+ }
92
+ interface HttpServerLike {
93
+ register(route: {
94
+ kind: "exact";
95
+ path: string;
96
+ handler: (request: HttpRequestLike, response: HttpResponseLike) => void | Promise<void>;
97
+ }): () => void;
98
+ }
99
+ declare module "@deepseek-ai/cordis" {
100
+ interface Context {
101
+ webServer: HttpServerLike;
102
+ }
103
+ }
104
+ //#endregion
105
+ 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 };
package/dist/index.mjs ADDED
@@ -0,0 +1,3 @@
1
+ import { n as SessionEditor, t as SESSION_BRANCH_VERSION_SCHEMA } from "./src-CXYW0Bc0.mjs";
2
+ import { closedTurns, editableMessages, retryableTurns } from "./plan.mjs";
3
+ 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 };
@@ -0,0 +1,8 @@
1
+ //#region src/invariant.ts
2
+ const PACKAGE_NAME = "@morlay/ui-conversation-message-actions";
3
+ const name = "ui-conversation-message-actions-invariant";
4
+ const inject = ["invariants"];
5
+ const install = () => {};
6
+ const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
7
+ //#endregion
8
+ export { apply, inject, name };
@@ -0,0 +1,86 @@
1
+ import { BranchTimeline, CascadePolicy, SessionBranchVersionEvent } from "@morlay/session-branch";
2
+ import { SessionEvent, SessionId } from "@deepseek-ai/dsh-session";
3
+ import { AssistantMessage, UserMessage } from "@deepseek-ai/dsh-llm";
4
+ //#region src/types.d.ts
5
+ interface EditOperation {
6
+ action: "edit";
7
+ sessionId: SessionId;
8
+ eventSeq: number;
9
+ blockIndex: number;
10
+ text: string;
11
+ cascade: CascadePolicy;
12
+ }
13
+ interface RerollOperation {
14
+ action: "reroll";
15
+ sessionId: SessionId;
16
+ }
17
+ interface RetryOperation {
18
+ action: "retry";
19
+ sessionId: SessionId;
20
+ turn: number;
21
+ cascade: CascadePolicy;
22
+ }
23
+ interface RewindOperation {
24
+ action: "rewind";
25
+ sessionId: SessionId;
26
+ toBoundary: number;
27
+ }
28
+ interface ForkOperation {
29
+ action: "fork";
30
+ sessionId: SessionId;
31
+ atSeq?: number;
32
+ childSessionId?: SessionId;
33
+ }
34
+ type SessionEditorOperation = EditOperation | RerollOperation | RetryOperation | RewindOperation | ForkOperation;
35
+ interface SessionEditorResult {
36
+ sessionId: SessionId;
37
+ queuedTurns: number;
38
+ live?: boolean;
39
+ }
40
+ interface EditableMessageBlock {
41
+ key: string;
42
+ turn: number;
43
+ eventSeq: number;
44
+ blockIndex: number;
45
+ kind: "user" | "assistant.reasoning" | "assistant.response";
46
+ text: string;
47
+ time: number;
48
+ }
49
+ interface RetryableTurn {
50
+ turn: number;
51
+ userEventSeq: number;
52
+ preview: string;
53
+ time: number;
54
+ }
55
+ //#endregion
56
+ //#region src/plan.d.ts
57
+ interface ClosedTurn {
58
+ turn: number;
59
+ startSeq: number;
60
+ endSeq?: number;
61
+ closed: boolean;
62
+ user?: SessionEvent<"user/message">;
63
+ /** 轮内全部 user/message(agent 运行中 followup 追加的输入也计入)。 */
64
+ users: SessionEvent<"user/message">[];
65
+ assistants: SessionEvent<"assistant/message">[];
66
+ }
67
+ interface OperationPlan {
68
+ anchorSeq: number;
69
+ rewindBoundary?: number;
70
+ version: SessionBranchVersionEvent;
71
+ manualTurn?: {
72
+ turn: number;
73
+ user: UserMessage;
74
+ assistant: AssistantMessage;
75
+ };
76
+ queuedUsers: UserMessage[];
77
+ }
78
+ declare function closedTurns(events: readonly SessionEvent[]): ClosedTurn[];
79
+ declare function editableMessages(turns: readonly ClosedTurn[]): EditableMessageBlock[];
80
+ declare function retryableTurns(turns: readonly ClosedTurn[]): RetryableTurn[];
81
+ declare function downstreamUsers(turns: readonly ClosedTurn[], start: number): UserMessage[];
82
+ declare function editPlan(operation: EditOperation, turns: readonly ClosedTurn[]): OperationPlan;
83
+ declare function retryPlan(operation: RetryOperation, turns: readonly ClosedTurn[]): OperationPlan;
84
+ declare function rerollPlan(operation: RerollOperation, turns: readonly ClosedTurn[]): OperationPlan;
85
+ //#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 };
@@ -0,0 +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-Bw4zoI4N.mjs";
2
+ export { ClosedTurn, OperationPlan, closedTurns, downstreamUsers, editPlan, editableMessages, rerollPlan, retryPlan, retryableTurns };
package/dist/plan.mjs ADDED
@@ -0,0 +1,225 @@
1
+ import { SESSION_BRANCH_VERSION_SCHEMA, SessionBranchError } from "@morlay/session-branch";
2
+ import { randomUUID } from "node:crypto";
3
+ //#region src/plan.ts
4
+ function isTextualBlock(block) {
5
+ return block?.type === "text" || block?.type === "reasoning";
6
+ }
7
+ function userText(message) {
8
+ return message.content.filter((block) => block.type === "text").map((block) => block.text).join("\n");
9
+ }
10
+ function cloneUser(message, content = structuredClone(message.content)) {
11
+ return Object.freeze({
12
+ id: randomUUID(),
13
+ role: "user",
14
+ content: Object.freeze(content),
15
+ source: Object.freeze({ kind: "user" })
16
+ });
17
+ }
18
+ function replaceTextBlock(content, blockIndex, text) {
19
+ const block = content[blockIndex];
20
+ if (!isTextualBlock(block)) throw new SessionBranchError("所选内容块不是可编辑文本。", "INVALID_BOUNDARY");
21
+ return content.map((candidate, index) => index === blockIndex ? {
22
+ ...candidate,
23
+ text
24
+ } : structuredClone(candidate));
25
+ }
26
+ function closedTurns(events) {
27
+ const result = [];
28
+ let current;
29
+ for (const event of events) {
30
+ if (event.type === "turn/start") {
31
+ current = {
32
+ turn: event.data.turn,
33
+ startSeq: event.seq,
34
+ users: [],
35
+ assistants: []
36
+ };
37
+ continue;
38
+ }
39
+ if (current === void 0) continue;
40
+ if (event.type === "user/message" && event.data.source.kind === "user") {
41
+ if (current.user === void 0) current.user = event;
42
+ current.users.push(event);
43
+ continue;
44
+ }
45
+ if (event.type === "assistant/message" && event.data.turn === current.turn) {
46
+ current.assistants.push(event);
47
+ continue;
48
+ }
49
+ if (event.type === "turn/end" && event.data.turn === current.turn) {
50
+ result.push({
51
+ ...current,
52
+ endSeq: event.seq,
53
+ closed: true
54
+ });
55
+ current = void 0;
56
+ }
57
+ }
58
+ if (current !== void 0) result.push({
59
+ ...current,
60
+ closed: false
61
+ });
62
+ return result;
63
+ }
64
+ function editableMessages(turns) {
65
+ const result = [];
66
+ for (const turn of turns) {
67
+ for (const user of turn.users) for (const [blockIndex, block] of user.data.content.entries()) {
68
+ if (block.type !== "text") continue;
69
+ result.push({
70
+ key: `${String(user.seq)}:${String(blockIndex)}`,
71
+ turn: turn.turn,
72
+ eventSeq: user.seq,
73
+ blockIndex,
74
+ kind: "user",
75
+ text: block.text,
76
+ time: user.time
77
+ });
78
+ }
79
+ if (!turn.closed) continue;
80
+ for (const event of turn.assistants) for (const [blockIndex, block] of event.data.message.content.entries()) {
81
+ if (!isTextualBlock(block)) continue;
82
+ result.push({
83
+ key: `${String(event.seq)}:${String(blockIndex)}`,
84
+ turn: turn.turn,
85
+ eventSeq: event.seq,
86
+ blockIndex,
87
+ kind: block.type === "reasoning" ? "assistant.reasoning" : "assistant.response",
88
+ text: block.text,
89
+ time: event.time
90
+ });
91
+ }
92
+ }
93
+ return result;
94
+ }
95
+ function retryableTurns(turns) {
96
+ return turns.flatMap((turn) => turn.user === void 0 || !turn.closed ? [] : [{
97
+ turn: turn.turn,
98
+ userEventSeq: turn.user.seq,
99
+ preview: userText(turn.user.data),
100
+ time: turn.user.time
101
+ }]);
102
+ }
103
+ function downstreamUsers(turns, start) {
104
+ return turns.slice(start).flatMap((turn) => turn.users.map((user) => cloneUser(user.data)));
105
+ }
106
+ function assistantReplacement(event, blockIndex, text) {
107
+ const replaced = replaceTextBlock(event.data.message.content, blockIndex, text).filter((block) => block.type === "text" || block.type === "reasoning");
108
+ return Object.freeze({
109
+ id: randomUUID(),
110
+ role: "assistant",
111
+ content: Object.freeze(replaced),
112
+ source: Object.freeze({
113
+ kind: "model",
114
+ provider: event.data.message.source.provider,
115
+ model: event.data.message.source.model
116
+ })
117
+ });
118
+ }
119
+ function pairVersionEffect(sourceSessionId, effect) {
120
+ return {
121
+ schemaVersion: SESSION_BRANCH_VERSION_SCHEMA,
122
+ effect: {
123
+ ...effect,
124
+ id: randomUUID()
125
+ },
126
+ inverse: {
127
+ kind: "restore-version",
128
+ sessionId: sourceSessionId
129
+ }
130
+ };
131
+ }
132
+ function editPlan(operation, turns) {
133
+ const turnIndex = turns.findIndex((turn) => operation.eventSeq > turn.startSeq && (turn.endSeq === void 0 || operation.eventSeq < turn.endSeq));
134
+ const turn = turns[turnIndex];
135
+ if (turn === void 0) throw new SessionBranchError("所选消息不属于已落定回合。", "INVALID_BOUNDARY");
136
+ const event = turn.users.find((candidate) => candidate.seq === operation.eventSeq) ?? turn.assistants.find((candidate) => candidate.seq === operation.eventSeq);
137
+ if (event === void 0) throw new SessionBranchError("所选消息不存在或不可编辑。", "INVALID_BOUNDARY");
138
+ if (event.type === "user/message") {
139
+ const before = event.data.content[operation.blockIndex];
140
+ if (before?.type !== "text") throw new SessionBranchError("所选用户消息块不是文本。", "INVALID_BOUNDARY");
141
+ const edited = cloneUser(event.data, replaceTextBlock(event.data.content, operation.blockIndex, operation.text));
142
+ const userIndex = turn.users.findIndex((candidate) => candidate.seq === event.seq);
143
+ const sameTurnFollowups = turn.users.slice(userIndex + 1).map((user) => cloneUser(user.data));
144
+ const later = operation.cascade === "preserve" ? downstreamUsers(turns, turnIndex + 1) : [];
145
+ return {
146
+ anchorSeq: turn.startSeq,
147
+ ...turn.closed && userIndex === 0 ? {} : { rewindBoundary: event.seq },
148
+ version: pairVersionEffect(operation.sessionId, {
149
+ operation: "edit",
150
+ cascade: operation.cascade,
151
+ targetTurn: turn.turn,
152
+ targetEventSeq: event.seq,
153
+ targetBlockIndex: operation.blockIndex,
154
+ blockKind: "user",
155
+ before: before.text,
156
+ after: operation.text
157
+ }),
158
+ queuedUsers: [
159
+ edited,
160
+ ...sameTurnFollowups,
161
+ ...later
162
+ ]
163
+ };
164
+ }
165
+ if (!turn.closed) throw new SessionBranchError("未闭合轮次的助手消息不可编辑。", "INVALID_BOUNDARY");
166
+ const before = event.data.message.content[operation.blockIndex];
167
+ if (!isTextualBlock(before)) throw new SessionBranchError("所选助手消息块不是文本或思考。", "INVALID_BOUNDARY");
168
+ const blockKind = before.type === "reasoning" ? "assistant.reasoning" : "assistant.response";
169
+ if (turn.user === void 0) throw new SessionBranchError("所选助手消息没有可重建的用户输入。", "INVALID_BOUNDARY");
170
+ return {
171
+ anchorSeq: turn.startSeq,
172
+ version: pairVersionEffect(operation.sessionId, {
173
+ operation: "edit",
174
+ cascade: operation.cascade,
175
+ targetTurn: turn.turn,
176
+ targetEventSeq: event.seq,
177
+ targetBlockIndex: operation.blockIndex,
178
+ blockKind,
179
+ before: before.text,
180
+ after: operation.text
181
+ }),
182
+ manualTurn: {
183
+ turn: turn.turn,
184
+ user: cloneUser(turn.user.data),
185
+ assistant: assistantReplacement(event, operation.blockIndex, operation.text)
186
+ },
187
+ queuedUsers: operation.cascade === "preserve" ? downstreamUsers(turns, turnIndex + 1) : []
188
+ };
189
+ }
190
+ function retryPlan(operation, turns) {
191
+ const turnIndex = turns.findIndex((turn) => turn.turn === operation.turn);
192
+ const turn = turns[turnIndex];
193
+ if (turn?.user === void 0) throw new SessionBranchError("所选回合没有可重放的用户输入。", "INVALID_BOUNDARY");
194
+ return {
195
+ anchorSeq: turn.startSeq,
196
+ version: pairVersionEffect(operation.sessionId, {
197
+ operation: "retry",
198
+ cascade: operation.cascade,
199
+ targetTurn: turn.turn,
200
+ targetEventSeq: turn.user.seq
201
+ }),
202
+ queuedUsers: operation.cascade === "preserve" ? downstreamUsers(turns, turnIndex) : turn.users.map((user) => cloneUser(user.data))
203
+ };
204
+ }
205
+ function rerollPlan(operation, turns) {
206
+ for (let index = turns.length - 1; index >= 0; index -= 1) {
207
+ const turn = turns[index];
208
+ if (turn?.user === void 0 || !turn.closed) continue;
209
+ const target = turn.assistants.findLast((event) => event.data.message.content.some(isTextualBlock));
210
+ if (target === void 0) continue;
211
+ return {
212
+ anchorSeq: turn.startSeq,
213
+ version: pairVersionEffect(operation.sessionId, {
214
+ operation: "reroll",
215
+ cascade: "truncate",
216
+ targetTurn: turn.turn,
217
+ targetEventSeq: target.seq
218
+ }),
219
+ queuedUsers: turn.users.map((user) => cloneUser(user.data))
220
+ };
221
+ }
222
+ throw new SessionBranchError("当前会话没有可重生成的已落定助手回复。", "INVALID_BOUNDARY");
223
+ }
224
+ //#endregion
225
+ export { closedTurns, downstreamUsers, editPlan, editableMessages, rerollPlan, retryPlan, retryableTurns };