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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +29 -98
  2. package/dist/client.cjs +1380 -0
  3. package/dist/client.d.cts +6 -0
  4. package/dist/client.d.mts +6 -0
  5. package/dist/index.d.cts +98 -0
  6. package/dist/index.d.mts +3 -10
  7. package/dist/index.mjs +460 -2
  8. package/dist/invariant.d.cts +7 -0
  9. package/dist/{plan-BSa7k9HO.d.mts → plan-CT_6Y1fD.d.cts} +11 -9
  10. package/dist/plan-Cm9hCord.d.mts +101 -0
  11. package/dist/plan.d.cts +2 -0
  12. package/dist/plan.d.mts +2 -2
  13. package/dist/plan.mjs +145 -11
  14. package/dist/shared-LK3nImWW.mjs +55 -0
  15. package/dist/testing.d.cts +3242 -0
  16. package/dist/testing.d.mts +25 -19
  17. package/dist/testing.mjs +2140 -3939
  18. package/package.json +35 -32
  19. package/src/client/chat-node/MessageIconActions.styles.ts +76 -0
  20. package/src/client/chat-node/MessageIconActions.tsx +30 -11
  21. package/src/client/chat-node/MessageItem.styles.ts +247 -0
  22. package/src/client/chat-node/MessageItem.tsx +32 -39
  23. package/src/client/chat-node/message-chrome.ts +0 -2
  24. package/src/client/chat-node/register.ts +3 -4
  25. package/src/client/chat-node/use-calendar-day.ts +0 -4
  26. package/src/client/composer-stats/StatsPills.styles.ts +57 -0
  27. package/src/client/composer-stats/StatsPills.tsx +384 -0
  28. package/src/client/composer-stats/register.ts +17 -0
  29. package/src/client/composer-stats/stat-dialog.styles.ts +70 -0
  30. package/src/client/composer-stats/stat-dialog.ts +52 -0
  31. package/src/client/composer-stats/token-format.ts +85 -0
  32. package/src/client/composer-stats/turn-reading.ts +35 -0
  33. package/src/client/controller.ts +32 -114
  34. package/src/client/index.ts +8 -24
  35. package/src/index.ts +102 -111
  36. package/src/plan.ts +189 -24
  37. package/src/shared.ts +1 -1
  38. package/src/testing.ts +1 -5
  39. package/src/types.ts +1 -1
  40. package/dist/client.js +0 -806
  41. package/dist/src-B4HwXsob.mjs +0 -499
  42. package/src/client/chat-node/MessageIconActions.module.css +0 -86
  43. package/src/client/chat-node/MessageItem.module.css +0 -290
  44. package/src/client/css-modules.d.ts +0 -4
  45. package/src/client/import-action.tsx +0 -74
@@ -0,0 +1,1380 @@
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("@morlay/dsh-client-ui-conversation/client");
9
+ let _morlay_dsh_client_ui_primitives_client = require("@morlay/dsh-client-ui-primitives/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
+ let react_dom = require("react-dom");
14
+ //#region src/shared.ts
15
+ const SESSION_EDITOR_PATH = "/session-editor";
16
+ //#endregion
17
+ //#region src/client/controller.ts
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
+ uiWorkspace;
43
+ sessionSource;
44
+ sessionSourceDispose;
45
+ sessionRevision;
46
+ disposed = false;
47
+ users = 0;
48
+ constructor(ctx, sessionId) {
49
+ this.sessionId = sessionId;
50
+ this.ctx = ctx;
51
+ this.sessions = ctx.get("sessions");
52
+ this.uiWorkspace = ctx.get("uiWorkspace");
53
+ this.face = {
54
+ hooks: { sessionEditor: this.store },
55
+ acquire: () => {
56
+ this.users += 1;
57
+ if (this.users === 1 && this.disposed) this.revive();
58
+ return () => this.release();
59
+ },
60
+ load: () => {
61
+ this.load();
62
+ },
63
+ edit: (message, text, cascade) => this.mutate({
64
+ action: "edit",
65
+ sessionId: this.sessionId,
66
+ eventSeq: message.eventSeq,
67
+ blockIndex: message.blockIndex,
68
+ text,
69
+ cascade
70
+ }),
71
+ retry: (turn, cascade) => this.mutate({
72
+ action: "retry",
73
+ sessionId: this.sessionId,
74
+ turn,
75
+ cascade
76
+ }),
77
+ reroll: () => this.mutate({
78
+ action: "reroll",
79
+ sessionId: this.sessionId
80
+ }),
81
+ rewind: (toBoundary) => this.mutate({
82
+ action: "rewind",
83
+ sessionId: this.sessionId,
84
+ toBoundary
85
+ }),
86
+ recall: (message) => this.mutate({
87
+ action: "recall",
88
+ sessionId: this.sessionId,
89
+ eventSeq: message.eventSeq
90
+ }, () => this.setComposerDraft(this.messageTexts(message))),
91
+ openVersion: (sessionId) => {
92
+ this.uiWorkspace.openSession(sessionId);
93
+ return Promise.resolve();
94
+ }
95
+ };
96
+ this.observe();
97
+ }
98
+ observe() {
99
+ this.sessionSource = void 0;
100
+ this.sessionSourceDispose?.();
101
+ this.bindSessionSource();
102
+ this.sessions.list.subscribe(() => this.invalidate());
103
+ }
104
+ bindSessionSource() {
105
+ const source = this.sessions.binding(this.sessionId)?.session;
106
+ if (source === this.sessionSource) return;
107
+ this.sessionSourceDispose?.();
108
+ this.sessionSource = source;
109
+ this.sessionRevision = source === void 0 ? void 0 : conversationRevision(source.getSnapshot());
110
+ this.sessionSourceDispose = source?.subscribe(() => {
111
+ this.invalidate();
112
+ });
113
+ }
114
+ invalidate() {
115
+ if (this.disposed || this.store.getSnapshot().status === "idle") return;
116
+ this.load();
117
+ }
118
+ release() {
119
+ this.users -= 1;
120
+ if (this.users <= 0) this.dispose();
121
+ }
122
+ dispose() {
123
+ if (this.disposed) return;
124
+ this.disposed = true;
125
+ this.sessionSourceDispose?.();
126
+ this.sessionSourceDispose = void 0;
127
+ this.sessionSource = void 0;
128
+ this.sessionRevision = void 0;
129
+ }
130
+ revive() {
131
+ this.disposed = false;
132
+ this.observe();
133
+ this.load();
134
+ }
135
+ async load() {
136
+ if (this.disposed) return;
137
+ this.store.update((state) => {
138
+ state.status = "loading";
139
+ state.error = null;
140
+ });
141
+ try {
142
+ const response = await fetch(`${editorApiPath()}?sessionId=${encodeURIComponent(this.sessionId)}`, {
143
+ method: "GET",
144
+ headers: { accept: "application/json" },
145
+ cache: "no-store"
146
+ });
147
+ const value = await response.json();
148
+ if (this.disposed) return;
149
+ if (response.ok) this.store.update((state) => {
150
+ state.status = "ready";
151
+ state.error = null;
152
+ state.timeline = value;
153
+ });
154
+ else {
155
+ const error = value["error"];
156
+ this.store.update((state) => {
157
+ state.status = "error";
158
+ state.error = typeof error === "string" ? error : `请求失败:HTTP ${String(response.status)}`;
159
+ });
160
+ }
161
+ } catch (error) {
162
+ if (this.disposed) return;
163
+ this.store.update((state) => {
164
+ state.status = "error";
165
+ state.error = messageOf(error);
166
+ });
167
+ }
168
+ }
169
+ async mutate(operation, onApplied) {
170
+ if (this.store.getSnapshot().pending !== null) return false;
171
+ this.store.update((state) => {
172
+ state.pending = operation.action;
173
+ state.error = null;
174
+ });
175
+ try {
176
+ const response = await fetch(editorApiPath(), {
177
+ method: "POST",
178
+ headers: {
179
+ accept: "application/json",
180
+ "content-type": "application/json"
181
+ },
182
+ body: JSON.stringify(operation)
183
+ });
184
+ const value = await response.json();
185
+ if (this.disposed) return true;
186
+ if (!response.ok) {
187
+ const error = value["error"];
188
+ throw new Error(typeof error === "string" ? error : `请求失败:HTTP ${String(response.status)}`);
189
+ }
190
+ this.store.update((state) => {
191
+ state.pending = null;
192
+ });
193
+ onApplied?.();
194
+ const result = value;
195
+ if (String(result.sessionId) !== String(this.sessionId)) {
196
+ this.uiWorkspace.openSession(result.sessionId);
197
+ return true;
198
+ }
199
+ const face = this.sessions.binding(this.sessionId)?.session;
200
+ const resync = face.resync;
201
+ if (resync !== void 0) try {
202
+ await resync.call(face);
203
+ const projections = face.projections;
204
+ if (typeof projections?.truncate !== "function") {
205
+ location.reload();
206
+ return true;
207
+ }
208
+ projections.truncate(-1);
209
+ this.load();
210
+ return true;
211
+ } catch {}
212
+ location.reload();
213
+ return true;
214
+ } catch (error) {
215
+ if (this.disposed) return false;
216
+ this.store.update((state) => {
217
+ state.pending = null;
218
+ state.error = messageOf(error);
219
+ });
220
+ return false;
221
+ }
222
+ }
223
+ messageTexts(message) {
224
+ const prefix = `${String(message.eventSeq)}:`;
225
+ const blocks = (this.store.getSnapshot().timeline?.messages ?? []).filter((row) => row.kind === "user" && row.key.startsWith(prefix)).sort((left, right) => left.blockIndex - right.blockIndex).map((row) => row.text);
226
+ return blocks.length === 0 ? [message.text] : blocks;
227
+ }
228
+ setComposerDraft(texts) {
229
+ if (this.sessions.binding(this.sessionId) === void 0) return;
230
+ const scoped = this.sessions.scope(this.sessionId);
231
+ if (scoped === void 0) return;
232
+ scoped.get("conversation")?.input?.for(scoped).restoreDraft(texts.join("\n\n"));
233
+ }
234
+ };
235
+ //#endregion
236
+ //#region src/client/chat-node/message-chrome.ts
237
+ function pad2(n) {
238
+ return String(n).padStart(2, "0");
239
+ }
240
+ function startOfLocalDay(ms) {
241
+ const d = new Date(ms);
242
+ d.setHours(0, 0, 0, 0);
243
+ return d.getTime();
244
+ }
245
+ function msUntilNextLocalMidnight(ms) {
246
+ const next = new Date(ms);
247
+ next.setHours(24, 0, 0, 0);
248
+ return Math.max(next.getTime() - ms, 1);
249
+ }
250
+ function formatRunDuration(ms, t) {
251
+ const total = Math.max(0, Math.floor(ms / 1e3));
252
+ const minutes = Math.floor(total / 60);
253
+ const seconds = total % 60;
254
+ return minutes > 0 ? t("duration.minutes", {
255
+ minutes,
256
+ seconds: String(seconds).padStart(2, "0")
257
+ }) : t("duration.seconds", { seconds });
258
+ }
259
+ function formatLatencySeconds(ms) {
260
+ const s = Math.max(0, ms) / 1e3;
261
+ return s < 10 ? String(Math.round(s * 10) / 10) : String(Math.round(s));
262
+ }
263
+ function formatTokensPerSecond(tps) {
264
+ const clamped = Math.max(0, tps);
265
+ return clamped >= 10 ? String(Math.round(clamped)) : String(Math.round(clamped * 10) / 10);
266
+ }
267
+ function formatMessageClock(time, t, now = Date.now()) {
268
+ const d = new Date(time);
269
+ const n = new Date(now);
270
+ const clock = `${pad2(d.getHours())}:${pad2(d.getMinutes())}`;
271
+ if (d.getFullYear() === n.getFullYear() && d.getMonth() === n.getMonth() && d.getDate() === n.getDate()) return clock;
272
+ const params = {
273
+ y: d.getFullYear(),
274
+ m: d.getMonth() + 1,
275
+ d: d.getDate()
276
+ };
277
+ return `${d.getFullYear() === n.getFullYear() ? t("clock.md", params) : t("clock.ymd", params)} ${clock}`;
278
+ }
279
+ //#endregion
280
+ //#region src/client/chat-node/use-calendar-day.ts
281
+ function useCalendarDay() {
282
+ const [day, setDay] = (0, react.useState)(() => startOfLocalDay(Date.now()));
283
+ (0, react.useEffect)(() => {
284
+ let timer;
285
+ const arm = () => {
286
+ const now = Date.now();
287
+ setDay(startOfLocalDay(now));
288
+ timer = setTimeout(arm, msUntilNextLocalMidnight(now));
289
+ };
290
+ timer = setTimeout(arm, msUntilNextLocalMidnight(Date.now()));
291
+ return () => {
292
+ clearTimeout(timer);
293
+ };
294
+ }, []);
295
+ return day;
296
+ }
297
+ //#endregion
298
+ //#region src/client/chat-node/MessageIconActions.styles.ts
299
+ const styles$3 = {
300
+ actions: {
301
+ display: "flex",
302
+ alignItems: "center",
303
+ gap: "10px",
304
+ height: "28px"
305
+ },
306
+ timeStart: {
307
+ paddingRight: "12px",
308
+ fontSize: "14px",
309
+ lineHeight: "24px",
310
+ color: "var(--dsw-alias-label-tertiary)",
311
+ whiteSpace: "nowrap"
312
+ },
313
+ timeEnd: {
314
+ paddingLeft: "12px",
315
+ fontSize: "14px",
316
+ lineHeight: "24px",
317
+ color: "var(--dsw-alias-label-tertiary)",
318
+ whiteSpace: "nowrap",
319
+ "&)": { "@media (hover: hover)": { opacity: "1" } }
320
+ },
321
+ runTimeDot: { margin: "0 10px" },
322
+ action: {
323
+ display: "inline-flex",
324
+ alignItems: "center",
325
+ justifyContent: "center",
326
+ width: "28px",
327
+ height: "28px",
328
+ padding: "6px",
329
+ border: "none",
330
+ borderRadius: "28px",
331
+ background: "transparent",
332
+ color: "var(--dsw-alias-label-tertiary)",
333
+ cursor: "pointer",
334
+ "&:hover": {
335
+ background: "var(--dsw-alias-interactive-bg-hover)",
336
+ color: "var(--dsw-alias-label-secondary)"
337
+ },
338
+ "&[data-unavailable]": {
339
+ cursor: "default",
340
+ opacity: "0.4"
341
+ },
342
+ "&[data-unavailable]:hover": {
343
+ background: "transparent",
344
+ color: "var(--dsw-alias-label-tertiary)"
345
+ }
346
+ },
347
+ visuallyHidden: {
348
+ position: "absolute",
349
+ width: "1px",
350
+ height: "1px",
351
+ overflow: "hidden",
352
+ clip: "rect(0 0 0 0)",
353
+ whiteSpace: "nowrap"
354
+ }
355
+ };
356
+ const globals = {
357
+ "[data-time-hover-root] [data-time-label]": {
358
+ opacity: "0",
359
+ transition: "opacity 80ms ease"
360
+ },
361
+ "[data-time-hover-root]:hover :is(.timeStart, .timeEnd),\n [data-time-hover-root]:focus-within :is(.timeStart, .timeEnd)": { opacity: "1" }
362
+ };
363
+ //#endregion
364
+ //#region src/client/chat-node/MessageIconActions.tsx
365
+ function MessageIconActions({ text, time, runMs, ttftMs, tokensPerSecond, clock, onBranch, branchUnavailable = false, className, extraActions, onEdit, onRetry, t }) {
366
+ const day = useCalendarDay();
367
+ const reasonId = (0, react.useId)();
368
+ const [copied, setCopied] = (0, react.useState)(false);
369
+ const copyPending = (0, react.useRef)(false);
370
+ const copyTimer = (0, react.useRef)(null);
371
+ const copyEpoch = (0, react.useRef)(0);
372
+ (0, react.useEffect)(() => () => {
373
+ copyEpoch.current += 1;
374
+ copyPending.current = false;
375
+ if (copyTimer.current !== null) clearTimeout(copyTimer.current);
376
+ }, []);
377
+ const onCopy = (0, react.useCallback)(() => {
378
+ if (copied || copyPending.current) return;
379
+ const epoch = copyEpoch.current;
380
+ copyPending.current = true;
381
+ (0, _deepseek_ai_dsh_client_ui_primitives.writeClipboard)(text).then((ok) => {
382
+ if (epoch !== copyEpoch.current) return;
383
+ copyPending.current = false;
384
+ if (!ok) return;
385
+ setCopied(true);
386
+ copyTimer.current = window.setTimeout(() => {
387
+ copyTimer.current = null;
388
+ setCopied(false);
389
+ }, 1e3);
390
+ });
391
+ }, [copied, text]);
392
+ const clockEl = time === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
393
+ "data-time-label": "",
394
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(clock === "start" ? styles$3.timeStart : styles$3.timeEnd),
395
+ children: [
396
+ formatMessageClock(time, t, day),
397
+ runMs !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
398
+ " ",
399
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
400
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles$3.runTimeDot),
401
+ "aria-hidden": true,
402
+ children: "·"
403
+ }),
404
+ " ",
405
+ t("message.ranFor", { duration: formatRunDuration(runMs, t) })
406
+ ] }),
407
+ ttftMs !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
408
+ " ",
409
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
410
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles$3.runTimeDot),
411
+ "aria-hidden": true,
412
+ children: "·"
413
+ }),
414
+ " ",
415
+ t("message.turnTime.ttft"),
416
+ " ",
417
+ t("duration.seconds", { seconds: formatLatencySeconds(ttftMs) })
418
+ ] }),
419
+ tokensPerSecond !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
420
+ " ",
421
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
422
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles$3.runTimeDot),
423
+ "aria-hidden": true,
424
+ children: "·"
425
+ }),
426
+ " ",
427
+ t("message.tokensPerSecond", { tps: formatTokensPerSecond(tokensPerSecond) })
428
+ ] })
429
+ ]
430
+ });
431
+ const actionsClass = _morlay_dsh_client_ui_primitives_client.styling.className(styles$3.actions);
432
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
433
+ "data-time-hover-root": "",
434
+ className: className === void 0 ? actionsClass : `${actionsClass} ${className}`,
435
+ children: [
436
+ clock === "start" ? clockEl : null,
437
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
438
+ label: copied ? t("copied") : t("copy"),
439
+ side: "bottom",
440
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
441
+ type: "button",
442
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles$3.action),
443
+ "aria-label": copied ? t("copied") : t("copy"),
444
+ onClick: onCopy,
445
+ 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, {})
446
+ })
447
+ }),
448
+ extraActions,
449
+ onEdit !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
450
+ label: "编辑",
451
+ side: "bottom",
452
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
453
+ type: "button",
454
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles$3.action),
455
+ "aria-label": "编辑",
456
+ onClick: onEdit,
457
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconEditOutline16, {})
458
+ })
459
+ }),
460
+ onRetry !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
461
+ label: "重试此回合",
462
+ side: "bottom",
463
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
464
+ type: "button",
465
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles$3.action),
466
+ "aria-label": "重试此回合",
467
+ onClick: onRetry,
468
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconRefreshOutline16, {})
469
+ })
470
+ }),
471
+ onBranch !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
472
+ label: branchUnavailable ? t("message.branchUnavailable") : t("message.branch"),
473
+ side: "bottom",
474
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
475
+ type: "button",
476
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles$3.action),
477
+ "aria-label": t("message.branch"),
478
+ "aria-disabled": branchUnavailable || void 0,
479
+ "aria-describedby": branchUnavailable ? reasonId : void 0,
480
+ "data-unavailable": branchUnavailable || void 0,
481
+ onClick: branchUnavailable ? void 0 : onBranch,
482
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconBranchOutline16, {})
483
+ })
484
+ }),
485
+ onBranch !== void 0 && branchUnavailable && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
486
+ id: reasonId,
487
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles$3.visuallyHidden),
488
+ children: t("message.branchUnavailable")
489
+ }),
490
+ clock === "end" ? clockEl : null
491
+ ]
492
+ });
493
+ }
494
+ //#endregion
495
+ //#region src/client/chat-node/MessageItem.styles.ts
496
+ const styles$2 = {
497
+ userRow: {
498
+ display: "flex",
499
+ flexDirection: "column",
500
+ alignItems: "flex-end",
501
+ gap: "6px"
502
+ },
503
+ userStack: {
504
+ display: "flex",
505
+ flexDirection: "column",
506
+ alignItems: "flex-end",
507
+ gap: "8px",
508
+ minWidth: "0",
509
+ maxWidth: "min(525px, 82%)"
510
+ },
511
+ bubble: {
512
+ maxWidth: "100%",
513
+ background: "var(--dsw-specific-bubble)",
514
+ borderRadius: "22px",
515
+ padding: "10px 16px",
516
+ fontSize: "16px",
517
+ lineHeight: "24px",
518
+ color: "var(--dsw-alias-label-primary)"
519
+ },
520
+ contextRow: { padding: "2px 0" },
521
+ compactionRow: { padding: "2px 0" },
522
+ compactionButton: {
523
+ display: "flex",
524
+ alignItems: "center",
525
+ width: "100%",
526
+ height: "24px",
527
+ minWidth: "0",
528
+ padding: "0",
529
+ border: "none",
530
+ borderRadius: "6px",
531
+ background: "none",
532
+ color: "inherit",
533
+ font: "inherit",
534
+ textAlign: "left",
535
+ "&:not(:disabled)": { cursor: "pointer" },
536
+ "&:not(:disabled):hover": { background: "var(--dsw-alias-interactive-bg-hover)" },
537
+ "&:not(:disabled):hover .compactionContextIcon": { opacity: "0" },
538
+ "&:not(:disabled):focus-visible .compactionContextIcon": { opacity: "0" },
539
+ "&:not(:disabled):hover .compactionDisclosureIcon": { opacity: "1" },
540
+ "&:not(:disabled):focus-visible .compactionDisclosureIcon": { opacity: "1" }
541
+ },
542
+ compactionLeading: {
543
+ flex: "none",
544
+ display: "inline-grid",
545
+ placeItems: "center",
546
+ width: "16px",
547
+ height: "16px",
548
+ marginRight: "6px",
549
+ color: "var(--dsw-alias-label-secondary)"
550
+ },
551
+ compactionContextIcon: {
552
+ display: "inline-flex",
553
+ gridArea: "1 / 1",
554
+ alignItems: "center",
555
+ justifyContent: "center"
556
+ },
557
+ compactionDisclosureIcon: {
558
+ display: "inline-flex",
559
+ gridArea: "1 / 1",
560
+ alignItems: "center",
561
+ justifyContent: "center",
562
+ opacity: "0"
563
+ },
564
+ compactionTitle: {
565
+ flex: "none",
566
+ fontSize: "14px",
567
+ lineHeight: "24px",
568
+ color: "var(--dsw-alias-label-primary-dimmed)"
569
+ },
570
+ compactionSep: {
571
+ flex: "none",
572
+ width: "2px",
573
+ height: "2px",
574
+ margin: "0 8px",
575
+ borderRadius: "1px",
576
+ background: "var(--dsw-alias-label-caption)"
577
+ },
578
+ compactionSummary: {
579
+ flex: "1 1 auto",
580
+ minWidth: "0",
581
+ overflow: "hidden",
582
+ color: "var(--dsw-alias-label-tertiary)",
583
+ fontSize: "14px",
584
+ lineHeight: "24px",
585
+ textOverflow: "ellipsis",
586
+ whiteSpace: "nowrap"
587
+ },
588
+ compactionBody: {
589
+ padding: "4px 0 4px 22px",
590
+ color: "var(--dsw-alias-label-tertiary)",
591
+ fontSize: "14px",
592
+ lineHeight: "24px"
593
+ },
594
+ retryRow: {
595
+ color: "var(--dsw-alias-label-tertiary)",
596
+ fontSize: "13px",
597
+ lineHeight: "20px",
598
+ "&[data-active] .retryText": {
599
+ background: "linear-gradient(\n 90deg,\n var(--dsw-alias-label-tertiary) 0%,\n var(--dsw-alias-label-tertiary) 40%,\n var(--dsw-alias-label-secondary) 50%,\n var(--dsw-alias-label-tertiary) 60%,\n var(--dsw-alias-label-tertiary) 100%\n )",
600
+ backgroundPosition: "100% 50%",
601
+ backgroundSize: "200% 100%",
602
+ backgroundClip: "text",
603
+ color: "transparent",
604
+ animation: "retry-shimmer 1.6s ease-in-out infinite",
605
+ "@media (prefers-reduced-motion: reduce)": {
606
+ background: "none",
607
+ color: "inherit",
608
+ animation: "none"
609
+ }
610
+ },
611
+ "&[open] .retrySummary::after": { transform: "rotate(45deg)" }
612
+ },
613
+ retrySummary: {
614
+ display: "inline-flex",
615
+ alignItems: "center",
616
+ width: "fit-content",
617
+ padding: "2px 0",
618
+ gap: "7px",
619
+ borderRadius: "3px",
620
+ color: "inherit",
621
+ cursor: "pointer",
622
+ listStyle: "none",
623
+ userSelect: "none",
624
+ "&::-webkit-details-marker": { display: "none" },
625
+ "&::after": {
626
+ width: "6px",
627
+ height: "6px",
628
+ borderRight: "1.5px solid currentcolor",
629
+ borderBottom: "1.5px solid currentcolor",
630
+ content: "\"\"",
631
+ opacity: "0.8",
632
+ transform: "rotate(-45deg)",
633
+ transition: "transform 120ms ease"
634
+ },
635
+ "&:hover": { color: "var(--dsw-alias-label-secondary)" },
636
+ "&:focus-visible": {
637
+ outline: "1.5px solid var(--dsw-alias-button-info-fill)",
638
+ outlineOffset: "2px"
639
+ }
640
+ },
641
+ retryText: { color: "inherit" },
642
+ retryDetails: {
643
+ display: "grid",
644
+ gap: "2px",
645
+ marginTop: "3px",
646
+ paddingLeft: "14px",
647
+ overflowWrap: "anywhere",
648
+ fontSize: "12px",
649
+ lineHeight: "18px"
650
+ },
651
+ retryDetailLabel: { color: "var(--dsw-alias-label-secondary)" },
652
+ turnErrorRow: {
653
+ display: "grid",
654
+ gridTemplateColumns: "10px minmax(0, 1fr) auto",
655
+ gap: "8px",
656
+ alignItems: "start",
657
+ padding: "2px 0",
658
+ fontSize: "13px",
659
+ lineHeight: "20px"
660
+ },
661
+ turnErrorDot: { marginTop: "5px" },
662
+ turnErrorCopy: {
663
+ minWidth: "0",
664
+ overflowWrap: "anywhere"
665
+ },
666
+ turnErrorTitle: {
667
+ marginRight: "6px",
668
+ color: "var(--dsw-alias-state-error-primary)",
669
+ fontWeight: "600"
670
+ },
671
+ turnErrorMessage: { color: "var(--dsw-alias-label-secondary)" },
672
+ turnErrorCode: {
673
+ color: "var(--dsw-alias-label-tertiary)",
674
+ font: "var(--dsw-font-markdown-code-block-small)"
675
+ },
676
+ maxTokensTitle: {
677
+ marginRight: "6px",
678
+ color: "var(--dsw-alias-state-warn-primary)",
679
+ fontWeight: "600"
680
+ },
681
+ refChip: {
682
+ display: "inline-block",
683
+ margin: "0 2px",
684
+ padding: "0 8px",
685
+ borderRadius: "6px",
686
+ background: "rgba(97, 135, 216, 0.22)",
687
+ color: "var(--dsw-alias-label-primary)",
688
+ fontSize: "0.85em",
689
+ lineHeight: "1.6",
690
+ whiteSpace: "nowrap",
691
+ verticalAlign: "baseline"
692
+ },
693
+ confirmActions: {
694
+ display: "flex",
695
+ justifyContent: "flex-end",
696
+ gap: "8px"
697
+ }
698
+ };
699
+ //#endregion
700
+ //#region src/client/chat-node/MessageItem.tsx
701
+ function contentParts(content) {
702
+ const texts = [];
703
+ const images = [];
704
+ const rest = [];
705
+ for (const block of content) {
706
+ const b = block;
707
+ if (b.type === "text" && typeof b.text === "string") texts.push(b.text);
708
+ else if (b.type === "image" && b.attachment !== void 0) images.push({ attachment: b.attachment });
709
+ else rest.push(block);
710
+ }
711
+ return {
712
+ text: texts.join("\n\n"),
713
+ images,
714
+ rest
715
+ };
716
+ }
717
+ function UserStyleBubble({ content, renderMessageImages, actions, references, t }) {
718
+ const { text, images, rest } = contentParts(content);
719
+ const truncated = (total) => t("json.truncated", { total });
720
+ const labels = (0, react.useMemo)(() => (0, _morlay_dsh_client_ui_primitives_client.markdownLabels)(t), [t]);
721
+ const showBubble = text !== "" || rest.length > 0;
722
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
723
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles$2.userRow),
724
+ "data-time-hover-root": true,
725
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
726
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles$2.userStack),
727
+ children: [renderMessageImages({
728
+ images,
729
+ align: "end"
730
+ }), showBubble && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
731
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles$2.bubble),
732
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_morlay_dsh_client_ui_primitives_client.ReferenceMarkdown, {
733
+ text,
734
+ labels,
735
+ actions: references
736
+ }), rest.map((block, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.JsonBlock, {
737
+ label: t("message.extraBlock"),
738
+ payload: block,
739
+ truncatedLabel: truncated
740
+ }, i))]
741
+ })]
742
+ }), actions?.(text)]
743
+ });
744
+ }
745
+ const UserMessageNodeView = (0, react.memo)(function UserMessageNodeView({ node, renderMessageImages, t, openFile, openSkill, recall, retry }) {
746
+ const data = node.data;
747
+ const [confirmingRecall, setConfirmingRecall] = (0, react.useState)(null);
748
+ const [confirmingRetry, setConfirmingRetry] = (0, react.useState)(false);
749
+ const turnLocation = node.location.kind === "turn" || node.location.kind === "step" ? node.location.turn : void 0;
750
+ const turn = turnLocation?.turn;
751
+ const retryable = turnLocation?.status === "closed";
752
+ const textBlockIndex = data.content.findIndex((block) => block.type === "text");
753
+ const textBlock = textBlockIndex === -1 ? void 0 : data.content[textBlockIndex];
754
+ const onEdit = textBlock === void 0 ? void 0 : () => {
755
+ setConfirmingRecall({
756
+ key: `${node.anchorSeq}:${String(textBlockIndex)}`,
757
+ ...turn === void 0 ? {} : { turn },
758
+ eventSeq: node.anchorSeq,
759
+ blockIndex: textBlockIndex,
760
+ kind: "user",
761
+ text: textBlock.text ?? "",
762
+ time: data.time
763
+ });
764
+ };
765
+ const onRetry = retryable && turn !== void 0 ? () => {
766
+ setConfirmingRetry(true);
767
+ } : void 0;
768
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
769
+ confirmingRecall !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
770
+ open: true,
771
+ onClose: () => setConfirmingRecall(null),
772
+ title: "编辑消息",
773
+ closeLabel: "关闭",
774
+ description: "将撤回该消息及其之后的对话内容到输入框,请修改后重新发送。",
775
+ footer: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
776
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles$2.confirmActions),
777
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
778
+ variant: "outline",
779
+ onClick: () => setConfirmingRecall(null),
780
+ children: "取消"
781
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
782
+ variant: "primary",
783
+ onClick: () => {
784
+ const target = confirmingRecall;
785
+ setConfirmingRecall(null);
786
+ recall(target);
787
+ },
788
+ children: "撤回并编辑"
789
+ })]
790
+ })
791
+ }),
792
+ confirmingRetry && turn !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
793
+ open: true,
794
+ onClose: () => setConfirmingRetry(false),
795
+ title: "重试回合",
796
+ closeLabel: "关闭",
797
+ description: `将重新生成第 ${turn} 轮的回复,并抛弃该回合之后的内容。`,
798
+ footer: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
799
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles$2.confirmActions),
800
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
801
+ variant: "outline",
802
+ onClick: () => setConfirmingRetry(false),
803
+ children: "取消"
804
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
805
+ variant: "primary",
806
+ onClick: () => {
807
+ setConfirmingRetry(false);
808
+ retry(turn, "truncate");
809
+ },
810
+ children: "确认重试"
811
+ })]
812
+ })
813
+ }),
814
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UserStyleBubble, {
815
+ content: data.content,
816
+ renderMessageImages,
817
+ references: {
818
+ openFile,
819
+ openSkill
820
+ },
821
+ t,
822
+ actions: (text) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MessageIconActions, {
823
+ text,
824
+ time: data.time,
825
+ clock: "start",
826
+ t,
827
+ onEdit,
828
+ onRetry
829
+ })
830
+ })
831
+ ] });
832
+ });
833
+ //#endregion
834
+ //#region src/client/chat-node/register.ts
835
+ const NS$1 = "conversation";
836
+ function registerChatNodeRenderers(ctx, controllerFor) {
837
+ const injectFace = (sessionId) => controllerFor(sessionId).face;
838
+ for (const key of ["user", "steering"]) ctx.slots.inject("conversation.chat.node", () => ctx.slots.register({
839
+ name: "conversation.chat.node",
840
+ key,
841
+ locale: NS$1,
842
+ priority: -1,
843
+ inject: injectFace
844
+ }, UserMessageNodeView));
845
+ }
846
+ //#endregion
847
+ //#region src/client/composer-stats/turn-reading.ts
848
+ function usageOutputTokens(usage) {
849
+ if (typeof usage !== "object" || usage === null) return null;
850
+ const value = usage.outputTokens;
851
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : null;
852
+ }
853
+ function assistantStepReading(node) {
854
+ const timing = node.timing;
855
+ return {
856
+ ttftMs: timing !== void 0 && timing.stepStartTime !== null && timing.firstTokenTime !== null ? Math.max(0, timing.firstTokenTime - timing.stepStartTime) : null,
857
+ decodeMs: timing !== void 0 && timing.firstTokenTime !== null ? Math.max(0, timing.completedTime - timing.firstTokenTime) : null,
858
+ outputTokens: usageOutputTokens(node.usage)
859
+ };
860
+ }
861
+ //#endregion
862
+ //#region src/client/composer-stats/token-format.ts
863
+ function formatTokens(value, t) {
864
+ const scaled = (candidate) => candidate >= 100 ? String(Math.round(candidate)) : String(Math.round(candidate * 10) / 10);
865
+ if (value < 1e3) return String(value);
866
+ if (value < 1e6) return t("number.thousand", { value: scaled(value / 1e3) });
867
+ return t("number.million", { value: scaled(value / 1e6) });
868
+ }
869
+ function formatExactTokens(value, t) {
870
+ const digits = String(value);
871
+ const groups = [];
872
+ for (let end = digits.length; end > 0; end -= 3) groups.unshift(digits.slice(Math.max(0, end - 3), end));
873
+ return groups.join(t("number.groupSeparator"));
874
+ }
875
+ function roundedPercentUnits(cacheReadTokens, denominator, decimalPlaces) {
876
+ const scale = (decimalPlaces === 0 ? 1 : 10) * 100;
877
+ const doubledScale = scale * 2;
878
+ const denominatorQuotient = Math.floor(denominator / doubledScale);
879
+ const denominatorRemainder = denominator % doubledScale;
880
+ let lower = 0;
881
+ let upper = scale;
882
+ while (lower < upper) {
883
+ const candidate = Math.floor((lower + upper + 1) / 2);
884
+ const factor = candidate * 2 - 1;
885
+ if (cacheReadTokens >= factor * denominatorQuotient + Math.ceil(factor * denominatorRemainder / doubledScale)) lower = candidate;
886
+ else upper = candidate - 1;
887
+ }
888
+ return lower;
889
+ }
890
+ function displayPercentUnits(units, decimalPlaces) {
891
+ if (decimalPlaces === 0) return String(units);
892
+ const whole = Math.floor(units / 10);
893
+ const tenths = units % 10;
894
+ return tenths === 0 ? String(whole) : `${whole}.${tenths}`;
895
+ }
896
+ function formatCacheHitPercent(cacheReadTokens, promptTokens, decimalPlaces = 0) {
897
+ if (promptTokens === 0) return null;
898
+ const missedInputTokens = promptTokens - cacheReadTokens;
899
+ if (missedInputTokens <= 0) return "100";
900
+ const roundedUnits = roundedPercentUnits(cacheReadTokens, promptTokens, decimalPlaces);
901
+ if (roundedUnits < (decimalPlaces === 0 ? 100 : 1e3)) return displayPercentUnits(roundedUnits, decimalPlaces);
902
+ let distinguishingPlaces = 1;
903
+ let scaledDoubleGap = missedInputTokens * 200;
904
+ const denominatorTens = Math.floor(promptTokens / 10);
905
+ while (scaledDoubleGap <= denominatorTens) {
906
+ scaledDoubleGap *= 10;
907
+ distinguishingPlaces += 1;
908
+ }
909
+ const denominatorOnes = promptTokens % 10;
910
+ let roundedLoss = 5;
911
+ for (let loss = 1; loss < 5; loss += 1) {
912
+ const factor = loss * 2 + 1;
913
+ const threshold = factor * denominatorTens + Math.floor(factor * denominatorOnes / 10);
914
+ if (scaledDoubleGap <= threshold) {
915
+ roundedLoss = loss;
916
+ break;
917
+ }
918
+ }
919
+ return `99.${"9".repeat(distinguishingPlaces - 1)}${10 - roundedLoss}`;
920
+ }
921
+ //#endregion
922
+ //#region src/client/composer-stats/stat-dialog.ts
923
+ const PANEL_MARGIN = 12;
924
+ const PANEL_GAP = 8;
925
+ const MEASURE_STYLE = {
926
+ visibility: "hidden",
927
+ left: 0,
928
+ top: 0
929
+ };
930
+ function useStatDialog(controlled) {
931
+ const [ownOpen, setOwnOpen] = (0, react.useState)(false);
932
+ const open = controlled?.open ?? ownOpen;
933
+ const setOpen = controlled?.setOpen ?? setOwnOpen;
934
+ const rootRef = (0, react.useRef)(null);
935
+ const panelRef = (0, react.useRef)(null);
936
+ const pos = (0, _deepseek_ai_dsh_client_ui_primitives.useAnchoredPosition)({
937
+ open,
938
+ anchorRef: rootRef,
939
+ panelRef,
940
+ side: "top",
941
+ gap: PANEL_GAP,
942
+ margin: PANEL_MARGIN
943
+ });
944
+ (0, _deepseek_ai_dsh_client_ui_primitives.useDismissOnOutsidePointer)(rootRef, open, setOpen, panelRef);
945
+ (0, react.useEffect)(() => {
946
+ if (!open) return;
947
+ const onKeyDown = (e) => {
948
+ if (e.key === "Escape") setOpen(false);
949
+ };
950
+ document.addEventListener("keydown", onKeyDown);
951
+ return () => {
952
+ document.removeEventListener("keydown", onKeyDown);
953
+ };
954
+ }, [open, setOpen]);
955
+ return {
956
+ open,
957
+ setOpen,
958
+ rootRef,
959
+ panelRef,
960
+ pos
961
+ };
962
+ }
963
+ //#endregion
964
+ //#region src/client/composer-stats/StatsPills.styles.ts
965
+ const styles$1 = {
966
+ root: {
967
+ display: "flex",
968
+ justifyContent: "center",
969
+ gap: "12px",
970
+ minWidth: "0",
971
+ maxWidth: "100%",
972
+ boxSizing: "border-box",
973
+ fontSize: "var(--dsh-content-font-size-secondary, 13px)",
974
+ lineHeight: "calc(20px + var(--dsh-content-font-delta-secondary, 0px))"
975
+ },
976
+ anchor: {
977
+ display: "inline-flex",
978
+ minWidth: "0"
979
+ },
980
+ pill: {
981
+ cursor: "pointer",
982
+ "&:hover, &[aria-expanded='true']": {
983
+ background: "var(--dsw-alias-interactive-bg-hover)",
984
+ color: "var(--dsw-alias-label-secondary)"
985
+ },
986
+ display: "inline-flex",
987
+ alignItems: "center",
988
+ gap: "6px",
989
+ boxSizing: "border-box",
990
+ maxWidth: "100%",
991
+ padding: "1px 8px",
992
+ border: "none",
993
+ borderRadius: "24px",
994
+ background: "transparent",
995
+ color: "var(--dsw-alias-label-tertiary)",
996
+ font: "inherit",
997
+ fontVariantNumeric: "tabular-nums",
998
+ lineHeight: "inherit",
999
+ whiteSpace: "nowrap",
1000
+ "& svg": {
1001
+ width: "14px",
1002
+ height: "14px",
1003
+ flex: "none"
1004
+ }
1005
+ },
1006
+ label: {
1007
+ minWidth: "0",
1008
+ overflow: "hidden",
1009
+ textOverflow: "ellipsis"
1010
+ },
1011
+ sep: {
1012
+ color: "var(--dsw-alias-separator-primary)",
1013
+ margin: "0 6px"
1014
+ }
1015
+ };
1016
+ //#endregion
1017
+ //#region src/client/composer-stats/stat-dialog.styles.ts
1018
+ const styles = {
1019
+ panel: {
1020
+ position: "fixed",
1021
+ zIndex: "1100",
1022
+ boxSizing: "border-box",
1023
+ width: "max-content",
1024
+ minWidth: "min(300px, calc(100vw - 24px))",
1025
+ maxWidth: "min(440px, calc(100vw - 24px))",
1026
+ padding: "16px",
1027
+ border: "0",
1028
+ borderRadius: "12px",
1029
+ background: "var(--dsw-specific-menu)",
1030
+ "--dsw-elevation-stroke-color": "var(--dsw-alias-border-l1)",
1031
+ boxShadow: "var(--dsw-elevation-prominent)",
1032
+ fontSize: "12px",
1033
+ lineHeight: "18px",
1034
+ color: "var(--dsw-alias-label-secondary)",
1035
+ cursor: "default"
1036
+ },
1037
+ title: {
1038
+ display: "flex",
1039
+ justifyContent: "space-between",
1040
+ gap: "16px",
1041
+ marginBottom: "8px",
1042
+ color: "var(--dsw-alias-label-primary)",
1043
+ fontWeight: "500"
1044
+ },
1045
+ titleRule: {
1046
+ marginBottom: "10px",
1047
+ borderTop: "0.5px solid var(--dsw-alias-border-l2)"
1048
+ },
1049
+ titleValue: { fontVariantNumeric: "tabular-nums" },
1050
+ titleLabel: {
1051
+ display: "inline-flex",
1052
+ alignItems: "center",
1053
+ gap: "6px",
1054
+ minWidth: "0",
1055
+ "& svg": {
1056
+ width: "14px",
1057
+ height: "14px",
1058
+ flex: "none"
1059
+ }
1060
+ },
1061
+ details: {
1062
+ display: "grid",
1063
+ gridTemplateColumns: "minmax(76px, auto) minmax(0, 1fr)",
1064
+ gap: "6px 16px",
1065
+ margin: "0",
1066
+ color: "var(--dsw-alias-label-tertiary)",
1067
+ "& dt": {
1068
+ minWidth: "0",
1069
+ margin: "0"
1070
+ },
1071
+ "& dd": {
1072
+ minWidth: "0",
1073
+ margin: "0",
1074
+ color: "var(--dsw-alias-label-secondary)",
1075
+ fontVariantNumeric: "tabular-nums",
1076
+ textAlign: "right"
1077
+ }
1078
+ },
1079
+ reasoning: {
1080
+ color: "var(--dsw-alias-label-tertiary)",
1081
+ whiteSpace: "nowrap"
1082
+ }
1083
+ };
1084
+ //#endregion
1085
+ //#region src/client/composer-stats/StatsPills.tsx
1086
+ /**
1087
+ * Fold assistant and tool-result nodes into window-scoped display totals —
1088
+ * the FALLBACK for assemblies without the `sessionStats` projection.
1089
+ *
1090
+ * Every displayed figure rides that durable whole-log projection (and token
1091
+ * accounting rides `tokenUsage`) because the window is paged and compaction
1092
+ * rewrites it; this fold answers "what is on screen" only when no projection
1093
+ * value is served. Its field names deliberately mirror the projection's so
1094
+ * the two swap wholesale.
1095
+ * @param nodes - snapshot nodes.
1096
+ * @returns fallback counts and summed wall times.
1097
+ */
1098
+ function deriveStats(nodes) {
1099
+ const turns = /* @__PURE__ */ new Set();
1100
+ let steps = 0;
1101
+ let llmMs = 0;
1102
+ let toolMs = 0;
1103
+ let ttftMs = 0;
1104
+ let ttftSteps = 0;
1105
+ let decodeMs = 0;
1106
+ let decodeTokens = 0;
1107
+ for (const node of nodes) {
1108
+ if (node.kind === "tool-result") {
1109
+ if (node.callTime !== null) toolMs += Math.max(0, node.time - node.callTime);
1110
+ continue;
1111
+ }
1112
+ if (node.kind !== "assistant") continue;
1113
+ turns.add(node.turn);
1114
+ steps += 1;
1115
+ if (node.timing !== void 0 && node.timing.stepStartTime !== null) llmMs += Math.max(0, node.timing.completedTime - node.timing.stepStartTime);
1116
+ const reading = assistantStepReading(node);
1117
+ if (reading.ttftMs !== null) {
1118
+ ttftMs += reading.ttftMs;
1119
+ ttftSteps += 1;
1120
+ }
1121
+ if (reading.decodeMs !== null && reading.outputTokens !== null) {
1122
+ decodeMs += reading.decodeMs;
1123
+ decodeTokens += reading.outputTokens;
1124
+ }
1125
+ }
1126
+ return {
1127
+ turns: turns.size,
1128
+ steps,
1129
+ llmMs,
1130
+ toolMs,
1131
+ ttftMs,
1132
+ ttftSteps,
1133
+ decodeMs,
1134
+ decodeTokens
1135
+ };
1136
+ }
1137
+ /**
1138
+ * Compact duration: 45.2s under a minute, 2m42s from there on.
1139
+ * @param ms - duration in milliseconds.
1140
+ * @returns display string.
1141
+ */
1142
+ function formatDuration(ms, t) {
1143
+ const s = ms / 1e3;
1144
+ if (s < 60) return t("duration.compactSeconds", { seconds: Math.round(s * 10) / 10 });
1145
+ const whole = Math.round(s);
1146
+ return t("duration.compactMinutes", {
1147
+ minutes: Math.floor(whole / 60),
1148
+ seconds: whole % 60
1149
+ });
1150
+ }
1151
+ /**
1152
+ * Display-ready cache-hit share of prompt-side input over the whole durable log.
1153
+ * @param usage - the session's token-usage projection value.
1154
+ * @returns integer text when integer rounding stays below 100, otherwise the
1155
+ * minimum decimal precision that still rounds below 100; a full hit returns
1156
+ * 100, and no billed input returns null.
1157
+ */
1158
+ function cacheHitPercent(usage) {
1159
+ const denominator = billedInputTokens(usage);
1160
+ return formatCacheHitPercent(usage.cacheReadTokens, denominator);
1161
+ }
1162
+ /**
1163
+ * Sum the three disjoint prompt-side billing buckets.
1164
+ * @param usage - the session's token-usage projection value.
1165
+ * @returns billed input tokens.
1166
+ */
1167
+ function billedInputTokens(usage) {
1168
+ return usage.uncachedInputTokens + usage.cacheReadTokens + usage.cacheWriteTokens;
1169
+ }
1170
+ function exactCount(value, t) {
1171
+ return t("message.turnUsage.count", { count: formatExactTokens(value, t) });
1172
+ }
1173
+ function TimePill({ stats, t, dialog }) {
1174
+ const { open, setOpen, rootRef, panelRef, pos } = useStatDialog(dialog);
1175
+ const counts = t("stats.counts", {
1176
+ turns: stats.turns,
1177
+ steps: stats.steps
1178
+ });
1179
+ const tps = stats.decodeMs > 0 ? t("message.tokensPerSecond", { tps: formatTokensPerSecond(stats.decodeTokens / (stats.decodeMs / 1e3)) }) : null;
1180
+ const label = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1181
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles$1.label),
1182
+ children: [counts, tps !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1183
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles$1.sep),
1184
+ "aria-hidden": true,
1185
+ children: "·"
1186
+ }), tps] })]
1187
+ });
1188
+ if (stats.llmMs <= 0 && stats.toolMs <= 0 && stats.ttftSteps <= 0 && stats.decodeMs <= 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1189
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles$1.anchor),
1190
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1191
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles$1.pill),
1192
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconGaugeOutline16, {}), label]
1193
+ })
1194
+ });
1195
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1196
+ ref: rootRef,
1197
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles$1.anchor),
1198
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1199
+ type: "button",
1200
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles$1.pill),
1201
+ "aria-haspopup": "dialog",
1202
+ "aria-expanded": open,
1203
+ "aria-label": tps === null ? counts : `${counts} · ${tps}`,
1204
+ onClick: () => {
1205
+ setOpen(!open);
1206
+ },
1207
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconGaugeOutline16, {}), label]
1208
+ }), open && (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1209
+ ref: panelRef,
1210
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.panel),
1211
+ role: "dialog",
1212
+ "aria-label": t("stats.dialog.title"),
1213
+ style: pos ?? MEASURE_STYLE,
1214
+ children: [
1215
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1216
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.title),
1217
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1218
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.titleLabel),
1219
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconGaugeOutline16, {}), t("stats.dialog.title")]
1220
+ })
1221
+ }),
1222
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1223
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.titleRule),
1224
+ "aria-hidden": true
1225
+ }),
1226
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("dl", {
1227
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.details),
1228
+ "data-session-stats-details": true,
1229
+ children: [
1230
+ stats.llmMs > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: t("stats.dialog.llmTime") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: formatDuration(stats.llmMs, t) })] }),
1231
+ stats.toolMs > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: t("stats.dialog.toolTime") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: formatDuration(stats.toolMs, t) })] }),
1232
+ stats.ttftSteps > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: t("stats.dialog.ttft") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: formatDuration(stats.ttftMs / stats.ttftSteps, t) })] }),
1233
+ stats.decodeMs > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: t("stats.dialog.speed") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: t("message.tokensPerSecond", { tps: formatTokensPerSecond(stats.decodeTokens / (stats.decodeMs / 1e3)) }) })] })
1234
+ ]
1235
+ })
1236
+ ]
1237
+ }), document.body)]
1238
+ });
1239
+ }
1240
+ function UsagePill({ usage, t, dialog }) {
1241
+ const { open, setOpen, rootRef, panelRef, pos } = useStatDialog(dialog);
1242
+ const total = billedInputTokens(usage) + usage.outputTokens;
1243
+ const totalText = t("message.turnUsage.count", { count: formatTokens(total, t) });
1244
+ const cacheHit = cacheHitPercent(usage);
1245
+ const cacheHitText = cacheHit !== null ? t("stats.cacheHit", { percent: cacheHit }) : null;
1246
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1247
+ ref: rootRef,
1248
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles$1.anchor),
1249
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1250
+ type: "button",
1251
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles$1.pill),
1252
+ "aria-haspopup": "dialog",
1253
+ "aria-expanded": open,
1254
+ "aria-label": cacheHitText === null ? totalText : `${totalText} · ${cacheHitText}`,
1255
+ onClick: () => {
1256
+ setOpen(!open);
1257
+ },
1258
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconDatabaseOutline16, {}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1259
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles$1.label),
1260
+ children: [totalText, cacheHitText !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1261
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles$1.sep),
1262
+ "aria-hidden": true,
1263
+ children: "·"
1264
+ }), cacheHitText] })]
1265
+ })]
1266
+ }), open && (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1267
+ ref: panelRef,
1268
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.panel),
1269
+ role: "dialog",
1270
+ "aria-label": t("stats.dialog.usageTitle"),
1271
+ style: pos ?? MEASURE_STYLE,
1272
+ children: [
1273
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1274
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.title),
1275
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1276
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.titleLabel),
1277
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconDatabaseOutline16, {}), t("stats.dialog.usageTitle")]
1278
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1279
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.titleValue),
1280
+ children: exactCount(total, t)
1281
+ })]
1282
+ }),
1283
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1284
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.titleRule),
1285
+ "aria-hidden": true
1286
+ }),
1287
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("dl", {
1288
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.details),
1289
+ "data-session-stats-usage": true,
1290
+ children: [
1291
+ cacheHit !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: t("message.turnUsage.cacheHit") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: `${cacheHit}%` })] }),
1292
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: t("message.turnUsage.input") }),
1293
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: exactCount(usage.uncachedInputTokens, t) }),
1294
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: t("message.turnUsage.cacheRead") }),
1295
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: exactCount(usage.cacheReadTokens, t) }),
1296
+ usage.cacheWriteTokens !== 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: t("message.turnUsage.cacheWrite") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: exactCount(usage.cacheWriteTokens, t) })] }),
1297
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: t("message.turnUsage.output") }),
1298
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: exactCount(usage.outputTokens, t) })
1299
+ ]
1300
+ })
1301
+ ]
1302
+ }), document.body)]
1303
+ });
1304
+ }
1305
+ const StatsPills = (0, react.memo)(function StatsPills({ useChat, useProjection, t }) {
1306
+ const settledNodes = useChat((s) => s.legacy.nodes);
1307
+ const usage = useProjection("tokenUsage");
1308
+ const [openPill, setOpenPill] = (0, react.useState)(null);
1309
+ const projected = useProjection("sessionStats");
1310
+ const stats = (0, react.useMemo)(() => projected ?? deriveStats(settledNodes), [projected, settledNodes]);
1311
+ const hasTokens = usage !== void 0 && (billedInputTokens(usage) > 0 || usage.outputTokens > 0);
1312
+ if (stats.steps === 0 && !hasTokens) return null;
1313
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1314
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles$1.root),
1315
+ "data-composer-stats": true,
1316
+ children: [stats.steps > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TimePill, {
1317
+ stats,
1318
+ t,
1319
+ dialog: {
1320
+ open: openPill === "time",
1321
+ setOpen: (open) => {
1322
+ setOpenPill(open ? "time" : null);
1323
+ }
1324
+ }
1325
+ }), hasTokens && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsagePill, {
1326
+ usage,
1327
+ t,
1328
+ dialog: {
1329
+ open: openPill === "usage",
1330
+ setOpen: (open) => {
1331
+ setOpenPill(open ? "usage" : null);
1332
+ }
1333
+ }
1334
+ })]
1335
+ });
1336
+ });
1337
+ //#endregion
1338
+ //#region src/client/composer-stats/register.ts
1339
+ const NS = "chat";
1340
+ function registerComposerStats(slots) {
1341
+ return slots.register({
1342
+ name: "conversation.composer.dock",
1343
+ id: "stats",
1344
+ order: 0,
1345
+ priority: -1,
1346
+ locale: NS
1347
+ }, StatsPills);
1348
+ }
1349
+ //#endregion
1350
+ //#region src/client/index.ts
1351
+ const inject = [
1352
+ "slots",
1353
+ "conversation",
1354
+ "connection",
1355
+ "sessions",
1356
+ "uiWorkspace"
1357
+ ];
1358
+ function apply(ctx) {
1359
+ _morlay_dsh_client_ui_primitives_client.styling.injectGlobals(globals);
1360
+ const controllers = /* @__PURE__ */ new Map();
1361
+ const controllerFor = (sessionId) => {
1362
+ let controller = controllers.get(sessionId);
1363
+ if (controller === void 0) {
1364
+ controller = new SessionEditorController(ctx, sessionId);
1365
+ controllers.set(sessionId, controller);
1366
+ }
1367
+ return controller;
1368
+ };
1369
+ ctx.on("connection/reset", () => {
1370
+ for (const controller of controllers.values()) controller.load();
1371
+ });
1372
+ registerChatNodeRenderers(ctx, controllerFor);
1373
+ ctx.slots.inject("conversation.composer.dock", () => registerComposerStats(ctx.slots));
1374
+ }
1375
+ //#endregion
1376
+ exports.apply = apply;
1377
+ exports.inject = inject;
1378
+ return module.exports;
1379
+ }
1380
+ });