@morlay/ui-conversation-message-actions 0.0.19-alpha.3 → 0.0.19-alpha.5
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 +4 -3
- package/dist/client.cjs +39 -179
- package/dist/index.d.cts +3 -6
- package/dist/index.d.mts +3 -6
- package/dist/index.mjs +8 -86
- package/dist/{plan-CT_6Y1fD.d.cts → plan-BKgGPZdD.d.mts} +2 -3
- package/dist/{plan-Cm9hCord.d.mts → plan-BUTplrpz.d.cts} +2 -3
- package/dist/plan.d.cts +1 -1
- package/dist/plan.d.mts +1 -1
- package/dist/plan.mjs +2 -49
- package/dist/shared-BBbOTwy4.mjs +4 -0
- package/dist/testing.d.cts +2 -2
- package/dist/testing.d.mts +2 -2
- package/dist/testing.mjs +582 -199
- package/package.json +6 -6
- package/src/client/chat-node/MessageItem.tsx +6 -3
- package/src/client/controller.ts +60 -222
- package/src/client/index.ts +0 -4
- package/src/index.ts +5 -143
- package/src/plan.ts +2 -54
- package/src/shared.ts +1 -97
- package/src/testing.ts +0 -2
- package/src/types.ts +1 -3
- package/dist/shared-LK3nImWW.mjs +0 -55
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.5",
|
|
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",
|
|
@@ -38,12 +38,12 @@
|
|
|
38
38
|
}
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@morlay/session-branch": "0.0.10-alpha.
|
|
41
|
+
"@morlay/session-branch": "0.0.10-alpha.5"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@deepseek-ai/dsh-session-stats": "0.1.6-alpha.2",
|
|
45
|
-
"@morlay/dsh-client-ui-primitives": "0.0.2-alpha.
|
|
46
|
-
"@morlay/session-rdb": "0.0.21-alpha.
|
|
45
|
+
"@morlay/dsh-client-ui-primitives": "0.0.2-alpha.5",
|
|
46
|
+
"@morlay/session-rdb": "0.0.21-alpha.5",
|
|
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.1.6-alpha.2",
|
|
68
68
|
"@deepseek-ai/dsh-session-projection": "0.1.6-alpha.2",
|
|
69
69
|
"@deepseek-ai/dsh-token-meter": "0.1.6-alpha.2",
|
|
70
|
-
"@morlay/dsh-client-ui-conversation": "0.0.2-alpha.
|
|
71
|
-
"@morlay/dsh-client-ui-primitives": "0.0.2-alpha.
|
|
70
|
+
"@morlay/dsh-client-ui-conversation": "0.0.2-alpha.5",
|
|
71
|
+
"@morlay/dsh-client-ui-primitives": "0.0.2-alpha.5"
|
|
72
72
|
},
|
|
73
73
|
"dsh": {
|
|
74
74
|
"bundle": {
|
|
@@ -26,6 +26,7 @@ type UserImage = Extract<UserMessageNode["content"][number], { type: "image" }>;
|
|
|
26
26
|
|
|
27
27
|
function contentParts(content: readonly unknown[]): {
|
|
28
28
|
text: string;
|
|
29
|
+
texts: string[];
|
|
29
30
|
images: { attachment: UserImage["attachment"] }[];
|
|
30
31
|
rest: unknown[];
|
|
31
32
|
} {
|
|
@@ -39,8 +40,8 @@ function contentParts(content: readonly unknown[]): {
|
|
|
39
40
|
images.push({ attachment: (b as UserImage).attachment });
|
|
40
41
|
} else rest.push(block);
|
|
41
42
|
}
|
|
42
|
-
// 多块消息按空行拼回 raw markdown
|
|
43
|
-
return { text: texts.join("\n\n"), images, rest };
|
|
43
|
+
// 多块消息按空行拼回 raw markdown(新式提交就是单块原文);撤回回填用的是 texts(全部块)。
|
|
44
|
+
return { text: texts.join("\n\n"), texts, images, rest };
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
function UserStyleBubble({
|
|
@@ -100,6 +101,8 @@ export const UserMessageNodeView = memo(function UserMessageNodeView({
|
|
|
100
101
|
openSkill: (name: string) => void;
|
|
101
102
|
} & InjectFace<SessionEditorFace>) {
|
|
102
103
|
const data = node.data;
|
|
104
|
+
// 撤回回填的文本:该消息的全部文本块(撤回截断整条消息,只回填首块会丢内容)。
|
|
105
|
+
const recallTexts = contentParts(data.content).texts;
|
|
103
106
|
const [confirmingRecall, setConfirmingRecall] = useState<EditableMessageBlock | null>(null);
|
|
104
107
|
const [confirmingRetry, setConfirmingRetry] = useState(false);
|
|
105
108
|
const turnLocation =
|
|
@@ -155,7 +158,7 @@ export const UserMessageNodeView = memo(function UserMessageNodeView({
|
|
|
155
158
|
onClick={() => {
|
|
156
159
|
const target = confirmingRecall;
|
|
157
160
|
setConfirmingRecall(null);
|
|
158
|
-
void recall(target);
|
|
161
|
+
void recall(target, recallTexts);
|
|
159
162
|
}}
|
|
160
163
|
>
|
|
161
164
|
撤回并编辑
|
package/src/client/controller.ts
CHANGED
|
@@ -1,282 +1,120 @@
|
|
|
1
1
|
import type { Context as ClientContext } from "@deepseek-ai/cordis";
|
|
2
|
-
import type {
|
|
3
|
-
ISessions,
|
|
4
|
-
SessionFace,
|
|
5
|
-
SessionSnapshot,
|
|
6
|
-
} from "@deepseek-ai/dsh-api-session-controller/client";
|
|
7
|
-
import type { ObservableSnapshot, SnapshotStore } from "@deepseek-ai/dsh-client-store";
|
|
8
|
-
import type { UiWorkspace } from "@deepseek-ai/dsh-client-ui-workspace/client";
|
|
9
|
-
import { createSnapshotStore } from "@deepseek-ai/dsh-client-store";
|
|
2
|
+
import type { ISessions } from "@deepseek-ai/dsh-api-session-controller/client";
|
|
10
3
|
import type { SessionId } from "@deepseek-ai/dsh-session";
|
|
11
4
|
import {
|
|
12
5
|
SESSION_EDITOR_PATH,
|
|
13
6
|
type EditableMessageBlock,
|
|
14
7
|
type SessionEditorOperation,
|
|
15
|
-
type SessionEditorOperationResult,
|
|
16
|
-
type SessionEditorTimeline,
|
|
17
8
|
type VersionOperation,
|
|
18
9
|
} from "../shared.ts";
|
|
19
10
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
32
|
-
|
|
11
|
+
/** 编辑器的 HTTP 路径:宿主(web 与桌面)在同一张路由表上服务它,页面不再按 ownsHost 加前缀。 */
|
|
12
|
+
const EDITOR_API_PATH = SESSION_EDITOR_PATH;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 会话编辑的浏览器半门面:只保留消息渲染面真正用到的两个动作。
|
|
16
|
+
*
|
|
17
|
+
* 曾经这里还有一条「订阅会话列表 / 快照 → 拉全量 timeline → 装进 store」的刷新管路,
|
|
18
|
+
* 它没有任何 UI 消费方,却把重放 / 流式期间的每一次快照抖动都换成一次全量 GET
|
|
19
|
+
* (编辑 / 重试 / 撤回后尤甚),订阅本身也从不释放。现在动作成功后只刷新会话列表元数据,
|
|
20
|
+
* 会话窗口交给上游的事件流收敛(不重建窗口、不整页重载)。
|
|
21
|
+
*/
|
|
33
22
|
export interface SessionEditorFace {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
load(): void;
|
|
37
|
-
edit(
|
|
38
|
-
message: EditableMessageBlock,
|
|
39
|
-
text: string,
|
|
40
|
-
cascade: "truncate" | "preserve",
|
|
41
|
-
): Promise<boolean>;
|
|
42
|
-
retry(turn: number, cascade: "truncate" | "preserve"): Promise<boolean>;
|
|
43
|
-
reroll(): Promise<boolean>;
|
|
44
|
-
rewind(toBoundary: number): Promise<boolean>;
|
|
45
|
-
recall(message: EditableMessageBlock): Promise<boolean>;
|
|
46
|
-
openVersion(sessionId: string): Promise<void>;
|
|
47
|
-
}
|
|
23
|
+
// 属性式函数类型而非方法签名:这两动作要被渲染面解构后直接调用,没有 `this` 可言。
|
|
24
|
+
retry: (turn: number, cascade: "truncate" | "preserve") => Promise<boolean>;
|
|
48
25
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function conversationRevision(snapshot: SessionSnapshot): string {
|
|
54
|
-
return [snapshot.openState, snapshot.removed, snapshot.hasMore].join("|");
|
|
26
|
+
// 撤回回填:文本由调用方(消息渲染面)给出**该消息的全部文本块**——不再靠 timeline 反查。
|
|
27
|
+
recall: (message: EditableMessageBlock, texts: readonly string[]) => Promise<boolean>;
|
|
55
28
|
}
|
|
56
29
|
|
|
57
30
|
export class SessionEditorController {
|
|
58
|
-
readonly store: SnapshotStore<SessionEditorState> = createSnapshotStore<SessionEditorState>({
|
|
59
|
-
status: "idle",
|
|
60
|
-
error: null,
|
|
61
|
-
pending: null,
|
|
62
|
-
timeline: null,
|
|
63
|
-
});
|
|
64
|
-
|
|
65
31
|
readonly face: SessionEditorFace;
|
|
66
|
-
|
|
32
|
+
|
|
67
33
|
private readonly sessions: ISessions;
|
|
68
|
-
private
|
|
69
|
-
private sessionSource: SessionFace | undefined;
|
|
70
|
-
private sessionSourceDispose: (() => void) | undefined;
|
|
71
|
-
private sessionRevision: string | undefined;
|
|
72
|
-
private disposed = false;
|
|
73
|
-
private users = 0;
|
|
34
|
+
private pending: VersionOperation | "recall" | null = null;
|
|
74
35
|
|
|
75
36
|
constructor(
|
|
76
37
|
ctx: ClientContext,
|
|
77
38
|
private readonly sessionId: SessionId,
|
|
78
39
|
) {
|
|
79
|
-
this.ctx = ctx;
|
|
80
40
|
this.sessions = ctx.get("sessions") as unknown as ISessions;
|
|
81
|
-
this.uiWorkspace = ctx.get("uiWorkspace") as unknown as UiWorkspace;
|
|
82
41
|
this.face = {
|
|
83
|
-
hooks: { sessionEditor: this.store },
|
|
84
|
-
acquire: () => {
|
|
85
|
-
this.users += 1;
|
|
86
|
-
if (this.users === 1 && this.disposed) this.revive();
|
|
87
|
-
return () => this.release();
|
|
88
|
-
},
|
|
89
|
-
load: () => {
|
|
90
|
-
void this.load();
|
|
91
|
-
},
|
|
92
|
-
edit: (message, text, cascade) =>
|
|
93
|
-
this.mutate({
|
|
94
|
-
action: "edit",
|
|
95
|
-
sessionId: this.sessionId,
|
|
96
|
-
eventSeq: message.eventSeq,
|
|
97
|
-
blockIndex: message.blockIndex,
|
|
98
|
-
text,
|
|
99
|
-
cascade,
|
|
100
|
-
}),
|
|
101
42
|
retry: (turn, cascade) =>
|
|
102
43
|
this.mutate({ action: "retry", sessionId: this.sessionId, turn, cascade }),
|
|
103
|
-
|
|
104
|
-
rewind: (toBoundary) =>
|
|
105
|
-
this.mutate({ action: "rewind", sessionId: this.sessionId, toBoundary }),
|
|
106
|
-
recall: (message) =>
|
|
44
|
+
recall: (message, texts) =>
|
|
107
45
|
this.mutate(
|
|
108
46
|
{ action: "recall", sessionId: this.sessionId, eventSeq: message.eventSeq },
|
|
109
|
-
() => this.setComposerDraft(
|
|
47
|
+
() => this.setComposerDraft(texts.join("\n\n")),
|
|
110
48
|
),
|
|
111
|
-
openVersion: (sessionId) => {
|
|
112
|
-
this.uiWorkspace.openSession(sessionId as SessionId);
|
|
113
|
-
return Promise.resolve();
|
|
114
|
-
},
|
|
115
49
|
};
|
|
116
|
-
this.observe();
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
private observe(): void {
|
|
120
|
-
this.sessionSource = undefined;
|
|
121
|
-
this.sessionSourceDispose?.();
|
|
122
|
-
this.bindSessionSource();
|
|
123
|
-
this.sessions.list.subscribe(() => this.invalidate());
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
private bindSessionSource(): void {
|
|
127
|
-
const source = this.sessions.binding(this.sessionId)?.session;
|
|
128
|
-
if (source === this.sessionSource) return;
|
|
129
|
-
this.sessionSourceDispose?.();
|
|
130
|
-
this.sessionSource = source;
|
|
131
|
-
this.sessionRevision =
|
|
132
|
-
source === undefined ? undefined : conversationRevision(source.getSnapshot());
|
|
133
|
-
this.sessionSourceDispose = source?.subscribe(() => {
|
|
134
|
-
this.invalidate();
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
private invalidate(): void {
|
|
139
|
-
if (this.disposed || this.store.getSnapshot().status === "idle") return;
|
|
140
|
-
|
|
141
|
-
void this.load();
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
private release(): void {
|
|
145
|
-
this.users -= 1;
|
|
146
|
-
if (this.users <= 0) this.dispose();
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
private dispose(): void {
|
|
150
|
-
if (this.disposed) return;
|
|
151
|
-
this.disposed = true;
|
|
152
|
-
this.sessionSourceDispose?.();
|
|
153
|
-
this.sessionSourceDispose = undefined;
|
|
154
|
-
this.sessionSource = undefined;
|
|
155
|
-
this.sessionRevision = undefined;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
private revive(): void {
|
|
159
|
-
this.disposed = false;
|
|
160
|
-
this.observe();
|
|
161
|
-
void this.load();
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
async load(): Promise<void> {
|
|
165
|
-
if (this.disposed) return;
|
|
166
|
-
this.store.update((state) => {
|
|
167
|
-
state.status = "loading";
|
|
168
|
-
state.error = null;
|
|
169
|
-
});
|
|
170
|
-
try {
|
|
171
|
-
const response = await fetch(
|
|
172
|
-
`${editorApiPath()}?sessionId=${encodeURIComponent(this.sessionId)}`,
|
|
173
|
-
{
|
|
174
|
-
method: "GET",
|
|
175
|
-
headers: { accept: "application/json" },
|
|
176
|
-
cache: "no-store",
|
|
177
|
-
},
|
|
178
|
-
);
|
|
179
|
-
const value = (await response.json()) as unknown;
|
|
180
|
-
if (this.disposed) return;
|
|
181
|
-
if (response.ok) {
|
|
182
|
-
this.store.update((state) => {
|
|
183
|
-
state.status = "ready";
|
|
184
|
-
state.error = null;
|
|
185
|
-
state.timeline = value as SessionEditorTimeline;
|
|
186
|
-
});
|
|
187
|
-
} else {
|
|
188
|
-
const error = (value as { error?: unknown })["error"];
|
|
189
|
-
this.store.update((state) => {
|
|
190
|
-
state.status = "error";
|
|
191
|
-
state.error =
|
|
192
|
-
typeof error === "string" ? error : `请求失败:HTTP ${String(response.status)}`;
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
} catch (error) {
|
|
196
|
-
if (this.disposed) return;
|
|
197
|
-
this.store.update((state) => {
|
|
198
|
-
state.status = "error";
|
|
199
|
-
state.error = messageOf(error);
|
|
200
|
-
});
|
|
201
|
-
}
|
|
202
50
|
}
|
|
203
51
|
|
|
204
52
|
private async mutate(
|
|
205
53
|
operation: SessionEditorOperation,
|
|
206
54
|
onApplied?: () => void,
|
|
207
55
|
): Promise<boolean> {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
this.store.update((state) => {
|
|
211
|
-
state.pending = operation.action;
|
|
212
|
-
state.error = null;
|
|
213
|
-
});
|
|
56
|
+
if (this.pending !== null) return false;
|
|
57
|
+
this.pending = operation.action;
|
|
214
58
|
try {
|
|
215
|
-
const response = await fetch(
|
|
59
|
+
const response = await fetch(EDITOR_API_PATH, {
|
|
216
60
|
method: "POST",
|
|
217
61
|
headers: { accept: "application/json", "content-type": "application/json" },
|
|
218
62
|
body: JSON.stringify(operation),
|
|
219
63
|
});
|
|
220
64
|
const value = (await response.json()) as unknown;
|
|
221
|
-
if (this.disposed) return true;
|
|
222
65
|
if (!response.ok) {
|
|
223
66
|
const error = (value as { error?: unknown })["error"];
|
|
224
67
|
throw new Error(
|
|
225
68
|
typeof error === "string" ? error : `请求失败:HTTP ${String(response.status)}`,
|
|
226
69
|
);
|
|
227
70
|
}
|
|
228
|
-
this.store.update((state) => {
|
|
229
|
-
state.pending = null;
|
|
230
|
-
});
|
|
231
71
|
|
|
232
72
|
onApplied?.();
|
|
233
|
-
const result = value as SessionEditorOperationResult;
|
|
234
|
-
|
|
235
|
-
if (String(result.sessionId) !== String(this.sessionId)) {
|
|
236
|
-
this.uiWorkspace.openSession(result.sessionId as SessionId);
|
|
237
|
-
return true;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
const face = this.sessions.binding(this.sessionId)?.session;
|
|
241
|
-
const resync = (face as unknown as { resync?: () => Promise<void> }).resync;
|
|
242
|
-
if (resync !== undefined) {
|
|
243
|
-
try {
|
|
244
|
-
await resync.call(face);
|
|
245
73
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
void this.load();
|
|
255
|
-
return true;
|
|
256
|
-
} catch {}
|
|
74
|
+
// 会话窗口必须重建:rewind 把日志 seq 回退了,窗口里已渲染的旧节点不会自己消失
|
|
75
|
+
// (实测:被裁剪的消息残留、新消息也追加不进去)。重建入口只在**运行时**对象上
|
|
76
|
+
// (ClientSession 的 `resync()`;类型面 `SessionFace` 并未暴露它),所以逐个探测;
|
|
77
|
+
// 都拿不到时记一条 warn 便于诊断,且不回退整页重载(刷新会打断用户)。
|
|
78
|
+
try {
|
|
79
|
+
await this.rebuildWindow();
|
|
80
|
+
} catch (error: unknown) {
|
|
81
|
+
console.warn("[session-editor] 操作已生效,但会话窗口重建失败:", error);
|
|
257
82
|
}
|
|
258
|
-
location.reload();
|
|
259
83
|
return true;
|
|
260
|
-
} catch
|
|
261
|
-
if (this.disposed) return false;
|
|
262
|
-
this.store.update((state) => {
|
|
263
|
-
state.pending = null;
|
|
264
|
-
state.error = messageOf(error);
|
|
265
|
-
});
|
|
84
|
+
} catch {
|
|
266
85
|
return false;
|
|
86
|
+
} finally {
|
|
87
|
+
this.pending = null;
|
|
267
88
|
}
|
|
268
89
|
}
|
|
269
90
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
91
|
+
/**
|
|
92
|
+
* 让上游重开该会话的历史窗口(rewind 之后客户端窗口的 seq 基线已经失效)。
|
|
93
|
+
*
|
|
94
|
+
* 探测顺序:`binding(id).session.resync()`(上游 ClientSession 的重建入口,类型面没暴露)
|
|
95
|
+
* → `sessions.refresh()`(只刷列表元数据,聊胜于无)→ 都没有就记一条 warn。
|
|
96
|
+
*/
|
|
97
|
+
private async rebuildWindow(): Promise<void> {
|
|
98
|
+
const sessions = this.sessions as unknown as {
|
|
99
|
+
binding?: (id: SessionId) => { session?: { resync?: () => Promise<void> } } | undefined;
|
|
100
|
+
refresh?: () => Promise<void>;
|
|
101
|
+
};
|
|
102
|
+
const face = sessions.binding?.(this.sessionId)?.session;
|
|
103
|
+
const resync = face?.resync;
|
|
104
|
+
if (typeof resync === "function") {
|
|
105
|
+
await resync.call(face);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (typeof sessions.refresh === "function") {
|
|
109
|
+
await sessions.refresh();
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
console.warn(
|
|
113
|
+
"[session-editor] 未找到会话窗口重建入口(binding.resync / refresh 都不可用),窗口可能停留在 rewind 之前的状态",
|
|
114
|
+
);
|
|
277
115
|
}
|
|
278
116
|
|
|
279
|
-
private setComposerDraft(
|
|
117
|
+
private setComposerDraft(text: string): void {
|
|
280
118
|
if (this.sessions.binding(this.sessionId) === undefined) return;
|
|
281
119
|
const scoped = this.sessions.scope(this.sessionId);
|
|
282
120
|
if (scoped === undefined) return;
|
|
@@ -287,6 +125,6 @@ export class SessionEditorController {
|
|
|
287
125
|
};
|
|
288
126
|
}
|
|
289
127
|
| undefined;
|
|
290
|
-
conversation?.input?.for(scoped).restoreDraft(
|
|
128
|
+
conversation?.input?.for(scoped).restoreDraft(text);
|
|
291
129
|
}
|
|
292
130
|
}
|
package/src/client/index.ts
CHANGED
|
@@ -24,10 +24,6 @@ export function apply(ctx: Context): void {
|
|
|
24
24
|
return controller;
|
|
25
25
|
};
|
|
26
26
|
|
|
27
|
-
ctx.on("connection/reset", () => {
|
|
28
|
-
for (const controller of controllers.values()) void controller.load();
|
|
29
|
-
});
|
|
30
|
-
|
|
31
27
|
registerChatNodeRenderers(ctx, controllerFor);
|
|
32
28
|
|
|
33
29
|
ctx.slots.inject("conversation.composer.dock", () => registerComposerStats(ctx.slots));
|