@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/locale/en.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Message Actions",
4
+ "description": "Rewind / retry / fork orchestration on the host side, plus the user-bubble renderer replacement in the browser."
5
+ }
6
+ }
package/locale/zh.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "消息动作",
4
+ "description": "host 侧的 rewind / retry / fork 编排,以及浏览器侧 user 消息气泡的渲染替换。"
5
+ }
6
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@morlay/ui-conversation-message-actions",
3
- "version": "0.0.19-alpha.5",
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.5"
43
+ "@morlay/session-branch": "0.0.10-alpha.7"
42
44
  },
43
45
  "devDependencies": {
44
- "@deepseek-ai/dsh-session-stats": "0.1.6-alpha.2",
45
- "@morlay/dsh-client-ui-primitives": "0.0.2-alpha.5",
46
- "@morlay/session-rdb": "0.0.21-alpha.5",
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.2",
54
- "@deepseek-ai/dsh-api-session-controller": "0.1.6-alpha.2",
55
- "@deepseek-ai/dsh-attachment": "0.1.6-alpha.2",
56
- "@deepseek-ai/dsh-client-connection": "0.1.6-alpha.2",
57
- "@deepseek-ai/dsh-client-store": "0.1.6-alpha.2",
58
- "@deepseek-ai/dsh-client-ui-attachment": "0.1.6-alpha.2",
59
- "@deepseek-ai/dsh-client-ui-chat": "0.1.6-alpha.2",
60
- "@deepseek-ai/dsh-client-ui-primitives": "0.1.6-alpha.2",
61
- "@deepseek-ai/dsh-client-ui-renderer": "0.1.6-alpha.2",
62
- "@deepseek-ai/dsh-client-ui-slots": "0.1.6-alpha.2",
63
- "@deepseek-ai/dsh-client-ui-workspace": "0.1.6-alpha.2",
64
- "@deepseek-ai/dsh-invariants": "0.1.6-alpha.2",
65
- "@deepseek-ai/dsh-llm": "0.1.6-alpha.2",
66
- "@deepseek-ai/dsh-session": "0.1.6-alpha.2",
67
- "@deepseek-ai/dsh-session-persistence": "0.1.6-alpha.2",
68
- "@deepseek-ai/dsh-session-projection": "0.1.6-alpha.2",
69
- "@deepseek-ai/dsh-token-meter": "0.1.6-alpha.2",
70
- "@morlay/dsh-client-ui-conversation": "0.0.2-alpha.5",
71
- "@morlay/dsh-client-ui-primitives": "0.0.2-alpha.5"
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
- IconBranchOutline16,
8
- IconCheckOutline16,
9
- IconCopyOutline16,
10
- IconEditOutline16,
11
- IconRefreshOutline16,
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
- // The dot is decorative and stays hidden, but its margins separate the
100
- // readings only on screen: without the flanking spaces a reader hears one
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 ? <IconCheckOutline16 /> : <IconCopyOutline16 />}
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
- <IconEditOutline16 />
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
- <IconRefreshOutline16 />
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
- <IconBranchOutline16 />
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 pairs with ui-conversation's InputBar `:has()` bottom clearance.
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 { IconDatabaseOutline16, IconGaugeOutline16 } from "@deepseek-ai/dsh-client-ui-primitives";
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
- <IconGaugeOutline16 />
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
- <IconGaugeOutline16 />
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
- <IconGaugeOutline16 />
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
- <IconDatabaseOutline16 />
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
- <IconDatabaseOutline16 />
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: InputBar's `.root:has([data-composer-stats])` rule
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 && (
@@ -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.effect(() => {
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 = ctx.get("webServer") as unknown as HttpServerLike | undefined;
651
- if (webServer === undefined) return () => {};
652
- const editor = ctx.sessionEditor;
653
- return webServer.register({
654
- kind: "exact",
655
- path: SESSION_EDITOR_PATH,
656
- handler: (request, response) => handleRoute(editor, request, response),
657
- });
658
- }, "session-editor: HTTP route");
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 { EmptySettings, meta, oneTurnLog } from "@morlay/session-rdb/testing";
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(