@morlay/ui-conversation-message-actions 0.0.18 → 0.0.19-alpha.1

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