@chestnut23/dsh-conversation-outline 0.1.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.
package/lib/client.js ADDED
@@ -0,0 +1,750 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "@chestnut23/dsh-conversation-outline",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ let react_jsx_runtime = require("react/jsx-runtime");
8
+ let react = require("react");
9
+ //#region lib/client/outline.js
10
+ /**
11
+ * Pure logic for the conversation outline (implementation-spec §2.3).
12
+ *
13
+ * Deliberately has NO DOM access and NO React imports so scripts/verify.mjs can
14
+ * import it directly in Node from the tsc output (lib/client/outline.js) and
15
+ * assert the derivations offline.
16
+ *
17
+ * Input shapes are structural "lite" views of the runtime snapshot types
18
+ * (@deepseek-ai/dsh-client-runtime/client) — the real objects are assignable
19
+ * to them, and Node can import this module without the browser type graph.
20
+ */
21
+ const IMAGE_PLACEHOLDER = "[image]";
22
+ /**
23
+ * Flatten message content blocks into a single display string: text blocks are
24
+ * joined, image blocks become a placeholder, and whitespace is trimmed and
25
+ * collapsed.
26
+ */
27
+ function flattenQuestionText(content) {
28
+ return content.map((block) => {
29
+ if (block.type === "text") return block.text ?? "";
30
+ if (block.type === "image") return IMAGE_PLACEHOLDER;
31
+ return "";
32
+ }).join(" ").trim().replace(/\s+/g, " ");
33
+ }
34
+ /** Turn number of a node location: step/turn → location.turn.turn, else undefined. */
35
+ function turnOf(location) {
36
+ if (!location) return void 0;
37
+ if (location.kind === "step" || location.kind === "turn") return location.turn?.turn;
38
+ }
39
+ /**
40
+ * Walk the chat flow in order, keep user/steering nodes, skip empty text, and
41
+ * produce chronological outline items (spec §2.3).
42
+ */
43
+ function collectQuestions(snapshot) {
44
+ const items = [];
45
+ for (const key of snapshot.chat.order) {
46
+ const node = snapshot.chat.nodes.get(key);
47
+ if (!node) continue;
48
+ if (node.kind !== "user" && node.kind !== "steering") continue;
49
+ const data = node.data;
50
+ const text = flattenQuestionText(data?.content ?? []);
51
+ if (!text) continue;
52
+ items.push({
53
+ key: node.key,
54
+ kind: node.kind,
55
+ seq: data?.seq ?? 0,
56
+ time: data?.time ?? 0,
57
+ turn: turnOf(node.location),
58
+ text
59
+ });
60
+ }
61
+ return items;
62
+ }
63
+ /** Case-insensitive substring filter over the flattened question text. */
64
+ function filterQuestions(items, query) {
65
+ const q = query.trim().toLowerCase();
66
+ if (!q) return items;
67
+ return items.filter((item) => item.text.toLowerCase().includes(q));
68
+ }
69
+ /** Local `HH:MM` from a unix-ms timestamp (spec §2.3). */
70
+ function formatTime(ms) {
71
+ const d = new Date(ms);
72
+ return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
73
+ }
74
+ /** Pure DOM predicate for the jump loop: is this row the target node? */
75
+ function isJumpTargetRow(row, key) {
76
+ return row.getAttribute("data-chat-anchor-key") === key;
77
+ }
78
+ //#endregion
79
+ //#region lib/client/OutlinePanel.js
80
+ /** Stable getSnapshot value while no session is current (uSES contract). */
81
+ const NO_SESSION = null;
82
+ const JUMP_HEADROOM = 96;
83
+ const JUMP_TIMEOUT_MS = 1500;
84
+ const FLASH_MS = 1900;
85
+ const COPIED_MS = 1500;
86
+ /** Grace period before the hover panel collapses (lets the pointer travel). */
87
+ const COLLAPSE_DELAY_MS = 240;
88
+ /** Rail capacity; older questions fold into the "+N" marker. */
89
+ const MAX_BARS = 60;
90
+ /**
91
+ * The conversation view's header tablist, scoped to the conversation root:
92
+ * walk up from the scrollport's PARENT (the scrollport itself may contain
93
+ * other tablists, e.g. the trajectory event-details tabs) and return the
94
+ * nearest ancestor containing a `[role="tablist"]`. Never falls back to a
95
+ * document-wide query, so other tablists (trajectory event details,
96
+ * settings, cordis source viewer) are not hit.
97
+ */
98
+ function findConversationTablist(scrollport) {
99
+ let node = scrollport.parentElement;
100
+ while (node) {
101
+ const tablist = node.querySelector("[role=\"tablist\"]");
102
+ if (tablist) return tablist;
103
+ node = node.parentElement;
104
+ }
105
+ return null;
106
+ }
107
+ /** RAF that prunes itself from the tracking ref once it fires. */
108
+ function scheduleRaf(rafRef, fn) {
109
+ const id = requestAnimationFrame(() => {
110
+ rafRef.current = rafRef.current.filter((x) => x !== id);
111
+ fn();
112
+ });
113
+ rafRef.current.push(id);
114
+ }
115
+ /** Timeout that prunes itself from the tracking ref once it fires. */
116
+ function scheduleTimeout(timeoutRef, fn, ms) {
117
+ const id = window.setTimeout(() => {
118
+ timeoutRef.current = timeoutRef.current.filter((x) => x !== id);
119
+ fn();
120
+ }, ms);
121
+ timeoutRef.current.push(id);
122
+ }
123
+ function OutlinePanel({ sessions, useSessions, t }) {
124
+ const current = useSessions((s) => s.current);
125
+ const session = current ? sessions.binding(current)?.session : void 0;
126
+ const snapshot = (0, react.useSyncExternalStore)((0, react.useCallback)((onChange) => session ? session.subscribe(onChange) : () => {}, [session]), (0, react.useCallback)(() => session ? session.getSnapshot() : NO_SESSION, [session]));
127
+ const [open, setOpen] = (0, react.useState)(false);
128
+ const [query, setQuery] = (0, react.useState)("");
129
+ const [copiedKey, setCopiedKey] = (0, react.useState)(null);
130
+ const timeoutsRef = (0, react.useRef)([]);
131
+ const rafsRef = (0, react.useRef)([]);
132
+ const flashedRowRef = (0, react.useRef)(null);
133
+ const collapseTimerRef = (0, react.useRef)(null);
134
+ const clearPending = (0, react.useCallback)(() => {
135
+ for (const id of rafsRef.current) window.cancelAnimationFrame(id);
136
+ for (const id of timeoutsRef.current) window.clearTimeout(id);
137
+ rafsRef.current = [];
138
+ timeoutsRef.current = [];
139
+ if (collapseTimerRef.current !== null) {
140
+ window.clearTimeout(collapseTimerRef.current);
141
+ collapseTimerRef.current = null;
142
+ }
143
+ if (flashedRowRef.current) {
144
+ flashedRowRef.current.removeAttribute("data-dsh-outline-flash");
145
+ flashedRowRef.current = null;
146
+ }
147
+ }, []);
148
+ (0, react.useEffect)(() => {
149
+ clearPending();
150
+ setOpen(false);
151
+ }, [current, clearPending]);
152
+ (0, react.useEffect)(() => {
153
+ if (!open) return;
154
+ const onKey = (event) => {
155
+ if (event.key === "Escape") setOpen(false);
156
+ };
157
+ window.addEventListener("keydown", onKey);
158
+ return () => window.removeEventListener("keydown", onKey);
159
+ }, [open]);
160
+ (0, react.useEffect)(() => clearPending, [clearPending]);
161
+ const allItems = (0, react.useMemo)(() => snapshot ? collectQuestions(snapshot) : [], [snapshot]);
162
+ const items = (0, react.useMemo)(() => filterQuestions(allItems, query), [allItems, query]);
163
+ const blank = snapshot?.blank ?? true;
164
+ const hasMore = snapshot?.hasMore ?? false;
165
+ const loadingOlder = snapshot?.loadingOlder ?? false;
166
+ const cancelCollapse = (0, react.useCallback)(() => {
167
+ if (collapseTimerRef.current !== null) {
168
+ window.clearTimeout(collapseTimerRef.current);
169
+ collapseTimerRef.current = null;
170
+ }
171
+ }, []);
172
+ const scheduleCollapse = (0, react.useCallback)(() => {
173
+ cancelCollapse();
174
+ collapseTimerRef.current = window.setTimeout(() => {
175
+ collapseTimerRef.current = null;
176
+ setOpen(false);
177
+ }, COLLAPSE_DELAY_MS);
178
+ }, [cancelCollapse]);
179
+ const showPanel = (0, react.useCallback)(() => {
180
+ cancelCollapse();
181
+ setOpen(true);
182
+ }, [cancelCollapse]);
183
+ /** Find the chat row for a node key (pure predicate over DOM rows). */
184
+ const findRow = (0, react.useCallback)((key) => {
185
+ const rows = document.querySelectorAll("[data-chat-anchor-key]");
186
+ for (const row of rows) if (isJumpTargetRow(row, key)) return row;
187
+ return null;
188
+ }, []);
189
+ /** Scroll the row into view (96px headroom) and flash a highlight. */
190
+ const flashAndScroll = (0, react.useCallback)((row) => {
191
+ const scrollport = row.closest("[data-conversation-scroll]") ?? document.querySelector("[data-conversation-scroll]");
192
+ if (scrollport) {
193
+ const flowTop = row.getBoundingClientRect().top - scrollport.getBoundingClientRect().top;
194
+ const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
195
+ scrollport.scrollTo({
196
+ top: scrollport.scrollTop + flowTop - JUMP_HEADROOM,
197
+ behavior: reduced ? "auto" : "smooth"
198
+ });
199
+ }
200
+ if (flashedRowRef.current && flashedRowRef.current !== row) flashedRowRef.current.removeAttribute("data-dsh-outline-flash");
201
+ flashedRowRef.current = row;
202
+ row.setAttribute("data-dsh-outline-flash", "true");
203
+ scheduleTimeout(timeoutsRef, () => {
204
+ if (flashedRowRef.current === row) {
205
+ row.removeAttribute("data-dsh-outline-flash");
206
+ flashedRowRef.current = null;
207
+ }
208
+ }, FLASH_MS);
209
+ }, []);
210
+ /** Jump-to-message algorithm (spec §2.2). */
211
+ const jumpTo = (0, react.useCallback)((key) => {
212
+ const scrollport = document.querySelector("[data-conversation-scroll]");
213
+ if (!scrollport) return;
214
+ const tablist = findConversationTablist(scrollport);
215
+ if (tablist) {
216
+ const firstTab = tablist.querySelector("button[role=\"tab\"]");
217
+ if (firstTab) firstTab.click();
218
+ }
219
+ const deadline = Date.now() + JUMP_TIMEOUT_MS;
220
+ const poll = () => {
221
+ const row = findRow(key);
222
+ if (row) {
223
+ flashAndScroll(row);
224
+ setOpen(false);
225
+ return;
226
+ }
227
+ if (Date.now() < deadline) scheduleRaf(rafsRef, poll);
228
+ else console.warn(t("jumpFailed"), { key });
229
+ };
230
+ scheduleRaf(rafsRef, poll);
231
+ }, [
232
+ findRow,
233
+ flashAndScroll,
234
+ t
235
+ ]);
236
+ /** Copy a question's text (clipboard API with execCommand fallback). */
237
+ const copyText = (0, react.useCallback)((text, key) => {
238
+ const done = () => {
239
+ setCopiedKey(key);
240
+ scheduleTimeout(timeoutsRef, () => setCopiedKey((k) => k === key ? null : k), COPIED_MS);
241
+ };
242
+ const fallback = () => {
243
+ try {
244
+ const ta = document.createElement("textarea");
245
+ ta.value = text;
246
+ ta.setAttribute("readonly", "");
247
+ ta.style.position = "fixed";
248
+ ta.style.opacity = "0";
249
+ document.body.appendChild(ta);
250
+ ta.select();
251
+ document.execCommand("copy");
252
+ ta.remove();
253
+ done();
254
+ } catch {}
255
+ };
256
+ if (navigator.clipboard?.writeText) navigator.clipboard.writeText(text).then(done, fallback);
257
+ else fallback();
258
+ }, []);
259
+ if (!current || !session || blank) return null;
260
+ const start = Math.max(0, allItems.length - MAX_BARS);
261
+ const bars = allItems.slice(start);
262
+ const overflow = start;
263
+ return (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [(0, react_jsx_runtime.jsxs)("div", {
264
+ className: "dso-rail",
265
+ role: "group",
266
+ "aria-label": t("title"),
267
+ tabIndex: 0,
268
+ onMouseEnter: showPanel,
269
+ onMouseLeave: scheduleCollapse,
270
+ onFocus: showPanel,
271
+ onBlur: scheduleCollapse,
272
+ onClick: (event) => {
273
+ if (event.target === event.currentTarget) setOpen((v) => !v);
274
+ },
275
+ onKeyDown: (event) => {
276
+ if (event.key === "Enter" || event.key === " ") {
277
+ event.preventDefault();
278
+ setOpen((v) => !v);
279
+ }
280
+ },
281
+ children: [overflow > 0 && (0, react_jsx_runtime.jsxs)("span", {
282
+ className: "dso-rail-more",
283
+ "aria-label": t("moreBars", { count: overflow }),
284
+ children: ["+", overflow]
285
+ }), bars.map((item, index) => (0, react_jsx_runtime.jsx)("button", {
286
+ type: "button",
287
+ className: "dso-bar",
288
+ "aria-label": t("barLabel", { n: start + index + 1 }),
289
+ onClick: () => jumpTo(item.key)
290
+ }, item.key))]
291
+ }), open && (0, react_jsx_runtime.jsxs)("div", {
292
+ className: "dso-panel",
293
+ role: "region",
294
+ "aria-label": t("title"),
295
+ onMouseEnter: showPanel,
296
+ onMouseLeave: scheduleCollapse,
297
+ onFocus: showPanel,
298
+ onBlur: scheduleCollapse,
299
+ children: [
300
+ (0, react_jsx_runtime.jsxs)("header", {
301
+ className: "dso-panel-header",
302
+ children: [
303
+ (0, react_jsx_runtime.jsx)("span", {
304
+ className: "dso-panel-title",
305
+ children: t("title")
306
+ }),
307
+ (0, react_jsx_runtime.jsx)("span", {
308
+ className: "dso-panel-count",
309
+ children: t("count", { count: allItems.length })
310
+ }),
311
+ (0, react_jsx_runtime.jsx)("button", {
312
+ type: "button",
313
+ className: "dso-panel-close",
314
+ "aria-label": t("close"),
315
+ onClick: () => setOpen(false),
316
+ children: "×"
317
+ })
318
+ ]
319
+ }),
320
+ (0, react_jsx_runtime.jsx)("input", {
321
+ className: "dso-search",
322
+ type: "search",
323
+ value: query,
324
+ placeholder: t("searchPlaceholder"),
325
+ "aria-label": t("searchPlaceholder"),
326
+ onChange: (event) => setQuery(event.target.value)
327
+ }),
328
+ (0, react_jsx_runtime.jsx)("div", {
329
+ className: "dso-list",
330
+ children: items.length === 0 ? (0, react_jsx_runtime.jsx)("div", {
331
+ className: "dso-empty",
332
+ children: t("empty")
333
+ }) : items.map((item) => (0, react_jsx_runtime.jsxs)("div", {
334
+ className: "dso-row",
335
+ children: [(0, react_jsx_runtime.jsxs)("button", {
336
+ type: "button",
337
+ className: "dso-row-main",
338
+ onClick: () => jumpTo(item.key),
339
+ children: [
340
+ (0, react_jsx_runtime.jsx)("span", {
341
+ className: "dso-turn",
342
+ children: item.turn !== void 0 ? `#${item.turn}` : ""
343
+ }),
344
+ (0, react_jsx_runtime.jsx)("span", {
345
+ className: "dso-text",
346
+ children: item.text
347
+ }),
348
+ item.kind === "steering" && (0, react_jsx_runtime.jsx)("span", {
349
+ className: "dso-steer",
350
+ children: t("steerTag")
351
+ }),
352
+ (0, react_jsx_runtime.jsx)("span", {
353
+ className: "dso-time",
354
+ children: formatTime(item.time)
355
+ })
356
+ ]
357
+ }), (0, react_jsx_runtime.jsx)("button", {
358
+ type: "button",
359
+ className: copiedKey === item.key ? "dso-copy dso-copied" : "dso-copy",
360
+ "aria-label": copiedKey === item.key ? t("copied") : t("copy"),
361
+ onClick: () => copyText(item.text, item.key),
362
+ children: copiedKey === item.key ? t("copied") : t("copy")
363
+ })]
364
+ }, item.key))
365
+ }),
366
+ hasMore && (0, react_jsx_runtime.jsx)("footer", {
367
+ className: "dso-footer",
368
+ children: (0, react_jsx_runtime.jsx)("button", {
369
+ type: "button",
370
+ className: "dso-load-older",
371
+ disabled: loadingOlder,
372
+ onClick: () => {
373
+ session.loadOlder();
374
+ },
375
+ children: loadingOlder ? t("loadingOlder") : t("loadOlder")
376
+ })
377
+ })
378
+ ]
379
+ })] });
380
+ }
381
+ //#endregion
382
+ //#region lib/client/locales.js
383
+ const NS = "dsh-conversation-outline";
384
+ const dictionaries = {
385
+ zh: {
386
+ title: "会话大纲",
387
+ count: "{count} 个问题",
388
+ searchPlaceholder: "搜索问题…",
389
+ empty: "当前会话还没有问题",
390
+ loadOlder: "加载更早",
391
+ loadingOlder: "加载中…",
392
+ copy: "复制",
393
+ copied: "已复制",
394
+ steerTag: "追问",
395
+ close: "关闭",
396
+ jumpFailed: "跳转失败:未找到对应消息",
397
+ barLabel: "跳到第 {n} 个问题",
398
+ moreBars: "还有 {count} 个更早的问题"
399
+ },
400
+ en: {
401
+ title: "Outline",
402
+ count: "{count} questions",
403
+ searchPlaceholder: "Search questions…",
404
+ empty: "No questions in this conversation yet",
405
+ loadOlder: "Load older",
406
+ loadingOlder: "Loading…",
407
+ copy: "Copy",
408
+ copied: "Copied",
409
+ steerTag: "steer",
410
+ close: "Close",
411
+ jumpFailed: "Jump failed: target message not found",
412
+ barLabel: "Jump to question {n}",
413
+ moreBars: "{count} more earlier questions"
414
+ }
415
+ };
416
+ //#endregion
417
+ //#region lib/client/styles.js
418
+ /**
419
+ * Outline rail + hover panel CSS (implementation-spec §1.8/§2.1, revised):
420
+ * a thin right-edge rail (conversation minimap) that is always visible while
421
+ * the session has questions, and a preview panel that expands on hover.
422
+ * Plain CSS string injected via an HMR-safe
423
+ * `<style data-plugin="dsh-conversation-outline" data-plugin-css="...">` tag.
424
+ * Stable prefixed class names (`dso_*`), DSH theme vars (`--dsw-alias-*`) —
425
+ * never hashed class names of other packages. No layout shift: the panel is a
426
+ * pure overlay.
427
+ */
428
+ const panelCss = `
429
+ /* ---- Right-edge rail (always-visible minimap) --------------------------- */
430
+ .dso-rail {
431
+ position: fixed;
432
+ top: 50%;
433
+ right: 0;
434
+ transform: translateY(-50%);
435
+ z-index: 2147483000;
436
+ box-sizing: border-box;
437
+ display: flex;
438
+ flex-direction: column;
439
+ align-items: center;
440
+ gap: 3px;
441
+ width: 30px;
442
+ max-height: 55vh;
443
+ padding: 8px 10px;
444
+ overflow: hidden;
445
+ cursor: pointer;
446
+ }
447
+ .dso-rail-more {
448
+ flex: none;
449
+ font-size: 9px;
450
+ font-weight: 600;
451
+ line-height: 10px;
452
+ color: var(--dsw-alias-label-tertiary);
453
+ font-variant-numeric: tabular-nums;
454
+ }
455
+ .dso-bar {
456
+ flex: none;
457
+ width: 10px;
458
+ height: 6px;
459
+ padding: 0;
460
+ border: none;
461
+ border-radius: 3px;
462
+ background: color-mix(in srgb, var(--dsw-alias-label-tertiary) 45%, transparent);
463
+ cursor: pointer;
464
+ transition: background 0.12s, transform 0.12s;
465
+ }
466
+ .dso-bar:hover {
467
+ background: var(--dsw-alias-state-business-primary);
468
+ transform: scaleX(1.35);
469
+ }
470
+ .dso-rail:focus-visible,
471
+ .dso-bar:focus-visible {
472
+ outline: 2px solid var(--dsw-alias-state-business-primary);
473
+ outline-offset: 2px;
474
+ }
475
+
476
+ /* ---- Hover-expanded preview panel --------------------------------------- */
477
+ .dso-panel {
478
+ position: fixed;
479
+ top: 50%;
480
+ right: 16px;
481
+ transform: translateY(-50%);
482
+ z-index: 2147483000;
483
+ box-sizing: border-box;
484
+ display: flex;
485
+ flex-direction: column;
486
+ width: min(340px, calc(100vw - 24px));
487
+ max-height: min(70vh, 600px);
488
+ overflow: hidden;
489
+ border: 1px solid var(--dsw-alias-line-normal);
490
+ border-radius: 16px;
491
+ background: color-mix(in srgb, var(--dsw-alias-bg-module-platform) 92%, transparent);
492
+ backdrop-filter: blur(20px);
493
+ box-shadow: 0 16px 48px color-mix(in srgb, var(--dsw-alias-label-primary) 18%, transparent);
494
+ color: var(--dsw-alias-label-primary);
495
+ animation: dso-panel-in 140ms ease-out;
496
+ }
497
+ @keyframes dso-panel-in {
498
+ from { opacity: 0; transform: translateY(-50%) translateX(12px); }
499
+ to { opacity: 1; transform: translateY(-50%) translateX(0); }
500
+ }
501
+
502
+ .dso-panel-header {
503
+ display: flex;
504
+ align-items: center;
505
+ gap: 8px;
506
+ padding: 12px 14px 10px;
507
+ border-bottom: 1px solid var(--dsw-alias-line-normal);
508
+ }
509
+ .dso-panel-title {
510
+ font-size: 14px;
511
+ font-weight: 600;
512
+ color: var(--dsw-alias-label-primary);
513
+ }
514
+ .dso-panel-count {
515
+ flex: 1;
516
+ font-size: 12px;
517
+ color: var(--dsw-alias-label-tertiary);
518
+ }
519
+ .dso-panel-close {
520
+ display: inline-flex;
521
+ align-items: center;
522
+ justify-content: center;
523
+ width: 24px;
524
+ height: 24px;
525
+ border: none;
526
+ border-radius: 6px;
527
+ background: transparent;
528
+ color: var(--dsw-alias-label-secondary);
529
+ font-size: 16px;
530
+ line-height: 1;
531
+ cursor: pointer;
532
+ transition: background 0.12s;
533
+ }
534
+ .dso-panel-close:hover { background: var(--dsw-alias-interactive-bg-hover-solid); }
535
+
536
+ .dso-panel-close:focus-visible,
537
+ .dso-search:focus-visible,
538
+ .dso-row-main:focus-visible,
539
+ .dso-copy:focus-visible,
540
+ .dso-load-older:focus-visible {
541
+ outline: 2px solid var(--dsw-alias-state-business-primary);
542
+ outline-offset: 2px;
543
+ }
544
+
545
+ .dso-search {
546
+ margin: 10px 12px 4px;
547
+ box-sizing: border-box;
548
+ height: 30px;
549
+ padding: 0 10px;
550
+ border: 1px solid var(--dsw-alias-line-normal);
551
+ border-radius: 8px;
552
+ background: var(--dsw-alias-interactive-bg-hover-solid);
553
+ color: var(--dsw-alias-label-primary);
554
+ font: inherit;
555
+ font-size: 12px;
556
+ outline: none;
557
+ }
558
+ .dso-search::placeholder { color: var(--dsw-alias-label-tertiary); }
559
+
560
+ /* ---- Question list ------------------------------------------------------ */
561
+ .dso-list {
562
+ flex: 1;
563
+ min-height: 0;
564
+ overflow-y: auto;
565
+ display: flex;
566
+ flex-direction: column;
567
+ gap: 2px;
568
+ padding: 6px;
569
+ }
570
+ /* Row container: two sibling buttons (jump + copy) — no nested interactive
571
+ controls. Hover highlights the whole row. */
572
+ .dso-row {
573
+ display: flex;
574
+ align-items: center;
575
+ gap: 4px;
576
+ padding: 2px;
577
+ border-radius: 10px;
578
+ transition: background 0.12s;
579
+ }
580
+ .dso-row:hover { background: var(--dsw-alias-interactive-bg-hover-solid); }
581
+ .dso-row-main {
582
+ flex: 1;
583
+ min-width: 0;
584
+ display: flex;
585
+ align-items: center;
586
+ gap: 6px;
587
+ height: 28px;
588
+ padding: 2px 6px;
589
+ border: none;
590
+ border-radius: 8px;
591
+ background: transparent;
592
+ color: inherit;
593
+ font: inherit;
594
+ text-align: left;
595
+ cursor: pointer;
596
+ transition: background 0.12s;
597
+ }
598
+ .dso-row-main:hover { background: var(--dsw-alias-interactive-bg-hover-solid); }
599
+
600
+ .dso-turn {
601
+ flex: none;
602
+ min-width: 26px;
603
+ height: 16px;
604
+ display: inline-flex;
605
+ align-items: center;
606
+ justify-content: center;
607
+ border-radius: 8px;
608
+ background: var(--dsw-alias-bg-fill-business);
609
+ color: var(--dsw-alias-label-on-fill);
610
+ font-size: 10px;
611
+ font-weight: 600;
612
+ font-variant-numeric: tabular-nums;
613
+ }
614
+ /* Single-line truncation: shows the opening words of each question —
615
+ anything longer simply ellipsizes (per user request). */
616
+ .dso-text {
617
+ flex: 1;
618
+ min-width: 0;
619
+ font-size: 13px;
620
+ line-height: 18px;
621
+ white-space: nowrap;
622
+ overflow: hidden;
623
+ text-overflow: ellipsis;
624
+ }
625
+ .dso-time {
626
+ flex: none;
627
+ font-size: 11px;
628
+ font-variant-numeric: tabular-nums;
629
+ color: var(--dsw-alias-label-tertiary);
630
+ }
631
+ .dso-steer {
632
+ flex: none;
633
+ padding: 0 6px;
634
+ border: 1px solid var(--dsw-alias-line-normal);
635
+ border-radius: 999px;
636
+ font-size: 10px;
637
+ color: var(--dsw-alias-label-secondary);
638
+ }
639
+ .dso-copy {
640
+ flex: none;
641
+ padding: 2px 6px;
642
+ border: none;
643
+ border-radius: 6px;
644
+ background: transparent;
645
+ color: var(--dsw-alias-label-secondary);
646
+ font: inherit;
647
+ font-size: 11px;
648
+ cursor: pointer;
649
+ opacity: 0;
650
+ transition: opacity 0.12s, color 0.12s, background 0.12s;
651
+ }
652
+ .dso-row:hover .dso-copy,
653
+ .dso-row:focus-within .dso-copy,
654
+ .dso-copy:focus-visible { opacity: 1; }
655
+ .dso-copy:hover { background: var(--dsw-alias-bg-fill-neutral); }
656
+ .dso-copy.dso-copied { color: var(--dsw-alias-state-success); }
657
+
658
+ .dso-empty {
659
+ padding: 28px 16px;
660
+ text-align: center;
661
+ font-size: 13px;
662
+ color: var(--dsw-alias-label-tertiary);
663
+ }
664
+
665
+ /* ---- Footer / load older ------------------------------------------------ */
666
+ .dso-footer {
667
+ display: flex;
668
+ justify-content: center;
669
+ padding: 8px 12px 10px;
670
+ border-top: 1px solid var(--dsw-alias-line-normal);
671
+ }
672
+ .dso-load-older {
673
+ padding: 4px 14px;
674
+ border: none;
675
+ border-radius: 14px;
676
+ background: var(--dsw-alias-interactive-bg-hover-solid);
677
+ color: var(--dsw-alias-label-secondary);
678
+ font: inherit;
679
+ font-size: 12px;
680
+ cursor: pointer;
681
+ }
682
+ .dso-load-older:disabled { cursor: default; opacity: 0.6; }
683
+
684
+ /* ---- Jump flash highlight ----------------------------------------------- */
685
+ [data-dsh-outline-flash] {
686
+ animation: dso-flash 1.8s ease-out;
687
+ }
688
+ @keyframes dso-flash {
689
+ 0% {
690
+ box-shadow: 0 0 0 0 color-mix(in srgb, var(--dsw-alias-state-business-primary) 50%, transparent);
691
+ background-color: color-mix(in srgb, var(--dsw-alias-state-business-primary) 24%, transparent);
692
+ }
693
+ 70% {
694
+ box-shadow: 0 0 0 6px transparent;
695
+ background-color: color-mix(in srgb, var(--dsw-alias-state-business-primary) 8%, transparent);
696
+ }
697
+ 100% {
698
+ box-shadow: 0 0 0 0 transparent;
699
+ background-color: transparent;
700
+ }
701
+ }
702
+
703
+ @media (prefers-reduced-motion: reduce) {
704
+ .dso-panel { animation: none; }
705
+ .dso-bar { transition: none; }
706
+ [data-dsh-outline-flash] { animation: none; }
707
+ }
708
+ `;
709
+ /** Inject the style tag, owned by the client fiber (removed on dispose/HMR). */
710
+ function injectStyle(ctx) {
711
+ ctx.effect(() => {
712
+ const tag = document.createElement("style");
713
+ tag.dataset.plugin = "dsh-conversation-outline";
714
+ tag.dataset.pluginCss = "dsh-conversation-outline/panel.css";
715
+ tag.textContent = panelCss;
716
+ document.head.appendChild(tag);
717
+ return () => {
718
+ tag.remove();
719
+ };
720
+ }, "dsh-conversation-outline: panel css");
721
+ }
722
+ //#endregion
723
+ //#region lib/client/index.js
724
+ /**
725
+ * Client entry (implementation-spec §1.1/§2.4): the browser half of the
726
+ * plugin. Cordis services this fiber waits for before apply() runs.
727
+ */
728
+ const inject = [
729
+ "slots",
730
+ "sessions",
731
+ "locale"
732
+ ];
733
+ function apply(ctx) {
734
+ ctx.effect(() => ctx.locale.register(NS, dictionaries), "dsh-conversation-outline: locale");
735
+ injectStyle(ctx);
736
+ ctx.slots.inject("shell.overlay", () => ctx.slots.register({
737
+ name: "shell.overlay",
738
+ id: "dsh-conversation-outline.rail",
739
+ locale: NS,
740
+ inject: () => ({ sessions: ctx.sessions })
741
+ }, OutlinePanel));
742
+ }
743
+ //#endregion
744
+ exports.apply = apply;
745
+ exports.inject = inject;
746
+ return module.exports;
747
+ }
748
+ });
749
+
750
+ //# sourceMappingURL=client.js.map