@morlay/ui-conversation-message-actions 0.0.19-alpha.5 → 0.0.19-alpha.7
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/dist/client.cjs +39 -64
- package/dist/index.mjs +8 -9
- package/dist/testing.d.cts +203 -414
- package/dist/testing.d.mts +203 -414
- package/dist/testing.mjs +2160 -825
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +27 -25
- package/src/client/chat-node/MessageIconActions.tsx +12 -56
- package/src/client/chat-node/message-chrome.ts +0 -15
- package/src/client/composer-stats/StatsPills.tsx +10 -10
- package/src/client/controller.ts +31 -5
- package/src/index.ts +17 -12
- package/src/plan.ts +1 -1
- package/src/testing.ts +2 -4
package/locale/en.json
ADDED
package/locale/zh.json
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@morlay/ui-conversation-message-actions",
|
|
3
|
-
"version": "0.0.19-alpha.
|
|
3
|
+
"version": "0.0.19-alpha.7",
|
|
4
4
|
"description": "Rewind / retry / fork orchestration (host) plus conversation.chat.node renderer replacement (browser) — message edit / retry actions on user bubbles.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"conversation-chat-node",
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"url": "https://github.com/morlay/dsh-plugin.git"
|
|
20
20
|
},
|
|
21
21
|
"files": [
|
|
22
|
+
"locale/*.json",
|
|
22
23
|
"dist",
|
|
23
24
|
"src",
|
|
24
25
|
"cordis.patch.yml",
|
|
@@ -35,40 +36,41 @@
|
|
|
35
36
|
"./client": {
|
|
36
37
|
"types": "./dist/client.d.cts",
|
|
37
38
|
"default": "./dist/client.cjs"
|
|
38
|
-
}
|
|
39
|
+
},
|
|
40
|
+
"./locale/*.json": "./locale/*.json"
|
|
39
41
|
},
|
|
40
42
|
"dependencies": {
|
|
41
|
-
"@morlay/session-branch": "0.0.10-alpha.
|
|
43
|
+
"@morlay/session-branch": "0.0.10-alpha.7"
|
|
42
44
|
},
|
|
43
45
|
"devDependencies": {
|
|
44
|
-
"@deepseek-ai/dsh-session-stats": "0.1.
|
|
45
|
-
"@morlay/dsh-client-ui-primitives": "0.0.2-alpha.
|
|
46
|
-
"@morlay/session-rdb": "0.0.21-alpha.
|
|
46
|
+
"@deepseek-ai/dsh-session-stats": "0.1.7-alpha.2",
|
|
47
|
+
"@morlay/dsh-client-ui-primitives": "0.0.2-alpha.7",
|
|
48
|
+
"@morlay/session-rdb": "0.0.21-alpha.7",
|
|
47
49
|
"@types/react": "~18.3.1",
|
|
48
50
|
"@types/react-dom": "~18.3.0",
|
|
49
51
|
"react": "^18.2.0",
|
|
50
52
|
"react-dom": "^18.2.0"
|
|
51
53
|
},
|
|
52
54
|
"peerDependencies": {
|
|
53
|
-
"@deepseek-ai/cordis": "4.0.
|
|
54
|
-
"@deepseek-ai/dsh-api-session-controller": "0.1.
|
|
55
|
-
"@deepseek-ai/dsh-attachment": "0.1.
|
|
56
|
-
"@deepseek-ai/dsh-client-connection": "0.1.
|
|
57
|
-
"@deepseek-ai/dsh-client-store": "0.1.
|
|
58
|
-
"@deepseek-ai/dsh-client-ui-attachment": "0.1.
|
|
59
|
-
"@deepseek-ai/dsh-client-ui-chat": "0.1.
|
|
60
|
-
"@deepseek-ai/dsh-client-ui-primitives": "0.1.
|
|
61
|
-
"@deepseek-ai/dsh-client-ui-renderer": "0.1.
|
|
62
|
-
"@deepseek-ai/dsh-client-ui-slots": "0.1.
|
|
63
|
-
"@deepseek-ai/dsh-client-ui-workspace": "0.1.
|
|
64
|
-
"@deepseek-ai/dsh-invariants": "0.1.
|
|
65
|
-
"@deepseek-ai/dsh-llm": "0.1.
|
|
66
|
-
"@deepseek-ai/dsh-session": "0.1.
|
|
67
|
-
"@deepseek-ai/dsh-session-persistence": "0.1.
|
|
68
|
-
"@deepseek-ai/dsh-session-projection": "0.1.
|
|
69
|
-
"@deepseek-ai/dsh-token-meter": "0.1.
|
|
70
|
-
"@morlay/dsh-client-ui-conversation": "0.0.2-alpha.
|
|
71
|
-
"@morlay/dsh-client-ui-primitives": "0.0.2-alpha.
|
|
55
|
+
"@deepseek-ai/cordis": "4.0.4",
|
|
56
|
+
"@deepseek-ai/dsh-api-session-controller": "0.1.7-alpha.2",
|
|
57
|
+
"@deepseek-ai/dsh-attachment": "0.1.7-alpha.2",
|
|
58
|
+
"@deepseek-ai/dsh-client-connection": "0.1.7-alpha.2",
|
|
59
|
+
"@deepseek-ai/dsh-client-store": "0.1.7-alpha.2",
|
|
60
|
+
"@deepseek-ai/dsh-client-ui-attachment": "0.1.7-alpha.2",
|
|
61
|
+
"@deepseek-ai/dsh-client-ui-chat": "0.1.7-alpha.2",
|
|
62
|
+
"@deepseek-ai/dsh-client-ui-primitives": "0.1.7-alpha.2",
|
|
63
|
+
"@deepseek-ai/dsh-client-ui-renderer": "0.1.7-alpha.2",
|
|
64
|
+
"@deepseek-ai/dsh-client-ui-slots": "0.1.7-alpha.2",
|
|
65
|
+
"@deepseek-ai/dsh-client-ui-workspace": "0.1.7-alpha.2",
|
|
66
|
+
"@deepseek-ai/dsh-invariants": "0.1.7-alpha.2",
|
|
67
|
+
"@deepseek-ai/dsh-llm": "0.1.7-alpha.2",
|
|
68
|
+
"@deepseek-ai/dsh-session": "0.1.7-alpha.2",
|
|
69
|
+
"@deepseek-ai/dsh-session-persistence": "0.1.7-alpha.2",
|
|
70
|
+
"@deepseek-ai/dsh-session-projection": "0.1.7-alpha.2",
|
|
71
|
+
"@deepseek-ai/dsh-token-meter": "0.1.7-alpha.2",
|
|
72
|
+
"@morlay/dsh-client-ui-conversation": "0.0.2-alpha.7",
|
|
73
|
+
"@morlay/dsh-client-ui-primitives": "0.0.2-alpha.7"
|
|
72
74
|
},
|
|
73
75
|
"dsh": {
|
|
74
76
|
"bundle": {
|
|
@@ -4,21 +4,16 @@
|
|
|
4
4
|
import { styling } from "@morlay/dsh-client-ui-primitives/client";
|
|
5
5
|
import { useCallback, useEffect, useId, useRef, useState, type ReactNode } from "react";
|
|
6
6
|
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
IconBranchOutlineRegular,
|
|
8
|
+
IconCheckOutlineRegular,
|
|
9
|
+
IconCopyOutlineRegular,
|
|
10
|
+
IconEditOutlineRegular,
|
|
11
|
+
IconRefreshOutlineRegular,
|
|
12
12
|
Tooltip,
|
|
13
13
|
writeClipboard,
|
|
14
14
|
} from "@deepseek-ai/dsh-client-ui-primitives";
|
|
15
15
|
import type { ChatViewSlotProps } from "@deepseek-ai/dsh-client-ui-chat/client";
|
|
16
|
-
import {
|
|
17
|
-
formatLatencySeconds,
|
|
18
|
-
formatMessageClock,
|
|
19
|
-
formatRunDuration,
|
|
20
|
-
formatTokensPerSecond,
|
|
21
|
-
} from "./message-chrome.ts";
|
|
16
|
+
import { formatMessageClock } from "./message-chrome.ts";
|
|
22
17
|
import { useCalendarDay } from "./use-calendar-day.ts";
|
|
23
18
|
import { styles } from "./MessageIconActions.styles.ts";
|
|
24
19
|
|
|
@@ -27,12 +22,6 @@ export interface MessageIconActionsProps {
|
|
|
27
22
|
|
|
28
23
|
time?: number | undefined;
|
|
29
24
|
|
|
30
|
-
runMs?: number | undefined;
|
|
31
|
-
|
|
32
|
-
ttftMs?: number | undefined;
|
|
33
|
-
|
|
34
|
-
tokensPerSecond?: number | undefined;
|
|
35
|
-
|
|
36
25
|
clock: "start" | "end";
|
|
37
26
|
|
|
38
27
|
onBranch?: (() => void) | undefined;
|
|
@@ -53,9 +42,6 @@ export interface MessageIconActionsProps {
|
|
|
53
42
|
export function MessageIconActions({
|
|
54
43
|
text,
|
|
55
44
|
time,
|
|
56
|
-
runMs,
|
|
57
|
-
ttftMs,
|
|
58
|
-
tokensPerSecond,
|
|
59
45
|
clock,
|
|
60
46
|
onBranch,
|
|
61
47
|
branchUnavailable = false,
|
|
@@ -96,10 +82,8 @@ export function MessageIconActions({
|
|
|
96
82
|
}, 1000);
|
|
97
83
|
});
|
|
98
84
|
}, [copied, text]);
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
// run-on string ("Ran for 13sTime to first token (TTFT) 0.2s12 tok/s")
|
|
102
|
-
// instead of three facts.
|
|
85
|
+
// 只剩时钟:耗时 / TTFT / tok-s 的读数在 0.1.7 已不属于消息 chrome(上游把它们收进
|
|
86
|
+
// 会话统计弹窗与轮次过程节点),上游那套聊天文案表也不再提供这三个 key。
|
|
103
87
|
const clockEl =
|
|
104
88
|
time === undefined ? null : (
|
|
105
89
|
<span
|
|
@@ -107,34 +91,6 @@ export function MessageIconActions({
|
|
|
107
91
|
{...styling.props(clock === "start" ? styles.timeStart : styles.timeEnd)}
|
|
108
92
|
>
|
|
109
93
|
{formatMessageClock(time, t, day)}
|
|
110
|
-
{runMs !== undefined && (
|
|
111
|
-
<>
|
|
112
|
-
{" "}
|
|
113
|
-
<span {...styling.props(styles.runTimeDot)} aria-hidden>
|
|
114
|
-
·
|
|
115
|
-
</span>{" "}
|
|
116
|
-
{t("message.ranFor", { duration: formatRunDuration(runMs, t) })}
|
|
117
|
-
</>
|
|
118
|
-
)}
|
|
119
|
-
{ttftMs !== undefined && (
|
|
120
|
-
<>
|
|
121
|
-
{" "}
|
|
122
|
-
<span {...styling.props(styles.runTimeDot)} aria-hidden>
|
|
123
|
-
·
|
|
124
|
-
</span>{" "}
|
|
125
|
-
{t("message.turnTime.ttft")}{" "}
|
|
126
|
-
{t("duration.seconds", { seconds: formatLatencySeconds(ttftMs) })}
|
|
127
|
-
</>
|
|
128
|
-
)}
|
|
129
|
-
{tokensPerSecond !== undefined && (
|
|
130
|
-
<>
|
|
131
|
-
{" "}
|
|
132
|
-
<span {...styling.props(styles.runTimeDot)} aria-hidden>
|
|
133
|
-
·
|
|
134
|
-
</span>{" "}
|
|
135
|
-
{t("message.tokensPerSecond", { tps: formatTokensPerSecond(tokensPerSecond) })}
|
|
136
|
-
</>
|
|
137
|
-
)}
|
|
138
94
|
</span>
|
|
139
95
|
);
|
|
140
96
|
// 外部 className 需要拼接:给 actions 一个真实类名(而不是 data-css 属性)。
|
|
@@ -152,7 +108,7 @@ export function MessageIconActions({
|
|
|
152
108
|
aria-label={copied ? t("copied" as never) : t("copy" as never)}
|
|
153
109
|
onClick={onCopy}
|
|
154
110
|
>
|
|
155
|
-
{copied ? <
|
|
111
|
+
{copied ? <IconCheckOutlineRegular /> : <IconCopyOutlineRegular />}
|
|
156
112
|
</button>
|
|
157
113
|
</Tooltip>
|
|
158
114
|
{extraActions}
|
|
@@ -164,7 +120,7 @@ export function MessageIconActions({
|
|
|
164
120
|
aria-label="编辑"
|
|
165
121
|
onClick={onEdit}
|
|
166
122
|
>
|
|
167
|
-
<
|
|
123
|
+
<IconEditOutlineRegular />
|
|
168
124
|
</button>
|
|
169
125
|
</Tooltip>
|
|
170
126
|
)}
|
|
@@ -176,7 +132,7 @@ export function MessageIconActions({
|
|
|
176
132
|
aria-label="重试此回合"
|
|
177
133
|
onClick={onRetry}
|
|
178
134
|
>
|
|
179
|
-
<
|
|
135
|
+
<IconRefreshOutlineRegular />
|
|
180
136
|
</button>
|
|
181
137
|
</Tooltip>
|
|
182
138
|
)}
|
|
@@ -195,7 +151,7 @@ export function MessageIconActions({
|
|
|
195
151
|
data-unavailable={branchUnavailable || undefined}
|
|
196
152
|
onClick={branchUnavailable ? undefined : onBranch}
|
|
197
153
|
>
|
|
198
|
-
<
|
|
154
|
+
<IconBranchOutlineRegular />
|
|
199
155
|
</button>
|
|
200
156
|
</Tooltip>
|
|
201
157
|
)}
|
|
@@ -2,7 +2,6 @@ import type { Translate } from "@deepseek-ai/dsh-client-ui-slots";
|
|
|
2
2
|
|
|
3
3
|
export type ClockTranslate = Translate<"clock.md" | "clock.ymd">;
|
|
4
4
|
|
|
5
|
-
export type RunDurationTranslate = Translate<"duration.seconds" | "duration.minutes">;
|
|
6
5
|
function pad2(n: number): string {
|
|
7
6
|
return String(n).padStart(2, "0");
|
|
8
7
|
}
|
|
@@ -19,20 +18,6 @@ export function msUntilNextLocalMidnight(ms: number): number {
|
|
|
19
18
|
return Math.max(next.getTime() - ms, 1);
|
|
20
19
|
}
|
|
21
20
|
|
|
22
|
-
export function formatRunDuration(ms: number, t: RunDurationTranslate): string {
|
|
23
|
-
const total = Math.max(0, Math.floor(ms / 1000));
|
|
24
|
-
const minutes = Math.floor(total / 60);
|
|
25
|
-
const seconds = total % 60;
|
|
26
|
-
return minutes > 0
|
|
27
|
-
? t("duration.minutes", { minutes, seconds: String(seconds).padStart(2, "0") })
|
|
28
|
-
: t("duration.seconds", { seconds });
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function formatLatencySeconds(ms: number): string {
|
|
32
|
-
const s = Math.max(0, ms) / 1000;
|
|
33
|
-
return s < 10 ? String(Math.round(s * 10) / 10) : String(Math.round(s));
|
|
34
|
-
}
|
|
35
|
-
|
|
36
21
|
export function formatTokensPerSecond(tps: number): string {
|
|
37
22
|
const clamped = Math.max(0, tps);
|
|
38
23
|
return clamped >= 10 ? String(Math.round(clamped)) : String(Math.round(clamped * 10) / 10);
|
|
@@ -3,12 +3,13 @@
|
|
|
3
3
|
// database pill (total tokens + cache hit) opening the token-usage dialog.
|
|
4
4
|
// Settled-node identity prevents stream-delta updates from rerendering the row.
|
|
5
5
|
// Carried from upstream ui-chat (composer.dock id 'stats', priority -1 shadows it)
|
|
6
|
-
// so the fixed token-format (out-of-range cacheRead no longer hangs) stays wired
|
|
7
|
-
// data-composer-stats
|
|
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.
|
|
8
9
|
|
|
9
10
|
import { memo, useMemo, useState } from "react";
|
|
10
11
|
import { createPortal } from "react-dom";
|
|
11
|
-
import {
|
|
12
|
+
import { IconDatabaseOutlineRegular, IconGaugeOutlineRegular } from "@deepseek-ai/dsh-client-ui-primitives";
|
|
12
13
|
import type { UseProjection } from "@deepseek-ai/dsh-api-session-controller/client";
|
|
13
14
|
import type { SnapshotSelectorHook } from "@deepseek-ai/dsh-client-ui-slots";
|
|
14
15
|
// Type-only: merges the sessionStats key into SessionProjectionMap for useProjection.
|
|
@@ -172,7 +173,7 @@ function TimePill({
|
|
|
172
173
|
return (
|
|
173
174
|
<span {...styling.props(styles.anchor)}>
|
|
174
175
|
<span {...styling.props(styles.pill)}>
|
|
175
|
-
<
|
|
176
|
+
<IconGaugeOutlineRegular />
|
|
176
177
|
{label}
|
|
177
178
|
</span>
|
|
178
179
|
</span>
|
|
@@ -190,7 +191,7 @@ function TimePill({
|
|
|
190
191
|
setOpen(!open);
|
|
191
192
|
}}
|
|
192
193
|
>
|
|
193
|
-
<
|
|
194
|
+
<IconGaugeOutlineRegular />
|
|
194
195
|
{label}
|
|
195
196
|
</button>
|
|
196
197
|
{open &&
|
|
@@ -204,7 +205,7 @@ function TimePill({
|
|
|
204
205
|
>
|
|
205
206
|
<div {...styling.props(dialogStyles.title)}>
|
|
206
207
|
<span {...styling.props(dialogStyles.titleLabel)}>
|
|
207
|
-
<
|
|
208
|
+
<IconGaugeOutlineRegular />
|
|
208
209
|
{t("stats.dialog.title")}
|
|
209
210
|
</span>
|
|
210
211
|
</div>
|
|
@@ -273,7 +274,7 @@ function UsagePill({
|
|
|
273
274
|
setOpen(!open);
|
|
274
275
|
}}
|
|
275
276
|
>
|
|
276
|
-
<
|
|
277
|
+
<IconDatabaseOutlineRegular />
|
|
277
278
|
<span {...styling.props(styles.label)}>
|
|
278
279
|
{totalText}
|
|
279
280
|
{cacheHitText !== null && (
|
|
@@ -297,7 +298,7 @@ function UsagePill({
|
|
|
297
298
|
>
|
|
298
299
|
<div {...styling.props(dialogStyles.title)}>
|
|
299
300
|
<span {...styling.props(dialogStyles.titleLabel)}>
|
|
300
|
-
<
|
|
301
|
+
<IconDatabaseOutlineRegular />
|
|
301
302
|
{t("stats.dialog.usageTitle")}
|
|
302
303
|
</span>
|
|
303
304
|
<span {...styling.props(dialogStyles.titleValue)}>{exactCount(total, t)}</span>
|
|
@@ -351,8 +352,7 @@ export const StatsPills = memo(function StatsPills({ useChat, useProjection, t }
|
|
|
351
352
|
// billing (e.g. every request failed) shows its counts without a usage pill.
|
|
352
353
|
const hasTokens = usage !== undefined && (billedInputTokens(usage) > 0 || usage.outputTokens > 0);
|
|
353
354
|
if (stats.steps === 0 && !hasTokens) return null;
|
|
354
|
-
// data-composer-stats:
|
|
355
|
-
// tightens the composer's bottom clearance only while this row renders.
|
|
355
|
+
// data-composer-stats: 这一行的稳定锚点(样式 / 测试定位用)。
|
|
356
356
|
return (
|
|
357
357
|
<div {...styling.props(styles.root)} data-composer-stats>
|
|
358
358
|
{stats.steps > 0 && (
|
package/src/client/controller.ts
CHANGED
|
@@ -41,14 +41,22 @@ export class SessionEditorController {
|
|
|
41
41
|
this.face = {
|
|
42
42
|
retry: (turn, cascade) =>
|
|
43
43
|
this.mutate({ action: "retry", sessionId: this.sessionId, turn, cascade }),
|
|
44
|
-
recall: (message, texts) =>
|
|
45
|
-
this.mutate(
|
|
46
|
-
{ action: "recall", sessionId: this.sessionId, eventSeq: message.eventSeq },
|
|
47
|
-
() => this.setComposerDraft(texts.join("\n\n")),
|
|
48
|
-
),
|
|
44
|
+
recall: (message, texts) => this.recallAndEdit(message, texts),
|
|
49
45
|
};
|
|
50
46
|
}
|
|
51
47
|
|
|
48
|
+
private async recallAndEdit(
|
|
49
|
+
message: EditableMessageBlock,
|
|
50
|
+
texts: readonly string[],
|
|
51
|
+
): Promise<boolean> {
|
|
52
|
+
const applied = await this.mutate(
|
|
53
|
+
{ action: "recall", sessionId: this.sessionId, eventSeq: message.eventSeq },
|
|
54
|
+
() => this.setComposerDraft(texts.join("\n\n")),
|
|
55
|
+
);
|
|
56
|
+
if (applied) this.returnViewportToEnd();
|
|
57
|
+
return applied;
|
|
58
|
+
}
|
|
59
|
+
|
|
52
60
|
private async mutate(
|
|
53
61
|
operation: SessionEditorOperation,
|
|
54
62
|
onApplied?: () => void,
|
|
@@ -114,6 +122,24 @@ export class SessionEditorController {
|
|
|
114
122
|
);
|
|
115
123
|
}
|
|
116
124
|
|
|
125
|
+
/**
|
|
126
|
+
* 撤回后把会话视口送回底部:rewind 把窗口换短,而上游 ChatView 只在读者已经贴底时
|
|
127
|
+
* 才跟随(`tipMoved && atBottom`),停在中途的视口会悬在被截断的位置上;撤回的下一步
|
|
128
|
+
* 是改完再发,所以这里无条件送到底。容器用上游既有契约 `[data-conversation-scroll]`
|
|
129
|
+
* (ChatView / ConversationWidthControls / StatsPills 同用)。
|
|
130
|
+
*
|
|
131
|
+
* 窗口替换的渲染落在 resync 之后:写早了会被下一轮布局覆盖,因此等两帧再写。
|
|
132
|
+
*/
|
|
133
|
+
private returnViewportToEnd(): void {
|
|
134
|
+
const scrollport = document.querySelector<HTMLElement>("[data-conversation-scroll]");
|
|
135
|
+
if (scrollport === null) return;
|
|
136
|
+
requestAnimationFrame(() => {
|
|
137
|
+
requestAnimationFrame(() => {
|
|
138
|
+
scrollport.scrollTop = scrollport.scrollHeight;
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
117
143
|
private setComposerDraft(text: string): void {
|
|
118
144
|
if (this.sessions.binding(this.sessionId) === undefined) return;
|
|
119
145
|
const scoped = this.sessions.scope(this.sessionId);
|
package/src/index.ts
CHANGED
|
@@ -179,7 +179,7 @@ export class SessionEditor extends Service {
|
|
|
179
179
|
constructor(ctx: Context) {
|
|
180
180
|
super(ctx, "sessionEditor");
|
|
181
181
|
|
|
182
|
-
registerHttpRoutes(ctx);
|
|
182
|
+
registerHttpRoutes(ctx, this);
|
|
183
183
|
}
|
|
184
184
|
|
|
185
185
|
readBranchPrefix(
|
|
@@ -643,17 +643,22 @@ async function handleRoute(
|
|
|
643
643
|
}
|
|
644
644
|
}
|
|
645
645
|
|
|
646
|
-
function registerHttpRoutes(ctx: Context): void {
|
|
647
|
-
ctx.
|
|
646
|
+
function registerHttpRoutes(ctx: Context, editor: SessionEditor): void {
|
|
647
|
+
// webServer **必须等**:它可能比本行晚激活,而 `ctx.get` 取到 undefined 之后不会再试一次——路由没注册的
|
|
648
|
+
// 后果是请求落到 frontend-static 的 fallback,非 GET/HEAD 一律 405(编辑撤回 / 重试就是这个症状)。
|
|
649
|
+
ctx.inject(["webServer"], (scope) => {
|
|
648
650
|
// webServer 的类型由上游 @deepseek-ai/dsh-host-webserver 声明(`Context.webServer: WebServer`);
|
|
649
651
|
// 这里只按用到的 register 面做结构转换,不再 declare module 覆盖,否则两处声明冲突(TS2717)。
|
|
650
|
-
const webServer =
|
|
651
|
-
if (webServer === undefined) return
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
652
|
+
const webServer = scope.get("webServer") as unknown as HttpServerLike | undefined;
|
|
653
|
+
if (webServer === undefined) return;
|
|
654
|
+
scope.effect(
|
|
655
|
+
() =>
|
|
656
|
+
webServer.register({
|
|
657
|
+
kind: "exact",
|
|
658
|
+
path: SESSION_EDITOR_PATH,
|
|
659
|
+
handler: (request, response) => handleRoute(editor, request, response),
|
|
660
|
+
}),
|
|
661
|
+
"session-editor: HTTP route",
|
|
662
|
+
);
|
|
663
|
+
});
|
|
659
664
|
}
|
package/src/plan.ts
CHANGED
|
@@ -52,7 +52,7 @@ function userText(message: UserMessage): string {
|
|
|
52
52
|
|
|
53
53
|
function cloneUser(
|
|
54
54
|
message: UserMessage,
|
|
55
|
-
content: ContentBlock[] = structuredClone(message.content),
|
|
55
|
+
content: readonly ContentBlock[] = structuredClone(message.content) as ContentBlock[],
|
|
56
56
|
): UserMessage {
|
|
57
57
|
return Object.freeze({
|
|
58
58
|
id: randomUUID(),
|
package/src/testing.ts
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
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
|
-
import {
|
|
14
|
+
import { meta, oneTurnLog } from "@morlay/session-rdb/testing";
|
|
15
15
|
import { SESSION_EDITOR_PATH } from "./shared.ts";
|
|
16
16
|
import { SessionEditor } from "@morlay/ui-conversation-message-actions";
|
|
17
17
|
|
|
@@ -22,7 +22,6 @@ export {
|
|
|
22
22
|
SessionSeq,
|
|
23
23
|
SessionStore,
|
|
24
24
|
TokenMeter,
|
|
25
|
-
EmptySettings,
|
|
26
25
|
meta,
|
|
27
26
|
oneTurnLog,
|
|
28
27
|
parseJsonlArtifact,
|
|
@@ -37,7 +36,6 @@ export interface Harness {
|
|
|
37
36
|
|
|
38
37
|
export async function harness(provide?: (ctx: Context) => void): Promise<Harness> {
|
|
39
38
|
const ctx = new Context();
|
|
40
|
-
await ctx.plugin(EmptySettings);
|
|
41
39
|
await ctx.plugin(SessionStore);
|
|
42
40
|
new SessionProjectionRegistry(ctx);
|
|
43
41
|
const fiber = await ctx.plugin(SessionPersistenceSqlite, { type: "sqlite", path: ":memory:" });
|
|
@@ -87,7 +85,7 @@ export function userMessage(seq: number, id: string, text: string, time = seq):
|
|
|
87
85
|
source: { kind: "user" },
|
|
88
86
|
},
|
|
89
87
|
surfaceOp: "append",
|
|
90
|
-
} as SessionEvent;
|
|
88
|
+
} as unknown as SessionEvent;
|
|
91
89
|
}
|
|
92
90
|
|
|
93
91
|
export function assistantMessage(
|