@chestnut23/dsh-conversation-outline 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -53,13 +53,27 @@ export function OutlinePanel({ sessions, useSessions, t }) {
53
53
  const subscribe = useCallback((onChange) => (session ? session.subscribe(onChange) : () => { }), [session]);
54
54
  const getSnapshot = useCallback(() => (session ? session.getSnapshot() : NO_SESSION), [session]);
55
55
  const snapshot = useSyncExternalStore(subscribe, getSnapshot);
56
- const [open, setOpen] = useState(false);
56
+ // Two independent sources for the panel being open: `pinned` is a click
57
+ // gesture (sticky until Escape / outside click / session change), `hovered`
58
+ // is pointer presence over the rail or the panel. Deriving `open` from both
59
+ // means a click on the rail opens the panel even when no hover event ever
60
+ // fires (touch, synthetic/automation input, or a pointer that entered the
61
+ // strip without crossing its boundary), and a hover preview still collapses
62
+ // on its own once the pointer leaves.
63
+ const [pinned, setPinned] = useState(false);
64
+ const [hovered, setHovered] = useState(false);
65
+ const open = pinned || hovered;
57
66
  const [query, setQuery] = useState('');
58
67
  const [copiedKey, setCopiedKey] = useState(null);
68
+ // The question whose jump could not be completed; the panel says so instead
69
+ // of failing silently (the row may sit outside the loaded history window).
70
+ const [jumpFailed, setJumpFailed] = useState(null);
59
71
  const timeoutsRef = useRef([]);
60
72
  const rafsRef = useRef([]);
61
73
  const flashedRowRef = useRef(null);
62
74
  const collapseTimerRef = useRef(null);
75
+ const railRef = useRef(null);
76
+ const panelRef = useRef(null);
63
77
  // Cancel pending jump work (RAFs/timeouts), the flash highlight, and a
64
78
  // scheduled hover collapse.
65
79
  const clearPending = useCallback(() => {
@@ -78,23 +92,47 @@ export function OutlinePanel({ sessions, useSessions, t }) {
78
92
  flashedRowRef.current = null;
79
93
  }
80
94
  }, []);
95
+ /** Close the panel from any trigger: drop the pin and the hover preview. */
96
+ const closePanel = useCallback(() => {
97
+ setPinned(false);
98
+ setHovered(false);
99
+ cancelCollapseRef.current();
100
+ }, []);
81
101
  // Session change → cancel pending work and collapse (navigate closes the
82
102
  // panel, spec §2.1; a mid-jump poll must not target the stale session).
83
103
  useEffect(() => {
84
104
  clearPending();
85
- setOpen(false);
105
+ setPinned(false);
106
+ setHovered(false);
107
+ setJumpFailed(null);
86
108
  }, [current, clearPending]);
87
- // Escape closes the panel (hover-opened or tap-pinned).
109
+ // Escape closes the panel (hover-opened or click-pinned).
88
110
  useEffect(() => {
89
111
  if (!open)
90
112
  return;
91
113
  const onKey = (event) => {
92
114
  if (event.key === 'Escape')
93
- setOpen(false);
115
+ closePanel();
94
116
  };
95
117
  window.addEventListener('keydown', onKey);
96
118
  return () => window.removeEventListener('keydown', onKey);
97
- }, [open]);
119
+ }, [open, closePanel]);
120
+ // A click anywhere outside the rail and the panel releases a pin, so the
121
+ // panel behaves like every other dismissible overlay on the page.
122
+ useEffect(() => {
123
+ if (!pinned)
124
+ return;
125
+ const onPointerDown = (event) => {
126
+ const target = event.target;
127
+ if (!(target instanceof Node))
128
+ return;
129
+ if (railRef.current?.contains(target) || panelRef.current?.contains(target))
130
+ return;
131
+ closePanel();
132
+ };
133
+ window.addEventListener('mousedown', onPointerDown);
134
+ return () => window.removeEventListener('mousedown', onPointerDown);
135
+ }, [pinned, closePanel]);
98
136
  // Unmount cleanup: cancel pending flash/copied timeouts, jump RAFs, collapse
99
137
  // timer, and the flash attribute.
100
138
  useEffect(() => clearPending, [clearPending]);
@@ -103,30 +141,64 @@ export function OutlinePanel({ sessions, useSessions, t }) {
103
141
  const blank = snapshot?.blank ?? true;
104
142
  const hasMore = snapshot?.hasMore ?? false;
105
143
  const loadingOlder = snapshot?.loadingOlder ?? false;
106
- // ---- hover-open mechanics (rail panel travel survives via the grace) ---
144
+ // ---- open mechanics: click pins, hover previews, grace period between ---
107
145
  const cancelCollapse = useCallback(() => {
108
146
  if (collapseTimerRef.current !== null) {
109
147
  window.clearTimeout(collapseTimerRef.current);
110
148
  collapseTimerRef.current = null;
111
149
  }
112
150
  }, []);
151
+ // `closePanel` above runs before this declaration; a ref keeps the two
152
+ // callbacks independent of declaration order.
153
+ const cancelCollapseRef = useRef(cancelCollapse);
154
+ cancelCollapseRef.current = cancelCollapse;
113
155
  const scheduleCollapse = useCallback(() => {
114
156
  cancelCollapse();
115
157
  collapseTimerRef.current = window.setTimeout(() => {
116
158
  collapseTimerRef.current = null;
117
- setOpen(false);
159
+ // Only a preview collapses on its own: a pinned panel stays until it is
160
+ // closed, and a pointer still inside the rail or the panel (Chromium
161
+ // raises no further enter events while the pointer sits still) keeps the
162
+ // preview up.
163
+ if (railRef.current?.matches(':hover') || panelRef.current?.matches(':hover'))
164
+ return;
165
+ setHovered(false);
118
166
  }, COLLAPSE_DELAY_MS);
119
167
  }, [cancelCollapse]);
120
- const showPanel = useCallback(() => {
168
+ /** Pointer entered the rail or the panel: cancel a pending collapse, preview. */
169
+ const onEnter = useCallback(() => {
121
170
  cancelCollapse();
122
- setOpen(true);
171
+ setHovered(true);
123
172
  }, [cancelCollapse]);
173
+ /** Pointer left: let the grace period decide, unless the panel is pinned. */
174
+ const onLeave = useCallback(() => {
175
+ if (pinned)
176
+ return;
177
+ scheduleCollapse();
178
+ }, [pinned, scheduleCollapse]);
179
+ /** A click on the rail (or any bar) opens the panel and keeps it open. */
180
+ const togglePin = useCallback(() => {
181
+ cancelCollapse();
182
+ setPinned((value) => !value);
183
+ setHovered(true);
184
+ }, [cancelCollapse]);
185
+ /** The panel's own controls: clicking inside never closes it. */
186
+ const keepOpen = useCallback(() => {
187
+ cancelCollapse();
188
+ setHovered(true);
189
+ }, [cancelCollapse]);
190
+ /** Grid of candidate keys for one node: the anchor the row renders first. */
191
+ function jumpKeyCandidates(key) {
192
+ return key.includes('|') ? [key.split('|')[0], key] : [key];
193
+ }
124
194
  /** Find the chat row for a node key (pure predicate over DOM rows). */
125
195
  const findRow = useCallback((key) => {
126
196
  const rows = document.querySelectorAll('[data-chat-anchor-key]');
127
- for (const row of rows) {
128
- if (isJumpTargetRow(row, key))
129
- return row;
197
+ for (const candidate of jumpKeyCandidates(key)) {
198
+ for (const row of rows) {
199
+ if (isJumpTargetRow(row, candidate))
200
+ return row;
201
+ }
130
202
  }
131
203
  return null;
132
204
  }, []);
@@ -155,11 +227,12 @@ export function OutlinePanel({ sessions, useSessions, t }) {
155
227
  }, FLASH_MS);
156
228
  }, []);
157
229
  /** Jump-to-message algorithm (spec §2.2). */
158
- const jumpTo = useCallback((key) => {
230
+ const jumpTo = useCallback((key, label) => {
159
231
  // The conversation page must be mounted; abort silently otherwise.
160
232
  const scrollport = document.querySelector('[data-conversation-scroll]');
161
233
  if (!scrollport)
162
234
  return;
235
+ setJumpFailed(null);
163
236
  // Ensure the Chat view is active: chat is order 0 — the FIRST tab of the
164
237
  // conversation-root tablist, when one exists (setView is idempotent).
165
238
  // A profile without extra views has no header tablist at all — that is
@@ -178,18 +251,21 @@ export function OutlinePanel({ sessions, useSessions, t }) {
178
251
  const row = findRow(key);
179
252
  if (row) {
180
253
  flashAndScroll(row);
181
- setOpen(false);
254
+ closePanel();
182
255
  return;
183
256
  }
184
257
  if (Date.now() < deadline) {
185
258
  scheduleRaf(rafsRef, poll);
186
259
  }
187
260
  else {
261
+ // Never fail silently: the panel states what happened, and the
262
+ // console keeps the key for bug reports.
188
263
  console.warn(t('jumpFailed'), { key });
264
+ setJumpFailed(label ?? key);
189
265
  }
190
266
  };
191
267
  scheduleRaf(rafsRef, poll);
192
- }, [findRow, flashAndScroll, t]);
268
+ }, [findRow, flashAndScroll, t, closePanel]);
193
269
  /** Copy a question's text (clipboard API with execCommand fallback). */
194
270
  const copyText = useCallback((text, key) => {
195
271
  const done = () => {
@@ -231,16 +307,27 @@ export function OutlinePanel({ sessions, useSessions, t }) {
231
307
  const start = Math.max(0, allItems.length - MAX_BARS);
232
308
  const bars = allItems.slice(start);
233
309
  const overflow = start;
234
- return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "dso-rail", role: "group", "aria-label": t('title'), tabIndex: 0, onMouseEnter: showPanel, onMouseLeave: scheduleCollapse, onFocus: showPanel, onBlur: scheduleCollapse, onClick: (event) => {
235
- // Tap (touch) on the strip itself pins/unpins the panel.
236
- if (event.target === event.currentTarget)
237
- setOpen((v) => !v);
310
+ return (_jsxs(_Fragment, { children: [_jsxs("div", { ref: railRef, className: "dso-rail", role: "group", "aria-label": t('title'), tabIndex: 0, onMouseEnter: onEnter, onMouseLeave: onLeave, onFocus: onEnter, onBlur: onLeave, onClick: (event) => {
311
+ // Any click on the strip pins the panel open — the container itself,
312
+ // its padding, or the "+N" marker. Bars stop propagation and reach
313
+ // the same pin through the shared jump gesture below.
314
+ if (event.target === event.currentTarget || event.target instanceof HTMLSpanElement) {
315
+ togglePin();
316
+ }
238
317
  }, onKeyDown: (event) => {
239
318
  if (event.key === 'Enter' || event.key === ' ') {
240
319
  event.preventDefault();
241
- setOpen((v) => !v);
320
+ togglePin();
242
321
  }
243
- }, children: [overflow > 0 && (_jsxs("span", { className: "dso-rail-more", "aria-label": t('moreBars', { count: overflow }), children: ["+", overflow] })), bars.map((item, index) => (_jsx("button", { type: "button", className: "dso-bar", "aria-label": t('barLabel', { n: start + index + 1 }), onClick: () => jumpTo(item.key) }, item.key)))] }), open && (_jsxs("div", { className: "dso-panel", role: "region", "aria-label": t('title'), onMouseEnter: showPanel, onMouseLeave: scheduleCollapse, onFocus: showPanel, onBlur: scheduleCollapse, children: [_jsxs("header", { className: "dso-panel-header", children: [_jsx("span", { className: "dso-panel-title", children: t('title') }), _jsx("span", { className: "dso-panel-count", children: t('count', { count: allItems.length }) }), _jsx("button", { type: "button", className: "dso-panel-close", "aria-label": t('close'), onClick: () => setOpen(false), children: "\u00D7" })] }), _jsx("input", { className: "dso-search", type: "search", value: query, placeholder: t('searchPlaceholder'), "aria-label": t('searchPlaceholder'), onChange: (event) => setQuery(event.target.value) }), _jsx("div", { className: "dso-list", children: items.length === 0 ? (_jsx("div", { className: "dso-empty", children: t('empty') })) : (items.map((item) => (_jsxs("div", { className: "dso-row", children: [_jsxs("button", { type: "button", className: "dso-row-main", onClick: () => jumpTo(item.key), children: [_jsx("span", { className: "dso-turn", children: item.turn !== undefined ? `#${item.turn}` : '' }), _jsx("span", { className: "dso-text", children: item.text }), item.kind === 'steering' && _jsx("span", { className: "dso-steer", children: t('steerTag') }), _jsx("span", { className: "dso-time", children: formatTime(item.time) })] }), _jsx("button", { type: "button", className: copiedKey === item.key ? 'dso-copy dso-copied' : 'dso-copy', "aria-label": copiedKey === item.key ? t('copied') : t('copy'), onClick: () => copyText(item.text, item.key), children: copiedKey === item.key ? t('copied') : t('copy') })] }, item.key)))) }), hasMore && (_jsx("footer", { className: "dso-footer", children: _jsx("button", { type: "button", className: "dso-load-older", disabled: loadingOlder, onClick: () => {
322
+ }, "data-dso-open": open ? 'true' : undefined, children: [overflow > 0 && (_jsxs("span", { className: "dso-rail-more", "aria-label": t('moreBars', { count: overflow }), children: ["+", overflow] })), bars.map((item, index) => (_jsx("button", { type: "button", className: "dso-bar", "aria-label": t('barLabel', { n: start + index + 1 }), onClick: (event) => {
323
+ event.stopPropagation();
324
+ // The bar's own job is the jump; the panel comes up pinned first
325
+ // so a click always has a visible effect, then jumpTo collapses
326
+ // it once the row is on screen.
327
+ setPinned(true);
328
+ setHovered(true);
329
+ jumpTo(item.key, item.text);
330
+ } }, item.key)))] }), open && (_jsxs("div", { ref: panelRef, className: "dso-panel", role: "region", "aria-label": t('title'), "data-dso-pinned": pinned ? 'true' : undefined, onMouseEnter: onEnter, onMouseLeave: onLeave, onFocus: onEnter, onBlur: onLeave, onMouseDown: keepOpen, children: [_jsxs("header", { className: "dso-panel-header", children: [_jsx("span", { className: "dso-panel-title", children: t('title') }), _jsx("span", { className: "dso-panel-count", children: t('count', { count: allItems.length }) }), _jsx("button", { type: "button", className: "dso-panel-close", "aria-label": t('close'), onClick: closePanel, children: "\u00D7" })] }), _jsx("input", { className: "dso-search", type: "search", value: query, placeholder: t('searchPlaceholder'), "aria-label": t('searchPlaceholder'), onChange: (event) => setQuery(event.target.value) }), _jsx("div", { className: "dso-list", children: items.length === 0 ? (_jsx("div", { className: "dso-empty", children: t('empty') })) : (items.map((item) => (_jsxs("div", { className: "dso-row", children: [_jsxs("button", { type: "button", className: "dso-row-main", onClick: () => jumpTo(item.key, item.text), children: [_jsx("span", { className: "dso-turn", children: item.turn !== undefined ? `#${item.turn}` : '' }), _jsx("span", { className: "dso-text", children: item.text }), item.kind === 'steering' && _jsx("span", { className: "dso-steer", children: t('steerTag') }), _jsx("span", { className: "dso-time", children: formatTime(item.time) })] }), _jsx("button", { type: "button", className: copiedKey === item.key ? 'dso-copy dso-copied' : 'dso-copy', "aria-label": copiedKey === item.key ? t('copied') : t('copy'), onClick: () => copyText(item.text, item.key), children: copiedKey === item.key ? t('copied') : t('copy') })] }, item.key)))) }), jumpFailed !== null && (_jsx("p", { className: "dso-jump-failed", role: "status", "data-dso-jump-failed": "true", children: t('jumpFailed') })), hasMore && (_jsx("footer", { className: "dso-footer", children: _jsx("button", { type: "button", className: "dso-load-older", disabled: loadingOlder, onClick: () => {
244
331
  void session.loadOlder();
245
332
  }, children: loadingOlder ? t('loadingOlder') : t('loadOlder') }) }))] }))] }));
246
333
  }
@@ -55,6 +55,15 @@ export const panelCss = `
55
55
  outline: 2px solid var(--dsw-alias-state-business-primary);
56
56
  outline-offset: 2px;
57
57
  }
58
+ /* Clickability affordance: a hovered or pinned strip lights its bars up, so
59
+ it reads as a control rather than as decoration. */
60
+ .dso-rail:hover .dso-bar,
61
+ .dso-rail[data-dso-open] .dso-bar {
62
+ background: color-mix(in srgb, var(--dsw-alias-label-tertiary) 72%, transparent);
63
+ }
64
+ .dso-rail:hover .dso-bar:hover {
65
+ background: var(--dsw-alias-state-business-primary);
66
+ }
58
67
 
59
68
  /* ---- Hover-expanded preview panel --------------------------------------- */
60
69
  .dso-panel {
@@ -77,6 +86,11 @@ export const panelCss = `
77
86
  color: var(--dsw-alias-label-primary);
78
87
  animation: dso-panel-in 140ms ease-out;
79
88
  }
89
+ /* A pinned panel (clicked open) keeps a defined edge, so it is obvious that it
90
+ no longer collapses when the pointer leaves. */
91
+ .dso-panel[data-dso-pinned] {
92
+ border-color: var(--dsw-alias-state-business-primary);
93
+ }
80
94
  @keyframes dso-panel-in {
81
95
  from { opacity: 0; transform: translateY(-50%) translateX(12px); }
82
96
  to { opacity: 1; transform: translateY(-50%) translateX(0); }
@@ -245,6 +259,16 @@ export const panelCss = `
245
259
  color: var(--dsw-alias-label-tertiary);
246
260
  }
247
261
 
262
+ /* ---- Jump failure notice ------------------------------------------------ */
263
+ .dso-jump-failed {
264
+ margin: 0;
265
+ padding: 8px 14px;
266
+ border-top: 1px solid var(--dsw-alias-line-normal);
267
+ font-size: 12px;
268
+ line-height: 1.5;
269
+ color: var(--dsw-alias-state-warning, var(--dsw-alias-label-secondary));
270
+ }
271
+
248
272
  /* ---- Footer / load older ------------------------------------------------ */
249
273
  .dso-footer {
250
274
  display: flex;
package/lib/client.js CHANGED
@@ -124,13 +124,18 @@ window.__ModuleLoader__.load({
124
124
  const current = useSessions((s) => s.current);
125
125
  const session = current ? sessions.binding(current)?.session : void 0;
126
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);
127
+ const [pinned, setPinned] = (0, react.useState)(false);
128
+ const [hovered, setHovered] = (0, react.useState)(false);
129
+ const open = pinned || hovered;
128
130
  const [query, setQuery] = (0, react.useState)("");
129
131
  const [copiedKey, setCopiedKey] = (0, react.useState)(null);
132
+ const [jumpFailed, setJumpFailed] = (0, react.useState)(null);
130
133
  const timeoutsRef = (0, react.useRef)([]);
131
134
  const rafsRef = (0, react.useRef)([]);
132
135
  const flashedRowRef = (0, react.useRef)(null);
133
136
  const collapseTimerRef = (0, react.useRef)(null);
137
+ const railRef = (0, react.useRef)(null);
138
+ const panelRef = (0, react.useRef)(null);
134
139
  const clearPending = (0, react.useCallback)(() => {
135
140
  for (const id of rafsRef.current) window.cancelAnimationFrame(id);
136
141
  for (const id of timeoutsRef.current) window.clearTimeout(id);
@@ -145,18 +150,37 @@ window.__ModuleLoader__.load({
145
150
  flashedRowRef.current = null;
146
151
  }
147
152
  }, []);
153
+ /** Close the panel from any trigger: drop the pin and the hover preview. */
154
+ const closePanel = (0, react.useCallback)(() => {
155
+ setPinned(false);
156
+ setHovered(false);
157
+ cancelCollapseRef.current();
158
+ }, []);
148
159
  (0, react.useEffect)(() => {
149
160
  clearPending();
150
- setOpen(false);
161
+ setPinned(false);
162
+ setHovered(false);
163
+ setJumpFailed(null);
151
164
  }, [current, clearPending]);
152
165
  (0, react.useEffect)(() => {
153
166
  if (!open) return;
154
167
  const onKey = (event) => {
155
- if (event.key === "Escape") setOpen(false);
168
+ if (event.key === "Escape") closePanel();
156
169
  };
157
170
  window.addEventListener("keydown", onKey);
158
171
  return () => window.removeEventListener("keydown", onKey);
159
- }, [open]);
172
+ }, [open, closePanel]);
173
+ (0, react.useEffect)(() => {
174
+ if (!pinned) return;
175
+ const onPointerDown = (event) => {
176
+ const target = event.target;
177
+ if (!(target instanceof Node)) return;
178
+ if (railRef.current?.contains(target) || panelRef.current?.contains(target)) return;
179
+ closePanel();
180
+ };
181
+ window.addEventListener("mousedown", onPointerDown);
182
+ return () => window.removeEventListener("mousedown", onPointerDown);
183
+ }, [pinned, closePanel]);
160
184
  (0, react.useEffect)(() => clearPending, [clearPending]);
161
185
  const allItems = (0, react.useMemo)(() => snapshot ? collectQuestions(snapshot) : [], [snapshot]);
162
186
  const items = (0, react.useMemo)(() => filterQuestions(allItems, query), [allItems, query]);
@@ -169,21 +193,45 @@ window.__ModuleLoader__.load({
169
193
  collapseTimerRef.current = null;
170
194
  }
171
195
  }, []);
196
+ const cancelCollapseRef = (0, react.useRef)(cancelCollapse);
197
+ cancelCollapseRef.current = cancelCollapse;
172
198
  const scheduleCollapse = (0, react.useCallback)(() => {
173
199
  cancelCollapse();
174
200
  collapseTimerRef.current = window.setTimeout(() => {
175
201
  collapseTimerRef.current = null;
176
- setOpen(false);
202
+ if (railRef.current?.matches(":hover") || panelRef.current?.matches(":hover")) return;
203
+ setHovered(false);
177
204
  }, COLLAPSE_DELAY_MS);
178
205
  }, [cancelCollapse]);
179
- const showPanel = (0, react.useCallback)(() => {
206
+ /** Pointer entered the rail or the panel: cancel a pending collapse, preview. */
207
+ const onEnter = (0, react.useCallback)(() => {
208
+ cancelCollapse();
209
+ setHovered(true);
210
+ }, [cancelCollapse]);
211
+ /** Pointer left: let the grace period decide, unless the panel is pinned. */
212
+ const onLeave = (0, react.useCallback)(() => {
213
+ if (pinned) return;
214
+ scheduleCollapse();
215
+ }, [pinned, scheduleCollapse]);
216
+ /** A click on the rail (or any bar) opens the panel and keeps it open. */
217
+ const togglePin = (0, react.useCallback)(() => {
180
218
  cancelCollapse();
181
- setOpen(true);
219
+ setPinned((value) => !value);
220
+ setHovered(true);
182
221
  }, [cancelCollapse]);
222
+ /** The panel's own controls: clicking inside never closes it. */
223
+ const keepOpen = (0, react.useCallback)(() => {
224
+ cancelCollapse();
225
+ setHovered(true);
226
+ }, [cancelCollapse]);
227
+ /** Grid of candidate keys for one node: the anchor the row renders first. */
228
+ function jumpKeyCandidates(key) {
229
+ return key.includes("|") ? [key.split("|")[0], key] : [key];
230
+ }
183
231
  /** Find the chat row for a node key (pure predicate over DOM rows). */
184
232
  const findRow = (0, react.useCallback)((key) => {
185
233
  const rows = document.querySelectorAll("[data-chat-anchor-key]");
186
- for (const row of rows) if (isJumpTargetRow(row, key)) return row;
234
+ for (const candidate of jumpKeyCandidates(key)) for (const row of rows) if (isJumpTargetRow(row, candidate)) return row;
187
235
  return null;
188
236
  }, []);
189
237
  /** Scroll the row into view (96px headroom) and flash a highlight. */
@@ -208,9 +256,10 @@ window.__ModuleLoader__.load({
208
256
  }, FLASH_MS);
209
257
  }, []);
210
258
  /** Jump-to-message algorithm (spec §2.2). */
211
- const jumpTo = (0, react.useCallback)((key) => {
259
+ const jumpTo = (0, react.useCallback)((key, label) => {
212
260
  const scrollport = document.querySelector("[data-conversation-scroll]");
213
261
  if (!scrollport) return;
262
+ setJumpFailed(null);
214
263
  const tablist = findConversationTablist(scrollport);
215
264
  if (tablist) {
216
265
  const firstTab = tablist.querySelector("button[role=\"tab\"]");
@@ -221,17 +270,21 @@ window.__ModuleLoader__.load({
221
270
  const row = findRow(key);
222
271
  if (row) {
223
272
  flashAndScroll(row);
224
- setOpen(false);
273
+ closePanel();
225
274
  return;
226
275
  }
227
276
  if (Date.now() < deadline) scheduleRaf(rafsRef, poll);
228
- else console.warn(t("jumpFailed"), { key });
277
+ else {
278
+ console.warn(t("jumpFailed"), { key });
279
+ setJumpFailed(label ?? key);
280
+ }
229
281
  };
230
282
  scheduleRaf(rafsRef, poll);
231
283
  }, [
232
284
  findRow,
233
285
  flashAndScroll,
234
- t
286
+ t,
287
+ closePanel
235
288
  ]);
236
289
  /** Copy a question's text (clipboard API with execCommand fallback). */
237
290
  const copyText = (0, react.useCallback)((text, key) => {
@@ -261,23 +314,25 @@ window.__ModuleLoader__.load({
261
314
  const bars = allItems.slice(start);
262
315
  const overflow = start;
263
316
  return (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [(0, react_jsx_runtime.jsxs)("div", {
317
+ ref: railRef,
264
318
  className: "dso-rail",
265
319
  role: "group",
266
320
  "aria-label": t("title"),
267
321
  tabIndex: 0,
268
- onMouseEnter: showPanel,
269
- onMouseLeave: scheduleCollapse,
270
- onFocus: showPanel,
271
- onBlur: scheduleCollapse,
322
+ onMouseEnter: onEnter,
323
+ onMouseLeave: onLeave,
324
+ onFocus: onEnter,
325
+ onBlur: onLeave,
272
326
  onClick: (event) => {
273
- if (event.target === event.currentTarget) setOpen((v) => !v);
327
+ if (event.target === event.currentTarget || event.target instanceof HTMLSpanElement) togglePin();
274
328
  },
275
329
  onKeyDown: (event) => {
276
330
  if (event.key === "Enter" || event.key === " ") {
277
331
  event.preventDefault();
278
- setOpen((v) => !v);
332
+ togglePin();
279
333
  }
280
334
  },
335
+ "data-dso-open": open ? "true" : void 0,
281
336
  children: [overflow > 0 && (0, react_jsx_runtime.jsxs)("span", {
282
337
  className: "dso-rail-more",
283
338
  "aria-label": t("moreBars", { count: overflow }),
@@ -286,16 +341,24 @@ window.__ModuleLoader__.load({
286
341
  type: "button",
287
342
  className: "dso-bar",
288
343
  "aria-label": t("barLabel", { n: start + index + 1 }),
289
- onClick: () => jumpTo(item.key)
344
+ onClick: (event) => {
345
+ event.stopPropagation();
346
+ setPinned(true);
347
+ setHovered(true);
348
+ jumpTo(item.key, item.text);
349
+ }
290
350
  }, item.key))]
291
351
  }), open && (0, react_jsx_runtime.jsxs)("div", {
352
+ ref: panelRef,
292
353
  className: "dso-panel",
293
354
  role: "region",
294
355
  "aria-label": t("title"),
295
- onMouseEnter: showPanel,
296
- onMouseLeave: scheduleCollapse,
297
- onFocus: showPanel,
298
- onBlur: scheduleCollapse,
356
+ "data-dso-pinned": pinned ? "true" : void 0,
357
+ onMouseEnter: onEnter,
358
+ onMouseLeave: onLeave,
359
+ onFocus: onEnter,
360
+ onBlur: onLeave,
361
+ onMouseDown: keepOpen,
299
362
  children: [
300
363
  (0, react_jsx_runtime.jsxs)("header", {
301
364
  className: "dso-panel-header",
@@ -312,7 +375,7 @@ window.__ModuleLoader__.load({
312
375
  type: "button",
313
376
  className: "dso-panel-close",
314
377
  "aria-label": t("close"),
315
- onClick: () => setOpen(false),
378
+ onClick: closePanel,
316
379
  children: "×"
317
380
  })
318
381
  ]
@@ -335,7 +398,7 @@ window.__ModuleLoader__.load({
335
398
  children: [(0, react_jsx_runtime.jsxs)("button", {
336
399
  type: "button",
337
400
  className: "dso-row-main",
338
- onClick: () => jumpTo(item.key),
401
+ onClick: () => jumpTo(item.key, item.text),
339
402
  children: [
340
403
  (0, react_jsx_runtime.jsx)("span", {
341
404
  className: "dso-turn",
@@ -363,6 +426,12 @@ window.__ModuleLoader__.load({
363
426
  })]
364
427
  }, item.key))
365
428
  }),
429
+ jumpFailed !== null && (0, react_jsx_runtime.jsx)("p", {
430
+ className: "dso-jump-failed",
431
+ role: "status",
432
+ "data-dso-jump-failed": "true",
433
+ children: t("jumpFailed")
434
+ }),
366
435
  hasMore && (0, react_jsx_runtime.jsx)("footer", {
367
436
  className: "dso-footer",
368
437
  children: (0, react_jsx_runtime.jsx)("button", {
@@ -472,6 +541,15 @@ window.__ModuleLoader__.load({
472
541
  outline: 2px solid var(--dsw-alias-state-business-primary);
473
542
  outline-offset: 2px;
474
543
  }
544
+ /* Clickability affordance: a hovered or pinned strip lights its bars up, so
545
+ it reads as a control rather than as decoration. */
546
+ .dso-rail:hover .dso-bar,
547
+ .dso-rail[data-dso-open] .dso-bar {
548
+ background: color-mix(in srgb, var(--dsw-alias-label-tertiary) 72%, transparent);
549
+ }
550
+ .dso-rail:hover .dso-bar:hover {
551
+ background: var(--dsw-alias-state-business-primary);
552
+ }
475
553
 
476
554
  /* ---- Hover-expanded preview panel --------------------------------------- */
477
555
  .dso-panel {
@@ -494,6 +572,11 @@ window.__ModuleLoader__.load({
494
572
  color: var(--dsw-alias-label-primary);
495
573
  animation: dso-panel-in 140ms ease-out;
496
574
  }
575
+ /* A pinned panel (clicked open) keeps a defined edge, so it is obvious that it
576
+ no longer collapses when the pointer leaves. */
577
+ .dso-panel[data-dso-pinned] {
578
+ border-color: var(--dsw-alias-state-business-primary);
579
+ }
497
580
  @keyframes dso-panel-in {
498
581
  from { opacity: 0; transform: translateY(-50%) translateX(12px); }
499
582
  to { opacity: 1; transform: translateY(-50%) translateX(0); }
@@ -662,6 +745,16 @@ window.__ModuleLoader__.load({
662
745
  color: var(--dsw-alias-label-tertiary);
663
746
  }
664
747
 
748
+ /* ---- Jump failure notice ------------------------------------------------ */
749
+ .dso-jump-failed {
750
+ margin: 0;
751
+ padding: 8px 14px;
752
+ border-top: 1px solid var(--dsw-alias-line-normal);
753
+ font-size: 12px;
754
+ line-height: 1.5;
755
+ color: var(--dsw-alias-state-warning, var(--dsw-alias-label-secondary));
756
+ }
757
+
665
758
  /* ---- Footer / load older ------------------------------------------------ */
666
759
  .dso-footer {
667
760
  display: flex;
package/lib/client.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","names":["_Fragment"],"sources":["client/outline.js","client/OutlinePanel.js","client/locales.js","client/styles.js","client/index.js"],"sourcesContent":["/**\n * Pure logic for the conversation outline (implementation-spec §2.3).\n *\n * Deliberately has NO DOM access and NO React imports so scripts/verify.mjs can\n * import it directly in Node from the tsc output (lib/client/outline.js) and\n * assert the derivations offline.\n *\n * Input shapes are structural \"lite\" views of the runtime snapshot types\n * (@deepseek-ai/dsh-client-runtime/client) — the real objects are assignable\n * to them, and Node can import this module without the browser type graph.\n */\nconst IMAGE_PLACEHOLDER = '[image]';\n/**\n * Flatten message content blocks into a single display string: text blocks are\n * joined, image blocks become a placeholder, and whitespace is trimmed and\n * collapsed.\n */\nexport function flattenQuestionText(content) {\n return content\n .map((block) => {\n if (block.type === 'text')\n return block.text ?? '';\n if (block.type === 'image')\n return IMAGE_PLACEHOLDER;\n return '';\n })\n .join(' ')\n .trim()\n .replace(/\\s+/g, ' ');\n}\n/** Turn number of a node location: step/turn → location.turn.turn, else undefined. */\nfunction turnOf(location) {\n if (!location)\n return undefined;\n if (location.kind === 'step' || location.kind === 'turn')\n return location.turn?.turn;\n return undefined;\n}\n/**\n * Walk the chat flow in order, keep user/steering nodes, skip empty text, and\n * produce chronological outline items (spec §2.3).\n */\nexport function collectQuestions(snapshot) {\n const items = [];\n for (const key of snapshot.chat.order) {\n const node = snapshot.chat.nodes.get(key);\n if (!node)\n continue;\n if (node.kind !== 'user' && node.kind !== 'steering')\n continue;\n const data = node.data;\n const text = flattenQuestionText(data?.content ?? []);\n if (!text)\n continue;\n items.push({\n key: node.key,\n kind: node.kind,\n seq: data?.seq ?? 0,\n time: data?.time ?? 0,\n turn: turnOf(node.location),\n text,\n });\n }\n return items;\n}\n/** Case-insensitive substring filter over the flattened question text. */\nexport function filterQuestions(items, query) {\n const q = query.trim().toLowerCase();\n if (!q)\n return items;\n return items.filter((item) => item.text.toLowerCase().includes(q));\n}\n/** Local `HH:MM` from a unix-ms timestamp (spec §2.3). */\nexport function formatTime(ms) {\n const d = new Date(ms);\n const hh = String(d.getHours()).padStart(2, '0');\n const mm = String(d.getMinutes()).padStart(2, '0');\n return `${hh}:${mm}`;\n}\n/** Pure DOM predicate for the jump loop: is this row the target node? */\nexport function isJumpTargetRow(row, key) {\n return row.getAttribute('data-chat-anchor-key') === key;\n}\n","import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from \"react/jsx-runtime\";\nimport { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore, } from 'react';\nimport { collectQuestions, filterQuestions, formatTime, isJumpTargetRow, } from \"./outline.js\";\n/** Stable getSnapshot value while no session is current (uSES contract). */\nconst NO_SESSION = null;\nconst JUMP_HEADROOM = 96;\nconst JUMP_TIMEOUT_MS = 1500;\nconst FLASH_MS = 1900;\nconst COPIED_MS = 1500;\n/** Grace period before the hover panel collapses (lets the pointer travel). */\nconst COLLAPSE_DELAY_MS = 240;\n/** Rail capacity; older questions fold into the \"+N\" marker. */\nconst MAX_BARS = 60;\n/**\n * The conversation view's header tablist, scoped to the conversation root:\n * walk up from the scrollport's PARENT (the scrollport itself may contain\n * other tablists, e.g. the trajectory event-details tabs) and return the\n * nearest ancestor containing a `[role=\"tablist\"]`. Never falls back to a\n * document-wide query, so other tablists (trajectory event details,\n * settings, cordis source viewer) are not hit.\n */\nfunction findConversationTablist(scrollport) {\n let node = scrollport.parentElement;\n while (node) {\n const tablist = node.querySelector('[role=\"tablist\"]');\n if (tablist)\n return tablist;\n node = node.parentElement;\n }\n return null;\n}\n/** RAF that prunes itself from the tracking ref once it fires. */\nfunction scheduleRaf(rafRef, fn) {\n const id = requestAnimationFrame(() => {\n rafRef.current = rafRef.current.filter((x) => x !== id);\n fn();\n });\n rafRef.current.push(id);\n}\n/** Timeout that prunes itself from the tracking ref once it fires. */\nfunction scheduleTimeout(timeoutRef, fn, ms) {\n const id = window.setTimeout(() => {\n timeoutRef.current = timeoutRef.current.filter((x) => x !== id);\n fn();\n }, ms);\n timeoutRef.current.push(id);\n}\nexport function OutlinePanel({ sessions, useSessions, t }) {\n const current = useSessions((s) => s.current);\n const session = current ? sessions.binding(current)?.session : undefined;\n // Conversation snapshot subscription (spec §1.5): live updates while the\n // session runs, load-older, blank detection — all ride this one store.\n const subscribe = useCallback((onChange) => (session ? session.subscribe(onChange) : () => { }), [session]);\n const getSnapshot = useCallback(() => (session ? session.getSnapshot() : NO_SESSION), [session]);\n const snapshot = useSyncExternalStore(subscribe, getSnapshot);\n const [open, setOpen] = useState(false);\n const [query, setQuery] = useState('');\n const [copiedKey, setCopiedKey] = useState(null);\n const timeoutsRef = useRef([]);\n const rafsRef = useRef([]);\n const flashedRowRef = useRef(null);\n const collapseTimerRef = useRef(null);\n // Cancel pending jump work (RAFs/timeouts), the flash highlight, and a\n // scheduled hover collapse.\n const clearPending = useCallback(() => {\n for (const id of rafsRef.current)\n window.cancelAnimationFrame(id);\n for (const id of timeoutsRef.current)\n window.clearTimeout(id);\n rafsRef.current = [];\n timeoutsRef.current = [];\n if (collapseTimerRef.current !== null) {\n window.clearTimeout(collapseTimerRef.current);\n collapseTimerRef.current = null;\n }\n if (flashedRowRef.current) {\n flashedRowRef.current.removeAttribute('data-dsh-outline-flash');\n flashedRowRef.current = null;\n }\n }, []);\n // Session change → cancel pending work and collapse (navigate closes the\n // panel, spec §2.1; a mid-jump poll must not target the stale session).\n useEffect(() => {\n clearPending();\n setOpen(false);\n }, [current, clearPending]);\n // Escape closes the panel (hover-opened or tap-pinned).\n useEffect(() => {\n if (!open)\n return;\n const onKey = (event) => {\n if (event.key === 'Escape')\n setOpen(false);\n };\n window.addEventListener('keydown', onKey);\n return () => window.removeEventListener('keydown', onKey);\n }, [open]);\n // Unmount cleanup: cancel pending flash/copied timeouts, jump RAFs, collapse\n // timer, and the flash attribute.\n useEffect(() => clearPending, [clearPending]);\n const allItems = useMemo(() => (snapshot ? collectQuestions(snapshot) : []), [snapshot]);\n const items = useMemo(() => filterQuestions(allItems, query), [allItems, query]);\n const blank = snapshot?.blank ?? true;\n const hasMore = snapshot?.hasMore ?? false;\n const loadingOlder = snapshot?.loadingOlder ?? false;\n // ---- hover-open mechanics (rail ⇄ panel travel survives via the grace) ---\n const cancelCollapse = useCallback(() => {\n if (collapseTimerRef.current !== null) {\n window.clearTimeout(collapseTimerRef.current);\n collapseTimerRef.current = null;\n }\n }, []);\n const scheduleCollapse = useCallback(() => {\n cancelCollapse();\n collapseTimerRef.current = window.setTimeout(() => {\n collapseTimerRef.current = null;\n setOpen(false);\n }, COLLAPSE_DELAY_MS);\n }, [cancelCollapse]);\n const showPanel = useCallback(() => {\n cancelCollapse();\n setOpen(true);\n }, [cancelCollapse]);\n /** Find the chat row for a node key (pure predicate over DOM rows). */\n const findRow = useCallback((key) => {\n const rows = document.querySelectorAll('[data-chat-anchor-key]');\n for (const row of rows) {\n if (isJumpTargetRow(row, key))\n return row;\n }\n return null;\n }, []);\n /** Scroll the row into view (96px headroom) and flash a highlight. */\n const flashAndScroll = useCallback((row) => {\n const scrollport = row.closest('[data-conversation-scroll]') ?? document.querySelector('[data-conversation-scroll]');\n if (scrollport) {\n const flowTop = row.getBoundingClientRect().top - scrollport.getBoundingClientRect().top;\n const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n scrollport.scrollTo({\n top: scrollport.scrollTop + flowTop - JUMP_HEADROOM,\n behavior: reduced ? 'auto' : 'smooth',\n });\n }\n // A second jump within FLASH_MS: clear the previous row first.\n if (flashedRowRef.current && flashedRowRef.current !== row) {\n flashedRowRef.current.removeAttribute('data-dsh-outline-flash');\n }\n flashedRowRef.current = row;\n row.setAttribute('data-dsh-outline-flash', 'true');\n scheduleTimeout(timeoutsRef, () => {\n if (flashedRowRef.current === row) {\n row.removeAttribute('data-dsh-outline-flash');\n flashedRowRef.current = null;\n }\n }, FLASH_MS);\n }, []);\n /** Jump-to-message algorithm (spec §2.2). */\n const jumpTo = useCallback((key) => {\n // The conversation page must be mounted; abort silently otherwise.\n const scrollport = document.querySelector('[data-conversation-scroll]');\n if (!scrollport)\n return;\n // Ensure the Chat view is active: chat is order 0 — the FIRST tab of the\n // conversation-root tablist, when one exists (setView is idempotent).\n // A profile without extra views has no header tablist at all — that is\n // fine, the rows are already visible.\n const tablist = findConversationTablist(scrollport);\n if (tablist) {\n const firstTab = tablist.querySelector('button[role=\"tab\"]');\n if (firstTab)\n firstTab.click();\n }\n // Poll for the target row regardless of tablist presence — render timing\n // only; the row is guaranteed in the loaded window (it came from the\n // snapshot). Canceled on session change/unmount via clearPending.\n const deadline = Date.now() + JUMP_TIMEOUT_MS;\n const poll = () => {\n const row = findRow(key);\n if (row) {\n flashAndScroll(row);\n setOpen(false);\n return;\n }\n if (Date.now() < deadline) {\n scheduleRaf(rafsRef, poll);\n }\n else {\n console.warn(t('jumpFailed'), { key });\n }\n };\n scheduleRaf(rafsRef, poll);\n }, [findRow, flashAndScroll, t]);\n /** Copy a question's text (clipboard API with execCommand fallback). */\n const copyText = useCallback((text, key) => {\n const done = () => {\n setCopiedKey(key);\n scheduleTimeout(timeoutsRef, () => setCopiedKey((k) => (k === key ? null : k)), COPIED_MS);\n };\n const fallback = () => {\n try {\n const ta = document.createElement('textarea');\n ta.value = text;\n ta.setAttribute('readonly', '');\n ta.style.position = 'fixed';\n ta.style.opacity = '0';\n document.body.appendChild(ta);\n ta.select();\n document.execCommand('copy');\n ta.remove();\n done();\n }\n catch {\n // clipboard unavailable — ignore\n }\n };\n if (navigator.clipboard?.writeText) {\n navigator.clipboard.writeText(text).then(done, fallback);\n }\n else {\n fallback();\n }\n }, []);\n // No current session, or a blank session → render nothing (rail included).\n // MUST stay below every hook: this overlay entry stays mounted across\n // session changes, and blank flips false on the first accepted prompt, so a\n // conditional hook would crash React (reviewer MUST-FIX #1).\n if (!current || !session || blank)\n return null;\n // Rail bars: chronological top→bottom, capped at MAX_BARS with the overflow\n // folded into a \"+N\" marker (the rail is a minimap, not the full list).\n const start = Math.max(0, allItems.length - MAX_BARS);\n const bars = allItems.slice(start);\n const overflow = start;\n return (_jsxs(_Fragment, { children: [_jsxs(\"div\", { className: \"dso-rail\", role: \"group\", \"aria-label\": t('title'), tabIndex: 0, onMouseEnter: showPanel, onMouseLeave: scheduleCollapse, onFocus: showPanel, onBlur: scheduleCollapse, onClick: (event) => {\n // Tap (touch) on the strip itself pins/unpins the panel.\n if (event.target === event.currentTarget)\n setOpen((v) => !v);\n }, onKeyDown: (event) => {\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault();\n setOpen((v) => !v);\n }\n }, children: [overflow > 0 && (_jsxs(\"span\", { className: \"dso-rail-more\", \"aria-label\": t('moreBars', { count: overflow }), children: [\"+\", overflow] })), bars.map((item, index) => (_jsx(\"button\", { type: \"button\", className: \"dso-bar\", \"aria-label\": t('barLabel', { n: start + index + 1 }), onClick: () => jumpTo(item.key) }, item.key)))] }), open && (_jsxs(\"div\", { className: \"dso-panel\", role: \"region\", \"aria-label\": t('title'), onMouseEnter: showPanel, onMouseLeave: scheduleCollapse, onFocus: showPanel, onBlur: scheduleCollapse, children: [_jsxs(\"header\", { className: \"dso-panel-header\", children: [_jsx(\"span\", { className: \"dso-panel-title\", children: t('title') }), _jsx(\"span\", { className: \"dso-panel-count\", children: t('count', { count: allItems.length }) }), _jsx(\"button\", { type: \"button\", className: \"dso-panel-close\", \"aria-label\": t('close'), onClick: () => setOpen(false), children: \"\\u00D7\" })] }), _jsx(\"input\", { className: \"dso-search\", type: \"search\", value: query, placeholder: t('searchPlaceholder'), \"aria-label\": t('searchPlaceholder'), onChange: (event) => setQuery(event.target.value) }), _jsx(\"div\", { className: \"dso-list\", children: items.length === 0 ? (_jsx(\"div\", { className: \"dso-empty\", children: t('empty') })) : (items.map((item) => (_jsxs(\"div\", { className: \"dso-row\", children: [_jsxs(\"button\", { type: \"button\", className: \"dso-row-main\", onClick: () => jumpTo(item.key), children: [_jsx(\"span\", { className: \"dso-turn\", children: item.turn !== undefined ? `#${item.turn}` : '' }), _jsx(\"span\", { className: \"dso-text\", children: item.text }), item.kind === 'steering' && _jsx(\"span\", { className: \"dso-steer\", children: t('steerTag') }), _jsx(\"span\", { className: \"dso-time\", children: formatTime(item.time) })] }), _jsx(\"button\", { type: \"button\", className: copiedKey === item.key ? 'dso-copy dso-copied' : 'dso-copy', \"aria-label\": copiedKey === item.key ? t('copied') : t('copy'), onClick: () => copyText(item.text, item.key), children: copiedKey === item.key ? t('copied') : t('copy') })] }, item.key)))) }), hasMore && (_jsx(\"footer\", { className: \"dso-footer\", children: _jsx(\"button\", { type: \"button\", className: \"dso-load-older\", disabled: loadingOlder, onClick: () => {\n void session.loadOlder();\n }, children: loadingOlder ? t('loadingOlder') : t('loadOlder') }) }))] }))] }));\n}\n","export const NS = 'dsh-conversation-outline';\nexport const zh = {\n title: '会话大纲',\n count: '{count} 个问题',\n searchPlaceholder: '搜索问题…',\n empty: '当前会话还没有问题',\n loadOlder: '加载更早',\n loadingOlder: '加载中…',\n copy: '复制',\n copied: '已复制',\n steerTag: '追问',\n close: '关闭',\n jumpFailed: '跳转失败:未找到对应消息',\n barLabel: '跳到第 {n} 个问题',\n moreBars: '还有 {count} 个更早的问题',\n};\nexport const en = {\n title: 'Outline',\n count: '{count} questions',\n searchPlaceholder: 'Search questions…',\n empty: 'No questions in this conversation yet',\n loadOlder: 'Load older',\n loadingOlder: 'Loading…',\n copy: 'Copy',\n copied: 'Copied',\n steerTag: 'steer',\n close: 'Close',\n jumpFailed: 'Jump failed: target message not found',\n barLabel: 'Jump to question {n}',\n moreBars: '{count} more earlier questions',\n};\nexport const dictionaries = { zh, en };\n","/**\n * Outline rail + hover panel CSS (implementation-spec §1.8/§2.1, revised):\n * a thin right-edge rail (conversation minimap) that is always visible while\n * the session has questions, and a preview panel that expands on hover.\n * Plain CSS string injected via an HMR-safe\n * `<style data-plugin=\"dsh-conversation-outline\" data-plugin-css=\"...\">` tag.\n * Stable prefixed class names (`dso_*`), DSH theme vars (`--dsw-alias-*`) —\n * never hashed class names of other packages. No layout shift: the panel is a\n * pure overlay.\n */\nexport const panelCss = `\n/* ---- Right-edge rail (always-visible minimap) --------------------------- */\n.dso-rail {\n position: fixed;\n top: 50%;\n right: 0;\n transform: translateY(-50%);\n z-index: 2147483000;\n box-sizing: border-box;\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 3px;\n width: 30px;\n max-height: 55vh;\n padding: 8px 10px;\n overflow: hidden;\n cursor: pointer;\n}\n.dso-rail-more {\n flex: none;\n font-size: 9px;\n font-weight: 600;\n line-height: 10px;\n color: var(--dsw-alias-label-tertiary);\n font-variant-numeric: tabular-nums;\n}\n.dso-bar {\n flex: none;\n width: 10px;\n height: 6px;\n padding: 0;\n border: none;\n border-radius: 3px;\n background: color-mix(in srgb, var(--dsw-alias-label-tertiary) 45%, transparent);\n cursor: pointer;\n transition: background 0.12s, transform 0.12s;\n}\n.dso-bar:hover {\n background: var(--dsw-alias-state-business-primary);\n transform: scaleX(1.35);\n}\n.dso-rail:focus-visible,\n.dso-bar:focus-visible {\n outline: 2px solid var(--dsw-alias-state-business-primary);\n outline-offset: 2px;\n}\n\n/* ---- Hover-expanded preview panel --------------------------------------- */\n.dso-panel {\n position: fixed;\n top: 50%;\n right: 16px;\n transform: translateY(-50%);\n z-index: 2147483000;\n box-sizing: border-box;\n display: flex;\n flex-direction: column;\n width: min(340px, calc(100vw - 24px));\n max-height: min(70vh, 600px);\n overflow: hidden;\n border: 1px solid var(--dsw-alias-line-normal);\n border-radius: 16px;\n background: color-mix(in srgb, var(--dsw-alias-bg-module-platform) 92%, transparent);\n backdrop-filter: blur(20px);\n box-shadow: 0 16px 48px color-mix(in srgb, var(--dsw-alias-label-primary) 18%, transparent);\n color: var(--dsw-alias-label-primary);\n animation: dso-panel-in 140ms ease-out;\n}\n@keyframes dso-panel-in {\n from { opacity: 0; transform: translateY(-50%) translateX(12px); }\n to { opacity: 1; transform: translateY(-50%) translateX(0); }\n}\n\n.dso-panel-header {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 12px 14px 10px;\n border-bottom: 1px solid var(--dsw-alias-line-normal);\n}\n.dso-panel-title {\n font-size: 14px;\n font-weight: 600;\n color: var(--dsw-alias-label-primary);\n}\n.dso-panel-count {\n flex: 1;\n font-size: 12px;\n color: var(--dsw-alias-label-tertiary);\n}\n.dso-panel-close {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 24px;\n height: 24px;\n border: none;\n border-radius: 6px;\n background: transparent;\n color: var(--dsw-alias-label-secondary);\n font-size: 16px;\n line-height: 1;\n cursor: pointer;\n transition: background 0.12s;\n}\n.dso-panel-close:hover { background: var(--dsw-alias-interactive-bg-hover-solid); }\n\n.dso-panel-close:focus-visible,\n.dso-search:focus-visible,\n.dso-row-main:focus-visible,\n.dso-copy:focus-visible,\n.dso-load-older:focus-visible {\n outline: 2px solid var(--dsw-alias-state-business-primary);\n outline-offset: 2px;\n}\n\n.dso-search {\n margin: 10px 12px 4px;\n box-sizing: border-box;\n height: 30px;\n padding: 0 10px;\n border: 1px solid var(--dsw-alias-line-normal);\n border-radius: 8px;\n background: var(--dsw-alias-interactive-bg-hover-solid);\n color: var(--dsw-alias-label-primary);\n font: inherit;\n font-size: 12px;\n outline: none;\n}\n.dso-search::placeholder { color: var(--dsw-alias-label-tertiary); }\n\n/* ---- Question list ------------------------------------------------------ */\n.dso-list {\n flex: 1;\n min-height: 0;\n overflow-y: auto;\n display: flex;\n flex-direction: column;\n gap: 2px;\n padding: 6px;\n}\n/* Row container: two sibling buttons (jump + copy) — no nested interactive\n controls. Hover highlights the whole row. */\n.dso-row {\n display: flex;\n align-items: center;\n gap: 4px;\n padding: 2px;\n border-radius: 10px;\n transition: background 0.12s;\n}\n.dso-row:hover { background: var(--dsw-alias-interactive-bg-hover-solid); }\n.dso-row-main {\n flex: 1;\n min-width: 0;\n display: flex;\n align-items: center;\n gap: 6px;\n height: 28px;\n padding: 2px 6px;\n border: none;\n border-radius: 8px;\n background: transparent;\n color: inherit;\n font: inherit;\n text-align: left;\n cursor: pointer;\n transition: background 0.12s;\n}\n.dso-row-main:hover { background: var(--dsw-alias-interactive-bg-hover-solid); }\n\n.dso-turn {\n flex: none;\n min-width: 26px;\n height: 16px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border-radius: 8px;\n background: var(--dsw-alias-bg-fill-business);\n color: var(--dsw-alias-label-on-fill);\n font-size: 10px;\n font-weight: 600;\n font-variant-numeric: tabular-nums;\n}\n/* Single-line truncation: shows the opening words of each question —\n anything longer simply ellipsizes (per user request). */\n.dso-text {\n flex: 1;\n min-width: 0;\n font-size: 13px;\n line-height: 18px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.dso-time {\n flex: none;\n font-size: 11px;\n font-variant-numeric: tabular-nums;\n color: var(--dsw-alias-label-tertiary);\n}\n.dso-steer {\n flex: none;\n padding: 0 6px;\n border: 1px solid var(--dsw-alias-line-normal);\n border-radius: 999px;\n font-size: 10px;\n color: var(--dsw-alias-label-secondary);\n}\n.dso-copy {\n flex: none;\n padding: 2px 6px;\n border: none;\n border-radius: 6px;\n background: transparent;\n color: var(--dsw-alias-label-secondary);\n font: inherit;\n font-size: 11px;\n cursor: pointer;\n opacity: 0;\n transition: opacity 0.12s, color 0.12s, background 0.12s;\n}\n.dso-row:hover .dso-copy,\n.dso-row:focus-within .dso-copy,\n.dso-copy:focus-visible { opacity: 1; }\n.dso-copy:hover { background: var(--dsw-alias-bg-fill-neutral); }\n.dso-copy.dso-copied { color: var(--dsw-alias-state-success); }\n\n.dso-empty {\n padding: 28px 16px;\n text-align: center;\n font-size: 13px;\n color: var(--dsw-alias-label-tertiary);\n}\n\n/* ---- Footer / load older ------------------------------------------------ */\n.dso-footer {\n display: flex;\n justify-content: center;\n padding: 8px 12px 10px;\n border-top: 1px solid var(--dsw-alias-line-normal);\n}\n.dso-load-older {\n padding: 4px 14px;\n border: none;\n border-radius: 14px;\n background: var(--dsw-alias-interactive-bg-hover-solid);\n color: var(--dsw-alias-label-secondary);\n font: inherit;\n font-size: 12px;\n cursor: pointer;\n}\n.dso-load-older:disabled { cursor: default; opacity: 0.6; }\n\n/* ---- Jump flash highlight ----------------------------------------------- */\n[data-dsh-outline-flash] {\n animation: dso-flash 1.8s ease-out;\n}\n@keyframes dso-flash {\n 0% {\n box-shadow: 0 0 0 0 color-mix(in srgb, var(--dsw-alias-state-business-primary) 50%, transparent);\n background-color: color-mix(in srgb, var(--dsw-alias-state-business-primary) 24%, transparent);\n }\n 70% {\n box-shadow: 0 0 0 6px transparent;\n background-color: color-mix(in srgb, var(--dsw-alias-state-business-primary) 8%, transparent);\n }\n 100% {\n box-shadow: 0 0 0 0 transparent;\n background-color: transparent;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .dso-panel { animation: none; }\n .dso-bar { transition: none; }\n [data-dsh-outline-flash] { animation: none; }\n}\n`;\n/** Inject the style tag, owned by the client fiber (removed on dispose/HMR). */\nexport function injectStyle(ctx) {\n ctx.effect(() => {\n const tag = document.createElement('style');\n tag.dataset.plugin = 'dsh-conversation-outline';\n tag.dataset.pluginCss = 'dsh-conversation-outline/panel.css';\n tag.textContent = panelCss;\n document.head.appendChild(tag);\n return () => {\n tag.remove();\n };\n }, 'dsh-conversation-outline: panel css');\n}\n","import { OutlinePanel } from \"./OutlinePanel.js\";\nimport { NS, dictionaries } from \"./locales.js\";\nimport { injectStyle } from \"./styles.js\";\n/**\n * Client entry (implementation-spec §1.1/§2.4): the browser half of the\n * plugin. Cordis services this fiber waits for before apply() runs.\n */\nexport const inject = ['slots', 'sessions', 'locale'];\nexport function apply(ctx) {\n // Locale dictionaries — the typed register form (both shipped locales\n // required, keys compile-checked). The disposer is owned by the fiber, so\n // HMR removes the namespace on unload.\n ctx.effect(() => ctx.locale.register(NS, dictionaries), 'dsh-conversation-outline: locale');\n // Panel CSS as an HMR-safe <style data-plugin> tag (effect-owned).\n injectStyle(ctx);\n // Right-edge rail + hover panel in the frame-wide overlay layer.\n // slots.inject waits for the layout's declaration of `shell.overlay` and\n // routes the registration (and its unload cascade) through this fiber.\n // `locale: NS` gives the component the typed `t` seat; the inject factory\n // passes the sessions service face.\n ctx.slots.inject('shell.overlay', () => ctx.slots.register({\n name: 'shell.overlay',\n id: 'dsh-conversation-outline.rail',\n locale: NS,\n inject: () => ({ sessions: ctx.sessions }),\n }, OutlinePanel));\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"client.js","names":["_Fragment"],"sources":["client/outline.js","client/OutlinePanel.js","client/locales.js","client/styles.js","client/index.js"],"sourcesContent":["/**\n * Pure logic for the conversation outline (implementation-spec §2.3).\n *\n * Deliberately has NO DOM access and NO React imports so scripts/verify.mjs can\n * import it directly in Node from the tsc output (lib/client/outline.js) and\n * assert the derivations offline.\n *\n * Input shapes are structural \"lite\" views of the runtime snapshot types\n * (@deepseek-ai/dsh-client-runtime/client) — the real objects are assignable\n * to them, and Node can import this module without the browser type graph.\n */\nconst IMAGE_PLACEHOLDER = '[image]';\n/**\n * Flatten message content blocks into a single display string: text blocks are\n * joined, image blocks become a placeholder, and whitespace is trimmed and\n * collapsed.\n */\nexport function flattenQuestionText(content) {\n return content\n .map((block) => {\n if (block.type === 'text')\n return block.text ?? '';\n if (block.type === 'image')\n return IMAGE_PLACEHOLDER;\n return '';\n })\n .join(' ')\n .trim()\n .replace(/\\s+/g, ' ');\n}\n/** Turn number of a node location: step/turn → location.turn.turn, else undefined. */\nfunction turnOf(location) {\n if (!location)\n return undefined;\n if (location.kind === 'step' || location.kind === 'turn')\n return location.turn?.turn;\n return undefined;\n}\n/**\n * Walk the chat flow in order, keep user/steering nodes, skip empty text, and\n * produce chronological outline items (spec §2.3).\n */\nexport function collectQuestions(snapshot) {\n const items = [];\n for (const key of snapshot.chat.order) {\n const node = snapshot.chat.nodes.get(key);\n if (!node)\n continue;\n if (node.kind !== 'user' && node.kind !== 'steering')\n continue;\n const data = node.data;\n const text = flattenQuestionText(data?.content ?? []);\n if (!text)\n continue;\n items.push({\n key: node.key,\n kind: node.kind,\n seq: data?.seq ?? 0,\n time: data?.time ?? 0,\n turn: turnOf(node.location),\n text,\n });\n }\n return items;\n}\n/** Case-insensitive substring filter over the flattened question text. */\nexport function filterQuestions(items, query) {\n const q = query.trim().toLowerCase();\n if (!q)\n return items;\n return items.filter((item) => item.text.toLowerCase().includes(q));\n}\n/** Local `HH:MM` from a unix-ms timestamp (spec §2.3). */\nexport function formatTime(ms) {\n const d = new Date(ms);\n const hh = String(d.getHours()).padStart(2, '0');\n const mm = String(d.getMinutes()).padStart(2, '0');\n return `${hh}:${mm}`;\n}\n/** Pure DOM predicate for the jump loop: is this row the target node? */\nexport function isJumpTargetRow(row, key) {\n return row.getAttribute('data-chat-anchor-key') === key;\n}\n","import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from \"react/jsx-runtime\";\nimport { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore, } from 'react';\nimport { collectQuestions, filterQuestions, formatTime, isJumpTargetRow, } from \"./outline.js\";\n/** Stable getSnapshot value while no session is current (uSES contract). */\nconst NO_SESSION = null;\nconst JUMP_HEADROOM = 96;\nconst JUMP_TIMEOUT_MS = 1500;\nconst FLASH_MS = 1900;\nconst COPIED_MS = 1500;\n/** Grace period before the hover panel collapses (lets the pointer travel). */\nconst COLLAPSE_DELAY_MS = 240;\n/** Rail capacity; older questions fold into the \"+N\" marker. */\nconst MAX_BARS = 60;\n/**\n * The conversation view's header tablist, scoped to the conversation root:\n * walk up from the scrollport's PARENT (the scrollport itself may contain\n * other tablists, e.g. the trajectory event-details tabs) and return the\n * nearest ancestor containing a `[role=\"tablist\"]`. Never falls back to a\n * document-wide query, so other tablists (trajectory event details,\n * settings, cordis source viewer) are not hit.\n */\nfunction findConversationTablist(scrollport) {\n let node = scrollport.parentElement;\n while (node) {\n const tablist = node.querySelector('[role=\"tablist\"]');\n if (tablist)\n return tablist;\n node = node.parentElement;\n }\n return null;\n}\n/** RAF that prunes itself from the tracking ref once it fires. */\nfunction scheduleRaf(rafRef, fn) {\n const id = requestAnimationFrame(() => {\n rafRef.current = rafRef.current.filter((x) => x !== id);\n fn();\n });\n rafRef.current.push(id);\n}\n/** Timeout that prunes itself from the tracking ref once it fires. */\nfunction scheduleTimeout(timeoutRef, fn, ms) {\n const id = window.setTimeout(() => {\n timeoutRef.current = timeoutRef.current.filter((x) => x !== id);\n fn();\n }, ms);\n timeoutRef.current.push(id);\n}\nexport function OutlinePanel({ sessions, useSessions, t }) {\n const current = useSessions((s) => s.current);\n const session = current ? sessions.binding(current)?.session : undefined;\n // Conversation snapshot subscription (spec §1.5): live updates while the\n // session runs, load-older, blank detection — all ride this one store.\n const subscribe = useCallback((onChange) => (session ? session.subscribe(onChange) : () => { }), [session]);\n const getSnapshot = useCallback(() => (session ? session.getSnapshot() : NO_SESSION), [session]);\n const snapshot = useSyncExternalStore(subscribe, getSnapshot);\n // Two independent sources for the panel being open: `pinned` is a click\n // gesture (sticky until Escape / outside click / session change), `hovered`\n // is pointer presence over the rail or the panel. Deriving `open` from both\n // means a click on the rail opens the panel even when no hover event ever\n // fires (touch, synthetic/automation input, or a pointer that entered the\n // strip without crossing its boundary), and a hover preview still collapses\n // on its own once the pointer leaves.\n const [pinned, setPinned] = useState(false);\n const [hovered, setHovered] = useState(false);\n const open = pinned || hovered;\n const [query, setQuery] = useState('');\n const [copiedKey, setCopiedKey] = useState(null);\n // The question whose jump could not be completed; the panel says so instead\n // of failing silently (the row may sit outside the loaded history window).\n const [jumpFailed, setJumpFailed] = useState(null);\n const timeoutsRef = useRef([]);\n const rafsRef = useRef([]);\n const flashedRowRef = useRef(null);\n const collapseTimerRef = useRef(null);\n const railRef = useRef(null);\n const panelRef = useRef(null);\n // Cancel pending jump work (RAFs/timeouts), the flash highlight, and a\n // scheduled hover collapse.\n const clearPending = useCallback(() => {\n for (const id of rafsRef.current)\n window.cancelAnimationFrame(id);\n for (const id of timeoutsRef.current)\n window.clearTimeout(id);\n rafsRef.current = [];\n timeoutsRef.current = [];\n if (collapseTimerRef.current !== null) {\n window.clearTimeout(collapseTimerRef.current);\n collapseTimerRef.current = null;\n }\n if (flashedRowRef.current) {\n flashedRowRef.current.removeAttribute('data-dsh-outline-flash');\n flashedRowRef.current = null;\n }\n }, []);\n /** Close the panel from any trigger: drop the pin and the hover preview. */\n const closePanel = useCallback(() => {\n setPinned(false);\n setHovered(false);\n cancelCollapseRef.current();\n }, []);\n // Session change → cancel pending work and collapse (navigate closes the\n // panel, spec §2.1; a mid-jump poll must not target the stale session).\n useEffect(() => {\n clearPending();\n setPinned(false);\n setHovered(false);\n setJumpFailed(null);\n }, [current, clearPending]);\n // Escape closes the panel (hover-opened or click-pinned).\n useEffect(() => {\n if (!open)\n return;\n const onKey = (event) => {\n if (event.key === 'Escape')\n closePanel();\n };\n window.addEventListener('keydown', onKey);\n return () => window.removeEventListener('keydown', onKey);\n }, [open, closePanel]);\n // A click anywhere outside the rail and the panel releases a pin, so the\n // panel behaves like every other dismissible overlay on the page.\n useEffect(() => {\n if (!pinned)\n return;\n const onPointerDown = (event) => {\n const target = event.target;\n if (!(target instanceof Node))\n return;\n if (railRef.current?.contains(target) || panelRef.current?.contains(target))\n return;\n closePanel();\n };\n window.addEventListener('mousedown', onPointerDown);\n return () => window.removeEventListener('mousedown', onPointerDown);\n }, [pinned, closePanel]);\n // Unmount cleanup: cancel pending flash/copied timeouts, jump RAFs, collapse\n // timer, and the flash attribute.\n useEffect(() => clearPending, [clearPending]);\n const allItems = useMemo(() => (snapshot ? collectQuestions(snapshot) : []), [snapshot]);\n const items = useMemo(() => filterQuestions(allItems, query), [allItems, query]);\n const blank = snapshot?.blank ?? true;\n const hasMore = snapshot?.hasMore ?? false;\n const loadingOlder = snapshot?.loadingOlder ?? false;\n // ---- open mechanics: click pins, hover previews, grace period between ---\n const cancelCollapse = useCallback(() => {\n if (collapseTimerRef.current !== null) {\n window.clearTimeout(collapseTimerRef.current);\n collapseTimerRef.current = null;\n }\n }, []);\n // `closePanel` above runs before this declaration; a ref keeps the two\n // callbacks independent of declaration order.\n const cancelCollapseRef = useRef(cancelCollapse);\n cancelCollapseRef.current = cancelCollapse;\n const scheduleCollapse = useCallback(() => {\n cancelCollapse();\n collapseTimerRef.current = window.setTimeout(() => {\n collapseTimerRef.current = null;\n // Only a preview collapses on its own: a pinned panel stays until it is\n // closed, and a pointer still inside the rail or the panel (Chromium\n // raises no further enter events while the pointer sits still) keeps the\n // preview up.\n if (railRef.current?.matches(':hover') || panelRef.current?.matches(':hover'))\n return;\n setHovered(false);\n }, COLLAPSE_DELAY_MS);\n }, [cancelCollapse]);\n /** Pointer entered the rail or the panel: cancel a pending collapse, preview. */\n const onEnter = useCallback(() => {\n cancelCollapse();\n setHovered(true);\n }, [cancelCollapse]);\n /** Pointer left: let the grace period decide, unless the panel is pinned. */\n const onLeave = useCallback(() => {\n if (pinned)\n return;\n scheduleCollapse();\n }, [pinned, scheduleCollapse]);\n /** A click on the rail (or any bar) opens the panel and keeps it open. */\n const togglePin = useCallback(() => {\n cancelCollapse();\n setPinned((value) => !value);\n setHovered(true);\n }, [cancelCollapse]);\n /** The panel's own controls: clicking inside never closes it. */\n const keepOpen = useCallback(() => {\n cancelCollapse();\n setHovered(true);\n }, [cancelCollapse]);\n /** Grid of candidate keys for one node: the anchor the row renders first. */\n function jumpKeyCandidates(key) {\n return key.includes('|') ? [key.split('|')[0], key] : [key];\n }\n /** Find the chat row for a node key (pure predicate over DOM rows). */\n const findRow = useCallback((key) => {\n const rows = document.querySelectorAll('[data-chat-anchor-key]');\n for (const candidate of jumpKeyCandidates(key)) {\n for (const row of rows) {\n if (isJumpTargetRow(row, candidate))\n return row;\n }\n }\n return null;\n }, []);\n /** Scroll the row into view (96px headroom) and flash a highlight. */\n const flashAndScroll = useCallback((row) => {\n const scrollport = row.closest('[data-conversation-scroll]') ?? document.querySelector('[data-conversation-scroll]');\n if (scrollport) {\n const flowTop = row.getBoundingClientRect().top - scrollport.getBoundingClientRect().top;\n const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n scrollport.scrollTo({\n top: scrollport.scrollTop + flowTop - JUMP_HEADROOM,\n behavior: reduced ? 'auto' : 'smooth',\n });\n }\n // A second jump within FLASH_MS: clear the previous row first.\n if (flashedRowRef.current && flashedRowRef.current !== row) {\n flashedRowRef.current.removeAttribute('data-dsh-outline-flash');\n }\n flashedRowRef.current = row;\n row.setAttribute('data-dsh-outline-flash', 'true');\n scheduleTimeout(timeoutsRef, () => {\n if (flashedRowRef.current === row) {\n row.removeAttribute('data-dsh-outline-flash');\n flashedRowRef.current = null;\n }\n }, FLASH_MS);\n }, []);\n /** Jump-to-message algorithm (spec §2.2). */\n const jumpTo = useCallback((key, label) => {\n // The conversation page must be mounted; abort silently otherwise.\n const scrollport = document.querySelector('[data-conversation-scroll]');\n if (!scrollport)\n return;\n setJumpFailed(null);\n // Ensure the Chat view is active: chat is order 0 — the FIRST tab of the\n // conversation-root tablist, when one exists (setView is idempotent).\n // A profile without extra views has no header tablist at all — that is\n // fine, the rows are already visible.\n const tablist = findConversationTablist(scrollport);\n if (tablist) {\n const firstTab = tablist.querySelector('button[role=\"tab\"]');\n if (firstTab)\n firstTab.click();\n }\n // Poll for the target row regardless of tablist presence — render timing\n // only; the row is guaranteed in the loaded window (it came from the\n // snapshot). Canceled on session change/unmount via clearPending.\n const deadline = Date.now() + JUMP_TIMEOUT_MS;\n const poll = () => {\n const row = findRow(key);\n if (row) {\n flashAndScroll(row);\n closePanel();\n return;\n }\n if (Date.now() < deadline) {\n scheduleRaf(rafsRef, poll);\n }\n else {\n // Never fail silently: the panel states what happened, and the\n // console keeps the key for bug reports.\n console.warn(t('jumpFailed'), { key });\n setJumpFailed(label ?? key);\n }\n };\n scheduleRaf(rafsRef, poll);\n }, [findRow, flashAndScroll, t, closePanel]);\n /** Copy a question's text (clipboard API with execCommand fallback). */\n const copyText = useCallback((text, key) => {\n const done = () => {\n setCopiedKey(key);\n scheduleTimeout(timeoutsRef, () => setCopiedKey((k) => (k === key ? null : k)), COPIED_MS);\n };\n const fallback = () => {\n try {\n const ta = document.createElement('textarea');\n ta.value = text;\n ta.setAttribute('readonly', '');\n ta.style.position = 'fixed';\n ta.style.opacity = '0';\n document.body.appendChild(ta);\n ta.select();\n document.execCommand('copy');\n ta.remove();\n done();\n }\n catch {\n // clipboard unavailable — ignore\n }\n };\n if (navigator.clipboard?.writeText) {\n navigator.clipboard.writeText(text).then(done, fallback);\n }\n else {\n fallback();\n }\n }, []);\n // No current session, or a blank session → render nothing (rail included).\n // MUST stay below every hook: this overlay entry stays mounted across\n // session changes, and blank flips false on the first accepted prompt, so a\n // conditional hook would crash React (reviewer MUST-FIX #1).\n if (!current || !session || blank)\n return null;\n // Rail bars: chronological top→bottom, capped at MAX_BARS with the overflow\n // folded into a \"+N\" marker (the rail is a minimap, not the full list).\n const start = Math.max(0, allItems.length - MAX_BARS);\n const bars = allItems.slice(start);\n const overflow = start;\n return (_jsxs(_Fragment, { children: [_jsxs(\"div\", { ref: railRef, className: \"dso-rail\", role: \"group\", \"aria-label\": t('title'), tabIndex: 0, onMouseEnter: onEnter, onMouseLeave: onLeave, onFocus: onEnter, onBlur: onLeave, onClick: (event) => {\n // Any click on the strip pins the panel open — the container itself,\n // its padding, or the \"+N\" marker. Bars stop propagation and reach\n // the same pin through the shared jump gesture below.\n if (event.target === event.currentTarget || event.target instanceof HTMLSpanElement) {\n togglePin();\n }\n }, onKeyDown: (event) => {\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault();\n togglePin();\n }\n }, \"data-dso-open\": open ? 'true' : undefined, children: [overflow > 0 && (_jsxs(\"span\", { className: \"dso-rail-more\", \"aria-label\": t('moreBars', { count: overflow }), children: [\"+\", overflow] })), bars.map((item, index) => (_jsx(\"button\", { type: \"button\", className: \"dso-bar\", \"aria-label\": t('barLabel', { n: start + index + 1 }), onClick: (event) => {\n event.stopPropagation();\n // The bar's own job is the jump; the panel comes up pinned first\n // so a click always has a visible effect, then jumpTo collapses\n // it once the row is on screen.\n setPinned(true);\n setHovered(true);\n jumpTo(item.key, item.text);\n } }, item.key)))] }), open && (_jsxs(\"div\", { ref: panelRef, className: \"dso-panel\", role: \"region\", \"aria-label\": t('title'), \"data-dso-pinned\": pinned ? 'true' : undefined, onMouseEnter: onEnter, onMouseLeave: onLeave, onFocus: onEnter, onBlur: onLeave, onMouseDown: keepOpen, children: [_jsxs(\"header\", { className: \"dso-panel-header\", children: [_jsx(\"span\", { className: \"dso-panel-title\", children: t('title') }), _jsx(\"span\", { className: \"dso-panel-count\", children: t('count', { count: allItems.length }) }), _jsx(\"button\", { type: \"button\", className: \"dso-panel-close\", \"aria-label\": t('close'), onClick: closePanel, children: \"\\u00D7\" })] }), _jsx(\"input\", { className: \"dso-search\", type: \"search\", value: query, placeholder: t('searchPlaceholder'), \"aria-label\": t('searchPlaceholder'), onChange: (event) => setQuery(event.target.value) }), _jsx(\"div\", { className: \"dso-list\", children: items.length === 0 ? (_jsx(\"div\", { className: \"dso-empty\", children: t('empty') })) : (items.map((item) => (_jsxs(\"div\", { className: \"dso-row\", children: [_jsxs(\"button\", { type: \"button\", className: \"dso-row-main\", onClick: () => jumpTo(item.key, item.text), children: [_jsx(\"span\", { className: \"dso-turn\", children: item.turn !== undefined ? `#${item.turn}` : '' }), _jsx(\"span\", { className: \"dso-text\", children: item.text }), item.kind === 'steering' && _jsx(\"span\", { className: \"dso-steer\", children: t('steerTag') }), _jsx(\"span\", { className: \"dso-time\", children: formatTime(item.time) })] }), _jsx(\"button\", { type: \"button\", className: copiedKey === item.key ? 'dso-copy dso-copied' : 'dso-copy', \"aria-label\": copiedKey === item.key ? t('copied') : t('copy'), onClick: () => copyText(item.text, item.key), children: copiedKey === item.key ? t('copied') : t('copy') })] }, item.key)))) }), jumpFailed !== null && (_jsx(\"p\", { className: \"dso-jump-failed\", role: \"status\", \"data-dso-jump-failed\": \"true\", children: t('jumpFailed') })), hasMore && (_jsx(\"footer\", { className: \"dso-footer\", children: _jsx(\"button\", { type: \"button\", className: \"dso-load-older\", disabled: loadingOlder, onClick: () => {\n void session.loadOlder();\n }, children: loadingOlder ? t('loadingOlder') : t('loadOlder') }) }))] }))] }));\n}\n","export const NS = 'dsh-conversation-outline';\nexport const zh = {\n title: '会话大纲',\n count: '{count} 个问题',\n searchPlaceholder: '搜索问题…',\n empty: '当前会话还没有问题',\n loadOlder: '加载更早',\n loadingOlder: '加载中…',\n copy: '复制',\n copied: '已复制',\n steerTag: '追问',\n close: '关闭',\n jumpFailed: '跳转失败:未找到对应消息',\n barLabel: '跳到第 {n} 个问题',\n moreBars: '还有 {count} 个更早的问题',\n};\nexport const en = {\n title: 'Outline',\n count: '{count} questions',\n searchPlaceholder: 'Search questions…',\n empty: 'No questions in this conversation yet',\n loadOlder: 'Load older',\n loadingOlder: 'Loading…',\n copy: 'Copy',\n copied: 'Copied',\n steerTag: 'steer',\n close: 'Close',\n jumpFailed: 'Jump failed: target message not found',\n barLabel: 'Jump to question {n}',\n moreBars: '{count} more earlier questions',\n};\nexport const dictionaries = { zh, en };\n","/**\n * Outline rail + hover panel CSS (implementation-spec §1.8/§2.1, revised):\n * a thin right-edge rail (conversation minimap) that is always visible while\n * the session has questions, and a preview panel that expands on hover.\n * Plain CSS string injected via an HMR-safe\n * `<style data-plugin=\"dsh-conversation-outline\" data-plugin-css=\"...\">` tag.\n * Stable prefixed class names (`dso_*`), DSH theme vars (`--dsw-alias-*`) —\n * never hashed class names of other packages. No layout shift: the panel is a\n * pure overlay.\n */\nexport const panelCss = `\n/* ---- Right-edge rail (always-visible minimap) --------------------------- */\n.dso-rail {\n position: fixed;\n top: 50%;\n right: 0;\n transform: translateY(-50%);\n z-index: 2147483000;\n box-sizing: border-box;\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 3px;\n width: 30px;\n max-height: 55vh;\n padding: 8px 10px;\n overflow: hidden;\n cursor: pointer;\n}\n.dso-rail-more {\n flex: none;\n font-size: 9px;\n font-weight: 600;\n line-height: 10px;\n color: var(--dsw-alias-label-tertiary);\n font-variant-numeric: tabular-nums;\n}\n.dso-bar {\n flex: none;\n width: 10px;\n height: 6px;\n padding: 0;\n border: none;\n border-radius: 3px;\n background: color-mix(in srgb, var(--dsw-alias-label-tertiary) 45%, transparent);\n cursor: pointer;\n transition: background 0.12s, transform 0.12s;\n}\n.dso-bar:hover {\n background: var(--dsw-alias-state-business-primary);\n transform: scaleX(1.35);\n}\n.dso-rail:focus-visible,\n.dso-bar:focus-visible {\n outline: 2px solid var(--dsw-alias-state-business-primary);\n outline-offset: 2px;\n}\n/* Clickability affordance: a hovered or pinned strip lights its bars up, so\n it reads as a control rather than as decoration. */\n.dso-rail:hover .dso-bar,\n.dso-rail[data-dso-open] .dso-bar {\n background: color-mix(in srgb, var(--dsw-alias-label-tertiary) 72%, transparent);\n}\n.dso-rail:hover .dso-bar:hover {\n background: var(--dsw-alias-state-business-primary);\n}\n\n/* ---- Hover-expanded preview panel --------------------------------------- */\n.dso-panel {\n position: fixed;\n top: 50%;\n right: 16px;\n transform: translateY(-50%);\n z-index: 2147483000;\n box-sizing: border-box;\n display: flex;\n flex-direction: column;\n width: min(340px, calc(100vw - 24px));\n max-height: min(70vh, 600px);\n overflow: hidden;\n border: 1px solid var(--dsw-alias-line-normal);\n border-radius: 16px;\n background: color-mix(in srgb, var(--dsw-alias-bg-module-platform) 92%, transparent);\n backdrop-filter: blur(20px);\n box-shadow: 0 16px 48px color-mix(in srgb, var(--dsw-alias-label-primary) 18%, transparent);\n color: var(--dsw-alias-label-primary);\n animation: dso-panel-in 140ms ease-out;\n}\n/* A pinned panel (clicked open) keeps a defined edge, so it is obvious that it\n no longer collapses when the pointer leaves. */\n.dso-panel[data-dso-pinned] {\n border-color: var(--dsw-alias-state-business-primary);\n}\n@keyframes dso-panel-in {\n from { opacity: 0; transform: translateY(-50%) translateX(12px); }\n to { opacity: 1; transform: translateY(-50%) translateX(0); }\n}\n\n.dso-panel-header {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 12px 14px 10px;\n border-bottom: 1px solid var(--dsw-alias-line-normal);\n}\n.dso-panel-title {\n font-size: 14px;\n font-weight: 600;\n color: var(--dsw-alias-label-primary);\n}\n.dso-panel-count {\n flex: 1;\n font-size: 12px;\n color: var(--dsw-alias-label-tertiary);\n}\n.dso-panel-close {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 24px;\n height: 24px;\n border: none;\n border-radius: 6px;\n background: transparent;\n color: var(--dsw-alias-label-secondary);\n font-size: 16px;\n line-height: 1;\n cursor: pointer;\n transition: background 0.12s;\n}\n.dso-panel-close:hover { background: var(--dsw-alias-interactive-bg-hover-solid); }\n\n.dso-panel-close:focus-visible,\n.dso-search:focus-visible,\n.dso-row-main:focus-visible,\n.dso-copy:focus-visible,\n.dso-load-older:focus-visible {\n outline: 2px solid var(--dsw-alias-state-business-primary);\n outline-offset: 2px;\n}\n\n.dso-search {\n margin: 10px 12px 4px;\n box-sizing: border-box;\n height: 30px;\n padding: 0 10px;\n border: 1px solid var(--dsw-alias-line-normal);\n border-radius: 8px;\n background: var(--dsw-alias-interactive-bg-hover-solid);\n color: var(--dsw-alias-label-primary);\n font: inherit;\n font-size: 12px;\n outline: none;\n}\n.dso-search::placeholder { color: var(--dsw-alias-label-tertiary); }\n\n/* ---- Question list ------------------------------------------------------ */\n.dso-list {\n flex: 1;\n min-height: 0;\n overflow-y: auto;\n display: flex;\n flex-direction: column;\n gap: 2px;\n padding: 6px;\n}\n/* Row container: two sibling buttons (jump + copy) — no nested interactive\n controls. Hover highlights the whole row. */\n.dso-row {\n display: flex;\n align-items: center;\n gap: 4px;\n padding: 2px;\n border-radius: 10px;\n transition: background 0.12s;\n}\n.dso-row:hover { background: var(--dsw-alias-interactive-bg-hover-solid); }\n.dso-row-main {\n flex: 1;\n min-width: 0;\n display: flex;\n align-items: center;\n gap: 6px;\n height: 28px;\n padding: 2px 6px;\n border: none;\n border-radius: 8px;\n background: transparent;\n color: inherit;\n font: inherit;\n text-align: left;\n cursor: pointer;\n transition: background 0.12s;\n}\n.dso-row-main:hover { background: var(--dsw-alias-interactive-bg-hover-solid); }\n\n.dso-turn {\n flex: none;\n min-width: 26px;\n height: 16px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border-radius: 8px;\n background: var(--dsw-alias-bg-fill-business);\n color: var(--dsw-alias-label-on-fill);\n font-size: 10px;\n font-weight: 600;\n font-variant-numeric: tabular-nums;\n}\n/* Single-line truncation: shows the opening words of each question —\n anything longer simply ellipsizes (per user request). */\n.dso-text {\n flex: 1;\n min-width: 0;\n font-size: 13px;\n line-height: 18px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.dso-time {\n flex: none;\n font-size: 11px;\n font-variant-numeric: tabular-nums;\n color: var(--dsw-alias-label-tertiary);\n}\n.dso-steer {\n flex: none;\n padding: 0 6px;\n border: 1px solid var(--dsw-alias-line-normal);\n border-radius: 999px;\n font-size: 10px;\n color: var(--dsw-alias-label-secondary);\n}\n.dso-copy {\n flex: none;\n padding: 2px 6px;\n border: none;\n border-radius: 6px;\n background: transparent;\n color: var(--dsw-alias-label-secondary);\n font: inherit;\n font-size: 11px;\n cursor: pointer;\n opacity: 0;\n transition: opacity 0.12s, color 0.12s, background 0.12s;\n}\n.dso-row:hover .dso-copy,\n.dso-row:focus-within .dso-copy,\n.dso-copy:focus-visible { opacity: 1; }\n.dso-copy:hover { background: var(--dsw-alias-bg-fill-neutral); }\n.dso-copy.dso-copied { color: var(--dsw-alias-state-success); }\n\n.dso-empty {\n padding: 28px 16px;\n text-align: center;\n font-size: 13px;\n color: var(--dsw-alias-label-tertiary);\n}\n\n/* ---- Jump failure notice ------------------------------------------------ */\n.dso-jump-failed {\n margin: 0;\n padding: 8px 14px;\n border-top: 1px solid var(--dsw-alias-line-normal);\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-state-warning, var(--dsw-alias-label-secondary));\n}\n\n/* ---- Footer / load older ------------------------------------------------ */\n.dso-footer {\n display: flex;\n justify-content: center;\n padding: 8px 12px 10px;\n border-top: 1px solid var(--dsw-alias-line-normal);\n}\n.dso-load-older {\n padding: 4px 14px;\n border: none;\n border-radius: 14px;\n background: var(--dsw-alias-interactive-bg-hover-solid);\n color: var(--dsw-alias-label-secondary);\n font: inherit;\n font-size: 12px;\n cursor: pointer;\n}\n.dso-load-older:disabled { cursor: default; opacity: 0.6; }\n\n/* ---- Jump flash highlight ----------------------------------------------- */\n[data-dsh-outline-flash] {\n animation: dso-flash 1.8s ease-out;\n}\n@keyframes dso-flash {\n 0% {\n box-shadow: 0 0 0 0 color-mix(in srgb, var(--dsw-alias-state-business-primary) 50%, transparent);\n background-color: color-mix(in srgb, var(--dsw-alias-state-business-primary) 24%, transparent);\n }\n 70% {\n box-shadow: 0 0 0 6px transparent;\n background-color: color-mix(in srgb, var(--dsw-alias-state-business-primary) 8%, transparent);\n }\n 100% {\n box-shadow: 0 0 0 0 transparent;\n background-color: transparent;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .dso-panel { animation: none; }\n .dso-bar { transition: none; }\n [data-dsh-outline-flash] { animation: none; }\n}\n`;\n/** Inject the style tag, owned by the client fiber (removed on dispose/HMR). */\nexport function injectStyle(ctx) {\n ctx.effect(() => {\n const tag = document.createElement('style');\n tag.dataset.plugin = 'dsh-conversation-outline';\n tag.dataset.pluginCss = 'dsh-conversation-outline/panel.css';\n tag.textContent = panelCss;\n document.head.appendChild(tag);\n return () => {\n tag.remove();\n };\n }, 'dsh-conversation-outline: panel css');\n}\n","import { OutlinePanel } from \"./OutlinePanel.js\";\nimport { NS, dictionaries } from \"./locales.js\";\nimport { injectStyle } from \"./styles.js\";\n/**\n * Client entry (implementation-spec §1.1/§2.4): the browser half of the\n * plugin. Cordis services this fiber waits for before apply() runs.\n */\nexport const inject = ['slots', 'sessions', 'locale'];\nexport function apply(ctx) {\n // Locale dictionaries — the typed register form (both shipped locales\n // required, keys compile-checked). The disposer is owned by the fiber, so\n // HMR removes the namespace on unload.\n ctx.effect(() => ctx.locale.register(NS, dictionaries), 'dsh-conversation-outline: locale');\n // Panel CSS as an HMR-safe <style data-plugin> tag (effect-owned).\n injectStyle(ctx);\n // Right-edge rail + hover panel in the frame-wide overlay layer.\n // slots.inject waits for the layout's declaration of `shell.overlay` and\n // routes the registration (and its unload cascade) through this fiber.\n // `locale: NS` gives the component the typed `t` seat; the inject factory\n // passes the sessions service face.\n ctx.slots.inject('shell.overlay', () => ctx.slots.register({\n name: 'shell.overlay',\n id: 'dsh-conversation-outline.rail',\n locale: NS,\n inject: () => ({ sessions: ctx.sessions }),\n }, OutlinePanel));\n}\n"],"mappings":"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"}
@@ -9,6 +9,6 @@ import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client';
9
9
  * never hashed class names of other packages. No layout shift: the panel is a
10
10
  * pure overlay.
11
11
  */
12
- export declare const panelCss = "\n/* ---- Right-edge rail (always-visible minimap) --------------------------- */\n.dso-rail {\n position: fixed;\n top: 50%;\n right: 0;\n transform: translateY(-50%);\n z-index: 2147483000;\n box-sizing: border-box;\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 3px;\n width: 30px;\n max-height: 55vh;\n padding: 8px 10px;\n overflow: hidden;\n cursor: pointer;\n}\n.dso-rail-more {\n flex: none;\n font-size: 9px;\n font-weight: 600;\n line-height: 10px;\n color: var(--dsw-alias-label-tertiary);\n font-variant-numeric: tabular-nums;\n}\n.dso-bar {\n flex: none;\n width: 10px;\n height: 6px;\n padding: 0;\n border: none;\n border-radius: 3px;\n background: color-mix(in srgb, var(--dsw-alias-label-tertiary) 45%, transparent);\n cursor: pointer;\n transition: background 0.12s, transform 0.12s;\n}\n.dso-bar:hover {\n background: var(--dsw-alias-state-business-primary);\n transform: scaleX(1.35);\n}\n.dso-rail:focus-visible,\n.dso-bar:focus-visible {\n outline: 2px solid var(--dsw-alias-state-business-primary);\n outline-offset: 2px;\n}\n\n/* ---- Hover-expanded preview panel --------------------------------------- */\n.dso-panel {\n position: fixed;\n top: 50%;\n right: 16px;\n transform: translateY(-50%);\n z-index: 2147483000;\n box-sizing: border-box;\n display: flex;\n flex-direction: column;\n width: min(340px, calc(100vw - 24px));\n max-height: min(70vh, 600px);\n overflow: hidden;\n border: 1px solid var(--dsw-alias-line-normal);\n border-radius: 16px;\n background: color-mix(in srgb, var(--dsw-alias-bg-module-platform) 92%, transparent);\n backdrop-filter: blur(20px);\n box-shadow: 0 16px 48px color-mix(in srgb, var(--dsw-alias-label-primary) 18%, transparent);\n color: var(--dsw-alias-label-primary);\n animation: dso-panel-in 140ms ease-out;\n}\n@keyframes dso-panel-in {\n from { opacity: 0; transform: translateY(-50%) translateX(12px); }\n to { opacity: 1; transform: translateY(-50%) translateX(0); }\n}\n\n.dso-panel-header {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 12px 14px 10px;\n border-bottom: 1px solid var(--dsw-alias-line-normal);\n}\n.dso-panel-title {\n font-size: 14px;\n font-weight: 600;\n color: var(--dsw-alias-label-primary);\n}\n.dso-panel-count {\n flex: 1;\n font-size: 12px;\n color: var(--dsw-alias-label-tertiary);\n}\n.dso-panel-close {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 24px;\n height: 24px;\n border: none;\n border-radius: 6px;\n background: transparent;\n color: var(--dsw-alias-label-secondary);\n font-size: 16px;\n line-height: 1;\n cursor: pointer;\n transition: background 0.12s;\n}\n.dso-panel-close:hover { background: var(--dsw-alias-interactive-bg-hover-solid); }\n\n.dso-panel-close:focus-visible,\n.dso-search:focus-visible,\n.dso-row-main:focus-visible,\n.dso-copy:focus-visible,\n.dso-load-older:focus-visible {\n outline: 2px solid var(--dsw-alias-state-business-primary);\n outline-offset: 2px;\n}\n\n.dso-search {\n margin: 10px 12px 4px;\n box-sizing: border-box;\n height: 30px;\n padding: 0 10px;\n border: 1px solid var(--dsw-alias-line-normal);\n border-radius: 8px;\n background: var(--dsw-alias-interactive-bg-hover-solid);\n color: var(--dsw-alias-label-primary);\n font: inherit;\n font-size: 12px;\n outline: none;\n}\n.dso-search::placeholder { color: var(--dsw-alias-label-tertiary); }\n\n/* ---- Question list ------------------------------------------------------ */\n.dso-list {\n flex: 1;\n min-height: 0;\n overflow-y: auto;\n display: flex;\n flex-direction: column;\n gap: 2px;\n padding: 6px;\n}\n/* Row container: two sibling buttons (jump + copy) \u2014 no nested interactive\n controls. Hover highlights the whole row. */\n.dso-row {\n display: flex;\n align-items: center;\n gap: 4px;\n padding: 2px;\n border-radius: 10px;\n transition: background 0.12s;\n}\n.dso-row:hover { background: var(--dsw-alias-interactive-bg-hover-solid); }\n.dso-row-main {\n flex: 1;\n min-width: 0;\n display: flex;\n align-items: center;\n gap: 6px;\n height: 28px;\n padding: 2px 6px;\n border: none;\n border-radius: 8px;\n background: transparent;\n color: inherit;\n font: inherit;\n text-align: left;\n cursor: pointer;\n transition: background 0.12s;\n}\n.dso-row-main:hover { background: var(--dsw-alias-interactive-bg-hover-solid); }\n\n.dso-turn {\n flex: none;\n min-width: 26px;\n height: 16px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border-radius: 8px;\n background: var(--dsw-alias-bg-fill-business);\n color: var(--dsw-alias-label-on-fill);\n font-size: 10px;\n font-weight: 600;\n font-variant-numeric: tabular-nums;\n}\n/* Single-line truncation: shows the opening words of each question \u2014\n anything longer simply ellipsizes (per user request). */\n.dso-text {\n flex: 1;\n min-width: 0;\n font-size: 13px;\n line-height: 18px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.dso-time {\n flex: none;\n font-size: 11px;\n font-variant-numeric: tabular-nums;\n color: var(--dsw-alias-label-tertiary);\n}\n.dso-steer {\n flex: none;\n padding: 0 6px;\n border: 1px solid var(--dsw-alias-line-normal);\n border-radius: 999px;\n font-size: 10px;\n color: var(--dsw-alias-label-secondary);\n}\n.dso-copy {\n flex: none;\n padding: 2px 6px;\n border: none;\n border-radius: 6px;\n background: transparent;\n color: var(--dsw-alias-label-secondary);\n font: inherit;\n font-size: 11px;\n cursor: pointer;\n opacity: 0;\n transition: opacity 0.12s, color 0.12s, background 0.12s;\n}\n.dso-row:hover .dso-copy,\n.dso-row:focus-within .dso-copy,\n.dso-copy:focus-visible { opacity: 1; }\n.dso-copy:hover { background: var(--dsw-alias-bg-fill-neutral); }\n.dso-copy.dso-copied { color: var(--dsw-alias-state-success); }\n\n.dso-empty {\n padding: 28px 16px;\n text-align: center;\n font-size: 13px;\n color: var(--dsw-alias-label-tertiary);\n}\n\n/* ---- Footer / load older ------------------------------------------------ */\n.dso-footer {\n display: flex;\n justify-content: center;\n padding: 8px 12px 10px;\n border-top: 1px solid var(--dsw-alias-line-normal);\n}\n.dso-load-older {\n padding: 4px 14px;\n border: none;\n border-radius: 14px;\n background: var(--dsw-alias-interactive-bg-hover-solid);\n color: var(--dsw-alias-label-secondary);\n font: inherit;\n font-size: 12px;\n cursor: pointer;\n}\n.dso-load-older:disabled { cursor: default; opacity: 0.6; }\n\n/* ---- Jump flash highlight ----------------------------------------------- */\n[data-dsh-outline-flash] {\n animation: dso-flash 1.8s ease-out;\n}\n@keyframes dso-flash {\n 0% {\n box-shadow: 0 0 0 0 color-mix(in srgb, var(--dsw-alias-state-business-primary) 50%, transparent);\n background-color: color-mix(in srgb, var(--dsw-alias-state-business-primary) 24%, transparent);\n }\n 70% {\n box-shadow: 0 0 0 6px transparent;\n background-color: color-mix(in srgb, var(--dsw-alias-state-business-primary) 8%, transparent);\n }\n 100% {\n box-shadow: 0 0 0 0 transparent;\n background-color: transparent;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .dso-panel { animation: none; }\n .dso-bar { transition: none; }\n [data-dsh-outline-flash] { animation: none; }\n}\n";
12
+ export declare const panelCss = "\n/* ---- Right-edge rail (always-visible minimap) --------------------------- */\n.dso-rail {\n position: fixed;\n top: 50%;\n right: 0;\n transform: translateY(-50%);\n z-index: 2147483000;\n box-sizing: border-box;\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 3px;\n width: 30px;\n max-height: 55vh;\n padding: 8px 10px;\n overflow: hidden;\n cursor: pointer;\n}\n.dso-rail-more {\n flex: none;\n font-size: 9px;\n font-weight: 600;\n line-height: 10px;\n color: var(--dsw-alias-label-tertiary);\n font-variant-numeric: tabular-nums;\n}\n.dso-bar {\n flex: none;\n width: 10px;\n height: 6px;\n padding: 0;\n border: none;\n border-radius: 3px;\n background: color-mix(in srgb, var(--dsw-alias-label-tertiary) 45%, transparent);\n cursor: pointer;\n transition: background 0.12s, transform 0.12s;\n}\n.dso-bar:hover {\n background: var(--dsw-alias-state-business-primary);\n transform: scaleX(1.35);\n}\n.dso-rail:focus-visible,\n.dso-bar:focus-visible {\n outline: 2px solid var(--dsw-alias-state-business-primary);\n outline-offset: 2px;\n}\n/* Clickability affordance: a hovered or pinned strip lights its bars up, so\n it reads as a control rather than as decoration. */\n.dso-rail:hover .dso-bar,\n.dso-rail[data-dso-open] .dso-bar {\n background: color-mix(in srgb, var(--dsw-alias-label-tertiary) 72%, transparent);\n}\n.dso-rail:hover .dso-bar:hover {\n background: var(--dsw-alias-state-business-primary);\n}\n\n/* ---- Hover-expanded preview panel --------------------------------------- */\n.dso-panel {\n position: fixed;\n top: 50%;\n right: 16px;\n transform: translateY(-50%);\n z-index: 2147483000;\n box-sizing: border-box;\n display: flex;\n flex-direction: column;\n width: min(340px, calc(100vw - 24px));\n max-height: min(70vh, 600px);\n overflow: hidden;\n border: 1px solid var(--dsw-alias-line-normal);\n border-radius: 16px;\n background: color-mix(in srgb, var(--dsw-alias-bg-module-platform) 92%, transparent);\n backdrop-filter: blur(20px);\n box-shadow: 0 16px 48px color-mix(in srgb, var(--dsw-alias-label-primary) 18%, transparent);\n color: var(--dsw-alias-label-primary);\n animation: dso-panel-in 140ms ease-out;\n}\n/* A pinned panel (clicked open) keeps a defined edge, so it is obvious that it\n no longer collapses when the pointer leaves. */\n.dso-panel[data-dso-pinned] {\n border-color: var(--dsw-alias-state-business-primary);\n}\n@keyframes dso-panel-in {\n from { opacity: 0; transform: translateY(-50%) translateX(12px); }\n to { opacity: 1; transform: translateY(-50%) translateX(0); }\n}\n\n.dso-panel-header {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 12px 14px 10px;\n border-bottom: 1px solid var(--dsw-alias-line-normal);\n}\n.dso-panel-title {\n font-size: 14px;\n font-weight: 600;\n color: var(--dsw-alias-label-primary);\n}\n.dso-panel-count {\n flex: 1;\n font-size: 12px;\n color: var(--dsw-alias-label-tertiary);\n}\n.dso-panel-close {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 24px;\n height: 24px;\n border: none;\n border-radius: 6px;\n background: transparent;\n color: var(--dsw-alias-label-secondary);\n font-size: 16px;\n line-height: 1;\n cursor: pointer;\n transition: background 0.12s;\n}\n.dso-panel-close:hover { background: var(--dsw-alias-interactive-bg-hover-solid); }\n\n.dso-panel-close:focus-visible,\n.dso-search:focus-visible,\n.dso-row-main:focus-visible,\n.dso-copy:focus-visible,\n.dso-load-older:focus-visible {\n outline: 2px solid var(--dsw-alias-state-business-primary);\n outline-offset: 2px;\n}\n\n.dso-search {\n margin: 10px 12px 4px;\n box-sizing: border-box;\n height: 30px;\n padding: 0 10px;\n border: 1px solid var(--dsw-alias-line-normal);\n border-radius: 8px;\n background: var(--dsw-alias-interactive-bg-hover-solid);\n color: var(--dsw-alias-label-primary);\n font: inherit;\n font-size: 12px;\n outline: none;\n}\n.dso-search::placeholder { color: var(--dsw-alias-label-tertiary); }\n\n/* ---- Question list ------------------------------------------------------ */\n.dso-list {\n flex: 1;\n min-height: 0;\n overflow-y: auto;\n display: flex;\n flex-direction: column;\n gap: 2px;\n padding: 6px;\n}\n/* Row container: two sibling buttons (jump + copy) \u2014 no nested interactive\n controls. Hover highlights the whole row. */\n.dso-row {\n display: flex;\n align-items: center;\n gap: 4px;\n padding: 2px;\n border-radius: 10px;\n transition: background 0.12s;\n}\n.dso-row:hover { background: var(--dsw-alias-interactive-bg-hover-solid); }\n.dso-row-main {\n flex: 1;\n min-width: 0;\n display: flex;\n align-items: center;\n gap: 6px;\n height: 28px;\n padding: 2px 6px;\n border: none;\n border-radius: 8px;\n background: transparent;\n color: inherit;\n font: inherit;\n text-align: left;\n cursor: pointer;\n transition: background 0.12s;\n}\n.dso-row-main:hover { background: var(--dsw-alias-interactive-bg-hover-solid); }\n\n.dso-turn {\n flex: none;\n min-width: 26px;\n height: 16px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border-radius: 8px;\n background: var(--dsw-alias-bg-fill-business);\n color: var(--dsw-alias-label-on-fill);\n font-size: 10px;\n font-weight: 600;\n font-variant-numeric: tabular-nums;\n}\n/* Single-line truncation: shows the opening words of each question \u2014\n anything longer simply ellipsizes (per user request). */\n.dso-text {\n flex: 1;\n min-width: 0;\n font-size: 13px;\n line-height: 18px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.dso-time {\n flex: none;\n font-size: 11px;\n font-variant-numeric: tabular-nums;\n color: var(--dsw-alias-label-tertiary);\n}\n.dso-steer {\n flex: none;\n padding: 0 6px;\n border: 1px solid var(--dsw-alias-line-normal);\n border-radius: 999px;\n font-size: 10px;\n color: var(--dsw-alias-label-secondary);\n}\n.dso-copy {\n flex: none;\n padding: 2px 6px;\n border: none;\n border-radius: 6px;\n background: transparent;\n color: var(--dsw-alias-label-secondary);\n font: inherit;\n font-size: 11px;\n cursor: pointer;\n opacity: 0;\n transition: opacity 0.12s, color 0.12s, background 0.12s;\n}\n.dso-row:hover .dso-copy,\n.dso-row:focus-within .dso-copy,\n.dso-copy:focus-visible { opacity: 1; }\n.dso-copy:hover { background: var(--dsw-alias-bg-fill-neutral); }\n.dso-copy.dso-copied { color: var(--dsw-alias-state-success); }\n\n.dso-empty {\n padding: 28px 16px;\n text-align: center;\n font-size: 13px;\n color: var(--dsw-alias-label-tertiary);\n}\n\n/* ---- Jump failure notice ------------------------------------------------ */\n.dso-jump-failed {\n margin: 0;\n padding: 8px 14px;\n border-top: 1px solid var(--dsw-alias-line-normal);\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-state-warning, var(--dsw-alias-label-secondary));\n}\n\n/* ---- Footer / load older ------------------------------------------------ */\n.dso-footer {\n display: flex;\n justify-content: center;\n padding: 8px 12px 10px;\n border-top: 1px solid var(--dsw-alias-line-normal);\n}\n.dso-load-older {\n padding: 4px 14px;\n border: none;\n border-radius: 14px;\n background: var(--dsw-alias-interactive-bg-hover-solid);\n color: var(--dsw-alias-label-secondary);\n font: inherit;\n font-size: 12px;\n cursor: pointer;\n}\n.dso-load-older:disabled { cursor: default; opacity: 0.6; }\n\n/* ---- Jump flash highlight ----------------------------------------------- */\n[data-dsh-outline-flash] {\n animation: dso-flash 1.8s ease-out;\n}\n@keyframes dso-flash {\n 0% {\n box-shadow: 0 0 0 0 color-mix(in srgb, var(--dsw-alias-state-business-primary) 50%, transparent);\n background-color: color-mix(in srgb, var(--dsw-alias-state-business-primary) 24%, transparent);\n }\n 70% {\n box-shadow: 0 0 0 6px transparent;\n background-color: color-mix(in srgb, var(--dsw-alias-state-business-primary) 8%, transparent);\n }\n 100% {\n box-shadow: 0 0 0 0 transparent;\n background-color: transparent;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .dso-panel { animation: none; }\n .dso-bar { transition: none; }\n [data-dsh-outline-flash] { animation: none; }\n}\n";
13
13
  /** Inject the style tag, owned by the client fiber (removed on dispose/HMR). */
14
14
  export declare function injectStyle(ctx: ClientContext): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chestnut23/dsh-conversation-outline",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Codex-style conversation outline for DeepSeek Harness: a floating panel listing every user question in the current conversation with search, load-older and click-to-jump (scroll + highlight). Bilingual zh-CN / en.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",