@morlay/ui-conversation-message-actions 0.0.11 → 0.0.13

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 (42) hide show
  1. package/README.md +0 -6
  2. package/dist/client.js +831 -0
  3. package/dist/index.d.mts +105 -0
  4. package/dist/index.mjs +3 -0
  5. package/dist/invariant.d.mts +7 -0
  6. package/dist/invariant.mjs +8 -0
  7. package/dist/plan-Bw4zoI4N.d.mts +86 -0
  8. package/dist/plan.d.mts +2 -0
  9. package/dist/plan.mjs +225 -0
  10. package/dist/src-CXYW0Bc0.mjs +408 -0
  11. package/dist/testing.d.mts +707 -0
  12. package/dist/testing.mjs +23690 -0
  13. package/package.json +60 -31
  14. package/src/client/chat-node/MessageEditDialog.module.css +24 -0
  15. package/src/client/chat-node/MessageEditDialog.tsx +71 -0
  16. package/src/client/chat-node/MessageIconActions.module.css +86 -0
  17. package/src/client/chat-node/MessageIconActions.tsx +189 -0
  18. package/src/client/chat-node/MessageItem.module.css +290 -0
  19. package/src/client/chat-node/MessageItem.tsx +198 -0
  20. package/src/client/chat-node/message-chrome.ts +61 -0
  21. package/src/client/chat-node/register.ts +33 -0
  22. package/src/client/chat-node/use-calendar-day.ts +23 -0
  23. package/src/client/controller.ts +327 -0
  24. package/src/client/css-modules.d.ts +4 -0
  25. package/src/client/import-action.tsx +74 -0
  26. package/src/client/index.ts +50 -0
  27. package/src/index.ts +650 -0
  28. package/src/invariant.ts +13 -0
  29. package/src/plan.ts +322 -0
  30. package/src/shared.ts +167 -0
  31. package/src/testing.ts +151 -0
  32. package/src/types.ts +73 -0
  33. package/lib/client.js +0 -2243
  34. package/lib/client.js.map +0 -1
  35. package/lib/index.d.mts +0 -211
  36. package/lib/index.d.mts.map +0 -1
  37. package/lib/index.mjs +0 -685
  38. package/lib/index.mjs.map +0 -1
  39. package/lib/invariant.d.mts +0 -15
  40. package/lib/invariant.d.mts.map +0 -1
  41. package/lib/invariant.mjs +0 -22
  42. package/lib/invariant.mjs.map +0 -1
package/dist/client.js ADDED
@@ -0,0 +1,831 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "@morlay/ui-conversation-message-actions",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ let _deepseek_ai_dsh_client_store = require("@deepseek-ai/dsh-client-store");
8
+ require("@deepseek-ai/dsh-client-ui-conversation/client");
9
+ require("@deepseek-ai/dsh-client-ui-chat/client");
10
+ let react = require("react");
11
+ let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
12
+ let react_jsx_runtime = require("react/jsx-runtime");
13
+ //#region src/shared.ts
14
+ const SESSION_EDITOR_PATH = "/session-editor";
15
+ //#endregion
16
+ //#region src/client/controller.ts
17
+ function messageOf(error) {
18
+ return error instanceof Error ? error.message : String(error);
19
+ }
20
+ function conversationRevision(snapshot) {
21
+ return [
22
+ snapshot.openState,
23
+ snapshot.removed,
24
+ snapshot.hasMore
25
+ ].join("|");
26
+ }
27
+ var SessionEditorController = class {
28
+ sessionId;
29
+ store = (0, _deepseek_ai_dsh_client_store.createSnapshotStore)({
30
+ status: "idle",
31
+ error: null,
32
+ pending: null,
33
+ timeline: null
34
+ });
35
+ face;
36
+ ctx;
37
+ sessions;
38
+ sessionSource;
39
+ sessionSourceDispose;
40
+ sessionRevision;
41
+ disposed = false;
42
+ users = 0;
43
+ navigationWaits = /* @__PURE__ */ new Set();
44
+ constructor(ctx, sessionId) {
45
+ this.sessionId = sessionId;
46
+ this.ctx = ctx;
47
+ this.sessions = ctx.get("sessions");
48
+ this.face = {
49
+ hooks: { sessionEditor: this.store },
50
+ acquire: () => {
51
+ this.users += 1;
52
+ if (this.users === 1 && this.disposed) this.revive();
53
+ return () => this.release();
54
+ },
55
+ load: () => {
56
+ this.load();
57
+ },
58
+ edit: (message, text, cascade) => this.mutate({
59
+ action: "edit",
60
+ sessionId: this.sessionId,
61
+ eventSeq: message.eventSeq,
62
+ blockIndex: message.blockIndex,
63
+ text,
64
+ cascade
65
+ }),
66
+ retry: (turn, cascade) => this.mutate({
67
+ action: "retry",
68
+ sessionId: this.sessionId,
69
+ turn,
70
+ cascade
71
+ }),
72
+ reroll: () => this.mutate({
73
+ action: "reroll",
74
+ sessionId: this.sessionId
75
+ }),
76
+ rewind: (toBoundary) => this.mutate({
77
+ action: "rewind",
78
+ sessionId: this.sessionId,
79
+ toBoundary
80
+ }),
81
+ importSession: (file) => this.importSession(file),
82
+ openVersion: (sessionId) => this.openWhenListed(sessionId)
83
+ };
84
+ this.observe();
85
+ }
86
+ observe() {
87
+ this.sessionSource = void 0;
88
+ this.sessionSourceDispose?.();
89
+ this.bindSessionSource();
90
+ this.sessions.list.subscribe(() => this.invalidate());
91
+ }
92
+ bindSessionSource() {
93
+ const source = this.sessions.binding(this.sessionId)?.session;
94
+ if (source === this.sessionSource) return;
95
+ this.sessionSourceDispose?.();
96
+ this.sessionSource = source;
97
+ this.sessionRevision = source === void 0 ? void 0 : conversationRevision(source.getSnapshot());
98
+ this.sessionSourceDispose = source?.subscribe(() => {
99
+ this.invalidate();
100
+ });
101
+ }
102
+ invalidate() {
103
+ if (this.disposed || this.store.getSnapshot().status === "idle") return;
104
+ this.load();
105
+ }
106
+ release() {
107
+ this.users -= 1;
108
+ if (this.users <= 0) this.dispose();
109
+ }
110
+ dispose() {
111
+ if (this.disposed) return;
112
+ this.disposed = true;
113
+ this.sessionSourceDispose?.();
114
+ this.sessionSourceDispose = void 0;
115
+ this.sessionSource = void 0;
116
+ this.sessionRevision = void 0;
117
+ }
118
+ revive() {
119
+ this.disposed = false;
120
+ this.observe();
121
+ this.load();
122
+ }
123
+ async load() {
124
+ if (this.disposed) return;
125
+ this.store.update((state) => {
126
+ state.status = "loading";
127
+ state.error = null;
128
+ });
129
+ try {
130
+ const response = await fetch(`${SESSION_EDITOR_PATH}?sessionId=${encodeURIComponent(this.sessionId)}`, {
131
+ method: "GET",
132
+ headers: { accept: "application/json" },
133
+ cache: "no-store"
134
+ });
135
+ const value = await response.json();
136
+ if (this.disposed) return;
137
+ if (response.ok) this.store.update((state) => {
138
+ state.status = "ready";
139
+ state.error = null;
140
+ state.timeline = value;
141
+ });
142
+ else {
143
+ const error = value["error"];
144
+ this.store.update((state) => {
145
+ state.status = "error";
146
+ state.error = typeof error === "string" ? error : `请求失败:HTTP ${String(response.status)}`;
147
+ });
148
+ }
149
+ } catch (error) {
150
+ if (this.disposed) return;
151
+ this.store.update((state) => {
152
+ state.status = "error";
153
+ state.error = messageOf(error);
154
+ });
155
+ }
156
+ }
157
+ async mutate(operation) {
158
+ if (this.store.getSnapshot().pending !== null) return false;
159
+ this.store.update((state) => {
160
+ state.pending = operation.action;
161
+ state.error = null;
162
+ });
163
+ try {
164
+ const response = await fetch(SESSION_EDITOR_PATH, {
165
+ method: "POST",
166
+ headers: {
167
+ accept: "application/json",
168
+ "content-type": "application/json"
169
+ },
170
+ body: JSON.stringify(operation)
171
+ });
172
+ const value = await response.json();
173
+ if (this.disposed) return true;
174
+ if (!response.ok) {
175
+ const error = value["error"];
176
+ throw new Error(typeof error === "string" ? error : `请求失败:HTTP ${String(response.status)}`);
177
+ }
178
+ this.store.update((state) => {
179
+ state.pending = null;
180
+ });
181
+ const result = value;
182
+ if (String(result.sessionId) !== String(this.sessionId)) {
183
+ await this.openWhenListed(result.sessionId);
184
+ return true;
185
+ }
186
+ const face = this.sessions.binding(this.sessionId)?.session;
187
+ const resync = face.resync;
188
+ if (resync !== void 0) try {
189
+ await resync.call(face);
190
+ this.load();
191
+ return true;
192
+ } catch {}
193
+ location.reload();
194
+ return true;
195
+ } catch (error) {
196
+ if (this.disposed) return false;
197
+ this.store.update((state) => {
198
+ state.pending = null;
199
+ state.error = messageOf(error);
200
+ });
201
+ return false;
202
+ }
203
+ }
204
+ async importSession(file) {
205
+ if (this.store.getSnapshot().pending !== null) return false;
206
+ this.store.update((state) => {
207
+ state.pending = "import";
208
+ state.error = null;
209
+ });
210
+ try {
211
+ const zip = await new Promise((resolve, reject) => {
212
+ const reader = new FileReader();
213
+ reader.onload = () => {
214
+ const dataUrl = typeof reader.result === "string" ? reader.result : "";
215
+ const comma = dataUrl.indexOf(",");
216
+ resolve(comma < 0 ? dataUrl : dataUrl.slice(comma + 1));
217
+ };
218
+ reader.onerror = () => reject(reader.error ?? /* @__PURE__ */ new Error("failed to read the selected file"));
219
+ reader.readAsDataURL(file);
220
+ });
221
+ const response = await fetch("/api/session.import", {
222
+ method: "POST",
223
+ headers: {
224
+ accept: "application/json",
225
+ "content-type": "application/json"
226
+ },
227
+ body: JSON.stringify({
228
+ zip,
229
+ sessionId: this.sessionId
230
+ })
231
+ });
232
+ const value = await response.json();
233
+ if (this.disposed) return true;
234
+ if (!response.ok) {
235
+ const error = value["error"];
236
+ throw new Error(typeof error === "string" ? error : `请求失败:HTTP ${String(response.status)}`);
237
+ }
238
+ this.store.update((state) => {
239
+ state.pending = null;
240
+ });
241
+ location.reload();
242
+ return true;
243
+ } catch (error) {
244
+ if (this.disposed) return false;
245
+ this.store.update((state) => {
246
+ state.pending = null;
247
+ state.error = messageOf(error);
248
+ });
249
+ return false;
250
+ }
251
+ }
252
+ openWhenListed(sessionId) {
253
+ if (this.sessions.list.getSnapshot().byId[sessionId] !== void 0) {
254
+ this.sessions.open(sessionId);
255
+ return Promise.resolve();
256
+ }
257
+ return new Promise((resolve) => {
258
+ let settled = false;
259
+ let dispose = () => {};
260
+ const finish = (open) => {
261
+ if (settled) return;
262
+ settled = true;
263
+ dispose();
264
+ this.navigationWaits.delete(cancel);
265
+ if (open) this.sessions.open(sessionId);
266
+ resolve();
267
+ };
268
+ const cancel = () => {
269
+ finish(false);
270
+ };
271
+ this.navigationWaits.add(cancel);
272
+ dispose = this.sessions.list.subscribe(() => {
273
+ if (this.sessions.list.getSnapshot().byId[sessionId] === void 0) return;
274
+ finish(true);
275
+ });
276
+ if (this.sessions.list.getSnapshot().byId[sessionId] !== void 0) finish(true);
277
+ });
278
+ }
279
+ };
280
+ //#endregion
281
+ //#region src/client/chat-node/message-chrome.ts
282
+ function pad2(n) {
283
+ return String(n).padStart(2, "0");
284
+ }
285
+ function startOfLocalDay(ms) {
286
+ const d = new Date(ms);
287
+ d.setHours(0, 0, 0, 0);
288
+ return d.getTime();
289
+ }
290
+ function msUntilNextLocalMidnight(ms) {
291
+ const next = new Date(ms);
292
+ next.setHours(24, 0, 0, 0);
293
+ return Math.max(next.getTime() - ms, 1);
294
+ }
295
+ function formatRunDuration(ms, t) {
296
+ const total = Math.max(0, Math.floor(ms / 1e3));
297
+ const minutes = Math.floor(total / 60);
298
+ const seconds = total % 60;
299
+ return minutes > 0 ? t("duration.minutes", {
300
+ minutes,
301
+ seconds: String(seconds).padStart(2, "0")
302
+ }) : t("duration.seconds", { seconds });
303
+ }
304
+ function formatLatencySeconds(ms) {
305
+ const s = Math.max(0, ms) / 1e3;
306
+ return s < 10 ? String(Math.round(s * 10) / 10) : String(Math.round(s));
307
+ }
308
+ function formatTokensPerSecond(tps) {
309
+ const clamped = Math.max(0, tps);
310
+ return clamped >= 10 ? String(Math.round(clamped)) : String(Math.round(clamped * 10) / 10);
311
+ }
312
+ function formatMessageClock(time, t, now = Date.now()) {
313
+ const d = new Date(time);
314
+ const n = new Date(now);
315
+ const clock = `${pad2(d.getHours())}:${pad2(d.getMinutes())}`;
316
+ if (d.getFullYear() === n.getFullYear() && d.getMonth() === n.getMonth() && d.getDate() === n.getDate()) return clock;
317
+ const params = {
318
+ y: d.getFullYear(),
319
+ m: d.getMonth() + 1,
320
+ d: d.getDate()
321
+ };
322
+ return `${d.getFullYear() === n.getFullYear() ? t("clock.md", params) : t("clock.ymd", params)} ${clock}`;
323
+ }
324
+ //#endregion
325
+ //#region src/client/chat-node/use-calendar-day.ts
326
+ function useCalendarDay() {
327
+ const [day, setDay] = (0, react.useState)(() => startOfLocalDay(Date.now()));
328
+ (0, react.useEffect)(() => {
329
+ let timer;
330
+ const arm = () => {
331
+ const now = Date.now();
332
+ setDay(startOfLocalDay(now));
333
+ timer = setTimeout(arm, msUntilNextLocalMidnight(now));
334
+ };
335
+ timer = setTimeout(arm, msUntilNextLocalMidnight(Date.now()));
336
+ return () => {
337
+ clearTimeout(timer);
338
+ };
339
+ }, []);
340
+ return day;
341
+ }
342
+ //#endregion
343
+ //#region \0dsh-css:/Users/morlay/src/github.com/morlay/better-session/packages/ui-conversation-message-actions/src/client/chat-node/MessageIconActions.module.css.mjs
344
+ const css$2 = ".lKOSUG_actions{align-items:center;gap:10px;height:28px;display:flex}.lKOSUG_timeStart{color:var(--dsw-alias-label-tertiary);white-space:nowrap;padding-right:12px;font-size:14px;line-height:24px}.lKOSUG_timeEnd{color:var(--dsw-alias-label-tertiary);white-space:nowrap;padding-left:12px;font-size:14px;line-height:24px}.lKOSUG_runTimeDot{margin:0 10px}@media (hover:hover){[data-time-hover-root] :is(.lKOSUG_timeStart,.lKOSUG_timeEnd){opacity:0;transition:opacity 80ms}[data-time-hover-root]:hover :is(.lKOSUG_timeStart,.lKOSUG_timeEnd),[data-time-hover-root]:focus-within :is(.lKOSUG_timeStart,.lKOSUG_timeEnd){opacity:1}}.lKOSUG_action{width:28px;height:28px;color:var(--dsw-alias-label-tertiary);cursor:pointer;background:0 0;border:none;border-radius:28px;justify-content:center;align-items:center;padding:6px;display:inline-flex}.lKOSUG_action:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary)}.lKOSUG_action[data-unavailable]{cursor:default;opacity:.4}.lKOSUG_action[data-unavailable]:hover{color:var(--dsw-alias-label-tertiary);background:0 0}.lKOSUG_visuallyHidden{clip:rect(0 0 0 0);white-space:nowrap;width:1px;height:1px;position:absolute;overflow:hidden}";
345
+ const tagId$2 = "@morlay/ui-conversation-message-actions/MessageIconActions.module.css";
346
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$2) + "]") === null) {
347
+ const tag = document.createElement("style");
348
+ tag.dataset.plugin = "@morlay/ui-conversation-message-actions";
349
+ tag.dataset.pluginCss = tagId$2;
350
+ tag.textContent = css$2;
351
+ document.head.appendChild(tag);
352
+ }
353
+ var MessageIconActions_module_css_default = {
354
+ "timeStart": "lKOSUG_timeStart",
355
+ "action": "lKOSUG_action",
356
+ "timeEnd": "lKOSUG_timeEnd",
357
+ "actions": "lKOSUG_actions",
358
+ "runTimeDot": "lKOSUG_runTimeDot",
359
+ "visuallyHidden": "lKOSUG_visuallyHidden"
360
+ };
361
+ //#endregion
362
+ //#region src/client/chat-node/MessageIconActions.tsx
363
+ function MessageIconActions({ text, time, runMs, ttftMs, tokensPerSecond, clock, onBranch, branchUnavailable = false, className, extraActions, onEdit, onRetry, t }) {
364
+ const day = useCalendarDay();
365
+ const reasonId = (0, react.useId)();
366
+ const [copied, setCopied] = (0, react.useState)(false);
367
+ const copyPending = (0, react.useRef)(false);
368
+ const copyTimer = (0, react.useRef)(null);
369
+ const copyEpoch = (0, react.useRef)(0);
370
+ (0, react.useEffect)(() => () => {
371
+ copyEpoch.current += 1;
372
+ copyPending.current = false;
373
+ if (copyTimer.current !== null) clearTimeout(copyTimer.current);
374
+ }, []);
375
+ const onCopy = (0, react.useCallback)(() => {
376
+ if (copied || copyPending.current) return;
377
+ const epoch = copyEpoch.current;
378
+ copyPending.current = true;
379
+ (0, _deepseek_ai_dsh_client_ui_primitives.writeClipboard)(text).then((ok) => {
380
+ if (epoch !== copyEpoch.current) return;
381
+ copyPending.current = false;
382
+ if (!ok) return;
383
+ setCopied(true);
384
+ copyTimer.current = window.setTimeout(() => {
385
+ copyTimer.current = null;
386
+ setCopied(false);
387
+ }, 1e3);
388
+ });
389
+ }, [copied, text]);
390
+ const clockEl = time === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
391
+ className: clock === "start" ? MessageIconActions_module_css_default.timeStart : MessageIconActions_module_css_default.timeEnd,
392
+ children: [
393
+ formatMessageClock(time, t, day),
394
+ runMs !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
395
+ " ",
396
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
397
+ className: MessageIconActions_module_css_default.runTimeDot,
398
+ "aria-hidden": true,
399
+ children: "·"
400
+ }),
401
+ " ",
402
+ t("message.ranFor", { duration: formatRunDuration(runMs, t) })
403
+ ] }),
404
+ ttftMs !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
405
+ " ",
406
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
407
+ className: MessageIconActions_module_css_default.runTimeDot,
408
+ "aria-hidden": true,
409
+ children: "·"
410
+ }),
411
+ " ",
412
+ t("stats.ttftAverage", { duration: formatLatencySeconds(ttftMs) })
413
+ ] }),
414
+ tokensPerSecond !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
415
+ " ",
416
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
417
+ className: MessageIconActions_module_css_default.runTimeDot,
418
+ "aria-hidden": true,
419
+ children: "·"
420
+ }),
421
+ " ",
422
+ t("message.tokensPerSecond", { tps: formatTokensPerSecond(tokensPerSecond) })
423
+ ] })
424
+ ]
425
+ });
426
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
427
+ className: className === void 0 ? MessageIconActions_module_css_default.actions : `${MessageIconActions_module_css_default.actions} ${className}`,
428
+ children: [
429
+ clock === "start" ? clockEl : null,
430
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
431
+ label: copied ? t("copied") : t("copy"),
432
+ side: "bottom",
433
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
434
+ type: "button",
435
+ className: MessageIconActions_module_css_default.action,
436
+ "aria-label": copied ? t("copied") : t("copy"),
437
+ onClick: onCopy,
438
+ children: copied ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutline16, {}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCopyOutline16, {})
439
+ })
440
+ }),
441
+ extraActions,
442
+ onEdit !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
443
+ label: "编辑",
444
+ side: "bottom",
445
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
446
+ type: "button",
447
+ className: MessageIconActions_module_css_default.action,
448
+ "aria-label": "编辑",
449
+ onClick: onEdit,
450
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconEditOutline16, {})
451
+ })
452
+ }),
453
+ onRetry !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
454
+ label: "重试此回合",
455
+ side: "bottom",
456
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
457
+ type: "button",
458
+ className: MessageIconActions_module_css_default.action,
459
+ "aria-label": "重试此回合",
460
+ onClick: onRetry,
461
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconRefreshOutline16, {})
462
+ })
463
+ }),
464
+ onBranch !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
465
+ label: branchUnavailable ? t("message.branchUnavailable") : t("message.branch"),
466
+ side: "bottom",
467
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
468
+ type: "button",
469
+ className: MessageIconActions_module_css_default.action,
470
+ "aria-label": t("message.branch"),
471
+ "aria-disabled": branchUnavailable || void 0,
472
+ "aria-describedby": branchUnavailable ? reasonId : void 0,
473
+ "data-unavailable": branchUnavailable || void 0,
474
+ onClick: branchUnavailable ? void 0 : onBranch,
475
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconBranchOutline16, {})
476
+ })
477
+ }),
478
+ onBranch !== void 0 && branchUnavailable && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
479
+ id: reasonId,
480
+ className: MessageIconActions_module_css_default.visuallyHidden,
481
+ children: t("message.branchUnavailable")
482
+ }),
483
+ clock === "end" ? clockEl : null
484
+ ]
485
+ });
486
+ }
487
+ //#endregion
488
+ //#region \0dsh-css:/Users/morlay/src/github.com/morlay/better-session/packages/ui-conversation-message-actions/src/client/chat-node/MessageEditDialog.module.css.mjs
489
+ const css$1 = "._7VqDXa_input{box-sizing:border-box;width:100%;min-height:96px;font:inherit;border:1px solid var(--dsw-alias-border-l2-darkmode-thin);background:var(--dsw-specific-input-major);box-shadow:var(--dsw-shadow-lv2);color:var(--dsw-text,inherit);resize:none;border-radius:22px;padding:10px 16px;font-size:16px;line-height:24px;overflow-y:auto}._7VqDXa_actions{justify-content:flex-end;gap:8px;display:flex}";
490
+ const tagId$1 = "@morlay/ui-conversation-message-actions/MessageEditDialog.module.css";
491
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]") === null) {
492
+ const tag = document.createElement("style");
493
+ tag.dataset.plugin = "@morlay/ui-conversation-message-actions";
494
+ tag.dataset.pluginCss = tagId$1;
495
+ tag.textContent = css$1;
496
+ document.head.appendChild(tag);
497
+ }
498
+ var MessageEditDialog_module_css_default = {
499
+ "input": "_7VqDXa_input",
500
+ "actions": "_7VqDXa_actions"
501
+ };
502
+ //#endregion
503
+ //#region src/client/chat-node/MessageEditDialog.tsx
504
+ const BLOCK_TITLE = {
505
+ user: "编辑用户消息",
506
+ "assistant.reasoning": "编辑助手思考",
507
+ "assistant.response": "编辑助手回复"
508
+ };
509
+ function MessageEditDialog({ block, onSave, onClose }) {
510
+ const [text, setText] = (0, react.useState)(block.text);
511
+ const [saving, setSaving] = (0, react.useState)(false);
512
+ const inputRef = (0, react.useRef)(null);
513
+ const autosize = () => {
514
+ const el = inputRef.current;
515
+ if (el === null) return;
516
+ el.style.height = "auto";
517
+ el.style.height = `${el.scrollHeight}px`;
518
+ };
519
+ const save = () => {
520
+ if (saving) return;
521
+ setSaving(true);
522
+ onSave(text).then((applied) => {
523
+ if (applied) onClose();
524
+ else setSaving(false);
525
+ });
526
+ };
527
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
528
+ open: true,
529
+ onClose,
530
+ title: BLOCK_TITLE[block.kind],
531
+ closeLabel: "关闭",
532
+ footer: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
533
+ className: MessageEditDialog_module_css_default.actions,
534
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
535
+ variant: "outline",
536
+ onClick: onClose,
537
+ disabled: saving,
538
+ children: "取消"
539
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
540
+ variant: "primary",
541
+ onClick: save,
542
+ disabled: saving,
543
+ children: saving ? "保存中…" : "保存"
544
+ })]
545
+ }),
546
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
547
+ ref: inputRef,
548
+ className: MessageEditDialog_module_css_default.input,
549
+ value: text,
550
+ onChange: (event) => {
551
+ setText(event.target.value);
552
+ autosize();
553
+ },
554
+ autoFocus: true,
555
+ rows: 4
556
+ })
557
+ });
558
+ }
559
+ //#endregion
560
+ //#region \0dsh-css:/Users/morlay/src/github.com/morlay/better-session/packages/ui-conversation-message-actions/src/client/chat-node/MessageItem.module.css.mjs
561
+ const css = ".okW0Ua_userRow{flex-direction:column;align-items:flex-end;gap:6px;display:flex}.okW0Ua_userStack{flex-direction:column;align-items:flex-end;gap:8px;min-width:0;max-width:min(525px,82%);display:flex}.okW0Ua_bubble{background:var(--dsw-specific-bubble);max-width:100%;color:var(--dsw-alias-label-primary);border-radius:22px;padding:10px 16px;font-size:16px;line-height:24px}.okW0Ua_contextRow,.okW0Ua_compactionRow{padding:2px 0}.okW0Ua_compactionButton{width:100%;min-width:0;height:24px;color:inherit;font:inherit;text-align:left;background:0 0;border:none;border-radius:6px;align-items:center;padding:0;display:flex}.okW0Ua_compactionButton:not(:disabled){cursor:pointer}.okW0Ua_compactionButton:not(:disabled):hover{background:var(--dsw-alias-interactive-bg-hover)}.okW0Ua_compactionLeading{width:16px;height:16px;color:var(--dsw-alias-label-secondary);flex:none;place-items:center;margin-right:6px;display:inline-grid}.okW0Ua_compactionContextIcon,.okW0Ua_compactionDisclosureIcon{grid-area:1/1;justify-content:center;align-items:center;display:inline-flex}.okW0Ua_compactionDisclosureIcon,.okW0Ua_compactionButton:not(:disabled):hover .okW0Ua_compactionContextIcon,.okW0Ua_compactionButton:not(:disabled):focus-visible .okW0Ua_compactionContextIcon{opacity:0}.okW0Ua_compactionButton:not(:disabled):hover .okW0Ua_compactionDisclosureIcon,.okW0Ua_compactionButton:not(:disabled):focus-visible .okW0Ua_compactionDisclosureIcon{opacity:1}.okW0Ua_compactionTitle{color:var(--dsw-alias-label-primary-dimmed);flex:none;font-size:14px;line-height:24px}.okW0Ua_compactionSep{background:var(--dsw-alias-label-caption);border-radius:1px;flex:none;width:2px;height:2px;margin:0 8px}.okW0Ua_compactionSummary{min-width:0;color:var(--dsw-alias-label-tertiary);text-overflow:ellipsis;white-space:nowrap;flex:auto;font-size:14px;line-height:24px;overflow:hidden}.okW0Ua_compactionBody{color:var(--dsw-alias-label-tertiary);padding:4px 0 4px 22px;font-size:14px;line-height:24px}.okW0Ua_retryRow{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:20px}.okW0Ua_retrySummary{width:fit-content;color:inherit;cursor:pointer;user-select:none;border-radius:3px;align-items:center;gap:7px;padding:2px 0;list-style:none;display:inline-flex}.okW0Ua_retrySummary::-webkit-details-marker{display:none}.okW0Ua_retrySummary:after{content:\"\";opacity:.8;border-bottom:1.5px solid;border-right:1.5px solid;width:6px;height:6px;transition:transform .12s;transform:rotate(-45deg)}.okW0Ua_retrySummary:hover{color:var(--dsw-alias-label-secondary)}.okW0Ua_retrySummary:focus-visible{outline:1.5px solid var(--dsw-alias-button-info-fill);outline-offset:2px}.okW0Ua_retryText{color:inherit}.okW0Ua_retryRow[data-active] .okW0Ua_retryText{background:linear-gradient(90deg, var(--dsw-alias-label-tertiary) 0%, var(--dsw-alias-label-tertiary) 40%, var(--dsw-alias-label-secondary) 50%, var(--dsw-alias-label-tertiary) 60%, var(--dsw-alias-label-tertiary) 100%);color:#0000;background-position:100%;background-size:200% 100%;background-clip:text;animation:1.6s ease-in-out infinite okW0Ua_retry-shimmer}.okW0Ua_retryRow[open] .okW0Ua_retrySummary:after{transform:rotate(45deg)}.okW0Ua_retryDetails{overflow-wrap:anywhere;gap:2px;margin-top:3px;padding-left:14px;font-size:12px;line-height:18px;display:grid}.okW0Ua_retryDetailLabel{color:var(--dsw-alias-label-secondary)}.okW0Ua_turnErrorRow{grid-template-columns:10px minmax(0,1fr) auto;align-items:start;gap:8px;padding:2px 0;font-size:13px;line-height:20px;display:grid}.okW0Ua_turnErrorDot{margin-top:5px}.okW0Ua_turnErrorCopy{overflow-wrap:anywhere;min-width:0}.okW0Ua_turnErrorTitle{color:var(--dsw-alias-state-error-primary);margin-right:6px;font-weight:600}.okW0Ua_turnErrorMessage{color:var(--dsw-alias-label-secondary)}.okW0Ua_turnErrorCode{color:var(--dsw-alias-label-tertiary);font:var(--dsw-font-markdown-code-block-small)}.okW0Ua_maxTokensTitle{color:var(--dsw-alias-state-warn-primary);margin-right:6px;font-weight:600}@keyframes okW0Ua_retry-shimmer{0%{background-position:100%}to{background-position:0}}@media (prefers-reduced-motion:reduce){.okW0Ua_retryRow[data-active] .okW0Ua_retryText{color:inherit;background:0 0;animation:none}}.okW0Ua_refChip{color:var(--dsw-alias-label-primary);white-space:nowrap;vertical-align:baseline;background:#6187d838;border-radius:6px;margin:0 2px;padding:0 8px;font-size:.85em;line-height:1.6;display:inline-block}.okW0Ua_confirmActions{justify-content:flex-end;gap:8px;display:flex}";
562
+ const tagId = "@morlay/ui-conversation-message-actions/MessageItem.module.css";
563
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
564
+ const tag = document.createElement("style");
565
+ tag.dataset.plugin = "@morlay/ui-conversation-message-actions";
566
+ tag.dataset.pluginCss = tagId;
567
+ tag.textContent = css;
568
+ document.head.appendChild(tag);
569
+ }
570
+ var MessageItem_module_css_default = {
571
+ "compactionBody": "okW0Ua_compactionBody",
572
+ "userRow": "okW0Ua_userRow",
573
+ "turnErrorCode": "okW0Ua_turnErrorCode",
574
+ "compactionLeading": "okW0Ua_compactionLeading",
575
+ "bubble": "okW0Ua_bubble",
576
+ "retryText": "okW0Ua_retryText",
577
+ "compactionButton": "okW0Ua_compactionButton",
578
+ "retrySummary": "okW0Ua_retrySummary",
579
+ "compactionDisclosureIcon": "okW0Ua_compactionDisclosureIcon",
580
+ "compactionSummary": "okW0Ua_compactionSummary",
581
+ "retryDetailLabel": "okW0Ua_retryDetailLabel",
582
+ "retryDetails": "okW0Ua_retryDetails",
583
+ "turnErrorRow": "okW0Ua_turnErrorRow",
584
+ "compactionContextIcon": "okW0Ua_compactionContextIcon",
585
+ "turnErrorCopy": "okW0Ua_turnErrorCopy",
586
+ "turnErrorMessage": "okW0Ua_turnErrorMessage",
587
+ "maxTokensTitle": "okW0Ua_maxTokensTitle",
588
+ "compactionRow": "okW0Ua_compactionRow",
589
+ "retry-shimmer": "okW0Ua_retry-shimmer",
590
+ "refChip": "okW0Ua_refChip",
591
+ "turnErrorDot": "okW0Ua_turnErrorDot",
592
+ "confirmActions": "okW0Ua_confirmActions",
593
+ "compactionSep": "okW0Ua_compactionSep",
594
+ "compactionTitle": "okW0Ua_compactionTitle",
595
+ "userStack": "okW0Ua_userStack",
596
+ "contextRow": "okW0Ua_contextRow",
597
+ "retryRow": "okW0Ua_retryRow",
598
+ "turnErrorTitle": "okW0Ua_turnErrorTitle"
599
+ };
600
+ //#endregion
601
+ //#region src/client/chat-node/MessageItem.tsx
602
+ function contentParts(content) {
603
+ const texts = [];
604
+ const images = [];
605
+ const rest = [];
606
+ for (const block of content) {
607
+ const b = block;
608
+ if (b.type === "text" && typeof b.text === "string") texts.push(b.text);
609
+ else if (b.type === "image" && b.attachment !== void 0) images.push({ attachment: b.attachment });
610
+ else rest.push(block);
611
+ }
612
+ return {
613
+ text: texts.join(""),
614
+ images,
615
+ rest
616
+ };
617
+ }
618
+ function projectUserText(text) {
619
+ const re = /(^|\s)([/@][\w-]+)(?=\s|$)/g;
620
+ const parts = [];
621
+ let cursor = 0;
622
+ let m;
623
+ while ((m = re.exec(text)) !== null) {
624
+ const tokenStart = m.index + (m[1]?.length ?? 0);
625
+ const label = m[2] ?? "";
626
+ if (tokenStart > cursor) parts.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.MessageText, { text: text.slice(cursor, tokenStart) }, cursor));
627
+ parts.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
628
+ className: MessageItem_module_css_default.refChip,
629
+ "data-ref-chip": label.startsWith("@") ? "subagent" : "skill",
630
+ children: label
631
+ }, tokenStart));
632
+ cursor = tokenStart + label.length;
633
+ }
634
+ if (parts.length === 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.MessageText, { text });
635
+ if (cursor < text.length) parts.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.MessageText, { text: text.slice(cursor) }, cursor));
636
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: parts });
637
+ }
638
+ function UserStyleBubble({ content, renderMessageImages, actions, t }) {
639
+ const { text, images, rest } = contentParts(content);
640
+ const truncated = (total) => t("json.truncated", { total });
641
+ const showBubble = text !== "" || rest.length > 0;
642
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
643
+ className: MessageItem_module_css_default.userRow,
644
+ "data-time-hover-root": true,
645
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
646
+ className: MessageItem_module_css_default.userStack,
647
+ children: [renderMessageImages({
648
+ images,
649
+ align: "end"
650
+ }), showBubble && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
651
+ className: MessageItem_module_css_default.bubble,
652
+ children: [projectUserText(text), rest.map((block, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.JsonBlock, {
653
+ label: t("message.extraBlock"),
654
+ payload: block,
655
+ truncatedLabel: truncated
656
+ }, i))]
657
+ })]
658
+ }), actions?.(text)]
659
+ });
660
+ }
661
+ const UserMessageNodeView = (0, react.memo)(function UserMessageNodeView({ node, renderMessageImages, t, edit, retry }) {
662
+ const data = node.data;
663
+ const [editing, setEditing] = (0, react.useState)(null);
664
+ const [confirmingRetry, setConfirmingRetry] = (0, react.useState)(false);
665
+ const turnLocation = node.location.kind === "turn" || node.location.kind === "step" ? node.location.turn : void 0;
666
+ const turn = turnLocation?.turn;
667
+ const retryable = turnLocation?.status === "closed";
668
+ const textBlockIndex = data.content.findIndex((block) => block.type === "text");
669
+ const textBlock = textBlockIndex === -1 ? void 0 : data.content[textBlockIndex];
670
+ const onEdit = textBlock === void 0 || turn === void 0 ? void 0 : () => {
671
+ setEditing({
672
+ key: `${node.anchorSeq}:${String(textBlockIndex)}`,
673
+ turn,
674
+ eventSeq: node.anchorSeq,
675
+ blockIndex: textBlockIndex,
676
+ kind: "user",
677
+ text: textBlock.text ?? "",
678
+ time: data.time
679
+ });
680
+ };
681
+ const onRetry = retryable && turn !== void 0 ? () => {
682
+ setConfirmingRetry(true);
683
+ } : void 0;
684
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
685
+ editing !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MessageEditDialog, {
686
+ block: editing,
687
+ onSave: (text) => edit(editing, text, "truncate"),
688
+ onClose: () => setEditing(null)
689
+ }),
690
+ confirmingRetry && turn !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
691
+ open: true,
692
+ onClose: () => setConfirmingRetry(false),
693
+ title: "重试回合",
694
+ closeLabel: "关闭",
695
+ description: `将重新生成第 ${turn} 轮的回复,并抛弃该回合之后的内容。`,
696
+ footer: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
697
+ className: MessageItem_module_css_default.confirmActions,
698
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
699
+ variant: "outline",
700
+ onClick: () => setConfirmingRetry(false),
701
+ children: "取消"
702
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
703
+ variant: "primary",
704
+ onClick: () => {
705
+ setConfirmingRetry(false);
706
+ retry(turn, "truncate");
707
+ },
708
+ children: "确认重试"
709
+ })]
710
+ })
711
+ }),
712
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UserStyleBubble, {
713
+ content: data.content,
714
+ renderMessageImages,
715
+ t,
716
+ actions: (text) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MessageIconActions, {
717
+ text,
718
+ time: data.time,
719
+ clock: "start",
720
+ className: MessageItem_module_css_default.actions,
721
+ t,
722
+ onEdit,
723
+ onRetry
724
+ })
725
+ })
726
+ ] });
727
+ });
728
+ //#endregion
729
+ //#region src/client/chat-node/register.ts
730
+ const NS = "conversation";
731
+ function registerChatNodeRenderers(ctx, controllerFor) {
732
+ const injectFace = (sessionId) => controllerFor(sessionId).face;
733
+ for (const key of ["user", "steering"]) ctx.slots.inject("conversation.chat.node", () => ctx.slots.register({
734
+ name: "conversation.chat.node",
735
+ key,
736
+ locale: NS,
737
+ priority: -1,
738
+ inject: injectFace
739
+ }, UserMessageNodeView));
740
+ }
741
+ //#endregion
742
+ //#region src/client/import-action.tsx
743
+ const IconUpload = ({ size = 14, className }) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
744
+ width: size,
745
+ height: size,
746
+ className,
747
+ viewBox: "0 0 24 24",
748
+ fill: "none",
749
+ xmlns: "http://www.w3.org/2000/svg",
750
+ stroke: "currentColor",
751
+ "stroke-width": "2",
752
+ "stroke-linecap": "round",
753
+ "stroke-linejoin": "round",
754
+ children: [
755
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M12 3v12" }),
756
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "m17 8-5-5-5 5" }),
757
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" })
758
+ ]
759
+ });
760
+ function SessionImportAction({ useSessionEditor, importSession }) {
761
+ const inputRef = (0, react.useRef)(null);
762
+ const importing = useSessionEditor((s) => s.pending === "import");
763
+ const error = useSessionEditor((s) => s.error);
764
+ const [busy, setBusy] = (0, react.useState)(false);
765
+ const onPick = (0, react.useCallback)((file) => {
766
+ if (file === void 0) return;
767
+ setBusy(true);
768
+ importSession(file).finally(() => setBusy(false));
769
+ }, [importSession]);
770
+ const working = busy || importing;
771
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
772
+ size: "sm",
773
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconUpload, {}),
774
+ disabled: working,
775
+ "aria-busy": working,
776
+ "aria-label": working ? "导入中…" : "导入会话",
777
+ title: error ?? "导入会话:用导出的 zip 覆盖当前会话内容",
778
+ onClick: () => inputRef.current?.click(),
779
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
780
+ ref: inputRef,
781
+ type: "file",
782
+ accept: ".zip,application/zip",
783
+ style: { display: "none" },
784
+ onChange: (e) => {
785
+ const file = e.target.files?.[0];
786
+ e.target.value = "";
787
+ onPick(file);
788
+ }
789
+ })
790
+ }) });
791
+ }
792
+ //#endregion
793
+ //#region src/client/index.ts
794
+ const inject = [
795
+ "slots",
796
+ "conversation",
797
+ "connection",
798
+ "sessions"
799
+ ];
800
+ function apply(ctx) {
801
+ const controllers = /* @__PURE__ */ new Map();
802
+ const controllerFor = (sessionId) => {
803
+ let controller = controllers.get(sessionId);
804
+ if (controller === void 0) {
805
+ controller = new SessionEditorController(ctx, sessionId);
806
+ controllers.set(sessionId, controller);
807
+ }
808
+ return controller;
809
+ };
810
+ ctx.on("connection/reset", () => {
811
+ for (const controller of controllers.values()) controller.load();
812
+ });
813
+ registerChatNodeRenderers(ctx, controllerFor);
814
+ ctx.slots.inject("conversation.session.header.utilities", () => ctx.slots.register({
815
+ name: "conversation.session.header.utilities",
816
+ id: "session-editor.import",
817
+ inject: (sessionId) => {
818
+ const face = controllerFor(sessionId).face;
819
+ return {
820
+ hooks: face.hooks,
821
+ importSession: (file) => face.importSession(file)
822
+ };
823
+ }
824
+ }, SessionImportAction));
825
+ }
826
+ //#endregion
827
+ exports.apply = apply;
828
+ exports.inject = inject;
829
+ return module.exports;
830
+ }
831
+ });