@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
@@ -1,499 +0,0 @@
1
- import { closedTurns, editPlan, editableMessages, precedingContentIndex, recallBoundary, rerollPlan, retryPlan, retryableTurns } from "./plan.mjs";
2
- import { Service } from "@deepseek-ai/cordis";
3
- import { SessionSeq } from "@deepseek-ai/dsh-session";
4
- import { SESSION_BRANCH_VERSION_SCHEMA as SESSION_BRANCH_VERSION_SCHEMA$1, SessionBranchError, balanceRewindPrefix } from "@morlay/session-branch";
5
- //#region src/shared.ts
6
- const SESSION_EDITOR_PATH = "/session-editor";
7
- function toTimelinePayload(sessionId, timeline, messages, retryableTurns) {
8
- const currentPath = /* @__PURE__ */ new Set();
9
- for (const node of timeline.nodes) currentPath.add(String(node.sessionId));
10
- const versions = timeline.nodes.map((node) => ({
11
- sessionId: String(node.sessionId),
12
- ...node.parentSessionId === void 0 ? {} : { parentSessionId: String(node.parentSessionId) },
13
- ...node.effect === void 0 ? {} : {
14
- effectId: node.effect.id,
15
- inverseSessionId: String(node.inverseSessionId),
16
- operation: node.effect.operation,
17
- cascade: node.effect.cascade,
18
- targetTurn: node.effect.targetTurn,
19
- ...node.effect.blockKind === void 0 ? {} : { blockKind: node.effect.blockKind },
20
- ...node.effect.before === void 0 ? {} : { before: node.effect.before },
21
- ...node.effect.after === void 0 ? {} : { after: node.effect.after }
22
- },
23
- createdAt: node.createdAt,
24
- depth: depthOf(timeline, node.sessionId),
25
- current: String(node.sessionId) === String(sessionId),
26
- onCurrentEffectPath: currentPath.has(String(node.sessionId))
27
- }));
28
- const versionsById = new Map(versions.map((version) => [version.sessionId, version]));
29
- const undoStack = [];
30
- let cursor = versionsById.get(String(sessionId));
31
- while (cursor?.inverseSessionId !== void 0) {
32
- if (undoStack.includes(cursor.inverseSessionId)) break;
33
- undoStack.push(cursor.inverseSessionId);
34
- cursor = versionsById.get(cursor.inverseSessionId);
35
- }
36
- const redoSessionIds = versions.filter((version) => version.inverseSessionId === String(sessionId)).map((version) => version.sessionId);
37
- return {
38
- sessionId: String(sessionId),
39
- messages,
40
- retryableTurns,
41
- versions,
42
- undoStack,
43
- redoSessionIds
44
- };
45
- }
46
- function depthOf(timeline, sessionId) {
47
- const byId = new Map(timeline.nodes.map((node) => [String(node.sessionId), node]));
48
- let depth = 0;
49
- let cursor = byId.get(String(sessionId));
50
- const seen = /* @__PURE__ */ new Set();
51
- while (cursor?.parentSessionId !== void 0 && !seen.has(String(cursor.sessionId))) {
52
- seen.add(String(cursor.sessionId));
53
- depth += 1;
54
- cursor = byId.get(String(cursor.parentSessionId));
55
- }
56
- return depth;
57
- }
58
- //#endregion
59
- //#region src/index.ts
60
- function appendLogSeedEvent(events, type, data, ignorable = false) {
61
- events.push({
62
- type,
63
- seq: events.length,
64
- time: Date.now(),
65
- data,
66
- ...ignorable ? { ignorable: true } : {}
67
- });
68
- }
69
- function appendSurfaceSeedEvent(events, type, data, intent) {
70
- events.push({
71
- type,
72
- seq: SessionSeq(events.length),
73
- time: Date.now(),
74
- data,
75
- surfaceOp: intent.surfaceOp,
76
- ...intent.sourceEventSeqs === void 0 ? {} : { sourceEventSeqs: intent.sourceEventSeqs }
77
- });
78
- }
79
- function appendManualTurn(events, manual) {
80
- const { turn, user, assistant } = manual;
81
- appendLogSeedEvent(events, "turn/start", { turn });
82
- appendSurfaceSeedEvent(events, "user/message", user, { surfaceOp: "append" });
83
- appendLogSeedEvent(events, "step/start", {
84
- turn,
85
- step: 1
86
- });
87
- appendSurfaceSeedEvent(events, "assistant/message", {
88
- turn,
89
- step: 1,
90
- message: assistant,
91
- stream: []
92
- }, { surfaceOp: "append" });
93
- appendLogSeedEvent(events, "step/end", {
94
- turn,
95
- step: 1
96
- });
97
- appendLogSeedEvent(events, "turn/end", {
98
- turn,
99
- reason: { kind: "completed" }
100
- });
101
- }
102
- async function appendSeedSuffixLive(session, seedSuffix, appendDirect) {
103
- for (const event of seedSuffix) {
104
- if (event.ignorable === true) {
105
- const s = session;
106
- const seq = s.log.length;
107
- await appendDirect([{
108
- ...event,
109
- seq
110
- }]);
111
- s.log.push({
112
- ...event,
113
- seq
114
- });
115
- s.eventsSnapshot = void 0;
116
- continue;
117
- }
118
- const s = session;
119
- const raw = event;
120
- if (raw.surfaceOp !== void 0) s.append(event.type, event.data, {
121
- surfaceOp: raw.surfaceOp,
122
- ...raw.sourceEventSeqs === void 0 ? {} : { sourceEventSeqs: raw.sourceEventSeqs }
123
- });
124
- else s.append(event.type, event.data);
125
- }
126
- }
127
- var SessionEditor = class extends Service {
128
- static inject = [
129
- "sessionBranch",
130
- "sessionPersistence",
131
- "sessions"
132
- ];
133
- constructor(ctx) {
134
- super(ctx, "sessionEditor");
135
- registerHttpRoutes(ctx);
136
- }
137
- readBranchPrefix(id, atSeq, mode, signal) {
138
- return this.ctx.sessionBranch.readBranchPrefix(id, atSeq, mode, signal);
139
- }
140
- fork(sourceId, atSeq, childSessionId, meta, signal) {
141
- return this.ctx.sessionBranch.forkFrom(sourceId, {
142
- ...atSeq === void 0 ? {} : { atSeq },
143
- ...childSessionId === void 0 ? {} : { childSessionId },
144
- ...meta === void 0 ? {} : { meta }
145
- }, signal);
146
- }
147
- async rewind(id, toBoundary, signal) {
148
- await this.stopLoop(id, signal);
149
- return this.ctx.sessionBranch.rewind(id, toBoundary, signal);
150
- }
151
- timeline(sessionId, signal) {
152
- return this.ctx.sessionBranch.timeline(sessionId, signal);
153
- }
154
- edit(operation, signal) {
155
- return this.branchOperation(operation, signal);
156
- }
157
- reroll(operation, signal) {
158
- return this.branchOperation(operation, signal);
159
- }
160
- retry(operation, signal) {
161
- return this.branchOperation(operation, signal);
162
- }
163
- recall(operation, signal) {
164
- return this.recallOperation(operation, signal);
165
- }
166
- async editableMessages(sessionId, signal) {
167
- const events = await this.readEvents(sessionId, signal);
168
- return editableMessages(closedTurns(events));
169
- }
170
- async retryableTurns(sessionId, signal) {
171
- const events = await this.readEvents(sessionId, signal);
172
- return retryableTurns(closedTurns(events));
173
- }
174
- async branchOperation(operation, signal) {
175
- signal?.throwIfAborted();
176
- const events = await this.readEvents(operation.sessionId, signal);
177
- const turns = closedTurns(events);
178
- const plan = operation.action === "edit" ? editPlan(operation, turns) : operation.action === "retry" ? retryPlan(operation, turns) : rerollPlan(operation, turns);
179
- const headerConfig = events.findLast((event) => event.type === "request/header")?.data.header.config;
180
- const turnIndex = turns.findIndex((turn) => turn.startSeq === plan.anchorSeq);
181
- const preceding = precedingContentIndex(turns, turnIndex);
182
- const boundary = plan.rewindBoundary !== void 0 ? plan.rewindBoundary : preceding < 0 ? -1 : turns[preceding].endSeq;
183
- const replayTurn = preceding < 0 ? 1 : turns[preceding].turn + 1;
184
- const seedSuffix = [];
185
- appendLogSeedEvent(seedSuffix, "session-branch/version", plan.version, true);
186
- if (plan.manualTurn !== void 0) appendManualTurn(seedSuffix, {
187
- ...plan.manualTurn,
188
- turn: replayTurn
189
- });
190
- const replay = await this.prepareReplay(operation.sessionId, plan.queuedUsers, signal, headerConfig);
191
- await this.stopLoop(operation.sessionId, signal);
192
- const live = this.ctx.sessions.get(operation.sessionId);
193
- await this.ctx.sessionBranch.rewind(operation.sessionId, boundary, signal);
194
- if (seedSuffix.length > 0) {
195
- if (live !== void 0) {
196
- const handle = this.ctx.sessionPersistence.tracker.writerOf(operation.sessionId);
197
- if (handle === void 0) throw new Error(`session "${operation.sessionId}" has no live write handle for the version effect`);
198
- await this.ctx.sessions.flush(live);
199
- await appendSeedSuffixLive(live, seedSuffix, (events) => handle.append(events));
200
- await this.ctx.sessions.flush(live);
201
- } else {
202
- const rawKeepLength = boundary + (plan.rewindBoundary === void 0 ? 1 : 0);
203
- const keepLength = balanceRewindPrefix(events.slice(0, rawKeepLength)).length;
204
- const renumbered = seedSuffix.map((event, index) => ({
205
- ...event,
206
- seq: keepLength + index
207
- }));
208
- const handle = await this.ctx.sessionPersistence.open(operation.sessionId, "write");
209
- try {
210
- await handle.append(renumbered);
211
- } finally {
212
- await handle.close();
213
- }
214
- }
215
- }
216
- let queuedTurns = 0;
217
- if (replay.agent !== void 0 && plan.queuedUsers.length > 0) {
218
- const lastSeedTurn = seedSuffix.findLast((event) => event.type === "turn/start")?.data.turn;
219
- if (lastSeedTurn !== void 0) {
220
- const phase = replay.agent.phase;
221
- if (phase !== void 0) phase.lastTurn = lastSeedTurn;
222
- }
223
- for (const message of plan.queuedUsers) replay.agent.followup(message);
224
- await this.ctx.sessions.flush(replay.agent.session);
225
- queuedTurns = plan.queuedUsers.length;
226
- }
227
- return {
228
- sessionId: operation.sessionId,
229
- queuedTurns,
230
- live: this.ctx.sessions.get(operation.sessionId) !== void 0
231
- };
232
- }
233
- /**
234
- * 撤回(recall):只 rewind 截断,不重写、不重放——被撤回的 user 消息文本
235
- * 由客户端回填到输入框,交给用户修改后重新发送。
236
- *
237
- * 边界见 {@link recallBoundary}:轮首消息整轮截断到前一轮 `turn/end`
238
- * (无前轮则 -1),轮内 followup 与轮外 user 消息只截断到该消息本身
239
- * (exclusive drop),避免留下悬空 `turn/start` 让下一次发送开到错误轮号。
240
- */
241
- async recallOperation(operation, signal) {
242
- signal?.throwIfAborted();
243
- const events = await this.readEvents(operation.sessionId, signal);
244
- const boundary = recallBoundary(events, closedTurns(events), operation.eventSeq);
245
- await this.stopLoop(operation.sessionId, signal);
246
- await this.ctx.sessionBranch.rewind(operation.sessionId, boundary, signal);
247
- return {
248
- sessionId: operation.sessionId,
249
- queuedTurns: 0,
250
- live: this.ctx.sessions.get(operation.sessionId) !== void 0
251
- };
252
- }
253
- async stopLoop(sessionId, signal) {
254
- signal?.throwIfAborted();
255
- const existing = this.ctx.get("agents")?.get(sessionId);
256
- if (existing === void 0) return;
257
- existing.cancel?.({ kind: "user" }, { keepInbox: true });
258
- await existing.whenIdle();
259
- signal?.throwIfAborted();
260
- }
261
- async readEvents(sessionId, signal) {
262
- const live = this.ctx.sessions.get(sessionId);
263
- if (live !== void 0) return live.snapshotEvents();
264
- return (await this.ctx.sessionBranch.readRawEvents(sessionId, signal)).events;
265
- }
266
- async prepareReplay(sessionId, queuedUsers, signal, headerConfig) {
267
- if (queuedUsers.length === 0) return { agent: void 0 };
268
- signal?.throwIfAborted();
269
- const agents = this.ctx.get("agents");
270
- if (agents === void 0) return { agent: void 0 };
271
- const existing = agents.get(sessionId);
272
- if (existing !== void 0) return { agent: existing };
273
- const provider = headerConfig?.provider ?? "";
274
- const model = headerConfig?.model ?? "";
275
- if (provider.length === 0 || model.length === 0) {
276
- const config = (await this.readEvents(sessionId, signal)).findLast((event) => event.type === "request/header")?.data.header.config;
277
- const fallbackProvider = config?.provider ?? "";
278
- const fallbackModel = config?.model ?? "";
279
- if (fallbackProvider.length === 0 || fallbackModel.length === 0) throw new SessionBranchError("无法重放:会话没有可解析的模型配置。", "INVALID_BOUNDARY");
280
- const handle = await agents.resume({
281
- resumeSessionId: sessionId,
282
- agentOptions: {
283
- provider: fallbackProvider,
284
- model: fallbackModel
285
- }
286
- });
287
- signal?.throwIfAborted();
288
- return { agent: handle.agent };
289
- }
290
- const handle = await agents.resume({
291
- resumeSessionId: sessionId,
292
- agentOptions: {
293
- provider,
294
- model
295
- }
296
- });
297
- signal?.throwIfAborted();
298
- return { agent: handle.agent };
299
- }
300
- };
301
- function objectValue(value) {
302
- if (typeof value !== "object" || value === null || Array.isArray(value)) throw new TypeError("请求体必须是 JSON 对象。");
303
- return value;
304
- }
305
- function sessionIdOf(value) {
306
- if (typeof value !== "string" || value.length === 0) throw new TypeError("sessionId 必须是非空字符串。");
307
- return value;
308
- }
309
- function integerOf(value, name) {
310
- if (!Number.isSafeInteger(value) || value < 0) throw new TypeError(`${name} 必须是非负安全整数。`);
311
- return value;
312
- }
313
- function cascadeOf(value) {
314
- if (value !== "truncate" && value !== "preserve") throw new TypeError("cascade 必须是 truncate 或 preserve。");
315
- return value;
316
- }
317
- function decodeOperation(value) {
318
- const record = objectValue(value);
319
- const sessionId = sessionIdOf(record["sessionId"]);
320
- switch (record["action"]) {
321
- case "edit":
322
- if (typeof record["text"] !== "string") throw new TypeError("text 必须是字符串。");
323
- return {
324
- action: "edit",
325
- sessionId,
326
- eventSeq: integerOf(record["eventSeq"], "eventSeq"),
327
- blockIndex: integerOf(record["blockIndex"], "blockIndex"),
328
- text: record["text"],
329
- cascade: cascadeOf(record["cascade"])
330
- };
331
- case "reroll": return {
332
- action: "reroll",
333
- sessionId
334
- };
335
- case "retry": return {
336
- action: "retry",
337
- sessionId,
338
- turn: integerOf(record["turn"], "turn"),
339
- cascade: cascadeOf(record["cascade"])
340
- };
341
- case "rewind": return {
342
- action: "rewind",
343
- sessionId,
344
- toBoundary: integerOf(record["toBoundary"], "toBoundary")
345
- };
346
- case "recall": return {
347
- action: "recall",
348
- sessionId,
349
- eventSeq: integerOf(record["eventSeq"], "eventSeq")
350
- };
351
- case "fork": return {
352
- action: "fork",
353
- sessionId,
354
- ...record["atSeq"] === void 0 ? {} : { atSeq: integerOf(record["atSeq"], "atSeq") },
355
- ...record["childSessionId"] === void 0 ? {} : { childSessionId: sessionIdOf(record["childSessionId"]) }
356
- };
357
- default: throw new TypeError("action 必须是 edit、reroll、retry、rewind、recall 或 fork。");
358
- }
359
- }
360
- function requestJson(request) {
361
- return new Promise((resolve, reject) => {
362
- const decoder = new TextDecoder();
363
- let text = "";
364
- request.on("data", (chunk) => {
365
- text += typeof chunk === "string" ? chunk : decoder.decode(chunk, { stream: true });
366
- });
367
- request.on("end", () => {
368
- try {
369
- text += decoder.decode();
370
- resolve(JSON.parse(text));
371
- } catch (error) {
372
- reject(error);
373
- }
374
- });
375
- request.on("error", reject);
376
- });
377
- }
378
- function respondJson(response, status, value) {
379
- response.writeHead(status, {
380
- "content-type": "application/json; charset=utf-8",
381
- "cache-control": "no-store"
382
- });
383
- response.end(JSON.stringify(value));
384
- }
385
- async function readTimeline(editor, sessionId) {
386
- return toTimelinePayload(sessionId, await editor.timeline(sessionId), await editor.editableMessages(sessionId), await editor.retryableTurns(sessionId));
387
- }
388
- async function runOperation(editor, operation) {
389
- switch (operation.action) {
390
- case "edit": {
391
- const result = await editor.edit(operation);
392
- return {
393
- sessionId: result.sessionId,
394
- queuedTurns: result.queuedTurns,
395
- ...result.live === void 0 ? {} : { live: result.live }
396
- };
397
- }
398
- case "reroll": {
399
- const result = await editor.reroll(operation);
400
- return {
401
- sessionId: result.sessionId,
402
- queuedTurns: result.queuedTurns,
403
- ...result.live === void 0 ? {} : { live: result.live }
404
- };
405
- }
406
- case "retry": {
407
- const result = await editor.retry(operation);
408
- return {
409
- sessionId: result.sessionId,
410
- queuedTurns: result.queuedTurns,
411
- ...result.live === void 0 ? {} : { live: result.live }
412
- };
413
- }
414
- case "rewind":
415
- await editor.rewind(operation.sessionId, operation.toBoundary);
416
- return {
417
- sessionId: operation.sessionId,
418
- queuedTurns: 0
419
- };
420
- case "recall": {
421
- const result = await editor.recall(operation);
422
- return {
423
- sessionId: result.sessionId,
424
- queuedTurns: result.queuedTurns,
425
- ...result.live === void 0 ? {} : { live: result.live }
426
- };
427
- }
428
- case "fork": return {
429
- sessionId: await editor.fork(operation.sessionId, operation.atSeq, operation.childSessionId),
430
- queuedTurns: 0
431
- };
432
- }
433
- }
434
- async function handleRoute(editor, request, response) {
435
- try {
436
- if (request.method === "GET") {
437
- respondJson(response, 200, await readTimeline(editor, sessionIdOf(new URL(request.url ?? "/session-editor", "http://session-editor.local").searchParams.get("sessionId"))));
438
- return;
439
- }
440
- if (request.method === "POST") {
441
- respondJson(response, 200, await runOperation(editor, decodeOperation(await requestJson(request))));
442
- return;
443
- }
444
- response.writeHead(405);
445
- response.end();
446
- } catch (error) {
447
- const message = error instanceof Error ? error.message : String(error);
448
- respondJson(response, error instanceof TypeError ? 400 : 409, { error: message });
449
- }
450
- }
451
- function registerHttpRoutes(ctx) {
452
- ctx.effect(() => {
453
- const webServer = ctx.get("webServer");
454
- if (webServer === void 0) return () => {};
455
- const editor = ctx.sessionEditor;
456
- return webServer.register({
457
- kind: "exact",
458
- path: SESSION_EDITOR_PATH,
459
- handler: (request, response) => handleRoute(editor, request, response)
460
- });
461
- }, "session-editor: HTTP route");
462
- const registerConnectionRoute = () => {
463
- const connection = ctx.get("connection", false);
464
- if (connection === void 0) return;
465
- const editor = ctx.sessionEditor;
466
- ctx.effect(() => connection.fetch.register({
467
- path: `/api${SESSION_EDITOR_PATH}`,
468
- methods: ["GET", "POST"],
469
- requestBody: "buffered",
470
- fetch: (request) => handleFetchRoute(editor, request)
471
- }), "session-editor: connection fetch route");
472
- };
473
- ctx.on("internal/service", (name) => {
474
- if (name === "connection") registerConnectionRoute();
475
- });
476
- registerConnectionRoute();
477
- }
478
- /** connection.fetch 路由的 Fetch 形态处理(与 webServer 的 node:http 形态同语义)。 */
479
- async function handleFetchRoute(editor, request) {
480
- try {
481
- if (request.method === "GET") return jsonResponse(200, await readTimeline(editor, sessionIdOf(new URL(request.url).searchParams.get("sessionId"))));
482
- if (request.method === "POST") return jsonResponse(200, await runOperation(editor, decodeOperation(await request.json())));
483
- return new Response(null, { status: 405 });
484
- } catch (error) {
485
- const message = error instanceof Error ? error.message : String(error);
486
- return jsonResponse(error instanceof TypeError ? 400 : 409, { error: message });
487
- }
488
- }
489
- function jsonResponse(status, value) {
490
- return new Response(JSON.stringify(value), {
491
- status,
492
- headers: {
493
- "content-type": "application/json; charset=utf-8",
494
- "cache-control": "no-store"
495
- }
496
- });
497
- }
498
- //#endregion
499
- export { SessionEditor as n, SESSION_EDITOR_PATH as r, SESSION_BRANCH_VERSION_SCHEMA$1 as t };
@@ -1,86 +0,0 @@
1
- /* Shared message IconActions row (user + assistant). Parent modules own
2
- layout offsets via the composed className. Icons stay visible when mounted;
3
- the time label is hover-revealed inside a data-time-hover-root scope. */
4
-
5
- .actions {
6
- display: flex;
7
- align-items: center;
8
- gap: 10px;
9
- height: 28px;
10
- }
11
-
12
- /* Clock before icons (user figma 388:20051) / after (assistant 43:32997). */
13
- .timeStart {
14
- padding-right: 12px;
15
- font-size: 14px;
16
- line-height: 24px;
17
- color: var(--dsw-alias-label-tertiary);
18
- white-space: nowrap;
19
- }
20
-
21
- .timeEnd {
22
- padding-left: 12px;
23
- font-size: 14px;
24
- line-height: 24px;
25
- color: var(--dsw-alias-label-tertiary);
26
- white-space: nowrap;
27
- }
28
-
29
- /* Separator between the clock and the run-time label (time · Ran for 15s). */
30
- .runTimeDot {
31
- margin: 0 10px;
32
- }
33
-
34
- /* Message containers opt in with data-time-hover-root: the time label fades
35
- in on message hover (or keyboard focus within). Opacity keeps the layout
36
- stable, and devices without hover keep the label always visible. */
37
- @media (hover: hover) {
38
- [data-time-hover-root] :is(.timeStart, .timeEnd) {
39
- opacity: 0;
40
- transition: opacity 80ms ease;
41
- }
42
-
43
- [data-time-hover-root]:hover :is(.timeStart, .timeEnd),
44
- [data-time-hover-root]:focus-within :is(.timeStart, .timeEnd) {
45
- opacity: 1;
46
- }
47
- }
48
-
49
- .action {
50
- display: inline-flex;
51
- align-items: center;
52
- justify-content: center;
53
- width: 28px;
54
- height: 28px;
55
- padding: 6px;
56
- border: none;
57
- border-radius: 28px;
58
- background: transparent;
59
- color: var(--dsw-alias-label-tertiary);
60
- cursor: pointer;
61
- }
62
-
63
- .action:hover {
64
- background: var(--dsw-alias-interactive-bg-hover);
65
- color: var(--dsw-alias-label-secondary);
66
- }
67
-
68
- /* Unavailable stays focusable and hoverable so Tooltip can explain why. */
69
- .action[data-unavailable] {
70
- cursor: default;
71
- opacity: 0.4;
72
- }
73
-
74
- .action[data-unavailable]:hover {
75
- background: transparent;
76
- color: var(--dsw-alias-label-tertiary);
77
- }
78
-
79
- .visuallyHidden {
80
- position: absolute;
81
- width: 1px;
82
- height: 1px;
83
- overflow: hidden;
84
- clip: rect(0 0 0 0);
85
- white-space: nowrap;
86
- }