@morlay/ui-conversation-message-actions 0.0.18 → 0.0.19-alpha.1
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 +29 -98
- package/dist/client.cjs +1380 -0
- package/dist/client.d.cts +6 -0
- package/dist/client.d.mts +6 -0
- package/dist/index.d.cts +75 -0
- package/dist/index.d.mts +3 -33
- package/dist/index.mjs +460 -2
- package/dist/invariant.d.cts +7 -0
- package/dist/{plan-BSa7k9HO.d.mts → plan-CT_6Y1fD.d.cts} +11 -9
- package/dist/plan-Cm9hCord.d.mts +101 -0
- package/dist/plan.d.cts +2 -0
- package/dist/plan.d.mts +2 -2
- package/dist/plan.mjs +145 -11
- package/dist/shared-LK3nImWW.mjs +55 -0
- package/dist/testing.d.cts +3242 -0
- package/dist/testing.d.mts +11 -5
- package/dist/testing.mjs +1976 -3775
- package/package.json +34 -31
- package/src/client/chat-node/MessageIconActions.styles.ts +76 -0
- package/src/client/chat-node/MessageIconActions.tsx +30 -11
- package/src/client/chat-node/MessageItem.styles.ts +247 -0
- package/src/client/chat-node/MessageItem.tsx +32 -39
- package/src/client/chat-node/message-chrome.ts +0 -2
- package/src/client/chat-node/register.ts +3 -4
- package/src/client/chat-node/use-calendar-day.ts +0 -4
- package/src/client/composer-stats/StatsPills.styles.ts +57 -0
- package/src/client/composer-stats/StatsPills.tsx +384 -0
- package/src/client/composer-stats/register.ts +17 -0
- package/src/client/composer-stats/stat-dialog.styles.ts +70 -0
- package/src/client/composer-stats/stat-dialog.ts +52 -0
- package/src/client/composer-stats/token-format.ts +85 -0
- package/src/client/composer-stats/turn-reading.ts +35 -0
- package/src/client/controller.ts +32 -114
- package/src/client/index.ts +8 -24
- package/src/index.ts +105 -118
- package/src/plan.ts +189 -24
- package/src/shared.ts +1 -1
- package/src/testing.ts +1 -5
- package/src/types.ts +1 -1
- package/dist/client.js +0 -806
- package/dist/src-B4HwXsob.mjs +0 -499
- package/src/client/chat-node/MessageIconActions.module.css +0 -86
- package/src/client/chat-node/MessageItem.module.css +0 -290
- package/src/client/css-modules.d.ts +0 -4
- package/src/client/import-action.tsx +0 -74
package/src/plan.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import type { AssistantMessage, ContentBlock, UserMessage } from "@deepseek-ai/dsh-llm";
|
|
3
|
+
import { SessionSeq } from "@deepseek-ai/dsh-session";
|
|
3
4
|
import type { SessionEvent, SessionId } from "@deepseek-ai/dsh-session";
|
|
4
5
|
import {
|
|
5
6
|
SESSION_BRANCH_VERSION_SCHEMA,
|
|
@@ -24,7 +25,7 @@ export interface ClosedTurn {
|
|
|
24
25
|
|
|
25
26
|
closed: boolean;
|
|
26
27
|
user?: SessionEvent<"user/message">;
|
|
27
|
-
|
|
28
|
+
|
|
28
29
|
users: SessionEvent<"user/message">[];
|
|
29
30
|
assistants: SessionEvent<"assistant/message">[];
|
|
30
31
|
}
|
|
@@ -38,6 +39,9 @@ export interface OperationPlan {
|
|
|
38
39
|
manualTurn?: { turn: number; user: UserMessage; assistant: AssistantMessage };
|
|
39
40
|
|
|
40
41
|
queuedUsers: UserMessage[];
|
|
42
|
+
|
|
43
|
+
// 操作目标事件(被重放或撤回的那条)的 seq
|
|
44
|
+
targetSeq: number;
|
|
41
45
|
}
|
|
42
46
|
|
|
43
47
|
function isTextualBlock(
|
|
@@ -88,8 +92,6 @@ export function closedTurns(events: readonly SessionEvent[]): ClosedTurn[] {
|
|
|
88
92
|
}
|
|
89
93
|
if (current === undefined) continue;
|
|
90
94
|
if (event.type === "user/message" && event.data.source.kind === "user") {
|
|
91
|
-
// 轮内全部 user/message 都记录:agent 运行中 followup 追加的输入
|
|
92
|
-
// (第二条及之后)也是已落定文本,可编辑。
|
|
93
95
|
if (current.user === undefined) current.user = event;
|
|
94
96
|
current.users.push(event);
|
|
95
97
|
continue;
|
|
@@ -103,8 +105,7 @@ export function closedTurns(events: readonly SessionEvent[]): ClosedTurn[] {
|
|
|
103
105
|
current = undefined;
|
|
104
106
|
}
|
|
105
107
|
}
|
|
106
|
-
|
|
107
|
-
// 消息(exclusive drop)重放即可。
|
|
108
|
+
|
|
108
109
|
if (current !== undefined) result.push({ ...current, closed: false });
|
|
109
110
|
return result;
|
|
110
111
|
}
|
|
@@ -126,7 +127,7 @@ export function editableMessages(turns: readonly ClosedTurn[]): EditableMessageB
|
|
|
126
127
|
});
|
|
127
128
|
}
|
|
128
129
|
}
|
|
129
|
-
|
|
130
|
+
|
|
130
131
|
if (!turn.closed) continue;
|
|
131
132
|
for (const event of turn.assistants) {
|
|
132
133
|
for (const [blockIndex, block] of event.data.message.content.entries()) {
|
|
@@ -148,7 +149,6 @@ export function editableMessages(turns: readonly ClosedTurn[]): EditableMessageB
|
|
|
148
149
|
|
|
149
150
|
export function retryableTurns(turns: readonly ClosedTurn[]): RetryableTurn[] {
|
|
150
151
|
return turns.flatMap((turn): RetryableTurn[] =>
|
|
151
|
-
// 未闭合轮次无已落定回复可重生成,不可重试。
|
|
152
152
|
turn.user === undefined || !turn.closed
|
|
153
153
|
? []
|
|
154
154
|
: [
|
|
@@ -162,12 +162,6 @@ export function retryableTurns(turns: readonly ClosedTurn[]): RetryableTurn[] {
|
|
|
162
162
|
);
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
-
/**
|
|
166
|
-
* 目标轮之前最后一个「有内容」轮次的下标:跳过既无用户输入也无助手回复的
|
|
167
|
-
* 空轮。空轮是早期重放缺陷的遗留(turn/start 后直接 turn/end),在轨迹 /
|
|
168
|
-
* 轮次导航里表现为点不开的空项,并让后续轮号出现空洞。rewind 把它们随
|
|
169
|
-
* 目标轮一起截断,重放按保留前缀续号即可自愈。
|
|
170
|
-
*/
|
|
171
165
|
export function precedingContentIndex(turns: readonly ClosedTurn[], turnIndex: number): number {
|
|
172
166
|
let index = turnIndex - 1;
|
|
173
167
|
while (index >= 0) {
|
|
@@ -184,9 +178,6 @@ export function downstreamUsers(turns: readonly ClosedTurn[], start: number): Us
|
|
|
184
178
|
.flatMap((turn): UserMessage[] => turn.users.map((user) => cloneUser(user.data)));
|
|
185
179
|
}
|
|
186
180
|
|
|
187
|
-
// 撤回的 rewind 边界:轮内消息沿用 edit 的轮首规则(轮首整轮截断到前一轮
|
|
188
|
-
// turn/end,无前轮则 -1;轮内 followup 截断到该消息本身),轮外 user 消息按
|
|
189
|
-
// 消息自身位置 exclusive drop(截断该消息及其后全部)。
|
|
190
181
|
export function recallBoundary(
|
|
191
182
|
events: readonly SessionEvent[],
|
|
192
183
|
turns: readonly ClosedTurn[],
|
|
@@ -263,17 +254,13 @@ export function editPlan(operation: EditOperation, turns: readonly ClosedTurn[])
|
|
|
263
254
|
event.data,
|
|
264
255
|
replaceTextBlock(event.data.content, operation.blockIndex, operation.text),
|
|
265
256
|
);
|
|
266
|
-
|
|
267
|
-
// 输入:rewind 会 drop 它们,重放时保留(truncate 只截断回复,不丢输入)。
|
|
257
|
+
|
|
268
258
|
const userIndex = turn.users.findIndex((candidate) => candidate.seq === event.seq);
|
|
269
259
|
const sameTurnFollowups = turn.users.slice(userIndex + 1).map((user) => cloneUser(user.data));
|
|
270
260
|
const later = operation.cascade === "preserve" ? downstreamUsers(turns, turnIndex + 1) : [];
|
|
271
261
|
return {
|
|
272
262
|
anchorSeq: turn.startSeq,
|
|
273
|
-
|
|
274
|
-
// 目标轮号;未闭合轮同样整轮截断——保留悬空的 turn/start 会让重放
|
|
275
|
-
// 开到下一轮,轮号对不上。轮内 followup 只 rewind 到该消息本身
|
|
276
|
-
// (exclusive drop),保留轮内已落定的前置输入与回复。
|
|
263
|
+
|
|
277
264
|
...(userIndex === 0 ? {} : { rewindBoundary: event.seq }),
|
|
278
265
|
version: pairVersionEffect(operation.sessionId, {
|
|
279
266
|
operation: "edit",
|
|
@@ -286,10 +273,10 @@ export function editPlan(operation: EditOperation, turns: readonly ClosedTurn[])
|
|
|
286
273
|
after: operation.text,
|
|
287
274
|
}),
|
|
288
275
|
queuedUsers: [edited, ...sameTurnFollowups, ...later],
|
|
276
|
+
targetSeq: event.seq,
|
|
289
277
|
};
|
|
290
278
|
}
|
|
291
279
|
|
|
292
|
-
// 未闭合轮次的助手消息是流式 partial,无最终内容可编辑。
|
|
293
280
|
if (!turn.closed)
|
|
294
281
|
throw new SessionBranchError("未闭合轮次的助手消息不可编辑。", "INVALID_BOUNDARY");
|
|
295
282
|
const before = event.data.message.content[operation.blockIndex];
|
|
@@ -317,6 +304,7 @@ export function editPlan(operation: EditOperation, turns: readonly ClosedTurn[])
|
|
|
317
304
|
assistant: assistantReplacement(event, operation.blockIndex, operation.text),
|
|
318
305
|
},
|
|
319
306
|
queuedUsers: operation.cascade === "preserve" ? downstreamUsers(turns, turnIndex + 1) : [],
|
|
307
|
+
targetSeq: event.seq,
|
|
320
308
|
};
|
|
321
309
|
}
|
|
322
310
|
|
|
@@ -337,6 +325,7 @@ export function retryPlan(operation: RetryOperation, turns: readonly ClosedTurn[
|
|
|
337
325
|
operation.cascade === "preserve"
|
|
338
326
|
? downstreamUsers(turns, turnIndex)
|
|
339
327
|
: turn.users.map((user) => cloneUser(user.data)),
|
|
328
|
+
targetSeq: turn.user.seq,
|
|
340
329
|
};
|
|
341
330
|
}
|
|
342
331
|
|
|
@@ -346,7 +335,7 @@ export function rerollPlan(
|
|
|
346
335
|
): OperationPlan {
|
|
347
336
|
for (let index = turns.length - 1; index >= 0; index -= 1) {
|
|
348
337
|
const turn = turns[index];
|
|
349
|
-
|
|
338
|
+
|
|
350
339
|
if (turn?.user === undefined || !turn.closed) continue;
|
|
351
340
|
const target = turn.assistants.findLast((event) =>
|
|
352
341
|
event.data.message.content.some(isTextualBlock),
|
|
@@ -361,7 +350,183 @@ export function rerollPlan(
|
|
|
361
350
|
targetEventSeq: target.seq,
|
|
362
351
|
}),
|
|
363
352
|
queuedUsers: turn.users.map((user) => cloneUser(user.data)),
|
|
353
|
+
targetSeq: target.seq,
|
|
364
354
|
};
|
|
365
355
|
}
|
|
366
356
|
throw new SessionBranchError("当前会话没有可重生成的已落定助手回复。", "INVALID_BOUNDARY");
|
|
367
357
|
}
|
|
358
|
+
|
|
359
|
+
export interface CompactionBracket {
|
|
360
|
+
start: SessionEvent;
|
|
361
|
+
summary?: SessionEvent;
|
|
362
|
+
checkpoint: SessionEvent;
|
|
363
|
+
end: SessionEvent;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const COMPACTION_TYPES: ReadonlySet<string> = new Set([
|
|
367
|
+
"compaction/start",
|
|
368
|
+
"compaction/summary",
|
|
369
|
+
"compaction/end",
|
|
370
|
+
]);
|
|
371
|
+
|
|
372
|
+
// 本包不加载 compaction 的事件类型增强,按类型字符串与结构性字段读取(与 session-rdb 的 schema、log 同一处理方式)。
|
|
373
|
+
function compactionTypeOf(event: SessionEvent): string | undefined {
|
|
374
|
+
return COMPACTION_TYPES.has(event.type) ? event.type : undefined;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// 压缩事务的替换消息:compact 插件写入的 surface replace 用户消息(checkpoint)。
|
|
378
|
+
function isCompactionCheckpoint(event: SessionEvent): boolean {
|
|
379
|
+
if (event.type !== "user/message") return false;
|
|
380
|
+
const op = (event as unknown as { surfaceOp?: unknown }).surfaceOp;
|
|
381
|
+
if (typeof op !== "object" || op === null) return false;
|
|
382
|
+
const source = event.data.source as unknown as { kind?: unknown; plugin?: unknown };
|
|
383
|
+
return source.kind === "plugin" && source.plugin === "compact";
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function compactionBrackets(events: readonly SessionEvent[]): CompactionBracket[] {
|
|
387
|
+
const brackets: CompactionBracket[] = [];
|
|
388
|
+
let open: { start: SessionEvent; summary?: SessionEvent; checkpoint?: SessionEvent } | undefined;
|
|
389
|
+
for (const event of events) {
|
|
390
|
+
const type = compactionTypeOf(event);
|
|
391
|
+
if (type === "compaction/start") {
|
|
392
|
+
open = { start: event };
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
if (open === undefined) continue;
|
|
396
|
+
if (type === "compaction/summary") {
|
|
397
|
+
open.summary = event;
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
if (type === "compaction/end") {
|
|
401
|
+
if (open.checkpoint !== undefined) {
|
|
402
|
+
brackets.push({
|
|
403
|
+
start: open.start,
|
|
404
|
+
...(open.summary === undefined ? {} : { summary: open.summary }),
|
|
405
|
+
checkpoint: open.checkpoint,
|
|
406
|
+
end: event,
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
open = undefined;
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
if (isCompactionCheckpoint(event)) open.checkpoint = event;
|
|
413
|
+
}
|
|
414
|
+
return brackets;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// 截断丢掉、但仍在重放目标之前的完整压缩事务。
|
|
418
|
+
export function droppedCompactions(
|
|
419
|
+
events: readonly SessionEvent[],
|
|
420
|
+
keepFrom: number,
|
|
421
|
+
targetSeq: number,
|
|
422
|
+
): CompactionBracket[] {
|
|
423
|
+
return compactionBrackets(events).filter(
|
|
424
|
+
(bracket) => bracket.start.seq >= keepFrom && bracket.end.seq < targetSeq,
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function replacementRange(event: SessionEvent): { start: number; end: number } | undefined {
|
|
429
|
+
const op = (
|
|
430
|
+
event as unknown as { surfaceOp?: { op?: unknown; startSeq?: unknown; endSeq?: unknown } }
|
|
431
|
+
).surfaceOp;
|
|
432
|
+
if (op?.op !== "replace") return undefined;
|
|
433
|
+
if (typeof op.startSeq !== "number" || typeof op.endSeq !== "number") return undefined;
|
|
434
|
+
return { start: op.startSeq, end: op.endSeq };
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function numberList(value: unknown): number[] {
|
|
438
|
+
return Array.isArray(value)
|
|
439
|
+
? value.filter((item): item is number => typeof item === "number")
|
|
440
|
+
: [];
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// 把截断丢掉的压缩重排到 base 起的连续 seq:standalone 形态(不属于任何 turn,与手动 /compact 一致),
|
|
444
|
+
// 摘要替换引用的 seq 同步重编号(保留前缀内的 seq 不变),metering 事件与替换消息保持相邻。
|
|
445
|
+
export function restoreCompactions(
|
|
446
|
+
brackets: readonly CompactionBracket[],
|
|
447
|
+
keepFrom: number,
|
|
448
|
+
base: number,
|
|
449
|
+
): SessionEvent[] {
|
|
450
|
+
const moved = new Map<number, number>();
|
|
451
|
+
const resolve = (seq: number): number | undefined => (seq < keepFrom ? seq : moved.get(seq));
|
|
452
|
+
const resolveAll = (seqs: readonly number[]): number[] | undefined => {
|
|
453
|
+
const resolved: number[] = [];
|
|
454
|
+
for (const seq of seqs) {
|
|
455
|
+
const next = resolve(seq);
|
|
456
|
+
if (next === undefined) return undefined;
|
|
457
|
+
resolved.push(next);
|
|
458
|
+
}
|
|
459
|
+
return resolved;
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
const restored: SessionEvent[] = [];
|
|
463
|
+
for (const bracket of brackets) {
|
|
464
|
+
const range = replacementRange(bracket.checkpoint);
|
|
465
|
+
const shadowedSeqs = resolveAll(
|
|
466
|
+
numberList((bracket.summary?.data as { shadowedSeqs?: unknown } | undefined)?.shadowedSeqs),
|
|
467
|
+
);
|
|
468
|
+
const start = range === undefined ? undefined : resolve(range.start);
|
|
469
|
+
const end = range === undefined ? undefined : resolve(range.end);
|
|
470
|
+
// 引用了被丢弃事件的压缩无法重建替换范围,只能一并丢弃
|
|
471
|
+
if (start === undefined || end === undefined || shadowedSeqs === undefined) continue;
|
|
472
|
+
|
|
473
|
+
const at = base + restored.length;
|
|
474
|
+
const summary = bracket.summary;
|
|
475
|
+
const checkpointSeq = at + (summary === undefined ? 1 : 2);
|
|
476
|
+
moved.set(bracket.start.seq, at);
|
|
477
|
+
if (summary !== undefined) moved.set(summary.seq, at + 1);
|
|
478
|
+
moved.set(bracket.checkpoint.seq, checkpointSeq);
|
|
479
|
+
moved.set(bracket.end.seq, checkpointSeq + 1);
|
|
480
|
+
|
|
481
|
+
// 引用自身事务的事件按新位置重算:开标记、metering 事件、替换范围内的 surface 节点
|
|
482
|
+
const sources = [at, ...(summary === undefined ? [] : [at + 1]), ...shadowedSeqs];
|
|
483
|
+
restored.push(
|
|
484
|
+
standaloneCompaction(bracket.start, at),
|
|
485
|
+
...(summary === undefined
|
|
486
|
+
? []
|
|
487
|
+
: [meteringShadowPrice(summary, at + 1, { start, end }, shadowedSeqs)]),
|
|
488
|
+
replacementCheckpoint(bracket.checkpoint, checkpointSeq, { start, end }, sources),
|
|
489
|
+
standaloneCompaction(bracket.end, checkpointSeq + 1),
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
return restored;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function standaloneCompaction(event: SessionEvent, seq: number): SessionEvent {
|
|
496
|
+
return {
|
|
497
|
+
...event,
|
|
498
|
+
seq: SessionSeq(seq),
|
|
499
|
+
data: { ...event.data, turn: null },
|
|
500
|
+
} as unknown as SessionEvent;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function meteringShadowPrice(
|
|
504
|
+
event: SessionEvent,
|
|
505
|
+
seq: number,
|
|
506
|
+
range: { start: number; end: number },
|
|
507
|
+
shadowedSeqs: readonly number[],
|
|
508
|
+
): SessionEvent {
|
|
509
|
+
return {
|
|
510
|
+
...event,
|
|
511
|
+
seq: SessionSeq(seq),
|
|
512
|
+
data: {
|
|
513
|
+
...event.data,
|
|
514
|
+
shadowedRange: { start: range.start, end: range.end },
|
|
515
|
+
shadowedSeqs: [...shadowedSeqs],
|
|
516
|
+
},
|
|
517
|
+
} as unknown as SessionEvent;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function replacementCheckpoint(
|
|
521
|
+
event: SessionEvent,
|
|
522
|
+
seq: number,
|
|
523
|
+
range: { start: number; end: number },
|
|
524
|
+
sources: readonly number[],
|
|
525
|
+
): SessionEvent {
|
|
526
|
+
return {
|
|
527
|
+
...event,
|
|
528
|
+
seq: SessionSeq(seq),
|
|
529
|
+
surfaceOp: { op: "replace", startSeq: range.start, endSeq: range.end },
|
|
530
|
+
sourceEventSeqs: [...sources],
|
|
531
|
+
} as unknown as SessionEvent;
|
|
532
|
+
}
|
package/src/shared.ts
CHANGED
package/src/testing.ts
CHANGED
|
@@ -43,8 +43,7 @@ export async function harness(provide?: (ctx: Context) => void): Promise<Harness
|
|
|
43
43
|
await ctx.plugin(SessionStore);
|
|
44
44
|
new SessionProjectionRegistry(ctx);
|
|
45
45
|
const fiber = await ctx.plugin(SessionPersistenceSqlite, { type: "sqlite", path: ":memory:" });
|
|
46
|
-
|
|
47
|
-
// 就绪:构造函数内的 effect 只在服务可解析时注册路由。
|
|
46
|
+
|
|
48
47
|
provide?.(ctx);
|
|
49
48
|
await ctx.plugin(SessionEditor);
|
|
50
49
|
return { ctx, editor: ctx.sessionEditor, dispose: () => fiber.dispose() };
|
|
@@ -78,7 +77,6 @@ export async function createPersisted(
|
|
|
78
77
|
}
|
|
79
78
|
}
|
|
80
79
|
|
|
81
|
-
/** 构造一条 user/message 事件。 */
|
|
82
80
|
export function userMessage(seq: number, id: string, text: string, time = seq): SessionEvent {
|
|
83
81
|
return {
|
|
84
82
|
type: "user/message",
|
|
@@ -94,7 +92,6 @@ export function userMessage(seq: number, id: string, text: string, time = seq):
|
|
|
94
92
|
} as SessionEvent;
|
|
95
93
|
}
|
|
96
94
|
|
|
97
|
-
/** 构造一条 assistant/message 事件(纯文本回复)。 */
|
|
98
95
|
export function assistantMessage(
|
|
99
96
|
seq: number,
|
|
100
97
|
turn: number,
|
|
@@ -122,7 +119,6 @@ export function assistantMessage(
|
|
|
122
119
|
} as unknown as SessionEvent;
|
|
123
120
|
}
|
|
124
121
|
|
|
125
|
-
/** 真实 agent-loop 形状的一轮:轮首输入 + 可选 followup,可闭合。 */
|
|
126
122
|
export function turnLog(
|
|
127
123
|
base: number,
|
|
128
124
|
turn: number,
|