@morlay/ui-conversation-message-actions 0.0.17 → 0.0.19-alpha.0

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 (45) hide show
  1. package/README.md +29 -98
  2. package/dist/client.cjs +1380 -0
  3. package/dist/client.d.cts +6 -0
  4. package/dist/client.d.mts +6 -0
  5. package/dist/index.d.cts +98 -0
  6. package/dist/index.d.mts +3 -10
  7. package/dist/index.mjs +460 -2
  8. package/dist/invariant.d.cts +7 -0
  9. package/dist/{plan-BSa7k9HO.d.mts → plan-CT_6Y1fD.d.cts} +11 -9
  10. package/dist/plan-Cm9hCord.d.mts +101 -0
  11. package/dist/plan.d.cts +2 -0
  12. package/dist/plan.d.mts +2 -2
  13. package/dist/plan.mjs +145 -11
  14. package/dist/shared-LK3nImWW.mjs +55 -0
  15. package/dist/testing.d.cts +3242 -0
  16. package/dist/testing.d.mts +25 -19
  17. package/dist/testing.mjs +2140 -3939
  18. package/package.json +35 -32
  19. package/src/client/chat-node/MessageIconActions.styles.ts +76 -0
  20. package/src/client/chat-node/MessageIconActions.tsx +30 -11
  21. package/src/client/chat-node/MessageItem.styles.ts +247 -0
  22. package/src/client/chat-node/MessageItem.tsx +32 -39
  23. package/src/client/chat-node/message-chrome.ts +0 -2
  24. package/src/client/chat-node/register.ts +3 -4
  25. package/src/client/chat-node/use-calendar-day.ts +0 -4
  26. package/src/client/composer-stats/StatsPills.styles.ts +57 -0
  27. package/src/client/composer-stats/StatsPills.tsx +384 -0
  28. package/src/client/composer-stats/register.ts +17 -0
  29. package/src/client/composer-stats/stat-dialog.styles.ts +70 -0
  30. package/src/client/composer-stats/stat-dialog.ts +52 -0
  31. package/src/client/composer-stats/token-format.ts +85 -0
  32. package/src/client/composer-stats/turn-reading.ts +35 -0
  33. package/src/client/controller.ts +32 -114
  34. package/src/client/index.ts +8 -24
  35. package/src/index.ts +102 -111
  36. package/src/plan.ts +189 -24
  37. package/src/shared.ts +1 -1
  38. package/src/testing.ts +1 -5
  39. package/src/types.ts +1 -1
  40. package/dist/client.js +0 -806
  41. package/dist/src-B4HwXsob.mjs +0 -499
  42. package/src/client/chat-node/MessageIconActions.module.css +0 -86
  43. package/src/client/chat-node/MessageItem.module.css +0 -290
  44. package/src/client/css-modules.d.ts +0 -4
  45. package/src/client/import-action.tsx +0 -74
@@ -0,0 +1,70 @@
1
+ import type { CSSProps } from "@morlay/dsh-client-ui-primitives/client";
2
+
3
+ export const styles = {
4
+ panel: {
5
+ position: "fixed",
6
+ zIndex: "1100",
7
+ boxSizing: "border-box",
8
+ width: "max-content",
9
+ minWidth: "min(300px, calc(100vw - 24px))",
10
+ maxWidth: "min(440px, calc(100vw - 24px))",
11
+ padding: "16px",
12
+ border: "0",
13
+ borderRadius: "12px",
14
+ background: "var(--dsw-specific-menu)",
15
+ "--dsw-elevation-stroke-color": "var(--dsw-alias-border-l1)",
16
+ boxShadow: "var(--dsw-elevation-prominent)",
17
+ fontSize: "12px",
18
+ lineHeight: "18px",
19
+ color: "var(--dsw-alias-label-secondary)",
20
+ cursor: "default",
21
+ },
22
+ title: {
23
+ display: "flex",
24
+ justifyContent: "space-between",
25
+ gap: "16px",
26
+ marginBottom: "8px",
27
+ color: "var(--dsw-alias-label-primary)",
28
+ fontWeight: "500",
29
+ },
30
+ titleRule: {
31
+ marginBottom: "10px",
32
+ borderTop: "0.5px solid var(--dsw-alias-border-l2)",
33
+ },
34
+ titleValue: {
35
+ fontVariantNumeric: "tabular-nums",
36
+ },
37
+ titleLabel: {
38
+ display: "inline-flex",
39
+ alignItems: "center",
40
+ gap: "6px",
41
+ minWidth: "0",
42
+ "& svg": {
43
+ width: "14px",
44
+ height: "14px",
45
+ flex: "none",
46
+ },
47
+ },
48
+ details: {
49
+ display: "grid",
50
+ gridTemplateColumns: "minmax(76px, auto) minmax(0, 1fr)",
51
+ gap: "6px 16px",
52
+ margin: "0",
53
+ color: "var(--dsw-alias-label-tertiary)",
54
+ "& dt": {
55
+ minWidth: "0",
56
+ margin: "0",
57
+ },
58
+ "& dd": {
59
+ minWidth: "0",
60
+ margin: "0",
61
+ color: "var(--dsw-alias-label-secondary)",
62
+ fontVariantNumeric: "tabular-nums",
63
+ textAlign: "right",
64
+ },
65
+ },
66
+ reasoning: {
67
+ color: "var(--dsw-alias-label-tertiary)",
68
+ whiteSpace: "nowrap",
69
+ },
70
+ } satisfies Record<string, CSSProps>;
@@ -0,0 +1,52 @@
1
+ import { useEffect, useRef, useState, type CSSProperties, type MutableRefObject } from "react";
2
+ import {
3
+ useAnchoredPosition,
4
+ useDismissOnOutsidePointer,
5
+ } from "@deepseek-ai/dsh-client-ui-primitives";
6
+
7
+ const PANEL_MARGIN = 12;
8
+
9
+ const PANEL_GAP = 8;
10
+
11
+ export const MEASURE_STYLE: CSSProperties = { visibility: "hidden", left: 0, top: 0 };
12
+
13
+ export interface StatDialogSeat {
14
+ open: boolean;
15
+ setOpen: (open: boolean) => void;
16
+ rootRef: MutableRefObject<HTMLSpanElement | null>;
17
+ panelRef: MutableRefObject<HTMLDivElement | null>;
18
+ pos: CSSProperties | null;
19
+ }
20
+
21
+ export function useStatDialog(
22
+ controlled?: Pick<StatDialogSeat, "open" | "setOpen">,
23
+ ): StatDialogSeat {
24
+ const [ownOpen, setOwnOpen] = useState(false);
25
+ const open = controlled?.open ?? ownOpen;
26
+ const setOpen = controlled?.setOpen ?? setOwnOpen;
27
+ const rootRef = useRef<HTMLSpanElement | null>(null);
28
+ const panelRef = useRef<HTMLDivElement | null>(null);
29
+
30
+ const pos = useAnchoredPosition({
31
+ open,
32
+ anchorRef: rootRef,
33
+ panelRef,
34
+ side: "top",
35
+ gap: PANEL_GAP,
36
+ margin: PANEL_MARGIN,
37
+ });
38
+
39
+ useDismissOnOutsidePointer(rootRef, open, setOpen, panelRef);
40
+ useEffect(() => {
41
+ if (!open) return;
42
+ const onKeyDown = (e: KeyboardEvent): void => {
43
+ if (e.key === "Escape") setOpen(false);
44
+ };
45
+ document.addEventListener("keydown", onKeyDown);
46
+ return () => {
47
+ document.removeEventListener("keydown", onKeyDown);
48
+ };
49
+ }, [open, setOpen]);
50
+
51
+ return { open, setOpen, rootRef, panelRef, pos };
52
+ }
@@ -0,0 +1,85 @@
1
+ // Token 展示口径,随 composer 统计行从上游 ui-chat 搬来:`formatCacheHitPercent`
2
+ // 的越界分支是**主动修的缺陷**(上游只在未命中侧为 0 时返回 "100",
3
+ // cacheRead 超出 prompt 时展开循环的缩放间隙恒为负 → 同步死循环冻结主线程)。
4
+ import type { ChatViewSlotProps } from "@deepseek-ai/dsh-client-ui-chat/client";
5
+
6
+ export function formatTokens(value: number, t: ChatViewSlotProps["t"]): string {
7
+ const scaled = (candidate: number): string =>
8
+ candidate >= 100 ? String(Math.round(candidate)) : String(Math.round(candidate * 10) / 10);
9
+ if (value < 1_000) return String(value);
10
+ if (value < 1_000_000) return t("number.thousand", { value: scaled(value / 1_000) });
11
+ return t("number.million", { value: scaled(value / 1_000_000) });
12
+ }
13
+
14
+ export function formatExactTokens(value: number, t: ChatViewSlotProps["t"]): string {
15
+ const digits = String(value);
16
+ const groups: string[] = [];
17
+ for (let end = digits.length; end > 0; end -= 3) {
18
+ groups.unshift(digits.slice(Math.max(0, end - 3), end));
19
+ }
20
+ return groups.join(t("number.groupSeparator"));
21
+ }
22
+
23
+ function roundedPercentUnits(
24
+ cacheReadTokens: number,
25
+ denominator: number,
26
+ decimalPlaces: 0 | 1,
27
+ ): number {
28
+ const unitsPerPercent = decimalPlaces === 0 ? 1 : 10;
29
+ const scale = unitsPerPercent * 100;
30
+ const doubledScale = scale * 2;
31
+ const denominatorQuotient = Math.floor(denominator / doubledScale);
32
+ const denominatorRemainder = denominator % doubledScale;
33
+ let lower = 0;
34
+ let upper = scale;
35
+ while (lower < upper) {
36
+ const candidate = Math.floor((lower + upper + 1) / 2);
37
+ const factor = candidate * 2 - 1;
38
+ const threshold =
39
+ factor * denominatorQuotient + Math.ceil((factor * denominatorRemainder) / doubledScale);
40
+ if (cacheReadTokens >= threshold) lower = candidate;
41
+ else upper = candidate - 1;
42
+ }
43
+ return lower;
44
+ }
45
+
46
+ function displayPercentUnits(units: number, decimalPlaces: 0 | 1): string {
47
+ if (decimalPlaces === 0) return String(units);
48
+ const whole = Math.floor(units / 10);
49
+ const tenths = units % 10;
50
+ return tenths === 0 ? String(whole) : `${whole}.${tenths}`;
51
+ }
52
+
53
+ export function formatCacheHitPercent(
54
+ cacheReadTokens: number,
55
+ promptTokens: number,
56
+ decimalPlaces: 0 | 1 = 0,
57
+ ): string | null {
58
+ if (promptTokens === 0) return null;
59
+ const missedInputTokens = promptTokens - cacheReadTokens;
60
+ // 未命中侧为 0,或为负(cacheRead 超出 prompt 的越界输入)时都按全命中返回。
61
+ if (missedInputTokens <= 0) return "100";
62
+
63
+ const roundedUnits = roundedPercentUnits(cacheReadTokens, promptTokens, decimalPlaces);
64
+ const fullHitUnits = decimalPlaces === 0 ? 100 : 1_000;
65
+ if (roundedUnits < fullHitUnits) return displayPercentUnits(roundedUnits, decimalPlaces);
66
+
67
+ let distinguishingPlaces = 1;
68
+ let scaledDoubleGap = missedInputTokens * 200;
69
+ const denominatorTens = Math.floor(promptTokens / 10);
70
+ while (scaledDoubleGap <= denominatorTens) {
71
+ scaledDoubleGap *= 10;
72
+ distinguishingPlaces += 1;
73
+ }
74
+ const denominatorOnes = promptTokens % 10;
75
+ let roundedLoss = 5;
76
+ for (let loss = 1; loss < 5; loss += 1) {
77
+ const factor = loss * 2 + 1;
78
+ const threshold = factor * denominatorTens + Math.floor((factor * denominatorOnes) / 10);
79
+ if (scaledDoubleGap <= threshold) {
80
+ roundedLoss = loss;
81
+ break;
82
+ }
83
+ }
84
+ return `99.${"9".repeat(distinguishingPlaces - 1)}${10 - roundedLoss}`;
85
+ }
@@ -0,0 +1,35 @@
1
+ // 窗口折叠(无 `sessionStats` 投影时的兜底)读一条 assistant 步的时序 / 用量。
2
+ // 从上游 ui-chat 的 `contract/turn-metrics.ts` 取出统计行真正用到的那一半,
3
+ // 只服务 composer 统计行。
4
+ import type { AssistantMessageNode } from "@deepseek-ai/dsh-client-ui-chat/client";
5
+
6
+ export interface StepReading {
7
+ ttftMs: number | null;
8
+
9
+ decodeMs: number | null;
10
+
11
+ outputTokens: number | null;
12
+ }
13
+
14
+ interface UsageLike {
15
+ outputTokens?: number;
16
+ }
17
+
18
+ function usageOutputTokens(usage: unknown): number | null {
19
+ if (typeof usage !== "object" || usage === null) return null;
20
+ const value = (usage as UsageLike).outputTokens;
21
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : null;
22
+ }
23
+
24
+ export function assistantStepReading(node: AssistantMessageNode): StepReading {
25
+ const timing = node.timing;
26
+ const ttftMs =
27
+ timing !== undefined && timing.stepStartTime !== null && timing.firstTokenTime !== null
28
+ ? Math.max(0, timing.firstTokenTime - timing.stepStartTime)
29
+ : null;
30
+ const decodeMs =
31
+ timing !== undefined && timing.firstTokenTime !== null
32
+ ? Math.max(0, timing.completedTime - timing.firstTokenTime)
33
+ : null;
34
+ return { ttftMs, decodeMs, outputTokens: usageOutputTokens(node.usage) };
35
+ }
@@ -5,6 +5,7 @@ import type {
5
5
  SessionSnapshot,
6
6
  } from "@deepseek-ai/dsh-api-session-controller/client";
7
7
  import type { ObservableSnapshot, SnapshotStore } from "@deepseek-ai/dsh-client-store";
8
+ import type { UiWorkspace } from "@deepseek-ai/dsh-client-ui-workspace/client";
8
9
  import { createSnapshotStore } from "@deepseek-ai/dsh-client-store";
9
10
  import type { SessionId } from "@deepseek-ai/dsh-session";
10
11
  import {
@@ -16,7 +17,6 @@ import {
16
17
  type VersionOperation,
17
18
  } from "../shared.ts";
18
19
 
19
- /** desktop 壳(官方 dsh-desktop-host)注入的 transport 标记:ownsHost 时 API 走 /api 前缀。 */
20
20
  function editorApiPath(): string {
21
21
  const transport = (globalThis as { __DSH_TRANSPORT__?: { ownsHost?: boolean } })
22
22
  .__DSH_TRANSPORT__;
@@ -26,7 +26,7 @@ function editorApiPath(): string {
26
26
  export interface SessionEditorState {
27
27
  status: "idle" | "loading" | "ready" | "error";
28
28
  error: string | null;
29
- pending: VersionOperation | "recall" | "import" | null;
29
+ pending: VersionOperation | "recall" | null;
30
30
  timeline: SessionEditorTimeline | null;
31
31
  }
32
32
 
@@ -43,8 +43,6 @@ export interface SessionEditorFace {
43
43
  reroll(): Promise<boolean>;
44
44
  rewind(toBoundary: number): Promise<boolean>;
45
45
  recall(message: EditableMessageBlock): Promise<boolean>;
46
-
47
- importSession(file: File): Promise<boolean>;
48
46
  openVersion(sessionId: string): Promise<void>;
49
47
  }
50
48
 
@@ -53,8 +51,6 @@ function messageOf(error: unknown): string {
53
51
  }
54
52
 
55
53
  function conversationRevision(snapshot: SessionSnapshot): string {
56
- // 新版 SessionSnapshot 已无 turnEnds(会话级事件窗口不再暴露轮次边界);
57
- // 用生命周期字段作为会话变化指纹即可。
58
54
  return [snapshot.openState, snapshot.removed, snapshot.hasMore].join("|");
59
55
  }
60
56
 
@@ -69,12 +65,12 @@ export class SessionEditorController {
69
65
  readonly face: SessionEditorFace;
70
66
  private readonly ctx: ClientContext;
71
67
  private readonly sessions: ISessions;
68
+ private readonly uiWorkspace: UiWorkspace;
72
69
  private sessionSource: SessionFace | undefined;
73
70
  private sessionSourceDispose: (() => void) | undefined;
74
71
  private sessionRevision: string | undefined;
75
72
  private disposed = false;
76
73
  private users = 0;
77
- private readonly navigationWaits = new Set<() => void>();
78
74
 
79
75
  constructor(
80
76
  ctx: ClientContext,
@@ -82,6 +78,7 @@ export class SessionEditorController {
82
78
  ) {
83
79
  this.ctx = ctx;
84
80
  this.sessions = ctx.get("sessions") as unknown as ISessions;
81
+ this.uiWorkspace = ctx.get("uiWorkspace") as unknown as UiWorkspace;
85
82
  this.face = {
86
83
  hooks: { sessionEditor: this.store },
87
84
  acquire: () => {
@@ -109,10 +106,12 @@ export class SessionEditorController {
109
106
  recall: (message) =>
110
107
  this.mutate(
111
108
  { action: "recall", sessionId: this.sessionId, eventSeq: message.eventSeq },
112
- () => this.setComposerDraft(message.text),
109
+ () => this.setComposerDraft(this.messageTexts(message)),
113
110
  ),
114
- importSession: (file) => this.importSession(file),
115
- openVersion: (sessionId) => this.openWhenListed(sessionId as SessionId),
111
+ openVersion: (sessionId) => {
112
+ this.uiWorkspace.openSession(sessionId as SessionId);
113
+ return Promise.resolve();
114
+ },
116
115
  };
117
116
  this.observe();
118
117
  }
@@ -138,7 +137,7 @@ export class SessionEditorController {
138
137
 
139
138
  private invalidate(): void {
140
139
  if (this.disposed || this.store.getSnapshot().status === "idle") return;
141
- // 会话状态变化 → 刷新投影(debounce 由调用方按需)。
140
+
142
141
  void this.load();
143
142
  }
144
143
 
@@ -206,8 +205,6 @@ export class SessionEditorController {
206
205
  operation: SessionEditorOperation,
207
206
  onApplied?: () => void,
208
207
  ): Promise<boolean> {
209
- // 只拦截并发操作;不要求 status === "ready"——编辑/重试的数据来自客户端
210
- // conversation 节点,与 timeline 加载无关,status 为 idle 时也可发起请求。
211
208
  const current = this.store.getSnapshot();
212
209
  if (current.pending !== null) return false;
213
210
  this.store.update((state) => {
@@ -231,27 +228,21 @@ export class SessionEditorController {
231
228
  this.store.update((state) => {
232
229
  state.pending = null;
233
230
  });
234
- // recall 的草稿回填必须在刷新(resync / 整页重载)之前:草稿经会话级
235
- // 持久化 mirror 落 store,整页重载后仍能恢复。
231
+
236
232
  onApplied?.();
237
233
  const result = value as SessionEditorOperationResult;
238
- // 仅 fork 产生新 id 时需要等列表发布并打开新版本;就地编辑不改 id。
234
+
239
235
  if (String(result.sessionId) !== String(this.sessionId)) {
240
- await this.openWhenListed(result.sessionId as SessionId);
236
+ this.uiWorkspace.openSession(result.sessionId as SessionId);
241
237
  return true;
242
238
  }
243
- // 就地编辑:rewind 删除事件,append-only 事件流无法表达删除——seq
244
- // 回退只做增量,被剪掉的旧节点残留。优先会话级刷新(resync 重置窗口
245
- // 并重新拉取历史);不可用时回退整页重载。
239
+
246
240
  const face = this.sessions.binding(this.sessionId)?.session;
247
241
  const resync = (face as unknown as { resync?: () => Promise<void> }).resync;
248
242
  if (resync !== undefined) {
249
243
  try {
250
244
  await resync.call(face);
251
- // resync 只重建事件窗口;投影 store 按 higher-seq-wins 保留 rewind
252
- // 前的高 seq 旧值(截断后的正确值 seq 更小,永远覆盖不上),轮次
253
- // 导航会残留已删除的轮次。丢弃全部投影行,等 host 推送重建;拿不到
254
- // 丢弃入口时整页重载兜底。
245
+
255
246
  const projections = (
256
247
  face as unknown as { projections?: { truncate?(lastSeq: number): void } }
257
248
  ).projections;
@@ -262,9 +253,7 @@ export class SessionEditorController {
262
253
  projections.truncate(-1);
263
254
  void this.load();
264
255
  return true;
265
- } catch {
266
- // fall through to full reload
267
- }
256
+ } catch {}
268
257
  }
269
258
  location.reload();
270
259
  return true;
@@ -278,97 +267,26 @@ export class SessionEditorController {
278
267
  }
279
268
  }
280
269
 
281
- /**
282
- * 把撤回的消息文本回填到该会话的 composer 草稿(`conversation.input` 的
283
- * `setDraft`,即主输入框的整段替换语义)。会话 scope / binding / conversation
284
- * 服务缺失时静默跳过——撤回本身已成功,草稿回填失败不应回滚会话状态。
285
- */
286
- private setComposerDraft(text: string): void {
287
- // binding 缺失时 input.for 会抛(会话 scope 已拆除),先短路。
270
+ private messageTexts(message: EditableMessageBlock): readonly string[] {
271
+ const prefix = `${String(message.eventSeq)}:`;
272
+ const blocks = (this.store.getSnapshot().timeline?.messages ?? [])
273
+ .filter((row) => row.kind === "user" && row.key.startsWith(prefix))
274
+ .sort((left, right) => left.blockIndex - right.blockIndex)
275
+ .map((row) => row.text);
276
+ return blocks.length === 0 ? [message.text] : blocks;
277
+ }
278
+
279
+ private setComposerDraft(texts: readonly string[]): void {
288
280
  if (this.sessions.binding(this.sessionId) === undefined) return;
289
281
  const scoped = this.sessions.scope(this.sessionId);
290
282
  if (scoped === undefined) return;
291
283
  const conversation = scoped.get("conversation") as
292
- | { input?: { for(ctx: ClientContext): { setDraft(text: string): void } } }
284
+ | {
285
+ input?: {
286
+ for(ctx: ClientContext): { restoreDraft(draft: string): void };
287
+ };
288
+ }
293
289
  | undefined;
294
- conversation?.input?.for(scoped).setDraft(text);
295
- }
296
-
297
- private async importSession(file: File): Promise<boolean> {
298
- const current = this.store.getSnapshot();
299
- if (current.pending !== null) return false;
300
- this.store.update((state) => {
301
- state.pending = "import";
302
- state.error = null;
303
- });
304
- try {
305
- const zip = await new Promise<string>((resolve, reject) => {
306
- const reader = new FileReader();
307
- reader.onload = () => {
308
- const dataUrl = typeof reader.result === "string" ? reader.result : "";
309
- const comma = dataUrl.indexOf(",");
310
- resolve(comma < 0 ? dataUrl : dataUrl.slice(comma + 1));
311
- };
312
- reader.onerror = () =>
313
- reject(reader.error ?? new Error("failed to read the selected file"));
314
- reader.readAsDataURL(file);
315
- });
316
- const response = await fetch("/api/session.import", {
317
- method: "POST",
318
- headers: { accept: "application/json", "content-type": "application/json" },
319
- body: JSON.stringify({ zip, sessionId: this.sessionId }),
320
- });
321
- const value = (await response.json()) as unknown;
322
- if (this.disposed) return true;
323
- if (!response.ok) {
324
- const error = (value as { error?: unknown })["error"];
325
- throw new Error(
326
- typeof error === "string" ? error : `请求失败:HTTP ${String(response.status)}`,
327
- );
328
- }
329
- this.store.update((state) => {
330
- state.pending = null;
331
- });
332
- // 覆盖成功但**不能**走 resync:resync 重开事件流时 observeSession 仍
333
- // 优先读 live session(rewind 已截断其内存 log,append 只写 DB),会
334
- // 再读到空/截断数据。整页重载让会话从 live 卸载,冷读 DB 完整数据。
335
- location.reload();
336
- return true;
337
- } catch (error) {
338
- if (this.disposed) return false;
339
- this.store.update((state) => {
340
- state.pending = null;
341
- state.error = messageOf(error);
342
- });
343
- return false;
344
- }
345
- }
346
-
347
- private openWhenListed(sessionId: SessionId): Promise<void> {
348
- if (this.sessions.list.getSnapshot().byId[sessionId] !== undefined) {
349
- this.sessions.open(sessionId);
350
- return Promise.resolve();
351
- }
352
- return new Promise((resolve) => {
353
- let settled = false;
354
- let dispose = (): void => {};
355
- const finish = (open: boolean): void => {
356
- if (settled) return;
357
- settled = true;
358
- dispose();
359
- this.navigationWaits.delete(cancel);
360
- if (open) this.sessions.open(sessionId);
361
- resolve();
362
- };
363
- const cancel = (): void => {
364
- finish(false);
365
- };
366
- this.navigationWaits.add(cancel);
367
- dispose = this.sessions.list.subscribe(() => {
368
- if (this.sessions.list.getSnapshot().byId[sessionId] === undefined) return;
369
- finish(true);
370
- });
371
- if (this.sessions.list.getSnapshot().byId[sessionId] !== undefined) finish(true);
372
- });
290
+ conversation?.input?.for(scoped).restoreDraft(texts.join("\n\n"));
373
291
  }
374
292
  }
@@ -1,15 +1,19 @@
1
1
  import type { Context } from "@deepseek-ai/cordis";
2
- import type {} from "@deepseek-ai/dsh-client-ui-conversation/client";
2
+ import type {} from "@morlay/dsh-client-ui-conversation/client";
3
3
  import type {} from "@deepseek-ai/dsh-client-ui-chat/client";
4
4
  import type {} from "@deepseek-ai/dsh-client-ui-renderer/client";
5
5
  import type { SessionId } from "@deepseek-ai/dsh-session";
6
6
  import { SessionEditorController } from "./controller.ts";
7
7
  import { registerChatNodeRenderers } from "./chat-node/register.ts";
8
- import { SessionImportAction } from "./import-action.tsx";
8
+ import { registerComposerStats } from "./composer-stats/register.ts";
9
+ import { styling } from "@morlay/dsh-client-ui-primitives/client";
10
+ import { globals as messageIconGlobals } from "./chat-node/MessageIconActions.styles.ts";
9
11
 
10
- export const inject = ["slots", "conversation", "connection", "sessions"];
12
+ export const inject = ["slots", "conversation", "connection", "sessions", "uiWorkspace"];
11
13
 
12
14
  export function apply(ctx: Context): void {
15
+ styling.injectGlobals(messageIconGlobals);
16
+
13
17
  const controllers = new Map<SessionId, SessionEditorController>();
14
18
  const controllerFor = (sessionId: SessionId): SessionEditorController => {
15
19
  let controller = controllers.get(sessionId);
@@ -24,27 +28,7 @@ export function apply(ctx: Context): void {
24
28
  for (const controller of controllers.values()) void controller.load();
25
29
  });
26
30
 
27
- // 新版 `conversation.chat.node` 是 keyed slot(reuse key 即替换该节点的
28
- // 渲染器);register 内部经 ctx.slots.inject 等待声明。
29
31
  registerChatNodeRenderers(ctx, controllerFor);
30
32
 
31
- // 会话头部「导入会话」入口:导出按钮旁(header.utilities list 槽),
32
- // 用导出的 zip 覆盖当前会话内容(host 端 `/api/session.import`)。
33
- ctx.slots.inject("conversation.session.header.utilities", () =>
34
- ctx.slots.register(
35
- {
36
- name: "conversation.session.header.utilities",
37
- id: "session-editor.import",
38
- inject: (sessionId: SessionId) => {
39
- const face = controllerFor(sessionId).face;
40
- return {
41
- hooks: face.hooks,
42
- // face 方法是闭包(已绑定 this),直接透传。
43
- importSession: (file: File) => face.importSession(file),
44
- };
45
- },
46
- } as never,
47
- SessionImportAction as never,
48
- ),
49
- );
33
+ ctx.slots.inject("conversation.composer.dock", () => registerComposerStats(ctx.slots));
50
34
  }