@morlay/ui-conversation-message-actions 0.1.1 → 0.1.3
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 +16 -29
- package/dist/client.cjs +12 -47
- package/dist/client.d.cts +3 -4
- package/dist/client.d.mts +3 -4
- package/dist/index.d.cts +6 -6
- package/dist/index.d.mts +6 -6
- package/dist/index.mjs +22 -13
- package/dist/invariant.d.cts +4 -5
- package/dist/invariant.d.mts +4 -5
- package/dist/{plan-BKgGPZdD.d.mts → plan-CRswvwkb.d.mts} +2 -1
- package/dist/{plan-BUTplrpz.d.cts → plan-DeCB5_cR.d.cts} +2 -1
- package/dist/plan.d.cts +2 -2
- package/dist/plan.d.mts +2 -2
- package/dist/plan.mjs +13 -1
- package/dist/rolldown-runtime-BMI-E3GI.mjs +44 -0
- package/dist/shared-DG86bLrc.mjs +11 -0
- package/dist/testing.d.cts +2041 -93
- package/dist/testing.d.mts +2040 -93
- package/dist/testing.mjs +92 -290
- package/package.json +6 -6
- package/src/client/chat-node/MessageItem.tsx +2 -4
- package/src/client/composer-stats/StatsPills.tsx +25 -50
- package/src/client/composer-stats/token-format.ts +2 -3
- package/src/client/controller.ts +15 -32
- package/src/index.ts +29 -15
- package/src/plan.ts +28 -0
- package/src/shared.ts +12 -1
- package/src/testing.ts +3 -2
- package/dist/shared-BBbOTwy4.mjs +0 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@morlay/ui-conversation-message-actions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Rewind / retry / recall / fork orchestration (host) plus the browser half — replaces the conversation.chat.node user / steering rendering and overrides the conversation.composer.dock stats row.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"conversation-chat-node",
|
|
@@ -38,12 +38,12 @@
|
|
|
38
38
|
"./locale/*.json": "./locale/*.json"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@morlay/session-branch": "0.1.
|
|
41
|
+
"@morlay/session-branch": "0.1.3"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@deepseek-ai/dsh-session-stats": "0.2.0-rc.1",
|
|
45
|
-
"@morlay/dsh-client-ui-primitives": "0.0.
|
|
46
|
-
"@morlay/session-rdb": "0.1.
|
|
45
|
+
"@morlay/dsh-client-ui-primitives": "0.0.6",
|
|
46
|
+
"@morlay/session-rdb": "0.1.3",
|
|
47
47
|
"@types/react": "~18.3.1",
|
|
48
48
|
"@types/react-dom": "~18.3.0",
|
|
49
49
|
"react": "^18.2.0",
|
|
@@ -67,8 +67,8 @@
|
|
|
67
67
|
"@deepseek-ai/dsh-session-persistence": "^0.2.0-rc.1",
|
|
68
68
|
"@deepseek-ai/dsh-session-projection": "^0.2.0-rc.1",
|
|
69
69
|
"@deepseek-ai/dsh-token-meter": "^0.2.0-rc.1",
|
|
70
|
-
"@morlay/dsh-client-ui-conversation": "0.1.
|
|
71
|
-
"@morlay/dsh-client-ui-primitives": "0.0.
|
|
70
|
+
"@morlay/dsh-client-ui-conversation": "0.1.3",
|
|
71
|
+
"@morlay/dsh-client-ui-primitives": "0.0.6"
|
|
72
72
|
},
|
|
73
73
|
"dsh": {
|
|
74
74
|
"client": {
|
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
// MessageItem:
|
|
2
|
-
//
|
|
3
|
-
// `user`/`steering` 两个 key(keyed slot reuse 即替换),在消息动作行上提供
|
|
4
|
-
// edit / retry(上游无此能力)。其余 key 由官方内置渲染器处理。
|
|
1
|
+
// MessageItem: 替换官方内置的 `user` / `steering` 两个 chat-node 渲染器(keyed slot reuse 即替换),
|
|
2
|
+
// 在消息动作行上提供 edit / retry;其余 key 由官方内置渲染器处理。
|
|
5
3
|
|
|
6
4
|
import {
|
|
7
5
|
markdownLabels,
|
|
@@ -1,11 +1,8 @@
|
|
|
1
|
-
// Session stats under the composer, split into two icon pills: a gauge pill
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// so the fixed token-format (out-of-range cacheRead no longer hangs) stays wired.
|
|
7
|
-
// `data-composer-stats` stays as this row's stable anchor (style/test hooks) — the InputBar it once
|
|
8
|
-
// paired with is upstream's again, so nothing tightens the composer clearance off it any more.
|
|
1
|
+
// Session stats under the composer, split into two icon pills: a gauge pill (turn/step counts + output speed)
|
|
2
|
+
// opening the time-and-speed dialog, and a database pill (total tokens + cache hit) opening the token-usage
|
|
3
|
+
// dialog. Settled-node identity prevents stream-delta updates from rerendering the row; this row shadows
|
|
4
|
+
// upstream ui-chat's `composer.dock` id `stats`(priority −1),`data-composer-stats` is its stable anchor
|
|
5
|
+
// (style / test hooks)。
|
|
9
6
|
|
|
10
7
|
import { memo, useMemo, useState } from "react";
|
|
11
8
|
import { createPortal } from "react-dom";
|
|
@@ -30,32 +27,24 @@ import { styles as dialogStyles } from "./stat-dialog.styles.ts";
|
|
|
30
27
|
interface WindowStats {
|
|
31
28
|
turns: number;
|
|
32
29
|
steps: number;
|
|
33
|
-
|
|
30
|
+
// Summed request wall time (step/start → assistant/message); 0 when no node carries timing.
|
|
34
31
|
llmMs: number;
|
|
35
|
-
|
|
32
|
+
// Summed tool wall time (tool/call → tool/result); 0 when no pair is in-window.
|
|
36
33
|
toolMs: number;
|
|
37
|
-
|
|
34
|
+
// Summed first-token latency over `ttftSteps`; 0 when no step records it.
|
|
38
35
|
ttftMs: number;
|
|
39
|
-
|
|
36
|
+
// Steps carrying a recorded TTFT.
|
|
40
37
|
ttftSteps: number;
|
|
41
|
-
|
|
38
|
+
// Summed decode wall time over steps that also report output tokens.
|
|
42
39
|
decodeMs: number;
|
|
43
|
-
|
|
40
|
+
// Summed output tokens over the same decode-timed steps.
|
|
44
41
|
decodeTokens: number;
|
|
45
42
|
}
|
|
46
43
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
* Every displayed figure rides that durable whole-log projection (and token
|
|
52
|
-
* accounting rides `tokenUsage`) because the window is paged and compaction
|
|
53
|
-
* rewrites it; this fold answers "what is on screen" only when no projection
|
|
54
|
-
* value is served. Its field names deliberately mirror the projection's so
|
|
55
|
-
* the two swap wholesale.
|
|
56
|
-
* @param nodes - snapshot nodes.
|
|
57
|
-
* @returns fallback counts and summed wall times.
|
|
58
|
-
*/
|
|
44
|
+
// Fold assistant and tool-result nodes into window-scoped display totals — the fallback for assemblies
|
|
45
|
+
// without the `sessionStats` projection. Every displayed figure otherwise rides that durable whole-log
|
|
46
|
+
// projection (token accounting rides `tokenUsage`), because the window is paged and compaction rewrites it;
|
|
47
|
+
// field names deliberately mirror the projection's so the two swap wholesale.
|
|
59
48
|
export function deriveStats(nodes: ChatSnapshot["legacy"]["nodes"]): WindowStats {
|
|
60
49
|
const turns = new Set<number>();
|
|
61
50
|
let steps = 0;
|
|
@@ -89,11 +78,7 @@ export function deriveStats(nodes: ChatSnapshot["legacy"]["nodes"]): WindowStats
|
|
|
89
78
|
return { turns: turns.size, steps, llmMs, toolMs, ttftMs, ttftSteps, decodeMs, decodeTokens };
|
|
90
79
|
}
|
|
91
80
|
|
|
92
|
-
|
|
93
|
-
* Compact duration: 45.2s under a minute, 2m42s from there on.
|
|
94
|
-
* @param ms - duration in milliseconds.
|
|
95
|
-
* @returns display string.
|
|
96
|
-
*/
|
|
81
|
+
// Compact duration: 45.2s under a minute, 2m42s from there on.
|
|
97
82
|
export function formatDuration(ms: number, t: ChatViewSlotProps["t"]): string {
|
|
98
83
|
const s = ms / 1_000;
|
|
99
84
|
if (s < 60) return t("duration.compactSeconds", { seconds: Math.round(s * 10) / 10 });
|
|
@@ -104,32 +89,24 @@ export function formatDuration(ms: number, t: ChatViewSlotProps["t"]): string {
|
|
|
104
89
|
});
|
|
105
90
|
}
|
|
106
91
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
* @returns integer text when integer rounding stays below 100, otherwise the
|
|
111
|
-
* minimum decimal precision that still rounds below 100; a full hit returns
|
|
112
|
-
* 100, and no billed input returns null.
|
|
113
|
-
*/
|
|
92
|
+
// Display-ready cache-hit share of prompt-side input over the whole durable log: integer text while integer
|
|
93
|
+
// rounding stays below 100, otherwise the minimum decimal precision that still rounds below 100; a full hit
|
|
94
|
+
// returns 100, and no billed input returns null.
|
|
114
95
|
export function cacheHitPercent(usage: TokenUsageProjection): string | null {
|
|
115
96
|
const denominator = billedInputTokens(usage);
|
|
116
97
|
return formatCacheHitPercent(usage.cacheReadTokens, denominator);
|
|
117
98
|
}
|
|
118
99
|
|
|
119
|
-
|
|
120
|
-
* Sum the three disjoint prompt-side billing buckets.
|
|
121
|
-
* @param usage - the session's token-usage projection value.
|
|
122
|
-
* @returns billed input tokens.
|
|
123
|
-
*/
|
|
100
|
+
// Sum the three disjoint prompt-side billing buckets.
|
|
124
101
|
export function billedInputTokens(usage: TokenUsageProjection): number {
|
|
125
102
|
return usage.uncachedInputTokens + usage.cacheReadTokens + usage.cacheWriteTokens;
|
|
126
103
|
}
|
|
127
104
|
|
|
128
|
-
|
|
105
|
+
// Props: the conversation-snapshot selector plus the projection read seat.
|
|
129
106
|
export interface StatsPillsProps {
|
|
130
107
|
useChat: SnapshotSelectorHook<ChatSnapshot>;
|
|
131
108
|
useProjection: UseProjection;
|
|
132
|
-
|
|
109
|
+
// The owning dock's locale seat.
|
|
133
110
|
t: ChatViewSlotProps["t"];
|
|
134
111
|
}
|
|
135
112
|
|
|
@@ -137,7 +114,7 @@ function exactCount(value: number, t: ChatViewSlotProps["t"]): string {
|
|
|
137
114
|
return t("message.turnUsage.count", { count: formatExactTokens(value, t) });
|
|
138
115
|
}
|
|
139
116
|
|
|
140
|
-
|
|
117
|
+
// External open state one pill's dialog reads and writes (the row's exclusive slot).
|
|
141
118
|
type PillDialog = Pick<ReturnType<typeof useStatDialog>, "open" | "setOpen">;
|
|
142
119
|
|
|
143
120
|
function TimePill({
|
|
@@ -345,10 +322,8 @@ export const StatsPills = memo(function StatsPills({ useChat, useProjection, t }
|
|
|
345
322
|
const usage = useProjection("tokenUsage");
|
|
346
323
|
// One exclusive slot for both dialogs: opening either pill closes the other.
|
|
347
324
|
const [openPill, setOpenPill] = useState<"time" | "usage" | null>(null);
|
|
348
|
-
// Every figure rides the durable sessionStats projection, so paging and
|
|
349
|
-
//
|
|
350
|
-
// back to the window-scoped fold wholesale (same field names), paid only
|
|
351
|
-
// while no projection value is served.
|
|
325
|
+
// Every figure rides the durable sessionStats projection, so paging and compaction cannot change any of
|
|
326
|
+
// them; an assembly without the unit falls back to the window-scoped fold wholesale (same field names).
|
|
352
327
|
const projected = useProjection("sessionStats");
|
|
353
328
|
const stats = useMemo(() => projected ?? deriveStats(settledNodes), [projected, settledNodes]);
|
|
354
329
|
// Gated on actual token activity: a session whose steps all settled without
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
// Token
|
|
2
|
-
//
|
|
3
|
-
// cacheRead 超出 prompt 时展开循环的缩放间隙恒为负 → 同步死循环冻结主线程)。
|
|
1
|
+
// Token 展示口径:`formatCacheHitPercent` 的越界分支是**主动修的缺陷**(上游只在未命中侧为 0 时返回 "100",
|
|
2
|
+
// cacheRead 超出 prompt 时展开循环的缩放间隙恒为负 → 同步死循环冻结主线程),这里保持修好的口径。
|
|
4
3
|
import type { ChatViewSlotProps } from "@deepseek-ai/dsh-client-ui-chat/client";
|
|
5
4
|
|
|
6
5
|
export function formatTokens(value: number, t: ChatViewSlotProps["t"]): string {
|
package/src/client/controller.ts
CHANGED
|
@@ -2,23 +2,14 @@ import type { Context as ClientContext } from "@deepseek-ai/cordis";
|
|
|
2
2
|
import type { ISessions } from "@deepseek-ai/dsh-api-session-controller/client";
|
|
3
3
|
import type { SessionId } from "@deepseek-ai/dsh-session";
|
|
4
4
|
import {
|
|
5
|
-
|
|
5
|
+
SESSION_EDITOR_PATHS,
|
|
6
6
|
type EditableMessageBlock,
|
|
7
7
|
type SessionEditorOperation,
|
|
8
8
|
type VersionOperation,
|
|
9
9
|
} from "../shared.ts";
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* 会话编辑的浏览器半门面:只保留消息渲染面真正用到的两个动作。
|
|
16
|
-
*
|
|
17
|
-
* 曾经这里还有一条「订阅会话列表 / 快照 → 拉全量 timeline → 装进 store」的刷新管路,
|
|
18
|
-
* 它没有任何 UI 消费方,却把重放 / 流式期间的每一次快照抖动都换成一次全量 GET
|
|
19
|
-
* (编辑 / 重试 / 撤回后尤甚),订阅本身也从不释放。现在动作成功后只刷新会话列表元数据,
|
|
20
|
-
* 会话窗口交给上游的事件流收敛(不重建窗口、不整页重载)。
|
|
21
|
-
*/
|
|
11
|
+
// 会话编辑的浏览器半门面:只保留消息渲染面真正用到的两个动作;动作成功后只刷新会话列表元数据,
|
|
12
|
+
// 会话窗口交给上游的事件流收敛(不重建窗口、不整页重载)。
|
|
22
13
|
export interface SessionEditorFace {
|
|
23
14
|
// 属性式函数类型而非方法签名:这两动作要被渲染面解构后直接调用,没有 `this` 可言。
|
|
24
15
|
retry: (turn: number, cascade: "truncate" | "preserve") => Promise<boolean>;
|
|
@@ -64,10 +55,11 @@ export class SessionEditorController {
|
|
|
64
55
|
if (this.pending !== null) return false;
|
|
65
56
|
this.pending = operation.action;
|
|
66
57
|
try {
|
|
67
|
-
const
|
|
58
|
+
const { action, ...payload } = operation;
|
|
59
|
+
const response = await fetch(SESSION_EDITOR_PATHS[action], {
|
|
68
60
|
method: "POST",
|
|
69
61
|
headers: { accept: "application/json", "content-type": "application/json" },
|
|
70
|
-
body: JSON.stringify(
|
|
62
|
+
body: JSON.stringify(payload),
|
|
71
63
|
});
|
|
72
64
|
const value = (await response.json()) as unknown;
|
|
73
65
|
if (!response.ok) {
|
|
@@ -79,10 +71,9 @@ export class SessionEditorController {
|
|
|
79
71
|
|
|
80
72
|
onApplied?.();
|
|
81
73
|
|
|
82
|
-
// 会话窗口必须重建:rewind 把日志 seq
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
// 都拿不到时记一条 warn 便于诊断,且不回退整页重载(刷新会打断用户)。
|
|
74
|
+
// 会话窗口必须重建:rewind 把日志 seq 回退了,窗口里已渲染的旧节点不会自己消失(被裁剪的消息残留、
|
|
75
|
+
// 新消息也追加不进去)。重建入口只在**运行时**对象上(`resync()`,类型面 `SessionFace` 未暴露),所以
|
|
76
|
+
// 逐个探测;都拿不到时记一条 warn 便于诊断,不回退整页重载。
|
|
86
77
|
try {
|
|
87
78
|
await this.rebuildWindow();
|
|
88
79
|
} catch (error: unknown) {
|
|
@@ -96,12 +87,9 @@ export class SessionEditorController {
|
|
|
96
87
|
}
|
|
97
88
|
}
|
|
98
89
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
* 探测顺序:`binding(id).session.resync()`(上游 ClientSession 的重建入口,类型面没暴露)
|
|
103
|
-
* → `sessions.refresh()`(只刷列表元数据,聊胜于无)→ 都没有就记一条 warn。
|
|
104
|
-
*/
|
|
90
|
+
// 让上游重开该会话的历史窗口(rewind 之后客户端窗口的 seq 基线已经失效):探测顺序
|
|
91
|
+
// `binding(id).session.resync()`(上游 ClientSession 的重建入口,类型面没暴露)→ `sessions.refresh()`
|
|
92
|
+
// (只刷列表元数据)→ 都没有就记一条 warn。
|
|
105
93
|
private async rebuildWindow(): Promise<void> {
|
|
106
94
|
const sessions = this.sessions as unknown as {
|
|
107
95
|
binding?: (id: SessionId) => { session?: { resync?: () => Promise<void> } } | undefined;
|
|
@@ -122,14 +110,9 @@ export class SessionEditorController {
|
|
|
122
110
|
);
|
|
123
111
|
}
|
|
124
112
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
* 是改完再发,所以这里无条件送到底。容器用上游既有契约 `[data-conversation-scroll]`
|
|
129
|
-
* (ChatView / ConversationWidthControls / StatsPills 同用)。
|
|
130
|
-
*
|
|
131
|
-
* 窗口替换的渲染落在 resync 之后:写早了会被下一轮布局覆盖,因此等两帧再写。
|
|
132
|
-
*/
|
|
113
|
+
// 撤回后把会话视口送回底部:rewind 把窗口换短,而上游 ChatView 只在读者已经贴底时才跟随
|
|
114
|
+
// (`tipMoved && atBottom`),停在中途的视口会悬在被截断的位置上;撤回的下一步是改完再发,所以这里无条件
|
|
115
|
+
// 送到底。容器用上游既有契约 `[data-conversation-scroll]`;窗口替换的渲染落在 resync 之后,因此等两帧再写。
|
|
133
116
|
private returnViewportToEnd(): void {
|
|
134
117
|
const scrollport = document.querySelector<HTMLElement>("[data-conversation-scroll]");
|
|
135
118
|
if (scrollport === null) return;
|
package/src/index.ts
CHANGED
|
@@ -22,7 +22,8 @@ import type {
|
|
|
22
22
|
SessionEditorResult,
|
|
23
23
|
} from "./types.ts";
|
|
24
24
|
import {
|
|
25
|
-
|
|
25
|
+
SESSION_EDITOR_PATHS,
|
|
26
|
+
type SessionEditorAction,
|
|
26
27
|
type SessionEditorOperation,
|
|
27
28
|
type SessionEditorOperationResult,
|
|
28
29
|
} from "./shared.ts";
|
|
@@ -30,6 +31,7 @@ import {
|
|
|
30
31
|
closedTurns,
|
|
31
32
|
droppedCompactions,
|
|
32
33
|
editPlan,
|
|
34
|
+
keptConfigEvents,
|
|
33
35
|
precedingContentIndex,
|
|
34
36
|
recallBoundary,
|
|
35
37
|
restoreCompactions,
|
|
@@ -259,6 +261,8 @@ export class SessionEditor extends Service {
|
|
|
259
261
|
// 补回被截断丢掉、但在重放目标之前的压缩
|
|
260
262
|
const keepFrom = keepFromOf(events, boundary);
|
|
261
263
|
const compactions = droppedCompactions(events, keepFrom, plan.targetSeq);
|
|
264
|
+
// 同理保留这之前的配置事件(它们表达的是当前生效的配置)
|
|
265
|
+
const configs = keptConfigEvents(events, keepFrom, plan.targetSeq);
|
|
262
266
|
|
|
263
267
|
const replayTurn = preceding < 0 ? 1 : turns[preceding]!.turn + 1;
|
|
264
268
|
|
|
@@ -281,6 +285,7 @@ export class SessionEditor extends Service {
|
|
|
281
285
|
const seedStart = this.seedStart(live, events, keepFrom);
|
|
282
286
|
const seedSuffix: SessionEvent[] = [
|
|
283
287
|
...restoreCompactions(compactions, keepFrom, seedStart),
|
|
288
|
+
...configs,
|
|
284
289
|
...manualSeed,
|
|
285
290
|
];
|
|
286
291
|
await this.appendSeedSuffix(operation.sessionId, live, seedSuffix, events, keepFrom);
|
|
@@ -311,9 +316,10 @@ export class SessionEditor extends Service {
|
|
|
311
316
|
signal?.throwIfAborted();
|
|
312
317
|
const events = await this.readEvents(operation.sessionId, signal);
|
|
313
318
|
const boundary = recallBoundary(events, closedTurns(events), operation.eventSeq);
|
|
314
|
-
//
|
|
319
|
+
// 撤回同样要补回被丢掉、但在撤回目标之前的压缩与配置事件
|
|
315
320
|
const keepFrom = keepFromOf(events, boundary);
|
|
316
321
|
const compactions = droppedCompactions(events, keepFrom, operation.eventSeq);
|
|
322
|
+
const configs = keptConfigEvents(events, keepFrom, operation.eventSeq);
|
|
317
323
|
|
|
318
324
|
await this.stopLoop(operation.sessionId, signal);
|
|
319
325
|
const live = this.ctx.sessions.get(operation.sessionId);
|
|
@@ -321,7 +327,10 @@ export class SessionEditor extends Service {
|
|
|
321
327
|
await this.appendSeedSuffix(
|
|
322
328
|
operation.sessionId,
|
|
323
329
|
live,
|
|
324
|
-
|
|
330
|
+
[
|
|
331
|
+
...restoreCompactions(compactions, keepFrom, this.seedStart(live, events, keepFrom)),
|
|
332
|
+
...configs,
|
|
333
|
+
],
|
|
325
334
|
events,
|
|
326
335
|
keepFrom,
|
|
327
336
|
);
|
|
@@ -491,10 +500,10 @@ function cascadeOf(value: unknown): import("@morlay/session-branch").CascadePoli
|
|
|
491
500
|
return value;
|
|
492
501
|
}
|
|
493
502
|
|
|
494
|
-
function decodeOperation(value: unknown): SessionEditorOperation {
|
|
503
|
+
function decodeOperation(value: unknown, action: SessionEditorAction): SessionEditorOperation {
|
|
495
504
|
const record = objectValue(value);
|
|
496
505
|
const sessionId = sessionIdOf(record["sessionId"]);
|
|
497
|
-
switch (
|
|
506
|
+
switch (action) {
|
|
498
507
|
case "edit":
|
|
499
508
|
if (typeof record["text"] !== "string") throw new TypeError("text 必须是字符串。");
|
|
500
509
|
return {
|
|
@@ -621,6 +630,7 @@ async function runOperation(
|
|
|
621
630
|
|
|
622
631
|
async function handleRoute(
|
|
623
632
|
editor: SessionEditor,
|
|
633
|
+
action: SessionEditorAction,
|
|
624
634
|
request: HttpRequestLike,
|
|
625
635
|
response: HttpResponseLike,
|
|
626
636
|
): Promise<void> {
|
|
@@ -629,7 +639,7 @@ async function handleRoute(
|
|
|
629
639
|
respondJson(
|
|
630
640
|
response,
|
|
631
641
|
200,
|
|
632
|
-
await runOperation(editor, decodeOperation(await requestJson(request))),
|
|
642
|
+
await runOperation(editor, decodeOperation(await requestJson(request), action)),
|
|
633
643
|
);
|
|
634
644
|
return;
|
|
635
645
|
}
|
|
@@ -651,14 +661,18 @@ function registerHttpRoutes(ctx: Context, editor: SessionEditor): void {
|
|
|
651
661
|
// 这里只按用到的 register 面做结构转换,不再 declare module 覆盖,否则两处声明冲突(TS2717)。
|
|
652
662
|
const webServer = scope.get("webServer") as unknown as HttpServerLike | undefined;
|
|
653
663
|
if (webServer === undefined) return;
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
664
|
+
// 一条动作一条 exact 路由:动作由路径定,body 里不带 `action`(见 shared.ts 的 SESSION_EDITOR_PATHS)。
|
|
665
|
+
for (const [action, path] of Object.entries(SESSION_EDITOR_PATHS)) {
|
|
666
|
+
const verb = action as SessionEditorAction;
|
|
667
|
+
scope.effect(
|
|
668
|
+
() =>
|
|
669
|
+
webServer.register({
|
|
670
|
+
kind: "exact",
|
|
671
|
+
path,
|
|
672
|
+
handler: (request, response) => handleRoute(editor, verb, request, response),
|
|
673
|
+
}),
|
|
674
|
+
`session-editor: HTTP route ${path}`,
|
|
675
|
+
);
|
|
676
|
+
}
|
|
663
677
|
});
|
|
664
678
|
}
|
package/src/plan.ts
CHANGED
|
@@ -171,6 +171,8 @@ export function downstreamUsers(turns: readonly ClosedTurn[], start: number): Us
|
|
|
171
171
|
.flatMap((turn): UserMessage[] => turn.users.map((user) => cloneUser(user.data)));
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
+
// 同轮内第一条 user 退到前一轮的 turn/end(撤回整轮):留着本轮的 turn/start,之后的重放会按
|
|
175
|
+
// `phase.turn + 1` 再开一轮、本轮成空轮。该轮里这条消息之前写下的配置事件由 recallOperation 重写保留。
|
|
174
176
|
export function recallBoundary(
|
|
175
177
|
events: readonly SessionEvent[],
|
|
176
178
|
turns: readonly ClosedTurn[],
|
|
@@ -243,6 +245,9 @@ export function editPlan(operation: EditOperation, turns: readonly ClosedTurn[])
|
|
|
243
245
|
return {
|
|
244
246
|
anchorSeq: turn.startSeq,
|
|
245
247
|
|
|
248
|
+
// 轮内第一条 user 退到前一轮的 turn/end(整轮重放):留着本轮的 turn/start,上游重放会按
|
|
249
|
+
// `phase.turn + 1` 再开一轮,本轮就成了空轮(agent-loop 的 turn 推进不看未闭合轮)。
|
|
250
|
+
// 该轮里这条消息之前写下的配置事件由 branchOperation 重写保留。
|
|
246
251
|
...(userIndex === 0 ? {} : { rewindBoundary: event.seq }),
|
|
247
252
|
queuedUsers: [edited, ...sameTurnFollowups, ...later],
|
|
248
253
|
targetSeq: event.seq,
|
|
@@ -373,6 +378,29 @@ export function droppedCompactions(
|
|
|
373
378
|
);
|
|
374
379
|
}
|
|
375
380
|
|
|
381
|
+
// 会话级配置事件:写入即「最后一条生效」,与某一轮的正文无关(分类同 session-rdb 的事件 schema)。
|
|
382
|
+
const CONFIG_EVENT_TYPES: ReadonlySet<string> = new Set([
|
|
383
|
+
"model/selection",
|
|
384
|
+
"permission/preset",
|
|
385
|
+
"approval/policy",
|
|
386
|
+
"sandbox/mode",
|
|
387
|
+
"plan/mode",
|
|
388
|
+
"agent-preset/selected",
|
|
389
|
+
"session-mode/selected",
|
|
390
|
+
]);
|
|
391
|
+
|
|
392
|
+
// 截断丢掉、但位于目标消息之前的配置事件:撤回整轮时它们要重写回保留前缀之后——否则当前生效的配置
|
|
393
|
+
// 跟着撤回了。轮内后续 user 消息的边界不退整轮,这个区间自然为空(那些事件本来就在保留前缀里)。
|
|
394
|
+
export function keptConfigEvents(
|
|
395
|
+
events: readonly SessionEvent[],
|
|
396
|
+
keepFrom: number,
|
|
397
|
+
targetSeq: number,
|
|
398
|
+
): SessionEvent[] {
|
|
399
|
+
return events.filter(
|
|
400
|
+
(event) => event.seq >= keepFrom && event.seq < targetSeq && CONFIG_EVENT_TYPES.has(event.type),
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
|
|
376
404
|
function replacementRange(event: SessionEvent): { start: number; end: number } | undefined {
|
|
377
405
|
const op = (
|
|
378
406
|
event as unknown as { surfaceOp?: { op?: unknown; startSeq?: unknown; endSeq?: unknown } }
|
package/src/shared.ts
CHANGED
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
import type { SessionId } from "@deepseek-ai/dsh-session";
|
|
2
2
|
import type { CascadePolicy } from "@morlay/session-branch";
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
// 会话编辑面的六条 POST 路径:一条动作一条路径(路径定动作,body 只带该动作的参数)。
|
|
5
|
+
// 与上游官方路由分开的理由见 ADR-管理面HTTP路由前缀与上游分家。
|
|
6
|
+
export const SESSION_EDITOR_PATHS = {
|
|
7
|
+
edit: "/api/morlay/v1/session/edit",
|
|
8
|
+
reroll: "/api/morlay/v1/session/reroll",
|
|
9
|
+
retry: "/api/morlay/v1/session/retry",
|
|
10
|
+
rewind: "/api/morlay/v1/session/rewind",
|
|
11
|
+
recall: "/api/morlay/v1/session/recall",
|
|
12
|
+
fork: "/api/morlay/v1/session/fork",
|
|
13
|
+
} as const;
|
|
14
|
+
|
|
15
|
+
export type SessionEditorAction = keyof typeof SESSION_EDITOR_PATHS;
|
|
5
16
|
|
|
6
17
|
export type { VersionOperation } from "@morlay/session-branch";
|
|
7
18
|
|
package/src/testing.ts
CHANGED
|
@@ -12,11 +12,11 @@ import SessionProjectionRegistry from "@deepseek-ai/dsh-session-projection";
|
|
|
12
12
|
import SessionPersistenceSqlite from "@morlay/session-rdb";
|
|
13
13
|
import { parseJsonlArtifact } from "@morlay/session-rdb/artifact";
|
|
14
14
|
import { meta, oneTurnLog } from "@morlay/session-rdb/testing";
|
|
15
|
-
import {
|
|
15
|
+
import { SESSION_EDITOR_PATHS } from "./shared.ts";
|
|
16
16
|
import { SessionEditor } from "@morlay/ui-conversation-message-actions";
|
|
17
17
|
|
|
18
18
|
export {
|
|
19
|
-
|
|
19
|
+
SESSION_EDITOR_PATHS,
|
|
20
20
|
Session,
|
|
21
21
|
SessionIdBrand,
|
|
22
22
|
SessionSeq,
|
|
@@ -26,6 +26,7 @@ export {
|
|
|
26
26
|
oneTurnLog,
|
|
27
27
|
parseJsonlArtifact,
|
|
28
28
|
};
|
|
29
|
+
export type { SessionEditorAction } from "./shared.ts";
|
|
29
30
|
export type { SessionEvent, SessionHeader };
|
|
30
31
|
|
|
31
32
|
export interface Harness {
|
package/dist/shared-BBbOTwy4.mjs
DELETED