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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/client.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
+ "runTimeDot": "lKOSUG_runTimeDot",
356
+ "timeEnd": "lKOSUG_timeEnd",
357
+ "actions": "lKOSUG_actions",
358
+ "action": "lKOSUG_action",
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
+ "userStack": "okW0Ua_userStack",
572
+ "retryDetailLabel": "okW0Ua_retryDetailLabel",
573
+ "compactionTitle": "okW0Ua_compactionTitle",
574
+ "turnErrorRow": "okW0Ua_turnErrorRow",
575
+ "refChip": "okW0Ua_refChip",
576
+ "compactionContextIcon": "okW0Ua_compactionContextIcon",
577
+ "turnErrorMessage": "okW0Ua_turnErrorMessage",
578
+ "confirmActions": "okW0Ua_confirmActions",
579
+ "turnErrorDot": "okW0Ua_turnErrorDot",
580
+ "bubble": "okW0Ua_bubble",
581
+ "compactionSep": "okW0Ua_compactionSep",
582
+ "turnErrorCopy": "okW0Ua_turnErrorCopy",
583
+ "turnErrorTitle": "okW0Ua_turnErrorTitle",
584
+ "compactionBody": "okW0Ua_compactionBody",
585
+ "userRow": "okW0Ua_userRow",
586
+ "retry-shimmer": "okW0Ua_retry-shimmer",
587
+ "turnErrorCode": "okW0Ua_turnErrorCode",
588
+ "retryRow": "okW0Ua_retryRow",
589
+ "retrySummary": "okW0Ua_retrySummary",
590
+ "contextRow": "okW0Ua_contextRow",
591
+ "compactionRow": "okW0Ua_compactionRow",
592
+ "compactionButton": "okW0Ua_compactionButton",
593
+ "compactionLeading": "okW0Ua_compactionLeading",
594
+ "retryDetails": "okW0Ua_retryDetails",
595
+ "compactionDisclosureIcon": "okW0Ua_compactionDisclosureIcon",
596
+ "retryText": "okW0Ua_retryText",
597
+ "maxTokensTitle": "okW0Ua_maxTokensTitle",
598
+ "compactionSummary": "okW0Ua_compactionSummary"
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
+ });